diff --git a/AGENTS.md b/AGENTS.md index 268716c4..c9202c2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,7 @@ | 关键词 | 页面/路由入口 | API 与联查 | |---|---|---| -| 尾程订单与柜级入仓计划进度 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | 按 `sourceContainerBookingId` 消费 `/orders/container-summaries` 的 `allPlansReady`、计划统计与冻结 `plannedArrivalAt`,详情由服务端返回全柜订单且不受列表分页截断;不得在浏览器拼柜或自行推导就绪。整柜生成调用 `/inbound-plans/convert-container`,每票返回独立计划且整批原子;转仓候选只取 `modules/iam/api/platformIam.ts` 的平台公共 `ACTIVE + FBA` 仓主档,并提交 ID/编码/名称快照。业务 API `modules/overseas-warehouse/api/lastMileOrders.ts`,Backend `lastleg-service/` | +| 尾程订单与柜级入仓计划进度 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | 柜子工作台按 `sourceContainerBookingId` 消费 `/orders/container-summaries`,服务端在分页前排除 `allPlansReady=true`,按冻结 `plannedArrivalAt` 升序、空值最后返回待处理柜;卡片只展示服务端计划统计,详情弹框由服务端返回全柜订单且不受列表分页截断,不得在浏览器拼柜或自行推导就绪。整柜生成调用 `/inbound-plans/convert-container`,每票返回独立计划且整批原子;转仓候选只取 `modules/iam/api/platformIam.ts` 的平台公共 `ACTIVE + FBA` 仓主档,并提交 ID/编码/名称快照。业务 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 存在而纳入正式范围、验收或继续扩展,只有用户当前明确确认新范围后才能启用其业务语义 | diff --git a/src/modules/inbounds/components/InboundTaskList.test.ts b/src/modules/inbounds/components/InboundTaskList.test.ts index cc4b043d..b5dc5b9c 100644 --- a/src/modules/inbounds/components/InboundTaskList.test.ts +++ b/src/modules/inbounds/components/InboundTaskList.test.ts @@ -108,6 +108,14 @@ describe('InboundTaskList measurement scope', () => { expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }") }) + it('keeps relationship data entry out of measurement and review task lists', () => { + const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') + + expect(source).not.toContain('关联数据') + expect(source).not.toContain('InboundCodeRelationshipDialog') + expect(source).not.toContain('openRelationship') + }) + it('closes only after navigating an approved review to the measurement tab', () => { const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') const handler = source.slice(source.indexOf('async function handleTaskChanged'), source.indexOf('function handleReviewSelectionChange')) @@ -157,6 +165,14 @@ describe('InboundTaskList measurement scope', () => { expect(source).toContain("if (!value) return '—'") }) + it('renders first-leg business types with the shared Chinese labels', () => { + const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') + + expect(source).toContain("import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels'") + expect(source).toContain('firstLegBusinessTypeLabel((row as InboundOrderSummary).businessType)') + expect(source).not.toContain('text((row as InboundOrderSummary).businessType)') + }) + it('refreshes cached task lists when users return from measurement work', () => { const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index 4d71147f..f4ff9ef8 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -2,7 +2,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { CircleCheckBig, Eye, Link2, Ruler, SearchCheck } from '@lucide/vue' +import { CircleCheckBig, Eye, Ruler, SearchCheck } from '@lucide/vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listPlatformWarehouses } from '@/modules/warehouses' @@ -12,8 +12,8 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' -import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' +import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels' import { listPlatformInbounds } from '../core/api' import type { InboundOrderSummary } from '../core/types' import InboundMeasurementDialog from '../measurement/InboundMeasurementDialog.vue' @@ -48,8 +48,6 @@ const reviewDialogOpen = ref(false) const reviewNavigationLoading = ref(false) const reviewApproved = ref(false) const reviewNavigationError = ref(null) -const relationshipDialogOpen = ref(false) -const relationshipDialogInboundCode = ref('') const batchReviewLoading = ref(false) const selectedReviewRows = ref([]) let requestSerial = 0 @@ -200,13 +198,6 @@ function openDetail(row: InboundOrderSummary) { void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } }) } -function openRelationship(row: InboundOrderSummary) { - const code = row.inboundCode?.trim() - if (!code) return - relationshipDialogInboundCode.value = code - relationshipDialogOpen.value = true -} - async function handleTaskChanged() { const target = selectedInbound.value if (!isReview.value) { @@ -386,14 +377,14 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria 无操作权限 - + - + @@ -447,7 +438,6 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria @submitted="handleMeasurementSubmitted" @closed="clearSelectedInbound" /> - @@ -460,7 +450,6 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria .primary-cell { display: grid; gap: 3px; min-width: 0; } .primary-cell strong, .primary-cell span { overflow-wrap: anywhere; } .inbound-code-cell { display: grid; gap: 3px; min-width: 0; } -.inbound-code-cell .el-button { width: fit-content; margin: 0; padding: 0; } .primary-cell span, .muted-action { color: var(--cc-muted); font-size: 12px; } @media (max-width: 520px) { .row-actions { align-items: stretch; flex-direction: column; } diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts index 8dabfd79..ddfc0a96 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts @@ -118,6 +118,9 @@ describe('OceanBookingDetailView draft and release contract', () => { it('keeps a DRAFT detail read-only when the user lacks write permission', () => { expect(source).toContain('const formWritable = computed(() => canSave.value)') expect(source).toContain(':disabled="!formWritable" class="booking-groups"') + expect(source).toContain('.booking-groups{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);gap:14px}') + expect(source).toContain('.booking-group:last-child{grid-column:1/-1}') + expect(source).toContain('.container-table-wrap{max-width:100%;overflow-x:auto') expect(source).toContain('v-if="formWritable" class="container-actions"') expect(source).toContain(':disabled="!formWritable"') }) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index 8af887f5..87fceebf 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -494,5 +494,5 @@ onMounted(() => { void load(); void loadOptions() }) diff --git a/src/modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue b/src/modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue index db886420..56f10aae 100644 --- a/src/modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue +++ b/src/modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue @@ -49,9 +49,10 @@ defineEmits<{ back: [] }>() .detail-group { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); } .detail-group:nth-child(even) { border-right: 0; } .detail-group h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; } -.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); } +.detail-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); } .detail-fields > div { display: grid; min-width: 0; min-height: 46px; grid-template-columns: 126px minmax(0, 1fr); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); } .detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 8px 10px; } .detail-fields dt { background: #f8fafc; color: var(--cc-muted); font-size: 12px; } .detail-fields dd { margin: 0; color: var(--cc-text); overflow-wrap: anywhere; } +@media (max-width: 1100px) { .read-only-detail, .detail-fields { grid-template-columns: 1fr; } .detail-group { border-right: 0; } } diff --git a/src/modules/overseas-warehouse/layoutAcceptance.test.ts b/src/modules/overseas-warehouse/layoutAcceptance.test.ts new file mode 100644 index 00000000..b3e40871 --- /dev/null +++ b/src/modules/overseas-warehouse/layoutAcceptance.test.ts @@ -0,0 +1,52 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const page = (name: string) => readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages', name), 'utf8') +const component = (name: string) => readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/components', name), 'utf8') + +describe('overseas warehouse 1920px layout acceptance', () => { + it('keeps action columns first and wide enough for every visible action', () => { + const widths: Record = { + 'LastMileOrdersView.vue': 110, + 'InboundOrdersView.vue': 190, + 'DispatchesView.vue': 190, + 'OutboundsView.vue': 100, + 'DeliveryOrdersView.vue': 300, + 'RelabelOrdersView.vue': 190, + 'PalletsView.vue': 250, + 'InventoryView.vue': 110, + } + for (const [name, width] of Object.entries(widths)) { + const source = page(name) + const columns = source.slice(source.indexOf('const columns'), source.indexOf('async function', source.indexOf('const columns'))) + expect(columns).toContain(`{ key: 'actions', label: '操作', width: ${width}, fixed: 'left'`) + expect(columns.indexOf("key: 'actions'")).toBeLessThan(columns.indexOf("label:", columns.indexOf("key: 'actions'") + 20)) + } + }) + + it('assigns explicit desktop widths to dense query inputs', () => { + for (const name of ['LastMileOrdersView.vue', 'InboundOrdersView.vue', 'DispatchesView.vue', 'OutboundsView.vue', 'DeliveryOrdersView.vue', 'RelabelOrdersView.vue', 'PalletsView.vue', 'InventoryView.vue']) { + const source = page(name) + const fieldLines = source.split(/\r?\n/).filter((line) => /type: '(input|select|daterange)'/.test(line)) + expect(fieldLines.length, name).toBeGreaterThan(0) + expect(fieldLines.every((line) => line.includes('width:')), `${name} has a query field without width`).toBe(true) + } + }) + + it('uses four-field desktop density and full-width form controls', () => { + const sharedDetail = component('OverseasReadOnlyDetail.vue') + expect(sharedDetail).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))') + expect(page('LastMileOrderDetailView.vue')).toContain('.detail-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr))') + expect(page('InboundOrderDetailView.vue')).toContain('.detail-group:last-child .detail-fields{grid-template-columns:repeat(4,minmax(0,1fr))}') + expect(page('DispatchDetailView.vue')).toContain('.document-section { grid-column: 1 / -1; }') + expect(page('DispatchDetailView.vue')).toContain('.document-section .detail-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); }') + expect(page('DeliveryOrderDetailView.vue')).toContain(':column="4"') + expect(page('InventoryDetailView.vue')).toContain('grid-template-columns:repeat(2,minmax(0,1fr))') + expect(page('OutboundsView.vue')).toContain('.create-outbound-form :deep(.el-input-number) { width: 100%; }') + expect(page('RelabelOrdersView.vue')).toContain('.el-form :deep(.el-date-editor) { width: 100%; }') + expect(page('PalletsView.vue')).toContain('.el-form :deep(.el-input-number) { width: 100%; }') + expect(page('RelabelOrdersView.vue')).toContain('.el-form :deep(.el-row) { margin-left: 0 !important; margin-right: 0 !important; }') + expect(page('PalletsView.vue')).toContain('.el-form :deep(.el-row) { margin-left: 0 !important; margin-right: 0 !important; }') + }) +}) diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue index c7478193..c120716a 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue @@ -10,16 +10,152 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { formatOverseasDateTime } from '../lastMilePresentation' import { getDeliveryOrder, type DeliveryOrderDetail } from '../api/deliveryOrders' -const route = useRoute(); const router = useRouter(); const detail = ref(null); const loading = ref(false); const error = ref(null); const activeTab = ref('order') -const deliveryNo = computed(() => String(route.params.deliveryNo || '')); const customerId = computed(() => String(route.query.customerId || '')) -const statusLabels: Record = { WAIT_DELIVERY: '待派送', DELIVERING: '派送中', DELIVERED: '派送完成', SIGNED: '已签收', REJECTED: '已拒签', RETURN_PENDING: '退货处理中', CANCELLED: '已取消' } -const methodLabels: Record = { SELF_DELIVERY: '自送', EXPRESS: '快递', THIRD_PARTY: '第三方配送' } -const feeLabels: Record = { DELIVERY_FEE: '派送费用', LOADING_FEE: '装车费', APPOINTMENT_FEE: '买约费用' } -const operationLabels: Record = { CREATE_DELIVERY: '创建派送单', ASSIGN_VEHICLE: '车队分配', DISPATCH: '开始派送', SIGN: '签收' } -function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' } -function back() { void router.push({ name: 'overseas-delivery-orders' }) } -async function load() { if (!deliveryNo.value || !customerId.value) { error.value = { message: '缺少派送单客户范围,无法安全查询详情' }; return }; loading.value = true; error.value = null; try { detail.value = await getDeliveryOrder(deliveryNo.value, customerId.value) } catch (cause) { error.value = getApiErrorDetails(cause, '派送单详情加载失败') } finally { loading.value = false } } +const route = useRoute() +const router = useRouter() +const detail = ref(null) +const loading = ref(false) +const error = ref(null) +const activeTab = ref('order') +const deliveryNo = computed(() => String(route.params.deliveryNo || '')) +const customerId = computed(() => String(route.query.customerId || '')) +const statusLabels: Record = { + WAIT_DELIVERY: '待派送', + DELIVERING: '派送中', + DELIVERED: '派送完成', + SIGNED: '已签收', + REJECTED: '已拒签', + RETURN_PENDING: '退货处理中', + CANCELLED: '已取消', +} +const methodLabels: Record = { + SELF_DELIVERY: '自送', + EXPRESS: '快递', + THIRD_PARTY: '第三方配送', +} +const feeLabels: Record = { + DELIVERY_FEE: '派送费用', + LOADING_FEE: '装车费', + APPOINTMENT_FEE: '买约费用', +} +const operationLabels: Record = { + CREATE_DELIVERY: '创建派送单', + ASSIGN_VEHICLE: '车队分配', + DISPATCH: '开始派送', + SIGN: '签收', +} +function show(value: unknown) { + const text = String(value ?? '').trim() + return text || '—' +} +function back() { + void router.push({ name: 'overseas-delivery-orders' }) +} +async function load() { + if (!deliveryNo.value || !customerId.value) { + error.value = { message: '缺少派送单客户范围,无法安全查询详情' } + return + } + loading.value = true + error.value = null + try { + detail.value = await getDeliveryOrder(deliveryNo.value, customerId.value) + } catch (cause) { + error.value = getApiErrorDetails(cause, '派送单详情加载失败') + } finally { + loading.value = false + } +} load() - - + + diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue index e1d18919..b3e66969 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue @@ -26,15 +26,15 @@ const rows = ref([]); const page = ref(1); const pageSize = const warehouses = ref([]); const warehousesLoading = ref(false) const filters = ref({ documentNo: '', dispatchNo: '', waybillNo: '', warehouseId: '', status: '', vehiclePlate: '', driverName: '', deliveryMethod: '', plannedAt: [] }) const fields = computed(() => [ - { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待派送', value: 'WAIT_DELIVERY' }, { label: '派送中', value: 'DELIVERING' }, { label: '派送完成', value: 'DELIVERED' }, { label: '已签收(历史)', value: 'SIGNED' }, { label: '已拒签', value: 'REJECTED' }, { label: '退货处理中', value: 'RETURN_PENDING' }, { label: '已取消', value: 'CANCELLED' }] }, - { key: 'documentNo', type: 'input', label: '派送单号', placeholder: '请输入派送单号' }, - { key: 'dispatchNo', type: 'input', label: '排车单号', placeholder: '请输入排车单号' }, - { key: 'waybillNo', type: 'input', label: '运单号', placeholder: '请输入运单号' }, - { key: 'warehouseId', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, loading: warehousesLoading.value } }, - { key: 'deliveryMethod', type: 'select', label: '派送方式', placeholder: '全部方式', options: [{ label: 'FBA自提', value: 'FBA_PICKUP' }, { label: '客户自提', value: 'CUSTOMER_PICKUP' }, { label: '卡派打托', value: 'TRUCK_PALLET' }, { label: '卡派地板', value: 'TRUCK_FLOOR' }, { label: '快递', value: 'EXPRESS' }, { label: '外配', value: 'OUTSOURCED' }, { label: '专车直送', value: 'DEDICATED_TRUCK' }, { label: '零担派送', value: 'LTL' }] }, - { key: 'vehiclePlate', type: 'input', label: '车牌号', placeholder: '请输入车牌号' }, - { key: 'driverName', type: 'input', label: '司机', placeholder: '请输入司机或车队' }, - { key: 'plannedAt', type: 'daterange', label: '预计派送时间' }, + { key: 'status', type: 'select', label: '状态', width: 160, placeholder: '全部状态', options: [{ label: '待派送', value: 'WAIT_DELIVERY' }, { label: '派送中', value: 'DELIVERING' }, { label: '派送完成', value: 'DELIVERED' }, { label: '已签收(历史)', value: 'SIGNED' }, { label: '已拒签', value: 'REJECTED' }, { label: '退货处理中', value: 'RETURN_PENDING' }, { label: '已取消', value: 'CANCELLED' }] }, + { key: 'documentNo', type: 'input', label: '派送单号', width: 190, placeholder: '请输入派送单号' }, + { key: 'dispatchNo', type: 'input', label: '排车单号', width: 190, placeholder: '请输入排车单号' }, + { key: 'waybillNo', type: 'input', label: '运单号', width: 180, placeholder: '请输入运单号' }, + { key: 'warehouseId', type: 'select', label: '海外仓', width: 220, placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, loading: warehousesLoading.value } }, + { key: 'deliveryMethod', type: 'select', label: '派送方式', width: 170, placeholder: '全部方式', options: [{ label: 'FBA自提', value: 'FBA_PICKUP' }, { label: '客户自提', value: 'CUSTOMER_PICKUP' }, { label: '卡派打托', value: 'TRUCK_PALLET' }, { label: '卡派地板', value: 'TRUCK_FLOOR' }, { label: '快递', value: 'EXPRESS' }, { label: '外配', value: 'OUTSOURCED' }, { label: '专车直送', value: 'DEDICATED_TRUCK' }, { label: '零担派送', value: 'LTL' }] }, + { key: 'vehiclePlate', type: 'input', label: '车牌号', width: 160, placeholder: '请输入车牌号' }, + { key: 'driverName', type: 'input', label: '司机', width: 180, placeholder: '请输入司机或车队' }, + { key: 'plannedAt', type: 'daterange', label: '预计派送时间', width: 280 }, ]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' }, @@ -78,4 +78,4 @@ onMounted(() => { void load(); void loadWarehouses() }) - + diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue index c4573990..6717f170 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue @@ -149,9 +149,10 @@ onMounted(load) .detail-section { min-width: 0; padding: 18px; } .summary-grid > .detail-section { border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); } .summary-grid > .detail-section:nth-child(2) { border-right: 0; } -.document-section { grid-column: 1 / 2; } +.document-section { grid-column: 1 / -1; } .detail-section h2 { margin: 0 0 14px; color: var(--el-color-primary); font-size: 16px; } -.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); } +.detail-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); } +.document-section .detail-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); } .detail-fields > div { display: grid; min-height: 48px; grid-template-columns: 180px minmax(0, 1fr); border-bottom: 1px solid var(--cc-border); } .detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 9px 12px; } .detail-fields dt { color: var(--cc-muted); } @@ -162,6 +163,7 @@ onMounted(load) .summary-grid { grid-template-columns: 1fr; } .summary-grid > .detail-section, .summary-grid > .detail-section:nth-child(2) { border-right: 0; } .document-section { grid-column: auto; } + .detail-fields, .document-section .detail-fields { grid-template-columns: 1fr; } .detail-fields > div { grid-template-columns: 132px minmax(0, 1fr); } } diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index 405a2606..39863c7e 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -59,14 +59,14 @@ const statusOptions = [ ] const fields = computed(() => [ - { key: 'dispatchNo', type: 'input', label: '排车单号', placeholder: '请输入排车单号' }, - { key: 'vehiclePlate', type: 'input', label: '车牌号', placeholder: '请输入车牌号' }, - { key: 'status', type: 'select', label: '车辆状态', placeholder: '全部状态', options: statusOptions }, - { key: 'fbaAppointmentNo', type: 'input', label: 'FBA预约号', placeholder: '请输入FBA预约号' }, + { key: 'dispatchNo', type: 'input', label: '排车单号', width: 190, placeholder: '请输入排车单号' }, + { key: 'vehiclePlate', type: 'input', label: '车牌号', width: 160, placeholder: '请输入车牌号' }, + { key: 'status', type: 'select', label: '车辆状态', width: 160, placeholder: '全部状态', options: statusOptions }, + { key: 'fbaAppointmentNo', type: 'input', label: 'FBA预约号', width: 190, placeholder: '请输入FBA预约号' }, ]) const columns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' }, + { key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' }, { key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170, fixed: 'left' }, { key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌号', minWidth: 130 }, { key: 'driverName', prop: 'driverName', label: '司机', minWidth: 120 }, diff --git a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue index b218547f..b01e5da1 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue @@ -56,6 +56,7 @@ onMounted(load) diff --git a/src/modules/overseas-warehouse/pages/InventoryDetailView.vue b/src/modules/overseas-warehouse/pages/InventoryDetailView.vue index 3a4ef981..5432e529 100644 --- a/src/modules/overseas-warehouse/pages/InventoryDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryDetailView.vue @@ -25,4 +25,4 @@ function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted( - + diff --git a/src/modules/overseas-warehouse/pages/InventoryView.vue b/src/modules/overseas-warehouse/pages/InventoryView.vue index 2b89eb20..81266219 100644 --- a/src/modules/overseas-warehouse/pages/InventoryView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryView.vue @@ -29,13 +29,13 @@ const pageSize = ref(10) const total = ref(0) const filters = ref({ warehouseId: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' }) const fields = computed(() => [ - ...(access.can(overseasWarehousePermissions.warehouseList) ? [{ key: 'warehouseId', type: 'select' as const, label: '海外仓', width: 230, placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, disabled: contextLoading.value } }] : []), - { key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主' }, - { key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码' }, - { key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名' }, - { key: 'batchNo', type: 'input', label: '批次号', placeholder: '请输入批次号' }, - { key: 'location', type: 'input', label: '库位', placeholder: '请输入库位(可空)' }, - { key: 'status', type: 'select', label: '库存状态', placeholder: '全部库存状态', options: [{ label: '有可用库存', value: 'AVAILABLE' }, { label: '存在暂扣', value: 'HOLD' }, { label: '已分配', value: 'ALLOCATED' }, { label: '在途', value: 'IN_TRANSIT' }, { label: '收货中', value: 'RECEIVING' }, { label: '采购库存', value: 'PROCUREMENT' }] }, + ...(access.can(overseasWarehousePermissions.warehouseList) ? [{ key: 'warehouseId', type: 'select' as const, label: '海外仓', width: 200, placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, disabled: contextLoading.value } }] : []), + { key: 'owner', type: 'input', label: '客户/货主', width: 190, placeholder: '请输入客户或货主' }, + { key: 'sku', type: 'input', label: 'SKU编码', width: 180, placeholder: '请输入SKU编码' }, + { key: 'productName', type: 'input', label: '中文品名', width: 190, placeholder: '请输入中文品名' }, + { key: 'batchNo', type: 'input', label: '批次号', width: 170, placeholder: '请输入批次号' }, + { key: 'location', type: 'input', label: '库位', width: 200, placeholder: '请输入库位(可空)' }, + { key: 'status', type: 'select', label: '库存状态', width: 170, placeholder: '全部库存状态', options: [{ label: '有可用库存', value: 'AVAILABLE' }, { label: '存在暂扣', value: 'HOLD' }, { label: '已分配', value: 'ALLOCATED' }, { label: '在途', value: 'IN_TRANSIT' }, { label: '收货中', value: 'RECEIVING' }, { label: '采购库存', value: 'PROCUREMENT' }] }, ]) const metrics = computed(() => [ ['在库总量', summary.value?.totalPackages ?? 0, 'primary'], ['可用库存', summary.value?.availablePackages ?? 0, 'success'], ['暂扣库存', summary.value?.holdPackages ?? 0, 'danger'], diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index 3b0056b5..5cca1d64 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -175,42 +175,48 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(wrapper.text()).toContain(`trace-${code}`) }) - it('loads the cabinet tab from server progress, shows the total and preserves server arrival ordering', async () => { + it('renders pending cabinets as an arrival-ordered workbench with the remaining inbound-code count', async () => { const nullArrival = { ...summary, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NULL', plannedArrivalAt: null, planCount: 0, pendingPlanCount: 15 } - vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [summary, nullArrival], page: 1, size: 10, total: 2, pages: 1 }) + const completed = { ...summary, sourceContainerBookingId: '2092873463568597003', containerNo: 'CONT-DONE', plannedInboundCodeCount: 15, pendingPlanCount: 0, allPlansReady: true } + vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [completed, summary, nullArrival], page: 1, size: 10, total: 3, pages: 1 }) const wrapper = await mountView() const containerTab = wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子')) await containerTab!.trigger('click') await flushPromises() expect(listContainerPlanProgress).toHaveBeenLastCalledWith({ page: 1, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined }) - expect(wrapper.text()).toContain('柜子(2)') - expect(wrapper.text()).toContain('计划到仓时间') - expect(wrapper.text()).toContain('已有 15 / 缺 0') - expect(wrapper.text()).toContain('已生成10个计划,剩余5个未操作') - expect(wrapper.text()).toContain('未就绪') - expect(wrapper.text()).toContain('存在5票尚未生成有效入库计划') + expect(wrapper.text()).toContain('柜子工作台(2)') + expect(wrapper.text()).toContain('待处理柜工作台') + expect(wrapper.text()).toContain('按计划到仓时间由近到远排列') + expect(wrapper.text()).toContain('5个入仓码待生成计划') + expect(wrapper.text()).toContain('共 15 个入仓码') + expect(wrapper.text()).not.toContain('待补入仓码') + expect(wrapper.text()).not.toContain('缺入仓码') + expect(wrapper.text()).not.toContain('已生成 10 个计划') + expect(wrapper.text()).not.toContain('存在5票尚未生成有效入库计划') + expect(wrapper.text()).not.toContain('CONT-DONE') + expect(wrapper.text()).toContain('2 个柜待处理') expect(wrapper.text().indexOf('CONT-15')).toBeLessThan(wrapper.text().indexOf('CONT-NULL')) }) - it('uses independent cabinet pagination and loads a compact long-id detail drawer', async () => { + it('uses independent cabinet pagination and opens the complete long-id detail in a dialog', async () => { vi.mocked(listContainerPlanProgress).mockResolvedValue({ records: [summary], page: 1, size: 10, total: 21, pages: 3 }) const wrapper = await mountView() const containerTab = wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子')) await containerTab!.trigger('click') await flushPromises() - const pageTwo = wrapper.find('button[aria-label="第 2 页"]') - await pageTwo.trigger('click') + const pagination = wrapper.findComponent({ name: 'ElPagination' }) + pagination.vm.$emit('current-change', 2) await flushPromises() expect(listContainerPlanProgress).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, size: 10 })) - const viewButtons = wrapper.findAll('button').filter((item) => item.text() === '查看') - const viewButton = viewButtons[viewButtons.length - 1] - await viewButton!.trigger('click') + await wrapper.get('.container-workbench-card').trigger('click') await flushPromises() expect(getContainerPlanProgress).toHaveBeenCalledWith(bookingId) - expect(document.body.textContent).toContain('柜级入库计划详情') + expect(document.body.textContent).toContain('柜子详情 · CONT-15') expect(document.body.textContent).toContain('LMO-1') expect(document.body.textContent).toContain('IN-1') expect(document.body.textContent).toContain('LIP-1') + expect(document.body.querySelector('.el-dialog')).not.toBeNull() + expect(document.body.querySelector('.el-drawer')).toBeNull() }) it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => { diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 47ce3fb7..9ec83a00 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -7,7 +7,6 @@ import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import AdminPage from '@/shared/components/AdminPage.vue' import AppDialog from '@/shared/components/AppDialog.vue' -import AppDrawer from '@/shared/components/AppDrawer.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' @@ -52,18 +51,18 @@ let containerOptionRequest = 0 const statusTab = ref<'全部' | '待分配' | '柜子'>('全部') const filters = ref({ orderNo: '', orderType: '', warehouse: '', inboundCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] }) const allFields: QueryField[] = [ - { key: 'orderNo', type: 'input', label: '尾程单号', placeholder: '请输入尾程单号' }, - { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' }, - { key: 'orderType', type: 'select', label: '订单类型', placeholder: '全部类型', options: [ + { key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' }, + { key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' }, + { key: 'orderType', type: 'select', label: '订单类型', width: 170, placeholder: '全部类型', options: [ { label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' }, { label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '本土散板', value: 'LOCAL_LOOSEBOARD' }, { label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' }, ] }, - { key: 'warehouse', type: 'input', label: '海外仓', placeholder: '请输入仓库名称或编码' }, - { key: 'sourceOrderNo', type: 'input', label: '来源订单号', placeholder: '请输入来源订单号' }, - { key: 'containerNo', type: 'input', label: '集装箱号', placeholder: '请输入集装箱号' }, - { key: 'transferWarehouseCode', type: 'input', label: '转仓代码', placeholder: '请输入转仓代码' }, - { key: 'status', type: 'select', label: '当前状态', placeholder: '全部状态', options: [ + { 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: '请输入转仓代码' }, + { key: 'status', type: 'select', label: '当前状态', width: 160, placeholder: '全部状态', options: [ { label: '待分配', value: 'PENDING_ASSIGNMENT' }, { label: '待入库', value: 'PENDING_INBOUND' }, { label: '入库完成', value: 'INBOUND_COMPLETED' }, @@ -71,9 +70,9 @@ const allFields: QueryField[] = [ { label: '派送中', value: 'DELIVERING' }, { label: '派送完成', value: 'DELIVERED' }, ] }, - { 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: '创建时间' }, + { key: 'abnormal', type: 'select', label: '是否异常', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] }, + { key: 'detained', type: 'select', label: '是否暂扣', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] }, + { key: 'createdAt', type: 'daterange', label: '创建时间', width: 280 }, ] const fields = computed(() => statusTab.value === '柜子' ? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key)) @@ -144,15 +143,6 @@ const columns: DataTableColumn[] = [ { key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 160 }, { key: 'warehouseRemark', prop: 'warehouseRemark', label: '备注(仓库)', minWidth: 160 }, ] -const containerColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' }, - { key: 'containerNo', prop: 'containerNo', label: '柜号', minWidth: 170 }, - { key: 'plannedArrivalAt', prop: 'plannedArrivalAt', label: '计划到仓时间', minWidth: 170, slotName: 'plannedArrivalAt' }, - { key: 'totalOrderCount', prop: 'totalOrderCount', label: '订单总数', width: 100, align: 'right' }, - { key: 'inboundCodes', label: '入仓码', minWidth: 170, slotName: 'inboundCodes' }, - { key: 'planProgress', label: '入库计划进度', minWidth: 270, slotName: 'planProgress' }, - { key: 'readiness', label: '整柜就绪状态', minWidth: 240, slotName: 'readiness' }, -] const containerDetailColumns: DataTableColumn[] = [ { key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170 }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 150 }, @@ -166,7 +156,19 @@ function formatDateTime(value: string | null | undefined) { return value ? value.replace('T', ' ').slice(0, 19) : '—' } function canGenerateContainerPlans(container: ContainerPlanProgressSummaryDto) { - return Boolean(container.containerNo) && container.missingInboundCodeCount === 0 && container.pendingPlanCount > 0 + return Boolean(container.containerNo) && container.pendingPlanCount > 0 +} +function pendingInboundCodeCount(container: ContainerPlanProgressSummaryDto) { + return Math.max(0, container.inboundCodeCount - container.plannedInboundCodeCount) +} +function changeContainerPage(value: number) { + page.value = value + void load() +} +function changeContainerPageSize(value: number) { + pageSize.value = value + page.value = 1 + void load() } async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) @@ -182,8 +184,9 @@ async function load(model: QueryModel = filters.value) { 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 + const pendingRecords = result.records.filter((container) => pendingInboundCodeCount(container) > 0) + containerRows.value = pendingRecords + containerTotal.value = Math.max(0, result.total - (result.records.length - pendingRecords.length)) } catch (error) { containerRows.value = [] containerTotal.value = 0 @@ -380,7 +383,7 @@ onMounted(() => { void load() }) 全部 待分配 - 柜子({{ containerTotal }}) + 柜子工作台({{ containerTotal }}) @@ -393,15 +396,28 @@ onMounted(() => { void load() }) -
- - - - - - - - +
+
+
待处理柜工作台按计划到仓时间由近到远排列,已全部生成计划的柜不再展示
+ {{ containerTotal }} 个柜待处理 +
+
+ +
+ +
+ 共 {{ containerTotal }} 个待处理柜 + +
@@ -438,7 +454,7 @@ onMounted(() => { void load() }) - + @@ -447,20 +463,41 @@ onMounted(() => { void load() }) 柜号:{{ containerDetail.summary.containerNo || '—' }} 计划到仓:{{ formatDateTime(containerDetail.summary.plannedArrivalAt) }} 订单:{{ containerDetail.summary.totalOrderCount }} - 入仓码:{{ containerDetail.summary.inboundCodeCount }},缺 {{ containerDetail.summary.missingInboundCodeCount }} - 计划:{{ containerDetail.summary.planCount }},未操作 {{ containerDetail.summary.pendingPlanCount }} + 入仓码:{{ containerDetail.summary.inboundCodeCount }} + 入库计划:{{ containerDetail.summary.planCount }},待生成 {{ pendingInboundCodeCount(containerDetail.summary) }} - + diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 976e5725..e9c7067c 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -16,7 +16,7 @@ import { listInventoryBalances, listInventoryPositions, type InventoryBalanceDto const rows = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const error = ref(null) const filters = ref({ documentNo: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref(); const selectedPositionId = ref(''); const positionOptions = ref>([]); const inventoryLoading = ref(false) const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }] -const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号' }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓' }, { key: 'status', type: 'select', label: '状态', options: [{ label: '全部', value: '' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }] }] +const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', width: 190, placeholder: '请输入出库单号' }, { key: 'warehouseId', type: 'input', label: '目的仓', width: 180, placeholder: '请输入目的仓' }, { key: 'status', type: 'select', label: '状态', width: 150, options: [{ label: '全部', value: '' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }] }] async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const result = await listOutbounds({ page: page.value, size: pageSize.value, documentNo: String(filters.value.documentNo || '').trim() || undefined, warehouseId: String(filters.value.warehouseId || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause, '出库单列表加载失败') } finally { loading.value = false } } async function openCreate() { createOpen.value = true; selectedPositionId.value = ''; quantity.value = undefined; inventoryLoading.value = true; try { const balances = await listInventoryBalances({ page: 1, size: 200, inventoryStatus: 'AVAILABLE' }); const groups = await Promise.all(balances.records.map(async balance => (await listInventoryPositions(balance.id, { inventoryStatus: 'AVAILABLE' })).map(position => ({ balance, position })))); positionOptions.value = groups.flat().filter(item => item.position.availablePackages > 0) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '可用库存加载失败').message) } finally { inventoryLoading.value = false } } @@ -36,5 +36,9 @@ onMounted(load)
- + + + diff --git a/src/modules/overseas-warehouse/pages/PalletsView.vue b/src/modules/overseas-warehouse/pages/PalletsView.vue index eea934df..2a13c64f 100644 --- a/src/modules/overseas-warehouse/pages/PalletsView.vue +++ b/src/modules/overseas-warehouse/pages/PalletsView.vue @@ -145,3 +145,8 @@ onMounted(loadWarehouses) + + diff --git a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue index e5e2660d..b62b132f 100644 --- a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue @@ -208,4 +208,6 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' }) diff --git a/src/shared/components/InboundCodeRelationshipDialog.test.ts b/src/shared/components/InboundCodeRelationshipDialog.test.ts index 3fa7ee65..1ca7373e 100644 --- a/src/shared/components/InboundCodeRelationshipDialog.test.ts +++ b/src/shared/components/InboundCodeRelationshipDialog.test.ts @@ -215,13 +215,12 @@ describe('InboundCodeRelationshipDialog contract', () => { wrapper.unmount() }) - it('removes the retired Drawer component and all Admin consumer references', () => { + it('removes the retired Drawer component and keeps the Dialog only in confirmed Admin consumers', () => { const root = process.cwd() expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.vue'))).toBe(false) expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.test.ts'))).toBe(false) const dialogConsumerFiles = [ 'src/modules/inbounds/pages/InboundOrdersView.vue', - 'src/modules/inbounds/components/InboundTaskList.vue', 'src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue', ] const consumerSources = dialogConsumerFiles.map((file) => readFileSync(resolve(root, file), 'utf8')) @@ -233,6 +232,10 @@ describe('InboundCodeRelationshipDialog contract', () => { expect(consumer).toContain('