diff --git a/src/api/order/index.ts b/src/api/order/index.ts index 3246ae7..a199c53 100644 --- a/src/api/order/index.ts +++ b/src/api/order/index.ts @@ -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, diff --git a/src/components/InterfaceDetail/index.vue b/src/components/InterfaceDetail/index.vue index ce5205e..6d18e4b 100644 --- a/src/components/InterfaceDetail/index.vue +++ b/src/components/InterfaceDetail/index.vue @@ -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 @@ 保存固定配置 @@ -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 || '') diff --git a/src/views/channel/salesProducts/components/PriceListHotTable.vue b/src/views/channel/salesProducts/components/PriceListHotTable.vue index 68865bb..41449ba 100644 --- a/src/views/channel/salesProducts/components/PriceListHotTable.vue +++ b/src/views/channel/salesProducts/components/PriceListHotTable.vue @@ -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, diff --git a/src/views/channel/salesProducts/index.vue b/src/views/channel/salesProducts/index.vue index 54418e8..da97570 100644 --- a/src/views/channel/salesProducts/index.vue +++ b/src/views/channel/salesProducts/index.vue @@ -3243,10 +3243,12 @@ /** 当前价格表行对应的客户组编码顺序(用于同步时按编码保留价格) */ const priceListCustomerCodesSnapshot = ref([]); const priceListSelectedIds = ref([]); + /** 上次非空选中(点工具栏时 HOT 可能先清空当前选中) */ + const lastPriceListSelectedIds = ref([]); + const batchFillTargetIds = ref([]); const priceListHotRef = ref<{ selectAll: () => void; clearSelection: () => void } | null>(null); const batchFillModalVisible = ref(false); const batchFillValue = ref(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) { - 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 @@ {{ t('channel.salesProducts.drawer.priceList.batchFill') }} - - {{ t('channel.salesProducts.drawer.priceList.batchFillEmpty') }} - - - {{ t('channel.salesProducts.drawer.priceList.copyPrev') }} - 导出 导入 @@ -5593,19 +5571,19 @@ - +
- {{ t('channel.salesProducts.drawer.priceList.batchFillSelected', { count: priceListSelectedIds.length }) }} + {{ t('channel.salesProducts.drawer.priceList.batchFillSelected', { count: batchFillTargetIds.length || priceListSelectedIds.length }) }}
@@ -149,7 +145,7 @@ - diff --git a/src/views/order/myOrders/components/OrderDetailDrawer.vue b/src/views/order/myOrders/components/OrderDetailDrawer.vue index 0472845..4082ef2 100644 --- a/src/views/order/myOrders/components/OrderDetailDrawer.vue +++ b/src/views/order/myOrders/components/OrderDetailDrawer.vue @@ -5,7 +5,10 @@ @register="register" @cancel="handleCancel" > -
+
-
-
- - - - -
-
-
@@ -97,7 +37,7 @@ {{ $t('order.myOrders.drawer.failGuideRaw') }}

{{ predeclareFailGuide.rawReason }}

-
+
{{ $t('order.myOrders.drawer.failGuideEdit') }} @@ -134,6 +74,7 @@