diff --git a/docs/应收应付 b/docs/应收应付 index ccb85d1..a92b859 100644 --- a/docs/应收应付 +++ b/docs/应收应付 @@ -229,8 +229,10 @@ flowchart TD | `POST /operation/outbound/create` | 出库:不写费用 | | `GET /order/weight/downstream-import-template` | 对账件重尺导入模板 | | `POST /order/weight/downstream-import` | 导入 downstream 重尺并锁定渠道重算应付 | -| `POST /order/fee/recalculate-receivable` | 重算应收(已支付拒绝) | -| `POST /order/fee/recalculate-payable` | 重算应付(已有渠道时锁定渠道,不换渠道) | +| `POST /order/fee/recalculate-receivable` | 重算应收(已支付拒绝;沿用订单绑定价格/规则版本) | +| `POST /order/fee/recalculate-payable` | 重算应付(已有渠道时锁定渠道;允许渠道不在旧规则排货池,只重算买价) | +| `POST /order/fee/bind-snapshot-versions` | 显式切换订单绑定的规则/价格版本(可选随后重算应收/应付) | +| `POST /order/fee/reallocate` | 重新排货(取当前有效规则;成功后写回订单规则版本) | | `POST /order/fee/rerun-inbound-billing` | 重跑入库计费(已支付拒绝,应收已锁) | | `POST /order/fee/add` | 手工加费(应收已支付拒绝) | | `POST /order/additional-services/update` | 未支付改附加服务并重算 | diff --git a/src/api/order/index.ts b/src/api/order/index.ts index ce236ee..3246ae7 100644 --- a/src/api/order/index.ts +++ b/src/api/order/index.ts @@ -2152,6 +2152,25 @@ export async function postOrderRecalculatePayable(body: OrderIdRequest): Promise return res.data?.data ?? res.data?.Data ?? res.data } +/** POST /order/fee/bind-snapshot-versions — 切换订单绑定的规则/价格版本 */ +export async function postOrderBindSnapshotVersions(body: { + id: number | string + rulesVersion?: string + priceVersion?: string + recalculateReceivable?: boolean + recalculatePayable?: boolean +}): Promise { + const res = await axiosInstance.post(orderUrl('/order/fee/bind-snapshot-versions'), { + id: Number(body.id), + rulesVersion: body.rulesVersion || undefined, + priceVersion: body.priceVersion || undefined, + recalculateReceivable: !!body.recalculateReceivable, + recalculatePayable: !!body.recalculatePayable, + }) + assertBusinessOk(res.data) + return res.data?.data ?? res.data?.Data ?? res.data +} + /** POST /order/fee/rerun-inbound-billing — 重跑入库计费(应收+排货应付) */ export async function postOrderRerunInboundBilling(body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/fee/rerun-inbound-billing'), { id: body.id }) diff --git a/src/constants/pageButtonCatalog.ts b/src/constants/pageButtonCatalog.ts index 85b1607..cdb0786 100644 --- a/src/constants/pageButtonCatalog.ts +++ b/src/constants/pageButtonCatalog.ts @@ -175,6 +175,7 @@ export const PERMISSION_ACTION_LABELS: Record = { AddFee: '添加费用', RecalculateReceivable: '重算应收', RecalculatePayable: '重算应付', + BindSnapshotVersions: '切换版本', ImportDownstreamWeight: '导入对账件重尺', AddDeclarationRow: '添加申报行', DownstreamForecastResult: '下游预报结果', @@ -537,6 +538,7 @@ const CORE_GROUPS: PageButtonGroup[] = [ { code: 'AddFee', name: '添加费用' }, { code: 'RecalculateReceivable', name: '重算应收' }, { code: 'RecalculatePayable', name: '重算应付' }, + { code: 'BindSnapshotVersions', name: '切换版本' }, { code: 'AddDeclarationRow', name: '添加申报行' }, ]), B('Order.FulfillOrders', [ @@ -573,6 +575,7 @@ const CORE_GROUPS: PageButtonGroup[] = [ { code: 'AddFee', name: '添加费用' }, { code: 'RecalculateReceivable', name: '重算应收' }, { code: 'RecalculatePayable', name: '重算应付' }, + { code: 'BindSnapshotVersions', name: '切换版本' }, { code: 'ImportDownstreamWeight', name: '导入对账件重尺' }, { code: 'AddDeclarationRow', name: '添加申报行' }, ]), diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index e169002..a532149 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -1876,7 +1876,20 @@ export default { 'order.waybillManage.table.id': 'Order ID', 'order.waybillManage.table.productCode': 'Product code', 'order.waybillManage.table.channelCode': 'Channel code', + 'order.waybillManage.table.downstreamSupplierName': 'Service provider', 'order.waybillManage.table.downstreamSupplierCode': 'Downstream supplier', + 'order.waybillManage.table.maxTimeliness': 'Max transit days', + 'order.waybillManage.table.salesProductRulesVersion': 'Rules version', + 'order.waybillManage.table.salesProductPriceVersion': 'Price version', + 'order.waybillManage.table.forecastReceivableAmount': 'Forecast receivable', + 'order.waybillManage.table.forecastPayableAmount': 'Forecast payable', + 'order.waybillManage.table.hasInsurance': 'Insured', + 'order.waybillManage.table.hasAllocationTrace': 'Has allocation trace', + 'order.waybillManage.table.allocationScheme': 'Allocation scheme', + 'order.waybillManage.table.exportQualificationId': 'Export qualification ID', + 'order.waybillManage.table.pickupOrderId': 'Pickup order ID', + 'order.waybillManage.table.stockBindLineCount': 'Bind line count', + 'order.waybillManage.table.primarySkuCodes': 'Bound SKUs', 'order.waybillManage.table.downstreamAppKey': 'Downstream AppKey', 'order.waybillManage.table.downstreamAppId': 'Downstream AppId', 'order.waybillManage.table.channelForecastCode': 'Forecast code', @@ -2205,6 +2218,8 @@ export default { 'order.myOrders.drawer.productName': 'Sales Product', 'order.myOrders.drawer.channelCode': 'Channel Code', 'order.myOrders.drawer.channelName': 'Service Channel', + 'order.myOrders.drawer.downstreamSupplierName': 'Service provider', + 'order.myOrders.drawer.downstreamSupplierCode': 'Provider code', 'order.myOrders.allocationTrace.view': 'View allocation process', 'order.myOrders.allocationTrace.title': 'Allocation process', 'order.myOrders.allocationTrace.fromSnapshot': 'Historical snapshot (at allocation time)', @@ -2371,6 +2386,25 @@ export default { 'order.myOrders.drawer.actions.cancel': 'Cancel', 'order.myOrders.drawer.actions.recalculateReceivable': 'Recalc Receivable', 'order.myOrders.drawer.actions.recalculatePayable': 'Recalc Payable', + 'order.myOrders.drawer.actions.bindSnapshotVersions': 'Switch Versions', + 'order.myOrders.drawer.fee.rulesVersion': 'Rules version', + 'order.myOrders.drawer.fee.priceVersion': 'Price version', + 'order.myOrders.drawer.bindSnapshot.title': 'Switch price/rules versions', + 'order.myOrders.drawer.bindSnapshot.hint': + 'Explicitly switch the publish versions bound to this order. Recalculation follows these bindings.', + 'order.myOrders.drawer.bindSnapshot.rulesVersion': 'Rules version', + 'order.myOrders.drawer.bindSnapshot.priceVersion': 'Price version', + 'order.myOrders.drawer.bindSnapshot.boundAs': 'Bound: {version}', + 'order.myOrders.drawer.bindSnapshot.keepUnchanged': 'Keep current', + 'order.myOrders.drawer.bindSnapshot.useCurrent': 'Current effective publish', + 'order.myOrders.drawer.bindSnapshot.currentBound': 'current binding', + 'order.myOrders.drawer.bindSnapshot.recalcReceivable': 'Recalc receivable after bind', + 'order.myOrders.drawer.bindSnapshot.recalcPayable': 'Recalc payable after bind', + 'order.myOrders.drawer.bindSnapshot.needVersion': 'Select at least a rules or price version', + 'order.myOrders.drawer.bindSnapshot.noProduct': 'Order has no sales product', + 'order.myOrders.drawer.bindSnapshot.success': 'Versions updated', + 'order.myOrders.drawer.bindSnapshot.failed': 'Failed to switch versions', + 'order.myOrders.drawer.bindSnapshot.loadFailed': 'Failed to load publish versions', 'order.myOrders.drawer.actions.addFee': 'Add Fee', 'order.myOrders.drawer.actions.additionalServices': 'Additional services', 'order.myOrders.drawer.editBasicTitle': 'Edit Order Details', @@ -2402,7 +2436,24 @@ export default { 'order.myOrders.drawer.fee.diff': 'Delta', 'order.myOrders.drawer.fee.negativeGrossProfitWarn': 'Negative gross profit detected — review cost and pricing', 'order.myOrders.drawer.fee.additionalServicesTitle': 'Additional services', + 'order.myOrders.drawer.fee.additionalServicesManageTitle': 'Manage additional services', + 'order.myOrders.drawer.fee.additionalServicesViewTitle': 'View additional services', 'order.myOrders.drawer.fee.additionalServicesEmpty': 'No additional services for this product', + 'order.myOrders.drawer.fee.additionalServicesNone': 'None', + 'order.myOrders.drawer.fee.additionalServicesManage': 'Manage', + 'order.myOrders.drawer.fee.additionalServicesView': 'View', + 'order.myOrders.drawer.fee.additionalServicesSave': 'Save & recalc', + 'order.myOrders.drawer.fee.additionalServicesClose': 'Close', + 'order.myOrders.drawer.fee.additionalServicesHint': + 'Check or uncheck product-allowed services. Saving recalculates receivable (unpaid only).', + 'order.myOrders.drawer.fee.additionalServicesPaidHint': + 'Order is paid; additional services cannot be changed.', + 'order.myOrders.drawer.fee.additionalServicesCurrent': 'Currently bound', + 'order.myOrders.drawer.fee.additionalServicesPick': 'Available services', + 'order.myOrders.drawer.fee.additionalServicesOrphan': + 'These are no longer on the product and will be removed on save', + 'order.myOrders.drawer.fee.additionalServicesOrphanCleared': + 'Removed out-of-scope services: {names}', 'order.myOrders.drawer.fee.additionalServicesSuccess': 'Additional services updated', 'order.myOrders.drawer.fee.additionalServicesFailed': 'Failed to update additional services', 'order.myOrders.drawer.fee.addTitle': 'Add Fee', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 9c000a6..4b1a7c6 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -1808,10 +1808,23 @@ export default { 'order.waybillManage.table.id': '订单ID', 'order.waybillManage.table.productCode': '产品编码', 'order.waybillManage.table.channelCode': '渠道编码', + 'order.waybillManage.table.downstreamSupplierName': '服务商名称', 'order.waybillManage.table.downstreamSupplierCode': '下游服务商编码', 'order.waybillManage.table.downstreamAppKey': '下游AppKey', 'order.waybillManage.table.downstreamAppId': '下游AppId', 'order.waybillManage.table.channelForecastCode': '渠道预报编码', + 'order.waybillManage.table.maxTimeliness': '最大时效(天)', + 'order.waybillManage.table.salesProductRulesVersion': '规则版本', + 'order.waybillManage.table.salesProductPriceVersion': '价格版本', + 'order.waybillManage.table.forecastReceivableAmount': '预报应收', + 'order.waybillManage.table.forecastPayableAmount': '预报应付', + 'order.waybillManage.table.hasInsurance': '是否投保', + 'order.waybillManage.table.hasAllocationTrace': '有排货快照', + 'order.waybillManage.table.allocationScheme': '排货方案', + 'order.waybillManage.table.exportQualificationId': '出口资质ID', + 'order.waybillManage.table.pickupOrderId': '揽收单ID', + 'order.waybillManage.table.stockBindLineCount': '绑定行数', + 'order.waybillManage.table.primarySkuCodes': '绑定SKU', 'order.waybillManage.table.consigneeCountryName': '收件国家名称', 'order.waybillManage.table.warehouseId': '仓库ID', 'order.waybillManage.table.customerId': '客户ID', @@ -2066,6 +2079,8 @@ export default { 'order.myOrders.drawer.productName': '销售产品', 'order.myOrders.drawer.channelCode': '渠道code', 'order.myOrders.drawer.channelName': '服务渠道', + 'order.myOrders.drawer.downstreamSupplierName': '服务商名称', + 'order.myOrders.drawer.downstreamSupplierCode': '服务商编码', 'order.myOrders.allocationTrace.view': '查看排货过程', 'order.myOrders.allocationTrace.title': '排货过程', 'order.myOrders.allocationTrace.fromSnapshot': '历史快照(排货当时结果)', @@ -2232,8 +2247,27 @@ export default { 'order.myOrders.drawer.actions.cancel': '取消', 'order.myOrders.drawer.actions.recalculateReceivable': '重算应收', 'order.myOrders.drawer.actions.recalculatePayable': '重算应付', + 'order.myOrders.drawer.actions.bindSnapshotVersions': '切换版本', 'order.myOrders.drawer.actions.addFee': '添加费用', 'order.myOrders.drawer.actions.additionalServices': '附加服务', + 'order.myOrders.drawer.fee.rulesVersion': '规则版本', + 'order.myOrders.drawer.fee.priceVersion': '价格版本', + 'order.myOrders.drawer.bindSnapshot.title': '切换价格/规则版本', + 'order.myOrders.drawer.bindSnapshot.hint': + '显式切换订单绑定的发布版本;默认仍保护下单契约。重算应收/应付会沿用此处绑定。', + 'order.myOrders.drawer.bindSnapshot.rulesVersion': '规则版本', + 'order.myOrders.drawer.bindSnapshot.priceVersion': '价格版本', + 'order.myOrders.drawer.bindSnapshot.boundAs': '当前绑定:{version}', + 'order.myOrders.drawer.bindSnapshot.keepUnchanged': '不修改(保持当前)', + 'order.myOrders.drawer.bindSnapshot.useCurrent': '当前有效发布', + 'order.myOrders.drawer.bindSnapshot.currentBound': '当前绑定', + 'order.myOrders.drawer.bindSnapshot.recalcReceivable': '绑定后重算应收', + 'order.myOrders.drawer.bindSnapshot.recalcPayable': '绑定后重算应付', + 'order.myOrders.drawer.bindSnapshot.needVersion': '请至少选择规则版本或价格版本', + 'order.myOrders.drawer.bindSnapshot.noProduct': '订单未绑定销售产品', + 'order.myOrders.drawer.bindSnapshot.success': '版本已切换', + 'order.myOrders.drawer.bindSnapshot.failed': '切换版本失败', + 'order.myOrders.drawer.bindSnapshot.loadFailed': '加载发布版本失败', 'order.myOrders.drawer.editBasicTitle': '编辑订单详情', 'order.myOrders.drawer.editWeightTitle': '编辑件重尺', 'order.myOrders.drawer.message.saveSuccess': '保存成功', @@ -2263,7 +2297,23 @@ export default { 'order.myOrders.drawer.fee.diff': '差额', 'order.myOrders.drawer.fee.negativeGrossProfitWarn': '存在负毛利,请关注成本与报价', 'order.myOrders.drawer.fee.additionalServicesTitle': '附加服务', + 'order.myOrders.drawer.fee.additionalServicesManageTitle': '管理附加服务', + 'order.myOrders.drawer.fee.additionalServicesViewTitle': '查看附加服务', 'order.myOrders.drawer.fee.additionalServicesEmpty': '该产品暂无可选附加服务', + 'order.myOrders.drawer.fee.additionalServicesNone': '未绑定', + 'order.myOrders.drawer.fee.additionalServicesManage': '管理', + 'order.myOrders.drawer.fee.additionalServicesView': '查看', + 'order.myOrders.drawer.fee.additionalServicesSave': '保存并重算', + 'order.myOrders.drawer.fee.additionalServicesClose': '关闭', + 'order.myOrders.drawer.fee.additionalServicesHint': + '勾选增删产品允许的附加服务;保存后按当前阶段重算应收(未支付可改)。', + 'order.myOrders.drawer.fee.additionalServicesPaidHint': '订单已支付,附加服务不可修改。', + 'order.myOrders.drawer.fee.additionalServicesCurrent': '订单当前绑定', + 'order.myOrders.drawer.fee.additionalServicesPick': '可选附加服务', + 'order.myOrders.drawer.fee.additionalServicesOrphan': + '以下服务已不在产品可选范围,保存后将自动移除', + 'order.myOrders.drawer.fee.additionalServicesOrphanCleared': + '已移除不在产品范围的附加服务:{names}', 'order.myOrders.drawer.fee.additionalServicesSuccess': '附加服务已更新', 'order.myOrders.drawer.fee.additionalServicesFailed': '附加服务更新失败', 'order.myOrders.drawer.fee.addTitle': '添加费用', diff --git a/src/views/order/components/ColumnConfigModal.vue b/src/views/order/components/ColumnConfigModal.vue index e9a5fe1..38c841a 100644 --- a/src/views/order/components/ColumnConfigModal.vue +++ b/src/views/order/components/ColumnConfigModal.vue @@ -145,7 +145,7 @@ apply: [payload: { activeId: string; schemes: ColumnScheme[] }]; }>(); - const { t } = useI18n(); + const { t, te } = useI18n(); const [registerModal, { openModal, closeModal, setModalProps }] = useModal(); const editingSchemeId = ref(''); @@ -173,8 +173,9 @@ selectedKeys.value .map((key) => { const col = catalogMap.value.get(key); - if (!col) return null; - return { key: col.key, label: resolveColumnLabel(col, t) }; + // 方案里可能有目录尚未收录的 key:仍用中文兜底,避免表头/配置显示英文字段名 + const def = col || { key }; + return { key, label: resolveColumnLabel(def, t, te) }; }) .filter(Boolean) as { key: string; label: string }[], ); @@ -183,7 +184,7 @@ const selected = new Set(selectedKeys.value); return props.catalog .filter((col) => !selected.has(col.key)) - .map((col) => ({ key: col.key, label: resolveColumnLabel(col, t) })); + .map((col) => ({ key: col.key, label: resolveColumnLabel(col, t, te) })); }); function readSelectedKeysFromDom(): string[] { diff --git a/src/views/order/components/orderDetailFieldConfig.ts b/src/views/order/components/orderDetailFieldConfig.ts index 4eb8968..00d48a7 100644 --- a/src/views/order/components/orderDetailFieldConfig.ts +++ b/src/views/order/components/orderDetailFieldConfig.ts @@ -32,6 +32,8 @@ export const ORDER_DETAIL_VALUE_FIELD_MAP: Record = { declaredCurrency: ['declaredCurrency', 'DeclaredCurrency'], declaredSum: ['declaredSum', 'DeclaredSum'], additionalServices: ['additionalServices', 'AdditionalServices'], + downstreamSupplierName: ['downstreamSupplierName', 'DownstreamSupplierName'], + downstreamSupplierCode: ['downstreamSupplierCode', 'DownstreamSupplierCode'], }; export interface OrderDetailFieldGroup { @@ -58,7 +60,9 @@ export function patchOrderBasicFieldGroups( const fields = [ 'productCode', 'productName', - ...(includeChannel ? (['channelCode', 'channelName'] as const) : []), + ...(includeChannel + ? (['channelCode', 'channelName', 'downstreamSupplierName', 'downstreamSupplierCode'] as const) + : []), 'warehouse', 'consigneeCountry', 'consigneeCountryName', diff --git a/src/views/order/myOrders/columnConfig.ts b/src/views/order/myOrders/columnConfig.ts index 5671e9d..58ab54c 100644 --- a/src/views/order/myOrders/columnConfig.ts +++ b/src/views/order/myOrders/columnConfig.ts @@ -1,6 +1,7 @@ /** 我的订单列表字段配置(本地命名方案,最多显示 30 列) */ import { filterMyOrdersFieldKeys, isMyOrdersHiddenField, MY_ORDERS_HIDDEN_FIELD_KEYS } from './customerFieldMask'; +import { getOrderColumnLabelZh } from '../orderColumnLabelsZh'; export const MAX_VISIBLE_COLUMNS = 30; @@ -182,29 +183,67 @@ export function getActiveScheme(state: ColumnConfigState): ColumnScheme { ); } +function isUsableLabel(text: string | undefined | null, fieldKey: string): text is string { + if (!text) return false; + const s = String(text).trim(); + if (!s) return false; + // 翻译失败时 vue-i18n 常回传 i18n key 或字段 key + if (s === fieldKey) return false; + if (s.startsWith('order.') && s.includes('.')) return false; + return true; +} + export function resolveColumnLabel( col: OrderListColumnDef, - t: (key: string) => string, + t: (key: string, ...args: any[]) => string, + _te?: (key: string) => boolean, ): string { - if (col.labelKey) return t(col.labelKey); - if (col.label) return col.label; - return col.key; + const fieldKey = col.key; + const tryI18n = (i18nKey: string) => { + try { + const text = t(i18nKey); + return isUsableLabel(text, fieldKey) ? text : null; + } catch { + return null; + } + }; + + if (col.labelKey) { + const byLabelKey = tryI18n(col.labelKey); + if (byLabelKey) return byLabelKey; + } + if (isUsableLabel(col.label, fieldKey)) return col.label!.trim(); + + const autoKeys = [ + `order.waybillManage.table.${fieldKey}`, + `order.myOrders.table.${fieldKey}`, + `order.myOrders.drawer.${fieldKey}`, + ]; + for (const k of autoKeys) { + const hit = tryI18n(k); + if (hit) return hit; + } + // 最终兜底:中文列名表,绝不直接返回 camelCase key + return getOrderColumnLabelZh(fieldKey); } export function buildHotColumns( catalog: OrderListColumnDef[], keys: string[], - t: (key: string) => string, + t: (key: string, ...args: any[]) => string, + te?: (key: string) => boolean, ) { const map = new Map(catalog.map((c) => [c.key, c])); return keys - .map((key) => map.get(key)) - .filter(Boolean) - .map((col) => ({ - key: col!.key, - label: resolveColumnLabel(col!, t), - width: col!.width ?? 120, - })); + .map((key) => { + const col = map.get(key) || { key, width: 120 }; + return { + key, + label: resolveColumnLabel(col, t, te), + width: col.width ?? 120, + }; + }) + .filter((col) => !!col.key); } /** 将接口行中的标量字段并入可选列目录(跳过对象/数组与内部字段) */ @@ -228,10 +267,19 @@ export function mergeDiscoveredColumns( for (const [key, value] of Object.entries(row)) { if (!key || skip.has(key) || map.has(key)) continue; if (value != null && typeof value === 'object') continue; - map.set(key, { key, label: key, width: 120 }); + map.set(key, { + key, + label: getOrderColumnLabelZh(key), + labelKey: `order.waybillManage.table.${key}`, + width: 120, + }); } } - return Array.from(map.values()); + return Array.from(map.values()).map((col) => + isUsableLabel(col.label, col.key) + ? col + : { ...col, label: getOrderColumnLabelZh(col.key) }, + ); } export function createSchemeId() { diff --git a/src/views/order/myOrders/components/OrderDetailDrawer.vue b/src/views/order/myOrders/components/OrderDetailDrawer.vue index 0f35f56..0472845 100644 --- a/src/views/order/myOrders/components/OrderDetailDrawer.vue +++ b/src/views/order/myOrders/components/OrderDetailDrawer.vue @@ -1053,7 +1053,6 @@ const tabList = computed(() => [ { key: 'track', label: t('order.myOrders.drawer.tabs.track') }, { key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') }, { key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') }, - { key: 'operationLog', label: t('order.myOrders.drawer.tabs.operationLog') }, ]); const operationLogList = ref([]); @@ -1067,6 +1066,8 @@ const slidingTabItems = computed(() => function resolveDrawerTab(tab?: string) { const next = tab || 'basic'; if (next === 'weight' || next === 'fee') return 'weightFee'; + // 我的运单不展示操作日志(管理端运单管理保留) + if (next === 'operationLog') return 'basic'; return next; } @@ -2150,7 +2151,7 @@ watch( ); defineExpose({ - openDrawer: (opts?: { tab?: 'basic' | 'weightFee' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'operationLog' }) => { + openDrawer: (opts?: { tab?: 'basic' | 'weightFee' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' }) => { const tab = resolveDrawerTab(opts?.tab); activeTab.value = tab; basicViewMode.value = 'all'; diff --git a/src/views/order/myOrders/customerFieldMask.ts b/src/views/order/myOrders/customerFieldMask.ts index 1c70ea3..e293463 100644 --- a/src/views/order/myOrders/customerFieldMask.ts +++ b/src/views/order/myOrders/customerFieldMask.ts @@ -7,6 +7,7 @@ export const MY_ORDERS_HIDDEN_FIELD_KEYS = new Set([ 'channelName', 'downstreamProvider', 'downstreamSupplierCode', + 'downstreamSupplierName', 'downstreamAppKey', 'downstreamAppId', 'channelForecastCode', diff --git a/src/views/order/myOrders/index.vue b/src/views/order/myOrders/index.vue index 9ad5d2c..09de706 100644 --- a/src/views/order/myOrders/index.vue +++ b/src/views/order/myOrders/index.vue @@ -122,7 +122,7 @@ name: 'MyOrders', }); - const { t } = useI18n(); + const { t, te } = useI18n(); const route = useRoute(); const router = useRouter(); const isFulfillList = computed(() => route.meta.listMode === 'fulfill'); @@ -1623,7 +1623,7 @@ 'trackNo', ]); const base = withServerFilterColumns( - buildHotColumns(listColumnCatalog.value, activeColumnKeys.value, t), + buildHotColumns(listColumnCatalog.value, activeColumnKeys.value, t, te), ORDER_SERVER_FILTER_KEYS, (col) => { if (numberKeys.has(col.key)) { diff --git a/src/views/order/orderColumnLabelsZh.ts b/src/views/order/orderColumnLabelsZh.ts new file mode 100644 index 0000000..ac871a7 --- /dev/null +++ b/src/views/order/orderColumnLabelsZh.ts @@ -0,0 +1,161 @@ +/** + * 运单/订单列表列中文名(表头 + 字段配置兜底)。 + * 优先保证中文展示,避免出现 maxTimeliness 等英文字段名。 + */ +export const ORDER_COLUMN_LABEL_ZH: Record = { + // 常用别名 + customerName: '客户名称', + customerCode: '客户编码', + waybillNo: '运单号', + customerOrderNo: '客户单号', + trackingNo: '跟踪号', + downstreamServiceNo: '服务商单号', + shippingMethod: '销售产品', + serviceChannel: '服务渠道', + consignee: '收件人', + consigneePhone: '收件电话', + consigneeCity: '收件城市', + consigneeAddress: '收件地址', + consigneePostcode: '收件邮编', + consigneeState: '收件省州', + ioss: 'IOSS', + eori: 'EORI', + + // OrderOutput 标量 + id: '订单ID', + clientNo: '客户单号', + trackNo: '跟踪号', + serviceNo: '运单号', + productCode: '产品编码', + productName: '销售产品', + maxTimeliness: '最大时效(天)', + channelCode: '渠道编码', + channelName: '服务渠道', + downstreamSupplierName: '服务商名称', + downstreamSupplierCode: '服务商编码', + downstreamAppKey: '下游AppKey', + downstreamAppId: '下游AppId', + channelForecastCode: '渠道预报编码', + consigneeCountry: '收件国家', + consigneeCountryName: '收件国家名称', + cargoAttribute: '货物属性', + warehouseId: '仓库ID', + warehouse: '仓库', + customerId: '客户ID', + orderSource: '订单来源', + pickupTime: '期望揽收时间', + carrierWaybillNo: '承运商单号', + pickupNo: '揽收单号', + inboundMethod: '到仓方式', + fulfillmentType: '履约类型', + fulfillProgress: '代发进度', + stockBindStatus: '绑定状态', + domesticTrackingNos: '国内快递单号', + stockBindQtyTotal: '绑定件数', + stockBindLineCount: '绑定行数', + primaryBinCode: '主库位', + primarySkuCodes: '绑定SKU', + fulfillmentSource: '代发来源', + customerSenderAddressId: '发件地址簿ID', + requestId: '请求标识', + exportCustomsMode: '出口报关方式', + exportQualificationId: '出口资质ID', + predeclareFailReason: '预报失败原因', + allocationPendingReason: '待排货原因', + allocationScheme: '排货方案', + hasAllocationTrace: '有排货快照', + declaredCurrency: '申报币种', + declaredSum: '申报总金额', + quantity: '包裹数量', + isCod: '是否COD', + codCurrency: 'COD币种', + codSum: 'COD金额', + orderStatus: '订单状态', + status: '状态码', + handleStatus: '处理状态码', + handleStatusName: '处理状态', + issueType: '问题类型码', + issueTypeName: '问题类型', + issueTitle: '问题标题', + issueContent: '问题内容', + issueTemplateId: '问题模板ID', + issueRemark: '问题备注', + issueTime: '问题时间', + claimType: '理赔类型码', + claimTypeName: '理赔类型', + claimAmount: '理赔金额', + claimCurrency: '理赔币种', + claimReason: '理赔原因', + claimTime: '理赔时间', + previousOrderStatus: '原订单状态', + receiveTime: '收货时间', + shipTime: '发货时间', + payTime: '支付时间', + isPaid: '是否已支付', + signTime: '签收时间', + isIntercept: '是否拦截', + isRemote: '是否偏远', + hasInsurance: '是否投保', + forecastReceivableAmount: '预报应收', + forecastPayableAmount: '预报应付', + salesProductRulesVersion: '规则版本', + salesProductPriceVersion: '价格版本', + createTime: '创建时间', + createBy: '创建人', + updateTime: '更新时间', + updateBy: '更新人', + chargeableWeight: '计费重', + receivableAmount: '应收金额', + inboundTime: '入库时间', + outboundTime: '出库时间', + pickupOrderId: '揽收单ID', + + // 寄件人展平 + senderName: '寄件人', + senderCompany: '寄件公司', + senderPhone: '寄件手机', + senderTelePhone: '寄件电话', + senderEmail: '寄件邮箱', + senderCountryCode: '寄件国家代码', + senderProvince: '寄件省州', + senderCity: '寄件城市', + senderZipCode: '寄件邮编', + senderStreetAddress1: '寄件地址1', + senderStreetAddress2: '寄件地址2', + senderStreetAddress3: '寄件地址3', + senderHouseNumber: '寄件门牌号', + + // 收件人展平 + receiverRecipientsName: '收件人', + receiverCompanyName: '收件公司', + receiverPhone: '收件手机', + receiverTelePhone: '收件电话', + receiverEmail: '收件邮箱', + receiverCountryCode: '收件国家代码', + receiverProvince: '收件省州', + receiverCity: '收件城市', + receiverStreetNumber: '收件门牌号', + receiverStreetAddress1: '收件地址1', + receiverStreetAddress2: '收件地址2', + receiverStreetAddress3: '收件地址3', + receiverZipCode: '收件邮编', + receiverTaxType: '税号类型', + receiverTaxNo: '税号', + receiverCertificateType: '证件类型', + receiverCertificateNo: '证件号码', + receiverEori: '收件EORI', + receiverVat: '收件VAT', +}; + +/** 按字段 key 取中文列名;未知字段做可读化,避免直接甩 camelCase。 */ +export function getOrderColumnLabelZh(key: string): string { + const k = String(key || '').trim(); + if (!k) return ''; + if (ORDER_COLUMN_LABEL_ZH[k]) return ORDER_COLUMN_LABEL_ZH[k]; + // fooBarBaz → Foo Bar Baz,再包一层中文前缀,避免纯英文 key 当表头 + const spaced = k + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + .trim(); + return spaced || k; +} diff --git a/src/views/order/waybillManage/columnConfig.ts b/src/views/order/waybillManage/columnConfig.ts index 42d2ca6..3670f38 100644 --- a/src/views/order/waybillManage/columnConfig.ts +++ b/src/views/order/waybillManage/columnConfig.ts @@ -4,6 +4,7 @@ */ import type { OrderListColumnDef } from '../myOrders/columnConfig'; import { MY_ORDERS_COLUMN_CATALOG } from '../myOrders/columnConfig'; +import { getOrderColumnLabelZh } from '../orderColumnLabelsZh'; export const WAYBILL_MANAGE_COLUMN_STORAGE_KEY = 'oms.order.waybillManage.columnSchemes'; @@ -40,10 +41,23 @@ export const WAYBILL_MANAGE_COLUMN_CATALOG: OrderListColumnDef[] = [ { key: 'productName', labelKey: M('shippingMethod'), width: 120 }, { key: 'channelCode', labelKey: L('channelCode'), width: 110 }, { key: 'channelName', labelKey: M('serviceChannel'), width: 120 }, + { key: 'downstreamSupplierName', labelKey: L('downstreamSupplierName'), width: 140 }, { key: 'downstreamSupplierCode', labelKey: L('downstreamSupplierCode'), width: 140 }, { key: 'downstreamAppKey', labelKey: L('downstreamAppKey'), width: 140 }, { key: 'downstreamAppId', labelKey: L('downstreamAppId'), width: 110 }, { key: 'channelForecastCode', labelKey: L('channelForecastCode'), width: 130 }, + { key: 'maxTimeliness', labelKey: L('maxTimeliness'), width: 100 }, + { key: 'salesProductRulesVersion', labelKey: L('salesProductRulesVersion'), width: 150 }, + { key: 'salesProductPriceVersion', labelKey: L('salesProductPriceVersion'), width: 150 }, + { key: 'forecastReceivableAmount', labelKey: L('forecastReceivableAmount'), width: 120 }, + { key: 'forecastPayableAmount', labelKey: L('forecastPayableAmount'), width: 120 }, + { key: 'hasInsurance', labelKey: L('hasInsurance'), width: 90 }, + { key: 'hasAllocationTrace', labelKey: L('hasAllocationTrace'), width: 110 }, + { key: 'allocationScheme', labelKey: L('allocationScheme'), width: 120 }, + { key: 'exportQualificationId', labelKey: L('exportQualificationId'), width: 120 }, + { key: 'pickupOrderId', labelKey: L('pickupOrderId'), width: 110 }, + { key: 'stockBindLineCount', labelKey: L('stockBindLineCount'), width: 100 }, + { key: 'primarySkuCodes', labelKey: L('primarySkuCodes'), width: 140 }, { key: 'consigneeCountry', labelKey: M('consigneeCountry'), width: 110 }, { key: 'consigneeCountryName', labelKey: L('consigneeCountryName'), width: 120 }, { key: 'cargoAttribute', labelKey: M('cargoAttribute'), width: 100 }, @@ -182,7 +196,16 @@ export function dedupeCatalog(list: OrderListColumnDef[]): OrderListColumnDef[] return Array.from(map.values()); } -/** 基础目录 = 运单全量 + 我的订单目录中可能遗漏的项 */ -export function getWaybillBaseCatalog(): OrderListColumnDef[] { - return dedupeCatalog([...WAYBILL_MANAGE_COLUMN_CATALOG, ...MY_ORDERS_COLUMN_CATALOG]); +function withZhLabels(list: OrderListColumnDef[]): OrderListColumnDef[] { + return list.map((col) => ({ + ...col, + label: col.label && col.label !== col.key ? col.label : getOrderColumnLabelZh(col.key), + })); +} + +/** 基础目录 = 运单全量 + 我的订单目录中可能遗漏的项(带中文 label) */ +export function getWaybillBaseCatalog(): OrderListColumnDef[] { + return withZhLabels( + dedupeCatalog([...WAYBILL_MANAGE_COLUMN_CATALOG, ...MY_ORDERS_COLUMN_CATALOG]), + ); } diff --git a/src/views/order/waybillManage/components/BindSnapshotVersionsModal.vue b/src/views/order/waybillManage/components/BindSnapshotVersionsModal.vue new file mode 100644 index 0000000..6237cd8 --- /dev/null +++ b/src/views/order/waybillManage/components/BindSnapshotVersionsModal.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/order/waybillManage/components/OrderDetailDrawer.vue b/src/views/order/waybillManage/components/OrderDetailDrawer.vue index 724af2e..3702c44 100644 --- a/src/views/order/waybillManage/components/OrderDetailDrawer.vue +++ b/src/views/order/waybillManage/components/OrderDetailDrawer.vue @@ -48,12 +48,14 @@ @@ -900,6 +1014,7 @@ import { getBooleanTagStyle } from '@/utils/statusTag'; import { resolveOmsAssetUrl } from '@/api/oms/common'; import SlidingTabs from '@/components/SlidingTabs/index.vue'; import AddOrderFeeModal from './AddOrderFeeModal.vue'; +import BindSnapshotVersionsModal from './BindSnapshotVersionsModal.vue'; import AllocationTraceModal from './AllocationTraceModal.vue'; import OrderStockBindTab from '../../myOrders/components/OrderStockBindTab.vue'; import OrderAttachmentsTab from '../../components/OrderAttachmentsTab.vue'; @@ -949,6 +1064,8 @@ const weightSaving = ref(false); const recalcReceivableLoading = ref(false); const recalcPayableLoading = ref(false); const addFeeModalVisible = ref(false); +const bindSnapshotVersionsVisible = ref(false); +const bindSnapshotSalesProductId = ref(null); const allocationTraceVisible = ref(false); const addFeeDefaultType = ref<'receivable' | 'payable'>('receivable'); const additionalServicesModalVisible = ref(false); @@ -1288,6 +1405,8 @@ const orderBasicFields = mergeOrderBasicFields([ { key: 'productName', labelKey: 'order.myOrders.drawer.productName' }, { key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' }, { key: 'channelName', labelKey: 'order.myOrders.drawer.channelName' }, + { key: 'downstreamSupplierName', labelKey: 'order.myOrders.drawer.downstreamSupplierName' }, + { key: 'downstreamSupplierCode', labelKey: 'order.myOrders.drawer.downstreamSupplierCode' }, { key: 'consigneeCountry', labelKey: 'order.myOrders.drawer.consigneeCountry' }, { key: 'consigneeCountryName', labelKey: 'order.myOrders.drawer.consigneeCountryName' }, { key: 'cargoAttribute', labelKey: 'order.myOrders.drawer.cargoAttribute' }, @@ -1849,26 +1968,27 @@ const orderAdditionalServices = computed(() => ) ); -const orderHasAdditionalServicesField = computed(() => { - const o = detail.value; - if (!o) return false; - return ( - Object.prototype.hasOwnProperty.call(o, 'additionalServices') || - Object.prototype.hasOwnProperty.call(o, 'AdditionalServices') || - o.additionalServices != null || - o.AdditionalServices != null - ); -}); - const isOrderUnpaid = computed(() => { const v = detail.value?.isPaid ?? detail.value?.IsPaid; return !(v === true || v === 1 || v === '1'); }); -const canEditAdditionalServices = computed( - () => - isOrderUnpaid.value && - (productHasAdditionalServices.value || orderHasAdditionalServicesField.value) +/** 未支付可勾选/取消附加服务;已支付仅可查看。 */ +const canEditAdditionalServices = computed(() => isOrderUnpaid.value); + +/** 订单上有、但当前产品配置已不包含的附加服务(保存时只能去掉,不能再写回)。 */ +const orphanAdditionalServices = computed(() => { + const allowed = new Set(productAdditionalServiceOptions.value.map((o) => o.value)); + if (!allowed.size) { + return [...orderAdditionalServices.value]; + } + return orderAdditionalServices.value.filter((code) => !allowed.has(code)); +}); + +const dismissedOrphanServices = ref([]); + +const visibleOrphanAdditionalServices = computed(() => + orphanAdditionalServices.value.filter((code) => !dismissedOrphanServices.value.includes(code)) ); const grossProfitMarginText = computed(() => { @@ -2057,25 +2177,45 @@ async function refreshProductAdditionalServicesCapability() { async function openAdditionalServicesModal() { additionalServicesModalVisible.value = true; additionalServicesLoading.value = true; + dismissedOrphanServices.value = []; try { await refreshProductAdditionalServicesCapability(); const allowed = new Set(productAdditionalServiceOptions.value.map((o) => o.value)); selectedAdditionalServices.value = orderAdditionalServices.value.filter((c) => - allowed.size ? allowed.has(c) : true + allowed.size ? allowed.has(c) : false ); } finally { additionalServicesLoading.value = false; } } +function dismissOrphanAdditionalService(code: string) { + if (!dismissedOrphanServices.value.includes(code)) { + dismissedOrphanServices.value = [...dismissedOrphanServices.value, code]; + } +} + async function handleAdditionalServicesSubmit() { + if (!canEditAdditionalServices.value) { + additionalServicesModalVisible.value = false; + return; + } const id = orderId.value; if (id == null) return Promise.reject(new Error('missing order')); additionalServicesSaving.value = true; try { + const allowed = new Set(productAdditionalServiceOptions.value.map((o) => o.value)); + const next = selectedAdditionalServices.value.filter((c) => allowed.has(c)); + if (orphanAdditionalServices.value.length) { + Message.info( + t('order.myOrders.drawer.fee.additionalServicesOrphanCleared', { + names: orphanAdditionalServices.value.map((c) => labelAdditionalService(c)).join('、'), + }) + ); + } const result = await postOrderAdditionalServicesUpdate({ id, - additionalServices: [...selectedAdditionalServices.value], + additionalServices: next, }); applyDetailFromApi(result); additionalServicesModalVisible.value = false; @@ -2437,6 +2577,36 @@ async function handleRecalculatePayable() { } } +const boundRulesVersion = computed(() => { + const o = detail.value; + if (!o) return ''; + return String(o.salesProductRulesVersion ?? o.SalesProductRulesVersion ?? '').trim(); +}); + +const boundPriceVersion = computed(() => { + const o = detail.value; + if (!o) return ''; + return String(o.salesProductPriceVersion ?? o.SalesProductPriceVersion ?? '').trim(); +}); + +async function openBindSnapshotVersionsModal() { + const code = String(detail.value?.productCode ?? detail.value?.ProductCode ?? '').trim(); + if (!code) { + Message.warning(t('order.myOrders.drawer.bindSnapshot.noProduct')); + return; + } + bindSnapshotSalesProductId.value = await resolveProductIdByCode(code); + if (bindSnapshotSalesProductId.value == null) { + Message.warning(t('order.myOrders.drawer.bindSnapshot.noProduct')); + return; + } + bindSnapshotVersionsVisible.value = true; +} + +function handleBindSnapshotVersionsSuccess(result: any) { + applyDetailFromApi(result); +} + async function loadOperationLogs() { const id = orderId.value; if (!id) { @@ -3146,6 +3316,49 @@ defineExpose({ gap: 12px; } +.fee-additional-services { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding: 8px 12px; + border-radius: 4px; + background: var(--color-fill-1); + font-size: 12px; + line-height: 20px; +} + +.fee-additional-services__label { + color: var(--color-text-2); + flex-shrink: 0; +} + +.fee-additional-services__empty { + color: var(--color-text-3); +} + +.fee-additional-services__edit { + margin-left: auto; +} + +.fee-snapshot-versions { + display: flex; + flex-wrap: wrap; + gap: 12px 20px; + margin-bottom: 12px; + padding: 8px 12px; + border-radius: 4px; + background: var(--color-fill-1); + font-size: 12px; + color: var(--color-text-2); + line-height: 20px; +} + +.fee-snapshot-versions__item { + word-break: break-all; +} + .fee-summary { display: grid; grid-template-columns: 1fr 1fr 1fr; @@ -3733,4 +3946,45 @@ defineExpose({ color: var(--app-primary-color); font-weight: 500; } + +.additional-services-hint { + margin: 0 0 12px; + font-size: 13px; + line-height: 1.5; + color: var(--color-text-3); +} + +.additional-services-current { + margin-bottom: 16px; + padding: 10px 12px; + border-radius: 4px; + background: var(--color-fill-1); +} + +.additional-services-current__label, +.additional-services-orphan__label, +.additional-services-options-label { + margin-bottom: 8px; + font-size: 13px; + font-weight: 500; + color: var(--color-text-1); +} + +.additional-services-current__empty { + font-size: 13px; + color: var(--color-text-3); +} + +.additional-services-orphan { + margin-bottom: 16px; + padding: 10px 12px; + border-radius: 4px; + background: rgba(255, 125, 0, 0.08); +} + +.additional-services-checkbox-group { + display: flex; + flex-direction: column; + gap: 8px; +} diff --git a/src/views/order/waybillManage/index.vue b/src/views/order/waybillManage/index.vue index 4384c8a..693d31c 100644 --- a/src/views/order/waybillManage/index.vue +++ b/src/views/order/waybillManage/index.vue @@ -131,7 +131,7 @@ name: 'WaybillManage', }); - const { t } = useI18n(); + const { t, te } = useI18n(); const { tableContainerRef, tableHeight } = useTableContainerHeight(); const { customerOptions, withCustomerId } = useWarehouseAdminCustomer(); @@ -248,6 +248,10 @@ productName: productName ?? productCode ?? '', channelCode: channelCode ?? '', channelName: channelName ?? channelCode ?? '', + downstreamSupplierName: + pick(raw, 'downstreamSupplierName', 'DownstreamSupplierName') ?? '', + downstreamSupplierCode: + pick(raw, 'downstreamSupplierCode', 'DownstreamSupplierCode') ?? '', isPaid: boolish(isPaidRaw), isCod: boolish(pick(raw, 'isCod', 'IsCod')), isIntercept: boolish(pick(raw, 'isIntercept', 'IsIntercept')), @@ -1567,7 +1571,7 @@ ]); return withSortableColumns( withServerFilterColumns( - buildHotColumns(columnCatalog.value, activeColumnKeys.value, t), + buildHotColumns(columnCatalog.value, activeColumnKeys.value, t, te), ORDER_SERVER_FILTER_KEYS, (col) => { if (numberKeys.has(col.key)) {