diff --git a/AGENTS.md b/AGENTS.md index 4d69de1e..b13bbd78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ 1. 用“模块快速路由”选择最小业务模块,进入后读取该目录最近的 `AGENTS.md`;按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不先扫描全部 `src/`。 2. 页面、布局或交互任务同时读取 `../../docs/Admin-Client前端原型还原验收规则.md` 和后台原型 `../../docs/悦/货代SAAS平台后台/AGENTS.md`。 - 订单样板和国内仓样式统一还须读取 `../../docs/Admin高密度界面规范.md`;以其已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式,公共样式按已确认菜单范围接入。 + Admin 样式优化还须读取 `../../docs/Admin高密度界面规范.md` 的“样式维护边界”和“新模块接入规则”;以已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式。国内仓、客户管理是样板;新增菜单按本轮范围显式接入,货柜管理入口见 `../../docs/Admin货柜管理样式核对记录.md`。 3. 标准列表或公共组件任务读取 `src/shared/components/AGENTS.md`;国内/海外 HTTP、Token 或刷新任务读取 `src/shared/api/AGENTS.md`;Finance 任务读取 `src/modules/finance/AGENTS.md`;海外仓全流程任务先读 `../../docs/海外仓业务/README.md`。 4. 平台/客户层级与权限公共语义读取 `../../docs/平台客户层级与权限契约.md`;双区域 API、海外 Token 和跨区事件读取 `../../docs/跨区域认证与服务通信契约.md`。 @@ -51,6 +51,7 @@ | 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | | 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | | 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | | 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 | | 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 | | 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;实际、冻结、可用余额、授信与流水只消费 Finance 权威结果,充值审核和手工充扣携带版本与幂等键。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | @@ -105,6 +106,9 @@ ## 命令与质量门禁 +- 国内仓、客户管理密度默认值统一维护在 `src/styles/adminDensityTokens.css`,通用按钮/页签/描述表/弹框规则统一维护在 `adminDensity.css`;页面只选择公共类与语义参数、保留业务布局,不复制通用外观或在页面重新定义 `--admin-*` 默认值。修改前沿实际引用读取模块 CSS,接管后删除冲突旧声明。 +- 修改密度样式或接入消费者时更新并运行 `src/styles/adminDensity.test.ts` 维护合同;不得为通过检查删除权限、业务提交或错误处理断言。未接入菜单通过显式密度类隔离。 + 在 `apps/admin-web` 执行: ```bash diff --git a/src/app/DomesticOutboundListStates.test.ts b/src/app/DomesticOutboundListStates.test.ts index 738e665a..360dc95b 100644 --- a/src/app/DomesticOutboundListStates.test.ts +++ b/src/app/DomesticOutboundListStates.test.ts @@ -6,7 +6,7 @@ const pages = [ ['src/modules/container-loading/pages/ContainersView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无货柜"', 'statusText(row.planStatus)', 'platform:firstleg:container:list', '@retry="loadRows"'], ['src/modules/loading-lists/components/LoadingListsView.vue', ':loading="loading"', 'LoadErrorState', ':empty-text="`暂无符合筛选条件的${title}`"', 'loadingListStatusLabel(row.status)', 'canView', '@retry="load"'], ['src/modules/ocean-bookings/pages/OceanBookingsView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合筛选条件的订舱"', 'oceanBookingStatusLabel(row.status)', "{{ row.status === 'DRAFT' ? '编辑' : '查看' }}", '@retry="load"'], - ['src/modules/trucking/pages/TruckAppointmentsView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合筛选条件的拖车预约"', 'statusLabel(row.status)', '详情', '@retry="load"'], + ['src/modules/trucking/pages/TruckAppointmentsView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合筛选条件的拖车预约"', 'statusLabel(row.status)', '@click="openDetail(row as TruckAppointmentResource)">详情', '@retry="load"'], ] as const function source(path: string) { diff --git a/src/app/components/WorkspaceTabs.vue b/src/app/components/WorkspaceTabs.vue index 8007dda5..1dc761e1 100644 --- a/src/app/components/WorkspaceTabs.vue +++ b/src/app/components/WorkspaceTabs.vue @@ -1,4 +1,5 @@ -

{{ selectedCandidate.palletNo || selectedCandidate.groupNo || selectedCandidate.inboundCode || '入仓码暂缺' }}

+

{{ selectedCandidate.palletNo || selectedCandidate.groupNo || selectedCandidate.inboundCode || '入仓码暂缺' }}

