fix: 订单详情编辑体验、接口参数与价格表批量填价
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2612a225ad
commit
b9702cd0bc
@ -1200,6 +1200,8 @@ export interface OrderOperationLogItem {
|
||||
source?: string
|
||||
changes: OrderOperationLogChangeItem[]
|
||||
remark?: string
|
||||
/** 列表「内容」列:备注 + 字段变更摘要 */
|
||||
summary?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
@ -1217,6 +1219,12 @@ function mapOperationLogItem(raw: any): OrderOperationLogItem {
|
||||
after: c?.after ?? c?.After ?? null,
|
||||
}))
|
||||
: []
|
||||
const remark = raw?.remark ?? raw?.Remark
|
||||
const summaryRaw = raw?.summary ?? raw?.Summary
|
||||
const summary =
|
||||
summaryRaw != null && String(summaryRaw).trim()
|
||||
? String(summaryRaw).trim()
|
||||
: buildOperationLogSummaryFallback(changes, remark)
|
||||
return {
|
||||
id: Number(raw?.id ?? raw?.Id ?? 0),
|
||||
orderId: Number(raw?.orderId ?? raw?.OrderId ?? 0),
|
||||
@ -1228,11 +1236,33 @@ function mapOperationLogItem(raw: any): OrderOperationLogItem {
|
||||
operatorType: raw?.operatorType ?? raw?.OperatorType,
|
||||
source: raw?.source ?? raw?.Source,
|
||||
changes,
|
||||
remark: raw?.remark ?? raw?.Remark,
|
||||
remark,
|
||||
summary,
|
||||
createTime: raw?.createTime ?? raw?.CreateTime,
|
||||
}
|
||||
}
|
||||
|
||||
function buildOperationLogSummaryFallback(
|
||||
changes: OrderOperationLogChangeItem[],
|
||||
remark?: string | null,
|
||||
): string {
|
||||
const parts: string[] = []
|
||||
if (remark != null && String(remark).trim()) {
|
||||
parts.push(String(remark).trim())
|
||||
}
|
||||
const maxItems = 5
|
||||
for (const change of changes.slice(0, maxItems)) {
|
||||
const label = change.label || change.path || '字段'
|
||||
const before = change.before?.trim() ? change.before : '空'
|
||||
const after = change.after?.trim() ? change.after : '空'
|
||||
parts.push(`${label}:${before}→${after}`)
|
||||
}
|
||||
if (changes.length > maxItems) {
|
||||
parts.push(`等${changes.length}项变更`)
|
||||
}
|
||||
return parts.length ? parts.join(';') : '—'
|
||||
}
|
||||
|
||||
/** GET /order/operation-log/list */
|
||||
export async function getOrderOperationLogList(
|
||||
orderId: number | string,
|
||||
|
||||
@ -287,6 +287,8 @@
|
||||
|
||||
const formDirtyBaseline = ref('');
|
||||
const exampleDirtyBaseline = ref({ request: '', response: '' });
|
||||
/** 参数表内容快照:比 VXE recordset / sticky manualDirty 更抗误报 */
|
||||
const tablesDirtyBaseline = ref('');
|
||||
const manualDirty = ref(false);
|
||||
const isDirty = ref(false);
|
||||
/** 接口明细加载/回填中:不算未保存,避免误报 */
|
||||
@ -296,23 +298,88 @@
|
||||
|
||||
function pickFormDirtySnapshot() {
|
||||
const d: any = props.basicFormData || {};
|
||||
// 字典/SelectPro 常把 number 回写成 string,快照统一成字符串避免误报脏
|
||||
const norm = (v: unknown) => (v == null || v === '' ? '' : String(v));
|
||||
return JSON.stringify({
|
||||
name: d.name,
|
||||
abbreviation: d.abbreviation,
|
||||
code: d.code,
|
||||
description: d.description,
|
||||
category: d.category,
|
||||
requestMethod: d.requestMethod,
|
||||
requestAddress: d.requestAddress,
|
||||
requestProtocol: d.requestProtocol,
|
||||
contentType: d.contentType,
|
||||
responseContentType: d.responseContentType,
|
||||
mappedParamType: d.mappedParamType,
|
||||
qps: d.qps,
|
||||
isBatch: d.isBatch,
|
||||
name: norm(d.name),
|
||||
abbreviation: norm(d.abbreviation),
|
||||
code: norm(d.code),
|
||||
description: norm(d.description),
|
||||
category: norm(d.category),
|
||||
requestMethod: norm(d.requestMethod),
|
||||
requestAddress: norm(d.requestAddress),
|
||||
requestProtocol: norm(d.requestProtocol),
|
||||
contentType: norm(d.contentType),
|
||||
responseContentType: norm(d.responseContentType),
|
||||
mappedParamType: norm(d.mappedParamType),
|
||||
qps: norm(d.qps),
|
||||
isBatch: !!d.isBatch,
|
||||
});
|
||||
}
|
||||
|
||||
function stabilizeDirtyRows(rows: any[]): any[] {
|
||||
return flattenTreeTableRows(Array.isArray(rows) ? rows : [])
|
||||
.filter((r) => r && !r.__isBodyRoot && r.id !== '__oms_body_root__')
|
||||
.map((r) => ({
|
||||
id: r.id ?? r.Id ?? null,
|
||||
pId: r.pId ?? r.PId ?? 0,
|
||||
name: String(r.name ?? r.Name ?? ''),
|
||||
type: r.type ?? r.Type ?? null,
|
||||
isRequired: !!(r.isRequired ?? r.IsRequired),
|
||||
description: String(r.description ?? r.Description ?? ''),
|
||||
cnName: String(r.cnName ?? r.CnName ?? ''),
|
||||
alias: String(r.alias ?? r.Alias ?? ''),
|
||||
value: String(r.value ?? r.Value ?? ''),
|
||||
}))
|
||||
.sort((a, b) => String(a.id).localeCompare(String(b.id), 'en'));
|
||||
}
|
||||
|
||||
function pickTablesDirtySnapshot() {
|
||||
try {
|
||||
const mappingFromGrid =
|
||||
mappingBodyTableRef.value?.getTableData?.()?.fullData
|
||||
|| mappingBodyTableRef.value?.xGrid?.getTableData?.()?.fullData;
|
||||
const responseFromGrid =
|
||||
responseTableRef.value?.getTableData?.()?.fullData
|
||||
|| responseTableRef.value?.xGrid?.getTableData?.()?.fullData;
|
||||
return JSON.stringify({
|
||||
query: stabilizeDirtyRows(
|
||||
stripCommonRows(
|
||||
queryTableRef.value?.getTableData?.()?.fullData
|
||||
|| queryTableRef.value?.xGrid?.getTableData?.()?.fullData
|
||||
|| queryTableProps.data
|
||||
|| [],
|
||||
),
|
||||
),
|
||||
header: stabilizeDirtyRows(
|
||||
stripCommonRows(
|
||||
headerTableRef.value?.getTableData?.()?.fullData
|
||||
|| headerTableRef.value?.xGrid?.getTableData?.()?.fullData
|
||||
|| headerTableProps.data
|
||||
|| [],
|
||||
),
|
||||
),
|
||||
bodyForm: stabilizeDirtyRows(
|
||||
stripCommonRows(
|
||||
bodyFormTableRef.value?.getTableData?.()?.fullData
|
||||
|| bodyFormTableRef.value?.xGrid?.getTableData?.()?.fullData
|
||||
|| bodyFormTableProps.data
|
||||
|| [],
|
||||
),
|
||||
),
|
||||
request: stabilizeDirtyRows(getRequestBodyFlatParams()),
|
||||
mappingBody: stabilizeDirtyRows(
|
||||
getTreeTableFlatRows(mappingFromGrid, mappingBodyTableProps.data),
|
||||
),
|
||||
response: stabilizeDirtyRows(
|
||||
getTreeTableFlatRows(responseFromGrid, responseTableProps.data),
|
||||
),
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function actionRowsDirty(getAction?: () => any) {
|
||||
const a = getAction?.() || {};
|
||||
return (
|
||||
@ -325,31 +392,36 @@
|
||||
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(() => filterCommonFromActionRows(queryTableRef.value?.getActionRows?.()))) {
|
||||
return true;
|
||||
}
|
||||
if (actionRowsDirty(() => filterCommonFromActionRows(headerTableRef.value?.getActionRows?.()))) {
|
||||
return true;
|
||||
}
|
||||
if (actionRowsDirty(() => filterCommonFromActionRows(bodyFormTableRef.value?.getActionRows?.()))) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
actionRowsDirty(() =>
|
||||
// 以参数表内容快照为准(切 tab / SelectPro 回填导致的假 VXE insert、sticky markDirty 不再误报)
|
||||
if (tablesDirtyBaseline.value) {
|
||||
const currentTables = pickTablesDirtySnapshot();
|
||||
if (currentTables && currentTables !== tablesDirtyBaseline.value) return true;
|
||||
} else if (
|
||||
// 基线尚未采到表快照时,回退 VXE recordset
|
||||
actionRowsDirty(() => filterCommonFromActionRows(queryTableRef.value?.getActionRows?.()))
|
||||
|| actionRowsDirty(() => filterCommonFromActionRows(headerTableRef.value?.getActionRows?.()))
|
||||
|| actionRowsDirty(() => filterCommonFromActionRows(bodyFormTableRef.value?.getActionRows?.()))
|
||||
|| actionRowsDirty(() =>
|
||||
stripBodyRootFromActionRows(requestTableRef.value?.getActionRows?.() || {}),
|
||||
)
|
||||
|| actionRowsDirty(() => mappingBodyTableRef.value?.getActionRows?.())
|
||||
|| actionRowsDirty(() => responseTableRef.value?.getActionRows?.())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (actionRowsDirty(() => mappingBodyTableRef.value?.getActionRows?.())) return true;
|
||||
if (actionRowsDirty(() => responseTableRef.value?.getActionRows?.())) return true;
|
||||
// sticky manualDirty:仅在内容快照不可用时作补充;有快照且内容一致则清掉误标
|
||||
if (manualDirty.value) {
|
||||
if (tablesDirtyBaseline.value) {
|
||||
manualDirty.value = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -360,7 +432,8 @@
|
||||
function markDirty() {
|
||||
if (detailHydrating.value) return;
|
||||
manualDirty.value = true;
|
||||
isDirty.value = true;
|
||||
// 立刻按内容快照重算,避免「点一下必填又点回去」仍显示未保存
|
||||
refreshDirtyState();
|
||||
}
|
||||
|
||||
function captureDirtyBaseline() {
|
||||
@ -370,6 +443,7 @@
|
||||
request: requestExample.value || '',
|
||||
response: responseExample.value || '',
|
||||
};
|
||||
tablesDirtyBaseline.value = pickTablesDirtySnapshot();
|
||||
isDirty.value = false;
|
||||
dirtyBaselineReady.value = true;
|
||||
}
|
||||
@ -1164,6 +1238,7 @@
|
||||
if (!row || row.children?.length) return;
|
||||
row.type = typeKey;
|
||||
paramTypePopoverRowId.value = null;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// Query 表格配置(使用下游接口的配置方式)
|
||||
@ -2359,8 +2434,19 @@
|
||||
async function deleteRequestRow(row: any) {
|
||||
if (isBodyRootRow(row)) return;
|
||||
markDirty();
|
||||
const ref = requestTab.value === 'mappingBody' ? mappingBodyTableRef : requestTableRef;
|
||||
const isMapping = requestTab.value === 'mappingBody';
|
||||
const ref = isMapping ? mappingBodyTableRef : requestTableRef;
|
||||
await ref.value?.removeRow(row);
|
||||
const fromGrid =
|
||||
ref.value?.getTableData?.()?.fullData
|
||||
|| ref.value?.xGrid?.getTableData?.()?.fullData
|
||||
|| [];
|
||||
if (isMapping) {
|
||||
mappingBodyTableProps.data = Array.isArray(fromGrid) ? [...fromGrid] : [];
|
||||
} else {
|
||||
const next = Array.isArray(fromGrid) ? [...fromGrid] : [];
|
||||
requestTableProps.data = next.length ? next : ensureBodyRootInRows([]);
|
||||
}
|
||||
refreshTabCounts();
|
||||
}
|
||||
|
||||
@ -2523,6 +2609,12 @@
|
||||
async function deleteResponseRow(row: any) {
|
||||
markDirty();
|
||||
await responseTableRef.value?.removeRow(row);
|
||||
// 与表格同步,避免 props 残留旧树被校验/保存误读
|
||||
const fromGrid =
|
||||
responseTableRef.value?.getTableData?.()?.fullData
|
||||
|| responseTableRef.value?.xGrid?.getTableData?.()?.fullData
|
||||
|| [];
|
||||
responseTableProps.data = Array.isArray(fromGrid) ? [...fromGrid] : [];
|
||||
refreshTabCounts();
|
||||
}
|
||||
|
||||
@ -2807,15 +2899,19 @@
|
||||
await writeTextToClipboard(content);
|
||||
}
|
||||
|
||||
/** 复制当前 Body 参数列表为带 // 描述的格式化 JSON */
|
||||
/** 复制当前 Body / Mapping Body 参数为带 // 描述的格式化 JSON */
|
||||
async function copyBodyParamsAsCommentedJson() {
|
||||
if (isNoneBodyContentType.value) {
|
||||
let content = '';
|
||||
if (requestTab.value === 'mappingBody') {
|
||||
const fromGrid =
|
||||
mappingBodyTableRef.value?.getTableData?.()?.fullData
|
||||
|| mappingBodyTableRef.value?.xGrid?.getTableData()?.fullData;
|
||||
const rows = getTreeTableFlatRows(fromGrid, mappingBodyTableProps.data);
|
||||
content = buildCommentedJsonFromParameterFlatList(rows);
|
||||
} else if (isNoneBodyContentType.value) {
|
||||
Message.warning('当前请求类型为 none,无 Body 参数可复制');
|
||||
return;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
if (isBodyKvMode.value) {
|
||||
} else if (isBodyKvMode.value) {
|
||||
const rows = getTableFullData(bodyFormTableRef, bodyFormTableProps.data);
|
||||
content = buildCommentedJsonFromKvRows(rows);
|
||||
} else {
|
||||
@ -3106,11 +3202,8 @@
|
||||
});
|
||||
|
||||
await postParameterValidateParameter(arr);
|
||||
|
||||
const sortedData = [...dataList1.value].sort((a: any, b: any) => a.id - b.id);
|
||||
sortedData.forEach((node: any) => {
|
||||
responseTableRef.value?.addRowsAt(node, -1);
|
||||
});
|
||||
|
||||
await loadFlatListIntoTreeTable(arr, responseTableProps, responseTableRef);
|
||||
refreshTabCounts();
|
||||
|
||||
$baseMessage('导入成功', 'success');
|
||||
@ -3215,13 +3308,16 @@
|
||||
});
|
||||
|
||||
await postParameterValidateParameter(arr);
|
||||
$baseMessage('导入成功', 'success');
|
||||
|
||||
dataList1.value.sort((a: any, b: any) => a.id - b.id);
|
||||
dataList1.value.forEach((node: any) => {
|
||||
responseTableRef.value?.addRowsAt(node, -1);
|
||||
});
|
||||
|
||||
const rows = (dataList1.value || []).map((i: any) => ({
|
||||
...i,
|
||||
cnName: i.cnName ?? '',
|
||||
optionType: 1,
|
||||
alias: i?.alias || i?.name,
|
||||
}));
|
||||
await loadFlatListIntoTreeTable(rows, responseTableProps, responseTableRef);
|
||||
refreshTabCounts();
|
||||
$baseMessage('导入成功', 'success');
|
||||
|
||||
drawerVisible1.value = false;
|
||||
clearResponseImportEditors();
|
||||
@ -3244,8 +3340,9 @@
|
||||
async function loadFlatListIntoTreeTable(
|
||||
flatList: any[],
|
||||
tableProps: { data: any[] },
|
||||
tableRef: { value?: { reloadData?: (rows: any[]) => void; xGrid?: any } },
|
||||
tableRef: { value?: { reloadData?: (rows: any[]) => void; xGrid?: any; addRowsAt?: (row: any, index: number) => any } },
|
||||
) {
|
||||
// 全量覆盖:必须 reloadData 整表替换;禁止 addRowsAt 追加(否则肉眼像全量、保存仍是增量)
|
||||
let rows = flatParameterListToTableRows(sortFlatParameterListParentFirst(flatList));
|
||||
if (tableProps === requestTableProps) {
|
||||
rows = ensureBodyRootInRows(rows);
|
||||
@ -3255,18 +3352,18 @@
|
||||
if (tableRef.value?.reloadData) {
|
||||
await tableRef.value.reloadData(rows);
|
||||
} else {
|
||||
const ordered = sortFlatParameterListParentFirst(
|
||||
tableProps === requestTableProps ? rows : flatList,
|
||||
);
|
||||
for (const node of ordered) {
|
||||
await tableRef.value?.addRowsAt?.(node, -1);
|
||||
}
|
||||
// 无 reloadData 时先清空再写入,避免残留旧行
|
||||
tableProps.data = [];
|
||||
await nextTick();
|
||||
tableProps.data = rows;
|
||||
}
|
||||
await nextTick();
|
||||
const xGrid = tableRef.value?.xGrid;
|
||||
if (xGrid?.setAllTreeExpand) {
|
||||
xGrid.setAllTreeExpand(true);
|
||||
}
|
||||
// reloadData 会清空 VXE recordset,必须手动标脏,否则保存按钮不出现
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/** Json/Xml → Body 树;FormUrlEncoded / None → Mapping Body 树 */
|
||||
@ -3701,11 +3798,27 @@
|
||||
<a-button size="small" @click="saveFixedParams">保存固定配置</a-button>
|
||||
</template>
|
||||
<template v-else-if="requestTab === 'body' && isBodyKvMode">
|
||||
<a-button
|
||||
size="small"
|
||||
class="section-icon-btn"
|
||||
title="复制参数为带 // 描述的 JSON"
|
||||
@click="copyBodyParamsAsCommentedJson"
|
||||
>
|
||||
<IconCopy />
|
||||
</a-button>
|
||||
<a-button size="small" class="section-icon-btn" title="新增" @click="addNewBodyFormParam">
|
||||
<IconPlus />
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="requestTab === 'body' && !isNoneBodyContentType">
|
||||
<a-button
|
||||
size="small"
|
||||
class="section-icon-btn"
|
||||
title="复制参数为带 // 描述的 JSON"
|
||||
@click="copyBodyParamsAsCommentedJson"
|
||||
>
|
||||
<IconCopy />
|
||||
</a-button>
|
||||
<a-button size="small" class="section-icon-btn" title="新增" @click="addNewRequestParam">
|
||||
<IconPlus />
|
||||
</a-button>
|
||||
@ -3714,6 +3827,14 @@
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="requestTab === 'mappingBody'">
|
||||
<a-button
|
||||
size="small"
|
||||
class="section-icon-btn"
|
||||
title="复制参数为带 // 描述的 JSON"
|
||||
@click="copyBodyParamsAsCommentedJson"
|
||||
>
|
||||
<IconCopy />
|
||||
</a-button>
|
||||
<a-button size="small" class="section-icon-btn" title="新增" @click="addNewRequestParam">
|
||||
<IconPlus />
|
||||
</a-button>
|
||||
@ -3787,14 +3908,6 @@
|
||||
aria-label="Request content type"
|
||||
@change="updateContentType"
|
||||
/>
|
||||
<a-button
|
||||
size="mini"
|
||||
class="content-type-copy-btn section-icon-btn"
|
||||
title="复制当前参数为带描述的 JSON"
|
||||
@click="copyBodyParamsAsCommentedJson"
|
||||
>
|
||||
<IconCopy />
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- Query tab 内容 -->
|
||||
<div v-show="requestTab === 'query'" class="query-tab-pane">
|
||||
@ -4785,7 +4898,7 @@
|
||||
>
|
||||
<div class="analysis-content">
|
||||
<div class="analysis-tip">
|
||||
粘贴 JSON 或 XML 内容后导入参数树。JSON 可用「前端解析」(支持 <code>//</code> 备注写入描述)、「仅导入注释」(只更新已有字段描述,不新增),或使用「解析并导入」走后端解析。
|
||||
粘贴 JSON 或 XML 内容后<strong>全量替换</strong>当前参数树(先清空再导入,保存时会删除库中旧节点)。JSON 可用「前端解析」(支持 <code>//</code> 备注写入描述)、「仅导入注释」(只更新已有字段描述,不新增/不删树),或使用「解析并导入」走后端解析。
|
||||
</div>
|
||||
<div class="analysis-tabs">
|
||||
<SlidingTabs v-model="tabs" size="mini" :items="analysisTypeItems" aria-label="Import format" />
|
||||
@ -4878,7 +4991,7 @@
|
||||
>
|
||||
<div class="analysis-content">
|
||||
<div class="analysis-tip">
|
||||
粘贴 JSON 或 XML 响应示例,点击「解析并导入」生成响应参数树。
|
||||
粘贴 JSON 或 XML 响应示例,点击「解析并导入」将<strong>全量替换</strong>当前响应参数树(先清空再导入)。
|
||||
</div>
|
||||
<div class="analysis-tabs">
|
||||
<SlidingTabs v-model="tabs1" size="mini" :items="analysisTypeItems" aria-label="Import format" />
|
||||
|
||||
@ -2152,6 +2152,9 @@ export default {
|
||||
'order.myOrders.drawer.operationLog.after': 'After',
|
||||
'order.myOrders.drawer.operationLog.noChanges': 'No field changes',
|
||||
'order.myOrders.drawer.operationLog.operator': 'Operator',
|
||||
'order.myOrders.drawer.operationLog.time': 'Time',
|
||||
'order.myOrders.drawer.operationLog.action': 'Action',
|
||||
'order.myOrders.drawer.operationLog.content': 'Content',
|
||||
'order.myOrders.drawer.tabs.issueHandling': 'Issue handling',
|
||||
'order.myOrders.drawer.issueHandling.pendingCount': 'Pending {count}',
|
||||
'order.myOrders.drawer.issueHandling.processedCount': 'Processed {count}',
|
||||
@ -2384,6 +2387,8 @@ export default {
|
||||
'order.myOrders.drawer.actions.edit': 'Edit',
|
||||
'order.myOrders.drawer.actions.save': 'Save',
|
||||
'order.myOrders.drawer.actions.cancel': 'Cancel',
|
||||
'order.myOrders.drawer.editAfterInboundForbidden':
|
||||
'Order details cannot be edited after the parcel is received',
|
||||
'order.myOrders.drawer.actions.recalculateReceivable': 'Recalc Receivable',
|
||||
'order.myOrders.drawer.actions.recalculatePayable': 'Recalc Payable',
|
||||
'order.myOrders.drawer.actions.bindSnapshotVersions': 'Switch Versions',
|
||||
@ -2406,7 +2411,7 @@ export default {
|
||||
'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.actions.additionalServices': 'Add service',
|
||||
'order.myOrders.drawer.editBasicTitle': 'Edit Order Details',
|
||||
'order.myOrders.drawer.editWeightTitle': 'Edit Weight & Dimensions',
|
||||
'order.myOrders.drawer.message.saveSuccess': 'Saved successfully',
|
||||
@ -3956,9 +3961,9 @@ export default {
|
||||
'operation.inbound.pageTitle': 'Waybill Information',
|
||||
'operation.inbound.broadcastSettings': 'Broadcast Settings',
|
||||
'operation.inbound.scan.title': 'Scan inbound',
|
||||
'operation.inbound.tip': 'Scan to verify product and destCountry, then enter received weight and dimensions before inbound.',
|
||||
'operation.inbound.tip': 'Scan to load order info, enter weight and dimensions, then inbound (Enter submits when metrics are filled).',
|
||||
'operation.inbound.preview.title': 'Verify',
|
||||
'operation.inbound.preview.empty': 'Scan a number to show order info and enter weight / dimensions',
|
||||
'operation.inbound.preview.empty': 'Scan or enter a number and press Enter to load order info',
|
||||
'operation.inbound.form.scanNumber': 'Scan Number:',
|
||||
'operation.inbound.form.receivedWeight': 'Received Weight',
|
||||
'operation.inbound.form.dimensions': 'L × W × H',
|
||||
@ -4008,9 +4013,9 @@ export default {
|
||||
'operation.outbound.pageTitle': 'Waybill Information',
|
||||
'operation.outbound.broadcastSettings': 'Broadcast Settings',
|
||||
'operation.outbound.scan.title': 'Scan outbound',
|
||||
'operation.outbound.tip': 'Scan waybill to verify product, destCountry and channel. Weight and dimensions are optional.',
|
||||
'operation.outbound.tip': 'Scan to load product, destCountry and channel, enter weight and dimensions, then outbound (Enter submits when metrics are filled).',
|
||||
'operation.outbound.preview.title': 'Verify',
|
||||
'operation.outbound.preview.empty': 'Scan a number to show order info; weight / dimensions are optional',
|
||||
'operation.outbound.preview.empty': 'Scan or enter a number and press Enter to load order info',
|
||||
'operation.outbound.form.scanNumber': 'Scan Number:',
|
||||
'operation.outbound.form.shipWeight': 'Shipped weight',
|
||||
'operation.outbound.form.dimensions': 'L × W × H',
|
||||
@ -4042,6 +4047,7 @@ export default {
|
||||
'operation.outbound.table.operateTime': 'Operate Time',
|
||||
'operation.outbound.table.operator': 'Operator',
|
||||
'operation.outbound.message.waybillNumberRequired': 'Please enter or scan waybill number',
|
||||
'operation.outbound.message.metricsRequired': 'Please enter ship weight and dimensions',
|
||||
'operation.outbound.message.outboundSuccess': 'Outbound successful',
|
||||
'operation.outbound.message.outboundFailed': 'Outbound failed',
|
||||
'operation.outbound.message.loadRecentFailed': 'Failed to load recent outbound records',
|
||||
@ -5040,6 +5046,8 @@ export default {
|
||||
'channel.labelDesign.actions.search': 'Search',
|
||||
'channel.labelDesign.actions.create': 'New template',
|
||||
'channel.labelDesign.actions.edit': 'Design',
|
||||
'channel.labelDesign.actions.rename': 'Rename',
|
||||
'channel.labelDesign.actions.editTemplate': 'Edit',
|
||||
'channel.labelDesign.actions.delete': 'Delete',
|
||||
'channel.labelDesign.actions.refresh': 'Refresh',
|
||||
'channel.labelDesign.actions.back': 'Back',
|
||||
@ -5049,6 +5057,7 @@ export default {
|
||||
'channel.labelDesign.actions.openTemplate': 'Open template',
|
||||
'channel.labelDesign.actions.open': 'Open',
|
||||
'channel.labelDesign.actions.startDesign': 'Start design',
|
||||
'channel.labelDesign.actions.confirm': 'OK',
|
||||
'channel.labelDesign.actions.cancel': 'Cancel',
|
||||
'channel.labelDesign.loadingList': 'Loading templates…',
|
||||
'channel.labelDesign.emptySelect': 'Select a template on the left, or create one',
|
||||
@ -5059,7 +5068,12 @@ export default {
|
||||
'channel.labelDesign.message.saveSuccess': 'Template saved',
|
||||
'channel.labelDesign.message.saveFailed': 'Save failed',
|
||||
'channel.labelDesign.message.deleteSuccess': 'Deleted',
|
||||
'channel.labelDesign.message.renameSuccess': 'Name updated',
|
||||
'channel.labelDesign.message.renameFailed': 'Failed to rename',
|
||||
'channel.labelDesign.message.editSuccess': 'Updated',
|
||||
'channel.labelDesign.message.editFailed': 'Update failed',
|
||||
'channel.labelDesign.message.nameRequired': 'Template name is required',
|
||||
'channel.labelDesign.message.typeRequired': 'Please select a template type',
|
||||
'channel.labelDesign.message.alreadyOpen': 'This template is already open',
|
||||
'channel.labelDesign.message.openSuccess': 'Opened: {name}',
|
||||
'channel.labelDesign.fieldDictionary': 'Order fields',
|
||||
|
||||
@ -2013,6 +2013,9 @@ export default {
|
||||
'order.myOrders.drawer.operationLog.after': '修改后',
|
||||
'order.myOrders.drawer.operationLog.noChanges': '无字段变更',
|
||||
'order.myOrders.drawer.operationLog.operator': '操作人',
|
||||
'order.myOrders.drawer.operationLog.time': '时间',
|
||||
'order.myOrders.drawer.operationLog.action': '操作动作',
|
||||
'order.myOrders.drawer.operationLog.content': '内容',
|
||||
'order.myOrders.drawer.tabs.issueHandling': '问题处理',
|
||||
'order.myOrders.drawer.issueHandling.pendingCount': '待处理 {count}',
|
||||
'order.myOrders.drawer.issueHandling.processedCount': '已处理 {count}',
|
||||
@ -2245,11 +2248,12 @@ export default {
|
||||
'order.myOrders.drawer.actions.edit': '编辑',
|
||||
'order.myOrders.drawer.actions.save': '保存',
|
||||
'order.myOrders.drawer.actions.cancel': '取消',
|
||||
'order.myOrders.drawer.editAfterInboundForbidden': '包裹入库后不可再编辑订单详情',
|
||||
'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.actions.additionalServices': '添加服务',
|
||||
'order.myOrders.drawer.fee.rulesVersion': '规则版本',
|
||||
'order.myOrders.drawer.fee.priceVersion': '价格版本',
|
||||
'order.myOrders.drawer.bindSnapshot.title': '切换价格/规则版本',
|
||||
@ -3985,9 +3989,9 @@ export default {
|
||||
'operation.inbound.pageTitle': '运单信息',
|
||||
'operation.inbound.broadcastSettings': '播报设置',
|
||||
'operation.inbound.scan.title': '扫码入库',
|
||||
'operation.inbound.tip': '扫描单号核对产品与国家后,填写收货实重与尺寸再入库,避免录错。',
|
||||
'operation.inbound.tip': '扫描单号自动带出订单信息,填写收货实重与尺寸后点入库(件重尺已填时可回车直接入库)。',
|
||||
'operation.inbound.preview.title': '核对信息',
|
||||
'operation.inbound.preview.empty': '扫描单号后显示订单信息,并填写收货实重与尺寸',
|
||||
'operation.inbound.preview.empty': '扫描或输入单号并回车,自动带出订单信息',
|
||||
'operation.inbound.form.scanNumber': '扫描单号:',
|
||||
'operation.inbound.form.receivedWeight': '收货实重',
|
||||
'operation.inbound.form.dimensions': '长*宽*高',
|
||||
@ -4037,9 +4041,9 @@ export default {
|
||||
'operation.outbound.pageTitle': '运单信息',
|
||||
'operation.outbound.broadcastSettings': '播报设置',
|
||||
'operation.outbound.scan.title': '扫码出库',
|
||||
'operation.outbound.tip': '扫描运单号核对产品、国家与渠道后,可按需补录发货实重尺寸再出库。',
|
||||
'operation.outbound.tip': '扫描单号自动带出产品、国家与渠道,填写发货实重与尺寸后点出库(件重尺已填时可回车直接出库)。',
|
||||
'operation.outbound.preview.title': '核对信息',
|
||||
'operation.outbound.preview.empty': '扫描单号后显示订单信息,可填写发货实重与尺寸',
|
||||
'operation.outbound.preview.empty': '扫描或输入单号并回车,自动带出订单信息',
|
||||
'operation.outbound.form.scanNumber': '扫描单号:',
|
||||
'operation.outbound.form.shipWeight': '发货实重',
|
||||
'operation.outbound.form.dimensions': '长*宽*高',
|
||||
@ -4071,6 +4075,7 @@ export default {
|
||||
'operation.outbound.table.operateTime': '操作时间',
|
||||
'operation.outbound.table.operator': '操作人',
|
||||
'operation.outbound.message.waybillNumberRequired': '请输入或扫描运单号',
|
||||
'operation.outbound.message.metricsRequired': '请填写发货实重与长宽高',
|
||||
'operation.outbound.message.outboundSuccess': '出库成功',
|
||||
'operation.outbound.message.outboundFailed': '出库失败',
|
||||
'operation.outbound.message.loadRecentFailed': '加载最近出库记录失败',
|
||||
@ -5068,6 +5073,8 @@ export default {
|
||||
'channel.labelDesign.actions.search': '查询',
|
||||
'channel.labelDesign.actions.create': '新建模板',
|
||||
'channel.labelDesign.actions.edit': '设计',
|
||||
'channel.labelDesign.actions.rename': '编辑名称',
|
||||
'channel.labelDesign.actions.editTemplate': '编辑',
|
||||
'channel.labelDesign.actions.delete': '删除',
|
||||
'channel.labelDesign.actions.refresh': '刷新列表',
|
||||
'channel.labelDesign.actions.back': '返回列表',
|
||||
@ -5077,6 +5084,7 @@ export default {
|
||||
'channel.labelDesign.actions.openTemplate': '打开模板',
|
||||
'channel.labelDesign.actions.open': '打开',
|
||||
'channel.labelDesign.actions.startDesign': '开始设计',
|
||||
'channel.labelDesign.actions.confirm': '确定',
|
||||
'channel.labelDesign.actions.cancel': '取消',
|
||||
'channel.labelDesign.loadingList': '正在加载模板…',
|
||||
'channel.labelDesign.emptySelect': '请在左侧选择模板,或新建一个',
|
||||
@ -5087,7 +5095,12 @@ export default {
|
||||
'channel.labelDesign.message.saveSuccess': '模板已保存',
|
||||
'channel.labelDesign.message.saveFailed': '保存失败',
|
||||
'channel.labelDesign.message.deleteSuccess': '已删除',
|
||||
'channel.labelDesign.message.renameSuccess': '名称已更新',
|
||||
'channel.labelDesign.message.renameFailed': '修改名称失败',
|
||||
'channel.labelDesign.message.editSuccess': '已更新',
|
||||
'channel.labelDesign.message.editFailed': '修改失败',
|
||||
'channel.labelDesign.message.nameRequired': '请填写模板名称',
|
||||
'channel.labelDesign.message.typeRequired': '请选择标签类型',
|
||||
'channel.labelDesign.message.alreadyOpen': '该模板已在设计器中',
|
||||
'channel.labelDesign.message.openSuccess': '已打开:{name}',
|
||||
'channel.labelDesign.fieldDictionary': '订单字段',
|
||||
|
||||
@ -3,11 +3,15 @@ import ArcoVue from '@arco-design/web-vue';
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import 'uno.css';
|
||||
import { installGlobalMessageDefaults } from '@/utils/message';
|
||||
import router from './router';
|
||||
import store, { useAppStore } from './store';
|
||||
import i18n, { initPreferredLocale, ensureFullLocaleMessages } from './locale';
|
||||
import directive from './directive';
|
||||
import globalProperties from './plugins/globalProperties';
|
||||
|
||||
// 全局 Message:可手动关闭;错误/警告默认 8s
|
||||
installGlobalMessageDefaults();
|
||||
import {
|
||||
bindHeavyPluginsContext,
|
||||
ensureHeavyPlugins,
|
||||
|
||||
@ -41,10 +41,7 @@ export const initDynamicRouter = async (options?: {
|
||||
/** 路由初始化错误 */
|
||||
const routerError = (isNotice: boolean = true, title: string = '无权限访问', message: string = '当前账号无任何菜单权限,请联系系统管理员!') => {
|
||||
if (isNotice) {
|
||||
Message.warning({
|
||||
content: message,
|
||||
duration: 3000
|
||||
});
|
||||
Message.warning(message);
|
||||
}
|
||||
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
|
||||
const currentPath = router.currentRoute.value.fullPath;
|
||||
|
||||
@ -70,14 +70,16 @@ function flattenTree(arr: any[]): any[] {
|
||||
return flattenTreeTableRows(arr);
|
||||
}
|
||||
|
||||
/** 读取树表完整扁平数据(避免 fullData 仅返回根节点导致子级丢失) */
|
||||
/**
|
||||
* 读取树表完整扁平数据。
|
||||
* 已传入表格 fullData(含清空后的 [])时必须以表格为准;
|
||||
* 旧逻辑在 grid 为空时回退 props,会把已删除行「复活」,导致清空后保存仍报重名。
|
||||
*/
|
||||
export function getTreeTableFlatRows(fromGrid?: any[], propsData?: any[]): any[] {
|
||||
const flatFromGrid = flattenTreeTableRows(fromGrid || []);
|
||||
const propsFlat = Array.isArray(propsData) ? propsData : [];
|
||||
if (flatFromGrid.length >= propsFlat.length) {
|
||||
return flatFromGrid.length ? flatFromGrid : [...propsFlat];
|
||||
if (Array.isArray(fromGrid)) {
|
||||
return flattenTreeTableRows(fromGrid);
|
||||
}
|
||||
return propsFlat.length ? [...propsFlat] : flatFromGrid;
|
||||
return flattenTreeTableRows(Array.isArray(propsData) ? propsData : []);
|
||||
}
|
||||
|
||||
function getRowSeq(row: any, tableRef: any): string {
|
||||
@ -278,11 +280,33 @@ function mapTreeChangeRows(
|
||||
return [...install, ...remove, ...update];
|
||||
}
|
||||
|
||||
function mapTreeDeleteFromBaseline(
|
||||
serverId: number,
|
||||
baseline: NormalizedParamRow,
|
||||
paraKind: number,
|
||||
) {
|
||||
return {
|
||||
optionType: 2,
|
||||
id: serverId,
|
||||
pId: baseline.pId,
|
||||
alias: baseline.alias || baseline.name,
|
||||
name: baseline.name,
|
||||
cnName: baseline.cnName,
|
||||
type: baseline.type,
|
||||
isRequired: baseline.isRequired,
|
||||
sort: baseline.sort || '0',
|
||||
description: baseline.description,
|
||||
paraKind,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑保存时对齐后端 MaintainParametersAsync:
|
||||
* - 仅提交增/删/改增量
|
||||
* - 未变更的已有参数不传(后端会自动补全 DB 节点)
|
||||
* - 假 id + 已存在路径 → 按服务端 id 走修改,避免与 DB 补全节点重复
|
||||
* - 解析全量导入会 reloadData(清空 removeRecords)且行 id 全是新假 id:走「删光快照 + 整树新增」
|
||||
* - VXE removeRecords 丢失时,按「快照有、当前表无」补删(与 FormKV 一致)
|
||||
*/
|
||||
function buildTreeParameterList(
|
||||
actionRows: { insertRecords?: any[]; removeRecords?: any[]; updateRecords?: any[] },
|
||||
@ -298,55 +322,90 @@ function buildTreeParameterList(
|
||||
changes.filter((c) => c.optionType === 2).map((c) => Number(c.id)),
|
||||
);
|
||||
|
||||
const flatCurrent = flattenTree(treeData).filter(
|
||||
(item) => item?.name && String(item.name).trim() !== '',
|
||||
);
|
||||
|
||||
if (!isEditMode) {
|
||||
if (changes.length > 0) return changes;
|
||||
return flattenTree(treeData)
|
||||
.filter((item) => item?.name && String(item.name).trim() !== '')
|
||||
.map((item) => mapTreeRow(item, 1, tableRef, paraKind));
|
||||
return flatCurrent.map((item) => mapTreeRow(item, 1, tableRef, paraKind));
|
||||
}
|
||||
|
||||
// 全量替换:当前行没有任何加载时的服务端 id(解析导入假 id / 或已清空整表)
|
||||
const preservesAnyServerId = flatCurrent.some((item) =>
|
||||
original.ids.has(normalizeRowId(item)),
|
||||
);
|
||||
if (original.ids.size > 0 && !preservesAnyServerId) {
|
||||
const result: any[] = [];
|
||||
original.ids.forEach((serverId) => {
|
||||
const baseline = original.rowById.get(serverId);
|
||||
if (!baseline?.name) return;
|
||||
result.push(mapTreeDeleteFromBaseline(serverId, baseline, paraKind));
|
||||
});
|
||||
flatCurrent.forEach((item) => {
|
||||
result.push(mapTreeRow(item, 1, tableRef, paraKind));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const result: any[] = [];
|
||||
changes.filter((c) => c.optionType === 2).forEach((c) => result.push(c));
|
||||
const presentServerIds = new Set<number>();
|
||||
|
||||
flattenTree(treeData)
|
||||
.filter((item) => item?.name && String(item.name).trim() !== '')
|
||||
.forEach((item) => {
|
||||
const id = normalizeRowId(item);
|
||||
const pId = Number(item?.pId ?? item?.PId ?? 0);
|
||||
const name = String(item?.name ?? item?.Name ?? '').trim();
|
||||
const pathKey = buildParamPathKey(pId, name);
|
||||
flatCurrent.forEach((item) => {
|
||||
const id = normalizeRowId(item);
|
||||
const pId = Number(item?.pId ?? item?.PId ?? 0);
|
||||
const name = String(item?.name ?? item?.Name ?? '').trim();
|
||||
const pathKey = buildParamPathKey(pId, name);
|
||||
|
||||
if (removedIds.has(id)) return;
|
||||
if (removedIds.has(id)) return;
|
||||
|
||||
const tracked = changeById.get(id);
|
||||
if (tracked && tracked.optionType !== 2) {
|
||||
result.push(tracked);
|
||||
return;
|
||||
const tracked = changeById.get(id);
|
||||
if (tracked && tracked.optionType !== 2) {
|
||||
result.push(tracked);
|
||||
const trackedServerId = Number(tracked.id);
|
||||
if (trackedServerId > 0 && original.ids.has(trackedServerId)) {
|
||||
presentServerIds.add(trackedServerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const serverIdByPath = original.pathToId.get(pathKey);
|
||||
const serverId = original.ids.has(id) ? id : serverIdByPath;
|
||||
const baseline = serverId ? original.rowById.get(serverId) : undefined;
|
||||
const serverIdByPath = original.pathToId.get(pathKey);
|
||||
const serverId = original.ids.has(id) ? id : serverIdByPath;
|
||||
const baseline = serverId ? original.rowById.get(serverId) : undefined;
|
||||
|
||||
if (serverId && baseline) {
|
||||
if (!isSameTreeParamRow(item, baseline)) {
|
||||
result.push(mapTreeRow(item, 3, tableRef, paraKind, serverId));
|
||||
}
|
||||
return;
|
||||
if (serverId && baseline) {
|
||||
presentServerIds.add(serverId);
|
||||
if (!isSameTreeParamRow(item, baseline)) {
|
||||
result.push(mapTreeRow(item, 3, tableRef, paraKind, serverId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serverId) {
|
||||
result.push(mapTreeRow(item, 1, tableRef, paraKind));
|
||||
}
|
||||
});
|
||||
if (!serverId) {
|
||||
result.push(mapTreeRow(item, 1, tableRef, paraKind));
|
||||
}
|
||||
});
|
||||
|
||||
// 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(mapTreeDeleteFromBaseline(serverId, baseline, paraKind));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 客户端校验:同一 pId 下参数名不可重复(对齐后端 ValidateParameter) */
|
||||
/** 客户端校验:同一 pId 下参数名不可重复(对齐后端 ValidateParameter,跳过删除项) */
|
||||
export function validateTreeParameterPaths(rows: any[]): string | null {
|
||||
const seen = new Set<string>();
|
||||
for (const item of flattenTree(rows)) {
|
||||
// 后端 ValidateParameter 会先排除 OptionType.删除;全量替换时「删旧 + 增同名」不能当重复
|
||||
const optionType = Number(item?.optionType ?? item?.OptionType ?? 0);
|
||||
if (optionType === 2) continue;
|
||||
|
||||
const name = String(item?.name ?? item?.Name ?? '').trim();
|
||||
if (!name) continue;
|
||||
const pId = Number(item?.pId ?? item?.PId ?? 0);
|
||||
|
||||
@ -1,18 +1,94 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
export type BaseMessageType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export type BaseMessageOptions = {
|
||||
/** 显示时长 ms */
|
||||
duration?: number;
|
||||
/** 是否显示关闭按钮 */
|
||||
closable?: boolean;
|
||||
/** 悬停时重置倒计时 */
|
||||
resetOnHover?: boolean;
|
||||
};
|
||||
|
||||
type MessageConfigLike = {
|
||||
content: any;
|
||||
duration?: number;
|
||||
closable?: boolean;
|
||||
resetOnHover?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/** 全局默认:可手动关闭;错误/警告多停留约 5s(合计 8s) */
|
||||
const GLOBAL_MESSAGE_DEFAULTS: Record<
|
||||
'info' | 'success' | 'warning' | 'error' | 'normal',
|
||||
{ duration: number; closable: boolean; resetOnHover: boolean }
|
||||
> = {
|
||||
info: { duration: 3000, closable: true, resetOnHover: true },
|
||||
success: { duration: 3000, closable: true, resetOnHover: true },
|
||||
warning: { duration: 8000, closable: true, resetOnHover: true },
|
||||
error: { duration: 8000, closable: true, resetOnHover: true },
|
||||
normal: { duration: 3000, closable: true, resetOnHover: true },
|
||||
};
|
||||
|
||||
type PatchableType = keyof typeof GLOBAL_MESSAGE_DEFAULTS;
|
||||
|
||||
let messageDefaultsInstalled = false;
|
||||
|
||||
function mergeMessageConfig(
|
||||
type: PatchableType,
|
||||
config: string | MessageConfigLike,
|
||||
): MessageConfigLike {
|
||||
const defaults = GLOBAL_MESSAGE_DEFAULTS[type];
|
||||
if (typeof config === 'string') {
|
||||
return {
|
||||
content: config,
|
||||
...defaults,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...defaults,
|
||||
...config,
|
||||
duration: config.duration ?? defaults.duration,
|
||||
closable: config.closable ?? defaults.closable,
|
||||
resetOnHover: config.resetOnHover ?? defaults.resetOnHover,
|
||||
};
|
||||
}
|
||||
|
||||
/** 安装全局 Message 默认行为(幂等)。覆盖 Message.* 与 $baseMessage。 */
|
||||
export function installGlobalMessageDefaults() {
|
||||
if (messageDefaultsInstalled) return;
|
||||
messageDefaultsInstalled = true;
|
||||
|
||||
(Object.keys(GLOBAL_MESSAGE_DEFAULTS) as PatchableType[]).forEach((type) => {
|
||||
const original = (Message[type] as any).bind(Message);
|
||||
|
||||
(Message as any)[type] = (config: string | MessageConfigLike, appContext?: any) => {
|
||||
return original(mergeMessageConfig(type, config), appContext);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 被任何路径 import 时即安装,保证 request 拦截器等直接 Message.error 也生效
|
||||
installGlobalMessageDefaults();
|
||||
|
||||
/**
|
||||
* 全局消息提示工具函数
|
||||
* 全局消息提示
|
||||
* @param message 消息内容
|
||||
* @param type 消息类型 'success' | 'error' | 'warning' | 'info'
|
||||
* @param type 消息类型
|
||||
* @param options 可覆盖全局默认(duration / closable / resetOnHover)
|
||||
*/
|
||||
export const baseMessage = (
|
||||
message: string,
|
||||
type: 'success' | 'error' | 'warning' | 'info' = 'info'
|
||||
type: BaseMessageType = 'info',
|
||||
options?: BaseMessageOptions,
|
||||
) => {
|
||||
Message[type](message);
|
||||
Message[type]({
|
||||
content: message,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Export as a global function for direct use
|
||||
export const $baseMessage = baseMessage;
|
||||
|
||||
export default baseMessage;
|
||||
export default baseMessage;
|
||||
|
||||
@ -12,6 +12,8 @@ import { useUserStore } from '@/store';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { resolvePostLogoutLogin } from '@/utils/login-portal';
|
||||
import { rewriteBrowserApiPath } from '@/utils/rewriteBrowserApiPath';
|
||||
// 安装全局 Message 默认(可关闭 / 错误加长),须在本文件 Message.* 调用前生效
|
||||
import '@/utils/message';
|
||||
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: '/',
|
||||
@ -149,10 +151,7 @@ service.interceptors.response.use(
|
||||
// 根据不同的状态码处理不同的错误
|
||||
switch (status) {
|
||||
case 400:
|
||||
Message.error({
|
||||
content: `请求参数错误: ${data?.message || statusText}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`请求参数错误: ${data?.message || statusText}`);
|
||||
break;
|
||||
case 401:
|
||||
// 401 未授权,登录过期
|
||||
@ -162,10 +161,7 @@ service.interceptors.response.use(
|
||||
|
||||
if (isRbacApi) {
|
||||
// RBAC API的401,说明token过期,需要重新登录
|
||||
Message.error({
|
||||
content: errorMessage,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(errorMessage);
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
|
||||
const currentPath = router.currentRoute.value.fullPath;
|
||||
@ -184,10 +180,7 @@ service.interceptors.response.use(
|
||||
}
|
||||
} else {
|
||||
// 非RBAC API的401,统一处理为登录失效,跳转到登录页
|
||||
Message.error({
|
||||
content: errorMessage || '登录已过期,请重新登录',
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(errorMessage || '登录已过期,请重新登录');
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
|
||||
const currentPath = router.currentRoute.value.fullPath;
|
||||
@ -202,41 +195,23 @@ service.interceptors.response.use(
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
Message.error({
|
||||
content: `禁止访问: ${data?.message || statusText}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`禁止访问: ${data?.message || statusText}`);
|
||||
break;
|
||||
case 404:
|
||||
Message.error({
|
||||
content: `请求的资源不存在: ${data?.message || statusText}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`请求的资源不存在: ${data?.message || statusText}`);
|
||||
break;
|
||||
case 500:
|
||||
Message.error({
|
||||
content: `服务器内部错误: ${data?.message || statusText}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`服务器内部错误: ${data?.message || statusText}`);
|
||||
break;
|
||||
default:
|
||||
Message.error({
|
||||
content: `请求失败 (${status}): ${data?.message || statusText}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`请求失败 (${status}): ${data?.message || statusText}`);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已经发出,但没有收到响应
|
||||
Message.error({
|
||||
content: '网络连接失败,请检查网络设置',
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error('网络连接失败,请检查网络设置');
|
||||
} else {
|
||||
// 请求配置出错
|
||||
Message.error({
|
||||
content: `请求配置错误: ${error.message}`,
|
||||
duration: 3000,
|
||||
});
|
||||
Message.error(`请求配置错误: ${error.message}`);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
||||
@ -230,6 +230,10 @@
|
||||
if (!forceRefresh && isInitializing.value && lastInitId.value === currentId) {
|
||||
return;
|
||||
}
|
||||
// 已加载同一接口且非强制刷新(来回切 tab):跳过重拉,避免误报未保存
|
||||
if (!forceRefresh && lastInitId.value === currentId && shouldRenderForm.value && !isInitializing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记正在初始化和当前ID
|
||||
isInitializing.value = true;
|
||||
|
||||
@ -53,17 +53,29 @@
|
||||
return row;
|
||||
}
|
||||
|
||||
/** SelectPro / 字典 key 常把 number 写成 string,脏检查必须宽松比较 */
|
||||
function sameScalar(a: unknown, b: unknown) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
return String(a) === String(b);
|
||||
}
|
||||
|
||||
function rowSourceId(row: any): number | null {
|
||||
const id = Number(row?.id);
|
||||
return Number.isFinite(id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
function snapshotSource(rows: any[]) {
|
||||
const map = new Map<number | string, any>();
|
||||
rows.forEach((row) => {
|
||||
if (row.id != null) {
|
||||
map.set(row.id, {
|
||||
id: row.id,
|
||||
configType: row.configType,
|
||||
parameterId: row.parameterId,
|
||||
value: row.value == null ? '' : String(row.value),
|
||||
});
|
||||
}
|
||||
const id = rowSourceId(row);
|
||||
if (id == null) return;
|
||||
map.set(id, {
|
||||
id,
|
||||
configType: row.configType == null ? '' : String(row.configType),
|
||||
parameterId: row.parameterId == null ? '' : String(row.parameterId),
|
||||
value: row.value == null ? '' : String(row.value),
|
||||
});
|
||||
});
|
||||
sourceMap.value = map;
|
||||
removedRecords.value = [];
|
||||
@ -74,19 +86,20 @@
|
||||
const updateRecords: any[] = [];
|
||||
|
||||
listData.value.forEach((row) => {
|
||||
if (row._isNew || row.id == null) {
|
||||
const id = rowSourceId(row);
|
||||
if (row._isNew || id == null) {
|
||||
insertRecords.push(row);
|
||||
return;
|
||||
}
|
||||
const src = sourceMap.value.get(row.id);
|
||||
const src = sourceMap.value.get(id);
|
||||
if (!src) {
|
||||
insertRecords.push(row);
|
||||
return;
|
||||
}
|
||||
const currentValue = row.value == null ? '' : String(row.value);
|
||||
if (
|
||||
src.configType !== row.configType ||
|
||||
src.parameterId !== row.parameterId ||
|
||||
!sameScalar(src.configType, row.configType) ||
|
||||
!sameScalar(src.parameterId, row.parameterId) ||
|
||||
src.value !== currentValue
|
||||
) {
|
||||
updateRecords.push(row);
|
||||
@ -141,12 +154,27 @@
|
||||
});
|
||||
|
||||
const tableData = [...addData, ...removeData, ...updataData];
|
||||
// map 里若因类型空而 push 了 Message 返回值,拦截掉
|
||||
const items = tableData.filter((i) => i && typeof i === 'object' && i.optionType != null);
|
||||
if (items.length !== tableData.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await postInterfaceReturnConfigSave({
|
||||
interfaceId: addId.value,
|
||||
items: tableData,
|
||||
items,
|
||||
});
|
||||
|
||||
// 保存成功后以当前表为准重新采基线,避免仍按旧快照误报脏
|
||||
listData.value.forEach((row) => {
|
||||
if (row._isNew) delete row._isNew;
|
||||
});
|
||||
snapshotSource(listData.value);
|
||||
// 保存后需重新拉 id(新增行),否则下次会当 insert
|
||||
if (resolvedInterfaceId.value) {
|
||||
await loadData(resolvedInterfaceId.value);
|
||||
}
|
||||
|
||||
if (!options?.skipSuccessToast) {
|
||||
$baseMessage('保存成功,如需生效请去发布', 'success');
|
||||
}
|
||||
@ -171,8 +199,9 @@
|
||||
if (idx >= 0) {
|
||||
listData.value.splice(idx, 1);
|
||||
}
|
||||
if (currRow?.id != null && !currRow._isNew) {
|
||||
if (!removedRecords.value.some((r) => r.id === currRow.id)) {
|
||||
const removedId = rowSourceId(currRow);
|
||||
if (removedId != null && !currRow._isNew) {
|
||||
if (!removedRecords.value.some((r) => rowSourceId(r) === removedId)) {
|
||||
removedRecords.value.push(currRow);
|
||||
}
|
||||
}
|
||||
@ -181,12 +210,15 @@
|
||||
function handleTypeChange(e: any, row: any) {
|
||||
row.parameterId = undefined;
|
||||
const fullData = getFullData();
|
||||
const nextType = Number(e?.value ?? e);
|
||||
|
||||
if (e.value === 2 || e.value === 3) {
|
||||
const flag = fullData?.filter((item: any) => item.configType === e.value);
|
||||
if (nextType === 2 || nextType === 3) {
|
||||
const flag = fullData?.filter(
|
||||
(item: any) => Number(item.configType) === nextType,
|
||||
);
|
||||
if (flag && flag?.length >= 2) {
|
||||
const msg =
|
||||
e.value === 2
|
||||
nextType === 2
|
||||
? '标签上传只能保存一条数据'
|
||||
: '错误返回只能保存一条数据';
|
||||
row.configType = undefined;
|
||||
@ -232,6 +264,9 @@
|
||||
);
|
||||
snapshotSource(listData.value);
|
||||
await nextTick();
|
||||
// SelectPro 异步回写 key 类型后再采一次基线,避免刚打开就误报脏
|
||||
await nextTick();
|
||||
snapshotSource(listData.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
@ -244,9 +279,11 @@
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
/** 与后端 onlyLeafs 一致:排除 Object/Array。错误返回(3)需要整树,其余只要叶子 */
|
||||
function formatNameType(row: any) {
|
||||
if (row.configType === 1 || row.configType === 2) return true;
|
||||
if (row.configType === 3) return false;
|
||||
const type = Number(row?.configType);
|
||||
if (type === 3) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const valueList = ref<API.EnumCacheEntry[]>([]);
|
||||
@ -325,7 +362,7 @@
|
||||
|
||||
<template #value="{ record }">
|
||||
<SelectPro
|
||||
v-if="record.configType === 2"
|
||||
v-if="Number(record.configType) === 2"
|
||||
v-model="record.value"
|
||||
:api="getDictionaryGetList"
|
||||
:extra="{
|
||||
@ -338,7 +375,7 @@
|
||||
placeholder="请选择值"
|
||||
/>
|
||||
<a-input
|
||||
v-else-if="record.configType === 1"
|
||||
v-else-if="Number(record.configType) === 1"
|
||||
v-model="record.value"
|
||||
placeholder="请输入值"
|
||||
/>
|
||||
|
||||
@ -1437,9 +1437,19 @@
|
||||
addClick();
|
||||
}
|
||||
|
||||
/** 刷新左侧服务商列表:清空当前选中 */
|
||||
function handleRefresh() {
|
||||
selectedProvider.value = null;
|
||||
selectedInterface.value = null;
|
||||
interfaceList.value = [];
|
||||
loadServiceProviderList();
|
||||
}
|
||||
|
||||
/** 刷新中间接口列表:清空当前接口选中 */
|
||||
function handleRefreshInterfaceList() {
|
||||
selectedInterface.value = null;
|
||||
loadInterfaceList();
|
||||
}
|
||||
|
||||
// 接口弹窗关闭
|
||||
function handleInterfaceModalClose() {
|
||||
@ -1787,7 +1797,7 @@
|
||||
actions-title="操作"
|
||||
delete-label="删除"
|
||||
@search="handleInterfaceSearch"
|
||||
@refresh="loadInterfaceList"
|
||||
@refresh="handleRefreshInterfaceList"
|
||||
@add="addInterface"
|
||||
@select="handleInterfaceSelectFromTree"
|
||||
@node-click="handleInterfaceNodeClick"
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
getEditMapDetails,
|
||||
getGetMapParameterCascadedList,
|
||||
} from '@/api/InterfaceMappingServices/interfaceMap';
|
||||
import { ParameterType } from '@/constants/itfxEnums';
|
||||
|
||||
import AddParameter from './AddParameter.vue';
|
||||
import MapAiAssistModal from './MapAiAssistModal.vue';
|
||||
@ -290,11 +291,36 @@
|
||||
);
|
||||
};
|
||||
}
|
||||
/** 与 GetParameterList?onlyLeafs=true 一致:排除 Object/Array */
|
||||
function isLeafParamType(type: unknown) {
|
||||
const t = Number(type);
|
||||
return t !== ParameterType.Object && t !== ParameterType.Array;
|
||||
}
|
||||
|
||||
/** 接口根保留,参数列表只留叶子(服务商出参接口原先未过滤) */
|
||||
function prepareCascaderRoots(roots: any[]) {
|
||||
return (roots || []).map((root) => ({
|
||||
...root,
|
||||
parameters: (Array.isArray(root.parameters) ? root.parameters : []).filter(
|
||||
(p: any) => isLeafParamType(p?.type),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function setlabel(data: any) {
|
||||
data.forEach((item: any) => {
|
||||
if (item.parameters) item.parameters = setlabel(item.parameters);
|
||||
const children = Array.isArray(item.parameters) ? item.parameters : null;
|
||||
if (children?.length) {
|
||||
item.parameters = setlabel(children);
|
||||
} else if (item.interfaceId != null && item.alias == null) {
|
||||
// 接口根无参数:保留空 children,避免被当成可选叶子
|
||||
item.parameters = [];
|
||||
} else {
|
||||
// 参数叶子:去掉空 parameters,否则 Arco 会当成不可选的非叶子
|
||||
delete item.parameters;
|
||||
}
|
||||
|
||||
item.getlabel = item.parameters
|
||||
item.getlabel = children?.length
|
||||
? item.name
|
||||
: item.description
|
||||
? `${item.alias}-${item.description}`
|
||||
@ -304,6 +330,13 @@
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Arco Cascader 无 show-all-levels,用 formatLabel 只展示叶子文案 */
|
||||
function formatCascaderLeafLabel(options: any[]) {
|
||||
if (!options?.length) return '';
|
||||
const leaf = options[options.length - 1];
|
||||
return leaf?.getlabel ?? leaf?.alias ?? leaf?.name ?? '';
|
||||
}
|
||||
const mapAliasEditRef = ref();
|
||||
/** 级联 options 按步骤缓存,避免每行渲染都深拷贝/打标 */
|
||||
const cascaderOptionsCache = ref<Map<string, any[]>>(new Map());
|
||||
@ -321,7 +354,9 @@
|
||||
|
||||
let result: any[];
|
||||
if (isReverse) {
|
||||
result = setlabel(cascadedList.value.slice(1, cascadedList.value.length));
|
||||
result = setlabel(
|
||||
prepareCascaderRoots(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);
|
||||
@ -335,7 +370,7 @@
|
||||
}),
|
||||
).filter(Boolean);
|
||||
}
|
||||
result = setlabel(data);
|
||||
result = setlabel(prepareCascaderRoots(data));
|
||||
}
|
||||
cascaderOptionsCache.value.set(cacheKey, result);
|
||||
return result;
|
||||
@ -347,16 +382,45 @@
|
||||
return getOptions(activeInParaItem.value, false);
|
||||
});
|
||||
const outputCascaderOptions = computed(() => getOptions({ interfaceId: 0 }, true));
|
||||
function findCascadedParamById(id: number): any | null {
|
||||
let found: any = null;
|
||||
const walk = (nodes: any[] | undefined) => {
|
||||
if (!nodes?.length || found) return;
|
||||
for (const node of nodes) {
|
||||
if (Number(node.id) === Number(id)) {
|
||||
found = node;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node.parameters) && node.parameters.length) {
|
||||
walk(node.parameters);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const root of cascadedList.value) {
|
||||
walk(root.parameters);
|
||||
if (found) break;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function mapAliasEditCheng(
|
||||
e: any,
|
||||
row: API.InterfaceParameterMapOutputValueObject
|
||||
) {
|
||||
if (e) {
|
||||
let arr: any[] = [];
|
||||
cascadedList.value.forEach((t: any) => {
|
||||
if (t.parameters) arr = arr.concat(t.parameters);
|
||||
});
|
||||
const item = arr.find((t: any) => t.id === e);
|
||||
const item = findCascadedParamById(e);
|
||||
// 仅允许叶子:Object/Array 或仍带子节点的不算有效映射
|
||||
if (
|
||||
!item ||
|
||||
!isLeafParamType(item.type) ||
|
||||
(Array.isArray(item.parameters) && item.parameters.length)
|
||||
) {
|
||||
row.mapAlias = '';
|
||||
row.mapId = 0;
|
||||
row.mapCnName = '';
|
||||
row.mapDescription = '';
|
||||
return;
|
||||
}
|
||||
row.mapAlias = item?.alias;
|
||||
row.mapId = item?.id;
|
||||
row.mapCnName = item?.cnName;
|
||||
@ -740,6 +804,8 @@
|
||||
v-model="row.mapId"
|
||||
:options="activeInputCascaderOptions"
|
||||
:filter-option="filterMethod"
|
||||
:format-label="formatCascaderLeafLabel"
|
||||
search-option-only-label
|
||||
allow-clear
|
||||
allow-search
|
||||
:field-names="{
|
||||
@ -747,17 +813,12 @@
|
||||
label: 'getlabel',
|
||||
children: 'parameters',
|
||||
}"
|
||||
:show-all-levels="false"
|
||||
@change="
|
||||
(value) => {
|
||||
mapAliasEditCheng(value, row);
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #label="{ data: _data }">
|
||||
{{ _data.label }}
|
||||
</template>
|
||||
</a-cascader>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 参数类型(请求侧)在必填时加红色星号 -->
|
||||
@ -798,6 +859,8 @@
|
||||
v-model="row.mapId"
|
||||
:options="outputCascaderOptions"
|
||||
:filter-option="filterMethod"
|
||||
:format-label="formatCascaderLeafLabel"
|
||||
search-option-only-label
|
||||
allow-search
|
||||
allow-clear
|
||||
:field-names="{
|
||||
@ -805,17 +868,12 @@
|
||||
label: 'getlabel',
|
||||
children: 'parameters',
|
||||
}"
|
||||
:show-all-levels="false"
|
||||
@change="
|
||||
(value) => {
|
||||
mapAliasEditCheng(value, row);
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #label="{ data: _data }">
|
||||
{{ _data.label }}
|
||||
</template>
|
||||
</a-cascader>
|
||||
/>
|
||||
</template>
|
||||
<!-- 参数类型(响应侧)在必填时加红色星号 -->
|
||||
<template #typeResp="{ row }">
|
||||
|
||||
@ -100,8 +100,12 @@
|
||||
// 上游接口的验证规则(使用 form-config.ts 中的规则)
|
||||
const upstreamFormRules = formRules;
|
||||
|
||||
// 初始化函数
|
||||
async function init(data?: { id?: number | string; interfaceId?: number | string }) {
|
||||
// 初始化函数;forceRefresh 用于保存后强制重拉,切 tab 回来不重复 init
|
||||
async function init(data?: {
|
||||
id?: number | string;
|
||||
interfaceId?: number | string;
|
||||
forceRefresh?: boolean;
|
||||
}) {
|
||||
// 优先使用传入的参数
|
||||
let idRaw: any = null;
|
||||
|
||||
@ -118,6 +122,7 @@
|
||||
tableForm.isUpStream = true; // 上游接口默认设置为 true
|
||||
tableForm.requestMethod = 2; // 默认 POST
|
||||
tableForm.qps = 1;
|
||||
lastInitId.value = null;
|
||||
await nextTick();
|
||||
if (interfaceDetailRef.value) {
|
||||
interfaceDetailRef.value.setBasicFormValues({ ...tableForm });
|
||||
@ -127,11 +132,16 @@
|
||||
}
|
||||
|
||||
const id = Number(idRaw);
|
||||
const forceRefresh = !!data?.forceRefresh;
|
||||
|
||||
// 防止重复调用:如果正在加载相同的接口,直接返回
|
||||
// 正在加载同一接口:忽略重复调用
|
||||
if (isInitializing.value && lastInitId.value === id) {
|
||||
return;
|
||||
}
|
||||
// 已加载同一接口且非强制刷新(例如明细 ↔ 调试来回切):跳过重拉,避免误报未保存
|
||||
if (!forceRefresh && lastInitId.value === id && shouldRenderForm.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记正在初始化和当前ID
|
||||
isInitializing.value = true;
|
||||
|
||||
@ -125,7 +125,10 @@
|
||||
async function loadList(options?: { keepSelection?: boolean }) {
|
||||
if (!props.id) {
|
||||
list.value = [];
|
||||
selectedId.value = null;
|
||||
if (selectedId.value != null) {
|
||||
selectedId.value = null;
|
||||
emit('row-click', null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -142,8 +145,10 @@
|
||||
const data: any = raw.items ?? raw.data ?? raw.list ?? raw.records ?? raw;
|
||||
list.value = Array.isArray(data) ? data : [];
|
||||
if (!options?.keepSelection) {
|
||||
// 切换上游接口时不默认选中第一条映射
|
||||
// 刷新 / 切换上游接口:清空选中
|
||||
const hadSelection = selectedId.value != null;
|
||||
selectedId.value = null;
|
||||
if (hadSelection) emit('row-click', null);
|
||||
} else if (
|
||||
selectedId.value !== null &&
|
||||
!list.value.some(
|
||||
@ -151,10 +156,12 @@
|
||||
)
|
||||
) {
|
||||
selectedId.value = null;
|
||||
emit('row-click', null);
|
||||
}
|
||||
} catch (e) {
|
||||
list.value = [];
|
||||
selectedId.value = null;
|
||||
emit('row-click', null);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -398,7 +405,7 @@
|
||||
:show-node-index="true"
|
||||
:show-node-code="true"
|
||||
@search="handleSearch"
|
||||
@refresh="() => loadList({ keepSelection: true })"
|
||||
@refresh="() => loadList({ keepSelection: false })"
|
||||
@add="handleAdd"
|
||||
@select="handleMenuSelect"
|
||||
@node-click="handleNodeClick"
|
||||
|
||||
@ -998,7 +998,17 @@
|
||||
}
|
||||
|
||||
function rowClick(row: any) {
|
||||
MapServiceProviderROW.value = row;
|
||||
MapServiceProviderROW.value = row || null;
|
||||
if (!row) {
|
||||
serviceProviderId.value = 0;
|
||||
serviceProviderName.value = '';
|
||||
mappingPanelReady.value = false;
|
||||
if (activeContentTab.value === 'mapping' || activeContentTab.value === 'debugger') {
|
||||
activeContentTab.value = 'detail';
|
||||
currentActiveTab.value = 'detail';
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 联动右侧映射明细:根据左侧选中映射行初始化 EnteredMap
|
||||
serviceProviderId.value = row.serviceProviderId;
|
||||
serviceProviderName.value = row.serviceProviderName;
|
||||
@ -1462,6 +1472,22 @@
|
||||
loadInterfaceTree();
|
||||
}
|
||||
|
||||
/** 刷新左侧列表:清空当前选中,回到未选中状态 */
|
||||
async function handleRefreshInterfaceTree() {
|
||||
selectedKeys.value = [];
|
||||
selectedInterface.value = null;
|
||||
upStreamIdRef.value = undefined;
|
||||
serviceProviderId.value = 0;
|
||||
serviceProviderName.value = '';
|
||||
MapServiceProviderROW.value = null;
|
||||
mappingPanelReady.value = false;
|
||||
if (activeContentTab.value === 'mapping' || activeContentTab.value === 'debugger') {
|
||||
activeContentTab.value = 'detail';
|
||||
currentActiveTab.value = 'detail';
|
||||
}
|
||||
await loadInterfaceTree();
|
||||
}
|
||||
|
||||
// 处理下拉菜单选择
|
||||
function handleActionSelect(value: string, option: any) {
|
||||
try {
|
||||
@ -1519,7 +1545,7 @@
|
||||
:actions-title="$t('system.interface.table.actions')"
|
||||
method-label="POST"
|
||||
@search="handleSearch"
|
||||
@refresh="loadInterfaceTree"
|
||||
@refresh="handleRefreshInterfaceTree"
|
||||
@add="interfaceClick"
|
||||
@select="handleInterfaceSelect"
|
||||
@expand="handleTreeExpand"
|
||||
|
||||
@ -494,8 +494,138 @@ onBeforeUnmount(() => {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
--vg-header-bg: var(--app-primary-color, rgb(var(--arcoblue-6)));
|
||||
/* vg-print 大量写死 #580df8;同步系统主色 + Element Plus 主色 */
|
||||
--vg-primary: var(--app-primary-color, rgb(var(--arcoblue-6)));
|
||||
--vg-primary-soft: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
--vg-header-bg: var(--vg-primary);
|
||||
--vg-header-text: #fff;
|
||||
--el-color-primary: var(--vg-primary);
|
||||
--el-color-primary-light-3: color-mix(in srgb, var(--app-primary-color) 70%, #fff);
|
||||
--el-color-primary-light-5: color-mix(in srgb, var(--app-primary-color) 50%, #fff);
|
||||
--el-color-primary-light-7: color-mix(in srgb, var(--app-primary-color) 30%, #fff);
|
||||
--el-color-primary-light-8: color-mix(in srgb, var(--app-primary-color) 20%, #fff);
|
||||
--el-color-primary-light-9: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
--el-color-primary-dark-2: var(--app-primary-color-hover, var(--app-primary-color));
|
||||
}
|
||||
|
||||
/* 顶栏 / 左栏标题 / 属性头 / 结构树头 / 历史头 */
|
||||
.ld-designer__body :deep(.primary-toolbar),
|
||||
.ld-designer__body :deep(.left-sider-header),
|
||||
.ld-designer__body :deep(.prop-header),
|
||||
.ld-designer__body :deep(.hip-struct-header),
|
||||
.ld-designer__body :deep(.hip-history-header) {
|
||||
background-color: var(--vg-primary) !important;
|
||||
background: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.primary-toolbar) {
|
||||
border-bottom-color: color-mix(in srgb, var(--app-primary-color) 40%, #dcdfe6) !important;
|
||||
}
|
||||
|
||||
/* 顶栏按钮:主色底上用浅色按钮,避免与主色 primary 按钮糊在一起 */
|
||||
.ld-designer__body :deep(.primary-toolbar .el-button) {
|
||||
--el-button-bg-color: rgba(255, 255, 255, 0.95);
|
||||
--el-button-border-color: rgba(255, 255, 255, 0.85);
|
||||
--el-button-text-color: var(--vg-primary);
|
||||
--el-button-hover-bg-color: #fff;
|
||||
--el-button-hover-border-color: #fff;
|
||||
--el-button-hover-text-color: var(--app-primary-color-hover, var(--vg-primary));
|
||||
--el-button-active-bg-color: #fff;
|
||||
--el-button-active-border-color: #fff;
|
||||
--el-button-active-text-color: var(--app-primary-color-hover, var(--vg-primary));
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.primary-toolbar .el-button--primary) {
|
||||
--el-button-bg-color: #fff;
|
||||
--el-button-border-color: #fff;
|
||||
--el-button-text-color: var(--vg-primary);
|
||||
--el-button-hover-bg-color: rgba(255, 255, 255, 0.92);
|
||||
--el-button-hover-border-color: #fff;
|
||||
--el-button-hover-text-color: var(--app-primary-color-hover, var(--vg-primary));
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.secondary-toolbar) {
|
||||
background-color: color-mix(in srgb, var(--app-primary-color) 6%, #fff) !important;
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.secondary-toolbar .scrollbar-track) {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, transparent) !important;
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.secondary-toolbar .scrollbar-thumb) {
|
||||
background-color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
/* 文字 / 勾选强调色 */
|
||||
.ld-designer__body :deep(.template-name),
|
||||
.ld-designer__body :deep(.hiprint-option-item-iconBtn),
|
||||
.ld-designer__body :deep(.template-item.is-active .template-check) {
|
||||
color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
/* 左侧模板选中态 */
|
||||
.ld-designer__body :deep(.template-item.is-active) {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--vg-primary-soft),
|
||||
#fff 60%
|
||||
) !important;
|
||||
border-color: var(--vg-primary) !important;
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 12%, transparent) !important;
|
||||
}
|
||||
|
||||
/* 画布缩放把手 / 表格把手 */
|
||||
.ld-designer__body :deep(.resize-handle:hover),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table-handle),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .n),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .s),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .e),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .w),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .ne),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .nw),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .se),
|
||||
.ld-designer__body :deep(.hiprint-printElement-table .resize-panel .sw) {
|
||||
background: var(--vg-primary) !important;
|
||||
background-color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
/* 左侧组件拖拽项(库内边框写死 #590df8) */
|
||||
.ld-designer__body :deep(.ep-draggable-item),
|
||||
.ld-designer__body :deep(.rect-printElement-types .hiprint-printElement-type > li > ul > li > a),
|
||||
.ld-designer__body :deep(.small-printElement-types .hiprint-printElement-type > li > ul > li > a) {
|
||||
border-color: var(--vg-primary) !important;
|
||||
color: var(--color-text-1, #303133) !important;
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.ep-draggable-item:hover),
|
||||
.ld-designer__body :deep(.rect-printElement-types .hiprint-printElement-type > li > ul > li > a:hover),
|
||||
.ld-designer__body :deep(.small-printElement-types .hiprint-printElement-type > li > ul > li > a:hover) {
|
||||
background: var(--vg-primary-soft) !important;
|
||||
border-color: var(--vg-primary) !important;
|
||||
color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
/* 左栏折叠钮(库内写死 #590df8) */
|
||||
.ld-designer__body :deep(.left-sider-toggle) {
|
||||
background: var(--vg-primary) !important;
|
||||
background-color: var(--vg-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* 属性 Tab / 分页选中(库内 #2196f3) */
|
||||
.ld-designer__body :deep(.prop-tabs .prop-tab-items li.active) {
|
||||
border-bottom-color: var(--vg-primary) !important;
|
||||
color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
.ld-designer__body :deep(.hiprint-pagination .selected) {
|
||||
border-color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
/* 移动端 sheet tab(库内 #409eff) */
|
||||
.ld-designer__body :deep(.mobile-bottom-sheet .mobile-sheet-tabs .tab-item.active) {
|
||||
color: var(--vg-primary) !important;
|
||||
border-bottom-color: var(--vg-primary) !important;
|
||||
}
|
||||
|
||||
.ld-spin {
|
||||
|
||||
@ -25,7 +25,18 @@
|
||||
@add="openCreateModal"
|
||||
@select="handleMenuSelect"
|
||||
@delete-node="handleDeleteNode"
|
||||
/>
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption @click="openRenameModal(nodeData)">
|
||||
<icon-edit />
|
||||
{{ $t('channel.labelDesign.actions.editTemplate') }}
|
||||
</a-doption>
|
||||
<a-doption @click="handleDeleteNode(nodeData)">
|
||||
<icon-delete />
|
||||
{{ $t('channel.labelDesign.actions.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
@ -82,6 +93,45 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="renameModalVisible"
|
||||
:title="$t('channel.labelDesign.actions.editTemplate')"
|
||||
:ok-text="$t('channel.labelDesign.actions.confirm')"
|
||||
:cancel-text="$t('channel.labelDesign.actions.cancel')"
|
||||
unmount-on-close
|
||||
:on-before-ok="confirmRename"
|
||||
>
|
||||
<a-form
|
||||
:model="renameForm"
|
||||
layout="horizontal"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item
|
||||
field="name"
|
||||
:label="$t('channel.labelDesign.table.name')"
|
||||
:rules="[{ required: true, message: $t('channel.labelDesign.message.nameRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="renameForm.name"
|
||||
allow-clear
|
||||
:placeholder="$t('channel.labelDesign.namePlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="templateType"
|
||||
:label="$t('channel.labelDesign.table.type')"
|
||||
:rules="[{ required: true, message: $t('channel.labelDesign.message.typeRequired') }]"
|
||||
>
|
||||
<a-select v-model="renameForm.templateType">
|
||||
<a-option v-for="opt in typeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -90,12 +140,14 @@ import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'
|
||||
import { h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Modal, Message, Spin } from '@arco-design/web-vue'
|
||||
import { IconDelete, IconEdit } from '@arco-design/web-vue/es/icon'
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue'
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types'
|
||||
import {
|
||||
deleteLabelTemplate,
|
||||
getLabelTemplate,
|
||||
getLabelTemplateList,
|
||||
saveLabelTemplate,
|
||||
type LabelTemplateListItem,
|
||||
} from '@/api/channel/labelTemplate'
|
||||
|
||||
@ -126,6 +178,12 @@ const createForm = reactive({
|
||||
name: '',
|
||||
templateType: 'lastMile',
|
||||
})
|
||||
const renameModalVisible = ref(false)
|
||||
const renameForm = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
templateType: 'lastMile',
|
||||
})
|
||||
const editing = reactive<{
|
||||
id?: number
|
||||
name?: string
|
||||
@ -261,6 +319,49 @@ function handleMenuSelect(keys: string[]) {
|
||||
openTemplate(id)
|
||||
}
|
||||
|
||||
function openRenameModal(nodeData: any) {
|
||||
const id = Number(nodeData?.id)
|
||||
if (!id) return
|
||||
renameForm.id = id
|
||||
renameForm.name = String(nodeData?.name || nodeData?.title || '')
|
||||
renameForm.templateType = String(nodeData?.templateType || 'lastMile')
|
||||
renameModalVisible.value = true
|
||||
}
|
||||
|
||||
async function confirmRename() {
|
||||
const id = renameForm.id
|
||||
const name = renameForm.name?.trim()
|
||||
const templateType = renameForm.templateType?.trim() || 'lastMile'
|
||||
if (!id) return false
|
||||
if (!name) {
|
||||
Message.warning(t('channel.labelDesign.message.nameRequired'))
|
||||
return false
|
||||
}
|
||||
if (!templateType) {
|
||||
Message.warning(t('channel.labelDesign.message.typeRequired'))
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const detail = await getLabelTemplate(id)
|
||||
await saveLabelTemplate({
|
||||
id,
|
||||
name,
|
||||
templateType,
|
||||
templateJson: detail.templateJson,
|
||||
})
|
||||
Message.success(t('channel.labelDesign.message.editSuccess'))
|
||||
if (editing.id === id) {
|
||||
editing.name = name
|
||||
editing.templateType = templateType
|
||||
}
|
||||
await loadList()
|
||||
return true
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('channel.labelDesign.message.editFailed'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteNode(nodeData: any) {
|
||||
const id = Number(nodeData?.id)
|
||||
const name = String(nodeData?.name || nodeData?.title || '')
|
||||
|
||||
@ -367,9 +367,27 @@
|
||||
enterBeginsEditing: true,
|
||||
fragmentSelection: false,
|
||||
selectionMode: 'multiple',
|
||||
// 点表格外的工具栏/弹窗按钮时不要清选中,否则「批量填价」等会在 mousedown 时先丢选中再点不到
|
||||
outsideClickDeselects(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement | null;
|
||||
return !target?.closest('.sales-price-config-modal, .arco-modal, .arco-trigger-popup');
|
||||
if (!target) return true;
|
||||
if (
|
||||
target.closest(
|
||||
[
|
||||
'.price-list-actions',
|
||||
'.price-list-header',
|
||||
'.price-list-content',
|
||||
'.sales-price-config-modal',
|
||||
'.arco-modal',
|
||||
'.arco-modal-wrapper',
|
||||
'.arco-trigger-popup',
|
||||
'.arco-btn',
|
||||
].join(','),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
allowInsertRow: false,
|
||||
allowInsertColumn: false,
|
||||
|
||||
@ -3243,10 +3243,12 @@
|
||||
/** 当前价格表行对应的客户组编码顺序(用于同步时按编码保留价格) */
|
||||
const priceListCustomerCodesSnapshot = ref<string[]>([]);
|
||||
const priceListSelectedIds = ref<number[]>([]);
|
||||
/** 上次非空选中(点工具栏时 HOT 可能先清空当前选中) */
|
||||
const lastPriceListSelectedIds = ref<number[]>([]);
|
||||
const batchFillTargetIds = ref<number[]>([]);
|
||||
const priceListHotRef = ref<{ selectAll: () => void; clearSelection: () => void } | null>(null);
|
||||
const batchFillModalVisible = ref(false);
|
||||
const batchFillValue = ref<number | undefined>(undefined);
|
||||
const batchFillOnlyEmpty = ref(false);
|
||||
|
||||
function buildPriceRowFactorKey(row: any, factorColKeys: string[]): string {
|
||||
return factorColKeys.map((k) => String(row?.[k] ?? '').trim()).join('\u0001');
|
||||
@ -3841,11 +3843,20 @@
|
||||
);
|
||||
|
||||
function onPriceListSelectionChange(ids: Array<number | string>) {
|
||||
priceListSelectedIds.value = ids.map((id) => Number(id)).filter((id) => Number.isFinite(id));
|
||||
const next = ids.map((id) => Number(id)).filter((id) => Number.isFinite(id));
|
||||
priceListSelectedIds.value = next;
|
||||
if (next.length) lastPriceListSelectedIds.value = next;
|
||||
}
|
||||
|
||||
function resolvePriceListSelectedIds() {
|
||||
if (priceListSelectedIds.value.length) return [...priceListSelectedIds.value];
|
||||
return [...lastPriceListSelectedIds.value];
|
||||
}
|
||||
|
||||
function clearPriceListChecked() {
|
||||
priceListSelectedIds.value = [];
|
||||
lastPriceListSelectedIds.value = [];
|
||||
batchFillTargetIds.value = [];
|
||||
try {
|
||||
priceListHotRef.value?.clearSelection?.();
|
||||
} catch {
|
||||
@ -3869,12 +3880,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const openBatchFillModal = (onlyEmpty = false) => {
|
||||
if (!priceListSelectedIds.value.length) {
|
||||
const openBatchFillModal = () => {
|
||||
const ids = resolvePriceListSelectedIds();
|
||||
if (!ids.length) {
|
||||
Message.warning('请先选中要填价的行');
|
||||
return;
|
||||
}
|
||||
batchFillOnlyEmpty.value = onlyEmpty;
|
||||
batchFillTargetIds.value = ids;
|
||||
batchFillValue.value = undefined;
|
||||
batchFillModalVisible.value = true;
|
||||
};
|
||||
@ -3885,7 +3897,11 @@
|
||||
Message.warning('请输入有效价格(≥0)');
|
||||
return;
|
||||
}
|
||||
const selected = new Set(priceListSelectedIds.value);
|
||||
const selected = new Set(
|
||||
batchFillTargetIds.value.length
|
||||
? batchFillTargetIds.value
|
||||
: resolvePriceListSelectedIds(),
|
||||
);
|
||||
const customerKeys = priceTableHeaders.value.filter((h) => h.isCustomer).map((h) => h.key);
|
||||
if (!customerKeys.length) {
|
||||
Message.warning('当前没有客户组价格列');
|
||||
@ -3895,41 +3911,17 @@
|
||||
for (const row of priceListData.value) {
|
||||
if (!selected.has(row.id)) continue;
|
||||
for (const key of customerKeys) {
|
||||
const cur = Number((row as any)[key]);
|
||||
const isEmpty = !Number.isFinite(cur) || cur === 0;
|
||||
if (batchFillOnlyEmpty.value && !isEmpty) continue;
|
||||
(row as any)[key] = val;
|
||||
touched += 1;
|
||||
}
|
||||
}
|
||||
batchFillModalVisible.value = false;
|
||||
batchFillTargetIds.value = [];
|
||||
// 触发 Handsontable 刷新可视区(数值已写回源对象)
|
||||
touchPriceListData();
|
||||
Message.success(touched ? `已填充 ${touched} 个价格格` : '没有需要填充的格子');
|
||||
};
|
||||
|
||||
/** 将上一行客户组价格复制到选中行 */
|
||||
const handleCopyPrevRowPrices = () => {
|
||||
if (!priceListSelectedIds.value.length) {
|
||||
Message.warning('请先选中目标行');
|
||||
return;
|
||||
}
|
||||
const selected = new Set(priceListSelectedIds.value);
|
||||
const customerKeys = priceTableHeaders.value.filter((h) => h.isCustomer).map((h) => h.key);
|
||||
let copied = 0;
|
||||
for (let i = 0; i < priceListData.value.length; i++) {
|
||||
const row = priceListData.value[i];
|
||||
if (!selected.has(row.id) || i === 0) continue;
|
||||
const prev = priceListData.value[i - 1];
|
||||
for (const key of customerKeys) {
|
||||
(row as any)[key] = Number((prev as any)[key]) || 0;
|
||||
}
|
||||
copied += 1;
|
||||
}
|
||||
if (copied) touchPriceListData();
|
||||
Message.success(copied ? `已将上一行价格复制到 ${copied} 行` : '没有可复制的行');
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancelPriceDrawer = () => {
|
||||
priceModalVisible.value = false;
|
||||
@ -5519,25 +5511,11 @@
|
||||
<span class="price-list-actions__sep" aria-hidden="true"></span>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="openBatchFillModal(false)"
|
||||
:disabled="!priceListSelectedIds.length && !lastPriceListSelectedIds.length"
|
||||
@mousedown.prevent="openBatchFillModal"
|
||||
>
|
||||
{{ t('channel.salesProducts.drawer.priceList.batchFill') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="openBatchFillModal(true)"
|
||||
>
|
||||
{{ t('channel.salesProducts.drawer.priceList.batchFillEmpty') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="handleCopyPrevRowPrices"
|
||||
>
|
||||
{{ t('channel.salesProducts.drawer.priceList.copyPrev') }}
|
||||
</a-button>
|
||||
<span class="price-list-actions__sep" aria-hidden="true"></span>
|
||||
<a-button size="small" @click="handleExportPriceList">导出</a-button>
|
||||
<a-button size="small" @click="handleImportPriceList">导入</a-button>
|
||||
@ -5593,19 +5571,19 @@
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 批量填价 -->
|
||||
<!-- 批量填价:须高于价格配置大弹窗,否则会被盖住看起来像没弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="batchFillModalVisible"
|
||||
:title="batchFillOnlyEmpty
|
||||
? t('channel.salesProducts.drawer.priceList.batchFillEmpty')
|
||||
: t('channel.salesProducts.drawer.priceList.batchFill')"
|
||||
:title="t('channel.salesProducts.drawer.priceList.batchFill')"
|
||||
width="420px"
|
||||
:z-index="3200"
|
||||
unmount-on-close
|
||||
@ok="handleConfirmBatchFill"
|
||||
@cancel="batchFillModalVisible = false"
|
||||
>
|
||||
<div class="batch-fill-form">
|
||||
<div class="batch-fill-meta">
|
||||
{{ t('channel.salesProducts.drawer.priceList.batchFillSelected', { count: priceListSelectedIds.length }) }}
|
||||
{{ t('channel.salesProducts.drawer.priceList.batchFillSelected', { count: batchFillTargetIds.length || priceListSelectedIds.length }) }}
|
||||
</div>
|
||||
<a-form-item :label="t('channel.salesProducts.drawer.priceList.batchFillValue')" required>
|
||||
<a-input-number
|
||||
|
||||
@ -787,25 +787,11 @@
|
||||
<span class="price-list-actions__sep" aria-hidden="true"></span>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="openBatchFillModal(false)"
|
||||
:disabled="!priceListSelectedIds.length && !lastPriceListSelectedIds.length"
|
||||
@mousedown.prevent="openBatchFillModal"
|
||||
>
|
||||
{{ t('channel.serviceChannels.drawer.priceList.batchFill') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="openBatchFillModal(true)"
|
||||
>
|
||||
{{ t('channel.serviceChannels.drawer.priceList.batchFillEmpty') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!priceListSelectedIds.length"
|
||||
@click="handleCopyPrevRowPrices"
|
||||
>
|
||||
{{ t('channel.serviceChannels.drawer.priceList.copyPrev') }}
|
||||
</a-button>
|
||||
<span class="price-list-actions__sep" aria-hidden="true"></span>
|
||||
<a-button size="small" @click="handleExportPriceList">导出</a-button>
|
||||
<a-button size="small" @click="handleImportPriceList">导入</a-button>
|
||||
@ -863,19 +849,19 @@
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 批量填价 -->
|
||||
<!-- 批量填价:须高于价格配置大弹窗,否则会被盖住看起来像没弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="batchFillModalVisible"
|
||||
:title="batchFillOnlyEmpty
|
||||
? t('channel.serviceChannels.drawer.priceList.batchFillEmpty')
|
||||
: t('channel.serviceChannels.drawer.priceList.batchFill')"
|
||||
:title="t('channel.serviceChannels.drawer.priceList.batchFill')"
|
||||
width="420px"
|
||||
:z-index="3200"
|
||||
unmount-on-close
|
||||
@ok="handleConfirmBatchFill"
|
||||
@cancel="batchFillModalVisible = false"
|
||||
>
|
||||
<div class="batch-fill-form">
|
||||
<div class="batch-fill-meta">
|
||||
{{ t('channel.serviceChannels.drawer.priceList.batchFillSelected', { count: priceListSelectedIds.length }) }}
|
||||
{{ t('channel.serviceChannels.drawer.priceList.batchFillSelected', { count: batchFillTargetIds.length || priceListSelectedIds.length }) }}
|
||||
</div>
|
||||
<a-form-item :label="t('channel.serviceChannels.drawer.priceList.batchFillValue')" required>
|
||||
<a-input-number
|
||||
@ -2753,10 +2739,12 @@ function buildServiceChannelDetailsPayload(): any[] {
|
||||
// 价格表数据:shallowRef + 普通对象,避免上千行 deep 响应式拖垮 Handsontable
|
||||
const priceListData = shallowRef<any[]>([])
|
||||
const priceListSelectedIds = ref<number[]>([])
|
||||
/** 上次非空选中(点工具栏时 HOT 可能先清空当前选中) */
|
||||
const lastPriceListSelectedIds = ref<number[]>([])
|
||||
const batchFillTargetIds = ref<number[]>([])
|
||||
const priceListHotRef = ref<{ selectAll: () => void; clearSelection: () => void } | null>(null)
|
||||
const batchFillModalVisible = ref(false)
|
||||
const batchFillValue = ref<number | undefined>(undefined)
|
||||
const batchFillOnlyEmpty = ref(false)
|
||||
|
||||
function replacePriceListData(rows: any[]) {
|
||||
priceListData.value = (rows || []).map((row) => ({
|
||||
@ -2772,11 +2760,20 @@ function touchPriceListData() {
|
||||
|
||||
function clearPriceListChecked() {
|
||||
priceListSelectedIds.value = []
|
||||
lastPriceListSelectedIds.value = []
|
||||
batchFillTargetIds.value = []
|
||||
priceListHotRef.value?.clearSelection()
|
||||
}
|
||||
|
||||
function onPriceListSelectionChange(ids: Array<number | string>) {
|
||||
priceListSelectedIds.value = ids.map((id) => Number(id)).filter((id) => Number.isFinite(id))
|
||||
const next = ids.map((id) => Number(id)).filter((id) => Number.isFinite(id))
|
||||
priceListSelectedIds.value = next
|
||||
if (next.length) lastPriceListSelectedIds.value = next
|
||||
}
|
||||
|
||||
function resolvePriceListSelectedIds() {
|
||||
if (priceListSelectedIds.value.length) return [...priceListSelectedIds.value]
|
||||
return [...lastPriceListSelectedIds.value]
|
||||
}
|
||||
|
||||
function selectAllPriceListRows() {
|
||||
@ -3395,12 +3392,13 @@ const handleGeneratePriceList = async () => {
|
||||
applySyncedPriceList(preview)
|
||||
}
|
||||
|
||||
const openBatchFillModal = (onlyEmpty = false) => {
|
||||
if (!priceListSelectedIds.value.length) {
|
||||
const openBatchFillModal = () => {
|
||||
const ids = resolvePriceListSelectedIds()
|
||||
if (!ids.length) {
|
||||
Message.warning('请先选中要填价的行')
|
||||
return
|
||||
}
|
||||
batchFillOnlyEmpty.value = onlyEmpty
|
||||
batchFillTargetIds.value = ids
|
||||
batchFillValue.value = undefined
|
||||
batchFillModalVisible.value = true
|
||||
}
|
||||
@ -3411,40 +3409,23 @@ const handleConfirmBatchFill = () => {
|
||||
Message.warning('请输入有效价格(≥0)')
|
||||
return
|
||||
}
|
||||
const selected = new Set(priceListSelectedIds.value)
|
||||
const selected = new Set(
|
||||
batchFillTargetIds.value.length
|
||||
? batchFillTargetIds.value
|
||||
: resolvePriceListSelectedIds(),
|
||||
)
|
||||
let touched = 0
|
||||
for (const row of priceListData.value) {
|
||||
if (!selected.has(row.id)) continue
|
||||
const cur = Number(row.calculatedValue)
|
||||
const isEmpty = !Number.isFinite(cur) || cur === 0
|
||||
if (batchFillOnlyEmpty.value && !isEmpty) continue
|
||||
row.calculatedValue = val
|
||||
touched += 1
|
||||
}
|
||||
batchFillModalVisible.value = false
|
||||
batchFillTargetIds.value = []
|
||||
touchPriceListData()
|
||||
Message.success(touched ? `已填充 ${touched} 行单价` : '没有需要填充的行')
|
||||
}
|
||||
|
||||
/** 将上一行单价复制到勾选行 */
|
||||
const handleCopyPrevRowPrices = () => {
|
||||
if (!priceListSelectedIds.value.length) {
|
||||
Message.warning('请先选中目标行')
|
||||
return
|
||||
}
|
||||
const selected = new Set(priceListSelectedIds.value)
|
||||
let copied = 0
|
||||
for (let i = 0; i < priceListData.value.length; i++) {
|
||||
const row = priceListData.value[i]
|
||||
if (!selected.has(row.id) || i === 0) continue
|
||||
const prev = priceListData.value[i - 1]
|
||||
row.calculatedValue = Number(prev.calculatedValue) || 0
|
||||
copied += 1
|
||||
}
|
||||
touchPriceListData()
|
||||
Message.success(copied ? `已将上一行单价复制到 ${copied} 行` : '没有可复制的行')
|
||||
}
|
||||
|
||||
const savingPriceDrawer = ref(false)
|
||||
|
||||
function nextServiceChannelPriceSerialNumber(): number {
|
||||
|
||||
@ -31,13 +31,9 @@
|
||||
type="primary"
|
||||
class="ops-btn ops-btn--primary"
|
||||
:loading="submitting || lookingUp"
|
||||
@click="formData.orderId ? handleSubmit() : handleWaybillLookup()"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
{{
|
||||
formData.orderId
|
||||
? $t('operation.inbound.actions.inbound')
|
||||
: $t('operation.inbound.actions.lookup')
|
||||
}}
|
||||
{{ $t('operation.inbound.actions.inbound') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -149,7 +145,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
@ -341,6 +337,8 @@ function handlePageSizeChange(size: number) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const lookedUpWaybillNo = ref('')
|
||||
|
||||
function clearLookupFields() {
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
@ -349,6 +347,7 @@ function clearLookupFields() {
|
||||
formData.length = ''
|
||||
formData.width = ''
|
||||
formData.height = ''
|
||||
lookedUpWaybillNo.value = ''
|
||||
}
|
||||
|
||||
function clearLookup() {
|
||||
@ -356,11 +355,19 @@ function clearLookup() {
|
||||
clearFeedback()
|
||||
}
|
||||
|
||||
async function handleWaybillLookup() {
|
||||
function hasMetrics() {
|
||||
return !!(formData.weight && formData.length && formData.width && formData.height)
|
||||
}
|
||||
|
||||
/** 扫码后静默查单;不弹「匹配成功」,失败才提示 */
|
||||
async function ensureOrderLookup(): Promise<boolean> {
|
||||
const no = formData.waybillNumber?.trim()
|
||||
if (!no) {
|
||||
clearLookup()
|
||||
return
|
||||
return false
|
||||
}
|
||||
if (formData.orderId && lookedUpWaybillNo.value === no) {
|
||||
return true
|
||||
}
|
||||
lookingUp.value = true
|
||||
try {
|
||||
@ -372,24 +379,29 @@ async function handleWaybillLookup() {
|
||||
formData.salesProductName = info.salesProductName || info.productCode || ''
|
||||
formData.destinationCountryName =
|
||||
info.destinationCountryName || info.destinationCountry || ''
|
||||
showSuccess(t('operation.common.message.lookupSuccess'))
|
||||
lookedUpWaybillNo.value = no
|
||||
return true
|
||||
} catch (e: any) {
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
formData.orderId = undefined
|
||||
lookedUpWaybillNo.value = ''
|
||||
showError(e?.message || t('operation.common.message.orderNotFound'))
|
||||
maybeRefocusScan()
|
||||
return false
|
||||
} finally {
|
||||
lookingUp.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onScanEnter() {
|
||||
if (formData.orderId && formData.waybillNumber?.trim()) {
|
||||
const ok = await ensureOrderLookup()
|
||||
if (!ok) return
|
||||
if (hasMetrics()) {
|
||||
await handleSubmit()
|
||||
return
|
||||
}
|
||||
await handleWaybillLookup()
|
||||
clearFeedback()
|
||||
}
|
||||
|
||||
function buildSuccessMessage(result: any) {
|
||||
@ -410,11 +422,9 @@ async function handleSubmit() {
|
||||
maybeRefocusScan()
|
||||
return
|
||||
}
|
||||
if (!formData.orderId) {
|
||||
await handleWaybillLookup()
|
||||
if (!formData.orderId) return
|
||||
}
|
||||
if (!formData.weight || !formData.length || !formData.width || !formData.height) {
|
||||
const ok = await ensureOrderLookup()
|
||||
if (!ok) return
|
||||
if (!hasMetrics()) {
|
||||
showWarning(t('operation.inbound.message.metricsRequired'))
|
||||
return
|
||||
}
|
||||
@ -443,6 +453,19 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.waybillNumber,
|
||||
(value) => {
|
||||
const no = String(value || '').trim()
|
||||
if (!no || no !== lookedUpWaybillNo.value) {
|
||||
formData.orderId = undefined
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
lookedUpWaybillNo.value = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function handleReset(clearStatus = true) {
|
||||
formData.waybillNumber = ''
|
||||
clearLookupFields()
|
||||
|
||||
@ -31,13 +31,9 @@
|
||||
type="primary"
|
||||
class="ops-btn ops-btn--primary"
|
||||
:loading="submitting || lookingUp"
|
||||
@click="formData.orderId ? handleSubmit() : handleWaybillLookup()"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
{{
|
||||
formData.orderId
|
||||
? $t('operation.outbound.actions.outbound')
|
||||
: $t('operation.outbound.actions.lookup')
|
||||
}}
|
||||
{{ $t('operation.outbound.actions.outbound') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -150,7 +146,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
@ -272,6 +268,8 @@ function handlePageSizeChange(size: number) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const lookedUpWaybillNo = ref('')
|
||||
|
||||
function clearLookupFields() {
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
@ -281,6 +279,7 @@ function clearLookupFields() {
|
||||
formData.length = ''
|
||||
formData.width = ''
|
||||
formData.height = ''
|
||||
lookedUpWaybillNo.value = ''
|
||||
}
|
||||
|
||||
function clearLookup() {
|
||||
@ -288,11 +287,19 @@ function clearLookup() {
|
||||
clearFeedback()
|
||||
}
|
||||
|
||||
async function handleWaybillLookup() {
|
||||
function hasMetrics() {
|
||||
return !!(formData.weight && formData.length && formData.width && formData.height)
|
||||
}
|
||||
|
||||
/** 扫码后静默查单;不弹「匹配成功」,失败才提示 */
|
||||
async function ensureOrderLookup(): Promise<boolean> {
|
||||
const no = formData.waybillNumber?.trim()
|
||||
if (!no) {
|
||||
clearLookup()
|
||||
return
|
||||
return false
|
||||
}
|
||||
if (formData.orderId && lookedUpWaybillNo.value === no) {
|
||||
return true
|
||||
}
|
||||
lookingUp.value = true
|
||||
try {
|
||||
@ -305,25 +312,30 @@ async function handleWaybillLookup() {
|
||||
formData.destinationCountryName =
|
||||
info.destinationCountryName || info.destinationCountry || ''
|
||||
formData.serviceChannelName = info.serviceChannelName || info.channelCode || ''
|
||||
showSuccess(t('operation.common.message.lookupSuccess'))
|
||||
lookedUpWaybillNo.value = no
|
||||
return true
|
||||
} catch (e: any) {
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
formData.serviceChannelName = ''
|
||||
formData.orderId = undefined
|
||||
lookedUpWaybillNo.value = ''
|
||||
showError(e?.message || t('operation.common.message.orderNotFound'))
|
||||
maybeRefocusScan()
|
||||
return false
|
||||
} finally {
|
||||
lookingUp.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onScanEnter() {
|
||||
if (formData.orderId && formData.waybillNumber?.trim()) {
|
||||
const ok = await ensureOrderLookup()
|
||||
if (!ok) return
|
||||
if (hasMetrics()) {
|
||||
await handleSubmit()
|
||||
return
|
||||
}
|
||||
await handleWaybillLookup()
|
||||
clearFeedback()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
@ -332,9 +344,11 @@ async function handleSubmit() {
|
||||
maybeRefocusScan()
|
||||
return
|
||||
}
|
||||
if (!formData.orderId) {
|
||||
await handleWaybillLookup()
|
||||
if (!formData.orderId) return
|
||||
const ok = await ensureOrderLookup()
|
||||
if (!ok) return
|
||||
if (!hasMetrics()) {
|
||||
showWarning(t('operation.outbound.message.metricsRequired'))
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
@ -357,6 +371,20 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.waybillNumber,
|
||||
(value) => {
|
||||
const no = String(value || '').trim()
|
||||
if (!no || no !== lookedUpWaybillNo.value) {
|
||||
formData.orderId = undefined
|
||||
formData.salesProductName = ''
|
||||
formData.destinationCountryName = ''
|
||||
formData.serviceChannelName = ''
|
||||
lookedUpWaybillNo.value = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function handleReset(clearStatus = true) {
|
||||
formData.waybillNumber = ''
|
||||
clearLookupFields()
|
||||
|
||||
@ -1,159 +1,419 @@
|
||||
<template>
|
||||
<div class="order-declaration-hot-table">
|
||||
<HandsontablePro
|
||||
:key="tableKey"
|
||||
:columns="hotColumns"
|
||||
:data="data"
|
||||
:total="data.length"
|
||||
:current-page="1"
|
||||
:page-size="Math.max(data.length, 1)"
|
||||
:height="tableHeight"
|
||||
:read-only="!editing"
|
||||
:show-action-column="editing"
|
||||
:action-column-header="actionHeader"
|
||||
:action-renderer="actionRenderer"
|
||||
:serial-no-header="serialHeader"
|
||||
:custom-cell-renderer="customCellRenderer"
|
||||
:stretch-columns="true"
|
||||
:enable-header-search="false"
|
||||
:auto-hybrid-header="false"
|
||||
:enable-header-sort="false"
|
||||
:enable-header-filter="false"
|
||||
:show-active-filter-bar="false"
|
||||
:enable-column-filter="false"
|
||||
@action-click="handleActionClick"
|
||||
@cell-click="handleCellClick"
|
||||
<div
|
||||
ref="wrapRef"
|
||||
class="order-declaration-hot-wrap"
|
||||
:style="{ height: `${tableHeight}px` }"
|
||||
>
|
||||
<div
|
||||
ref="hostRef"
|
||||
class="order-declaration-hot"
|
||||
:class="{ 'is-ready': ready }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import type { ColumnDef } from '@/components/HandsontablePro/types'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import { ORDER_DETAIL_DECLARATION_COLUMNS } from '@/views/order/components/orderDetailFieldConfig'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Handsontable from 'handsontable';
|
||||
import { registerAllModules } from 'handsontable/registry';
|
||||
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n';
|
||||
import 'handsontable/styles/handsontable.min.css';
|
||||
import 'handsontable/styles/ht-theme-classic.min.css';
|
||||
import { ORDER_DETAIL_DECLARATION_COLUMNS } from '@/views/order/components/orderDetailFieldConfig';
|
||||
|
||||
const NUMBER_KEYS = new Set(['unitPrice', 'unitWeight', 'quantity', 'amount', 'weight'])
|
||||
registerAllModules();
|
||||
registerLanguageDictionary(zhCN);
|
||||
|
||||
const NUMBER_KEYS = new Set(['unitPrice', 'unitWeight', 'quantity', 'amount', 'weight']);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: any[]
|
||||
editing?: boolean
|
||||
data: any[];
|
||||
editing?: boolean;
|
||||
}>(),
|
||||
{
|
||||
editing: false,
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'remove-row': [rowIndex: number]
|
||||
'preview-sku-image': [url: string]
|
||||
}>()
|
||||
'remove-row': [rowIndex: number];
|
||||
'preview-sku-image': [url: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n();
|
||||
|
||||
const tableKey = computed(() => `decl-${props.editing ? 'edit' : 'view'}-${props.data.length}`)
|
||||
|
||||
const serialHeader = computed(() => t('order.myOrders.table.serialNo'))
|
||||
const actionHeader = computed(() => t('order.myOrders.drawer.declaration.action'))
|
||||
|
||||
const hotColumns = computed<ColumnDef[]>(() =>
|
||||
ORDER_DETAIL_DECLARATION_COLUMNS.map((col) => {
|
||||
const base: ColumnDef = {
|
||||
key: col.key,
|
||||
label: t(col.labelKey),
|
||||
width: col.key === 'skuImage' ? (props.editing ? 160 : 72) : col.width,
|
||||
searchable: false,
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
}
|
||||
if (col.key === 'isMagnetic') {
|
||||
base.type = 'checkbox'
|
||||
} else if (NUMBER_KEYS.has(col.key)) {
|
||||
base.type = 'numeric'
|
||||
}
|
||||
if (col.key === 'skuImage') {
|
||||
base.className = 'htCenter declaration-sku-cell'
|
||||
// 查看态只读,避免把长 URL 当文本画进单元格再被 ellipsis 裁成 ...
|
||||
if (!props.editing) base.readOnly = true
|
||||
}
|
||||
return base
|
||||
}),
|
||||
)
|
||||
const wrapRef = ref<HTMLElement | null>(null);
|
||||
const hostRef = ref<HTMLElement | null>(null);
|
||||
const ready = ref(false);
|
||||
let hot: Handsontable | null = null;
|
||||
let ignoreChange = false;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let lastWidth = 0;
|
||||
let resizeRaf = 0;
|
||||
let mountTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const tableHeight = computed(() => {
|
||||
const rows = Math.max(props.data.length, 1)
|
||||
return Math.min(280, Math.max(120, 40 + rows * 32 + 16))
|
||||
})
|
||||
const rows = Math.max(props.data?.length || 0, 1);
|
||||
return Math.min(280, Math.max(120, 40 + rows * 32 + 16));
|
||||
});
|
||||
|
||||
function hasSkuImage(raw: unknown) {
|
||||
const value = String(raw ?? '').trim()
|
||||
return !!value && value !== '0' && value !== '--'
|
||||
const value = String(raw ?? '').trim();
|
||||
return !!value && value !== '0' && value !== '--';
|
||||
}
|
||||
|
||||
function escapeAttr(text: string) {
|
||||
return String(text).replace(/"/g, '"')
|
||||
return String(text).replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function eyeSvg() {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;display:block;pointer-events:none"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;display:block;pointer-events:none"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看态图片列:只渲染眼睛,不写入长 URL 文本。
|
||||
* HandsontablePro 只读态全局带 text-overflow:ellipsis,长链会被裁成「...」叠在眼睛旁。
|
||||
*/
|
||||
function customCellRenderer(
|
||||
_row: number,
|
||||
col: number,
|
||||
_value: any,
|
||||
rowData: any,
|
||||
): string | null {
|
||||
if (props.editing) return null
|
||||
// 布局:操作(可选) → 序号 → 数据列
|
||||
const dataColStart = props.editing ? 2 : 1
|
||||
const colDef = hotColumns.value[col - dataColStart]
|
||||
if (colDef?.key !== 'skuImage') return null
|
||||
if (!hasSkuImage(rowData?.skuImage)) return '--'
|
||||
const title = t('order.myOrders.drawer.declarationSkuImageView')
|
||||
return `<span class="declaration-sku-eye" title="${escapeAttr(title)}" role="button">${eyeSvg()}</span>`
|
||||
function decorateRows(rows: any[]): any[] {
|
||||
return (rows || []).map((row, idx) => {
|
||||
if (row && typeof row === 'object') {
|
||||
row._serial = idx + 1;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function actionRenderer(_row: any, rowIndex: number) {
|
||||
return wrapWarehouseActions(
|
||||
[renderWarehouseActionBtn('delete', t('order.myOrders.drawer.declaration.delete'))],
|
||||
rowIndex,
|
||||
)
|
||||
function readWrapWidth() {
|
||||
const el = wrapRef.value;
|
||||
if (!el) return 0;
|
||||
const w = Math.floor(el.clientWidth || el.getBoundingClientRect().width);
|
||||
return w > 0 ? w : 0;
|
||||
}
|
||||
|
||||
function handleActionClick(_row: any, rowIndex: number, action?: string) {
|
||||
if (action === 'delete' || !action) {
|
||||
emit('remove-row', rowIndex)
|
||||
function buildColHeaders(): string[] {
|
||||
const headers = [t('order.myOrders.table.serialNo')];
|
||||
if (props.editing) {
|
||||
headers.push(t('order.myOrders.drawer.declaration.action'));
|
||||
}
|
||||
headers.push(...ORDER_DETAIL_DECLARATION_COLUMNS.map((col) => t(col.labelKey)));
|
||||
return headers;
|
||||
}
|
||||
|
||||
function buildColumns(): any[] {
|
||||
const cols: any[] = [
|
||||
{
|
||||
data: '_serial',
|
||||
readOnly: true,
|
||||
width: 50,
|
||||
className: 'htCenter',
|
||||
},
|
||||
];
|
||||
|
||||
if (props.editing) {
|
||||
cols.push({
|
||||
data: '_action',
|
||||
readOnly: true,
|
||||
width: 64,
|
||||
className: 'htCenter declaration-action-cell',
|
||||
renderer(
|
||||
_instance: Handsontable,
|
||||
td: HTMLTableCellElement,
|
||||
row: number,
|
||||
) {
|
||||
td.className = 'htCenter declaration-action-cell';
|
||||
td.innerHTML = `<button type="button" class="declaration-del-btn" data-row="${row}">${t(
|
||||
'order.myOrders.drawer.declaration.delete',
|
||||
)}</button>`;
|
||||
return td;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const col of ORDER_DETAIL_DECLARATION_COLUMNS) {
|
||||
const conf: any = {
|
||||
data: col.key,
|
||||
readOnly: !props.editing,
|
||||
width: col.key === 'skuImage' ? (props.editing ? 120 : 72) : col.width || 100,
|
||||
className: 'htLeft',
|
||||
};
|
||||
|
||||
if (col.key === 'isMagnetic') {
|
||||
conf.type = 'checkbox';
|
||||
conf.className = 'htCenter';
|
||||
} else if (NUMBER_KEYS.has(col.key)) {
|
||||
conf.type = 'numeric';
|
||||
conf.numericFormat = { pattern: '0.[000]' };
|
||||
conf.className = 'htRight';
|
||||
} else if (col.key === 'skuImage') {
|
||||
conf.className = 'htCenter declaration-sku-cell';
|
||||
conf.renderer = (
|
||||
_instance: Handsontable,
|
||||
td: HTMLTableCellElement,
|
||||
_row: number,
|
||||
_col: number,
|
||||
_prop: any,
|
||||
value: any,
|
||||
) => {
|
||||
td.className = 'htCenter declaration-sku-cell';
|
||||
if (!props.editing) {
|
||||
if (!hasSkuImage(value)) {
|
||||
td.textContent = '--';
|
||||
return td;
|
||||
}
|
||||
const title = t('order.myOrders.drawer.declarationSkuImageView');
|
||||
td.innerHTML = `<span class="declaration-sku-eye" title="${escapeAttr(
|
||||
title,
|
||||
)}" role="button">${eyeSvg()}</span>`;
|
||||
return td;
|
||||
}
|
||||
td.textContent = value == null || value === '' ? '' : String(value);
|
||||
return td;
|
||||
};
|
||||
} else {
|
||||
conf.type = 'text';
|
||||
}
|
||||
|
||||
cols.push(conf);
|
||||
}
|
||||
|
||||
return cols;
|
||||
}
|
||||
|
||||
function beforeStretchingColumnWidth(stretchedWidth: number, column: number) {
|
||||
// 序号、操作列不拉伸
|
||||
if (column === 0) return 50;
|
||||
if (props.editing && column === 1) return 64;
|
||||
return stretchedWidth;
|
||||
}
|
||||
|
||||
function destroyHot() {
|
||||
if (mountTimer) {
|
||||
clearTimeout(mountTimer);
|
||||
mountTimer = null;
|
||||
}
|
||||
if (hot) {
|
||||
hot.destroy();
|
||||
hot = null;
|
||||
}
|
||||
ready.value = false;
|
||||
}
|
||||
|
||||
function mountHot() {
|
||||
if (!hostRef.value || !wrapRef.value) return;
|
||||
destroyHot();
|
||||
|
||||
let width = readWrapWidth();
|
||||
if (width < 200) {
|
||||
const parent = wrapRef.value.parentElement;
|
||||
width = Math.max(Math.floor(parent?.clientWidth || 0), 600);
|
||||
}
|
||||
lastWidth = width;
|
||||
|
||||
hot = new Handsontable(hostRef.value, {
|
||||
data: decorateRows(props.data),
|
||||
columns: buildColumns(),
|
||||
colHeaders: buildColHeaders(),
|
||||
rowHeaders: false,
|
||||
width,
|
||||
height: tableHeight.value,
|
||||
licenseKey: 'non-commercial-and-evaluation',
|
||||
themeName: 'ht-theme-classic',
|
||||
language: 'zh-CN',
|
||||
stretchH: 'all',
|
||||
beforeStretchingColumnWidth,
|
||||
autoRowSize: false,
|
||||
autoColumnSize: false,
|
||||
rowHeights: 32,
|
||||
columnHeaderHeight: 36,
|
||||
renderAllRows: false,
|
||||
viewportRowRenderingOffset: 20,
|
||||
viewportColumnRenderingOffset: 6,
|
||||
enterBeginsEditing: true,
|
||||
fragmentSelection: false,
|
||||
selectionMode: 'range',
|
||||
readOnly: !props.editing,
|
||||
allowInsertRow: false,
|
||||
allowInsertColumn: false,
|
||||
allowRemoveRow: false,
|
||||
allowRemoveColumn: false,
|
||||
contextMenu: false,
|
||||
manualColumnResize: true,
|
||||
afterGetColHeader(_col, th) {
|
||||
th.style.verticalAlign = 'middle';
|
||||
th.style.textAlign = 'center';
|
||||
},
|
||||
afterChange(changes, source) {
|
||||
if (!changes || source === 'loadData' || ignoreChange) return;
|
||||
for (const [rowIndex, prop, _oldVal, newVal] of changes) {
|
||||
const row = hot?.getSourceDataAtRow(rowIndex as number) as any;
|
||||
if (!row || typeof prop !== 'string') continue;
|
||||
if (prop === '_serial' || prop === '_action') continue;
|
||||
row[prop] = newVal;
|
||||
}
|
||||
},
|
||||
afterOnCellMouseDown(_event, coords, td) {
|
||||
if (coords.row < 0) return;
|
||||
const target = _event.target as HTMLElement | null;
|
||||
if (props.editing && target?.closest?.('.declaration-del-btn')) {
|
||||
const row = Number(target.getAttribute('data-row'));
|
||||
if (Number.isFinite(row)) emit('remove-row', row);
|
||||
return;
|
||||
}
|
||||
if (!props.editing && td?.querySelector?.('.declaration-sku-eye')) {
|
||||
const row = hot?.getSourceDataAtRow(coords.row) as any;
|
||||
const url = String(row?.skuImage ?? '').trim();
|
||||
if (hasSkuImage(url)) emit('preview-sku-image', url);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
hot.render();
|
||||
ready.value = true;
|
||||
requestAnimationFrame(() => syncWidth(true));
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
if (!hot) {
|
||||
mountHot();
|
||||
return;
|
||||
}
|
||||
ignoreChange = true;
|
||||
try {
|
||||
hot.loadData(decorateRows(props.data));
|
||||
} finally {
|
||||
requestAnimationFrame(() => {
|
||||
ignoreChange = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCellClick(payload: {
|
||||
row: any
|
||||
rowIndex: number
|
||||
columnKey: string
|
||||
event: MouseEvent
|
||||
}) {
|
||||
if (props.editing) return
|
||||
if (payload.columnKey !== 'skuImage') return
|
||||
const url = String(payload.row?.skuImage ?? '').trim()
|
||||
if (!hasSkuImage(url)) return
|
||||
emit('preview-sku-image', url)
|
||||
function syncWidth(force = false) {
|
||||
if (!hot) return;
|
||||
const width = readWrapWidth();
|
||||
if (width < 200) return;
|
||||
if (!force && Math.abs(width - lastWidth) < 2) return;
|
||||
lastWidth = width;
|
||||
hot.updateSettings(
|
||||
{
|
||||
width,
|
||||
height: tableHeight.value,
|
||||
stretchH: 'all',
|
||||
},
|
||||
false,
|
||||
);
|
||||
hot.refreshDimensions();
|
||||
}
|
||||
|
||||
function setupResizeObserver() {
|
||||
if (!wrapRef.value) return;
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(() => {
|
||||
resizeRaf = 0;
|
||||
syncWidth();
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(wrapRef.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void nextTick(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
await new Promise<void>((resolve) => {
|
||||
mountTimer = setTimeout(() => {
|
||||
mountTimer = null;
|
||||
resolve();
|
||||
}, 40);
|
||||
});
|
||||
if (!wrapRef.value || !hostRef.value) return;
|
||||
mountHot();
|
||||
setupResizeObserver();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
if (mountTimer) {
|
||||
clearTimeout(mountTimer);
|
||||
mountTimer = null;
|
||||
}
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
destroyHot();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => tableHeight.value,
|
||||
(h) => {
|
||||
if (!hot || !h) return;
|
||||
hot.updateSettings({ height: h }, false);
|
||||
hot.refreshDimensions();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.editing,
|
||||
() => {
|
||||
void nextTick(() => mountHot());
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.data, props.data?.length] as const,
|
||||
() => {
|
||||
reloadData();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.order-declaration-hot-table {
|
||||
<style scoped lang="less">
|
||||
.order-declaration-hot-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
min-width: 0;
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.order-declaration-hot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease;
|
||||
|
||||
&.is-ready {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.handsontable) {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
:deep(.htCore th) {
|
||||
background: var(--color-fill-1) !important;
|
||||
color: var(--color-text-2) !important;
|
||||
font-weight: 600;
|
||||
border-color: var(--color-border-2) !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
:deep(.htCore td) {
|
||||
border-color: var(--color-border-2) !important;
|
||||
}
|
||||
|
||||
:deep(.htCore tr:hover td) {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 5%, #fff) !important;
|
||||
}
|
||||
|
||||
:deep(.ht-wrapper),
|
||||
:deep(.handsontable) {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(td.declaration-sku-cell) {
|
||||
overflow: visible !important;
|
||||
@ -179,8 +439,20 @@ function handleCellClick(payload: {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ht-action-cell) {
|
||||
overflow: visible;
|
||||
:deep(.declaration-del-btn) {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-danger-6, #f53f3f);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 0 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--color-danger-6, #f53f3f) 10%, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
@register="register"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="order-detail-drawer order-detail-drawer-shell">
|
||||
<div
|
||||
class="order-detail-drawer order-detail-drawer-shell"
|
||||
:class="{ 'has-action-bar': showActionBar }"
|
||||
>
|
||||
<div class="drawer-tabs">
|
||||
<div class="tabs-slot">
|
||||
<SlidingTabs
|
||||
@ -16,69 +19,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||
class="drawer-tab-toolbar"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
<template v-if="activeTab === 'basic'">
|
||||
<template v-if="basicEditing">
|
||||
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
|
||||
<a-button class="app-btn-search" :loading="basicSaving" @click="handleBasicEditSave">
|
||||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button v-else class="app-btn-search" @click="startBasicEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="handleAskAdvisor">
|
||||
{{ $t('nanaAssistant.askAboutWaybill') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'track'">
|
||||
<span class="toolbar-meta">
|
||||
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||||
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
|
||||
</span>
|
||||
<a-tag
|
||||
v-if="currentOrderStatusMeta"
|
||||
size="small"
|
||||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||||
>
|
||||
{{ $t(currentOrderStatusMeta.labelKey) }}
|
||||
</a-tag>
|
||||
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="trackLoading"
|
||||
@click="loadTrackTimeline"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'stockBind'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="stockBindLoading"
|
||||
@click="reloadStockBind"
|
||||
>{{ $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>
|
||||
|
||||
<div class="drawer-tab-content">
|
||||
<!-- 基本信息 -->
|
||||
<div v-if="activeTab === 'basic'" class="tab-panel basic-info">
|
||||
@ -97,7 +37,7 @@
|
||||
<summary>{{ $t('order.myOrders.drawer.failGuideRaw') }}</summary>
|
||||
<p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p>
|
||||
</details>
|
||||
<div class="predeclare-fail-guide__actions">
|
||||
<div v-if="canEditOrderDetail" class="predeclare-fail-guide__actions">
|
||||
<a-button class="app-btn-search" @click="startBasicEdit">
|
||||
{{ $t('order.myOrders.drawer.failGuideEdit') }}
|
||||
</a-button>
|
||||
@ -134,6 +74,7 @@
|
||||
<div
|
||||
v-if="section.key === 'declaration'"
|
||||
class="declaration-header-sum"
|
||||
:class="{ 'is-editable': basicEditing }"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="basicEditing">
|
||||
@ -196,6 +137,7 @@
|
||||
v-for="fieldKey in group.fields"
|
||||
:key="fieldKey"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isOrderFieldEditable(fieldKey) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(getOrderFieldLabelKey(fieldKey)) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -281,6 +223,7 @@
|
||||
v-for="field in senderFields"
|
||||
:key="field.key"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isSenderFieldEditable(field.key) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -312,6 +255,7 @@
|
||||
v-for="field in recipientFields"
|
||||
:key="field.key"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isRecipientFieldEditable(field.key) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -635,6 +579,85 @@
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showActionBar" class="drawer-action-bar">
|
||||
<div class="drawer-action-bar__meta">
|
||||
<template v-if="activeTab === 'track' && !showEditFooter">
|
||||
<span class="action-bar-meta">
|
||||
<span class="action-bar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||||
<span class="action-bar-meta-value">{{ trackOrderNumber || '--' }}</span>
|
||||
</span>
|
||||
<a-tag
|
||||
v-if="currentOrderStatusMeta"
|
||||
size="small"
|
||||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||||
>
|
||||
{{ $t(currentOrderStatusMeta.labelKey) }}
|
||||
</a-tag>
|
||||
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag
|
||||
v-else-if="activeTab === 'operationLog' && !showEditFooter && operationLogList.length"
|
||||
size="small"
|
||||
color="arcoblue"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="drawer-action-bar__actions">
|
||||
<template v-if="showEditFooter">
|
||||
<a-button @click="cancelBasicEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.cancel') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="app-btn-primary-soft"
|
||||
:loading="basicSaving"
|
||||
@click="handleBasicEditSave"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'basic'">
|
||||
<a-button
|
||||
v-if="canEditOrderDetail"
|
||||
class="app-btn-search"
|
||||
@click="startBasicEdit"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="handleAskAdvisor">
|
||||
{{ $t('nanaAssistant.askAboutWaybill') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'track'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="trackLoading"
|
||||
@click="loadTrackTimeline"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'stockBind'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="stockBindLoading"
|
||||
@click="reloadStockBind"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'operationLog'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="operationLogLoading"
|
||||
@click="loadOperationLogs"
|
||||
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
|
||||
@ -664,7 +687,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, reactive } from 'vue';
|
||||
import { ref, computed, watch, reactive, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
@ -823,9 +846,10 @@ interface FormFieldDef {
|
||||
options?: Array<{ value: string; labelKey: string }>;
|
||||
}
|
||||
|
||||
/** 我的订单可编辑字段:跟踪号不开放;入库后整单不可编辑 */
|
||||
const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||
{
|
||||
key: 'cargoAttribute',
|
||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||
@ -841,6 +865,9 @@ const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'codSum', labelKey: 'order.myOrders.drawer.codSum', inputType: 'number' },
|
||||
];
|
||||
|
||||
/** 入库前(草稿/已预报)才允许客户编辑 */
|
||||
const MY_ORDERS_EDITABLE_STATUSES = new Set(['draft', 'predeclared']);
|
||||
|
||||
const orderEditMetaMap = Object.fromEntries(
|
||||
orderEditFieldDefs.map((def) => [def.key, def])
|
||||
) as Record<string, FormFieldDef>;
|
||||
@ -875,7 +902,7 @@ function getRecipientFormKey(key: string) {
|
||||
|
||||
const basicEditForm = reactive({
|
||||
clientNo: '',
|
||||
trackNo: '',
|
||||
downstreamServiceNo: '',
|
||||
cargoAttribute: 'general',
|
||||
isCod: false,
|
||||
codSum: undefined as number | undefined,
|
||||
@ -1045,6 +1072,19 @@ function resetEditState() {
|
||||
const [register, { openDrawer: innerOpenDrawer, setDrawerProps, closeDrawer }] = useDrawer();
|
||||
|
||||
const activeTab = ref('basic');
|
||||
|
||||
const canEditOrderDetail = computed(() => {
|
||||
const code = resolveOrderStatusCode(detail.value);
|
||||
return !!code && MY_ORDERS_EDITABLE_STATUSES.has(code);
|
||||
});
|
||||
|
||||
const showEditFooter = computed(
|
||||
() => activeTab.value === 'basic' && basicEditing.value,
|
||||
);
|
||||
|
||||
const showActionBar = computed(() =>
|
||||
['basic', 'track', 'stockBind', 'operationLog'].includes(activeTab.value),
|
||||
);
|
||||
const CUSTOMER_WEIGHT_TYPE_KEYS = ['forecast', 'receive'] as const;
|
||||
|
||||
const tabList = computed(() => [
|
||||
@ -1863,7 +1903,7 @@ function fillBasicEditForm() {
|
||||
const o = detail.value;
|
||||
if (!o?.id) return;
|
||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||||
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
||||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||
@ -1915,10 +1955,26 @@ function fillBasicEditForm() {
|
||||
declarationEditList.value = cloneDeclarationRows(declarationList.value);
|
||||
}
|
||||
|
||||
async function focusFirstEditableField(rootSelector: string) {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const root = document.querySelector(rootSelector);
|
||||
if (!root) return;
|
||||
const el = root.querySelector(
|
||||
'.is-editable input:not([disabled]), .is-editable textarea:not([disabled]), .is-editable .arco-select-view',
|
||||
) as HTMLElement | null;
|
||||
el?.focus?.();
|
||||
}
|
||||
|
||||
function startBasicEdit() {
|
||||
if (!canEditOrderDetail.value) {
|
||||
Message.warning(t('order.myOrders.drawer.editAfterInboundForbidden'));
|
||||
return;
|
||||
}
|
||||
fillBasicEditForm();
|
||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||
basicEditing.value = true;
|
||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||
}
|
||||
|
||||
function cancelBasicEdit() {
|
||||
@ -1951,7 +2007,7 @@ async function handleBasicEditSave() {
|
||||
const result = await postOrderUpdate({
|
||||
id,
|
||||
clientNo: basicEditForm.clientNo,
|
||||
trackNo: basicEditForm.trackNo,
|
||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||
cargoAttribute: basicEditForm.cargoAttribute,
|
||||
isCod: basicEditForm.isCod,
|
||||
codSum: basicEditForm.codSum,
|
||||
@ -2192,43 +2248,81 @@ defineExpose({
|
||||
min-width: 0;
|
||||
|
||||
.tabs-slot {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-tab-toolbar {
|
||||
.drawer-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
.drawer-action-bar__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.drawer-action-bar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.action-bar-meta {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-bar-meta-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.action-bar-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
/* 编辑态:仅高亮输入控件本身 */
|
||||
.detail-field-item.is-editable,
|
||||
.declaration-header-sum.is-editable {
|
||||
:deep(.arco-input-wrapper),
|
||||
:deep(.arco-select-view-single),
|
||||
:deep(.arco-input-number),
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
background: #fff;
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--app-primary-color, #1d7281) 25%, transparent);
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.toolbar-meta-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.toolbar-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
:deep(.arco-input-wrapper:hover),
|
||||
:deep(.arco-select-view-single:hover),
|
||||
:deep(.arco-input-number:hover),
|
||||
:deep(.arco-textarea-wrapper:hover),
|
||||
:deep(.arco-input-wrapper.arco-input-focus),
|
||||
:deep(.arco-select-view-single.arco-select-view-focus),
|
||||
:deep(.arco-input-number.arco-input-number-focus),
|
||||
:deep(.arco-textarea-wrapper.arco-input-focus) {
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary-color, #1d7281) 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3506,9 +3506,18 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
singleShowFieldErrors.value = false;
|
||||
const pickupNo = created?.pickupNo || created?.PickupNo;
|
||||
const pickupErr = created?.pickupCreateError || created?.PickupCreateError;
|
||||
const pickupQueued = !!(created?.pickupQueued || created?.PickupQueued);
|
||||
const forecastQueued = !!(
|
||||
created?.forecastBillingQueued || created?.ForecastBillingQueued
|
||||
);
|
||||
let content = t('order.placeOrder.single.successContent');
|
||||
if (forecastQueued) {
|
||||
content = `${content}\n预报计费/排货处理中,费用与渠道稍后可在订单列表查看`;
|
||||
}
|
||||
if (pickupNo) {
|
||||
content = `${content}\n揽收单号:${pickupNo}`;
|
||||
} else if (pickupQueued) {
|
||||
content = `${content}\n揽收单后台创建中,请稍后在揽收单列表查看`;
|
||||
} else if (pickupErr) {
|
||||
content = `${content}\n揽收未创建成功:${pickupErr}(可到我的订单补建)`;
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
@register="register"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="order-detail-drawer order-detail-drawer-shell">
|
||||
<div
|
||||
class="order-detail-drawer order-detail-drawer-shell"
|
||||
:class="{ 'has-action-bar': showActionBar }"
|
||||
>
|
||||
<div class="drawer-tabs">
|
||||
<div class="tabs-slot">
|
||||
<SlidingTabs
|
||||
@ -16,112 +19,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||
class="drawer-tab-toolbar"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
<template v-if="activeTab === 'basic'">
|
||||
<template v-if="basicEditing">
|
||||
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
|
||||
<a-button class="app-btn-search" :loading="basicSaving" @click="handleBasicEditSave">
|
||||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button v-else class="app-btn-search" @click="startBasicEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="handleAskAdvisor">
|
||||
{{ $t('nanaAssistant.askAboutWaybill') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'weight'">
|
||||
<template v-if="weightEditing">
|
||||
<a-button class="app-btn-search" @click="cancelWeightEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
|
||||
<a-button class="app-btn-search" :loading="weightSaving" @click="handleWeightEditSave">
|
||||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button v-else class="app-btn-search" @click="startWeightEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'fee'">
|
||||
<a-button
|
||||
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') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="recalcReceivableLoading"
|
||||
@click="handleRecalculateReceivable"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.recalculateReceivable') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="recalcPayableLoading"
|
||||
@click="handleRecalculatePayable"
|
||||
>
|
||||
{{ $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">
|
||||
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||||
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
|
||||
</span>
|
||||
<a-tag
|
||||
v-if="currentOrderStatusMeta"
|
||||
size="small"
|
||||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||||
>
|
||||
{{ $t(currentOrderStatusMeta.labelKey) }}
|
||||
</a-tag>
|
||||
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="trackLoading"
|
||||
@click="loadTrackTimeline"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'stockBind'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="stockBindLoading"
|
||||
@click="reloadStockBind"
|
||||
>{{ $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>
|
||||
|
||||
<div class="drawer-tab-content">
|
||||
<!-- 基本信息 -->
|
||||
<div v-if="activeTab === 'basic'" class="tab-panel basic-info">
|
||||
@ -173,6 +70,7 @@
|
||||
<div
|
||||
v-if="section.key === 'declaration'"
|
||||
class="declaration-header-sum"
|
||||
:class="{ 'is-editable': basicEditing }"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="basicEditing">
|
||||
@ -230,6 +128,7 @@
|
||||
v-for="fieldKey in group.fields"
|
||||
:key="fieldKey"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isOrderFieldEditable(fieldKey) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(getOrderFieldLabelKey(fieldKey)) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -331,6 +230,7 @@
|
||||
v-for="field in senderFields"
|
||||
:key="field.key"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isSenderFieldEditable(field.key) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -362,6 +262,7 @@
|
||||
v-for="field in recipientFields"
|
||||
:key="field.key"
|
||||
class="detail-field-item"
|
||||
:class="{ 'is-editable': basicEditing && isRecipientFieldEditable(field.key) }"
|
||||
>
|
||||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||||
<div class="detail-field-value">
|
||||
@ -458,7 +359,10 @@
|
||||
v-for="field in weightMetricFields"
|
||||
:key="field.key"
|
||||
class="weight-metric-item"
|
||||
:class="{ highlight: field.highlight }"
|
||||
:class="{
|
||||
highlight: field.highlight && !weightEditing,
|
||||
'is-editable': weightEditing && field.editable && hasWeightRow(section.key),
|
||||
}"
|
||||
>
|
||||
<span class="metric-label">{{ $t(field.labelKey) }}</span>
|
||||
<div class="metric-value-wrap">
|
||||
@ -473,7 +377,7 @@
|
||||
<span
|
||||
v-else
|
||||
class="metric-value"
|
||||
:class="{ 'metric-value-highlight': field.highlight }"
|
||||
:class="{ 'metric-value-highlight': field.highlight && !weightEditing }"
|
||||
>
|
||||
{{ formatWeightCellValue(section.row, field.key) }}
|
||||
</span>
|
||||
@ -524,18 +428,6 @@
|
||||
<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
|
||||
@ -805,52 +697,16 @@
|
||||
<!-- 操作日志 -->
|
||||
<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-table
|
||||
v-if="operationLogList.length"
|
||||
class="operation-log-table"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:bordered="{ cell: true }"
|
||||
:columns="operationLogColumns"
|
||||
:data="operationLogTableData"
|
||||
/>
|
||||
<a-empty
|
||||
v-else-if="!operationLogLoading"
|
||||
:description="$t('order.myOrders.drawer.operationLog.empty')"
|
||||
@ -859,6 +715,120 @@
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showActionBar" class="drawer-action-bar">
|
||||
<div class="drawer-action-bar__meta">
|
||||
<template v-if="activeTab === 'track' && !showEditFooter">
|
||||
<span class="action-bar-meta">
|
||||
<span class="action-bar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||||
<span class="action-bar-meta-value">{{ trackOrderNumber || '--' }}</span>
|
||||
</span>
|
||||
<a-tag
|
||||
v-if="currentOrderStatusMeta"
|
||||
size="small"
|
||||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||||
>
|
||||
{{ $t(currentOrderStatusMeta.labelKey) }}
|
||||
</a-tag>
|
||||
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
|
||||
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag
|
||||
v-else-if="activeTab === 'operationLog' && !showEditFooter && operationLogList.length"
|
||||
size="small"
|
||||
color="arcoblue"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="drawer-action-bar__actions">
|
||||
<template v-if="showEditFooter">
|
||||
<a-button
|
||||
@click="activeTab === 'weight' ? cancelWeightEdit() : cancelBasicEdit()"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.cancel') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="app-btn-primary-soft"
|
||||
:loading="activeTab === 'weight' ? weightSaving : basicSaving"
|
||||
@click="activeTab === 'weight' ? handleWeightEditSave() : handleBasicEditSave()"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'basic'">
|
||||
<a-button class="app-btn-search" @click="startBasicEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
<a-button class="app-btn-search" @click="handleAskAdvisor">
|
||||
{{ $t('nanaAssistant.askAboutWaybill') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'weight'">
|
||||
<a-button class="app-btn-search" @click="startWeightEdit">
|
||||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'fee'">
|
||||
<a-button
|
||||
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') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="recalcReceivableLoading"
|
||||
@click="handleRecalculateReceivable"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.actions.recalculateReceivable') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="recalcPayableLoading"
|
||||
@click="handleRecalculatePayable"
|
||||
>
|
||||
{{ $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'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="trackLoading"
|
||||
@click="loadTrackTimeline"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'stockBind'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="stockBindLoading"
|
||||
@click="reloadStockBind"
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'operationLog'">
|
||||
<a-button
|
||||
class="app-btn-search"
|
||||
:loading="operationLogLoading"
|
||||
@click="loadOperationLogs"
|
||||
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
|
||||
@ -991,11 +961,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, reactive } from 'vue';
|
||||
import { ref, computed, watch, reactive, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconRefresh, IconRobot, IconEye, IconDown, IconUp } from '@arco-design/web-vue/es/icon';
|
||||
import { IconRefresh, IconRobot, IconEye } from '@arco-design/web-vue/es/icon';
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import useDrawer from '@/components/DrawerPro/useDrawer';
|
||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||
import {
|
||||
@ -1137,7 +1108,7 @@ interface FormFieldDef {
|
||||
const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||
{ key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' },
|
||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||
{
|
||||
key: 'cargoAttribute',
|
||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||
@ -1188,7 +1159,7 @@ function getRecipientFormKey(key: string) {
|
||||
const basicEditForm = reactive({
|
||||
clientNo: '',
|
||||
trackNo: '',
|
||||
channelCode: '',
|
||||
downstreamServiceNo: '',
|
||||
cargoAttribute: 'general',
|
||||
isCod: false,
|
||||
codSum: undefined as number | undefined,
|
||||
@ -1376,6 +1347,17 @@ function resetEditState() {
|
||||
const [register, { openDrawer: innerOpenDrawer, setDrawerProps, closeDrawer }] = useDrawer();
|
||||
|
||||
const activeTab = ref('basic');
|
||||
|
||||
const showEditFooter = computed(
|
||||
() =>
|
||||
(activeTab.value === 'basic' && basicEditing.value) ||
|
||||
(activeTab.value === 'weight' && weightEditing.value),
|
||||
);
|
||||
|
||||
const showActionBar = computed(() =>
|
||||
['basic', 'weight', 'fee', 'track', 'stockBind', 'operationLog'].includes(activeTab.value),
|
||||
);
|
||||
|
||||
const tabList = computed(() => [
|
||||
{ key: 'basic', label: t('order.myOrders.drawer.tabs.basic') },
|
||||
{ key: 'weight', label: t('order.myOrders.drawer.tabs.weight') },
|
||||
@ -1389,7 +1371,40 @@ const tabList = computed(() => [
|
||||
|
||||
const operationLogList = ref<OrderOperationLogItem[]>([]);
|
||||
const operationLogLoading = ref(false);
|
||||
const operationLogExpandedIds = ref<Set<number>>(new Set());
|
||||
|
||||
const operationLogColumns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: t('order.myOrders.drawer.operationLog.time'),
|
||||
dataIndex: 'createTime',
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
title: t('order.myOrders.drawer.operationLog.action'),
|
||||
dataIndex: 'actionName',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: t('order.myOrders.drawer.operationLog.content'),
|
||||
dataIndex: 'summary',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: t('order.myOrders.drawer.operationLog.operator'),
|
||||
dataIndex: 'operatorName',
|
||||
width: 120,
|
||||
},
|
||||
]);
|
||||
|
||||
const operationLogTableData = computed(() =>
|
||||
operationLogList.value.map((item) => ({
|
||||
id: item.id,
|
||||
createTime: formatOperationLogTime(item.createTime),
|
||||
actionName: item.actionName || item.action || '--',
|
||||
summary: item.summary || item.remark || '—',
|
||||
operatorName: item.operatorName || '--',
|
||||
})),
|
||||
);
|
||||
|
||||
const slidingTabItems = computed(() =>
|
||||
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
||||
@ -2376,7 +2391,7 @@ function fillBasicEditForm() {
|
||||
if (!o?.id) return;
|
||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||||
basicEditForm.channelCode = String(o.channelCode ?? '');
|
||||
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
||||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||
@ -2428,10 +2443,22 @@ function fillBasicEditForm() {
|
||||
declarationEditList.value = cloneDeclarationRows(declarationList.value);
|
||||
}
|
||||
|
||||
async function focusFirstEditableField(rootSelector: string) {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const root = document.querySelector(rootSelector);
|
||||
if (!root) return;
|
||||
const el = root.querySelector(
|
||||
'.is-editable input:not([disabled]), .is-editable textarea:not([disabled]), .is-editable .arco-select-view',
|
||||
) as HTMLElement | null;
|
||||
el?.focus?.();
|
||||
}
|
||||
|
||||
function startBasicEdit() {
|
||||
fillBasicEditForm();
|
||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||
basicEditing.value = true;
|
||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||
}
|
||||
|
||||
function cancelBasicEdit() {
|
||||
@ -2449,6 +2476,7 @@ function startWeightEdit() {
|
||||
actualWeight: row.actualWeight != null ? Number(row.actualWeight) : undefined,
|
||||
}));
|
||||
weightEditing.value = true;
|
||||
void focusFirstEditableField('.order-detail-drawer .weight-info');
|
||||
}
|
||||
|
||||
function cancelWeightEdit() {
|
||||
@ -2465,7 +2493,7 @@ async function handleBasicEditSave() {
|
||||
id,
|
||||
clientNo: basicEditForm.clientNo,
|
||||
trackNo: basicEditForm.trackNo,
|
||||
channelCode: basicEditForm.channelCode,
|
||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||
cargoAttribute: basicEditForm.cargoAttribute,
|
||||
isCod: basicEditForm.isCod,
|
||||
codSum: basicEditForm.codSum,
|
||||
@ -2617,9 +2645,6 @@ async function loadOperationLogs() {
|
||||
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 {
|
||||
@ -2627,17 +2652,6 @@ async function loadOperationLogs() {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@ -2740,43 +2754,82 @@ defineExpose({
|
||||
min-width: 0;
|
||||
|
||||
.tabs-slot {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-tab-toolbar {
|
||||
.drawer-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
.drawer-action-bar__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.drawer-action-bar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.action-bar-meta {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-bar-meta-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.action-bar-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
/* 编辑态:仅高亮输入控件本身 */
|
||||
.detail-field-item.is-editable,
|
||||
.weight-metric-item.is-editable,
|
||||
.declaration-header-sum.is-editable {
|
||||
:deep(.arco-input-wrapper),
|
||||
:deep(.arco-select-view-single),
|
||||
:deep(.arco-input-number),
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
background: #fff;
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--app-primary-color, #1d7281) 25%, transparent);
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.toolbar-meta-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.toolbar-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
:deep(.arco-input-wrapper:hover),
|
||||
:deep(.arco-select-view-single:hover),
|
||||
:deep(.arco-input-number:hover),
|
||||
:deep(.arco-textarea-wrapper:hover),
|
||||
:deep(.arco-input-wrapper.arco-input-focus),
|
||||
:deep(.arco-select-view-single.arco-select-view-focus),
|
||||
:deep(.arco-input-number.arco-input-number-focus),
|
||||
:deep(.arco-textarea-wrapper.arco-input-focus) {
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary-color, #1d7281) 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3338,10 +3391,6 @@ defineExpose({
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.fee-additional-services__edit {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.fee-snapshot-versions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -3849,102 +3898,35 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.operation-log-spin {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.operation-log-list {
|
||||
.operation-log-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.operation-log-item {
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.operation-log-spin {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.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);
|
||||
:deep(.arco-spin-children) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-log-head-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.operation-log-table {
|
||||
width: 100%;
|
||||
|
||||
.operation-log-action {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
:deep(.arco-table-th) {
|
||||
background: var(--color-fill-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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;
|
||||
:deep(.arco-table-td) {
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-services-hint {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user