From d8bbfe05f399400354b7cce70baee6d063231f6d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 12 Aug 2026 16:31:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +- src/core/navigation/adminNavigation.ts | 46 ++-- .../navigation/prototypeMenuIcons.test.ts | 32 +++ src/core/navigation/prototypeMenuIcons.ts | 90 +++++++ .../pages/ContainersView.test.ts | 22 ++ .../pages/ContainersView.vue | 25 +- .../customers/pages/CustomersView.test.ts | 17 +- src/modules/customers/pages/CustomersView.vue | 60 ++++- .../InboundWorkflowWorkspace.test.ts | 36 ++- .../components/InboundWorkflowWorkspace.vue | 134 ++++++++-- .../inbounds/core/InboundHistoryPanel.vue | 21 +- src/modules/inbounds/fee/api.test.ts | 1 - src/modules/inbounds/fee/api.ts | 1 - src/modules/inbounds/fee/index.ts | 1 - src/modules/inbounds/fee/types.ts | 1 - .../InboundMeasurementPanel.test.ts | 5 + .../measurement/InboundMeasurementPanel.vue | 5 +- .../pages/InboundOrderDetailView.test.ts | 40 ++- .../inbounds/pages/InboundOrderDetailView.vue | 23 +- .../inbounds/pages/InboundOrdersView.test.ts | 31 +-- .../inbounds/pages/InboundOrdersView.vue | 67 +---- .../review/MeasurementReviewPanel.test.ts | 2 + .../review/MeasurementReviewPanel.vue | 2 + .../loading-lists/api/containerFees.test.ts | 28 +++ .../loading-lists/api/containerFees.ts | 125 ++++++++++ .../components/ContainerFeesPanel.test.ts | 34 +++ .../components/ContainerFeesPanel.vue | 143 +++++++++++ .../components/LoadingListDetailDrawer.vue | 23 +- .../components/LoadingListsView.test.ts | 23 +- .../components/LoadingListsView.vue | 24 +- .../pages/PreloadPlanDetailView.test.ts | 28 +++ .../pages/PreloadPlanDetailView.vue | 126 ++++++++++ src/modules/loading-lists/routes.test.ts | 3 +- src/modules/loading-lists/routes.ts | 10 + .../api/inboundFeeDetails.test.ts | 30 +++ .../unified-orders/api/inboundFeeDetails.ts | 105 ++++++++ .../InboundFeeDetailManager.test.ts | 108 +++++++++ .../components/InboundFeeDetailManager.vue | 229 ++++++++++++++++++ .../pages/UnifiedOrdersView.test.ts | 3 +- .../pages/UnifiedOrdersView.vue | 78 +----- 40 files changed, 1541 insertions(+), 248 deletions(-) create mode 100644 src/core/navigation/prototypeMenuIcons.test.ts create mode 100644 src/core/navigation/prototypeMenuIcons.ts create mode 100644 src/modules/loading-lists/api/containerFees.test.ts create mode 100644 src/modules/loading-lists/api/containerFees.ts create mode 100644 src/modules/loading-lists/components/ContainerFeesPanel.test.ts create mode 100644 src/modules/loading-lists/components/ContainerFeesPanel.vue create mode 100644 src/modules/loading-lists/pages/PreloadPlanDetailView.test.ts create mode 100644 src/modules/loading-lists/pages/PreloadPlanDetailView.vue create mode 100644 src/modules/unified-orders/api/inboundFeeDetails.test.ts create mode 100644 src/modules/unified-orders/api/inboundFeeDetails.ts create mode 100644 src/modules/unified-orders/components/InboundFeeDetailManager.test.ts create mode 100644 src/modules/unified-orders/components/InboundFeeDetailManager.vue diff --git a/AGENTS.md b/AGENTS.md index 4d9ac086..ac3f54ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,8 +79,11 @@ ## UI 与交互 -- 已向客户讲解或确认过的产品原型是页面排版、配色、间距、尺寸、图标、按钮、表格、Tab 和组件形态的直接视觉事实来源;实现时必须按原型还原,不得脱离原型自行重新设计、添加装饰或替换风格。只有用户当前明确要求、阶段范围或可访问性/真实数据约束与原型冲突时才允许调整,并须先说明具体差异及原因。 -- 原型已经给出明确图标、字符图标或按钮形态时直接使用原型方案,不得以 Lucide、Element Plus 默认样式或个人偏好替换;仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。 +- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。 +- 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。 +- 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。 +- 上述原型还原规则优先于本文件中“延续现有视觉体系”、默认组件样式和默认图标库等一般性规则。只有用户当前明确要求、阶段范围、真实字段长度、响应式、可访问性或接口事实与原型冲突时才允许做最小调整,并须在修改前说明具体差异、原因和影响范围;不得以技术实现方便为由偏离原型。 +- 详情、新增、编辑、审核等操作采用独立页面、居中弹框、侧滑面板或其他承载方式,必须逐页面按对应原型确认,不得全局写死为某一种容器。原型为独立页面时使用正式路由和页面布局;原型为居中弹框时使用模态弹框;只有原型明确为侧滑面板时才使用抽屉。不得因为已有公共组件或改动方便,把原型中的独立页面改成弹框、把弹框改成抽屉,或反向替换。 - 延续现有后台视觉体系:克制的企业级界面、清晰的信息层级和高密度但可扫描的数据展示。 - 路由级页面标题和说明统一由 `AppLayout` 顶部 `page-heading` 根据路由 `meta.title`、`meta.description` 展示。标准列表页正文不得重复渲染 `PageToolbar`、页面标题或说明,只保留查询区与表格区。`QueryToolbar` 只承载筛选项及其内置查询、重置按钮,不把新增、导入、导出或批量操作放入其 `actions` 插槽;重新执行查询即刷新当前结果,标准列表页任何位置都不得再提供独立刷新按钮。新增、导入、导出和批量操作统一放入 `DataTable` 的 `toolbar-actions` 插槽并靠表格工具栏右侧展示。仅没有查询组件的实时工作台或详情页在业务确实需要主动重载时,才可保留刷新;仅详情页或工作台在路由明确设置 `showPageHeading: false` 时,才可在正文使用独立详情头。 - 标准列表页的查询面板与表格面板使用 `ListPageLayout` 保持单一 `16px` 间距,不再叠加页面级 margin、空白操作区或额外容器间距。`DataTable` 必须位于连续的 `surface-panel table-panel` 中,使右侧业务按钮、列设置、表头和分页属于同一表格面板;不得使用 `toolbar-start` 重复显示列表标题、数量或说明,数量统一放在 `pagination-left`。当工具栏只有右侧操作和列设置时使用紧凑高度,不保留空白的左侧标题区域。 diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index cd55993d..5959474f 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -1,7 +1,6 @@ import type { Component } from 'vue' import { Building2, - BarChart3, ContactRound, ClipboardList, CircleDollarSign, @@ -15,6 +14,21 @@ import { Users, Warehouse as WarehouseIcon, } from '@lucide/vue' +import { + PrototypeContainerIcon, + PrototypeCustomerIcon, + PrototypeDashboardIcon, + PrototypeDeliveryIcon, + PrototypeDomesticWarehouseIcon, + PrototypeFinanceIcon, + PrototypeGeneralIcon, + PrototypeOverseasBusinessIcon, + PrototypeOverseasWarehouseIcon, + PrototypeSalesIcon, + PrototypeServiceIcon, + PrototypeSupplierIcon, + PrototypeSystemIcon, +} from './prototypeMenuIcons' export interface AdminNavigationNode { key: string @@ -36,12 +50,11 @@ export interface AdminNavigationLeaf extends AdminNavigationNode { } export const adminNavigation: AdminNavigationNode[] = [ - { key: 'dashboard', title: '首页数据看板', icon: BarChart3, path: '/dashboard' }, + { key: 'dashboard', title: '首页数据看板', icon: PrototypeDashboardIcon, path: '/dashboard' }, { key: 'customers', title: '客户管理', - icon: Building2, - iconColor: '#52c41a', + icon: PrototypeCustomerIcon, children: [ { key: 'customers-list', title: '客户列表', icon: Building2, path: '/customers', permission: 'platform:customer:list', permissionAnyOf: ['platform:customer:review'] }, { key: 'warehouse-tier-fee', title: '海外仓阶梯费用', icon: Building2, path: '/customers/overseas-storage-fees', permission: 'platform:overseas-storage-fee:list' }, @@ -50,8 +63,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'domestic-warehouse', title: '国内仓', - icon: WarehouseIcon, - iconColor: '#52c41a', + icon: PrototypeDomesticWarehouseIcon, children: [ { key: 'order-list', title: '订单列表', icon: ClipboardList, path: '/orders', permission: 'platform:order:list', permissionAnyOf: ['platform:firstleg:inbound:list'] }, { key: 'inbound-dedup', title: '订单测量', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-dedup', hidden: true }, @@ -63,7 +75,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'overseas-delivery', title: '海外派送', - icon: Truck, + icon: PrototypeDeliveryIcon, hidden: true, children: [ { key: 'outbound-dispatch', title: '派送单列表', icon: Truck, path: '/prototype/overseas-delivery/dispatch-list' }, @@ -75,7 +87,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'service-center', title: '服务中心', - icon: ClipboardList, + icon: PrototypeServiceIcon, children: [ { key: 'booking-list', title: '订舱列表', icon: ClipboardList, path: '/ocean-bookings', permission: 'platform:firstleg:ocean-booking:list' }, { key: 'port-schedules', title: '船期信息', icon: ClipboardList, path: '/port-schedules', permission: 'platform:firstleg:port-schedule:list' }, @@ -87,7 +99,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'supplier-center', title: '供应商中心', - icon: ContactRound, + icon: PrototypeSupplierIcon, children: [ { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/prototype/supplier-center/supplier-list', hidden: true }, { key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list' }, @@ -97,7 +109,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'container-management', title: '货柜管理', - icon: WarehouseIcon, + icon: PrototypeContainerIcon, children: [ { key: 'container-list', title: '货柜列表', icon: WarehouseIcon, path: '/container-loading', permission: 'platform:firstleg:container:list' }, { key: 'container-smart-loading', title: '智能装柜', icon: ClipboardList, path: '/container-loading/smart-loading', permission: 'platform:firstleg:smart-loading:list' }, @@ -108,7 +120,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'overseas-business', title: '海外业务', - icon: Building2, + icon: PrototypeOverseasBusinessIcon, hidden: true, children: [ { key: 'overseas-purchase', title: '采购订单', icon: ClipboardList, path: '/prototype/overseas-business/purchase-orders' }, @@ -119,7 +131,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'overseas-inventory', title: '海外仓库存', - icon: WarehouseIcon, + icon: PrototypeOverseasWarehouseIcon, hidden: true, children: [ { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/prototype/fba-returns/inbound-orders' }, @@ -135,7 +147,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'finance', title: '财务管理', - icon: CircleDollarSign, + icon: PrototypeFinanceIcon, children: [ { key: 'finance-customer-accounts', title: '客户账户', icon: CircleDollarSign, path: '/finance/customer-accounts', permission: 'platform:finance:account:view' }, { key: 'finance-customer-arap', title: '客户应收应付', icon: Database, path: '/finance/customer-arap', permission: 'platform:finance:bill:view' }, @@ -147,7 +159,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'sales', title: '销售管理', - icon: Users, + icon: PrototypeSalesIcon, hidden: true, children: [ { key: 'sales-visits', title: '拜访记录', icon: Users, path: '/prototype/sales/visit-records' }, @@ -157,8 +169,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'general', title: '通用', - icon: Database, - iconColor: '#722ed1', + icon: PrototypeGeneralIcon, children: [ // { key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'platform:order:list', hidden: true }, { key: 'general-product', title: '产品信息', icon: Database, path: '/prototype/general/product-information', hidden: true }, @@ -172,8 +183,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'system', title: '系统管理', - icon: Settings, - iconColor: '#8c8c8c', + icon: PrototypeSystemIcon, 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' }, diff --git a/src/core/navigation/prototypeMenuIcons.test.ts b/src/core/navigation/prototypeMenuIcons.test.ts new file mode 100644 index 00000000..c8ce5a2e --- /dev/null +++ b/src/core/navigation/prototypeMenuIcons.test.ts @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { + PrototypeCustomerIcon, + PrototypeDashboardIcon, + PrototypeFinanceIcon, +} from './prototypeMenuIcons' + +describe('prototype menu icons', () => { + it('renders the prototype shapes at the requested size', () => { + const wrapper = mount(PrototypeDashboardIcon, { props: { size: 22 } }) + + expect(wrapper.attributes()).toMatchObject({ + width: '22', + height: '22', + viewBox: '0 0 24 24', + 'aria-hidden': 'true', + }) + expect(wrapper.findAll('rect')).toHaveLength(4) + expect(wrapper.findAll('rect').map((node) => node.attributes('fill'))).toEqual([ + '#1677ff', + '#1677ff', + '#1677ff', + '#1677ff', + ]) + }) + + it('preserves the original menu colors', () => { + expect(mount(PrototypeCustomerIcon).find('path').attributes('fill')).toBe('#52c41a') + expect(mount(PrototypeFinanceIcon).find('circle').attributes('stroke')).toBe('#f5a623') + }) +}) diff --git a/src/core/navigation/prototypeMenuIcons.ts b/src/core/navigation/prototypeMenuIcons.ts new file mode 100644 index 00000000..221a65cf --- /dev/null +++ b/src/core/navigation/prototypeMenuIcons.ts @@ -0,0 +1,90 @@ +import { defineComponent, h, type Component, type SVGAttributes } from 'vue' + +type SvgChild = ReturnType + +function createPrototypeMenuIcon(name: string, children: SvgChild[]): Component { + return defineComponent({ + name, + inheritAttrs: false, + props: { + size: { type: [Number, String], default: 20 }, + }, + setup(props, { attrs }) { + return () => h('svg', { + ...attrs, + width: props.size, + height: props.size, + viewBox: '0 0 24 24', + fill: 'none', + 'aria-hidden': 'true', + } satisfies SVGAttributes, children) + }, + }) +} + +export const PrototypeDashboardIcon = createPrototypeMenuIcon('PrototypeDashboardIcon', [ + h('rect', { x: 3, y: 3, width: 7, height: 8, rx: 1.5, fill: '#1677ff' }), + h('rect', { x: 14, y: 3, width: 7, height: 5, rx: 1.5, fill: '#1677ff', opacity: 0.6 }), + h('rect', { x: 3, y: 14, width: 7, height: 7, rx: 1.5, fill: '#1677ff', opacity: 0.3 }), + h('rect', { x: 14, y: 11, width: 7, height: 10, rx: 1.5, fill: '#1677ff', opacity: 0.8 }), +]) + +export const PrototypeCustomerIcon = createPrototypeMenuIcon('PrototypeCustomerIcon', [ + h('path', { d: 'M3 21h7v-7H3v7zm0-9h7V3H3v9zm11 9h7v-5h-7v5zm0-9h7V3h-7v9z', fill: '#52c41a' }), +]) + +export const PrototypeDomesticWarehouseIcon = createPrototypeMenuIcon('PrototypeDomesticWarehouseIcon', [ + h('path', { d: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', stroke: '#52c41a', 'stroke-width': 2, 'stroke-linecap': 'round' }), + h('path', { d: 'M9 14l2 2 4-4', stroke: '#52c41a', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), +]) + +export const PrototypeOverseasWarehouseIcon = createPrototypeMenuIcon('PrototypeOverseasWarehouseIcon', [ + h('path', { d: 'M4 12a8 8 0 112.34 5.6', stroke: '#eb2f96', 'stroke-width': 2, 'stroke-linecap': 'round' }), + h('path', { d: 'M4 12V7M4 12h5', stroke: '#eb2f96', 'stroke-width': 2, 'stroke-linecap': 'round' }), + h('circle', { cx: 12, cy: 12, r: 2.4, fill: '#eb2f96' }), +]) + +export const PrototypeDeliveryIcon = createPrototypeMenuIcon('PrototypeDeliveryIcon', [ + h('path', { d: 'M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z', fill: '#1890ff' }), +]) + +export const PrototypeServiceIcon = createPrototypeMenuIcon('PrototypeServiceIcon', [ + h('path', { d: 'M4 6h16v12H4z', stroke: '#722ed1', 'stroke-width': 2 }), + h('path', { d: 'M4 10h16M8 14h3', stroke: '#722ed1', 'stroke-width': 2, 'stroke-linecap': 'round' }), +]) + +export const PrototypeSupplierIcon = createPrototypeMenuIcon('PrototypeSupplierIcon', [ + h('path', { d: 'M3 21V9l9-6 9 6v12', stroke: '#eb2f96', 'stroke-width': 2, 'stroke-linejoin': 'round' }), + h('path', { d: 'M9 21v-6h6v6', stroke: '#eb2f96', 'stroke-width': 2, 'stroke-linejoin': 'round' }), +]) + +export const PrototypeContainerIcon = createPrototypeMenuIcon('PrototypeContainerIcon', [ + h('rect', { x: 2, y: 3, width: 20, height: 18, rx: 1, stroke: '#52c41a', 'stroke-width': 2 }), + h('path', { d: 'M2 8h20M8 3v5M16 3v5', stroke: '#52c41a', 'stroke-width': 2, 'stroke-linecap': 'round' }), +]) + +export const PrototypeOverseasBusinessIcon = createPrototypeMenuIcon('PrototypeOverseasBusinessIcon', [ + h('circle', { cx: 12, cy: 12, r: 9, stroke: '#8c8c8c', 'stroke-width': 1.5 }), + h('path', { d: 'M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18', stroke: '#8c8c8c', 'stroke-width': 1.5, 'stroke-linecap': 'round' }), +]) + +export const PrototypeFinanceIcon = createPrototypeMenuIcon('PrototypeFinanceIcon', [ + h('circle', { cx: 12, cy: 12, r: 10, stroke: '#f5a623', 'stroke-width': 2 }), + h('path', { d: 'M12 6v6l4 2', stroke: '#f5a623', 'stroke-width': 2, 'stroke-linecap': 'round' }), + h('circle', { cx: 12, cy: 12, r: 2, fill: '#f5a623' }), +]) + +export const PrototypeSalesIcon = createPrototypeMenuIcon('PrototypeSalesIcon', [ + h('circle', { cx: 12, cy: 12, r: 10, stroke: '#1677ff', 'stroke-width': 2 }), + h('path', { d: 'M12 6v12M6 12h12', stroke: '#1677ff', 'stroke-width': 2, 'stroke-linecap': 'round' }), +]) + +export const PrototypeGeneralIcon = createPrototypeMenuIcon('PrototypeGeneralIcon', [ + h('rect', { x: 3, y: 3, width: 18, height: 18, rx: 2, stroke: '#722ed1', 'stroke-width': 2 }), + h('path', { d: 'M8 12h8M12 8v8', stroke: '#722ed1', 'stroke-width': 2, 'stroke-linecap': 'round' }), +]) + +export const PrototypeSystemIcon = createPrototypeMenuIcon('PrototypeSystemIcon', [ + h('circle', { cx: 12, cy: 12, r: 3, fill: '#8c8c8c' }), + h('path', { d: 'M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83', stroke: '#8c8c8c', 'stroke-width': 1.5, 'stroke-linecap': 'round' }), +]) diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 4f7a9e72..19f3c0de 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -158,4 +158,26 @@ describe('ContainersView', () => { expect(source).toContain("if (nextPage !== previousPage || nextSize !== previousSize) void loadRows()") expect(source).not.toContain('@reset=') }) + + it('uses the frozen prototype modal contract for create and edit while retaining the detail drawer', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') + expect(source).toContain(' { + const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') + const labels = ['集装箱号', '柜号', '装柜仓库', '提单号', '船名', '航次', '箱型(货柜类型)', '船公司', '起运港', '目的港', '目的仓', '货柜容量(m³)'] + let previous = -1 + for (const label of labels) { + const current = source.indexOf(`label="${label}"`, previous + 1) + expect(current, label).toBeGreaterThan(previous) + previous = current + } + }) }) diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index adcce45f..c096a5f3 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -9,6 +9,7 @@ import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/ma import { listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' +import AppDialog from '@/shared/components/AppDialog.vue' import AppDrawer from '@/shared/components/AppDrawer.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' @@ -75,7 +76,7 @@ const canEdit = computed(() => access.can('platform:firstleg:container:edit')) const canSmartLoading = computed(() => access.can('platform:firstleg:smart-loading:list')) const canViewPreload = computed(() => access.can('platform:firstleg:preload:list')) const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list')) -const editorTitle = computed(() => form.id ? (detail.value?.planStatus === 'LOADING' ? '修改运输信息' : '编辑货柜') : '新增货柜') +const editorTitle = computed(() => form.id ? '编辑' : '新增') const transportOnly = computed(() => Boolean(form.id) && detail.value?.planStatus === 'LOADING') const shippingCompanies = computed(() => partners.value.filter((partner) => partner.status === 'ACTIVE' && partner.types.includes('CARRIER'))) const truckingCompanies = computed(() => partners.value.filter((partner) => partner.status === 'ACTIVE' && partner.types.some((type) => ['CARRIER', 'SUPPLIER'].includes(type)))) @@ -341,35 +342,41 @@ onMounted(() => { void Promise.all([loadWarehouses(), loadBusinessOptions(), loa {{ detail.cabinetNo }}{{ detail.containerNo || '—' }}{{ statusText(detail.planStatus) }}{{ detail.containerType }}{{ detail.warehouseName || '—' }}{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}{{ detail.shippingCompanyName || '—' }}{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}{{ detail.billOfLadingNo || '—' }}{{ detail.terminalTruckingCompany || '—' }}{{ formatTime(detail.estimatedSailingAt) }}{{ formatTime(detail.estimatedPortArrivalAt) }} - + 重新加载业务选项 - + - - - - + + + + - + diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index 15f0a693..713ce5c9 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -1,5 +1,7 @@ import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { @@ -26,6 +28,8 @@ import QueryToolbar from '@/shared/components/QueryToolbar.vue' import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' import CustomersView from './CustomersView.vue' +const customersSource = readFileSync(resolve(process.cwd(), 'src/modules/customers/pages/CustomersView.vue'), 'utf8') + const { routerPush, routerReplace, routeControl } = vi.hoisted(() => ({ routerPush: vi.fn(), routerReplace: vi.fn(), @@ -152,6 +156,15 @@ describe('CustomersView', () => { vi.mocked(showExportTaskCreatedDialog).mockResolvedValue(false) }) + it('matches the frozen prototype containers for customer operations', () => { + expect(customersSource).toContain("router.push({ name: 'customer-detail'") + expect(customersSource).toContain("'min(640px, calc(100vw - 28px))'") + expect(customersSource).toContain("'min(680px, calc(100vw - 28px))'") + expect(customersSource).toContain('width="min(520px, calc(100vw - 28px))" class="customer-password-dialog"') + expect(customersSource).toContain('width="min(900px, calc(100vw - 28px))" class="review-decision-dialog"') + expect(customersSource).not.toContain(' { mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() @@ -368,7 +381,7 @@ describe('CustomersView', () => { tableButtonByText('审核')?.click() await flushPromises() ;[...document.querySelectorAll('button')] - .find((button) => button.textContent?.trim() === '确认通过')?.click() + .find((button) => button.textContent?.trim() === '审核通过')?.click() await flushPromises() expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料与必填资质完整、清晰,审核通过') @@ -398,7 +411,7 @@ describe('CustomersView', () => { tableButtonByText('审核')?.click() await flushPromises() ;[...document.querySelectorAll('button')] - .find((button) => button.textContent?.trim() === '确认通过')?.click() + .find((button) => button.textContent?.trim() === '审核通过')?.click() await flushPromises() expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料与必填资质完整、清晰,审核通过') diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index 966afa12..8728b0b7 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -39,7 +39,6 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import AdminPage from '@/shared/components/AdminPage.vue' import AppDialog from '@/shared/components/AppDialog.vue' -import AppDrawer from '@/shared/components/AppDrawer.vue' import FormSection from '@/shared/components/FormSection.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' @@ -204,6 +203,9 @@ let loadSequence = 0 let reviewLoadSequence = 0 let editorLoadSequence = 0 const editorTitle = computed(() => editingId.value ? '编辑客户' : '新增客户') +const editorDialogWidth = computed(() => editingId.value + ? 'min(640px, calc(100vw - 28px))' + : 'min(680px, calc(100vw - 28px))') const canUploadQualifications = computed(() => !editorStatus.value || ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value)) const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value || '')) const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading)) @@ -963,7 +965,7 @@ onMounted(() => { void loadCustomers() }) - +
@@ -1006,7 +1008,7 @@ onMounted(() => { void loadCustomers() }) - +
@@ -1026,16 +1028,35 @@ onMounted(() => { void loadCustomers() }) - -
{{ selectedReview.tenantName || '当前客户' }}客户资料审核
+ +
+
+

