优化
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
|
||||
| 关键词 | 页面/路由入口 | API 与联查 |
|
||||
|---|---|---|
|
||||
| 尾程订单 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | `modules/overseas-warehouse/api/lastMileOrders.ts`;Backend `lastleg-service/` |
|
||||
| 尾程订单与待分配整柜 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | 待分配页按 `sourceContainerBookingId` 消费服务端 `/orders/container-summaries` 柜级分页,不在浏览器拼柜;整柜生成调用 `/inbound-plans/convert-container`,每票返回独立计划且整批原子;API `modules/overseas-warehouse/api/lastMileOrders.ts`,Backend `lastleg-service/` |
|
||||
| 入仓单、入仓计划、FBA 预约 | `modules/overseas-warehouse/pages/InboundOrdersView.vue`、`InboundOrderDetailView.vue` | 列表/预约 `modules/overseas-warehouse/api/inboundOrders.ts`,计划列表/详情 `inboundPlans.ts`;Backend `lastleg-service/inboundplan/` |
|
||||
| 排车查询与智能预排 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`components/SmartDispatchDialog.vue` | `modules/overseas-warehouse/api/dispatches.ts`;Backend `lastleg-service/dispatch/`;覆盖详情、候选、草稿柜、托盘即时锁/解锁/整草稿释放和 `pre-arrange`,不据此开放出库编辑、取消、发车、PDA 出库或派送 |
|
||||
| 海外出库、派送、异常与回访(范围外现存代码) | `pages/OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue`、`ExceptionOrdersView.vue`、`CustomerVisitsView.vue` 及对应 `api/outbounds.ts`、`deliveryOrders.ts`、`exceptionOrders.ts`、`customerVisits.ts`;当前路由和导航已有注册 | 当前四期契约仍排除海外仓出库、派送及其派生运营页面;不得因代码、菜单、权限或 API 存在而纳入正式范围、验收或继续扩展,只有用户当前明确确认新范围后才能启用其业务语义 |
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
| 页面/能力 | Method + Path | Params / Body(当前 adapter) | 权限与承载 |
|
||||
|---|---|---|---|
|
||||
| 尾程列表/详情 | `GET /platform/v1/last-leg/orders`;`GET /orders/{id}` | list query:`page,size,orderNo,orderType,warehouse,inboundCode,sourceOrderNo,containerNo,currentStatus,transferWarehouseCode,abnormal,detained,createdFrom,createdTo`;平台跨客户,不传 `customerId`;货主只消费响应 `customerName`,空值显示 `—`;整柜操作要求列表响应携带稳定 `sourceContainerBookingId` | `platform:overseas-warehouse:last-mile-order:list`;真实分页、空态、错误及 request/trace 元数据;详情只读展示离仓关联事实 |
|
||||
| 尾程列表/详情 | `GET /platform/v1/last-leg/orders`;`GET /orders/{id}` | list query:`page,size,orderNo,orderType,warehouse,inboundCode,sourceOrderNo,containerNo,currentStatus,transferWarehouseCode,abnormal,detained,createdFrom,createdTo`;平台跨客户,不传 `customerId`;货主只消费响应 `customerName`,空值显示 `—` | `platform:overseas-warehouse:last-mile-order:list`;真实分页、空态、错误及 request/trace 元数据;详情只读展示离仓关联事实 |
|
||||
| 尾程转目的仓 | `PUT /platform/v1/last-leg/orders/destination` | body:`orderIds:string[],warehouseId,warehouseCode,warehouseName,remark?` | `...:last-mile-order:edit`;失败保留弹框,不提示成功 |
|
||||
| 尾程转入仓计划 | `POST /platform/v1/last-leg/inbound-plans/convert` | body:`orderId:string,customerId:string`,客户作用域精确取自所选尾程行;缺失时前端失败关闭 | `...:inbound-order:create`;A0404/A0409 保留 code/requestId/traceId;不接补托/PDA |
|
||||
| 尾程整柜转入仓计划 | `POST /platform/v1/last-leg/inbound-plans/convert-container` | body:`sourceContainerBookingId:string`;只调用一次柜级原子接口,不在前端循环逐票;返回一个计划数组,每个尾程订单/入仓码对应一个独立计划 | `...:inbound-order:create`;成功刷新待分配列表;计划数与整柜订单数不一致时不提示成功;错误保留 code/requestId/traceId |
|
||||
| 待分配整柜摘要分页 | `GET /platform/v1/last-leg/orders/container-summaries` | DTO:`sourceContainerBookingId,containerNo?,orderCount,inboundCodeCount,customerCount,totalPackageCount,totalPalletCount,totalVolumeCbm,eligible,ineligibleReason?`;query:`page,size,containerNo?,createdFrom?,createdTo?` | 服务端一柜一行并负责全量聚合;前端只消费摘要分页,不从订单分页本地聚合 |
|
||||
| 入库列表 | `GET /platform/v1/last-leg/inbound-plans` | query:`page,size,planNo,inboundCode,status,warehouse,fbaWarehouseCode,appointmentStatus,dispatchStatus,createdFrom,createdTo`;平台跨客户 | `...:inbound-order:list`;`lineId/planId/orderId/customerId` 均为字符串 |
|
||||
| 入库详情 | `GET /platform/v1/last-leg/inbound-plans/{planNo}` | query:显式 `customerId`;path 编码 | 同 list;缺客户范围时页面明确报错;`sourceOrderId` 字符串化 |
|
||||
| FBA 预约新增 | `POST /platform/v1/last-leg/inbound-plans/fba-appointments/batch` | body:`customerId,lineIds:string[],appointmentNo,appointmentAt,expectedLoadingAt,appointmentStatus,appointmentRemark?` | `...:inbound-order:fba-appointment`;只允许同客户、同 FBA 仓勾选 |
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, transferLastMileOrders } from './lastMileOrders'
|
||||
import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, listPendingContainerSummaries, transferLastMileOrders } from './lastMileOrders'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
describe('last-mile order overseas API', () => {
|
||||
beforeEach(() => vi.mocked(overseasHttp.get).mockReset())
|
||||
beforeEach(() => {
|
||||
vi.mocked(overseasHttp.get).mockReset()
|
||||
vi.mocked(overseasHttp.put).mockReset()
|
||||
vi.mocked(overseasHttp.post).mockReset()
|
||||
})
|
||||
|
||||
it('loads all platform orders without a customer scope', async () => {
|
||||
const record = { id: '190000000000000099', customerId: '190000000000000001', sourceContainerBookingId: '190000000000000088', customerName: '华远贸易', orderNo: 'LMO-260827-0001' }
|
||||
@@ -64,4 +68,25 @@ describe('last-mile order overseas API', () => {
|
||||
sourceContainerBookingId: '2092873463568597001',
|
||||
})
|
||||
})
|
||||
|
||||
it('loads server-side pending container summaries with pagination and filters', async () => {
|
||||
const record = {
|
||||
sourceContainerBookingId: '2092873463568597001', containerNo: 'CONT-15', orderCount: 15,
|
||||
inboundCodeCount: 15, customerCount: 3, totalPackageCount: 150, totalPalletCount: 30,
|
||||
totalVolumeCbm: 45.5, eligible: true,
|
||||
}
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: { records: [record], page: '2', size: '10', total: '21', pages: '3' },
|
||||
})
|
||||
await expect(listPendingContainerSummaries({
|
||||
page: 2, size: 10, containerNo: 'CONT-15',
|
||||
createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
|
||||
})).resolves.toEqual({ records: [record], page: 2, size: 10, total: 21, pages: 3 })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries', {
|
||||
params: {
|
||||
page: 2, size: 10, containerNo: 'CONT-15',
|
||||
createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -155,6 +155,27 @@ export interface ConvertInboundPlanPayload {
|
||||
customerId: string
|
||||
}
|
||||
|
||||
export interface PendingContainerSummaryQuery {
|
||||
page: number
|
||||
size: number
|
||||
containerNo?: string
|
||||
createdFrom?: string
|
||||
createdTo?: string
|
||||
}
|
||||
|
||||
export interface PendingContainerSummaryDto {
|
||||
sourceContainerBookingId: string
|
||||
containerNo: string | null
|
||||
orderCount: number
|
||||
inboundCodeCount: number
|
||||
customerCount: number
|
||||
totalPackageCount: number
|
||||
totalPalletCount: number
|
||||
totalVolumeCbm: number
|
||||
eligible: boolean
|
||||
ineligibleReason?: string | null
|
||||
}
|
||||
|
||||
export interface ConvertContainerInboundPlanPayload {
|
||||
sourceContainerBookingId: string
|
||||
}
|
||||
@@ -203,6 +224,14 @@ export async function convertLastMileOrderToInboundPlan(payload: ConvertInboundP
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listPendingContainerSummaries(query: PendingContainerSummaryQuery): Promise<OverseasPageResult<PendingContainerSummaryDto>> {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<PendingContainerSummaryDto>>, ApiEnvelope<OverseasPagePayload<PendingContainerSummaryDto>>>(
|
||||
'/platform/v1/last-leg/orders/container-summaries',
|
||||
{ params: query },
|
||||
)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<InboundPlanDetailDto[]>, ApiEnvelope<InboundPlanDetailDto[]>>(
|
||||
'/platform/v1/last-leg/inbound-plans/convert-container', payload,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
type DisplayTotal = number | '—'
|
||||
|
||||
export interface LastMileContainerPlanRow {
|
||||
id: string
|
||||
sourceContainerBookingId: string
|
||||
containerNo: string
|
||||
orderCount: number
|
||||
inboundCodeCount: number
|
||||
customerCount: number
|
||||
totalCartons: DisplayTotal
|
||||
totalPallets: DisplayTotal
|
||||
totalVolumeCbm: DisplayTotal
|
||||
eligibility: string
|
||||
planNotice: string
|
||||
eligible: boolean
|
||||
orderIds: string[]
|
||||
customerIds: string[]
|
||||
}
|
||||
@@ -2,14 +2,15 @@ import ElementPlus from 'element-plus'
|
||||
import { createPinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, listLastMileOrders } from '../api/lastMileOrders'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, listLastMileOrders, listPendingContainerSummaries } from '../api/lastMileOrders'
|
||||
import LastMileOrdersView from './LastMileOrdersView.vue'
|
||||
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
||||
vi.mock('../api/lastMileOrders', () => ({
|
||||
listLastMileOrders: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(),
|
||||
convertLastMileOrderToInboundPlan: vi.fn(), transferLastMileOrders: vi.fn(),
|
||||
listLastMileOrders: vi.fn(), listPendingContainerSummaries: vi.fn(),
|
||||
convertLastMileContainerToInboundPlans: vi.fn(), convertLastMileOrderToInboundPlan: vi.fn(), transferLastMileOrders: vi.fn(),
|
||||
}))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
||||
@@ -21,27 +22,96 @@ function memoryStorage(): Storage {
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
sourceContainerBookingId: '2092873463568597001', containerNo: 'CONT-15', orderCount: 15,
|
||||
inboundCodeCount: 15, customerCount: 3, totalPackageCount: 150, totalPalletCount: 30,
|
||||
totalVolumeCbm: 45.5, eligible: true, ineligibleReason: null,
|
||||
}
|
||||
function plans(count = 15) {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
id: String(index + 1), planNo: `LIP-${index + 1}`, customerId: String(index % 3 + 1), sourceOrderId: String(index + 1),
|
||||
containerNo: 'CONT-15', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 1, totalVolumeCbm: 1,
|
||||
lines: [], pallets: [], allocations: [],
|
||||
}))
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
afterAll(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('LastMileOrdersView container boundary', () => {
|
||||
async function openPending() {
|
||||
const wrapper = mount(LastMileOrdersView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
|
||||
await flushPromises()
|
||||
const pendingTab = wrapper.findAll('.el-radio-button').find((item) => item.text().includes('待分配'))
|
||||
await pendingTab!.trigger('click')
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('LastMileOrdersView server-side container summaries', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('localStorage', memoryStorage())
|
||||
vi.mocked(listLastMileOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockReset()
|
||||
vi.mocked(listPendingContainerSummaries).mockReset().mockResolvedValue({ records: [summary], page: 1, size: 10, total: 21, pages: 3 })
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue(plans())
|
||||
vi.mocked(convertLastMileOrderToInboundPlan).mockReset()
|
||||
})
|
||||
|
||||
it('fails closed instead of grouping the current order page as a whole container', async () => {
|
||||
const wrapper = mount(LastMileOrdersView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
|
||||
await flushPromises()
|
||||
const pendingTab = wrapper.findAll('.el-radio-button').find((item) => item.text().includes('待分配'))
|
||||
await pendingTab!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('已停止从订单分页本地聚合整柜')
|
||||
it('uses one server summary for a 15-order container that could span order pages', async () => {
|
||||
const wrapper = await openPending()
|
||||
expect(listPendingContainerSummaries).toHaveBeenCalledWith({ page: 1, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined })
|
||||
expect(wrapper.text()).toContain('CONT-15')
|
||||
expect(wrapper.text()).toContain('15')
|
||||
expect(wrapper.text()).toContain('将为 15 个尾程订单生成 15 个独立入库计划')
|
||||
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requests the next server summary page instead of deriving containers from order rows', async () => {
|
||||
const wrapper = await openPending()
|
||||
const nextPage = wrapper.find('button[aria-label="第 2 页"]')
|
||||
expect(nextPage.exists()).toBe(true)
|
||||
await nextPage.trigger('click')
|
||||
await flushPromises()
|
||||
expect(listPendingContainerSummaries).toHaveBeenLastCalledWith({
|
||||
page: 2, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined,
|
||||
})
|
||||
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls convert-container once, validates N plans and refreshes the server summary', async () => {
|
||||
const wrapper = await openPending()
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划'))
|
||||
await button!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: '2092873463568597001' })
|
||||
expect(convertLastMileOrderToInboundPlan).not.toHaveBeenCalled()
|
||||
expect(listPendingContainerSummaries).toHaveBeenCalledTimes(2)
|
||||
expect(document.body.textContent).toContain('已为15个尾程订单生成15个入库计划')
|
||||
})
|
||||
|
||||
it('does not report success or refresh when N returned plans do not match N orders', async () => {
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValueOnce(plans(14))
|
||||
const wrapper = await openPending()
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划'))
|
||||
await button!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
|
||||
expect(listPendingContainerSummaries).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('返回 14 个计划')
|
||||
expect(document.body.textContent).not.toContain('已为15个尾程订单生成14个入库计划')
|
||||
})
|
||||
|
||||
it.each(['A0404', 'A0409'])('keeps %s request evidence visible', async (code) => {
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError(
|
||||
'柜级生成失败', code, 200, undefined, `request-${code}`, `trace-${code}`,
|
||||
))
|
||||
const wrapper = await openPending()
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划'))
|
||||
await button!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain(code)
|
||||
expect(wrapper.text()).toContain(`request-${code}`)
|
||||
expect(wrapper.text()).toContain(`trace-${code}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,12 +12,11 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { convertLastMileContainerToInboundPlans, listLastMileOrders, transferLastMileOrders } from '../api/lastMileOrders'
|
||||
import { convertLastMileContainerToInboundPlans, listLastMileOrders, listPendingContainerSummaries, transferLastMileOrders, type PendingContainerSummaryDto } from '../api/lastMileOrders'
|
||||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { toLastMileOrderListItem } from '../lastMilePresentation'
|
||||
import type { LastMileContainerPlanRow } from '../lastMileContainerPlanning'
|
||||
import type { LastMileOrderListItem } from '../types'
|
||||
|
||||
const rows = ref<LastMileOrderListItem[]>([])
|
||||
@@ -28,15 +27,16 @@ const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const containerTotal = ref(0)
|
||||
const selectedRows = ref<LastMileOrderListItem[]>([])
|
||||
const containerRows = ref<LastMileContainerPlanRow[]>([])
|
||||
const containerRows = ref<PendingContainerSummaryDto[]>([])
|
||||
const actionLoading = ref(false)
|
||||
const actionError = ref<ApiErrorDetails | null>(null)
|
||||
const transferOpen = ref(false)
|
||||
const transferForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', remark: '' })
|
||||
const statusTab = ref<'全部' | '待分配'>('全部')
|
||||
const filters = ref<QueryModel>({ orderNo: '', orderType: '', warehouse: '', inboundCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
const allFields: QueryField[] = [
|
||||
{ key: 'orderNo', type: 'input', label: '尾程单号', placeholder: '请输入尾程单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' },
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', placeholder: '全部类型', options: [
|
||||
@@ -59,7 +59,10 @@ const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'abnormal', type: 'select', label: '是否异常', placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'detained', type: 'select', label: '是否暂扣', placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'createdAt', type: 'daterange', label: '创建时间' },
|
||||
])
|
||||
]
|
||||
const fields = computed(() => statusTab.value === '待分配'
|
||||
? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key))
|
||||
: allFields)
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||||
@@ -101,11 +104,11 @@ const containerColumns: DataTableColumn[] = [
|
||||
{ key: 'orderCount', prop: 'orderCount', label: '订单数', width: 90, align: 'right' },
|
||||
{ key: 'inboundCodeCount', prop: 'inboundCodeCount', label: '入仓码数', width: 100, align: 'right' },
|
||||
{ key: 'customerCount', prop: 'customerCount', label: '客户数', width: 90, align: 'right' },
|
||||
{ key: 'totalCartons', prop: 'totalCartons', label: '总箱数', width: 100, align: 'right' },
|
||||
{ key: 'totalPallets', prop: 'totalPallets', label: '总托数', width: 100, align: 'right' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 100, align: 'right' },
|
||||
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', width: 100, align: 'right' },
|
||||
{ key: 'totalVolumeCbm', prop: 'totalVolumeCbm', label: '总体积(m³)', width: 120, align: 'right' },
|
||||
{ key: 'eligibility', prop: 'eligibility', label: '资格', minWidth: 160, slotName: 'eligibility' },
|
||||
{ key: 'planNotice', prop: 'planNotice', label: '生成说明', minWidth: 320 },
|
||||
{ key: 'planNotice', label: '生成说明', minWidth: 320, slotName: 'planNotice' },
|
||||
]
|
||||
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
@@ -114,10 +117,24 @@ async function load(model: QueryModel = filters.value) {
|
||||
loadError.value = null
|
||||
if (statusTab.value === '待分配') {
|
||||
rows.value = []
|
||||
containerRows.value = []
|
||||
total.value = 0
|
||||
loadError.value = { message: '柜级待分配摘要分页接口尚未在当前 test 后端提供,已停止从订单分页本地聚合整柜' }
|
||||
loading.value = false
|
||||
const dateRange = Array.isArray(filters.value.createdAt) ? filters.value.createdAt.map(String) : []
|
||||
try {
|
||||
const result = await listPendingContainerSummaries({
|
||||
page: page.value,
|
||||
size: pageSize.value,
|
||||
containerNo: String(filters.value.containerNo || '').trim() || undefined,
|
||||
createdFrom: dateRange[0] ? `${dateRange[0]}T00:00:00` : undefined,
|
||||
createdTo: dateRange[1] ? `${dateRange[1]}T23:59:59` : undefined,
|
||||
})
|
||||
containerRows.value = result.records
|
||||
containerTotal.value = result.total
|
||||
} catch (error) {
|
||||
containerRows.value = []
|
||||
containerTotal.value = 0
|
||||
loadError.value = getApiErrorDetails(error, '柜级待分配摘要加载失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -174,9 +191,9 @@ async function submitTransfer() {
|
||||
} catch (error) { actionError.value = getApiErrorDetails(error, '转仓失败,请重试') }
|
||||
finally { actionLoading.value = false }
|
||||
}
|
||||
async function generateContainerInboundPlans(row: LastMileContainerPlanRow) {
|
||||
async function generateContainerInboundPlans(row: PendingContainerSummaryDto) {
|
||||
if (!row.eligible) {
|
||||
actionError.value = { message: row.eligibility }
|
||||
actionError.value = { message: row.ineligibleReason || '当前整柜不满足生成资格' }
|
||||
return
|
||||
}
|
||||
if (!row.sourceContainerBookingId) {
|
||||
@@ -229,13 +246,15 @@ onMounted(() => { void load() })
|
||||
</LoadErrorState>
|
||||
<section v-if="!loadError && statusTab === '待分配'" class="surface-panel table-panel container-planning-panel">
|
||||
<el-alert title="选择一行即代表整柜;一次调用将为柜内每个尾程订单/入仓码分别生成独立入库计划,海外 PDA 仍按同一柜号作业。" type="info" :closable="false" show-icon />
|
||||
<DataTable :data="containerRows" :columns="containerColumns" :loading="loading" preference-key="overseas-last-mile-container-plans" row-key="id" empty-text="暂无待分配整柜">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="containerRows" :columns="containerColumns" :loading="loading" :total="containerTotal" preference-key="overseas-last-mile-container-plans" row-key="sourceContainerBookingId" empty-text="暂无待分配整柜" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #actions="{ row }">
|
||||
<el-tooltip content="按整柜原子生成入库计划" placement="top">
|
||||
<span><el-button v-if="access.can(permission.inboundOrderCreate)" size="small" type="primary" plain :disabled="!(row as LastMileContainerPlanRow).eligible || actionLoading" @click="generateContainerInboundPlans(row as LastMileContainerPlanRow)">生成整柜入库计划</el-button></span>
|
||||
<span><el-button v-if="access.can(permission.inboundOrderCreate)" size="small" type="primary" plain :disabled="!(row as PendingContainerSummaryDto).eligible || actionLoading" @click="generateContainerInboundPlans(row as PendingContainerSummaryDto)">生成整柜入库计划</el-button></span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #eligibility="{ row }"><el-tag :type="(row as LastMileContainerPlanRow).eligible ? 'success' : 'danger'" effect="plain">{{ (row as LastMileContainerPlanRow).eligibility }}</el-tag></template>
|
||||
<template #eligibility="{ row }"><el-tag :type="(row as PendingContainerSummaryDto).eligible ? 'success' : 'danger'" effect="plain">{{ (row as PendingContainerSummaryDto).eligible ? '可生成' : (row as PendingContainerSummaryDto).ineligibleReason || '不可生成' }}</el-tag></template>
|
||||
<template #planNotice="{ row }">将为 {{ (row as PendingContainerSummaryDto).orderCount }} 个尾程订单生成 {{ (row as PendingContainerSummaryDto).orderCount }} 个独立入库计划,PDA仍按同柜作业</template>
|
||||
<template #pagination-left>共 {{ containerTotal }} 个待分配整柜</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
<section v-else-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-last-mile-orders" row-key="id" empty-text="暂无尾程订单" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
|
||||
Reference in New Issue
Block a user