Skip to content

Pàgines personalitzades ​

Quan una pantalla no encaixa en un Resource ni en una Page (un calendari, un assistent de diversos passos, un editor visual...), CustomPage la registra a la navegació del panell i el seu contingut és un component Vue que escrius tu. Arrel et dona el kit de components de la seva interfície perquè la pantalla es vegi com la resta del panell.

Una pàgina personalitzada té tres peces:

  1. L'entrada de navegació, a PHP (CustomPage).
  2. El component Vue, al teu frontend.
  3. Els endpoints que el component consumeix, a la teva aplicació.

L'entrada de navegació ​

php
Panel::make('admin')
    ->customPages([
        CustomPage::make('pass-list', 'Pass list')
            ->icon('clipboard-document-check')
            ->group('Attendance')
            ->sort(10)
            ->visible(fn (): bool => auth()->user()->can('attendance.create'))
            ->badge(fn (): int => Absence::pending()->count()),
    ])

CustomPage::make($slug, $label) reserva la URL /{prefix}/{panel}/custom/{slug} i l'entrada de menú.

MètodeDescripció
icon(string $icon)Icona de la navegació. Vegeu la llista.
group(string $group)Grup de la navegació. Les pàgines i els Resources que comparteixen nom de grup es mostren junts.
sort(int $sort)Posició dins la navegació, de menor a major.
visible(Closure $callback)Si retorna fals, l'entrada no apareix. S'avalua a cada petició, per usuari.
badge(Closure $count)Un recompte que es mostra a l'enllaç. Només es mostra si és major que zero.

visible() només amaga l'enllaç. Els endpoints que alimenten la pàgina han de fer la seva pròpia autorització (vegeu Endpoints propis).

El component ​

Una pàgina es registra des de JavaScript, amb un script que carregues al panell amb Panel::scripts(). Hi ha dues maneres de fer-ho. La recomanada és registerMountablePage.

registerMountablePage: la teva aplicació Vue ​

Arrel et dona un contenidor buit i tu hi muntes la teva pròpia aplicació Vue, amb el teu build, les teves dependències i la teva versió de Vue:

ts
import '@arrel/ui.css'
import { createApp, type App } from 'vue'
import PassListPage from './PassListPage.vue'

const mounted = new WeakMap<HTMLElement, App>()

window.Arrel.registerMountablePage('pass-list', {
    mount(container, props) {
        const app = createApp(PassListPage, props)
        app.mount(container)
        mounted.set(container, app)
    },
    unmount(container) {
        mounted.get(container)?.unmount()
        mounted.delete(container)
    },
})

props conté panel (l'identificador del panell) i slug (el de la pàgina). Arrel mai reconcilia els vnodes del teu component, així que no hi ha cap risc de tenir dues instàncies de Vue barrejades.

registerPage: components dins de l'arbre d'Arrel ​

registerPage munta el component dins l'arbre d'Arrel, amb la seva instància de Vue. Aquesta és la versió runtime only, sense compilador de plantilles: cal h() o components precompilats.

js
const { h } = window.Arrel.Vue

window.Arrel.registerPage('about', {
    render() {
        return h('div', [h('h1', 'About')])
    },
})

window.Arrel.Vue exposa h, ref, reactive, computed, watch, onMounted i onBeforeUnmount.

registerMountablePageregisterPage
Instància de VueLa teva, separadaLa d'Arrel
PlantillesQualsevol, amb el teu buildNomés h() o components precompilats
Cicle de vidaEl gestiones tu (mount() i unmount())El gestiona Arrel

L'API global window.Arrel ​

MembreDescripció
panelL'identificador del panell actual.
prefixEl prefix configurat a config('arrel.prefix').
url(path = '')Construeix una URL dins del panell: Arrel.url('posts').
navigate(path)Navega dins la SPA sense recarregar la pàgina. Retorna una promesa.
refreshNavigation()Torna a carregar la navegació, per exemple per refrescar un badge() després d'una acció.
registerPage(slug, component)Registra un component Vue d'Arrel.
registerMountablePage(slug, page)Registra una pàgina muntable.
VueFuncions de Vue per a registerPage.

El kit de components (@arrel/ui) ​

El paquet publica el kit de components de la seva pròpia interfície, perquè les teves pàgines tinguin els mateixos botons, camps i estats. No s'injecta res a la pàgina: l'incorpores al teu build.

ComponentProps principals
Badgecolor: gray, success, warning, danger o info.
Buttonvariant: primary, secondary o danger. type, disabled, href, newTab.
DatePickerv-model, displayFormat, min, max, isDisabled(iso), clearable, invalid.
Iconname, size.
Inputv-model, type, placeholder, invalid.
Labelfor.
RichEditorv-model, field, error.
Selectv-model, options, nullable, placeholder, invalid.
Textareav-model, rows, placeholder, invalid.
Togglev-model.

A més dels components, exporta requestJson, ApiError, formatDate i t.

  • requestJson<T>(url, init) fa un fetch amb la sessió i la capçalera CSRF que Sanctum espera, i llança un ApiError si la resposta no és correcta. Per a una resposta 422, error.validationErrors té els errors de validació per camp.
  • formatDate(value, format) formata una data amb la sintaxi de date() de PHP.
  • t(key) retorna un text de la interfície, ja traduït amb Panel::translations().

Connectar-lo al teu build ​

El kit es distribueix compilat a dist/ui dins del paquet, amb Vue com a dependència externa (és el Vue del teu build). Defineix un àlies a Vite:

js
// vite.config.js
import path from 'node:path'

const arrel = path.resolve(__dirname, 'vendor/arrel/arrel')

export default defineConfig({
    resolve: {
        alias: {
            '@arrel/ui.css': `${arrel}/dist/ui/arrel-ui.css`,
            '@arrel/ui': `${arrel}/dist/ui/arrel-ui.js`,
        },
    },
})

I, per a TypeScript, la ruta als tipus:

json
{
    "compilerOptions": {
        "paths": {
            "@arrel/ui": ["./vendor/arrel/arrel/dist/ui/types/ui.d.ts"]
        }
    }
}

Importa @arrel/ui.css una sola vegada a l'entrada de les teves pàgines, perquè els components portin els estils d'Arrel.

Endpoints propis ​

Les dades d'una pàgina personalitzada viuen a la teva aplicació, no a Arrel. Perquè hereten l'autenticació, el segon factor i la identificació del tenant, munta'ls amb la mateixa pila de middleware que Arrel:

php
Route::middleware([
    ...config('arrel.middleware'),
    ForceJsonResponse::class,
    EnsureFrontendRequestsAreStateful::class,
    'web',
    'auth:sanctum',
    EnsureEmailIsVerified::class,
    EnsureTwoFactorIsSetUp::class,
    ...config('arrel.authenticated_middleware'),
])->group(function (): void {
    Route::get('/arrel-app/pass-list', PassListController::class);
});

A dins, autoritza cada petició amb les teves Policies o amb Gate. Que visible() amagui l'entrada no impedeix que algú cridi l'endpoint a mà.

Un cop una acció de la pàgina canvia un recompte, crida window.Arrel.refreshNavigation() perquè el badge() es refresqui.