当前位置: 首页 > news >正文

用 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的版本

相关文章:

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

回顾 3年前曾经用 Vue 2.0 开发了一个甘特图组件&#xff0c;如今3年过去了&#xff0c;计划使用Vue 3.5 TypeScript 把组件重新开发&#xff0c;有机会的话再开发一个React版本。 关于之前的组件以前文章 Vue 2.0 甘特图组件 下面录屏是是 用 Vue 3.5 TypeScript 开发的目前…...

Python使用总结之Flask构建文件服务器,通过网络地址访问本地文件

Python使用总结之Flask构建文件服务器,通过网络地址访问本地文件 在 Web 开发中,静态文件(如图片、CSS、JavaScript)的管理是基础且重要的环节。Flask 提供的 send_from_directory 函数为开发者提供了灵活的文件服务解决方案。本文将详细解析其原理、用法及最佳实践。 一…...

从Excel到搭贝的转变过程

从Excel到搭贝 1. 简介 1.1 Excel简介 Excel 作为元老级的数据管理工具&#xff0c;功能强大且被广泛使用&#xff0c;但在现代工作场景中仍存在一些局限性&#xff0c;例如&#xff1a; 数据量处理有限&#xff1a;处理大规模数据时&#xff0c;Excel可能运行缓慢或崩溃。…...

C语言经典代码练习题

1.输入一个4位数&#xff1a;输出这个输的个位 十位 百位 千位 #include <stdio.h> int main(int argc, char const *argv[]) {int a;printf("输入一个&#xff14;位数&#xff1a;");scanf("%d",&a);printf("个位&#xff1a;%d\n"…...

Compose 的产生和原理

引言 compose 出现的目的&#xff1a; 重新定义android 上ui 的编写方式。为了提高android 原生ui开发效率。让android 的UI开发方式跟上时代。 正文 compose 是什么&#xff1f; 就是一套ui框架 和flutter 一样是一套ui框架 Flutter&#xff1a;跨平台开发趋势与企业应用的…...

JS做贪吃蛇小游戏(源码)

一、HTML代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><link rel…...

c语言笔记 结构体内嵌套结构体的表示方式

目录 结构体内嵌套结构体 问&#xff1a;我们都该如何去访问该结构体里面的结构体的成员呢?怎么去给里面的成员赋值呢? 说明&#xff1a; 运行上述代码后&#xff0c;输出结果如下&#xff1a; 结构体内嵌套结构体 背景&#xff1a;如果我们在结构体中放结构体&#xff0…...

Vue3一个组件绑定多个 v-model,自定义 prop 和 event 名称

Vue3一个组件绑定多个 v-model&#xff0c;自定义 prop 和 event 名称 Vue3中v-model默认使用modelValue作为prop&#xff0c;update:modelValue作为事件&#xff0c;而Vue2使用的是value和input。此外&#xff0c;Vue3允许通过参数的方式为组件添加多个v-model绑定&#xff0…...

STM32---FreeRTOS事件标志组

一、简介 事件标志位&#xff1a;用一个位&#xff0c;来表示事件是否发生 事件标志组&#xff1a;一组事件标志位的集合&#xff0c;可以简单的理解时间标志组&#xff0c;就是一个整体。 事件标志租的特点&#xff1a; 它的每一个位表示一个时间&#xff08;高8位不算&…...

分享一个项目中遇到的一个算法题

需求背景&#xff1a; 需求是用户要创建一个任务计划在未来执行&#xff0c;要求在创建任务计划的时候判断选择的时间是否符合要求&#xff0c;否则不允许创建&#xff0c;创建的任务类型有两种&#xff0c;一种是单次&#xff0c;任务只执行一次&#xff1b;另一种是周期&…...

入门 Sui Move 开发:9. 一个 Sui dApp 前端项目

内容概览 接下来一起通过 PTB 和 Navi SDK 实现一个一键存入借出的简单 DApp。 本节分为两部分&#xff1a; 创建一个 DApp 前端项目以及 Sui dApp Kit 的使用&#xff1b;了解 Navi SDK&#xff0c;主要包含的功能以及如何实现存入和借出功能&#xff1b; 最终完成我们的项…...