diff --git a/src/modules/container-loading/types.ts b/src/modules/container-loading/types.ts index b8d786c1..566dd08e 100644 --- a/src/modules/container-loading/types.ts +++ b/src/modules/container-loading/types.ts @@ -208,7 +208,7 @@ export interface ContainerQuery { export type ContainerWorkbenchKind = 'DOMESTIC' | 'OVERSEAS' export type ContainerWorkbenchScope = 'ACTIVE' | 'HISTORY' -export type ContainerWorkbenchAction = 'EDIT_CONTAINER' | 'MANAGE_CUSTOMS' | 'CONFIRM_OUTBOUND' | 'MANAGE_DESTINATION_DOCUMENTS' | 'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL' +export type ContainerWorkbenchAction = 'EDIT_CONTAINER' | 'MANAGE_CUSTOMS' | 'CONFIRM_OUTBOUND' | 'MANAGE_DESTINATION_DOCUMENTS' | 'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL' export interface ContainerWorkbenchStatusSummary { loadingStatus?: string | null @@ -244,7 +244,7 @@ export interface ContainerWorkbenchTerminalDrayage { actualEmptyReturnAt: string | null unloadingCompletedAt: string | null emptyReturnWarning: { status: 'NOT_APPLICABLE' | 'PENDING' | 'OVERDUE' | 'RESOLVED'; deadlineAt: string | null; overdueHours: number; overdueDays: number } - allowedActions: ReadonlyArray<'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL'> + allowedActions: ReadonlyArray<'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL'> updatedAt: string | null } diff --git a/src/modules/customers/CustomerOverseasStorageFeePanel.vue b/src/modules/customers/CustomerOverseasStorageFeePanel.vue index 782f9a42..2f5744c7 100644 --- a/src/modules/customers/CustomerOverseasStorageFeePanel.vue +++ b/src/modules/customers/CustomerOverseasStorageFeePanel.vue @@ -81,15 +81,15 @@ onMounted(load)
当前客户暂无海外仓费用配置
-

{{ groupIndex === 0 ? '仓租(CBM/天)' : '退货仓储费(托/天)' }} · {{ group.length }}档

新增阶梯
操作开始天数结束天数价格
删除以上
+

{{ groupIndex === 0 ? '仓租(CBM/天)' : '退货仓储费(托/天)' }} · {{ group.length }}档

新增阶梯
操作开始天数结束天数价格
删除以上
diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts index d0488a20..fadb5e78 100644 --- a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts +++ b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts @@ -15,7 +15,7 @@ describe('DomesticContainersView', () => { for (const fact of ['summary.inboundBoxes', 'summary.outboundBoxes', 'summary.availableBoxes', 'summary.inboundVolumeCbm']) expect(source).toContain(fact) expect(source).toContain("import './domesticContainersSummary.css'") expect(summaryStyles).toContain('display: flex; flex-wrap: wrap') - expect(summaryStyles).toContain('font-size: 14px') + expect(summaryStyles).toContain('font-size: var(--admin-font-section, 14px)') expect(summaryStyles).not.toContain('position: absolute') }) it('does not offer an inventory detail action before an inbound has formed inventory', () => { expect(source).toContain("canViewInventory(row as ContainerOrder | InventoryRecord)"); expect(source).toContain("row.status === 'COMPLETED'") }) diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.vue b/src/modules/domestic-containers/pages/DomesticContainersView.vue index 312f024e..b375a394 100644 --- a/src/modules/domestic-containers/pages/DomesticContainersView.vue +++ b/src/modules/domestic-containers/pages/DomesticContainersView.vue @@ -2,7 +2,7 @@ import '@/styles/adminDensity.css' import { computed, onMounted, reactive, ref, watch } from 'vue' import { ElMessage, type FormInstance, type FormRules } from 'element-plus' -import { Eye, Plus } from '@lucide/vue' +import { Plus } from '@lucide/vue' import { useRouter } from 'vue-router' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -74,7 +74,7 @@ watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWa - + diff --git a/src/modules/domestic-containers/pages/domesticContainersSummary.css b/src/modules/domestic-containers/pages/domesticContainersSummary.css index 82421555..0b979554 100644 --- a/src/modules/domestic-containers/pages/domesticContainersSummary.css +++ b/src/modules/domestic-containers/pages/domesticContainersSummary.css @@ -1,9 +1,9 @@ .domestic-containers-page .table-panel .inventory-summary { - display: flex; flex-wrap: wrap; gap: 6px 28px; margin: 0; - padding: 8px 12px; border: 0; border-bottom: 1px solid var(--cc-border); background: #fff; + display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px) 28px; margin: 0; + padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); border: 0; border-bottom: 1px solid var(--cc-border); background: var(--admin-surface, #fff); } .domestic-containers-page .table-panel .inventory-summary > div { - display: flex; align-items: baseline; min-width: 0; gap: 8px; padding: 0; border: 0; + display: flex; align-items: baseline; min-width: 0; gap: var(--admin-space-3, 8px); padding: 0; border: 0; } -.domestic-containers-page .table-panel .inventory-summary span { color: var(--cc-muted); font-size: 12px; } -.domestic-containers-page .table-panel .inventory-summary strong { font-size: 14px; font-weight: 600; } +.domestic-containers-page .table-panel .inventory-summary span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); } +.domestic-containers-page .table-panel .inventory-summary strong { font-size: var(--admin-font-section, 14px); font-weight: 600; } diff --git a/src/modules/downloads/pages/DownloadCenterView.test.ts b/src/modules/downloads/pages/DownloadCenterView.test.ts index fc5ec843..00901441 100644 --- a/src/modules/downloads/pages/DownloadCenterView.test.ts +++ b/src/modules/downloads/pages/DownloadCenterView.test.ts @@ -161,7 +161,8 @@ describe('DownloadCenterView', () => { expect(document.body.textContent).toContain('任务详情 · EXP9') expect(document.body.textContent).toContain('客户列表.xlsx') expect(document.querySelector('.el-drawer')).toBeNull() - expect(document.querySelector('.el-dialog')).not.toBeNull() + expect(document.querySelector('.el-dialog.admin-density-dialog')).not.toBeNull() + expect(document.querySelectorAll('.task-detail-grid.density-fields > div')).toHaveLength(11) const detailDownload = [...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '下载文件') detailDownload?.click() diff --git a/src/modules/downloads/pages/DownloadCenterView.vue b/src/modules/downloads/pages/DownloadCenterView.vue index 2ba8b3b1..3f887a34 100644 --- a/src/modules/downloads/pages/DownloadCenterView.vue +++ b/src/modules/downloads/pages/DownloadCenterView.vue @@ -1,6 +1,6 @@ diff --git a/src/modules/loading-lists/status.test.ts b/src/modules/loading-lists/status.test.ts index 601698c5..571fb044 100644 --- a/src/modules/loading-lists/status.test.ts +++ b/src/modules/loading-lists/status.test.ts @@ -1,7 +1,22 @@ import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import StatusBadge from '@/shared/components/StatusBadge.vue' import { loadingListStatusLabel } from './status' describe('loading list status labels', () => { + it.each([ + ['WAIT_LOADING', '待装柜', 'warning'], + ['EDITING', '预装柜中', 'warning'], + ['LOADING', '拣货中', 'info'], + ['COMPLETED', '装柜完成', 'success'], + ['NEW_LOADING_STATE', '未知状态', 'neutral'], + ])('renders %s with its shared status tone', (status, label, tone) => { + const wrapper = mount(StatusBadge, { props: { status: loadingListStatusLabel(status) } }) + expect(wrapper.text()).toBe(label) + expect(wrapper.classes()).toContain(`is-${tone}`) + wrapper.unmount() + }) + it('translates known states and hides unknown backend enums', () => { expect(loadingListStatusLabel('EDITING')).toBe('预装柜中') expect(loadingListStatusLabel('COMPLETED')).toBe('装柜完成') diff --git a/src/modules/master-data/counterparties/pages/PartnersView.test.ts b/src/modules/master-data/counterparties/pages/PartnersView.test.ts index f0b39b09..aaac30ce 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.test.ts +++ b/src/modules/master-data/counterparties/pages/PartnersView.test.ts @@ -195,6 +195,8 @@ describe('PartnersView real API integration', () => { const sections = detailContent?.querySelectorAll('section') const otherInfo = sections?.item(sections.length - 1) expect(detailContent).not.toBeNull() + expect(detailContent?.closest('.el-drawer')?.classList.contains('admin-density-drawer')).toBe(true) + expect(detailContent?.querySelectorAll('.density-fields')).toHaveLength(3) expect(otherInfo?.querySelectorAll('.detail-grid > .detail-span')).toHaveLength(2) expect(otherInfo?.textContent).toContain('更新时间') expect(otherInfo?.textContent).toContain('长期合作单位') @@ -278,13 +280,10 @@ describe('PartnersView real API integration', () => { }) it('keeps all three row actions visible without a premature more menu', () => { - expect(source).toContain('详情') - expect(source).toContain('编辑') - expect(source).toContain('') - expect(source).not.toContain('MoreHorizontal') - expect(source).not.toContain('详情') + expect(actions).toContain('>编辑') + expect(actions).toContain(':data-tone=') + expect(actions).not.toMatch(/<(?:el-dropdown|Pencil|Power|Eye|Trash2|Download|CircleX)\b/) + })}) diff --git a/src/modules/master-data/counterparties/pages/PartnersView.vue b/src/modules/master-data/counterparties/pages/PartnersView.vue index bf1662a7..274c6676 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.vue +++ b/src/modules/master-data/counterparties/pages/PartnersView.vue @@ -1,7 +1,8 @@