From a917216c3bf2a6b6d00d9ec4866c6711ff468634 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 20 Oct 2026 11:23:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E8=BF=90=E5=8D=95/?= =?UTF-8?q?=E4=BB=A3=E5=8F=91=E8=BF=90=E5=8D=95=E5=BC=B9=E7=AA=97=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E5=9F=BA=E6=9C=AC=E4=BF=A1=E6=81=AF=E4=B8=8E=E4=B8=8B?= =?UTF-8?q?=E5=8D=95=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/locale/en-US.ts | 2 + src/locale/zh-CN.ts | 1 + .../CreateLogisticsModal.vue | 526 ++++++++++++++++-- 3 files changed, 474 insertions(+), 55 deletions(-) diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index c40dcb7..fe7a24a 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -1058,6 +1058,8 @@ export default { 'order.platformStoreOrder.createLogistics.tip': 'Review recipient and line items for each order, then confirm the shipping product before submitting.', 'order.platformStoreOrder.createLogistics.product': 'Shipping Product', 'order.platformStoreOrder.createLogistics.productRequired': 'Please select a shipping product', + 'order.platformStoreOrder.createLogistics.basicRequired': + 'Please complete waybill basic info (warehouse, cargo type, weight, dimensions, inbound method, etc.)', 'order.platformStoreOrder.createLogistics.orderList': 'Selected Orders', 'order.platformStoreOrder.createLogistics.receiverTitle': 'Recipient Check', 'order.platformStoreOrder.createLogistics.linesTitle': 'Line Items', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 5478774..67e1dba 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -1047,6 +1047,7 @@ export default { 'order.platformStoreOrder.createLogistics.tip': '请逐单核对收件人与商品明细,确认运输方式后再提交,避免下错单。', 'order.platformStoreOrder.createLogistics.product': '运输方式', 'order.platformStoreOrder.createLogistics.productRequired': '请选择运输方式', + 'order.platformStoreOrder.createLogistics.basicRequired': '请完整填写运单基本信息(仓库、货物属性、实重、长宽高、到仓方式等)', 'order.platformStoreOrder.createLogistics.orderList': '待创建清单', 'order.platformStoreOrder.createLogistics.receiverTitle': '收件人校对', 'order.platformStoreOrder.createLogistics.linesTitle': '商品明细', diff --git a/src/views/order/platformStoreOrder/CreateLogisticsModal.vue b/src/views/order/platformStoreOrder/CreateLogisticsModal.vue index e659513..699e6b4 100644 --- a/src/views/order/platformStoreOrder/CreateLogisticsModal.vue +++ b/src/views/order/platformStoreOrder/CreateLogisticsModal.vue @@ -13,13 +13,33 @@ import { postOrderStockBindInventoryCandidates, postOrderStockBindParcelCandidates, } from '@/api/order' +import { getSenderAddressList } from '@/api/order/senderAddress' +import { getExportQualificationList } from '@/api/customer/exportQualification' +import { getDictListAll } from '@/api/system/dict' import { resolveOmsAssetUrl } from '@/api/oms/common' import SlidingTabs from '@/components/SlidingTabs/index.vue' import { $baseMessage } from '@/utils' +import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute' import { normalizeListRow, pickField } from '@/utils/warehouseListMapper' import type { CreateLogisticsMode } from './constants' +type SelectOption = { label: string; value: string } +type QualOption = { id: number; label: string; isDefault: boolean } + +const SAMPLE_EXPORT_CUSTOMS_MODE_CODE = '9610Simple' +const EXPORT_CUSTOMS_MODE_9610_SUMMARY = '9610Summary' +const AGREEMENT_TEXT = `《物流下单用户协议》及《物流服务赔偿协议》 + +一、服务说明 +您确认通过本系统创建的运单/代发订单,将按所选运输方式、仓库与件重尺计费并履约。 + +二、责任与理赔 +请以正式协议文本为准;勾选即视为已阅读并同意全部条款。 + +三、勾选确认 +本人已仔细阅读并充分理解全部条款,自愿接受本协议所有约定。` + export type { CreateLogisticsMode } export type BindDraftItem = { @@ -43,6 +63,8 @@ type OrderDraft = { customerId?: number warehouseId?: number platformOrderNo: string + /** 可编辑客户单号,默认平台订单号 */ + clientNo: string orderCountry: string recipientName: string recipientPhone: string @@ -103,6 +125,28 @@ const batchMatchDeclaration = ref(false) const batchQtyPerOrder = ref(1) let searchTimer: ReturnType | null = null +/** 运单基本信息(对齐下单页) */ +const cargoAttribute = ref('general') +const actualWeight = ref(undefined) +const quantity = ref(1) +const lengthCm = ref(10) +const widthCm = ref(10) +const heightCm = ref(10) +const exportCustomsMode = ref(SAMPLE_EXPORT_CUSTOMS_MODE_CODE) +const exportQualificationId = ref() +const platformCode = ref() +const inboundMethod = ref<'selfShip' | 'doorPickup'>('selfShip') +const senderAddressId = ref() +const expectPickupTime = ref() +const agreementChecked = ref(false) +const agreementModalVisible = ref(false) +const dictLoading = ref(false) +const cargoAttributeOptions = ref([]) +const exportCustomsModeOptions = ref([]) +const ecommercePlatformOptions = ref([]) +const exportQualificationOptions = ref([]) +const pickupAddressOptions = ref>([]) + const modalVisible = computed({ get: () => props.visible, set: (v: boolean) => emit('update:visible', v), @@ -167,17 +211,172 @@ const warehouseLabel = computed(() => { return opt?.label || '-' }) -const canSubmit = computed(() => { +const needExportQualification = computed( + () => + String(exportCustomsMode.value ?? '').trim().toLowerCase() === + EXPORT_CUSTOMS_MODE_9610_SUMMARY.toLowerCase(), +) + +const basicInfoReady = computed(() => { if (!productCode.value) return false + if (!warehouseId.value) return false + if (!String(cargoAttribute.value || '').trim()) return false + if (!(Number(actualWeight.value) > 0)) return false + if (!(Number(quantity.value) > 0)) return false + if (!(Number(lengthCm.value) > 0) || !(Number(widthCm.value) > 0) || !(Number(heightCm.value) > 0)) { + return false + } + if (needExportQualification.value) { + if (!(Number(exportQualificationId.value) > 0)) return false + if (!String(platformCode.value || '').trim()) return false + } + if (inboundMethod.value === 'doorPickup') { + if (!(Number(senderAddressId.value) > 0)) return false + if (!expectPickupTime.value) return false + } + if (drafts.value.some((d) => !String(d.clientNo || '').trim())) return false + return true +}) + +const canSubmit = computed(() => { + if (!basicInfoReady.value) return false + if (!agreementChecked.value) return false if (drafts.value.length === 0) return false if (drafts.value.some((d) => d.receiverMissing || d.linesMissing)) return false if (isFulfill.value) { - if (!warehouseId.value) return false if (drafts.value.some((d) => d.bindItems.length === 0)) return false } return true }) +function resolveExportCustomsMode9610Simple() { + const opts = exportCustomsModeOptions.value + const byCode = opts.find( + (o) => o.value === SAMPLE_EXPORT_CUSTOMS_MODE_CODE || o.value.toLowerCase() === '9610simple', + ) + if (byCode) return byCode.value + const byLabel = opts.find((o) => /9610.*简易|简易.*9610/i.test(o.label)) + if (byLabel) return byLabel.value + return SAMPLE_EXPORT_CUSTOMS_MODE_CODE +} + +function mapDictOptions(rows: any[]): SelectOption[] { + return (rows || []) + .map((row) => { + const value = String(row?.dictCode ?? row?.DictCode ?? '').trim() + const label = String(row?.dictName ?? row?.DictName ?? value).trim() + if (!value) return null + return { label: label || value, value } + }) + .filter((x): x is SelectOption => !!x) +} + +async function loadBasicDicts() { + dictLoading.value = true + try { + const [cargoRows, exportRows, platformRows] = await Promise.all([ + getDictListAll({ dictType: 'cargo_attribute' }), + getDictListAll({ dictType: 'export_customs_mode' }), + getDictListAll({ dictType: 'e-commerce_platform' }), + ]) + cargoAttributeOptions.value = mapDictOptions(cargoRows as any[]) + exportCustomsModeOptions.value = mapDictOptions(exportRows as any[]) + ecommercePlatformOptions.value = mapDictOptions(platformRows as any[]) + if (!cargoAttribute.value && cargoAttributeOptions.value[0]) { + cargoAttribute.value = normalizeCargoAttributeCode(cargoAttributeOptions.value[0].value) + } else { + cargoAttribute.value = normalizeCargoAttributeCode(cargoAttribute.value) + } + exportCustomsMode.value = resolveExportCustomsMode9610Simple() + } catch { + cargoAttributeOptions.value = [ + { label: '普货', value: 'general' }, + { label: '带电', value: 'electronics' }, + { label: '敏感', value: 'sensitive' }, + ] + exportCustomsModeOptions.value = [ + { label: '9610 简易申报', value: SAMPLE_EXPORT_CUSTOMS_MODE_CODE }, + { label: '9610 汇总申报', value: EXPORT_CUSTOMS_MODE_9610_SUMMARY }, + ] + } finally { + dictLoading.value = false + } +} + +async function loadExportQualifications() { + const customerId = resolveCandidatesCustomerId() + try { + const res = await getExportQualificationList({ + page: 1, + pageSize: 100, + customerId, + }) + exportQualificationOptions.value = (res.items || []) + .map((item: Record) => { + const id = Number(pickField(item, 'id', 'Id') ?? 0) + if (!id) return null + const companyName = String(pickField(item, 'companyName', 'CompanyName') ?? '').trim() + const creditCode = String(pickField(item, 'socialCreditCode', 'SocialCreditCode') ?? '').trim() + const isDefault = Boolean(pickField(item, 'isDefault', 'IsDefault')) + const base = companyName || creditCode || `#${id}` + return { id, label: isDefault ? `${base}(默认)` : base, isDefault } + }) + .filter((x): x is QualOption => !!x) + if (!exportQualificationId.value) { + const def = + exportQualificationOptions.value.find((x) => x.isDefault) || exportQualificationOptions.value[0] + exportQualificationId.value = def?.id + } + } catch { + exportQualificationOptions.value = [] + } +} + +async function loadPickupAddresses() { + const customerId = resolveCandidatesCustomerId() + try { + const res = await getSenderAddressList({ + page: 1, + pageSize: 200, + addressType: 'pickup', + customerId, + }) + pickupAddressOptions.value = (res.items || []) + .map((item: Record) => { + const id = Number(pickField(item, 'id', 'Id') ?? 0) + if (!id) return null + const name = String(pickField(item, 'contactName', 'ContactName', 'name', 'Name') ?? '').trim() + const phone = String(pickField(item, 'phone', 'Phone') ?? '').trim() + const label = [name, phone].filter(Boolean).join(' · ') || `#${id}` + return { label, value: id } + }) + .filter((x): x is { label: string; value: number } => !!x) + } catch { + pickupAddressOptions.value = [] + } +} + +function openAgreementModal() { + agreementModalVisible.value = true +} + +function buildParcelBasicPayload() { + return { + cargoAttribute: normalizeCargoAttributeCode(cargoAttribute.value), + actualWeight: Number(actualWeight.value), + quantity: Number(quantity.value), + length: Number(lengthCm.value), + width: Number(widthCm.value), + height: Number(heightCm.value), + exportCustomsMode: String(exportCustomsMode.value || '').trim() || resolveExportCustomsMode9610Simple(), + exportQualificationId: needExportQualification.value ? exportQualificationId.value : undefined, + platformCode: needExportQualification.value ? platformCode.value : undefined, + inboundMethod: inboundMethod.value, + senderAddressId: inboundMethod.value === 'doorPickup' ? senderAddressId.value : undefined, + expectPickupTime: inboundMethod.value === 'doorPickup' ? expectPickupTime.value : undefined, + } +} + function close() { emit('update:visible', false) } @@ -208,11 +407,13 @@ function mapDetail(raw: Record): OrderDraft { const shipLines = lines const customerIdRaw = Number(pickField(row, 'customerId', 'CustomerId') ?? 0) const warehouseIdRaw = Number(pickField(row, 'warehouseId', 'WarehouseId') ?? 0) + const platformOrderNo = String(pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? '') return { storeOrderId: Number(pickField(row, 'id', 'Id')), customerId: Number.isFinite(customerIdRaw) && customerIdRaw > 0 ? customerIdRaw : undefined, warehouseId: Number.isFinite(warehouseIdRaw) && warehouseIdRaw > 0 ? warehouseIdRaw : undefined, - platformOrderNo: String(pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? ''), + platformOrderNo, + clientNo: platformOrderNo, orderCountry: String(pickField(row, 'orderCountry', 'OrderCountry') ?? ''), recipientName, recipientPhone: String(pickField(row, 'recipientPhone', 'RecipientPhone') ?? ''), @@ -525,6 +726,10 @@ function buildBindPayload(items: BindDraftItem[]) { async function handleSubmit(): Promise { if (!canSubmit.value) { + if (!agreementChecked.value) { + $baseMessage(t('order.placeOrder.agreement.required'), 'warning') + return false + } if (!productCode.value) { $baseMessage( isFulfillWms.value @@ -534,10 +739,14 @@ async function handleSubmit(): Promise { ) return false } - if (isFulfill.value && !warehouseId.value) { + if (!warehouseId.value) { $baseMessage(t('order.platformStoreOrder.message.warehouseRequired'), 'warning') return false } + if (!basicInfoReady.value) { + $baseMessage(t('order.platformStoreOrder.createLogistics.basicRequired'), 'warning') + return false + } const bad = drafts.value.find( (d) => d.receiverMissing || d.linesMissing || (isFulfill.value && d.bindItems.length === 0), ) @@ -579,12 +788,14 @@ async function handleSubmit(): Promise { try { const items = drafts.value.map((d) => ({ storeOrderId: d.storeOrderId, + clientNo: String(d.clientNo || '').trim() || undefined, bindItems: isFulfill.value ? buildBindPayload(d.bindItems) : undefined, })) const body = { productCode: productCode.value, warehouseId: warehouseId.value, customerId: props.customerId, + parcelBasic: buildParcelBasicPayload(), items, } const result = isFulfillWms.value @@ -636,7 +847,29 @@ watch( batchMode.value = props.storeOrderIds.length > 1 batchMatchDeclaration.value = false batchQtyPerOrder.value = 1 + cargoAttribute.value = 'general' + actualWeight.value = undefined + quantity.value = 1 + lengthCm.value = 10 + widthCm.value = 10 + heightCm.value = 10 + exportCustomsMode.value = SAMPLE_EXPORT_CUSTOMS_MODE_CODE + exportQualificationId.value = undefined + platformCode.value = undefined + inboundMethod.value = 'selfShip' + senderAddressId.value = undefined + expectPickupTime.value = undefined + agreementChecked.value = false + await loadBasicDicts() await loadDrafts() + // 实重默认按订单申报行估算(取首单) + const first = drafts.value[0] + if (first && !(Number(actualWeight.value) > 0)) { + const est = first.lines.reduce((s, x) => s + Math.max(1, Number(x.quantity) || 0) * 0.01, 0) + actualWeight.value = Number(est.toFixed(3)) || 0.01 + } + await loadExportQualifications() + await loadPickupAddresses() if (isFulfill.value) await loadCandidates() }, { immediate: true }, @@ -652,14 +885,24 @@ watch(leftTab, () => { if (!props.visible || !isFulfill.value) return void loadCandidates() }) + +watch(needExportQualification, (need) => { + if (!props.visible || !need) return + void loadExportQualifications() +}) + +watch(inboundMethod, (method) => { + if (!props.visible || method !== 'doorPickup') return + void loadPickupAddresses() +})