<template>
<div class="page-container">
<el-row :gutter="20">
<!-- 查询区域 -->
<el-col :span="24">
<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="请输入名称" />
</el-form-item>
<el-form-item label="责任人">
<el-input v-model="queryParams.respPerson" placeholder="请输入责任人" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
<!-- 列表与甘特图切换按钮 -->
<el-col :span="24">
<div class="toggle-button">
<el-button
type="primary"
@click="toggleGantt"
style="margin-bottom: 15px;"
>
{{ showGantt ? '收起甘特图' : '展开甘特图' }}
</el-button>
</div>
</el-col>
<!-- 左侧列表 -->
<el-col :span="showGantt ? 12 : 24">
<div class="table-container">
<el-table
ref="table"
:data="listData"
row-key="uid"
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@row-click="rowClick"
@expand-change="handleExpandChange"
>
<el-table-column prop="code" label="编号" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="respPerson" label="责任人" />
<el-table-column prop="schedule" label="完成百分比" />
<el-table-column prop="planStartDate" label="计划开始日期" />
<el-table-column prop="planEndDate" label="计划结束日期" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" icon="el-icon-view" @click="handleUpdate(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
layout="prev, pager, next"
:total="total"
:page-size="queryParams.pageSize"
@current-change="handleCurrentChange"
/>
</div>
</el-col>
<!-- 右侧甘特图容器 -->
<el-col :span="12" v-if="showGantt">
<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="请输入备注" />
</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 { listPlan, getPlan } from '@/api/dw/plan/planview';
export default {
name: 'Planview',
data() {
return {
listData: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
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: [], // 扁平化数据
baseDate: new Date('2023-01-01'), // 基准日期
maxDuration: 365, // 最大工期(天)
maxOffset: 365 // 最大偏移天数
};
},
mounted() {
this.getList();
this.initGantt();
},
methods: {
async getList() {
const res = await listPlan(this.queryParams);
this.listData = this.handleTree(res.data, 'uid', 'parentUid');
this.total = res.total;
this.flatData = this.flattenTree(this.listData);
this.getMaxDuration();
this.$nextTick(() => {
const tasks = this.ganttData(this.flatData);
this.updateGantt(tasks);
});
},
// 获取最大工期
getMaxDuration() {
const durations = this.flatData.map(item => item.planDuration || 0);
this.maxDuration = Math.max(...durations, 1);
},
// 计算甘特图宽度
calculateGanttWidth(row) {
const duration = row.planDuration || 0;
const width = (duration / this.maxDuration) * 100;
return `${Math.max(5, width)}%`;
},
// 计算甘特图偏移
calculateGanttOffset(row) {
if (!row.planStartDate) return '0%';
const startDate = new Date(row.planStartDate);
const daysOffset = Math.floor((startDate - this.baseDate) / (1000 * 60 * 60 * 24));
return `${(daysOffset / this.maxOffset) * 100}%`;
},
// 初始化甘特图
initGantt() {
if (!this.$refs.ganttContainer) return;
gantt.config.date_format = '%Y-%m-%d';
gantt.config.columns = [
{ name: 'text', label: '任务名称', tree: true, width: '*' },
{ name: 'start_date', label: '开始时间', align: 'center' },
{ name: 'duration', label: '工期(天)', align: 'center' }
];
gantt.templates.task_text = (start, end, task) => task.text;
gantt.init(this.$refs.ganttContainer);
gantt.parse({ data: [], links: [] });
// 确保事件监听器只绑定一次
if (!this.ganttEventInitialized) {
gantt.attachEvent('onTaskSelected', id => {
const row = this.flatData.find(item => item.uid === id);
if (row) {
this.$refs.table.setCurrentRow(row);
}
});
this.ganttEventInitialized = true;
}
},
// 更新甘特图
updateGantt(tasks) {
gantt.clearAll();
gantt.parse({ data: tasks, links: [] });
},
// 树形结构转扁平结构
flattenTree(data) {
const result = [];
const stack = [...data];
while (stack.length) {
const node = stack.pop();
result.push(node);
if (node.children) {
stack.push(...node.children);
}
}
return result;
},
// 转换为甘特图数据
ganttData(data) {
return data
.filter(item => item.uid && item.planStartDate)
.map(item => ({
id: item.uid,
text: item.name,
start_date: item.planStartDate,
duration: item.planDuration || 0,
progress: (item.schedule || 0) / 100,
parent: item.parentUid || 0
}));
},
// 处理树形结构
handleTree(data, idKey = 'id', parentKey = 'parentId') {
const map = {};
const tree = [];
data.forEach(item => (map[item[idKey]] = item));
data.forEach(item => {
const parent = map[item[parentKey]];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
tree.push(item);
}
});
return tree;
},
// 行点击事件
rowClick(row) {
const taskId = row.uid;
this.$nextTick(() => {
if (gantt.$initialized) {
gantt.showTask(taskId);
gantt.selectTask(taskId);
// 强制重绘确保高亮生效
gantt.render();
}
});
},
// 树展开/折叠更新甘特图
handleExpandChange(row, expanded) {
if (expanded) {
const allChildren = this.getAllChildren(row);
const tasks = this.ganttData(allChildren);
this.$nextTick(() => {
this.updateGantt(tasks);
if (gantt.$initialized && tasks.length > 0) {
// 获取展开节点的最早和最晚日期
const dates = tasks
.filter(t => t.start_date)
.map(t => new Date(t.start_date));
if (dates.length > 0) {
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
const maxDate = new Date(Math.max(...dates.map(d => {
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
});
// 调整视图缩放级别
gantt.config.scale_unit = 'day';
gantt.config.step = 1;
gantt.config.scale_height = 28;
// 重新渲染并定位第一个任务
gantt.render();
gantt.showTask(tasks[0].id);
gantt.selectTask(tasks[0].id);
}
}
});
} else {
const topLevelTasks = this.listData.map(item => ({
id: item.uid,
text: item.name,
start_date: item.planStartDate,
duration: item.planDuration || 0,
progress: (item.schedule || 0) / 100,
parent: item.parentUid || 0
}));
this.$nextTick(() => {
this.updateGantt(topLevelTasks);
if (gantt.$initialized) {
// 恢复默认时间范围
gantt.setWorkTime({
start_date: new Date('2023-01-01'),
end_date: new Date('2023-12-31')
});
gantt.config.scale_unit = 'month';
gantt.config.step = 1;
gantt.config.scale_height = 28;
gantt.render();
}
});
}
},
// 递归获取所有子节点
getAllChildren(node) {
let children = [node];
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
children = children.concat(this.getAllChildren(child));
});
}
return children;
},
// 切换甘特图
toggleGantt() {
this.showGantt = !this.showGantt;
if (this.showGantt) {
this.$nextTick(() => {
const tasks = this.ganttData(this.flatData);
this.updateGantt(tasks);
});
}
},
// 获取数据
async handleUpdate(row) {
const res = await getPlan(row.uid);
this.form = res.data;
this.open = true;
this.title = '查看治理计划';
},
// 取消按钮
cancel() {
this.open = false;
},
// 重置查询
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
name: null,
respPerson: null
};
this.getList();
},
// 分页切换
handleCurrentChange(page) {
this.queryParams.pageNum = page;
this.getList();
}
}
};
</script>
<style scoped>
.page-container {
padding: 20px;
}
.table-container {
background-color: #fff;
padding: 10px;
border-radius: 4px;
}
.gantt-container {
background-color: #f9f9f9;
border: 1px solid #ebeef5;
padding: 10px;
border-radius: 4px;
}
.dialog-footer {
text-align: right;
}
.search-wrapper {
margin-bottom: 20px;
background-color: #fff;
padding: 10px;
border-radius: 4px;
}
.toggle-button {
margin-bottom: 15px;
}
.gantt-bar-container {
position: relative;
height: 30px;
background-color: #f5f7fa;
border-radius: 4px;
overflow: hidden;
margin: 5px 0;
}
.gantt-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #409EFF;
color: white;
text-align: center;
font-size: 12px;
line-height: 30px;
}
</style>
列表的展开和收缩,甘特图没有同步,请重新优化