From 430015e3fe7c6bcbb8ebbf2da368fb3e35752931 Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Sat, 7 Feb 2026 18:44:23 +0800 Subject: [PATCH] v1.9.0-rc.8 - SIT finished --- CHANGELOG.md | 40 +- demo/App.vue | 184 ++-- demo/VersionHistoryDrawer.vue | 34 +- demo/version-history.json | 38 + npm-demo/src/components/GanttTest.vue | 509 +++++------ npm-webpack-demo/src/App.vue | 818 ++++++++++++------ package-lock.json | 31 +- package.json | 2 +- src/components/GanttChart.vue | 53 +- src/components/GanttToolbar.vue | 14 +- src/components/TaskBar.vue | 30 +- src/components/TaskDrawer.vue | 10 +- src/components/TaskList/TaskList.vue | 18 +- .../composables/taskList/useTaskListLayout.ts | 2 +- .../composables/taskList/useTaskListResize.ts | 47 +- src/components/TaskList/taskRow/TaskRow.vue | 220 +++-- src/components/Timeline.vue | 270 ++---- src/components/Timeline/GanttConflicts.vue | 37 +- src/components/Timeline/TaskBarTab.vue | 6 + src/composables/useResourceLayout.ts | 211 +++++ src/composables/useViewMode.ts | 76 ++ src/index.ts | 2 +- src/models/{types => classes}/Resource.ts | 0 src/models/classes/Task.ts | 2 +- src/models/configs/ResourceListConfig.ts | 2 +- src/utils/conflictUtils.ts | 6 +- src/utils/perfMonitor.ts | 6 - src/utils/resourceUtils.ts | 2 +- 28 files changed, 1676 insertions(+), 994 deletions(-) create mode 100644 src/composables/useResourceLayout.ts create mode 100644 src/composables/useViewMode.ts rename src/models/{types => classes}/Resource.ts (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9f972..bf23f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,45 @@ 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-02-05 +## [1.9.0] - 2026-02-07 + +### Added +- 🎉 新增:GanttChart支持资源视图模式,通过viewMode属性切换任务视图和资源视图 +- 🎉 新增:资源视图以资源为行维度展示,支持一行多任务条布局 +- 🎉 新增:Task对象增加resources字段,支持配置每个资源的投入占比(10%-100%) +- 🎉 新增:TaskBar高度按资源占比动态缩放,支持占比文字标注和Tooltip显示 +- 🎉 新增:基于占比累计的资源超负荷检测和视觉预警 +- 🎉 新增:TaskDrawer增加资源占比配置控件,支持输入校验 +- 🎉 新增:资源视图支持任务拖拽、拉伸、计时等交互操作 +- 🎉 新增:资源列表支持声明式组件配置列 +- 🎉 新增:Resource数据模型类和useResourceLayout、useViewMode等composables +- 🎉 Added: GanttChart supports resource view mode, switch via viewMode property +- 🎉 Added: Resource view displays by resource dimension with multiple task bars per row +- 🎉 Added: Task object adds resources field for configuring resource allocation percentage (10%-100%) +- 🎉 Added: TaskBar height scales by percentage with text label and Tooltip support +- 🎉 Added: Resource overload detection and visual alerts based on percentage accumulation +- 🎉 Added: TaskDrawer adds resource percentage configuration with input validation +- 🎉 Added: Resource view supports task drag, resize, timing and other interactions +- 🎉 Added: Resource list supports declarative component column configuration +- 🎉 Added: Resource model class and composables including useResourceLayout, useViewMode + +### Enhancement +- 🎉 优化:抽取资源布局计算逻辑到独立composable,避免组件职责膨胀 +- 🎉 优化:扩展v-memo优化覆盖范围,提升渲染性能 +- 🎉 优化:重构provide/inject依赖传递,简化组件依赖关系 +- 🎉 优化:恢复滚动防抖机制,平衡响应性和性能 +- 🎉 优化:历史数据向后兼容,未配置占比时默认为100% +- 🎉 Optimized: Extracted resource layout logic to independent composable +- 🎉 Optimized: Extended v-memo optimization coverage for better performance +- 🎉 Optimized: Refactored provide/inject dependency passing +- 🎉 Optimized: Restored scroll debounce mechanism +- 🎉 Optimized: Backward compatible with historical data, defaults to 100% + +### Fixed +- 🔧 修复: 将Theme设置从HTML root变更到Gantt Component root +- 🔧 Fixed: Changed Theme setting from HTML root to Gantt Component root + +## [1.8.1] - 2026-02-05 ### Enhancement - 🎉 优化:重构组件,Theme作用域从全局调整至局部.gantt-root diff --git a/demo/App.vue b/demo/App.vue index 5e55512..6fa81d7 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -17,7 +17,7 @@ import { useI18n } from '../src/composables/useI18n' import { useDemoLocale } from './useDemoLocale' import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import type { Task } from '../src/models/Task' -import type { Resource } from '../src/models/types/Resource' +import type { Resource } from '../src/models/classes/Resource' import { createResource, addTaskToResource, updateResourceUtilization } from '../src/utils/resourceUtils' import type { TaskListConfig, TaskListColumnConfig } from '../src/models/configs/TaskListConfig' import type { ResourceListConfig } from '../src/models/configs/ResourceListConfig' @@ -626,7 +626,7 @@ const handleTaskClick = (task: Task) => { } // v1.9.7 处理任务双击事件(资源视图下显示资源编辑提示) -const handleTaskDoubleClick = (taskOrResource: Task | Resource) => { +const handleTaskDoubleClick = (taskOrResource: Task | Resource) => { // 使用类型守卫严格判断是否为Resource对象 if (viewMode.value === 'resource' && isResource(taskOrResource)) { // 这是Resource对象,显示资源编辑提示 @@ -635,7 +635,7 @@ const handleTaskDoubleClick = (taskOrResource: Task | Resource) => { resourceEditHintVisible.value = true return } - + // 对于真正的Task对象,GanttChart会正常打开TaskDrawer // useDefaultDrawer保持为true,确保新建任务和TaskBar双击都能正常工作 } @@ -643,9 +643,9 @@ const handleTaskDoubleClick = (taskOrResource: Task | Resource) => { // v1.9.7 类型守卫:判断是否为Resource对象 // Resource独有的特征:有tasks数组属性,且没有resources属性 const isResource = (obj: Task | Resource): obj is Resource => { - return obj && - typeof obj === 'object' && - 'tasks' in obj && + return obj && + typeof obj === 'object' && + 'tasks' in obj && Array.isArray((obj as Resource).tasks) && !('resources' in obj) // Task有resources属性,Resource没有 } @@ -1169,7 +1169,7 @@ const confirmResourceDrag = () => { task.resources.push({ id: targetResource.id, name: targetResource.name, - capacity: oldCapacity + capacity: oldCapacity, }) } } @@ -3453,196 +3453,196 @@ const handleCustomMenuAction = (action: string, task: Task) => { } /* 全局暗色主题支持 */ -:global(html[data-theme='dark']) { +:global(.gantt-root[data-theme='dark']) { background: #1e1e1e !important; } -:global(html[data-theme='dark']) body { +:global(.gantt-root[data-theme='dark']) body { background: #1e1e1e !important; color: #e5e5e5 !important; } /* 暗色主题下的页面标题 */ -:global(html[data-theme='dark']) .page-title { +:global(.gantt-root[data-theme='dark']) .page-title { background: #1e1e1e; color: #e5e5e5; } /* 暗色主题下的配置面板样式 */ -:global(html[data-theme='dark']) .config-panel { +:global(.gantt-root[data-theme='dark']) .config-panel { background: var(--gantt-bg-primary, #2d3748); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .data-source-panel { +:global(.gantt-root[data-theme='dark']) .data-source-panel { background: var(--gantt-bg-primary, #2d3748); border-color: var(--gantt-border-color, #4a5568); box-shadow: none; } -:global(html[data-theme='dark']) .data-source-sub { +:global(.gantt-root[data-theme='dark']) .data-source-sub { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .data-source-button { +:global(.gantt-root[data-theme='dark']) .data-source-button { background: var(--gantt-bg-secondary, #1a202c); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .data-source-button.active { +:global(.gantt-root[data-theme='dark']) .data-source-button.active { background: rgba(64, 158, 255, 0.18); border-color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .ds-label, -:global(html[data-theme='dark']) .ds-desc, -:global(html[data-theme='dark']) .ds-file { +:global(.gantt-root[data-theme='dark']) .ds-label, +:global(.gantt-root[data-theme='dark']) .ds-desc, +:global(.gantt-root[data-theme='dark']) .ds-file { color: var(--gantt-text-primary, #e2e8f0); } -:global(html[data-theme='dark']) .ds-file { +:global(.gantt-root[data-theme='dark']) .ds-file { color: var(--gantt-text-muted, #a0aec0); } -:global(html[data-theme='dark']) .config-title { +:global(.gantt-root[data-theme='dark']) .config-title { color: var(--gantt-text-primary, #e2e8f0); } -:global(html[data-theme='dark']) .section-title { +:global(.gantt-root[data-theme='dark']) .section-title { color: var(--gantt-text-primary, #e2e8f0); border-bottom-color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .section-header { +:global(.gantt-root[data-theme='dark']) .section-header { border-bottom-color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .section-header:hover { +:global(.gantt-root[data-theme='dark']) .section-header:hover { border-bottom-color: var(--gantt-primary-color-light, #74c0fc); } -:global(html[data-theme='dark']) .section-header-title { +:global(.gantt-root[data-theme='dark']) .section-header-title { color: var(--gantt-text-primary, #e2e8f0); } -:global(html[data-theme='dark']) .section-collapse-button { +:global(.gantt-root[data-theme='dark']) .section-collapse-button { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .section-collapse-button:hover { +:global(.gantt-root[data-theme='dark']) .section-collapse-button:hover { color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .section-icon { +:global(.gantt-root[data-theme='dark']) .section-icon { color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .subsection { +:global(.gantt-root[data-theme='dark']) .subsection { border-left-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .subsection-title { +:global(.gantt-root[data-theme='dark']) .subsection-title { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .section-subtitle { +:global(.gantt-root[data-theme='dark']) .section-subtitle { color: var(--gantt-primary-color, #66b3ff); background: rgba(102, 179, 255, 0.15); border-left-color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .subsection-title::before { +:global(.gantt-root[data-theme='dark']) .subsection-title::before { background-color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .column-control { +:global(.gantt-root[data-theme='dark']) .column-control { background: var(--gantt-bg-secondary, #1a202c); } -:global(html[data-theme='dark']) .column-control:hover { +:global(.gantt-root[data-theme='dark']) .column-control:hover { background: var(--gantt-hover-bg, #2d3748); } -:global(html[data-theme='dark']) .column-label { +:global(.gantt-root[data-theme='dark']) .column-label { color: var(--gantt-text-primary, #e2e8f0); } -:global(html[data-theme='dark']) .column-control:hover .column-label { +:global(.gantt-root[data-theme='dark']) .column-control:hover .column-label { color: var(--gantt-primary-color, #66b3ff); } /* 暗色主题下的TaskBar配置样式 */ -:global(html[data-theme='dark']) .taskbar-control { +:global(.gantt-root[data-theme='dark']) .taskbar-control { background: var(--gantt-bg-secondary, #1a202c); } -:global(html[data-theme='dark']) .taskbar-control:hover { +:global(.gantt-root[data-theme='dark']) .taskbar-control:hover { background: var(--gantt-hover-bg, #2d3748); } -:global(html[data-theme='dark']) .taskbar-label { +:global(.gantt-root[data-theme='dark']) .taskbar-label { color: var(--gantt-text-primary, #e2e8f0); } -:global(html[data-theme='dark']) .taskbar-control:hover .taskbar-label { +:global(.gantt-root[data-theme='dark']) .taskbar-control:hover .taskbar-label { color: var(--gantt-primary-color, #66b3ff); } /* 暗色主题下的 TaskBar 高级配置 */ -:global(html[data-theme='dark']) .control-row { +:global(.gantt-root[data-theme='dark']) .control-row { background: var(--gantt-bg-secondary, #1a202c); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .control-label { +:global(.gantt-root[data-theme='dark']) .control-label { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .control-input { +:global(.gantt-root[data-theme='dark']) .control-input { background: var(--gantt-bg-primary, #2d3748); color: var(--gantt-text-primary, #e2e8f0); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .control-hint { +:global(.gantt-root[data-theme='dark']) .control-hint { color: var(--gantt-text-muted, #718096); } -:global(html[data-theme='dark']) .taskbar-field-control { +:global(.gantt-root[data-theme='dark']) .taskbar-field-control { background: var(--gantt-bg-secondary, #1a202c); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .field-label { +:global(.gantt-root[data-theme='dark']) .field-label { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .field-select { +:global(.gantt-root[data-theme='dark']) .field-select { background: var(--gantt-bg-primary, #2d3748); color: var(--gantt-text-primary, #e2e8f0); border-color: var(--gantt-border-color, #4a5568); } /* 暗色主题下的折叠面板样式 */ -:global(html[data-theme='dark']) .config-header { +:global(.gantt-root[data-theme='dark']) .config-header { border-bottom-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .config-header:hover { +:global(.gantt-root[data-theme='dark']) .config-header:hover { background-color: var(--gantt-hover-bg, #2d3748); } -:global(html[data-theme='dark']) .collapse-button { +:global(.gantt-root[data-theme='dark']) .collapse-button { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .collapse-button:hover { +:global(.gantt-root[data-theme='dark']) .collapse-button:hover { background-color: var(--gantt-hover-bg, #2d3748); color: var(--gantt-primary-color, #66b3ff); } /* 暗黑模式下的版本标签 */ -:global(html[data-theme='dark']) .version-badge { +:global(.gantt-root[data-theme='dark']) .version-badge { background: linear-gradient(135deg, #1a73e8 0%, #00bcd4 50%, #3f51b5 100%); box-shadow: 0 0 25px rgba(102, 177, 255, 0.4), @@ -3650,7 +3650,7 @@ const handleCustomMenuAction = (action: string, task: Task) => { inset 0 1px 0 rgba(255, 255, 255, 0.1); } -:global(html[data-theme='dark']) .version-badge:hover { +:global(.gantt-root[data-theme='dark']) .version-badge:hover { background: linear-gradient(135deg, #2196f3 0%, #00e5ff 50%, #5c6bc0 100%); box-shadow: 0 0 35px rgba(102, 177, 255, 0.6), @@ -3763,53 +3763,53 @@ const handleCustomMenuAction = (action: string, task: Task) => { /* 移除旧的基于 SVG color 的样式,现在使用 filter */ /* 暗黑模式下覆盖所有链接样式 */ -:global(html[data-theme='dark']) .doc-link { +:global(.gantt-root[data-theme='dark']) .doc-link { color: #ffffff; } -:global(html[data-theme='dark']) .doc-link:hover { +:global(.gantt-root[data-theme='dark']) .doc-link:hover { color: #ffffff; background-color: rgba(255, 255, 255, 0.1); } -:global(html[data-theme='dark']) .doc-link:nth-child(2) { +:global(.gantt-root[data-theme='dark']) .doc-link:nth-child(2) { color: #ffffff !important; } -:global(html[data-theme='dark']) .doc-link:nth-child(2):hover { +:global(.gantt-root[data-theme='dark']) .doc-link:nth-child(2):hover { color: #ffffff !important; background-color: rgba(199, 29, 35, 0.1); } /* 暗黑模式下图标样式 */ -:global(html[data-theme='dark']) .github-link .doc-icon { +:global(.gantt-root[data-theme='dark']) .github-link .doc-icon { filter: brightness(0) saturate(100%) invert(100%); } -:global(html[data-theme='dark']) .github-link:hover .doc-icon { +:global(.gantt-root[data-theme='dark']) .github-link:hover .doc-icon { filter: brightness(0) saturate(100%) invert(70%) sepia(50%) saturate(2000%) hue-rotate(190deg) brightness(1.2); } -:global(html[data-theme='dark']) .gitee-link .doc-icon { +:global(.gantt-root[data-theme='dark']) .gitee-link .doc-icon { filter: brightness(0) saturate(100%) invert(45%) sepia(100%) saturate(1500%) hue-rotate(340deg) brightness(1.1); } -:global(html[data-theme='dark']) .gitee-link:hover .doc-icon { +:global(.gantt-root[data-theme='dark']) .gitee-link:hover .doc-icon { filter: brightness(0) saturate(100%) invert(50%) sepia(100%) saturate(1800%) hue-rotate(340deg) brightness(1.2); } /* 暗黑模式下的里程碑图标发光效果 */ -:global(html[data-theme='dark']) .milestone-group-icon { +:global(.gantt-root[data-theme='dark']) .milestone-group-icon { color: var(--gantt-danger, #f67c7c); fill: var(--gantt-danger, #f67c7c); filter: drop-shadow(0 0 6px var(--gantt-danger, #f67c7c)); animation: milestone-icon-glow-dark 2.5s ease-in-out infinite alternate; } -:global(html[data-theme='dark']) .task-row:hover .milestone-group-icon { +:global(.gantt-root[data-theme='dark']) .task-row:hover .milestone-group-icon { filter: drop-shadow(0 0 10px var(--gantt-danger, #f67c7c)) drop-shadow(0 0 16px rgba(246, 124, 124, 0.4)); animation: milestone-icon-glow-intense-dark 1.8s ease-in-out infinite alternate; @@ -4019,28 +4019,28 @@ const handleCustomMenuAction = (action: string, task: Task) => { } /* 暗黑模式适配 */ -:global(html[data-theme='dark']) .task-click-dialog { +:global(.gantt-root[data-theme='dark']) .task-click-dialog { background: var(--gantt-bg-primary, #1a1a1a); } -:global(html[data-theme='dark']) .task-click-dialog-header h3 { +:global(.gantt-root[data-theme='dark']) .task-click-dialog-header h3 { color: var(--gantt-text-primary, #e0e0e0); } -:global(html[data-theme='dark']) .close-button { +:global(.gantt-root[data-theme='dark']) .close-button { color: var(--gantt-text-secondary, #b0b0b0); } -:global(html[data-theme='dark']) .close-button:hover { +:global(.gantt-root[data-theme='dark']) .close-button:hover { background: var(--gantt-bg-hover, #2a2a2a); color: var(--gantt-text-primary, #e0e0e0); } -:global(html[data-theme='dark']) .property-key { +:global(.gantt-root[data-theme='dark']) .property-key { color: var(--gantt-text-secondary, #b0b0b0); } -:global(html[data-theme='dark']) .property-value { +:global(.gantt-root[data-theme='dark']) .property-value { color: var(--gantt-text-primary, #e0e0e0); } @@ -4079,27 +4079,27 @@ const handleCustomMenuAction = (action: string, task: Task) => { } /* 暗黑模式下的任务行样式 */ -:global(html[data-theme='dark']) :deep(.task-row-success) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-success) { background: linear-gradient(90deg, #0a3a2a 0%, #1b4d3e 100%) !important; } -:global(html[data-theme='dark']) :deep(.task-row-success:hover) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-success:hover) { background: linear-gradient(90deg, #0f4d35 0%, #276749 100%) !important; } -:global(html[data-theme='dark']) :deep(.task-row-warning) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-warning) { background: linear-gradient(90deg, #3d2f1f 0%, #4d3b2a 100%) !important; } -:global(html[data-theme='dark']) :deep(.task-row-warning:hover) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-warning:hover) { background: linear-gradient(90deg, #4d3b26 0%, #5d4a35 100%) !important; } -:global(html[data-theme='dark']) :deep(.task-row-info) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-info) { background: linear-gradient(90deg, #2a2a2a 0%, #333333 100%) !important; } -:global(html[data-theme='dark']) :deep(.task-row-info:hover) { +:global(.gantt-root[data-theme='dark']) :deep(.task-row-info:hover) { background: linear-gradient(90deg, #353535 0%, #3d3d3d 100%) !important; } @@ -4151,34 +4151,34 @@ const handleCustomMenuAction = (action: string, task: Task) => { } /* 暗色主题下的自定义菜单 */ -:global(html[data-theme='dark']) .custom-menu { +:global(.gantt-root[data-theme='dark']) .custom-menu { background: #2a2a2a; border-color: #444; } -:global(html[data-theme='dark']) .custom-menu-header { +:global(.gantt-root[data-theme='dark']) .custom-menu-header { background: #1e1e1e; color: #e0e0e0; border-bottom-color: #444; } -:global(html[data-theme='dark']) .custom-menu-item { +:global(.gantt-root[data-theme='dark']) .custom-menu-item { color: #e0e0e0; } -:global(html[data-theme='dark']) .custom-menu-item:hover { +:global(.gantt-root[data-theme='dark']) .custom-menu-item:hover { background: #353535; } -:global(html[data-theme='dark']) .custom-menu-item.danger { +:global(.gantt-root[data-theme='dark']) .custom-menu-item.danger { color: #ff6b6b; } -:global(html[data-theme='dark']) .custom-menu-item.danger:hover { +:global(.gantt-root[data-theme='dark']) .custom-menu-item.danger:hover { background: #3a2020; } -:global(html[data-theme='dark']) .custom-menu-divider { +:global(.gantt-root[data-theme='dark']) .custom-menu-divider { background: #444; } @@ -4485,61 +4485,61 @@ const handleCustomMenuAction = (action: string, task: Task) => { } /* 暗色主题支持 */ -:global(html[data-theme='dark']) .status-panel { +:global(.gantt-root[data-theme='dark']) .status-panel { background: var(--gantt-bg-secondary, #1a202c); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .status-value { +:global(.gantt-root[data-theme='dark']) .status-value { background: var(--gantt-bg-secondary, #1a202c); color: var(--gantt-text-primary, #e2e8f0); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .status-value.active { +:global(.gantt-root[data-theme='dark']) .status-value.active { background: rgba(64, 158, 255, 0.15); border-color: var(--gantt-primary-color, #66b3ff); color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .tool-button { +:global(.gantt-root[data-theme='dark']) .tool-button { background: var(--gantt-bg-secondary, #1a202c); color: var(--gantt-text-primary, #e2e8f0); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .tool-button:hover { +:global(.gantt-root[data-theme='dark']) .tool-button:hover { background: var(--gantt-hover-bg, #2d3748); border-color: var(--gantt-primary-color, #66b3ff); color: var(--gantt-primary-color, #66b3ff); } -:global(html[data-theme='dark']) .tool-button.primary { +:global(.gantt-root[data-theme='dark']) .tool-button.primary { background: var(--gantt-primary-color, #409eff); border-color: var(--gantt-primary-color, #409eff); color: white; } -:global(html[data-theme='dark']) .tool-button.primary:hover { +:global(.gantt-root[data-theme='dark']) .tool-button.primary:hover { background: var(--gantt-primary-hover, #66b1ff); border-color: var(--gantt-primary-hover, #66b1ff); } -:global(html[data-theme='dark']) .tool-divider { +:global(.gantt-root[data-theme='dark']) .tool-divider { background: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .tool-input { +:global(.gantt-root[data-theme='dark']) .tool-input { background: var(--gantt-bg-primary, #2d3748); color: var(--gantt-text-primary, #e2e8f0); border-color: var(--gantt-border-color, #4a5568); } -:global(html[data-theme='dark']) .tool-label { +:global(.gantt-root[data-theme='dark']) .tool-label { color: var(--gantt-text-secondary, #a0aec0); } -:global(html[data-theme='dark']) .tool-note { +:global(.gantt-root[data-theme='dark']) .tool-note { background: var(--gantt-bg-secondary, #1a202c); color: var(--gantt-text-secondary, #a0aec0); border-left-color: var(--gantt-primary-color, #66b3ff); diff --git a/demo/VersionHistoryDrawer.vue b/demo/VersionHistoryDrawer.vue index 56820f6..b8bd83d 100644 --- a/demo/VersionHistoryDrawer.vue +++ b/demo/VersionHistoryDrawer.vue @@ -268,65 +268,65 @@ onMounted(async () => { } /* 暗黑主题适配,完全对齐TaskDrawer风格 */ -:global(html[data-theme='dark']) .version-history-drawer { +:global(.gantt-root[data-theme='dark']) .version-history-drawer { background: var(--gantt-bg-primary, #6b6b6b) !important; box-shadow: 2px 0 24px rgba(0, 0, 0, 0.4) !important; } -:global(html[data-theme='dark']) .drawer-header { +:global(.gantt-root[data-theme='dark']) .drawer-header { background: var(--gantt-bg-secondary, #4b4b4b) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important; } -:global(html[data-theme='dark']) .drawer-title { +:global(.gantt-root[data-theme='dark']) .drawer-title { color: var(--gantt-text-white, #fff) !important; } -:global(html[data-theme='dark']) .drawer-close-btn:hover { +:global(.gantt-root[data-theme='dark']) .drawer-close-btn:hover { background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.1)) !important; border-radius: 4px; } -:global(html[data-theme='dark']) .drawer-body { +:global(.gantt-root[data-theme='dark']) .drawer-body { background: var(--gantt-bg-primary, #6b6b6b) !important; scrollbar-color: #888888 #4b4b4b !important; } -:global(html[data-theme='dark']) .drawer-body::-webkit-scrollbar-thumb { +:global(.gantt-root[data-theme='dark']) .drawer-body::-webkit-scrollbar-thumb { background: #888888 !important; } -:global(html[data-theme='dark']) .drawer-body::-webkit-scrollbar-track { +:global(.gantt-root[data-theme='dark']) .drawer-body::-webkit-scrollbar-track { background: #4b4b4b !important; } -:global(html[data-theme='dark']) .version-timeline-content.version-card { +:global(.gantt-root[data-theme='dark']) .version-timeline-content.version-card { background: var(--gantt-bg-secondary, #4b4b4b) !important; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.32) !important; color: var(--gantt-text-white, #fff) !important; } -:global(html[data-theme='dark']) +:global(.gantt-root[data-theme='dark']) .version-timeline-group:hover .version-timeline-content.version-card { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.44) !important; } -:global(html[data-theme='dark']) .version-timeline-content.version-card::before { +:global(.gantt-root[data-theme='dark']) .version-timeline-content.version-card::before { filter: drop-shadow(-2px 0 2px #222) !important; } -:global(html[data-theme='dark']) .dot-version { +:global(.gantt-root[data-theme='dark']) .dot-version { color: var(--gantt-primary, #3399ff) !important; text-shadow: 0 1px 4px rgba(51, 153, 255, 0.12) !important; } -:global(html[data-theme='dark']) .dot-date { +:global(.gantt-root[data-theme='dark']) .dot-date { color: #e0e0e0 !important; } -:global(html[data-theme='dark']) .version-notes { +:global(.gantt-root[data-theme='dark']) .version-notes { color: #e0e0e0 !important; } -:global(html[data-theme='dark']) .version-timeline-dot { +:global(.gantt-root[data-theme='dark']) .version-timeline-dot { background: var(--gantt-timeline-dot, #3399ff) !important; box-shadow: 0 0 0 2px #222 !important; } -:global(html[data-theme='dark']) .version-timeline-dot.latest { +:global(.gantt-root[data-theme='dark']) .version-timeline-dot.latest { background: var(--gantt-primary, #3399ff) !important; } -:global(html[data-theme='dark']) .version-timeline-group:hover .version-timeline-dot { +:global(.gantt-root[data-theme='dark']) .version-timeline-group:hover .version-timeline-dot { background: var(--gantt-primary, #3399ff) !important; } -:global(html[data-theme='dark']) .version-timeline::before { +:global(.gantt-root[data-theme='dark']) .version-timeline::before { background: repeating-linear-gradient(to bottom, #3399ff 0 8px, transparent 8px 16px) !important; } diff --git a/demo/version-history.json b/demo/version-history.json index f1c06f7..fd570d1 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -506,5 +506,43 @@ "🎉 Optimized: Refactored components, Perfectly supports Nuxt3, TailwindCSS and other frameworks", "Special thanks to andrijor@gmail.com for their professional feedback" ] + }, + { + "version": "1.9.0", + "date": "2026-02-07", + "notes": [ + "🎉 新增:GanttChart支持资源视图模式,通过viewMode属性切换任务视图和资源视图", + "🎉 新增:资源视图以资源为行维度展示,支持一行多任务条布局", + "🎉 新增:Task对象增加resources字段,支持配置每个资源的投入占比(10%-100%)", + "🎉 新增:TaskBar高度按资源占比动态缩放,支持占比文字标注和Tooltip显示", + "🎉 新增:基于占比累计的资源超负荷检测和视觉预警", + "🎉 新增:TaskDrawer增加资源占比配置控件,支持输入校验", + "🎉 新增:资源视图支持任务拖拽、拉伸、计时等交互操作", + "🎉 新增:资源列表支持声明式组件配置列", + "🎉 新增:Resource数据模型类和useResourceLayout、useViewMode等composables", + "🎉 优化:抽取资源布局计算逻辑到独立composable,避免组件职责膨胀", + "🎉 优化:扩展v-memo优化覆盖范围,提升渲染性能", + "🎉 优化:重构provide/inject依赖传递,简化组件依赖关系", + "🎉 优化:恢复滚动防抖机制,平衡响应性和性能", + "🎉 优化:历史数据向后兼容,未配置占比时默认为100%", + "🔧 修复: 将Theme设置从HTML root变更到Gantt Component root", + "特别感谢a_sunshine@gitee的使用与反馈", + "🎉 Added: GanttChart supports resource view mode, switch via viewMode property", + "🎉 Added: Resource view displays by resource dimension with multiple task bars per row", + "🎉 Added: Task object adds resources field for configuring resource allocation percentage (10%-100%)", + "🎉 Added: TaskBar height scales by percentage with text label and Tooltip support", + "🎉 Added: Resource overload detection and visual alerts based on percentage accumulation", + "🎉 Added: TaskDrawer adds resource percentage configuration with input validation", + "🎉 Added: Resource view supports task drag, resize, timing and other interactions", + "🎉 Added: Resource list supports declarative component column configuration", + "🎉 Added: Resource model class and composables including useResourceLayout, useViewMode", + "🎉 Optimized: Extracted resource layout logic to independent composable", + "🎉 Optimized: Extended v-memo optimization coverage for better performance", + "🎉 Optimized: Refactored provide/inject dependency passing", + "🎉 Optimized: Restored scroll debounce mechanism", + "🎉 Optimized: Backward compatible with historical data, defaults to 100%", + "🔧 Fixed: Changed Theme setting from HTML root to Gantt Component root", + "Special thanks to a_sunshine@gitee for their use and feedback" + ] } ] diff --git a/npm-demo/src/components/GanttTest.vue b/npm-demo/src/components/GanttTest.vue index 38dc54f..78c33e2 100644 --- a/npm-demo/src/components/GanttTest.vue +++ b/npm-demo/src/components/GanttTest.vue @@ -3,7 +3,7 @@ import { ref, computed, watch } from 'vue' import { GanttChart, TaskListColumn, useI18n, TaskListContextMenu, TaskBarContextMenu } from 'jordium-gantt-vue3' import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css' -const { t, getTranslation } = useI18n() +const { t, getTranslation } = useI18n(); // GanttChart ref const ganttRef = ref(null) @@ -114,7 +114,7 @@ const tasks = ref([ department: '管理部', departmentCode: 'D001', type: 'task', - }, + } ]) const milestones = ref([ @@ -123,8 +123,234 @@ const milestones = ref([ name: '项目立项', startDate: '2025-10-29', type: 'milestone', - icon: 'diamond', + icon: 'diamond' + } +]) + +// 资源数据源 - 按照Resource类型结构定义 +const resources = ref([ + { + id: 'R001', + name: '张三', + type: '开发工程师', + avatar: 'https://i.pravatar.cc/150?img=1', + description: '前端开发专家,擅长Vue.js和React', + department: '技术部', + skills: ['Vue.js', 'React', 'TypeScript', 'Node.js'], + capacity: 85, + color: '#1890ff', + tasks: [ + { + id: 1000, + name: '前端框架搭建', + startDate: '2025-11-01', + endDate: '2025-11-05', + progress: 80, + type: 'task' + }, + { + id: 1001, + name: '组件库开发', + startDate: '2025-11-06', + endDate: '2025-11-15', + progress: 50, + type: 'task' + }, + { + id: 1002, + name: '页面开发', + startDate: '2025-11-16', + endDate: '2025-11-25', + progress: 20, + type: 'task' + } + ] }, + { + id: 'R002', + name: '李四', + type: '后端工程师', + avatar: 'https://i.pravatar.cc/150?img=2', + description: 'Java后端开发,熟悉Spring全家桶', + department: '技术部', + skills: ['Java', 'Spring Boot', 'MySQL', 'Redis'], + capacity: 85, + color: '#52c41a', + tasks: [ + { + id: 1003, + name: '数据库设计', + startDate: '2025-11-01', + endDate: '2025-11-03', + progress: 100, + type: 'task' + }, + { + id: 1004, + name: 'API接口开发', + startDate: '2025-11-04', + endDate: '2025-11-12', + progress: 60, + type: 'task' + }, + { + id: 1005, + name: '性能优化', + startDate: '2025-11-20', + endDate: '2025-11-28', + progress: 0, + type: 'task' + } + ] + }, + { + id: 'R003', + name: '王五', + type: 'UI设计师', + avatar: 'https://i.pravatar.cc/150?img=3', + description: '资深UI/UX设计师', + department: '设计部', + skills: ['Figma', 'Sketch', 'Photoshop', 'Illustrator'], + capacity: 8, + color: '#faad14', + tasks: [ + { + id: 1006, + name: 'UI原型设计', + startDate: '2025-10-28', + endDate: '2025-11-02', + progress: 100, + type: 'task' + }, + { + id: 1007, + name: '视觉规范制定', + startDate: '2025-11-03', + endDate: '2025-11-08', + progress: 90, + type: 'task' + }, + { + id: 1008, + name: '界面设计', + startDate: '2025-11-09', + endDate: '2025-11-18', + progress: 40, + type: 'task' + } + ] + }, + { + id: 'R004', + name: '赵六', + type: '测试工程师', + avatar: 'https://i.pravatar.cc/150?img=4', + description: '软件测试专家,自动化测试经验丰富', + department: '质量部', + skills: ['Selenium', 'Jest', 'Cypress', 'JMeter'], + capacity: 8, + color: '#f5222d', + tasks: [ + { + id: 1009, + name: '测试计划编写', + startDate: '2025-11-10', + endDate: '2025-11-12', + progress: 70, + type: 'task' + }, + { + id: 1010, + name: '自动化测试脚本', + startDate: '2025-11-13', + endDate: '2025-11-20', + progress: 30, + type: 'task' + }, + { + id: 1011, + name: '功能测试', + startDate: '2025-11-21', + endDate: '2025-11-30', + progress: 0, + type: 'task' + } + ] + }, + { + id: 'R005', + name: '钱七', + type: '产品经理', + avatar: 'https://i.pravatar.cc/150?img=5', + description: '5年产品经验,擅长用户需求分析', + department: '产品部', + skills: ['需求分析', 'Axure', 'PRD撰写', '用户研究'], + capacity: 8, + color: '#722ed1', + tasks: [ + { + id: 1012, + name: '需求调研', + startDate: '2025-10-25', + endDate: '2025-10-30', + progress: 100, + type: 'task' + }, + { + id: 1013, + name: 'PRD文档编写', + startDate: '2025-10-31', + endDate: '2025-11-05', + progress: 85, + type: 'task' + }, + { + id: 1014, + name: '产品验收', + startDate: '2025-11-25', + endDate: '2025-11-30', + progress: 0, + type: 'task' + } + ] + }, + { + id: 'R006', + name: '孙八', + type: '全栈工程师', + avatar: 'https://i.pravatar.cc/150?img=6', + description: '全栈开发,前后端通吃', + department: '技术部', + skills: ['Vue.js', 'Node.js', 'Python', 'Docker'], + capacity: 8, + color: '#13c2c2', + tasks: [ + { + id: 1015, + name: '服务器部署', + startDate: '2025-11-01', + endDate: '2025-11-04', + progress: 100, + type: 'task' + }, + { + id: 1016, + name: 'CI/CD配置', + startDate: '2025-11-05', + endDate: '2025-11-10', + progress: 75, + type: 'task' + }, + { + id: 1017, + name: '微服务架构', + startDate: '2025-11-11', + endDate: '2025-11-22', + progress: 35, + type: 'task' + } + ] + } ]) const customMessages = { @@ -139,7 +365,7 @@ const customMessages = { gantt: { planStartDate: '计划开始时间', //planEndDate: '计划结束时间', - }, + } }, 'en-US': { department: 'Department', @@ -152,16 +378,16 @@ const customMessages = { gantt: { planStartDate: 'Plan Start Date', planEndDate: 'Plan End Date', - }, - }, + } + } } // const tasks = ref([]) // const milestones = ref([]) -const showAddTaskDrawer = ref(false) -const showAddMilestoneDialog = ref(false) -const showTodayLocate = ref(true) +const showAddTaskDrawer = ref(false); +const showAddMilestoneDialog = ref(false); +const showTodayLocate = ref(true); // 定义可动态配置的列 const availableColumns = ref([ @@ -186,7 +412,7 @@ const taskListConfig = { defaultWidth: '50%', // 默认展开宽度50% minWidth: '300px', // 最小宽度300px(默认280px) maxWidth: '1200px', // 最大宽度1200px(默认1160px) - columns: availableColumns.value, + columns: availableColumns.value } // toolbar配置示例 @@ -201,17 +427,18 @@ const toolbarConfig: ToolbarConfig = { showFullscreen: true, // 显示全屏按钮 showTimeScale: true, // 显示时间刻度按钮组 timeScaleDimensions: [ // 显示所有时间刻度维度 - 'hour', 'day', 'week', 'month', 'quarter', 'year', + 'hour', 'day', 'week', 'month', 'quarter', 'year' ], defaultTimeScale: 'week', // 默认选中周视图 - showExpandCollapse: false, // 显示展开/折叠按钮 + showExpandCollapse: false // 显示展开/折叠按钮 } + const newTask = ref({ name: '', startDate: '', - endDate: '', -}) + endDate: '' +}); const addTask = () => { tasks.value.push({ @@ -220,10 +447,10 @@ const addTask = () => { startDate: newTask.value.startDate, endDate: newTask.value.endDate, progress: 0, - }) - newTask.value = { name: '', startDate: '', endDate: '' } - showAddTaskDrawer.value = false -} + }); + newTask.value = { name: '', startDate: '', endDate: '' }; + showAddTaskDrawer.value = false; +}; const addMilestone = () => { milestones.value.push({ @@ -232,11 +459,11 @@ const addMilestone = () => { startDate: newTask.value.startDate, progress: 0, type: 'milestone', - icon: 'diamond', - }) - //console.log('milestones: ', milestones.value) - newTask.value = { name: '', startDate: '', endDate: '' } - showAddMilestoneDialog.value = false + icon: 'diamond' + }); + console.log('milestones: ', milestones.value) + newTask.value = { name: '', startDate: '', endDate: '' }; + showAddMilestoneDialog.value = false; } const onTaskDblclick = (task) => { @@ -293,18 +520,18 @@ const handleTaskRowMoved = async (payload: { // 此回调仅用于补充业务逻辑,例如根据assignee填充assigneeName等 const onTaskAdded = (res) => { // 组件已自动添加任务,这里只需要找到并更新额外字段 - const addedTask = tasks.value.find(t => t.id === res.task.id) + const addedTask = tasks.value.find(t => t.id === res.task.id); if (addedTask && addedTask.assignee) { // 根据assignee值查找对应的label并赋值给assigneeName - const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee) + const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee); if (assigneeOption) { - addedTask.assigneeName = assigneeOption.label + addedTask.assigneeName = assigneeOption.label; } } // 不需要手动push,组件已处理 -} +}; // 自定义右键菜单操作处理 const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) => { @@ -315,234 +542,16 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)