feat: 运单费用版本切换、服务商名称与列配置中文,我的运单隐藏操作日志
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m40s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-09 18:30:48 +08:00
parent 5b6cb43457
commit 50b3eac99c
16 changed files with 929 additions and 64 deletions

View File

@ -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` | 未支付改附加服务并重算 |

View File

@ -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<any> {
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<any> {
const res = await axiosInstance.post(orderUrl('/order/fee/rerun-inbound-billing'), { id: body.id })

View File

@ -175,6 +175,7 @@ export const PERMISSION_ACTION_LABELS: Record<string, string> = {
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: '添加申报行' },
]),

View File

@ -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',

View File

@ -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': '添加费用',

View File

@ -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[] {

View File

@ -32,6 +32,8 @@ export const ORDER_DETAIL_VALUE_FIELD_MAP: Record<string, string[]> = {
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',

View File

@ -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() {

View File

@ -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<OrderOperationLogItem[]>([]);
@ -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';

View File

@ -7,6 +7,7 @@ export const MY_ORDERS_HIDDEN_FIELD_KEYS = new Set([
'channelName',
'downstreamProvider',
'downstreamSupplierCode',
'downstreamSupplierName',
'downstreamAppKey',
'downstreamAppId',
'channelForecastCode',

View File

@ -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)) {

View File

@ -0,0 +1,161 @@
/**
* 运单/订单列表列中文名(表头 + 字段配置兜底)。
* 优先保证中文展示,避免出现 maxTimeliness 等英文字段名。
*/
export const ORDER_COLUMN_LABEL_ZH: Record<string, string> = {
// 常用别名
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;
}

View File

@ -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]),
);
}

View File

@ -0,0 +1,243 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import {
getSalesProductPublishPriceList,
getSalesProductPublishRulesList,
} from '@/api/channel/salesProduct'
import { postOrderBindSnapshotVersions } from '@/api/order'
const props = defineProps<{
visible: boolean
orderId: number
salesProductId: number | string | null
currentRulesVersion?: string | null
currentPriceVersion?: string | null
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
success: [order: any]
}>()
const { t } = useI18n()
const [registerModal] = useModal()
const submitting = ref(false)
const loading = ref(false)
const formState = reactive({
rulesVersion: undefined as string | undefined,
priceVersion: undefined as string | undefined,
recalculateReceivable: true,
recalculatePayable: true,
})
const rulesOptions = ref<Array<{ label: string; value: string }>>([])
const priceOptions = ref<Array<{ label: string; value: string }>>([])
const title = computed(() => t('order.myOrders.drawer.bindSnapshot.title'))
function mapVersionOptions(items: any[], current?: string | null) {
const rows = (items || []).map((row) => {
const version = String(row.version ?? row.Version ?? '').trim()
const publishTime = row.publishTime ?? row.PublishTime ?? ''
const desc = String(row.description ?? row.Description ?? '').trim()
const parts = [version]
if (publishTime) parts.push(String(publishTime))
if (desc) parts.push(desc)
const isCurrent = current && version === current
return {
value: version,
label: isCurrent
? `${parts.join(' · ')}(${t('order.myOrders.drawer.bindSnapshot.currentBound')})`
: parts.join(' · '),
}
}).filter((x) => x.value)
return [
{
value: 'CURRENT',
label: t('order.myOrders.drawer.bindSnapshot.useCurrent'),
},
...rows,
]
}
async function loadVersionOptions() {
rulesOptions.value = [
{ value: 'CURRENT', label: t('order.myOrders.drawer.bindSnapshot.useCurrent') },
]
priceOptions.value = [
{ value: 'CURRENT', label: t('order.myOrders.drawer.bindSnapshot.useCurrent') },
]
if (props.salesProductId == null || props.salesProductId === '') return
loading.value = true
try {
const [rules, price] = await Promise.all([
getSalesProductPublishRulesList({
salesProductId: props.salesProductId,
page: 1,
pageSize: 50,
includeContent: false,
}),
getSalesProductPublishPriceList({
salesProductId: props.salesProductId,
page: 1,
pageSize: 50,
includeContent: false,
}),
])
rulesOptions.value = mapVersionOptions(rules.items, props.currentRulesVersion)
priceOptions.value = mapVersionOptions(price.items, props.currentPriceVersion)
} catch (e: any) {
Message.error(e?.message || t('order.myOrders.drawer.bindSnapshot.loadFailed'))
} finally {
loading.value = false
}
}
function resetForm() {
formState.rulesVersion = undefined
formState.priceVersion = undefined
formState.recalculateReceivable = true
formState.recalculatePayable = true
}
watch(
() => props.visible,
async (v) => {
if (!v) return
resetForm()
await loadVersionOptions()
},
{ immediate: true },
)
function onUpdateVisible(val: boolean) {
if (val === props.visible) return
emit('update:visible', val)
}
async function handleOk() {
if (!formState.rulesVersion && !formState.priceVersion) {
Message.warning(t('order.myOrders.drawer.bindSnapshot.needVersion'))
return
}
submitting.value = true
try {
const result = await postOrderBindSnapshotVersions({
id: props.orderId,
rulesVersion: formState.rulesVersion,
priceVersion: formState.priceVersion,
recalculateReceivable: formState.recalculateReceivable,
recalculatePayable: formState.recalculatePayable,
})
Message.success(t('order.myOrders.drawer.bindSnapshot.success'))
emit('success', result)
emit('update:visible', false)
} catch (e: any) {
Message.error(e?.message || t('order.myOrders.drawer.bindSnapshot.failed'))
} finally {
submitting.value = false
}
}
function handleCancel() {
emit('update:visible', false)
}
</script>
<template>
<ModalPro
:visible="visible"
:title="title"
:width="520"
:mask-closable="false"
:confirm-loading="submitting"
@register="registerModal"
@update:visible="onUpdateVisible"
@ok="handleOk"
@cancel="handleCancel"
>
<p class="bind-snapshot-hint">{{ t('order.myOrders.drawer.bindSnapshot.hint') }}</p>
<a-spin :loading="loading" style="width: 100%">
<div class="bind-snapshot-modal">
<div class="bind-snapshot-modal__field">
<div class="bind-snapshot-modal__label">
{{ t('order.myOrders.drawer.bindSnapshot.rulesVersion') }}
</div>
<div class="bind-snapshot-modal__current">
{{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
version: currentRulesVersion || '--',
}) }}
</div>
<a-select
v-model="formState.rulesVersion"
allow-clear
allow-search
:options="rulesOptions"
:placeholder="t('order.myOrders.drawer.bindSnapshot.keepUnchanged')"
/>
</div>
<div class="bind-snapshot-modal__field">
<div class="bind-snapshot-modal__label">
{{ t('order.myOrders.drawer.bindSnapshot.priceVersion') }}
</div>
<div class="bind-snapshot-modal__current">
{{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
version: currentPriceVersion || '--',
}) }}
</div>
<a-select
v-model="formState.priceVersion"
allow-clear
allow-search
:options="priceOptions"
:placeholder="t('order.myOrders.drawer.bindSnapshot.keepUnchanged')"
/>
</div>
<div class="bind-snapshot-modal__checks">
<a-checkbox v-model="formState.recalculateReceivable">
{{ t('order.myOrders.drawer.bindSnapshot.recalcReceivable') }}
</a-checkbox>
<a-checkbox v-model="formState.recalculatePayable">
{{ t('order.myOrders.drawer.bindSnapshot.recalcPayable') }}
</a-checkbox>
</div>
</div>
</a-spin>
</ModalPro>
</template>
<style scoped lang="less">
.bind-snapshot-hint {
margin: 0 0 12px;
color: var(--color-text-3);
font-size: 13px;
line-height: 1.5;
}
.bind-snapshot-modal__field {
margin-bottom: 16px;
}
.bind-snapshot-modal__label {
margin-bottom: 4px;
font-weight: 500;
color: var(--color-text-1);
}
.bind-snapshot-modal__current {
margin-bottom: 8px;
font-size: 12px;
color: var(--color-text-3);
}
.bind-snapshot-modal__checks {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

View File

@ -48,12 +48,14 @@
</template>
<template v-else-if="activeTab === 'fee'">
<a-button
v-if="canEditAdditionalServices"
class="app-btn-search"
:loading="additionalServicesLoading"
@click="openAdditionalServicesModal"
>
{{ $t('order.myOrders.drawer.actions.additionalServices') }}
<template v-if="orderAdditionalServices.length">
({{ orderAdditionalServices.length }})
</template>
</a-button>
<a-button class="app-btn-search" @click="openAddFeeModal">
{{ $t('order.myOrders.drawer.actions.addFee') }}
@ -72,6 +74,9 @@
>
{{ $t('order.myOrders.drawer.actions.recalculatePayable') }}
</a-button>
<a-button class="app-btn-search" @click="openBindSnapshotVersionsModal">
{{ $t('order.myOrders.drawer.actions.bindSnapshotVersions') }}
</a-button>
</template>
<template v-else-if="activeTab === 'track'">
<span class="toolbar-meta">
@ -494,6 +499,44 @@
<!-- 应收应付 -->
<div v-if="activeTab === 'fee'" class="tab-panel fee-info">
<div v-if="boundRulesVersion || boundPriceVersion" class="fee-snapshot-versions">
<span class="fee-snapshot-versions__item">
{{ $t('order.myOrders.drawer.fee.rulesVersion') }}:{{ boundRulesVersion || '--' }}
</span>
<span class="fee-snapshot-versions__item">
{{ $t('order.myOrders.drawer.fee.priceVersion') }}:{{ boundPriceVersion || '--' }}
</span>
</div>
<div class="fee-additional-services">
<span class="fee-additional-services__label">
{{ $t('order.myOrders.drawer.additionalServices') }}:
</span>
<a-space v-if="orderAdditionalServices.length" wrap :size="4">
<a-tag
v-for="code in orderAdditionalServices"
:key="code"
size="small"
color="arcoblue"
>
{{ labelAdditionalService(code) }}
</a-tag>
</a-space>
<span v-else class="fee-additional-services__empty">
{{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
</span>
<a-button
type="text"
size="mini"
class="fee-additional-services__edit"
@click="openAdditionalServicesModal"
>
{{
canEditAdditionalServices
? $t('order.myOrders.drawer.fee.additionalServicesManage')
: $t('order.myOrders.drawer.fee.additionalServicesView')
}}
</a-button>
</div>
<div class="fee-summary">
<button
type="button"
@ -826,6 +869,16 @@
@success="handleAddFeeSuccess"
/>
<BindSnapshotVersionsModal
v-if="bindSnapshotVersionsVisible && orderId != null"
v-model:visible="bindSnapshotVersionsVisible"
:order-id="Number(orderId)"
:sales-product-id="bindSnapshotSalesProductId"
:current-rules-version="boundRulesVersion"
:current-price-version="boundPriceVersion"
@success="handleBindSnapshotVersionsSuccess"
/>
<a-modal
v-model:visible="declarationSkuPreviewVisible"
:title="$t('order.myOrders.drawer.declarationSkuImage')"
@ -850,28 +903,89 @@
<a-modal
v-model:visible="additionalServicesModalVisible"
:title="$t('order.myOrders.drawer.fee.additionalServicesTitle')"
:title="
canEditAdditionalServices
? $t('order.myOrders.drawer.fee.additionalServicesManageTitle')
: $t('order.myOrders.drawer.fee.additionalServicesViewTitle')
"
:ok-loading="additionalServicesSaving"
:ok-text="
canEditAdditionalServices
? $t('order.myOrders.drawer.fee.additionalServicesSave')
: $t('order.myOrders.drawer.fee.additionalServicesClose')
"
:hide-cancel="!canEditAdditionalServices"
unmount-on-close
@ok="handleAdditionalServicesSubmit"
>
<a-spin :loading="additionalServicesLoading">
<a-checkbox-group
v-if="productAdditionalServiceOptions.length"
v-model="selectedAdditionalServices"
>
<a-checkbox
v-for="opt in productAdditionalServiceOptions"
:key="opt.value"
:value="opt.value"
<p class="additional-services-hint">
{{
canEditAdditionalServices
? $t('order.myOrders.drawer.fee.additionalServicesHint')
: $t('order.myOrders.drawer.fee.additionalServicesPaidHint')
}}
</p>
<div class="additional-services-current">
<div class="additional-services-current__label">
{{ $t('order.myOrders.drawer.fee.additionalServicesCurrent') }}
</div>
<a-space v-if="orderAdditionalServices.length" wrap :size="4">
<a-tag
v-for="code in orderAdditionalServices"
:key="`cur-${code}`"
size="small"
color="arcoblue"
>
{{ labelAdditionalService(code) }}
</a-tag>
</a-space>
<span v-else class="additional-services-current__empty">
{{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
</span>
</div>
<template v-if="canEditAdditionalServices">
<div
v-if="visibleOrphanAdditionalServices.length"
class="additional-services-orphan"
>
{{ opt.label }}
</a-checkbox>
</a-checkbox-group>
<a-empty
v-else
:description="$t('order.myOrders.drawer.fee.additionalServicesEmpty')"
/>
<div class="additional-services-orphan__label">
{{ $t('order.myOrders.drawer.fee.additionalServicesOrphan') }}
</div>
<a-space wrap :size="4">
<a-tag
v-for="code in visibleOrphanAdditionalServices"
:key="`orphan-${code}`"
size="small"
color="orangered"
closable
@close="dismissOrphanAdditionalService(code)"
>
{{ labelAdditionalService(code) }}
</a-tag>
</a-space>
</div>
<div class="additional-services-options-label">
{{ $t('order.myOrders.drawer.fee.additionalServicesPick') }}
</div>
<a-checkbox-group
v-if="productAdditionalServiceOptions.length"
v-model="selectedAdditionalServices"
class="additional-services-checkbox-group"
>
<a-checkbox
v-for="opt in productAdditionalServiceOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-checkbox>
</a-checkbox-group>
<a-empty
v-else
:description="$t('order.myOrders.drawer.fee.additionalServicesEmpty')"
/>
</template>
</a-spin>
</a-modal>
</template>
@ -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<number | string | null>(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<string[]>([]);
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;
}
</style>

View File

@ -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)) {