用 Vue 3.5 TypeScript 重新开发3年前甘特图的核心组件

回顾

3年前曾经用 Vue 2.0 开发了一个甘特图组件,如今3年过去了,计划使用Vue 3.5 TypeScript 把组件重新开发,有机会的话再开发一个React版本。

关于之前的组件以前文章
Vue 2.0 甘特图组件

下面录屏是是 用 Vue 3.5 TypeScript 开发的目前进展,不再使用 Vue 2 里用过的 snapsvg-cjs 库,主要是对TypeScript支持的不太好,使用 SVG.js 库代替 snapsvg-cjs 库。然后拖拽和改变大小依旧用的interactjs 库,小有名气的 DHTMLX 甘特图就是用的 interactjs 库,别问我是怎么知道的,我看过源码引用链接
新版本的核心Bar组件开发完成了
定义一个甘特图的结构体,store.js

import { reactive } from 'vue';interface StoreType {monthHeaders: any[];weekHeaders: any[];dayHeaders: any[];hourHeaders: any[];tasks: any[];taskHeaders: any[];mapFields: Record<string, any>;scale: number;timelineCellCount: number;startGanttDate: Date | null;endGanttDate: Date | null;scrollFlag: boolean;mode: string | null;expandRow: {pid: number;expand: boolean;};rootTask: any;subTask: any;editTask: any;removeTask: any;allowChangeTaskDate: any;barDate: {id: string;startDate: string;endDate: string;};
}interface MutationsType {setMonthHeaders: (monthHeaders: any[]) => void;setDayHeaders: (dayHeaders: any[]) => void;setTasks: (tasks: any[]) => void;setTaskHeaders: (taskHeaders: any[]) => void;setWeekHeaders: (weekHeaders: any[]) => void;setHourHeaders: (hourHeaders: any[]) => void;setScale: (scale: number) => void;setMapFields: (mapFields: Record<string, any>) => void;setTimelineCellCount: (timelineCellCount: number) => void;setStartGanttDate: (startGanttDate: Date | null) => void;setEndGanttDate: (endGanttDate: Date | null) => void;setScrollFlag: (scrollFlag: boolean) => void;setMode: (mode: string | null) => void;setExpandRow: (expandRow: { pid: number; expand: boolean }) => void;setRootTask: (rootTask: any) => void;setSubTask: (subTask: any) => void;setEditTask: (editTask: any) => void;setRemoveTask: (removeTask: any) => void;setBarDate: (barDate: { id: string; startDate: string; endDate: string }) => void;setAllowChangeTaskDate: (task: any) => void;
}export let serialNumber: number = 0;
export let store: StoreType = reactive({monthHeaders: [],weekHeaders: [],dayHeaders: [],hourHeaders: [],tasks: [],taskHeaders: [],mapFields: {},scale: 90,timelineCellCount: 0,startGanttDate: null,endGanttDate: null,scrollFlag: true,mode: null,expandRow: {pid: 0,expand: true},rootTask: {},subTask: {},editTask: {},removeTask: {},allowChangeTaskDate: {},barDate: {id: '',startDate: '',endDate: ''}
});export let mutations: MutationsType = {setMonthHeaders(monthHeaders: any[]): void {store.monthHeaders = monthHeaders;},setDayHeaders(dayHeaders: any[]): void {store.dayHeaders = dayHeaders;},setTasks(tasks: any[]): void {store.tasks = tasks;},setTaskHeaders(taskHeaders: any[]): void {store.taskHeaders = taskHeaders;},setWeekHeaders(weekHeaders: any[]): void {store.weekHeaders = weekHeaders;},setHourHeaders(hourHeaders: any[]): void {store.hourHeaders = hourHeaders;},setScale(scale: number): void {store.scale = scale;},setMapFields(mapFields: Record<string, any>): void {store.mapFields = mapFields;},setTimelineCellCount(timelineCellCount: number): void {store.timelineCellCount = timelineCellCount;},setStartGanttDate(startGanttDate: Date | null): void {store.startGanttDate = startGanttDate;},setEndGanttDate(endGanttDate: Date | null): void {store.endGanttDate = endGanttDate;},setScrollFlag(scrollFlag: boolean): void {store.scrollFlag = scrollFlag;},setMode(mode: string | null): void {store.mode = mode;},setExpandRow(expandRow: { pid: number; expand: boolean }): void {store.expandRow = expandRow;},setRootTask(rootTask: any): void {store.rootTask = rootTask;},setSubTask(subTask: any): void {store.subTask = subTask;},setEditTask(editTask: any): void {store.editTask = editTask;},setRemoveTask(removeTask: any): void {store.removeTask = removeTask;},setBarDate(barDate: { id: string; startDate: string; endDate: string }): void {store.barDate = barDate;},setAllowChangeTaskDate(task: any): void {store.allowChangeTaskDate = task;}
};