如何打造安全稳定的亚马逊采购测评自养号下单系统?

在当今的电商领域&#xff0c;亚马逊作为全球领先的在线购物平台&#xff0c;其商品种类繁多&#xff0c;用户基数庞大&#xff0c;成为了众多商家和消费者的首选。而对于一些需要进行商品测评或市场调研的用户来说&#xff0c;拥有一个稳定、安全的亚马逊账号体系显得尤为重要…...

c语言笔记 结构体基础

目录 基础知识 结构体定义 基础知识 在c语言中变量是有类型的&#xff0c;比如整型&#xff0c;char型&#xff0c;浮点型等&#xff0c;这些都是单一的类型&#xff0c;那么如果说我要定义一个学生的信息&#xff0c;那么这些单一的类型是不足以表达一个学生的全部信息&#…...

添加 ChatGPT/Grok/Gemini 到浏览器搜索引擎

文章目录 添加 ChatGPT/Grok/Gemini 到浏览器搜索引擎如何添加步骤 1: 打开浏览器设置步骤 2: 添加新搜索引擎步骤 3: 保存设置 注意事项 添加 ChatGPT/Grok/Gemini 到浏览器搜索引擎 在使用 ChatGPT/Grok/Gemini 进行对话时&#xff0c;每次都需要先打开对应的网页&#xff0…...

golang-struct结构体

struct结构体 概述 Go 语言中数组可以存储同一类型的数据&#xff0c;但在结构体中我们可以为不同项定义不同的数据类型。 结构体是 Golang 中一种复合类型&#xff0c;它是由一组具有相同或不同类型的数据字段组成的数据结构。 结构体是一种用户自定义类型&#xff0c;它可…...

矫平机:工业制造的“误差归零者”,如何重塑智造新生态?

在新能源汽车电池托盘的生产线上&#xff0c;一块2米长的铝合金板材因焊接应力产生了0.5毫米的隐形翘曲。这个看似微不足道的变形&#xff0c;却导致激光焊接工序的良率暴跌至65%。当工程师们尝试传统矫正方案时&#xff0c;发现高强度铝合金既不能加热校形&#xff0c;又无法承…...

Springboot中的@ConditionalOnBean注解:使用指南与最佳实践

在使用Spring Boot进行开发时&#xff0c;大家应该都听说过条件注解&#xff08;Conditional Annotations&#xff09;。其中的ConditionalOnBean注解就很有趣&#xff0c;它帮助开发者在特定条件下创建和注入Bean&#xff0c;让你的应用更加灵活。今天就来聊聊这个注解的使用场…...

Spring 中 BeanFactoryPostProcessor 的作用和示例

一、概览 1. 核心定位 BeanFactoryPostProcessor 是 Spring 容器级别的扩展接口&#xff0c;在 Bean 实例化之前&#xff0c;对 Bean 的配置元数据&#xff08;即 BeanDefinition&#xff09;进行动态修改或扩展。其核心功能围绕以下两点&#xff1a; 修改现有 Bean 的定义&…...

PDFMathTranslate 安装、使用及接入deepseek

PDFMathTranslate 安装、使用及接入deepseek 介绍安装及使用接入deepseek注意 介绍 PDFMathTranslate 是非常好用的科学 PDF 文档翻译及双语对照工具&#xff0c;可以将论文按照其原本的排版结构执行多种语言翻译&#xff0c;并且可以接入如&#xff1a;谷歌翻译、deepl、deep…...

Docker生存手册:安装到服务一本通

文章目录 一. Docker 容器介绍1.1 什么是Docker容器&#xff1f;1.2 为什么需要Docker容器&#xff1f;1.3 Docker架构1.4 Docker 相关概念1.5 Docker特点 二. Docker 安装2.1 查看Linux内核版本2.2 卸载老版本docker&#xff0c;避免产生影响2.3 升级yum 和配置源2.4 安装Dock…...

JAVA中关于图形化界面的学习(GUI)动作监听,鼠标监听,键盘监听

