diff --git a/README-EN.md b/README-EN.md index 0c39835..131a5a1 100644 --- a/README-EN.md +++ b/README-EN.md @@ -29,11 +29,11 @@

A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling

-

+

@@ -72,11 +72,15 @@ jordium-gantt-vue3 is a modern Gantt chart component built with Vue 3 and TypeSc #### Light Theme -Light Theme +Light Theme + +Light Theme #### Dark Theme -Dark Theme +Dark Theme + +Dark Theme --- @@ -195,6 +199,8 @@ npm run dev | --------------------------- | --------- | ------- | ------------------------------------------------------------------------- | | `tasks` | `Task[]` | `[]` | Array of task data | | `milestones` | `Task[]` | `[]` | Array of milestone data (Note: Type is Task[], must set type='milestone') | +| `resources` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `Resource[]` | `[]` | Array of resource data (used in resource planning view) | +| `viewMode` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `'task' \| 'resource'` | `'task'` | View mode: 'task' for task planning view \| 'resource' for resource planning view | | `showToolbar` | `boolean` | `true` | Whether to show the toolbar | | `useDefaultDrawer` | `boolean` | `true` | Whether to use the built-in task edit drawer (TaskDrawer) | | `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use the built-in milestone edit dialog (MilestoneDialog) | @@ -215,7 +221,9 @@ npm run dev | `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) | +| `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) | +| `showConflicts` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to display resource conflict visualization layer (shows diagonal stripe background for overload zones in resource view) | +| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to display resource tab on TaskBar (shows resource allocation label on TaskBar in resource view) | #### TaskListColumn Component Props @@ -362,6 +370,7 @@ For complete configuration object documentation, see [⚙️ Configuration & Cus | ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------------------- | | `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration | | `taskListConfig` | `TaskListConfig` | `undefined` | Task list configuration | +| `resourceListConfig` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `ResourceListConfig` | `undefined` | Resource list configuration | | `taskBarConfig` | `TaskBarConfig` | `undefined` | Task bar style configuration | | `localeMessages` | `Partial` | `undefined` | Custom localization messages | | `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | Working hours configuration | @@ -408,6 +417,7 @@ For complete event documentation, see: | `milestone-icon-changed` | `{ milestoneId, icon }` | Milestone icon changed | | `milestone-drag-end` | `(milestone: Task)` | Milestone drag ended | | `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | TaskRow drag ended (optional) | +| `taskbar-resource-change` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | Task moved across resources (dragging task to another resource row in resource view) | #### Example 1: Simplest Gantt Chart @@ -1100,7 +1110,207 @@ const handleTaskRowMoved = async (payload: { - Cannot drag onto its own child tasks (avoid circular reference) - Milestones and milestone groups cannot be dragged -### Milestone Management +### Resource Management ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) + +Resource management is used to manage human resources or equipment in a project, supporting task allocation, resource load analysis, and conflict detection in resource view. Switch to resource planning view using the `viewMode="resource"` prop. + +> **Core Features**: +> - 📊 **Resource View**: Display task allocation by resource dimension +> - 🎯 **Load Analysis**: Real-time display of resource utilization and overload status +> - ⚠️ **Conflict Detection**: Automatically detect resource time conflicts (e.g., Task A:40% + Task B:40% + Task C:30% = 110% overload) +> - 🎨 **Visualization**: Diagonal stripe background marks conflict zones, resource tabs show utilization percentage +> - 🔄 **Cross-Resource Move**: Support dragging tasks to different resource rows for reallocation +> +> **View Limitations**: +> - ❌ **Task Links Disabled**: Resource view does not display predecessor/successor relationship lines between tasks, as resource view focuses on resource allocation rather than task dependencies +> - ❌ **No Actual TaskBar**: `showActualTaskbar` prop has no effect in resource view, actual execution progress bar will not be displayed + +#### Resource Data Structure + +| Field | Type | Required | Default | Description | +| --------------- | ------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- | +| `id` | `string \| number` | ✅ | - | Unique resource identifier | +| `name` | `string` | ✅ | - | Resource name (e.g., person name, device name) | +| `type` | `string` | - | - | Resource type (e.g., 'developer', 'designer', 'device') | +| `avatar` | `string` | - | - | Resource avatar URL | +| `description` | `string` | - | - | Resource description | +| `department` | `string` | - | - | Department | +| `skills` | `string[]` | - | - | Skill tags array (e.g., `['Vue', 'React', 'TypeScript']`) | +| `capacity` | `number` | - | - | Resource capacity/utilization (0-100), can represent overall load level | +| `color` | `string` | - | - | Custom resource row left border color (e.g., `'#ff5733'`), uses default color scheme if not set | +| `tasks` | `Task[]` | - | `[]` | Array of tasks assigned to this resource, **each task needs `resources` field to mark resource utilization** | +| `[key: string]` | `unknown` | - | - | Support custom property extension, can add any additional fields | + +> **Custom Property Extension**: Resource interface supports adding any custom fields, e.g., `email`, `phone`, `location`, `workHours`, etc. +> +> **Task-Resource Association**: +> +> - Each Resource contains a `tasks` array storing tasks assigned to that resource +> - Each Task should include a `resources` field marking which resources are used and their utilization percentage +> - Resource utilization format: `task.resources = [{ id: 'resource1', capacity: 60 }, { id: 'resource2', capacity: 40 }]` +> - `capacity` range: 20-100, representing the percentage of resource used by that task +> - Conflict detection: When multiple tasks' `capacity` sum > 100% for the same resource in the same time period, a conflict warning is displayed + +**Resource Data Example**: + +```typescript +import type { Resource, Task } from 'jordium-gantt-vue3' + +const resources: Resource[] = [ + { + id: 'dev-001', + name: 'Zhang San', + type: 'developer', + avatar: '/avatars/zhangsan.jpg', + department: 'R&D', + skills: ['Vue', 'TypeScript', 'Node.js'], + capacity: 85, // Overall load level + color: '#409eff', + tasks: [ + { + id: 1, + name: 'Frontend Development', + startDate: '2026-02-01', + endDate: '2026-02-10', + progress: 50, + resources: [ + { id: 'dev-001', capacity: 60 }, // This task uses 60% of Zhang San's time + { id: 'dev-002', capacity: 40 } // Also uses 40% of Li Si's time + ] + }, + { + id: 2, + name: 'Code Review', + startDate: '2026-02-05', + endDate: '2026-02-08', + progress: 0, + resources: [ + { id: 'dev-001', capacity: 40 } // This task uses 40% of Zhang San's time + ] + } + // Note: If two tasks overlap, Zhang San's total utilization on Feb 5-8 is 100% (60%+40%), at threshold + ] + }, + { + id: 'dev-002', + name: 'Li Si', + type: 'developer', + avatar: '/avatars/lisi.jpg', + department: 'R&D', + skills: ['React', 'TypeScript'], + tasks: [] + } +] +``` + +**Resource Conflict Detection Example**: + +```typescript +// Scenario: Zhang San is assigned 3 tasks in the same time period +const resource = { + id: 'dev-001', + name: 'Zhang San', + tasks: [ + { + id: 1, + name: 'Task A', + startDate: '2026-02-10', + endDate: '2026-02-15', + resources: [{ id: 'dev-001', capacity: 40 }] // Uses 40% + }, + { + id: 2, + name: 'Task B', + startDate: '2026-02-10', + endDate: '2026-02-20', + resources: [{ id: 'dev-001', capacity: 40 }] // Uses 40% + }, + { + id: 3, + name: 'Task C', + startDate: '2026-02-12', + endDate: '2026-02-18', + resources: [{ id: 'dev-001', capacity: 30 }] // Uses 30% + } + ] +} + +// Conflict Analysis: +// - Feb 10-11: A(40%) + B(40%) = 80%, not overloaded +// - Feb 12-15: A(40%) + B(40%) + C(30%) = 110%, overloaded! Shows conflict warning +// - Feb 16-18: B(40%) + C(30%) = 70%, not overloaded +// - Feb 19-20: B(40%), not overloaded +``` + +#### Resource-Related Props + +| Prop | Type | Default | Description | +| --------------------- | ---------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | +| `resources` | `Resource[]` | `[]` | Array of resource data | +| `viewMode` | `'task' \| 'resource'` | `'task'` | View mode: 'task' for task planning view, 'resource' for resource planning view | +| `resourceListConfig` | `ResourceListConfig` | `undefined` | Resource list configuration, similar to TaskListConfig, for configuring resource list columns, width etc | +| `showConflicts` | `boolean` | `true` | Whether to display resource conflict visualization layer (diagonal stripe background in resource view) | +| `showTaskbarTab` | `boolean` | `true` | Whether to display resource tab on TaskBar (resource utilization label on TaskBar in resource view) | + +#### Resource Events + +| Event Name | Parameters | Trigger Timing | Description | +| -------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------- | +| `taskbar-resource-change` | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | When task drag across resources | Triggered when dragging task to another resource row in resource view, component auto-updates task's `resources` field | + +**Data Sync Notes**: + +- ✅ **Auto Update**: Resource-related operations (like task cross-resource move) are automatically updated by the component in `props.resources` and task's `resources` field +- ✅ **Events for Notification**: External event listeners are mainly used for: showing messages, calling backend APIs, updating other related data, etc. +- ❌ **Avoid Duplicate Operations**: Do not modify data again in event handlers, as this causes duplicate updates + +#### Example: Basic Resource View Usage + +```vue + + + +``` ### Milestone Management diff --git a/README.md b/README.md index f44cda0..4138831 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,11 @@

