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

news2024/12/25 10:32:37

实现样式

在这里插入图片描述

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

实现功能

  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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1407518.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

某顺cookie逆向

目标网站:aHR0cHM6Ly9xLjEwanFrYS5jb20uY24v 这个网站是对cookie进行反爬虫的&#xff0c;可以看到cookie中有一个加密参数v 二、分析参数 可以使用hook方法&#xff0c;来hook住cookie中v生成的位置&#xff0c;可以直接在控制台中输入hook函数 (function () {use strict;v…

【详细解释深度学习图像分类检测评价指标】准确率Accuracy、精确率Precision、召回率Recall、mAP等(一文搞懂,建议收藏)

前言&#xff1a; &#x1f60a;&#x1f60a;&#x1f60a;欢迎来到本博客&#x1f60a;&#x1f60a;&#x1f60a; &#x1f31f;&#x1f31f;&#x1f31f; 本专栏主要是记录工作中、学习中关于AI(Deep Learning)相关知识并分享。 &#x1f60a;&#x1f60a;&#x1f…

Parallels Desktop 19 mac 虚拟机软件 兼容M1 M2

Parallels Desktop 19 for Mac 是一款适用于 macOS 的虚拟机软件。无需重启即可在 Mac 上运行 Windows、Linux 等系统&#xff0c;具有速度快、操作简单且功能强大的优点。包括 30 余种实用工具&#xff0c;可简化 Mac 和 Windows 上的日常任务。 软件下载&#xff1a;Parallel…

大模型时代的计算机系统革新:更大规模、更分布式、更智能化

编者按&#xff1a;2023年是微软亚洲研究院建院25周年。借此机会&#xff0c;我们特别策划了“智启未来”系列文章&#xff0c;邀请到微软亚洲研究院不同研究领域的领军人物&#xff0c;以署名文章的形式分享他们对人工智能、计算机及其交叉学科领域的观点洞察及前沿展望。希望…

搭建网站使用花生壳的内网穿透实现公网访问

目录 一 搭建网站 二 使用花生壳进行内网穿透 1、创建内网映射 2、linux系统安装花生壳客户端 3、重新打开浏览器&#xff0c;输入http://b.oray.com&#xff0c;完成账户登录&#xff0c;激活&#xff08;SN登录&#xff09; 一 搭建网站 准备工作&#xff1a; [rootse…

主播产品对比话术

—、价格对比 主播产品A︰这款产品定价相对较高&#xff0c;但是其品质和功能都是一流的&#xff0c;对于追求高端体验的消费者来说&#xff0c;物有所值。 主播产品B∶这款产品的价格相对较低&#xff0c;性价比很高&#xff0c;对于预算有限的消费者来说&#xff0c;是个不…

基于Java SSM框架现图书馆借阅管理系统项目【项目源码+论文说明】

基于java的SSM框架实现图书馆借阅管理系统演示 摘要 以往的图书馆管理事务处理主要使用的是传统的人工管理方式&#xff0c;这种管理方式存在着管理效率低、操作流程繁琐、保密性差等缺点&#xff0c;长期的人工管理模式会产生大量的文本借书与文本数据&#xff0c;这对事务的…

控制项目风险

一、风险预算 暴雪公司经理艾莉森&#xff0c;暴雪公司是一家小型工业企业&#xff0c;该公司的高管为了降低生产成本&#xff0c;决定搬迁工厂。项目经理明白实际情况与初始计划之间常常会有很大的出入。项目经理需要事先为一些事情做好准备&#xff0c;并在项目运作或预算方面…

Vue基础-Computed-Watch

一、computed计算属性使用 1.复杂data的处理方式 我们知道&#xff0c;在模板中可以直接通过插值语法显示一些data中的数据。 但是在某些情况&#xff0c;我们可能需要对数据进行一些转化后再显示&#xff0c;或者需要将多个数据结合起来进行显示&#xff1b; 比如我们需要…

ORA-12528: TNS: 监听程序: 所有适用例程都无法建立新连

用了网上的办法&#xff1a; 1、修改listener.ora的参数,把动态的参数设置为静态的参数,红色标注部分 位置D:\oracle\product\10.2.0\db_1\NETWORK\ADMIN SID_LIST_LISTENER (SID_LIST (SID_DESC (SID_NAME PLSExtProc) (ORACLE_HOME D:\oracle\produ…

微信小程序(十)表单组件(入门)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.type 属性指定表单类型 2.placeholder 属性指定输入框为空时的占位文字 源码&#xff1a; form.wxml <!-- 提前准备好的布局结构代码 --> <view class"register"><view class"…

【LangChain学习之旅】—(10) 用RouterChain确定客户意图

【【LangChain学习之旅】—&#xff08;10&#xff09; 用RouterChain确定客户意图 任务设定整体框架具体步骤如下&#xff1a; 具体实现构建提示信息的模板构建目标链 Reference&#xff1a;LangChain 实战课 任务设定 首先&#xff0c;还是先看一下今天要完成一个什么样的任…

Pyro —— DOP Nodes

目录 Smoke Object —— 创建smoke对象及相关场 Smoke Solver —— Smoke解算器 Color Relationships Advanced Pyro Solver —— Pyro解算器 Smoke Object (Sparse) —— 创建smoke对象及相关场 Smoke Solver (Sparse) —— Sparse Smoke解算器 Simulation Advanced …

[足式机器人]Part2 Dr. CAN学习笔记- 最优控制Optimal Control Ch07

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记 - 最优控制Optimal Control Ch07-1最优控制问题与性能指标 1. 最优控制问题与性能指标2. 动态规划 Dynamic Programming2.1 基本概念2.2 代码详解2.3 简单一维案例 3. 线性二次型调节器&#xff…

SourceTree修改仓库密码

1、找到 SourceTree缓存文件目录&#xff1a; passwd 目录保存账号对应的密码&#xff08;已加密&#xff09; 2、删除密码 删除passwd文件即可。重启 SourceTree 软件&#xff0c;进行操作&#xff0c;就会有输入密码的弹窗&#xff0c;输入即可。

高标准农田气象站

在当今社会&#xff0c;科技的发展正在深刻地改变着我们的生活。特别是在农业领域&#xff0c;科技的运用已经成为了保障粮食安全、提高农业生产效率的重要手段。其中&#xff0c;高标准农田气象站作为现代农业的重要组成部分&#xff0c;正在发挥着越来越重要的作用。 TH-NQ14…

【原生小程序-分包】

1.创建分包-文件夹 subPackages app.json中写入subPackges对象&#xff0c;在里面写分包路径 {"pages": ["pages/index/index"],"subPackages": [{"root": "subPackages","name": "分包A","pag…

【强化学习】QAC、A2C、A3C学习笔记

强化学习算法&#xff1a;QAC vs A2C vs A3C 引言 经典的REINFORCE算法为我们提供了一种直接优化策略的方式&#xff0c;它通过梯度上升方法来寻找最优策略。然而&#xff0c;REINFORCE算法也有其局限性&#xff0c;采样效率低、高方差、收敛性差、难以处理高维离散空间。 为…

leetcode—课程表 拓扑排序

1 题目描述 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示如果要学习课程 ai 则 必须 先学习课程 …

仓储管理系统——软件工程报告(总体设计)③

总体设计 一、需求规定 软件工程仓库存储管理系统的需求规定是确保系统能够满足用户期望、提高工作效率、确保数据安全性和系统可维护性的基石。其涵盖了功能性、性能、数据管理、用户界面和系统可维护性等多个方面。通过严格的验收标准&#xff0c;可以确保系统在实际应用中…