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 @@ + + + + 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 @@
    @@ -48,12 +48,14 @@ +
    @@ -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) }} + + +
    +
    +
    +

    {{ item.remark }}

    +
    +
    + {{ $t('order.myOrders.drawer.operationLog.field') }} + {{ $t('order.myOrders.drawer.operationLog.before') }} + {{ $t('order.myOrders.drawer.operationLog.after') }} +
    +
    + {{ 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') }} + +
    +
    @@ -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 }} +