diff --git a/AGENTS.md b/AGENTS.md index d2463417..f474d914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ | 海外采购、卡车、散板 | `modules/overseas-business/` | `gateway-overseas/`、`lastleg-service/` | | 海外仓业务合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | | 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/海外仓业务/船节点标准化契约.md` | -| 海外排车、出库与派送 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue` | API 为 `dispatches.ts`、`outbounds.ts`、`deliveryOrders.ts`;Backend `lastleg-service/dispatch/`、`delivery/`、`dropship/`。一件代发出库从服务端库存位置选择并携带幂等键;排车转派送按稳定 `dispatchNo` 进入服务端命令,可在 `PENDING_DEPARTURE`、`PICKING`、`READY_TO_DEPART` 状态执行并须在 PDA 发车前生成待出库单 | +| 海外排车、出库与派送 | `modules/overseas-warehouse/pages/DispatchesView.vue`、`DispatchDetailView.vue`、`OutboundsView.vue`、`DeliveryOrdersView.vue`、`DeliveryOrderDetailView.vue` | API 为 `dispatches.ts`、`outbounds.ts`、`deliveryOrders.ts`;Backend `lastleg-service/dispatch/`、`delivery/`、`dropship/`。一件代发独立出库;普通出库及换标单均经头程排车转出库,转出库时按托盘快照绑定头程箱唛,PDA 仍按托盘作业,详情页展示箱唛与已拣数量 | | 海外异常退回与客户回访 | `modules/overseas-warehouse/pages/ExceptionOrdersView.vue`、`CustomerVisitsView.vue` | API 为 `exceptionOrders.ts`、`customerVisits.ts`;Backend `lastleg-service/exceptionorder/`、`customervisit/`。派送退回先从服务端查询可关联入仓单,回访附件和派送完成/状态均走海外真实 API | 全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外仓业务开发先读 `../../docs/海外仓业务/README.md`。进度、排期或验收状态问题再读取进度表和报告。 diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 2e7affd5..27471b70 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -129,6 +129,8 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fba-relabel-order', title: '换标单', icon: ClipboardList, path: '/overseas-warehouse/relabel-orders', permission: 'platform:overseas-warehouse:relabel-order:list' }, { key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/overseas-warehouse/pallets', permission: 'platform:overseas-warehouse:pallet:list' }, { key: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:overseas-warehouse:inventory:list' }, + { key: 'return-inventory-list', title: '退货库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/return-inventory', permission: 'platform:overseas-warehouse:return-inventory:list' }, + { key: 'return-cartons-list', title: '退货箱号管理', icon: WarehouseIcon, path: '/overseas-warehouse/return-cartons', permission: 'platform:overseas-warehouse:return-cartons:list' }, ], }, { diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 9fa660bb..a9b549f9 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -215,6 +215,7 @@ export interface TransferDeliveryPayload { sellerName?: string remark?: string fees: Array<{ feeType: string; currency: string; amount: number; remark?: string }> + palletCartonMarks?: Record } export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) { const response = await overseasHttp.post, ApiEnvelope>( diff --git a/src/modules/overseas-warehouse/api/outbounds.ts b/src/modules/overseas-warehouse/api/outbounds.ts index 311a65db..d46b2e5d 100644 --- a/src/modules/overseas-warehouse/api/outbounds.ts +++ b/src/modules/overseas-warehouse/api/outbounds.ts @@ -4,7 +4,8 @@ import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrder import type { DeliveryOrderRow } from './deliveryOrders' export interface OutboundRow { id: string; outboundNo: string; dispatchNo?: string | null; overseasWarehouseId?: string | null; lxCodes?: string | null; status?: string | null; palletCount?: number | null; packageCount?: number | null; createdAt?: string | null } -export interface OutboundDetail { outbound: OutboundRow; delivery?: DeliveryOrderRow | null } +export interface OutboundPalletRow { id: string; palletNo?: string | null; cartonMarks?: string | null; skuCode?: string | null; inboundCode?: string | null; packageCount?: number | null; pickedPackageCount?: number | null; status?: string | null } +export interface OutboundDetail { outbound: OutboundRow; delivery?: DeliveryOrderRow | null; pallets?: OutboundPalletRow[] } export interface OutboundQuery { page: number; size: number; documentNo?: string; lxCode?: string; warehouseId?: string; status?: string } export async function listOutbounds(query: OutboundQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/outbound-orders', { params: query }) @@ -14,7 +15,7 @@ export async function getOutbound(outboundNo: string) { const response = await overseasHttp.get, ApiEnvelope>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}`) return unwrapApiResponse(response) } -export async function createOneClickDropshipOutbound(customerId: string, payload: { recipient: string; items: Array<{ inventoryPositionId: string; packages: number; sku?: string }>; remark?: string; idempotencyKey: string }) { +export async function createOneClickDropshipOutbound(customerId: string, payload: { recipient: string; items: Array<{ inventoryPositionId: string; packages: number; sku?: string; cartonMarks?: string[] }>; remark?: string; idempotencyKey: string }) { const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } }) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/api/returnInventory.ts b/src/modules/overseas-warehouse/api/returnInventory.ts new file mode 100644 index 00000000..6ed7d881 --- /dev/null +++ b/src/modules/overseas-warehouse/api/returnInventory.ts @@ -0,0 +1,16 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' + +export interface ReturnStockRow { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; totalQuantity: number; availableQuantity: number; cartonQuantity: number; updatedAt: string } +export interface ReturnCartonRow { customerId: string; overseasWarehouseId: string; cartonMark: string; sourceReturnNo?: string; sourceBatchNo?: string; status: string; skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; createdAt: string } +export interface ReturnCartonDetail extends ReturnCartonRow { customerId: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> } +export interface ReturnCartonLabel extends ReturnCartonDetail { printableHtml: string } +export interface ReturnInventoryMovement { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; movementType: 'RECEIPT' | 'MERGE' | 'OUTBOUND'; referenceNo: string; totalDelta: number; availableDelta: number; occurredAt: string } +export interface ReturnInventoryQuery { page: number; size: number; customerId?: string; overseasWarehouseId?: string; skuCode?: string; cartonMark?: string; returnNo?: string; status?: string } + +export async function listReturnStock(params: ReturnInventoryQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/return-inventory', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } +export async function listReturnCartons(params: ReturnInventoryQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/return-inventory/cartons', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } +export async function listReturnInventoryMovements(params: ReturnInventoryQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/return-inventory/movements', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } +export async function getReturnCarton(cartonMark: string) { const response = await overseasHttp.get, ApiEnvelope>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}`); return unwrapApiResponse(response) } +export async function getReturnCartonLabel(cartonMark: string) { const response = await overseasHttp.get, ApiEnvelope>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label`); return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index 914d3a79..86f9e72a 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -33,7 +33,7 @@ const mutationError = ref(null) const editOpen = ref(false); const editRow = ref(null); const editSaving = ref(false) const dispatchSnapshots = new Map() const editForm = reactive({ vehiclePlate: '', driverName: '', driverPhone: '', carrierName: '', route: '', departAt: '', fbaAppointmentNo: '', fbaAppointmentStatus: '', vehicleAppointmentStatus: '', estimatedArrivalAt: '' }) -const transferForm = reactive({ plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: '', pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: '', driverPhone: '', vehiclePlate: '', appointmentAccount: '', sellerName: '', deliveryFee: undefined as number | undefined, loadingFee: undefined as number | undefined, appointmentFee: undefined as number | undefined, remark: '' }) +const transferForm = reactive({ plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: '', pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: '', driverPhone: '', vehiclePlate: '', appointmentAccount: '', sellerName: '', deliveryFee: undefined as number | undefined, loadingFee: undefined as number | undefined, appointmentFee: undefined as number | undefined, cartonMarksJson: '', remark: '' }) const deliveryMethodOptions = [ { label: 'FBA自提', value: 'FBA_PICKUP' }, { label: '客户自提', value: 'CUSTOMER_PICKUP' }, @@ -190,7 +190,7 @@ function canEdit(row: DispatchListItem) { return access.can(permission.dispatchE function canCancel(row: DispatchListItem) { return access.can(permission.dispatchCancel) && row.statusCode === 'PENDING_DEPARTURE' } function canTransfer(row: DispatchListItem) { return access.can(permission.dispatchTransferDelivery) && ['PENDING_DEPARTURE', 'PICKING', 'READY_TO_DEPART'].includes(row.statusCode) } function openEdit(row: DispatchListItem) { if (!canEdit(row)) return; const source = dispatchSnapshots.get(row.dispatchNo); editRow.value = row; Object.assign(editForm, { vehiclePlate: source?.vehiclePlate || '', driverName: source?.driverName || '', driverPhone: source?.driverPhone || '', carrierName: source?.carrierName || '', route: source?.route || '', departAt: source?.departAt || '', fbaAppointmentNo: source?.fbaAppointmentNo || '', fbaAppointmentStatus: source?.fbaAppointmentStatus || '', vehicleAppointmentStatus: source?.vehicleAppointmentStatus || '', estimatedArrivalAt: source?.estimatedArrivalAt || '' }); mutationError.value = null; editOpen.value = true } -function openTransfer(row: DispatchListItem) { if (!canTransfer(row)) return; transferRow.value = row; Object.assign(transferForm, { plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: row.carrierName === '—' ? '' : row.carrierName, pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: row.driverName === '—' ? '' : row.driverName, driverPhone: row.driverPhone === '—' ? '' : row.driverPhone, vehiclePlate: row.vehiclePlate === '—' ? '' : row.vehiclePlate, appointmentAccount: '', sellerName: '', deliveryFee: undefined, loadingFee: undefined, appointmentFee: undefined, remark: '' }); mutationError.value = null; transferOpen.value = true } +function openTransfer(row: DispatchListItem) { if (!canTransfer(row)) return; transferRow.value = row; Object.assign(transferForm, { plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: row.carrierName === '—' ? '' : row.carrierName, pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: row.driverName === '—' ? '' : row.driverName, driverPhone: row.driverPhone === '—' ? '' : row.driverPhone, vehiclePlate: row.vehiclePlate === '—' ? '' : row.vehiclePlate, appointmentAccount: '', sellerName: '', deliveryFee: undefined, loadingFee: undefined, appointmentFee: undefined, cartonMarksJson: '', remark: '' }); mutationError.value = null; transferOpen.value = true } async function submitEdit() { if (!editRow.value || !canEdit(editRow.value) || editSaving.value) return; editSaving.value = true; mutationError.value = null; try { await updateDispatch(editRow.value.dispatchNo, { vehiclePlate: editForm.vehiclePlate.trim() || null, driverName: editForm.driverName.trim() || null, driverPhone: editForm.driverPhone.trim() || null, carrierName: editForm.carrierName.trim() || null, route: editForm.route.trim() || null, departAt: editForm.departAt || null, fbaAppointmentNo: editForm.fbaAppointmentNo.trim() || null, fbaAppointmentStatus: editForm.fbaAppointmentStatus || null, vehicleAppointmentStatus: editForm.vehicleAppointmentStatus || null, estimatedArrivalAt: editForm.estimatedArrivalAt || null }); editOpen.value = false; ElMessage.success('排车信息已保存'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '排车信息保存失败,请重试') } finally { editSaving.value = false } } async function cancel(row: DispatchListItem) { if (!canCancel(row)) return; try { await ElMessageBox.confirm(`确认取消排车单 ${row.dispatchNo}?取消后将释放托盘锁。`, '取消排车', { confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }) } catch { return } mutationError.value = null; try { await cancelDispatch(row.dispatchNo); ElMessage.success('排车单已取消'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '排车取消失败,请重试') } } function handleMore(command: string, row: DispatchListItem) { if (command === 'edit') openEdit(row); else if (command === 'cancel') void cancel(row) } @@ -202,8 +202,12 @@ async function submitTransfer() { if (isBuyAppointment.value && (!transferForm.sellerName.trim() || transferForm.appointmentFee == null)) { ElMessage.warning('买约时请填写卖家姓名和买约费用'); return } const feeInputs = [['DELIVERY_FEE', transferForm.deliveryFee, '派送费用'], ['LOADING_FEE', needsLoadingFee.value ? transferForm.loadingFee : undefined, '装车费'], ['APPOINTMENT_FEE', isBuyAppointment.value ? transferForm.appointmentFee : undefined, '买约费用']] as const const fees = feeInputs.filter(([, amount]) => amount != null).map(([feeType, amount, remark]) => ({ feeType, currency: 'USD', amount: Number(amount), remark })) + let palletCartonMarks: Record | undefined + if (transferForm.cartonMarksJson.trim()) { + try { const parsed = JSON.parse(transferForm.cartonMarksJson); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error(); palletCartonMarks = Object.fromEntries(Object.entries(parsed).map(([pallet, marks]) => [pallet, Array.isArray(marks) ? marks.map((mark) => String(mark).trim()).filter(Boolean) : []])) } catch { ElMessage.warning('托盘箱唛分配格式不正确,请使用 JSON:{"托盘号":["箱唛1","箱唛2"]}'); return } + } transferSaving.value = true - try { await transferDispatchToDelivery(transferRow.value.dispatchNo, { idempotencyKey: `web-transfer-${transferRow.value.dispatchNo}-${Date.now()}`, plannedDeliveryAt: transferForm.plannedDeliveryAt, deliveryMethod: transferForm.deliveryMethod, waybillNo: transferForm.waybillNo.trim() || undefined, carrierName: transferForm.carrierName.trim() || undefined, pickupMethod: transferForm.pickupMethod, loadingType: transferForm.loadingType, vehicleType: transferForm.vehicleType || undefined, vehicleSize: transferForm.vehicleSize || undefined, driverName: transferForm.driverName.trim() || undefined, driverPhone: transferForm.driverPhone.trim() || undefined, vehiclePlate: transferForm.vehiclePlate.trim() || undefined, appointmentAccount: transferForm.appointmentAccount || undefined, sellerName: isBuyAppointment.value ? transferForm.sellerName.trim() : undefined, remark: transferForm.remark.trim() || undefined, fees }); transferOpen.value = false; ElMessage.success('已生成出库单和派送单'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '转派送失败,请重试') } finally { transferSaving.value = false } + try { await transferDispatchToDelivery(transferRow.value.dispatchNo, { idempotencyKey: `web-transfer-${transferRow.value.dispatchNo}-${Date.now()}`, plannedDeliveryAt: transferForm.plannedDeliveryAt, deliveryMethod: transferForm.deliveryMethod, waybillNo: transferForm.waybillNo.trim() || undefined, carrierName: transferForm.carrierName.trim() || undefined, pickupMethod: transferForm.pickupMethod, loadingType: transferForm.loadingType, vehicleType: transferForm.vehicleType || undefined, vehicleSize: transferForm.vehicleSize || undefined, driverName: transferForm.driverName.trim() || undefined, driverPhone: transferForm.driverPhone.trim() || undefined, vehiclePlate: transferForm.vehiclePlate.trim() || undefined, appointmentAccount: transferForm.appointmentAccount || undefined, sellerName: isBuyAppointment.value ? transferForm.sellerName.trim() : undefined, remark: transferForm.remark.trim() || undefined, fees, palletCartonMarks }); transferOpen.value = false; ElMessage.success('已生成出库单和派送单'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '转派送失败,请重试') } finally { transferSaving.value = false } } onMounted(load) @@ -219,7 +223,7 @@ onMounted(load) -

派送信息

费用信息(USD)

+

派送信息

入库未逐箱绑定时,在出库前确认箱唛属于哪个实际托盘。

费用信息(USD)

diff --git a/src/modules/overseas-warehouse/pages/OutboundDetailView.vue b/src/modules/overseas-warehouse/pages/OutboundDetailView.vue index 22d4dc93..3fb029db 100644 --- a/src/modules/overseas-warehouse/pages/OutboundDetailView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundDetailView.vue @@ -18,10 +18,12 @@ import { overseasWarehousePermissions as permission } from '../permissions' const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore() const outboundNo = computed(() => String(route.params.outboundNo || '').trim()) const detail = ref(null); const loading = ref(false); const cancelling = ref(false) +const palletRows = computed(() => detail.value?.pallets || []) const loadError = ref(null); const mutationError = ref(null) const statusLabels: Record = { WAIT_OUTBOUND: '待出库', PICKING: '拣货中', PICKED: '已拣货', OUTBOUNDED: '已出库', CANCELLED: '已取消' } function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' } function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) } +function marks(value: unknown) { return String(value || '').split(',').map((item) => item.trim()).filter(Boolean) } const canCancel = computed(() => access.can(permission.outboundCancel) && detail.value?.outbound.status === 'PICKED') async function load() { if (!outboundNo.value) { loadError.value = { message: '详情缺少出库单号,请从出库单列表重新进入' }; return } loading.value = true; loadError.value = null; try { detail.value = await getOutbound(outboundNo.value) } catch (cause) { detail.value = null; loadError.value = getApiErrorDetails(cause, '出库单详情加载失败,请重试') } finally { loading.value = false } } async function cancel() { if (!canCancel.value || cancelling.value) return; let reason = ''; try { const result = await ElMessageBox.prompt(`确认取消出库单 ${outboundNo.value}?`, '取消出库单', { inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return } cancelling.value = true; mutationError.value = null; try { await cancelOutbound(outboundNo.value, reason); ElMessage.success('出库单已取消'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败,请重试') } finally { cancelling.value = false } } @@ -34,7 +36,8 @@ watch(outboundNo, () => { void load() }, { immediate: true }) - + diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 11f25ca3..6cbdb53d 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -21,7 +21,7 @@ import { overseasWarehousePermissions as permission } from '../permissions' const router = useRouter(); const access = useAccessPreviewStore() const rows = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const error = ref(null); const mutationError = ref(null) -const filters = ref({ documentNo: '', lxCode: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref(); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref>([]); const inventoryLoading = ref(false) +const filters = ref({ documentNo: '', lxCode: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref(); const cartonMarks = ref(''); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref>([]); const inventoryLoading = ref(false) const customerOptions = computed(() => { const customers = new Map() positionOptions.value.forEach(({ balance }) => customers.set(balance.customerId, balance.customerName?.trim() || '客户名称待补充')) @@ -34,7 +34,7 @@ const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库 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, lxCode: String(filters.value.lxCode || '').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; selectedCustomerId.value = ''; 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 } } function selectCustomer() { selectedPositionId.value = ''; quantity.value = undefined } -async function submit() { const selected = availablePositionOptions.value.find(item => item.position.id === selectedPositionId.value); if (!selectedCustomerId.value || !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(selectedCustomerId.value, { 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 submit() { const selected = availablePositionOptions.value.find(item => item.position.id === selectedPositionId.value); const marks = cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean); if (!selectedCustomerId.value || !recipient.value.trim() || !selected || !quantity.value || quantity.value < 1) { ElMessage.warning('请选择客户,填写收件信息并选择托盘和出库数量'); return }; if (quantity.value > selected.position.availablePackages) { ElMessage.warning('出库数量不能超过可用库存'); return }; if (marks.length && marks.length !== quantity.value) { ElMessage.warning('指定箱唛数量必须等于出库数量'); return }; saving.value = true; try { await createOneClickDropshipOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), items: [{ inventoryPositionId: selected.position.id, packages: quantity.value, sku: selected.balance.skuCode || undefined, cartonMarks: marks.length ? marks : 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 } } function openDetail(row: OutboundRow) { void router.push({ name: 'overseas-outbound-detail', params: { outboundNo: row.outboundNo } }) } function canCancel(row: OutboundRow) { return access.can(permission.outboundCancel) && row.status === 'PICKED' } async function cancel(row: OutboundRow) { if (!canCancel(row)) return; 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 }; mutationError.value = null; try { await cancelOutbound(row.outboundNo, reason); ElMessage.success('出库单已取消,库存及关联单据已回滚'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败') } } @@ -53,9 +53,9 @@ onMounted(load) - +
填写后按指定箱唛出库,数量必须与出库数量一致。
diff --git a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue new file mode 100644 index 00000000..e0f23423 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue @@ -0,0 +1,55 @@ + + + + diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index 7045a30a..e323bdd8 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -25,6 +25,8 @@ export const overseasWarehousePermissions = { inventoryExport: 'platform:overseas-warehouse:inventory:export', inventoryEdit: 'platform:overseas-warehouse:inventory:edit', inventoryAdjust: 'platform:overseas-warehouse:inventory:adjust', + returnInventoryList: 'platform:overseas-warehouse:return-inventory:list', + returnCartonsList: 'platform:overseas-warehouse:return-cartons:list', relabelOrderList: 'platform:overseas-warehouse:relabel-order:list', relabelOrderCreate: 'platform:overseas-warehouse:relabel-order:create', relabelOrderStatus: 'platform:overseas-warehouse:relabel-order:status', diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts index de4bd8f5..4394c76d 100644 --- a/src/modules/overseas-warehouse/routes.ts +++ b/src/modules/overseas-warehouse/routes.ts @@ -80,6 +80,20 @@ export const overseasWarehouseRoutes = [ component: () => import('./pages/InventoryView.vue'), meta: { title: '库存管理', description: '查看海外仓库存汇总、可用量和库内分布', permission: permission.inventoryList }, }, + { + path: 'overseas-warehouse/return-inventory', + name: 'overseas-return-inventory', + component: () => import('./pages/ReturnInventoryManagementView.vue'), + props: { mode: 'inventory' }, + meta: { title: '退货库存管理', description: '按客户和 SKU 查询退货签收后的可用库存', permission: permission.returnInventoryList }, + }, + { + path: 'overseas-warehouse/return-cartons', + name: 'overseas-return-cartons', + component: () => import('./pages/ReturnInventoryManagementView.vue'), + props: { mode: 'cartons' }, + meta: { title: '退货箱号管理', description: '查询合箱后的箱号、箱规和箱内 SKU', permission: permission.returnCartonsList }, + }, { path: 'overseas-warehouse/inventory-transactions', name: 'overseas-inventory-transactions',