feat(项目看板): 新增项目综合看板功能
新增项目综合看板功能,聚合展示项目、任务、进度主表和步骤数据
- 新增后端聚合接口 GET /oa/project/dashboard/{projectId}
- 新增前端看板页面,包含项目列表、任务表格和进度导图
- 优化思维导图组件,支持看板模式下的紧凑展示
- 新增进度明细表格视图和状态图例
- 实现任务与进度步骤的关联展示
- 添加项目模糊搜索功能
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
<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>
|
||||
<div class="xmind-box" :class="{ 'xmind-box--dashboard': dashboardMode }">
|
||||
<div class="action-panel">
|
||||
<el-button type="primary" :size="dashboardMode ? 'mini' : 'small'" 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>
|
||||
<div class="xmind-container" ref="chart" :style="containerStyle"></div>
|
||||
<!-- 新增:三级节点点击弹窗-查看完整信息 -->
|
||||
<el-dialog title="节点详情信息" :visible.sync="dialogVisible" width="1200px" center append-to-body>
|
||||
<el-form>
|
||||
@@ -90,6 +89,25 @@ export default {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 容器高度,综合看板等场景可传 100% 以撑满父级 */
|
||||
height: {
|
||||
type: String,
|
||||
default: '800px'
|
||||
},
|
||||
/** 综合看板:紧凑、防重叠、三色状态、小圆点 */
|
||||
dashboardMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerStyle () {
|
||||
return {
|
||||
width: '100%',
|
||||
height: this.height,
|
||||
minHeight: this.dashboardMode ? '300px' : '240px'
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@@ -115,7 +133,7 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.initChart();
|
||||
// 监听窗口大小变化,自适应重绘
|
||||
this.$nextTick(() => this.resizeChart());
|
||||
window.addEventListener('resize', this.resizeChart);
|
||||
},
|
||||
beforeDestroy () {
|
||||
@@ -175,53 +193,97 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 核心方法:把扁平数组 转为 ECharts树图需要的嵌套树形结构
|
||||
/** 看板长标签按字折行(与折线图区域可读性一致) */
|
||||
wrapLabelText (text, maxCharsPerLine) {
|
||||
if (!text) return ''
|
||||
const max = Math.max(4, maxCharsPerLine || 16)
|
||||
if (text.length <= max) return text
|
||||
const lines = []
|
||||
for (let i = 0; i < text.length; i += max) {
|
||||
lines.push(text.slice(i, i + max))
|
||||
}
|
||||
return lines.join('\n')
|
||||
},
|
||||
|
||||
// Tab节点 → 一级节点 → 二级节点(叶子带业务数据)
|
||||
transformToTreeData (list) {
|
||||
if (!list.length) return { name: '暂无项目数据', children: [] };
|
||||
|
||||
// 1. 获取项目名称(所有数据是同一个项目,取第一条即可)
|
||||
const dm = this.dashboardMode
|
||||
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 tabMap = new Map();
|
||||
|
||||
list.forEach(item => {
|
||||
const tab = item.tabNode || '默认分组';
|
||||
const firstLevel = item.firstLevelNode || '未分类一级节点';
|
||||
const secondLevel = item.secondLevelNode || item.stepName || '未命名节点';
|
||||
const st = Number(item.status);
|
||||
let statusText = '进行中';
|
||||
let statusColor = '#E6A23C';
|
||||
let lineToNode = { color: '#dcdfe6', width: 1.2 };
|
||||
if (st === 2) {
|
||||
statusText = '已完成';
|
||||
statusColor = '#67C23A';
|
||||
lineToNode = { color: '#67C23A', width: 1.8 };
|
||||
} else if (st === 0) {
|
||||
statusText = '未开始';
|
||||
statusColor = '#909399';
|
||||
} else if (st === 1) {
|
||||
statusText = '待验收/进行中';
|
||||
statusColor = '#E6A23C';
|
||||
} else if (st === 3) {
|
||||
statusText = '暂停';
|
||||
statusColor = '#909399';
|
||||
}
|
||||
|
||||
// 组装节点数据:显示名称+业务信息+样式
|
||||
const nodeData = {
|
||||
name: secondLevel,
|
||||
itemStyle: { color: statusColor },
|
||||
/* 看板:小圆点 + 白边,贴近折线图主色风格 */
|
||||
itemStyle: dm
|
||||
? { color: statusColor, borderColor: '#ffffff', borderWidth: 1.25, shadowBlur: 3, shadowColor: 'rgba(0,0,0,0.12)' }
|
||||
: { color: statusColor, borderColor: statusColor },
|
||||
label: { color: statusColor },
|
||||
// 自定义业务数据,鼠标悬浮时显示
|
||||
lineStyle: lineToNode,
|
||||
value: {
|
||||
...item,
|
||||
负责人: item.nodeHeader || '无',
|
||||
状态: statusText,
|
||||
计划完成: item.planEnd || '无',
|
||||
说明: item.specification || '无'
|
||||
statusLabel: statusText
|
||||
}
|
||||
};
|
||||
|
||||
// 归集一级节点和二级节点
|
||||
if (!levelMap.has(firstLevel)) {
|
||||
levelMap.set(firstLevel, []);
|
||||
if (!tabMap.has(tab)) {
|
||||
tabMap.set(tab, new Map());
|
||||
}
|
||||
levelMap.get(firstLevel).push(nodeData);
|
||||
const firstMap = tabMap.get(tab);
|
||||
if (!firstMap.has(firstLevel)) {
|
||||
firstMap.set(firstLevel, []);
|
||||
}
|
||||
firstMap.get(firstLevel).push(nodeData);
|
||||
});
|
||||
|
||||
// 3. 组装最终的树形结构
|
||||
const treeChildren = Array.from(levelMap).map(([firstName, children]) => ({
|
||||
name: firstName,
|
||||
itemStyle: { color: '#303133' }, // 一级节点统一深灰色
|
||||
children: children
|
||||
const treeChildren = Array.from(tabMap).map(([tabName, firstMap]) => ({
|
||||
name: tabName,
|
||||
itemStyle: dm
|
||||
? { color: '#606266', borderColor: '#ffffff', borderWidth: 1, shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.06)' }
|
||||
: { color: '#606266', borderColor: '#dcdfe6' },
|
||||
lineStyle: { color: '#c0c4cc', width: 1.2 },
|
||||
children: Array.from(firstMap).map(([firstName, children]) => ({
|
||||
name: firstName,
|
||||
itemStyle: dm
|
||||
? { color: '#303133', borderColor: '#ffffff', borderWidth: 1, shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.06)' }
|
||||
: { color: '#303133', borderColor: '#dcdfe6' },
|
||||
lineStyle: { color: '#c0c4cc', width: 1.2 },
|
||||
children
|
||||
}))
|
||||
}));
|
||||
|
||||
return {
|
||||
name: projectName,
|
||||
itemStyle: { color: '#1890FF' }, // 根节点(项目名)蓝色高亮
|
||||
symbolSize: dm ? 7 : undefined,
|
||||
itemStyle: dm
|
||||
? { color: '#409eff', borderColor: '#ffffff', borderWidth: 1.5, shadowBlur: 4, shadowColor: 'rgba(64,158,255,0.35)' }
|
||||
: { color: '#409eff', borderColor: '#409eff' },
|
||||
lineStyle: { color: '#a0cfff', width: 1.5 },
|
||||
label: dm ? { distance: 8, fontSize: 11 } : undefined,
|
||||
children: treeChildren
|
||||
};
|
||||
},
|
||||
@@ -238,68 +300,136 @@ export default {
|
||||
}
|
||||
// 转换数据格式
|
||||
const treeData = this.transformToTreeData(this.list);
|
||||
// 设置图表配置项
|
||||
const dm = this.dashboardMode;
|
||||
const escapeHtml = (s) => String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
const option = {
|
||||
backgroundColor: dm ? 'transparent' : undefined,
|
||||
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>`;
|
||||
enterable: true,
|
||||
confine: true,
|
||||
extraCssText: 'max-width:420px;white-space:normal;word-break:break-word;border-radius:6px;box-shadow:0 2px 12px rgba(0,0,0,0.08);',
|
||||
formatter: (params) => {
|
||||
const data = params.data
|
||||
const title = (data && data.name) != null ? data.name : (params.name || '')
|
||||
let tip = `<div style="font-size:13px;font-weight:600;margin-bottom:6px;line-height:1.45;color:#303133">${escapeHtml(title)}</div>`
|
||||
if (data && data.value) {
|
||||
const v = data.value
|
||||
tip += `<div style="font-size:12px;line-height:1.55;color:#606266">负责人:${v.nodeHeader != null && v.nodeHeader !== '' ? escapeHtml(v.nodeHeader) : '无'}</div>`
|
||||
tip += `<div style="font-size:12px;line-height:1.55;color:#606266">规格需求:${v.specification != null && v.specification !== '' ? escapeHtml(v.specification) : '无'}</div>`
|
||||
tip += `<div style="font-size:12px;line-height:1.55;color:#606266">状态:${v.statusLabel != null && v.statusLabel !== '' ? escapeHtml(v.statusLabel) : '无'}</div>`
|
||||
tip += `<div style="font-size:12px;line-height:1.55;color:#606266">计划完成:${v.planEnd != null && v.planEnd !== '' ? escapeHtml(v.planEnd) : '无'}</div>`
|
||||
}
|
||||
return tip;
|
||||
return tip
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'tree', // 树图核心类型
|
||||
type: 'tree',
|
||||
data: [treeData],
|
||||
symbol: 'circle', // 节点形状:圆点
|
||||
symbolSize: 6, // 节点大小
|
||||
orient: 'LR', // 树图展开方向:LR=从左到右(脑图样式),可选 TB(从上到下)
|
||||
initialTreeDepth: 2, // 默认展开层级:2级
|
||||
roam: true, // 开启鼠标拖拽+滚轮缩放
|
||||
/* 与折线图区域一致:留白、白底在容器上 */
|
||||
...(dm ? { left: '1%', right: '5%', top: '2%', bottom: '2%' } : {}),
|
||||
symbol: 'circle',
|
||||
...(dm ? {} : { symbolSize: 6 }),
|
||||
edgeShape: dm ? 'polyline' : 'curve',
|
||||
edgeForkPosition: dm ? '74%' : '50%',
|
||||
orient: 'LR',
|
||||
initialTreeDepth: 4,
|
||||
roam: true,
|
||||
scaleLimit: dm ? { min: 0.22, max: 5 } : undefined,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
position: 'left', // 文字在节点左侧
|
||||
verticalAlign: 'middle'
|
||||
fontSize: dm ? 11 : 12,
|
||||
fontWeight: 400,
|
||||
position: 'left',
|
||||
verticalAlign: 'middle',
|
||||
...(dm ? { align: 'right' } : {}),
|
||||
distance: 8,
|
||||
overflow: 'none',
|
||||
lineHeight: dm ? 15 : 14,
|
||||
color: dm ? '#606266' : undefined
|
||||
},
|
||||
/*
|
||||
* levels[i]:根下一层起 Tab / 分类 / 叶;缩小状态圆点,叶节点标签放右侧防挤压
|
||||
*/
|
||||
levels: dm
|
||||
? [
|
||||
{
|
||||
symbolSize: 6,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'left',
|
||||
distance: 8,
|
||||
fontSize: 11,
|
||||
width: 118,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
padding: [2, 6, 2, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 5,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'left',
|
||||
distance: 10,
|
||||
fontSize: 11,
|
||||
width: 160,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
padding: [2, 8, 2, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 4,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff', shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.1)' },
|
||||
label: {
|
||||
position: 'right',
|
||||
verticalAlign: 'middle',
|
||||
align: 'left',
|
||||
distance: 12,
|
||||
fontSize: 11,
|
||||
width: 232,
|
||||
overflow: 'break',
|
||||
lineHeight: 15,
|
||||
padding: [2, 8, 2, 8],
|
||||
formatter: (p) => this.wrapLabelText(p.name, 17)
|
||||
}
|
||||
}
|
||||
]
|
||||
: [
|
||||
{ symbolSize: 10 },
|
||||
{ symbolSize: 7 },
|
||||
{ symbolSize: 5 },
|
||||
{ symbolSize: 3 }
|
||||
],
|
||||
lineStyle: {
|
||||
width: 1.2,
|
||||
curveness: 0.3, // 连接线曲率,0=直线,0.3=轻微曲线
|
||||
color: '#ccc'
|
||||
width: dm ? 1 : 1.2,
|
||||
curveness: dm ? 0.1 : 0.3,
|
||||
color: dm ? '#e4e7ed' : '#ccc'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'descendant' // 鼠标悬浮时高亮当前节点及子节点
|
||||
focus: 'descendant',
|
||||
lineStyle: { width: 2, color: '#409eff' },
|
||||
itemStyle: dm ? { shadowBlur: 6, shadowColor: 'rgba(64,158,255,0.45)' } : undefined
|
||||
},
|
||||
expandAndCollapse: true, // 开启节点折叠/展开功能
|
||||
animationDuration: 300 // 展开折叠动画时长
|
||||
expandAndCollapse: true,
|
||||
animationDuration: 280
|
||||
}
|
||||
]
|
||||
};
|
||||
// 渲染图表
|
||||
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; // 打开弹窗
|
||||
const data = params.data;
|
||||
if (data && data.value && data.value.trackId) {
|
||||
this.currentNode = { ...data.value };
|
||||
this.dialogVisible = true;
|
||||
}
|
||||
};
|
||||
// 绑定点击事件
|
||||
@@ -326,6 +456,23 @@ export default {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.xmind-box--dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 与综合看板折线图区域一致:白底、细边框、轻圆角 */
|
||||
.xmind-box--dashboard .xmind-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 新增:弹窗内部样式美化 */
|
||||
:deep(.dialog-content) {
|
||||
padding: 10px 0;
|
||||
|
||||
Reference in New Issue
Block a user