diff --git a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md index 18cac6a3..9d310c78 100644 --- a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md +++ b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md @@ -63,6 +63,16 @@ 续批验证:81个文件671项定向回归首轮669项通过,出车图表两项旧颜色期望按公共主题更新后,所属1文件5项复验通过;50个修改Vue模板与局部样式编译通过。写前、写后快照核对未发现外部漂移;所有事件/表单绑定、非图表业务脚本保留,图表仅接入配色/字体/tooltip主题。未改API、日期计算、driver、权限、状态与路由。类型检查、全量测试和构建由总控统一执行;截图均待人工确认。 +## 自有业务列表字段补齐(2026-10-21) + +依据本轮用户字段截图 `codex-clipboard-9d532c45-a5f3-4f90-a8f2-f8a3cdfb8a3e.png`;用户确认派送仓库就是目的仓,最晚装车完成时间先空着。此前详情、运营登记、照片的承载与权限不变。 + +| 页面/操作 | 截图与当前确认 | 本次调整 | 验收 | +|---|---|---|---| +| 自有业务列表/读取字段 | 总价、单价、预约到达、实际到达各为独立列 | 拆开原合并列;一车一行,各到达时间保留目的仓标识,金额沿服务端原值 | 8项定向检查通过;待修改后人工截图 | +| 自有业务列表/仓库与时间 | 派送仓库等于目的仓;最晚装车完成时间暂无来源 | 派送仓库复用目的仓展示;最晚装车完成时间显示“—”,不拿计划发车替代;保留已有计划发车列 | 8项定向检查通过;待修改后人工截图 | +| 自有业务列表/字段名称 | 图中第一处“托数”装的是仓库名称 | 保留正确的“提货仓库”和唯一“实际托数”;“阶段”列改名“状态”,取值不变 | 8项定向检查通过;待修改后人工截图 | + ## 自有业务行操作样式核对(2026-10-21) 依据本轮用户截图 `codex-clipboard-eb6aa95b-c8a2-4b85-9294-22f3127cf72c.png` 与 Admin 界面规范“列表”纯文字行操作规则;仅修正列表按钮外观。 diff --git a/src/modules/overseas-master-data/pages/OwnedBusinessView.vue b/src/modules/overseas-master-data/pages/OwnedBusinessView.vue index 1fe40873..100e461f 100644 --- a/src/modules/overseas-master-data/pages/OwnedBusinessView.vue +++ b/src/modules/overseas-master-data/pages/OwnedBusinessView.vue @@ -37,11 +37,17 @@ const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 245, fixed: 'left', slotName: 'actions' }, { key: 'appointments', label: '预约号', minWidth: 195, slotName: 'appointments' }, { prop: 'dispatchNo', label: '排车单号', minWidth: 300 }, { prop: 'vehicle.plate', label: '车牌', minWidth: 130 }, { key: 'driver', label: '司机', minWidth: 150, slotName: 'driver' }, { prop: 'origin.warehouseName', label: '提货仓库', minWidth: 150 }, - { key: 'destinations', label: '目的仓 / 预约到达 / 实际到达', minWidth: 285, slotName: 'destinations' }, + { key: 'destinations', label: '目的仓', minWidth: 150, slotName: 'destinations' }, + { key: 'deliveryWarehouses', label: '派送仓库', minWidth: 150, slotName: 'destinations' }, { key: 'fuel', label: '油耗(L/100km)', minWidth: 145, slotName: 'fuel' }, { prop: 'actualPalletCount', label: '实际托数', minWidth: 105 }, - { key: 'money', label: '整车总金额 / 单价', minWidth: 170, slotName: 'money' }, - { key: 'plannedDepartureAt', label: '计划发车时间', minWidth: 180, slotName: 'plannedDeparture' }, { key: 'loadingCompletedAt', label: '实际装车完成时间', minWidth: 180, slotName: 'loaded' }, - { key: 'stage', label: '阶段', minWidth: 110, slotName: 'stage' }, + { key: 'money', label: '总价', minWidth: 170, align: 'right', slotName: 'money' }, + { key: 'unitPrice', label: '单价(/托)', minWidth: 160, align: 'right', slotName: 'unitPrice' }, + { key: 'plannedDepartureAt', label: '计划发车时间', minWidth: 180, slotName: 'plannedDeparture' }, + { key: 'latestLoadingCompletedAt', label: '最晚装车完成时间', minWidth: 180, slotName: 'latestLoaded' }, + { key: 'loadingCompletedAt', label: '实际装车完成时间', minWidth: 180, slotName: 'loaded' }, + { key: 'scheduledArrival', label: '预约到达时间', minWidth: 190, slotName: 'scheduledArrival' }, + { key: 'actualArrival', label: '实际到达时间', minWidth: 190, slotName: 'actualArrival' }, + { key: 'stage', label: '状态', minWidth: 110, slotName: 'stage' }, ] function time(value: string | null | undefined) { return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—' } async function load() { @@ -97,9 +103,13 @@ onBeforeUnmount(() => { active = false; generation++; openGeneration++ }) 查看明细登记运营信息{{ access.can(permission.ownedBusinessUpload) && row.actions.upload.allowed ? '上传照片' : '查看照片' }} {{ text(row.driver.name) }}{{ text(row.driver.phone) }} {{ destination.warehouseCode || destination.warehouseName || '—' }}{{ appointment.appointmentNo }}确认号 {{ appointment.confirmationNo }}— - {{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}预约 {{ time(appointment.scheduledAt) }}实到 {{ time(destination.actualArrivedAt) }} + {{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}— + {{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}{{ time(appointment.scheduledAt) }}—— + {{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}{{ time(destination.actualArrivedAt) }}— {{ row.operations.fuelConsumptionLitresPer100Km ?? '未登记' }} - {{ row.operations.money.currency }} {{ row.operations.money.amount }}单价 {{ row.unitPrice ?? '—' }}未登记 + {{ row.operations.money.currency }} {{ row.operations.money.amount }}未登记 + {{ row.operations.money.currency }} {{ row.unitPrice }}{{ row.operations.money ? '—' : '未登记' }} + — {{ time(row.plannedDepartureAt) }}{{ time(row.loadingCompletedAt) }}{{ ownedBusinessStages[row.stage as OwnedBusinessStage] }} diff --git a/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts index be651c4e..c55c656e 100644 --- a/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts +++ b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts @@ -17,7 +17,7 @@ const Button = defineComponent({ props: ['disabled'], template: '' }) const Pane = defineComponent({ props: ['name','label'], template: '' }) const Query = defineComponent({ props: ['modelValue','fields'], emits: ['update:modelValue','search'], template: '' }) -const Table = defineComponent({ props: ['data','columns','total','page','pageSize'], emits: ['update:page','update:pageSize'], setup(props,{slots}) { return () => h('div', [slots['toolbar-start']?.(), slots.filters?.(), slots.notices?.(), ...(props.data || []).map((row: object) => h('div', Object.entries(slots).filter(([name]) => !['toolbar-start','filters','notices'].includes(name)).map(([,slot]) => slot?.({row}))))]) } }) +const Table = defineComponent({ props: ['data','columns','total','page','pageSize'], emits: ['update:page','update:pageSize'], setup(props,{slots}) { return () => h('div', [slots['toolbar-start']?.(), slots.filters?.(), slots.notices?.(), ...(props.data || []).map((row: object) => h('div', Object.entries(slots).filter(([name]) => !['toolbar-start','filters','notices'].includes(name)).map(([name,slot]) => h('section', { 'data-column': name }, slot?.({row})))))]) } }) const OperationsEditor = defineComponent({ name: 'OwnedBusinessOperationsEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '' }) const PhotosEditor = defineComponent({ name: 'OwnedBusinessPhotosEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '' }) const global = { stubs: { AdminPage: Surface, DetailPageLayout: Surface, DataTable: Table, QueryToolbar: Query, ElTabs: Tabs, ElTabPane: Pane, ElButton: Button, OwnedBusinessOperationsEditor: OperationsEditor, OwnedBusinessPhotosEditor: PhotosEditor, LoadErrorState: { props:['message'], template:'{{message}}' }, ApiErrorMeta: true }, directives: { loading: () => {} } } @@ -30,6 +30,27 @@ beforeEach(() => { }) describe('owned business views', () => { + it('keeps split prices authoritative and arrival times associated with each destination', async () => { + const detail = ownedBusinessFixture() + detail.row.operations.money = { currency: 'USD', amount: '0.00' }; detail.row.unitPrice = '0.00' + detail.row.destinations[0]!.appointments[0]!.scheduledAt = '2026-10-21T09:00:00Z' + detail.row.destinations.push({ destinationKey: 'destination-2', warehouseCode: 'FBA-2', warehouseName: null, address: null, appointments: [], actualArrivedAt: '2026-10-21T10:00:00Z' }) + calls.list.mockResolvedValue({ records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } }) + const wrapper = mount(OwnedBusinessView, { global }); await flushPromises() + const columns = wrapper.findComponent(Table).props('columns') as { key?: string; label: string; slotName?: string }[] + expect(columns.map(column => column.label)).toEqual(expect.arrayContaining(['目的仓', '派送仓库', '总价', '单价(/托)', '最晚装车完成时间', '预约到达时间', '实际到达时间', '状态'])) + expect(columns.find(column => column.key === 'deliveryWarehouses')!.slotName).toBe('destinations') + expect(wrapper.get('[data-column="money"]').text()).toBe('USD 0.00') + expect(wrapper.get('[data-column="unitPrice"]').text()).toBe('USD 0.00') + expect(wrapper.get('[data-column="latestLoaded"]').text()).toBe('—') + const scheduled = wrapper.findAll('[data-column="scheduledArrival"] .owned-business-destination') + expect(scheduled.map(cell => cell.text())).toEqual(['FBA-1' + dayjs('2026-10-21T09:00:00Z').format('YYYY-MM-DD HH:mm'), 'FBA-2—']) + const actual = wrapper.findAll('[data-column="actualArrival"] .owned-business-destination') + expect(actual.map(cell => cell.text())).toEqual(['FBA-1—', 'FBA-2' + dayjs('2026-10-21T10:00:00Z').format('YYYY-MM-DD HH:mm')]) + expect(calls.detail).not.toHaveBeenCalled() + wrapper.unmount() + }) + it('keeps a non-pool external vehicle returned by the server without filtering or requiring a vehicle id', async () => { const detail = ownedBusinessFixture() detail.row.vehicle = { vehicleId: null, plate: 'EXTERNAL-NO-POOL', ownership: 'DISPATCHED' }