Vue3+TS+dhtmlx-gantt实现甘特图

实现样式

在这里插入图片描述

因为只做展示,所以实现很简单

实现功能

  1. 自定义列头
  2. 增加斑马线,实际结束时间(自定义实现)
  3. 自定义进度展示,根据层级让进度背景颜色变浅
  4. marker标记今天
  5. 自定义提示框内容

实现

import { gantt } from "dhtmlx-gantt"; // 引入模块
import { ref } from "vue";
import dayjs from "dayjs";
import { WorkGantt } from "@/api/information-overview/types";

export const useGantt = () => {
  const ganttRef = ref();
  gantt.config.date_format = "%Y/%m/%d"; //整体格式
  gantt.config.duration_unit = "month"; //工期计算的基本单位
  gantt.config.scale_unit = "month"; //列间隔
  gantt.config.date_scale = "%Y/%m/%d"; //设置x轴的日期格式
  gantt.config.step = 1; //间隔
  gantt.i18n.setLocale("cn"); //中文
  gantt.config.autosize = true; //自适应尺寸
  gantt.config.autofit = true; // 表格列宽自适应
  gantt.config.open_tree_initially = true; // 默认是否展开树结构
  //只读模式
  gantt.config.readonly = true;
  // 显示网格
  gantt.config.show_grid = true;
  //更改树状的图标
  gantt.templates.grid_open = (item: any) => {
    return (
      "<div data-icon='" +
      (item.$open ? "close" : "open") +
      "' class='gantt_tree_icon gantt_" +
      (item.$open ? "close" : "open") +
      "'></div>"
    );
  };
  //更改父项图标
  gantt.templates.grid_folder = (item: any) => {
    return "";
  };
  //更改子项图标
  gantt.templates.grid_file = (item: any) => {
    return "";
  };
  // timeLine 文字
  gantt.templates.task_text = function (start, end, task) {
    if (task.real_end_date) {
      const sizes = gantt.getTaskPosition(
        task,
        task.start_date,
        new Date(dayjs(task.real_end_date).format("YYYY-MM-DD"))
      );
      return `<div class="real-task" style="position:absolute;left:0px;top:0px;width:${sizes.width}px;height:100%"></div>`;
    }
    return "";
  };
  // 指定工单栏已完成部分的文本
  gantt.templates.progress_text = function (start, end, task) {
    const level = task.$level as number; //层级
    if (task.progress) {
      return `<div style="text-align:right;color:#000;background-color:${adjustColor(
        "#04aac1",
        level * 20,
        0.7
      )}">${Math.round(task.progress * 100)}%</div>`;
    }
    return "";
  };
  // 列配置
  gantt.config.columns = [
    {
      name: "keyNode",
      resize: true,
      label: "关键节点",
      width: 200,
      align: "center",
      tree: true,
    },
    {
      name: "receiver",
      resize: true,
      label: "签收人",
      width: 80,
      align: "center",
    },
  ];
  // 开启marker插件
  gantt.plugins({ marker: true, tooltip: true });
  const today = new Date(dayjs(new Date()).format("YYYY-MM-DD"));
  const dateToStr = gantt.date.date_to_str(gantt.config.task_date);
  // 添加固定时间线
  gantt.addMarker({
    start_date: today,
    css: "today",
    text: "今日:" + dayjs(new Date()).format("YYYY-MM-DD"),
    title: "Today: " + dateToStr(today),
  });
  // 提示框内容
  gantt.templates.tooltip_text = function (start, end, task) {
    return `
    <h3>关键节点详情</h3>
    <div class="pop-message"><span>关键节点</span><span>${
      task.keyNode ? task.keyNode : "暂无"
    }</span></div>
    <div class="pop-message"><span>签收人</span><span>${
      task.receiver ? task.receiver : "暂无"
    }</span></div>
    <div class="pop-message"><span>节点数量</span><span>${
      task.quantity
    }</span></div>
    <div class="pop-message"><span>完成数量</span><span>${
      task.progressValue
    }</span></div>
    <div class="pop-message"><span>复盘认识</span><span>${
      task.reflectionOnKnowledge ? task.reflectionOnKnowledge : "暂无"
    }</span></div>
    <div class="pop-message"><span>复盘问题</span><span>${
      task.reflectionOnProblems ? task.reflectionOnProblems : "暂无"
    }</span></div>
    <div class="pop-message"><span>复盘总结</span><span>${
      task.reflectionOnCountermeasures
        ? task.reflectionOnCountermeasures
        : "暂无"
    }</span></div>
    `;
  };

  const init = (data: WorkGantt, startDate: string, endDate: string) => {
    gantt.config.start_date = new Date(startDate);
    gantt.config.end_date = new Date(endDate);
    gantt.init(ganttRef.value);
    gantt.parse(data);
  };

  const refresh = (data: WorkGantt, startDate: string, endDate: string) => {
    gantt.clearAll();
    gantt.config.start_date = new Date(startDate);
    gantt.config.end_date = new Date(endDate);
    gantt.parse(data);
    gantt.refreshData();
  };

  const destroyed = () => {
    gantt.clearAll();
  };

  return {
    init,
    refresh,
    ganttRef,
    destroyed,
  };
};

