Industrial-image-management.../src/views/task/task-gantt/TaskGantt.vue

396 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<GiPageLayout>
<div class="gantt-container">
<div class="page-header">
<h2 class="page-title">人力甘特图页面</h2>
</div>
<!-- 人员列表区域 -->
<div class="person-container">
<div v-for="(person, index) in personList" :key="person.id" class="person-gantt">
<!-- 人员标题栏 -->
<div class="person-header" @click="togglePerson(index)">
<div class="name-container">
<span class="avatar">{{ person.name.charAt(0) }}</span>
<span>{{ person.name }}</span>
<span class="task-count">({{ person.tasks.length }}个项目)</span>
</div>
<button class="expand-button">
<i :class="person.expanded ? 'collapse-icon' : 'expand-icon'"></i>
</button>
</div>
<!-- 甘特图容器 - 根据展开状态控制显示 -->
<div v-show="person.expanded" class="chart-container">
<div :ref="el => chartRefs[index] = el" class="progress-chart"></div>
</div>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang='ts'>
import * as echarts from 'echarts'
import { ref, onMounted, onUnmounted, watch } from 'vue'
// 人员数据
const personList = ref([
{
id: 1,
name: '张三',
expanded: true,
tasks: [
{ name: '项目一', startDate: '2025-08-01', days: 5, color: '#5470C6' },
{ name: '项目二', startDate: '2025-08-10', days: 7, color: '#91CC75' },
{ name: '项目三', startDate: '2025-08-20', days: 4, color: '#FAC858' }
]
},
{
id: 2,
name: '李四',
expanded: true,
tasks: [
{ name: '产品设计', startDate: '2025-08-05', days: 8, color: '#EE6666' },
{ name: '技术评审', startDate: '2025-08-15', days: 3, color: '#73C0DE' },
{ name: '系统测试', startDate: '2025-08-18', days: 6, color: '#3BA272' }
]
},
{
id: 3,
name: '王五',
expanded: true,
tasks: [
{ name: '需求分析', startDate: '2025-08-02', days: 4, color: '#FC8452' },
{ name: '前端开发', startDate: '2025-08-08', days: 7, color: '#9A60B4' },
{ name: '后端对接', startDate: '2025-08-17', days: 5, color: '#EA7CCC' }
]
},
{
id: 4,
name: '赵六',
expanded: false,
tasks: [
{ name: '文档编写', startDate: '2025-08-03', days: 6, color: '#5470C6' },
{ name: '用户培训', startDate: '2025-08-12', days: 4, color: '#91CC75' },
{ name: '上线支持', startDate: '2025-08-22', days: 7, color: '#FAC858' }
]
}
])
// 图表引用数组
const chartRefs = ref<HTMLElement[]>([]);
// 图表实例数组
const chartInstances = ref<echarts.ECharts[]>([]);
// 辅助函数格式化日期为YYYY-MM-DD
const formatDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 切换人员展开/收起状态
const togglePerson = (index: number) => {
personList.value[index].expanded = !personList.value[index].expanded;
// 如果展开,重新渲染图表
if (personList.value[index].expanded) {
setTimeout(() => {
if (chartRefs.value[index] && chartInstances.value[index]) {
chartInstances.value[index].resize();
} else {
initChart(index);
}
}, 10);
}
};
// 初始化特定人员的甘特图
const initChart = (personIndex: number) => {
const container = chartRefs.value[personIndex];
if (!container) return;
// 如果实例已存在,先销毁
if (chartInstances.value[personIndex]) {
chartInstances.value[personIndex].dispose();
}
const person = personList.value[personIndex];
const tasks = person.tasks;
// 计算日期 - 以当前日期为基准
const today = new Date(2025, 7, 14);
// 准备数据
const projectNames: string[] = [];
const dataItems: any[] = [];
const colors: string[] = [];
tasks.forEach((task) => {
const startDate = new Date(task.startDate);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + task.days);
projectNames.push(task.name);
colors.push(task.color);
dataItems.push({
name: task.name,
value: [
formatDate(startDate),
formatDate(endDate),
task.days
],
itemStyle: {
color: task.color
}
});
});
// 初始化图表实例
const chart = echarts.init(container);
// 图表配置
const option = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const task = params.data;
return `
<div class="task-tooltip">
<strong>${task.name}</strong><br>
开始: ${params.value[0]}<br>
结束: ${params.value[1]}<br>
耗时: ${task.value[2]}
</div>
`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'time',
min: formatDate(new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)),
max: formatDate(new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000)),
axisLabel: {
formatter: function (value: number) {
return echarts.time.format(value, '{MM}/{dd}', false);
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
opacity: 0.3
}
}
},
yAxis: {
type: 'category',
data: projectNames,
axisLine: {
show: true
},
axisTick: {
show: false
},
axisLabel: {
margin: 16
}
},
dataZoom: [{
type: 'inside',
start: 20,
end: 100
}],
series: [{
name: '项目进度',
type: 'bar',
data: dataItems,
barCategoryGap: '40%',
label: {
show: true,
position: 'inside',
formatter: '{c[2]}天'
},
barWidth: '60%'
}],
animation: true,
animationDuration: 800
};
chart.setOption(option);
chartInstances.value[personIndex] = chart;
}
// 窗口大小改变时重绘图表
const handleResize = () => {
chartInstances.value.forEach((chart, index) => {
if (chart && personList.value[index].expanded) {
chart.resize();
}
});
};
// 组件挂载时初始化所有展开人员的图表
onMounted(() => {
window.addEventListener('resize', handleResize);
personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
});
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstances.value.forEach(chart => {
if (chart) {
chart.dispose();
}
});
});
</script>
<style lang='scss' scoped>
.gantt-container {
height: 100%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.page-header {
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
.page-title {
margin: 0;
font-size: 1.5rem;
color: #2c3e50;
font-weight: 600;
}
}
.person-container {
flex: 1;
overflow-y: auto;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #c2c6cc;
border-radius: 3px;
}
}
.person-gantt {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
background-color: white;
&:last-child {
margin-bottom: 0;
}
}
.person-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f1f3f5;
}
.name-container {
display: flex;
align-items: center;
font-size: 1.1rem;
font-weight: 500;
color: #495057;
.avatar {
display: inline-flex;
justify-content: center;
align-items: center;
width: 28px;
height: 28px;
margin-right: 10px;
background-color: #3a7afe;
color: white;
border-radius: 50%;
font-weight: bold;
}
.task-count {
margin-left: 8px;
font-size: 0.85rem;
font-weight: normal;
color: #868e96;
}
}
.expand-button {
background: none;
border: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
&:hover {
background-color: #e9ecef;
}
i {
display: block;
width: 0;
height: 0;
border-style: solid;
}
.collapse-icon {
border-width: 0 8px 10px 8px;
border-color: transparent transparent #495057 transparent;
}
.expand-icon {
border-width: 10px 8px 0 8px;
border-color: #495057 transparent transparent transparent;
}
}
}
.chart-container {
padding: 15px;
background-color: #fff;
}
.progress-chart {
width: 100%;
height: 250px;
}
</style>