feat: 分类顺序校验

This commit is contained in:
JiangSheng
2025-12-01 09:39:01 +08:00
parent c2a451edbc
commit 9be1b934e9
5 changed files with 148 additions and 12 deletions

View File

@@ -162,6 +162,7 @@
:nodeType="modalNodeType"
:detail="modalDetail"
:belongProject="belongProject"
:parentCategoryPath="parentNodeCategoryPath"
@confirm="onNodeDetailConfirmFun"
/>
<!-- <userSelectTable
@@ -178,7 +179,7 @@ import { ref, onMounted, nextTick, computed } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import TreeCaseTable from '@/components/common/treeCaseTable/treeTable.vue';
import { getTagKeyMap, NODE_TYPE } from '@/utils/enum/node';
import { canAddChild } from '@/utils/node';
import { canAddChild, findNodePath, extractCategoryNodeTypes } from '@/utils/node';
import nodeDetailDialog from './nodeDetailDialog.vue';
import { TASK_CALCULATE_STATUS, TASK_PROCESS_STATUS } from '@/utils/enum/task';
import { disposeTagKey } from '@/views/task/projectDetail/components/project';
@@ -193,6 +194,7 @@ const formItemList = ref<any>([]);
const modalVisible = ref<boolean>(false);
const formData = ref<any>({});
const addNodeDisabled = ref<any>({});
const parentNodeCategoryPath = ref<string[]>([]);
interface Props {
data: any;
@@ -267,9 +269,21 @@ const operationType = ref<OPERATION_TYPE>(OPERATION_TYPE.ADD);
const modalNodeType = ref<string>('');
const modalDetail = ref<any>(null);
const addRowFun = (nodeType?: any) => {
const checkRowData = getVxeRef()?.getCheckboxRecords(true);
let categoryPath: string[] = [];
if (checkRowData && checkRowData.length > 0) {
const { fullData } = getVxeRef().getTableData();
const nodePath = findNodePath(fullData, checkRowData[0]);
if (nodePath) {
categoryPath = extractCategoryNodeTypes(nodePath);
}
}
operationType.value = OPERATION_TYPE.ADD;
modalNodeType.value = nodeType;
modalDetail.value = null;
parentNodeCategoryPath.value = categoryPath;
modalVisible.value = true;
};
const editRowFun = (row: any) => {

View File

@@ -39,16 +39,18 @@
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue';
import { ref, computed, nextTick, watch, onMounted } from 'vue';
import Dialog from '@/components/common/dialog/index.vue';
import TableForm from '@/components/common/table/tableForm.vue';
import { getTagKeyMap, NODE_TYPE } from '@/utils/enum/node';
import { getMemberListIds } from '@/utils/task';
import { disposeTagKey } from '@/views/task/projectDetail/components/project';
import { isCategoryType, isCategoryNodeType } from '@/utils/node';
import { isCategoryType, isCategoryNodeType, validateCategoryLevel } from '@/utils/node';
import flowTemplateSelect from './flowTemplateSelect.vue';
import knowledgeSelect from './knowledgeSelect.vue';
import { TABLE_NAME } from '@/utils/enum/tableName';
import { useDict } from '@/utils/useDict';
import { useI18n } from 'vue-i18n';
enum OPERATION_TYPE {
ADD = 'add',
@@ -62,6 +64,7 @@ interface Props {
nodeType?: string;
modalTableNameList?: string[];
belongProject?: boolean;
parentCategoryPath?: string[];
}
const props = withDefaults(defineProps<Props>(), {
@@ -72,6 +75,7 @@ const props = withDefaults(defineProps<Props>(), {
modalTableNameList: () => ([TABLE_NAME.TASK_POOL_CATEGORY, TABLE_NAME.TASK_POOL_TASK, TABLE_NAME.TASK_POOL_PERFORMANCE]),
detail: null,
belongProject: false,
parentCategoryPath: () => [],
});
const emits = defineEmits(['update:modelValue', 'confirm']);
@@ -80,6 +84,7 @@ const visible = computed({
set: (val) => emits('update:modelValue', val),
});
const tableFormRef = ref<any>();
const { t } = useI18n();
const localDiaTitle = ref('');
const localDetail = ref<any>(null);
@@ -88,6 +93,8 @@ const localTableName = ref(props.tableName || '');
// 流程模板相关
const selectedFlowTemplate = ref<any>(null);
const standard = ref();
const tagSortOrderList = ref<string[]>([]);
const tagNameMap = ref<Map<string, string>>(new Map());
const onShowFun = () => {
resetFun();
nextTick(() => {
@@ -154,28 +161,28 @@ const prepareFromProps = () => {
const operation = props.operationType;
const detail = props.detail;
let text = operation === OPERATION_TYPE.ADD ? '新增' : '编辑';
let text = operation === OPERATION_TYPE.ADD ? t('通用.新增') : t('通用.编辑');
let targetTableName = props.modalTableNameList?.[1] || '';
switch (nodeType) {
case NODE_TYPE.CATEGORY:
text += '分类';
text += t('工况库.分类');
targetTableName = props.modalTableNameList?.[0] || targetTableName;
break;
case NODE_TYPE.TASK:
if (props.belongProject) {
text += '任务';
text += t('通用.任务');
} else {
text += '工况';
text += t('工况库.工况');
}
targetTableName = props.modalTableNameList?.[1] || targetTableName;
break;
case NODE_TYPE.PERFORMANCE:
text += '指标';
text += t('工况库.指标');
targetTableName = props.modalTableNameList?.[2] || targetTableName;
break;
}
if (isCategoryNodeType(nodeType)) {
text += '分类';
text += t('工况库.分类');
targetTableName = props.modalTableNameList?.[0] || targetTableName;
}
@@ -202,10 +209,48 @@ watch(() => props.modelValue, (val) => {
}
});
const ruleData = ref<any>({
const ruleData = computed(() => ({
nodeType: [
{
message: computed(() => {
if (tagSortOrderList.value.length === 0) {
return t('工况库.需按照配置顺序逐层添加');
}
const orderNames = tagSortOrderList.value
.filter(type => isCategoryNodeType(type))
.map(type => tagNameMap.value.get(type) || type)
.join(' → ');
return t('工况库.请按照顺序添加', { order: orderNames });
}),
trigger: 'change',
validator: (val: unknown) => {
if (localOperationType.value === OPERATION_TYPE.EDIT) {
return true;
}
if (!isCategoryNodeType(val as string)) {
return true;
}
if (!props.parentCategoryPath || props.parentCategoryPath.length === 0) {
return true;
}
if (tagSortOrderList.value.length === 0) {
return true;
}
return validateCategoryLevel(
val as string,
props.parentCategoryPath,
tagSortOrderList.value
);
},
},
],
highValue: [
{
message: '指标类型为数值时目标值应为数字',
message: t('工况库.指标类型为数值时目标值应为数字'),
trigger: 'change',
validator: (val: unknown) => {
if (val === null || val === undefined || val === '') {
@@ -227,6 +272,22 @@ const ruleData = ref<any>({
},
],
}));
const initDictData = () => {
const { TAG_TYPE_MAP_LIST, POOL_CATEGORY_TYPE } = useDict('TAG_TYPE_MAP_LIST', 'POOL_CATEGORY_TYPE');
tagSortOrderList.value = TAG_TYPE_MAP_LIST?.value?.A?.reduce((acc: string[], curr: { value: string; }) => {
acc.push(curr.value);
return acc;
}, []) || [];
const categoryTypeList = POOL_CATEGORY_TYPE?.value.A || [];
categoryTypeList.forEach((item: { value: string; label: string; }) => {
tagNameMap.value.set(item.value, item.label);
});
};
onMounted(() => {
initDictData();
});
</script>

View File

@@ -19,6 +19,7 @@ const lang = {
'下一步': 'Next',
'查看': 'View',
'审批类型': 'Approval Type',
'任务': 'Task',
},
'菜单': {
'首页': 'Home',
@@ -89,6 +90,9 @@ const lang = {
'节点': 'Node',
'工况': 'Task',
'指标': 'Performance',
'需按照配置顺序逐层添加': 'Please add in configured order',
'请按照顺序添加': 'Please add in order ({order})',
'指标类型为数值时目标值应为数字': 'Target value must be a number when indicator type is numeric',
'删除': 'Delete',
'创建清单库': 'Create Pool',
'导入Excel': 'Import Excel',

View File

@@ -19,6 +19,7 @@ const lang = {
'下一步': '下一步',
'查看': '查看',
'审批类型': '审批类型',
'任务': '任务',
},
'菜单': {
'首页': '首页',
@@ -89,6 +90,9 @@ const lang = {
'节点': '节点',
'工况': '工况',
'指标': '指标',
'需按照配置顺序逐层添加': '需按照配置顺序逐层添加',
'请按照顺序添加': '请按照顺序({order})添加',
'指标类型为数值时目标值应为数字': '指标类型为数值时,目标值应为数字',
'删除': '删除',
'创建清单库': '创建清单库',
'导入Excel': '导入Excel',

View File

@@ -1,7 +1,9 @@
import { nodeTypeList, NODE_TYPE, LEVEL_TYPE, nodeTypeRelations, poolNodePropPickMap, poolNodeExtraPropPickMap } from '@/utils/enum/node';
import type { TreeNode } from '@/utils/enum/node';
import { getMemberListIds } from './task';
import { CommonStore } from '@/stores/common';
const commonStore = CommonStore();
export const toMap = <T extends string | number, U>(list: { value: T, label: U }[]): Record<T, U> => {
return (list.reduce((obj, { value, label }) => {
obj[value] = label;
@@ -50,7 +52,8 @@ export const isCategoryType = (nodeType: string): boolean => {
return nodeType === NODE_TYPE.CATEGORY;
};
export const isCategoryNodeType = (nodeType: string): boolean => {
return !isInnerNodeType(nodeType);
const categoryTypeList = (commonStore.getDictData('POOL_CATEGORY_TYPE'))?.A || [];
return categoryTypeList.some((item: { value: string }) => item.value === nodeType);
};
export const isMixedNodeType = (nodeType: string): boolean => {
return nodeType === NODE_TYPE.MIXED;
@@ -320,3 +323,53 @@ export const extractLeafNodesWithParentTypes = (list: any[] | undefined): any[]
walk(list, []);
return result;
};
export const findNodePath = (tree: TreeNode[], targetNode: TreeNode): TreeNode[] | null => {
const dfs = (nodes: TreeNode[], path: TreeNode[]): TreeNode[] | null => {
for (const node of nodes) {
const currentPath = [...path, node];
if (node.fakeId === targetNode.fakeId ) {
return currentPath;
}
if (node.children && node.children.length > 0) {
const result = dfs(node.children, currentPath);
if (result) return result;
}
}
return null;
};
return dfs(tree, []);
};
export const extractCategoryNodeTypes = (nodePath: TreeNode[]): string[] => {
return nodePath
.filter(node => isCategoryNodeType(node.nodeType))
.map(node => node.nodeType);
};
export const validateCategoryLevel = (
selectedNodeType: string,
parentCategoryPath: string[],
tagOrder: string[]
): boolean => {
if (!isCategoryNodeType(selectedNodeType)) {
return true;
}
if (parentCategoryPath.length === 0) {
return true;
}
const lastParentNodeType = parentCategoryPath[parentCategoryPath.length - 1];
const selectedIndex = tagOrder.indexOf(selectedNodeType);
const lastParentIndex = tagOrder.indexOf(lastParentNodeType);
if (selectedIndex === -1 || lastParentIndex === -1) {
return false;
}
return selectedIndex >= lastParentIndex;
};