feat(stores): add Pinia domain stores and update docs

Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.feat(stores): add Pinia domain stores and update docs

Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.
This commit is contained in:
skytek_xinliang
2026-05-05 11:54:19 +08:00
parent 6eab4d9744
commit b37f4363eb
23 changed files with 1531 additions and 1588 deletions
+124 -1
View File
@@ -1 +1,124 @@
export * from './stores/breadcrumbs'
import type { LayoutMenuItem } from './menu'
import { mdiHome } from '@mdi/js'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export interface BreadcrumbItem {
title: string
to?: string
disabled?: boolean
icon?: string
}
interface BreadcrumbPayload {
path: string
menuItems: LayoutMenuItem[]
favoriteItems?: LayoutMenuItem[]
fallbackTitle?: string | null
homeLabel?: string
homeIcon?: string
}
function buildTrail(items: LayoutMenuItem[], targetPath: string): LayoutMenuItem[] | null {
const walk = (nodes: LayoutMenuItem[], trail: LayoutMenuItem[]): LayoutMenuItem[] | null => {
for (const node of nodes) {
const nextTrail = [...trail, node]
if (node.path && node.path === targetPath) return nextTrail
if (node.subItems?.length) {
const found = walk(node.subItems, nextTrail)
if (found) return found
}
}
return null
}
return walk(items || [], [])
}
function toBreadcrumbItems(
trail: LayoutMenuItem[],
homeLabel: string,
homeIcon: string
): BreadcrumbItem[] {
const isHomePath = (path?: string) => path === '/' || path === ''
const startsWithHome = trail.length > 0 && isHomePath(trail[0]?.path)
const crumbs: BreadcrumbItem[] = []
if (!startsWithHome) {
crumbs.push({
title: homeLabel,
to: '/',
icon: homeIcon,
})
}
for (const [index, node] of trail.entries()) {
const isLast = index === trail.length - 1
crumbs.push({
title: node.title,
to: isLast ? undefined : node.path,
icon: startsWithHome && index === 0 ? homeIcon : undefined,
})
}
return crumbs
}
export const useBreadcrumbStore = defineStore('breadcrumbs', () => {
const items = ref<BreadcrumbItem[]>([])
const homeLabel = ref('首頁')
const homeIcon = ref(mdiHome)
const setBreadcrumbs = (payload: BreadcrumbPayload) => {
if (!payload?.path) return
homeLabel.value = payload.homeLabel ?? homeLabel.value
homeIcon.value = payload.homeIcon ?? homeIcon.value
const trailFromMenu = buildTrail(payload.menuItems || [], payload.path)
const trailFromFavorite = payload.favoriteItems?.length
? buildTrail(payload.favoriteItems, payload.path)
: null
const trail = trailFromMenu || trailFromFavorite
if (trail?.length) {
items.value = toBreadcrumbItems(trail, homeLabel.value, homeIcon.value)
return
}
if (payload.fallbackTitle && payload.fallbackTitle !== homeLabel.value) {
items.value = [
{
title: homeLabel.value,
to: '/',
icon: homeIcon.value,
},
{
title: payload.fallbackTitle,
},
]
return
}
items.value = [
{
title: homeLabel.value,
to: '/',
icon: homeIcon.value,
},
]
}
const reset = () => {
items.value = []
}
const breadcrumbItems = computed(() => items.value)
return {
items,
breadcrumbItems,
setBreadcrumbs,
reset,
}
})