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="gantt-container">
<div class="page-header"> <div class="page-header">
<h2 class="page-title">人力甘特图页面</h2> <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>
<!-- 人员列表 --> <!-- 人员列表区域 -->
<div class="person-container"> <div class="person-container">
<div <div v-for="(person, index) in personList" :key="person.id" class="person-gantt">
v-for="(person, index) in personList" <!-- 人员标题栏 -->
:key="person.id"
class="person-gantt"
>
<!-- 头部 -->
<div class="person-header" @click="togglePerson(index)"> <div class="person-header" @click="togglePerson(index)">
<div class="name-container"> <div class="name-container">
<span class="avatar">{{ person.name.charAt(0) }}</span> <span class="avatar">{{ person.name.charAt(0) }}</span>
<span>{{ person.name }}</span> <span>{{ person.name }}</span>
<span class="task-count">({{ person.tasks.length }}任务)</span> <span class="task-count">({{ person.tasks.length }}个项目)</span>
</div> </div>
<button class="expand-button"> <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> </button>
</div> </div>
<!-- 甘特图 --> <!-- 甘特图容器 - 根据展开状态控制显示 -->
<div v-show="person.expanded" class="chart-container"> <div v-show="person.expanded" class="chart-container">
<div <div :ref="el => chartRefs[index] = el" class="progress-chart"></div>
:ref="el => chartRefs[index] = el"
class="progress-chart"
></div>
</div> </div>
</div> </div>
</div> </div>
@ -53,207 +30,205 @@
</GiPageLayout> </GiPageLayout>
</template> </template>
<script setup lang="ts"> <script setup lang='ts'>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted, watch } 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()); //
//
const personList = ref([ const personList = ref([
{ {
id: 1, id: 1,
name: "张三", name: '张三',
expanded: true, expanded: true,
tasks: [ tasks: [
{ name: "项目一", startDate: "2025-08-01", days: 5, expectedEndDate: "2025-08-07", color: "#5470C6" }, { name: '项目一', startDate: '2025-08-01', days: 5, color: '#5470C6' },
{ name: "项目二", startDate: "2025-08-10", days: 7, expectedEndDate: "2025-08-18", color: "#91CC75" }, { name: '项目二', startDate: '2025-08-10', days: 7, color: '#91CC75' },
{ name: "项目三", startDate: "2025-08-20", days: 4, expectedEndDate: "2025-08-25", color: "#FAC858" } { name: '项目三', startDate: '2025-08-20', days: 4, color: '#FAC858' }
] ]
}, },
{ {
id: 2, id: 2,
name: "李四", name: '李四',
expanded: true, expanded: true,
tasks: [ tasks: [
{ name: "产品设计", startDate: "2025-08-05", days: 8, expectedEndDate: "2025-08-15", color: "#EE6666" }, { name: '产品设计', startDate: '2025-08-05', days: 8, color: '#EE6666' },
{ name: "技术评审", startDate: "2025-08-15", days: 3, expectedEndDate: "2025-08-19", color: "#73C0DE" }, { name: '技术评审', startDate: '2025-08-15', days: 3, color: '#73C0DE' },
{ name: "系统测试", startDate: "2025-08-18", days: 6, expectedEndDate: "2025-08-27", color: "#3BA272" } { 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 chartRefs = ref<HTMLElement[]>([]);
//
const chartInstances = ref<echarts.ECharts[]>([]); const chartInstances = ref<echarts.ECharts[]>([]);
const setScale = (scale: string) => { // YYYY-MM-DD
currentScale.value = scale; const formatDate = (date: Date): string => {
currentStartDate.value = new Date(); const year = date.getFullYear()
renderAllCharts(); const month = String(date.getMonth() + 1).padStart(2, '0')
}; const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
// }
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();
};
// /
const togglePerson = (index: number) => { const togglePerson = (index: number) => {
personList.value[index].expanded = !personList.value[index].expanded; personList.value[index].expanded = !personList.value[index].expanded;
//
if (personList.value[index].expanded) { if (personList.value[index].expanded) {
setTimeout(() => { setTimeout(() => {
initChart(index); if (chartRefs.value[index] && chartInstances.value[index]) {
chartInstances.value[index].resize();
} else {
initChart(index);
}
}, 10); }, 10);
} }
}; };
//
const initChart = (personIndex: number) => { const initChart = (personIndex: number) => {
const container = chartRefs.value[personIndex]; const container = chartRefs.value[personIndex];
if (!container) return; if (!container) return;
//
if (chartInstances.value[personIndex]) { if (chartInstances.value[personIndex]) {
chartInstances.value[personIndex].dispose(); chartInstances.value[personIndex].dispose();
} }
const person = personList.value[personIndex]; const person = personList.value[personIndex];
const sortedTasks = [...person.tasks].sort( const tasks = person.tasks;
(a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
const rangeDays = timeScales.find(s => s.value === currentScale.value)?.range || 30; // -
const startTime = currentStartDate.value.getTime(); const today = new Date(2025, 7, 14);
const minTime = startTime;
const maxTime = startTime + rangeDays * 86400000;
const projectNames = sortedTasks.map(t => t.name); //
const projectNames: string[] = [];
const dataItems: any[] = [];
const colors: string[] = [];
// tasks.forEach((task) => {
const expectedData = sortedTasks.map((task, idx) => { const startDate = new Date(task.startDate);
let start = new Date(task.startDate).getTime(); const endDate = new Date(startDate);
let end = new Date(task.expectedEndDate).getTime(); endDate.setDate(startDate.getDate() + task.days);
if (start < minTime) start = minTime; projectNames.push(task.name);
if (end > maxTime) end = maxTime; colors.push(task.color);
return { dataItems.push({
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 {
name: task.name, 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 = { const option = {
tooltip: { tooltip: {
formatter: (params: any) => ` trigger: 'item',
<strong>${params.data.name}</strong><br> formatter: (params: any) => {
开始: ${echarts.time.format(params.data.value[0], "{yyyy}-{MM}-{dd}", false)}<br> const task = params.data;
结束: ${echarts.time.format(params.data.value[1], "{yyyy}-{MM}-{dd}", false)}<br> return `
耗时: ${params.data.value[2]} <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: { xAxis: {
type: "time", type: 'time',
min: minTime, min: formatDate(new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)),
max: maxTime, max: formatDate(new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000)),
splitNumber: 10, axisLabel: {
splitLine: { show: true, lineStyle: { type: "solid", opacity: 0.3 } } formatter: function (value: number) {
return echarts.time.format(value, '{MM}/{dd}', false);
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
opacity: 0.3
}
}
}, },
yAxis: { yAxis: {
type: "category", type: 'category',
data: projectNames, data: projectNames,
axisTick: { show: false } axisLine: {
}, show: true
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
}, },
// axisTick: {
{ show: false
type: "custom", },
renderItem: (params: any, api: any) => { axisLabel: {
const categoryIndex = api.value(3); margin: 16
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
} }
] },
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); chart.setOption(option);
chartInstances.value[personIndex] = chart; chartInstances.value[personIndex] = chart;
}; }
const renderAllCharts = () => {
personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
};
//
const handleResize = () => { const handleResize = () => {
chartInstances.value.forEach((chart, index) => { chartInstances.value.forEach((chart, index) => {
if (chart && personList.value[index].expanded) { if (chart && personList.value[index].expanded) {
@ -262,96 +237,158 @@ const handleResize = () => {
}); });
}; };
//
onMounted(() => { onMounted(() => {
window.addEventListener("resize", handleResize); window.addEventListener('resize', handleResize);
renderAllCharts(); personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
}); });
//
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("resize", handleResize); window.removeEventListener('resize', handleResize);
chartInstances.value.forEach(chart => chart.dispose()); chartInstances.value.forEach(chart => {
if (chart) {
chart.dispose();
}
});
}); });
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.gantt-container { .gantt-container {
height: 100%; height: 100%;
padding: 20px; padding: 20px;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.page-header { .page-header {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between; .page-title {
align-items: center; margin: 0;
} font-size: 1.5rem;
.scale-switch { color: #2c3e50;
display: flex; font-weight: 600;
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;
} }
.person-container { .person-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #c2c6cc;
border-radius: 3px;
}
} }
.person-gantt { .person-gantt {
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 8px; border-radius: 8px;
background-color: white; overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
background-color: white;
&:last-child {
margin-bottom: 0;
}
} }
.person-header { .person-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 16px; padding: 12px 16px;
background-color: #f8f9fa; background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
cursor: pointer; 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 { .chart-container {
padding: 15px; padding: 15px;
background-color: #fff;
} }
.progress-chart { .progress-chart {
width: 100%; width: 100%;
height: 250px; height: 250px;