整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
<template>
<div class="xmind-box">
<div class='action-panel'>
<!-- <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button> -->
<el-button type="primary" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
<!-- <el-button type="primary" icon="el-icon-view" @click="handleRefresh">详情</el-button>
<el-button type="primary" icon="el-icon-edit" @click="handleRefresh">编辑</el-button>
<el-button type="primary" icon="el-icon-folder" @click="previewFiles(currentNode)">文件</el-button>
<el-button type="primary" icon="el-icon-picture" @click="previewImages(currentNode)">图片</el-button> -->
</div>
<div class="xmind-container" ref="chart" style="width: 100%; height: 800px;"></div>
<!-- 新增三级节点点击弹窗-查看完整信息 -->
<el-dialog title="节点详情信息" :visible.sync="dialogVisible" width="1200px" center append-to-body>
<el-form>
<el-row :gutter="10">
<el-col :span="6">
<el-form-item label="进度类别" prop="name">
<el-input v-model="currentNode.tabNode" placeholder="请输入进度类型"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="一级分类" prop="firstLevelNode">
<el-input v-model="currentNode.firstLevelNode" placeholder="请输入一级分类"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="步骤名称" prop="secondLevelNode">
<el-input v-model="currentNode.secondLevelNode" placeholder="请输入步骤名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="nodeHeader">
<el-input v-model="currentNode.nodeHeader" placeholder="请输入负责人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格需求" prop="specification">
<el-input v-model="currentNode.specification" placeholder="规格需求"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="成果资料" prop="relatedDocs">
<file-upload @success="handleFileSuccess" @delete="handleFileDelete" v-model="currentNode.relatedDocs"
placeholder="成果资料"></file-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="相关图片" prop="relatedImages">
<image-upload v-model="currentNode.relatedImages" placeholder="相关图片"></image-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="需求资料" prop="requirementFile">
<file-upload @success="handleFileSuccess" @delete="handleFileDelete" v-model="currentNode.requirementFile"
placeholder="需求资料"></file-upload>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- <div class="dialog-content">
<div class="node-title">{{ currentNode.name }}</div>
<div class="node-info-item" v-for="(val, key) in currentNode.value" :key="key">
<span class="label">{{ key }}</span>
<span class="value">{{ val || '无' }}</span>
</div>
</div> -->
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="handleSubmit">提交修改</el-button>
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { addFileOperationRecord } from '@/api/oa/fileOperationRecord';
import { updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
import * as echarts from 'echarts';
export default {
name: "Xmind",
props: {
list: {
type: Array,
default: () => []
}
},
data () {
return {
chartInstance: null, // 保存图表实例,用于后续重绘/销毁
dialogVisible: false, // 新增:弹窗显示隐藏开关
currentNode: {}, // 新增:存储当前点击的三级节点完整数据
clickEvent: null, // 新增:存储点击事件句柄,用于销毁解绑
users: [],
supplierList: [],
};
},
watch: {
// 监听列表数据变化,自动更新图表
list: {
deep: true,
handler () {
if (this.chartInstance) {
this.initChart();
}
}
}
},
mounted () {
this.initChart();
// 监听窗口大小变化,自适应重绘
window.addEventListener('resize', this.resizeChart);
},
beforeDestroy () {
window.removeEventListener('resize', this.resizeChart);
// 新增解绑Echarts点击事件防止内存泄漏
if (this.chartInstance && this.clickEvent) {
this.chartInstance.off('click', this.clickEvent);
}
// 销毁图表实例,防止内存泄漏
this.chartInstance?.dispose();
},
methods: {
// 优化:增加防抖处理-窗口自适应,避免频繁触发
resizeChart () {
this.chartInstance?.resize()
},
handleSubmit () {
updateProjectScheduleStep(this.currentNode).then(response => {
this.$modal.msgSuccess("修改成功");
this.dialogVisible = false;
this.handleRefresh();
});
},
handleRefresh () {
this.$emit('refresh')
},
handleFileSuccess (resList, res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 1,
projectId: this.currentNode.projectId,
trackId: this.currentNode.trackId,
})
console.log(this.currentNode, this.currentNode.relatedDocs);
updateProjectScheduleStep({
...this.currentNode,
relatedDocs: this.currentNode.relatedDocs,
})
},
handleFileDelete (res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 2,
projectId: this.currentNode.projectId,
trackId: this.currentNode.trackId,
})
console.log(this.currentNode, this.currentNode.relatedDocs);
updateProjectScheduleStep({
...this.currentNode,
relatedDocs: this.currentNode.relatedDocs,
})
},
// 核心方法:把扁平数组 转为 ECharts树图需要的嵌套树形结构
transformToTreeData (list) {
if (!list.length) return { name: '暂无项目数据', children: [] };
// 1. 获取项目名称(所有数据是同一个项目,取第一条即可)
const projectName = list[0].projectName || '项目进度树图';
// 2. 构建层级Map去重+归集子节点
const levelMap = new Map();
list.forEach(item => {
const firstLevel = item.firstLevelNode || '未分类一级节点';
const secondLevel = item.secondLevelNode || '未分类二级节点';
// 状态映射0=未开始(蓝色) 2=已完成(绿色) 其他=进行中(橙色),可根据业务调整
const statusText = item.status === 0 ? '待开始' : item.status === 2 ? '✅已完成' : '🔵进行中';
const statusColor = item.status === 0 ? '#409EFF' : item.status === 2 ? '#67C23A' : '#E6A23C';
// 组装节点数据:显示名称+业务信息+样式
const nodeData = {
name: secondLevel,
itemStyle: { color: statusColor },
label: { color: statusColor },
// 自定义业务数据,鼠标悬浮时显示
value: {
...item,
负责人: item.nodeHeader || '无',
状态: statusText,
计划完成: item.planEnd || '无',
说明: item.specification || '无'
}
};
// 归集一级节点和二级节点
if (!levelMap.has(firstLevel)) {
levelMap.set(firstLevel, []);
}
levelMap.get(firstLevel).push(nodeData);
});
// 3. 组装最终的树形结构
const treeChildren = Array.from(levelMap).map(([firstName, children]) => ({
name: firstName,
itemStyle: { color: '#303133' }, // 一级节点统一深灰色
children: children
}));
return {
name: projectName,
itemStyle: { color: '#1890FF' }, // 根节点(项目名)蓝色高亮
children: treeChildren
};
},
// 初始化图表
initChart () {
// 初始化图表实例
if (!this.chartInstance) {
this.chartInstance = echarts.init(this.$refs.chart);
}
// 重要:先解绑已有点击事件,防止多次绑定导致弹窗多次触发
if (this.clickEvent) {
this.chartInstance.off('click', this.clickEvent);
}
// 转换数据格式
const treeData = this.transformToTreeData(this.list);
// 设置图表配置项
const option = {
tooltip: {
trigger: 'item',
formatter: ({ data }) => {
// 鼠标悬浮展示完整业务信息
let tip = `<div style="font-size:14px"><b>${data.name}</b></div>`;
if (data.value) {
// Object.keys(data.value).forEach(key => {
// tip += `<div>${key}${data.value[key]}</div>`;
// });
tip += `<div>负责人:${data.value.nodeHeader || '无'}</div>`;
tip += `<div>规格需求:${data.value.specification || '无'}</div>`;
tip += `<div>任务状态:${data.value.statusText || '无'}</div>`;
tip += `<div>计划完成:${data.value.planEnd || '无'}</div>`;
}
return tip;
}
},
series: [
{
type: 'tree', // 树图核心类型
data: [treeData],
symbol: 'circle', // 节点形状:圆点
symbolSize: 6, // 节点大小
orient: 'LR', // 树图展开方向LR=从左到右(脑图样式),可选 TB(从上到下)
initialTreeDepth: 2, // 默认展开层级2级
roam: true, // 开启鼠标拖拽+滚轮缩放
label: {
show: true,
fontSize: 12,
fontWeight: 500,
position: 'left', // 文字在节点左侧
verticalAlign: 'middle'
},
lineStyle: {
width: 1.2,
curveness: 0.3, // 连接线曲率0=直线0.3=轻微曲线
color: '#ccc'
},
emphasis: {
focus: 'descendant' // 鼠标悬浮时高亮当前节点及子节点
},
expandAndCollapse: true, // 开启节点折叠/展开功能
animationDuration: 300 // 展开折叠动画时长
}
]
};
// 渲染图表
this.chartInstance?.setOption(option, true);
// ========== 核心新增绑定ECharts点击事件只对三级节点生效 ==========
this.clickEvent = (params) => {
console.log(params);
const { data, treeAncestors } = params;
// ✅ 核心判断treeAncestors是当前节点的「所有上级节点数组」
// 根节点(项目名) → 一级节点 → 三级节点 treeAncestors.length = 2 → 精准匹配第三级节点
// 层级对应关系:根节点(0级) → 一级分类(1级) → 业务节点(3级/你要的三级)
if (treeAncestors.length === 4) {
console.log(data);
this.currentNode = { ...data.value }; // 深拷贝当前节点完整数据
this.dialogVisible = true; // 打开弹窗
}
};
// 绑定点击事件
this.chartInstance.on('click', this.clickEvent);
}
}
};
</script>
<style scoped>
.xmind-box {
position: relative;
}
.action-panel {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
.xmind-container {
background: #fafafa;
border-radius: 8px;
}
/* 新增:弹窗内部样式美化 */
:deep(.dialog-content) {
padding: 10px 0;
}
:deep(.node-title) {
font-size: 16px;
font-weight: bold;
color: #1890FF;
text-align: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 15px;
}
:deep(.node-info-item) {
display: flex;
padding: 6px 0;
font-size: 14px;
line-height: 1.6;
}
:deep(.label) {
width: 80px;
color: #666;
font-weight: 500;
}
:deep(.value) {
flex: 1;
color: #333;
}
:deep(.dialog-footer) {
text-align: center;
}
</style>