193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
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('first-leg-quick-order', '快速建草稿'),
|
|
requireLeaf('cross-border-procurement'),
|
|
requireLeaf('cross-border-domestic-trucking'),
|
|
requireLeaf('cross-border-pallet-shipping'),
|
|
requireLeaf('orders-inbound', '入库订单'),
|
|
requireLeaf('orders-delivery', '派送订单'),
|
|
],
|
|
},
|
|
{
|
|
key: 'overseas-warehouse-center',
|
|
title: '海外仓',
|
|
icon: Warehouse,
|
|
children: [
|
|
requireLeaf('overseas-labeling'),
|
|
requireLeaf('overseas-storage-transfer'),
|
|
requireLeaf('overseas-cargo-disposal'),
|
|
requireLeaf('overseas-return-relabel', '退货预报与签收'),
|
|
requireLeaf('overseas-return-inventory'),
|
|
requireLeaf('overseas-return-cartons'),
|
|
],
|
|
},
|
|
{
|
|
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)
|
|
}
|
|
}
|