feat: 仿真策划审批

This commit is contained in:
JiangSheng
2026-02-05 19:42:29 +08:00
parent 1af593bb45
commit b4938b8584
3 changed files with 332 additions and 70 deletions

View File

@@ -100,6 +100,10 @@ watch(
immediate: true,
}
);
defineExpose({ rollBackValue });
const getNodeInfo = () => {
return nodeLevel2ListOptions.value?.find((item: any) => item.value === nodeUuid.value)?.info;
};
defineExpose({ rollBackValue, getNodeInfo });
</script>
<style lang="scss" scoped></style>

View File

@@ -1,6 +1,37 @@
<template>
<div class="approval-page">
<div class="approval-page-content">
<div class="full">
<div class="header">
<div class="label">{{ $t('仿真策划.仿真策划') }}</div>
<div class="info-group">
<div v-if="projectName" class="info-item">
<span class="info-label">{{ $t('仿真策划.项目名称') }}</span>
<span class="info-value">{{ projectName }}</span>
</div>
<div v-if="projectCode" class="info-item">
<span class="info-label">{{ $t('仿真策划.项目编号') }}</span>
<span class="info-value">{{ projectCode }}</span>
</div>
<div v-if="phaseName" class="info-item">
<span class="info-label">{{ $t('仿真策划.阶段') }}</span>
<span class="info-value">{{ phaseName }}</span>
</div>
</div>
<div class="legend-tooltip">
<div class="legend-item">
<span class="legend-color added"></span>
<span class="legend-label">{{ $t('通用.新增') }}</span>
</div>
<div class="legend-item">
<span class="legend-color updated"></span>
<span class="legend-label">{{ $t('通用.编辑') }}</span>
</div>
<div class="legend-item">
<span class="legend-color deleted"></span>
<span class="legend-label">{{ $t('通用.删除') }}</span>
</div>
</div>
</div>
<div class="body">
<loadCaseTable
:editMode="false"
ref="treeTableRef"
@@ -17,101 +48,300 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { nextTick, onMounted, ref, watchEffect } from 'vue';
import loadCaseTable from '@/components/common/treeCaseTable/loadCaseTable.vue';
import { getTaskTreeFun } from '@/views/task/projectDetail/components/projectApi';
import type { VxeTablePropTypes } from 'vxe-table';
const props = defineProps({
pageData: {
type: Object,
default: () => {},
},
interface Props {
pageData: any;
}
const props = withDefaults(defineProps<Props>(), {
pageData: {},
});
enum PreviewAction {
Added = 'added',
Updated = 'updated',
Deleted = 'deleted',
}
const projectName = ref('');
const projectCode = ref('');
const phaseName = ref('');
const approveContents = ref<any>(null);
const mergedPreviewTree = ref<any>([]);
const addUuids = ref<any>([]);
const delUuids = ref<any>([]);
const editUuids = ref<any>([]);
const isCreatePool = ref(false);
// 获取审批内容
const getapprovalCOntentFun = async (data: any) => {
const approveContents = JSON.parse(data?.approveContents as string);
const { phaseNodeId, projectNodeId, deleteNodeList, editNodeList, addNodeList } = approveContents;
getUUids(deleteNodeList, delUuids.value);
getUUids(editNodeList, editUuids.value);
getUUids(addNodeList, addUuids.value);
const treeData = await getTaskTreeFun(projectNodeId, phaseNodeId);
if (treeData.length) {
mergedPreviewTree.value = treeData;
isCreatePool.value = false;
} else {
mergedPreviewTree.value = addNodeList;
isCreatePool.value = true;
}
const buildMapById = (list: any[] = []) => {
const map: Record<string, any> = {};
const walk = (nodes: any[]) => {
(nodes || []).forEach((n) => {
if (n?.fakeId) map[n.fakeId] = n;
if (n.children) walk(n.children);
});
};
walk(list);
return map;
};
const getUUids = (list: any, arr: any) => {
for (let i = 0; i < list.length; i++) {
if (list[i].uuid) {
arr.push(list[i].uuid);
const cloneNode = (node: any) => JSON.parse(JSON.stringify(node));
const collectDeletedNodes = (original: any[] = [], removeIds: string[] = []) => {
const origMap = buildMapById(original);
const parentMap: Record<string, string | null> = {};
const childrenOrder: Record<string, string[]> = {};
const rootOrder: string[] = [];
const walk = (nodes: any[], parentId: string | null) => {
(nodes || []).forEach((n: any) => {
if (!n || !n.fakeId) return;
const id = n.fakeId;
if (parentId === null) rootOrder.push(id);
parentMap[id] = parentId;
childrenOrder[id] = (n.children || []).map((c: any) => c.fakeId).filter(Boolean);
if (n.children) walk(n.children, id);
});
};
walk(original, null);
const removedSet = new Set(removeIds.filter(Boolean));
const topRemoved = Array.from(removedSet).filter((id) => {
let p = parentMap[id];
while (p) {
if (removedSet.has(p)) return false;
p = parentMap[p];
}
if (list[i].nodeId) {
arr.push(list[i].nodeId);
}
if (list[i]?.children?.length) {
getUUids(list[i]?.children, arr);
}
}
return true;
});
const cloneAndMark = (node: any) => {
const n = cloneNode(node);
const markRec = (x: any) => {
if (!x) return;
delete x._X_ROW_KEY;
delete x._X_ROW_CHILD;
x._previewAction = PreviewAction.Deleted;
if (x.children) x.children.forEach((c: any) => markRec(c));
};
markRec(n);
return n;
};
const entries: { node: any; parentId: string | null; rootIndex: number; siblingIndex: number }[] =
[];
topRemoved.forEach((id) => {
const node = origMap[id];
if (!node) return;
const parentId = parentMap[id] ?? null;
const rootIndex = rootOrder.indexOf(id);
const siblingIndex = parentId ? (childrenOrder[parentId] || []).indexOf(id) : rootIndex;
entries.push({ node: cloneAndMark(node), parentId, rootIndex, siblingIndex });
});
entries.sort((a, b) => {
const ai = a.rootIndex >= 0 ? a.rootIndex : 0;
const bi = b.rootIndex >= 0 ? b.rootIndex : 0;
return ai - bi;
});
return { entries, rootOrder, childrenOrder };
};
const rowClassName: any = ({ row }: any) => {
const rowClassName: VxeTablePropTypes.RowClassName<any> = ({ row }) => {
if (!row) return '';
if (isCreatePool.value) {
if (row._previewAction === PreviewAction.Added) {
return 'preview-added';
}
if (addUuids.value.includes(row.uuid) || addUuids.value.includes(row.nodeId)) {
return 'preview-added';
} else if (editUuids.value.includes(row.uuid) || editUuids.value.includes(row.nodeId)) {
} else if (row._previewAction === PreviewAction.Updated) {
return 'preview-updated';
} else if (delUuids.value.includes(row.uuid) || delUuids.value.includes(row.nodeId)) {
} else if (row._previewAction === PreviewAction.Deleted) {
return 'preview-deleted';
}
return '';
};
onMounted(async () => {
const initPreviewData = () => {
mergedPreviewTree.value = [];
approveContents.value = JSON.parse(props.pageData?.approveContents || '{}');
const approvePreviewInfo = approveContents.value?.approvePreviewInfo || {};
projectName.value = approvePreviewInfo.projectName || '';
projectCode.value = approvePreviewInfo.projectCode || '';
phaseName.value = approvePreviewInfo.phaseName || '';
const finalFullData = approvePreviewInfo.finalFullData || [];
const originalFullData = approvePreviewInfo.originalFullData || [];
const insertRecords = approvePreviewInfo.insertRecords || [];
const updateRecords = approvePreviewInfo.updateRecords || [];
const removeRecords = approvePreviewInfo.removeRecords || [];
const previewData = cloneNode(finalFullData);
const markTree = (nodes: any[]) => {
(nodes || []).forEach((n) => {
delete n._X_ROW_KEY;
delete n._X_ROW_CHILD;
if (!n) return;
if ((insertRecords || []).some((x: any) => x.fakeId === n.fakeId)) {
n._previewAction = PreviewAction.Added;
} else if ((updateRecords || []).some((x: any) => x.fakeId === n.fakeId)) {
n._previewAction = PreviewAction.Updated;
}
if (n.children) markTree(n.children);
});
};
markTree(previewData);
const removeIds = (removeRecords || []).map((r: any) => r.fakeId || r);
const {
entries: deletedEntries,
rootOrder,
childrenOrder,
} = collectDeletedNodes(originalFullData, removeIds);
const findNodeRef = (nodes: any[], id: string): any | null => {
for (const n of nodes || []) {
if (!n) continue;
if (n.fakeId === id) return n;
if (n.children) {
const r = findNodeRef(n.children, id);
if (r) return r;
}
}
return null;
};
const existsInPreview = (id: string) => !!findNodeRef(previewData, id);
deletedEntries.forEach(({ node, parentId, rootIndex, siblingIndex }) => {
if (!node || !node.fakeId) return;
if (existsInPreview(node.fakeId)) return;
if (parentId && existsInPreview(parentId)) {
const parentRef = findNodeRef(previewData, parentId);
if (!parentRef.children) parentRef.children = [];
const origSiblings = childrenOrder[parentId] || [];
const beforeOrig = origSiblings.slice(0, Math.max(0, siblingIndex));
const insertPos = beforeOrig.reduce((cnt, sid) => {
return cnt + (parentRef.children.some((c: any) => c.fakeId === sid) ? 1 : 0);
}, 0);
parentRef.children.splice(insertPos, 0, node);
} else {
const beforeRoots = rootOrder.slice(0, Math.max(0, rootIndex));
const insertPos = beforeRoots.reduce((cnt, rid) => {
return cnt + (previewData.some((n: any) => n.fakeId === rid) ? 1 : 0);
}, 0);
previewData.splice(insertPos, 0, node);
}
});
mergedPreviewTree.value = previewData;
expandAllFun();
};
const treeTableRef = ref();
const getVxeRef = () => {
return treeTableRef?.value?.loadcaseTableRef?.TreeTableRef?.treeTableRef;
};
const expandAllFun = () => {
nextTick(() => {
getVxeRef()?.setAllTreeExpand(true);
});
};
onMounted(() => {
if (props.pageData) {
getapprovalCOntentFun(props.pageData);
console.log(props.pageData, 'props.pageData');
initPreviewData();
}
});
watchEffect(() => {
if (props.pageData) {
initPreviewData();
}
});
</script>
<style lang="scss" scoped>
.approval-page {
.full {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
}
.approval-page-tooltips {
width: 100%;
height: 40px;
.header {
height: 20px;
display: flex;
align-items: center;
justify-content: flex-start;
.label {
font-weight: 600;
}
.info-group {
margin-left: var(--padding-small);
display: flex;
align-items: center;
justify-content: flex-end;
}
flex-direction: row;
gap: 20px;
.approval-page-content {
width: 100%;
// height: calc(100% - 40px);
height: 100%;
.info-item {
display: flex;
align-items: center;
}
.info-label {
color: var(--el-text-color-secondary);
}
.info-value {
color: var(--el-text-color-primary);
font-weight: 500;
}
}
}
.body {
flex: 1;
overflow: auto;
}
:deep(.preview-added) {
background-color: var(--el-color-success-light-7);
}
:deep(.preview-updated) {
background-color: var(--el-color-primary-light-7);
}
:deep(.preview-deleted) {
background-color: var(--el-color-danger-light-7);
}
.legend-tooltip {
display: flex;
flex-direction: row;
gap: 20px;
padding: 6px 8px;
flex: 1;
margin-left: auto;
justify-content: flex-end;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.legend-color {
width: 24px;
height: 12px;
border-radius: 2px;
display: inline-block;
}
.legend-color.added {
background-color: var(--el-color-success-light-7);
}
@@ -123,4 +353,9 @@ onMounted(async () => {
.legend-color.deleted {
background-color: var(--el-color-danger-light-7);
}
.legend-label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -343,6 +343,7 @@ const props = defineProps<{
}>();
const dialogApproveUserVisible = ref(false);
const isEmptyPool = ref(true);
let originalSnapshot: any = null;
const dialogVisible = computed(() => {
return props.showTaskDialog;
@@ -427,8 +428,14 @@ const getNodeLevel3TreeByLevel2Uuid = async () => {
rightTableLoading.value = true;
rightTableData.value = await getTaskTreeFun(props.nodeLevel1Uuid, nodeLevel2Uuid.value);
rightTableLoading.value = false;
nextTick(() => {
originalSnapshot = cloneDeep(rightTableData.value);
});
isEmptyPool.value = !rightTableData.value || rightTableData.value.length === 0;
} else {
rightTableData.value = [];
originalSnapshot = null;
isEmptyPool.value = true;
}
nextTick(() => {
// 待修改
@@ -812,6 +819,7 @@ const addOrEditTaskFun = async () => {
// } else {
approveParam.value = {
insertTreeList,
insertRecords: insertList,
removeRecords,
updateList,
};
@@ -853,6 +861,25 @@ const onAddApproveConfirmFun = async (formData: any) => {
return;
}
const { fullData } = getRightVxeRef()?.getTableData() || { fullData: [] };
const columns = getRightVxeRef()?.getColumns() || [];
const currentPhaseInfo = nodeLevel2SelectRef.value?.getNodeInfo();
const approvePreviewInfo = {
isCreate: isEmptyPool.value,
isUpdate: !isEmptyPool.value,
columns,
originalFullData: cloneDeep(originalSnapshot),
finalFullData: cloneDeep(fullData),
insertRecords: approveParam.value.insertRecords,
updateRecords: approveParam.value.updateList,
removeRecords: approveParam.value.removeRecords,
projectName: props.nodeLevel1Name,
projectCode: props.nodeLevel1Info?.nodeCode,
phaseName: currentPhaseInfo?.nodeName || '',
};
const param = {
addNodeList: approveParam.value.insertTreeList,
editNodeList: approveParam.value.updateList,
@@ -861,6 +888,7 @@ const onAddApproveConfirmFun = async (formData: any) => {
tagMap: getTagMapList(),
projectNodeId: props.nodeLevel1Uuid,
phaseNodeId: nodeLevel2Uuid.value,
approvePreviewInfo,
...formData,
};
@@ -1277,12 +1305,7 @@ const queryDesignVersionsFun = async () => {
if (res.data?.length) {
projectPhaseVersionList.value = [res.data.at(-1)];
currentProjectPhaseTaskTreeVersion.value = projectPhaseVersionList.value[0]?.currentVersion;
isEmptyPool.value = false;
} else {
isEmptyPool.value = true;
}
}
} catch {}