function adjustColor(color: string, depth: number, alpha: number) {
  // 判断颜色格式
  const isRgb = color.length === 3 || color.length === 4;
  const isHex = /^#[0-9a-fA-F]{6}$/.test(color);

  if (!isRgb && !isHex) {
    throw new Error(
      "Invalid color format. Accepted formats: RGB (e.g., [255, 0, 0]) or Hex (e.g., #ff0000)"
    );
  }

  // 将RGB或十六进制颜色转为RGBA格式
  let rgbaColor: any;
  if (isRgb) {
    rgbaColor = [...color, alpha];
  } else if (isHex) {
    const rgbColor = hexToRgb(color) as number[];
    rgbaColor = [...rgbColor, alpha];
  }

  // 根据深浅值调整RGBA值
  rgbaColor = adjustColorValue(rgbaColor, depth);

  return `rgba(${rgbaColor[0]},${rgbaColor[1]},${rgbaColor[2]},${rgbaColor[3]})`;
}

// 十六进制转RGB
function hexToRgb(hex: string) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result
    ? [
        parseInt(result[1], 16),
        parseInt(result[2], 16),
        parseInt(result[3], 16),
      ]
    : null;
}

// 调整颜色深浅值和透明度
function adjustColorValue(rgba: number[], depth: number) {
  return [
    Math.round(rgba[0] + depth) > 255 ? 255 : Math.round(rgba[0] + depth),
    Math.round(rgba[1] + depth) > 255 ? 255 : Math.round(rgba[1] + depth),
    Math.round(rgba[2] + depth) > 255 ? 255 : Math.round(rgba[2] + depth),
    rgba[3], // 保持透明度不变
  ];
}

使用

<template>
  <div class="bg-white">
    <div class="flex justify-between p-2">
      <div class="flex">
        <el-radio-group v-model="state.type">
          <el-radio-button label="self">个人任务</el-radio-button>
          <el-radio-button label="team">全局任务</el-radio-button>
        </el-radio-group>
        <div class="ml-8 flex items-center">
          <span class="font-size-4 mr-4">日期范围</span>
          <el-date-picker
            v-model="state.time"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            @change="changeDate"
          />
        </div>
      </div>
      <el-button type="primary" @click="exportImg" :icon="Download"
        >导出图片</el-button
      >
    </div>
    <div
      v-loading="state.loading"
      id="gantt"
      ref="ganttRef"
      class="h-full w-full"
    ></div>
  </div>
</template>

<script lang="ts">
export default { name: "ObjectProgress" };
</script>
<script lang="ts" setup>
import "dhtmlx-gantt/codebase/dhtmlxgantt.css"; //皮肤
import { onMounted, reactive } from "vue";
import html2canvas from "html2canvas";
import { useGantt } from ".";
import { Download } from "@element-plus/icons-vue";
import { gantt } from "dhtmlx-gantt";
import { getWorkGantt } from "@/api/january-post";
import { useUserStoreHook } from "@/store/modules/user";
import { WorkGantt } from "@/api/information-overview/types";
import dayjs from "dayjs";

