<template>
<div class="page-container">
<el-row :gutter="20">
<!-- 查询区域 -->
<div class="search-wrapper">
<el-form :inline="true" label-width="100px" @submit.prevent="getList">
<el-form-item label="名称">
<el-input v-model="queryParams.name" placeholder="请输入名称" clearable/>
</el-form-item>
<el-form-item label="责任人">
<el-input v-model="queryParams.respPerson" placeholder="请输入责任人" clearable/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button
type="primary"
@click="toggleGantt"
style="margin-left: 10px;"
>
{{ showGantt ? '收起甘特图' : '展开甘特图' }}
</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-container">
<el-table
ref="table"
:data="listData"
row-key="uid"
border
:row-style="{ height: '30px' }"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@row-click="handleRowClick"
@expand-change="handleExpandChange"
highlight-current-row
>
<el-table-column prop="code" label="编号" width="120"/>
<el-table-column prop="name" label="名称" min-width="180"/>
<el-table-column prop="respPerson" label="责任人" width="120"/>
<el-table-column prop="schedule" label="完成百分比" width="120">
<template slot-scope="{row}">
<el-progress
:percentage="Number(row.schedule)"
:show-text="row.schedule > 10"
:stroke-width="18"
:color="getProgressColor(row.schedule)"
/>
</template>
</el-table-column>
<el-table-column prop="planStartDate" label="计划开始日期" width="150"/>
<el-table-column prop="planEndDate" label="计划结束日期" width="150"/>
<!-- 新增甘特图列 -->
<el-table-column label="时间线" min-width="300">
<template slot-scope="{row}">
<div class="gantt-cell" :ref="'gantt_'+row.uid" style="height: 50px;"></div>
</template>
</el-table-column>
<!-- <el-table-column label="操作" width="100">-->
<!-- <template slot-scope="scope">-->
<!-- <el-button size="mini" icon="el-icon-view" @click.stop="handleUpdate(scope.row)">查看</el-button>-->
<!-- </template>-->
<!-- </el-table-column>-->
</el-table>
</div>
<!-- <!– 右侧甘特图容器 –>-->
<!-- <el-col v-if="showGantt" :span="12">-->
<!-- <div ref="ganttContainer" class="gantt-container" style="width: 100%; height: 600px;"></div>-->
<!-- </el-col>-->
</el-row>
<!-- 查看弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="850px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" :disabled="disable">
<el-row>
<el-col :span="12">
<el-form-item label="编号" prop="code">
<el-input v-model="form.code" placeholder="请输入编号"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" rows="3"/>
</el-form-item>
</el-col>
</el-row>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
</div>
</el-form>
</el-dialog>
</div>
</template>
<script>
import gantt from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import {getPlan, listPlan} from '@/api/dw/plan/planview';
export default {
name: 'Planview',
data() {
return {
expandedKeys: new Set(), // 存储所有展开节点的UID
listData: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 1000, // 树形结构不适合分页,增加单页大小
name: null,
respPerson: null
},
open: false,
title: '',
form: {},
rules: {
name: [{required: true, message: '名称不能为空', trigger: 'blur'}],
schedule: [
{required: true, message: '完成百分比不能为空', trigger: 'blur'},
{type: 'number', message: '输入内容不是有效的数字', trigger: 'blur'}
]
},
disable: true,
showGantt: true, // 控制甘特图显示
flatData: [], // 扁平化数据
ganttInitialized: false, // 甘特图初始化标志
currentSelectedTask: null, // 当前选中的任务ID
ganttExpandState: new Map() // 存储甘特图的展开状态
};
},
mounted() {
this.getList();
},
methods: {
// 初始化单个任务的甘特图
initTaskGantt(row) {
const container = this.$refs[`gantt_${row.uid}`]?.[0];
if (!container || container._ganttInitialized) return;
// 创建独立的甘特图实例
const taskGantt = gantt.createGanttInstance();
taskGantt.config.show_chart = false;
taskGantt.config.show_grid = false;
taskGantt.config.scale_height = 0;
taskGantt.config.readonly = true;
// 配置时间轴
taskGantt.config.scales = [
{unit: "day", step: 1, format: "%j %D"}
];
// 设置时间范围
const start = new Date(row.planStartDate);
const end = new Date(row.planEndDate);
taskGantt.setWorkTime({start_date: start, end_date: end});
// 添加任务
taskGantt.parse({
data: [{
id: row.uid,
text: row.name,
start_date: row.planStartDate,
end_date: row.planEndDate,
progress: row.schedule / 100,
duration: this.calculateDuration(row.planStartDate, row.planEndDate)
}]
});
// 初始化甘特图
taskGantt.init(container);
container._ganttInstance = taskGantt;
container._ganttInitialized = true;
},
// 计算任务持续时间(天)
calculateDuration(start, end) {
const startDate = new Date(start);
const endDate = new Date(end);
return Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
},
// 获取进度条颜色
getProgressColor(percentage) {
// if (percentage < 30) return '#F56C6C';
// if (percentage < 70) return '#E6A23C';
return '#67C23A';
},
// 初始化甘特图
initGantt() {
if (!this.$refs.ganttContainer) return;
try {
// 清除之前的实例(如果存在)
if (gantt.$container) {
gantt.destructor();
}
gantt.config.date_format = '%Y-%m-%d';
gantt.config.scale_unit = 'month';
gantt.config.step = 1;
gantt.config.subscales = [
{unit: 'day', step: 1, date: '%j, %D'}
];
gantt.config.columns = [
{name: 'text', label: '任务名称', tree: true, width: 200},
{name: 'start_date', label: '开始时间', align: 'center', width: 100},
{name: 'end_date', label: '结束时间', align: 'center', width: 100},
{name: 'progress', label: '进度', align: 'center', width: 80, template: (task) => `${task.progress * 100}%`}
];
gantt.config.row_height = 30;
gantt.config.grid_width = 500;
gantt.templates.task_text = (start, end, task) => task.text;
gantt.init(this.$refs.ganttContainer);
// 绑定事件
gantt.attachEvent('onTaskSelected', (id) => {
this.currentSelectedTask = id;
this.scrollToTableRow(id);
});
// 绑定展开/折叠事件
gantt.attachEvent('onAfterTaskOpen', (id) => {
this.ganttExpandState.set(id, true);
this.syncGanttExpandToTable(id, true);
});
gantt.attachEvent('onAfterTaskClose', (id) => {
this.ganttExpandState.set(id, false);
this.syncGanttExpandToTable(id, false);
});
this.ganttInitialized = true;
console.log('甘特图初始化成功');
} catch (e) {
console.error('甘特图初始化失败:', e);
}
},
// 将甘特图的展开状态同步到表格
syncGanttExpandToTable(taskId, expanded) {
const row = this.flatData.find(item => item.uid === taskId);
if (!row) return;
// 更新展开状态
if (expanded) {
this.expandedKeys.add(row.uid);
} else {
this.expandedKeys.delete(row.uid);
}
// 更新表格UI
this.$nextTick(() => {
const tableRow = this.$refs.table.$el.querySelector(`[data-id="${row.uid}"]`);
if (tableRow) {
const expandIcon = tableRow.querySelector('.el-table__expand-icon');
if (expandIcon) {
const isExpanded = expandIcon.classList.contains('el-table__expand-icon--expanded');
if (isExpanded !== expanded) {
this.$refs.table.toggleRowExpansion(row, expanded);
}
}
}
});
},
// 获取数据
async getList() {
try {
const res = await listPlan(this.queryParams);
this.listData = this.handleTree(res.data, 'uid', 'parentUid');
this.$nextTick(() => {
// 初始化所有可见行的甘特图
this.initVisibleGantts();
});
} catch (error) {
console.error('获取数据失败:', error);
}
},
// 初始化所有可见行的甘特图
initVisibleGantts() {
this.listData.forEach(item => {
this.initTaskGantt(item);
if (item.children) {
item.children.forEach(child => this.initTaskGantt(child));
}
});
},
// 更新甘特图数据
updateGantt() {
if (!this.ganttInitialized) return;
const tasks = this.getVisibleTasks();
console.log('更新甘特图任务数量:', tasks.length);
try {
// 保存当前甘特图的展开状态
this.saveGanttExpandState();
gantt.clearAll();
gantt.parse({data: tasks, links: []});
// 恢复甘特图的展开状态
this.restoreGanttExpandState();
this.adjustGanttView(tasks);
} catch (e) {
console.error('更新甘特图失败:', e);
}
},
// 保存甘特图的展开状态
saveGanttExpandState() {
if (!this.flatData.length) return;
// 遍历所有任务,保存展开状态
this.flatData.forEach(item => {
if (gantt.isTaskExists(item.uid)) {
this.ganttExpandState.set(item.uid, gantt.isTaskOpen(item.uid));
}
});
},
// 恢复甘特图的展开状态
restoreGanttExpandState() {
this.ganttExpandState.forEach((isOpen, taskId) => {
if (gantt.isTaskExists(taskId)) {
gantt.openTask(taskId, isOpen);
}
});
},
// 获取当前可见的任务(根据展开状态)
getVisibleTasks() {
const visibleTasks = [];
const collectVisible = (nodes) => {
nodes.forEach(node => {
visibleTasks.push({
id: node.uid,
text: node.name,
start_date: node.planStartDate,
duration: node.planDuration || 1,
progress: (node.schedule || 0) / 100,
parent: node.parentUid || 0,
open: this.expandedKeys.has(node.uid) // 设置初始展开状态
});
// 如果节点是展开的,递归收集子节点
if (this.expandedKeys.has(node.uid) && node.children) {
collectVisible(node.children);
}
});
};
collectVisible(this.listData);
return visibleTasks;
},
// 自动调整甘特图视图
adjustGanttView(tasks) {
if (!tasks.length) return;
// 计算时间范围
const dates = tasks
.filter(t => t.start_date)
.map(t => new Date(t.start_date));
if (!dates.length) return;
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
const maxDate = new Date(Math.max(...dates.map(t => {
const endDate = new Date(t.start_date);
endDate.setDate(endDate.getDate() + (t.duration || 0));
return endDate.getTime();
})));
// 设置时间范围
gantt.setWorkTime({
start_date: minDate,
end_date: maxDate
});
// 根据时间跨度调整缩放级别
const timeDiffInDays = Math.ceil((maxDate - minDate) / (1000 * 60 * 60 * 24));
if (timeDiffInDays <= 7) {
gantt.config.scale_unit = 'day';
gantt.config.step = 1;
} else if (timeDiffInDays <= 31) {
gantt.config.scale_unit = 'week';
gantt.config.step = 1;
} else if (timeDiffInDays <= 365) {
gantt.config.scale_unit = 'month';
gantt.config.step = 1;
} else {
gantt.config.scale_unit = 'year';
gantt.config.step = 1;
}
gantt.render();
},
// 处理树形结构
handleTree(data, idKey = 'uid', parentKey = 'parentUid') {
const map = {};
const tree = [];
// 创建映射
data.forEach(item => {
map[item[idKey]] = {...item, children: []};
});
// 构建树
data.forEach(item => {
const parentId = item[parentKey];
if (parentId && map[parentId]) {
map[parentId].children.push(map[item[idKey]]);
} else {
tree.push(map[item[idKey]]);
}
});
return tree;
},
// 行点击事件
handleRowClick(row) {
this.$nextTick(() => {
// 高亮当前行
this.$refs.table.setCurrentRow(row);
// 在甘特图中选中对应任务
if (this.ganttInitialized) {
gantt.selectTask(row.uid);
gantt.showTask(row.uid);
}
});
},
// 滚动到表格行
scrollToTableRow(taskId) {
const row = this.flatData.find(item => item.uid === taskId);
if (!row) return;
this.$nextTick(() => {
// 确保所有父节点都展开
this.expandParents(row);
// 高亮当前行
this.$refs.table.setCurrentRow(row);
// 滚动到元素
const tableBody = this.$refs.table.$el.querySelector('.el-table__body-wrapper');
const rowEl = this.$refs.table.$el.querySelector(`[data-id="${row.uid}"]`);
if (tableBody && rowEl) {
const rowTop = rowEl.offsetTop;
const tableHeight = tableBody.clientHeight;
tableBody.scrollTop = rowTop - tableHeight / 2;
}
});
},
// 展开父节点
expandParents(row) {
if (!row.parentUid) return;
const parent = this.flatData.find(item => item.uid === row.parentUid);
if (parent && !this.expandedKeys.has(parent.uid)) {
this.expandedKeys.add(parent.uid);
this.$refs.table.toggleRowExpansion(parent, true);
this.expandParents(parent);
}
},
// 树展开/折叠更新甘特图
handleExpandChange(row, expanded) {
if (expanded && row.children) {
this.$nextTick(() => {
row.children.forEach(child => this.initTaskGantt(child));
});
}
},
// 递归折叠子节点
collapseChildren(node) {
if (node.children && node.children.length) {
node.children.forEach(child => {
this.expandedKeys.delete(child.uid);
this.$refs.table.toggleRowExpansion(child, false);
this.collapseChildren(child);
});
}
},
// 切换甘特图显示 - 解决重新初始化问题
toggleGantt() {
this.showGantt = !this.showGantt;
if (this.showGantt) {
this.$nextTick(() => {
// 确保每次展开都重新初始化甘特图
this.ganttInitialized = false;
this.initGantt();
this.updateGantt();
});
}
},
// 获取数据详情
async handleUpdate(row) {
try {
const res = await getPlan(row.uid);
this.form = res.data;
this.open = true;
this.title = '查看治理计划';
} catch (error) {
console.error('获取详情失败:', error);
}
},
// 取消按钮
cancel() {
this.open = false;
},
// 重置查询
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 1000,
name: null,
respPerson: null
};
this.getList();
}
}
};
</script>
<style scoped>
.page-container {
padding: 20px;
background-color: #f5f7fa;
}
.search-wrapper {
background-color: #fff;
padding: 15px 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.table-container {
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.gantt-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 5px;
}
.dialog-footer {
text-align: right;
margin-top: 20px;
}
.toggle-button {
margin-bottom: 15px;
}
.el-table {
width: 100%;
}
.el-table--border {
border: 1px solid #ebeef5;
}
.el-table__row:hover {
background-color: #f5f7fa !important;
}
.el-progress {
margin-top: 8px;
}
.el-form-item {
margin-bottom: 18px;
}
</style>
优化后的代码,甘特图时间线列没有数据是怎么回事
最新发布