diff --git a/AGENTS.md b/AGENTS.md index 0fab185f..ad61bb6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ - 默认验收身份:`platform_admin`,用于“后台管理”的样式、路由、权限展示和交互验收。 - 密码不写入仓库;从本机未提交环境变量 `CHONGCHONG_ADMIN_ACCEPTANCE_PASSWORD` 或 `C:/Users/BY-OY/.codex/chongchong-global-dev-acceptance.env` 读取。若凭据不存在,再向用户索取,不重复询问账号名。 - 该身份仅适用于开发环境。不要把密码、Token 或真实接口地址写入源码、测试快照、日志或 `AGENTS.md`;开发环境变更时只更新本机凭据。 -- 样式验收优先使用该身份进入真实页面,并按原型还原规则检查加载、空态、错误态、权限态和窄屏布局。 +- 样式验收优先使用该身份进入真实页面,当前固定使用 `1920×1080` 视口和 `100%` 浏览器缩放,按原型还原规则检查加载、空态、错误态、权限态和桌面布局。 ## 模块快速查询 @@ -54,7 +54,7 @@ | 导航与原型菜单图标 | `core/navigation/adminNavigation.ts`、`core/navigation/prototypeMenuIcons.ts` | 菜单结构、权限与路由同步核对;已有原型图标优先使用映射资源 | | 平台仓库与仓储资源 | `modules/warehouses/`、`modules/warehouse-resources/` | `warehouse/` | | 订单列表、详情与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;费用入口 `modules/unified-orders/components/InboundFeeDetailManager.vue`、`modules/unified-orders/api/inboundFeeDetails.ts` | `order/`、`firstleg/`;费用来源审核发布联查 `firstleg/AGENTS.md` | -| 入库、测量和计费工作流 | `modules/inbounds/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`file/`、`integration-service/` | +| 入库、箱唛、手工/设备测量和计费工作流 | `modules/inbounds/pages/InboundOrdersView.vue`;测量接口与页面从 `modules/inbounds/measurement/` 进入;审核、称重和费用继续按 `modules/inbounds/` 相邻子域定位;计价主数据在 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`order/`、`file/`、`integration-service/`;箱码生成、打印与测量绑定先读 `firstleg/AGENTS.md` | | 财务账户、账单、收款与成本 | `modules/finance/` | `finance/`;费用来源与柜次成本快照联查 `firstleg/`,凭证联查 `file/` | | 货柜与智能排柜 | `modules/container-loading/` | `firstleg/loading/`、`firstleg/split/` | | 预装详情、装柜清单与柜次费用 | `modules/loading-lists/pages/PreloadPlanDetailView.vue`、`modules/loading-lists/components/LoadingListDetailDrawer.vue`、`modules/loading-lists/components/ContainerFeesPanel.vue`、`modules/loading-lists/api/containerFees.ts` | `firstleg/loading/`、`firstleg` 柜次费用、`finance/` 成本分摊、`file/`、`warehouse/` | @@ -62,7 +62,7 @@ | 拖车预约 | `modules/trucking/` | `firstleg/outbound/dispatch/`、`firstleg/`;承运商联查 `modules/suppliers/` 与 `warehouse/src/main/resources/openapi/supplier-v1.yaml`,服务端契约见 `firstleg/src/main/resources/openapi/truck-appointments-v1.yaml` | | 报关协同 | `modules/customs/` | `firstleg/customs/`、`file/`;报关代理联查 `modules/suppliers/` 与 `warehouse/src/main/resources/openapi/supplier-v1.yaml`,服务端契约见 `firstleg/src/main/resources/openapi/customs-v1.yaml` | | 国内出仓确认 | `modules/outbound-dispatch/` | `firstleg/outbound/dispatch/` | -| 主数据与下载 | `modules/master-data/`、`modules/downloads/` | `warehouse/`、`file/` | +| 主数据与下载 | `modules/master-data/`、`modules/downloads/` | `warehouse/`、`file/`;Finance 导出联查 `finance/` 内部 Export Provider 与 File 导出定义 | ## Admin 全量验收快速索引 @@ -113,7 +113,7 @@ - 优先使用 Element Plus 与现有复用组件;图标使用 Lucide,不用表情符号充当功能图标。 - 表格、筛选、抽屉和表单应按实际语义覆盖适用的加载、空数据、错误、禁用和无权限状态,不为不适用的组件机械增加状态。 - 保持键盘可操作、可见焦点、表单标签和足够的颜色对比度;不要只用颜色表达状态。 -- 新页面至少适配现有 320px 最小宽度策略,并检查侧栏、表格、抽屉和表单在窄屏下的退化方式。 +- 当前样式开发与验收只处理 `1920×1080` 桌面视口,不检查或整改 `720px`、`320px` 及其他窄屏;这些视口不作为测试、截图或交付门禁。保留现有响应式 CSS 和组件能力,不为当前阶段主动删除,用户恢复多视口验收后再统一处理。 ## 公共前端组件复用 @@ -146,4 +146,4 @@ pnpm build - Admin 全量验收唯一入口为 `../../baiyue-firstleg/scripts/admin-test-env-acceptance.mjs`,默认模块列表覆盖平台全模块;单模块定位先查 `../../baiyue-firstleg/scripts/admin-test-env-modules/README.md`。 - 该入口通过测试环境 HTTP 验证真实服务、权限、状态、幂等和跨服务链路,会写入或停用测试数据,必须获得当前对话明确授权。 - `pnpm test/typecheck` 和 `frontend-full-regression.ps1` 仅是本地质量检查,不代表 Admin 服务已完成全量验收。 -- HTTP 验收通过后,再使用开发验收账号通过浏览器逐页检查真实接口调用、原型、权限、错误态、空态和窄屏布局。 +- HTTP 验收通过后,再使用开发验收账号通过浏览器逐页检查真实接口调用、原型、权限、错误态、空态和 `1920×1080` 桌面布局;当前不追加窄屏浏览器验收。 diff --git a/src/app/components/AdminSidebarMenu.test.ts b/src/app/components/AdminSidebarMenu.test.ts index b74f2b3f..8b259de6 100644 --- a/src/app/components/AdminSidebarMenu.test.ts +++ b/src/app/components/AdminSidebarMenu.test.ts @@ -20,8 +20,8 @@ const visibleNavigation = filterAdminNavigation(adminNavigation, new Set([ const stubs = { 'el-menu': { - props: ['defaultActive', 'collapse'], - template: '', + props: ['defaultActive', 'defaultOpeneds', 'collapse'], + template: '', }, 'el-sub-menu': { props: ['index'], @@ -41,6 +41,7 @@ describe('AdminSidebarMenu', () => { }) expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users') + expect(wrapper.get('nav').attributes('data-openeds')).toBe('[]') 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')) diff --git a/src/app/components/AdminSidebarMenu.vue b/src/app/components/AdminSidebarMenu.vue index ea4cc052..7577593e 100644 --- a/src/app/components/AdminSidebarMenu.vue +++ b/src/app/components/AdminSidebarMenu.vue @@ -15,8 +15,9 @@ function getGroupKeys(nodes: AdminNavigationNode[]): string[] { : []) } -const defaultOpeneds = computed(() => getGroupKeys(props.items)) -const menuInstanceKey = computed(() => `${props.collapsed ? 'collapsed' : 'expanded'}:${defaultOpeneds.value.join('|')}`) +const groupKeys = computed(() => getGroupKeys(props.items)) +const defaultOpeneds: string[] = [] +const menuInstanceKey = computed(() => `${props.collapsed ? 'collapsed' : 'expanded'}:${groupKeys.value.join('|')}`) diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index bf348c5b..7062a573 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -56,10 +56,10 @@ describe('platform container loading API', () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: 101, oceanBookingRequirementId: 202 } }) vi.mocked(http.delete).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: null, oceanBookingRequirementId: null } }) await createContainerSimulation({ channelCode: 'NY_EXX', warehouseId: '8', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000 }) - await linkContainerOceanBooking('7', { realContainerBookingId: '101', expectedVersion: 3 }) + await linkContainerOceanBooking('7', { oceanBookingId: '101', requirementId: '202', containerNo: 'MSCU1234567', expectedVersion: 3 }) await unlinkContainerOceanBooking('7', 4, '空柜改配') expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/containers', { channelCode: 'NY_EXX', warehouseId: '8', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000 }) - expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/containers/7/ocean-booking-link', { realContainerBookingId: '101', expectedVersion: 3 }) + expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/containers/7/ocean-booking-link', { oceanBookingId: '101', requirementId: '202', containerNo: 'MSCU1234567', expectedVersion: 3 }) expect(http.delete).toHaveBeenCalledWith('/platform/v1/containers/7/ocean-booking-link', { data: { expectedVersion: 4, reason: '空柜改配' } }) }) diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 420218bd..d4d0c005 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -246,17 +246,17 @@ describe('ContainersView', () => { expect(source).not.toContain('v-model="form.maxWeightKg"') }) - it('allows first linking after preloading but only offers unlinking for wait-loading containers', () => { + it('links a simulation to a released requirement and records the port container number', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') - expect(source).toContain("row.sourceType === 'SIMULATION' && row.recordStatus === 'ACTIVE' && !row.oceanBookingId") - expect(source).toContain("row.sourceType === 'SIMULATION' && row.recordStatus === 'ACTIVE' && row.oceanBookingId") - expect(source).toContain('已预排货但尚未进入 PDA、取证或单证阶段的模拟柜仍可首次关联') - expect(source).toContain('柜次号与装柜明细保持不变') - expect(source).toContain('只有尚未排货的空柜可以解除') - expect(source).toContain('booking.containers') - expect(source).toContain('realContainerBookingId: container.containerBookingId') - expect(source).toContain('linkContainerOceanBooking(target.id, { realContainerBookingId: candidate.realContainerBookingId, expectedVersion: target.version })') - expect(source).toContain('unlinkContainerOceanBooking(target.id, target.version, unlinkReason.value.trim())') + expect(source).toContain("listOceanBookings({ page: 1, size: 200, status: 'RELEASED' })") + expect(source).toContain('booking.containerRequirements') + expect(source).toContain('booking.containers.filter((container) => container.requirementId === requirement.requirementId).length') + expect(source).toContain('请输入港口取得的真实集装箱号') + expect(source).toContain('oceanBookingId: candidate.oceanBookingId, requirementId: candidate.requirementId, containerNo') + expect(source).toContain(':disabled="candidate.disabled"') + expect(source).not.toContain('realContainerBookingId') + expect(source).not.toContain('MERGED') + expect(source).not.toContain('解除订舱关联') }) }) diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index c69acc7c..23fa291e 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -1,7 +1,7 @@ @@ -150,7 +184,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading }) 修改重量 通过并进入待分柜 - 刷新 + 刷新 @@ -163,6 +197,84 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading }) + + + + 审核数据总览 + + {{ sourceLabel }} + + + + + 客户申报 + + 箱数{{ formatNumber(detail.originalPackageCount, 0) }} + 毛重{{ formatNumber(detail.originalGrossWeightKg) }} kg + 体积{{ formatNumber(detail.originalVolumeCbm, 4) }} m³ + + + + 平台实测 + + 箱数{{ formatNumber(detail.actualPackageCount, 0) }} {{ formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0) }} + 毛重{{ formatNumber(detail.actualGrossWeightKg) }} kg · {{ formatDifference(detail.actualGrossWeightKg, detail.originalGrossWeightKg, 3, ' kg') }} + 货物净重{{ formatNumber(detail.actualNetWeightKg) }} kg + 体积{{ formatNumber(detail.actualVolumeCbm, 4) }} m³ · {{ formatDifference(detail.actualVolumeCbm, detail.originalVolumeCbm, 4, ' m³') }} + 体积重{{ formatNumber(detail.volumeWeightKg) }} kg + + + + + + + + + 箱规测量明细 + + {{ detail.items.length }} 条 + + + + + + {{ row.boxCode || row.itemNo || '—' }} + + + + + + + + {{ formatNumber(row.lengthCm) }} × {{ formatNumber(row.widthCm) }} × {{ formatNumber(row.heightCm) }} + + + {{ formatNumber(row.packageCount, 0) }} + + + {{ formatNumber(row.unitVolumeCbm, 4) }} + + + {{ formatNumber(row.totalVolumeCbm, 4) }} + + + {{ formatNumber(row.volumeWeightKg) }} + + + {{ formatDate(row.deviceMeasuredAt) }} + + + + + + - - - - {{ historyError.message }}重试 - - - - - - {{ resultLabel(item.reviewResult) }}{{ item.exceptionRelease ? ' · 异常放行' : '' }}{{ formatDate(item.reviewedAt) }}{{ item.releaseReason || item.reviewReason || '未填写说明' }}{{ item.reviewerName || '系统' }} - - - - + + + + + + {{ imagePreviewError.message }} + + + + + + @@ -197,26 +307,45 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading }) .review-toolbar__actions :deep(.el-button) { margin-left: 0; } .review-toolbar__status > .review-toolbar__context { padding-left: 8px; border-left: 1px solid var(--cc-border); font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; } .review-alert-title { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; } -.review-history { border-top: 1px solid var(--cc-border); border-bottom: 0; } -.review-history :deep(.el-collapse-item__header) { height: 42px; color: var(--cc-muted); font-size: 12px; } -.review-history :deep(.el-collapse-item__wrap) { border-bottom: 0; } -.history-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; } -.history-list li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 10px; position: relative; padding-bottom: 16px; } -.history-list li:not(:last-child)::after { content: ''; position: absolute; top: 16px; bottom: 0; left: 7px; width: 1px; background: var(--cc-border); } -.history-marker { z-index: 1; width: 14px; height: 14px; margin-top: 2px; border: 3px solid #909399; border-radius: 50%; background: var(--cc-surface); } -.history-marker--success { border-color: #67c23a; } -.history-marker--danger { border-color: #f56c6c; } -.history-content { min-width: 0; } -.history-content > div { display: flex; justify-content: space-between; gap: 12px; } -.history-content time, .history-content > span { color: var(--cc-muted); font-size: 11px; } -.history-content p { margin: 5px 0 4px; color: var(--cc-text); font-size: 13px; overflow-wrap: anywhere; } +.review-data, .review-items { min-width: 0; padding: 0 0 12px; border-bottom: 1px solid var(--cc-border); } +.review-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 8px; } +.review-section-heading > div { min-width: 0; } +.review-section-heading h3 { margin: 0; color: var(--cc-text); font-size: 14px; line-height: 1.35; } +.review-section-heading > span { color: var(--cc-muted); font-size: 12px; white-space: nowrap; } +.review-comparison { border-top: 1px solid var(--cc-border); } +.review-comparison__row { display: grid; grid-template-columns: 78px minmax(0, 1fr); align-items: center; min-width: 0; padding: 8px 0; } +.review-comparison__row + .review-comparison__row { border-top: 1px dashed var(--cc-border); } +.review-comparison__row > strong { color: #344054; font-size: 12px; } +.review-metrics, .review-declaration { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px 16px; min-width: 0; margin: 0; } +.review-declaration { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.review-metrics > div, .review-declaration > div { display: flex; min-width: 0; align-items: baseline; gap: 6px; } +.review-metrics dt, .review-declaration dt { color: var(--cc-muted); font-size: 11px; line-height: 1.4; } +.review-metrics dd { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 15px; font-weight: 700; line-height: 1.35; } +.review-metrics dd small, .review-declaration dd small { color: var(--cc-muted); font-size: 11px; font-weight: 500; } +.review-metrics .review-metric--primary dd { color: #067647; } +.review-declaration dd { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; font-weight: 650; line-height: 1.4; } +.review-table-scroll { width: 100%; min-width: 0; overflow: hidden; } +.review-table { width: 100%; font-size: 12px; } +.review-table :deep(.el-table__header th) { padding: 8px 0; background: #f6f8fa; color: #475467; font-size: 11px; font-weight: 700; } +.review-table :deep(.el-table__header .cell) { white-space: nowrap; } +.review-table :deep(.el-table__cell) { padding: 7px 0; } +.review-table :deep(.review-time-cell .cell) { white-space: nowrap; } +.review-table :deep(.measurement-evidence-thumb) { min-width: 72px; border-radius: 4px; } +.review-image-preview { display: grid; min-height: 240px; place-items: center; } +.review-image-preview img { display: block; width: auto; max-width: 100%; max-height: 70vh; object-fit: contain; } +.review-image-preview__error { display: grid; justify-items: center; gap: 8px; color: var(--el-color-danger); } @media (max-width: 720px) { .review-toolbar { align-items: stretch; flex-direction: column; margin: -14px -14px 0; padding: 12px 14px; } .review-toolbar__status > .review-toolbar__context { width: 100%; padding-top: 8px; padding-left: 0; border-top: 1px solid var(--cc-border); border-left: 0; } .review-toolbar__actions { display: grid; width: 100%; grid-template-columns: 1fr; justify-content: stretch; } .review-toolbar__actions :deep(.el-button) { width: 100%; min-height: 44px; } + .review-section-heading { align-items: stretch; flex-direction: column; } + .review-comparison__row { grid-template-columns: 78px minmax(0, 1fr); align-items: start; } + .review-metrics, .review-declaration { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 360px) { .review-toolbar { margin: -10px -10px 0; padding: 10px; } + .review-comparison__row { grid-template-columns: 1fr; gap: 6px; } + .review-metrics, .review-declaration { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 10px; } } diff --git a/src/modules/inbounds/review/types.ts b/src/modules/inbounds/review/types.ts index 3da97c9f..5d80bddf 100644 --- a/src/modules/inbounds/review/types.ts +++ b/src/modules/inbounds/review/types.ts @@ -17,7 +17,7 @@ export interface MeasurementReviewItem { volumeWeightKg: number | null intelligentImageFileId: string | null machineNo: string | null - measuredAt: string | null + deviceMeasuredAt: string | null overLimitWarning: string | null outerLengthCm: number | null outerWidthCm: number | null diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts index 8f6ec72f..527c16c4 100644 --- a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts +++ b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts @@ -81,4 +81,9 @@ describe('WeightOcrPanel contract', () => { expect(source).toContain('@media (max-width: 720px)') expect(source).toContain('@media (max-width: 360px)') }) + + it('keeps each weight summary label and value on one compact line', () => { + expect(source).toContain('.weight-summary { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }') + expect(source).not.toContain('.weight-summary { display: grid;') + }) }) diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.vue b/src/modules/inbounds/weight/components/WeightOcrPanel.vue index 5d68c1e9..e8db56cb 100644 --- a/src/modules/inbounds/weight/components/WeightOcrPanel.vue +++ b/src/modules/inbounds/weight/components/WeightOcrPanel.vue @@ -609,16 +609,16 @@ defineExpose({ reload: loadWorkspace })
{{ item.releaseReason || item.reviewReason || '未填写说明' }}