diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bae13..f4dc1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.0] - 2026-01-17 + +### Added +- 🎉 新增:GanttChart允许展示实际Taskbar +- 🎉 新增:TaskDrawer增加实际开始日期和实际结束日期的属性 +- 🎉 新增:TaskDrawer增加当前Task状态:待处理、进行中、已完成、已逾期 +- 🎉 新增:GanttChart增加Taskbar不同状态下背景色预设属性pendingTaskBackgroundColor、delayTaskBackgroundColo- pleteTaskBackgroundColor、ongoingTaskBackgroundColor +- 🎉 新增:Task对象增加barColor属性,允许自定义对应Taskbar的背景色,优先级高于GanttChart预设背景色 +- 🎉 新增:Taskbar增加气泡提示框 +- 🎉 新增:GanttChart增加showActualTaskbar属性用于控制是否展示实际Taskbar,默认值为false,需要时手动打开 +- 🎉 新增:GanttChart增加enableTaskbarTooltip属性用于控制是否展示Taskbar的气泡提示框,默认值为true,需要时手动关闭 +- 🎉 Added: GanttChart allows displaying actual Taskbar +- 🎉 Added: TaskDrawer adds properties for actual start date and actual end date +- 🎉 Added: TaskDrawer adds current Task status: Pending, Ongoing, Completed, Overdue +- 🎉 Added: GanttChart adds preset background color properties for Taskbar in different statuses: pendingTaskBackgroundColor, delayTaskBackgroundColor, completeTaskBackgroundColor, ongoingTaskBackgroundColor +- 🎉 Added: Task object adds barColor property to allow customization of the corresponding Taskbar background color, with higher priority than GanttChart preset background colors +- 🎉 Added: Taskbar adds tooltip +- 🎉 Added: GanttChart adds showActualTaskbar property to control whether to show actual Taskbar +- 🎉 Added: GanttChart adds enableTaskbarTooltip property to control whether to show Taskbar tooltip + +### Enhancement +- 🎉 优化:Task对象assignee以及avatar属性允许接收数组,标记多为负责人 +- Optimized: The assignee and avatar properties of the Task object now accept arrays to mark multiple assignees + ## [1.7.2] - 2026-01-15 ### Added diff --git a/README-EN.md b/README-EN.md index 3f01bbe..0c39835 100644 --- a/README-EN.md +++ b/README-EN.md @@ -209,7 +209,13 @@ npm run dev | `timeScale` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'week'` | Time scale (reactive). Timeline scale will follow changes | | `fullscreen` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | Fullscreen state control (reactive). Component's fullscreen state will follow changes | | `expandAll` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Expand/collapse all tasks (reactive). All tasks' expand state will follow changes | -| `enableLinkAnchor` ![v1.7.2](https://img.shields.io/badge/v1.7.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to enable Link Anchor,Default: true | +| `enableLinkAnchor` ![v1.7.2](https://img.shields.io/badge/v1.7.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to enable Link Anchor,Default: true | +| `pendingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#409eff'` | Background color for pending tasks' TaskBar. Supports hex color values (e.g., `'#409eff'`). **Priority**: Higher than system default, lower than Task object's `barColor` property | +| `delayTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#f56c6c'` | Background color for overdue tasks' TaskBar. Supports hex color values (e.g., `'#f56c6c'`). **Priority**: Higher than system default, lower than Task object's `barColor` property | +| `completeTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#909399'` | Background color for completed tasks' TaskBar. Supports hex color values (e.g., `'#909399'`). **Priority**: Higher than system default, lower than Task object's `barColor` property | +| `ongoingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#e6a23c'` | Background color for ongoing tasks' TaskBar. Supports hex color values (e.g., `'#e6a23c'`). **Priority**: Higher than system default, lower than Task object's `barColor` property | +| `showActualTaskbar` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | Whether to display actual TaskBar (shows actual execution progress below planned TaskBar) | +| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to enable TaskBar hover tooltip (shows task details on mouse hover) | #### TaskListColumn Component Props @@ -618,9 +624,9 @@ Tasks are the core elements of the Gantt chart. The component provides complete | `endDate` | `string` | - | - | End date, format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm' | | `progress` | `number` | - | `0` | Task progress, range 0-100 | | `predecessor` | `number[]` | - | - | Array of predecessor task IDs, standard format: `[1, 2, 3]`
**Compatible formats**: Also supports string `'1,2,3'` or string array `['1', '2', '3']`, component will auto-parse | -| `assignee` | `string` | - | - | Task assignee, used as the value binding for the assignee dropdown menu | -| `assigneeName` | `string` | - | - | Task assignee name, automatically obtained from the label in the bound `assigneeOptions` dataset; for custom display, you can set it in the `task-added` callback event of GanttChart | -| `avatar` | `string` | - | - | Avatar URL of task assignee | +| `assignee` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | Task assignee, used as the value binding for the assignee dropdown menu. Supports single assignee (string) or multiple assignees (string array) | +| `assigneeName` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | Task assignee name, automatically obtained from the label in the bound `assigneeOptions` dataset; for custom display, you can set it in the `task-added` callback event of GanttChart. Supports single name (string) or multiple names (string array) | +| `avatar` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | Avatar URL of task assignee. Supports single avatar (string) or multiple avatars (string array) | | `estimatedHours` | `number` | - | - | Estimated hours | | `actualHours` | `number` | - | - | Actual hours | | `parentId` | `number` | - | - | Parent task ID, used for task grouping | diff --git a/README.md b/README.md index f6b338c..f44cda0 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,13 @@ npm run dev | `timeScale` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'week'` | 时间刻度(响应式)。设置后时间线刻度将跟随变化 | | `fullscreen` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | 全屏状态控制(响应式)。设置后组件全屏状态将跟随变化 | | `expandAll` ![v1.7.1](https://img.shields.io/badge/v1.7.1-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 展开/收起所有任务(响应式)。设置后所有任务的展开状态将跟随变化 | -| `enableLinkAnchor` ![v1.7.2](https://img.shields.io/badge/v1.7.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否开启Taskbar的关系线锚点,默认值: true | +| `enableLinkAnchor` ![v1.7.2](https://img.shields.io/badge/v1.7.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否开启Taskbar的关系线锚点,默认值: true | +| `pendingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#409eff'` | 待处理任务的TaskBar背景色。支持十六进制颜色值(如 `'#409eff'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 | +| `delayTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#f56c6c'` | 已逾期任务的TaskBar背景色。支持十六进制颜色值(如 `'#f56c6c'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 | +| `completeTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#909399'` | 已完成任务的TaskBar背景色。支持十六进制颜色值(如 `'#909399'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 | +| `ongoingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#e6a23c'` | 进行中任务的TaskBar背景色。支持十六进制颜色值(如 `'#e6a23c'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 | +| `showActualTaskbar` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | 是否显示实际TaskBar(在计划TaskBar下方显示实际执行进度) | +| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) | #### TaskListColumn 属性 @@ -616,9 +622,9 @@ const propsExpandAll = ref(false) | `endDate` | `string` | - | - | 结束日期,格式:'YYYY-MM-DD' 或 'YYYY-MM-DD HH:mm' | | `progress` | `number` | - | `0` | 任务进度,范围 0-100 | | `predecessor` | `number[]` | - | - | 前置任务 ID 数组,标准格式:`[1, 2, 3]`
**兼容格式**:也支持字符串 `'1,2,3'` 或字符串数组 `['1', '2', '3']`,组件会自动解析 | -| `assignee` | `string` | - | - | 任务负责人,用作负责人下拉菜单的值绑定 | -| `assigneeName` | `string` | - | - | 任务负责人姓名,自动从绑定的数据集`assigneeOptions`中获取Label作为显示,如果需要自定义,可以在GanttChart回调事件`task-added`中自定义信息 | -| `avatar` | `string` | - | - | 任务负责人头像 URL | +| `assignee` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | 任务负责人,用作负责人下拉菜单的值绑定。支持单个负责人(字符串)或多个负责人(字符串数组) | +| `assigneeName` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | 任务负责人姓名,自动从绑定的数据集`assigneeOptions`中获取Label作为显示,如果需要自定义,可以在GanttChart回调事件`task-added`中自定义信息。支持单个姓名(字符串)或多个姓名(字符串数组) | +| `avatar` | `string` \| `string[]` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | - | - | 任务负责人头像 URL。支持单个头像(字符串)或多个头像(字符串数组) | | `estimatedHours` | `number` | - | - | 预估工时(小时) | | `actualHours` | `number` | - | - | 实际工时(小时) | | `parentId` | `number` | - | - | 父任务 ID,用于任务分组 | diff --git a/demo/App.vue b/demo/App.vue index b72cd03..9168f41 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -25,6 +25,9 @@ const { locale: demoLocale, messages: demoMessages, setLocale: setDemoLocale, fo // Tool Settings 多语言 const ts = computed(() => demoMessages.value.toolSettings || {}) +// TaskBar Config 多语言 +const taskBarConfigMessages = computed(() => demoMessages.value.taskBarConfig || {}) + // GanttChart ref const gantt = ref | null>(null) @@ -242,6 +245,13 @@ const taskBarOptions = ref({ dragDelayTime: 150, // 拖拽延迟时间(毫秒) }) +// 自定义任务状态背景色 +const showActualTaskBar = ref(true) +const pendingTaskBackgroundColor = ref('#409eff') +const delayTaskBackgroundColor = ref('#f56c6c') +const completeTaskBackgroundColor = ref('#909399') +const ongoingTaskBackgroundColor = ref('#e6a23c') + const taskBarConfig = computed(() => ({ showAvatar: taskBarOptions.value.showAvatar, showTitle: taskBarOptions.value.showTitle, @@ -1302,6 +1312,53 @@ const handleCustomMenuAction = (action: string, task: Task) => { {{ t.taskBarConfig.display.showProgress }} + + + + +
+
{{ taskBarConfigMessages.colorConfig?.title }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -1324,11 +1381,11 @@ const handleCustomMenuAction = (action: string, task: Task) => { - 允许通过拖拽TaskRow来调整任务的层级和顺序 + {{ taskBarConfigMessages.enableTaskRowMove?.hint }}
@@ -1819,12 +1876,16 @@ const handleCustomMenuAction = (action: string, task: Task) => { :allow-drag-and-resize="allowDragAndResize" :enable-task-row-move="enableTaskRowMove" :assignee-options="assigneeOptions" - :task-list-row-class-name="getTaskRowClassName" - :task-list-row-style="getTaskRowStyle" :on-export-csv="handleCustomCsvExport" :on-language-change="handleLanguageChange" :on-theme-change="handleThemeChange" :task-list-column-render-mode="taskListColumnRenderMode" + :show-actual-taskbar="showActualTaskBar" + :enable-task-bar-tooltip="true" + :pending-task-background-color="pendingTaskBackgroundColor" + :delay-task-background-color="delayTaskBackgroundColor" + :complete-task-background-color="completeTaskBackgroundColor" + :ongoing-task-background-color="ongoingTaskBackgroundColor" @milestone-saved="handleMilestoneSaved" @milestone-deleted="handleMilestoneDeleted" @milestone-icon-changed="handleMilestoneIconChanged" @@ -2528,6 +2589,62 @@ const handleCustomMenuAction = (action: string, task: Task) => { color: var(--gantt-primary-color, #409eff); } +/* 自定义颜色配置组 */ +.color-config-group { + margin-top: 16px; + padding: 12px; + background: var(--gantt-bg-tertiary, #fafafa); + border-radius: 8px; + border: 1px solid var(--gantt-border-color, #e4e7ed); +} + +.color-config-title { + font-size: 13px; + font-weight: 600; + color: var(--gantt-text-secondary, #606266); + margin: 0 0 12px 0; +} + +.color-inputs-row { + display: flex; + gap: 12px; + flex-wrap: nowrap; +} + +.color-input-item { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; +} + +.color-label { + font-size: 12px; + font-weight: 500; + color: var(--gantt-text-secondary, #606266); +} + +.color-input { + padding: 6px 10px; + font-size: 13px; + border: 1px solid var(--gantt-border-color, #dcdfe6); + border-radius: 4px; + background: var(--gantt-bg-primary, #ffffff); + color: var(--gantt-text-primary, #333); + transition: all 0.2s ease; +} + +.color-input:focus { + outline: none; + border-color: var(--gantt-primary-color, #409eff); + box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); +} + +.color-input::placeholder { + color: var(--gantt-text-placeholder, #c0c4cc); +} + /* TaskBar 高级配置样式 */ .taskbar-advanced-controls { display: flex; diff --git a/demo/data.json b/demo/data.json index 555878f..c04c5bd 100644 --- a/demo/data.json +++ b/demo/data.json @@ -27,21 +27,25 @@ "description": "评估ADX-2024在健康志愿者和患者中的安全性、耐受性和药代动力学特征", "custom": "test11111", "priority": "高", + "barColor": "#7B3FF2", "children": [ { "id": 1101, "name": "试验方案设计与伦理审查", "assignee": "方案设计师 李明", - "avatar": "50-1.jpg", + "avatar": ["50-1.jpg", "50-2.jpg"], "startDate": "2025-01-01", "endDate": "2025-02-28", + "actualStartDate": "2025-01-03", + "actualEndDate": "2025-02-25", "progress": 100, "estimatedHours": 464, "actualHours": 480, "type": "task", "parentId": 1100, "description": "完成I期试验方案设计、伦理委员会审批及监管部门申报", - "custom": "test11111" + "custom": "test11111", + "barColor": "#2E7D6B" }, { "id": 1102, @@ -50,6 +54,8 @@ "avatar": "50-2.jpg", "startDate": "2025-03-01", "endDate": "2025-04-30", + "actualStartDate": "2025-03-05", + "actualEndDate": "2025-04-28", "progress": 90, "estimatedHours": 1392.5, "actualHours": 1250.25, @@ -57,15 +63,17 @@ "predecessor": [1101], "parentId": 1100, "description": "招募24名健康志愿者,完成医学筛选和入组评估", - "custom": "test11111" + "custom": "test11111", + "barColor": "#FF4D4F" }, { "id": 1103, "name": "药物给药与安全性监测", "assignee": "临床医生 Dr. Liu", - "avatar": "50-3.jpg", + "avatar": ["50-3.jpg", "50-2.jpg"], "startDate": "2025-05-01", "endDate": "2025-08-31", + "actualStartDate": "2025-05-10", "progress": 40, "estimatedHours": 3400, "actualHours": 1687, @@ -73,7 +81,8 @@ "predecessor": [1102], "parentId": 1100, "description": "按照剂量递增方案给药,密切监测不良反应和药代动力学参数", - "custom": "test11111" + "custom": "test11111", + "barColor": "#7B3FF2" } ], "collapsed": false @@ -91,14 +100,16 @@ "parentId": 1000, "description": "在目标患者群体中评估ADX-2024的初步疗效和进一步的安全性", "custom": "test11111", + "barColor": "#3F6DF6", "children": [ { "id": 1201, "name": "多中心试验启动", "assignee": "项目经理 王芳", - "avatar": "50-10.jpg", + "avatar": ["50-10.jpg", "50-2.jpg"], "startDate": "2025-09-01", "endDate": "2025-11-30", + "actualStartDate": "2025-09-01", "progress": 25, "estimatedHours": 2160, "actualHours": 324, @@ -106,13 +117,14 @@ "predecessor": [1103], "parentId": 1200, "description": "启动5个临床中心,完成研究者培训和质量体系建立", - "custom": "test11111" + "custom": "test11111", + "barColor": "#3F6DF6" }, { "id": 1202, "name": "患者入组与随机化", "assignee": "数据管理员 陈静", - "avatar": "50-6.jpg", + "avatar": ["50-6.jpg", "50-10.jpg"], "startDate": "2025-12-01", "endDate": "2026-03-31", "progress": 0, @@ -138,7 +150,8 @@ "predecessor": [1202], "parentId": 1200, "description": "定期评估患者疗效指标,收集安全性数据,进行中期分析", - "custom": "test11111" + "custom": "test11111", + "barColor": "#FFA43A" } ], "collapsed": false diff --git a/demo/locales/en-US.json b/demo/locales/en-US.json index 5847606..3499556 100644 --- a/demo/locales/en-US.json +++ b/demo/locales/en-US.json @@ -111,5 +111,21 @@ "false": "False" } } + }, + "taskBarConfig": { + "display": { + "showActualTaskBar": "Show Actual TaskBar" + }, + "colorConfig": { + "title": "Custom Task Status Background Colors", + "pendingTask": "Pending Task", + "delayTask": "Overdue Task", + "completeTask": "Completed Task", + "ongoingTask": "Ongoing Task" + }, + "enableTaskRowMove": { + "label": "Enable TaskRow Drag and Drop", + "hint": "Allow dragging TaskRow to adjust task hierarchy and order" + } } } diff --git a/demo/locales/zh-CN.json b/demo/locales/zh-CN.json index a1dec5f..221d2e0 100644 --- a/demo/locales/zh-CN.json +++ b/demo/locales/zh-CN.json @@ -111,5 +111,21 @@ "false": "假" } } + }, + "taskBarConfig": { + "display": { + "showActualTaskBar": "显示实际TaskBar" + }, + "colorConfig": { + "title": "自定义任务状态背景色", + "pendingTask": "待处理任务", + "delayTask": "逾期任务", + "completeTask": "已完成任务", + "ongoingTask": "进行中任务" + }, + "enableTaskRowMove": { + "label": "启用TaskRow拖拽移动", + "hint": "允许通过拖拽TaskRow来调整任务的层级和顺序" + } } } diff --git a/demo/useDemoLocale.ts b/demo/useDemoLocale.ts index 2a75c72..c5e6d44 100644 --- a/demo/useDemoLocale.ts +++ b/demo/useDemoLocale.ts @@ -27,11 +27,114 @@ interface LocaleMessages { task: string } } + taskBarConfig: { + display: { + showActualTaskBar: string + } + colorConfig: { + title: string + pendingTask: string + delayTask: string + completeTask: string + ongoingTask: string + } + enableTaskRowMove: { + label: string + hint: string + } + } + toolSettings: { + title: string + currentStatus: { + title: string + fullscreen: string + expandAll: string + locale: string + timeScale: string + theme: string + controlMode: string + active: string + inactive: string + expanded: string + collapsed: string + } + controlMode: { + title: string + expose: string + props: string + exposeHint: string + propsHint: string + } + exposeMethods: { + sectionTitle: string + fullscreen: { + title: string + enter: string + exit: string + toggle: string + } + expand: { + title: string + all: string + none: string + toggle: string + } + timeScale: { + title: string + hour: string + day: string + week: string + month: string + quarter: string + year: string + zoomIn: string + zoomOut: string + } + locale: { + title: string + zhCN: string + enUS: string + } + theme: { + title: string + light: string + dark: string + } + navigation: { + title: string + today: string + taskIdPlaceholder: string + go: string + } + } + propsControl: { + sectionTitle: string + locale: { + title: string + } + theme: { + title: string + } + timeScale: { + title: string + } + fullscreen: { + title: string + true: string + false: string + } + expandAll: { + title: string + true: string + false: string + } + } + } } const localeMessages: Record = { - 'zh-CN': zhCN, - 'en-US': enUS, + 'zh-CN': zhCN as any, + 'en-US': enUS as any, } const currentLocale = ref('zh-CN') diff --git a/demo/version-history.json b/demo/version-history.json index bca4200..6d36d38 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -466,5 +466,31 @@ "🎉 Added: New property in GanttChart - enableLinkAnchor to control the relationship line anchor points of Taskbar", "Special thanks to YQ6494@gitee for their use and suggestions" ] + }, + { + "version": "1.8.0", + "date": "2026-01-17", + "notes": [ + "🎉 新增:GanttChart允许展示实际Taskbar", + "🎉 新增:TaskDrawer增加实际开始日期和实际结束日期的属性", + "🎉 新增:TaskDrawer增加当前Task状态:待处理、进行中、已完成、已逾期", + "🎉 新增:GanttChart增加Taskbar不同状态下背景色预设属性pendingTaskBackgroundColor、delayTaskBackgroundColor、completeTaskBackgroundColor、ongoingTaskBackgroundColor", + "🎉 新增:Task对象增加barColor属性,允许自定义对应Taskbar的背景色,优先级高于GanttChart预设背景色", + "🎉 新增:Taskbar增加气泡提示框", + "🎉 新增:GanttChart增加showActualTaskbar属性用于控制是否展示实际Taskbar,默认值为false,需要时手动打开", + "🎉 新增:GanttChart增加enableTaskbarTooltip属性用于控制是否展示Taskbar的气泡提示框,默认值为true,需要时手动关闭", + "🎉 优化:Task对象assignee以及avatar属性允许接收数组,标记多为负责人", + "特别感谢 yunlbs/fhjfhj/jakepan@gitee的使用及建议", + "🎉 Added: GanttChart allows displaying actual Taskbar", + "🎉 Added: TaskDrawer adds properties for actual start date and actual end date", + "🎉 Added: TaskDrawer adds current Task status: Pending, Ongoing, Completed, Overdue", + "🎉 Added: GanttChart adds preset background color properties for Taskbar in different statuses: pendingTaskBackgroundColor, delayTaskBackgroundColor, completeTaskBackgroundColor, ongoingTaskBackgroundColor", + "🎉 Added: Task object adds barColor property to allow customization of the corresponding Taskbar background color, with higher priority than GanttChart preset background colors", + "🎉 Added: Taskbar adds tooltip", + "🎉 Added: GanttChart adds showActualTaskbar property to control whether to show actual Taskbar", + "🎉 Added: GanttChart adds enableTaskbarTooltip property to control whether to show Taskbar tooltip", + "🎉 Optimized: The assignee and avatar properties of the Task object now accept arrays to mark multiple assignees", + "Special thanks to yunlbs/fhjfhj/jakepan@gitee for their use and suggestions" + ] } ] diff --git a/design/screenshots/dark-theme-en.png b/design/screenshots/dark-theme-en.png index 8835750..4f42a6c 100644 Binary files a/design/screenshots/dark-theme-en.png and b/design/screenshots/dark-theme-en.png differ diff --git a/design/screenshots/dark-theme.png b/design/screenshots/dark-theme.png index 2b979e2..7213541 100644 Binary files a/design/screenshots/dark-theme.png and b/design/screenshots/dark-theme.png differ diff --git a/design/screenshots/light-theme-en.png b/design/screenshots/light-theme-en.png index 2711144..05cd52e 100644 Binary files a/design/screenshots/light-theme-en.png and b/design/screenshots/light-theme-en.png differ diff --git a/design/screenshots/light-theme.png b/design/screenshots/light-theme.png index 1e36542..278b7f3 100644 Binary files a/design/screenshots/light-theme.png and b/design/screenshots/light-theme.png differ diff --git a/package.json b/package.json index 7c7cc0c..e3e3e0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jordium-gantt-vue3", - "version": "1.7.2", + "version": "1.8.0", "type": "module", "main": "npm-package/dist/jordium-gantt-vue3.cjs.js", "module": "npm-package/dist/jordium-gantt-vue3.es.js", diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index 8f08849..181ef02 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -59,6 +59,8 @@ const props = withDefaults(defineProps(), { enableTaskListContextMenu: true, enableTaskBarContextMenu: true, enableLinkAnchor: true, + showActualTaskbar: false, + enableTaskBarTooltip: true, fullscreen: false, expandAll: true, locale: 'zh-CN', @@ -201,6 +203,21 @@ interface Props { // 是否启用 LinkAnchor 连接触点功能(默认为 true) // 当设置为 false 时,TaskBar 上不显示前置/后置任务的连接触点 enableLinkAnchor?: boolean + // 是否启用 TaskBar 气泡提示框(默认为 true) + // 当设置为 false 时,TaskBar 上不显示悬停气泡提示框 + enableTaskBarTooltip?: boolean + // 是否显示实际任务条(默认为 false) + // 当设置为 true 且任务存在 actualStartDate 时,会在计划任务条下方显示实际任务条 + showActualTaskbar?: boolean + // 自定义任务状态背景色(优先级高于默认配色,低于Task.barColor) + // 待处理任务背景色:任务未开始且未逾期时使用 + pendingTaskBackgroundColor?: string + // 逾期任务背景色:当前日期晚于任务结束日期时使用 + delayTaskBackgroundColor?: string + // 已完成任务背景色:任务进度达到100%时使用 + completeTaskBackgroundColor?: string + // 进行中任务背景色:任务已开始但未完成且未逾期时使用 + ongoingTaskBackgroundColor?: string // 全屏状态控制(响应式) fullscreen?: boolean // 展开/收起所有任务(响应式) @@ -1201,6 +1218,8 @@ const milestonesForTimeline = computed((): Milestone[] => { startDate: task.startDate, // 此时已确保非空 endDate: task.endDate, assignee: task.assignee, + assigneeName: task.assigneeName, + avatar: task.avatar, type: task.type || 'milestone', icon: task.icon, description: task.description, @@ -2808,6 +2827,12 @@ defineExpose({ :working-hours="props.workingHours" :task-bar-config="props.taskBarConfig" :allow-drag-and-resize="props.allowDragAndResize" + :show-actual-taskbar="props.showActualTaskbar" + :enable-task-bar-tooltip="props.enableTaskBarTooltip" + :pending-task-background-color="props.pendingTaskBackgroundColor" + :delay-task-background-color="props.delayTaskBackgroundColor" + :complete-task-background-color="props.completeTaskBackgroundColor" + :ongoing-task-background-color="props.ongoingTaskBackgroundColor" :use-default-drawer="props.useDefaultDrawer" :use-default-milestone-dialog="props.useDefaultMilestoneDialog" :on-milestone-save="handleMilestoneSave" @@ -2861,6 +2886,10 @@ defineExpose({ :task="taskDrawerTask" :is-edit="taskDrawerEditMode" :assignee-options="props.assigneeOptions" + :pending-task-background-color="props.pendingTaskBackgroundColor" + :delay-task-background-color="props.delayTaskBackgroundColor" + :complete-task-background-color="props.completeTaskBackgroundColor" + :ongoing-task-background-color="props.ongoingTaskBackgroundColor" @submit="handleTaskDrawerSubmit" @close="taskDrawerVisible = false" @start-timer="handleStartTimer" diff --git a/src/components/GanttLinks.vue b/src/components/GanttLinks.vue index 94266c7..56064b1 100644 --- a/src/components/GanttLinks.vue +++ b/src/components/GanttLinks.vue @@ -509,7 +509,7 @@ defineExpose({ top: `${offsetTop}px`, width: `${width}px`, height: `${height}px`, - zIndex: highlightedTaskId !== null ? 1001 : 25, + zIndex: highlightedTaskId !== null ? 2000 : 500, pointerEvents: 'none', }" /> diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue index 412480a..f1fc999 100644 --- a/src/components/TaskBar.vue +++ b/src/components/TaskBar.vue @@ -34,6 +34,15 @@ interface Props { taskBarConfig?: TaskBarConfig // 是否允许拖拽和拉伸(默认为 true) allowDragAndResize?: boolean + // 是否显示实际任务条(默认为 false) + showActualTaskbar?: boolean + // 是否启用 TaskBar 气泡提示框(默认为 true) + enableTaskBarTooltip?: boolean + // 自定义任务状态背景色 + pendingTaskBackgroundColor?: string + delayTaskBackgroundColor?: string + completeTaskBackgroundColor?: string + ongoingTaskBackgroundColor?: string // 是否被高亮显示(前置或后置任务) isHighlighted?: boolean // 是否是主要高亮(被长按的任务) @@ -528,6 +537,45 @@ const shouldRenderTaskBar = computed(() => { // 计算任务状态和颜色 const taskStatus = computed(() => { + // 优先级最高:如果task设置了barColor自定义颜色 + if (props.task.barColor) { + // 将十六进制颜色转换为RGB + const hexToRgb = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { r: 64, g: 158, b: 255 } // 默认蓝色 + } + + const rgb = hexToRgb(props.task.barColor) + + // 父级任务直接使用自定义颜色作为主色 + if (props.isParent) { + return { + type: 'custom-parent', + color: props.task.barColor, + bgColor: props.task.barColor, + borderColor: props.task.barColor, + } + } + + // 普通任务生成浅色背景和中间色边框 + // 生成浅色背景(接近白色,保持色调)- 约95%白色 + 5%原色 + const bgColor = `rgb(${Math.round(255 * 0.95 + rgb.r * 0.05)}, ${Math.round(255 * 0.95 + rgb.g * 0.05)}, ${Math.round(255 * 0.95 + rgb.b * 0.05)})` + + // 生成中间色边框(约70%白色 + 30%原色) + const borderColor = `rgb(${Math.round(255 * 0.7 + rgb.r * 0.3)}, ${Math.round(255 * 0.7 + rgb.g * 0.3)}, ${Math.round(255 * 0.7 + rgb.b * 0.3)})` + + return { + type: 'custom', + color: props.task.barColor, + bgColor: bgColor, + borderColor: borderColor, + } + } + // 父级任务(Story类型)使用与新建按钮一致的配色 if (props.isParent) { return { @@ -538,45 +586,81 @@ const taskStatus = computed(() => { } } + // 辅助函数:根据主色生成浅色背景和中间色边框 + const generateColorsFromMain = (mainColor: string) => { + const hexToRgb = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { r: 64, g: 158, b: 255 } + } + const rgb = hexToRgb(mainColor) + const bgColor = `rgb(${Math.round(255 * 0.95 + rgb.r * 0.05)}, ${Math.round(255 * 0.95 + rgb.g * 0.05)}, ${Math.round(255 * 0.95 + rgb.b * 0.05)})` + const borderColor = `rgb(${Math.round(255 * 0.7 + rgb.r * 0.3)}, ${Math.round(255 * 0.7 + rgb.g * 0.3)}, ${Math.round(255 * 0.7 + rgb.b * 0.3)})` + return { bgColor, borderColor } + } + // 使用缓存的今天日期,避免频繁创建日期对象 const today = cachedToday.value const endDate = parsedEndDate.value const progress = props.task.progress || 0 + const actualStartDate = props.task.actualStartDate ? new Date(props.task.actualStartDate) : null - // 已完成 + // 已完成(优先级高) if (progress >= 100) { + // 优先使用自定义颜色,否则使用默认 + const mainColor = props.completeTaskBackgroundColor || '#909399' + const colors = generateColorsFromMain(mainColor) return { type: 'completed', - color: '#909399', // info color - bgColor: '#f4f4f5', - borderColor: '#d3d4d6', + color: mainColor, + bgColor: colors.bgColor, + borderColor: colors.borderColor, } } // 已延迟(结束日期早于今天且未完成) if (endDate && endDate < today && progress < 100) { + const mainColor = props.delayTaskBackgroundColor || '#f56c6c' + const colors = generateColorsFromMain(mainColor) return { type: 'delayed', - color: '#f56c6c', // danger color - bgColor: '#fef0f0', - borderColor: '#fbc4c4', + color: mainColor, + bgColor: colors.bgColor, + borderColor: colors.borderColor, } } - // 进行中(结束日期晚于今天且进度>0) - if (endDate && endDate >= today && progress > 0) { + // 进行中(actualStartDate存在,未完成,且未逾期) + if (actualStartDate && progress < 100 && endDate && endDate >= today) { + const mainColor = props.ongoingTaskBackgroundColor || '#e6a23c' + const colors = generateColorsFromMain(mainColor) return { - type: 'in-progress', - color: '#e6a23c', // warning color - bgColor: '#fdf6ec', - borderColor: '#f5dab1', + type: 'ongoing', + color: mainColor, + bgColor: colors.bgColor, + borderColor: colors.borderColor, } } - // 未开始(进度为0且未延迟) + // 待处理(未开始且未逾期)- actualStartDate不存在,当前日期早于计划结束日 + if (!actualStartDate && (!endDate || endDate >= today)) { + const mainColor = props.pendingTaskBackgroundColor || '#409eff' + const colors = generateColorsFromMain(mainColor) + return { + type: 'pending', + color: mainColor, + bgColor: colors.bgColor, + borderColor: colors.borderColor, + } + } + + // 默认状态(兜底) return { - type: 'not-started', - color: '#409eff', // primary color + type: 'default', + color: '#409eff', bgColor: '#ecf5ff', borderColor: '#b3d8ff', } @@ -595,6 +679,26 @@ const slotPayload = computed(() => ({ dayWidth: props.dayWidth, })) +// 处理avatar数组和assignee生成头像 +const avatarList = computed(() => { + const avatar = props.task.avatar + const assignee = props.task.assignee + + // 如果有avatar,使用avatar + if (avatar) { + return Array.isArray(avatar) ? avatar : [avatar] + } + + // 如果没有avatar但有assignee,使用assignee生成头像数据 + if (assignee) { + const assigneeList = Array.isArray(assignee) ? assignee : [assignee] + // 返回对象数组,包含名字信息用于生成文字头像 + return assigneeList.map(name => ({ isText: true, name })) + } + + return [] +}) + // 判断是否已完成 const isCompleted = computed(() => (props.task.progress || 0) >= 100) @@ -619,6 +723,128 @@ const isShortTaskBar = computed(() => { return width < 80 }) +// 判断是否有实际进度数据 +const hasActualProgress = computed(() => { + return !!(props.task.actualStartDate || props.task.actualEndDate) +}) + +// 计算实际进度条的样式(独立的TaskBar,在下层) +const actualBarStyle = computed(() => { + // 只有当showActualTaskbar=true且存在actualStartDate时才显示实际任务条 + if (!props.showActualTaskbar || !props.task.actualStartDate || props.isParent) { + return null + } + + const actualStart = createLocalDate(props.task.actualStartDate) + const actualEnd = createLocalDate(props.task.actualEndDate) + const planStart = createLocalDate(props.task.startDate) + const baseStartOnly = parsedBaseStartDate.value + + if (!baseStartOnly) { + return null + } + + // 实际开始日期,如果没有则使用计划开始日期 + const effectiveStart = actualStart || planStart + // 实际结束日期,如果没有则使用当前日期(任务进行中) + const effectiveEnd = actualEnd || (actualStart ? createLocalToday() : null) + + if (!effectiveStart || !effectiveEnd) { + return null + } + + // 计算实际进度条的绝对位置(与计划条使用相同逻辑) + let actualLeft = 0 + let actualWidth = 100 + + // 根据时间刻度计算位置(与taskBarStyle逻辑一致) + if ( + props.timelineData && + props.currentTimeScale && + (props.currentTimeScale === TimelineScale.WEEK || + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) + ) { + const startPosition = calculatePositionFromTimelineData( + effectiveStart, + props.timelineData, + props.currentTimeScale, + ) + const nextDay = new Date(effectiveEnd) + nextDay.setDate(nextDay.getDate() + 1) + let endPosition = calculatePositionFromTimelineData( + nextDay, + props.timelineData, + props.currentTimeScale, + ) + + if (endPosition === startPosition) { + let dayWidth = 60 / 30 + if (props.currentTimeScale === TimelineScale.WEEK) { + dayWidth = 60 / 7 + } else if (props.currentTimeScale === TimelineScale.QUARTER) { + dayWidth = 60 / 90 + } else if (props.currentTimeScale === TimelineScale.YEAR) { + dayWidth = 180 / 182 + } + endPosition = calculatePositionFromTimelineData( + effectiveEnd, + props.timelineData, + props.currentTimeScale, + ) + dayWidth + } + + actualLeft = startPosition + actualWidth = Math.max(endPosition - startPosition, 4) + } else if (props.timelineData && props.currentTimeScale === TimelineScale.DAY) { + const startPosition = calculatePositionFromTimelineData( + effectiveStart, + props.timelineData, + props.currentTimeScale, + ) + const nextDay = new Date(effectiveEnd) + nextDay.setDate(nextDay.getDate() + 1) + let endPosition = calculatePositionFromTimelineData( + nextDay, + props.timelineData, + props.currentTimeScale, + ) + + if (endPosition === startPosition) { + endPosition = calculatePositionFromTimelineData( + effectiveEnd, + props.timelineData, + props.currentTimeScale, + ) + 30 + } + + actualLeft = startPosition + actualWidth = Math.max(endPosition - startPosition, 4) + } else { + const startDiff = Math.floor( + (effectiveStart.getTime() - baseStartOnly.getTime()) / (1000 * 60 * 60 * 24), + ) + const timeDiffMs = effectiveEnd.getTime() - effectiveStart.getTime() + const daysDiff = Math.round(timeDiffMs / (1000 * 60 * 60 * 24)) + const duration = daysDiff === 0 ? 1 : daysDiff + 1 + + actualLeft = startDiff * props.dayWidth + actualWidth = duration * props.dayWidth + } + + // 实际进度条固定高度20px,垂直居中显示 + const actualHeight = 20 + const topOffset = (props.rowHeight - actualHeight) / 2 // 上下居中对齐 + + return { + left: `${actualLeft}px`, + width: `${actualWidth}px`, + height: `${actualHeight}px`, + top: `${topOffset}px`, + } +}) + // 判断是否需要溢出效果(周视图且短TaskBar) const needsOverflowEffect = computed(() => isWeekView.value && isShortTaskBar.value) @@ -1524,13 +1750,18 @@ const stickyStyles = computed(() => { // 估算文字内容的实际位置 const nameWidth = Math.max(nameTextWidth.value, 40) // 最小40px const progressWidth = 35 - const avatarWidth = 22 // avatar 宽度 + const singleAvatarWidth = 22 // 单个avatar 宽度 + // 计算实际avatar总宽度(多个头像时会重叠,每个头像露出18px) + const actualAvatarCount = avatarList.value.length + const avatarTotalWidth = actualAvatarCount > 0 + ? (actualAvatarCount === 1 ? singleAvatarWidth : singleAvatarWidth + (actualAvatarCount - 1) * 18) + : 0 const handleWidth = actualHandleWidth.value // 拉伸手柄宽度 // === 第一步:检测 Avatar 是否需要粘性定位 === const avatarDefaultLeft = handleWidth + 3 // 手柄宽度 + 3px 间距 const avatarLeftPos = taskLeft + avatarDefaultLeft - const avatarRightPos = taskLeft + avatarDefaultLeft + avatarWidth + const avatarRightPos = taskLeft + avatarDefaultLeft + avatarTotalWidth // Avatar 左侧粘性逻辑 const avatarNeedsLeftSticky = @@ -1548,17 +1779,17 @@ const stickyStyles = computed(() => { const offset = leftBoundary - taskLeft avatarLeft = `${offset + handleWidth + 3}px` // 手柄宽度 + 3px 间距 avatarPosition = 'absolute' - avatarStickyOffset = avatarWidth + 8 // avatar 宽度 + 右侧间距 + avatarStickyOffset = avatarTotalWidth + 8 // avatar 总宽度 + 右侧间距 } else if (avatarNeedsRightSticky) { // avatar 应该停靠在 name/progress 左侧 15px 的位置 // 计算 name/progress 在右边界时的位置(它们会贴在右边框上) const maxContentWidth = Math.max(nameWidth, progressWidth) // 内容贴右边框时的左侧位置,考虑手柄宽度 const contentRightPos = rightBoundary - taskLeft - maxContentWidth - handleWidth - 3 - const offset = contentRightPos - avatarWidth - 15 // avatar 在内容左侧 15px + const offset = contentRightPos - avatarTotalWidth - 15 // avatar 在内容左侧 15px avatarLeft = `${offset}px` avatarPosition = 'absolute' - avatarStickyOffset = -(avatarWidth + 15) // 负值表示在右侧,avatar宽度 + 15px间距 + avatarStickyOffset = -(avatarTotalWidth + 15) // 负值表示在右侧,avatar总宽度 + 15px间距 } // === 第二步:处理名称粘性定位(考虑 avatar 偏移) === @@ -1574,7 +1805,7 @@ const stickyStyles = computed(() => { // 如果 avatar 存在:当 name 左侧接近左边界(预留 avatar+间距 的空间)时触发 // name 默认居中,当它向左移动到需要为 avatar 留出空间时触发磁吸 const nameActualLeft = nameLeftPos // name 实际左侧位置 - const avatarReservedSpace = avatarWidth + 15 // avatar 需要的空间 + const avatarReservedSpace = avatarTotalWidth + 15 // avatar 需要的空间(实际总宽度 + 间距) nameNeedsLeftSticky = nameActualLeft < leftBoundary + avatarReservedSpace && taskRight > leftBoundary } else { @@ -1589,6 +1820,7 @@ const stickyStyles = computed(() => { if (nameNeedsLeftSticky) { const offset = leftBoundary - taskLeft // 如果 avatar 也在左侧粘性,则 title 需要在 avatar 右侧 + // 使用avatarStickyOffset,已经包含avatar总宽度 + 8px间距 const extraOffset = avatarNeedsLeftSticky ? avatarStickyOffset : 0 nameLeft = `${offset + handleWidth + 3 + extraOffset}px` // 考虑手柄宽度 + 间距 namePosition = 'absolute' @@ -1611,7 +1843,7 @@ const stickyStyles = computed(() => { // 如果 avatar 存在:当 progress 左侧接近左边界(预留 avatar+间距 的空间)时触发 // progress 默认居中,当它向左移动到需要为 avatar 留出空间时触发磁吸 const progressActualLeft = progressLeftPos // progress 实际左侧位置 - const avatarReservedSpace = avatarWidth + 15 // avatar 需要的空间 + const avatarReservedSpace = avatarTotalWidth + 15 // avatar 需要的空间(实际总宽度 + 间距) progressNeedsLeftSticky = progressActualLeft < leftBoundary + avatarReservedSpace && taskRight > leftBoundary } else { @@ -1626,6 +1858,7 @@ const stickyStyles = computed(() => { if (progressNeedsLeftSticky) { const offset = leftBoundary - taskLeft // 如果 avatar 也在左侧粘性,则进度需要在 avatar 右侧 + // 使用avatarStickyOffset,已经包含avatar总宽度 + 8px间距 const extraOffset = avatarNeedsLeftSticky ? avatarStickyOffset : 0 progressLeft = `${offset + handleWidth + 3 + extraOffset}px` // 考虑手柄宽度 + 间距 progressPosition = 'absolute' @@ -1723,6 +1956,11 @@ const bubbleIndicator = computed(() => { const showTooltip = ref(false) const tooltipPosition = ref({ x: 0, y: 0 }) +// TaskBar 悬停 tooltip 状态 +const showHoverTooltip = ref(false) +const hoverTooltipPosition = ref({ x: 0, y: 0 }) +let hoverTooltipTimer: number | null = null + // 跟踪滚动状态,避免非滚动时的动画 const isScrollingContext = ref(false) const scrollTimeout = ref(null) @@ -1915,6 +2153,38 @@ const handleBubbleMouseDown = (event: MouseEvent) => { showTooltip.value = false } +// 处理TaskBar悬停事件 +const handleTaskBarMouseEnter = (event: MouseEvent) => { + isTaskBarHovered.value = true + + // 如果启用了TaskBar Tooltip(父级任务也显示tooltip) + if (props.enableTaskBarTooltip !== false) { + // 保存event.currentTarget的引用,因为在setTimeout回调中它会变成null + const targetElement = event.currentTarget as HTMLElement + + // 延迟显示tooltip,避免快速滑过时显示 + hoverTooltipTimer = window.setTimeout(() => { + showHoverTooltip.value = true + const rect = targetElement.getBoundingClientRect() + hoverTooltipPosition.value = { + x: rect.left + rect.width / 2, + y: rect.top - 10, + } + }, 300) // 300ms延迟 + } +} + +const handleTaskBarMouseLeave = () => { + isTaskBarHovered.value = false + + // 清除定时器并隐藏tooltip + if (hoverTooltipTimer) { + clearTimeout(hoverTooltipTimer) + hoverTooltipTimer = null + } + showHoverTooltip.value = false +} + // 格式化日期显示 const formatDisplayDate = (dateStr: string | undefined): string => { if (!dateStr) return t('dateNotSet') @@ -2404,6 +2674,71 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'