Skip to content

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:

PluginItemZweck
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:

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
}
FeldPflichtZweck
idjaURL-Segment — erzeugt die Route /usercenter/{id}. Nur a-z0-9- erlaubt
labeljaSichtbarer Menue-Eintrag in der Sidebar
descriptionneinUntertitel unter dem Label
iconneinFont-Awesome-Klasse inkl. Variante (fa-light fa-award)
orderjaSortierung (10er-Schritte empfohlen, damit Plugins dazwischen einsortieren koennen)
componentjaVue-Komponente, die im Content-Bereich gerendert wird
isVisibleneinFunktion, 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:

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:

ts
alias: {
    '#extensions': resolve(process.cwd(), '../../_public/extensions/core/backend'),
    // ...
}

Damit lassen sich Cross-Plugin-Imports ohne fragile relative Pfade schreiben:

ts
// 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:

vue
<!-- 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.

PluginItemorder
shopoverview10
shopprofil20
shopadressen30
shopbestellungen40
shopabonnements50
elearningmy-courses60
elearningmy-certificates70

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:

ts
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:

vue
<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

ts
// 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