const state = reactive({
  tasks: {
    data: [],
  } as WorkGantt,
  type: "self",
  timelist: "",
  time: "",
  loading: false,
});
const { account } = useUserStoreHook().user;
const { init, ganttRef, refresh } = useGantt();

watch(
  () => state.type,
  () => {
    getWorkGanttList((data, startDate, endDate) => {
      refresh(data, startDate, endDate);
    });
  }
);

/**
 * @description 获取甘特图数据
 */
const getWorkGanttList = (
  callback: (data: any, startDate: string, endDate: string) => void
) => {
  state.loading = true;
  const parmas = {
    type: state.type,
    user: account,
    timelist: state.timelist,
  };
  // debugger;
  getWorkGantt(parmas)
    .then((response) => {
      const data = response.data;
      const handleData = data.map((item, index) => {
        const id = index + 1;
        const start_date = dayjs(item.releaseTime).format("YYYY-MM-DD");
        const end_date = dayjs(item.signingTime).format("YYYY-MM-DD");
        const real_end_date = item.completionTime
          ? dayjs(item.completionTime).format("YYYY-MM-DD")
          : "";
        return {
          id,
          start_date,
          end_date,
          real_end_date,
          progress: item.progressBar,
          keyNode: item.keyNode,
          receiver: item.receiver,
          name: item.name,
          reflectionOnKnowledge: item.reflectionOnKnowledge,
          reflectionOnProblems: item.reflectionOnProblems,
          reflectionOnCountermeasures: item.reflectionOnCountermeasures,
          quantity: item.quantity,
          progressValue: item.progressValue,
        };
      });
      const endDate = dayjs(
        Math.max(
          ...data
            .map((item) => [item.completionTime, item.signingTime])
            .flat()
            .map((item) => new Date(item).getTime())
        )
      ).format("YYYY-MM-DD");
      const startDate = dayjs(
        Math.min(
          ...data
            .map((item) => item.releaseTime)
            .map((item) => new Date(item).getTime())
        )
      ).format("YYYY-MM-DD");
      state.tasks.data = handleData;
      callback(state.tasks, startDate, endDate);
    })
    .finally(() => {
      state.loading = false;
    });
};

/**
 * @description 甘特图转canvas
 */
const exportImg = () => {
  html2canvas(document.querySelector("#gantt")!).then(function (canvas) {
    downloadPng(canvas);
  });
};
/**
 * @description 下载canvas
 */
const downloadPng = (el: HTMLCanvasElement) => {
  // 创建一个新的a元素,设置其href为canvas的toDataURL方法,并添加download属性
  var link = document.createElement("a");
  link.href = el.toDataURL("image/png");
  link.download = `${state.type === "personal" ? "个人任务" : "全局任务"}.png`;
  // 触发a元素的click事件以开始下载
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};
/**
 * @description 选择日期
 */
const changeDate = (date: Date[]) => {
  if (date) {
    state.timelist = date
      .map((item) => dayjs(item).format("YYYY/MM/DD"))
      .join(";");
  } else {
    state.timelist = "";
  }
  getWorkGanttList((data, startDate, endDate) => {
    refresh(data, startDate, endDate);
  });
};

onMounted(() => {
  getWorkGanttList((data, startDate, endDate) => {
    init(data, startDate, endDate);
  });
});
</script>

<style lang="scss" scoped>
:deep(.gantt_task_line) {
  background-color: #fff;
  border-color: rgb(220 223 230 / 100%);
  border-radius: 4px;

  .gantt_task_content {
    z-index: 1;
    overflow: initial;
    color: #000;
  }

  .gantt_task_progress_wrapper {
    z-index: 2;
    border-radius: 4px;
  }
}

:deep(.gantt_task_progress) {
  background-color: transparent;
}

:deep(.real-task) {
  z-index: 3;
  background: url("../../../../../assets/icons/diagonal-line.svg") repeat;
  border: 1px solid rgb(220 223 230 / 100%);
  border-radius: 4px;
  opacity: 0.5;
}

:deep(.gantt_marker) {
  z-index: 99;
}
</style>

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值