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 f71469f..3246ae7 100644
--- a/src/api/order/index.ts
+++ b/src/api/order/index.ts
@@ -1181,6 +1181,71 @@ function mapExceptionEventItem(raw: any): OrderExceptionEventItem {
}
}
+export interface OrderOperationLogChangeItem {
+ path: string
+ label: string
+ before?: string | null
+ after?: string | null
+}
+
+export interface OrderOperationLogItem {
+ id: number
+ orderId: number
+ serviceNo?: string
+ action: string
+ actionName: string
+ operatorId: number
+ operatorName?: string
+ operatorType?: string
+ source?: string
+ changes: OrderOperationLogChangeItem[]
+ remark?: string
+ createTime?: string
+}
+
+export interface OrderOperationLogListResult {
+ items: OrderOperationLogItem[]
+}
+
+function mapOperationLogItem(raw: any): OrderOperationLogItem {
+ const changesRaw = raw?.changes ?? raw?.Changes ?? []
+ const changes = Array.isArray(changesRaw)
+ ? changesRaw.map((c: any) => ({
+ path: String(c?.path ?? c?.Path ?? ''),
+ label: String(c?.label ?? c?.Label ?? c?.path ?? c?.Path ?? ''),
+ before: c?.before ?? c?.Before ?? null,
+ after: c?.after ?? c?.After ?? null,
+ }))
+ : []
+ return {
+ id: Number(raw?.id ?? raw?.Id ?? 0),
+ orderId: Number(raw?.orderId ?? raw?.OrderId ?? 0),
+ serviceNo: raw?.serviceNo ?? raw?.ServiceNo,
+ action: String(raw?.action ?? raw?.Action ?? ''),
+ actionName: String(raw?.actionName ?? raw?.ActionName ?? ''),
+ operatorId: Number(raw?.operatorId ?? raw?.OperatorId ?? 0),
+ operatorName: raw?.operatorName ?? raw?.OperatorName,
+ operatorType: raw?.operatorType ?? raw?.OperatorType,
+ source: raw?.source ?? raw?.Source,
+ changes,
+ remark: raw?.remark ?? raw?.Remark,
+ createTime: raw?.createTime ?? raw?.CreateTime,
+ }
+}
+
+/** GET /order/operation-log/list */
+export async function getOrderOperationLogList(
+ orderId: number | string,
+): Promise {
+ const res = await axiosInstance.get(orderUrl('/order/operation-log/list'), {
+ params: { orderId: Number(orderId) },
+ })
+ const data = unwrapData(res.data) ?? {}
+ const itemsRaw = data?.items ?? data?.Items ?? []
+ const items = Array.isArray(itemsRaw) ? itemsRaw.map(mapOperationLogItem) : []
+ return { items }
+}
+
/** GET /order/exception-events/list */
export async function getOrderExceptionEventList(orderId: number | string): Promise {
const res = await axiosInstance.get(orderUrl('/order/exception-events/list'), {
@@ -2087,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/api/order/pickupOrder.ts b/src/api/order/pickupOrder.ts
index b948fe8..aeddc19 100644
--- a/src/api/order/pickupOrder.ts
+++ b/src/api/order/pickupOrder.ts
@@ -73,6 +73,25 @@ export const schedulePickupOrder = (body: {
export const reschedulePickupOrder = (body: { id: number | string; expectPickupTime: string }) =>
postOk('/order/pickup-order/reschedule', body as unknown as Record)
+export const updatePickupOrderInfo = (body: {
+ id: number | string
+ warehouseId?: number
+ senderAddressId?: number
+ expectPickupTime?: string
+ carrierService?: string
+ carrierWaybillNo?: string
+ remark?: string
+}) => postOk('/order/pickup-order/update-info', body as unknown as Record)
+
+export const updatePickupOrderMeasure = (body: {
+ id: number | string
+ totalActualWeight: number
+ packageSizes: Array<{ length: number; width: number; height: number; count: number }>
+}) => postOk('/order/pickup-order/update-measure', body as unknown as Record)
+
+export const recalcPickupOrderFee = (id: number | string) =>
+ postOk('/order/pickup-order/recalc-fee', { id })
+
export const markPickupPicked = (id: number | string) =>
postOk('/order/pickup-order/mark-picked', { id })
diff --git a/src/api/system/warehouse.ts b/src/api/system/warehouse.ts
index ea0e347..f8d43fe 100644
--- a/src/api/system/warehouse.ts
+++ b/src/api/system/warehouse.ts
@@ -9,6 +9,10 @@ export interface WarehouseDto {
recipient?: string
contactPhone?: string
shippingAddress?: string
+ /** 展示排序,越小越靠前;可不填 */
+ sort?: number | null
+ /** 备注(选填) */
+ remark?: string
createTime?: string
updateTime?: string
createBy?: string
diff --git a/src/components/InterfaceDetail/index.vue b/src/components/InterfaceDetail/index.vue
index bfde355..ce5205e 100644
--- a/src/components/InterfaceDetail/index.vue
+++ b/src/components/InterfaceDetail/index.vue
@@ -220,7 +220,8 @@
originalParamIds.value = source
? buildOriginalParamIdsFromHydrated(source)
: createEmptyOriginalParamIds();
- nextTick(() => captureDirtyBaseline());
+ // 等表格/公共参数/表单回填落稳后再采基线,避免「刚打开就提示未保存」
+ void settleDirtyBaseline();
}
watch(
@@ -288,6 +289,10 @@
const exampleDirtyBaseline = ref({ request: '', response: '' });
const manualDirty = ref(false);
const isDirty = ref(false);
+ /** 接口明细加载/回填中:不算未保存,避免误报 */
+ const detailHydrating = ref(false);
+ let baselineSettleSeq = 0;
+ const dirtyBaselineReady = ref(false);
function pickFormDirtySnapshot() {
const d: any = props.basicFormData || {};
@@ -318,15 +323,24 @@
}
function hasUnsavedChanges() {
+ // 加载未完成时不提示「有未保存更改」
+ if (detailHydrating.value || !dirtyBaselineReady.value) return false;
if (manualDirty.value) return true;
if (fixedParamsHasChanges.value) return true;
if (returnConfigHasChanges.value) return true;
if (formDirtyBaseline.value && pickFormDirtySnapshot() !== formDirtyBaseline.value) return true;
if ((requestExample.value || '') !== (exampleDirtyBaseline.value.request || '')) return true;
if ((responseExample.value || '') !== (exampleDirtyBaseline.value.response || '')) return true;
- if (actionRowsDirty(() => queryTableRef.value?.getActionRows?.())) return true;
- if (actionRowsDirty(() => headerTableRef.value?.getActionRows?.())) return true;
- if (actionRowsDirty(() => bodyFormTableRef.value?.getActionRows?.())) return true;
+ // 公共参数行不参与脏检查
+ if (actionRowsDirty(() => filterCommonFromActionRows(queryTableRef.value?.getActionRows?.()))) {
+ return true;
+ }
+ if (actionRowsDirty(() => filterCommonFromActionRows(headerTableRef.value?.getActionRows?.()))) {
+ return true;
+ }
+ if (actionRowsDirty(() => filterCommonFromActionRows(bodyFormTableRef.value?.getActionRows?.()))) {
+ return true;
+ }
if (
actionRowsDirty(() =>
stripBodyRootFromActionRows(requestTableRef.value?.getActionRows?.() || {}),
@@ -344,6 +358,7 @@
}
function markDirty() {
+ if (detailHydrating.value) return;
manualDirty.value = true;
isDirty.value = true;
}
@@ -356,6 +371,60 @@
response: responseExample.value || '',
};
isDirty.value = false;
+ dirtyBaselineReady.value = true;
+ }
+
+ /** 用当前 props.data 再 reload 一次,清掉 VXE keep-source 在回填过程中留下的假 insert/update */
+ function reseatParamTables() {
+ const reload = (refLike: { value?: { reloadData?: (rows: any[]) => void } }, rows: any[]) => {
+ if (!refLike?.value?.reloadData) return;
+ refLike.value.reloadData(Array.isArray(rows) ? [...rows] : []);
+ };
+ reload(queryTableRef, queryTableProps.data);
+ reload(headerTableRef, headerTableProps.data);
+ reload(bodyFormTableRef, bodyFormTableProps.data);
+ reload(mappingBodyTableRef, mappingBodyTableProps.data);
+ reload(requestTableRef, requestTableProps.data);
+ reload(responseTableRef, responseTableProps.data);
+ }
+
+ /**
+ * 等待公共参数合并、表格挂载、FormPro/SelectPro 回填后再采脏检查基线。
+ * 解决:选中接口后立刻切 API 调试误报「有未保存的更改」。
+ */
+ async function settleDirtyBaseline() {
+ const seq = ++baselineSettleSeq;
+ detailHydrating.value = true;
+ dirtyBaselineReady.value = false;
+ manualDirty.value = false;
+ isDirty.value = false;
+
+ await nextTick();
+ await nextTick();
+ if (seq !== baselineSettleSeq) return;
+
+ // 等服务商公共参数拉取并合并进 Query/Header/Body
+ const waitStart = Date.now();
+ while (inheritedCommonParamsLoading.value && Date.now() - waitStart < 4000) {
+ await new Promise((r) => setTimeout(r, 40));
+ if (seq !== baselineSettleSeq) return;
+ }
+
+ await nextTick();
+ reseatParamTables();
+ await nextTick();
+ if (seq !== baselineSettleSeq) return;
+
+ captureDirtyBaseline();
+ detailHydrating.value = false;
+
+ // SelectPro/字典异步回写可能再改一帧表单值,二次采基线(仅无手动编辑时)
+ await new Promise((r) => setTimeout(r, 120));
+ if (seq !== baselineSettleSeq || manualDirty.value) return;
+ reseatParamTables();
+ await nextTick();
+ if (seq !== baselineSettleSeq || manualDirty.value) return;
+ captureDirtyBaseline();
}
/** 浏览器关闭/刷新:有未保存更改时提示 */
@@ -370,6 +439,8 @@
* resolve true = 可离开(丢弃更改),false = 留下继续编辑。
*/
async function confirmLeaveUnsaved(options?: { content?: string }): Promise {
+ // 仍在回填:用户不可能已编辑,直接放行
+ if (detailHydrating.value || !dirtyBaselineReady.value) return true;
refreshDirtyState();
if (!hasUnsavedChanges()) return true;
return new Promise((resolve) => {
@@ -562,11 +633,19 @@
(id, oldId) => {
if (oldId) saveTabMemory(oldId);
if (id) {
+ // 切换接口:进入回填态,避免沿用上一接口的脏标记
+ detailHydrating.value = true;
+ dirtyBaselineReady.value = false;
+ manualDirty.value = false;
+ isDirty.value = false;
nextTick(() => {
restoreTabMemory(id);
- // 加载完成后由父组件 hydrate 再 capture;此处先清 manual
- manualDirty.value = false;
});
+ } else {
+ detailHydrating.value = false;
+ dirtyBaselineReady.value = false;
+ manualDirty.value = false;
+ isDirty.value = false;
}
},
);
@@ -3283,7 +3362,8 @@
}
} else {
fixedParams.value = payload.fixedParams;
- fixedParamsHasChanges.value = payload.fixedParams.length > 0;
+ // 有固定配置数据 ≠ 未保存更改;加载后应视为干净
+ fixedParamsHasChanges.value = false;
await nextTick();
queryFixedTableRef.value?.reloadData?.(payload.fixedParams);
}
@@ -3825,7 +3905,7 @@
参数名:与服务商文档、curl、Mapping Body / Body 参数树、前置脚本写入字段名保持一致(如 _aop_signature、notifyBizEventDTO)。
参数值:使用占位符或留空,不要填写 curl 中的真实签名/业务样例;转发前由 Like API 替换。
$FromMappingBody(FormUrlEncoded):Query 上承载业务 JSON 的字段(如 *DTO)填此占位符,映射结果来自 Mapping Body 参数树;POST 参数仅在 URL、无 Body 时,DTO 应配置在 Query 而非 Body 表单。
- $FromAccount:按参数名从服务商账户取值;$TimeStamp / $DateTime:时间;$token:令牌;$Url:当前请求地址。
+ $FromAccount:按参数名从服务商账户取值;$TimeStamp / $DateTime:时间;$Token:令牌;$Url:当前请求地址。
留空:签名类参数(如 sign、_aop_signature)由前置脚本写入。
参数描述:可记录备注或 curl 片段,便于对照映射,不参与转发。
diff --git a/src/constants/itfxEnums.ts b/src/constants/itfxEnums.ts
index 06ebc3c..b616203 100644
--- a/src/constants/itfxEnums.ts
+++ b/src/constants/itfxEnums.ts
@@ -100,7 +100,7 @@ export const ParameterPlaceholderText: Record = {
[ParameterPlaceholder.TimeStamp]: '$TimeStamp',
[ParameterPlaceholder.DateTime]: '$DateTime',
[ParameterPlaceholder.Url]: '$Url',
- [ParameterPlaceholder.Token]: '$token',
+ [ParameterPlaceholder.Token]: '$Token',
[ParameterPlaceholder.FromMappingBody]: '$FromMappingBody',
};
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 4f7adff..a532149 100644
--- a/src/locale/en-US.ts
+++ b/src/locale/en-US.ts
@@ -896,6 +896,26 @@ export default {
'order.pickupOrder.settlementStatus.pending': 'Pending',
'order.pickupOrder.settlementStatus.settled': 'Settled',
'order.pickupOrder.actions.reschedule': 'Reschedule',
+ 'order.pickupOrder.actions.editInfo': 'Edit pickup info',
+ 'order.pickupOrder.actions.saveInfo': 'Save',
+ 'order.pickupOrder.actions.cancelEdit': 'Cancel',
+ 'order.pickupOrder.actions.editMeasure': 'Edit dimensions',
+ 'order.pickupOrder.actions.saveMeasure': 'Save dimensions',
+ 'order.pickupOrder.actions.recalcFee': 'Recalculate fee',
+ 'order.pickupOrder.actions.addSize': 'Add size',
+ 'order.pickupOrder.actions.removeSize': 'Remove',
+ 'order.pickupOrder.form.carrierServicePlaceholder': 'Select carrier service',
+ 'order.pickupOrder.form.sizeLength': 'L',
+ 'order.pickupOrder.form.sizeWidth': 'W',
+ 'order.pickupOrder.form.sizeHeight': 'H',
+ 'order.pickupOrder.form.sizeCount': 'Qty',
+ 'order.pickupOrder.message.infoSaveOk': 'Pickup info saved',
+ 'order.pickupOrder.message.measureSaveOk': 'Dimensions saved',
+ 'order.pickupOrder.message.recalcOk': 'Fee recalculated',
+ 'order.pickupOrder.message.recalcNeedMeasure': 'Save weight and sizes before recalculating',
+ 'order.pickupOrder.message.packageSizeRequired': 'Enter at least one valid size',
+ 'order.pickupOrder.message.expectTimeRequired': 'Enter expected pickup time',
+ 'order.pickupOrder.message.optionsLoadFailed': 'Failed to load options',
'order.pickupOrder.actions.batchConfirmFreight': 'Batch Confirm Freight',
'order.pickupOrder.actions.batchSchedule': 'Batch Draft Forecast',
'order.pickupOrder.actions.copyServiceNos': 'Copy all service Nos.',
@@ -1799,7 +1819,7 @@ export default {
'order.myOrders.actions.markProcessed': 'Mark processed',
'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)',
'order.myOrders.reallocate.hint':
- 'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Paid orders are skipped. Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.',
+ 'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Available for predeclared / pending allocation / payment / ship (including paid). Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.',
'order.myOrders.reallocate.section.scheme': 'Allocation scheme',
'order.myOrders.reallocate.section.channel': 'Designated channel',
'order.myOrders.reallocate.schemeLabel': 'Allocation scheme',
@@ -1856,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',
@@ -2110,6 +2143,15 @@ export default {
'order.myOrders.drawer.tabs.track': 'Tracking',
'order.myOrders.drawer.tabs.attachments': 'Attachments',
'order.myOrders.drawer.tabs.stockBind': 'Bound stock',
+ 'order.myOrders.drawer.tabs.operationLog': 'Operation log',
+ 'order.myOrders.drawer.operationLog.empty': 'No operation logs',
+ 'order.myOrders.drawer.operationLog.refresh': 'Refresh',
+ 'order.myOrders.drawer.operationLog.count': '{count} items',
+ 'order.myOrders.drawer.operationLog.field': 'Field',
+ 'order.myOrders.drawer.operationLog.before': 'Before',
+ 'order.myOrders.drawer.operationLog.after': 'After',
+ 'order.myOrders.drawer.operationLog.noChanges': 'No field changes',
+ 'order.myOrders.drawer.operationLog.operator': 'Operator',
'order.myOrders.drawer.tabs.issueHandling': 'Issue handling',
'order.myOrders.drawer.issueHandling.pendingCount': 'Pending {count}',
'order.myOrders.drawer.issueHandling.processedCount': 'Processed {count}',
@@ -2176,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)',
@@ -2342,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',
@@ -2373,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',
@@ -3046,6 +3126,8 @@ export default {
'system.interface.mapping.table.providerParamName': 'Provider parameter name',
'system.interface.mapping.table.providerParamNameCn': 'Provider parameter description',
'system.interface.mapping.table.paramValue': 'Parameter value',
+ 'system.interface.mapping.table.paramValueTip':
+ 'If a value looks like both a number and a string (e.g. phone 075525131031), wrap it in single quotes as \'075525131031\' to mark it as a string. Entering it without quotes treats it as a number and may drop leading zeros or cause type errors.',
'system.interface.mapping.table.inputParam': 'Input parameter',
'system.interface.mapping.table.inputParamCn': 'Input parameter description',
'system.interface.mapping.actions.prefixScript': 'Prefix script',
@@ -4085,8 +4167,12 @@ export default {
'system.warehouseAddress.fields.recipient': 'Recipient',
'system.warehouseAddress.fields.contactPhone': 'Contact Phone',
'system.warehouseAddress.fields.shippingAddress': 'Shipping Address',
+ 'system.warehouseAddress.fields.sort': 'Sort',
+ 'system.warehouseAddress.fields.remark': 'Remark',
'system.warehouseAddress.fields.servicePhone': 'Service Phone',
'system.warehouseAddress.fields.serviceWechat': 'Service WeChat',
+ 'system.warehouseAddress.placeholder.sort': 'Smaller first (optional)',
+ 'system.warehouseAddress.placeholder.remark': 'Optional note shown to customers',
'system.warehouseAddress.actions.add': 'Add',
'system.warehouseAddress.actions.edit': 'Edit',
'system.warehouseAddress.actions.copy': 'Copy',
diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts
index 82036bb..4b1a7c6 100644
--- a/src/locale/zh-CN.ts
+++ b/src/locale/zh-CN.ts
@@ -885,6 +885,26 @@ export default {
'order.pickupOrder.settlementStatus.pending': '待结算',
'order.pickupOrder.settlementStatus.settled': '已结算',
'order.pickupOrder.actions.reschedule': '改约',
+ 'order.pickupOrder.actions.editInfo': '编辑揽收信息',
+ 'order.pickupOrder.actions.saveInfo': '保存',
+ 'order.pickupOrder.actions.cancelEdit': '取消',
+ 'order.pickupOrder.actions.editMeasure': '编辑件重尺',
+ 'order.pickupOrder.actions.saveMeasure': '保存件重尺',
+ 'order.pickupOrder.actions.recalcFee': '重算费用',
+ 'order.pickupOrder.actions.addSize': '添加尺寸',
+ 'order.pickupOrder.actions.removeSize': '删除',
+ 'order.pickupOrder.form.carrierServicePlaceholder': '请选择承运服务',
+ 'order.pickupOrder.form.sizeLength': '长',
+ 'order.pickupOrder.form.sizeWidth': '宽',
+ 'order.pickupOrder.form.sizeHeight': '高',
+ 'order.pickupOrder.form.sizeCount': '件数',
+ 'order.pickupOrder.message.infoSaveOk': '揽收信息已保存',
+ 'order.pickupOrder.message.measureSaveOk': '件重尺已保存',
+ 'order.pickupOrder.message.recalcOk': '费用已重算',
+ 'order.pickupOrder.message.recalcNeedMeasure': '请先保存实重与尺寸后再重算',
+ 'order.pickupOrder.message.packageSizeRequired': '请至少填写一条有效尺寸',
+ 'order.pickupOrder.message.expectTimeRequired': '请填写期望揽收时间',
+ 'order.pickupOrder.message.optionsLoadFailed': '加载选项失败',
'order.pickupOrder.actions.batchConfirmFreight': '批量确认运费',
'order.pickupOrder.actions.batchSchedule': '批量草稿预报',
'order.pickupOrder.actions.copyServiceNos': '复制全部运单号',
@@ -1626,7 +1646,7 @@ export default {
'order.waybillManage.importDownstream.failRow': '第 {row} 行({waybill}):{reason}',
'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)',
'order.myOrders.reallocate.hint':
- '仅重算应付并更新服务渠道,不改应收与件重尺;已付款订单会自动跳过。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
+ '仅重算应付并更新服务渠道,不改应收与件重尺;已预报/待排货/待支付/待发货可用(含已付款)。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
'order.myOrders.reallocate.section.scheme': '排货方案',
'order.myOrders.reallocate.section.channel': '指定渠道',
'order.myOrders.reallocate.schemeLabel': '排货方案',
@@ -1788,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',
@@ -1971,6 +2004,15 @@ export default {
'order.myOrders.drawer.tabs.track': '轨迹',
'order.myOrders.drawer.tabs.attachments': '附件',
'order.myOrders.drawer.tabs.stockBind': '绑定包裹/库存',
+ 'order.myOrders.drawer.tabs.operationLog': '操作日志',
+ 'order.myOrders.drawer.operationLog.empty': '暂无操作日志',
+ 'order.myOrders.drawer.operationLog.refresh': '刷新',
+ 'order.myOrders.drawer.operationLog.count': '{count} 条',
+ 'order.myOrders.drawer.operationLog.field': '字段',
+ 'order.myOrders.drawer.operationLog.before': '修改前',
+ 'order.myOrders.drawer.operationLog.after': '修改后',
+ 'order.myOrders.drawer.operationLog.noChanges': '无字段变更',
+ 'order.myOrders.drawer.operationLog.operator': '操作人',
'order.myOrders.drawer.tabs.issueHandling': '问题处理',
'order.myOrders.drawer.issueHandling.pendingCount': '待处理 {count}',
'order.myOrders.drawer.issueHandling.processedCount': '已处理 {count}',
@@ -2037,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': '历史快照(排货当时结果)',
@@ -2203,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': '保存成功',
@@ -2234,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': '添加费用',
@@ -2908,6 +2987,8 @@ export default {
'system.interface.mapping.table.providerParamName': '服务商参数名',
'system.interface.mapping.table.providerParamNameCn': '服务商参数描述',
'system.interface.mapping.table.paramValue': '参数值',
+ 'system.interface.mapping.table.paramValueTip':
+ '若填写的值既像数字又像字符串(例如电话号 075525131031),请用单引号包起来写成 \'075525131031\',明确为字符串。直接填写会被当成数字解析,可能导致前导零丢失或类型错误。',
'system.interface.mapping.table.inputParam': '入参',
'system.interface.mapping.table.inputParamCn': '入参描述',
'system.interface.mapping.actions.prefixScript': '前置脚本',
@@ -4113,8 +4194,12 @@ export default {
'system.warehouseAddress.fields.recipient': '收件人',
'system.warehouseAddress.fields.contactPhone': '联系电话',
'system.warehouseAddress.fields.shippingAddress': '收货地址',
+ 'system.warehouseAddress.fields.sort': '排序',
+ 'system.warehouseAddress.fields.remark': '备注',
'system.warehouseAddress.fields.servicePhone': '客服电话',
'system.warehouseAddress.fields.serviceWechat': '客服微信',
+ 'system.warehouseAddress.placeholder.sort': '越小越靠前,可不填',
+ 'system.warehouseAddress.placeholder.remark': '选填,展示给客户的说明',
'system.warehouseAddress.actions.add': '新增',
'system.warehouseAddress.actions.edit': '编辑',
'system.warehouseAddress.actions.copy': '复制',
diff --git a/src/utils/interfaceParameterModel.ts b/src/utils/interfaceParameterModel.ts
index 89b139d..ccf2971 100644
--- a/src/utils/interfaceParameterModel.ts
+++ b/src/utils/interfaceParameterModel.ts
@@ -399,6 +399,14 @@ function isSameFormKvRow(row: any, baseline: NormalizedParamRow): boolean {
);
}
+function isPersistableFormKvRow(p: any) {
+ // 删除只依赖 id;参数值允许留空(签名类由前置脚本写入)
+ if (Number(p?.optionType) === 2) {
+ return Number(p?.id) > 0;
+ }
+ return !!(p.name && String(p.name).trim() !== '');
+}
+
function buildFormKvParameterList(
changes: any[],
tableData: any[],
@@ -406,24 +414,23 @@ function buildFormKvParameterList(
isEditMode: boolean,
original: OriginalParamSnapshot,
) {
- const isValid = (p: any) =>
- p.name && String(p.name).trim() !== '' && p.value && String(p.value).trim() !== '';
-
const changeById = new Map(changes.map((c) => [Number(c.id), c]));
const removedIds = new Set(
changes.filter((c) => c.optionType === 2).map((c) => Number(c.id)),
);
if (!isEditMode) {
- if (changes.length) return changes;
+ if (changes.length) return changes.filter(isPersistableFormKvRow);
return (tableData || [])
.map((item) => mapFormKvRow(stripFormKvMeta(item), 1, fieldPosition))
- .filter(isValid);
+ .filter(isPersistableFormKvRow);
}
const result: any[] = [];
changes.filter((c) => c.optionType === 2).forEach((c) => result.push(c));
+ const presentServerIds = new Set();
+
(tableData || []).forEach((item) => {
const rest = stripFormKvMeta(item);
const id = normalizeRowId(rest);
@@ -435,6 +442,8 @@ function buildFormKvParameterList(
const tracked = changeById.get(id);
if (tracked && tracked.optionType !== 2) {
result.push(tracked);
+ const trackedServerId = Number(tracked.id);
+ if (trackedServerId > 0) presentServerIds.add(trackedServerId);
return;
}
@@ -443,6 +452,7 @@ function buildFormKvParameterList(
const baseline = serverId ? original.rowById.get(serverId) : undefined;
if (serverId && baseline) {
+ presentServerIds.add(serverId);
if (!isSameFormKvRow(rest, baseline)) {
result.push(mapFormKvRow(rest, 3, fieldPosition, serverId));
}
@@ -454,7 +464,22 @@ function buildFormKvParameterList(
}
});
- return result.filter(isValid);
+ // VXE removeRecords 丢失时,仍按「原始快照有、当前表无」补删,避免库里残留
+ original.ids.forEach((serverId) => {
+ if (removedIds.has(serverId) || presentServerIds.has(serverId)) return;
+ const baseline = original.rowById.get(serverId);
+ if (!baseline?.name) return;
+ result.push({
+ id: serverId,
+ name: baseline.name,
+ value: baseline.alias || '',
+ description: baseline.description || '',
+ optionType: 2,
+ fieldPosition,
+ });
+ });
+
+ return result.filter(isPersistableFormKvRow);
}
function mapFormKvChanges(
@@ -476,9 +501,7 @@ function mapFormKvChanges(
...(actionRows.updateRecords || []).map((r) => mapRow(r, 3)),
];
- return list.filter(
- (p) => p.name && String(p.name).trim() !== '' && p.value && String(p.value).trim() !== '',
- );
+ return list.filter(isPersistableFormKvRow);
}
export interface InterfaceDetailSnapshot {
diff --git a/src/utils/warehouseShipAddress.ts b/src/utils/warehouseShipAddress.ts
new file mode 100644
index 0000000..bb382aa
--- /dev/null
+++ b/src/utils/warehouseShipAddress.ts
@@ -0,0 +1,49 @@
+/**
+ * 客户登录时展示/复制仓库寄件信息:
+ * - 收件人后拼接客户编号(空格分隔)
+ * - 地址后拼接客户编号 +「号」(无空格)
+ * 例:张志坚 AI0A1 / …二楼AI0A1号
+ */
+
+/** 有 Sort 的升序靠前,无 Sort 的靠后,再按 id */
+export function compareWarehouseSort(
+ a: { sort?: number | null; id?: number | string | null; value?: string | number | null },
+ b: { sort?: number | null; id?: number | string | null; value?: string | number | null },
+): number {
+ const aHas = a.sort != null && Number.isFinite(Number(a.sort));
+ const bHas = b.sort != null && Number.isFinite(Number(b.sort));
+ if (aHas !== bHas) return aHas ? -1 : 1;
+ if (aHas && bHas) {
+ const diff = Number(a.sort) - Number(b.sort);
+ if (diff !== 0) return diff;
+ }
+ const aId = Number(a.id ?? a.value ?? 0);
+ const bId = Number(b.id ?? b.value ?? 0);
+ return aId - bId;
+}
+
+export function formatWarehouseShipRecipient(
+ recipient?: string | null,
+ customerCode?: string | null,
+): string {
+ const base = String(recipient ?? '').trim();
+ const code = String(customerCode ?? '').trim();
+ if (!code) return base || '-';
+ if (!base) return code;
+ if (base === code || base.endsWith(` ${code}`) || base.endsWith(code)) return base;
+ return `${base} ${code}`;
+}
+
+export function formatWarehouseShipAddress(
+ address?: string | null,
+ customerCode?: string | null,
+): string {
+ const base = String(address ?? '').trim();
+ const code = String(customerCode ?? '').trim();
+ if (!code) return base || '-';
+ const withHao = `${code}号`;
+ if (!base) return withHao;
+ if (base.endsWith(withHao)) return base;
+ if (base.endsWith(code)) return `${base}号`;
+ return `${base}${withHao}`;
+}
diff --git a/src/views/api/downstreamInterface/comps/AccountList.vue b/src/views/api/downstreamInterface/comps/AccountList.vue
index 68d00f7..706092c 100644
--- a/src/views/api/downstreamInterface/comps/AccountList.vue
+++ b/src/views/api/downstreamInterface/comps/AccountList.vue
@@ -1,5 +1,6 @@
+
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.hint') }}
+
+
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.rulesVersion') }}
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
+ version: currentRulesVersion || '--',
+ }) }}
+
+
+
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.priceVersion') }}
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
+ version: currentPriceVersion || '--',
+ }) }}
+
+
+
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.recalcReceivable') }}
+
+
+ {{ t('order.myOrders.drawer.bindSnapshot.recalcPayable') }}
+
+
+
+
+
+
+
+
diff --git a/src/views/order/waybillManage/components/OrderDetailDrawer.vue b/src/views/order/waybillManage/components/OrderDetailDrawer.vue
index 55249d4..3702c44 100644
--- a/src/views/order/waybillManage/components/OrderDetailDrawer.vue
+++ b/src/views/order/waybillManage/components/OrderDetailDrawer.vue
@@ -17,7 +17,7 @@
@@ -483,6 +499,44 @@
+
+
+ {{ $t('order.myOrders.drawer.fee.rulesVersion') }}:{{ boundRulesVersion || '--' }}
+
+
+ {{ $t('order.myOrders.drawer.fee.priceVersion') }}:{{ boundPriceVersion || '--' }}
+
+
+
+
+ {{ $t('order.myOrders.drawer.additionalServices') }}:
+
+
+
+ {{ labelAdditionalService(code) }}
+
+
+
+ {{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
+
+
+ {{
+ canEditAdditionalServices
+ ? $t('order.myOrders.drawer.fee.additionalServicesManage')
+ : $t('order.myOrders.drawer.fee.additionalServicesView')
+ }}
+
+
+
+
+
+
+
+
+
+
+
{{ item.actionName || item.action }}
+
+ {{ item.changes.length }}
+
+
+
+ {{ item.operatorName || $t('order.myOrders.drawer.operationLog.operator') }}
+ {{ formatOperationLogTime(item.createTime) }}
+
+
+
+
+
+
+
+
+
+ {{ change.label || change.path }}
+ {{ change.before ?? '--' }}
+ {{ change.after ?? '--' }}
+
+
+
+
+
+
+
+
+
@@ -758,6 +869,16 @@
@success="handleAddFeeSuccess"
/>
+
+
-
-
+ {{
+ canEditAdditionalServices
+ ? $t('order.myOrders.drawer.fee.additionalServicesHint')
+ : $t('order.myOrders.drawer.fee.additionalServicesPaidHint')
+ }}
+
+
+
+ {{ $t('order.myOrders.drawer.fee.additionalServicesCurrent') }}
+
+
+
+ {{ labelAdditionalService(code) }}
+
+
+
+ {{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
+
+
+
+
- {{ opt.label }}
-
-
-
+
+ {{ $t('order.myOrders.drawer.fee.additionalServicesOrphan') }}
+
+
+
+ {{ labelAdditionalService(code) }}
+
+
+
+
+ {{ $t('order.myOrders.drawer.fee.additionalServicesPick') }}
+
+
+
+ {{ opt.label }}
+
+
+
+
@@ -813,7 +995,7 @@ import { ref, computed, watch, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
-import { IconRefresh, IconRobot, IconEye } from '@arco-design/web-vue/es/icon';
+import { IconRefresh, IconRobot, IconEye, IconDown, IconUp } from '@arco-design/web-vue/es/icon';
import useDrawer from '@/components/DrawerPro/useDrawer';
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
import {
@@ -822,6 +1004,8 @@ import {
postOrderRecalculateReceivable,
postOrderRecalculatePayable,
postOrderAdditionalServicesUpdate,
+ getOrderOperationLogList,
+ type OrderOperationLogItem,
} from '@/api/order';
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
import { getDictListAll, type DictDto } from '@/api/system/dict';
@@ -830,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';
@@ -879,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);
@@ -1197,8 +1384,13 @@ const tabList = computed(() => [
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
{ key: 'issueHandling', label: t('order.myOrders.drawer.tabs.issueHandling') },
+ { key: 'operationLog', label: t('order.myOrders.drawer.tabs.operationLog') },
]);
+const operationLogList = ref([]);
+const operationLogLoading = ref(false);
+const operationLogExpandedIds = ref>(new Set());
+
const slidingTabItems = computed(() =>
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
);
@@ -1213,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' },
@@ -1774,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(() => {
@@ -1982,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;
@@ -2362,6 +2577,75 @@ 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) {
+ operationLogList.value = [];
+ return;
+ }
+ operationLogLoading.value = true;
+ try {
+ const res = await getOrderOperationLogList(id);
+ operationLogList.value = res.items || [];
+ operationLogExpandedIds.value = new Set(
+ operationLogList.value.slice(0, 3).map((x) => x.id),
+ );
+ } catch {
+ operationLogList.value = [];
+ } finally {
+ operationLogLoading.value = false;
+ }
+}
+
+function isOperationLogExpanded(id: number) {
+ return operationLogExpandedIds.value.has(id);
+}
+
+function toggleOperationLogExpand(id: number) {
+ const next = new Set(operationLogExpandedIds.value);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
+ operationLogExpandedIds.value = next;
+}
+
+function formatOperationLogTime(value?: string) {
+ if (!value) return '--';
+ const d = new Date(value);
+ if (Number.isNaN(d.getTime())) return String(value);
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+}
+
function handleTabChange(tabKey: string) {
resetEditState();
if (tabKey === 'basic') {
@@ -2378,6 +2662,9 @@ function handleTabChange(tabKey: string) {
if (tabKey === 'track') {
loadTrackTimeline();
}
+ if (tabKey === 'operationLog') {
+ void loadOperationLogs();
+ }
}
function handleCancel() {
@@ -2400,13 +2687,16 @@ watch(
if (activeTab.value === 'track') {
loadTrackTimeline();
}
+ if (activeTab.value === 'operationLog') {
+ void loadOperationLogs();
+ }
}
},
{ immediate: true, deep: true }
);
defineExpose({
- openDrawer: (opts?: { tab?: 'basic' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'issueHandling' }) => {
+ openDrawer: (opts?: { tab?: 'basic' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'issueHandling' | 'operationLog' }) => {
activeTab.value = opts?.tab || 'basic';
basicViewMode.value = 'all';
feeViewMode.value = 'all';
@@ -3026,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;
@@ -3515,4 +3848,143 @@ defineExpose({
padding-bottom: 0;
}
}
+
+.operation-log-spin {
+ width: 100%;
+}
+
+.operation-log-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.operation-log-item {
+ border: 1px solid var(--color-border-2);
+ border-radius: 4px;
+ background: #fff;
+ overflow: hidden;
+}
+
+.operation-log-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 12px;
+ cursor: pointer;
+ user-select: none;
+
+ &:hover {
+ background: var(--color-fill-1);
+ }
+}
+
+.operation-log-head-main {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.operation-log-action {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-1);
+}
+
+.operation-log-head-meta {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+ font-size: 12px;
+ color: var(--color-text-3);
+}
+
+.operation-log-body {
+ padding: 0 12px 12px;
+ border-top: 1px solid var(--color-border-2);
+}
+
+.operation-log-remark {
+ margin: 10px 0 0;
+ font-size: 12px;
+ color: var(--color-text-3);
+ word-break: break-word;
+}
+
+.operation-log-changes {
+ margin-top: 10px;
+ border: 1px solid var(--color-border-2);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.operation-log-changes-header,
+.operation-log-changes-row {
+ display: grid;
+ grid-template-columns: 1.1fr 1fr 1fr;
+ gap: 8px;
+ padding: 8px 10px;
+ font-size: 12px;
+}
+
+.operation-log-changes-header {
+ background: var(--color-fill-2);
+ color: var(--color-text-3);
+ font-weight: 500;
+}
+
+.operation-log-changes-row {
+ color: var(--color-text-2);
+ border-top: 1px solid var(--color-border-2);
+ word-break: break-all;
+}
+
+.operation-log-after {
+ 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 fc0f4c7..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')),
@@ -423,7 +427,7 @@
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
);
const showReallocateAction = computed(() =>
- isActiveTab('all', 'pendingAllocation', 'pendingPayment'),
+ isActiveTab('all', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
);
const problematicModalVisible = ref(false);
@@ -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)) {
diff --git a/src/views/system/warehouseAddress/index.vue b/src/views/system/warehouseAddress/index.vue
index 9c7d22a..c67296b 100644
--- a/src/views/system/warehouseAddress/index.vue
+++ b/src/views/system/warehouseAddress/index.vue
@@ -18,6 +18,13 @@
{{ item.warehouseName }}
+
+ {{ $t('system.warehouseAddress.fields.sort') }} {{ item.sort }}
+
@@ -58,6 +65,10 @@
{{ $t('system.warehouseAddress.fields.shippingAddress') }}:
{{ displayShippingAddress(item) }}
+
@@ -105,6 +116,18 @@
+
+
+
+
+
{{ $t('system.warehouseAddress.section.contact') }}
@@ -134,6 +157,17 @@
/>
+
+
+
+
+
@@ -156,6 +190,11 @@ import {
} from '@/api/system/warehouse';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { getMapIdFromAccessToken } from '@/utils/jwt';
+import {
+ compareWarehouseSort,
+ formatWarehouseShipAddress,
+ formatWarehouseShipRecipient,
+} from '@/utils/warehouseShipAddress';
const { t } = useI18n();
const rbacUserStore = useRbacUserStore();
@@ -172,22 +211,12 @@ const editingId = ref(null);
const warehouseTypeOptions = ref>([]);
const currentCustomerCode = ref('');
-function appendCustomerCode(value?: string, codeSuffix = ''): string {
- const base = value?.trim() || '';
- const code = currentCustomerCode.value.trim();
- if (!code) return base || '-';
- const tail = `${code}${codeSuffix}`;
- if (!base) return tail;
- if (base.endsWith(tail)) return base;
- return `${base} ${tail}`;
-}
-
function displayRecipient(item: { recipient?: string }) {
- return appendCustomerCode(item.recipient);
+ return formatWarehouseShipRecipient(item.recipient, currentCustomerCode.value);
}
function displayShippingAddress(item: { shippingAddress?: string }) {
- return appendCustomerCode(item.shippingAddress, '号');
+ return formatWarehouseShipAddress(item.shippingAddress, currentCustomerCode.value);
}
async function loadCurrentCustomerCode() {
@@ -242,9 +271,11 @@ function getWarehouseTypeDisplay(item: {
const formData = reactive({
warehouseName: '',
warehouseType: undefined as string | undefined,
+ sort: undefined as number | undefined,
recipient: '',
contactPhone: '',
shippingAddress: '',
+ remark: '',
});
function handleDelete(item: any) {
@@ -268,7 +299,7 @@ function handleDelete(item: any) {
async function loadList() {
try {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 });
- list.value = items || [];
+ list.value = [...(items || [])].sort(compareWarehouseSort);
} catch (e: any) {
$baseMessage(e?.message || '加载仓库列表失败', 'error');
list.value = [];
@@ -281,9 +312,11 @@ function handleAdd() {
Object.assign(formData, {
warehouseName: '',
warehouseType: undefined,
+ sort: undefined,
recipient: '',
contactPhone: '',
shippingAddress: '',
+ remark: '',
});
modalVisible.value = true;
}
@@ -304,12 +337,15 @@ function handleCopy(item: any) {
function handleEdit(item: any) {
isEdit.value = true;
editingId.value = item.id ?? null;
+ const sortRaw = item.sort ?? item.Sort;
Object.assign(formData, {
warehouseName: item.warehouseName,
warehouseType: item.warehouseType,
+ sort: sortRaw == null || sortRaw === '' ? undefined : Number(sortRaw),
recipient: item.recipient,
contactPhone: item.contactPhone,
shippingAddress: item.shippingAddress,
+ remark: item.remark || '',
});
modalVisible.value = true;
}
@@ -322,9 +358,11 @@ async function handleModalOk() {
const payload = {
warehouseName: formData.warehouseName.trim(),
warehouseType: formData.warehouseType,
+ sort: formData.sort == null || Number.isNaN(Number(formData.sort)) ? null : Number(formData.sort),
recipient: formData.recipient,
contactPhone: formData.contactPhone,
shippingAddress: formData.shippingAddress,
+ remark: formData.remark?.trim() || null,
};
try {
if (isEdit.value && editingId.value != null) {
@@ -505,6 +543,10 @@ onMounted(() => {
color: var(--color-text-1);
}
+ .warehouse-card-sort-tag {
+ flex-shrink: 0;
+ }
+
.card-extra {
display: flex;
align-items: center;