优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, transferLastMileOrders } from './lastMileOrders'
|
||||
import { convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, setLastMileOrderDetained, setLastMileOrderPalletMergeRequired, transferLastMileOrders } from './lastMileOrders'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
@@ -47,6 +47,22 @@ describe('last-mile order overseas API', () => {
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/destination', expect.objectContaining({ orderIds: ['190000000000000099'] }))
|
||||
})
|
||||
|
||||
it('uses the order hold endpoint with the required operation remark', async () => {
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: null })
|
||||
await setLastMileOrderDetained('190000000000000099', { detained: true, remark: '等待质检确认' })
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099/detained', {
|
||||
detained: true, remark: '等待质检确认',
|
||||
})
|
||||
})
|
||||
|
||||
it('marks an order for a PDA pallet-merge reminder', async () => {
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: null })
|
||||
await setLastMileOrderPalletMergeRequired('190000000000000099', { palletMergeRequired: true })
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099/pallet-merge-required', {
|
||||
palletMergeRequired: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the inbound-plan conversion endpoint without customerId', async () => {
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: {} })
|
||||
await convertLastMileOrderToInboundPlan({ orderId: '190000000000000099' })
|
||||
|
||||
@@ -152,6 +152,15 @@ export interface ConvertInboundPlanPayload {
|
||||
orderId: string
|
||||
}
|
||||
|
||||
export interface SetLastMileOrderDetainedPayload {
|
||||
detained: boolean
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface SetLastMileOrderPalletMergePayload {
|
||||
palletMergeRequired: boolean
|
||||
}
|
||||
|
||||
export interface LastMileOrderDetailDto {
|
||||
order: LastMileOrderDto
|
||||
cargo: LastMileCargoDto[]
|
||||
@@ -195,3 +204,17 @@ export async function convertLastMileOrderToInboundPlan(payload: ConvertInboundP
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function setLastMileOrderDetained(orderId: string, payload: SetLastMileOrderDetainedPayload) {
|
||||
const response = await overseasHttp.put<ApiEnvelope<void>, ApiEnvelope<void>>(
|
||||
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/detained`, payload,
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function setLastMileOrderPalletMergeRequired(orderId: string, payload: SetLastMileOrderPalletMergePayload) {
|
||||
const response = await overseasHttp.put<ApiEnvelope<void>, ApiEnvelope<void>>(
|
||||
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/pallet-merge-required`, payload,
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ export async function listCustomerWarehouses() {
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listPallets(warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) {
|
||||
export async function listPallets(warehouseId: string | undefined, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<PalletDto>>, ApiEnvelope<OverseasPagePayload<PalletDto>>>(
|
||||
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`,
|
||||
warehouseId ? `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets` : '/platform/v1/warehouse-admin/pallets',
|
||||
{ params: { ...query } },
|
||||
)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
|
||||
@@ -5,6 +5,7 @@ import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrder
|
||||
export interface RelabelOrderQuery {
|
||||
page: number
|
||||
size: number
|
||||
relabelNo?: string
|
||||
skuCode?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
|
||||
type VehicleSpec = { value: string; label: string; capacity: number; maxWeightLb: number }
|
||||
@@ -77,7 +78,7 @@ const containerWeightLb = (container: LoadingContainer) => container.palletIds.r
|
||||
function palletId(item: DispatchPalletCandidateDto) { return String(item.planPalletId) }
|
||||
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
|
||||
function number(value: unknown, digits = 0) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' }
|
||||
function dateTime(value: unknown) { const raw = String(value || '').trim(); return raw ? raw.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function dateTime(value: unknown) { return formatOverseasDateTime(value) }
|
||||
function orderType(value: unknown) { return ({ FIRST_LEG: '头程订单', RELABEL_APPLICATION: '换标下单', RETURN_RELABEL: '退货换标', LOCAL_LOOSEBOARD: '本土散板', WAREHOUSE_TRANSFER: '仓储转运', ONE_CLICK_DROPSHIP: '一件代发', REJECTED: '拒收订单', EXPRESS: '快递订单' } as Record<string, string>)[String(value || '')] || text(value) }
|
||||
function cartonMarks(item: DispatchPalletCandidateDto) { return String(item.cartonMarks || '').split(',').map((value) => value.trim()).filter(Boolean) }
|
||||
function containerFor(id: string) { return containers.value.find((item) => item.palletIds.includes(id)) }
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('overseas inventory presentation adapter', () => {
|
||||
available: 18,
|
||||
held: 2,
|
||||
status: '存在暂扣',
|
||||
updatedAt: '2026-08-18 17:54:11',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InventoryBalanceDto } from './api/inventory'
|
||||
import type { InventoryListItem } from './types'
|
||||
import { formatOverseasDateTime } from './lastMilePresentation'
|
||||
|
||||
function text(value: unknown) {
|
||||
const normalized = String(value ?? '').trim()
|
||||
@@ -40,6 +41,6 @@ export function toInventoryListItem(dto: InventoryBalanceDto): InventoryListItem
|
||||
inTransit: count(dto.inTransitPackages),
|
||||
purchase: count(dto.procurementPackages),
|
||||
status: inventoryStatusLabel(dto),
|
||||
updatedAt: text(dto.updatedAt),
|
||||
updatedAt: formatOverseasDateTime(dto.updatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toLastMileOrderListItem } from './lastMilePresentation'
|
||||
import { formatOverseasDateTime, toLastMileOrderListItem } from './lastMilePresentation'
|
||||
|
||||
describe('last-mile order presentation adapter', () => {
|
||||
it('formats overseas timestamps with zero-padded seconds', () => {
|
||||
expect(formatOverseasDateTime('2026-08-27T16:19:46')).toBe('2026-08-27 16:19:46')
|
||||
expect(formatOverseasDateTime('2026-08-07T06:09:04')).toBe('2026-08-07 06:09:04')
|
||||
expect(formatOverseasDateTime('2026-08-07')).toMatch(/^2026-08-07 /)
|
||||
expect(formatOverseasDateTime(new Date(2026, 7, 7, 6, 9, 4))).toBe('2026-08-07 06:09:04')
|
||||
expect(formatOverseasDateTime('')).toBe('—')
|
||||
expect(formatOverseasDateTime('not-a-date')).toBe('not-a-date')
|
||||
})
|
||||
|
||||
it('maps backend codes, associations and long ids without inventing data', () => {
|
||||
const row = toLastMileOrderListItem({
|
||||
id: '190000000000000099', customerId: '190000000000000001', orderNo: 'LL-001', orderType: 'ONE_CLICK_DROPSHIP', currentStatus: 'PENDING_ASSIGNMENT',
|
||||
abnormal: true, detained: false, packageCount: 12, deliveryOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY',
|
||||
abnormal: true, detained: false, packageCount: 12, palletMergeFlag: 'REQUIRED', deliveryOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY',
|
||||
})
|
||||
expect(row).toMatchObject({
|
||||
id: '190000000000000099', customerId: '190000000000000001', orderType: '一件代发', status: '待分配',
|
||||
warning: '异常', abnormal: '是', held: '否', cartons: 12, dispatchOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY',
|
||||
warning: '异常', abnormal: '是', held: '否', cartons: 12, consolidated: '待合托', palletMergeFlag: 'REQUIRED', dispatchOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY',
|
||||
})
|
||||
expect(row.rejectedAt).toBe('—')
|
||||
})
|
||||
|
||||
@@ -48,8 +48,22 @@ export function lastMileOrderTypeLabel(value: unknown) {
|
||||
export function formatOverseasDateTime(value: unknown) {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return '—'
|
||||
const date = new Date(raw)
|
||||
return Number.isNaN(date.getTime()) ? raw : date.toLocaleString('zh-CN', { hour12: false })
|
||||
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw)
|
||||
const date = value instanceof Date
|
||||
? new Date(value.getTime())
|
||||
: typeof value === 'number'
|
||||
? new Date(value)
|
||||
: dateOnly
|
||||
? new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))
|
||||
: new Date(raw)
|
||||
if (Number.isNaN(date.getTime())) return raw
|
||||
const pad = (part: number) => String(part).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
export function palletMergeLabel(value: unknown) {
|
||||
const code = String(value ?? '').trim()
|
||||
return ({ REQUIRED: '待合托', MERGED: '已合托' } as Record<string, string>)[code] || (code ? text(code) : '未标记')
|
||||
}
|
||||
|
||||
export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileOrderListItem {
|
||||
@@ -69,6 +83,7 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO
|
||||
containerStatus: text(dto.containerStatus),
|
||||
abnormal: yesNo(dto.abnormal),
|
||||
held: yesNo(dto.detained),
|
||||
detained: dto.detained === true,
|
||||
pallets: count(dto.palletCount),
|
||||
cartons: count(dto.packageCount),
|
||||
volume: count(dto.volumeCbm),
|
||||
@@ -82,7 +97,8 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO
|
||||
dispatchOrderNo: text(dto.deliveryOrderNo),
|
||||
relabelOrderNo: text(dto.relabelOrderNo),
|
||||
relabelStatus: lastMileStatusLabel(dto.relabelStatus),
|
||||
consolidated: text(dto.palletMergeFlag),
|
||||
consolidated: palletMergeLabel(dto.palletMergeFlag),
|
||||
palletMergeFlag: String(dto.palletMergeFlag ?? '').trim(),
|
||||
status: lastMileStatusLabel(dto.currentStatus),
|
||||
internalRemark: text(dto.privateRemark),
|
||||
customerRemark: text(dto.customerRemark),
|
||||
|
||||
@@ -118,10 +118,10 @@ function toListItem(dto: DispatchSummaryDto): DispatchListItem {
|
||||
orderNos: text(dto.orderNos),
|
||||
route: text(dto.route),
|
||||
overseasWarehouse: text(dto.overseasWarehouseName || dto.overseasWarehouseId),
|
||||
departAt: text(dto.departAt),
|
||||
departAt: formatOverseasDateTime(dto.departAt),
|
||||
fbaAppointment: appointmentLabel(dto.fbaAppointmentNo, dto.fbaAppointmentStatus),
|
||||
vehicleAppointment: text(dto.vehicleAppointmentStatus),
|
||||
estimatedArrivalAt: text(dto.estimatedArrivalAt),
|
||||
estimatedArrivalAt: formatOverseasDateTime(dto.estimatedArrivalAt),
|
||||
status: statusLabel(dto.status),
|
||||
palletCount: number(dto.palletCount),
|
||||
packageCount: number(dto.packageCount),
|
||||
|
||||
@@ -92,7 +92,7 @@ function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
|
||||
orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), type: typeLabel(dto.orderType), warehouse: text(dto.warehouseName || dto.warehouseCode),
|
||||
fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount), volume: number(dto.volumeCbm), pallets: number(dto.palletCount),
|
||||
status: planStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus),
|
||||
appointmentAt: text(dto.appointmentAt), expectedLoadingAt: text(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车',
|
||||
appointmentAt: formatOverseasDateTime(dto.appointmentAt), expectedLoadingAt: formatOverseasDateTime(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车',
|
||||
createdAt: formatOverseasDateTime(dto.createdAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
@@ -12,7 +12,7 @@ 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 { convertLastMileOrderToInboundPlan, listLastMileOrders, transferLastMileOrders } from '../api/lastMileOrders'
|
||||
import { convertLastMileOrderToInboundPlan, listLastMileOrders, setLastMileOrderDetained, setLastMileOrderPalletMergeRequired, transferLastMileOrders } from '../api/lastMileOrders'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { toLastMileOrderListItem } from '../lastMilePresentation'
|
||||
@@ -30,6 +30,9 @@ const selectedRows = ref<LastMileOrderListItem[]>([])
|
||||
const actionLoading = ref(false)
|
||||
const actionError = ref<ApiErrorDetails | null>(null)
|
||||
const transferOpen = ref(false)
|
||||
const detainedOpen = ref(false)
|
||||
const detainedTarget = ref<LastMileOrderListItem | null>(null)
|
||||
const detainedRemark = ref('')
|
||||
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: [] })
|
||||
@@ -58,7 +61,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'createdAt', type: 'daterange', label: '创建时间' },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 270, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170 },
|
||||
{ key: 'warning', prop: 'warning', label: '预警', width: 90 },
|
||||
@@ -139,6 +142,42 @@ function openTransfer() {
|
||||
actionError.value = null
|
||||
transferOpen.value = true
|
||||
}
|
||||
function openDetained(row: LastMileOrderListItem) {
|
||||
detainedTarget.value = row
|
||||
detainedRemark.value = ''
|
||||
actionError.value = null
|
||||
detainedOpen.value = true
|
||||
}
|
||||
async function submitDetained() {
|
||||
const target = detainedTarget.value
|
||||
if (!target) return
|
||||
if (!detainedRemark.value.trim()) { ElMessage.warning('请填写操作备注'); return }
|
||||
actionLoading.value = true; actionError.value = null
|
||||
try {
|
||||
await setLastMileOrderDetained(target.id, { detained: !target.detained, remark: detainedRemark.value.trim() })
|
||||
detainedOpen.value = false; detainedTarget.value = null
|
||||
ElMessage.success(target.detained ? '已解除暂扣,库存已转为可用' : '已标记暂扣,后续入库将进入暂扣库存')
|
||||
await load()
|
||||
} catch (error) { actionError.value = getApiErrorDetails(error, target.detained ? '解除暂扣失败,请重试' : '暂扣失败,请重试') }
|
||||
finally { actionLoading.value = false }
|
||||
}
|
||||
async function togglePalletMerge(row: LastMileOrderListItem) {
|
||||
const required = row.palletMergeFlag !== 'REQUIRED'
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
required ? `标记后,PDA扫描入仓码 ${row.inboundCode} 的箱唛时会显示合托提示。` : '确认取消该订单的PDA合托提示?',
|
||||
required ? '合托标记' : '取消合托标记',
|
||||
{ type: 'warning', confirmButtonText: required ? '确认标记' : '确认取消', cancelButtonText: '返回' },
|
||||
)
|
||||
} catch { return }
|
||||
actionLoading.value = true; actionError.value = null
|
||||
try {
|
||||
await setLastMileOrderPalletMergeRequired(row.id, { palletMergeRequired: required })
|
||||
ElMessage.success(required ? '已标记合托,PDA扫描箱唛时将显示提示' : '已取消合托标记')
|
||||
await load()
|
||||
} catch (error) { actionError.value = getApiErrorDetails(error, required ? '合托标记失败,请重试' : '取消合托标记失败,请重试') }
|
||||
finally { actionLoading.value = false }
|
||||
}
|
||||
async function submitTransfer() {
|
||||
if (!transferForm.warehouseId.trim() || !transferForm.warehouseCode.trim() || !transferForm.warehouseName.trim()) {
|
||||
ElMessage.warning('请填写目的仓 ID、代码和名称')
|
||||
@@ -185,9 +224,9 @@ onMounted(() => { void load() })
|
||||
<template #details><ApiErrorMeta :details="loadError" /></template>
|
||||
</LoadErrorState>
|
||||
</template>
|
||||
<section v-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()">
|
||||
<section v-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="暂无尾程订单" :row-class-name="({ row }: { row: LastMileOrderListItem }) => row.detained ? 'is-detained-row' : ''" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" plain :disabled="selectedRows.length !== 1 || actionLoading" @click="convertInboundPlan">生成入库计划</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" size="small" plain :type="(row as LastMileOrderListItem).detained ? 'success' : 'danger'" :disabled="actionLoading" @click="openDetained(row as LastMileOrderListItem)">{{ (row as LastMileOrderListItem).detained ? '解除暂扣' : '暂扣' }}</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer) && (row as LastMileOrderListItem).palletMergeFlag !== 'MERGED'" size="small" plain :type="(row as LastMileOrderListItem).palletMergeFlag === 'REQUIRED' ? 'warning' : 'primary'" :disabled="actionLoading" @click="togglePalletMerge(row as LastMileOrderListItem)">{{ (row as LastMileOrderListItem).palletMergeFlag === 'REQUIRED' ? '取消合托' : '合托标记' }}</el-button></template>
|
||||
<template #pagination-left>共 {{ total }} 条尾程订单 · 已选 {{ selectedRows.length }} 条</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
@@ -199,9 +238,14 @@ onMounted(() => { void load() })
|
||||
<el-form-item label="备注"><el-input v-model="transferForm.remark" type="textarea" maxlength="500" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="detainedOpen" :title="detainedTarget?.detained ? '解除暂扣' : '暂扣订单'" :confirm-text="detainedTarget?.detained ? '确认解除' : '确认暂扣'" :confirm-loading="actionLoading" @confirm="submitDetained">
|
||||
<el-form label-position="top"><el-form-item label="操作备注" required><el-input v-model="detainedRemark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请填写暂扣或解除原因" /></el-form-item></el-form>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.last-mile-toolbar { padding: 14px 16px 0; }
|
||||
.last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; }
|
||||
.table-panel :deep(.el-table .is-detained-row > td.el-table__cell) { background: #fff1f0 !important; color: #b42318; }
|
||||
.table-panel :deep(.el-table .is-detained-row:hover > td.el-table__cell) { background: #ffe4e1 !important; }
|
||||
</style>
|
||||
|
||||
@@ -12,13 +12,14 @@ import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { cancelOutbound, createOneClickDropshipOutbound, listOutbounds, type OutboundRow } from '../api/outbounds'
|
||||
import { listInventoryBalances, listInventoryPositions, type InventoryBalanceDto, type InventoryPositionDto } from '../api/inventory'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
|
||||
const rows = ref<OutboundRow[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null)
|
||||
const filters = ref<QueryModel>({ documentNo: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref<number | undefined>(); const selectedPositionId = ref(''); const positionOptions = ref<Array<{ balance: InventoryBalanceDto; position: InventoryPositionDto }>>([]); 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' }] }]
|
||||
|
||||
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 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.map((row) => ({ ...row, createdAt: formatOverseasDateTime(row.createdAt) })); 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 } }
|
||||
async function submit() { const selected = positionOptions.value.find(item => item.position.id === selectedPositionId.value); if (!recipient.value.trim() || !selected || !quantity.value || quantity.value < 1) { ElMessage.warning('请填写收件信息并选择托盘和出库数量'); return }; if (quantity.value > selected.position.availablePackages) { ElMessage.warning('出库数量不能超过可用库存'); return }; saving.value = true; try { await createOneClickDropshipOutbound(selected.balance.customerId, { recipient: recipient.value.trim(), items: [{ inventoryPositionId: selected.position.id, packages: quantity.value, sku: selected.balance.skuCode || undefined }], remark: remark.value.trim() || undefined, idempotencyKey: `web-dropship-${Date.now()}` }); ElMessage.success('出库单已创建并推送PDA拣货任务'); createOpen.value = false; await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '一件代发出库失败').message) } finally { saving.value = false } }
|
||||
async function cancel(row: OutboundRow) { let reason = ''; try { const result = await ElMessageBox.prompt(`出库单 ${row.outboundNo} · 出库数量 ${row.packageCount ?? 0}`, '取消出库单', { inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return }; try { await cancelOutbound(row.outboundNo, reason); ElMessage.success('出库单已取消,库存及关联单据已回滚'); await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '取消出库单失败').message) } }
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getPallet, listCustomerWarehouses, type PalletDto } from '../api/pallets'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -22,7 +23,7 @@ const groups = [
|
||||
const values = computed(() => ({
|
||||
托盘编码: detail.value?.palletCode || '—', 托盘类型: detail.value?.palletType || '—', 仓库: warehouseName.value,
|
||||
'容积(m³)': detail.value?.capacityCbm ?? '—', 采购价格: detail.value?.unitPrice ?? '—', 所属FBA仓: detail.value?.fbaWarehouseCode || '—', 状态: detail.value?.status || '—',
|
||||
库位ID: detail.value?.locationId || '—', 创建时间: detail.value?.createdAt || '—', 更新时间: detail.value?.updatedAt || '—', 备注: detail.value?.remark || '—',
|
||||
库位ID: detail.value?.locationId || '—', 创建时间: formatOverseasDateTime(detail.value?.createdAt), 更新时间: formatOverseasDateTime(detail.value?.updatedAt), 备注: detail.value?.remark || '—',
|
||||
}))
|
||||
|
||||
async function load() {
|
||||
|
||||
@@ -76,7 +76,6 @@ async function loadWarehouses() {
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
const warehouseId = String(filters.value.warehouseId || '')
|
||||
if (!warehouseId) { rows.value = []; total.value = 0; return }
|
||||
loading.value = true; loadError.value = null; loadErrorKind.value = 'list'
|
||||
try {
|
||||
const result = await listPallets(warehouseId, { page: page.value, size: pageSize.value, keyword: String(filters.value.code || '').trim() || undefined, palletType: String(filters.value.type || '') || undefined, status: String(filters.value.status || '') || undefined })
|
||||
|
||||
@@ -133,4 +133,17 @@ describe('RelabelOrdersView', () => {
|
||||
expect(labels).toEqual(expect.arrayContaining(['处理中', '已驳回', '已取消']))
|
||||
expect(labels).not.toContain('已完成')
|
||||
})
|
||||
|
||||
it('passes the relabel order number search to the backend', async () => {
|
||||
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const input = wrapper.find('input[placeholder="请输入换标单号"]')
|
||||
expect(input.exists()).toBe(true)
|
||||
await input.setValue('FBR202608272092853218068373506')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('查询'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(listRelabelOrders).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
relabelNo: 'FBR202608272092853218068373506',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,8 +31,9 @@ const warehouseRequestSequence = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const filters = ref<QueryModel>({ sku: '', status: '' })
|
||||
const filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '' })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 },
|
||||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] },
|
||||
])
|
||||
@@ -157,7 +158,7 @@ async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
loading.value = true; loadError.value = null
|
||||
try {
|
||||
const result = await listRelabelOrders({ page: page.value, size: pageSize.value, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined })
|
||||
const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined })
|
||||
rows.value = result.records.map((item) => ({
|
||||
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
|
||||
productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), remark: '',
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface LastMileOrderListItem {
|
||||
containerStatus: string
|
||||
abnormal: string
|
||||
held: string
|
||||
detained: boolean
|
||||
pallets: number
|
||||
cartons: number
|
||||
volume: number
|
||||
@@ -35,6 +36,7 @@ export interface LastMileOrderListItem {
|
||||
relabelOrderNo: string
|
||||
relabelStatus: string
|
||||
consolidated: string
|
||||
palletMergeFlag: string
|
||||
status: string
|
||||
internalRemark: string
|
||||
customerRemark: string
|
||||
|
||||
Reference in New Issue
Block a user