动作监听&#xff1a; 先创建一个图形化界面&#xff0c;接着创建一个按钮对象&#xff0c;设置按钮的大小。 添加一个addActionListener()&#xff1b; addActionListener() 方法定义在 java.awt.event.ActionListener 接口相关的上下文中&#xff0c;许多支持用户交互产生…...

wepy微信小程序自定义底部弹出框功能,显示与隐藏效果(淡入淡出,滑入滑出)

视图html部分 <view class"salePz"><view class"btnSelPz" tap"pzModelClick">去选择</view><!-- modal --><view class"modal modal-bottom-dialog" hidden"{{hideFlag}}"><view class&q…...

Api架构设计--- HTTP + RESTful

Api架构设计--- HTTP RESTful 什么是RESTfulRESTful 设计原则RESTful 接口类型RESTful 状态码RESTful Uri设计原则Api传参&#xff1a;QueryString 和 UriPath RESTful和HTTP的区别注意事项 什么是RESTful RESTful&#xff08;Representational State Transfer&#xff09;是一…...

设计模式-适配器模式

适配器模式是一种结构型设计模式&#xff0c;用于将一个类的接口转换为客户端期望的另一个接口&#xff0c;使得原本不兼容的类可以协同工作。它的核心思想是通过中间层&#xff08;适配器&#xff09;解决接口不匹配的问题&#xff0c;类似于电源插头转换器。 核心思想 适配…...

MacBook部署达梦V8手记

背景 使用Java SpringBootDM开发Web应用&#xff0c;框架有License&#xff0c;OSX加载dll失败&#xff0c;安装了Windows 11&#xff0c;只有一个C盘&#xff0c;达梦安装后因为C盘权限问题&#xff0c;创建数据库失败&#xff0c;遂采用Docker容器方式部署。 下载介质 官网在…...

MySQL程序

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:数据库 JavaEE专栏:JavaEE 软件测试专栏:软件测试 关注博主带你了解更多知识 1. mysqld (MySQL服务器) mysqld也被称为MySQL服务器&#xff0c;是⼀个多线程程序&#xff0c;对数据⽬录进⾏访问管理(包含数据库…...

APB-清华联合腾讯等机构推出的分布式长上下文推理框架

APB (Accelerating Distributed Long-Context Inference by Passing Compressed Context Blocks acrossGPUs)是清华大学等机构联合提出的分布式长上下文推理框架。通过稀疏注意力机制和序列并行推理方式&#xff0c;有效解决了大模型处理长文本时的效率瓶颈。APB采用更小的Anch…...

python爬虫笔记(一)

文章目录 html基础标签和下划线无序列表和有序列表表格加边框 html的属性a标签&#xff08;网站&#xff09;target属性换行线和水平分割线 图片设置宽高width&#xff0c;height html区块——块元素与行内元素块元素与行内元素块元素举例行内元素举例 表单from标签type属性pla…...

Pycharm接入DeepSeek,提升自动化脚本的写作效率

一.效果展示&#xff1a; 二.实施步骤&#xff1a; 1.DeepSeek官网创建API key&#xff1a; 创建成功后&#xff0c;会生成一个API key&#xff1a; 2. PyCharm工具&#xff0c;打开文件->设置->插件&#xff0c;搜索“Continue”&#xff0c;点击安装 3.安装完成后&…...

spring boot 过滤器简单demo

1. 过滤器&#xff08;Filter&#xff09;的概念 过滤器是 Java Web 应用中的一种组件&#xff0c;它可以在请求到达目标资源&#xff08;如 Controller&#xff09;之前或响应返回客户端之后&#xff0c;对请求和响应进行统一处理。它的核心作用是对 HTTP 请求和响应进行拦截…...

3.8 Spring Boot监控:Actuator+Prometheus+Grafana可视化

在Spring Boot应用中&#xff0c;通过整合Actuator、Prometheus和Grafana可以构建完整的监控体系&#xff0c;实现指标采集、存储和可视化。以下是具体实现步骤&#xff1a; 一、Spring Boot Actuator 配置 作用&#xff1a;暴露应用健康指标、性能数据等监控端点。 1. 添加依…...

