From 789992bde6b0d21d75d2d6df770e143370918fda Mon Sep 17 00:00:00 2001 From: JiangSheng Date: Tue, 24 Mar 2026 10:52:01 +0800 Subject: [PATCH 1/2] fix: extra --- .../common/treeCaseTable/treeTable.vue | 69 ------------------ .../competenceCenter/standardScene/index.vue | 70 ++++++++---------- src/utils/node.ts | 25 ++++++- .../condition/components/taskPool.vue | 71 ++++++++----------- 4 files changed, 83 insertions(+), 152 deletions(-) diff --git a/src/components/common/treeCaseTable/treeTable.vue b/src/components/common/treeCaseTable/treeTable.vue index ba63f1b8..4fcff460 100644 --- a/src/components/common/treeCaseTable/treeTable.vue +++ b/src/components/common/treeCaseTable/treeTable.vue @@ -746,7 +746,6 @@ import { ElMessage } from 'element-plus'; import { FLOW_APPROVE_STATUS_ENUM } from '@/utils/enum/flow'; import ApprovalProcess from '@/components/common/approvalProcess/index.vue'; import { getTaskDetailApi } from '@/api/project/task'; -import { getFormConfigureApi } from '@/api/system/systemData'; const commonStore = CommonStore(); const allDictData = ref(commonStore.dictData); @@ -776,58 +775,6 @@ const { TASK_ACHIEVE_STATUS, RESULT_ACHIEVE_STATUS, TASK_APPROVE_STATUS, TASK_PR const treeData = ref([]); const taskStore = useTaskStore(); const reportStore = useReportStore(); -const extraFieldList = ref([]); - -const syncRowExtraFun = (row: any) => { - if (!row || extraFieldList.value.length === 0) { - return; - } - extraFieldList.value.forEach((key) => { - const currentValue = row[key]; - const extras = Array.isArray(row.extras) ? row.extras : (row.extras = []); - const targetExtra = extras.find((item: any) => item.propertyName === key); - const hasCurrentValue = currentValue !== undefined && currentValue !== null; - if (!hasCurrentValue && !targetExtra) { - return; - } - const nextValue = hasCurrentValue ? currentValue : ''; - if (targetExtra) { - if (targetExtra.propertyValue !== nextValue) { - targetExtra.propertyValue = nextValue; - } - return; - } - extras.push({ - propertyName: key, - propertyValue: nextValue, - valueType: '', - propertyClass: 'default', - }); - }); -}; - -const syncTreeDataExtrasFun = (rows: any[] = []) => { - rows.forEach((row: any) => { - syncRowExtraFun(row); - }); -}; - -const getExtraFieldListFun = async () => { - if (!props.tableName) { - extraFieldList.value = []; - return; - } - const res: any = await getFormConfigureApi({ - formName: props.tableName, - }); - if (res.code === 200 && res.data?.formConfig) { - const columns = JSON.parse(res.data.formConfig); - extraFieldList.value = Array.from( - new Set(columns.filter((item: any) => item.type === 2).map((item: any) => item.key)) - ); - syncTreeDataExtrasFun(treeData.value); - } -}; const treeDataFormatFun = (data: any, build: any, pId: any) => { data.forEach((item: any) => { @@ -855,22 +802,6 @@ watch( { immediate: true } ); -watch( - () => treeData.value, - (rows: any[]) => { - // syncTreeDataExtrasFun(rows); - }, - { deep: true } -); - -watch( - () => props.tableName, - () => { - getExtraFieldListFun(); - }, - { immediate: true } -); - const getCheckboxRecordsFun = (isFull: boolean) => { return TreeTableRef.value?.getCheckboxRecordsFun(isFull); }; diff --git a/src/tenants/lyric/views/competenceCenter/standardScene/index.vue b/src/tenants/lyric/views/competenceCenter/standardScene/index.vue index 9d0a1d4d..f2ad64f6 100644 --- a/src/tenants/lyric/views/competenceCenter/standardScene/index.vue +++ b/src/tenants/lyric/views/competenceCenter/standardScene/index.vue @@ -665,7 +665,8 @@ const updateTaskPoolFun = async (formData: any) => { Object.assign(req, addObj); } if (normalizedUpdateRecords.length > 0) { - const updateObj = formatUpdateFun(cloneDeep(normalizedUpdateRecords)); + const dynamicExtraFieldsMap = await getExtraFieldsMapFun(); + const updateObj = formatUpdateFun(cloneDeep(normalizedUpdateRecords), dynamicExtraFieldsMap); Object.assign(req, updateObj); } if (removeRecords.length > 0) { @@ -810,7 +811,10 @@ const formatAddFun = (insertRecords: TreeNode[]) => { addPerformanceExtras: performanceExtras, }; }; -const formatUpdateFun = (updateRecords: TreeNode[]) => { +const formatUpdateFun = ( + updateRecords: TreeNode[], + dynamicExtraFieldsMap?: Record +) => { const updateNodes: any[] = []; const updateNodeExtras: any[] = []; const addNodeExtras: any[] = []; @@ -832,7 +836,7 @@ const formatUpdateFun = (updateRecords: TreeNode[]) => { } else { updateNodes.push(item); } - item.extras = getNodeExtras(item, poolNodeExtraPropPickMap); + item.extras = getNodeExtras(item, poolNodeExtraPropPickMap, dynamicExtraFieldsMap); if (Array.isArray(item.extras) && item.extras.length > 0) { item.extras.forEach((extra) => { if (extra.uuid) { @@ -1160,44 +1164,6 @@ const openDelPoolFun = () => { const onDelPoolConfirmFun = () => { queryPoolListFun(); }; -// const compareTree = (originArr:TreeNode[], insertArr:TreeNode[]) => { -// const insertData: any = []; -// const findInsert = (originArr: any, insertArr: any, originNode: any = null) => { -// insertArr.forEach((insertNode: any) => { -// let found = false; -// // 在第一个树中查找是否存在相同的节点 -// originArr.some((originNode: any) => { -// if (insertNode.nodeName === originNode.nodeName && insertNode.nodeType === originNode.nodeType) { -// found = true; -// for (const key in insertNode) { -// if (!['children', 'fakeId', 'parentId', 'highValue', 'oldValue'].includes(key)) { -// originNode[key] = insertNode[key]; -// } -// } - -// // 如果存在相同节点且有子节点,则递归比较子节点 -// if (insertNode.children && originNode.children) { -// findInsert(originNode.children, insertNode.children, originNode); -// } -// return true; // 停止遍历 -// } -// return false; -// }); -// // 如果在第一个树中未找到相同的节点,则插入 -// if (!found) { -// if (originNode?.fakeId) { -// insertNode.parentId = originNode?.fakeId; -// insertNode.fakeId = insertNode.fakeId + random(0, 1, true); -// } -// insertNode._X_ROW_CHILD = []; -// insertNode._X_ROW_KEY = null; -// insertData.push(insertNode); -// } -// }); -// }; -// findInsert(originArr, insertArr); -// return insertData; -// }; const dict = computed(() => ({ difficult: commonStore.getDictData('DIFFICULTY_COEFFICIENT').A, discipline: commonStore.getDictData('DISCIPLINE_TYPE').A, @@ -1229,6 +1195,28 @@ const getFilterColumnsFun = async () => { return []; }; +const getExtraFieldsMapFun = async (): Promise> => { + const tableNameMap: Record = { + [NODE_TYPE.TASK]: TABLE_NAME.STANDARD_SCENE_TASK, + [NODE_TYPE.PERFORMANCE]: TABLE_NAME.STANDARD_SCENE_PERFORMANCE, + }; + const entries = Object.entries(tableNameMap); + const results = await Promise.all( + entries.map(([, formName]) => getFormConfigureApi({ formName })) + ); + const map: Record = {}; + entries.forEach(([nodeType], index) => { + const res: any = results[index]; + if (res?.code === 200 && res.data?.formConfig) { + const columns = JSON.parse(res.data.formConfig); + map[nodeType] = columns.filter((col: any) => col.type === 2).map((col: any) => col.key); + } else { + map[nodeType] = []; + } + }); + return map; +}; + const onImportPoolConfirmFun = async (formData: any) => { importPoolModalVisible.value = false; const filterColumns = await getFilterColumnsFun(); diff --git a/src/utils/node.ts b/src/utils/node.ts index c53ff17d..30deba03 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -179,7 +179,11 @@ export const pickTreeNodeProp = ( return newNode; }); }; -export const getNodeExtras = (node: TreeNode, extraPickMap: Record) => { +export const getNodeExtras = ( + node: TreeNode, + extraPickMap: Record, + dynamicExtraFieldsMap?: Record +) => { let extras = []; if (!node.extras) { let props = []; @@ -204,6 +208,25 @@ export const getNodeExtras = (node: TreeNode, extraPickMap: Record 0) { + const existingKeys = new Set(extras.map((e: any) => e.propertyName)); + fields.forEach((key) => { + if (!existingKeys.has(key) && node[key] !== undefined && node[key] !== null) { + extras.push({ + propertyName: key, + propertyValue: node[key], + valueType: '', + propertyClass: 'default', + }); + } + }); + } + } + return extras; }; export const transformTreeToPoolNodes = (tree: TreeNode[]) => { diff --git a/src/views/competenceCenter/condition/components/taskPool.vue b/src/views/competenceCenter/condition/components/taskPool.vue index 6f9b8c2e..7e0e4edc 100644 --- a/src/views/competenceCenter/condition/components/taskPool.vue +++ b/src/views/competenceCenter/condition/components/taskPool.vue @@ -609,7 +609,8 @@ const updateTaskPoolFun = async (formData: any) => { Object.assign(req, addObj); } if (normalizedUpdateRecords.length > 0) { - const updateObj = formatUpdateFun(cloneDeep(normalizedUpdateRecords)); + const dynamicExtraFieldsMap = await getExtraFieldsMapFun(); + const updateObj = formatUpdateFun(cloneDeep(normalizedUpdateRecords), dynamicExtraFieldsMap); Object.assign(req, updateObj); } if (removeRecords.length > 0) { @@ -734,7 +735,10 @@ const formatAddFun = (insertRecords: TreeNode[]) => { addPerformanceExtras: performanceExtras, }; }; -const formatUpdateFun = (updateRecords: TreeNode[]) => { +const formatUpdateFun = ( + updateRecords: TreeNode[], + dynamicExtraFieldsMap?: Record +) => { const updateNodes: any[] = []; const updateNodeExtras: any[] = []; const addNodeExtras: any[] = []; @@ -757,7 +761,7 @@ const formatUpdateFun = (updateRecords: TreeNode[]) => { } else { updateNodes.push(item); } - item.extras = getNodeExtras(item, poolNodeExtraPropPickMap); + item.extras = getNodeExtras(item, poolNodeExtraPropPickMap, dynamicExtraFieldsMap); if (Array.isArray(item.extras) && item.extras.length > 0) { item.extras.forEach((extra) => { if (extra.uuid) { @@ -1106,44 +1110,6 @@ const importPoolModalVisible = ref(false); const openImportPoolFun = () => { importPoolModalVisible.value = true; }; -// const compareTree = (originArr:TreeNode[], insertArr:TreeNode[]) => { -// const insertData: any = []; -// const findInsert = (originArr: any, insertArr: any, originNode: any = null) => { -// insertArr.forEach((insertNode: any) => { -// let found = false; -// // 在第一个树中查找是否存在相同的节点 -// originArr.some((originNode: any) => { -// if (insertNode.nodeName === originNode.nodeName && insertNode.nodeType === originNode.nodeType) { -// found = true; -// for (const key in insertNode) { -// if (!['children', 'fakeId', 'parentId', 'highValue', 'oldValue'].includes(key)) { -// originNode[key] = insertNode[key]; -// } -// } - -// // 如果存在相同节点且有子节点,则递归比较子节点 -// if (insertNode.children && originNode.children) { -// findInsert(originNode.children, insertNode.children, originNode); -// } -// return true; // 停止遍历 -// } -// return false; -// }); -// // 如果在第一个树中未找到相同的节点,则插入 -// if (!found) { -// if (originNode?.fakeId) { -// insertNode.parentId = originNode?.fakeId; -// insertNode.fakeId = insertNode.fakeId + random(0, 1, true); -// } -// insertNode._X_ROW_CHILD = []; -// insertNode._X_ROW_KEY = null; -// insertData.push(insertNode); -// } -// }); -// }; -// findInsert(originArr, insertArr); -// return insertData; -// }; const dict = computed(() => ({ difficult: commonStore.getDictData('DIFFICULTY_COEFFICIENT').A, discipline: commonStore.getDictData('DISCIPLINE_TYPE').A, @@ -1175,6 +1141,29 @@ const getFilterColumnsFun = async () => { return []; }; +const getExtraFieldsMapFun = async (): Promise> => { + const tableNameMap: Record = { + [NODE_TYPE.CATEGORY]: TABLE_NAME.TASK_POOL_CATEGORY, + [NODE_TYPE.TASK]: TABLE_NAME.TASK_POOL_TASK, + [NODE_TYPE.PERFORMANCE]: TABLE_NAME.TASK_POOL_PERFORMANCE, + }; + const entries = Object.entries(tableNameMap); + const results = await Promise.all( + entries.map(([, formName]) => getFormConfigureApi({ formName })) + ); + const map: Record = {}; + entries.forEach(([nodeType], index) => { + const res: any = results[index]; + if (res?.code === 200 && res.data?.formConfig) { + const columns = JSON.parse(res.data.formConfig); + map[nodeType] = columns.filter((col: any) => col.type === 2).map((col: any) => col.key); + } else { + map[nodeType] = []; + } + }); + return map; +}; + const onImportPoolConfirmFun = async (formData: any) => { importPoolModalVisible.value = false; const filterColumns = await getFilterColumnsFun(); From 37457577a997afa169de4dfad7da5ffac33b87b3 Mon Sep 17 00:00:00 2001 From: dongzhihuan Date: Tue, 24 Mar 2026 11:18:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?update=20=E9=87=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E3=80=81=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/fileTable/fileRename.vue | 25 ++- src/components/common/fileTable/index.vue | 152 ++++++++++++++---- src/views/system/recycleBin/index.vue | 25 +-- 3 files changed, 158 insertions(+), 44 deletions(-) diff --git a/src/components/common/fileTable/fileRename.vue b/src/components/common/fileTable/fileRename.vue index 68cc8c54..f17d0d4f 100644 --- a/src/components/common/fileTable/fileRename.vue +++ b/src/components/common/fileTable/fileRename.vue @@ -24,6 +24,8 @@ diff --git a/src/components/common/fileTable/index.vue b/src/components/common/fileTable/index.vue index b17f5be2..aa0ce17f 100644 --- a/src/components/common/fileTable/index.vue +++ b/src/components/common/fileTable/index.vue @@ -7,9 +7,8 @@ ...$attrs, ...(needContextMenu ? { - 'menu-config': menuConfig, + 'menu-config': triggerMenuConfig, 'row-class-name': rowClassName, - 'onMenu-click': handleMenuClick, 'onCell-menu': handleContextMenu, } : {}), @@ -38,6 +37,26 @@ :fileData="currentRenameData" @finish="renameFinishFun" /> + +
+ +
+
@@ -47,7 +66,8 @@ import BaseTable from '@/components/common/table/baseTable.vue'; import FilePreview from '@/components/common/filePreview/index.vue'; import { downloadFileById } from '@/utils/file'; import FileTypeChange from '@/components/common/fileTable/fileTypeChange.vue'; -import { ElMessage } from 'element-plus'; +import { ElMessage, ElMessageBox } from 'element-plus'; +import { View, Download, Delete, EditPen, Refresh } from '@element-plus/icons-vue'; import { dataDelFileApi } from '@/api/data/data'; import FileRename from '@/components/common/fileTable/fileRename.vue'; @@ -59,7 +79,7 @@ const props = defineProps({ // 操作栏默认列表 defaultActions: { type: Array, - default: () => ['preview', 'download', 'delete'], + default: () => [], // 'preview', 'download', 'delete', 'rename', 'refresh' }, // 文件id的字段, 1)用于操作文件; 2)用于在右键时高亮当前行, 默认查找顺序:id -> fileId -> uuid fileIdField: { @@ -71,10 +91,10 @@ const props = defineProps({ type: String, default: 'fileName', }, - // 是否需要右键菜单,默认不需要【待开发】 + // 是否需要右键菜单,默认需要 needContextMenu: { type: Boolean, - default: false, + default: true, }, // 文件标签字段【待开发】 fileTagField: { @@ -133,6 +153,15 @@ const defaultActionList = computed(() => [ }, ] : []), + ...(props.defaultActions?.includes('refresh') + ? [ + { + title: '刷新', + type: 'primary', + click: () => fileTableRef.value.resetFun(), + }, + ] + : []), ]); const deleteFileFun = async (id: number) => { @@ -161,29 +190,37 @@ const renameFun = (id: any, row: any) => { renameVisible.value = true; }; const renameFinishFun = () => { - // dzh todo 刷新数据 + // 刷新数据 + fileTableRef.value.resetFun(); }; // #endregion // #region ---------------------------右键菜单模块--------------------------- // 当前右键操作的行 const currentHighlightRowId = ref(''); -const menuConfig = ref({ +const currentContextRow = ref(null); +const contextMenuVisible = ref(false); +const contextMenuPosition = ref({ x: 0, y: 0 }); +const contextMenuActions = ref([]); +const triggerMenuConfig = ref({ enabled: true, body: { - options: [ - [ - { code: 'copy', name: '复制', prefixConfig: { icon: 'vxe-icon-copy' } }, - { code: 'edit', name: '编辑', prefixConfig: { icon: 'vxe-icon-edit' } }, - { code: 'delete', name: '删除', prefixConfig: { icon: 'vxe-icon-delete' } }, - ], - ], + options: [[]], }, }); const handleContextMenu = ({ row, $event }: { row: any; $event: any }) => { $event.preventDefault(); + currentContextRow.value = row; currentHighlightRowId.value = row[props.fileIdField] || row.fileId || row.uuid; // 每行的唯一标识 + contextMenuActions.value = allActionList.value.filter( + (action: any) => !(action.hide && action.hide(row)) + ); + contextMenuPosition.value = { + x: $event.clientX, + y: $event.clientY, + }; + contextMenuVisible.value = true; }; // 添加行样式计算 const rowClassName = ({ row }: { row: any }) => { @@ -191,25 +228,52 @@ const rowClassName = ({ row }: { row: any }) => { return currentHighlightRowId.value === rowId && rowId ? 'highlight-row' : ''; }; -const handleMenuClick = ({ menu, row }: { menu: any; row: any }) => { - // 根据点击的菜单项执行不同操作 - switch (menu.code) { - case 'copy': - console.info('copy', row); - break; - case 'edit': - console.info('edit', row); - break; - case 'delete': - console.info('delete', row); +const handleContextActionClick = (action: any) => { + const row = currentContextRow.value; + if (!row) { + contextMenuVisible.value = false; + currentHighlightRowId.value = ''; + return; } + if (action?.click) { + const tableData = fileTableRef.value?.tableData || []; + const currentRowId = row[props.fileIdField] || row.fileId || row.uuid; + const rowIndex = tableData.findIndex((item: any) => { + const id = item[props.fileIdField] || item.fileId || item.uuid; + return id === currentRowId; + }); + const tip = (action.confirmTipFun && action.confirmTipFun(row, rowIndex)) || action.confirmTip; + if (tip) { + ElMessageBox.confirm(tip, '提示', { + type: 'warning', + }) + .then(() => { + action.click(row, rowIndex); + }) + .catch(() => {}); + } else { + action.click(row, rowIndex); + } + } + contextMenuVisible.value = false; currentHighlightRowId.value = ''; }; + +const getMenuIcon = (action: any) => { + if (action?.icon) return action.icon; + const title = String(action?.title || ''); + if (title.includes('预览')) return View; + if (title.includes('下载')) return Download; + if (title.includes('删除')) return Delete; + if (title.includes('重命名')) return EditPen; + if (title.includes('刷新')) return Refresh; + return null; +}; // 全局点击处理 const handleGlobalClick = (event: any) => { - // 检查点击是否在菜单内 - const menuEl = document.querySelector('.vxe-table--context-menu-wrapper'); + const menuEl = document.querySelector('.file-table-context-menu'); if (menuEl && !menuEl.contains(event.target)) { + contextMenuVisible.value = false; currentHighlightRowId.value = ''; } }; @@ -262,4 +326,36 @@ defineExpose({ background-color: var(--el-color-primary-light-9) !important; } } + +.file-table-context-menu { + position: fixed; + z-index: 3000; + min-width: 120px; + padding: 4px 0; + background: #fff; + border: 1px solid var(--el-border-color-light); + border-radius: 4px; + box-shadow: var(--el-box-shadow-light); + .menu-item { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + color: var(--el-text-color-primary); + cursor: pointer; + user-select: none; + &:hover { + background-color: var(--el-fill-color-light); + } + } + .menu-item-icon { + font-size: 14px; + } + .menu-item-title { + line-height: 1; + } + .menu-item--danger { + color: var(--el-color-danger); + } +} diff --git a/src/views/system/recycleBin/index.vue b/src/views/system/recycleBin/index.vue index 4cc01b66..3f7d37af 100644 --- a/src/views/system/recycleBin/index.vue +++ b/src/views/system/recycleBin/index.vue @@ -68,6 +68,7 @@ const actionList = [ { title: t('通用.还原'), type: 'primary', + icon: 'RefreshLeft', click: (row: any) => { clickRestore('single', row); }, @@ -151,17 +152,19 @@ const clickRestore = (type?: any, row?: any) => { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning', - }).then(async () => { - const ids = type === 'single' ? [row.id] : chosenData.value.map((item: any) => item.id); - const params = { ids }; - restoreFromRecycleApi(params).then((res: any) => { - if (res && res.code === 200) { - ElMessage.success('还原成功'); - chosenData.value = []; - baseTableRef.value.resetFun(); // 刷新数据 - } - }); - }); + }) + .then(async () => { + const ids = type === 'single' ? [row.id] : chosenData.value.map((item: any) => item.id); + const params = { ids }; + restoreFromRecycleApi(params).then((res: any) => { + if (res && res.code === 200) { + ElMessage.success('还原成功'); + chosenData.value = []; + baseTableRef.value.resetFun(); // 刷新数据 + } + }); + }) + .catch(() => {}); };