Compare commits

..

No commits in common. "66d4a1bbfe7b4204d0dc9c792dd58dfab209d4e9" and "4ea535b82f0f0db332eed463869a6d9293e7c112" have entirely different histories.

1 changed files with 275 additions and 238 deletions

View File

@ -3,49 +3,26 @@
<div class="gantt-container">
<div class="page-header">
<h2 class="page-title">人力甘特图页面</h2>
<!-- 时间刻度切换 -->
<div class="scale-switch">
<button
v-for="scale in timeScales"
:key="scale.value"
:class="['scale-btn', { active: scale.value === currentScale }]"
@click="setScale(scale.value)"
>
{{ scale.label }}
</button>
<!-- 左右翻页按钮 -->
<button class="nav-btn" @click="shiftRange(-1)"></button>
<button class="nav-btn" @click="shiftRange(1)"></button>
</div>
</div>
<!-- 人员列表 -->
<!-- 人员列表区域 -->
<div class="person-container">
<div
v-for="(person, index) in personList"
:key="person.id"
class="person-gantt"
>
<!-- 头部 -->
<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>
<span class="task-count">({{ person.tasks.length }}个项目)</span>
</div>
<button class="expand-button">
<i :class="person.expanded ? 'menu-fold-icon' : 'menu-unfold-icon'"></i>
<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 :ref="el => chartRefs[index] = el" class="progress-chart"></div>
</div>
</div>
</div>
@ -53,207 +30,205 @@
</GiPageLayout>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
import { ref, onMounted, onUnmounted } from "vue";
const timeScales = [
{ label: "周", value: "week", range: 14 },
{ label: "月", value: "month", range: 30 },
{ label: "季", value: "quarter", range: 90 },
{ label: "年", value: "year", range: 365 }
];
const currentScale = ref("month");
const currentStartDate = ref(new Date()); //
<script setup lang='ts'>
import * as echarts from 'echarts'
import { ref, onMounted, onUnmounted, watch } from 'vue'
//
const personList = ref([
{
id: 1,
name: "张三",
name: '张三',
expanded: true,
tasks: [
{ name: "项目一", startDate: "2025-08-01", days: 5, expectedEndDate: "2025-08-07", color: "#5470C6" },
{ name: "项目二", startDate: "2025-08-10", days: 7, expectedEndDate: "2025-08-18", color: "#91CC75" },
{ name: "项目三", startDate: "2025-08-20", days: 4, expectedEndDate: "2025-08-25", color: "#FAC858" }
{ 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: "李四",
name: '李四',
expanded: true,
tasks: [
{ name: "产品设计", startDate: "2025-08-05", days: 8, expectedEndDate: "2025-08-15", color: "#EE6666" },
{ name: "技术评审", startDate: "2025-08-15", days: 3, expectedEndDate: "2025-08-19", color: "#73C0DE" },
{ name: "系统测试", startDate: "2025-08-18", days: 6, expectedEndDate: "2025-08-27", color: "#3BA272" }
{ 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[]>([]);
const setScale = (scale: string) => {
currentScale.value = scale;
currentStartDate.value = new Date();
renderAllCharts();
};
//
const shiftRange = (direction: number) => {
const rangeDays = timeScales.find(s => s.value === currentScale.value)?.range || 30;
const newDate = new Date(currentStartDate.value);
newDate.setDate(newDate.getDate() + direction * rangeDays);
currentStartDate.value = newDate;
renderAllCharts();
};
// 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(() => {
initChart(index);
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 sortedTasks = [...person.tasks].sort(
(a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
const tasks = person.tasks;
const rangeDays = timeScales.find(s => s.value === currentScale.value)?.range || 30;
const startTime = currentStartDate.value.getTime();
const minTime = startTime;
const maxTime = startTime + rangeDays * 86400000;
// -
const today = new Date(2025, 7, 14);
const projectNames = sortedTasks.map(t => t.name);
//
const projectNames: string[] = [];
const dataItems: any[] = [];
const colors: string[] = [];
//
const expectedData = sortedTasks.map((task, idx) => {
let start = new Date(task.startDate).getTime();
let end = new Date(task.expectedEndDate).getTime();
tasks.forEach((task) => {
const startDate = new Date(task.startDate);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + task.days);
if (start < minTime) start = minTime;
if (end > maxTime) end = maxTime;
projectNames.push(task.name);
colors.push(task.color);
return {
name: task.name + " (预期)",
value: [start, end, (end - start) / 86400000, idx, task.color]
};
});
//
const actualData = sortedTasks.map((task, idx) => {
let start = new Date(task.startDate).getTime();
let end = start + task.days * 86400000;
if (start < minTime) start = minTime;
if (end > maxTime) end = maxTime;
return {
dataItems.push({
name: task.name,
value: [start, end, (end - start) / 86400000, idx, task.color]
};
value: [
formatDate(startDate),
formatDate(endDate),
task.days
],
itemStyle: {
color: task.color
}
});
});
//
const chart = echarts.init(container);
//
const option = {
tooltip: {
formatter: (params: any) => `
<strong>${params.data.name}</strong><br>
开始: ${echarts.time.format(params.data.value[0], "{yyyy}-{MM}-{dd}", false)}<br>
结束: ${echarts.time.format(params.data.value[1], "{yyyy}-{MM}-{dd}", false)}<br>
耗时: ${params.data.value[2]}
`
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
},
grid: { left: 80, right: 30, top: 20, bottom: 20 },
xAxis: {
type: "time",
min: minTime,
max: maxTime,
splitNumber: 10,
splitLine: { show: true, lineStyle: { type: "solid", opacity: 0.3 } }
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",
type: 'category',
data: projectNames,
axisTick: { show: false }
},
series: [
//
{
type: "custom",
renderItem: (params: any, api: any) => {
const categoryIndex = api.value(3);
const startCoord = api.coord([api.value(0), categoryIndex]);
const endCoord = api.coord([api.value(1), categoryIndex]);
const barHeight = 20;
return {
type: "rect",
shape: {
x: startCoord[0],
y: startCoord[1] - barHeight / 2,
width: Math.max(0, endCoord[0] - startCoord[0]),
height: barHeight
},
style: {
fill: api.value(4),
opacity: 0.3
}
};
},
encode: { x: [0, 1], y: 3 },
data: expectedData,
z: 1
axisLine: {
show: true
},
//
{
type: "custom",
renderItem: (params: any, api: any) => {
const categoryIndex = api.value(3);
const startCoord = api.coord([api.value(0), categoryIndex]);
const endCoord = api.coord([api.value(1), categoryIndex]);
const barHeight = 12;
return {
type: "rect",
shape: {
x: startCoord[0],
y: startCoord[1] - barHeight / 2,
width: Math.max(0, endCoord[0] - startCoord[0]),
height: barHeight
},
style: {
fill: api.value(4)
}
};
},
encode: { x: [0, 1], y: 3 },
data: actualData,
z: 2
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
};
const chart = echarts.init(container);
chart.setOption(option);
chartInstances.value[personIndex] = chart;
};
const renderAllCharts = () => {
personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
};
}
//
const handleResize = () => {
chartInstances.value.forEach((chart, index) => {
if (chart && personList.value[index].expanded) {
@ -262,96 +237,158 @@ const handleResize = () => {
});
};
//
onMounted(() => {
window.addEventListener("resize", handleResize);
renderAllCharts();
window.addEventListener('resize', handleResize);
personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
});
//
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
chartInstances.value.forEach(chart => chart.dispose());
window.removeEventListener('resize', handleResize);
chartInstances.value.forEach(chart => {
if (chart) {
chart.dispose();
}
});
});
</script>
<style lang="scss" scoped>
<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;
display: flex;
justify-content: space-between;
align-items: center;
}
.scale-switch {
display: flex;
align-items: center;
}
.scale-btn {
padding: 6px 12px;
margin-left: 6px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
}
.scale-btn.active {
background: #3a7afe;
color: white;
border-color: #3a7afe;
}
.nav-btn {
padding: 6px 12px;
margin-left: 6px;
border: 1px solid #ccc;
background: #f5f5f5;
cursor: pointer;
.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;
background-color: white;
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;
}
}
}
.name-container {
display: flex;
align-items: center;
}
.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%;
}
.task-count {
margin-left: 8px;
font-size: 0.85rem;
color: #868e96;
}
.chart-container {
padding: 15px;
background-color: #fff;
}
.progress-chart {
width: 100%;
height: 250px;