统一履约状态和节点日志展示并整理装柜与尾程界面

This commit is contained in:
2026-10-28 16:56:32 +08:00
parent a52a6620fc
commit d990637b38
20 changed files with 270 additions and 43 deletions
@@ -446,7 +446,7 @@
| 页面 | 操作 | 原型/确认依据 | 承载与实现 | 验收 |
|---|---|---|---|---|
| 排车列表:装车中、装车后、派送中、配送完成 | 管理资料 | 用户管理资料截图及同车/派送单资料共用确认;2026-10-22 用户截图确认移除资料核对、油耗和装车照片改表格;签收完成即可,其他可补 | 1100px居中可全屏弹框,签收文件/油耗图片/装车资料三个页签;嵌入编辑器隐藏资料核对;三个页签仅显示各自资料,装车资料只保留整车装车照片,不再附带BOL及车辆/司机附件,保存资料按钮仅在签收页签显示,切换保留签收草稿;油耗两行、装车照片五行表格,操作列在首列,保留预览、上传/替换和原保存行为;装车中及装车后默认打开装车资料,可同步上传,确认装车且资料齐全后自动派送;按关联派送单切换,共用原派送资料编辑器;油耗沿本车附加照片保存 | 4文件27项定向测试及受影响组件与测试源码类型检查通过;页签隔离追加修改后5项定向测试及组件/测试类型检查通过;1920×1080、100%待人工截图确认 |
| 排车列表:装车中、装车后、派送中 | 管理资料 | 用户管理资料截图及同车/派送单资料共用确认;2026-10-22 用户截图确认移除资料核对、油耗和装车照片改表格;签收完成即可,其他可补 | 1100px居中可全屏弹框,签收文件/油耗图片/装车资料三个页签;嵌入编辑器隐藏资料核对;三个页签仅显示各自资料,装车资料只保留整车装车照片,不再附带BOL及车辆/司机附件,保存资料按钮仅在签收页签显示,切换保留签收草稿;油耗两行、装车照片五行表格,操作列在首列,保留预览、上传/替换和原保存行为;装车中及装车后默认打开装车资料,可同步上传,确认装车且资料齐全后自动派送;按关联派送单切换,共用原派送资料编辑器;油耗沿本车附加照片保存 | 4文件27项定向测试及受影响组件与测试源码类型检查通过;页签隔离追加修改后5项定向测试及组件/测试类型检查通过;1920×1080、100%待人工截图确认 |
| 仓库派送排车列表 → 维护车辆 | 提交审核入口合并 | 2026-10-21 用户确认:隐藏调整卸货顺序,提交审核移入车辆维护弹框,不需要备注 | 沿用原居中弹框;底部保存/提交审核,提交先保存本次资料及三类照片再按新版本送审;去掉维护和提交备注输入,保留审核驳回原因;详情保持只读 | 定向自动检查单独报告;1920×1080、100%待人工截图确认 |
@@ -448,6 +448,22 @@ it('clears an earlier input rejection after the corrected workflow succeeds', as
wrapper.unmount()
})
it.each([
['DELIVERING', 3, true],
['DELIVERED', 4, false],
] as const)('only opens list document management before delivery completes in %s', async (stage, tabIndex, available) => {
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'DELIVERY', stage, actions: {} })]))
const wrapper = mount(DispatchesView, { global: { ...options.global, stubs: { ...options.global.stubs, DispatchDocumentsDialog: true } } }); await flushPromises()
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[tabIndex]!.trigger('click'); await flushPromises()
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage }))
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
expect((row.props('actions') as Array<{ key: string }>).some(item => item.key === 'documents')).toBe(available)
expect(row.findAll('button').some(button => button.text() === '管理资料')).toBe(available)
row.vm.$emit('action', 'documents'); await flushPromises()
expect(wrapper.getComponent({ name: 'DispatchDocumentsDialog' }).props()).toMatchObject({ modelValue: available, dispatchNo: available ? 'PC/1' : '' })
wrapper.unmount()
})
it('opens the exception form from the dispatching row and hides it without permission', async () => {
const gate = { allowed: true, blockCode: null, blockReason: null }
const current = reviews()
@@ -122,7 +122,7 @@ function switchStage(value: DispatchStage) {
function switchSection(value: BeforeLoadingSection) { section.value = value; page.value = 1; void load() }
function openDetail(row: DispatchListRow) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
const rowActionOrder: DispatchAction[] = ['EDIT', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'MARK_EXCEPTION', 'CANCEL']
function canManageDocuments(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.status === 'ACTIVE' && ['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(row.stage) }
function canManageDocuments(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.status === 'ACTIVE' && ['LOADING', 'AFTER_LOADING', 'DELIVERING'].includes(row.stage) }
// 仅当前调度审核通过的仓库派送排车可下载,退回或审核失效后重新收起入口。
function canDownloadBol(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.reviews?.dispatchReview?.status === 'APPROVED' }
function rowActions(row: DispatchListRow): RowActionItem[] {
@@ -10,6 +10,8 @@ import { ApiResponseError } from '@/shared/api/http'
import DataTable from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import LayoutPreferenceList from '@/shared/components/LayoutPreferenceList.vue'
import { preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
import { listAllPlatformWarehouses } from '@/modules/warehouses'
import { listContainerArrivalTimes } from '@/modules/container-loading'
import {
@@ -322,10 +324,46 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(true)
})
it('uses the saved first three filters when collapsed and keeps reordered fields after remounting', async () => {
localStorage.setItem(preferenceStorageKey('query', 'anonymous', 'overseas-last-mile-orders-query'), JSON.stringify({
version: 1, order: ['orderNo', 'containerNo', 'inboundCode'], hidden: [],
}))
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const placeholders = () => toolbar.findAll('.query-toolbar__field').map(field => field.get('input').attributes('placeholder'))
expect(placeholders()).toEqual(['请输入尾程单号', '请输入集装箱号', '请输入入仓码'])
const initialCalls = vi.mocked(listLastMileOrders).mock.calls.length
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), containerNo: 'CONT-15' })
await wrapper.vm.$nextTick()
await button(wrapper, '高级筛选')!.trigger('click')
expect(placeholders()).toHaveLength(16)
expect(placeholders().slice(0, 3)).toEqual(['请输入尾程单号', '请输入集装箱号', '请输入入仓码'])
await toolbar.get('button[aria-label="调整筛选布局"]').trigger('click')
await flushPromises()
toolbar.findComponent(LayoutPreferenceList).vm.$emit('move', 'externalBookingNo', 0)
await wrapper.vm.$nextTick()
await button(wrapper, '收起筛选')!.trigger('click')
expect(placeholders()).toEqual(['请输入外接预订号', '请输入尾程单号', '请输入集装箱号'])
expect(toolbar.props('modelValue').containerNo).toBe('CONT-15')
expect(vi.mocked(listLastMileOrders)).toHaveBeenCalledTimes(initialCalls)
wrapper.unmount()
const restored = await mountView()
const restoredToolbar = restored.findComponent(QueryToolbar)
const restoredPlaceholders = () => restoredToolbar.findAll('.query-toolbar__field').map(field => field.get('input').attributes('placeholder'))
expect(restoredPlaceholders()).toEqual(['请输入外接预订号', '请输入尾程单号', '请输入集装箱号'])
await restoredToolbar.get('button[aria-label="调整筛选布局"]').trigger('click')
await flushPromises()
restoredToolbar.findComponent(LayoutPreferenceList).vm.$emit('reset')
await restored.vm.$nextTick()
expect(restoredPlaceholders()).toEqual(['请输入尾程单号', '请输入入仓码', '请输入仓库名称或编码'])
})
it('preserves advanced conditions when collapsed and clears them on reset', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
expect(toolbar.props('fields').map((field) => field.key)).toEqual(['orderNo', 'inboundCode', 'warehouse'])
expect(toolbar.findAll('.query-toolbar__field input').map(input => input.attributes('placeholder')))
.toEqual(['请输入尾程单号', '请输入入仓码', '请输入仓库名称或编码'])
const initialCalls = vi.mocked(listLastMileOrders).mock.calls.length
await button(wrapper, '高级筛选')!.trigger('click')
expect(toolbar.props('fields')).toHaveLength(16)
@@ -195,6 +195,9 @@ const statusTabs = ['全部', '开船前', '开船后'] as const
const showAdvancedFilters = ref(false)
const filters = ref<QueryModel>({ businessType: '', businessOrderNo: '', externalBookingNo: '', orderNo: '', relabelNo: '', orderType: '', warehouse: '', inboundCode: '', lxCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] })
const allFields: QueryField[] = [
{ key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' },
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
{ key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
{ key: 'businessType', type: 'select', label: '业务来源', width: 180, placeholder: '请选择业务来源', options: [
{ label: '本土散板(全部来源)', value: 'LOCAL_LOOSEBOARD' }, { label: '本土卡车', value: 'US_TRUCK' },
{ label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '头程订单', value: 'FIRST_LEG' },
@@ -202,9 +205,7 @@ const allFields: QueryField[] = [
] },
{ key: 'businessOrderNo', type: 'input', label: '关联商业单号', width: 210, placeholder: '请输入商业单号' },
{ key: 'externalBookingNo', type: 'input', label: '外接预订号', width: 210, placeholder: '请输入外接预订号' },
{ key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' },
{ key: 'relabelNo', type: 'input', label: '换标单号', width: 190, placeholder: '请输入换标单号' },
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
{ key: 'lxCode', type: 'input', label: 'LX代码', width: 180, placeholder: '请输入LX代码' },
{ key: 'orderType', type: 'select', label: '履约类型', width: 170, placeholder: '请选择履约类型', options: [
{ label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' },
@@ -212,7 +213,6 @@ const allFields: QueryField[] = [
{ label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' },
{ label: '货物废弃订单', value: 'CARGO_DISCARD' },
] },
{ key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', width: 190, placeholder: '请输入来源订单号' },
{ key: 'containerNo', type: 'input', label: '集装箱号', width: 180, placeholder: '请输入集装箱号' },
{ key: 'transferWarehouseCode', type: 'input', label: '转仓代码', width: 170, placeholder: '请输入转仓代码' },
@@ -234,9 +234,7 @@ const allFields: QueryField[] = [
const availableFields = computed(() => allFields.filter((field) => statusTab.value === '全部' || field.key !== 'orderType'))
const fields = computed<QueryField[]>(() => statusTab.value === '柜子'
? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key))
: showAdvancedFilters.value
? availableFields.value
: allFields.filter((field) => ['orderNo', 'inboundCode', 'warehouse'].includes(field.key)))
: availableFields.value)
const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value))
const selectedReceivingWarehouse = computed(() => receivingWarehouseOptions.value.find((row) => (
row.warehouseId === receivingWarehouseForm.warehouseId
@@ -1164,7 +1162,7 @@ onMounted(() => { void load() })
</template>
<template #filters>
<section class="last-mile-toolbar integrated-query-group" aria-label="尾程订单状态与筛选">
<QueryToolbar id="last-mile-orders-advanced-filters" v-model="filters" :fields="fields" :available-fields="availableFields" :loading="loading" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" :search-on-reset="false" @search="search" @reset="reset">
<QueryToolbar id="last-mile-orders-advanced-filters" v-model="filters" :fields="fields" :available-fields="availableFields" :max-visible="statusTab !== '柜子' && !showAdvancedFilters ? 3 : undefined" :loading="loading" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" :search-on-reset="false" @search="search" @reset="reset">
<template v-if="statusTab !== '柜子'" #actions>
<el-button :aria-expanded="showAdvancedFilters" aria-controls="last-mile-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}