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:
- L'entrada de navegació, a PHP (
CustomPage). - El component Vue, al teu frontend.
- Els endpoints que el component consumeix, a la teva aplicació.
L'entrada de navegació
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ètode | Descripció |
|---|---|
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:
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.
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.
registerMountablePage | registerPage | |
|---|---|---|
| Instància de Vue | La teva, separada | La d'Arrel |
| Plantilles | Qualsevol, amb el teu build | Només h() o components precompilats |
| Cicle de vida | El gestiones tu (mount() i unmount()) | El gestiona Arrel |
L'API global window.Arrel
| Membre | Descripció |
|---|---|
panel | L'identificador del panell actual. |
prefix | El 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. |
Vue | Funcions 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.
| Component | Props principals |
|---|---|
Badge | color: gray, success, warning, danger o info. |
Button | variant: primary, secondary o danger. type, disabled, href, newTab. |
DatePicker | v-model, displayFormat, min, max, isDisabled(iso), clearable, invalid. |
Icon | name, size. |
Input | v-model, type, placeholder, invalid. |
Label | for. |
RichEditor | v-model, field, error. |
Select | v-model, options, nullable, placeholder, invalid. |
Textarea | v-model, rows, placeholder, invalid. |
Toggle | v-model. |
A més dels components, exporta requestJson, ApiError, formatDate i t.
requestJson<T>(url, init)fa unfetchamb la sessió i la capçalera CSRF que Sanctum espera, i llança unApiErrorsi la resposta no és correcta. Per a una resposta 422,error.validationErrorsté els errors de validació per camp.formatDate(value, format)formata una data amb la sintaxi dedate()de PHP.t(key)retorna un text de la interfície, ja traduït ambPanel::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:
// 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:
{
"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:
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.