import { ClipboardList, ReceiptText, Settings, Warehouse } from '@lucide/vue' import { clientModuleRegistry } from '../modules/registry.ts' import type { ClientModuleManifest } from '../modules/types.ts' import { getNavigationLeaves, isNavigationGroup, type ClientNavigationGroup, type ClientNavigationLeaf, type ClientNavigationNode, } from './tree.ts' export { getNavigationLeaves, type ClientNavigationGroup, type ClientNavigationLeaf, type ClientNavigationNode, } from './tree.ts' function buildModuleNavigationLeaves(module: ClientModuleManifest): ClientNavigationLeaf[] { const leaves: ClientNavigationLeaf[] = [] for (const route of module.routes) { if (route.release !== 'active' || !route.navigation) continue leaves.push({ key: route.navigation.key, title: route.navigation.title ?? route.meta.title, icon: route.navigation.icon, path: route.path ? `/${route.path}` : '/', permission: route.meta.permission, }) } return leaves } const registeredLeaves = clientModuleRegistry.flatMap(buildModuleNavigationLeaves) function requireLeaf(key: string, title?: string): ClientNavigationLeaf { const leaf = registeredLeaves.find((item) => item.key === key) if (!leaf) throw new Error(`Missing client navigation leaf: ${key}`) return title ? { ...leaf, title } : leaf } /** * The sidebar is organised around customer tasks instead of implementation modules. * Account and notification routes remain hidden utility entries so breadcrumbs and * permission-aware fallbacks keep working from the fixed top-header shortcuts. */ export const clientNavigation: ClientNavigationNode[] = [ requireLeaf('dashboard', '首页'), { key: 'order-center', title: '订单中心', icon: ClipboardList, children: [ requireLeaf('first-leg-orders', '头程订单'), requireLeaf('cross-border-procurement'), requireLeaf('cross-border-domestic-trucking'), requireLeaf('cross-border-pallet-shipping'), ], }, requireLeaf('first-leg-customs-documents', '报关资料'), requireLeaf('first-leg-sailing-prices', '船期价格'), { key: 'overseas-warehouse-center', title: '海外仓', icon: Warehouse, children: [ requireLeaf('overseas-labeling'), requireLeaf('overseas-storage-transfer'), requireLeaf('overseas-cargo-disposal'), requireLeaf('overseas-return-relabel', '退货换标'), ], }, { key: 'finance-center', title: '费用与账单', icon: ReceiptText, children: [ requireLeaf('finance-account'), requireLeaf('finance-bills'), ], }, { key: 'company-center', title: '企业设置', icon: Settings, dividerBefore: true, children: [ requireLeaf('company-profile'), requireLeaf('company-members'), requireLeaf('company-roles'), requireLeaf('company-permissions'), ], }, { ...requireLeaf('account-profile'), hidden: true }, { ...requireLeaf('notifications-list'), hidden: true }, ] export function filterNavigationByPermissions( nodes: readonly ClientNavigationNode[], permissions: readonly string[], ): ClientNavigationNode[] { const allowed = new Set(permissions) const result: ClientNavigationNode[] = [] for (const node of nodes) { if (node.hidden) continue if (!isNavigationGroup(node)) { if (allowed.has(node.permission)) result.push(node) continue } const children = filterNavigationByPermissions(node.children, permissions) if (children.length > 0) result.push({ ...node, children: children as [ClientNavigationNode, ...ClientNavigationNode[]] }) } return result } export function getFirstAccessiblePath(permissions: readonly string[]) { const visiblePath = getNavigationLeaves(filterNavigationByPermissions(clientNavigation, permissions))[0]?.path if (visiblePath) return visiblePath const allowed = new Set(permissions) return getNavigationLeaves(clientNavigation).find((leaf) => allowed.has(leaf.permission))?.path ?? '/forbidden' } export interface ClientBreadcrumbItem { title: string path?: string } function findNavigationTrail( nodes: readonly ClientNavigationNode[], path: string, parents: readonly ClientBreadcrumbItem[] = [], ): ClientBreadcrumbItem[] | undefined { for (const node of nodes) { if (isNavigationGroup(node)) { const result = findNavigationTrail(node.children, path, [...parents, { title: node.title }]) if (result) return result continue } if (node.path === path) return [...parents, { title: node.title, path: node.path }] } } export function getRouteBreadcrumbs(path: string, currentTitle: string): ClientBreadcrumbItem[] { const home: ClientBreadcrumbItem = { title: '首页', path: '/' } if (path === '/') return [{ title: '首页' }] const leaves = getNavigationLeaves(clientNavigation) .filter((leaf) => leaf.path !== '/' && (path === leaf.path || path.startsWith(`${leaf.path}/`))) .sort((left, right) => right.path.length - left.path.length) const parentLeaf = leaves[0] const trail = parentLeaf ? findNavigationTrail(clientNavigation, parentLeaf.path) ?? [] : [] if (parentLeaf && path === parentLeaf.path && trail.length > 0) { trail[trail.length - 1] = { ...trail[trail.length - 1], title: currentTitle } } const items = [home, ...trail] if (items.at(-1)?.title !== currentTitle) items.push({ title: currentTitle }) if (items.length > 1) delete items.at(-1)!.path return items } function findNavigationParentKeys( nodes: readonly ClientNavigationNode[], path: string, parentKeys: readonly string[], ): string[] | undefined { for (const node of nodes) { if (isNavigationGroup(node)) { const result = findNavigationParentKeys(node.children, path, [...parentKeys, node.key]) if (result) return result } else if (node.path === path) { return [...parentKeys] } } } export function getNavigationParentKeys(nodes: readonly ClientNavigationNode[], path: string): string[] { return findNavigationParentKeys(nodes, path, []) ?? [] } export function openNavigationParentKeys( path: string, nodes: readonly ClientNavigationNode[], open: (key: string) => void, ) { for (const key of getNavigationParentKeys(nodes, path)) { open(key) } }