优化
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Admin 海外仓模块指引
|
||||
|
||||
- 排车装车后车辆信息分区的路线地图,通过`../overseas-master-data/index.ts`公开组件按起运仓ID和目的仓精确仓码查询完整地址后展示规划路线;多目的仓独立选择,不推配送顺序。原型及字段边界见`../overseas-master-data/PROTOTYPE_MATRIX.md`“运输路线地图”。
|
||||
|
||||
## 样式入口
|
||||
|
||||
- 导航“海外仓”的 10 个菜单及关联详情/弹框采用公共密度样式,逐操作范围见 [海外仓样式核对记录](../../../../../docs/Admin海外仓样式核对记录.md)。不要扩展到客户服务、海外业务或未注册产品/箱号页面。
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
| 尾程详情 | 查看关联单据 | 本轮用户截图2及已确认方案 | 保留独立详情、基本信息页签;增加紧凑关联单据区 | 待人工截图确认 |
|
||||
| 本土散板/卡车列表 | 来源商业单跳转 | 本轮已确认方案、现有承运安排入口 | 有列表权限才跳转既有列表,携带商业单号筛选;池来源保留1080px承运安排弹框 | 自动检查与人工确认分别报告 |
|
||||
|
||||
| 页面 | 操作 | 依据 | 当前实现与结论 | 验收 |
|
||||
|---|---|---|---|---|
|
||||
| 排车列表 | 阶段切换与审核页签 | 本轮用户截图 Image #1 与“只有装车前”显示审核的明确要求 | 原先各阶段均显示审核页签;调整为仅装车前显示调度审核、理货员审核,其他阶段仅车辆信息,离开装车前自动回到车辆信息;沿用现有页签布局和车次详情跳转 | 阶段切换与审核跳转自动回归;1920×1080、100% 缩放待人工截图确认 |
|
||||
|
||||
排车装车后车辆信息分区增加路线地图列,使用公开`TransportRouteDialog`。原型、字段来源和多目的仓选择见[运输路线地图](../overseas-master-data/PROTOTYPE_MATRIX.md#运输路线地图)。其他阶段不新增路线列,不逐行请求地图。
|
||||
|
||||
### F33 已有库存转运履约接入(2026-10-04)
|
||||
|
||||
| 页面 | 操作 | 依据 | 承载方式与本轮结论 | 验收 |
|
||||
|
||||
@@ -9,6 +9,10 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { cancelDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatches, removeDispatchItem, transferDispatchToDelivery, updateDispatch } from '../api/dispatches'
|
||||
import DeliveryFeeCoveragePanel from '../components/DeliveryFeeCoveragePanel.vue'
|
||||
import DispatchesView from './DispatchesView.vue'
|
||||
vi.mock('@/modules/warehouses', () => ({
|
||||
listAllPlatformWarehouses: vi.fn(async (query: { keyword: string }) => [{ id: query.keyword, warehouseCode: query.keyword, warehouseType: 'FBA' }]),
|
||||
getPlatformWarehouse: vi.fn(async (id: string) => ({ warehouse: { warehouseCode: id, warehouseType: id === '321' ? 'OVERSEAS' : 'FBA' }, addresses: [{ addressType: 'OPERATING', addressLine1: '100 Example Street', city: 'City', stateProvince: 'CA', countryCode: 'US' }], capabilities: [] })),
|
||||
}))
|
||||
|
||||
const push = vi.hoisted(() => vi.fn())
|
||||
vi.mock('vue-router', () => ({ useRouter: () => ({ push }) }))
|
||||
@@ -44,8 +48,8 @@ describe('DispatchesView', () => {
|
||||
vi.mocked(listDispatches).mockReset().mockResolvedValue({
|
||||
records: [{
|
||||
id: '190000000000000099', dispatchNo: 'PC-1', vehiclePlate: 'CA-100', driverName: '张师傅',
|
||||
driverPhone: '555-0100', carrierName: 'Carrier A', orderNos: 'LM-1,LM-2', route: 'LAX -> ONT8',
|
||||
overseasWarehouseName: '洛杉矶仓', departAt: '2026-08-20T09:00:00', fbaAppointmentNo: 'FBA-1',
|
||||
driverPhone: '555-0100', carrierName: 'Carrier A', orderNos: 'LM-1,LM-2', route: 'ONT8,LAX9',
|
||||
overseasWarehouseId: '321', overseasWarehouseName: '洛杉矶仓', departAt: '2026-08-20T09:00:00', fbaAppointmentNo: 'FBA-1',
|
||||
fbaAppointmentStatus: 'CONFIRMED', vehicleAppointmentStatus: 'BOOKED', estimatedArrivalAt: '2026-08-20T18:00:00',
|
||||
status: 'PENDING_DEPARTURE', palletCount: 12, packageCount: 80, createdAt: '2026-08-19T16:00:00',
|
||||
}],
|
||||
@@ -78,6 +82,25 @@ describe('DispatchesView', () => {
|
||||
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage: 'AFTER_LOADING', page: 1 }))
|
||||
})
|
||||
|
||||
it('shows the shared route map only in the after-loading vehicle section without another query', async () => {
|
||||
const wrapper = mount(DispatchesView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('查看路线')
|
||||
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[3]!.trigger('click')
|
||||
await flushPromises()
|
||||
const requests = vi.mocked(listDispatches).mock.calls.length
|
||||
await wrapper.findAll('button').find(button => button.text() === '查看路线')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('modelValue')).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('origin')).toEqual({ kind: 'warehouse', label: '洛杉矶仓', warehouseId: '321' })
|
||||
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('destinations')).toEqual(['ONT8', 'LAX9'].map(code => ({ kind: 'warehouse', label: code, warehouseCode: code })))
|
||||
expect(listDispatches).toHaveBeenCalledTimes(requests)
|
||||
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[1]!.trigger('click')
|
||||
await flushPromises()
|
||||
await wrapper.get('[aria-label="排车业务分区"]').findAll('button')[1]!.trigger('click')
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '查看路线')).toBe(false)
|
||||
})
|
||||
|
||||
it('shows review facts and opens the matching trip section with read permission', async () => {
|
||||
const result = await listDispatches({ page: 1, size: 10 })
|
||||
result.records[0]!.workflow = { tripNo: 'TR-A', dispatchReview: { status: 'REJECTED', reason: '请核对货量', reviewedAt: null }, loadingReview: { status: 'PENDING', reason: null, reviewedAt: null } }
|
||||
@@ -86,6 +109,9 @@ describe('DispatchesView', () => {
|
||||
const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const sections = wrapper.get('[aria-label="排车业务分区"]')
|
||||
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息'])
|
||||
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[1]!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息', '调度审核', '理货员审核'])
|
||||
expect(wrapper.findAll('[role="tablist"]')).toHaveLength(2)
|
||||
expect(wrapper.text()).not.toContain('请核对货量')
|
||||
@@ -103,7 +129,28 @@ describe('DispatchesView', () => {
|
||||
await links[0]!.trigger('click')
|
||||
expect(push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: 'TR-A' }, query: { tab: 'loading' } })
|
||||
expect(wrapper.text()).not.toContain('前往审核')
|
||||
expect(listDispatches).toHaveBeenCalledTimes(1)
|
||||
expect(listDispatches).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.each([0, 2, 3, 4].flatMap(stageIndex => [1, 2].map(sectionIndex => [stageIndex, sectionIndex])))('leaving a review section for stage %s resets section %s to vehicle information', async (stageIndex, sectionIndex) => {
|
||||
const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const stages = wrapper.get('[aria-label="排车执行阶段"]').findAll('button')
|
||||
const sections = wrapper.get('[aria-label="排车业务分区"]')
|
||||
await stages[1]!.trigger('click')
|
||||
await flushPromises()
|
||||
await sections.findAll('button')[sectionIndex]!.trigger('click')
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '打印BOL')).toBe(false)
|
||||
await stages[stageIndex]!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息'])
|
||||
expect(sections.get('button').attributes('aria-selected')).toBe('true')
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '打印BOL')).toBe(true)
|
||||
expect(wrapper.findAll('button').some(button => ['前往审核', '查看审核'].includes(button.text()))).toBe(false)
|
||||
await stages[1]!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息', '调度审核', '理货员审核'])
|
||||
expect(sections.get('button').attributes('aria-selected')).toBe('true')
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { TransportRouteDialog, routeLocation, type RouteLocation } from '@/modules/overseas-master-data'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -24,6 +25,14 @@ import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { dispatchVehicleSizeOptions } from '../dispatchVehicleSpecs'
|
||||
|
||||
const router = useRouter()
|
||||
const routeOpen = ref(false)
|
||||
const routeOrigin = ref<RouteLocation>(routeLocation('')), routeDestinations = ref<RouteLocation[]>([])
|
||||
function openRoute(row: DispatchListItem) {
|
||||
routeOrigin.value = { kind: 'warehouse', label: row.overseasWarehouse, warehouseId: dispatchSnapshots.get(row.dispatchNo)?.overseasWarehouseId || undefined }
|
||||
// The service's route field is a comma-separated set of destination warehouse codes, not a stop sequence.
|
||||
routeDestinations.value = [...new Set(row.route.split(',').map(code => code.trim()).filter(Boolean))].map(code => ({ kind: 'warehouse', label: code, warehouseCode: code }))
|
||||
routeOpen.value = true
|
||||
}
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<DispatchListItem[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -43,11 +52,13 @@ const sections = [
|
||||
{ value: 'dispatchReview', label: '调度审核' },
|
||||
{ value: 'loadingReview', label: '理货员审核' },
|
||||
] as const
|
||||
const visibleSections = computed(() => stage.value === 'BEFORE_LOADING' ? sections : sections.filter(item => item.value === 'vehicle'))
|
||||
const reviewLabels = { PENDING: '待审核', APPROVED: '已通过', REJECTED: '已打回' }
|
||||
let loadSequence = 0
|
||||
function switchStage(value: typeof stage.value) {
|
||||
if (stage.value === value) return
|
||||
stage.value = value
|
||||
if (value !== 'BEFORE_LOADING') section.value = 'vehicle'
|
||||
filters.value.status = ''
|
||||
page.value = 1
|
||||
void load()
|
||||
@@ -257,7 +268,12 @@ const columns: DataTableColumn[] = [
|
||||
]
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
if (section.value === 'vehicle') return columns.filter(column => !['dispatchReview', 'loadingReview'].includes(String(column.key)))
|
||||
if (section.value === 'vehicle') {
|
||||
const visible = columns.filter(column => !['dispatchReview', 'loadingReview'].includes(String(column.key)))
|
||||
if (stage.value === 'AFTER_LOADING') visible.splice(visible.findIndex(column => column.key === 'status'), 0,
|
||||
{ key: 'routeMap', label: '路线地图', width: 120, slotName: 'routeMap' })
|
||||
return visible
|
||||
}
|
||||
const keys = ['actions', 'dispatchNo', 'workflow', section.value, 'vehiclePlate', 'driverName', 'route', 'palletCount', 'packageCount', 'status']
|
||||
return keys.map(key => columns.find(column => column.key === key)!)
|
||||
})
|
||||
@@ -644,7 +660,7 @@ onMounted(load)
|
||||
<button v-for="item in stages" :key="item.value" role="tab" :aria-selected="stage === item.value" :class="{ 'is-active': stage === item.value }" @click="switchStage(item.value)">{{ item.label }}</button>
|
||||
</div>
|
||||
<div class="density-status-tabs" role="tablist" aria-label="排车业务分区">
|
||||
<button v-for="item in sections" :key="item.value" role="tab" :aria-selected="section === item.value" :class="{ 'is-active': section === item.value }" @click="section = item.value">{{ item.label }}</button>
|
||||
<button v-for="item in visibleSections" :key="item.value" role="tab" :aria-selected="section === item.value" :class="{ 'is-active': section === item.value }" @click="section = item.value">{{ item.label }}</button>
|
||||
</div>
|
||||
<QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="排车列表筛选" preference-key="overseas-dispatches-query" :search-on-reset="false" @search="search" @reset="reset" /></div></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="排车列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
@@ -662,8 +678,10 @@ onMounted(load)
|
||||
<span v-else>未关联车次</span>
|
||||
</template>
|
||||
<template #status="{ row }"><StatusBadge :status="String((row as DispatchListItem).status || '—')" /></template>
|
||||
<template #routeMap="{ row }"><el-button v-if="row.dispatchNo" link type="primary" @click="openRoute(row as DispatchListItem)">查看路线</el-button></template>
|
||||
<template #pagination-left>共 {{ total }} 条排车记录</template>
|
||||
</DataTable>
|
||||
<TransportRouteDialog v-model="routeOpen" :origin="routeOrigin" :destinations="routeDestinations" />
|
||||
</ListPageLayout></AdminPage><AppDialog class="admin-density-dialog" v-model="transferOpen" title="转派送" width="min(1120px, calc(100vw - 28px))" confirm-text="确认提交" :confirm-loading="transferSaving" :confirm-disabled="freightLoading || !externalFreight" @confirm="submitTransfer"><el-form class="density-form" label-position="top"><h3>派送信息</h3><div class="transfer-grid"><el-form-item label="排车单号"><el-input :model-value="transferRow?.dispatchNo" readonly /></el-form-item><el-form-item label="预计派送时间" required><el-date-picker v-model="transferForm.plannedDeliveryAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计派送时间" /></el-form-item><el-form-item label="派送方式" required><el-select v-model="transferForm.deliveryMethod" placeholder="请选择"><el-option v-for="option in deliveryMethodOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="快递/车队"><el-input v-model="transferForm.carrierName" placeholder="请输入快递或车队名称" /></el-form-item><el-form-item label="运单号"><el-input v-model="transferForm.waybillNo" placeholder="请输入运单号" /></el-form-item><el-form-item label="提货方式" required><el-select v-model="transferForm.pickupMethod" placeholder="请选择"><el-option v-for="option in pickupMethodOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="装车类型" required><el-select v-model="transferForm.loadingType" placeholder="请选择"><el-option v-for="option in loadingTypeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="司机/车队名称"><el-input v-model="transferForm.driverName" placeholder="请输入司机或车队名称" /></el-form-item><el-form-item label="联系电话"><el-input v-model="transferForm.driverPhone" placeholder="请输入联系电话" /></el-form-item><el-form-item label="车牌号"><el-input v-model="transferForm.vehiclePlate" placeholder="请输入车牌号" /></el-form-item><el-form-item label="车辆类型"><el-select v-model="transferForm.vehicleType" clearable placeholder="请选择"><el-option v-for="option in vehicleTypeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="车辆大小"><el-select v-model="transferForm.vehicleSize" clearable placeholder="请选择"><el-option v-for="option in vehicleSizeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="预约账号"><el-select v-model="transferForm.appointmentAccount" clearable placeholder="请选择"><el-option v-for="option in appointmentAccountOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item v-if="isBuyAppointment" label="卖家姓名" required><el-input v-model="transferForm.sellerName" placeholder="请输入卖家姓名" /></el-form-item></div><DeliveryFeeCoveragePanel v-model="coverageSelection" :preview="coveragePreview" :loading="freightLoading" :disabled="transferSaving || freightLoading" /><p v-if="coveredServices.length">本次全部货物已覆盖:{{ coveredServices.map(service => coveredServiceLabels[service]).join('、') }},无需重复登记对应应收。</p><h3>本次新增费用</h3><p v-if="freightLoading">正在读取约定运费…</p><p v-for="quote in externalFreight?.quotes || []" :key="quote.bookingNo">{{ quote.bookingNo }} · {{ quote.incomingNo }}:{{ quote.quantity }} {{ quote.quantityUnit }} × {{ quote.currency }} {{ quote.unitPrice }} = {{ quote.currency }} {{ quote.amount }}(外接约定运费)</p><div class="transfer-grid"><el-form-item v-if="requiresDeliveryFee" :label="externalFreight?.quotes.length || coverageSelection.length ? '派送费(未覆盖部分,USD)' : '派送费用(USD)'" required><el-input-number v-model="transferForm.deliveryFee" :min="0" :precision="2" controls-position="right" placeholder="请输入派送费用" /></el-form-item><el-form-item v-if="requiresLoadingFee" label="装车费(USD)" required><el-input-number v-model="transferForm.loadingFee" :min="0" :precision="2" controls-position="right" placeholder="请输入装车费" /></el-form-item><el-form-item v-if="requiresAppointmentFee" label="买约费用(USD)" required><el-input-number v-model="transferForm.appointmentFee" :min="0" :precision="2" controls-position="right" placeholder="请输入买约费用" /></el-form-item></div><el-form-item label="备注"><el-input v-model="transferForm.remark" type="textarea" :rows="3" placeholder="请输入派送备注(选填)" /></el-form-item><el-alert title="原运费仅按上方确认的服务范围引用;外接运费按实收约定带入。下方只登记未覆盖货物或服务的新增应收。" type="info" :closable="false" /></el-form></AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="editOpen" title="编辑排车" width="min(900px, calc(100vw - 48px))" confirm-text="保存" :confirm-loading="editSaving" @confirm="submitEdit"><el-alert :title="`当前状态:${editRow?.status || '—'};普通编辑不会修改排车状态;未生成出库单前可移除托盘。`" type="info" :closable="false" /><el-form class="density-form" label-position="top"><div class="edit-grid"><el-form-item label="车牌号"><el-input v-model="editForm.vehiclePlate" placeholder="请输入车牌号" /></el-form-item><el-form-item label="司机"><el-input v-model="editForm.driverName" placeholder="请输入司机姓名" /></el-form-item><el-form-item label="联系电话"><el-input v-model="editForm.driverPhone" placeholder="请输入联系电话" /></el-form-item><el-form-item label="拖车公司名称"><el-input v-model="editForm.carrierName" placeholder="请输入拖车公司名称" /></el-form-item><el-form-item label="路线"><el-input v-model="editForm.route" readonly placeholder="按已排托盘自动汇总 FBA 仓" /></el-form-item><el-form-item label="发车时间"><el-date-picker v-model="editForm.departAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="预计到达时间"><el-date-picker v-model="editForm.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="FBA预约号"><el-input v-model="editForm.fbaAppointmentNo" placeholder="请输入 FBA 预约号" /></el-form-item><el-form-item label="FBA预约状态"><el-select v-model="editForm.fbaAppointmentStatus" clearable><el-option label="待预约" value="PENDING" /><el-option label="已预约" value="BOOKED" /><el-option label="已确认" value="CONFIRMED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item><el-form-item label="车辆预约状态"><el-select v-model="editForm.vehicleAppointmentStatus" clearable><el-option label="待预约" value="PENDING" /><el-option label="已预约" value="BOOKED" /><el-option label="已确认" value="CONFIRMED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item></div></el-form><h3>已排车托盘</h3><el-table :data="editItems" border max-height="280" empty-text="暂无托盘"><el-table-column label="操作" width="90" fixed="left"><template #default="{ row }"><el-button class="density-action" data-tone="danger" :loading="removingItemId === String(row.id)" :disabled="Boolean(removingItemId)" @click="removePallet(row)">移除</el-button></template></el-table-column><el-table-column prop="palletNo" label="托盘号" min-width="160" /><el-table-column prop="orderNo" label="订单号" min-width="160" /><el-table-column prop="inboundCode" label="入仓码" min-width="140" /><el-table-column prop="packageCount" label="箱数" width="80" align="right" /></el-table></AppDialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user