Release/test 合并上生产 #2
@@ -27,7 +27,7 @@ pnpm test
|
||||
|
||||
工作台已从一期路由与菜单移除;登录后按当前角色进入第一个有权限的业务页面。一期 Admin 路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。
|
||||
|
||||
后台菜单按“业务管理”和“系统管理”分组。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;内部用户使用 IAM 的真实查询和新增接口。运行时不再提供本地演示业务数据或静态角色预览。
|
||||
后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;内部用户使用 IAM 的真实查询和新增接口。运行时不再提供本地演示业务数据或静态角色预览。
|
||||
|
||||
## 本地 Gateway 联调
|
||||
|
||||
|
||||
@@ -19,29 +19,23 @@ const stubs = {
|
||||
}
|
||||
|
||||
describe('AdminSidebarMenu', () => {
|
||||
it('renders grouped page entries and forwards the active route', () => {
|
||||
it('renders the visible phase-one modules and forwards the active route', () => {
|
||||
const wrapper = mount(AdminSidebarMenu, {
|
||||
props: { items: adminNavigation, activePath: '/system/users', collapsed: false },
|
||||
global: { stubs },
|
||||
})
|
||||
|
||||
expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users')
|
||||
expect(wrapper.findAll('[data-kind="group"] header').map((node) => node.text())).toEqual([
|
||||
'业务管理',
|
||||
'基础资料',
|
||||
'系统管理',
|
||||
])
|
||||
expect(wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index'))).toEqual([
|
||||
'/customers',
|
||||
'/customers/reviews',
|
||||
'/warehouses',
|
||||
'/orders/first-leg',
|
||||
'/master-data/express-companies',
|
||||
'/master-data/counterparties',
|
||||
'/system/users',
|
||||
'/system/roles',
|
||||
'/system/permissions',
|
||||
])
|
||||
const groupTitles = wrapper.findAll('[data-kind="group"] header').map((node) => node.text())
|
||||
const paths = wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index'))
|
||||
|
||||
expect(groupTitles).toContain('客户管理')
|
||||
expect(groupTitles).toContain('系统管理')
|
||||
expect(groupTitles).not.toContain('财务管理')
|
||||
expect(paths).not.toContain('/dashboard')
|
||||
expect(paths).toContain('/orders/first-leg')
|
||||
expect(paths).not.toContain('/prototype/reports/overview')
|
||||
expect(paths).toContain('/system/permissions')
|
||||
})
|
||||
|
||||
it('forwards desktop collapse state to Element Plus', () => {
|
||||
@@ -61,7 +55,8 @@ describe('AdminSidebarMenu', () => {
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('菜单权限')
|
||||
expect(wrapper.text()).not.toContain('客户管理')
|
||||
expect(wrapper.text()).not.toContain('首页数据看板')
|
||||
expect(wrapper.text()).not.toContain('客户列表')
|
||||
})
|
||||
|
||||
it('recursively renders three navigation levels', () => {
|
||||
|
||||
@@ -8,57 +8,59 @@ import {
|
||||
} from './adminNavigation'
|
||||
|
||||
describe('adminNavigation', () => {
|
||||
it('keeps phase-one pages grouped without exposing the deferred dashboard', () => {
|
||||
it('keeps only implemented phase-one menus visible while retaining prototype nodes as comments', () => {
|
||||
expect(adminNavigation.map((item) => item.title)).toEqual([
|
||||
'业务管理',
|
||||
'基础资料',
|
||||
'客户管理',
|
||||
'国内仓',
|
||||
'头程订单',
|
||||
'通用',
|
||||
'系统管理',
|
||||
])
|
||||
expect(adminNavigation[0].children?.map((item) => item.title)).toEqual([
|
||||
'客户管理',
|
||||
'客户列表',
|
||||
'企业审核',
|
||||
'仓库管理',
|
||||
'头程订单',
|
||||
])
|
||||
expect(adminNavigation[1].children?.map((item) => item.title)).toEqual(['快递公司', '往来单位'])
|
||||
expect(adminNavigation[2].children?.map((item) => item.title)).toEqual([
|
||||
'内部用户',
|
||||
'角色管理',
|
||||
expect(adminNavigation.find((item) => item.key === 'general')?.children?.map((item) => item.title)).toContain('快递公司')
|
||||
expect(adminNavigation.find((item) => item.key === 'system')?.children?.map((item) => item.title)).toEqual([
|
||||
'用户管理',
|
||||
'角色权限',
|
||||
'菜单权限',
|
||||
])
|
||||
})
|
||||
|
||||
it('assigns every page a unique path and permission code', () => {
|
||||
it('assigns every page a unique path and keeps real permissions unique', () => {
|
||||
const leaves = getNavigationLeaves(adminNavigation)
|
||||
const paths = leaves.map((item) => item.path)
|
||||
const permissions = leaves.map((item) => item.permission)
|
||||
const permissions = leaves.flatMap((item) => item.permission ? [item.permission] : [])
|
||||
|
||||
expect(paths).toEqual([
|
||||
'/customers',
|
||||
'/customers/reviews',
|
||||
'/warehouses',
|
||||
'/orders/first-leg',
|
||||
'/master-data/express-companies',
|
||||
'/master-data/counterparties',
|
||||
'/system/users',
|
||||
'/system/roles',
|
||||
'/system/permissions',
|
||||
])
|
||||
expect(paths).not.toContain('/dashboard')
|
||||
expect(paths).not.toContain('/prototype/finance/profit-report')
|
||||
expect(paths).not.toContain('/prototype/system-management/brand-settings')
|
||||
expect(paths).toContain('/system/permissions')
|
||||
expect(new Set(paths).size).toBe(paths.length)
|
||||
expect(new Set(permissions).size).toBe(permissions.length)
|
||||
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
|
||||
})
|
||||
|
||||
it('recursively filters leaves without mutating the source tree', () => {
|
||||
it('filters protected pages without mutating the source tree', () => {
|
||||
const sourceSnapshot = JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)
|
||||
const filtered = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list']))
|
||||
|
||||
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual(['/system/permissions'])
|
||||
const paths = getNavigationLeaves(filtered).map((item) => item.path)
|
||||
expect(paths).toContain('/system/permissions')
|
||||
expect(paths).not.toContain('/prototype/finance/profit-report')
|
||||
expect(paths).not.toContain('/customers')
|
||||
expect(filtered).not.toBe(adminNavigation)
|
||||
expect(filtered[0]).not.toBe(adminNavigation[2])
|
||||
expect(filtered.find((item) => item.key === 'system')).not.toBe(adminNavigation.find((item) => item.key === 'system'))
|
||||
expect(JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)).toBe(sourceSnapshot)
|
||||
})
|
||||
|
||||
it('supports manually hiding a module without removing its route definition', () => {
|
||||
const filtered = filterAdminNavigation([{ key: 'hidden', title: '隐藏模块', icon: adminNavigation[0].icon, path: '/hidden', hidden: true }], new Set())
|
||||
|
||||
expect(filtered).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps directories based on accessible descendants instead of a directory permission', () => {
|
||||
const nodes: AdminNavigationNode[] = [{
|
||||
key: 'operations',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Component } from 'vue'
|
||||
import {
|
||||
BriefcaseBusiness,
|
||||
Building2,
|
||||
ContactRound,
|
||||
ClipboardList,
|
||||
@@ -20,44 +19,215 @@ export interface AdminNavigationNode {
|
||||
icon: Component
|
||||
path?: string
|
||||
permission?: string
|
||||
/** Set to true to remove a prototype module from the sidebar without deleting its route. */
|
||||
hidden?: boolean
|
||||
children?: AdminNavigationNode[]
|
||||
}
|
||||
|
||||
export interface AdminNavigationLeaf extends AdminNavigationNode {
|
||||
path: string
|
||||
permission: string
|
||||
permission?: string
|
||||
children?: never
|
||||
}
|
||||
|
||||
export const adminNavigation: AdminNavigationNode[] = [
|
||||
// 原型菜单暂未接入一期范围;恢复展示时移除对应注释即可。
|
||||
// { key: 'dashboard', title: '首页数据看板', icon: ClipboardList, path: '/dashboard' },
|
||||
{
|
||||
key: 'business',
|
||||
title: '业务管理',
|
||||
icon: BriefcaseBusiness,
|
||||
key: 'customers',
|
||||
title: '客户管理',
|
||||
icon: Building2,
|
||||
children: [
|
||||
{ key: 'customers', title: '客户管理', icon: Building2, path: '/customers', permission: 'platform:customer:list' },
|
||||
{ key: 'customers-list', title: '客户列表', icon: Building2, path: '/customers', permission: 'platform:customer:list' },
|
||||
{ key: 'enterprise-reviews', title: '企业审核', icon: FileCheck2, path: '/customers/reviews', permission: 'platform:customer:review' },
|
||||
{ key: 'warehouses', title: '仓库管理', icon: WarehouseIcon, path: '/warehouses', permission: 'platform:warehouse:list' },
|
||||
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'platform:order:list' },
|
||||
// { key: 'warehouse-tier-fee', title: '海外仓阶梯费用', icon: Building2, path: '/prototype/customer-management/warehouse-tier-fee' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'master-data',
|
||||
title: '基础资料',
|
||||
key: 'domestic-warehouse',
|
||||
title: '国内仓',
|
||||
icon: WarehouseIcon,
|
||||
children: [
|
||||
{ key: 'warehouses', title: '仓库管理', icon: WarehouseIcon, path: '/warehouses', permission: 'platform:warehouse:list' },
|
||||
// { key: 'inbound-list', title: '入库单列表', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-list' },
|
||||
// { key: 'inbound-dedup', title: '入库单测量', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-dedup' },
|
||||
// { key: 'inbound-measure', title: '测量数据审核', icon: FileCheck2, path: '/prototype/domestic-warehouse/inbound-measure' },
|
||||
// { key: 'inbound-receive', title: '接收审核', icon: FileCheck2, path: '/prototype/domestic-warehouse/inbound-receive' },
|
||||
// { key: 'overseas-location', title: '库位管理', icon: WarehouseIcon, path: '/prototype/domestic-warehouse/location-management' },
|
||||
// { key: 'pallet-management', title: '托盘管理', icon: WarehouseIcon, path: '/prototype/domestic-warehouse/pallet-management' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'first-leg-orders',
|
||||
title: '头程订单',
|
||||
icon: ClipboardList,
|
||||
path: '/orders/first-leg',
|
||||
permission: 'platform:order:list',
|
||||
},
|
||||
/* {
|
||||
key: 'overseas-delivery',
|
||||
title: '海外派送',
|
||||
icon: Truck,
|
||||
children: [
|
||||
{ key: 'outbound-dispatch', title: '派送单列表', icon: Truck, path: '/prototype/overseas-delivery/dispatch-list' },
|
||||
{ key: 'outbound-vehicle', title: '排车列表', icon: Truck, path: '/prototype/overseas-delivery/vehicle-scheduling' },
|
||||
{ key: 'outbound-bol', title: 'BOL', icon: FileCheck2, path: '/prototype/overseas-delivery/bol' },
|
||||
{ key: 'outbound-selfpick', title: '自提单', icon: ClipboardList, path: '/prototype/overseas-delivery/self-pickup' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'overseas-inventory',
|
||||
title: '海外仓库存',
|
||||
icon: WarehouseIcon,
|
||||
children: [
|
||||
{ key: 'overseas-stock', title: '库存列表', icon: WarehouseIcon, path: '/prototype/overseas-inventory/stock-list' },
|
||||
{ key: 'overseas-inbound', title: '海外入库单', icon: ClipboardList, path: '/prototype/overseas-inventory/inbound-orders' },
|
||||
{ key: 'overseas-outbound', title: '出库单', icon: ClipboardList, path: '/prototype/overseas-inventory/outbound-orders' },
|
||||
{ key: 'overseas-relabel', title: '换标订单', icon: ClipboardList, path: '/prototype/overseas-inventory/relabel-orders' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'service-center',
|
||||
title: '服务中心',
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
{ key: 'booking-list', title: '订舱列表', icon: ClipboardList, path: '/prototype/service-center/booking-list' },
|
||||
{ key: 'truck-list', title: '拖车列表', icon: Truck, path: '/prototype/service-center/truck-list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'supplier-center',
|
||||
title: '供应商中心',
|
||||
icon: ContactRound,
|
||||
children: [
|
||||
{ key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/prototype/supplier-center/supplier-list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'container-management',
|
||||
title: '货柜管理',
|
||||
icon: WarehouseIcon,
|
||||
children: [
|
||||
{ key: 'container-list', title: '货柜列表', icon: WarehouseIcon, path: '/prototype/container-management/container-list' },
|
||||
{ key: 'container-manifest', title: '装柜清单', icon: ClipboardList, path: '/prototype/container-management/manifest' },
|
||||
{ key: 'container-preload', title: '预装清单', icon: ClipboardList, path: '/prototype/container-management/preload-list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'overseas-business',
|
||||
title: '海外业务',
|
||||
icon: Building2,
|
||||
children: [
|
||||
{ key: 'overseas-users', title: '用户管理', icon: Users, path: '/prototype/overseas-business/users' },
|
||||
{ key: 'overseas-purchase', title: '采购订单', icon: ClipboardList, path: '/prototype/overseas-business/purchase-orders' },
|
||||
{ key: 'overseas-truck', title: '本土卡车', icon: Truck, path: '/prototype/overseas-business/local-truck' },
|
||||
{ key: 'overseas-looseboard', title: '本土散板', icon: ClipboardList, path: '/prototype/overseas-business/local-looseboard' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'overseas-warehouse-orders',
|
||||
title: '海外仓下单',
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
{ key: 'overseas-order-inbound', title: '退货入库', icon: ClipboardList, path: '/prototype/overseas-warehouse-orders/return-inbound' },
|
||||
{ key: 'overseas-order-relabel', title: '退货换标', icon: ClipboardList, path: '/prototype/overseas-warehouse-orders/return-relabel' },
|
||||
{ key: 'overseas-order-outbound', title: '退货出库', icon: ClipboardList, path: '/prototype/overseas-warehouse-orders/return-outbound' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'fba-returns',
|
||||
title: 'FBA退货',
|
||||
icon: FileCheck2,
|
||||
children: [
|
||||
{ key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/prototype/fba-returns/inbound-orders' },
|
||||
{ key: 'fba-relabel-order', title: '换标单', icon: ClipboardList, path: '/prototype/fba-returns/relabel-orders' },
|
||||
{ key: 'fba-outbound-order', title: '出库单', icon: ClipboardList, path: '/prototype/fba-returns/outbound-orders' },
|
||||
{ key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/prototype/fba-returns/pallet-management' },
|
||||
{ key: 'fba-carton', title: '箱号管理', icon: ClipboardList, path: '/prototype/fba-returns/carton-management' },
|
||||
{ key: 'fba-product', title: '产品库', icon: Database, path: '/prototype/fba-returns/product-catalog' },
|
||||
{ key: 'fba-return-stock', title: '退货库存', icon: WarehouseIcon, path: '/prototype/fba-returns/return-stock' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
title: '财务管理',
|
||||
icon: Database,
|
||||
children: [
|
||||
{ key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list' },
|
||||
{ key: 'counterparties', title: '往来单位', icon: ContactRound, path: '/master-data/counterparties', permission: 'platform:counterparty:list' },
|
||||
{ key: 'customer-arap', title: '客户应收应付', icon: Database, path: '/prototype/finance/customer-arap' },
|
||||
{ key: 'customer-recon-stmt', title: '客户对账单', icon: FileCheck2, path: '/prototype/finance/customer-reconciliation' },
|
||||
{ key: 'settlement-ar', title: '结算单应收', icon: FileCheck2, path: '/prototype/finance/settlement-ar' },
|
||||
{ key: 'settlement-ap', title: '结算单应付', icon: FileCheck2, path: '/prototype/finance/settlement-ap' },
|
||||
{ key: 'finance-cost', title: '费用分析', icon: Database, path: '/prototype/finance/cost-analysis' },
|
||||
{ key: 'profit', title: '利润表', icon: Database, path: '/prototype/finance/profit-report' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'vehicle-management',
|
||||
title: '车辆管理',
|
||||
icon: Truck,
|
||||
children: [
|
||||
{ key: 'vehicle-list', title: '车辆列表', icon: Truck, path: '/prototype/vehicle-management/vehicle-list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'reports',
|
||||
title: '报表中心',
|
||||
icon: Database,
|
||||
children: [
|
||||
{ key: 'report-overview', title: '报表总览', icon: Database, path: '/prototype/reports/overview' },
|
||||
{ key: 'report-income', title: '收支报表', icon: Database, path: '/prototype/reports/income-expense' },
|
||||
{ key: 'report-ar-aging', title: '应收账龄分析', icon: Database, path: '/prototype/reports/ar-aging' },
|
||||
{ key: 'report-cost', title: '费用构成分析', icon: Database, path: '/prototype/reports/cost-composition' },
|
||||
{ key: 'report-freight', title: '运费报表', icon: Database, path: '/prototype/reports/freight' },
|
||||
{ key: 'report-business', title: '业务统计报表', icon: Database, path: '/prototype/reports/business-statistics' },
|
||||
],
|
||||
},
|
||||
*/
|
||||
{
|
||||
key: 'general',
|
||||
title: '通用',
|
||||
icon: Database,
|
||||
children: [
|
||||
// { key: 'general-product', title: '产品信息', icon: Database, path: '/prototype/general/product-information' },
|
||||
// { key: 'general-fee', title: '费用项目', icon: Database, path: '/prototype/general/fee-items' },
|
||||
// { key: 'general-fba', title: 'FBA编码地址', icon: Database, path: '/prototype/general/fba-address' },
|
||||
// { key: 'general-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/prototype/general/pallet-management' },
|
||||
// { key: 'general-location', title: '库位/货架管理', icon: WarehouseIcon, path: '/prototype/general/location-management' },
|
||||
{ key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list' },
|
||||
{ key: 'counterparties', title: '往来单位', icon: ContactRound, path: '/master-data/counterparties', permission: 'platform:counterparty:list' },
|
||||
// { key: 'general-voyage', title: '船期价格表', icon: Database, path: '/prototype/general/voyage-price-list' },
|
||||
],
|
||||
},
|
||||
/* {
|
||||
key: 'sales',
|
||||
title: '销售管理',
|
||||
icon: Users,
|
||||
children: [
|
||||
{ key: 'sales-visits', title: '拜访记录', icon: Users, path: '/prototype/sales/visit-records' },
|
||||
{ key: 'sales-commission', title: '销售提成', icon: Database, path: '/prototype/sales/commission' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
title: '消息中心',
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
{ key: 'message-management', title: '消息管理', icon: ClipboardList, path: '/prototype/messages/management' },
|
||||
{ key: 'message-template', title: '模板管理', icon: ClipboardList, path: '/prototype/messages/templates' },
|
||||
{ key: 'my-messages', title: '我的消息', icon: ClipboardList, path: '/prototype/messages/my-messages' },
|
||||
],
|
||||
},
|
||||
*/
|
||||
{
|
||||
key: 'system',
|
||||
title: '系统管理',
|
||||
icon: Settings,
|
||||
children: [
|
||||
{ key: 'internal-users', title: '内部用户', icon: Users, path: '/system/users', permission: 'platform:user:list' },
|
||||
{ key: 'roles', title: '角色管理', icon: ShieldCheck, path: '/system/roles', permission: 'platform:role:list' },
|
||||
{ key: 'internal-users', title: '用户管理', icon: Users, path: '/system/users', permission: 'platform:user:list' },
|
||||
{ key: 'roles', title: '角色权限', icon: ShieldCheck, path: '/system/roles', permission: 'platform:role:list' },
|
||||
{ key: 'permissions', title: '菜单权限', icon: KeyRound, path: '/system/permissions', permission: 'platform:permission:list' },
|
||||
// { key: 'tenant-management', title: '租户管理', icon: Building2, path: '/prototype/system-management/tenant-management' },
|
||||
// { key: 'brand-settings', title: '品牌设置', icon: Settings, path: '/prototype/system-management/brand-settings' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -65,7 +235,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
export function getNavigationLeaves(nodes: AdminNavigationNode[]): AdminNavigationLeaf[] {
|
||||
return nodes.flatMap((node) => {
|
||||
if (node.children?.length) return getNavigationLeaves(node.children)
|
||||
if (!node.path || !node.permission) return []
|
||||
if (!node.path) return []
|
||||
return [node as AdminNavigationLeaf]
|
||||
})
|
||||
}
|
||||
@@ -75,11 +245,12 @@ export function filterAdminNavigation(
|
||||
permissionCodes: ReadonlySet<string>,
|
||||
): AdminNavigationNode[] {
|
||||
return nodes.flatMap((node) => {
|
||||
if (node.hidden) return []
|
||||
if (node.children?.length) {
|
||||
const children = filterAdminNavigation(node.children, permissionCodes)
|
||||
return children.length ? [{ ...node, children }] : []
|
||||
}
|
||||
if (!node.path || !node.permission || !permissionCodes.has(node.permission)) return []
|
||||
if (!node.path || (node.permission && !permissionCodes.has(node.permission))) return []
|
||||
return [{ ...node }]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppLayout from '@/layouts/AppLayout.vue'
|
||||
import { decideRouteAccess } from '@/router/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { prototypeModuleRoutes } from '@/router/prototypeModuleRoutes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -24,6 +25,7 @@ const router = createRouter({
|
||||
component: () => import('@/views/ModuleView.vue'),
|
||||
meta: { title: '进入管理后台', description: '正在进入当前角色可访问的业务页面' },
|
||||
},
|
||||
...prototypeModuleRoutes,
|
||||
{
|
||||
path: 'customers',
|
||||
name: 'customers',
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
type PrototypeRoute = readonly [path: string, name: string, title: string]
|
||||
type ModuleView = () => Promise<Component>
|
||||
|
||||
const moduleViews = {
|
||||
dashboard: () => import('@/views/prototype/dashboard/ModuleView.vue'),
|
||||
customerManagement: () => import('@/views/prototype/customer-management/ModuleView.vue'),
|
||||
domesticWarehouse: () => import('@/views/prototype/domestic-warehouse/ModuleView.vue'),
|
||||
overseasDelivery: () => import('@/views/prototype/overseas-delivery/ModuleView.vue'),
|
||||
overseasInventory: () => import('@/views/prototype/overseas-inventory/ModuleView.vue'),
|
||||
serviceCenter: () => import('@/views/prototype/service-center/ModuleView.vue'),
|
||||
supplierCenter: () => import('@/views/prototype/supplier-center/ModuleView.vue'),
|
||||
containerManagement: () => import('@/views/prototype/container-management/ModuleView.vue'),
|
||||
overseasBusiness: () => import('@/views/prototype/overseas-business/ModuleView.vue'),
|
||||
overseasWarehouseOrders: () => import('@/views/prototype/overseas-warehouse-orders/ModuleView.vue'),
|
||||
fbaReturns: () => import('@/views/prototype/fba-returns/ModuleView.vue'),
|
||||
finance: () => import('@/views/prototype/finance/ModuleView.vue'),
|
||||
vehicleManagement: () => import('@/views/prototype/vehicle-management/ModuleView.vue'),
|
||||
reports: () => import('@/views/prototype/reports/ModuleView.vue'),
|
||||
general: () => import('@/views/prototype/general/ModuleView.vue'),
|
||||
sales: () => import('@/views/prototype/sales/ModuleView.vue'),
|
||||
messages: () => import('@/views/prototype/messages/ModuleView.vue'),
|
||||
systemManagement: () => import('@/views/prototype/system-management/ModuleView.vue'),
|
||||
} satisfies Record<string, ModuleView>
|
||||
|
||||
function createRoutes(view: ModuleView, routes: PrototypeRoute[]): RouteRecordRaw[] {
|
||||
return routes.map(([path, name, title]) => ({
|
||||
path,
|
||||
name,
|
||||
component: view,
|
||||
props: { title, description: `用于承载${title}相关业务信息` },
|
||||
meta: { title, description: `用于承载${title}相关业务信息` },
|
||||
}))
|
||||
}
|
||||
|
||||
export const prototypeModuleRoutes: RouteRecordRaw[] = [
|
||||
...createRoutes(moduleViews.dashboard, [['dashboard', 'dashboard', '首页数据看板']]),
|
||||
...createRoutes(moduleViews.customerManagement, [
|
||||
['prototype/customer-management/warehouse-tier-fee', 'warehouse-tier-fee', '海外仓阶梯费用'],
|
||||
]),
|
||||
...createRoutes(moduleViews.domesticWarehouse, [
|
||||
['prototype/domestic-warehouse/inbound-list', 'inbound-list', '入库单列表'],
|
||||
['prototype/domestic-warehouse/inbound-dedup', 'inbound-dedup', '入库单测量'],
|
||||
['prototype/domestic-warehouse/inbound-measure', 'inbound-measure', '测量数据审核'],
|
||||
['prototype/domestic-warehouse/inbound-receive', 'inbound-receive', '接收审核'],
|
||||
['prototype/domestic-warehouse/location-management', 'domestic-location-management', '库位管理'],
|
||||
['prototype/domestic-warehouse/pallet-management', 'domestic-pallet-management', '托盘管理'],
|
||||
]),
|
||||
...createRoutes(moduleViews.overseasDelivery, [
|
||||
['prototype/overseas-delivery/dispatch-list', 'outbound-dispatch', '派送单列表'],
|
||||
['prototype/overseas-delivery/vehicle-scheduling', 'outbound-vehicle', '排车列表'],
|
||||
['prototype/overseas-delivery/bol', 'outbound-bol', 'BOL'],
|
||||
['prototype/overseas-delivery/self-pickup', 'outbound-selfpick', '自提单'],
|
||||
]),
|
||||
...createRoutes(moduleViews.overseasInventory, [
|
||||
['prototype/overseas-inventory/stock-list', 'overseas-stock', '库存列表'],
|
||||
['prototype/overseas-inventory/inbound-orders', 'overseas-inbound', '海外入库单'],
|
||||
['prototype/overseas-inventory/outbound-orders', 'overseas-outbound', '出库单'],
|
||||
['prototype/overseas-inventory/relabel-orders', 'overseas-relabel', '换标订单'],
|
||||
]),
|
||||
...createRoutes(moduleViews.serviceCenter, [
|
||||
['prototype/service-center/booking-list', 'booking-list', '订舱列表'],
|
||||
['prototype/service-center/truck-list', 'truck-list', '拖车列表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.supplierCenter, [
|
||||
['prototype/supplier-center/supplier-list', 'supplier-list', '供应商列表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.containerManagement, [
|
||||
['prototype/container-management/container-list', 'container-list', '货柜列表'],
|
||||
['prototype/container-management/manifest', 'container-manifest', '装柜清单'],
|
||||
['prototype/container-management/preload-list', 'container-preload', '预装清单'],
|
||||
]),
|
||||
...createRoutes(moduleViews.overseasBusiness, [
|
||||
['prototype/overseas-business/users', 'overseas-users', '用户管理'],
|
||||
['prototype/overseas-business/purchase-orders', 'overseas-purchase', '采购订单'],
|
||||
['prototype/overseas-business/local-truck', 'overseas-truck', '本土卡车'],
|
||||
['prototype/overseas-business/local-looseboard', 'overseas-looseboard', '本土散板'],
|
||||
]),
|
||||
...createRoutes(moduleViews.overseasWarehouseOrders, [
|
||||
['prototype/overseas-warehouse-orders/return-inbound', 'overseas-order-inbound', '退货入库'],
|
||||
['prototype/overseas-warehouse-orders/return-relabel', 'overseas-order-relabel', '退货换标'],
|
||||
['prototype/overseas-warehouse-orders/return-outbound', 'overseas-order-outbound', '退货出库'],
|
||||
]),
|
||||
...createRoutes(moduleViews.fbaReturns, [
|
||||
['prototype/fba-returns/inbound-orders', 'fba-inbound-order', '入库单'],
|
||||
['prototype/fba-returns/relabel-orders', 'fba-relabel-order', '换标单'],
|
||||
['prototype/fba-returns/outbound-orders', 'fba-outbound-order', '出库单'],
|
||||
['prototype/fba-returns/pallet-management', 'fba-pallet', '托盘管理'],
|
||||
['prototype/fba-returns/carton-management', 'fba-carton', '箱号管理'],
|
||||
['prototype/fba-returns/product-catalog', 'fba-product', '产品库'],
|
||||
['prototype/fba-returns/return-stock', 'fba-return-stock', '退货库存'],
|
||||
]),
|
||||
...createRoutes(moduleViews.finance, [
|
||||
['prototype/finance/customer-arap', 'customer-arap', '客户应收应付'],
|
||||
['prototype/finance/customer-reconciliation', 'customer-recon-stmt', '客户对账单'],
|
||||
['prototype/finance/settlement-ar', 'settlement-ar', '结算单应收'],
|
||||
['prototype/finance/settlement-ap', 'settlement-ap', '结算单应付'],
|
||||
['prototype/finance/cost-analysis', 'finance-cost', '费用分析'],
|
||||
['prototype/finance/profit-report', 'profit', '利润表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.vehicleManagement, [
|
||||
['prototype/vehicle-management/vehicle-list', 'vehicle-list', '车辆列表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.reports, [
|
||||
['prototype/reports/overview', 'report-overview', '报表总览'],
|
||||
['prototype/reports/income-expense', 'report-income', '收支报表'],
|
||||
['prototype/reports/ar-aging', 'report-ar-aging', '应收账龄分析'],
|
||||
['prototype/reports/cost-composition', 'report-cost', '费用构成分析'],
|
||||
['prototype/reports/freight', 'report-freight', '运费报表'],
|
||||
['prototype/reports/business-statistics', 'report-business', '业务统计报表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.general, [
|
||||
['prototype/general/product-information', 'general-product', '产品信息'],
|
||||
['prototype/general/fee-items', 'general-fee', '费用项目'],
|
||||
['prototype/general/fba-address', 'general-fba', 'FBA编码地址'],
|
||||
['prototype/general/pallet-management', 'general-pallet', '托盘管理'],
|
||||
['prototype/general/location-management', 'general-location', '库位/货架管理'],
|
||||
['prototype/general/voyage-price-list', 'general-voyage', '船期价格表'],
|
||||
]),
|
||||
...createRoutes(moduleViews.sales, [
|
||||
['prototype/sales/visit-records', 'sales-visits', '拜访记录'],
|
||||
['prototype/sales/commission', 'sales-commission', '销售提成'],
|
||||
]),
|
||||
...createRoutes(moduleViews.messages, [
|
||||
['prototype/messages/management', 'message-management', '消息管理'],
|
||||
['prototype/messages/templates', 'message-template', '模板管理'],
|
||||
['prototype/messages/my-messages', 'my-messages', '我的消息'],
|
||||
]),
|
||||
...createRoutes(moduleViews.systemManagement, [
|
||||
['prototype/system-management/tenant-management', 'tenant-management', '租户管理'],
|
||||
['prototype/system-management/brand-settings', 'brand-settings', '品牌设置'],
|
||||
]),
|
||||
]
|
||||
+17
-1
@@ -7,7 +7,23 @@
|
||||
.brand-logo-mark { display: none; width: 40px; height: 40px; }
|
||||
.is-collapsed .brand-logo-full { display: none; }
|
||||
.is-collapsed .brand-logo-mark { display: block; }
|
||||
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 10px; }
|
||||
.side-nav {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 14px 10px;
|
||||
scrollbar-color: rgba(203, 214, 230, .5) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.side-nav::-webkit-scrollbar { width: 10px; }
|
||||
.side-nav::-webkit-scrollbar-track,
|
||||
.side-nav::-webkit-scrollbar-corner { background: transparent; }
|
||||
.side-nav::-webkit-scrollbar-thumb { border: 3px solid transparent; border-radius: 999px; background: rgba(203, 214, 230, .5); background-clip: padding-box; }
|
||||
.side-nav::-webkit-scrollbar-thumb:hover { background-color: rgba(225, 233, 244, .78); }
|
||||
.side-nav::-webkit-scrollbar-button,
|
||||
.side-nav::-webkit-scrollbar-button:single-button,
|
||||
.side-nav::-webkit-scrollbar-button:vertical:decrement,
|
||||
.side-nav::-webkit-scrollbar-button:vertical:increment { display: none; width: 0; height: 0; background: transparent; }
|
||||
.side-nav-menu {
|
||||
width: 100%;
|
||||
border-right: 0 !important;
|
||||
|
||||
@@ -255,13 +255,16 @@ onMounted(() => {
|
||||
<el-button size="small" @click="loadExpressCompanies">重新查询</el-button>
|
||||
</el-alert>
|
||||
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="快递公司筛选" :fields="queryFields" @search="applyFilters">
|
||||
<template #actions>
|
||||
<el-button v-if="access.can('platform:express:create')" type="primary" @click="openCreate"><Plus :size="16" />新增快递公司</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="快递公司筛选" :fields="queryFields" @search="applyFilters" />
|
||||
|
||||
<section class="surface-panel table-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>快递公司列表</h2>
|
||||
<p>共 {{ total }} 家快递公司</p>
|
||||
</div>
|
||||
<el-button v-if="access.can('platform:express:create')" type="primary" @click="openCreate"><Plus :size="16" />新增快递公司</el-button>
|
||||
</div>
|
||||
<DataTable
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@@ -343,4 +346,8 @@ onMounted(() => {
|
||||
.tracking-template { display: block; max-width: 100%; color: #475467; font-family: "Cascadia Code", Consolas, monospace; font-size: 11px; overflow-wrap: anywhere; }
|
||||
.remark-cell { display: -webkit-box; overflow: hidden; line-height: 1.5; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.form-help code { color: #475467; font-family: "Cascadia Code", Consolas, monospace; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.panel-heading .el-button { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -242,8 +242,9 @@ onMounted(loadUsers)
|
||||
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert">
|
||||
<template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadUsers">重新加载</el-button></template>
|
||||
</el-alert>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="内部用户筛选" :fields="queryFields" @search="applyFilters"><template #actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></template></QueryToolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="内部用户筛选" :fields="queryFields" @search="applyFilters" />
|
||||
<section class="surface-panel table-panel">
|
||||
<header class="panel-heading"><div><h2>内部用户 <span class="secondary-text">共 {{ filteredRows.length }} 条</span></h2><p>数据和维护操作均由平台 IAM 提供,角色关系按成员维度保存。</p></div><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></header>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRows" :columns="userColumns" :loading="loading" :total="filteredRows.length" row-key="membershipId" empty-text="没有符合条件的内部用户">
|
||||
<template #actions="{ row }"><div v-if="canEdit || canChangeStatus || canResetPassword || canGrantRoles" class="table-actions"><el-button v-if="canEdit" link type="primary" :disabled="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="openEdit(row as PlatformInternalUser)"><Pencil :size="15" />编辑</el-button><el-button v-if="canGrantRoles" link type="primary" :disabled="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="openRoleAssignment(row as PlatformInternalUser)"><ShieldCheck :size="15" />分配角色</el-button><el-button v-if="canChangeStatus" link :type="(row as PlatformInternalUser).status === 'ACTIVE' ? 'danger' : 'success'" :loading="rowActionId === String((row as PlatformInternalUser).membershipId)" :disabled="isCurrentUser(row as PlatformInternalUser)" :title="isCurrentUser(row as PlatformInternalUser) ? '不能停用当前登录用户' : undefined" @click="toggleStatus(row as PlatformInternalUser)">{{ (row as PlatformInternalUser).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="canResetPassword" link type="warning" :loading="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="resetPassword(row as PlatformInternalUser)"><KeyRound :size="15" />重置密码</el-button></div><span v-else class="secondary-text">仅查看</span></template>
|
||||
<template #roles="{ row }"><div class="tag-list"><el-tag v-for="roleId in (row as PlatformInternalUser).roleIds" :key="roleId" effect="plain" size="small">{{ roleLabel(roleId) }}</el-tag><span v-if="(row as PlatformInternalUser).roleIds.length === 0" class="secondary-text">未分配</span></div></template>
|
||||
|
||||
@@ -289,11 +289,7 @@ onMounted(loadPartners)
|
||||
|
||||
<template>
|
||||
<div class="business-page partners-page">
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="往来单位筛选" :fields="queryFields" @search="searchPartners">
|
||||
<template #actions>
|
||||
<el-button v-if="access.can('platform:counterparty:create')" class="create-partner-button" type="primary" :disabled="loading" @click="openCreate"><Plus :size="16" />新增往来单位</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="往来单位筛选" :fields="queryFields" @search="searchPartners" />
|
||||
|
||||
<section v-if="loadError" class="surface-panel partners-error" role="alert">
|
||||
<div>
|
||||
@@ -305,6 +301,13 @@ onMounted(loadPartners)
|
||||
</section>
|
||||
|
||||
<section v-else class="surface-panel table-panel" :aria-busy="loading">
|
||||
<div class="partners-table-header">
|
||||
<div class="partners-table-heading">
|
||||
<h2>往来单位列表</h2>
|
||||
<span>共 {{ total }} 个往来单位</span>
|
||||
</div>
|
||||
<el-button v-if="access.can('platform:counterparty:create')" type="primary" :disabled="loading" @click="openCreate"><Plus :size="16" />新增往来单位</el-button>
|
||||
</div>
|
||||
<DataTable
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@@ -329,7 +332,7 @@ onMounted(loadPartners)
|
||||
<template #contact="{ row }"><strong>{{ (row as PlatformPartnerResource).contactName }}</strong><span class="partner-subline">{{ (row as PlatformPartnerResource).contactPhone }}</span><span v-if="(row as PlatformPartnerResource).contactEmail" class="partner-subline">{{ (row as PlatformPartnerResource).contactEmail }}</span></template>
|
||||
<template #address="{ row }"><div class="partner-address"><MapPin :size="15" /><span>{{ [(row as PlatformPartnerResource).province, (row as PlatformPartnerResource).city, (row as PlatformPartnerResource).district, (row as PlatformPartnerResource).addressLine].filter(Boolean).join(' ') }}</span></div></template>
|
||||
<template #status="{ row }"><el-tag :type="(row as PlatformPartnerResource).status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ (row as PlatformPartnerResource).status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></template>
|
||||
<template #pagination-left>共 {{ total }} 个往来单位</template>
|
||||
<template #pagination-left />
|
||||
</DataTable>
|
||||
</section>
|
||||
|
||||
@@ -363,6 +366,10 @@ onMounted(loadPartners)
|
||||
.partners-error { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; padding: 18px 20px; border-color: #fecdca; background: #fffafa; }
|
||||
.partners-error strong { color: #7a271a; font-size: 13px; }
|
||||
.partners-error p { margin: 4px 0 0; color: #b54708; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.partners-table-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid var(--cc-border); }
|
||||
.partners-table-heading { display: flex; min-width: 0; align-items: baseline; gap: 10px; }
|
||||
.partners-table-heading h2 { margin: 0; color: var(--cc-text); font-size: 16px; line-height: 1.4; }
|
||||
.partners-table-heading span { color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
|
||||
.partner-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.partner-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); }
|
||||
.partner-cell > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
@@ -372,8 +379,9 @@ onMounted(loadPartners)
|
||||
.partner-address svg { flex: 0 0 15px; margin-top: 3px; color: var(--cc-muted); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.partners-page :deep(.query-toolbar__actions .create-partner-button) { grid-column: 1 / -1; width: 100%; }
|
||||
.partners-error { align-items: stretch; flex-direction: column; }
|
||||
.partners-error .el-button { min-height: 44px; }
|
||||
.partners-table-header { align-items: stretch; flex-direction: column; }
|
||||
.partners-table-header .el-button { width: 100%; min-height: 40px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('PermissionsView IAM mode', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(listPlatformPermissions).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('权限资源维护已接入 IAM')
|
||||
expect(wrapper.text()).toContain('系统内置资源不可修改')
|
||||
expect(wrapper.text()).toContain('权限资源树')
|
||||
expect(wrapper.text()).toContain('共 1 项')
|
||||
})
|
||||
|
||||
it('shows a recoverable state when the permission list fails', async () => {
|
||||
|
||||
@@ -232,10 +232,10 @@ onMounted(loadPermissions)
|
||||
<template>
|
||||
<div class="business-page">
|
||||
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadPermissions">重新加载</el-button></template></el-alert>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="权限筛选" :fields="queryFields" @search="applyFilters"><template #actions><el-button v-if="canCreate" type="primary" @click="openCreate(null)"><Plus :size="16" />新增权限</el-button></template></QueryToolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="权限筛选" :fields="queryFields" @search="applyFilters" />
|
||||
|
||||
<section class="surface-panel table-panel">
|
||||
<div class="panel-heading"><div class="permission-tree-tools"><div class="legend-list"><span><i class="is-menu" />目录 / 菜单</span><span><i class="is-action" />操作</span><span><i class="is-api" />接口预留</span></div><div class="tree-view-actions"><el-button link type="primary" @click="expandAllRows">展开全部</el-button><el-button link @click="collapseActionRows">收起操作权限</el-button></div></div></div>
|
||||
<header class="panel-heading"><div><h2>权限资源树 <span class="secondary-text">共 {{ permissions.length }} 项</span></h2><p>默认展示目录与菜单,展开菜单后查看按钮权限;接口权限将在服务端契约确定后补充。</p></div><div class="permission-tree-heading-actions"><div class="permission-tree-tools"><div class="legend-list"><span><i class="is-menu" />目录 / 菜单</span><span><i class="is-action" />操作</span><span><i class="is-api" />接口预留</span></div><div class="tree-view-actions"><el-button link type="primary" @click="expandAllRows">展开全部</el-button><el-button link @click="collapseActionRows">收起操作权限</el-button></div></div><el-button v-if="canCreate" type="primary" @click="openCreate(null)"><Plus :size="16" />新增权限</el-button></div></header>
|
||||
<DataTable :key="tableRenderKey" v-model:page="page" v-model:page-size="pageSize" :data="pagedTreeRows" :columns="permissionColumns" :loading="loading" :total="treeRows.length" row-key="id" :expand-row-keys="expandedRowKeys" :tree-props="{ children: 'children' }" empty-text="没有符合条件的权限资源">
|
||||
<template #actions="{ row }"><div v-if="canCreate || canEdit || canChangeStatus || canDelete" class="table-actions"><el-button v-if="canCreate && (row as Permission).type === '菜单'" link type="primary" @click="openCreate((row as Permission).id)"><Plus :size="15" />添加下级</el-button><el-button v-if="canEdit" link type="primary" :disabled="(row as Permission).builtIn || rowActionId === (row as Permission).id" @click="openEdit(row as Permission)"><Pencil :size="15" />编辑</el-button><el-button v-if="canChangeStatus" link :type="(row as Permission).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Permission).id" :disabled="(row as Permission).builtIn" @click="toggleStatus(row as Permission)">{{ (row as Permission).status === '启用' ? '停用' : '启用' }}</el-button><el-button v-if="canDelete" link type="danger" :disabled="(row as Permission).builtIn || rowActionId === (row as Permission).id" @click="removePermission(row as Permission)"><Trash2 :size="15" />删除</el-button><span v-if="(row as Permission).builtIn" class="secondary-text">系统内置</span></div><span v-else class="secondary-text">仅查看</span></template>
|
||||
<template #name="{ row }"><div class="permission-name"><span :class="`is-${typeClass((row as Permission).type)}`"><FolderTree v-if="(row as Permission).type === '目录' || (row as Permission).type === '菜单'" :size="16" /><ShieldCheck v-else-if="(row as Permission).type === '按钮'" :size="16" /><Braces v-else :size="16" /></span><div><strong>{{ (row as Permission).name }}</strong><small v-if="(row as PermissionTreeNode).children?.length">{{ permissionSummary(row as PermissionTreeNode) }}</small></div></div></template>
|
||||
@@ -267,13 +267,16 @@ onMounted(loadPermissions)
|
||||
|
||||
<style scoped>
|
||||
.permission-tree-tools { display: flex; align-items: center; gap: 16px; }
|
||||
.permission-tree-heading-actions { display: flex; align-items: center; gap: 12px; }
|
||||
.tree-view-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.tree-view-actions .el-button + .el-button { margin-left: 0; }
|
||||
.permission-name > div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||||
.permission-name small { color: var(--cc-muted); font-size: 11px; line-height: 1.35; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.permission-tree-heading-actions { width: 100%; align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.permission-tree-tools { width: 100%; align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.permission-tree-heading-actions > .el-button { min-height: 40px; }
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-page prototype-module-page">
|
||||
<section class="surface-panel prototype-module-panel">
|
||||
<div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
<div class="prototype-module-empty" role="status">
|
||||
<strong>暂无业务数据</strong>
|
||||
<span>页面基础已就绪,后续按对应业务契约接入。</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.prototype-module-panel { display: grid; min-height: 260px; align-content: start; gap: 28px; padding: 24px; }
|
||||
.prototype-module-panel h2 { margin: 0; color: var(--cc-text); font-size: 18px; line-height: 1.4; }
|
||||
.prototype-module-panel p { margin: 8px 0 0; color: var(--cc-muted); font-size: 13px; line-height: 1.65; }
|
||||
.prototype-module-empty { display: grid; min-height: 120px; place-items: center; align-content: center; gap: 6px; border: 1px dashed var(--cc-border); color: var(--cc-muted); text-align: center; }
|
||||
.prototype-module-empty strong { color: var(--cc-text); font-size: 14px; }
|
||||
.prototype-module-empty span { font-size: 13px; }
|
||||
</style>
|
||||
@@ -233,9 +233,10 @@ onMounted(loadRoles)
|
||||
<template>
|
||||
<div class="business-page">
|
||||
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadRoles">重新加载</el-button></template></el-alert>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="角色筛选" :fields="queryFields" @search="applyFilters"><template #actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增角色</el-button></template></QueryToolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="角色筛选" :fields="queryFields" @search="applyFilters" />
|
||||
|
||||
<section class="surface-panel table-panel">
|
||||
<header class="panel-heading"><div><h2>角色与授权 <span class="secondary-text">共 {{ filteredRows.length }} 条</span></h2><p>角色负责聚合菜单、操作和数据范围,内部用户可分配多个角色。</p></div><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增角色</el-button></header>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRows" :columns="roleColumns" :loading="loading" :total="filteredRows.length" row-key="id" empty-text="没有符合条件的角色">
|
||||
<template #actions="{ row }"><div v-if="canEdit || canGrant || access.can('platform:role:status') || access.can('platform:role:delete')" class="table-actions"><el-button v-if="canEdit || canGrant" link type="primary" :disabled="rowActionId === (row as Role).id || (row as Role).builtIn" @click="openEdit(row as Role)"><Pencil :size="15" />配置</el-button><el-button v-if="access.can('platform:role:status')" link :type="(row as Role).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Role).id" :disabled="(row as Role).builtIn" @click="toggleStatus(row as Role)">{{ (row as Role).status === '启用' ? '停用' : '启用' }}</el-button><el-button v-if="access.can('platform:role:delete')" link type="danger" :disabled="(row as Role).builtIn || (row as Role).userCount > 0 || rowActionId === (row as Role).id" @click="removeRole(row as Role)"><Trash2 :size="15" />删除</el-button></div><span v-else class="secondary-text">仅查看</span></template>
|
||||
<template #role="{ row }"><div class="role-cell"><span><ShieldCheck :size="18" /></span><div><strong>{{ (row as Role).name }}</strong><small>{{ (row as Role).code }}</small></div></div></template>
|
||||
|
||||
@@ -349,17 +349,21 @@ onMounted(() => {
|
||||
<p>{{ detailWarningCount }} 个仓库缺少地址或服务能力详情;编辑时会再次尝试加载,不会用模拟数据补齐。</p>
|
||||
</el-alert>
|
||||
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="仓库筛选" :fields="queryFields" @search="queryWarehouses">
|
||||
<template #actions>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="仓库筛选" :fields="queryFields" @search="queryWarehouses" />
|
||||
|
||||
<section class="surface-panel table-panel" aria-live="polite">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>平台收货仓</h2>
|
||||
<p>租户 {{ fixedTenantId }} · 共 {{ filteredRows.length }} 个收货仓</p>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="access.can('platform:warehouse:create')"
|
||||
type="primary"
|
||||
:disabled="!warehouseReady"
|
||||
@click="openCreate"
|
||||
><Plus :size="16" />新增仓库</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
|
||||
<section class="surface-panel table-panel" aria-live="polite">
|
||||
</div>
|
||||
<DataTable
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@@ -420,4 +424,8 @@ onMounted(() => {
|
||||
:deep(.has-field-error .el-input__wrapper) { box-shadow: 0 0 0 1px var(--el-color-danger) inset; }
|
||||
:deep(.has-field-error .el-textarea__inner) { box-shadow: 0 0 0 1px var(--el-color-danger) inset; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.panel-heading .el-button { width: 100%; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
Reference in New Issue
Block a user