现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案

-

+

@@ -74,10 +74,14 @@ jordium-gantt-vue3 是一个基于 Vue 3 和 TypeScript 开发的现代化甘特 亮色主题 +亮色主题 + #### 暗色主题 暗色主题 +暗色主题 + --- ## 📦 安装 @@ -193,6 +197,8 @@ npm run dev | --------------------------- | ----------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- | | `tasks` | `Task[]` | `[]` | 任务数据数组 | | `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone') | +| `resources` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `Resource[]` | `[]` | 资源数据数组(资源计划视图使用) | +| `viewMode` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `'task' \| 'resource'` | `'task'` | 视图模式:'task' 任务计划视图 \| 'resource' 资源计划视图 | | `showToolbar` | `boolean` | `true` | 是否显示工具栏 | | `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) | | `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) | @@ -213,7 +219,9 @@ npm run dev | `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悬停提示框(鼠标悬停显示任务详情) | +| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) | +| `showConflicts` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否显示资源冲突可视化层(资源视图下显示斜纹背景标识超载区域) | +| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) | #### TaskListColumn 属性 @@ -360,6 +368,7 @@ npm run dev | ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------- | | `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 | | `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置 | +| `resourceListConfig` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `ResourceListConfig` | `undefined` | 资源列表配置 | | `taskBarConfig` | `TaskBarConfig` | `undefined` | 任务条样式配置 | | `localeMessages` | `Partial` | `undefined` | 自定义多语言配置 | | `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | 工作时间配置 | @@ -406,6 +415,7 @@ npm run dev | `milestone-icon-changed` | `{ milestoneId, icon }` | 里程碑图标变更 | | `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 | | `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | 拖拽TaskRow结束(可选) | +| `taskbar-resource-change` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | 任务跨资源移动事件(资源视图下拖拽任务到另一资源行) | #### 示例1:最简单的甘特图 @@ -1098,6 +1108,208 @@ const handleTaskRowMoved = async (payload: { - 不能拖拽到自己的子任务上(避免循环引用) - 里程碑和里程碑分组不能被拖拽 +### 资源管理 ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) + +资源管理用于管理项目中的人力或设备资源,支持资源视图下的任务分配、资源负载分析、冲突检测等功能。通过 `viewMode="resource"` 属性切换到资源计划视图。 + +> **核心特性**: +> - 📊 **资源视图**:按资源维度展示任务分配情况 +> - 🎯 **负载分析**:实时显示资源占用率和超载状态 +> - ⚠️ **冲突检测**:自动检测资源时间冲突(如 A任务:40% + B任务:40% + C任务:30% = 110%超载) +> - 🎨 **可视化**:斜纹背景标识冲突区域,资源Tab显示占用比例 +> - 🔄 **跨资源移动**:支持拖拽任务到不同资源行进行重新分配 +> +> **视图限制**: +> - ❌ **任务关系线禁用**:资源视图下不显示任务之间的前后置关系线,因为资源视图关注资源分配而非任务依赖关系 +> - ❌ **不支持实际TaskBar**:`showActualTaskbar` 属性在资源视图下无效,不会显示实际执行进度条 + +#### Resource 数据结构 + +| 字段名 | 类型 | 必填 | 默认值 | 说明 | +| --------------- | ------------------- | ---- | ------ | ----------------------------------------------------------------------------------------------- | +| `id` | `string \| number` | ✅ | - | 资源唯一标识符 | +| `name` | `string` | ✅ | - | 资源名称(如人名、设备名) | +| `type` | `string` | - | - | 资源类型(如 'developer', 'designer', 'device' 等) | +| `avatar` | `string` | - | - | 资源头像 URL | +| `description` | `string` | - | - | 资源描述 | +| `department` | `string` | - | - | 所属部门 | +| `skills` | `string[]` | - | - | 技能标签数组(如 `['Vue', 'React', 'TypeScript']`) | +| `capacity` | `number` | - | - | 资源容量/利用率(0-100),可用于表示资源的整体负载水平 | +| `color` | `string` | - | - | 自定义资源行左边框颜色(如 `'#ff5733'`),若不设置则使用默认颜色方案 | +| `tasks` | `Task[]` | - | `[]` | 分配给该资源的任务数组,**每个任务需包含 `resources` 字段标注资源占用比例** | +| `[key: string]` | `unknown` | - | - | 支持自定义属性扩展,可添加任意额外字段 | + +> **自定义属性扩展**:Resource 接口支持添加任意自定义字段,例如:`email`、`phone`、`location`、`workHours` 等业务相关字段。 +> +> **任务资源关联说明**: +> +> - 每个 Resource 包含一个 `tasks` 数组,存储分配给该资源的任务 +> - 每个 Task 应包含 `resources` 字段,标注该任务使用了哪些资源及占用比例 +> - 资源占用比例格式:`task.resources = [{ id: 'resource1', capacity: 60 }, { id: 'resource2', capacity: 40 }]` +> - `capacity` 范围:20-100,表示该任务占用该资源的百分比 +> - 冲突检测:当同一资源在同一时间段的多个任务 `capacity` 总和 > 100% 时,会显示冲突警告 + +**Resource 数据示例**: + +```typescript +import type { Resource, Task } from 'jordium-gantt-vue3' + +const resources: Resource[] = [ + { + id: 'dev-001', + name: '张三', + type: 'developer', + avatar: '/avatars/zhangsan.jpg', + department: '研发部', + skills: ['Vue', 'TypeScript', 'Node.js'], + capacity: 85, // 整体负载水平 + color: '#409eff', + tasks: [ + { + id: 1, + name: '前端开发', + startDate: '2026-02-01', + endDate: '2026-02-10', + progress: 50, + resources: [ + { id: 'dev-001', capacity: 60 }, // 该任务占用张三60%的时间 + { id: 'dev-002', capacity: 40 } // 同时占用李四40%的时间 + ] + }, + { + id: 2, + name: '代码审查', + startDate: '2026-02-05', + endDate: '2026-02-08', + progress: 0, + resources: [ + { id: 'dev-001', capacity: 40 } // 该任务占用张三40%的时间 + ] + } + // 注意:如果两个任务时间重叠,张三在2月5-8日的总占用率为100%(60%+40%),临界值 + ] + }, + { + id: 'dev-002', + name: '李四', + type: 'developer', + avatar: '/avatars/lisi.jpg', + department: '研发部', + skills: ['React', 'TypeScript'], + tasks: [] + } +] +``` + +**资源冲突检测示例**: + +```typescript +// 场景:张三在同一时间段被分配了3个任务 +const resource = { + id: 'dev-001', + name: '张三', + tasks: [ + { + id: 1, + name: '任务A', + startDate: '2026-02-10', + endDate: '2026-02-15', + resources: [{ id: 'dev-001', capacity: 40 }] // 占用40% + }, + { + id: 2, + name: '任务B', + startDate: '2026-02-10', + endDate: '2026-02-20', + resources: [{ id: 'dev-001', capacity: 40 }] // 占用40% + }, + { + id: 3, + name: '任务C', + startDate: '2026-02-12', + endDate: '2026-02-18', + resources: [{ id: 'dev-001', capacity: 30 }] // 占用30% + } + ] +} + +// 冲突分析: +// - 2月10-11日:A(40%) + B(40%) = 80%,未超载 +// - 2月12-15日:A(40%) + B(40%) + C(30%) = 110%,超载!显示冲突警告 +// - 2月16-18日:B(40%) + C(30%) = 70%,未超载 +// - 2月19-20日:B(40%),未超载 +``` + +#### 资源相关属性 + +| 属性名 | 类型 | 默认值 | 说明 | +| --------------------- | --------------------- | ------------ | --------------------------------------------------------------------------- | +| `resources` | `Resource[]` | `[]` | 资源数据数组 | +| `viewMode` | `'task' \| 'resource'` | `'task'` | 视图模式:'task' 任务计划视图,'resource' 资源计划视图 | +| `resourceListConfig` | `ResourceListConfig` | `undefined` | 资源列表配置,类似 TaskListConfig,用于配置资源列表的列定义、宽度等 | +| `showConflicts` | `boolean` | `true` | 是否显示资源冲突可视化层(资源视图下显示斜纹背景标识超载区域) | +| `showTaskbarTab` | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar上的资源占用比例标签) | + +#### 资源事件 + +| 事件名 | 参数 | 触发时机 | 说明 | +| -------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- | +| `taskbar-resource-change` | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | 任务跨资源拖拽结束时 | 资源视图下拖拽任务到另一资源行时触发,组件已自动更新任务的 `resources` 字段 | + +**数据同步说明**: + +- ✅ **组件内部自动更新**:资源相关操作(如任务跨资源移动)组件会自动更新 `props.resources` 和任务的 `resources` 字段 +- ✅ **事件仅做通知**:外部监听事件主要用于:显示提示消息、调用后端 API、更新其他相关数据等 +- ❌ **避免重复操作**:不要在事件处理器中再次修改数据,否则会导致重复更新 + +#### 示例:资源视图基础用法 + +```vue + + + +``` + ### 里程碑管理 里程碑用于标记项目中的重要时间节点,如项目启动、阶段完成、产品发布等。组件提供了灵活的里程碑编辑配置,默认使用内置的 MilestoneDialog,也支持完全自定义编辑行为。 diff --git a/demo/App.vue b/demo/App.vue index 3ffc4e9..a4071c0 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -39,6 +39,7 @@ const tasks = ref([]) const milestones = ref([]) const resources = ref([]) const viewMode = ref<'task' | 'resource'>('task') +const useDefaultDrawer = ref(true) const rawDataSources = [ { @@ -202,6 +203,16 @@ const isMilestoneEditMode = ref(false) // 版本历史Drawer状态 const showVersionDrawer = ref(false) +// v1.9.7 资源编辑提示dialog状态 +const resourceEditHintVisible = ref(false) +const clickedResource = ref(null) + +// v1.9.7 监听viewMode变化,自动同步useDefaultDrawer +// 资源视图下关闭默认TaskDrawer,任务视图下开启 +watch(viewMode, (newMode) => { + useDefaultDrawer.value = newMode !== 'resource' +}) + const toolbarConfig = { showAddTask: true, showAddMilestone: true, @@ -615,6 +626,28 @@ const handleTaskClick = (task: Task) => { showTaskClickDialog.value = true } +// v1.9.7 处理任务双击事件(资源视图下显示资源编辑提示) +const handleTaskDoubleClick = (taskOrResource: Task | Resource) => { + // 在资源视图下,检查是否为资源对象(Resource类型有tasks属性) + if (viewMode.value === 'resource' && taskOrResource && typeof taskOrResource === 'object' && 'tasks' in taskOrResource) { + clickedResource.value = taskOrResource as Resource + resourceEditHintVisible.value = true + } + // 注意:useDefaultDrawer由watch(viewMode)自动管理,无需在此处手动设置 +} + +// 关闭资源编辑提示dialog +const closeResourceEditHint = () => { + resourceEditHintVisible.value = false + clickedResource.value = null +} + +// v1.9.7 处理视图模式变化事件,同步GanttChart内部状态 +const handleViewModeChanged = (newMode: 'task' | 'resource') => { + viewMode.value = newMode + // useDefaultDrawer会由watch(viewMode)自动更新 +} + // 关闭 Task Click Dialog const closeTaskClickDialog = () => { showTaskClickDialog.value = false @@ -911,6 +944,9 @@ onMounted(() => { nextTick(() => { updateStatus() }) + + // v1.9.7 监听资源双击事件 + //window.addEventListener('task-row-double-click', handleResourceDoubleClick as EventListener) }) // 清理被删除任务的predecessor依赖关系 @@ -1107,6 +1143,23 @@ const confirmResourceDrag = () => { destResource.tasks.push(task) } + // v1.9.7 Bug修复:同步更新task.resources数组,确保资源分配信息正确显示 + if (task.resources) { + // 移除旧的资源分配 + const oldResourceIndex = task.resources.findIndex((r: any) => String(r.id) === String(sourceResource.id)) + if (oldResourceIndex !== -1) { + const oldCapacity = task.resources[oldResourceIndex].capacity || 100 + task.resources.splice(oldResourceIndex, 1) + + // 添加新的资源分配(保持原有的capacity) + task.resources.push({ + id: targetResource.id, + name: targetResource.name, + capacity: oldCapacity + }) + } + } + // 显示资源分配和日期更新信息 const dateInfo = newStartDate && newEndDate ? `,日期已调整为 ${newStartDate} ~ ${newEndDate}` : '' showMessage(`任务 "${task.name}" 已分配给资源 "${targetResource.name}"${dateInfo}`, 'success') @@ -1147,6 +1200,58 @@ const cancelResourceDrag = () => { } } +// v1.9.7 处理资源双击事件(仅在资源视图下触发) +// const handleResourceDoubleClick = (event: CustomEvent) => { +// // 只在资源视图下处理 +// if (viewMode.value !== 'resource') { +// return +// } + +// const item = event.detail +// // 检查是否为资源(Resource类型) +// if (item && typeof item === 'object' && 'tasks' in item) { +// clickedResource.value = item as Resource +// resourceEditHintVisible.value = true +// } +// } + +// 关闭资源编辑提示dialog +// const closeResourceEditHint = () => { +// resourceEditHintVisible.value = false +// clickedResource.value = null + +// // 重置拖拽数据 +// resourceDragData.value = { +// task: null, +// sourceResourceIndex: -1, +// targetResourceIndex: -1, +// targetResource: null, +// newStartDate: undefined, +// newEndDate: undefined +// } +// } + +// v1.9.7 处理资源双击事件(仅在资源视图下触发) +// const handleResourceDoubleClick = (event: CustomEvent) => { +// // 只在资源视图下处理 +// if (viewMode.value !== 'resource') { +// return +// } + +// const item = event.detail +// // 检查是否为资源(Resource类型) +// if (item && typeof item === 'object' && 'tasks' in item) { +// clickedResource.value = item as Resource +// resourceEditHintVisible.value = true +// } +// } + +// // 关闭资源编辑提示dialog +// const closeResourceEditHint = () => { +// resourceEditHintVisible.value = false +// clickedResource.value = null +// } + // 自定义右键菜单操作处理 const handleCustomMenuAction = (action: string, task: Task) => { showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true }) @@ -1175,7 +1280,7 @@ const handleCustomMenuAction = (action: string, task: Task) => { packageInfo.version }} -

