2026-07-30 17:59:42 +08:00
|
|
|
import type { Component } from 'vue'
|
|
|
|
|
import {
|
|
|
|
|
Building2,
|
|
|
|
|
CircleUserRound,
|
2026-08-03 17:44:42 +08:00
|
|
|
FileText,
|
|
|
|
|
Gauge,
|
2026-07-30 17:59:42 +08:00
|
|
|
KeyRound,
|
|
|
|
|
Settings,
|
2026-08-03 17:44:42 +08:00
|
|
|
Ship,
|
2026-07-30 17:59:42 +08:00
|
|
|
ShieldCheck,
|
|
|
|
|
Users,
|
|
|
|
|
} from '@lucide/vue'
|
|
|
|
|
|
|
|
|
|
export interface ClientNavigationBaseNode {
|
|
|
|
|
key: string
|
|
|
|
|
title: string
|
|
|
|
|
icon: Component
|
2026-08-03 11:44:50 +08:00
|
|
|
dividerBefore?: boolean
|
|
|
|
|
hidden?: boolean
|
2026-07-30 17:59:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ClientNavigationLeaf extends ClientNavigationBaseNode {
|
|
|
|
|
path: string
|
2026-08-03 12:02:15 +08:00
|
|
|
permission: string
|
2026-07-30 17:59:42 +08:00
|
|
|
children?: never
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ClientNavigationGroup extends ClientNavigationBaseNode {
|
|
|
|
|
path?: never
|
|
|
|
|
permission?: never
|
|
|
|
|
children: readonly [ClientNavigationNode, ...ClientNavigationNode[]]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ClientNavigationNode = ClientNavigationLeaf | ClientNavigationGroup
|
|
|
|
|
|
|
|
|
|
export const clientNavigation: ClientNavigationNode[] = [
|
2026-08-03 17:44:42 +08:00
|
|
|
{ key: 'dashboard', title: '业务总览', icon: Gauge, path: '/', permission: 'dashboard:view' },
|
2026-07-30 17:59:42 +08:00
|
|
|
{
|
|
|
|
|
key: 'first-leg',
|
2026-08-03 11:44:50 +08:00
|
|
|
title: '头程物流',
|
2026-08-03 17:44:42 +08:00
|
|
|
icon: Ship,
|
2026-07-30 17:59:42 +08:00
|
|
|
children: [
|
2026-08-03 17:44:42 +08:00
|
|
|
{ key: 'first-leg-orders', title: '头程下单', icon: FileText, path: '/orders/first-leg', permission: 'order:first-leg:list' },
|
2026-08-03 12:02:15 +08:00
|
|
|
// 原型入口:接入批量导入接口与权限资源后再放开。
|
|
|
|
|
// { key: 'first-leg-import', title: '一键导入', icon: Package, path: '/first-leg/import', permission: 'merchant:first-leg:import' },
|
|
|
|
|
// 原型入口:接入快速下单接口与权限资源后再放开。
|
|
|
|
|
// { key: 'first-leg-quick-order', title: '快速下单', icon: Zap, path: '/first-leg/quick-order', permission: 'merchant:first-leg:quick-create' },
|
2026-07-30 17:59:42 +08:00
|
|
|
],
|
|
|
|
|
},
|
2026-08-03 12:02:15 +08:00
|
|
|
// 原型菜单:对应页面目录与路由已预留,接入后端接口、权限资源后取消注释。
|
|
|
|
|
// {
|
|
|
|
|
// key: 'cross-border',
|
|
|
|
|
// title: '出海下单',
|
|
|
|
|
// icon: PlaneTakeoff,
|
|
|
|
|
// children: [
|
|
|
|
|
// { key: 'cross-border-procurement', title: '海外采购', icon: Package, path: '/cross-border/procurement', permission: 'merchant:cross-border:procurement' },
|
|
|
|
|
// { key: 'cross-border-domestic-trucking', title: '美国本土卡', icon: Truck, path: '/cross-border/domestic-trucking', permission: 'merchant:cross-border:domestic-trucking' },
|
|
|
|
|
// { key: 'cross-border-pallet-shipping', title: '美本土散板', icon: Ship, path: '/cross-border/pallet-shipping', permission: 'merchant:cross-border:pallet-shipping' },
|
|
|
|
|
// ],
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// key: 'overseas-warehouse',
|
|
|
|
|
// title: '海外仓',
|
|
|
|
|
// icon: Warehouse,
|
|
|
|
|
// children: [
|
|
|
|
|
// { key: 'overseas-labeling', title: '贴标申请', icon: Tags, path: '/overseas-warehouse/labeling', permission: 'merchant:overseas-warehouse:labeling' },
|
|
|
|
|
// { key: 'overseas-return-inbound', title: '退货入库', icon: Package, path: '/overseas-warehouse/return-inbound', permission: 'merchant:overseas-warehouse:return-inbound' },
|
|
|
|
|
// { key: 'overseas-return-relabel', title: '退货换标', icon: Tags, path: '/overseas-warehouse/return-relabel', permission: 'merchant:overseas-warehouse:return-relabel' },
|
|
|
|
|
// { key: 'overseas-return-outbound', title: '退货出库', icon: Truck, path: '/overseas-warehouse/return-outbound', permission: 'merchant:overseas-warehouse:return-outbound' },
|
|
|
|
|
// ],
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// key: 'order-management',
|
|
|
|
|
// title: '订单管理',
|
|
|
|
|
// icon: ClipboardList,
|
|
|
|
|
// children: [
|
|
|
|
|
// { key: 'orders-my', title: '我的订单', icon: ReceiptText, path: '/orders/my', permission: 'merchant:order:list' },
|
|
|
|
|
// { key: 'orders-inbound', title: '入库订单列表', icon: Package, path: '/orders/inbound', permission: 'merchant:warehouse:inbound:list' },
|
|
|
|
|
// { key: 'orders-delivery', title: '派送订单列表', icon: Truck, path: '/orders/delivery', permission: 'merchant:warehouse:delivery:list' },
|
|
|
|
|
// ],
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// key: 'finance',
|
|
|
|
|
// title: '财务结算',
|
|
|
|
|
// icon: ReceiptText,
|
|
|
|
|
// children: [
|
|
|
|
|
// { key: 'finance-payables', title: '应付账单', icon: ReceiptText, path: '/finance/payables', permission: 'merchant:finance:payables:list' },
|
|
|
|
|
// ],
|
|
|
|
|
// },
|
2026-07-30 17:59:42 +08:00
|
|
|
{
|
|
|
|
|
key: 'company',
|
|
|
|
|
title: '企业管理',
|
|
|
|
|
icon: Settings,
|
2026-08-03 11:44:50 +08:00
|
|
|
dividerBefore: true,
|
2026-07-30 17:59:42 +08:00
|
|
|
children: [
|
|
|
|
|
{ key: 'company-profile', title: '企业资料', icon: Building2, path: '/company/profile', permission: 'company:profile:view' },
|
|
|
|
|
{ key: 'company-members', title: '企业成员', icon: Users, path: '/organization/members', permission: 'company:member:list' },
|
|
|
|
|
{ key: 'company-roles', title: '角色管理', icon: ShieldCheck, path: '/organization/roles', permission: 'company:role:list' },
|
|
|
|
|
{ key: 'company-permissions', title: '菜单权限', icon: KeyRound, path: '/organization/permissions', permission: 'company:permission:list' },
|
|
|
|
|
],
|
|
|
|
|
},
|
2026-08-03 11:44:50 +08:00
|
|
|
{ key: 'account-profile', title: '我的信息', icon: CircleUserRound, path: '/account/profile', permission: 'account:profile:view' },
|
2026-07-30 17:59:42 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function isNavigationGroup(node: ClientNavigationNode): node is ClientNavigationGroup {
|
|
|
|
|
return 'children' in node && node.children !== undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getNavigationLeaves(nodes: readonly ClientNavigationNode[]): ClientNavigationLeaf[] {
|
|
|
|
|
return nodes.flatMap((node) => {
|
|
|
|
|
if (isNavigationGroup(node)) return getNavigationLeaves(node.children)
|
|
|
|
|
return [node]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function filterNavigationByPermissions(
|
|
|
|
|
nodes: readonly ClientNavigationNode[],
|
|
|
|
|
permissions: readonly string[],
|
|
|
|
|
): ClientNavigationNode[] {
|
|
|
|
|
const allowed = new Set(permissions)
|
|
|
|
|
const result: ClientNavigationNode[] = []
|
|
|
|
|
for (const node of nodes) {
|
2026-08-03 11:44:50 +08:00
|
|
|
if (node.hidden) continue
|
2026-07-30 17:59:42 +08:00
|
|
|
if (!isNavigationGroup(node)) {
|
2026-08-03 12:02:15 +08:00
|
|
|
if (allowed.has(node.permission)) result.push(node)
|
2026-07-30 17:59:42 +08:00
|
|
|
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[]) {
|
2026-08-03 12:02:15 +08:00
|
|
|
return getNavigationLeaves(filterNavigationByPermissions(clientNavigation, permissions))[0]?.path ?? '/forbidden'
|
2026-07-30 17:59:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|