C++中的单例模式及具体应用示例

AI 摘要 本文深入探讨了C中的单例模式及其在机器人自主导航中的应用&#xff0c;特别是如何通过单例模式来管理地图数据。文章详细介绍了单例模式的基本结构、优缺点以及在多线程环境中的应用&#xff0c;强调了其在保证数据一致性和资源管理中的重要性。 接着&#xff0c;文章…...

网络编程——套接字、创建服务器、创建客户端

一、套接字 1.1什么是套接字 套接字文件&#xff0c;原本就是一个和管道文件类似&#xff0c;用来实现进程间通信的一个文件 既然有了管道文件&#xff0c;当时为什么还要开发套接字文件&#xff0c;去实现进程的通信 因为管道文件是半双工模式的 套接字文件是全双工模式的…...

【设计模式】3W 学习法深入剖析创建型模式:原理、实战与开源框架应用(含 Java 代码)

3W 学习法总结创建型模式&#xff08;附 Java 代码实战及开源框架应用&#xff09; 创建型模式主要关注 对象的创建&#xff0c;旨在提高代码的可复用性、可扩展性和灵活性。本文采用 3W 学习法&#xff08;What、Why、How&#xff09;&#xff0c;深入分析 五大创建型模式&am…...

软考系统架构师考试目录(2023新版)

论文 2023下半年 开发&#xff1a;论面向对象设计的应用与实现大数据&#xff1a;论多数据源集成的应用与实现测试&#xff1a;论软件可靠性评价的设计与实现运维&#xff1a;论边云协同的设计与实现 2024上半年 大数据&#xff1a;Lambda架构&#xff0c;分层批处理层、加…...

Apifox Helper 自动生成API接口文档

在我们开发过程中我们在编写请求地址和编写请求参数的时候特别花费时间耗费了我们很多时间&#xff0c;作为一个程序员&#xff0c;更应该把精力时间集中在开发上&#xff0c; Apifox Helper 是 Apifox 团队针对 IntelliJ IDEA 环境所推出的插件&#xff0c;可以在 IDEA 环境中…...

MySQL开发陷阱与最佳实践:第1章:MySQL开发基础概述-1.1 MySQL简介与应用场景

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 文章大纲 MySQL开发陷阱与最佳实践&#xff1a;第1章&#xff1a;MySQL开发基础概述-1.1 MySQL简介与应用场景1.1.1 MySQL的发展历程与市场地位1.1.2 MySQL的核心特性与技术优势1.1.2…...

电鱼智能EFISH-RK3576-SBC工控板已适配Android 14系统

EFISH-RK3576-SBC工控板此前已提供了Linux 6.1.57系统&#xff0c;为了满足更多客户的需求&#xff0c;电鱼智能近日又为其成功适配了Android 14系统——硬件性能卓越的核心板与Android 14的深度组合&#xff0c;将为用户带来更加流畅、开放、智能的使用体验。 一、高性能处理器…...

C++ 语法之函数和函数指针

在上一章中 C 语法之 指针的一些应用说明-CSDN博客 我们了解了指针变量&#xff0c;int *p;取变量a的地址这些。 那么函数同样也有个地址&#xff0c;直接输出函数名就可以得到地址&#xff0c;如下&#xff1a; #include<iostream> using namespace std; void fun() …...

LabVIEW生成EXE文件错误提示

在LabVIEW生成EXE时弹出 “The build is missing one or more source files or items the source files reference on disk”&#xff0c;表明项目中引用的某些文件&#xff08;如VI、子模块、依赖库或配置文件&#xff09;未被正确包含或路径丢失。以下是具体原因及解决方案&a…...

HTML,CSS,JavaScript

