1.02
This commit is contained in:
commit
61df911a5c
@ -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` | 未支付改附加服务并重算 |
|
||||
|
||||
@ -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<OrderOperationLogListResult> {
|
||||
const res = await axiosInstance.get(orderUrl('/order/operation-log/list'), {
|
||||
params: { orderId: Number(orderId) },
|
||||
})
|
||||
const data = unwrapData<any>(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<OrderExceptionEventListResult> {
|
||||
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<any> {
|
||||
const res = await axiosInstance.post(orderUrl('/order/fee/bind-snapshot-versions'), {
|
||||
id: Number(body.id),
|
||||
rulesVersion: body.rulesVersion || undefined,
|
||||
priceVersion: body.priceVersion || undefined,
|
||||
recalculateReceivable: !!body.recalculateReceivable,
|
||||
recalculatePayable: !!body.recalculatePayable,
|
||||
})
|
||||
assertBusinessOk(res.data)
|
||||
return res.data?.data ?? res.data?.Data ?? res.data
|
||||
}
|
||||
|
||||
/** POST /order/fee/rerun-inbound-billing — 重跑入库计费(应收+排货应付) */
|
||||
export async function postOrderRerunInboundBilling(body: OrderIdRequest): Promise<any> {
|
||||
const res = await axiosInstance.post(orderUrl('/order/fee/rerun-inbound-billing'), { id: body.id })
|
||||
|
||||
@ -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<string, unknown>)
|
||||
|
||||
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<string, unknown>)
|
||||
|
||||
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<string, unknown>)
|
||||
|
||||
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 })
|
||||
|
||||
|
||||
@ -9,6 +9,10 @@ export interface WarehouseDto {
|
||||
recipient?: string
|
||||
contactPhone?: string
|
||||
shippingAddress?: string
|
||||
/** 展示排序,越小越靠前;可不填 */
|
||||
sort?: number | null
|
||||
/** 备注(选填) */
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
createBy?: string
|
||||
|
||||
@ -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<boolean> {
|
||||
// 仍在回填:用户不可能已编辑,直接放行
|
||||
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 @@
|
||||
<li><strong>参数名</strong>:与服务商文档、curl、Mapping Body / Body 参数树、前置脚本写入字段名保持一致(如 <code>_aop_signature</code>、<code>notifyBizEventDTO</code>)。</li>
|
||||
<li><strong>参数值</strong>:使用<strong>占位符</strong>或留空,不要填写 curl 中的真实签名/业务样例;转发前由 Like API 替换。</li>
|
||||
<li><code>$FromMappingBody</code>(FormUrlEncoded):Query 上承载业务 JSON 的字段(如 <code>*DTO</code>)填此占位符,映射结果来自 <strong>Mapping Body</strong> 参数树;POST 参数仅在 URL、无 Body 时,DTO 应配置在 Query 而非 Body 表单。</li>
|
||||
<li><code>$FromAccount</code>:按参数名从<strong>服务商账户</strong>取值;<code>$TimeStamp</code> / <code>$DateTime</code>:时间;<code>$token</code>:令牌;<code>$Url</code>:当前请求地址。</li>
|
||||
<li><code>$FromAccount</code>:按参数名从<strong>服务商账户</strong>取值;<code>$TimeStamp</code> / <code>$DateTime</code>:时间;<code>$Token</code>:令牌;<code>$Url</code>:当前请求地址。</li>
|
||||
<li><strong>留空</strong>:签名类参数(如 <code>sign</code>、<code>_aop_signature</code>)由<strong>前置脚本</strong>写入。</li>
|
||||
<li><strong>参数描述</strong>:可记录备注或 curl 片段,便于对照映射,不参与转发。</li>
|
||||
</ul>
|
||||
|
||||
@ -100,7 +100,7 @@ export const ParameterPlaceholderText: Record<number, string> = {
|
||||
[ParameterPlaceholder.TimeStamp]: '$TimeStamp',
|
||||
[ParameterPlaceholder.DateTime]: '$DateTime',
|
||||
[ParameterPlaceholder.Url]: '$Url',
|
||||
[ParameterPlaceholder.Token]: '$token',
|
||||
[ParameterPlaceholder.Token]: '$Token',
|
||||
[ParameterPlaceholder.FromMappingBody]: '$FromMappingBody',
|
||||
};
|
||||
|
||||
|
||||
@ -175,6 +175,7 @@ export const PERMISSION_ACTION_LABELS: Record<string, string> = {
|
||||
AddFee: '添加费用',
|
||||
RecalculateReceivable: '重算应收',
|
||||
RecalculatePayable: '重算应付',
|
||||
BindSnapshotVersions: '切换版本',
|
||||
ImportDownstreamWeight: '导入对账件重尺',
|
||||
AddDeclarationRow: '添加申报行',
|
||||
DownstreamForecastResult: '下游预报结果',
|
||||
@ -537,6 +538,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
||||
{ code: 'AddFee', name: '添加费用' },
|
||||
{ code: 'RecalculateReceivable', name: '重算应收' },
|
||||
{ code: 'RecalculatePayable', name: '重算应付' },
|
||||
{ code: 'BindSnapshotVersions', name: '切换版本' },
|
||||
{ code: 'AddDeclarationRow', name: '添加申报行' },
|
||||
]),
|
||||
B('Order.FulfillOrders', [
|
||||
@ -573,6 +575,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
||||
{ code: 'AddFee', name: '添加费用' },
|
||||
{ code: 'RecalculateReceivable', name: '重算应收' },
|
||||
{ code: 'RecalculatePayable', name: '重算应付' },
|
||||
{ code: 'BindSnapshotVersions', name: '切换版本' },
|
||||
{ code: 'ImportDownstreamWeight', name: '导入对账件重尺' },
|
||||
{ code: 'AddDeclarationRow', name: '添加申报行' },
|
||||
]),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '复制',
|
||||
|
||||
@ -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<number>();
|
||||
|
||||
(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 {
|
||||
|
||||
49
src/utils/warehouseShipAddress.ts
Normal file
49
src/utils/warehouseShipAddress.ts
Normal file
@ -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}`;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
getServiceProviderAccountGetAccountPageList,
|
||||
getServiceProviderAccountGetFieldList,
|
||||
@ -18,6 +19,8 @@
|
||||
name: 'AccountList',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'saved', payload: { id: number; name?: string; code?: string }): void;
|
||||
}>();
|
||||
@ -487,7 +490,8 @@
|
||||
>
|
||||
<div class="account-list-body">
|
||||
<a-alert type="info" :show-icon="false" class="modal-form-tip">
|
||||
配置服务商账户凭证;可先自定义表头再录入。修改后点确定保存,发布后才生效。
|
||||
<p>配置服务商账户凭证;可先自定义表头再录入。修改后点确定保存,发布后才生效。</p>
|
||||
<p>{{ t('system.interface.mapping.table.paramValueTip') }}</p>
|
||||
</a-alert>
|
||||
|
||||
<div class="form-section-title">账户凭证</div>
|
||||
@ -670,6 +674,14 @@
|
||||
color: var(--color-text-2);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
getGetMapParameterCascadedList,
|
||||
} from '@/api/InterfaceMappingServices/interfaceMap';
|
||||
|
||||
import { getParameterGetParameterList } from '@/api/ParameterServices/parameter';
|
||||
import AddParameter from './AddParameter.vue';
|
||||
import MapAiAssistModal from './MapAiAssistModal.vue';
|
||||
|
||||
@ -41,52 +40,21 @@
|
||||
const tableRef1 = ref();
|
||||
const active = ref(0);
|
||||
const AddParameterRef = ref();
|
||||
const formsTypeList_new = ref<any[]>([]);
|
||||
const maxCompletedIndex = ref<number>(-1);
|
||||
const outputAttention = ref(false);
|
||||
const outputStepRef = ref<HTMLElement | null>(null);
|
||||
let outputAttentionTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
/** 当前已加载的映射上下文,避免切 tab 重复拉取 */
|
||||
const loadedContextKey = ref('');
|
||||
let initSeq = 0;
|
||||
let inflightInit: Promise<void> | null = null;
|
||||
let inflightInitKey = '';
|
||||
const detailLoading = ref(false);
|
||||
|
||||
interface Indexable {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 请求去重:记录正在请求的接口ID
|
||||
const loadingParameterId = ref<number | null>(null);
|
||||
|
||||
async function formsValueInfoGetList() {
|
||||
// 如果正在加载相同的接口,直接返回
|
||||
if (loadingParameterId.value === props.interfaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有接口ID,直接返回
|
||||
if (!props.interfaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingParameterId.value = props.interfaceId;
|
||||
|
||||
try {
|
||||
const { data } = await getParameterGetParameterList({
|
||||
isInPara: true,
|
||||
id: Number(props.interfaceId),
|
||||
onlyLeafs: true,
|
||||
});
|
||||
formsTypeList_new.value = data;
|
||||
formsTypeList.value = data;
|
||||
} catch (error) {
|
||||
} finally {
|
||||
loadingParameterId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 interfaceId 变化,只在变化时请求
|
||||
watch(() => props.interfaceId, (newId) => {
|
||||
if (newId) {
|
||||
formsValueInfoGetList();
|
||||
}
|
||||
}, { immediate: true });
|
||||
const tableProps1 = computed(() => {
|
||||
return {
|
||||
columnsKey: 'mapDetails111',
|
||||
@ -168,6 +136,7 @@
|
||||
title: t('system.interface.mapping.table.paramValue'),
|
||||
minWidth: 120,
|
||||
slots: {
|
||||
header: 'fixedValueHeader',
|
||||
edit: 'fixedValueEdit',
|
||||
},
|
||||
editRender: {},
|
||||
@ -285,9 +254,17 @@
|
||||
id: e.interfaceId,
|
||||
};
|
||||
});
|
||||
invalidateCascaderOptionsCache();
|
||||
};
|
||||
onMounted(() => {
|
||||
restaurants.value = loadAll();
|
||||
// 面板首次挂载时自行拉数,避免依赖父级 setTimeout
|
||||
if (props.serviceProviderId && props.interfaceId) {
|
||||
void initData({
|
||||
serviceProviderId: props.serviceProviderId,
|
||||
interfaceId: props.interfaceId,
|
||||
});
|
||||
}
|
||||
});
|
||||
type RestaurantItem = {
|
||||
value: string;
|
||||
@ -328,24 +305,48 @@
|
||||
return data;
|
||||
}
|
||||
const mapAliasEditRef = ref();
|
||||
function getOptions(item: any, isReverse = false) {
|
||||
if (isReverse)
|
||||
return setlabel(cascadedList.value.slice(1, cascadedList.value.length));
|
||||
/** 级联 options 按步骤缓存,避免每行渲染都深拷贝/打标 */
|
||||
const cascaderOptionsCache = ref<Map<string, any[]>>(new Map());
|
||||
|
||||
function invalidateCascaderOptionsCache() {
|
||||
cascaderOptionsCache.value = new Map();
|
||||
}
|
||||
|
||||
function getOptions(item: any, isReverse = false) {
|
||||
const cacheKey = isReverse
|
||||
? 'reverse'
|
||||
: `fwd-${item?.interfaceId ?? 'unknown'}`;
|
||||
const cached = cascaderOptionsCache.value.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
let result: any[];
|
||||
if (isReverse) {
|
||||
result = setlabel(cascadedList.value.slice(1, cascadedList.value.length));
|
||||
} else {
|
||||
const ids: number[] = inParaMapList.value.map((t) => t.interfaceId!);
|
||||
const findI = ids.findIndex((i) => i === item.interfaceId);
|
||||
let data: any[] = [];
|
||||
if (findI === -1 || findI === 0) {
|
||||
data = [cascadedList.value[0]];
|
||||
data = cascadedList.value[0] ? [cascadedList.value[0]] : [];
|
||||
} else {
|
||||
data = [cascadedList.value[0]].concat(
|
||||
cascadedList.value.filter((t) => {
|
||||
return ids.slice(0, findI).includes(t.interfaceId!);
|
||||
})
|
||||
);
|
||||
}),
|
||||
).filter(Boolean);
|
||||
}
|
||||
return setlabel(data);
|
||||
result = setlabel(data);
|
||||
}
|
||||
cascaderOptionsCache.value.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 当前入参步骤 / 返参步骤共用一份 options,模板里不要对每行再算 */
|
||||
const activeInputCascaderOptions = computed(() => {
|
||||
if (!activeInParaItem.value) return [];
|
||||
return getOptions(activeInParaItem.value, false);
|
||||
});
|
||||
const outputCascaderOptions = computed(() => getOptions({ interfaceId: 0 }, true));
|
||||
function mapAliasEditCheng(
|
||||
e: any,
|
||||
row: API.InterfaceParameterMapOutputValueObject
|
||||
@ -424,7 +425,6 @@
|
||||
temporaryItem.value = undefined;
|
||||
emits('save-model-enter');
|
||||
}
|
||||
const formsTypeList = ref<any[]>([]);
|
||||
// 支持多关键词(空格分隔)、大小写不敏感,匹配别名/中文名/名称/代码等字段
|
||||
function filterMethod(val: string, node: any) {
|
||||
const query = (val ?? '').toString().trim().toLowerCase();
|
||||
@ -471,7 +471,9 @@
|
||||
}
|
||||
|
||||
function getVali(row: Indexable) {
|
||||
tableRef.value?.forEach((element: any) => {
|
||||
const refs = tableRef.value;
|
||||
const tables = Array.isArray(refs) ? refs : refs ? [refs] : [];
|
||||
tables.forEach((element: any) => {
|
||||
setTimeout(() => {
|
||||
element?.editRowCellEvent(row, 'mapAlias');
|
||||
element?.xGrid.scrollToRow(row, 'mapAlias');
|
||||
@ -485,36 +487,77 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function resultsFn() {
|
||||
formsTypeList_new.value = [];
|
||||
formsTypeList.value = [];
|
||||
mapDetails.value = [];
|
||||
active.value = 0;
|
||||
maxCompletedIndex.value = -1;
|
||||
inParaMapList.value = [];
|
||||
}
|
||||
async function initData(data: any, options?: { activeIndex?: number }) {
|
||||
const nextActive = options?.activeIndex;
|
||||
resultsFn();
|
||||
if (typeof nextActive === 'number') {
|
||||
active.value = nextActive;
|
||||
function buildContextKey(data: { serviceProviderId?: number; interfaceId?: number }) {
|
||||
return `${data?.serviceProviderId ?? 0}:${data?.interfaceId ?? 0}`;
|
||||
}
|
||||
|
||||
getEditMapDetails(data).then((resp) => {
|
||||
const detail = (resp as any)?.data || resp || {};
|
||||
function hasLoadedData() {
|
||||
return !!(inParaMapList.value.length || mapDetails.value.length);
|
||||
}
|
||||
|
||||
async function initData(
|
||||
data: any,
|
||||
options?: { activeIndex?: number; force?: boolean },
|
||||
) {
|
||||
const ctx = buildContextKey(data);
|
||||
const nextActive = options?.activeIndex;
|
||||
const force = !!options?.force || typeof nextActive === 'number';
|
||||
|
||||
// 同服务商/接口已加载过:切回映射明细 tab 时跳过重复请求
|
||||
if (!force && loadedContextKey.value === ctx && hasLoadedData()) {
|
||||
return;
|
||||
}
|
||||
// 挂载与父级同时触发时合并为一次请求
|
||||
if (!force && inflightInit && inflightInitKey === ctx) {
|
||||
await inflightInit;
|
||||
return;
|
||||
}
|
||||
|
||||
const seq = ++initSeq;
|
||||
detailLoading.value = true;
|
||||
|
||||
const run = (async () => {
|
||||
try {
|
||||
const [detailResp] = await Promise.all([
|
||||
getEditMapDetails(data),
|
||||
getCascadedList(data),
|
||||
]);
|
||||
if (seq !== initSeq) return;
|
||||
|
||||
const detail = (detailResp as any)?.data || detailResp || {};
|
||||
inParaMapList.value = detail.inParaMapList || [];
|
||||
mapDetails.value = detail.mapDetails || [];
|
||||
});
|
||||
loadedContextKey.value = ctx;
|
||||
invalidateCascaderOptionsCache();
|
||||
|
||||
getCascadedList(data);
|
||||
if (typeof nextActive === 'number') {
|
||||
active.value = nextActive;
|
||||
} else {
|
||||
active.value = 0;
|
||||
maxCompletedIndex.value = -1;
|
||||
}
|
||||
} finally {
|
||||
if (seq === initSeq) detailLoading.value = false;
|
||||
}
|
||||
})();
|
||||
|
||||
setActive(active.value);
|
||||
inflightInit = run;
|
||||
inflightInitKey = ctx;
|
||||
try {
|
||||
await run;
|
||||
} finally {
|
||||
if (inflightInit === run) {
|
||||
inflightInit = null;
|
||||
inflightInitKey = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
tableRef,
|
||||
getdata,
|
||||
initData,
|
||||
hasLoadedData,
|
||||
getVali,
|
||||
getReturnVali,
|
||||
advanceAfterSave,
|
||||
@ -640,20 +683,38 @@
|
||||
<span class="output-hint-action">{{ t('system.interface.mapping.outputMappingGoFill') }}</span>
|
||||
</button>
|
||||
<div class="content-pane">
|
||||
<a-spin :loading="detailLoading" class="map-detail-spin">
|
||||
<div class="content-scroll">
|
||||
<div class="content-body">
|
||||
<template v-for="(item, index) in inParaMapList" :key="index">
|
||||
<div v-show="active === index" class="map-table-wrapper">
|
||||
<!-- 只挂载当前步骤表格,避免多步骤×多参数同时渲染 -->
|
||||
<div
|
||||
v-if="activeInParaItem && active >= 0"
|
||||
:key="`in-${activeInParaItem.interfaceMapId || active}`"
|
||||
class="map-table-wrapper"
|
||||
>
|
||||
<div class="map-scroll">
|
||||
<TablePro
|
||||
ref="tableRef"
|
||||
v-bind="tableProps"
|
||||
class="map-table"
|
||||
:table-config="{ data: item?.mapDetails || [] }"
|
||||
:table-config="{ data: activeInParaItem?.mapDetails || [] }"
|
||||
>
|
||||
<template #toolbar_right>
|
||||
<slot name="toolbar_right" />
|
||||
</template>
|
||||
<template #fixedValueHeader>
|
||||
<span class="param-value-header">
|
||||
{{ t('system.interface.mapping.table.paramValue') }}
|
||||
<a-tooltip position="top" :content-style="{ maxWidth: '320px' }">
|
||||
<template #content>
|
||||
<div class="param-value-help">
|
||||
{{ t('system.interface.mapping.table.paramValueTip') }}
|
||||
</div>
|
||||
</template>
|
||||
<IconQuestionCircle class="param-value-help-icon" @click.stop />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template #fixedValueEdit="{ row }">
|
||||
<a-auto-complete
|
||||
v-model="row.fixedValue"
|
||||
@ -677,7 +738,7 @@
|
||||
<a-cascader
|
||||
ref="mapAliasEditRef"
|
||||
v-model="row.mapId"
|
||||
:options="getOptions(item, false)"
|
||||
:options="activeInputCascaderOptions"
|
||||
:filter-option="filterMethod"
|
||||
allow-clear
|
||||
allow-search
|
||||
@ -712,9 +773,7 @@
|
||||
</TablePro>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="active === -1">
|
||||
<div class="map-table-wrapper">
|
||||
<div v-else-if="active === -1" class="map-table-wrapper">
|
||||
<a-alert
|
||||
v-if="outputNeedsAttention"
|
||||
type="warning"
|
||||
@ -734,11 +793,10 @@
|
||||
</span>
|
||||
</template>
|
||||
<template #mapAlias="{ row }">
|
||||
|
||||
<a-cascader
|
||||
ref="mapAliasEditRef"
|
||||
v-model="row.mapId"
|
||||
:options="getOptions({ interfaceId: 0 }, true)"
|
||||
:options="outputCascaderOptions"
|
||||
:filter-option="filterMethod"
|
||||
allow-search
|
||||
allow-clear
|
||||
@ -766,9 +824,9 @@
|
||||
</TablePro>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
<AddParameter ref="AddParameterRef" @seva="save" />
|
||||
<MapAiAssistModal
|
||||
@ -783,6 +841,13 @@
|
||||
</template>
|
||||
<style scoped>
|
||||
.entered-map { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; overflow: hidden; }
|
||||
.map-detail-spin { display: block; width: 100%; height: 100%; min-height: 0; }
|
||||
.map-detail-spin :deep(.arco-spin-children) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.steps-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -821,13 +886,24 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.unequal-help-icon,
|
||||
.script-help-icon {
|
||||
.script-help-icon,
|
||||
.param-value-help-icon {
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
cursor: help;
|
||||
}
|
||||
.param-value-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.param-value-help {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.unequal-help-icon:hover,
|
||||
.script-help-icon:hover {
|
||||
.script-help-icon:hover,
|
||||
.param-value-help-icon:hover {
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
.step-item { position: relative; display: flex; align-items: center; color: var(--color-text-3); cursor: pointer; flex-shrink: 0; }
|
||||
|
||||
@ -119,10 +119,10 @@
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadList();
|
||||
loadList({ keepSelection: true });
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
async function loadList(options?: { keepSelection?: boolean }) {
|
||||
if (!props.id) {
|
||||
list.value = [];
|
||||
selectedId.value = null;
|
||||
@ -141,10 +141,14 @@
|
||||
const raw: any = response || {};
|
||||
const data: any = raw.items ?? raw.data ?? raw.list ?? raw.records ?? raw;
|
||||
list.value = Array.isArray(data) ? data : [];
|
||||
// 仅当当前选中项在新列表中不存在时才清空,避免“点击一次又掉选中”
|
||||
if (
|
||||
if (!options?.keepSelection) {
|
||||
// 切换上游接口时不默认选中第一条映射
|
||||
selectedId.value = null;
|
||||
} else if (
|
||||
selectedId.value !== null &&
|
||||
!list.value.some((row: any, index: number) => getRowKey(row, index) === selectedId.value)
|
||||
!list.value.some(
|
||||
(row: any, index: number) => String(getRowKey(row, index)) === String(selectedId.value),
|
||||
)
|
||||
) {
|
||||
selectedId.value = null;
|
||||
}
|
||||
@ -157,19 +161,21 @@
|
||||
}
|
||||
|
||||
watch(() => props.id, (id) => {
|
||||
if (id) loadList();
|
||||
else {
|
||||
list.value = [];
|
||||
// 换接口先清选中,避免沿用上一次的服务商高亮
|
||||
selectedId.value = null;
|
||||
}
|
||||
if (id) loadList({ keepSelection: false });
|
||||
else list.value = [];
|
||||
}, { immediate: true });
|
||||
|
||||
// 暴露与旧用法兼容的 reload 接口
|
||||
tableRef.value = { reload: loadList };
|
||||
// 暴露与旧用法兼容的 reload:默认保留当前选中(发布/保存后刷新不丢高亮)
|
||||
tableRef.value = {
|
||||
reload: (options?: { keepSelection?: boolean }) =>
|
||||
loadList({ keepSelection: options?.keepSelection !== false }),
|
||||
};
|
||||
|
||||
function setSearchKeyword(val: string) {
|
||||
searchKeyword.value = val || '';
|
||||
loadList();
|
||||
loadList({ keepSelection: true });
|
||||
}
|
||||
|
||||
// 右侧内容
|
||||
@ -191,7 +197,7 @@
|
||||
serviceProviderId: row.serviceProviderId,
|
||||
}).then(() => {
|
||||
$baseMessage(t('system.interface.message.deleteSuccess'), 'success');
|
||||
loadList();
|
||||
loadList({ keepSelection: false });
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -350,6 +356,8 @@
|
||||
);
|
||||
|
||||
registerReleaseMethods.closeModal();
|
||||
// 发布后刷新列表,保留当前服务商选中
|
||||
void loadList({ keepSelection: true });
|
||||
},
|
||||
});
|
||||
|
||||
@ -390,7 +398,7 @@
|
||||
:show-node-index="true"
|
||||
:show-node-code="true"
|
||||
@search="handleSearch"
|
||||
@refresh="loadList"
|
||||
@refresh="() => loadList({ keepSelection: true })"
|
||||
@add="handleAdd"
|
||||
@select="handleMenuSelect"
|
||||
@node-click="handleNodeClick"
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
const active = ref(0);
|
||||
const activeContentTab = ref('detail'); // 接口明细、API调试、映射明细
|
||||
const currentActiveTab = ref('detail'); // 用于保存按钮判断的当前tab
|
||||
/** 映射明细首次打开后保持挂载,避免反复销毁重建 */
|
||||
const mappingPanelReady = ref(false);
|
||||
const apiDebuggerRef = ref<any>();
|
||||
const apiDebuggerRefreshKey = ref(0);
|
||||
|
||||
@ -1147,10 +1149,11 @@
|
||||
// 将上游接口ID同步给右侧明细区域
|
||||
upStreamIdRef.value = Number(nodeData.id);
|
||||
|
||||
// 重置映射明细区域,确保EnteredMap组件重新渲染
|
||||
// 重置映射明细区域
|
||||
serviceProviderId.value = 0;
|
||||
serviceProviderName.value = '';
|
||||
MapServiceProviderROW.value = null;
|
||||
mappingPanelReady.value = false;
|
||||
// 未选服务商时映射/调试不可用,回到接口明细
|
||||
if (activeContentTab.value === 'mapping' || activeContentTab.value === 'debugger') {
|
||||
activeContentTab.value = 'detail';
|
||||
@ -1159,7 +1162,8 @@
|
||||
// 刷新右侧映射服务商列表
|
||||
nextTick(() => {
|
||||
if (MapServiceProviderRef.value && MapServiceProviderRef.value.tableRef) {
|
||||
MapServiceProviderRef.value.tableRef.reload();
|
||||
// 切换上游接口:清空中间映射选中,不默认第一条
|
||||
MapServiceProviderRef.value.tableRef.reload({ keepSelection: false });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1262,17 +1266,22 @@
|
||||
initInterfaceConfig();
|
||||
}, 100);
|
||||
} else if (newTab === 'mapping') {
|
||||
// 切换回映射明细tab时,重新初始化EnteredMap组件
|
||||
mappingPanelReady.value = true;
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
if (enteredMapRef.value && selectedInterface.value && upStreamIdRef.value) {
|
||||
(unref(enteredMapRef) as any)?.initData?.({
|
||||
const map = unref(enteredMapRef) as any;
|
||||
// 已缓存同一服务商映射时不再重复拉数;首次挂载或未加载时再 init
|
||||
if (
|
||||
map &&
|
||||
selectedInterface.value &&
|
||||
upStreamIdRef.value &&
|
||||
serviceProviderId.value &&
|
||||
!map.hasLoadedData?.()
|
||||
) {
|
||||
await map.initData?.({
|
||||
serviceProviderId: serviceProviderId.value,
|
||||
interfaceId: upStreamIdRef.value,
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1566,10 +1575,14 @@
|
||||
|
||||
<div class="interface-detail-tab-content">
|
||||
<template v-if="selectedInterface">
|
||||
<!-- 映射明细内容 -->
|
||||
<div v-if="activeContentTab === 'mapping'" class="mapping-content">
|
||||
<!-- 映射明细:首次打开后 v-show 保活,避免切 tab 整表重挂 -->
|
||||
<div
|
||||
v-if="mappingPanelReady && serviceProviderId"
|
||||
v-show="activeContentTab === 'mapping'"
|
||||
class="mapping-content"
|
||||
>
|
||||
<EnteredMap
|
||||
:key="`entered-map-${selectedInterface?.id}-${serviceProviderId}-${activeContentTab}`"
|
||||
:key="`entered-map-${selectedInterface?.id}-${serviceProviderId}`"
|
||||
ref="enteredMapRef"
|
||||
class="h-full w-full"
|
||||
:service-provider-id="serviceProviderId"
|
||||
|
||||
@ -325,40 +325,67 @@
|
||||
</p>
|
||||
<div v-if="form.inboundMethod === 'selfShip'" class="h5-po__wh-card">
|
||||
<div class="h5-po__wh-head">
|
||||
<strong>{{ t('h5.placeOrder.selfShipAddressTitle') }}</strong>
|
||||
</div>
|
||||
<p v-if="!warehouseOptions.length" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.selfShipNoWarehouse') }}
|
||||
</p>
|
||||
<p v-else-if="!warehousesWithShipAddress.length" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.selfShipNoAddressConfigured') }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="h5-po__wh-list">
|
||||
<div
|
||||
v-for="opt in warehouseOptions"
|
||||
:key="opt.value"
|
||||
class="h5-po__wh-item"
|
||||
:class="{
|
||||
'is-selected': String(opt.value) === String(form.warehouseId ?? ''),
|
||||
'is-empty': !warehouseHasAddress(opt),
|
||||
}"
|
||||
>
|
||||
<div class="h5-po__wh-item-head">
|
||||
<strong>
|
||||
{{ t('h5.placeOrder.selfShipAddressTitle') }}
|
||||
<template v-if="selectedWarehouseOption?.label">
|
||||
· {{ selectedWarehouseOption.label }}
|
||||
</template>
|
||||
{{ opt.label || opt.value }}
|
||||
<span
|
||||
v-if="String(opt.value) === String(form.warehouseId ?? '')"
|
||||
class="h5-po__wh-badge"
|
||||
>
|
||||
{{ t('h5.placeOrder.selfShipCurrent') }}
|
||||
</span>
|
||||
</strong>
|
||||
<button
|
||||
v-if="selectedWarehouseHasAddress"
|
||||
v-if="warehouseHasAddress(opt)"
|
||||
type="button"
|
||||
class="h5-po__wh-copy"
|
||||
@click="copyWarehouseShipAddress"
|
||||
@click="copyWarehouseShipAddress(opt)"
|
||||
>
|
||||
{{ t('h5.placeOrder.selfShipCopy') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!selectedWarehouseOption" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.selfShipSelectWarehouse') }}
|
||||
<p v-if="!warehouseHasAddress(opt)" class="h5-po__hint">
|
||||
{{ t('h5.placeOrder.selfShipAddressEmpty') }}
|
||||
</p>
|
||||
<template v-else-if="!selectedWarehouseHasAddress">
|
||||
<p class="h5-po__hint">{{ t('h5.placeOrder.selfShipAddressEmpty') }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
<span>{{ t('h5.placeOrder.selfShipRecipient') }}</span>
|
||||
{{ selectedWarehouseOption.recipient || '-' }}
|
||||
{{ displayWarehouseRecipient(opt) }}
|
||||
</p>
|
||||
<p>
|
||||
<span>{{ t('h5.placeOrder.selfShipPhone') }}</span>
|
||||
{{ selectedWarehouseOption.contactPhone || '-' }}
|
||||
{{ opt.contactPhone || '-' }}
|
||||
</p>
|
||||
<p class="is-addr">
|
||||
<span>{{ t('h5.placeOrder.selfShipAddress') }}</span>
|
||||
{{ selectedWarehouseOption.shippingAddress || '-' }}
|
||||
{{ displayWarehouseShippingAddress(opt) }}
|
||||
</p>
|
||||
<p v-if="opt.remark">
|
||||
<span>{{ t('h5.placeOrder.selfShipRemark') }}</span>
|
||||
{{ opt.remark }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="h5-po__hint">{{ t('h5.placeOrder.selfShipAddressHint') }}</p>
|
||||
</template>
|
||||
</div>
|
||||
@ -1153,6 +1180,7 @@ import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesP
|
||||
import { getDictListAll } from '@/api/system/dict';
|
||||
import { getCountryList } from '@/api/system/country';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||||
import {
|
||||
@ -1160,6 +1188,13 @@ import {
|
||||
normalizeSkuImageForStorage,
|
||||
pickField,
|
||||
} from '@/utils/warehouseListMapper';
|
||||
import {
|
||||
compareWarehouseSort,
|
||||
formatWarehouseShipAddress,
|
||||
formatWarehouseShipRecipient,
|
||||
} from '@/utils/warehouseShipAddress';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { h5Navigate } from '@/utils/device';
|
||||
import { isCustomerContactRequiredError } from '@/utils/customerContactGate';
|
||||
import {
|
||||
@ -1306,6 +1341,8 @@ const warehouseOptions = ref<
|
||||
recipient?: string;
|
||||
contactPhone?: string;
|
||||
shippingAddress?: string;
|
||||
sort?: number | null;
|
||||
remark?: string;
|
||||
}[]
|
||||
>([]);
|
||||
const countryOptions = ref<{ value: string; label: string }[]>([]);
|
||||
@ -1526,25 +1563,49 @@ function skuImageSrc(url?: string | null) {
|
||||
return resolveOmsAssetUrl(url || '') || '';
|
||||
}
|
||||
|
||||
const selectedWarehouseOption = computed(() => {
|
||||
const id = String(form.warehouseId ?? '').trim();
|
||||
if (!id) return undefined;
|
||||
return warehouseOptions.value.find((opt) => String(opt.value) === id);
|
||||
});
|
||||
const selectedWarehouseHasAddress = computed(() => {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
type WarehouseOption = (typeof warehouseOptions.value)[number];
|
||||
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
const currentCustomerCode = ref('');
|
||||
|
||||
async function loadCurrentCustomerCode() {
|
||||
try {
|
||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||||
const data = await resolveCurrentCustomer({ mapId, account });
|
||||
currentCustomerCode.value = String(data?.customerCode ?? '').trim();
|
||||
} catch {
|
||||
currentCustomerCode.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function displayWarehouseRecipient(opt?: WarehouseOption) {
|
||||
return formatWarehouseShipRecipient(opt?.recipient, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
function displayWarehouseShippingAddress(opt?: WarehouseOption) {
|
||||
return formatWarehouseShipAddress(opt?.shippingAddress, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
function warehouseHasAddress(opt?: WarehouseOption) {
|
||||
if (!opt) return false;
|
||||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress);
|
||||
});
|
||||
}
|
||||
|
||||
async function copyWarehouseShipAddress() {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return;
|
||||
const warehousesWithShipAddress = computed(() =>
|
||||
warehouseOptions.value.filter((opt) => warehouseHasAddress(opt)),
|
||||
);
|
||||
|
||||
async function copyWarehouseShipAddress(opt?: WarehouseOption) {
|
||||
if (!opt || !warehouseHasAddress(opt)) {
|
||||
Message.warning(t('h5.placeOrder.selfShipAddressEmpty'));
|
||||
return;
|
||||
}
|
||||
const text = [
|
||||
opt.label || '',
|
||||
`${t('h5.placeOrder.selfShipRecipient')}:${opt.recipient || '-'}`,
|
||||
`${t('h5.placeOrder.selfShipRecipient')}:${displayWarehouseRecipient(opt)}`,
|
||||
`${t('h5.placeOrder.selfShipPhone')}:${opt.contactPhone || '-'}`,
|
||||
`${t('h5.placeOrder.selfShipAddress')}:${opt.shippingAddress || '-'}`,
|
||||
`${t('h5.placeOrder.selfShipAddress')}:${displayWarehouseShippingAddress(opt)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
@ -3172,15 +3233,20 @@ async function loadOptions(
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const sortRaw = row?.sort ?? row?.Sort;
|
||||
const sortNum = sortRaw == null || sortRaw === '' ? null : Number(sortRaw);
|
||||
return {
|
||||
value: String(id ?? ''),
|
||||
label: name || String(id),
|
||||
recipient: pick('recipient', 'Recipient'),
|
||||
contactPhone: pick('contactPhone', 'ContactPhone'),
|
||||
shippingAddress: pick('shippingAddress', 'ShippingAddress'),
|
||||
sort: sortNum != null && Number.isFinite(sortNum) ? sortNum : null,
|
||||
remark: pick('remark', 'Remark'),
|
||||
};
|
||||
})
|
||||
.filter((x) => x.value);
|
||||
.filter((x) => x.value)
|
||||
.sort(compareWarehouseSort);
|
||||
if (!opts?.skipDefaults && warehouseOptions.value.length && !form.warehouseId) {
|
||||
form.warehouseId = warehouseOptions.value[0].value;
|
||||
}
|
||||
@ -3342,6 +3408,7 @@ watch(
|
||||
return;
|
||||
}
|
||||
resetForm();
|
||||
void loadCurrentCustomerCode();
|
||||
if (isEditMode.value) {
|
||||
await loadOptions(null, { skipDefaults: true, skipEnsureClientNo: true });
|
||||
await loadSenders({ skipAutoSelect: true });
|
||||
@ -3838,6 +3905,61 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__wh-list {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.h5-po__wh-item {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e5e6eb;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
|
||||
}
|
||||
|
||||
&.is-empty {
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__wh-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__wh-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 14%, #fff);
|
||||
}
|
||||
|
||||
.h5-po__wh-copy {
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
@ -3847,9 +3969,11 @@ watch(
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.h5-po__wh-card p {
|
||||
.h5-po__wh-card p,
|
||||
.h5-po__wh-item p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: #1d2129;
|
||||
@ -3863,6 +3987,10 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.h5-po__wh-item .h5-po__hint {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.h5-po__more-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -35,11 +35,15 @@
|
||||
{{ t('h5.journey.warehouse.copy') }}
|
||||
</button>
|
||||
</div>
|
||||
<p><span>{{ t('h5.journey.warehouse.recipient') }}</span>{{ item.recipient || '-' }}</p>
|
||||
<p><span>{{ t('h5.journey.warehouse.recipient') }}</span>{{ displayRecipient(item) }}</p>
|
||||
<p><span>{{ t('h5.journey.warehouse.phone') }}</span>{{ item.contactPhone || '-' }}</p>
|
||||
<p class="is-addr">
|
||||
<span>{{ t('h5.journey.warehouse.address') }}</span>
|
||||
{{ item.shippingAddress || '-' }}
|
||||
{{ displayShippingAddress(item) }}
|
||||
</p>
|
||||
<p v-if="item.remark">
|
||||
<span>{{ t('h5.journey.warehouse.remark') }}</span>
|
||||
{{ item.remark }}
|
||||
</p>
|
||||
</article>
|
||||
</a-spin>
|
||||
@ -53,6 +57,14 @@ import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconClose, IconHome } from '@arco-design/web-vue/es/icon';
|
||||
import { getWarehouseList, type WarehouseDto } from '@/api/system/warehouse';
|
||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||
import {
|
||||
compareWarehouseSort,
|
||||
formatWarehouseShipAddress,
|
||||
formatWarehouseShipRecipient,
|
||||
} from '@/utils/warehouseShipAddress';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import H5EmptyState from './H5EmptyState.vue';
|
||||
|
||||
defineOptions({ name: 'H5WarehouseAddressSheet' });
|
||||
@ -61,8 +73,10 @@ const props = defineProps<{ visible: boolean }>();
|
||||
const emit = defineEmits<{ 'update:visible': [boolean] }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
const loading = ref(false);
|
||||
const list = ref<WarehouseDto[]>([]);
|
||||
const currentCustomerCode = ref('');
|
||||
const drawerStyle = {
|
||||
borderRadius: '20px 20px 0 0',
|
||||
overflow: 'hidden',
|
||||
@ -82,20 +96,46 @@ function pick(raw: any, ...keys: string[]) {
|
||||
}
|
||||
|
||||
function normalize(row: any): WarehouseDto {
|
||||
const sortRaw = row?.sort ?? row?.Sort;
|
||||
const sortNum = sortRaw == null || sortRaw === '' ? null : Number(sortRaw);
|
||||
return {
|
||||
id: row?.id ?? row?.Id,
|
||||
warehouseName: pick(row, 'warehouseName', 'WarehouseName'),
|
||||
recipient: pick(row, 'recipient', 'Recipient'),
|
||||
contactPhone: pick(row, 'contactPhone', 'ContactPhone'),
|
||||
shippingAddress: pick(row, 'shippingAddress', 'ShippingAddress'),
|
||||
sort: sortNum != null && Number.isFinite(sortNum) ? sortNum : null,
|
||||
remark: pick(row, 'remark', 'Remark') || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function displayRecipient(item: WarehouseDto) {
|
||||
return formatWarehouseShipRecipient(item.recipient, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
function displayShippingAddress(item: WarehouseDto) {
|
||||
return formatWarehouseShipAddress(item.shippingAddress, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
async function loadCurrentCustomerCode() {
|
||||
try {
|
||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||||
const data = await resolveCurrentCustomer({ mapId, account });
|
||||
currentCustomerCode.value = String(data?.customerCode ?? '').trim();
|
||||
} catch {
|
||||
currentCustomerCode.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getWarehouseList({ page: 1, pageSize: 100 });
|
||||
list.value = (res.items ?? []).map(normalize);
|
||||
const [res] = await Promise.all([
|
||||
getWarehouseList({ page: 1, pageSize: 100 }),
|
||||
loadCurrentCustomerCode(),
|
||||
]);
|
||||
list.value = (res.items ?? []).map(normalize).sort(compareWarehouseSort);
|
||||
} catch {
|
||||
list.value = [];
|
||||
} finally {
|
||||
@ -106,9 +146,9 @@ async function loadList() {
|
||||
async function copyItem(item: WarehouseDto) {
|
||||
const text = [
|
||||
item.warehouseName,
|
||||
`${t('h5.journey.warehouse.recipient')}${item.recipient || '-'}`,
|
||||
`${t('h5.journey.warehouse.recipient')}${displayRecipient(item)}`,
|
||||
`${t('h5.journey.warehouse.phone')}${item.contactPhone || '-'}`,
|
||||
`${t('h5.journey.warehouse.address')}${item.shippingAddress || '-'}`,
|
||||
`${t('h5.journey.warehouse.address')}${displayShippingAddress(item)}`,
|
||||
].join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
@ -406,13 +406,17 @@ export default {
|
||||
'h5.placeOrder.selfShipRecipient': 'Recipient',
|
||||
'h5.placeOrder.selfShipPhone': 'Phone',
|
||||
'h5.placeOrder.selfShipAddress': 'Address',
|
||||
'h5.placeOrder.selfShipRemark': 'Remark',
|
||||
'h5.placeOrder.selfShipCopy': 'Copy',
|
||||
'h5.placeOrder.selfShipCopySuccess': 'Warehouse shipping info copied',
|
||||
'h5.placeOrder.selfShipSelectWarehouse': 'Select a warehouse first',
|
||||
'h5.placeOrder.selfShipAddressEmpty':
|
||||
'This warehouse has no shipping address yet. Contact support or choose another warehouse.',
|
||||
'h5.placeOrder.selfShipCurrent': 'Current',
|
||||
'h5.placeOrder.selfShipNoWarehouse': 'No warehouse info yet. Retry later or contact support.',
|
||||
'h5.placeOrder.selfShipNoAddressConfigured':
|
||||
'No warehouse has a shipping address configured. Contact support.',
|
||||
'h5.placeOrder.selfShipAddressEmpty': 'No shipping address configured',
|
||||
'h5.placeOrder.selfShipAddressHint':
|
||||
'Ship to this address yourself. Put the client number on the courier label so the warehouse can match it.',
|
||||
'Ship to the selected warehouse address yourself. Put the client number on the courier label so the warehouse can match it.',
|
||||
'h5.placeOrder.pickupAddress': 'Pickup address',
|
||||
'h5.placeOrder.pickupAddressPh': 'Select a saved sender address',
|
||||
'h5.placeOrder.expectPickupTime': 'Expected pickup time',
|
||||
@ -792,6 +796,7 @@ export default {
|
||||
'h5.journey.warehouse.recipient': 'Recipient: ',
|
||||
'h5.journey.warehouse.phone': 'Phone: ',
|
||||
'h5.journey.warehouse.address': 'Address: ',
|
||||
'h5.journey.warehouse.remark': 'Remark: ',
|
||||
|
||||
'h5.journey.ship.title': 'Pickup / ship to warehouse',
|
||||
'h5.journey.ship.lead': 'Create a logistics order first. Then either self-ship to the warehouse, or add a sender address and request pickup.',
|
||||
|
||||
@ -404,11 +404,15 @@ export default {
|
||||
'h5.placeOrder.selfShipRecipient': '收件人',
|
||||
'h5.placeOrder.selfShipPhone': '电话',
|
||||
'h5.placeOrder.selfShipAddress': '地址',
|
||||
'h5.placeOrder.selfShipRemark': '备注',
|
||||
'h5.placeOrder.selfShipCopy': '复制',
|
||||
'h5.placeOrder.selfShipCopySuccess': '仓库寄件信息已复制',
|
||||
'h5.placeOrder.selfShipSelectWarehouse': '请先选择仓库',
|
||||
'h5.placeOrder.selfShipAddressEmpty': '该仓库暂未配置收货地址,请联系客服或更换仓库',
|
||||
'h5.placeOrder.selfShipAddressHint': '按此地址自行寄出。快递面单建议备注客户单号,方便仓库认领。',
|
||||
'h5.placeOrder.selfShipCurrent': '当前',
|
||||
'h5.placeOrder.selfShipNoWarehouse': '暂无仓库信息,请稍后重试或联系客服',
|
||||
'h5.placeOrder.selfShipNoAddressConfigured': '暂无已配置收货地址的仓库,请联系客服',
|
||||
'h5.placeOrder.selfShipAddressEmpty': '暂未配置收货地址',
|
||||
'h5.placeOrder.selfShipAddressHint': '按所选仓库地址自行寄出。快递面单建议备注客户单号,方便仓库认领。',
|
||||
'h5.placeOrder.pickupAddress': '取件地址',
|
||||
'h5.placeOrder.pickupAddressPh': '请选择发件地址簿',
|
||||
'h5.placeOrder.expectPickupTime': '期望揽收时间',
|
||||
@ -778,6 +782,7 @@ export default {
|
||||
'h5.journey.warehouse.recipient': '收件人:',
|
||||
'h5.journey.warehouse.phone': '电话:',
|
||||
'h5.journey.warehouse.address': '地址:',
|
||||
'h5.journey.warehouse.remark': '备注:',
|
||||
|
||||
'h5.journey.ship.title': '揽收寄仓',
|
||||
'h5.journey.ship.lead': '请先创建物流订单。两种方式任选:自己快递寄到仓库,或填写寄件地址后预约上门揽收。',
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
'login.hero.title': 'IDE',
|
||||
'login.hero.tagline': 'Faster · Simpler · Professional',
|
||||
'login.hero.desc.customer':
|
||||
'查价说得清、下单走得顺、轨迹跟得上。AI 帮你看风险、提建议、少踩坑——从揽收到签收,IDE 即配陪你把每一票发好、盯紧、交代清楚。',
|
||||
'查价说得清、下单走得顺、轨迹跟得上。AI 帮你看风险、提建议、少踩坑——从揽收到签收,IDE 陪你把每一票发好、盯紧、交代清楚。',
|
||||
'login.hero.admin.brief': '开工前对齐三件事,并让 AI 成为搭档',
|
||||
'login.hero.admin.pillar.care.title': '更专业',
|
||||
'login.hero.admin.pillar.care.desc': '线路选对 · 异常不丢 · 全程说得清',
|
||||
|
||||
@ -145,7 +145,7 @@
|
||||
apply: [payload: { activeId: string; schemes: ColumnScheme[] }];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, te } = useI18n();
|
||||
const [registerModal, { openModal, closeModal, setModalProps }] = useModal();
|
||||
|
||||
const editingSchemeId = ref('');
|
||||
@ -173,8 +173,9 @@
|
||||
selectedKeys.value
|
||||
.map((key) => {
|
||||
const col = catalogMap.value.get(key);
|
||||
if (!col) return null;
|
||||
return { key: col.key, label: resolveColumnLabel(col, t) };
|
||||
// 方案里可能有目录尚未收录的 key:仍用中文兜底,避免表头/配置显示英文字段名
|
||||
const def = col || { key };
|
||||
return { key, label: resolveColumnLabel(def, t, te) };
|
||||
})
|
||||
.filter(Boolean) as { key: string; label: string }[],
|
||||
);
|
||||
@ -183,7 +184,7 @@
|
||||
const selected = new Set(selectedKeys.value);
|
||||
return props.catalog
|
||||
.filter((col) => !selected.has(col.key))
|
||||
.map((col) => ({ key: col.key, label: resolveColumnLabel(col, t) }));
|
||||
.map((col) => ({ key: col.key, label: resolveColumnLabel(col, t, te) }));
|
||||
});
|
||||
|
||||
function readSelectedKeysFromDom(): string[] {
|
||||
|
||||
@ -32,6 +32,8 @@ export const ORDER_DETAIL_VALUE_FIELD_MAP: Record<string, string[]> = {
|
||||
declaredCurrency: ['declaredCurrency', 'DeclaredCurrency'],
|
||||
declaredSum: ['declaredSum', 'DeclaredSum'],
|
||||
additionalServices: ['additionalServices', 'AdditionalServices'],
|
||||
downstreamSupplierName: ['downstreamSupplierName', 'DownstreamSupplierName'],
|
||||
downstreamSupplierCode: ['downstreamSupplierCode', 'DownstreamSupplierCode'],
|
||||
};
|
||||
|
||||
export interface OrderDetailFieldGroup {
|
||||
@ -58,7 +60,9 @@ export function patchOrderBasicFieldGroups(
|
||||
const fields = [
|
||||
'productCode',
|
||||
'productName',
|
||||
...(includeChannel ? (['channelCode', 'channelName'] as const) : []),
|
||||
...(includeChannel
|
||||
? (['channelCode', 'channelName', 'downstreamSupplierName', 'downstreamSupplierCode'] as const)
|
||||
: []),
|
||||
'warehouse',
|
||||
'consigneeCountry',
|
||||
'consigneeCountryName',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/** 我的订单列表字段配置(本地命名方案,最多显示 30 列) */
|
||||
|
||||
import { filterMyOrdersFieldKeys, isMyOrdersHiddenField, MY_ORDERS_HIDDEN_FIELD_KEYS } from './customerFieldMask';
|
||||
import { getOrderColumnLabelZh } from '../orderColumnLabelsZh';
|
||||
|
||||
export const MAX_VISIBLE_COLUMNS = 30;
|
||||
|
||||
@ -182,29 +183,67 @@ export function getActiveScheme(state: ColumnConfigState): ColumnScheme {
|
||||
);
|
||||
}
|
||||
|
||||
function isUsableLabel(text: string | undefined | null, fieldKey: string): text is string {
|
||||
if (!text) return false;
|
||||
const s = String(text).trim();
|
||||
if (!s) return false;
|
||||
// 翻译失败时 vue-i18n 常回传 i18n key 或字段 key
|
||||
if (s === fieldKey) return false;
|
||||
if (s.startsWith('order.') && s.includes('.')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveColumnLabel(
|
||||
col: OrderListColumnDef,
|
||||
t: (key: string) => string,
|
||||
t: (key: string, ...args: any[]) => string,
|
||||
_te?: (key: string) => boolean,
|
||||
): string {
|
||||
if (col.labelKey) return t(col.labelKey);
|
||||
if (col.label) return col.label;
|
||||
return col.key;
|
||||
const fieldKey = col.key;
|
||||
const tryI18n = (i18nKey: string) => {
|
||||
try {
|
||||
const text = t(i18nKey);
|
||||
return isUsableLabel(text, fieldKey) ? text : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (col.labelKey) {
|
||||
const byLabelKey = tryI18n(col.labelKey);
|
||||
if (byLabelKey) return byLabelKey;
|
||||
}
|
||||
if (isUsableLabel(col.label, fieldKey)) return col.label!.trim();
|
||||
|
||||
const autoKeys = [
|
||||
`order.waybillManage.table.${fieldKey}`,
|
||||
`order.myOrders.table.${fieldKey}`,
|
||||
`order.myOrders.drawer.${fieldKey}`,
|
||||
];
|
||||
for (const k of autoKeys) {
|
||||
const hit = tryI18n(k);
|
||||
if (hit) return hit;
|
||||
}
|
||||
// 最终兜底:中文列名表,绝不直接返回 camelCase key
|
||||
return getOrderColumnLabelZh(fieldKey);
|
||||
}
|
||||
|
||||
export function buildHotColumns(
|
||||
catalog: OrderListColumnDef[],
|
||||
keys: string[],
|
||||
t: (key: string) => string,
|
||||
t: (key: string, ...args: any[]) => string,
|
||||
te?: (key: string) => boolean,
|
||||
) {
|
||||
const map = new Map(catalog.map((c) => [c.key, c]));
|
||||
return keys
|
||||
.map((key) => map.get(key))
|
||||
.filter(Boolean)
|
||||
.map((col) => ({
|
||||
key: col!.key,
|
||||
label: resolveColumnLabel(col!, t),
|
||||
width: col!.width ?? 120,
|
||||
}));
|
||||
.map((key) => {
|
||||
const col = map.get(key) || { key, width: 120 };
|
||||
return {
|
||||
key,
|
||||
label: resolveColumnLabel(col, t, te),
|
||||
width: col.width ?? 120,
|
||||
};
|
||||
})
|
||||
.filter((col) => !!col.key);
|
||||
}
|
||||
|
||||
/** 将接口行中的标量字段并入可选列目录(跳过对象/数组与内部字段) */
|
||||
@ -228,10 +267,19 @@ export function mergeDiscoveredColumns(
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (!key || skip.has(key) || map.has(key)) continue;
|
||||
if (value != null && typeof value === 'object') continue;
|
||||
map.set(key, { key, label: key, width: 120 });
|
||||
map.set(key, {
|
||||
key,
|
||||
label: getOrderColumnLabelZh(key),
|
||||
labelKey: `order.waybillManage.table.${key}`,
|
||||
width: 120,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
return Array.from(map.values()).map((col) =>
|
||||
isUsableLabel(col.label, col.key)
|
||||
? col
|
||||
: { ...col, label: getOrderColumnLabelZh(col.key) },
|
||||
);
|
||||
}
|
||||
|
||||
export function createSchemeId() {
|
||||
|
||||
@ -38,11 +38,10 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||
<a-select
|
||||
v-model="form.carrierService"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="loadingRefs"
|
||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||
>
|
||||
@ -178,7 +177,7 @@ async function open(rows: Array<{ id: number | string; serviceNo?: string; wareh
|
||||
}))
|
||||
form.warehouseId = whIds[0]
|
||||
form.expectPickupTime = defaultPickupTime()
|
||||
form.carrierService = ''
|
||||
form.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||
form.remark = ''
|
||||
form.senderAddressId = undefined
|
||||
|
||||
@ -200,6 +199,10 @@ async function handleOk() {
|
||||
$baseMessage(t('order.myOrders.createPickup.needTime'), 'warning')
|
||||
return
|
||||
}
|
||||
if (!form.carrierService?.trim()) {
|
||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
@ -216,7 +219,7 @@ async function handleOk() {
|
||||
senderAddressId: form.senderAddressId,
|
||||
direction: 'forward',
|
||||
warehouseId: form.warehouseId,
|
||||
carrierService: form.carrierService || undefined,
|
||||
carrierService: form.carrierService.trim(),
|
||||
expectPickupTime: form.expectPickupTime,
|
||||
orderIds: orderIds.value,
|
||||
remark: form.remark || undefined,
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind'"
|
||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||
class="drawer-tab-toolbar"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
@ -65,6 +65,17 @@
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'operationLog'">
|
||||
<a-tag v-if="operationLogList.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="operationLogLoading"
|
||||
@click="loadOperationLogs"
|
||||
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -566,6 +577,63 @@
|
||||
hide-refresh
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div v-if="activeTab === 'operationLog'" class="tab-panel operation-log-info">
|
||||
<a-spin :loading="operationLogLoading" class="operation-log-spin">
|
||||
<div v-if="operationLogList.length" class="operation-log-list">
|
||||
<div
|
||||
v-for="item in operationLogList"
|
||||
:key="item.id"
|
||||
class="operation-log-item"
|
||||
>
|
||||
<div class="operation-log-head" @click="toggleOperationLogExpand(item.id)">
|
||||
<div class="operation-log-head-main">
|
||||
<span class="operation-log-action">{{ item.actionName || item.action }}</span>
|
||||
<a-tag v-if="item.changes?.length" size="small" color="orangered">
|
||||
{{ item.changes.length }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="operation-log-head-meta">
|
||||
<span>{{ item.operatorName || $t('order.myOrders.drawer.operationLog.operator') }}</span>
|
||||
<span>{{ formatOperationLogTime(item.createTime) }}</span>
|
||||
<IconDown v-if="!isOperationLogExpanded(item.id)" />
|
||||
<IconUp v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isOperationLogExpanded(item.id)" class="operation-log-body">
|
||||
<p v-if="item.remark" class="operation-log-remark">{{ item.remark }}</p>
|
||||
<div v-if="item.changes?.length" class="operation-log-changes">
|
||||
<div class="operation-log-changes-header">
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.field') }}</span>
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.before') }}</span>
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.after') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(change, idx) in item.changes"
|
||||
:key="`${item.id}-${change.path}-${idx}`"
|
||||
class="operation-log-changes-row"
|
||||
>
|
||||
<span>{{ change.label || change.path }}</span>
|
||||
<span>{{ change.before ?? '--' }}</span>
|
||||
<span class="operation-log-after">{{ change.after ?? '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty
|
||||
v-else
|
||||
:description="$t('order.myOrders.drawer.operationLog.noChanges')"
|
||||
:image-style="{ height: '40px' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty
|
||||
v-else-if="!operationLogLoading"
|
||||
:description="$t('order.myOrders.drawer.operationLog.empty')"
|
||||
:image-style="{ height: '60px' }"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
@ -608,6 +676,8 @@ import {
|
||||
postOrderWeightUpdate,
|
||||
postOrderRecalculateReceivable,
|
||||
postOrderRecalculatePayable,
|
||||
getOrderOperationLogList,
|
||||
type OrderOperationLogItem,
|
||||
} from '@/api/order';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
@ -985,6 +1055,10 @@ const tabList = computed(() => [
|
||||
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
||||
]);
|
||||
|
||||
const operationLogList = ref<OrderOperationLogItem[]>([]);
|
||||
const operationLogLoading = ref(false);
|
||||
const operationLogExpandedIds = ref<Set<number>>(new Set());
|
||||
|
||||
const slidingTabItems = computed(() =>
|
||||
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
||||
);
|
||||
@ -992,6 +1066,8 @@ const slidingTabItems = computed(() =>
|
||||
function resolveDrawerTab(tab?: string) {
|
||||
const next = tab || 'basic';
|
||||
if (next === 'weight' || next === 'fee') return 'weightFee';
|
||||
// 我的运单不展示操作日志(管理端运单管理保留)
|
||||
if (next === 'operationLog') return 'basic';
|
||||
return next;
|
||||
}
|
||||
|
||||
@ -1987,6 +2063,45 @@ async function handleRecalculatePayable() {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const next = resolveDrawerTab(tabKey);
|
||||
if (next !== tabKey) {
|
||||
@ -2003,6 +2118,9 @@ function handleTabChange(tabKey: string) {
|
||||
weightViewMode.value = 'all';
|
||||
void ensureFeeItemDictLoaded();
|
||||
}
|
||||
if (next === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
@ -2024,6 +2142,9 @@ watch(
|
||||
if (activeTab.value === 'weightFee') {
|
||||
void ensureFeeItemDictLoaded();
|
||||
}
|
||||
if (activeTab.value === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
@ -3117,4 +3238,102 @@ 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,6 +7,7 @@ export const MY_ORDERS_HIDDEN_FIELD_KEYS = new Set([
|
||||
'channelName',
|
||||
'downstreamProvider',
|
||||
'downstreamSupplierCode',
|
||||
'downstreamSupplierName',
|
||||
'downstreamAppKey',
|
||||
'downstreamAppId',
|
||||
'channelForecastCode',
|
||||
|
||||
@ -122,7 +122,7 @@
|
||||
name: 'MyOrders',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, te } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const isFulfillList = computed(() => route.meta.listMode === 'fulfill');
|
||||
@ -1623,7 +1623,7 @@
|
||||
'trackNo',
|
||||
]);
|
||||
const base = withServerFilterColumns(
|
||||
buildHotColumns(listColumnCatalog.value, activeColumnKeys.value, t),
|
||||
buildHotColumns(listColumnCatalog.value, activeColumnKeys.value, t, te),
|
||||
ORDER_SERVER_FILTER_KEYS,
|
||||
(col) => {
|
||||
if (numberKeys.has(col.key)) {
|
||||
|
||||
161
src/views/order/orderColumnLabelsZh.ts
Normal file
161
src/views/order/orderColumnLabelsZh.ts
Normal file
@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 运单/订单列表列中文名(表头 + 字段配置兜底)。
|
||||
* 优先保证中文展示,避免出现 maxTimeliness 等英文字段名。
|
||||
*/
|
||||
export const ORDER_COLUMN_LABEL_ZH: Record<string, string> = {
|
||||
// 常用别名
|
||||
customerName: '客户名称',
|
||||
customerCode: '客户编码',
|
||||
waybillNo: '运单号',
|
||||
customerOrderNo: '客户单号',
|
||||
trackingNo: '跟踪号',
|
||||
downstreamServiceNo: '服务商单号',
|
||||
shippingMethod: '销售产品',
|
||||
serviceChannel: '服务渠道',
|
||||
consignee: '收件人',
|
||||
consigneePhone: '收件电话',
|
||||
consigneeCity: '收件城市',
|
||||
consigneeAddress: '收件地址',
|
||||
consigneePostcode: '收件邮编',
|
||||
consigneeState: '收件省州',
|
||||
ioss: 'IOSS',
|
||||
eori: 'EORI',
|
||||
|
||||
// OrderOutput 标量
|
||||
id: '订单ID',
|
||||
clientNo: '客户单号',
|
||||
trackNo: '跟踪号',
|
||||
serviceNo: '运单号',
|
||||
productCode: '产品编码',
|
||||
productName: '销售产品',
|
||||
maxTimeliness: '最大时效(天)',
|
||||
channelCode: '渠道编码',
|
||||
channelName: '服务渠道',
|
||||
downstreamSupplierName: '服务商名称',
|
||||
downstreamSupplierCode: '服务商编码',
|
||||
downstreamAppKey: '下游AppKey',
|
||||
downstreamAppId: '下游AppId',
|
||||
channelForecastCode: '渠道预报编码',
|
||||
consigneeCountry: '收件国家',
|
||||
consigneeCountryName: '收件国家名称',
|
||||
cargoAttribute: '货物属性',
|
||||
warehouseId: '仓库ID',
|
||||
warehouse: '仓库',
|
||||
customerId: '客户ID',
|
||||
orderSource: '订单来源',
|
||||
pickupTime: '期望揽收时间',
|
||||
carrierWaybillNo: '承运商单号',
|
||||
pickupNo: '揽收单号',
|
||||
inboundMethod: '到仓方式',
|
||||
fulfillmentType: '履约类型',
|
||||
fulfillProgress: '代发进度',
|
||||
stockBindStatus: '绑定状态',
|
||||
domesticTrackingNos: '国内快递单号',
|
||||
stockBindQtyTotal: '绑定件数',
|
||||
stockBindLineCount: '绑定行数',
|
||||
primaryBinCode: '主库位',
|
||||
primarySkuCodes: '绑定SKU',
|
||||
fulfillmentSource: '代发来源',
|
||||
customerSenderAddressId: '发件地址簿ID',
|
||||
requestId: '请求标识',
|
||||
exportCustomsMode: '出口报关方式',
|
||||
exportQualificationId: '出口资质ID',
|
||||
predeclareFailReason: '预报失败原因',
|
||||
allocationPendingReason: '待排货原因',
|
||||
allocationScheme: '排货方案',
|
||||
hasAllocationTrace: '有排货快照',
|
||||
declaredCurrency: '申报币种',
|
||||
declaredSum: '申报总金额',
|
||||
quantity: '包裹数量',
|
||||
isCod: '是否COD',
|
||||
codCurrency: 'COD币种',
|
||||
codSum: 'COD金额',
|
||||
orderStatus: '订单状态',
|
||||
status: '状态码',
|
||||
handleStatus: '处理状态码',
|
||||
handleStatusName: '处理状态',
|
||||
issueType: '问题类型码',
|
||||
issueTypeName: '问题类型',
|
||||
issueTitle: '问题标题',
|
||||
issueContent: '问题内容',
|
||||
issueTemplateId: '问题模板ID',
|
||||
issueRemark: '问题备注',
|
||||
issueTime: '问题时间',
|
||||
claimType: '理赔类型码',
|
||||
claimTypeName: '理赔类型',
|
||||
claimAmount: '理赔金额',
|
||||
claimCurrency: '理赔币种',
|
||||
claimReason: '理赔原因',
|
||||
claimTime: '理赔时间',
|
||||
previousOrderStatus: '原订单状态',
|
||||
receiveTime: '收货时间',
|
||||
shipTime: '发货时间',
|
||||
payTime: '支付时间',
|
||||
isPaid: '是否已支付',
|
||||
signTime: '签收时间',
|
||||
isIntercept: '是否拦截',
|
||||
isRemote: '是否偏远',
|
||||
hasInsurance: '是否投保',
|
||||
forecastReceivableAmount: '预报应收',
|
||||
forecastPayableAmount: '预报应付',
|
||||
salesProductRulesVersion: '规则版本',
|
||||
salesProductPriceVersion: '价格版本',
|
||||
createTime: '创建时间',
|
||||
createBy: '创建人',
|
||||
updateTime: '更新时间',
|
||||
updateBy: '更新人',
|
||||
chargeableWeight: '计费重',
|
||||
receivableAmount: '应收金额',
|
||||
inboundTime: '入库时间',
|
||||
outboundTime: '出库时间',
|
||||
pickupOrderId: '揽收单ID',
|
||||
|
||||
// 寄件人展平
|
||||
senderName: '寄件人',
|
||||
senderCompany: '寄件公司',
|
||||
senderPhone: '寄件手机',
|
||||
senderTelePhone: '寄件电话',
|
||||
senderEmail: '寄件邮箱',
|
||||
senderCountryCode: '寄件国家代码',
|
||||
senderProvince: '寄件省州',
|
||||
senderCity: '寄件城市',
|
||||
senderZipCode: '寄件邮编',
|
||||
senderStreetAddress1: '寄件地址1',
|
||||
senderStreetAddress2: '寄件地址2',
|
||||
senderStreetAddress3: '寄件地址3',
|
||||
senderHouseNumber: '寄件门牌号',
|
||||
|
||||
// 收件人展平
|
||||
receiverRecipientsName: '收件人',
|
||||
receiverCompanyName: '收件公司',
|
||||
receiverPhone: '收件手机',
|
||||
receiverTelePhone: '收件电话',
|
||||
receiverEmail: '收件邮箱',
|
||||
receiverCountryCode: '收件国家代码',
|
||||
receiverProvince: '收件省州',
|
||||
receiverCity: '收件城市',
|
||||
receiverStreetNumber: '收件门牌号',
|
||||
receiverStreetAddress1: '收件地址1',
|
||||
receiverStreetAddress2: '收件地址2',
|
||||
receiverStreetAddress3: '收件地址3',
|
||||
receiverZipCode: '收件邮编',
|
||||
receiverTaxType: '税号类型',
|
||||
receiverTaxNo: '税号',
|
||||
receiverCertificateType: '证件类型',
|
||||
receiverCertificateNo: '证件号码',
|
||||
receiverEori: '收件EORI',
|
||||
receiverVat: '收件VAT',
|
||||
};
|
||||
|
||||
/** 按字段 key 取中文列名;未知字段做可读化,避免直接甩 camelCase。 */
|
||||
export function getOrderColumnLabelZh(key: string): string {
|
||||
const k = String(key || '').trim();
|
||||
if (!k) return '';
|
||||
if (ORDER_COLUMN_LABEL_ZH[k]) return ORDER_COLUMN_LABEL_ZH[k];
|
||||
// fooBarBaz → Foo Bar Baz,再包一层中文前缀,避免纯英文 key 当表头
|
||||
const spaced = k
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.trim();
|
||||
return spaced || k;
|
||||
}
|
||||
@ -1,13 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import DrawerPro from '@/components/DrawerPro/index.vue'
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||
import useDrawer from '@/components/DrawerPro/useDrawer'
|
||||
import { formatDateTime } from '@/utils/warehouseListMapper'
|
||||
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||
import { getSenderAddressList } from '@/api/order/senderAddress'
|
||||
import { getWarehouseList } from '@/api/system/warehouse'
|
||||
import {
|
||||
recalcPickupOrderFee,
|
||||
updatePickupOrderInfo,
|
||||
updatePickupOrderMeasure,
|
||||
} from '@/api/order/pickupOrder'
|
||||
import { loadDomesticPickupProductOptions } from '@/utils/domesticPickupProducts'
|
||||
|
||||
defineOptions({ name: 'PickupOrderDetailDrawer' })
|
||||
|
||||
type SelectOpt = { value: number | string; label: string }
|
||||
type SizeRow = { length?: number; width?: number; height?: number; count?: number }
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
detail: any
|
||||
@ -25,17 +38,58 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
copyServiceNos: []
|
||||
openOrder: [orderId: number]
|
||||
updated: [detail: any]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const [register, { openDrawer, closeDrawer, setDrawerProps }] = useDrawer() as any
|
||||
const detailTab = ref('info')
|
||||
|
||||
const infoEditing = ref(false)
|
||||
const measureEditing = ref(false)
|
||||
const infoSaving = ref(false)
|
||||
const measureSaving = ref(false)
|
||||
const recalcLoading = ref(false)
|
||||
const optionsLoading = ref(false)
|
||||
|
||||
const warehouseOptions = ref<SelectOpt[]>([])
|
||||
const senderOptions = ref<SelectOpt[]>([])
|
||||
const carrierServiceOptions = ref<SelectOpt[]>([])
|
||||
|
||||
const infoForm = reactive({
|
||||
warehouseId: undefined as number | undefined,
|
||||
senderAddressId: undefined as number | undefined,
|
||||
expectPickupTime: '' as string,
|
||||
carrierService: '' as string,
|
||||
carrierWaybillNo: '' as string,
|
||||
})
|
||||
|
||||
const measureForm = reactive({
|
||||
totalActualWeight: undefined as number | undefined,
|
||||
packageSizes: [] as SizeRow[],
|
||||
})
|
||||
|
||||
const detailPayStatus = computed(() =>
|
||||
String(props.detail?.payStatus ?? props.detail?.PayStatus ?? ''),
|
||||
)
|
||||
const detailStatus = computed(() => String(props.detail?.status ?? props.detail?.Status ?? ''))
|
||||
|
||||
const canEditBase = computed(() => {
|
||||
const pay = detailPayStatus.value.toLowerCase()
|
||||
const status = detailStatus.value.toLowerCase()
|
||||
if (pay === 'paid') return false
|
||||
if (status === 'cancelled' || status === 'failed') return false
|
||||
return Boolean(props.detail)
|
||||
})
|
||||
|
||||
const canEditInfo = computed(() => {
|
||||
if (!canEditBase.value) return false
|
||||
const status = detailStatus.value.toLowerCase()
|
||||
return status === 'draft' || status === 'scheduled'
|
||||
})
|
||||
|
||||
const canEditMeasure = computed(() => canEditBase.value)
|
||||
|
||||
const orderColumns = computed(() => [
|
||||
{
|
||||
title: t('order.pickupOrder.table.serviceNo'),
|
||||
@ -69,6 +123,7 @@ watch(
|
||||
() => props.detail?.id ?? props.detail?.Id,
|
||||
() => {
|
||||
detailTab.value = 'info'
|
||||
resetEditState()
|
||||
},
|
||||
)
|
||||
|
||||
@ -78,6 +133,10 @@ watch(tabItems, (items) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(detailTab, () => {
|
||||
resetEditState()
|
||||
})
|
||||
|
||||
function pick(d: any, a: string, b: string) {
|
||||
return d?.[a] ?? d?.[b]
|
||||
}
|
||||
@ -94,18 +153,257 @@ function fmtMoney(v: unknown) {
|
||||
return Number.isFinite(n) ? n.toFixed(2) : String(v)
|
||||
}
|
||||
|
||||
function formatPackageSizes(raw: unknown) {
|
||||
const text = String(raw ?? '').trim()
|
||||
if (!text) return '-'
|
||||
return text
|
||||
.split(';')
|
||||
function normalizePackageSizes(raw: unknown): SizeRow[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.map((x: any) => ({
|
||||
length: Number(x?.length ?? x?.Length) || undefined,
|
||||
width: Number(x?.width ?? x?.Width) || undefined,
|
||||
height: Number(x?.height ?? x?.Height) || undefined,
|
||||
count: Number(x?.count ?? x?.Count) || 1,
|
||||
}))
|
||||
}
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
return raw
|
||||
.split(/[;;]/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.join(';')
|
||||
.map((p) => {
|
||||
const m = p.match(/([\d.]+)\s*[x×*]\s*([\d.]+)\s*[x×*]\s*([\d.]+)(?:\s*[x×*]\s*(\d+))?/i)
|
||||
if (!m) return { length: undefined, width: undefined, height: undefined, count: 1 }
|
||||
return {
|
||||
length: Number(m[1]) || undefined,
|
||||
width: Number(m[2]) || undefined,
|
||||
height: Number(m[3]) || undefined,
|
||||
count: Number(m[4]) || 1,
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function formatPackageSizes(raw: unknown) {
|
||||
const rows = normalizePackageSizes(raw)
|
||||
if (!rows.length) {
|
||||
const text = typeof raw === 'string' ? raw.trim() : ''
|
||||
return text || '-'
|
||||
}
|
||||
return rows
|
||||
.map((r) => {
|
||||
const dims = [r.length, r.width, r.height].filter((n) => n != null && Number(n) > 0)
|
||||
if (dims.length < 3) return ''
|
||||
const base = `${r.length}×${r.width}×${r.height}`
|
||||
return r.count && r.count > 1 ? `${base}×${r.count}` : base
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(';') || '-'
|
||||
}
|
||||
|
||||
function resetEditState() {
|
||||
infoEditing.value = false
|
||||
measureEditing.value = false
|
||||
infoSaving.value = false
|
||||
measureSaving.value = false
|
||||
recalcLoading.value = false
|
||||
}
|
||||
|
||||
async function ensureEditOptions() {
|
||||
if (warehouseOptions.value.length && senderOptions.value.length && carrierServiceOptions.value.length) {
|
||||
return
|
||||
}
|
||||
optionsLoading.value = true
|
||||
try {
|
||||
const customerId = Number(pick(props.detail, 'customerId', 'CustomerId') || 0)
|
||||
const senderBody: Record<string, unknown> = {
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
addressType: 'pickup',
|
||||
}
|
||||
if (props.isAdmin && customerId > 0) {
|
||||
senderBody.customerId = customerId
|
||||
}
|
||||
|
||||
const [senderRes, warehouseRes, pickupProducts] = await Promise.all([
|
||||
getSenderAddressList(senderBody),
|
||||
getWarehouseList({ page: 1, pageSize: 500 }),
|
||||
loadDomesticPickupProductOptions(),
|
||||
])
|
||||
|
||||
senderOptions.value = (senderRes.items || [])
|
||||
.map((item: any) => {
|
||||
const row = normalizeListRow(item)
|
||||
const id = Number(pickField(row, 'id', 'Id') || 0)
|
||||
const name = String(
|
||||
pickField(row, 'addressName', 'AddressName') ||
|
||||
pickField(row, 'senderName', 'SenderName') ||
|
||||
id,
|
||||
)
|
||||
const phone = String(pickField(row, 'phone', 'Phone') || '')
|
||||
return { value: id, label: phone ? `${name} · ${phone}` : name }
|
||||
})
|
||||
.filter((x) => Number(x.value) > 0)
|
||||
|
||||
warehouseOptions.value = (warehouseRes.items || [])
|
||||
.map((item: any) => {
|
||||
const id = Number(item?.id ?? item?.Id ?? 0)
|
||||
const name = String(item?.warehouseName ?? item?.WarehouseName ?? id)
|
||||
return { value: id, label: name }
|
||||
})
|
||||
.filter((x) => Number(x.value) > 0)
|
||||
|
||||
carrierServiceOptions.value = pickupProducts.map((p) => ({ value: p.value, label: p.label }))
|
||||
} catch (e: any) {
|
||||
Message.warning(e?.message || t('order.pickupOrder.message.optionsLoadFailed'))
|
||||
} finally {
|
||||
optionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startInfoEdit() {
|
||||
if (!canEditInfo.value || !props.detail) return
|
||||
await ensureEditOptions()
|
||||
const d = props.detail
|
||||
infoForm.warehouseId = Number(pick(d, 'warehouseId', 'WarehouseId') || 0) || undefined
|
||||
infoForm.senderAddressId = Number(pick(d, 'senderAddressId', 'SenderAddressId') || 0) || undefined
|
||||
const t0 = pick(d, 'expectPickupTime', 'ExpectPickupTime')
|
||||
infoForm.expectPickupTime = t0 ? dayjs(t0).format('YYYY-MM-DD HH:mm:ss') : ''
|
||||
infoForm.carrierService = String(pick(d, 'carrierService', 'CarrierService') || '')
|
||||
infoForm.carrierWaybillNo = String(pick(d, 'carrierWaybillNo', 'CarrierWaybillNo') || '')
|
||||
infoEditing.value = true
|
||||
}
|
||||
|
||||
function cancelInfoEdit() {
|
||||
infoEditing.value = false
|
||||
}
|
||||
|
||||
async function saveInfoEdit() {
|
||||
if (!props.detail) return
|
||||
if (!infoForm.warehouseId) {
|
||||
Message.warning(t('order.pickupOrder.message.warehouseRequired'))
|
||||
return
|
||||
}
|
||||
if (!infoForm.senderAddressId) {
|
||||
Message.warning(t('order.pickupOrder.message.senderRequired'))
|
||||
return
|
||||
}
|
||||
if (!infoForm.expectPickupTime) {
|
||||
Message.warning(t('order.pickupOrder.message.expectTimeRequired'))
|
||||
return
|
||||
}
|
||||
infoSaving.value = true
|
||||
try {
|
||||
const id = pick(props.detail, 'id', 'Id')
|
||||
const res = await updatePickupOrderInfo({
|
||||
id,
|
||||
warehouseId: infoForm.warehouseId,
|
||||
senderAddressId: infoForm.senderAddressId,
|
||||
expectPickupTime: infoForm.expectPickupTime,
|
||||
carrierService: infoForm.carrierService || undefined,
|
||||
carrierWaybillNo: infoForm.carrierWaybillNo,
|
||||
})
|
||||
infoEditing.value = false
|
||||
emit('updated', res)
|
||||
Message.success(t('order.pickupOrder.message.infoSaveOk'))
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||
} finally {
|
||||
infoSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startMeasureEdit() {
|
||||
if (!canEditMeasure.value || !props.detail) return
|
||||
const d = props.detail
|
||||
const weight = Number(pick(d, 'totalActualWeight', 'TotalActualWeight'))
|
||||
measureForm.totalActualWeight = Number.isFinite(weight) && weight > 0 ? weight : undefined
|
||||
const sizes = normalizePackageSizes(pick(d, 'packageSizes', 'PackageSizes'))
|
||||
measureForm.packageSizes = sizes.length
|
||||
? sizes.map((x) => ({ ...x }))
|
||||
: [{ length: undefined, width: undefined, height: undefined, count: 1 }]
|
||||
measureEditing.value = true
|
||||
}
|
||||
|
||||
function cancelMeasureEdit() {
|
||||
measureEditing.value = false
|
||||
}
|
||||
|
||||
function addSizeRow() {
|
||||
measureForm.packageSizes.push({ length: undefined, width: undefined, height: undefined, count: 1 })
|
||||
}
|
||||
|
||||
function removeSizeRow(index: number) {
|
||||
if (measureForm.packageSizes.length <= 1) return
|
||||
measureForm.packageSizes.splice(index, 1)
|
||||
}
|
||||
|
||||
async function saveMeasureEdit() {
|
||||
if (!props.detail) return
|
||||
const weight = Number(measureForm.totalActualWeight)
|
||||
if (!(weight > 0)) {
|
||||
Message.warning(t('order.pickupOrder.message.weightRequired'))
|
||||
return
|
||||
}
|
||||
const sizes = measureForm.packageSizes
|
||||
.map((r) => ({
|
||||
length: Number(r.length) || 0,
|
||||
width: Number(r.width) || 0,
|
||||
height: Number(r.height) || 0,
|
||||
count: Number(r.count) > 0 ? Number(r.count) : 1,
|
||||
}))
|
||||
.filter((r) => r.length > 0 && r.width > 0 && r.height > 0)
|
||||
if (!sizes.length) {
|
||||
Message.warning(t('order.pickupOrder.message.packageSizeRequired'))
|
||||
return
|
||||
}
|
||||
measureSaving.value = true
|
||||
try {
|
||||
const id = pick(props.detail, 'id', 'Id')
|
||||
const res = await updatePickupOrderMeasure({
|
||||
id,
|
||||
totalActualWeight: weight,
|
||||
packageSizes: sizes,
|
||||
})
|
||||
measureEditing.value = false
|
||||
emit('updated', res)
|
||||
Message.success(t('order.pickupOrder.message.measureSaveOk'))
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||
} finally {
|
||||
measureSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRecalcFee() {
|
||||
if (!props.detail || !canEditMeasure.value) return
|
||||
const weight = Number(pick(props.detail, 'totalActualWeight', 'TotalActualWeight'))
|
||||
const sizes = normalizePackageSizes(pick(props.detail, 'packageSizes', 'PackageSizes'))
|
||||
const carrier = String(pick(props.detail, 'carrierService', 'CarrierService') || '').trim()
|
||||
if (!(weight > 0) || !sizes.length) {
|
||||
Message.warning(t('order.pickupOrder.message.recalcNeedMeasure'))
|
||||
return
|
||||
}
|
||||
if (!carrier) {
|
||||
Message.warning(t('order.pickupOrder.message.carrierServiceRequired'))
|
||||
return
|
||||
}
|
||||
recalcLoading.value = true
|
||||
try {
|
||||
const id = pick(props.detail, 'id', 'Id')
|
||||
const res = await recalcPickupOrderFee(id)
|
||||
emit('updated', res)
|
||||
Message.success(t('order.pickupOrder.message.recalcOk'))
|
||||
if (props.isAdmin) {
|
||||
detailTab.value = 'fee'
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||
} finally {
|
||||
recalcLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
detailTab.value = 'info'
|
||||
resetEditState()
|
||||
setDrawerProps({
|
||||
title: t('order.pickupOrder.detail.title'),
|
||||
width: 720,
|
||||
@ -117,6 +415,7 @@ function open() {
|
||||
}
|
||||
|
||||
function close() {
|
||||
resetEditState()
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
@ -157,7 +456,24 @@ defineExpose({ open, close })
|
||||
<SlidingTabs v-model="detailTab" :items="tabItems" size="mini" class="pk-detail-tabs" />
|
||||
|
||||
<div class="pk-detail-body">
|
||||
<div v-show="detailTab === 'info'" class="pk-detail-grid">
|
||||
<div v-show="detailTab === 'info'">
|
||||
<div class="pk-detail-toolbar">
|
||||
<template v-if="canEditInfo && !infoEditing">
|
||||
<a-button type="primary" size="mini" @click="startInfoEdit">
|
||||
{{ t('order.pickupOrder.actions.editInfo') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="infoEditing">
|
||||
<a-button size="mini" :disabled="infoSaving" @click="cancelInfoEdit">
|
||||
{{ t('order.pickupOrder.actions.cancelEdit') }}
|
||||
</a-button>
|
||||
<a-button type="primary" size="mini" :loading="infoSaving" @click="saveInfoEdit">
|
||||
{{ t('order.pickupOrder.actions.saveInfo') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!infoEditing" class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.carrierWaybillNo') }}</span>
|
||||
<b>{{ detail.carrierWaybillNo || detail.CarrierWaybillNo || '-' }}</b>
|
||||
@ -184,18 +500,112 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="detailTab === 'measure'" class="pk-detail-grid">
|
||||
<a-spin v-else :loading="optionsLoading">
|
||||
<a-form layout="vertical" class="pk-edit-form">
|
||||
<a-form-item :label="t('order.pickupOrder.table.warehouse')" required>
|
||||
<a-select
|
||||
v-model="infoForm.warehouseId"
|
||||
allow-search
|
||||
:placeholder="t('order.pickupOrder.form.warehousePlaceholder')"
|
||||
>
|
||||
<a-option
|
||||
v-for="opt in warehouseOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.form.senderAddress')" required>
|
||||
<a-select
|
||||
v-model="infoForm.senderAddressId"
|
||||
allow-search
|
||||
:placeholder="t('order.pickupOrder.form.senderAddressPlaceholder')"
|
||||
>
|
||||
<a-option v-for="opt in senderOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.table.expectTime')" required>
|
||||
<a-date-picker
|
||||
v-model="infoForm.expectPickupTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.table.carrierService')">
|
||||
<a-select
|
||||
v-model="infoForm.carrierService"
|
||||
allow-search
|
||||
allow-clear
|
||||
:placeholder="t('order.pickupOrder.form.carrierServicePlaceholder')"
|
||||
>
|
||||
<a-option
|
||||
v-for="opt in carrierServiceOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.table.carrierWaybillNo')">
|
||||
<a-input
|
||||
v-model="infoForm.carrierWaybillNo"
|
||||
:placeholder="t('order.pickupOrder.form.carrierWaybillNoPh')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<div v-show="detailTab === 'measure'">
|
||||
<div class="pk-detail-toolbar">
|
||||
<template v-if="canEditMeasure && !measureEditing">
|
||||
<a-button type="primary" size="mini" @click="startMeasureEdit">
|
||||
{{ t('order.pickupOrder.actions.editMeasure') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
class="app-btn-primary-soft"
|
||||
:loading="recalcLoading"
|
||||
@click="handleRecalcFee"
|
||||
>
|
||||
{{ t('order.pickupOrder.actions.recalcFee') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="measureEditing">
|
||||
<a-button size="mini" :disabled="measureSaving" @click="cancelMeasureEdit">
|
||||
{{ t('order.pickupOrder.actions.cancelEdit') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
:loading="measureSaving"
|
||||
@click="saveMeasureEdit"
|
||||
>
|
||||
{{ t('order.pickupOrder.actions.saveMeasure') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!measureEditing" class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.actualWeightKg') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'actualWeightKg', 'ActualWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'totalActualWeight', 'TotalActualWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.chargeableWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'chargeableWeightKg', 'ChargeableWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'chargeableWeight', 'ChargeableWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.volumetricWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'volumetricWeightKg', 'VolumetricWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'volumetricWeight', 'VolumetricWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field pk-detail-grid-span">
|
||||
<span>{{ t('order.pickupOrder.detail.packageSizes') }}</span>
|
||||
@ -203,6 +613,69 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="pk-edit-form">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('order.pickupOrder.table.actualWeightKg')" required>
|
||||
<a-input-number
|
||||
v-model="measureForm.totalActualWeight"
|
||||
:min="0.001"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.detail.packageSizes')" required>
|
||||
<div class="pk-size-list">
|
||||
<div
|
||||
v-for="(row, idx) in measureForm.packageSizes"
|
||||
:key="idx"
|
||||
class="pk-size-row"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="row.length"
|
||||
:min="0.1"
|
||||
:precision="1"
|
||||
:placeholder="t('order.pickupOrder.form.sizeLength')"
|
||||
/>
|
||||
<span class="pk-size-sep">×</span>
|
||||
<a-input-number
|
||||
v-model="row.width"
|
||||
:min="0.1"
|
||||
:precision="1"
|
||||
:placeholder="t('order.pickupOrder.form.sizeWidth')"
|
||||
/>
|
||||
<span class="pk-size-sep">×</span>
|
||||
<a-input-number
|
||||
v-model="row.height"
|
||||
:min="0.1"
|
||||
:precision="1"
|
||||
:placeholder="t('order.pickupOrder.form.sizeHeight')"
|
||||
/>
|
||||
<span class="pk-size-sep">×</span>
|
||||
<a-input-number
|
||||
v-model="row.count"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:placeholder="t('order.pickupOrder.form.sizeCount')"
|
||||
/>
|
||||
<a-button
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger"
|
||||
:disabled="measureForm.packageSizes.length <= 1"
|
||||
@click="removeSizeRow(idx)"
|
||||
>
|
||||
{{ t('order.pickupOrder.actions.removeSize') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button size="mini" @click="addSizeRow">
|
||||
{{ t('order.pickupOrder.actions.addSize') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="detailTab === 'fee' && isAdmin">
|
||||
<div class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
@ -211,7 +684,7 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.detail.omsPayable') }}</span>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'payableAmount', 'PayableAmount')) }}</b>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'payableAmount', 'PayableAmount') ?? pick(detail, 'carrierPayableAmount', 'CarrierPayableAmount')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.form.freight') }}</span>
|
||||
@ -232,15 +705,15 @@ defineExpose({ open, close })
|
||||
<div class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.detail.carrierFreightWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'carrierFreightWeightKg', 'CarrierFreightWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'carrierFreightWeight', 'CarrierFreightWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.detail.carrierQuotedFreight') }}</span>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreight', 'CarrierQuotedFreight')) }}</b>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreightAmount', 'CarrierQuotedFreightAmount')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field pk-detail-grid-span">
|
||||
<span>{{ t('order.pickupOrder.detail.carrierQuotedReceivable') }}</span>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedReceivable', 'CarrierQuotedReceivable')) }}</b>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedReceivableAmount', 'CarrierQuotedReceivableAmount')) }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -312,6 +785,13 @@ defineExpose({ open, close })
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.pk-detail-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pk-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -443,6 +923,33 @@ defineExpose({ open, close })
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pk-edit-form {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.pk-size-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pk-size-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
:deep(.arco-input-number) {
|
||||
width: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.pk-size-sep {
|
||||
color: var(--color-text-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pk-detail-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
|
||||
@ -153,12 +153,11 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||
<a-select
|
||||
v-model="bindForm.carrierService"
|
||||
:options="carrierServiceOptions"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="refLoading"
|
||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||
/>
|
||||
@ -622,7 +621,7 @@ async function loadBindableOrders() {
|
||||
function resetBindForm() {
|
||||
bindForm.senderAddressId = senderOptions.value[0]?.value
|
||||
bindForm.expectPickupTime = defaultPickupTime()
|
||||
bindForm.carrierService = ''
|
||||
bindForm.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||
bindForm.remark = ''
|
||||
orderKeyword.value = ''
|
||||
orderRows.value = []
|
||||
@ -723,6 +722,10 @@ function validateBind() {
|
||||
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
|
||||
return false
|
||||
}
|
||||
if (!bindForm.carrierService?.trim()) {
|
||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -767,7 +770,7 @@ async function handleCreate() {
|
||||
senderAddressId: bindForm.senderAddressId!,
|
||||
direction: createDirection.value,
|
||||
warehouseId: selectedWarehouseId.value!,
|
||||
carrierService: bindForm.carrierService || undefined,
|
||||
carrierService: bindForm.carrierService.trim(),
|
||||
expectPickupTime: bindForm.expectPickupTime,
|
||||
orderIds: selectedOrders.value.map((x) => x.id),
|
||||
remark: bindForm.remark || undefined,
|
||||
|
||||
@ -27,7 +27,6 @@ import {
|
||||
postPickupOrderTrackUpsertBigBag,
|
||||
removePickupOrderOrders,
|
||||
reschedulePickupOrder,
|
||||
schedulePickupOrder,
|
||||
type PickupBigBagTrackSyncResult,
|
||||
type PickupBigBagTrackStepResult,
|
||||
} from '@/api/order/pickupOrder'
|
||||
@ -115,19 +114,12 @@ const detailLoading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const detailDrawerRef = ref<{ open: () => void; close: () => void } | null>(null)
|
||||
const [registerFreightModal, { openModal: openFreightModal, closeModal: closeFreightModal }] = useModal() as any
|
||||
const [registerScheduleModal, { openModal: openScheduleModal, closeModal: closeScheduleModal }] = useModal() as any
|
||||
const [registerRescheduleModal, { openModal: openRescheduleModal, closeModal: closeRescheduleModal }] = useModal() as any
|
||||
const [registerRebindModal, { openModal: openRebindModal, closeModal: closeRebindModal }] = useModal() as any
|
||||
const [registerBatchFreightModal, { openModal: openBatchFreightModal, closeModal: closeBatchFreightModal }] =
|
||||
useModal() as any
|
||||
|
||||
const freightForm = reactive({ id: 0, freightAmount: undefined as number | undefined, otherFeeAmount: undefined as number | undefined })
|
||||
const scheduleForm = reactive({
|
||||
id: 0,
|
||||
carrierService: '',
|
||||
carrierWaybillNo: '',
|
||||
expectPickupTime: '',
|
||||
})
|
||||
const rescheduleForm = reactive({
|
||||
id: 0,
|
||||
expectPickupTime: '',
|
||||
@ -148,15 +140,12 @@ const actionLoading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const batchPaying = ref(false)
|
||||
const batchFreighting = ref(false)
|
||||
const batchScheduling = ref(false)
|
||||
const batchDownstreamForwarding = ref(false)
|
||||
const downstreamForwardModalVisible = ref(false)
|
||||
const downstreamForwardResultText = ref('')
|
||||
const batchTrackUpserting = ref(false)
|
||||
const trackUpsertModalVisible = ref(false)
|
||||
const trackUpsertResultText = ref('')
|
||||
/** batch schedule uses same schedule modal with multiple ids */
|
||||
const batchScheduleIds = ref<number[]>([])
|
||||
|
||||
const canForwardDownstream = computed(
|
||||
() =>
|
||||
@ -187,9 +176,6 @@ const batchFreightCandidates = computed(() =>
|
||||
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed'
|
||||
}),
|
||||
)
|
||||
const batchScheduleCandidates = computed(() =>
|
||||
selectedRows.value.filter((row) => canShowSchedule(String(row?.status || ''))),
|
||||
)
|
||||
|
||||
const statusTabs = [
|
||||
{ value: STATUS_TAB_ALL, labelKey: 'common.all' },
|
||||
@ -249,10 +235,6 @@ function canShowPay(row: { status?: string; payStatus?: string; receivableAmount
|
||||
return Number.isFinite(amount) && amount > 0
|
||||
}
|
||||
|
||||
function canShowSchedule(status: string) {
|
||||
return status === 'draft' || status === 'scheduled'
|
||||
}
|
||||
|
||||
function canShowRebind(status: string) {
|
||||
return status === 'draft' || status === 'scheduled'
|
||||
}
|
||||
@ -362,9 +344,6 @@ function actionRenderer(rowData: any) {
|
||||
if (canShowPay(rowData)) {
|
||||
parts.push(renderWarehouseActionBtn('pay', t('order.pickupOrder.actions.pay'), 'pay'))
|
||||
}
|
||||
if (canShowSchedule(status)) {
|
||||
parts.push(renderWarehouseActionBtn('schedule', t('order.pickupOrder.actions.draftForecast'), 'publish'))
|
||||
}
|
||||
if (canShowRebind(status)) {
|
||||
parts.push(renderWarehouseActionBtn('rebind', t('order.pickupOrder.actions.rebind'), 'rebind'))
|
||||
}
|
||||
@ -458,7 +437,6 @@ function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
|
||||
const status = String(payload.status || 'draft')
|
||||
const payStatus = String(payload.payStatus || '')
|
||||
const amount = Number(payload.receivableAmount)
|
||||
const canSchedule = canShowSchedule(status)
|
||||
const canPay =
|
||||
payStatus !== 'paid' &&
|
||||
status !== 'cancelled' &&
|
||||
@ -481,20 +459,6 @@ function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
|
||||
'div',
|
||||
{ style: 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end' },
|
||||
[
|
||||
canSchedule
|
||||
? h(
|
||||
'button',
|
||||
{
|
||||
class: 'arco-btn arco-btn-secondary arco-btn-size-medium',
|
||||
style: 'height:32px',
|
||||
onClick: () => {
|
||||
Modal.destroyAll()
|
||||
if (id) void openScheduleById(id)
|
||||
},
|
||||
},
|
||||
t('order.pickupOrder.createNext.goSchedule'),
|
||||
)
|
||||
: null,
|
||||
canPay
|
||||
? h(
|
||||
'button',
|
||||
@ -836,26 +800,6 @@ async function submitBatchFreight() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchSchedule() {
|
||||
if (!hasPermission('Order.Pickup.Create')) {
|
||||
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
|
||||
return
|
||||
}
|
||||
if (!batchScheduleCandidates.value.length) {
|
||||
$baseMessage(t('order.pickupOrder.message.batchScheduleNoCandidates'), 'warning')
|
||||
return
|
||||
}
|
||||
await ensureCarrierServiceOptions()
|
||||
batchScheduleIds.value = batchScheduleCandidates.value
|
||||
.map((r) => Number(r.id))
|
||||
.filter((id) => id > 0)
|
||||
scheduleForm.id = 0
|
||||
scheduleForm.carrierService = ''
|
||||
scheduleForm.carrierWaybillNo = ''
|
||||
scheduleForm.expectPickupTime = dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
|
||||
openScheduleModal(true)
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
void loadList()
|
||||
@ -874,10 +818,6 @@ function handleRowAction(row: any, _rowIndex: number, action?: string) {
|
||||
void openDetail(row.id)
|
||||
return
|
||||
}
|
||||
if (key === 'schedule') {
|
||||
void openScheduleById(row.id)
|
||||
return
|
||||
}
|
||||
if (key === 'rebind') {
|
||||
void openRebind(row.id)
|
||||
return
|
||||
@ -918,6 +858,13 @@ async function openDetail(id: number | string) {
|
||||
}
|
||||
}
|
||||
|
||||
function onDetailUpdated(res: any) {
|
||||
if (res) {
|
||||
detail.value = res
|
||||
}
|
||||
void loadList()
|
||||
}
|
||||
|
||||
function openConfirmFreight() {
|
||||
if (!detail.value) return
|
||||
freightForm.id = Number(detail.value.id ?? detail.value.Id)
|
||||
@ -1003,93 +950,6 @@ async function doPayById(id: number | string) {
|
||||
}
|
||||
}
|
||||
|
||||
function fillScheduleForm(raw: any) {
|
||||
scheduleForm.id = Number(raw.id ?? raw.Id)
|
||||
scheduleForm.carrierService = String(raw.carrierService ?? raw.CarrierService ?? '')
|
||||
scheduleForm.carrierWaybillNo = String(raw.carrierWaybillNo ?? raw.CarrierWaybillNo ?? '')
|
||||
const t0 = raw.expectPickupTime ?? raw.ExpectPickupTime
|
||||
scheduleForm.expectPickupTime = t0
|
||||
? dayjs(t0).format('YYYY-MM-DD HH:mm:ss')
|
||||
: dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
async function openSchedule() {
|
||||
if (!detail.value) return
|
||||
batchScheduleIds.value = []
|
||||
await ensureCarrierServiceOptions()
|
||||
fillScheduleForm(detail.value)
|
||||
openScheduleModal(true)
|
||||
}
|
||||
|
||||
async function openScheduleById(id: number | string) {
|
||||
actionLoading.value = true
|
||||
batchScheduleIds.value = []
|
||||
try {
|
||||
await ensureCarrierServiceOptions()
|
||||
const raw = await getPickupOrderDetail(id)
|
||||
fillScheduleForm(raw)
|
||||
openScheduleModal(true)
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSchedule() {
|
||||
if (!scheduleForm.carrierService.trim()) {
|
||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
const ids =
|
||||
batchScheduleIds.value.length > 0
|
||||
? [...batchScheduleIds.value]
|
||||
: scheduleForm.id > 0
|
||||
? [scheduleForm.id]
|
||||
: []
|
||||
if (!ids.length) return
|
||||
|
||||
actionLoading.value = true
|
||||
batchScheduling.value = batchScheduleIds.value.length > 0
|
||||
let success = 0
|
||||
let fail = 0
|
||||
try {
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const res = await schedulePickupOrder({
|
||||
id,
|
||||
carrierService: scheduleForm.carrierService.trim(),
|
||||
carrierWaybillNo: scheduleForm.carrierWaybillNo.trim() || undefined,
|
||||
expectPickupTime: scheduleForm.expectPickupTime || undefined,
|
||||
})
|
||||
if (detail.value && Number(detail.value.id ?? detail.value.Id) === id) {
|
||||
detail.value = res
|
||||
}
|
||||
success += 1
|
||||
} catch {
|
||||
fail += 1
|
||||
}
|
||||
}
|
||||
closeScheduleModal()
|
||||
batchScheduleIds.value = []
|
||||
if (ids.length > 1) {
|
||||
$baseMessage(
|
||||
t('order.pickupOrder.message.batchScheduleResult', { success, fail }),
|
||||
fail > 0 ? 'warning' : 'success',
|
||||
)
|
||||
} else if (fail > 0) {
|
||||
$baseMessage(t('order.pickupOrder.message.actionFailed'), 'error')
|
||||
} else {
|
||||
$baseMessage(t('order.pickupOrder.message.scheduleOk'), 'success')
|
||||
}
|
||||
selectedRows.value = []
|
||||
void loadList()
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
batchScheduling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openReschedule() {
|
||||
if (!detail.value) return
|
||||
rescheduleForm.id = Number(detail.value.id ?? detail.value.Id)
|
||||
@ -1360,12 +1220,6 @@ const detailSettlementText = computed(() => {
|
||||
return settlementStatusLabel(String(detail.value.settlementStatus || detail.value.SettlementStatus || 'none'))
|
||||
})
|
||||
|
||||
const scheduleModalTitle = computed(() =>
|
||||
batchScheduleIds.value.length > 1
|
||||
? t('order.pickupOrder.actions.batchSchedule')
|
||||
: t('order.pickupOrder.actions.draftForecast'),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.status,
|
||||
() => {
|
||||
@ -1544,6 +1398,7 @@ onMounted(() => {
|
||||
:pay-status-text="payStatusLabel(detailPayStatus)"
|
||||
@copy-service-nos="copyAllServiceNos"
|
||||
@open-order="goLinkedOrder"
|
||||
@updated="onDetailUpdated"
|
||||
/>
|
||||
|
||||
<ModalPro
|
||||
@ -1585,51 +1440,6 @@ onMounted(() => {
|
||||
</a-form>
|
||||
</ModalPro>
|
||||
|
||||
<ModalPro
|
||||
@register="registerScheduleModal"
|
||||
:title="scheduleModalTitle"
|
||||
:confirm-loading="actionLoading || batchScheduling"
|
||||
@ok="submitSchedule"
|
||||
>
|
||||
<a-alert
|
||||
v-if="batchScheduleIds.length > 1"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
{{ t('order.pickupOrder.message.batchScheduleHint', { count: batchScheduleIds.length }) }}
|
||||
</a-alert>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||
<a-select
|
||||
v-model="scheduleForm.carrierService"
|
||||
allow-search
|
||||
allow-clear
|
||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||
>
|
||||
<a-option v-for="opt in carrierServiceOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.pickupOrder.table.carrierWaybillNo')">
|
||||
<a-input
|
||||
v-model="scheduleForm.carrierWaybillNo"
|
||||
:placeholder="t('order.pickupOrder.form.carrierWaybillNoPh')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.myOrders.createPickup.expectTime')">
|
||||
<a-date-picker
|
||||
v-model="scheduleForm.expectPickupTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ModalPro>
|
||||
|
||||
<ModalPro
|
||||
@register="registerRescheduleModal"
|
||||
:title="t('order.pickupOrder.actions.reschedule')"
|
||||
|
||||
@ -41,8 +41,15 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
import { getDictListAll } from '@/api/system/dict';
|
||||
import { getCountryList } from '@/api/system/country';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||
import { mapWarehouseSkuToDeclarationFields, normalizeListRow, normalizeSkuImageForStorage, pickField } from '@/utils/warehouseListMapper';
|
||||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||||
import {
|
||||
compareWarehouseSort,
|
||||
formatWarehouseShipAddress,
|
||||
formatWarehouseShipRecipient,
|
||||
} from '@/utils/warehouseShipAddress';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer';
|
||||
import { useNanaBatchPanel } from '@/composables/useNanaBatchPanel';
|
||||
@ -319,6 +326,8 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
recipient?: string;
|
||||
contactPhone?: string;
|
||||
shippingAddress?: string;
|
||||
sort?: number | null;
|
||||
remark?: string;
|
||||
};
|
||||
const singleWarehouseOptions = ref<WarehouseSelectOption[]>([]);
|
||||
const singleCurrencyOptions = ref<SingleSelectOption[]>([]);
|
||||
@ -362,12 +371,16 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const sortRaw = row?.sort ?? row?.Sort;
|
||||
const sortNum = sortRaw == null || sortRaw === '' ? null : Number(sortRaw);
|
||||
return {
|
||||
id: id != null && id !== '' ? String(id) : '',
|
||||
name,
|
||||
recipient: pick('recipient', 'Recipient'),
|
||||
contactPhone: pick('contactPhone', 'ContactPhone'),
|
||||
shippingAddress: pick('shippingAddress', 'ShippingAddress'),
|
||||
sort: sortNum != null && Number.isFinite(sortNum) ? sortNum : null,
|
||||
remark: pick('remark', 'Remark'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -2135,20 +2148,51 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
return singleWarehouseOptions.value.find((opt) => String(opt.value) === id);
|
||||
});
|
||||
|
||||
const selectedWarehouseHasAddress = computed(() => {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
/** 客户登录时拼接到仓库收件人/地址后的客户编号 */
|
||||
const currentCustomerCode = ref('');
|
||||
|
||||
async function loadCurrentCustomerCode() {
|
||||
if (!rbacUserStore.isCustomerPortal()) {
|
||||
currentCustomerCode.value = '';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||||
const data = await resolveCurrentCustomer({ mapId, account });
|
||||
currentCustomerCode.value = String(data?.customerCode ?? '').trim();
|
||||
} catch {
|
||||
currentCustomerCode.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function displayWarehouseRecipient(opt?: WarehouseSelectOption) {
|
||||
return formatWarehouseShipRecipient(opt?.recipient, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
function displayWarehouseShippingAddress(opt?: WarehouseSelectOption) {
|
||||
return formatWarehouseShipAddress(opt?.shippingAddress, currentCustomerCode.value);
|
||||
}
|
||||
|
||||
function warehouseHasAddress(opt?: WarehouseSelectOption) {
|
||||
if (!opt) return false;
|
||||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress);
|
||||
});
|
||||
}
|
||||
|
||||
function buildWarehouseShipCopyText() {
|
||||
const opt = selectedWarehouseOption.value;
|
||||
if (!opt) return '';
|
||||
const warehouseShipAddressList = computed(() => singleWarehouseOptions.value);
|
||||
|
||||
const warehousesWithShipAddress = computed(() =>
|
||||
singleWarehouseOptions.value.filter((opt) => warehouseHasAddress(opt)),
|
||||
);
|
||||
|
||||
function buildWarehouseShipCopyText(opt?: WarehouseSelectOption) {
|
||||
const target = opt ?? selectedWarehouseOption.value;
|
||||
if (!target) return '';
|
||||
const lines = [
|
||||
opt.label || '',
|
||||
`收件人:${opt.recipient || '-'}`,
|
||||
`电话:${opt.contactPhone || '-'}`,
|
||||
`地址:${opt.shippingAddress || '-'}`,
|
||||
target.label || '',
|
||||
`收件人:${displayWarehouseRecipient(target)}`,
|
||||
`电话:${target.contactPhone || '-'}`,
|
||||
`地址:${displayWarehouseShippingAddress(target)}`,
|
||||
];
|
||||
const clientNo = String(singleForm.clientNo ?? '').trim();
|
||||
if (clientNo) {
|
||||
@ -2157,15 +2201,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
return lines.filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
async function copyWarehouseShipAddress() {
|
||||
const text = buildWarehouseShipCopyText();
|
||||
if (!text.trim()) {
|
||||
Message.warning('该仓库暂未配置收货地址');
|
||||
return;
|
||||
}
|
||||
async function writeClipboardText(text: string, successMsg: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
Message.success('仓库寄件信息已复制');
|
||||
Message.success(successMsg);
|
||||
} catch {
|
||||
const area = document.createElement('textarea');
|
||||
area.value = text;
|
||||
@ -2175,7 +2214,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
area.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
Message.success('仓库寄件信息已复制');
|
||||
Message.success(successMsg);
|
||||
} catch {
|
||||
Message.error('复制失败');
|
||||
}
|
||||
@ -2183,6 +2222,15 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
}
|
||||
|
||||
async function copyWarehouseShipAddress(opt?: WarehouseSelectOption) {
|
||||
const text = buildWarehouseShipCopyText(opt);
|
||||
if (!text.trim()) {
|
||||
Message.warning('该仓库暂未配置收货地址');
|
||||
return;
|
||||
}
|
||||
await writeClipboardText(text, '仓库寄件信息已复制');
|
||||
}
|
||||
|
||||
function resolveDefaultDeclaredCurrency() {
|
||||
const opts = singleCurrencyOptions.value;
|
||||
const usd = opts.find((o) => String(o.value).trim().toUpperCase() === 'USD');
|
||||
@ -2446,7 +2494,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
recipient: w.recipient,
|
||||
contactPhone: w.contactPhone,
|
||||
shippingAddress: w.shippingAddress,
|
||||
}));
|
||||
sort: w.sort,
|
||||
remark: w.remark,
|
||||
}))
|
||||
.sort(compareWarehouseSort);
|
||||
singleWarehouseOptions.value = whOpts;
|
||||
if (whOpts.length && !String(singleForm.warehouseId ?? '').trim()) {
|
||||
singleForm.warehouseId = whOpts[0].value;
|
||||
@ -2592,6 +2643,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
onMounted(() => {
|
||||
loadSingleFormReferenceOptions();
|
||||
void loadCurrentCustomerCode();
|
||||
loadReceiverAiPrefs();
|
||||
void Promise.all([loadReceiverAiProviders(), loadReceiverAiSkills()]);
|
||||
void (async () => {
|
||||
@ -3926,49 +3978,85 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
<template #content>
|
||||
<div class="warehouse-ship-popover__body">
|
||||
<div class="warehouse-ship-popover__head">
|
||||
<strong>寄往仓库</strong>
|
||||
</div>
|
||||
<p
|
||||
v-if="!warehouseShipAddressList.length"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
暂无仓库信息,请稍后重试或联系客服
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!warehousesWithShipAddress.length"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
暂无已配置收货地址的仓库,请联系客服
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="warehouse-ship-popover__list">
|
||||
<div
|
||||
v-for="opt in warehouseShipAddressList"
|
||||
:key="opt.value"
|
||||
class="warehouse-ship-popover__item"
|
||||
:class="{
|
||||
'is-selected':
|
||||
String(opt.value) ===
|
||||
String(singleForm.warehouseId ?? ''),
|
||||
'is-empty': !warehouseHasAddress(opt),
|
||||
}"
|
||||
>
|
||||
<div class="warehouse-ship-popover__item-head">
|
||||
<strong>
|
||||
寄往仓库
|
||||
<template v-if="selectedWarehouseOption?.label">
|
||||
· {{ selectedWarehouseOption.label }}
|
||||
</template>
|
||||
{{ opt.label || opt.value }}
|
||||
<span
|
||||
v-if="
|
||||
String(opt.value) ===
|
||||
String(singleForm.warehouseId ?? '')
|
||||
"
|
||||
class="warehouse-ship-popover__badge"
|
||||
>
|
||||
当前
|
||||
</span>
|
||||
</strong>
|
||||
<a-button
|
||||
v-if="selectedWarehouseHasAddress"
|
||||
v-if="warehouseHasAddress(opt)"
|
||||
type="text"
|
||||
size="mini"
|
||||
class="warehouse-ship-popover__copy"
|
||||
@click="copyWarehouseShipAddress"
|
||||
@click="copyWarehouseShipAddress(opt)"
|
||||
>
|
||||
<template #icon><IconCopy /></template>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<p v-if="!selectedWarehouseOption" class="warehouse-ship-popover__empty">
|
||||
请先选择上方仓库
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!selectedWarehouseHasAddress"
|
||||
v-if="!warehouseHasAddress(opt)"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
该仓库暂未配置收货地址,请联系客服或更换仓库
|
||||
暂未配置收货地址
|
||||
</p>
|
||||
<template v-else>
|
||||
<dl class="warehouse-ship-popover__rows">
|
||||
<dl v-else class="warehouse-ship-popover__rows">
|
||||
<div>
|
||||
<dt>收件人</dt>
|
||||
<dd>{{ selectedWarehouseOption.recipient || '-' }}</dd>
|
||||
<dd>{{ displayWarehouseRecipient(opt) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>电话</dt>
|
||||
<dd>{{ selectedWarehouseOption.contactPhone || '-' }}</dd>
|
||||
<dd>{{ opt.contactPhone || '-' }}</dd>
|
||||
</div>
|
||||
<div class="is-addr">
|
||||
<dt>地址</dt>
|
||||
<dd>{{ selectedWarehouseOption.shippingAddress || '-' }}</dd>
|
||||
<dd>{{ displayWarehouseShippingAddress(opt) }}</dd>
|
||||
</div>
|
||||
<div v-if="opt.remark">
|
||||
<dt>备注</dt>
|
||||
<dd>{{ opt.remark }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<p class="warehouse-ship-popover__hint">
|
||||
按此地址自行寄出。快递面单建议备注客户单号,方便仓库认领。
|
||||
按所选仓库地址自行寄出。快递面单建议备注客户单号,方便仓库认领。
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
@ -3993,6 +4081,27 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="singleForm.inboundMethod === 'doorPickup'">
|
||||
<a-form-item
|
||||
label="仓库地址"
|
||||
required
|
||||
:validate-status="fieldValidateStatus('warehouseId')"
|
||||
:help="fieldHelp('warehouseId')"
|
||||
>
|
||||
<a-select
|
||||
v-model="singleForm.warehouseId"
|
||||
allow-search
|
||||
placeholder="请选择仓库"
|
||||
:loading="singleReferenceLoading"
|
||||
>
|
||||
<a-option
|
||||
v-for="opt in singleWarehouseOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="揽收地址" required>
|
||||
<a-select
|
||||
v-model="singleForm.senderAddressId"
|
||||
@ -7210,7 +7319,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
<style lang="less">
|
||||
.warehouse-ship-popover {
|
||||
max-width: 360px;
|
||||
max-width: 400px;
|
||||
padding: 0;
|
||||
|
||||
.arco-popover-content {
|
||||
@ -7219,8 +7328,8 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__body {
|
||||
min-width: 240px;
|
||||
max-width: 320px;
|
||||
min-width: 260px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__head {
|
||||
@ -7244,8 +7353,62 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__list {
|
||||
margin-top: 8px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 4px;
|
||||
background: var(--color-fill-1);
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
|
||||
}
|
||||
|
||||
&.is-empty {
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 14%, #fff);
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__rows {
|
||||
margin: 8px 0 0;
|
||||
margin: 6px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@ -7280,4 +7443,8 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
line-height: 18px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item .warehouse-ship-popover__empty {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
import type { OrderListColumnDef } from '../myOrders/columnConfig';
|
||||
import { MY_ORDERS_COLUMN_CATALOG } from '../myOrders/columnConfig';
|
||||
import { getOrderColumnLabelZh } from '../orderColumnLabelsZh';
|
||||
|
||||
export const WAYBILL_MANAGE_COLUMN_STORAGE_KEY = 'oms.order.waybillManage.columnSchemes';
|
||||
|
||||
@ -40,10 +41,23 @@ export const WAYBILL_MANAGE_COLUMN_CATALOG: OrderListColumnDef[] = [
|
||||
{ key: 'productName', labelKey: M('shippingMethod'), width: 120 },
|
||||
{ key: 'channelCode', labelKey: L('channelCode'), width: 110 },
|
||||
{ key: 'channelName', labelKey: M('serviceChannel'), width: 120 },
|
||||
{ key: 'downstreamSupplierName', labelKey: L('downstreamSupplierName'), width: 140 },
|
||||
{ key: 'downstreamSupplierCode', labelKey: L('downstreamSupplierCode'), width: 140 },
|
||||
{ key: 'downstreamAppKey', labelKey: L('downstreamAppKey'), width: 140 },
|
||||
{ key: 'downstreamAppId', labelKey: L('downstreamAppId'), width: 110 },
|
||||
{ key: 'channelForecastCode', labelKey: L('channelForecastCode'), width: 130 },
|
||||
{ key: 'maxTimeliness', labelKey: L('maxTimeliness'), width: 100 },
|
||||
{ key: 'salesProductRulesVersion', labelKey: L('salesProductRulesVersion'), width: 150 },
|
||||
{ key: 'salesProductPriceVersion', labelKey: L('salesProductPriceVersion'), width: 150 },
|
||||
{ key: 'forecastReceivableAmount', labelKey: L('forecastReceivableAmount'), width: 120 },
|
||||
{ key: 'forecastPayableAmount', labelKey: L('forecastPayableAmount'), width: 120 },
|
||||
{ key: 'hasInsurance', labelKey: L('hasInsurance'), width: 90 },
|
||||
{ key: 'hasAllocationTrace', labelKey: L('hasAllocationTrace'), width: 110 },
|
||||
{ key: 'allocationScheme', labelKey: L('allocationScheme'), width: 120 },
|
||||
{ key: 'exportQualificationId', labelKey: L('exportQualificationId'), width: 120 },
|
||||
{ key: 'pickupOrderId', labelKey: L('pickupOrderId'), width: 110 },
|
||||
{ key: 'stockBindLineCount', labelKey: L('stockBindLineCount'), width: 100 },
|
||||
{ key: 'primarySkuCodes', labelKey: L('primarySkuCodes'), width: 140 },
|
||||
{ key: 'consigneeCountry', labelKey: M('consigneeCountry'), width: 110 },
|
||||
{ key: 'consigneeCountryName', labelKey: L('consigneeCountryName'), width: 120 },
|
||||
{ key: 'cargoAttribute', labelKey: M('cargoAttribute'), width: 100 },
|
||||
@ -182,7 +196,16 @@ export function dedupeCatalog(list: OrderListColumnDef[]): OrderListColumnDef[]
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/** 基础目录 = 运单全量 + 我的订单目录中可能遗漏的项 */
|
||||
export function getWaybillBaseCatalog(): OrderListColumnDef[] {
|
||||
return dedupeCatalog([...WAYBILL_MANAGE_COLUMN_CATALOG, ...MY_ORDERS_COLUMN_CATALOG]);
|
||||
function withZhLabels(list: OrderListColumnDef[]): OrderListColumnDef[] {
|
||||
return list.map((col) => ({
|
||||
...col,
|
||||
label: col.label && col.label !== col.key ? col.label : getOrderColumnLabelZh(col.key),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 基础目录 = 运单全量 + 我的订单目录中可能遗漏的项(带中文 label) */
|
||||
export function getWaybillBaseCatalog(): OrderListColumnDef[] {
|
||||
return withZhLabels(
|
||||
dedupeCatalog([...WAYBILL_MANAGE_COLUMN_CATALOG, ...MY_ORDERS_COLUMN_CATALOG]),
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import {
|
||||
getSalesProductPublishPriceList,
|
||||
getSalesProductPublishRulesList,
|
||||
} from '@/api/channel/salesProduct'
|
||||
import { postOrderBindSnapshotVersions } from '@/api/order'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
orderId: number
|
||||
salesProductId: number | string | null
|
||||
currentRulesVersion?: string | null
|
||||
currentPriceVersion?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
success: [order: any]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const [registerModal] = useModal()
|
||||
const submitting = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const formState = reactive({
|
||||
rulesVersion: undefined as string | undefined,
|
||||
priceVersion: undefined as string | undefined,
|
||||
recalculateReceivable: true,
|
||||
recalculatePayable: true,
|
||||
})
|
||||
|
||||
const rulesOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const priceOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
const title = computed(() => t('order.myOrders.drawer.bindSnapshot.title'))
|
||||
|
||||
function mapVersionOptions(items: any[], current?: string | null) {
|
||||
const rows = (items || []).map((row) => {
|
||||
const version = String(row.version ?? row.Version ?? '').trim()
|
||||
const publishTime = row.publishTime ?? row.PublishTime ?? ''
|
||||
const desc = String(row.description ?? row.Description ?? '').trim()
|
||||
const parts = [version]
|
||||
if (publishTime) parts.push(String(publishTime))
|
||||
if (desc) parts.push(desc)
|
||||
const isCurrent = current && version === current
|
||||
return {
|
||||
value: version,
|
||||
label: isCurrent
|
||||
? `${parts.join(' · ')}(${t('order.myOrders.drawer.bindSnapshot.currentBound')})`
|
||||
: parts.join(' · '),
|
||||
}
|
||||
}).filter((x) => x.value)
|
||||
return [
|
||||
{
|
||||
value: 'CURRENT',
|
||||
label: t('order.myOrders.drawer.bindSnapshot.useCurrent'),
|
||||
},
|
||||
...rows,
|
||||
]
|
||||
}
|
||||
|
||||
async function loadVersionOptions() {
|
||||
rulesOptions.value = [
|
||||
{ value: 'CURRENT', label: t('order.myOrders.drawer.bindSnapshot.useCurrent') },
|
||||
]
|
||||
priceOptions.value = [
|
||||
{ value: 'CURRENT', label: t('order.myOrders.drawer.bindSnapshot.useCurrent') },
|
||||
]
|
||||
if (props.salesProductId == null || props.salesProductId === '') return
|
||||
loading.value = true
|
||||
try {
|
||||
const [rules, price] = await Promise.all([
|
||||
getSalesProductPublishRulesList({
|
||||
salesProductId: props.salesProductId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
includeContent: false,
|
||||
}),
|
||||
getSalesProductPublishPriceList({
|
||||
salesProductId: props.salesProductId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
includeContent: false,
|
||||
}),
|
||||
])
|
||||
rulesOptions.value = mapVersionOptions(rules.items, props.currentRulesVersion)
|
||||
priceOptions.value = mapVersionOptions(price.items, props.currentPriceVersion)
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.myOrders.drawer.bindSnapshot.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formState.rulesVersion = undefined
|
||||
formState.priceVersion = undefined
|
||||
formState.recalculateReceivable = true
|
||||
formState.recalculatePayable = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (v) => {
|
||||
if (!v) return
|
||||
resetForm()
|
||||
await loadVersionOptions()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function onUpdateVisible(val: boolean) {
|
||||
if (val === props.visible) return
|
||||
emit('update:visible', val)
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
if (!formState.rulesVersion && !formState.priceVersion) {
|
||||
Message.warning(t('order.myOrders.drawer.bindSnapshot.needVersion'))
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const result = await postOrderBindSnapshotVersions({
|
||||
id: props.orderId,
|
||||
rulesVersion: formState.rulesVersion,
|
||||
priceVersion: formState.priceVersion,
|
||||
recalculateReceivable: formState.recalculateReceivable,
|
||||
recalculatePayable: formState.recalculatePayable,
|
||||
})
|
||||
Message.success(t('order.myOrders.drawer.bindSnapshot.success'))
|
||||
emit('success', result)
|
||||
emit('update:visible', false)
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.myOrders.drawer.bindSnapshot.failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalPro
|
||||
:visible="visible"
|
||||
:title="title"
|
||||
:width="520"
|
||||
:mask-closable="false"
|
||||
:confirm-loading="submitting"
|
||||
@register="registerModal"
|
||||
@update:visible="onUpdateVisible"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<p class="bind-snapshot-hint">{{ t('order.myOrders.drawer.bindSnapshot.hint') }}</p>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<div class="bind-snapshot-modal">
|
||||
<div class="bind-snapshot-modal__field">
|
||||
<div class="bind-snapshot-modal__label">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.rulesVersion') }}
|
||||
</div>
|
||||
<div class="bind-snapshot-modal__current">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
|
||||
version: currentRulesVersion || '--',
|
||||
}) }}
|
||||
</div>
|
||||
<a-select
|
||||
v-model="formState.rulesVersion"
|
||||
allow-clear
|
||||
allow-search
|
||||
:options="rulesOptions"
|
||||
:placeholder="t('order.myOrders.drawer.bindSnapshot.keepUnchanged')"
|
||||
/>
|
||||
</div>
|
||||
<div class="bind-snapshot-modal__field">
|
||||
<div class="bind-snapshot-modal__label">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.priceVersion') }}
|
||||
</div>
|
||||
<div class="bind-snapshot-modal__current">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.boundAs', {
|
||||
version: currentPriceVersion || '--',
|
||||
}) }}
|
||||
</div>
|
||||
<a-select
|
||||
v-model="formState.priceVersion"
|
||||
allow-clear
|
||||
allow-search
|
||||
:options="priceOptions"
|
||||
:placeholder="t('order.myOrders.drawer.bindSnapshot.keepUnchanged')"
|
||||
/>
|
||||
</div>
|
||||
<div class="bind-snapshot-modal__checks">
|
||||
<a-checkbox v-model="formState.recalculateReceivable">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.recalcReceivable') }}
|
||||
</a-checkbox>
|
||||
<a-checkbox v-model="formState.recalculatePayable">
|
||||
{{ t('order.myOrders.drawer.bindSnapshot.recalcPayable') }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ModalPro>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.bind-snapshot-hint {
|
||||
margin: 0 0 12px;
|
||||
color: var(--color-text-3);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bind-snapshot-modal__field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bind-snapshot-modal__label {
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.bind-snapshot-modal__current {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.bind-snapshot-modal__checks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind'"
|
||||
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||
class="drawer-tab-toolbar"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
@ -48,12 +48,14 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'fee'">
|
||||
<a-button
|
||||
v-if="canEditAdditionalServices"
|
||||
class="app-btn-search"
|
||||
:loading="additionalServicesLoading"
|
||||
@click="openAdditionalServicesModal"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.additionalServices') }}
|
||||
<template v-if="orderAdditionalServices.length">
|
||||
({{ orderAdditionalServices.length }})
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="openAddFeeModal">
|
||||
{{ $t('order.myOrders.drawer.actions.addFee') }}
|
||||
@ -72,6 +74,9 @@
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.recalculatePayable') }}
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="openBindSnapshotVersionsModal">
|
||||
{{ $t('order.myOrders.drawer.actions.bindSnapshotVersions') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'track'">
|
||||
<span class="toolbar-meta">
|
||||
@ -103,6 +108,17 @@
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'operationLog'">
|
||||
<a-tag v-if="operationLogList.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="operationLogLoading"
|
||||
@click="loadOperationLogs"
|
||||
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -483,6 +499,44 @@
|
||||
|
||||
<!-- 应收应付 -->
|
||||
<div v-if="activeTab === 'fee'" class="tab-panel fee-info">
|
||||
<div v-if="boundRulesVersion || boundPriceVersion" class="fee-snapshot-versions">
|
||||
<span class="fee-snapshot-versions__item">
|
||||
{{ $t('order.myOrders.drawer.fee.rulesVersion') }}:{{ boundRulesVersion || '--' }}
|
||||
</span>
|
||||
<span class="fee-snapshot-versions__item">
|
||||
{{ $t('order.myOrders.drawer.fee.priceVersion') }}:{{ boundPriceVersion || '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="fee-additional-services">
|
||||
<span class="fee-additional-services__label">
|
||||
{{ $t('order.myOrders.drawer.additionalServices') }}:
|
||||
</span>
|
||||
<a-space v-if="orderAdditionalServices.length" wrap :size="4">
|
||||
<a-tag
|
||||
v-for="code in orderAdditionalServices"
|
||||
:key="code"
|
||||
size="small"
|
||||
color="arcoblue"
|
||||
>
|
||||
{{ labelAdditionalService(code) }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
<span v-else class="fee-additional-services__empty">
|
||||
{{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
|
||||
</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="mini"
|
||||
class="fee-additional-services__edit"
|
||||
@click="openAdditionalServicesModal"
|
||||
>
|
||||
{{
|
||||
canEditAdditionalServices
|
||||
? $t('order.myOrders.drawer.fee.additionalServicesManage')
|
||||
: $t('order.myOrders.drawer.fee.additionalServicesView')
|
||||
}}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="fee-summary">
|
||||
<button
|
||||
type="button"
|
||||
@ -747,6 +801,63 @@
|
||||
@changed="handleIssueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div v-if="activeTab === 'operationLog'" class="tab-panel operation-log-info">
|
||||
<a-spin :loading="operationLogLoading" class="operation-log-spin">
|
||||
<div v-if="operationLogList.length" class="operation-log-list">
|
||||
<div
|
||||
v-for="item in operationLogList"
|
||||
:key="item.id"
|
||||
class="operation-log-item"
|
||||
>
|
||||
<div class="operation-log-head" @click="toggleOperationLogExpand(item.id)">
|
||||
<div class="operation-log-head-main">
|
||||
<span class="operation-log-action">{{ item.actionName || item.action }}</span>
|
||||
<a-tag v-if="item.changes?.length" size="small" color="orangered">
|
||||
{{ item.changes.length }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="operation-log-head-meta">
|
||||
<span>{{ item.operatorName || $t('order.myOrders.drawer.operationLog.operator') }}</span>
|
||||
<span>{{ formatOperationLogTime(item.createTime) }}</span>
|
||||
<IconDown v-if="!isOperationLogExpanded(item.id)" />
|
||||
<IconUp v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isOperationLogExpanded(item.id)" class="operation-log-body">
|
||||
<p v-if="item.remark" class="operation-log-remark">{{ item.remark }}</p>
|
||||
<div v-if="item.changes?.length" class="operation-log-changes">
|
||||
<div class="operation-log-changes-header">
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.field') }}</span>
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.before') }}</span>
|
||||
<span>{{ $t('order.myOrders.drawer.operationLog.after') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(change, idx) in item.changes"
|
||||
:key="`${item.id}-${change.path}-${idx}`"
|
||||
class="operation-log-changes-row"
|
||||
>
|
||||
<span>{{ change.label || change.path }}</span>
|
||||
<span>{{ change.before ?? '--' }}</span>
|
||||
<span class="operation-log-after">{{ change.after ?? '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty
|
||||
v-else
|
||||
:description="$t('order.myOrders.drawer.operationLog.noChanges')"
|
||||
:image-style="{ height: '40px' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty
|
||||
v-else-if="!operationLogLoading"
|
||||
:description="$t('order.myOrders.drawer.operationLog.empty')"
|
||||
:image-style="{ height: '60px' }"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
@ -758,6 +869,16 @@
|
||||
@success="handleAddFeeSuccess"
|
||||
/>
|
||||
|
||||
<BindSnapshotVersionsModal
|
||||
v-if="bindSnapshotVersionsVisible && orderId != null"
|
||||
v-model:visible="bindSnapshotVersionsVisible"
|
||||
:order-id="Number(orderId)"
|
||||
:sales-product-id="bindSnapshotSalesProductId"
|
||||
:current-rules-version="boundRulesVersion"
|
||||
:current-price-version="boundPriceVersion"
|
||||
@success="handleBindSnapshotVersionsSuccess"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="declarationSkuPreviewVisible"
|
||||
:title="$t('order.myOrders.drawer.declarationSkuImage')"
|
||||
@ -782,15 +903,75 @@
|
||||
|
||||
<a-modal
|
||||
v-model:visible="additionalServicesModalVisible"
|
||||
:title="$t('order.myOrders.drawer.fee.additionalServicesTitle')"
|
||||
:title="
|
||||
canEditAdditionalServices
|
||||
? $t('order.myOrders.drawer.fee.additionalServicesManageTitle')
|
||||
: $t('order.myOrders.drawer.fee.additionalServicesViewTitle')
|
||||
"
|
||||
:ok-loading="additionalServicesSaving"
|
||||
:ok-text="
|
||||
canEditAdditionalServices
|
||||
? $t('order.myOrders.drawer.fee.additionalServicesSave')
|
||||
: $t('order.myOrders.drawer.fee.additionalServicesClose')
|
||||
"
|
||||
:hide-cancel="!canEditAdditionalServices"
|
||||
unmount-on-close
|
||||
@ok="handleAdditionalServicesSubmit"
|
||||
>
|
||||
<a-spin :loading="additionalServicesLoading">
|
||||
<p class="additional-services-hint">
|
||||
{{
|
||||
canEditAdditionalServices
|
||||
? $t('order.myOrders.drawer.fee.additionalServicesHint')
|
||||
: $t('order.myOrders.drawer.fee.additionalServicesPaidHint')
|
||||
}}
|
||||
</p>
|
||||
<div class="additional-services-current">
|
||||
<div class="additional-services-current__label">
|
||||
{{ $t('order.myOrders.drawer.fee.additionalServicesCurrent') }}
|
||||
</div>
|
||||
<a-space v-if="orderAdditionalServices.length" wrap :size="4">
|
||||
<a-tag
|
||||
v-for="code in orderAdditionalServices"
|
||||
:key="`cur-${code}`"
|
||||
size="small"
|
||||
color="arcoblue"
|
||||
>
|
||||
{{ labelAdditionalService(code) }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
<span v-else class="additional-services-current__empty">
|
||||
{{ $t('order.myOrders.drawer.fee.additionalServicesNone') }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="canEditAdditionalServices">
|
||||
<div
|
||||
v-if="visibleOrphanAdditionalServices.length"
|
||||
class="additional-services-orphan"
|
||||
>
|
||||
<div class="additional-services-orphan__label">
|
||||
{{ $t('order.myOrders.drawer.fee.additionalServicesOrphan') }}
|
||||
</div>
|
||||
<a-space wrap :size="4">
|
||||
<a-tag
|
||||
v-for="code in visibleOrphanAdditionalServices"
|
||||
:key="`orphan-${code}`"
|
||||
size="small"
|
||||
color="orangered"
|
||||
closable
|
||||
@close="dismissOrphanAdditionalService(code)"
|
||||
>
|
||||
{{ labelAdditionalService(code) }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="additional-services-options-label">
|
||||
{{ $t('order.myOrders.drawer.fee.additionalServicesPick') }}
|
||||
</div>
|
||||
<a-checkbox-group
|
||||
v-if="productAdditionalServiceOptions.length"
|
||||
v-model="selectedAdditionalServices"
|
||||
class="additional-services-checkbox-group"
|
||||
>
|
||||
<a-checkbox
|
||||
v-for="opt in productAdditionalServiceOptions"
|
||||
@ -804,6 +985,7 @@
|
||||
v-else
|
||||
:description="$t('order.myOrders.drawer.fee.additionalServicesEmpty')"
|
||||
/>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
@ -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<number | string | null>(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<OrderOperationLogItem[]>([]);
|
||||
const operationLogLoading = ref(false);
|
||||
const operationLogExpandedIds = ref<Set<number>>(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<string[]>([]);
|
||||
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -18,6 +18,13 @@
|
||||
<icon-home />
|
||||
</span>
|
||||
<span class="warehouse-card-title-text" :title="item.warehouseName">{{ item.warehouseName }}</span>
|
||||
<a-tag
|
||||
v-if="item.sort != null && item.sort !== ''"
|
||||
size="small"
|
||||
class="warehouse-card-sort-tag"
|
||||
>
|
||||
{{ $t('system.warehouseAddress.fields.sort') }} {{ item.sort }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
@ -58,6 +65,10 @@
|
||||
<span class="label">{{ $t('system.warehouseAddress.fields.shippingAddress') }}:</span>
|
||||
<span class="value value-address" :title="displayShippingAddress(item)">{{ displayShippingAddress(item) }}</span>
|
||||
</div>
|
||||
<div v-if="item.remark" class="card-row card-row--remark">
|
||||
<span class="label">{{ $t('system.warehouseAddress.fields.remark') }}:</span>
|
||||
<span class="value" :title="item.remark">{{ item.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
@ -105,6 +116,18 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$t('system.warehouseAddress.fields.sort')">
|
||||
<a-input-number
|
||||
v-model="formData.sort"
|
||||
:placeholder="$t('system.warehouseAddress.placeholder.sort')"
|
||||
:precision="0"
|
||||
allow-clear
|
||||
hide-button
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="form-section-title">{{ $t('system.warehouseAddress.section.contact') }}</div>
|
||||
@ -134,6 +157,17 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :label="$t('system.warehouseAddress.fields.remark')">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
:placeholder="$t('system.warehouseAddress.placeholder.remark')"
|
||||
:max-length="512"
|
||||
show-word-limit
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@ -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<string | number | null>(null);
|
||||
const warehouseTypeOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user