甘特图
vue3加原生三件套绘制,轻量且易扩展
源代码甘特图组件
<template>
<div class="gantt-container">
<div class="gantt-table-wrapper">
<table class="gantt-table">
<thead>
<tr>
<th rowspan="2" style="width: 3%;">序号</th>
<th rowspan="2" style="width: 8%;">编号</th>
<th rowspan="2" style="width: 5%;">申请单位</th>
<th rowspan="2" style="width: 10%;">检修主题</th>
<th rowspan="2" style="width: 6%;">开始时间</th>
<th rowspan="2" style="width: 6%;">结束时间</th>
<th rowspan="2" style="width: 4%;">工期(天)</th>
<th :colspan="daysInMonth" class="month-header">{{ currentYear }}年{{ currentMonth }}月</th>
</tr>
<tr>
<th v-for="day in daysInMonth" :key="day" style="min-width: 20px; white-space: nowrap;">{{ day }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(task, index) in tableData" :key="task.id" :title="`#工作内容\n${task['工作内容']}`">
<td>{{ index + 1 }}</td>
<td>{{ task.bianhao }}</td>
<td>{{ task['申请单位'] }}</td>
<td :title="task.name" class="ellipsis">{{ task.name }}</td>
<td>{{ formatDate(task.start) }}</td>
<td>{{ formatDate(task.end) }}</td>
<td>{{ calculateDuration(task.start, task.end) }}</td>
<td
v-for="day in daysInMonth"
:key="day"
:class="{
'highlight': isDateInRange(day, task.start, task.end),
'first-day': isFirstDay(day, task.start),
'last-day': isLastDay(day, task.end)
}"
:style="{ color: (isFirstDay(day, task.start) || isLastDay(day, task.end)) ? 'white' : '' }"
>
{{
isFirstDay(day, task.start) ? formatDateMD(task.start) : (isLastDay(day, task.end) ? formatDateMD(task.end) : '')
}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import {computed, defineProps} from 'vue';
import {dayjs} from 'element-plus'
import * as XLSX from "xlsx-js-style";
const props = defineProps({
tableData: {
type: Array,
required: true
},
yearMonth: {
type: String,
required: true
}
});
// 当前月份处理
const currentYear = computed(() => parseInt(props.yearMonth.split('-')[0]));
const currentMonth = computed(() => parseInt(props.yearMonth.split('-')[1]));
const daysInMonth = computed(() => dayjs(`${currentYear.value}-${currentMonth.value}`).daysInMonth());
// 日期格式化
const formatDate = (date) => dayjs(date).format('YYYY-MM-DD');
//日前格式化M-D
const formatDateMD = (date) => dayjs(date).format('M-D');
//计算工期,结束时间减去(day)开始时间+1
const calculateDuration = (start, end) => {
const startDate = dayjs(start);
const endDate = dayjs(end);
return endDate.diff(startDate, 'day') + 1;
};
// 判断日期是否在范围内
const isDateInRange = (day, start, end) => {
const yearMonth = props.yearMonth.split('-');
const currentDate = new Date(yearMonth[0], yearMonth[1] - 1, day);
const startDate = new Date(start);
const endDate = new Date(end);
// 设置时间为0时0分0秒
currentDate.setHours(0, 0, 0, 0);
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
return currentDate >= startDate && currentDate <= endDate;
};
// 判断是否是任务的第一天
const isFirstDay = (day, start) => {
const startDate = dayjs(start);
return startDate.date() === day && startDate.month() + 1 === currentMonth.value;
};
// 判断是否是任务的最后一天
const isLastDay = (day, end) => {
const endDate = dayjs(end);
return endDate.date() === day && endDate.month() + 1 === currentMonth.value;
};
const exportToExcel = () => {
const title = `甘特图${currentYear.value}年${currentMonth.value}月`
const ws = XLSX.utils.aoa_to_sheet([
[
{v: title, t: "s", s: {alignment: {vertical: 'center', horizontal: 'center'}, font: {sz: 16, bold: true}}}
],
['序号', '编号', '申请单位', '检修主题', '开始时间', '结束时间', '工期(天)', ...Array.from({length: daysInMonth.value}, (_, i) => i + 1)],
...props.tableData.map((task, index) => [
index + 1,
task.bianhao,
task['申请单位'],
task.name,
formatDate(task.start),
formatDate(task.end),
calculateDuration(task.start, task.end),
...Array.from({length: daysInMonth.value}, (_, day) =>
isDateInRange(day + 1, task.start, task.end)
? {
v: isFirstDay(day + 1, task.start) ? formatDateMD(task.start) : (isLastDay(day + 1, task.end) ? formatDateMD(task.end) : ''),
t: "s",
s: {
fill: {fgColor: {rgb: "0A938E"}},
font: {color: {rgb: "FFFFFF"}},
border: {top: {style: "thin"}, bottom: {style: "thin"}}
}
} : ''
)
])
]);
const wb = XLSX.utils.book_new();
ws['!cols'] = [
{wch: 5}, // 序号
{wch: 18}, // 编号
{wch: 10}, // 申请单位
{wch: 15}, // 检修主题
{wch: 12}, // 开始时间
{wch: 12}, // 结束时间
{wch: 8}, // 工期(天)
];
ws["!merges"] = [
{s: {r: 0, c: 0}, e: {r: 0, c: 6 + daysInMonth.value}},
];
for (let i = 8; i < 8 + daysInMonth.value; i++) {
ws['!cols'].push({wch: 4});
}
XLSX.utils.book_append_sheet(wb, ws, '甘特图');
XLSX.writeFile(wb, `甘特图${props.yearMonth}.xlsx`);
}
//暴露导出excel方法
defineExpose({
exportToExcel
})
</script>
<style scoped>
.gantt-container {
width: 100%;
overflow-x: auto;
}
.gantt-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.gantt-table th,
.gantt-table td {
border: 1px solid #ddd;
text-align: center;
}
.month-header {
background-color: #f2f2f2;
font-weight: bold;
}
.highlight {
background-color: #0A938E;
position: relative;
padding: 10px 0;
}
.highlight.first-day::before {
position: absolute;
left: 2px;
color: white;
}
.highlight.last-day::after {
position: absolute;
right: 2px;
color: white;
}
tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
tbody tr:hover {
background-color: #f1f1f1;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
使用甘特图示例,二次封装成弹窗dialog
<template>
<el-dialog
v-model="dialogVisible"
title="月度甘特图-流转中"
width="90%"
draggable
>
<div class="wh-100 container">
<div class="query-box">
<label style="font-weight: bold">编号</label>
<el-input v-model="queryParams.bianhao" size="small" clearable style="width: 150px;margin: 0 10px"
placeholder="请输入编号"></el-input>
<label style="font-weight: bold">检修主题</label>
<el-input v-model="queryParams.topic" size="small" clearable style="width: 150px;margin: 0 10px"
placeholder="请输入检修主题"></el-input>
<label style="font-weight: bold">工作内容</label>
<el-input v-model="queryParams.content" size="small" clearable style="width: 150px;margin: 0 10px"
placeholder="请输入工作内容"></el-input>
<label style="font-weight: bold">申请单位</label>
<el-input v-model="queryParams.applyUnit" size="small" clearable style="width: 150px;margin: 0 10px"
placeholder="请输入申请单位"></el-input>
<label style="font-weight: bold">审核处室</label>
<el-select v-model="queryParams.checkDep" placeholder="请选择" size="small" clearable style="width: 120px;margin: 0 10px">
<el-option
v-for="item in dicCheckDepOption"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<label style="font-weight: bold">年月</label>
<el-date-picker
v-model="queryParams.month"
type="month"
@change="getList"
value-format="YYYY-MM"
style="width: 120px;margin: 0 10px"
placeholder="选择年月"
/>
<el-button type="primary" size="small" :icon="Search" @click="getList" :loading="loading">
搜索
</el-button>
<el-button type="primary" size="small" :icon="Download" @click="ExportExcelGantt" :loading="loadingDown">
导出excel
</el-button>
</div>
<div class="query-table">
<gantt ref="ganttRef" v-loading="loading" :table-data="tableDataComputed" :year-month="queryParams.month"/>
</div>
</div>
</el-dialog>
</template>
<script setup>
import {computed, ref} from "vue";
import {monthlzz} from "@/api/monthFlow.js";
import gantt from "@/components/Gantt/index.vue"
import {Download, Search} from "@element-plus/icons-vue";
import {dayjs} from 'element-plus'
import {dicCheckDepOption} from "@/utils/dicc.js";
// 弹窗显示控制
const dialogVisible = ref(false)
const ganttRef = ref()
const queryParams = ref({
bianhao: '',
applyUnit: '',
checkDep:'',
current: 1,
size: 100,
content: '',
month: dayjs().add(1, 'month').format("YYYY-MM"),
topic: '',
total: 0
})
const loading = ref(false)
const loadingDown = ref(false)
const tableData = ref([])
//查询
const getList = (() => {
loading.value = true
monthlzz(queryParams.value).then(res => {
if (res.data) {
tableData.value = res.data.records
queryParams.value.total = res.data.total
loading.value = false
}
})
})
getList()
// 打开弹窗
const openGanttDialog = () => {
dialogVisible.value = true
getList() // 打开时自动查询
}
const ExportExcelGantt = () => {
loadingDown.value = true
ganttRef.value.exportToExcel()
loadingDown.value = false
}
const tableDataComputed = computed(() => {
return tableData.value.map((item) => {
return {
id: item['business_id'],
start: item['开工时间'],
end: item['完工时间'],
name: item['检修主题'],
bianhao: item['编号'], ...item
}
})
});
// 暴露方法给父组件
defineExpose({
openGanttDialog
})
</script>
<style scoped>
.container {
height: 600px;
.query-box {
display: flex;
align-items: center;
background-color: white;
padding-left: 10px;
width: 100%;
height: 50px;
}
.query-table {
width: 100%;
height: calc(100% - 100px);
overflow-y: scroll;
}
.query-page {
display: flex;
width: 100%;
height: 50px;
padding-left: 10px;
background-color: #F1F4F6;
}
}
</style>
前端导出甘特图为excel基于xlsx-js-style
第一步:安装xlsx-js-style
npm install xlsx-js-style
第二步:绘制表格
xlsx-js-style官网仓库xlsx-js-style
使用文档SheetJS 中文网
源代码
const exportToExcel = () => {
const title = `甘特图${currentYear.value}年${currentMonth.value}月`
const ws = XLSX.utils.aoa_to_sheet([
[
{v: title, t: "s", s: {alignment: {vertical: 'center', horizontal: 'center'}, font: {sz: 16, bold: true}}}
],
['序号', '编号', '申请单位', '检修主题', '开始时间', '结束时间', '工期(天)', ...Array.from({length: daysInMonth.value}, (_, i) => i + 1)],
...props.tableData.map((task, index) => [
index + 1,
task.bianhao,
task['申请单位'],
task.name,
formatDate(task.start),
formatDate(task.end),
calculateDuration(task.start, task.end),
...Array.from({length: daysInMonth.value}, (_, day) =>
isDateInRange(day + 1, task.start, task.end)
? {
v: isFirstDay(day + 1, task.start) ? formatDateMD(task.start) : (isLastDay(day + 1, task.end) ? formatDateMD(task.end) : ''),
t: "s",
s: {
fill: {fgColor: {rgb: "0A938E"}},
font: {color: {rgb: "FFFFFF"}},
border: {top: {style: "thin"}, bottom: {style: "thin"}}
}
} : ''
)
])
]);
const wb = XLSX.utils.book_new();
ws['!cols'] = [
{wch: 5}, // 序号
{wch: 18}, // 编号
{wch: 10}, // 申请单位
{wch: 15}, // 检修主题
{wch: 12}, // 开始时间
{wch: 12}, // 结束时间
{wch: 8}, // 工期(天)
];
ws["!merges"] = [
{s: {r: 0, c: 0}, e: {r: 0, c: 6 + daysInMonth.value}},
];
for (let i = 8; i < 8 + daysInMonth.value; i++) {
ws['!cols'].push({wch: 4});
}
XLSX.utils.book_append_sheet(wb, ws, '甘特图');
XLSX.writeFile(wb, `甘特图${props.yearMonth}.xlsx`);
}