HTML:负责网页的结构(页面元素和内容)。 CSS:负责网页的表现(页面元素的外观、位置等页面样式&#xff0c;如:颜色、大小等)。 Javascript:负责网页的行为(交互效果)。 MDN前端开发文档(MDN Web Docs) HTML HTML(HyperText Markup Language):超文本标记语言超文本:超越了文本的…...

SpringCloud 学习笔记2(Nacos)

Nacos Nacos 下载 Nacos Server 下载 | Nacos 官网 下载、解压、打开文件&#xff1a; 更改 Nacos 的启动方式 Nacos 的启动模式默认是集群模式。在学习时需要把他改为单机模式。 把 cluster 改为 standalone&#xff0c;记得保存&#xff01; 启动startup.cmd Ubuntu 启动…...

Qt5.15.2实现Qt for WebAssembly与示例

目录 1.什么是Qt for WebAssembly&#xff1f; 1.1 什么是 WebAssembly&#xff1f; 1.2 WebAssembly 的优势 1.3 什么是 Qt for WebAssembly&#xff1f; 1.4 Qt for WebAssembly 的特点 1.5 编译过程 1.6 运行时环境 注意&#xff01;&#xff01;&#xff01;注意&am…...

荣耀手机怎么录制屏幕?屏幕录制后为视频加水印更有“安全感”

在数字时代&#xff0c;屏幕录制已经成为记录和分享信息的重要方式之一。无论是记录游戏的高光时刻&#xff0c;还是制作教学视频&#xff0c;亦或是保存重要的线上会议内容&#xff0c;屏幕录制都能轻松搞定。 荣耀手机作为一款功能强大的设备&#xff0c;自然也提供了便捷的…...

3DXML 与 SOLIDWORKS 格式转换:技术协同及迪威模型方案

一、引言 在产品设计的前沿领域&#xff0c;3DXML 与 SOLIDWORKS 作为主流格式&#xff0c;虽各有所长&#xff0c;但因格式差异&#xff0c;常成为数据流通与协作的阻碍。对于技术人员和学生党而言&#xff0c;掌握二者间的转换技术&#xff0c;不仅能提升设计效率&#xff0…...

CH347使用笔记:CH347结合STM32CubeIDE实现单片机下载与调试

目录 基于 STM32CubeIDE的 CH347 JTAG/SWD调试器使用说明1. CH347驱动安装与配置2. STM32CubeIDE调试器配置2.1 打开相关工程后&#xff0c;进行以下操作2.2 openocd.exe替换2.3 脚本添加2.4 更改调试器选择 3. 下载程序4. 使用过程中可能遇到的问题4.1 CH347未插入4.2 Openocd…...

JS—基本数据类型和引用数据类型:1分钟掌握两者的区别

个人博客&#xff1a;haichenyi.com。感谢关注 一. 目录 一–目录二–分类三–核心区别四–实际场景中的问题五–总结对比 二. 分类 前面说过这么判断数据类型&#xff0c;今天来说说基本数据类型和引用数据类型的区别。 基本数据类型引用数据类型StringObjectNumberFunct…...

使用 CryptoJS 实现 AES 解密:动态数据解密示例

在现代加密应用中,AES(高级加密标准)是一种广泛使用的对称加密算法。它的安全性高、效率好,适合用于各种加密任务。今天,我们将通过一个实际的示例,展示如何使用 CryptoJS 实现 AES 解密,解密动态数据。CryptoJS 是一个基于 JavaScript 的加密库,它支持 AES、DES 等多种…...

[设计模式与源码]1_Spring三级缓存中的单例模式

欢迎来到啾啾的博客&#x1f431;&#xff0c;一个致力于构建完善的Java程序员知识体系的博客&#x1f4da;&#xff0c;记录学习的点滴&#xff0c;分享工作的思考、实用的技巧&#xff0c;偶尔分享一些杂谈&#x1f4ac;。 欢迎评论交流&#xff0c;感谢您的阅读&#x1f604…...

使用React和google gemini api 打造一个google gemini应用

实现一个简单的聊天应用&#xff0c;用户可以通过输入问题或点击“Surprise me”按钮获取随机问题&#xff0c;并从后端API获取回答。 import { useState } from "react"; function App() {const [ value, setValue] useState(""); // 存储用户输入的问题…...