优化
This commit is contained in:
@@ -28,57 +28,44 @@ function collectNavigationNodes(nodes) {
|
||||
])
|
||||
}
|
||||
|
||||
test('keeps the confirmed top-level sequence from the client workbench prototype', () => {
|
||||
test('keeps account and notification routes as fixed-header utilities', () => {
|
||||
assert.deepEqual(
|
||||
clientNavigation.filter((node) => node.path).map(({ title, path, permission }) => ({
|
||||
clientNavigation.filter((node) => node.hidden).map(({ title, path, permission }) => ({
|
||||
title,
|
||||
path,
|
||||
permission,
|
||||
})),
|
||||
[
|
||||
{ title: '业务总览', path: '/', permission: 'dashboard:view' },
|
||||
{ title: '我的信息', path: '/account/profile', permission: 'account:profile:view' },
|
||||
{ title: '我的消息', path: '/notifications', permission: 'merchant:notification:view' },
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps only implemented navigation groups visible', () => {
|
||||
test('organises every implemented business page under five task-oriented sidebar entries', () => {
|
||||
assert.deepEqual(
|
||||
clientNavigation.map((node) => ({
|
||||
clientNavigation.filter((node) => !node.hidden).map((node) => ({
|
||||
title: node.title,
|
||||
children: node.children?.map(({ title }) => title),
|
||||
})),
|
||||
[
|
||||
{ title: '业务总览', children: undefined },
|
||||
{ title: '首页', children: undefined },
|
||||
{
|
||||
title: '头程物流',
|
||||
children: ['订单列表', '快速下单'],
|
||||
},
|
||||
{
|
||||
title: '出海下单',
|
||||
children: ['海外采购', '美国本土卡', '美本土散板'],
|
||||
title: '订单中心',
|
||||
children: ['头程订单', '快速建草稿', '海外采购', '美国本土卡', '美本土散板', '入库订单', '派送订单'],
|
||||
},
|
||||
{
|
||||
title: '海外仓',
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '退货库存', '退货箱号管理'],
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货预报与签收', '退货库存', '退货箱号管理'],
|
||||
},
|
||||
{
|
||||
title: '订单管理',
|
||||
children: ['入库订单列表', '派送订单列表'],
|
||||
},
|
||||
{
|
||||
title: '财务结算',
|
||||
title: '费用与账单',
|
||||
children: ['资金账户', '客户账单'],
|
||||
},
|
||||
{
|
||||
title: '企业管理',
|
||||
title: '企业设置',
|
||||
children: ['企业资料', '企业成员', '角色管理', '菜单权限'],
|
||||
},
|
||||
{ title: '我的信息', children: undefined },
|
||||
{
|
||||
title: '消息中心',
|
||||
children: ['我的消息'],
|
||||
},
|
||||
],
|
||||
)
|
||||
})
|
||||
@@ -103,14 +90,14 @@ test('flattens visible leaves with unique paths', () => {
|
||||
'/cross-border/procurement',
|
||||
'/cross-border/domestic-trucking',
|
||||
'/cross-border/pallet-shipping',
|
||||
'/orders/inbound',
|
||||
'/orders/delivery',
|
||||
'/overseas-warehouse/labeling',
|
||||
'/overseas-warehouse/storage-transfer',
|
||||
'/overseas-warehouse/cargo-disposal',
|
||||
'/overseas-warehouse/return-relabel',
|
||||
'/overseas-warehouse/return-inventory',
|
||||
'/overseas-warehouse/return-cartons',
|
||||
'/orders/inbound',
|
||||
'/orders/delivery',
|
||||
'/finance/account',
|
||||
'/finance/bills',
|
||||
'/company/profile',
|
||||
@@ -127,8 +114,8 @@ test('flattens visible leaves with unique paths', () => {
|
||||
test('filters inaccessible leaves and empty navigation groups', () => {
|
||||
const visible = filterNavigationByPermissions(clientNavigation, ['account:profile:view', 'order:first-leg:list'])
|
||||
|
||||
assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/orders/first-leg', '/account/profile'])
|
||||
assert.deepEqual(visible.map((item) => item.title), ['头程物流', '我的信息'])
|
||||
assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/orders/first-leg'])
|
||||
assert.deepEqual(visible.map((item) => item.title), ['订单中心'])
|
||||
assert.equal(getFirstAccessiblePath(['account:profile:view']), '/account/profile')
|
||||
assert.equal(getFirstAccessiblePath([]), '/forbidden')
|
||||
})
|
||||
@@ -142,8 +129,8 @@ test('excludes entries marked for manual hiding without affecting other visible
|
||||
assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/shown'])
|
||||
})
|
||||
|
||||
test('puts enterprise management behind the prototype settings divider', () => {
|
||||
const company = clientNavigation.find((node) => node.key === 'company')
|
||||
test('puts enterprise settings behind the prototype settings divider', () => {
|
||||
const company = clientNavigation.find((node) => node.key === 'company-center')
|
||||
|
||||
assert.ok(company)
|
||||
assert.equal(company.dividerBefore, true)
|
||||
@@ -202,8 +189,8 @@ test('returns navigation parent keys from outermost to innermost', () => {
|
||||
]
|
||||
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/'), [])
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/orders/first-leg'), ['first-leg'])
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/organization/members'), ['company'])
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/orders/first-leg'), ['order-center'])
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/organization/members'), ['company-center'])
|
||||
assert.deepEqual(getNavigationParentKeys(syntheticTree, '/nested/leaf'), ['level-one', 'level-two'])
|
||||
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/not-found'), [])
|
||||
})
|
||||
@@ -212,18 +199,17 @@ test('builds complete breadcrumbs without repeating the current navigation page'
|
||||
assert.deepEqual(getRouteBreadcrumbs('/', '业务总览'), [{ title: '首页' }])
|
||||
assert.deepEqual(getRouteBreadcrumbs('/orders/first-leg', '订单列表'), [
|
||||
{ title: '首页', path: '/' },
|
||||
{ title: '头程物流' },
|
||||
{ title: '订单中心' },
|
||||
{ title: '订单列表' },
|
||||
])
|
||||
assert.deepEqual(getRouteBreadcrumbs('/orders/first-leg/create', '创建订单'), [
|
||||
{ title: '首页', path: '/' },
|
||||
{ title: '头程物流' },
|
||||
{ title: '订单列表', path: '/orders/first-leg' },
|
||||
{ title: '订单中心' },
|
||||
{ title: '头程订单', path: '/orders/first-leg' },
|
||||
{ title: '创建订单' },
|
||||
])
|
||||
assert.deepEqual(getRouteBreadcrumbs('/notifications/NOTICE-001', '消息详情'), [
|
||||
{ title: '首页', path: '/' },
|
||||
{ title: '消息中心' },
|
||||
{ title: '我的消息', path: '/notifications' },
|
||||
{ title: '消息详情' },
|
||||
])
|
||||
@@ -233,12 +219,12 @@ test('builds complete breadcrumbs without repeating the current navigation page'
|
||||
])
|
||||
})
|
||||
|
||||
test('opens the company parent group for a company route', () => {
|
||||
test('opens the enterprise settings parent group for a company route', () => {
|
||||
const openedKeys = []
|
||||
|
||||
openNavigationParentKeys('/organization/members', clientNavigation, (key) => openedKeys.push(key))
|
||||
|
||||
assert.deepEqual(openedKeys, ['company'])
|
||||
assert.deepEqual(openedKeys, ['company-center'])
|
||||
})
|
||||
|
||||
test('opens nested parent groups from outermost to innermost', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClipboardList, ReceiptText, Settings, Warehouse } from '@lucide/vue'
|
||||
import { clientModuleRegistry } from '../modules/registry.ts'
|
||||
import type { ClientModuleManifest } from '../modules/types.ts'
|
||||
import {
|
||||
@@ -15,7 +16,7 @@ export {
|
||||
type ClientNavigationNode,
|
||||
} from './tree.ts'
|
||||
|
||||
function buildModuleNavigation(module: ClientModuleManifest): ClientNavigationNode[] {
|
||||
function buildModuleNavigationLeaves(module: ClientModuleManifest): ClientNavigationLeaf[] {
|
||||
const leaves: ClientNavigationLeaf[] = []
|
||||
for (const route of module.routes) {
|
||||
if (route.release !== 'active' || !route.navigation) continue
|
||||
@@ -27,18 +28,75 @@ function buildModuleNavigation(module: ClientModuleManifest): ClientNavigationNo
|
||||
permission: route.meta.permission,
|
||||
})
|
||||
}
|
||||
if (!module.navigationGroup) return leaves
|
||||
if (leaves.length === 0) return []
|
||||
return [{
|
||||
key: module.key,
|
||||
title: module.navigationGroup.title,
|
||||
icon: module.navigationGroup.icon,
|
||||
dividerBefore: module.navigationGroup.dividerBefore,
|
||||
children: leaves as [ClientNavigationNode, ...ClientNavigationNode[]],
|
||||
}]
|
||||
return leaves
|
||||
}
|
||||
|
||||
export const clientNavigation: ClientNavigationNode[] = clientModuleRegistry.flatMap(buildModuleNavigation)
|
||||
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[],
|
||||
@@ -59,7 +117,11 @@ export function filterNavigationByPermissions(
|
||||
}
|
||||
|
||||
export function getFirstAccessiblePath(permissions: readonly string[]) {
|
||||
return getNavigationLeaves(filterNavigationByPermissions(clientNavigation, permissions))[0]?.path ?? '/forbidden'
|
||||
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 {
|
||||
@@ -91,6 +153,9 @@ export function getRouteBreadcrumbs(path: string, currentTitle: string): ClientB
|
||||
.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
|
||||
|
||||
@@ -116,7 +116,7 @@ export const useWorkspaceTabsStore = defineStore('client-workspace-tabs', () =>
|
||||
const next: WorkspaceTab = {
|
||||
key: routeName,
|
||||
routeName,
|
||||
title: String(route.meta.title || routeName),
|
||||
title: String(route.meta.workspaceTitle || route.meta.title || routeName),
|
||||
fullPath: route.fullPath,
|
||||
permission: typeof route.meta.permission === 'string' ? route.meta.permission : null,
|
||||
lastVisitedAt: Date.now(),
|
||||
|
||||
@@ -23,6 +23,7 @@ test('workspace tabs exclude public and forbidden routes and use one tab per rou
|
||||
assert.match(storeSource, /const routeName = String\(route\.name\)/)
|
||||
assert.match(storeSource, /key: routeName/)
|
||||
assert.match(storeSource, /tabs\.value\.find\(\(tab\) => tab\.key === routeName\)/)
|
||||
assert.match(storeSource, /title: String\(route\.meta\.workspaceTitle \|\| route\.meta\.title \|\| routeName\)/)
|
||||
})
|
||||
|
||||
test('closing tabs invalidates their cached instance and returns the most recently visited fallback', () => {
|
||||
|
||||
Reference in New Issue
Block a user