使用Symbol定义事件名称,独立出一个单独的文件,Symbol.ts

// 定义多个 Symbol
const SetBarColorSymbol = Symbol('SetBarColor');
const AddRootTaskSymbol = Symbol('AddRootTask');// 以对象形式导出
export const Symbols = {SetBarColorSymbol,AddRootTaskSymbol
};

核心甘特图的子组件 Bar.vue

<template><!-- 如果 showRow 为 true,则渲染 barRow 容器 --><div v-if='showRow' class="barRow" :style="{ height: rowHeight + 'px' }" @mouseover="hoverActive()"@mouseleave="hoverInactive()" :class="{ active: hover }"><!-- 如果 showRow 为 true,则渲染 SVG 元素 --><svg key="row.no" v-if='showRow' ref='bar' class="bar" :height="barHeight + 'px'":class="{ active: hover }"></svg><!-- 循环渲染时间轴单元格 --><template v-for='(count) in timelineCellCount':key="count + row.id + timelineCellCount + showRow + '_template'"><!-- 每个单元格的样式设置 --><div class="cell":style="{ width: scale + 'px', minWidth: scale + 'px', maxWidth: scale + 'px', height: rowHeight + 'px', background: WeekEndColor(count), opacity: 0.4 }"></div></template></div>
</template><script lang="ts">
import { defineComponent, inject, ref, computed, onMounted, onDeactivated, onBeforeUnmount } from 'vue';
import SVG from 'svg.js';
import interact from 'interactjs';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
// 扩展 dayjs 功能,使其支持 ISO 周
dayjs.extend(isoWeek);
import { store, mutations } from './store';
import { Symbols } from './Symbols';// 定义注入的 Symbol,用于组件间通信
const ReturnBarColorSymbol = Symbol('ReturnBarColor');
const BarHoverSymbol = Symbol('BarHover');
const MoveToBarStartSymbol = Symbol('MoveToBarStart');
const MoveToBarEndSymbol = Symbol('MoveToBarEnd');
const ScrollToBarSymbol = Symbol('ScrollToBar');
const TaskHoverSymbol = Symbol('TaskHover');/*** Bar 组件* 该组件用于渲染甘特图中的条形图,支持拖动和调整大小。* * @props {number} rowHeight - 行的高度* @props {Record<string, any>} row - 行数据对象* @props {string} startGanttDate - 甘特图的开始日期* @props {string} endGanttDate - 甘特图的结束日期*/
export default defineComponent({name: 'Bar',props: {rowHeight: {type: Number as () => number,default: 0},row: {type: Object as () => Record<string, any>,default: () => ({})},startGanttDate: {type: String as () => string},endGanttDate: {type: String as () => string}},setup(props) {// 引用 SVG 元素const bar = ref<SVGSVGElement | null>(null);// 条形图的高度,为行高的 70%const barHeight = ref(props.rowHeight * 0.7);// 拖动或调整大小的方向const direction = ref<string | null>(null);// 旧的条形图 X 坐标const oldBarDataX = ref(0);// 旧的条形图宽度const oldBarWidth = ref(0);// 是否显示行const showRow = ref(true);// 是否处于悬停状态const hover = ref(false);// 条形图的颜色const barColor = ref('');// 新增一个标志变量,用于记录元素是否已经设置了交互const isBarInteracted = ref(false);// 计算时间轴单元格的数量const timelineCellCount = computed(() => store.timelineCellCount);// 计算每个单元格的宽度const scale = computed(() => store.scale);// 计算甘特图的模式(月、日、时)const mode = computed(() => store.mode);// 计算映射字段const mapFields = computed(() => store.mapFields);// 百分比显示文本const progress = computed(() => Number(props.row[mapFields.value.progress]) * 100 + '%');// 注入事件处理函数const returnBarColor = inject(ReturnBarColorSymbol) as ((callback: (rowId: any, color: string) => void) => void) | undefined;const barHover = inject(BarHoverSymbol) as ((callback: (rowId: any, hover: boolean) => void) => void) | undefined;const moveToBarStart = inject(MoveToBarStartSymbol) as ((callback: (rowId: any) => void) => void) | undefined;const moveToBarEnd = inject(MoveToBarEndSymbol) as ((callback: (rowId: any) => void) => void) | undefined;const scrollToBar = inject(ScrollToBarSymbol) as ((x: number) => void) | undefined;const setBarColor = inject(Symbols.SetBarColorSymbol) as ((row: any) => string) | undefined;const taskHover = inject(TaskHoverSymbol) as ((rowId: any, hover: boolean) => void) | undefined;// 接收设置 Bar 颜色的事件if (returnBarColor) {returnBarColor((rowId, color) => {// 如果当前行的 ID 与传入的 ID 匹配,则更新条形图的颜色if (props.row[mapFields.value['id']] === rowId) {barColor.value = color;}});}// 接收 Bar 悬停事件if (barHover) {barHover((rowId, hoverValue) => {// 如果当前行的 ID 与传入的 ID 匹配,则更新悬停状态if (props.row[mapFields.value['id']] === rowId) {hover.value = hoverValue;}});}// 接收滚动到 Bar 开始位置的事件if (moveToBarStart) {moveToBarStart((rowId) => {if (props.row[mapFields.value['id']] === rowId) {if (bar.value) {if (scrollToBar) {// 滚动到条形图的开始位置scrollToBar(Number(bar.value.getAttribute('data-x')));}}}});}// 接收滚动到 Bar 结束位置的事件if (moveToBarEnd) {moveToBarEnd((rowId) => {if (props.row[mapFields.value['id']] === rowId) {if (bar.value) {if (scrollToBar) {// 滚动到条形图的结束位置scrollToBar(Number(bar.value.getAttribute('data-x')) + Number(bar.value.width.baseVal.value) - Number(scale.value));}}}});}// 从 mutations 中获取设置条形图日期的函数const setBarDate = mutations.setBarDate;// 从 mutations 中获取设置是否允许更改任务日期的函数const setAllowChangeTaskDate = mutations.setAllowChangeTaskDate;/*** 检查一个节点是否是另一个节点的子节点* * @param {Node | null} child - 子节点* @param {Node | null} parent - 父节点* @returns {boolean} - 如果是子节点返回 true,否则返回 false*/const isChildOf = (child: Node | null, parent: Node | null): boolean => {if (child && parent) {let parentNode = child.parentNode;// 循环遍历父节点,直到找到匹配的父节点或到达根节点while (parentNode) {if (parent === parentNode) {return true;}parentNode = parentNode.parentNode;}}return false;};/*** 更新条形图的数据和 UI* * @param {Object} event - 事件对象* @param {Object} props - 组件的属性* @param {Object} mode - 甘特图的模式* @param {Object} scale - 单元格的宽度* @param {Object} oldBarDataX - 旧的条形图 X 坐标* @param {Object} oldBarWidth - 旧的条形图宽度* @param {SVGSVGElement} barElement - SVG 元素* @param {Object} barHeight - 条形图的高度* @param {Object} mapFields - 映射字段* @param {Function} setBarDate - 设置条形图日期的函数* @param {boolean} [isResizable=false] - 是否可调整大小*/const updateBarDataAndUI = (event: { target: SVGSVGElement; rect: { width: number }; dx: number; edges?: { left: boolean; right: boolean } },props: {row: Record<string, any>;startGanttDate: string;endGanttDate: string;},mode: { value: string },scale: { value: number },oldBarDataX: { value: number },oldBarWidth: { value: number },barElement: SVGSVGElement,barHeight: { value: number },mapFields: { value: Record<string, string> },setBarDate: (data: { id: any; startDate: string; endDate: string }) => void,isResizable = false) => {let target = event.target;// 计算新的 X 坐标let x = (parseFloat(target.getAttribute('data-x') || '0') || 0) + event.dx;let width = event.rect.width;if (isResizable) {// 调整宽度以适应单元格的宽度let remainWidth = width % scale.value;if (remainWidth !== 0) {let multiple = Math.floor(width / scale.value);if (remainWidth < (scale.value / 2)) {width = multiple * scale.value;} else {width = (multiple + 1) * scale.value;}}let offsetWidth = oldBarWidth.value - width;if (event.edges && event.edges.left) {x += offsetWidth;}// 更新 SVG 元素的宽度target.setAttribute('width', width.toString());target.style.width = width + 'px';}// 更新 SVG 元素的位置target.style.transform = `translate(${x}px, 0px)`;target.setAttribute('data-x', x.toString());// 更新 SVG 元素的文本内容target.textContent = Math.round(width) + '\u00D7' + Math.round(barHeight.value);// 将 SVGSVGElement 转换为 HTMLElementlet svg = SVG(barElement as unknown as HTMLElement);// 查找现有的元素let p = svg.select('pattern').first();let g = (svg.children().filter((child) => child.type === 'g')[0] as any) || svg.group();let innerRect = svg.select('rect:has(.innerRect)').first();console.log(innerRect)let outerRect = svg.select('rect:not(.innerRect)').first();let text = svg.select('text').first();// 创建 SVG 图案if (!p) {p = svg.pattern(10, 10, (add) => {(add as any).path('M10 -5 -10,15M15,0,0,15M0 -5 -20,15').fill('none').stroke({ color: 'gray', opacity: 0.4, width: 5 });});}// 创建 SVG 组if (!g) {g = svg.group();} let innerRectWidth = 0;// 根据任务的进度计算内部矩形的宽度if (props.row[mapFields.value.progress]) {innerRectWidth = Number(width) * Number(props.row[mapFields.value.progress]);} else {innerRectWidth = Number(width);}if (!innerRect) {innerRect = svg.rect(innerRectWidth, barHeight.value).radius(10);if (!innerRect.hasClass('innerRect')) {innerRect.addClass('innerRect');innerRect.fill({ color: barColor.value, opacity: 0.4 });innerRect.width(innerRectWidth);if (!g.has(innerRect)) {g.add(innerRect);}}}if (!outerRect) {outerRect = svg.rect(width, barHeight.value).radius(10).fill(p).stroke({ color: '#cecece', width: 1 });// 外部矩形的鼠标悬停事件处理outerRect.on('mouseover', () => {outerRect.animate(200).attr({stroke: '#000',strokeWidth: 2,opacity: 1});});// 外部矩形的鼠标离开事件处理outerRect.on('mouseleave', () => {outerRect.animate(200).attr({stroke: '#0066ff',strokeWidth: 10,opacity: 0.4});});} else {outerRect.width(width);}if (!text) {text = svg.text(progress.value).stroke('#faf7ec');}const textBBox = text.bbox();// 设置文本元素的字体样式(text as any).font({size: 15,anchor: 'middle',leading: '1em'}).fill('#000').attr('opacity', 1).attr('dominant-baseline', 'middle').center(innerRect.width() / 2 + textBBox.width / 2, innerRect.height() / 2);let offsetStart = 0;let offsetEnd = 0;if (isResizable) {if (event.edges && event.edges.left) {// 计算开始日期的偏移量offsetStart = ((oldBarDataX.value - x) / scale.value);if (mode.value === '月' || mode.value === '日') {offsetStart *= 24;}} else {// 计算结束日期的偏移量offsetEnd = (oldBarWidth.value - width) / scale.value;if (mode.value === '月' || mode.value === '日') {offsetEnd *= 24;}}} else {// 计算开始和结束日期的偏移量offsetStart = (x - oldBarDataX.value) / scale.value;offsetEnd = offsetStart;if (mode.value === '月' || mode.value === '日') {offsetStart *= 24;offsetEnd *= 24;}}// 更新任务的开始日期props.row[mapFields.value.startdate] = dayjs(props.row[mapFields.value.startdate]).locale('zh-cn').add(-offsetStart, 'hours').format('YYYY-MM-DD HH:mm:ss');// 更新任务的结束日期props.row[mapFields.value.enddate] = dayjs(props.row[mapFields.value.enddate]).locale('zh-cn').add(-offsetEnd, 'hours').format('YYYY-MM-DD HH:mm:ss');// 根据甘特图的模式更新任务的耗时信息if (mode.value === '月' || mode.value === '日') {props.row[mapFields.value.takestime] = dayjs(props.row[mapFields.value.enddate]).diff(dayjs(props.row[mapFields.value.startdate]), 'days') + 1 + '天';} else if (mode.value === '时') {props.row[mapFields.value.takestime] = dayjs(props.row[mapFields.value.enddate]).diff(dayjs(props.row[mapFields.value.startdate]), 'hours') + 1 + '小时';}// 设置条形图的日期setBarDate({id: props.row[mapFields.value.id],startDate: props.row[mapFields.value.startdate],endDate: props.row[mapFields.value.enddate]});};/*** 绘制条形图* * @param {SVGSVGElement} barElement - SVG 元素*/const drowBar = (barElement: SVGSVGElement) => {// 清空 SVG 元素的内容// barElement.innerHTML = '';let dataX = 0;// 根据甘特图的模式计算条形图的位置和宽度switch (mode.value) {case '月':case '日': {// 计算从计划开始日期到条形图开始日期的天数let fromPlanStartDays = dayjs(props.row[mapFields.value.startdate]).diff(dayjs(props.startGanttDate), 'days');dataX = scale.value * fromPlanStartDays;// 计算条形图的持续天数let spendDays = dayjs(props.row[mapFields.value.enddate]).diff(dayjs(props.row[mapFields.value.startdate]), 'days') + 1;oldBarWidth.value = spendDays * scale.value;// 更新任务的耗时信息props.row[mapFields.value.takestime] = spendDays + '天';break;}case '时': {// 计算从计划开始日期到条形图开始日期的小时数let fromPlanStartHours = dayjs(props.row[mapFields.value.startdate]).diff(dayjs(props.startGanttDate), 'hours');dataX = scale.value * fromPlanStartHours;// 计算条形图的持续小时数let spendHours = dayjs(props.row[mapFields.value.enddate]).diff(dayjs(props.row[mapFields.value.startdate]), 'hours') + 1;oldBarWidth.value = spendHours * scale.value;// 更新任务的耗时信息props.row[mapFields.value.takestime] = spendHours + '小时';break;}}oldBarDataX.value = dataX;// 将 SVGSVGElement 转换为 HTMLElementlet svg = SVG(barElement as unknown as HTMLElement);// 设置 SVG 元素的属性barElement.setAttribute('data-x', dataX.toString());barElement.setAttribute('width', oldBarWidth.value.toString());barElement.setAttribute('stroke', '#cecece');barElement.setAttribute('stroke-width', '1px');barElement.style.transform = `translate(${dataX}px, 0px)`;// 查找现有的元素let p = svg.select('pattern').first();let g = (svg.children().filter((child) => child.type === 'g')[0] as any) || svg.group();let innerRect = svg.select('.innerRect').first();let outerRect = svg.select('rect:not(.innerRect)').first();let text = svg.select('text').first();// 创建 SVG 图案if (!p) {p = svg.pattern(10, 10, (add) => {(add as any).path('M10 -5 -10,15M15,0,0,15M0 -5 -20,15').fill('none').stroke({ color: 'gray', opacity: 0.4, width: 5 });});}// 创建 SVG 组if (!g) {g = svg.group();}let innerRectWidth: number = 0;if (props.row[mapFields.value.progress]) {innerRectWidth = Number(oldBarWidth.value) * Number(props.row[mapFields.value.progress]);} else {innerRectWidth = Number(oldBarWidth.value);}if (!innerRect) {innerRect = svg.rect(innerRectWidth, barHeight.value).radius(10);innerRect.addClass('innerRect');g.add(innerRect);} else {innerRect.fill({ color: barColor.value, opacity: 0.4 });innerRect.width(innerRectWidth);}// 性能优化避免重绘if (!outerRect) {outerRect = svg.rect(oldBarWidth.value, barHeight.value).radius(10).fill(p).stroke({ color: '#cecece', width: 1 });// 外部矩形的鼠标悬停事件处理outerRect.on('mouseover', () => {outerRect.animate(200).attr({stroke: '#000',strokeWidth: 2,opacity: 1});});// 外部矩形的鼠标离开事件处理outerRect.on('mouseleave', () => {outerRect.animate(200).attr({stroke: '#0066ff',strokeWidth: 10,opacity: 0.4,});});} else {outerRect.width(oldBarWidth.value);}// 性能优化避免重绘if (!text) {text = svg.text(progress.value).stroke('#faf7ec');}const textBBox = text.bbox();// 设置文本元素的字体样式(text as any).font({size: 15,anchor: 'middle',leading: '1em'}).fill('#000').attr('opacity', 1).attr('dominant-baseline', 'middle').center(innerRect.width() / 2 + textBBox.width / 2, innerRect.height() / 2);// 设置条形图的日期setBarDate({id: props.row[mapFields.value.id],startDate: props.row[mapFields.value.startdate],endDate: props.row[mapFields.value.enddate]});// 使 SVG 元素可拖动interact(barElement).draggable({inertia: false,modifiers: [interact.modifiers.restrictRect({restriction: 'parent',endOnly: true})],autoScroll: true,listeners: {start: (event: { target: SVGSVGElement }) => {// 记录拖动开始时的 X 坐标和宽度oldBarDataX.value = Number(event.target.getAttribute('data-x'));oldBarWidth.value = event.target.width.baseVal.value;},move: (event: { target: SVGSVGElement; dx: number; rect: { width: number; height: number } }) => {let { x } = event.target.dataset;// 计算新的 X 坐标x = ((parseFloat(event.target.getAttribute('data-x') || '0') || 0) + event.dx).toString();// 更新 SVG 元素的样式Object.assign(event.target.style, {width: `${event.rect.width}px`,height: `${event.rect.height}px`,transform: `translate(${x}px, 0px)`});if (typeof x !== 'undefined') {// 更新 SVG 元素的 data-x 属性event.target.setAttribute('data-x', x.toString());}// 更新 SVG 元素的 data-y 属性event.target.setAttribute('data-y', '0');},end: (event: { target: SVGSVGElement; dx: number; rect: { width: number } }) => {let target = event.target;// 计算新的 X 坐标let x = (parseFloat(target.getAttribute('data-x') || '0') || 0) + event.dx;let multiple = Math.floor(x / scale.value);x = multiple * scale.value;if (x > timelineCellCount.value * scale.value) {x = timelineCellCount.value * scale.value;}// 更新 SVG 元素的位置target.style.transform = `translate(${x}px, 0px)`;// 更新 SVG 元素的 data-x 属性target.setAttribute('data-x', x.toString());// 更新条形图的数据和 UIupdateBarDataAndUI(event, {row: props.row,startGanttDate: props.startGanttDate || '',endGanttDate: props.endGanttDate || ''}, { value: mode.value || '' }, scale, oldBarDataX, oldBarWidth, barElement, barHeight, mapFields, setBarDate, false);}}});// 使 SVG 元素可调整大小interact(barElement).resizable({edges: { left: true, right: true, bottom: false, top: false },listeners: {start: (event: { target: SVGSVGElement }) => {// 记录调整大小开始时的 X 坐标和宽度oldBarDataX.value = Number(event.target.getAttribute('data-x'));oldBarWidth.value = Number(event.target.getAttribute('width'));},end: (event: { target: SVGSVGElement; dx: number; rect: { width: number }; edges: { left: boolean; right: boolean } }) => {// 设置允许更改任务日期setAllowChangeTaskDate(props.row);// 手动构建符合类型要求的对象const updatedProps = {row: props.row,startGanttDate: props.startGanttDate as string,endGanttDate: props.endGanttDate as string};// 更新条形图的数据和 UIupdateBarDataAndUI(event, updatedProps, { value: mode.value || '' }, scale, oldBarDataX, oldBarWidth, barElement, barHeight, mapFields, setBarDate, true);}},modifiers: [interact.modifiers.restrictEdges({outer: 'parent'}),interact.modifiers.restrictSize({min: { width: scale.value, height: barHeight.value }})],inertia: false,hold: 1});};/*** 处理鼠标悬停激活事件*/const hoverActive = () => {// 设置悬停状态为 truehover.value = true;if (taskHover) {// 触发任务悬停事件taskHover(props.row[mapFields.value['id']], hover.value);}};/*** 处理鼠标悬停取消事件*/const hoverInactive = () => {// 设置悬停状态为 falsehover.value = false;if (taskHover) {// 触发任务悬停事件taskHover(props.row[mapFields.value['id']], hover.value);}};/*** 根据日期计算周末的背景颜色* * @param {number} count - 日期的偏移量* @returns {string | undefined} - 背景颜色*/const WeekEndColor = (count: number) => {switch (mode.value) {case '月':case '日': {// 计算当前日期let currentDate = dayjs(props.startGanttDate).add(count, 'days');// 如果是周六或周日,返回特定的背景颜色if (currentDate.isoWeekday() === 7 || currentDate.isoWeekday() === 1) {return '#F3F4F5';}break;}}};// 组件挂载后执行的钩子函数onMounted(() => {if (bar.value && !isBarInteracted.value) {// 绘制条形图drowBar(bar.value);// 设置标志变量为 true,表示元素已经设置了交互isBarInteracted.value = true;}if (setBarColor) {// 设置条形图的颜色barColor.value = setBarColor(props.row);// 更新颜色if (bar.value) {drowBar(bar.value);}}});// keep-alive 停用时的清理onDeactivated(() => {if (bar.value && interact.isSet(bar.value)) {// 取消 SVG 元素的交互设置interact(bar.value).unset()}// 隐藏行showRow.value = false})// 组件卸载前的清理onBeforeUnmount(() => {if (bar.value && interact.isSet(bar.value)) {// 取消 SVG 元素的交互设置interact(bar.value).unset()}// 隐藏行showRow.value = false})return {bar,barHeight,direction,oldBarDataX,oldBarWidth,showRow,hover,barColor,timelineCellCount,scale,mode,mapFields,setBarDate,setAllowChangeTaskDate,isChildOf,drowBar,hoverActive,hoverInactive,WeekEndColor};}
});
</script>
<style lang="scss" scoped>
.active {background: #FFF3A1;
}.barRow {display: flex;flex-flow: row nowrap;align-items: center;justify-content: flex-start;border-top: 1px solid #cecece;border-right: 0px solid #cecece;border-bottom: 0px solid #cecece;margin: 0px 1px -1px -1px;width: fit-content;position: relative;.bar {position: absolute;z-index: 100;background-color: #faf7ec;border-radius: 10px;}.cell {display: flex;align-items: center;justify-content: center;font-size: 10px;// 只保留右边框,避免重复计算宽度border-right: 1px solid #cecece;// 顶部和底部边框通过伪元素实现,不影响宽度position: relative;margin: -1px 0px 0px 0px;box-sizing: border-box;}// 为 .cell 添加顶部和底部的伪元素来显示边框.cell::before,.cell::after {content: '';position: absolute;left: 0;right: 0;border-top: 1px solid #cecece;}.cell::before {top: 0;}.cell::after {bottom: 0;}
}
</style>./Symbols./store

在app.vue 调用的例子

<template><div style="width: 100%;height: 100%;"><Bar :startGanttDate='startGanttDate' :endGanttDate='endGanttDate' :row='row' :rowHeight='rowHeight'></Bar><div>{{ store.barDate.startDate }}: {{ store.barDate.endDate }}</div></div>
</template><script setup lang="ts">
import { ref, provide } from 'vue';
import Bar from './components/gantt/Bar.vue';
import { store, mutations } from './components/gantt/store';
import { Symbols } from './components/gantt/Symbols';const startGanttDate = ref('2025-03-01');
const endGanttDate = ref('2025-03-31');
const row = {id: '1',pid: '0',taskNo: '1',level: '重要',start_date: '2025-03-02 00:00:00',end_date: '2025-03-08 00:00:00',job_progress: '0.3',spend_time: null,progress: '0.3'
};// 设置Bar的颜色
provide(Symbols.SetBarColorSymbol, (row : Record<string, any>) => {if(row.level === '重要') {return 'red';} else if(row.level === '一般') {return 'green';}return 'blue';
});const rowHeight = ref(60);
mutations.setMode('月');
mutations.setScale(60);
mutations.setTimelineCellCount(20);
mutations.setMapFields({// idid: 'id',// 父idparentId: 'pid',// 任务名称task: 'taskNo',// 优先级priority: 'level',// 工作开始时间startdate: 'start_date',// 工作结束时间enddate: 'end_date',// 耗时takestime: 'spend_time',// 进度progress: 'job_progress'
});
</script>./components/gantt/Symbols./components/gantt/store

收获

强化学习了TypeScript,不得不说TS写起来难度比JS要大
强化学习了Vue 3.5,比如认识了defineModel这些比较香的新功能

憧憬

希望以后能开发React版本,甚至Blazor的版本

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言【数据结构】:时间复杂度和空间复杂度.详解

引言 详细介绍什么是时间复杂度和空间复杂度。 前言&#xff1a;为什么要学习时间复杂度和空间复杂度 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的&#xff0c;即时…

Matlab 基于专家pid控制的时滞系统

1、内容简介 Matlab 185-基于专家pid控制的时滞系统 可以交流、咨询、答疑 2、内容说明 略 在处理时滞系统&#xff08;Time Delay Systems&#xff09;时&#xff0c;使用传统的PID控制可能会面临挑战&#xff0c;因为时滞会导致系统的不稳定或性能下降。专家PID控制通过结…

MyBatis源码分析のSql执行流程

文章目录 前言一、准备工作1.1、newExecutor 二、执行Sql2.1、getMappedStatement2.2、query 三、Cache装饰器的执行时机四、补充总结 前言 本篇主要介绍MyBatis解析配置文件完成后&#xff0c;执行sql的相关逻辑&#xff1a; public class Main {public static void main(Str…

【MySQL】数据库基础

目录 一、什么是数据库1.1 为什么要有数据库1.2 数据库的本质是什么1.3 在Linux下看一下数据库 二、主流数据库三、基本使用3.1 连接服务器3.2 服务器&#xff0c;数据库&#xff0c;表关系 四、MySQL架构五、SQL分类六、存储引擎6.1 存储引擎是什么6.2 查看存储引擎6.3 存储引…

算是解决可以访问github但无法clone的问题

本文的前提是使用了**且可以正常访问github 查看代理的端口 将其配置到git 首先查看git配置 git config --list然后添加配置&#xff0c;我这边使用的是Hiddfy默认的端口是12334&#xff0c;如果是clash应该是7890 git config --global http.proxy 127.0.0.1:12334其他 删除…

SpringBoot第三站:配置嵌入式服务器使用外置的Servlet容器

目录 1. 配置嵌入式服务器 1.1 如何定制和修改Servlet容器的相关配置 1.server.port8080 2. server.context-path/tx 3. server.tomcat.uri-encodingUTF-8 1.2 注册Servlet三大组件【Servlet&#xff0c;Filter&#xff0c;Listener】 1. servlet 2. filter 3. 监听器…

AdaLoRA 参数 配置:CAUSAL_LM“ 表示因果语言模型任务

AdaLoRA 参数 配置:CAUSAL_LM" 表示因果语言模型任务 config = AdaLoraConfig( init_r=16, # 增加 LoRA 矩阵的初始秩 lora_alpha=32, target_modules=[“q_proj”, “v_proj”], lora_dropout=0.1, bias=“none”, task_type=“CAUSAL_LM” ) 整体功能概述 AdaLoraCon…

IP 协议

文章目录 IP 协议概述数据包格式首部校验和实例分析实例一 分片抓包分析参考 本文为笔者学习以太网对网上资料归纳整理所做的笔记&#xff0c;文末均附有参考链接&#xff0c;如侵权&#xff0c;请联系删除。 IP 协议 概述 IP 协议是 TCP/IP 协议簇中的核心协议&#xff0c;也…

日常开发记录-radioGroup组件

日常开发记录-radioGroup组件 1.前提2.问题&#xff1a;无限循环调用3.解释Vue 事件传播机制分析与无限循环原因解释4.解决 1.前提 在上一章的&#xff0c;我们实现了radio组件。从这进入了解 新增个radioGroup组件呢。 <template><divclass"q-radio-group&quo…

API调用comfyui工作流,做一个自己的app,chatgpt给我写的前端,一键创建自己的卡通形象,附源码

前言 工具介绍 首先 comfyui你是少不了的&#xff0c;这个是工作流的后端支持&#xff0c;用这个去调试工作流和生成API可调用文件 前端我们就用很流行的gradio吧&#xff0c;什么你一时半会没有学gradio的计划&#xff0c;没事&#xff0c;笔者也没系统学过&#xff0c;我干…

【网络】数据流(Data Workflow)Routes(路由)、Controllers(控制器)、Models(模型) 和 Middleware(中间件)

在图片中&#xff0c;数据流&#xff08;Data Workflow&#xff09;描述了应用程序中数据的流动过程&#xff0c;涉及 Routes&#xff08;路由&#xff09;、Controllers&#xff08;控制器&#xff09;、Models&#xff08;模型&#xff09; 和 Middleware&#xff08;中间件&…

【通义千问】蓝耘智算 | 智启未来:蓝耘MaaS×通义QwQ-32B引领AI开发生产力

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈人工智能与大模型应用 ⌋ ⌋ ⌋ 人工智能&#xff08;AI&#xff09;通过算法模拟人类智能&#xff0c;利用机器学习、深度学习等技术驱动医疗、金融等领域的智能化。大模型是千亿参数的深度神经网络&#xff08;如ChatGPT&…

Scratch 3.0安装包,支持Win7/10/11、Mac电脑手机平板、少儿便编程的启蒙软件。

Scratch是一款由麻省理工学院&#xff08;MIT&#xff09; 设计开发的少儿编程工具。其特点是&#xff1a;使用者可以不认识英文单词&#xff0c;也可以不使用键盘&#xff0c;就可以进行编程。构成程序的命令和参数通过积木形状的模块来实现。用鼠标拖动指令模块到脚本区就可以…

Deepseek学习--工具篇之Ollama

Deepseek学习--工具篇之Ollama 用途特点简化部署‌轻量级与可扩展性‌API支持‌预构建模型库‌模型导入与定制‌跨平台支持‌命令行工具与环境变量‌ 来源缘起诞生爆发持续 安装使用方法下载安装安装模型调用API 用途 我们在进行Deepseek本地部署的时候&#xff0c;通常会用到…

Flask多参数模版使用

需要建立目录templates&#xff1b; 把建好的html文件放到templates目录里面&#xff1b; 约定好参数名字&#xff0c;单个名字可以直接使用&#xff1b;多参数使用字典传递&#xff1b; 样例&#xff1a; from flask import render_template # 模板 (Templates) #Flask 使用…

LabVIEW旋转设备状态在线监测系统

为了提高大型旋转设备如电机和水泵的监控效率和故障诊断能力&#xff0c;用LabVIEW软件开发了一套实时监测与故障诊断系统。该系统集成了趋势分析、振动数据处理等多项功能&#xff0c;可实时分析电机电流、压力、温度及振动数据&#xff0c;以早期识别和预报故障。 ​ 项目背…

汽车PKE无钥匙进入系统一键启动系统定义与原理

汽车智能钥匙&#xff08;PKE无钥匙进入系统&#xff09;一键启动介绍 系统定义与原理 汽车无钥匙进入系统&#xff0c;简称PKE&#xff08;Passive Keyless Entry&#xff09;&#xff0c;该系统采用了RFID无线射频技术和车辆身份编码识别系统&#xff0c;率先应用小型化、小…

【Idea】 xml 文本粘贴保持原有文本的缩进格式

Idea xml 文本粘贴保持原有文本的缩进格式 在使用 IntelliJ IDEA 2018 版本中的 MyBatis 时&#xff0c;粘贴 SQL 语句会自动对齐&#xff0c;此时需要进行相关设置来禁用此功能。 setting——>Editor——>Code Style——>XML 勾选“Keep white spaces”

Unity 和 Python 的连接(通过SocketIO)附源码

在游戏或者项目开发中&#xff0c;Unity 通常用于创建前端&#xff0c;而 Python 则因其强大的数据处理能力常被用作后端。通过 Socket.IO&#xff0c;我们可以轻松地实现 Unity 和 Python 的实时通信。本文将介绍如何通过 Socket.IO 连接 Unity 和 Python&#xff0c;并附上完…

[IP]UART

UART 是一个简易串口ip&#xff0c;用户及配置接口简单。 波特率从9600至2000000。 该 IP 支持以下特性&#xff1a; 异步串行通信&#xff1a;标准 UART 协议&#xff08;1 起始位&#xff0c;8 数据位&#xff0c;1 停止位&#xff0c;无奇偶校验&#xff09;。 参数化配置…