User-Center-Items
Das Shop-Widget user (das User-Center unter /usercenter) hat eine Plugin-Registry: andere Plugins koennen dort Menue-Eintraege registrieren, ohne das Shop-Widget anzufassen. Diese Seite erklaert das Interface UserCenterItem, die Registrierung via registerUserCenterItem() und den Nuxt-Alias #extensions fuer Cross-Plugin-Imports.
Wofuer?
Der User-Center zeigt standardmaessig Shop-spezifische Menuepunkte: Uebersicht, Profil, Adressen, Bestellungen, Abonnements. Andere Plugins brauchen aber oft eigene Bereiche:
| Plugin | Item | Zweck |
|---|---|---|
elearning | "Meine Kurse" | Kurs-Fortschritt + Episoden-Liste |
elearning | "Meine Zertifikate" | BZAEK-Zertifikate als PDF herunterladen |
| Custom-Plugin | "Meine Favoriten" | Wunschliste, Merkzettel, … |
| Custom-Plugin | "Support-Tickets" | Integration mit Helpdesk-System |
Ohne Registry muesste das Shop-User-Widget jedesmal geforkt werden — mit Registry registriert jedes Plugin seine Items selbst.
Das UserCenterItem-Interface
Das Composable liegt in _public/extensions/core/backend/shop/widgets/user/template/useUserCenter.ts:
export interface UserCenterItem {
id: string // URL-Segment, z. B. 'profil', 'my-courses'
label: string // Menue-Label
description?: string // Untertitel unter dem Label
icon?: string // Font-Awesome-Klasse (z. B. 'fa-light fa-award')
order: number // numerische Sortierung (aufsteigend)
component: Component // Vue-Komponente fuer den Content-Bereich
isVisible?: () => boolean // optional: reaktiver Berechtigungs-Check
}| Feld | Pflicht | Zweck |
|---|---|---|
id | ja | URL-Segment — erzeugt die Route /usercenter/{id}. Nur a-z0-9- erlaubt |
label | ja | Sichtbarer Menue-Eintrag in der Sidebar |
description | nein | Untertitel unter dem Label |
icon | nein | Font-Awesome-Klasse inkl. Variante (fa-light fa-award) |
order | ja | Sortierung (10er-Schritte empfohlen, damit Plugins dazwischen einsortieren koennen) |
component | ja | Vue-Komponente, die im Content-Bereich gerendert wird |
isVisible | nein | Funktion, die true/false zurueckgibt — reaktiv gegen Stores |
Registrierung per Plugin-Datei
Jedes Plugin legt eine Datei unter widgets/{widget-name}/template/plugins/userCenterItems.ts ab. Die Datei exportiert eine Default-Funktion, die beim Mount des User-Center-Widgets explizit aufgerufen wird — kein Side-Effect-Import.
Echtes Beispiel aus elearning/widgets/elearning/template/plugins/userCenterItems.ts:
import { registerUserCenterItem } from '#extensions/shop/widgets/user/template/useUserCenter'
import MyCoursesUserCenter from '../MyCoursesUserCenter.vue'
import MyCertificates from '../MyCertificates.vue'
export default () => {
registerUserCenterItem({
id: 'my-courses',
label: 'Meine Kurse',
description: 'Ihre gebuchten Kurse',
icon: 'fa-light fa-graduation-cap',
order: 60,
component: MyCoursesUserCenter,
})
registerUserCenterItem({
id: 'my-certificates',
label: 'Meine Zertifikate',
description: 'Zertifikate herunterladen',
icon: 'fa-light fa-award',
order: 70,
component: MyCertificates,
})
}Default-Funktion statt Top-Level-Aufruf — warum?
Ein Side-Effect-Import mit Top-Level-registerUserCenterItem(...) wuerde durch Tree-Shaking entfernt — der Import hat keine verwendeten Bindings, der Bundler optimiert ihn weg. Die Default-Funktion zwingt den Import-Graph, das Modul zu erhalten, und erlaubt zusaetzlich Lifecycle-Kontrolle (Registrierung erst bei Mount, nicht beim Modul-Load).
Der #extensions-Alias
#extensions zeigt auf _public/extensions/core/backend/ — konfiguriert in _theme/vue-base/nuxt.config.ts:
alias: {
'#extensions': resolve(process.cwd(), '../../_public/extensions/core/backend'),
// ...
}Damit lassen sich Cross-Plugin-Imports ohne fragile relative Pfade schreiben:
// statt:
import { registerUserCenterItem } from '../../../shop/widgets/user/template/useUserCenter'
// besser:
import { registerUserCenterItem } from '#extensions/shop/widgets/user/template/useUserCenter'Details zum Alias: Themes › Nuxt-Aliases.
Rendering im User-Center-Widget
Der Shop-User-Widget-Einstieg (shop/widgets/user/template/index.vue) importiert alle Registrator-Funktionen beim Mount und ruft sie einmal auf. Erst dann fuellt sich die reaktive Registry:
<!-- shop/widgets/user/template/index.vue (Schema) -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserCenter } from './useUserCenter'
import registerShopItems from './plugins/userCenterItems'
import registerElearningItems from '#extensions/elearning/widgets/elearning/template/plugins/userCenterItems'
onMounted(() => {
registerShopItems() // Shop-Items (Uebersicht, Profil, Adressen, Bestellungen, Abo)
registerElearningItems() // E-Learning-Items (my-courses, my-certificates)
})
const { items } = useUserCenter()
const activeId = computed(() => route.params.slug?.[0] || items.value[0]?.id)
const active = computed(() => items.value.find(i => i.id === activeId.value))
</script>UserMenu.vue rendert die Sidebar-Navigation aus items.value, der Content-Bereich laedt die aktive Komponente via <component :is="active.component" />.
Plugin-Datei muss importiert werden
Das bloße Ablegen einer userCenterItems.ts-Datei reicht nicht. Das Shop-User-Widget muss die Default-Funktion aktiv importieren und aufrufen. Ohne Import laeuft der Registrierungs-Code nie.
order — Sortierung
Die Standard-Shop-Items nutzen Werte 10-50, E-Learning 60-70. Konvention: 10er-Schritte lassen Luecken fuer dazwischen-einsortierende Items.
| Plugin | Item | order |
|---|---|---|
| shop | overview | 10 |
| shop | profil | 20 |
| shop | adressen | 30 |
| shop | bestellungen | 40 |
| shop | abonnements | 50 |
| elearning | my-courses | 60 |
| elearning | my-certificates | 70 |
Neue Plugin-Items: freie 10er-Slots waehlen. Alle Items werden in useUserCenter() nach order aufsteigend sortiert.
isVisible — reaktive Sichtbarkeit
Items koennen sich bedingt ein- oder ausblenden — z. B. "Meine Kurse" nur, wenn der User ueberhaupt Kurse gekauft hat:
import { useShopStore } from '#extensions/shop/widgets/user/template/useShopStore'
const shopStore = useShopStore()
registerUserCenterItem({
id: 'my-subscriptions',
label: 'Abonnements',
order: 50,
component: SubscriptionsView,
isVisible: () => shopStore.hasSubscriptions,
})isVisible muss reaktiv sein
useUserCenter() evaluiert isVisible in einem computed — die Funktion wird bei jedem Zugriff neu aufgerufen. Sie muss also reaktive Quellen lesen (Pinia-Store, useState, ref) — statische Werte oder Snapshot-Reads funktionieren nicht korrekt.
Component-Stil (Namespace)
Jede User-Center-Komponente bekommt eine eindeutige Wurzel-Klasse als CSS-Namespace:
<template>
<div class="ns-my-certificates">
<h2>Meine Zertifikate</h2>
<!-- ... -->
</div>
</template>
<style scoped>
.ns-my-certificates {
padding: 1.5rem;
}
.ns-my-certificates h2 {
margin-bottom: 1rem;
}
</style>Konvention: ns-{feature-name}. Alle Standard-Shop-Items halten sich daran (ns-user-panel-edit, ns-user-orders, ns-user-addresses, ns-my-courses, ns-my-certificates). Globales CSS vermeiden — Scoping ist wichtig, weil viele User-Center-Komponenten parallel existieren und sich nicht gegenseitig stoeren duerfen.
URL-Routing
id ist gleichzeitig das URL-Segment. Das Shop-User-Widget nutzt route.params.slug:
/usercenter → items[0] (Overview)
/usercenter/profil → id = "profil"
/usercenter/my-courses → id = "my-courses"
/usercenter/my-certificates → id = "my-certificates"id muss URL-safe sein — nur Kleinbuchstaben, Ziffern, Bindestriche. Keine Slashes, keine Umlaute, kein Whitespace.
Haeufige Fehler
Plugin-Datei nicht importiert
Das Shop-User-Widget muss die Registrator-Funktion aktiv importieren und aufrufen. Fehlt der Import in shop/widgets/user/template/index.vue, laeuft der Registrierungs-Code nie — Item erscheint nicht im Menue.
id nicht URL-safe
id: 'Meine Kurse' bricht das Routing. Immer a-z0-9- — z. B. my-courses, meine-kurse.
component: MyView, MyView nicht importiert
Der Import muss manuell oben in der Datei stehen. component bekommt die Vue-Komponente, nicht den Dateinamen.
isVisible greift auf non-reaktive Quelle zu
isVisible: () => localStorage.getItem('flag') === '1' funktioniert nicht — der Wert wird nicht neu evaluiert, wenn localStorage sich aendert. Nur Pinia-Store, useState, ref, computed.
Tree-Shaking entfernt Side-Effect-Import
// FALSCH — wird vom Bundler entfernt:
import './plugins/userCenterItems'
registerUserCenterItem(...) // auf Top-Level
// RICHTIG — Default-Funktion + expliziter Aufruf:
import registerItems from './plugins/userCenterItems'
onMounted(() => registerItems())CSS-Namespace fuer eigene Items
Alle Standard-Komponenten nutzen ns-{name} als Root-Klasse fuer Scoped-CSS. Neue Items sollten das Pattern uebernehmen, sonst kann CSS benachbarter Items ueberlappen.
Siehe auch
- Widget-Anatomie — der User-Center-Widget-Kontext (
shop/widgets/user/) - Themes › Nuxt-Aliases —
#extensions-Alias im Detail - Beispiel: Testimonial-Widget — kompletter Widget-Bau
_public/extensions/core/backend/shop/widgets/user/template/useUserCenter.ts— Composable-Quelle_public/extensions/core/backend/elearning/widgets/elearning/template/plugins/userCenterItems.ts— Echtes E-Learning-Registrierungs-Pattern