+
+ + + @@ -3849,7 +3842,7 @@ class="hover-tooltip-arrow" :style="{ top: 0; left: 0; right: 0; - height: calc((1 - var(--allocation-percent, 1)) * 100%); + height: calc((1 - var(--allocation-capacity, 1)) * 100%); border-top: 1.5px dashed currentColor; border-left: 1.5px dashed currentColor; border-right: 1.5px dashed currentColor; @@ -3863,7 +3856,7 @@ class="hover-tooltip-arrow" :style="{ } /* 占比100%时,隐藏上半部分镂空区域 */ -.task-bar.resource-view[style*="--allocation-percent: 1"]::before { +.task-bar.resource-view[style*="--allocation-capacity: 1"]::before { display: none; } @@ -3874,7 +3867,7 @@ class="hover-tooltip-arrow" :style="{ left: 0; right: 0; bottom: 0; - height: calc(var(--allocation-percent, 1) * 100%); + height: calc(var(--allocation-capacity, 1) * 100%); background: var(--task-bar-bg-color, #e3f2fd); /*border: 1px solid var(--task-bar-border-color, #90caf9);*/ border-radius: 0 0 4px 4px; @@ -3885,7 +3878,7 @@ class="hover-tooltip-arrow" :style="{ } /* 占比100%时,整个TaskBar都是实心,四个角圆角 */ -.task-bar.resource-view[style*="--allocation-percent: 1"]::after { +.task-bar.resource-view[style*="--allocation-capacity: 1"]::after { border-radius: 4px; } @@ -3895,7 +3888,7 @@ class="hover-tooltip-arrow" :style="{ bottom: 0; left: 0; top: auto; - height: calc(var(--allocation-percent, 1) * 100%); + height: calc(var(--allocation-capacity, 1) * 100%); z-index: 1; border-radius: 0 0 4px 4px; pointer-events: none; @@ -3903,7 +3896,7 @@ class="hover-tooltip-arrow" :style="{ } /* 占比100%时,进度条四个角圆角 */ -.task-bar.resource-view[style*="--allocation-percent: 1"] .progress-bar { +.task-bar.resource-view[style*="--allocation-capacity: 1"] .progress-bar { border-radius: 4px; } @@ -4380,7 +4373,7 @@ class="hover-tooltip-arrow" :style="{ } /* v1.9.0 资源占比文字样式 */ -.resource-percent-text { +.resource-capacity-text { display: inline-block; margin-left: 6px; font-size: 12px; @@ -4399,7 +4392,7 @@ class="hover-tooltip-arrow" :style="{ .task-bar[style*="width: 28px"], .task-bar[style*="width: 32px"], .task-bar[style*="width: 36px"] { - .resource-percent-text { + .resource-capacity-text { display: none; } } diff --git a/src/components/TaskDrawer.vue b/src/components/TaskDrawer.vue index e2a640d..8881ed1 100644 --- a/src/components/TaskDrawer.vue +++ b/src/components/TaskDrawer.vue @@ -7,6 +7,7 @@ import GanttConfirmDialog from './GanttConfirmDialog.vue' import MultiSelectPredecessor from './MultiSelectPredecessor.vue' import ConfirmTimerDialog from './ConfirmTimerDialog.vue' import type { Task } from '../models/classes/Task' +import { Resource } from '../models/classes/Resource' import '../styles/app.css' interface AssigneeOption { @@ -197,7 +198,7 @@ const getTaskTypeDisplay = (type: string): string => { const progressSliderStyle = computed(() => { const progressPercent = formData.progress || 0 return { - '--progress-percent': `${progressPercent}%`, + '--progress-capacity': `${progressPercent}%`, } }) @@ -500,6 +501,7 @@ const handleSubmit = async () => { const taskData: Task = { ...formData, id: props.isEdit && props.task ? props.task.id : Date.now(), + resources: formData.resources, // 确保提交 resources 字段 } emit('submit', taskData) handleClose() @@ -680,11 +682,12 @@ const addResource = () => { if (!formData.resources) { formData.resources = [] } - formData.resources.push({ + formData.resources.push(new Resource({ id: '', name: '', - percent: 100, - }) + capacity: 100, + tasks: [], + })) } const removeResource = (index: number) => { @@ -693,7 +696,7 @@ const removeResource = (index: number) => { } } -const handleResourceChange = (index: number, field: 'id' | 'percent', value: string | number) => { +const handleResourceChange = (index: number, field: 'id' | 'capacity', value: string | number) => { if (!formData.resources || !formData.resources[index]) return if (field === 'id') { @@ -702,12 +705,12 @@ const handleResourceChange = (index: number, field: 'id' | 'percent', value: str formData.resources[index].id = selected.value formData.resources[index].name = selected.label } - } else if (field === 'percent') { - let percent = typeof value === 'string' ? parseInt(value) : value + } else if (field === 'capacity') { + let capacity = typeof value === 'string' ? parseInt(value) : value // 校验范围 20-100 - if (percent < 20) percent = 20 - if (percent > 100) percent = 100 - formData.resources[index].percent = percent + if (capacity < 20) capacity = 20 + if (capacity > 100) capacity = 100 + formData.resources[index].capacity = capacity } } @@ -728,11 +731,12 @@ const mapAssigneeToResources = () => { formData.resources = assignees.map((assigneeId) => { // 查找对应的assigneeOption获取名称 const option = props.assigneeOptions?.find(opt => opt.value === assigneeId) - return { + return new Resource({ id: assigneeId, name: option?.label || String(assigneeId), - percent: 100, // 默认100% - } + capacity: 100, // 默认100% + tasks: [], + }) }) } @@ -1026,9 +1030,9 @@ const taskStatus = computed(() => {