甘特图Vue3 | 原生绘制

发布于:2025-05-01 ⋅ 阅读:(30) ⋅ 点赞:(0)

甘特图

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`);
}

网站公告

今日签到

点亮在社区的每一天
去签到