feat: 仿真策划审批
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user