整合前端
This commit is contained in:
365
ruoyi-ui/src/views/oa/project/pace/components/xmind.vue
Normal file
365
ruoyi-ui/src/views/oa/project/pace/components/xmind.vue
Normal 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>
|
||||
Reference in New Issue
Block a user