b37f4363eb
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.
125 lines
3.0 KiB
TypeScript
125 lines
3.0 KiB
TypeScript
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,
|
|
}
|
|
})
|