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

server/2025/3/18 13:49:57/

回顾

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

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

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

typescript"><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 调用的例子

typescript"><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.ppmy.cn/server/175972.html

相关文章

AGI大模型(8):提示词的安全与防护

1 前言 著名的「奶奶漏洞」&#xff0c;⽤套路把 AI 绕懵。 2 常⻅的提示词攻击技术 2.1 同类型⽬标劫持 同类⽬标劫持攻击&#xff0c;特别是在同类型任务的背景下&#xff0c;涉及到攻击者通过⾮法⼿段控制模型&#xff0c;并迫使其执行与原始任务性质相同但⽬标不同的操作…

Linux《进度条》

在之前的Linux基础开发工具当中我们已经了解了vim、gcc、makefile等基本的开发工具&#xff0c;那么有了这些开发工具我们就可以来实现我们Linux旅程当中的第一个程序——进度条。相信通过该项目的实现能让你对vim等开发工具更加的熟悉。一起加油吧&#xff01;&#xff01;&am…

时区转换工具

开发一个Python程序&#xff0c;将用户输入的北京日期时间转换为全球多个目标地区的对应时间&#xff0c;支持手动选择地区&#xff0c;并显示开始和结束两个时间段的转换结果 import pytz from datetime import datetime import pandas as pd from tabulate import tabulate …

Python 视频爬取教程

文章目录 前言基本原理环境准备Python安装选择Python开发环境安装必要库 示例 1&#xff1a;爬取简单直链视频示例 2&#xff1a;爬取基于 HTML5 的视频&#xff08;以某简单视频网站为例&#xff09; 前言 以下是一个较为完整的 Python 视频爬取教程&#xff0c;包含基本原理…

计算机基础:二进制基础13,十六进制与二进制的相互转换

专栏导航 本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏&#xff0c;故划分为两个专栏导航。读者可以自行选择前往哪个专栏。 &#xff08;一&#xff09;WIn32 专栏导航 上一篇&#xff1a;计算机基础&#xff1a;二进制基础12&#xff0c;十进制数转换为…

EasyExcel动态拆分非固定列Excel表格

使用EasyExcel动态拆分非固定列Excel表格 在Excel数据解析场景中&#xff0c;​动态列结构拆分是典型挑战&#xff08;如供应链系统中不同品类的属性字段差异较大&#xff09;。传统基于POJO映射的方案无法应对列数量不固定的场景。本方案采用EasyExcel的动态模型解析和Map数据…

C++之list类及模拟实现

目录 list的介绍 list的模拟实现 定义节点 有关遍历的重载运算符 list的操作实现 &#xff08;1&#xff09;构造函数 (2)拷贝构造函数 &#xff08;3&#xff09;赋值运算符重载函数 &#xff08;4&#xff09;析构函数和clear成员函数 &#xff08;5&#xff09;尾…

C# ManualResetEvent‌的高级用法

一、ManualResetEvent 的核心作用‌ ManualResetEvent 是 C# 中用于 ‌线程同步‌ 的类&#xff08;位于 System.Threading 命名空间&#xff09;&#xff0c;通过信号机制控制线程的等待与执行。其核心功能包括&#xff1a; 阻塞线程‌&#xff1a;调用 WaitOne() 的线程会等…