企业基本信息

+
+
客户名称
{{ selectedReview.tenantName || '—' }}
+
客户编号
{{ selectedReview.tenantCode || '—' }}
+
联系人
{{ selectedReview.contactName || '—' }}
+
联系电话
{{ selectedReview.contactPhone || '—' }}
+
联系邮箱
{{ selectedReview.contactEmail || '—' }}
+
提交时间
{{ formatDate(selectedReview.submittedAt || selectedReview.createdAt) }}
+
+
+
+

资质文件

+
+
文件类型证书编号状态
+
{{ selectedReview.qualificationType || '企业资质' }}{{ selectedReview.certificateNo || '—' }}待审核
+
+
+
通过驳回 - + - +
- +
正在重置企业管理员登录密码{{ passwordTenant.tenantName }}
客户编号:{{ passwordTenant.tenantCode }}
@@ -1115,12 +1136,29 @@ onMounted(() => { void loadCustomers() }) .qualification-preview img { display:block; max-width:100%; height:auto; } .qualification-preview iframe { width:100%; min-height:min(68vh,720px); border:0; background:#fff; } .qualification-load-error { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:12px; padding:10px 12px; border:1px solid #fecdca; border-radius:6px; background:#fffafa; color:#b42318; font-size:12px; } -.customer-editor :deep(.el-drawer__body) { min-height:0; overflow-y:auto; overscroll-behavior:contain; } -.customer-editor :deep(.el-drawer__footer) { flex:0 0 auto; } +:global(.customer-editor-dialog.el-dialog) { display:flex; max-height:min(90vh,900px); flex-direction:column; } +:global(.customer-editor-dialog .el-dialog__body) { min-height:0; overflow-y:auto; overscroll-behavior:contain; } +:global(.customer-editor-dialog .el-dialog__footer) { flex:0 0 auto; } .customer-editor-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; } .customer-editor-footer > span { color:var(--cc-muted); font-size:12px; text-align:left; } .customer-editor-footer > div { display:flex; flex:0 0 auto; gap:8px; } .customer-editor-footer :deep(.el-button + .el-button) { margin-left:0; } .decision-summary { display:flex; min-width:0; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; padding:12px 14px; border-radius:8px; background:#f8fafc; } .decision-summary > * { min-width:0; overflow-wrap:anywhere; } .decision-summary strong { color:var(--cc-text); font-size:13px; } .decision-summary span { color:var(--cc-muted); font-size:11px; } +:global(.review-decision-dialog.el-dialog) { display:flex; max-height:min(90vh,820px); flex-direction:column; } +:global(.review-decision-dialog .el-dialog__body) { min-height:0; overflow-y:auto; padding:20px 28px; } +:global(.review-decision-dialog .el-dialog__footer) { flex:0 0 auto; } +.review-dialog-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; margin-bottom:18px; } +.review-dialog-section { min-width:0; } +.review-dialog-section h3 { margin:0 0 12px; padding-bottom:8px; border-bottom:2px solid #667eea; color:#1a1a2e; font-size:14px; font-weight:600; } +.review-info-grid { display:grid; gap:7px; margin:0; } +.review-info-grid > div { display:grid; grid-template-columns:110px minmax(0,1fr); gap:12px; } +.review-info-grid dt { color:#64748b; font-size:12px; } +.review-info-grid dd { min-width:0; margin:0; overflow-wrap:anywhere; color:#1e293b; font-size:13px; } +.review-document-table { overflow:hidden; border:1px solid #e4e4e7; border-radius:6px; } +.review-document-header,.review-document-row { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr) 76px; } +.review-document-header { background:rgba(102,126,234,.08); color:#52525b; font-size:12px; font-weight:600; } +.review-document-header > span,.review-document-row > span { min-width:0; padding:9px 10px; overflow-wrap:anywhere; border-right:1px solid #e4e4e7; } +.review-document-header > span:last-child,.review-document-row > span:last-child { border-right:0; } +.review-document-row { border-top:1px solid #e4e4e7; color:#334155; font-size:13px; } .decision-summary > div > span { display:block; margin-top:4px; }.decision-type { margin:0 0 16px; }.password-input-row { display:grid; grid-template-columns:minmax(0,1fr) auto; width:100%; gap:8px; }.dialog-hint { margin:12px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; }.archive-confirm { display:grid; gap:8px; margin:16px 0; }.archive-confirm strong { color:var(--cc-text); }.archive-confirm span { color:var(--cc-muted); font-size:12px; } .approval-result-warning { display:flex; align-items:flex-start; gap:10px; padding:12px 14px; border:1px solid #fecdca; border-radius:8px; background:#fffafa; color:#b42318; } .approval-result-warning strong,.approval-result-warning span { display:block; } .approval-result-warning strong { font-size:14px; } .approval-result-warning span { margin-top:4px; font-size:12px; line-height:1.5; } .approval-credentials { margin:18px 0 0; border:1px solid var(--cc-border); border-radius:8px; } .approval-credentials > div { display:grid; grid-template-columns:90px minmax(0,1fr); gap:12px; padding:12px 14px; } .approval-credentials > div + div { border-top:1px solid var(--cc-border); } .approval-credentials dt { color:var(--cc-muted); font-size:12px; } .approval-credentials dd { display:flex; min-width:0; align-items:center; gap:10px; margin:0; } .approval-credentials code { min-width:0; flex:1; overflow-wrap:anywhere; color:var(--cc-text); font-size:14px; font-weight:700; } -@media (max-width:720px) { .customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .decision-summary { align-items:flex-start; flex-direction:column; }.password-input-row { grid-template-columns:1fr; }.table-actions :deep(.el-button) { min-height:40px; } } +@media (max-width:720px) { .customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .decision-summary { align-items:flex-start; flex-direction:column; }.review-dialog-grid { grid-template-columns:1fr; gap:18px; }.review-info-grid > div { grid-template-columns:92px minmax(0,1fr); }.password-input-row { grid-template-columns:1fr; }.table-actions :deep(.el-button) { min-height:40px; } } @media (max-width:360px) { .customers-table-actions,.capital-input { grid-template-columns:1fr; } } diff --git a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts index f3afd751..3d5184f7 100644 --- a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts +++ b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts @@ -15,10 +15,39 @@ describe('InboundWorkflowWorkspace', () => { expect(source).toContain('getPlatformInbound') expect(source).toContain('StatusBadge') expect(source).toContain('label="基本信息"') - expect(source).toContain('label="货物与测量"') + expect(source).toContain('label="货品与测量"') expect(source).toContain('label="测量审核"') expect(source).toContain('label="费用信息"') - expect(source).toContain('label="状态与其他"') + expect(source).toContain('label="状态历史"') + expect(source).toContain('label="其他记录"') + }) + + it('opens measurement editing and review in prototype-sized centered dialogs', () => { + expect(source).toContain('title="编辑入库测量数据"') + expect(source).toContain('width="min(1900px, calc(100vw - 32px))"') + expect(source).toContain('title="审核入库测量数据"') + expect(source).toContain('width="min(1700px, calc(100vw - 32px))"') + expect(source).toContain('height: 92vh') + expect(source).toContain('max-height: 92vh') + expect(source).toContain('.el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto;') + expect(source).toContain('@media (max-width: 720px)') + expect(source).toContain('@media (max-width: 360px)') + }) + + it('keeps domain writes inside their existing panels and mounts them only in dialog bodies', () => { + expect(source).toContain('@click="measurementDialogOpen = true"') + expect(source).toContain('@click="reviewDialogOpen = true"') + expect(source).toContain(':key="`measurement-dialog-${revision}`"') + expect(source).toContain(':key="`review-dialog-${revision}`"') + expect(source).toContain('@changed="handleReviewChanged"') + expect(source).toContain('