This commit is contained in:
biybuy 2026-03-07 18:16:44 +08:00
parent 3818c85b06
commit 814550cc0e
7 changed files with 352 additions and 196 deletions

View File

@ -195,7 +195,7 @@ declare namespace API {
/** 客户 */ /** 客户 */
customer?: string; customer?: string;
/** 拉取单号类型 */ /** 拉取单号类型 */
orderType?: TrackOrderType; orderNoType?: TrackOrderType;
/** 优先级 */ /** 优先级 */
priority?: number; priority?: number;
/** 服务商拉取频率(分钟) */ /** 服务商拉取频率(分钟) */
@ -213,7 +213,7 @@ declare namespace API {
/** 客户 */ /** 客户 */
customer?: string; customer?: string;
/** 拉取单号类型 */ /** 拉取单号类型 */
orderType?: TrackOrderType; orderNoType?: TrackOrderType;
/** 优先级 */ /** 优先级 */
priority?: number; priority?: number;
/** 服务商拉取频率(分钟) */ /** 服务商拉取频率(分钟) */
@ -232,7 +232,7 @@ declare namespace API {
/** 客户 */ /** 客户 */
customer?: string; customer?: string;
/** 拉取单号类型 */ /** 拉取单号类型 */
orderType?: TrackOrderType; orderNoType?: TrackOrderType;
/** 优先级 */ /** 优先级 */
priority?: number; priority?: number;
/** 服务商拉取频率(分钟) */ /** 服务商拉取频率(分钟) */

View File

@ -43,6 +43,8 @@ declare namespace API {
type EditTrackPushContentInput = { type EditTrackPushContentInput = {
/** 扩展字段参数值 */ /** 扩展字段参数值 */
expandFieldsValue?: string; expandFieldsValue?: string;
/** 自定义列键值对,JSON 字符串,如 "{\"trackingNumber\":\"string\",\"trackNo\":\"string\"}" */
extraConfig?: string;
id?: number; id?: number;
options?: OptionTypeNoNull; options?: OptionTypeNoNull;
orderNoType?: TrackOrderType; orderNoType?: TrackOrderType;
@ -154,6 +156,8 @@ declare namespace API {
}; };
type GetTrackPushContentPageOutputVO = { type GetTrackPushContentPageOutputVO = {
/** 自定义列键值对 JSON 字符串 */
extraConfig?: string;
/** 轨迹内容代码 */ /** 轨迹内容代码 */
code?: string; code?: string;
/** 轨迹内容id */ /** 轨迹内容id */

View File

@ -607,7 +607,7 @@
.map-count { .map-count {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #87f6ff; color: #3f9370;
padding: 0; padding: 0;
min-width: auto; min-width: auto;
text-align: left; text-align: left;
@ -616,7 +616,7 @@
color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 0.4);
} }
.count-num { .count-num {
color: #87f6ff; color: #3f9370;
} }
} }

View File

@ -20,6 +20,8 @@
sectionTitle?: string; sectionTitle?: string;
/** 新增列输入框占位 */ /** 新增列输入框占位 */
addPlaceholder?: string; addPlaceholder?: string;
/** 自定义表头弹窗内的提示文案 */
columnModalTip?: string;
/** 左侧固定列配置 */ /** 左侧固定列配置 */
fixedLeftColumns: TableColumnConfig[]; fixedLeftColumns: TableColumnConfig[];
/** 右侧固定列配置(如操作列) */ /** 右侧固定列配置(如操作列) */
@ -38,6 +40,7 @@
{ {
sectionTitle: '自定义列管理', sectionTitle: '自定义列管理',
addPlaceholder: '输入列名后点击加号添加', addPlaceholder: '输入列名后点击加号添加',
columnModalTip: '',
tableHeight: 600, tableHeight: 600,
showCheckbox: false, showCheckbox: false,
columnsKey: 'customList', columnsKey: 'customList',
@ -188,6 +191,7 @@
title: col.title, title: col.title,
minWidth: props.customColumnMinWidth, minWidth: props.customColumnMinWidth,
width: undefined, width: undefined,
align: 'left',
slots: { default: col.field }, slots: { default: col.field },
})); }));
const right = props.fixedRightColumns || []; const right = props.fixedRightColumns || [];
@ -270,7 +274,7 @@
<template v-if="mode === 'modal'"> <template v-if="mode === 'modal'">
<ModalPro <ModalPro
:title="sectionTitle" :title="sectionTitle"
width="440px" width="880px"
class="custom-list-column-modal" class="custom-list-column-modal"
:footer="true" :footer="true"
@register="registerColumnModal" @register="registerColumnModal"
@ -280,6 +284,7 @@
<a-button @click="closeColumnModal">取消</a-button> <a-button @click="closeColumnModal">取消</a-button>
<a-button type="primary" @click="saveDraftColumns">保存</a-button> <a-button type="primary" @click="saveDraftColumns">保存</a-button>
</template> </template>
<div v-if="columnModalTip" class="column-modal-tip">{{ columnModalTip }}</div>
<div class="column-management-section"> <div class="column-management-section">
<div class="column-list"> <div class="column-list">
<div v-for="(column, index) in draftColumns" :key="column.field" class="column-item"> <div v-for="(column, index) in draftColumns" :key="column.field" class="column-item">
@ -416,6 +421,16 @@
min-height: 0; min-height: 0;
} }
.column-modal-tip {
margin-bottom: 12px;
padding: 8px 12px;
font-size: 12px;
color: #86909c;
background-color: #f7f8fa;
border-radius: 6px;
line-height: 1.5;
}
.column-management-section { .column-management-section {
padding: 8px 16px; padding: 8px 16px;
background-color: #f8f9fa; background-color: #f8f9fa;
@ -435,6 +450,8 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
max-height: 640px;
overflow-y: auto;
.column-item { .column-item {
display: flex; display: flex;

View File

@ -626,9 +626,39 @@
} }
} }
// 位置字典(用于固定配置-位置列展示)
const fieldPositionDict = ref<{ key: number | string; value: string }[]>([]);
async function loadFieldPositionDict() {
try {
const res: any = await getDictionaryGetList({ type: 'FieldPosition' });
fieldPositionDict.value = res?.data || res || [];
} catch {
fieldPositionDict.value = [];
}
}
function formatFieldPositionsDisplay(row: any): string {
const list = row?.fieldPositionList;
if (list && Array.isArray(list) && list.length > 0) {
const labels = list.map((k: number | string) => {
const item = fieldPositionDict.value.find((d) => String(d.key) === String(k));
return item?.value ?? k;
});
return labels.filter(Boolean).join(',') || '-';
}
const fp = row?.fieldPositions;
if (Array.isArray(fp) && fp.length > 0) {
return fp.map((k: number | string) => {
const item = fieldPositionDict.value.find((d) => String(d.key) === String(k));
return item?.value ?? k;
}).filter(Boolean).join(',') || '-';
}
return typeof fp === 'string' ? fp : '-';
}
// 初始化时加载参数类型列表和参数占位符列表 // 初始化时加载参数类型列表和参数占位符列表
loadParameterTypeList(); loadParameterTypeList();
loadParameterPlaceholderList(); loadParameterPlaceholderList();
loadFieldPositionDict();
// 固定配置:加载 // 固定配置:加载
async function loadFixedParams() { async function loadFixedParams() {
@ -1562,7 +1592,7 @@
</template> </template>
<template #fieldPositions="{ row }"> <template #fieldPositions="{ row }">
<span>{{ row.fieldPositions || '-' }}</span> <span>{{ formatFieldPositionsDisplay(row) }}</span>
</template> </template>
<template #fieldPositions_edit="{ row }"> <template #fieldPositions_edit="{ row }">
<SelectPro <SelectPro

View File

@ -244,7 +244,7 @@
title: '客户', title: '客户',
}, },
{ {
field: 'orderType', field: 'orderNoType',
title: '拉取单号类型', title: '拉取单号类型',
formatter: ({ cellValue }: any) => { formatter: ({ cellValue }: any) => {
if (cellValue === 1) return '客户单号'; if (cellValue === 1) return '客户单号';
@ -313,7 +313,7 @@
span: 12, span: 12,
}, },
{ {
field: 'orderType', field: 'orderNoType',
label: '拉取单号类型', label: '拉取单号类型',
comp: 'ASelect', comp: 'ASelect',
compProps: { compProps: {
@ -852,9 +852,9 @@
activeTab.value = key === 'priority' ? 'priority' : 'detail'; activeTab.value = key === 'priority' ? 'priority' : 'detail';
nextTick(() => { nextTick(() => {
calcContentHeight(); calcContentHeight();
if (activeTab.value === 'priority' && currentId.value?.id) { // 移除优先级tab的手动reload调用,因为TablePro组件在挂载时会自动加载数据(immediate: true)
tableRefPriority.value?.reload(); // 如果手动调用reload会导致重复请求
} else if (activeTab.value === 'detail' && currentId.value?.id) { if (activeTab.value === 'detail' && currentId.value?.id) {
// 切换回明细tab时,重新加载明细列表数据 // 切换回明细tab时,重新加载明细列表数据
loadDetailTable(); loadDetailTable();
} }
@ -879,7 +879,7 @@
let formData: any = { let formData: any = {
customer: '', customer: '',
orderType: undefined, orderNoType: undefined,
pullInterval: undefined, pullInterval: undefined,
priority: undefined, priority: undefined,
}; };
@ -889,7 +889,7 @@
priorityModalParams.options = 3; priorityModalParams.options = 3;
formData = { formData = {
customer: row.customer, customer: row.customer,
orderType: row.orderType, orderNoType: row.orderNoType,
// 24 / 每天拉取次数 * 60 = 接口<EFBFBD>? // 24 / 每天拉取次数 * 60 = 接口<EFBFBD>?
// 每天拉取次数 = 24 * 60 / 接口<EFBFBD>?= 1440 / 接口<EFBFBD>? // 每天拉取次数 = 24 * 60 / 接口<EFBFBD>?= 1440 / 接口<EFBFBD>?
pullInterval: row.pullInterval && row.pullInterval !== 0 ? 1440 / row.pullInterval : undefined, pullInterval: row.pullInterval && row.pullInterval !== 0 ? 1440 / row.pullInterval : undefined,
@ -913,7 +913,7 @@
id: priorityModalParams.id, id: priorityModalParams.id,
trackPullConfigId: currentId.value.id, trackPullConfigId: currentId.value.id,
customer: formData.customer, customer: formData.customer,
orderType: formData.orderType, orderNoType: formData.orderNoType,
priority: formData.priority, priority: formData.priority,
// 24 / 每天拉取次数 * 60 = 接口<EFBFBD>? // 24 / 每天拉取次数 * 60 = 接口<EFBFBD>?
// 接口<EFBFBD>?= (24 / 每天拉取次数) * 60 = 1440 / 每天拉取次数 // 接口<EFBFBD>?= (24 / 每天拉取次数) * 60 = 1440 / 每天拉取次数
@ -926,9 +926,10 @@
// 优先级详情获取后处理 // 优先级详情获取后处理
function onGetPriorityInfo(data: any) { function onGetPriorityInfo(data: any) {
if (data && data.pullInterval !== undefined && data.pullInterval !== null && data.pullInterval !== 0) { if (!data) return;
// 24 / 每天拉取次数 * 60 = 接口<EFBFBD>? if (data.pullInterval !== undefined && data.pullInterval !== null && data.pullInterval !== 0) {
// 每天拉取次数 = 24 * 60 / 接口<EFBFBD>?= 1440 / 接口<EFBFBD>? // 24 / 每天拉取次数 * 60 = 接口间隔
// 每天拉取次数 = 24 * 60 / 接口间隔 = 1440 / 接口间隔
data.pullInterval = 1440 / data.pullInterval; data.pullInterval = 1440 / data.pullInterval;
} }
} }
@ -946,8 +947,11 @@
if (val) { if (val) {
// 明细表使用自定义分页加载 // 明细表使用自定义分页加载
loadDetailTable(); loadDetailTable();
// 优先级表仍用内置分页 // 优先级表只在优先级tab激活时才重新加载,避免重复请求
tableRefPriority.value?.reload(); // 因为TablePro组件在挂载时会自动加载数据(immediate: true)
if (activeTab.value === 'priority') {
tableRefPriority.value?.reload();
}
} }
} }
); );

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { $baseMessage } from '@/utils'; import { $baseMessage } from '@/utils';
import { IconPlus, IconEdit, IconDelete, IconSearch } from '@arco-design/web-vue/es/icon'; import { IconPlus, IconEdit, IconDelete, IconSearch, IconDragArrow } from '@arco-design/web-vue/es/icon';
import Sortable from 'sortablejs';
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
@ -143,6 +144,9 @@
// 明细列表数据(用于新增空白行后重载) // 明细列表数据(用于新增空白行后重载)
const detailListData = ref<any[]>([]); const detailListData = ref<any[]>([]);
// 轨迹内容下拉选项缓存,避免每行 SelectPro 各自请求
const trackContentOptions = ref<any[]>([]);
// 自定义表头列(与 CustomList 双向绑定) // 自定义表头列(与 CustomList 双向绑定)
const customColumns = ref<Array<{ field: string; title: string; isEditing?: boolean; isNew?: boolean }>>([]); const customColumns = ref<Array<{ field: string; title: string; isEditing?: boolean; isNew?: boolean }>>([]);
const customListRef = ref<InstanceType<typeof CustomList>>(); const customListRef = ref<InstanceType<typeof CustomList>>();
@ -153,44 +157,103 @@
const pageSize = ref(30); const pageSize = ref(30);
const jumpPage = ref(''); const jumpPage = ref('');
// 自定义列表:左侧固定列(序号、系统标准轨迹) // 固定列:与编辑页面字段一致(含拖动排序列)
const pushFixedLeftColumns = computed(() => [ const pushFixedLeftColumns = computed(() => [
{ {
field: 'index', field: 'index',
title: t('track.push.table.serialNo'), title: t('track.push.table.serialNo'),
width: 60, width: 60,
align: 'center', align: 'left',
fixed: 'left', fixed: 'left',
formatter: ({ rowIndex }: any) => rowIndex + 1, formatter: ({ rowIndex }: any) => rowIndex + 1,
}, },
{
field: 'sort',
title: '排序',
className: 'sort',
width: 60,
align: 'left',
fixed: 'left',
slots: { default: 'sort' },
},
{ {
field: 'standardTrack', field: 'standardTrack',
title: t('track.push.table.systemStandardTrack'), title: t('track.push.form.trackCode'),
width: 280, width: 280,
align: 'left', align: 'left',
fixed: 'left', fixed: 'left',
slots: { default: 'standardTrack' }, slots: { default: 'standardTrack' },
showOverflow: 'tooltip', showOverflow: 'tooltip',
}, },
{ field: 'trackStatusValue', title: t('track.push.form.pushTrackStatus'), width: 120, align: 'left', fixed: 'left', showOverflow: 'tooltip', slots: { default: 'trackStatusValue' } },
{ field: 'trackContentCodeValue', title: t('track.push.form.pushTrackCode'), width: 180, align: 'left', fixed: 'left', showOverflow: 'tooltip', slots: { default: 'trackContentCodeValue' } },
{ field: 'trackContentValue', title: t('track.push.form.pushTrackContent'), width: 120, align: 'left', fixed: 'left', showOverflow: 'tooltip', slots: { default: 'trackContentValue' } },
{ field: 'trackEnContentValue', title: t('track.push.form.pushTrackContentEn'), width: 120, align: 'left', fixed: 'left', showOverflow: 'tooltip', slots: { default: 'trackEnContentValue' } },
]); ]);
// 自定义列表:右侧固定列(操作) // 自定义列表:右侧固定列(操作)
const pushFixedRightColumns = computed(() => [ const pushFixedRightColumns = computed(() => [
{ {
field: 'action', field: 'action',
title: t('track.push.table.actions'), title: t('track.push.table.actions'),
width: 100, width: 60,
align: 'left', align: 'left',
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
}, },
]); ]);
const pushTableConfigExtra = { const pushTableConfigExtra = {
autoHeight: true, autoHeight: false,
height: undefined, height: 1000,
rowConfig: { useKey: true, keyField: 'id' }, rowConfig: { useKey: true, keyField: 'id' },
size: 'small', size: 'small',
}; };
// 双击激活输入:当前编辑的单元格 key = rowId_field
const editingCell = ref<string | null>(null);
function isCellEditing(row: any, field: string) {
const rowId = row?.id ?? row?._rowKey ?? '';
return editingCell.value === `${rowId}_${field}`;
}
function activateEdit(row: any, field: string) {
const rowId = row?.id ?? row?._rowKey ?? '';
editingCell.value = `${rowId}_${field}`;
}
function deactivateEdit() {
editingCell.value = null;
}
// 表格行拖动
let pushSortable: any = null;
function initPushRowSortable() {
const tableRef = customListRef.value?.tableRef;
if (!tableRef) return;
const tableEl = tableRef.$el || tableRef.xGrid?.$el;
if (!tableEl) return;
const tbody = tableEl.querySelector('.vxe-table--body-wrapper tbody');
if (!tbody) return;
if (pushSortable) pushSortable.destroy();
pushSortable = Sortable.create(tbody, {
handle: '.sort',
animation: 150,
onEnd: async (evt: any) => {
const { oldIndex, newIndex } = evt;
if (oldIndex === newIndex) return;
const tableData = tableRef?.getTableData?.()?.fullData || tableRef?.xGrid?.getTableData()?.fullData || [];
if (tableData?.length > 0) {
const movedRow = tableData[oldIndex];
tableData.splice(oldIndex, 1);
tableData.splice(newIndex, 0, movedRow);
tableData.forEach((row: any, index: number) => {
row.sort = index + 1;
});
detailListData.value = [...tableData];
await nextTick();
customListRef.value?.reloadData?.(tableData);
}
},
});
}
const leftFormProps = computed(() => { const leftFormProps = computed(() => {
return { return {
formLayout: 'vertical', formLayout: 'vertical',
@ -487,45 +550,6 @@
]; ];
}, },
}, },
{
label: t('track.push.form.pushExtendedField'),
field: 'expandFieldsValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
},
{
label: t('track.push.form.timezone'),
field: 'trackTimeZone',
comp: 'Timezone',
span: 12,
},
{
label: t('track.push.form.swapTrackContent'),
field: 'swapTrackContentId',
comp: 'SelectPro',
compProps: ({ formValue }: { formValue: any }) => {
return {
clearable: true,
labelKeys: ['code', 'content'],
titleStrs: [t('track.push.titleStrs.trackCode'), t('track.push.titleStrs.trackContent')],
idKey: 'contentId',
api: postGetContentPage,
disableFn: (id: number) => {
if (id === formValue?.id) return true;
return false;
},
searchKey: 'keywords',
extra: { trackPushConfigId: currentId.value?.id },
};
},
span: 12,
},
], ],
}; };
}); });
@ -558,9 +582,10 @@
async function cellClick(rowId: API.GetTrackPushConfigPageOutputVO) { async function cellClick(rowId: API.GetTrackPushConfigPageOutputVO) {
currentId.value = rowId; currentId.value = rowId;
const configTagsResponse = await getGetList({ const [configTagsResponse, contentRes] = await Promise.all([
type: 'TrackOrderType', getGetList({ type: 'TrackOrderType' }),
}); postGetList({ pageIndex: 1, pageSize: 100, appKey: userStore.appKey }),
]);
tabs.value = configTagsResponse as unknown as any[]; tabs.value = configTagsResponse as unknown as any[];
(configTagsResponse as unknown as any[]).length > 0 (configTagsResponse as unknown as any[]).length > 0
@ -568,6 +593,8 @@
: (activeTab.value = 0); : (activeTab.value = 0);
currentPage.value = 1; currentPage.value = 1;
jumpPage.value = '1'; jumpPage.value = '1';
const content = contentRes as any;
trackContentOptions.value = content?.items ?? content?.data?.items ?? content?.data ?? (Array.isArray(content) ? content : []);
loadTableData(); loadTableData();
} }
@ -679,33 +706,33 @@
title: t('track.push.modal.warningTitle'), title: t('track.push.modal.warningTitle'),
onOk: async () => { onOk: async () => {
await postEditContent({ options: 2, id }); await postEditContent({ options: 2, id });
unref(customListRef)?.tableRef?.reload?.(); loadTableData();
$baseMessage(t('track.push.message.deleteSuccess'), 'success'); $baseMessage(t('track.push.message.deleteSuccess'), 'success');
}, },
}); });
} }
/** ?? */
function onFormSubmitClick() { function onFormSubmitClick() {
loadPushList(); loadPushList();
currentId.value = undefined; currentId.value = undefined;
initialDataSnapshot.value = [];
customListRef.value?.reloadData([]); customListRef.value?.reloadData([]);
} }
/** ???? */
function handleSearch() { function handleSearch() {
Object.assign(searchModel, { keywords: searchKeyword.value }); Object.assign(searchModel, { keywords: searchKeyword.value });
loadPushList(); loadPushList();
currentId.value = undefined; currentId.value = undefined;
initialDataSnapshot.value = [];
customListRef.value?.reloadData([]); customListRef.value?.reloadData([]);
} }
/** ???? */
function handleRefresh() { function handleRefresh() {
searchKeyword.value = ''; searchKeyword.value = '';
Object.assign(searchModel, { keywords: '' }); Object.assign(searchModel, { keywords: '' });
loadPushList(); loadPushList();
currentId.value = undefined; currentId.value = undefined;
initialDataSnapshot.value = [];
customListRef.value?.reloadData([]); customListRef.value?.reloadData([]);
} }
@ -725,13 +752,15 @@
} }
watch( watch(
() => currentId.value, () => customListRef.value?.tableRef,
(val) => { () => {
if (val) cellClick(val); if (customListRef.value?.tableRef) {
nextTick(() => {
setTimeout(() => initPushRowSortable(), 500);
});
}
}, },
{ { immediate: true }
deep: true,
}
); );
onMounted(() => { onMounted(() => {
@ -739,11 +768,22 @@
loadPushList(); loadPushList();
}); });
onUnmounted(() => {
if (pushSortable) {
pushSortable.destroy();
pushSortable = null;
}
});
const loading = ref(false); const loading = ref(false);
const saveLoading = ref(false);
// 用于检测数据变动的快照(加载后深拷贝)
const initialDataSnapshot = ref<any[]>([]);
const loadTableData = async () => { const loadTableData = async () => {
if (!currentId.value?.id || !activeTab.value) { if (!currentId.value?.id || !activeTab.value) {
detailListData.value = []; detailListData.value = [];
initialDataSnapshot.value = [];
customListRef.value?.reloadData?.([]); customListRef.value?.reloadData?.([]);
totalCount.value = 0; totalCount.value = 0;
return; return;
@ -758,16 +798,118 @@
}) as API.GetTrackPushContentPageOutputVOPagedResult; }) as API.GetTrackPushContentPageOutputVOPagedResult;
totalCount.value = res?.totalCount || 0; totalCount.value = res?.totalCount || 0;
const items = res?.items || []; const items = res?.items || [];
const extraKeys = new Set<string>();
items.forEach((r: any) => { items.forEach((r: any) => {
r.trackContentId = r.trackContentId ?? r.contentId; r.trackContentId = r.trackContentId ?? r.contentId;
if (r.extraConfig) {
try {
const parsed = typeof r.extraConfig === 'string' ? JSON.parse(r.extraConfig) : r.extraConfig;
if (parsed && typeof parsed === 'object') {
Object.keys(parsed).forEach((k) => extraKeys.add(k));
}
} catch { /* ignore */ }
}
});
extraKeys.forEach((key) => {
if (!customColumns.value.some((c) => c.title === key)) {
customColumns.value.push({ field: key, title: key });
}
});
items.forEach((r: any) => {
if (r.extraConfig) {
try {
const parsed = typeof r.extraConfig === 'string' ? JSON.parse(r.extraConfig) : r.extraConfig;
if (parsed && typeof parsed === 'object') {
customColumns.value.forEach((col) => {
r[col.field] = parsed[col.title] ?? parsed[col.field] ?? '';
});
}
} catch { /* ignore */ }
}
}); });
detailListData.value = items; detailListData.value = items;
initialDataSnapshot.value = JSON.parse(JSON.stringify(items));
customListRef.value?.reloadData?.(items); customListRef.value?.reloadData?.(items);
nextTick(() => {
setTimeout(() => initPushRowSortable(), 300);
});
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// 获取需要保存的字段:固定列单独传,自定义列打包进 extraConfig(key 用用户定义的表头名称 title)
function getRowPayload(row: any): Partial<API.EditTrackPushContentInput> {
const extraConfig: Record<string, string> = {};
customColumns.value.forEach((col) => {
const v = row[col.field];
extraConfig[col.title] = v != null ? String(v) : '';
});
return {
trackPushConfigId: currentId.value?.id,
orderNoType: activeTab.value as API.TrackOrderType | undefined,
trackContentId: row.trackContentId ?? row.contentId,
trackStatusValue: row.trackStatusValue,
trackContentCodeValue: row.trackContentCodeValue,
trackContentValue: row.trackContentValue,
trackEnContentValue: row.trackEnContentValue,
sort: row.sort,
extraConfig: Object.keys(extraConfig).length > 0 ? JSON.stringify(extraConfig) : undefined,
};
}
// 判断行是否有变动(与快照比较)
function isRowChanged(row: any): boolean {
if (row.__isNew) return true;
const snap = initialDataSnapshot.value.find((s: any) => s.id === row.id);
if (!snap) return true;
const fixedFields = ['trackContentId', 'contentId', 'trackStatusValue', 'trackContentCodeValue', 'trackContentValue', 'trackEnContentValue', 'sort'];
if (fixedFields.some((f) => String(row[f] ?? '') !== String(snap[f] ?? ''))) return true;
const extra1: Record<string, string> = {};
const extra2: Record<string, string> = {};
customColumns.value.forEach((col) => {
extra1[col.title] = String(row[col.field] ?? '');
extra2[col.title] = String(snap[col.field] ?? '');
});
return JSON.stringify(extra1) !== JSON.stringify(extra2);
}
const hasDataChanges = computed(() => {
return detailListData.value.some((row) => isRowChanged(row));
});
async function handleSave() {
if (!currentId.value?.id || !activeTab.value) return;
const toSave = detailListData.value.filter((row) => isRowChanged(row));
if (toSave.length === 0) {
$baseMessage('没有需要保存的变更', 'info');
return;
}
saveLoading.value = true;
try {
for (const row of toSave) {
const payload = getRowPayload(row) as API.EditTrackPushContentInput;
if (row.__isNew) {
payload.options = 1;
delete (payload as any).id;
} else {
payload.options = 3;
payload.id = row.id;
}
await postEditContent(payload);
}
$baseMessage(`成功保存 ${toSave.length} 条`, 'success');
initialDataSnapshot.value = JSON.parse(JSON.stringify(detailListData.value));
// 清除 __isNew 标记并刷新
detailListData.value.forEach((r) => delete r.__isNew);
loadTableData();
} catch (e) {
$baseMessage('保存失败', 'error');
} finally {
saveLoading.value = false;
}
}
// 自定义列下拉建议:取当前列在本表数据中的去重非空值,供 a-auto-complete 使用 // 自定义列下拉建议:取当前列在本表数据中的去重非空值,供 a-auto-complete 使用
function getCustomColumnSuggestions(field: string): { value: string }[] { function getCustomColumnSuggestions(field: string): { value: string }[] {
const set = new Set<string>(); const set = new Set<string>();
@ -795,6 +937,11 @@
id: rowKey, id: rowKey,
trackPushConfigId: currentId.value.id, trackPushConfigId: currentId.value.id,
orderNoType: activeTab.value, orderNoType: activeTab.value,
sort: detailListData.value.length + 1,
trackStatusValue: '',
trackContentCodeValue: '',
trackContentValue: '',
trackEnContentValue: '',
}; };
customColumns.value.forEach((col) => { customColumns.value.forEach((col) => {
blankRow[col.field] = ''; blankRow[col.field] = '';
@ -916,26 +1063,38 @@
:data="detailListData" :data="detailListData"
mode="modal" mode="modal"
:section-title="$t('track.push.table.customHeader')" :section-title="$t('track.push.table.customHeader')"
column-modal-tip="请填写上游接口-推送轨迹的字段名,用于传递入参"
:add-placeholder="$t('track.push.table.customColumnPlaceholder')" :add-placeholder="$t('track.push.table.customColumnPlaceholder')"
:fixed-left-columns="pushFixedLeftColumns" :fixed-left-columns="pushFixedLeftColumns"
:fixed-right-columns="pushFixedRightColumns" :fixed-right-columns="pushFixedRightColumns"
:table-config-extra="pushTableConfigExtra" :table-config-extra="pushTableConfigExtra"
:table-height="400" :table-height="1500"
columns-key="trackPushDetail" columns-key="trackPushDetail"
:custom-header-disabled="!currentId || !currentId.id" :custom-header-disabled="!currentId || !currentId.id"
> >
<template #toolbar-left> <template #toolbar-left>
<TableButtons <div class="flex items-center toolbar-add-save-btns">
:actions="[ <TableButtons
{ :actions="[
label: $t('track.push.actions.add'), {
type: 'primary', label: $t('track.push.actions.add'),
disabled: !currentId || !currentId.id, type: 'primary',
onClick: handleAddBlankRow, disabled: !currentId || !currentId.id,
}, onClick: handleAddBlankRow,
]" },
class="big-btn" ]"
/> class="big-btn"
/>
<a-button
type="primary"
size="small"
:disabled="!currentId || !currentId.id || !hasDataChanges"
:loading="saveLoading"
@click="handleSave"
>
保存
</a-button>
</div>
</template> </template>
<template #toolbar-right> <template #toolbar-right>
<div class="custom-list-toolbar-right-inner"> <div class="custom-list-toolbar-right-inner">
@ -981,24 +1140,41 @@
</div> </div>
</div> </div>
</template> </template>
<template #sort>
<div class="flex justify-center items-center">
<IconDragArrow style="font-size: 20px; cursor: grab; color: var(--color-text-3)" />
</div>
</template>
<template #standardTrack="{ row }"> <template #standardTrack="{ row }">
<SelectPro <SelectPro
:model-value="normalizeTrackContentId(row.trackContentId ?? row.contentId)" :model-value="normalizeTrackContentId(row.trackContentId ?? row.contentId)"
:api="postGetList" :options="trackContentOptions"
:label-keys="['code', 'content', 'enContent']" :label-keys="['code', 'content', 'enContent']"
:title-strs="[$t('track.push.titleStrs.trackCode'), $t('track.push.titleStrs.trackContent'), $t('track.push.table.trackContentEn')]" :title-strs="[$t('track.push.titleStrs.trackCode'), $t('track.push.titleStrs.trackContent'), $t('track.push.table.trackContentEn')]"
separator="-" separator="-"
id-key="id" id-key="id"
search-key="keywords"
:extra="{ appKey: userStore.appKey, pageSize: 100 }"
:is-page="false"
immediate
clearable clearable
class="standard-track-select" class="standard-track-select"
@update:model-value="(val: number) => { row.trackContentId = val; row.contentId = val; }" @update:model-value="(val: number) => { row.trackContentId = val; row.contentId = val; }"
@select-change="(payload: any) => { if (payload?.item) { row.code = payload.item.code; row.content = payload.item.content; row.enContent = payload.item.enContent; } }" @select-change="(payload: any) => { if (payload?.item) { row.code = payload.item.code; row.content = payload.item.content; row.enContent = payload.item.enContent; } }"
/> />
</template> </template>
<template #trackStatusValue="{ row }">
<span v-if="!isCellEditing(row, 'trackStatusValue')" class="editable-cell" @dblclick="activateEdit(row, 'trackStatusValue')">{{ row.trackStatusValue || '-' }}</span>
<a-input v-else v-model="row.trackStatusValue" size="small" class="cell-input" placeholder="请输入" @blur="deactivateEdit" @keyup.enter="deactivateEdit" />
</template>
<template #trackContentCodeValue="{ row }">
<span v-if="!isCellEditing(row, 'trackContentCodeValue')" class="editable-cell" @dblclick="activateEdit(row, 'trackContentCodeValue')">{{ row.trackContentCodeValue || '-' }}</span>
<a-input v-else v-model="row.trackContentCodeValue" size="small" class="cell-input" placeholder="请输入" @blur="deactivateEdit" @keyup.enter="deactivateEdit" />
</template>
<template #trackContentValue="{ row }">
<span v-if="!isCellEditing(row, 'trackContentValue')" class="editable-cell" @dblclick="activateEdit(row, 'trackContentValue')">{{ row.trackContentValue || '-' }}</span>
<a-input v-else v-model="row.trackContentValue" size="small" class="cell-input" placeholder="请输入" @blur="deactivateEdit" @keyup.enter="deactivateEdit" />
</template>
<template #trackEnContentValue="{ row }">
<span v-if="!isCellEditing(row, 'trackEnContentValue')" class="editable-cell" @dblclick="activateEdit(row, 'trackEnContentValue')">{{ row.trackEnContentValue || '-' }}</span>
<a-input v-else v-model="row.trackEnContentValue" size="small" class="cell-input" placeholder="请输入" @blur="deactivateEdit" @keyup.enter="deactivateEdit" />
</template>
<template <template
v-for="col in customColumns" v-for="col in customColumns"
:key="col.field" :key="col.field"
@ -1015,15 +1191,6 @@
</template> </template>
<template #action="{ row }"> <template #action="{ row }">
<div class="action-buttons"> <div class="action-buttons">
<a-button
type="text"
size="small"
class="action-btn edit-btn"
@click="row.__isNew ? handleOpenRightModal('add') : handleOpenRightModal('edit', row)"
:title="$t('track.push.actions.edit')"
>
<IconEdit />
</a-button>
<a-button <a-button
type="text" type="text"
size="small" size="small"
@ -1342,6 +1509,13 @@
} }
} }
/* 新增、保存按钮间距 */
.toolbar-add-save-btns {
display: flex;
align-items: center;
gap: 30px;
}
/* CustomList 工具栏右侧:新增 + 分页,整体在列表右上角 */ /* CustomList 工具栏右侧:新增 + 分页,整体在列表右上角 */
.custom-list-toolbar-right-inner { .custom-list-toolbar-right-inner {
display: flex; display: flex;
@ -1426,96 +1600,12 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
min-height: 0; min-height: 0;
:deep(.vxe-table) { /* 表头、表体文字靠左 */
overflow-x: hidden !important; :deep(.vxe-table .vxe-header--column .vxe-cell),
:deep(.vxe-table .vxe-body--column .vxe-cell) {
.vxe-table--body-wrapper { text-align: left;
overflow-x: hidden !important; justify-content: flex-start;
}
.vxe-table--header-wrapper {
overflow-x: hidden !important;
}
.vxe-body--column:first-child,
.vxe-header--column:first-child {
&,
& .vxe-cell,
& .vxe-cell--title {
text-align: center !important;
justify-content: center !important;
padding-left: 0 !important;
}
.vxe-cell--title {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
}
.vxe-body--column:not(:first-child) .vxe-cell,
.vxe-header--column:not(:first-child) .vxe-cell {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-cell {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-header--column {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-cell--title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.vxe-table--body-wrapper,
.vxe-table--header-wrapper {
width: 100% !important;
}
.vxe-table--main-wrapper {
width: 100% !important;
}
.vxe-table--body,
.vxe-table--header {
width: 100% !important;
}
.vxe-table--body .vxe-body--column:last-child,
.vxe-table--header .vxe-header--column:last-child {
padding-right: 0 !important;
}
}
:deep(.vxe-table--container) {
overflow-x: hidden !important;
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
:deep(.vxe-table) {
width: 100% !important;
margin: 0 !important;
}
:deep(.vxe-table--main-wrapper) {
padding: 0 !important;
margin: 0 !important;
} }
} }
@ -1531,6 +1621,17 @@
width: 100%; width: 100%;
} }
.editable-cell {
cursor: text;
min-height: 20px;
display: block;
width: 100%;
}
.cell-input {
width: 100%;
}
.custom-column-autocomplete { .custom-column-autocomplete {
width: 100%; width: 100%;
} }
@ -1548,7 +1649,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
max-height: 320px; max-height: 640px;
overflow-y: auto; overflow-y: auto;
padding: 4px 0; padding: 4px 0;