v1.9.0-rc.6 - SIT bugfix

This commit is contained in:
LINING-PC\lining
2026-02-03 21:51:10 +08:00
parent 9d5cdef835
commit 6e727fea3f
30 changed files with 3377 additions and 2984 deletions
+215 -5
View File
@@ -29,11 +29,11 @@
<p align="center">A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling</p> <p align="center">A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling</p>
<p align="center"> <!-- <p align="center">
<a href="https://gitee.com/activity/2025opensource?ident=IOUNZP" target="_blank"> <a href="https://gitee.com/activity/2025opensource?ident=IOUNZP" target="_blank">
<img src="https://img.shields.io/badge/🥇_Gitee_2025_Open_Source_Awards-👉_Thanks_for_Your_Vote-gold?style=for-the-badge&labelColor=C71D23&logoColor=white" alt="Gitee 2025 Open Source Project Awards" height="40"> <img src="https://img.shields.io/badge/🥇_Gitee_2025_Open_Source_Awards-👉_Thanks_for_Your_Vote-gold?style=for-the-badge&labelColor=C71D23&logoColor=white" alt="Gitee 2025 Open Source Project Awards" height="40">
</a> </a>
</p> </p> -->
<p align="center"> <p align="center">
<a href="https://jordium.gitee.io/jordium-gantt-vue3/"> <a href="https://jordium.gitee.io/jordium-gantt-vue3/">
@@ -72,11 +72,15 @@ jordium-gantt-vue3 is a modern Gantt chart component built with Vue 3 and TypeSc
#### Light Theme #### Light Theme
<img src="design/screenshots/light-theme.png" alt="Light Theme" width="100%"> <img src="design/screenshots/light-theme-en.png" alt="Light Theme" width="100%">
<img src="design/screenshots/light-theme-en1.png" alt="Light Theme" width="100%">
#### Dark Theme #### Dark Theme
<img src="design/screenshots/dark-theme.png" alt="Dark Theme" width="100%"> <img src="design/screenshots/dark-theme-en.png" alt="Dark Theme" width="100%">
<img src="design/screenshots/dark-theme-en1.png" alt="Dark Theme" width="100%">
--- ---
@@ -195,6 +199,8 @@ npm run dev
| --------------------------- | --------- | ------- | ------------------------------------------------------------------------- | | --------------------------- | --------- | ------- | ------------------------------------------------------------------------- |
| `tasks` | `Task[]` | `[]` | Array of task data | | `tasks` | `Task[]` | `[]` | Array of task data |
| `milestones` | `Task[]` | `[]` | Array of milestone data (Note: Type is Task[], must set type='milestone') | | `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 | | `showToolbar` | `boolean` | `true` | Whether to show the toolbar |
| `useDefaultDrawer` | `boolean` | `true` | Whether to use the built-in task edit drawer (TaskDrawer) | | `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) | | `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use the built-in milestone edit dialog (MilestoneDialog) |
@@ -216,6 +222,8 @@ npm run dev
| `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 | | `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) | | `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 #### TaskListColumn Component Props
@@ -362,6 +370,7 @@ For complete configuration object documentation, see [⚙️ Configuration & Cus
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------------------- | | ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------------------- |
| `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration | | `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration |
| `taskListConfig` | `TaskListConfig` | `undefined` | Task list 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 | | `taskBarConfig` | `TaskBarConfig` | `undefined` | Task bar style configuration |
| `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | Custom localization messages | | `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | Custom localization messages |
| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | Working hours configuration | | `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-icon-changed` | `{ milestoneId, icon }` | Milestone icon changed |
| `milestone-drag-end` | `(milestone: Task)` | Milestone drag ended | | `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) | | `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 #### Example 1: Simplest Gantt Chart
@@ -1100,7 +1110,207 @@ const handleTaskRowMoved = async (payload: {
- Cannot drag onto its own child tasks (avoid circular reference) - Cannot drag onto its own child tasks (avoid circular reference)
- Milestones and milestone groups cannot be dragged - 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
<template>
<div style="height: 600px;">
<GanttChart
:resources="resources"
view-mode="resource"
:show-conflicts="true"
:show-taskbar-tab="true"
@taskbar-resource-change="handleTaskbarResourceChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { GanttChart } from 'jordium-gantt-vue3'
import type { Resource } from 'jordium-gantt-vue3'
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const resources = ref<Resource[]>([
{
id: 'dev-001',
name: 'Zhang San',
type: 'developer',
department: 'R&D',
tasks: [
{
id: 1,
name: 'Frontend Development',
startDate: '2026-02-01',
endDate: '2026-02-10',
progress: 50,
resources: [{ id: 'dev-001', capacity: 60 }]
}
]
}
])
const handleTaskbarResourceChange = (payload: any) => {
console.log('Task resource changed:', payload)
// Call backend API to save resource allocation change
// api.updateTaskResource(payload.task.id, payload.newResourceId)
}
</script>
```
### Milestone Management ### Milestone Management
+214 -2
View File
@@ -29,11 +29,11 @@
<p align="center">现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案</p> <p align="center">现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案</p>
<p align="center"> <!-- <p align="center">
<a href="https://gitee.com/activity/2025opensource?ident=IOUNZP" target="_blank"> <a href="https://gitee.com/activity/2025opensource?ident=IOUNZP" target="_blank">
<img src="https://img.shields.io/badge/🥇_Gitee_2025年度开源项目评选-👉_感谢您的参与投票-gold?style=for-the-badge&labelColor=C71D23&logoColor=white" alt="Gitee 2025年度开源项目评选" height="40"> <img src="https://img.shields.io/badge/🥇_Gitee_2025年度开源项目评选-👉_感谢您的参与投票-gold?style=for-the-badge&labelColor=C71D23&logoColor=white" alt="Gitee 2025年度开源项目评选" height="40">
</a> </a>
</p> </p> -->
<p align="center"> <p align="center">
<a href="https://jordium.gitee.io/jordium-gantt-vue3/"> <a href="https://jordium.gitee.io/jordium-gantt-vue3/">
@@ -74,10 +74,14 @@ jordium-gantt-vue3 是一个基于 Vue 3 和 TypeScript 开发的现代化甘特
<img src="design/screenshots/light-theme.png" alt="亮色主题" width="100%"> <img src="design/screenshots/light-theme.png" alt="亮色主题" width="100%">
<img src="design/screenshots/light-theme1.png" alt="亮色主题" width="100%">
#### 暗色主题 #### 暗色主题
<img src="design/screenshots/dark-theme.png" alt="暗色主题" width="100%"> <img src="design/screenshots/dark-theme.png" alt="暗色主题" width="100%">
<img src="design/screenshots/dark-theme1.png" alt="暗色主题" width="100%">
--- ---
## 📦 安装 ## 📦 安装
@@ -193,6 +197,8 @@ npm run dev
| --------------------------- | ----------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- | | --------------------------- | ----------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- |
| `tasks` | `Task[]` | `[]` | 任务数据数组 | | `tasks` | `Task[]` | `[]` | 任务数据数组 |
| `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone' | | `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` | 是否显示工具栏 | | `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) | | `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) |
| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog | | `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog |
@@ -214,6 +220,8 @@ npm run dev
| `ongoingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#e6a23c'` | 进行中任务的TaskBar背景色。支持十六进制颜色值(如 `'#e6a23c'`)。**优先级**:高于系统默认,低于 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下方显示实际执行进度) | | `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 属性 #### TaskListColumn 属性
@@ -360,6 +368,7 @@ npm run dev
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------- | | ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------- |
| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 | | `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 |
| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置 | | `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` | 任务条样式配置 | | `taskBarConfig` | `TaskBarConfig` | `undefined` | 任务条样式配置 |
| `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | 自定义多语言配置 | | `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | 自定义多语言配置 |
| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | 工作时间配置 | | `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-icon-changed` | `{ milestoneId, icon }` | 里程碑图标变更 |
| `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 | | `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 |
| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | 拖拽TaskRow结束(可选) | | `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:最简单的甘特图 #### 示例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
<template>
<div style="height: 600px;">
<GanttChart
:resources="resources"
view-mode="resource"
:show-conflicts="true"
:show-taskbar-tab="true"
@taskbar-resource-change="handleTaskbarResourceChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { GanttChart } from 'jordium-gantt-vue3'
import type { Resource } from 'jordium-gantt-vue3'
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const resources = ref<Resource[]>([
{
id: 'dev-001',
name: '张三',
type: 'developer',
department: '研发部',
tasks: [
{
id: 1,
name: '前端开发',
startDate: '2026-02-01',
endDate: '2026-02-10',
progress: 50,
resources: [{ id: 'dev-001', capacity: 60 }]
}
]
}
])
const handleTaskbarResourceChange = (payload: any) => {
console.log('任务资源变更:', payload)
// 调用后端API保存资源分配变更
// api.updateTaskResource(payload.task.id, payload.newResourceId)
}
</script>
```
### 里程碑管理 ### 里程碑管理
里程碑用于标记项目中的重要时间节点,如项目启动、阶段完成、产品发布等。组件提供了灵活的里程碑编辑配置,默认使用内置的 MilestoneDialog,也支持完全自定义编辑行为。 里程碑用于标记项目中的重要时间节点,如项目启动、阶段完成、产品发布等。组件提供了灵活的里程碑编辑配置,默认使用内置的 MilestoneDialog,也支持完全自定义编辑行为。
+129 -4
View File
@@ -39,6 +39,7 @@ const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([]) const milestones = ref<Task[]>([])
const resources = ref<Resource[]>([]) const resources = ref<Resource[]>([])
const viewMode = ref<'task' | 'resource'>('task') const viewMode = ref<'task' | 'resource'>('task')
const useDefaultDrawer = ref(true)
const rawDataSources = [ const rawDataSources = [
{ {
@@ -202,6 +203,16 @@ const isMilestoneEditMode = ref(false)
// Drawer // Drawer
const showVersionDrawer = ref(false) const showVersionDrawer = ref(false)
// v1.9.7 dialog
const resourceEditHintVisible = ref(false)
const clickedResource = ref<Resource | null>(null)
// v1.9.7 viewModeuseDefaultDrawer
// TaskDrawer
watch(viewMode, (newMode) => {
useDefaultDrawer.value = newMode !== 'resource'
})
const toolbarConfig = { const toolbarConfig = {
showAddTask: true, showAddTask: true,
showAddMilestone: true, showAddMilestone: true,
@@ -615,6 +626,28 @@ const handleTaskClick = (task: Task) => {
showTaskClickDialog.value = true showTaskClickDialog.value = true
} }
// v1.9.7
const handleTaskDoubleClick = (taskOrResource: Task | Resource) => {
// Resourcetasks
if (viewMode.value === 'resource' && taskOrResource && typeof taskOrResource === 'object' && 'tasks' in taskOrResource) {
clickedResource.value = taskOrResource as Resource
resourceEditHintVisible.value = true
}
// useDefaultDrawerwatch(viewMode)
}
// dialog
const closeResourceEditHint = () => {
resourceEditHintVisible.value = false
clickedResource.value = null
}
// v1.9.7 GanttChart
const handleViewModeChanged = (newMode: 'task' | 'resource') => {
viewMode.value = newMode
// useDefaultDrawerwatch(viewMode)
}
// Task Click Dialog // Task Click Dialog
const closeTaskClickDialog = () => { const closeTaskClickDialog = () => {
showTaskClickDialog.value = false showTaskClickDialog.value = false
@@ -911,6 +944,9 @@ onMounted(() => {
nextTick(() => { nextTick(() => {
updateStatus() updateStatus()
}) })
// v1.9.7
//window.addEventListener('task-row-double-click', handleResourceDoubleClick as EventListener)
}) })
// predecessor // predecessor
@@ -1107,6 +1143,23 @@ const confirmResourceDrag = () => {
destResource.tasks.push(task) destResource.tasks.push(task)
} }
// v1.9.7 Bugtask.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}` : '' const dateInfo = newStartDate && newEndDate ? `,日期已调整为 ${newStartDate} ~ ${newEndDate}` : ''
showMessage(`任务 "${task.name}" 已分配给资源 "${targetResource.name}"${dateInfo}`, 'success') 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) => { const handleCustomMenuAction = (action: string, task: Task) => {
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true }) showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
@@ -1175,7 +1280,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
packageInfo.version packageInfo.version
}}</span> }}</span>
</div> </div>
<div class="title-center"> <!-- <div class="title-center">
<a <a
:href="demoMessages.giteeBadge?.url || 'https://gitee.com/activity/2025opensource?ident=IOUNZP'" :href="demoMessages.giteeBadge?.url || 'https://gitee.com/activity/2025opensource?ident=IOUNZP'"
target="_blank" target="_blank"
@@ -1184,7 +1289,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
> >
{{ demoMessages.giteeBadge?.text || '🥇 Gitee 2025 Open Source Awards 👉 Thanks for Your Vote' }} {{ demoMessages.giteeBadge?.text || '🥇 Gitee 2025 Open Source Awards 👉 Thanks for Your Vote' }}
</a> </a>
</div> </div> -->
<div class="title-right docs-links"> <div class="title-right docs-links">
<a href="https://www.npmjs.com/package/jordium-gantt-vue3"> <a href="https://www.npmjs.com/package/jordium-gantt-vue3">
<img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version"> <img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version">
@@ -2114,6 +2219,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
:delay-task-background-color="delayTaskBackgroundColor" :delay-task-background-color="delayTaskBackgroundColor"
:complete-task-background-color="completeTaskBackgroundColor" :complete-task-background-color="completeTaskBackgroundColor"
:ongoing-task-background-color="ongoingTaskBackgroundColor" :ongoing-task-background-color="ongoingTaskBackgroundColor"
:use-default-drawer="useDefaultDrawer"
@milestone-saved="handleMilestoneSaved" @milestone-saved="handleMilestoneSaved"
@milestone-deleted="handleMilestoneDeleted" @milestone-deleted="handleMilestoneDeleted"
@milestone-icon-changed="handleMilestoneIconChanged" @milestone-icon-changed="handleMilestoneIconChanged"
@@ -2122,6 +2228,8 @@ const handleCustomMenuAction = (action: string, task: Task) => {
@taskbar-resize-end="handleTaskbarDragOrResizeEnd" @taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd" @milestone-drag-end="handleMilestoneDragEnd"
@task-click="handleTaskClick" @task-click="handleTaskClick"
@task-double-click="handleTaskDoubleClick"
@view-mode-changed="handleViewModeChanged"
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)" @edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
@close="() => showMessage('已关闭任务编辑', 'info')" @close="() => showMessage('已关闭任务编辑', 'info')"
@timer-started="onTimerStarted" @timer-started="onTimerStarted"
@@ -2138,8 +2246,6 @@ const handleCustomMenuAction = (action: string, task: Task) => {
@task-added="handleTaskAddEvent" @task-added="handleTaskAddEvent"
@task-updated="handleTaskUpdateEvent" @task-updated="handleTaskUpdateEvent"
@task-row-moved="handleTaskRowMoved" @task-row-moved="handleTaskRowMoved"
@view-mode-change="mode => viewMode = mode"
@resource-click="resource => showMessage(`资源点击: ${resource.name}`, 'info')"
@resource-drag-end="handleResourceDragEnd" @resource-drag-end="handleResourceDragEnd"
> >
<!-- 自定义任务名称内容 (TaskRow TaskBar) --> <!-- 自定义任务名称内容 (TaskRow TaskBar) -->
@@ -2339,6 +2445,25 @@ const handleCustomMenuAction = (action: string, task: Task) => {
</div> </div>
</div> </div>
<!-- v1.9.7 资源编辑提示对话框 -->
<div v-if="resourceEditHintVisible" class="modal-overlay" @click.self="closeResourceEditHint">
<div class="resource-drag-dialog">
<div class="resource-drag-dialog-header">
<h3>{{ demoMessages.resourceEditHint?.title || '资源编辑提示' }}</h3>
</div>
<div class="resource-drag-dialog-body">
<p style="white-space: pre-line; line-height: 1.6;">
{{ (demoMessages.resourceEditHint?.message || '').replace('{resourceName}', clickedResource?.name || '') }}
</p>
</div>
<div class="resource-drag-dialog-footer">
<button class="confirm-button" @click="closeResourceEditHint">
{{ demoMessages.resourceEditHint?.confirmText || '知道了' }}
</button>
</div>
</div>
</div>
<!-- v1.9.0 资源拖拽确认对话框 --> <!-- v1.9.0 资源拖拽确认对话框 -->
<div v-if="resourceDragConfirmVisible" class="modal-overlay" @click.self="cancelResourceDrag"> <div v-if="resourceDragConfirmVisible" class="modal-overlay" @click.self="cancelResourceDrag">
<div class="resource-drag-dialog"> <div class="resource-drag-dialog">
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -18,7 +18,7 @@
"progress": 15, "progress": 15,
"type": "story", "type": "story",
"resources": [ "resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 100 } { "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 100 }
] ]
}, },
{ {
@@ -29,7 +29,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 75 } { "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 75 }
] ]
}, },
{ {
@@ -40,7 +40,7 @@
"progress": 80, "progress": 80,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 50 } { "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 50 }
] ]
} }
] ]
@@ -63,7 +63,7 @@
"progress": 65, "progress": 65,
"type": "story", "type": "story",
"resources": [ "resources": [
{ "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "percent": 100 } { "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "capacity": 100 }
] ]
}, },
{ {
@@ -74,7 +74,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "percent": 75 } { "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "capacity": 75 }
] ]
}, },
{ {
@@ -85,7 +85,7 @@
"progress": 45, "progress": 45,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "percent": 50 } { "id": "resource-wang", "name": "I期试验主任 Dr. Wang", "capacity": 50 }
] ]
} }
] ]
@@ -107,7 +107,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-li", "name": "方案设计师 李明", "percent": 100 } { "id": "resource-li", "name": "方案设计师 李明", "capacity": 100 }
] ]
}, },
{ {
@@ -119,7 +119,7 @@
"type": "task", "type": "task",
"barColor": "#7B3FF2", "barColor": "#7B3FF2",
"resources": [ "resources": [
{ "id": "resource-li", "name": "方案设计师 李明", "percent": 75 } { "id": "resource-li", "name": "方案设计师 李明", "capacity": 75 }
] ]
} }
] ]
@@ -142,7 +142,7 @@
"progress": 50, "progress": 50,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 100 } { "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 100 }
] ]
}, },
{ {
@@ -153,7 +153,7 @@
"progress": 70, "progress": 70,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 50 } { "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 50 }
] ]
}, },
{ {
@@ -164,7 +164,7 @@
"progress": 25, "progress": 25,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 75 } { "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 75 }
] ]
} }
] ]
@@ -296,7 +296,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 100 } { "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 100 }
] ]
}, },
{ {
@@ -307,7 +307,7 @@
"progress": 75, "progress": 75,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 75 } { "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 75 }
] ]
}, },
{ {
@@ -318,7 +318,7 @@
"progress": 85, "progress": 85,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 50 } { "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 50 }
] ]
}, },
{ {
@@ -329,7 +329,7 @@
"progress": 30, "progress": 30,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 100 } { "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 100 }
] ]
} }
] ]
@@ -352,7 +352,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 100 } { "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 100 }
] ]
}, },
{ {
@@ -363,7 +363,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 75 } { "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 75 }
] ]
}, },
{ {
@@ -374,7 +374,7 @@
"progress": 90, "progress": 90,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 50 } { "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 50 }
] ]
}, },
{ {
@@ -385,7 +385,7 @@
"progress": 70, "progress": 70,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 25 } { "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 25 }
] ]
} }
] ]
@@ -408,7 +408,7 @@
"progress": 100, "progress": 100,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-xu", "name": "医学撰写员 徐静", "percent": 80 } { "id": "resource-xu", "name": "医学撰写员 徐静", "capacity": 80 }
] ]
}, },
{ {
@@ -419,7 +419,7 @@
"progress": 60, "progress": 60,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-xu", "name": "医学撰写员 徐静", "percent": 70 } { "id": "resource-xu", "name": "医学撰写员 徐静", "capacity": 70 }
] ]
}, },
{ {
@@ -430,7 +430,7 @@
"progress": 20, "progress": 20,
"type": "task", "type": "task",
"resources": [ "resources": [
{ "id": "resource-xu", "name": "医学撰写员 徐静", "percent": 60 } { "id": "resource-xu", "name": "医学撰写员 徐静", "capacity": 60 }
] ]
} }
] ]
+5
View File
@@ -3,6 +3,11 @@
"text": "🥇 Gitee 2025 Open Source Awards 👉 Thanks for Your Vote", "text": "🥇 Gitee 2025 Open Source Awards 👉 Thanks for Your Vote",
"url": "https://gitee.com/activity/2025opensource?ident=IOUNZP" "url": "https://gitee.com/activity/2025opensource?ident=IOUNZP"
}, },
"resourceEditHint": {
"title": "Resource Edit Hint",
"message": "The resource row has been double-clicked. This event can be customized to implement resource editing functionality.\n\nCurrent resource: {resourceName}\n\nYou can implement resource property editing here, such as modifying resource name, capacity, cost, and other information.",
"confirmText": "Got it"
},
"taskMoveConfirm": { "taskMoveConfirm": {
"title": "Confirm Task Move", "title": "Confirm Task Move",
"confirmText": "Confirm", "confirmText": "Confirm",
+5
View File
@@ -3,6 +3,11 @@
"text": "🥇 Gitee 2025年度开源项目评选 👉 感谢您的参与投票", "text": "🥇 Gitee 2025年度开源项目评选 👉 感谢您的参与投票",
"url": "https://gitee.com/activity/2025opensource?ident=IOUNZP" "url": "https://gitee.com/activity/2025opensource?ident=IOUNZP"
}, },
"resourceEditHint": {
"title": "资源编辑提示",
"message": "资源条目被双击,此事件可以自定义完成资源的编辑需求。\n\n当前资源:{resourceName}\n\n您可以在此处实现资源属性编辑功能,例如修改资源名称、容量、成本等信息。",
"confirmText": "知道了"
},
"taskMoveConfirm": { "taskMoveConfirm": {
"title": "确认移动任务", "title": "确认移动任务",
"confirmText": "确认", "confirmText": "确认",
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

+2 -2
View File
@@ -30,7 +30,7 @@ function generateLargeResourceData() {
const endDate = new Date(startDate) const endDate = new Date(startDate)
endDate.setDate(endDate.getDate() + duration) endDate.setDate(endDate.getDate() + duration)
const percent = [25, 50, 75, 100][Math.floor(Math.random() * 4)] const capacity = [25, 50, 75, 100][Math.floor(Math.random() * 4)]
tasks.push({ tasks.push({
id: taskId, id: taskId,
@@ -43,7 +43,7 @@ function generateLargeResourceData() {
{ {
id: resourceId, id: resourceId,
name: `资源${i}`, name: `资源${i}`,
percent, capacity,
}, },
], ],
}) })
+50 -8
View File
@@ -93,6 +93,7 @@ const emit = defineEmits([
'task-updated', 'task-updated',
'task-collapse-change', // 'task-collapse-change', //
'link-deleted', // 'link-deleted', //
'view-mode-changed', //
// //
'add-task', 'add-task',
'add-milestone', 'add-milestone',
@@ -106,8 +107,6 @@ const emit = defineEmits([
// TaskRow // TaskRow
'task-row-moved', 'task-row-moved',
// v1.9.0 // v1.9.0
'view-mode-change', //
'resource-click', //
'taskbar-resource-change', // 'taskbar-resource-change', //
'resource-drag-end', // v1.9.0 'resource-drag-end', // v1.9.0
]) ])
@@ -152,7 +151,7 @@ const resourceTaskLayouts = computed(() => {
resources.forEach(resource => { resources.forEach(resource => {
const resourceId = String(resource.id) const resourceId = String(resource.id)
if (resource.tasks && resource.tasks.length > 0) { if (resource.tasks && resource.tasks.length > 0) {
const layout = assignTaskRows(resource.tasks, resourceId, baseRowHeight) const layout = assignTaskRows(resource.tasks, baseRowHeight)
layouts.set(resourceId, layout) layouts.set(resourceId, layout)
} else { } else {
// 使 // 使
@@ -195,7 +194,7 @@ const resourceConflicts = computed(() => {
if (currentViewMode.value !== 'resource') return new Map() if (currentViewMode.value !== 'resource') return new Map()
const resources = currentDataSource.value as Resource[] const resources = currentDataSource.value as Resource[]
const conflictsMap = new Map<string, Set<number>>() const conflictsMap = new Map<string, Set<number | string>>()
// updateTaskTrigger 便 // updateTaskTrigger 便
if (updateTaskTrigger.value >= 0) { if (updateTaskTrigger.value >= 0) {
@@ -208,7 +207,7 @@ const resourceConflicts = computed(() => {
const conflictZones = detectConflicts(tasks, resource.id) const conflictZones = detectConflicts(tasks, resource.id)
if (conflictZones.length > 0) { if (conflictZones.length > 0) {
const conflicts = new Set<number>() const conflicts = new Set<number | string>()
// ID // ID
conflictZones.forEach(zone => { conflictZones.forEach(zone => {
@@ -548,7 +547,8 @@ const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
const handleViewModeChange = (newMode: 'task' | 'resource') => { const handleViewModeChange = (newMode: 'task' | 'resource') => {
if (currentViewMode.value !== newMode) { if (currentViewMode.value !== newMode) {
currentViewMode.value = newMode currentViewMode.value = newMode
emit('view-mode-change', newMode) // v1.9.7 emit
emit('view-mode-changed', newMode)
} }
} }
@@ -558,6 +558,8 @@ watch(
newMode => { newMode => {
if (newMode && currentViewMode.value !== newMode) { if (newMode && currentViewMode.value !== newMode) {
currentViewMode.value = newMode currentViewMode.value = newMode
// v1.9.7 emit
emit('view-mode-changed', newMode)
} }
}, },
) )
@@ -2525,6 +2527,7 @@ function handleToolbarAddTask() {
description: '', description: '',
parentId: undefined, parentId: undefined,
children: [], children: [],
resources: [],
} }
taskDrawerTask.value = newTask taskDrawerTask.value = newTask
taskDrawerEditMode.value = false taskDrawerEditMode.value = false
@@ -2581,7 +2584,8 @@ function handleTimelineEditTask(task: Task) {
emit('task-double-click', task) emit('task-double-click', task)
// useDefaultDrawer TaskDrawer // useDefaultDrawer TaskDrawer
if (props.useDefaultDrawer) { // v1.9.7 TaskTaskDrawerResourceResourceid
if (props.useDefaultDrawer && typeof task.id === 'number') {
taskDrawerTask.value = task taskDrawerTask.value = task
taskDrawerEditMode.value = true taskDrawerEditMode.value = true
taskDrawerVisible.value = true taskDrawerVisible.value = true
@@ -2614,6 +2618,7 @@ function handleAddPredecessor(targetTask: Task) {
description: '', description: '',
parentId: targetTask.parentId, parentId: targetTask.parentId,
children: [], children: [],
resources: [],
isTimerRunning: false, isTimerRunning: false,
timerStartTime: undefined, timerStartTime: undefined,
timerEndTime: undefined, timerEndTime: undefined,
@@ -2648,6 +2653,7 @@ function handleAddSuccessor(targetTask: Task) {
description: '', description: '',
parentId: targetTask.parentId, parentId: targetTask.parentId,
children: [], children: [],
resources: [],
isTimerRunning: false, isTimerRunning: false,
timerStartTime: undefined, timerStartTime: undefined,
timerEndTime: undefined, timerEndTime: undefined,
@@ -2739,6 +2745,40 @@ const updateTaskAndSyncToResources = (updatedTask: Task) => {
} }
} }
// v1.9.0
const addTaskToResource = (newTask: Task) => {
//
if (currentViewMode.value !== 'resource' || !props.resources) {
return
}
// 1. resources
if (newTask.resources && newTask.resources.length > 0) {
newTask.resources.forEach(resourceAlloc => {
const resource = props.resources.find(r => r.id === resourceAlloc.id)
if (resource) {
//
const exists = resource.tasks.find(t => t.id === newTask.id)
if (!exists) {
resource.tasks.push({ ...newTask })
}
}
})
}
// 2. assignee
else if (newTask.assignee) {
const assigneeId = Array.isArray(newTask.assignee) ? newTask.assignee[0] : newTask.assignee
const resource = props.resources.find(r => r.id === assigneeId)
if (resource) {
//
const exists = resource.tasks.find(t => t.id === newTask.id)
if (!exists) {
resource.tasks.push({ ...newTask })
}
}
}
}
// v1.9.0 timer // v1.9.0 timer
const updateTaskStateInTree = (taskId: number, updateFn: (task: Task) => void): boolean => { const updateTaskStateInTree = (taskId: number, updateFn: (task: Task) => void): boolean => {
const updateInList = (tasks: Task[]): boolean => { const updateInList = (tasks: Task[]): boolean => {
@@ -2779,6 +2819,9 @@ function handleTaskDrawerSubmit(task: Task) {
if (props.tasks) { if (props.tasks) {
insertTask(props.tasks, task) insertTask(props.tasks, task)
} }
// v1.9.0
addTaskToResource(task)
// emit // emit
emit('task-added', { task }) emit('task-added', { task })
if (taskToAddPredecessorTo.value) { if (taskToAddPredecessorTo.value) {
@@ -2984,7 +3027,6 @@ defineExpose({
@add-milestone="milestoneAddHandler" @add-milestone="milestoneAddHandler"
@expand-all="handleExpandAll" @expand-all="handleExpandAll"
@collapse-all="handleCollapseAll" @collapse-all="handleCollapseAll"
@view-mode-change="handleViewModeChange"
/> />
<!-- 甘特图主体 --> <!-- 甘特图主体 -->
+3 -4
View File
@@ -227,14 +227,13 @@ const handleCollapseAll = () => {
} }
} }
// v1.9.0 // v1.9.0 -
const handleViewModeChange = (mode: 'task' | 'resource') => { const handleViewModeChange = (mode: 'task' | 'resource') => {
if (currentViewMode.value !== mode) { if (currentViewMode.value !== mode) {
currentViewMode.value = mode currentViewMode.value = mode
// GanttChart
if (props.onViewModeChange && typeof props.onViewModeChange === 'function') { if (props.onViewModeChange && typeof props.onViewModeChange === 'function') {
props.onViewModeChange(mode) props.onViewModeChange(mode)
} else {
emit('view-mode-change', mode)
} }
} }
} }
@@ -608,7 +607,7 @@ onUnmounted(() => {
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg> </svg>
{{ t('resourceView') || '资源视图' }} {{ t('resourceView.desc') || '资源视图' }}
</button> </button>
</div> </div>
+15 -22
View File
@@ -64,8 +64,8 @@ const resourcePercent = computed(() => {
return String(r.id) === String(props.currentResourceId) return String(r.id) === String(props.currentResourceId)
}) })
if (allocation && allocation.percent !== undefined) { if (allocation && allocation.capacity !== undefined) {
const val = Number(allocation.percent) const val = Number(allocation.capacity)
if (Number.isFinite(val) && val >= 0) { if (Number.isFinite(val) && val >= 0) {
return Math.max(0, Math.min(100, val)) return Math.max(0, Math.min(100, val))
} }
@@ -87,13 +87,6 @@ const currentResourceColor = computed(() => {
return '#85ce61' return '#85ce61'
}) })
// v1.9.2
const currentResourceTotalLoad = computed(() => {
// undefined
// Timelineprops
return undefined
})
// v1.9.2 // v1.9.2
const currentResourceName = computed(() => { const currentResourceName = computed(() => {
if (!props.currentResourceId) return '' if (!props.currentResourceId) return ''
@@ -3357,7 +3350,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
'--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */, '--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */,
'--handle-width': `${actualHandleWidth}px` /* 传递手柄宽度给CSS变量 */, '--handle-width': `${actualHandleWidth}px` /* 传递手柄宽度给CSS变量 */,
'--parent-color': taskStatus.color, /* 传递父级TaskBar颜色给伪元素箭头使用 */ '--parent-color': taskStatus.color, /* 传递父级TaskBar颜色给伪元素箭头使用 */
'--allocation-percent': (Number.isFinite(resourcePercent) ? resourcePercent / 100 : 1), /* v1.9.1 传递占比给CSS变量 */ '--allocation-capacity': (Number.isFinite(resourcePercent) ? resourcePercent / 100 : 1), /* v1.9.1 传递占比给CSS变量 */
'--task-bar-bg-color': taskStatus.bgColor, /* v1.9.1 传递背景色给伪元素 */ '--task-bar-bg-color': taskStatus.bgColor, /* v1.9.1 传递背景色给伪元素 */
'--task-bar-border-color': dynamicBorderColor, /* v1.9.2 使用动态边框颜色 */ '--task-bar-border-color': dynamicBorderColor, /* v1.9.2 使用动态边框颜色 */
boxShadow: isParent boxShadow: isParent
@@ -3410,7 +3403,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
:task="task" :task="task"
:current-resource-id="currentResourceId" :current-resource-id="currentResourceId"
:resource-color="currentResourceColor" :resource-color="currentResourceColor"
:resource-percent="resourcePercent" :resource-capacity="resourcePercent"
:resource-name="currentResourceName" :resource-name="currentResourceName"
:task-bar-width="taskBarWidth" :task-bar-width="taskBarWidth"
:task-bar-left="taskBarLeft" :task-bar-left="taskBarLeft"
@@ -3505,7 +3498,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div v-else class="task-name"> <div v-else class="task-name">
{{ task.name }} {{ task.name }}
<!-- v1.9.0 资源视图显示占比文字 --> <!-- v1.9.0 资源视图显示占比文字 -->
<span v-if="shouldShowPercentText" class="resource-percent-text"> <span v-if="shouldShowPercentText" class="resource-capacity-text">
{{ resourcePercent }}% {{ resourcePercent }}%
</span> </span>
</div> </div>
@@ -3638,7 +3631,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div class="tooltip-content"> <div class="tooltip-content">
<!-- v1.9.0 资源视图显示利用率 --> <!-- v1.9.0 资源视图显示利用率 -->
<div v-if="viewMode === 'resource' && resourcePercent < 100" class="tooltip-row"> <div v-if="viewMode === 'resource' && resourcePercent < 100" class="tooltip-row">
<span class="tooltip-label">{{ t('investment') || '投入' }}:</span> <span class="tooltip-label">{{ t('resourceView.capacity') || '利用率' }}:</span>
<span class="tooltip-value">{{ resourcePercent }}%</span> <span class="tooltip-value">{{ resourcePercent }}%</span>
</div> </div>
<div class="tooltip-row"> <div class="tooltip-row">
@@ -3663,7 +3656,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
</div> </div>
<!-- v1.9.0 资源冲突警告 --> <!-- v1.9.0 资源冲突警告 -->
<div v-if="props.hasResourceConflict" class="tooltip-row tooltip-warning"> <div v-if="props.hasResourceConflict" class="tooltip-row tooltip-warning">
<span class="tooltip-label"> {{ t('resourceOverloaded') || '资源超负荷' }}</span> <span class="tooltip-label"> {{ t('resourceView.overloaded') || '资源超负荷' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -3849,7 +3842,7 @@ class="hover-tooltip-arrow" :style="{
top: 0; top: 0;
left: 0; left: 0;
right: 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-top: 1.5px dashed currentColor;
border-left: 1.5px dashed currentColor; border-left: 1.5px dashed currentColor;
border-right: 1.5px dashed currentColor; border-right: 1.5px dashed currentColor;
@@ -3863,7 +3856,7 @@ class="hover-tooltip-arrow" :style="{
} }
/* 占比100%时,隐藏上半部分镂空区域 */ /* 占比100%时,隐藏上半部分镂空区域 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::before { .task-bar.resource-view[style*="--allocation-capacity: 1"]::before {
display: none; display: none;
} }
@@ -3874,7 +3867,7 @@ class="hover-tooltip-arrow" :style="{
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: calc(var(--allocation-percent, 1) * 100%); height: calc(var(--allocation-capacity, 1) * 100%);
background: var(--task-bar-bg-color, #e3f2fd); background: var(--task-bar-bg-color, #e3f2fd);
/*border: 1px solid var(--task-bar-border-color, #90caf9);*/ /*border: 1px solid var(--task-bar-border-color, #90caf9);*/
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
@@ -3885,7 +3878,7 @@ class="hover-tooltip-arrow" :style="{
} }
/* 占比100%时,整个TaskBar都是实心,四个角圆角 */ /* 占比100%时,整个TaskBar都是实心,四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::after { .task-bar.resource-view[style*="--allocation-capacity: 1"]::after {
border-radius: 4px; border-radius: 4px;
} }
@@ -3895,7 +3888,7 @@ class="hover-tooltip-arrow" :style="{
bottom: 0; bottom: 0;
left: 0; left: 0;
top: auto; top: auto;
height: calc(var(--allocation-percent, 1) * 100%); height: calc(var(--allocation-capacity, 1) * 100%);
z-index: 1; z-index: 1;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
pointer-events: none; pointer-events: none;
@@ -3903,7 +3896,7 @@ class="hover-tooltip-arrow" :style="{
} }
/* 占比100%时,进度条四个角圆角 */ /* 占比100%时,进度条四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"] .progress-bar { .task-bar.resource-view[style*="--allocation-capacity: 1"] .progress-bar {
border-radius: 4px; border-radius: 4px;
} }
@@ -4380,7 +4373,7 @@ class="hover-tooltip-arrow" :style="{
} }
/* v1.9.0 资源占比文字样式 */ /* v1.9.0 资源占比文字样式 */
.resource-percent-text { .resource-capacity-text {
display: inline-block; display: inline-block;
margin-left: 6px; margin-left: 6px;
font-size: 12px; font-size: 12px;
@@ -4399,7 +4392,7 @@ class="hover-tooltip-arrow" :style="{
.task-bar[style*="width: 28px"], .task-bar[style*="width: 28px"],
.task-bar[style*="width: 32px"], .task-bar[style*="width: 32px"],
.task-bar[style*="width: 36px"] { .task-bar[style*="width: 36px"] {
.resource-percent-text { .resource-capacity-text {
display: none; display: none;
} }
} }
+26 -22
View File
@@ -7,6 +7,7 @@ import GanttConfirmDialog from './GanttConfirmDialog.vue'
import MultiSelectPredecessor from './MultiSelectPredecessor.vue' import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
import ConfirmTimerDialog from './ConfirmTimerDialog.vue' import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
import { Resource } from '../models/classes/Resource'
import '../styles/app.css' import '../styles/app.css'
interface AssigneeOption { interface AssigneeOption {
@@ -197,7 +198,7 @@ const getTaskTypeDisplay = (type: string): string => {
const progressSliderStyle = computed(() => { const progressSliderStyle = computed(() => {
const progressPercent = formData.progress || 0 const progressPercent = formData.progress || 0
return { return {
'--progress-percent': `${progressPercent}%`, '--progress-capacity': `${progressPercent}%`,
} }
}) })
@@ -500,6 +501,7 @@ const handleSubmit = async () => {
const taskData: Task = { const taskData: Task = {
...formData, ...formData,
id: props.isEdit && props.task ? props.task.id : Date.now(), id: props.isEdit && props.task ? props.task.id : Date.now(),
resources: formData.resources, // resources
} }
emit('submit', taskData) emit('submit', taskData)
handleClose() handleClose()
@@ -680,11 +682,12 @@ const addResource = () => {
if (!formData.resources) { if (!formData.resources) {
formData.resources = [] formData.resources = []
} }
formData.resources.push({ formData.resources.push(new Resource({
id: '', id: '',
name: '', name: '',
percent: 100, capacity: 100,
}) tasks: [],
}))
} }
const removeResource = (index: number) => { 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 (!formData.resources || !formData.resources[index]) return
if (field === 'id') { 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].id = selected.value
formData.resources[index].name = selected.label formData.resources[index].name = selected.label
} }
} else if (field === 'percent') { } else if (field === 'capacity') {
let percent = typeof value === 'string' ? parseInt(value) : value let capacity = typeof value === 'string' ? parseInt(value) : value
// 20-100 // 20-100
if (percent < 20) percent = 20 if (capacity < 20) capacity = 20
if (percent > 100) percent = 100 if (capacity > 100) capacity = 100
formData.resources[index].percent = percent formData.resources[index].capacity = capacity
} }
} }
@@ -728,11 +731,12 @@ const mapAssigneeToResources = () => {
formData.resources = assignees.map((assigneeId) => { formData.resources = assignees.map((assigneeId) => {
// assigneeOption // assigneeOption
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId) const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
return { return new Resource({
id: assigneeId, id: assigneeId,
name: option?.label || String(assigneeId), name: option?.label || String(assigneeId),
percent: 100, // 100% capacity: 100, // 100%
} tasks: [],
})
}) })
} }
@@ -1026,9 +1030,9 @@ const taskStatus = computed(() => {
</select> </select>
<select <select
v-model.number="resource.percent" v-model.number="resource.capacity"
class="form-select percent-select" class="form-select capacity-select"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)" @change="handleResourceChange(index, 'capacity', ($event.target as HTMLSelectElement).value)"
> >
<option :value="25">25%</option> <option :value="25">25%</option>
<option :value="50">50%</option> <option :value="50">50%</option>
@@ -1474,8 +1478,8 @@ const taskStatus = computed(() => {
background: linear-gradient( background: linear-gradient(
to right, to right,
var(--gantt-primary, #409eff) 0%, var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%), var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%), var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100% var(--gantt-border-light, #e4e7ed) 100%
); );
} }
@@ -1487,8 +1491,8 @@ const taskStatus = computed(() => {
background: linear-gradient( background: linear-gradient(
to right, to right,
var(--gantt-primary, #409eff) 0%, var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%), var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%), var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100% var(--gantt-border-light, #e4e7ed) 100%
); );
border: none; border: none;
@@ -1508,7 +1512,7 @@ const taskStatus = computed(() => {
top: 0; top: 0;
left: 0; left: 0;
height: 6px; height: 6px;
width: var(--progress-percent, 0%); width: var(--progress-capacity, 0%);
background: var(--gantt-primary, #409eff); background: var(--gantt-primary, #409eff);
border-radius: 3px; border-radius: 3px;
pointer-events: none; pointer-events: none;
@@ -1769,7 +1773,7 @@ const taskStatus = computed(() => {
padding: 8px 12px; padding: 8px 12px;
} }
.percent-select { .capacity-select {
width: 110px; width: 110px;
height: 36px; height: 36px;
flex-shrink: 0; flex-shrink: 0;
+50 -103
View File
@@ -13,7 +13,7 @@ import { getPredecessorIds } from '../utils/predecessorUtils'
import { perfMonitor } from '../utils/perfMonitor' import { perfMonitor } from '../utils/perfMonitor'
import { perfMonitor2 } from '../utils/perfMonitor2' // v1.9.6 import { perfMonitor2 } from '../utils/perfMonitor2' // v1.9.6
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
import type { Resource } from '../models/classes/Resource' import { Resource } from '../models/classes/Resource'
import type { Milestone } from '../models/classes/Milestone' import type { Milestone } from '../models/classes/Milestone'
import type { TimelineConfig } from '../models/configs/TimelineConfig' import type { TimelineConfig } from '../models/configs/TimelineConfig'
import { TimelineScale } from '../models/types/TimelineScale' import { TimelineScale } from '../models/types/TimelineScale'
@@ -115,7 +115,7 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n
if (viewMode.value !== 'resource') return [] if (viewMode.value !== 'resource') return []
const conflictTaskIds = resourceConflicts.value.get(String(resourceId)) const conflictTaskIds = resourceConflicts.value.get(String(resourceId))
if (!conflictTaskIds || !conflictTaskIds.has(taskId)) return [] if (!conflictTaskIds || !conflictTaskIds.has(Number(taskId))) return []
const resources = dataSource.value as Resource[] const resources = dataSource.value as Resource[]
const resource = resources.find(r => String(r.id) === String(resourceId)) const resource = resources.find(r => String(r.id) === String(resourceId))
@@ -180,7 +180,6 @@ const layoutCache = new Map<string, {
let resourceTaskLayoutsCallCount = 0 let resourceTaskLayoutsCallCount = 0
const resourceTaskLayouts = computed(() => { const resourceTaskLayouts = computed(() => {
resourceTaskLayoutsCallCount++ resourceTaskLayoutsCallCount++
const startTime = performance.now()
const layoutMap = new Map<string | number, { const layoutMap = new Map<string | number, {
taskRowMap: Map<string | number, number>, taskRowMap: Map<string | number, number>,
@@ -220,14 +219,6 @@ const resourceTaskLayouts = computed(() => {
} }
}) })
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
//
if (resources.length > 0) {
const hitRate = ((cacheHits / resources.length) * 100).toFixed(1)
}
return layoutMap return layoutMap
}) })
@@ -258,7 +249,7 @@ const getResourceLayout = (resource: Resource) => {
// //
let result let result
if (resourceTasks.length > 0) { if (resourceTasks.length > 0) {
result = assignTaskRows(resourceTasks, resource.id, 51) result = assignTaskRows(resourceTasks, 51)
} else { } else {
result = { result = {
taskRowMap: new Map(), taskRowMap: new Map(),
@@ -275,7 +266,6 @@ const getResourceLayout = (resource: Resource) => {
let resourceRowPositionsCallCount = 0 let resourceRowPositionsCallCount = 0
const resourceRowPositions = computed(() => { const resourceRowPositions = computed(() => {
resourceRowPositionsCallCount++ resourceRowPositionsCallCount++
const startTime = performance.now()
const positions = new Map<string | number, number>() const positions = new Map<string | number, number>()
if (viewMode.value !== 'resource') { if (viewMode.value !== 'resource') {
@@ -305,7 +295,6 @@ const resourceRowPositions = computed(() => {
// //
if (cumulativeTop > scrollBottom + ROW_HEIGHT * 20) { if (cumulativeTop > scrollBottom + ROW_HEIGHT * 20) {
// 51px // 51px
const remainingResources = resources.length - processedCount
for (let i = processedCount; i < resources.length; i++) { for (let i = processedCount; i < resources.length; i++) {
positions.set(resources[i].id, cumulativeTop) // positions.set(resources[i].id, cumulativeTop) //
cumulativeTop += 51 // cumulativeTop += 51 //
@@ -314,9 +303,6 @@ const resourceRowPositions = computed(() => {
} }
} }
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return positions return positions
}) })
@@ -1599,12 +1585,6 @@ const visibleTimeRange = computed(() => {
// //
const endDate = getDateByScrollPosition(scrollLeft + containerWidth + bufferWidth) const endDate = getDateByScrollPosition(scrollLeft + containerWidth + bufferWidth)
// v1.9.6 Sprint2(P1) -
if (visibleTimeRangeCallCount % 10 === 0) {
const scale = currentTimeScale.value
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
}
return { startDate, endDate } return { startDate, endDate }
}) })
@@ -1724,7 +1704,6 @@ const getDateByScrollPosition = (scrollPosition: number): Date => {
let visibleTaskRangeCallCount = 0 let visibleTaskRangeCallCount = 0
const visibleTaskRange = computed(() => { const visibleTaskRange = computed(() => {
visibleTaskRangeCallCount++ visibleTaskRangeCallCount++
const startTime = performance.now()
const scrollTop = timelineBodyScrollTop.value const scrollTop = timelineBodyScrollTop.value
const containerHeight = timelineBodyHeight.value || 600 const containerHeight = timelineBodyHeight.value || 600
@@ -1762,9 +1741,6 @@ const visibleTaskRange = computed(() => {
} }
} }
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return { return {
startIndex: Math.max(0, startIndex), startIndex: Math.max(0, startIndex),
endIndex: Math.min(resources.length, endIndex), endIndex: Math.min(resources.length, endIndex),
@@ -1856,8 +1832,6 @@ const scheduleResourceBatchRender = () => {
const frameStartTime = performance.now() const frameStartTime = performance.now()
const limits = new Map(resourceTaskRenderLimits.value) const limits = new Map(resourceTaskRenderLimits.value)
let hasMore = false let hasMore = false
let hasBackground = false
let processedInFrame = 0
resourceTaskQueues.value.forEach((queue, resourceId) => { resourceTaskQueues.value.forEach((queue, resourceId) => {
// //
@@ -1868,19 +1842,12 @@ const scheduleResourceBatchRender = () => {
const current = limits.get(resourceId) || 0 const current = limits.get(resourceId) || 0
// v1.9.6 Sprint4 - 使 visibleCount background // v1.9.6 Sprint4 - 使 visibleCount background
// queue.totalCount queue.visibleCount rebuildResourceTaskQueues backgroundTasks
const phaseTarget = queue.visibleCount const phaseTarget = queue.visibleCount
// v1.9.6 Sprint4 - hasBackground background
// if (queue.totalCount > queue.visibleCount) {
// hasBackground = true
// }
if (current < phaseTarget) { if (current < phaseTarget) {
const next = Math.min(phaseTarget, current + RESOURCE_BATCH_SIZE) const next = Math.min(phaseTarget, current + RESOURCE_BATCH_SIZE)
limits.set(resourceId, next) limits.set(resourceId, next)
hasMore = true hasMore = true
processedInFrame++
} }
}) })
@@ -1915,51 +1882,54 @@ const rebuildResourceTaskQueues = () => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR
// 1: - // v1.9.7 Bug//使visibleTimeRange
const scrollLeft = timelineScrollLeft.value // 28-31使30
const containerWidth = timelineContainerWidth.value // 22830TaskBar
const scale = currentTimeScale.value const scale = currentTimeScale.value
const timelineStart = timelineConfig.value.startDate.getTime() const usePixelLevelFilter = scale === TimelineScale.HOUR || scale === TimelineScale.DAY || scale === TimelineScale.WEEK
// let viewportStartTime: number
let pixelPerMs = 0 let viewportEndTime: number
if (scale === TimelineScale.HOUR) {
pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour if (usePixelLevelFilter) {
} else if (scale === TimelineScale.DAY) { // 1: - - //
pixelPerMs = 30 / (24 * 60 * 60 * 1000) // 30px per day (5030) const scrollLeft = timelineScrollLeft.value
} else if (scale === TimelineScale.WEEK) { const containerWidth = timelineContainerWidth.value
pixelPerMs = 60 / (7 * 24 * 60 * 60 * 1000) // 60px per week const timelineStart = timelineConfig.value.startDate.getTime()
} else if (scale === TimelineScale.MONTH) {
pixelPerMs = 60 / (30 * 24 * 60 * 60 * 1000) // 60px per month (approximate) //
} else if (scale === TimelineScale.QUARTER) { let pixelPerMs = 0
pixelPerMs = 90 / (90 * 24 * 60 * 60 * 1000) // 90px per quarter if (scale === TimelineScale.HOUR) {
} else if (scale === TimelineScale.YEAR) { pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour
pixelPerMs = 120 / (365 * 24 * 60 * 60 * 1000) // 120px per year } else if (scale === TimelineScale.DAY) {
pixelPerMs = 30 / (24 * 60 * 60 * 1000) // 30px per day
} else if (scale === TimelineScale.WEEK) {
pixelPerMs = 60 / (7 * 24 * 60 * 60 * 1000) // 60px per week
}
// 10%1
const ULTRA_TIGHT_BUFFER = 0.1 // 10%
const bufferPixels = containerWidth * ULTRA_TIGHT_BUFFER
const viewportStartPixel = Math.max(0, scrollLeft - bufferPixels)
const viewportEndPixel = scrollLeft + containerWidth + bufferPixels
//
viewportStartTime = timelineStart + viewportStartPixel / pixelPerMs
viewportEndTime = timelineStart + viewportEndPixel / pixelPerMs
} else {
// v1.9.7 //使visibleTimeRangetimelineData
viewportStartTime = visibleStartDate.getTime()
viewportEndTime = visibleEndDate.getTime()
} }
// 10%1
const ULTRA_TIGHT_BUFFER = 0.1 // 10%
const bufferPixels = containerWidth * ULTRA_TIGHT_BUFFER
const viewportStartPixel = Math.max(0, scrollLeft - bufferPixels)
const viewportEndPixel = scrollLeft + containerWidth + bufferPixels
//
const viewportStartTime = timelineStart + viewportStartPixel / pixelPerMs
const viewportEndTime = timelineStart + viewportEndPixel / pixelPerMs
const queues = new Map<string | number, ResourceTaskQueue>() const queues = new Map<string | number, ResourceTaskQueue>()
const limits = new Map<string | number, number>() const limits = new Map<string | number, number>()
// v1.9.6 Sprint4 -
const totalResources = (dataSource.value as Resource[]).length
const visibleResourcesCount = visibleResources.value.length
// v1.9.6 Sprint2(P5) - // v1.9.6 Sprint2(P5) -
const currentCache = new Map(taskBarRenderCache.value) const currentCache = new Map(taskBarRenderCache.value)
const newCache = new Map<string, TaskBarRenderCache>() const newCache = new Map<string, TaskBarRenderCache>()
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
let cachedCount = 0 // TaskBar let cachedCount = 0 // TaskBar
let totalVisibleTaskBars = 0 // v1.9.6 Sprint4 -
visibleResources.value.forEach(({ resource }) => { visibleResources.value.forEach(({ resource }) => {
const resourceId = resource.id as string | number const resourceId = resource.id as string | number
@@ -1995,7 +1965,11 @@ const rebuildResourceTaskQueues = () => {
taskId, taskId,
resourceId, resourceId,
rendered: isRendered, rendered: isRendered,
timestamp: isRendered ? existingCache.timestamp : currentTimestamp, timestamp: isRendered ?
(existingCache ?
existingCache.timestamp
: currentTimestamp)
: currentTimestamp,
}) })
}) })
@@ -2008,8 +1982,6 @@ const rebuildResourceTaskQueues = () => {
originalTasks, originalTasks,
}) })
totalVisibleTaskBars += visibleTasks.length // v1.9.6 Sprint4 -
// v1.9.6 Sprint2(P5) - TaskBar // v1.9.6 Sprint2(P5) - TaskBar
const previousLimit = resourceTaskRenderLimits.value.get(resourceId) const previousLimit = resourceTaskRenderLimits.value.get(resourceId)
let initialLimit: number let initialLimit: number
@@ -2030,10 +2002,6 @@ const rebuildResourceTaskQueues = () => {
resourceRenderPhase.value = 'visible' resourceRenderPhase.value = 'visible'
taskBarRenderCache.value = newCache // taskBarRenderCache.value = newCache //
// v1.9.6 Sprint2(P5) -
const totalTaskBars = newCache.size
const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0'
scheduleResourceBatchRender() scheduleResourceBatchRender()
} }
@@ -2090,11 +2058,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR
// v1.9.6 Sprint2(P1) -
if (filteredTasksCallCount <= 5 || filteredTasksCallCount % 10 === 0) {
const daysDiff = Math.ceil((visibleEndDate.getTime() - visibleStartDate.getTime()) / (1000 * 60 * 60 * 24))
}
// //
let totalOriginalTasks = 0 let totalOriginalTasks = 0
let totalFilteredTasks = 0 let totalFilteredTasks = 0
@@ -2131,21 +2094,16 @@ const visibleResourcesWithFilteredTasks = computed(() => {
totalFilteredTasks += renderTasks.length totalFilteredTasks += renderTasks.length
return { return {
resource: { resource: new Resource({
...resource, ...resource,
tasks: renderTasks, tasks: renderTasks,
// GanttConflicts // GanttConflicts
allTasks: originalTasks, allTasks: originalTasks,
} as Resource, }),
originalIndex, originalIndex,
} }
}) })
// v1.9.6 Sprint2(P1) - 10
if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) {
const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1)
}
return result return result
}) })
@@ -2524,7 +2482,6 @@ const handleTaskRowHover = (taskId: number | string | null) => {
// Timeline // Timeline
const contentHeight = computed(() => { const contentHeight = computed(() => {
const startTime = performance.now()
const minHeight = 400 // const minHeight = 400 //
// v1.9.0 使 // v1.9.0 使
@@ -2538,9 +2495,6 @@ const contentHeight = computed(() => {
totalHeight += layout?.totalHeight || 51 totalHeight += layout?.totalHeight || 51
}) })
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return Math.max(totalHeight, minHeight, timelineBodyHeight.value) return Math.max(totalHeight, minHeight, timelineBodyHeight.value)
} }
@@ -2603,10 +2557,8 @@ const handleMilestoneUpdate = (updatedMilestone: Milestone) => {
// //
const generateTimelineData = (): any => { const generateTimelineData = (): any => {
const startTime = performance.now()
// 使 // 使
const result = getCachedTimelineData() const result = getCachedTimelineData()
const duration = (performance.now() - startTime).toFixed(2)
return result return result
} }
@@ -2984,18 +2936,12 @@ watch(
[timelineData, currentTimeScale], [timelineData, currentTimeScale],
([newData, newScale]) => { ([newData, newScale]) => {
positionCacheWatchCount++ positionCacheWatchCount++
const watchStartTime = performance.now()
if (newData && newScale) { if (newData && newScale) {
//
const cacheStartTime = performance.now()
positionCache.buildCache(newData as any[], newScale) positionCache.buildCache(newData as any[], newScale)
const cacheDuration = (performance.now() - cacheStartTime).toFixed(2)
} }
const totalDuration = (performance.now() - watchStartTime).toFixed(2)
}, },
{ immediate: true } // { immediate: true }, //
) )
// props/ // props/
@@ -5673,7 +5619,7 @@ const handleAddSuccessor = (task: Task) => {
<!-- 为资源下的每个任务渲染 TaskBar --> <!-- 为资源下的每个任务渲染 TaskBar -->
<template v-if="(resource as any).tasks && (resource as any).tasks.length > 0"> <template v-if="(resource as any).tasks && (resource as any).tasks.length > 0">
<TaskBar <TaskBar
v-for="(task, taskIndex) in (resource as any).tasks" v-for="(task) in (resource as any).tasks"
:key="`taskbar-${task.id}-${taskBarRenderKey}`" :key="`taskbar-${task.id}-${taskBarRenderKey}`"
:task="task" :task="task"
:row-index="originalIndex" :row-index="originalIndex"
@@ -5754,9 +5700,10 @@ const handleAddSuccessor = (task: Task) => {
<!-- v1.9.5 修复传递任务行号信息正确计算冲突区域高度 --> <!-- v1.9.5 修复传递任务行号信息正确计算冲突区域高度 -->
<!-- v1.9.5 可通过 show-conflicts prop 控制是否显示 --> <!-- v1.9.5 可通过 show-conflicts prop 控制是否显示 -->
<!-- v1.9.6 修复width使用totalTimelineWidth用于坐标计算containerWidth用于Canvas宽度 --> <!-- v1.9.6 修复width使用totalTimelineWidth用于坐标计算containerWidth用于Canvas宽度 -->
<!-- v1.9.7 Bug修复使用渲染的tasks而不是allTasks避免滚动后显示已消失TaskBar的冲突 -->
<GanttConflicts <GanttConflicts
v-if="showConflicts" v-if="showConflicts"
:tasks="(resource as any).allTasks || (resource as any).tasks" :tasks="(resource as any).tasks"
:resource-id="resource.id" :resource-id="resource.id"
:day-width="dayWidth" :day-width="dayWidth"
:start-date=" :start-date="
+16 -35
View File
@@ -50,9 +50,6 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
// containerWidth1900px0使1920使props.width30px // containerWidth1900px0使1920使props.width30px
const canvasWidth = computed(() => { const canvasWidth = computed(() => {
const width = props.containerWidth || 1920 const width = props.containerWidth || 1920
if (import.meta.env.DEV) {
console.log(`[GanttConflicts] canvasWidth: ${width}px (containerWidth: ${props.containerWidth}, totalWidth: ${props.width})`)
}
return width return width
}) })
const canvasHeight = computed(() => props.height) const canvasHeight = computed(() => props.height)
@@ -161,8 +158,11 @@ watch([() => props.timelineData, () => props.currentTimeScale], () => {
if (import.meta.env.DEV) {} if (import.meta.env.DEV) {}
// v1.9.4 BUG - // v1.9.4 BUG -
coordsCache.clear() coordsCache.clear()
// v1.9.7 Bug - 使nextTick
recalculateConflicts() texturePatterns.value = { light: null, medium: null, severe: null }
nextTick(() => {
recalculateConflicts()
})
}, { deep: true }) }, { deep: true })
// v1.9.6 scrollLeft // v1.9.6 scrollLeft
@@ -186,8 +186,6 @@ watch(() => props.scrollLeft, () => {
// TaskBar // TaskBar
function recalculateConflictsIncremental(changedTaskId: string | number) { function recalculateConflictsIncremental(changedTaskId: string | number) {
const startTime = performance.now()
// //
const changedTask = props.tasks.find(t => t.id === changedTaskId) const changedTask = props.tasks.find(t => t.id === changedTaskId)
if (!changedTask) { if (!changedTask) {
@@ -198,10 +196,14 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
// //
const affectedTasks = props.tasks.filter(task => { const affectedTasks = props.tasks.filter(task => {
// //
const taskStart = new Date(task.startDate) const taskStart = task.startDate ? new Date(task.startDate) : null
const taskEnd = new Date(task.endDate) if (!taskStart) return false
const changedStart = new Date(changedTask.startDate) const taskEnd = task.endDate ? new Date(task.endDate) : null
const changedEnd = new Date(changedTask.endDate) if (!taskEnd) return false
const changedStart = changedTask.startDate ? new Date(changedTask.startDate) : null
if (!changedStart) return false
const changedEnd = changedTask.endDate ? new Date(changedTask.endDate) : null
if (!changedEnd) return false
return !(taskEnd < changedStart || taskStart > changedEnd) return !(taskEnd < changedStart || taskStart > changedEnd)
}) })
@@ -213,7 +215,7 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
const affectedTaskIds = new Set(affectedTasks.map(t => t.id)) const affectedTaskIds = new Set(affectedTasks.map(t => t.id))
const unchangedConflicts = previousConflictZones.value.filter(zone => { const unchangedConflicts = previousConflictZones.value.filter(zone => {
// //
return !zone.tasks.some(task => affectedTaskIds.has(task.id)) return !zone.tasks.some(task => affectedTaskIds.has(Number(task.id)))
}) })
// //
@@ -288,10 +290,7 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
top, top,
height, height,
} }
}).filter(Boolean) }).filter(z => z !== null) as ConflictZone[]
const endTime = performance.now()
const elapsed = endTime - startTime
// 使 // 使
renderConflictsIncremental() renderConflictsIncremental()
@@ -304,13 +303,6 @@ function recalculateConflicts() {
// //
const conflicts = detectConflicts(props.tasks, props.resourceId) const conflicts = detectConflicts(props.tasks, props.resourceId)
//
const tasksInfo = props.tasks.map(t => {
const resource = (t as any).resources?.find((r: any) => String(r.id) === String(props.resourceId))
const percent = resource?.percent || 0
return `${t.name}(${percent}%)`
}).join(', ')
// v1.9.4 P1 - 使 // v1.9.4 P1 - 使
conflictZones.value = conflicts.map((zone) => { conflictZones.value = conflicts.map((zone) => {
// key // key
@@ -386,9 +378,6 @@ function recalculateConflicts() {
} }
} }
//
if (import.meta.env.DEV) {}
return { return {
...zone, ...zone,
left: canvasLeft, left: canvasLeft,
@@ -396,19 +385,11 @@ function recalculateConflicts() {
top, top,
height, height,
} }
}).filter(Boolean) // }).filter(z => z !== null) as ConflictZone[]
const endTime = performance.now() const endTime = performance.now()
const elapsed = endTime - startTime const elapsed = endTime - startTime
// v1.9.6
if (import.meta.env.DEV) {
console.log(`[GanttConflicts] ${conflictZones.value.length} conflict zones after viewport clipping:`)
conflictZones.value.forEach((zone, index) => {
console.log(` Zone ${index}: left=${zone.left}, width=${zone.width}, top=${zone.top}, height=${zone.height}, level=${zone.level}`)
})
}
// //
if (import.meta.env.DEV && elapsed > 50) {} if (import.meta.env.DEV && elapsed > 50) {}
+23 -26
View File
@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue' import { ref, computed, onUnmounted } from 'vue'
import type { Task } from '../../models/classes/Task' import type { Task } from '../../models/classes/Task'
import { useI18n } from '../../composables/useI18n'
interface Props { interface Props {
task: Task task: Task
currentResourceId: string | number currentResourceId: string | number
resourceColor: string resourceColor: string
resourcePercent: number resourceCapacity: number
resourceName: string resourceName: string
taskBarWidth?: number taskBarWidth?: number
taskBarLeft?: number taskBarLeft?: number
@@ -33,6 +34,8 @@ const emit = defineEmits<{
'hover-change': [isHovered: boolean] 'hover-change': [isHovered: boolean]
}>() }>()
const { t } = useI18n()
// //
const isExpanded = ref(false) const isExpanded = ref(false)
const tabElement = ref<HTMLElement | null>(null) const tabElement = ref<HTMLElement | null>(null)
@@ -43,7 +46,7 @@ let debounceTimer: number | null = null
const DEBOUNCE_DELAY = 50 // 50ms const DEBOUNCE_DELAY = 50 // 50ms
// //
const percentText = computed(() => `${Math.round(props.resourcePercent)}%`) const percentText = computed(() => `${Math.round(props.resourceCapacity)}%`)
// Tab taskBarWidthtaskbar // Tab taskBarWidthtaskbar
// taskbartab // taskbartab
@@ -64,7 +67,6 @@ const tabLeftOffset = computed(() => {
} }
const taskBarLeft = props.taskBarLeft const taskBarLeft = props.taskBarLeft
const taskBarRight = taskBarLeft + (props.taskBarWidth || 0)
const viewportLeft = props.scrollLeft const viewportLeft = props.scrollLeft
// TaskBarTab // TaskBarTab
@@ -146,7 +148,7 @@ const expandedStyle = computed(() => {
if (shouldExpandUpward) { if (shouldExpandUpward) {
// //
return { return {
position: 'fixed', position: 'fixed' as const,
bottom: `${viewportHeight - rect.top + 2}px`, bottom: `${viewportHeight - rect.top + 2}px`,
left: `${rect.left}px`, left: `${rect.left}px`,
maxHeight: `${Math.min(spaceAbove - 10, 400)}px`, // 10px maxHeight: `${Math.min(spaceAbove - 10, 400)}px`, // 10px
@@ -156,7 +158,7 @@ const expandedStyle = computed(() => {
} else { } else {
// //
return { return {
position: 'fixed', position: 'fixed' as const,
top: `${rect.bottom + 2}px`, top: `${rect.bottom + 2}px`,
left: `${rect.left}px`, left: `${rect.left}px`,
maxHeight: `${Math.min(spaceBelow - 10, 400)}px`, // 10px maxHeight: `${Math.min(spaceBelow - 10, 400)}px`, // 10px
@@ -174,9 +176,10 @@ const formattedDateRange = computed(() => {
const end = new Date(props.task.endDate) const end = new Date(props.task.endDate)
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}` return `${year}/${month}/${day}`
} }
return `${formatDate(start)} ~ ${formatDate(end)}` return `${formatDate(start)} ~ ${formatDate(end)}`
@@ -194,9 +197,6 @@ const conflictInfoList = computed(() => {
const currentStart = new Date(currentTask.startDate).getTime() const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime() const currentEnd = new Date(currentTask.endDate).getTime()
//
const currentPercent = props.resourcePercent || 100
// v1.9.8 // v1.9.8
return props.conflictTasks.map(conflictTask => { return props.conflictTasks.map(conflictTask => {
if (!conflictTask.startDate || !conflictTask.endDate) return null if (!conflictTask.startDate || !conflictTask.endDate) return null
@@ -210,8 +210,8 @@ const conflictInfoList = computed(() => {
const allocation = conflictTask.resources.find( const allocation = conflictTask.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId), (r: any) => String(r.id) === String(props.currentResourceId),
) )
if (allocation && allocation.percent !== undefined) { if (allocation && allocation.capacity !== undefined) {
conflictPercent = Math.max(20, Math.min(100, allocation.percent)) conflictPercent = Math.max(20, Math.min(100, allocation.capacity))
} }
} }
@@ -221,7 +221,7 @@ const conflictInfoList = computed(() => {
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
const date = new Date(timestamp) const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}` return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
} }
return { return {
@@ -241,10 +241,7 @@ const totalOverloadPercent = computed(() => {
const currentTask = props.task const currentTask = props.task
if (!currentTask.startDate || !currentTask.endDate) return 0 if (!currentTask.startDate || !currentTask.endDate) return 0
const currentPercent = props.resourceCapacity || 100
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
const currentPercent = props.resourcePercent || 100
// v1.9.9 endDate +1 // v1.9.9 endDate +1
const DAY_MS = 24 * 60 * 60 * 1000 const DAY_MS = 24 * 60 * 60 * 1000
@@ -290,8 +287,8 @@ const totalOverloadPercent = computed(() => {
const allocation = task.resources.find( const allocation = task.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId), (r: any) => String(r.id) === String(props.currentResourceId),
) )
if (allocation && allocation.percent !== undefined) { if (allocation && allocation.capacity !== undefined) {
taskPercent = allocation.percent taskPercent = allocation.capacity
} }
} }
intervalTotal += taskPercent intervalTotal += taskPercent
@@ -432,12 +429,12 @@ onUnmounted(() => {
<div class="expanded-body"> <div class="expanded-body">
<!-- 利用率 --> <!-- 利用率 -->
<div class="expanded-row"> <div class="expanded-row">
<span class="info-label">利用率</span> <span class="info-label">{{ t.resourceView.capacity }}</span>
<span class="info-value">{{ percentText }}</span> <span class="info-value">{{ percentText }}</span>
</div> </div>
<!-- 日期范围 --> <!-- 日期范围 -->
<div class="expanded-row"> <div class="expanded-row">
<span class="info-label">时间范围</span> <span class="info-label">{{ t.resourceView.duration }}</span>
<span class="info-value">{{ formattedDateRange }}</span> <span class="info-value">{{ formattedDateRange }}</span>
</div> </div>
<!-- 冲突预警有冲突时才显示 --> <!-- 冲突预警有冲突时才显示 -->
@@ -447,20 +444,20 @@ onUnmounted(() => {
<svg class="warning-icon" viewBox="0 0 24 24" width="14" height="14"> <svg class="warning-icon" viewBox="0 0 24 24" width="14" height="14">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/> <path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg> </svg>
<span class="conflict-title">资源超载警告</span> <span class="conflict-title">{{ t.resourceView.overloadWarning }}</span>
<span class="total-overload"> {{ totalOverloadPercent }}%</span> <span class="total-overload"> {{ totalOverloadPercent }}%</span>
</div> </div>
<!-- 可滚动的冲突列表 --> <!-- 可滚动的冲突列表 -->
<div class="conflict-list-container"> <div class="conflict-list-container">
<div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item"> <div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item">
<div class="conflict-task-name">{{ info.taskName }}冲突</div> <div class="conflict-task-name">{{ t.resourceView.conflictWith }}{{ info ? info.taskName : '' }}{{ t.resourceView.conflictSuffix }}</div>
<div class="conflict-detail"> <div class="conflict-detail">
<span class="conflict-label">冲突时段</span> <span class="conflict-label">{{ t.resourceView.conflictDuration }}</span>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span> <span class="conflict-value">{{ info ? info.overlapStart : '' }} ~ {{ info ? info.overlapEnd : '' }}</span>
</div> </div>
<div class="conflict-detail"> <div class="conflict-detail">
<span class="conflict-label">任务利用率</span> <span class="conflict-label">{{ t.resourceView.capacity }}: </span>
<span class="conflict-value">{{ info.conflictPercent }}%</span> <span class="conflict-value">{{ info ? info.conflictPercent : '' }}%</span>
</div> </div>
</div> </div>
</div> </div>
+20 -2
View File
@@ -97,7 +97,6 @@ const messages = {
collapseAll: '全部折叠', collapseAll: '全部折叠',
// v1.9.0 视图模式 // v1.9.0 视图模式
taskView: '任务视图', taskView: '任务视图',
resourceView: '资源视图',
language: '中文', language: '中文',
languageTooltip: '选择语言', languageTooltip: '选择语言',
lightMode: '明亮模式', lightMode: '明亮模式',
@@ -297,6 +296,16 @@ const messages = {
}, },
}, },
}, },
resourceView: {
desc: '资源视图',
capacity: '利用率',
overloaded: '超负荷',
duration: '时间周期',
overloadWarning: '资源超负荷警告',
conflictDuration: '冲突时段',
conflictWith: '与',
conflictSuffix: '冲突',
},
}, },
'en-US': { 'en-US': {
dateNotSet: 'Not set', dateNotSet: 'Not set',
@@ -389,7 +398,6 @@ const messages = {
collapseAll: 'Collapse All', collapseAll: 'Collapse All',
// v1.9.0 视图模式 // v1.9.0 视图模式
taskView: 'Task View', taskView: 'Task View',
resourceView: 'Resource View',
language: 'English', language: 'English',
languageTooltip: 'Select language', languageTooltip: 'Select language',
lightMode: 'Light Mode', lightMode: 'Light Mode',
@@ -592,6 +600,16 @@ const messages = {
}, },
}, },
}, },
resourceView: {
desc: 'Resource View',
capacity: 'capacity',
overloaded: 'overLoaded',
duration: 'duration',
overloadWarning: 'Overload Warning',
conflictDuration: 'conflict duration',
conflictWith: 'conflict with',
conflictSuffix: '',
},
}, },
} }
+3 -3
View File
@@ -149,7 +149,7 @@ export class Resource {
* @param task * @param task
* @returns (20-100)100 * @returns (20-100)100
*/ */
private getTaskAllocationPercent(task: any): number { getTaskAllocationPercent(task: any): number {
if (!task.resources || !Array.isArray(task.resources)) { if (!task.resources || !Array.isArray(task.resources)) {
return 100 // 未配置resources时,默认100% return 100 // 未配置resources时,默认100%
} }
@@ -159,7 +159,7 @@ export class Resource {
return 100 // 未找到当前资源的分配信息,默认100% return 100 // 未找到当前资源的分配信息,默认100%
} }
const percent = allocation.percent ?? 100 const capacity = allocation.capacity ?? 100
return Math.max(20, Math.min(100, percent)) // 限制范围 20-100 return Math.max(20, Math.min(100, capacity)) // 限制范围 20-100
} }
} }
+2 -12
View File
@@ -1,14 +1,4 @@
/** import { Resource } from './Resource'// Task 类型定义
* (Resource Allocation)
* @version 1.9.0
*/
export interface ResourceAllocation {
id: string | number // 资源ID
name: string // 资源名称
percent?: number // 投入精力占比 (20-100),默认100
}
// Task 类型定义
export interface Task { export interface Task {
id: number id: number
name: string name: string
@@ -42,7 +32,7 @@ export interface Task {
// 自定义样式 // 自定义样式
barColor?: string // 自定义TaskBar颜色,如 '#ff5733',若不设置则使用默认颜色方案 barColor?: string // 自定义TaskBar颜色,如 '#ff5733',若不设置则使用默认颜色方案
// v1.9.0 资源占用比例 // v1.9.0 资源占用比例
resources?: ResourceAllocation[] // 资源分配列表,包含占比信息 resources?: Resource[] // 资源分配列表,包含占比信息
// 支持自定义属性 - 使用 unknown 允许任意类型 // 支持自定义属性 - 使用 unknown 允许任意类型
[key: string]: unknown [key: string]: unknown
} }
+16 -51
View File
@@ -8,7 +8,6 @@
*/ */
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
import { perfMonitor } from './perfMonitor'
/** /**
* *
@@ -26,7 +25,7 @@ export interface ConflictZone {
tasks: Array<{ tasks: Array<{
id: number | string id: number | string
name: string name: string
percent: number capacity: number
}> }>
/** Canvas渲染坐标(由GanttConflicts组件计算填充) */ /** Canvas渲染坐标(由GanttConflicts组件计算填充) */
left?: number left?: number
@@ -57,8 +56,6 @@ export function detectConflicts(
tasks: Task[], tasks: Task[],
resourceId: string | number, resourceId: string | number,
): ConflictZone[] { ): ConflictZone[] {
const startTime = performance.now()
// 过滤出包含指定资源的任务(没有resources字段时视为100%分配给该资源) // 过滤出包含指定资源的任务(没有resources字段时视为100%分配给该资源)
const resourceTasks = tasks.filter((task) => { const resourceTasks = tasks.filter((task) => {
// 如果没有resources字段或为空,视为100%分配 // 如果没有resources字段或为空,视为100%分配
@@ -82,9 +79,6 @@ export function detectConflicts(
result = detectConflictsBruteForce(resourceTasks, resourceId) result = detectConflictsBruteForce(resourceTasks, resourceId)
} }
const duration = performance.now() - startTime
perfMonitor.recordConflictDetection(resourceTasks.length, duration)
return result return result
} }
@@ -108,18 +102,6 @@ function detectConflictsBruteForce(
const intersection = getTimeIntersection(task1, task2) const intersection = getTimeIntersection(task1, task2)
if (!intersection) continue if (!intersection) continue
// 🔍 调试日志:输出检测到的时间交集(使用本地日期格式)
if (import.meta.env.DEV) {
const formatLocalDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
console.log(`[ConflictDetect] Tasks ${task1.name} & ${task2.name} overlap:`,
formatLocalDate(intersection.start), '~', formatLocalDate(intersection.end))
}
// 收集该时间段内的所有任务 // 收集该时间段内的所有任务
// 🔧 修复:使用本地日期格式,避免toISOString的UTC时区问题 // 🔧 修复:使用本地日期格式,避免toISOString的UTC时区问题
const formatLocalDate = (date: Date): string => { const formatLocalDate = (date: Date): string => {
@@ -140,49 +122,32 @@ function detectConflictsBruteForce(
return taskIntersection !== null return taskIntersection !== null
}) })
// 🔍 调试日志:输出收集到的重叠任务
if (import.meta.env.DEV) {
console.log(`[ConflictDetect] Found ${overlappingTasks.length} overlapping tasks:`,
overlappingTasks.map(t => `${t.name}[${t.startDate}~${t.endDate}]`).join(', '))
}
// 计算总投入比例 // 计算总投入比例
let totalPercent = 0 let totalPercent = 0
const taskDetails = overlappingTasks.map((task) => { const taskDetails = overlappingTasks.map((task) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent // 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = task.resources?.find((r) => String(r.id) === String(resourceId)) const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent = const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0) !task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += percent totalPercent += capacity
return { return {
id: task.id!, id: task.id!,
name: task.name || '未命名任务', name: task.name || '未命名任务',
percent, capacity,
} }
}) })
// 只有超载(>100%)才算冲突 // 只有超载(>100%)才算冲突
if (totalPercent <= 100) { if (totalPercent <= 100) {
// 🔍 调试:输出未超载的情况
if (import.meta.env.DEV && totalPercent > 0) {
console.log(`[ConflictDetect] No conflict: totalPercent=${totalPercent}% <= 100%`,
`Tasks: ${taskDetails.map(t => `${t.name}(${t.percent}%)`).join(' + ')}`)
}
continue continue
} }
// 🔍 调试:输出检测到的冲突
if (import.meta.env.DEV) {
console.log(`[ConflictDetect] ✓ Conflict detected: totalPercent=${totalPercent}% > 100%`,
`Tasks: ${taskDetails.map(t => `${t.name}(${t.percent}%)`).join(' + ')}`)
}
// 计算冲突范围:所有参与冲突的任务在intersection范围内的并集 // 计算冲突范围:所有参与冲突的任务在intersection范围内的并集
// 过滤出有资源分配的任务(没有resources字段视为100%分配) // 过滤出有资源分配的任务(没有resources字段视为100%分配)
const tasksWithResource = overlappingTasks.filter((task) => { const tasksWithResource = overlappingTasks.filter((task) => {
if (!task.resources || task.resources.length === 0) return true if (!task.resources || task.resources.length === 0) return true
const resource = task.resources?.find((r) => String(r.id) === String(resourceId)) const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
return resource && resource.percent > 0 return resource && resource.capacity && resource.capacity > 0
}) })
// v1.9.6 修复:精确计算真正超载的时间段 // v1.9.6 修复:精确计算真正超载的时间段
@@ -221,9 +186,9 @@ function detectConflictsBruteForce(
const taskEndInclusive = taskEnd.getTime() + 24 * 60 * 60 * 1000 const taskEndInclusive = taskEnd.getTime() + 24 * 60 * 60 * 1000
if (taskStart.getTime() <= segmentStart && taskEndInclusive > segmentStart) { if (taskStart.getTime() <= segmentStart && taskEndInclusive > segmentStart) {
const resource = task.resources?.find((r) => String(r.id) === String(resourceId)) const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent = const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0) !task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += percent segmentPercent += capacity
} }
} }
@@ -327,7 +292,7 @@ function mergeTasks(
} else { } else {
// 已存在,更新为更高的投入比例 // 已存在,更新为更高的投入比例
const existing = taskMap.get(task.id)! const existing = taskMap.get(task.id)!
if (task.percent > existing.percent) { if (task.capacity > existing.capacity) {
taskMap.set(task.id, task) taskMap.set(task.id, task)
} }
} }
@@ -594,12 +559,12 @@ function detectConflictsWithIntervalTree(
const taskDetails = overlappingTasks.map((t) => { const taskDetails = overlappingTasks.map((t) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent // 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = t.resources?.find((r) => String(r.id) === String(resourceId)) const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0) const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += percent totalPercent += capacity
return { return {
id: t.id!, id: t.id!,
name: t.name || '未命名任务', name: t.name || '未命名任务',
percent, capacity,
} }
}) })
@@ -611,7 +576,7 @@ function detectConflictsWithIntervalTree(
const tasksWithResource = overlappingTasks.filter((t) => { const tasksWithResource = overlappingTasks.filter((t) => {
if (!t.resources || t.resources.length === 0) return true if (!t.resources || t.resources.length === 0) return true
const resource = t.resources?.find((r) => String(r.id) === String(resourceId)) const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
return resource && resource.percent > 0 return resource && resource.capacity && resource.capacity > 0
}) })
// 收集所有任务的时间边界点 // 收集所有任务的时间边界点
@@ -648,8 +613,8 @@ function detectConflictsWithIntervalTree(
const tEndInclusive = tEnd.getTime() + 24 * 60 * 60 * 1000 const tEndInclusive = tEnd.getTime() + 24 * 60 * 60 * 1000
if (tStart.getTime() <= segmentStart && tEndInclusive > segmentStart) { if (tStart.getTime() <= segmentStart && tEndInclusive > segmentStart) {
const resource = t.resources?.find((r) => String(r.id) === String(resourceId)) const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0) const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += percent segmentPercent += capacity
} }
} }
+20 -84
View File
@@ -69,31 +69,24 @@ export const perfMonitor = {
/** /**
* *
*/ */
log(tag: string, data: unknown) { log() {
const now = Date.now() const now = Date.now()
if (now - lastLogTime < LOG_THROTTLE) { if (now - lastLogTime < LOG_THROTTLE) {
return return
} }
lastLogTime = now lastLogTime = now
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}:`, data)
}, },
/** /**
* *
*/ */
measure<T>(tag: string, fn: () => T): T { measure<T>(fn: () => T): T {
const start = performance.now() const start = performance.now()
const result = fn() const result = fn()
const end = performance.now() const end = performance.now()
const duration = end - start const duration = end - start
if (duration > 5) { if (duration > 5) { }
// 只记录耗时超过 5ms 的操作
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}: ${duration.toFixed(2)}ms`)
}
return result return result
}, },
@@ -115,7 +108,7 @@ export const perfMonitor = {
fpsValues = [] fpsValues = []
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[Perf] FPS 监控已启动') // console.log('[Perf] FPS 监控已启动')
const updateFps = () => { const updateFps = () => {
if (!fpsMonitorEnabled) return if (!fpsMonitorEnabled) return
@@ -133,19 +126,6 @@ export const perfMonitor = {
fpsValues.shift() fpsValues.shift()
} }
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// 根据 FPS 设置不同颜色
const color = fps >= 55 ? '#67c23a' : fps >= 30 ? '#e6a23c' : '#f56c6c'
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS: ${fps} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
`color: ${color}; font-weight: bold;`,
)
fpsFrameCount = 0 fpsFrameCount = 0
fpsLastTime = currentTime fpsLastTime = currentTime
} }
@@ -172,19 +152,6 @@ export const perfMonitor = {
fpsRafId = null fpsRafId = null
} }
// 输出最终统计
if (fpsValues.length > 0) {
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS 监控已停止 | 总采样: ${fpsValues.length} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
'color: #909399; font-weight: bold;',
)
}
fpsValues = [] fpsValues = []
}, },
@@ -229,7 +196,7 @@ export const perfMonitor = {
printDrawStats() { printDrawStats() {
if (drawStats.totalDraws === 0) { if (drawStats.totalDraws === 0) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[Perf] 暂无绘制数据') // console.log('[Perf] 暂无绘制数据')
return return
} }
@@ -238,10 +205,10 @@ export const perfMonitor = {
recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( // console.log(
'%c[Perf] Canvas 绘制统计', // '%c[Perf] Canvas 绘制统计',
'color: #409eff; font-weight: bold; font-size: 14px;', // 'color: #409eff; font-weight: bold; font-size: 14px;',
) // )
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.table({ console.table({
总绘制次数: drawStats.totalDraws, 总绘制次数: drawStats.totalDraws,
@@ -264,7 +231,7 @@ export const perfMonitor = {
drawStats.recentDraws = [] drawStats.recentDraws = []
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[Perf] 绘制统计已重置') // console.log('[Perf] 绘制统计已重置')
}, },
/** /**
@@ -273,10 +240,10 @@ export const perfMonitor = {
*/ */
startPerformanceTest(durationMs = 10000) { startPerformanceTest(durationMs = 10000) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( // console.log(
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`, // `%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
'color: #409eff; font-weight: bold; font-size: 16px;', // 'color: #409eff; font-weight: bold; font-size: 16px;',
) // )
this.resetDrawStats() this.resetDrawStats()
this.startFpsMonitor() this.startFpsMonitor()
@@ -286,10 +253,10 @@ export const perfMonitor = {
this.printDrawStats() this.printDrawStats()
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( // console.log(
'%c[Perf] 性能测试完成', // '%c[Perf] 性能测试完成',
'color: #67c23a; font-weight: bold; font-size: 16px;', // 'color: #67c23a; font-weight: bold; font-size: 16px;',
) // )
}, durationMs) }, durationMs)
}, },
@@ -306,21 +273,6 @@ export const perfMonitor = {
const now = Date.now() const now = Date.now()
if (now - linkDragStats.lastReportTime > 1000) { if (now - linkDragStats.lastReportTime > 1000) {
const avgCoordTime =
linkDragStats.coordUpdateCount > 0
? (linkDragStats.coordUpdateTotalTime / linkDragStats.coordUpdateCount).toFixed(3)
: 0
const avgTargetTime =
linkDragStats.targetDetectCount > 0
? (linkDragStats.targetDetectTotalTime / linkDragStats.targetDetectCount).toFixed(3)
: 0
// eslint-disable-next-line no-console
console.log(
`[LinkDrag Perf] 坐标更新: ${linkDragStats.coordUpdateCount}次, 平均${avgCoordTime}ms | ` +
`目标检测: ${linkDragStats.targetDetectCount}次, 平均${avgTargetTime}ms`,
)
this.resetLinkDragStats() this.resetLinkDragStats()
linkDragStats.lastReportTime = now linkDragStats.lastReportTime = now
return true return true
@@ -380,7 +332,7 @@ export const perfMonitor = {
if (frameTime > longFrameThreshold) { if (frameTime > longFrameThreshold) {
frameMonitorStats.longFrameCount++ frameMonitorStats.longFrameCount++
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(`[Frame Monitor] 长帧检测: ${frameTime.toFixed(1)}ms`) // console.warn(`[Frame Monitor] 长帧检测: ${frameTime.toFixed(1)}ms`)
} }
frameMonitorStats.lastFrameTime = now frameMonitorStats.lastFrameTime = now
@@ -397,17 +349,6 @@ export const perfMonitor = {
if (frameMonitorStats.frameMonitorId !== null) { if (frameMonitorStats.frameMonitorId !== null) {
cancelAnimationFrame(frameMonitorStats.frameMonitorId) cancelAnimationFrame(frameMonitorStats.frameMonitorId)
frameMonitorStats.frameMonitorId = null frameMonitorStats.frameMonitorId = null
const percentage =
frameMonitorStats.frameCount > 0
? (frameMonitorStats.longFrameCount / frameMonitorStats.frameCount * 100).toFixed(1)
: 0
// eslint-disable-next-line no-console
console.log(
`[Frame Monitor] 统计: ${frameMonitorStats.frameCount}帧, ` +
`长帧数: ${frameMonitorStats.longFrameCount} (${percentage}%)`,
)
} }
}, },
@@ -431,15 +372,10 @@ export const perfMonitor = {
* @param taskCount * @param taskCount
* @param duration * @param duration
*/ */
recordConflictDetection(taskCount: number, duration: number) { recordConflictDetection() {
const now = performance.now() const now = performance.now()
// 每秒最多输出一次日志 // 每秒最多输出一次日志
if (now - lastLogTime > 1000) { if (now - lastLogTime > 1000) {
// eslint-disable-next-line no-console
console.log(
`[Conflict Detection] 任务数: ${taskCount}, 耗时: ${duration.toFixed(2)}ms, ` +
`算法: ${taskCount > 100 ? '区间树(O(n log n))' : '暴力遍历(O(n²))'}`,
)
lastLogTime = now lastLogTime = now
} }
}, },
+4 -16
View File
@@ -14,7 +14,7 @@ class PerformanceMonitor {
*/ */
start(name: string): void { start(name: string): void {
this.marks.set(name, performance.now()) this.marks.set(name, performance.now())
console.log(`[⏱️ START] ${name}`) // console.log(`[⏱️ START] ${name}`)
} }
/** /**
@@ -23,7 +23,7 @@ class PerformanceMonitor {
end(name: string): number { end(name: string): number {
const startTime = this.marks.get(name) const startTime = this.marks.get(name)
if (!startTime) { if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for: ${name}`) // console.warn(`[⏱️ WARN] No start mark found for: ${name}`)
return 0 return 0
} }
@@ -33,14 +33,6 @@ class PerformanceMonitor {
duration, duration,
timestamp: Date.now(), timestamp: Date.now(),
}) })
// 根据耗时使用不同颜色
let icon = '✅'
if (duration > 1000) icon = '🔴'
else if (duration > 500) icon = '🟠'
else if (duration > 100) icon = '🟡'
console.log(`[⏱️ END] ${icon} ${name}: ${duration.toFixed(2)}ms`)
this.marks.delete(name) this.marks.delete(name)
return duration return duration
} }
@@ -48,15 +40,11 @@ class PerformanceMonitor {
/** /**
* *
*/ */
checkpoint(name: string, message: string): void { checkpoint(name: string): void {
const startTime = this.marks.get(name) const startTime = this.marks.get(name)
if (!startTime) { if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for checkpoint: ${name}`)
return return
} }
const elapsed = performance.now() - startTime
console.log(`[⏱️ CHECKPOINT] ${name} - ${message}: ${elapsed.toFixed(2)}ms`)
} }
/** /**
@@ -111,7 +99,7 @@ class PerformanceMonitor {
'Avg (ms)': stat.avg.toFixed(2), 'Avg (ms)': stat.avg.toFixed(2),
'Max (ms)': stat.max.toFixed(2), 'Max (ms)': stat.max.toFixed(2),
'Total (ms)': stat.total.toFixed(2), 'Total (ms)': stat.total.toFixed(2),
})) })),
) )
console.groupEnd() console.groupEnd()
+3 -12
View File
@@ -92,7 +92,6 @@ export class PositionCache {
this.timelineDataHash = newHash this.timelineDataHash = newHash
let cumulativePosition = 0 let cumulativePosition = 0
const startTime = performance.now()
// ⚠️ 关键优化:一次性遍历timelineData,构建完整的日期→位置映射表 // ⚠️ 关键优化:一次性遍历timelineData,构建完整的日期→位置映射表
for (const periodData of timelineData) { for (const periodData of timelineData) {
@@ -102,7 +101,7 @@ export class PositionCache {
for (let i = 0; i < hours.length; i++) { for (let i = 0; i < hours.length; i++) {
const hourData = hours[i] const hourData = hours[i]
const date = new Date(hourData.date) const date = hourData.date ? new Date(hourData.date) : new Date()
const key = this.getCacheKey(date, timeScale) const key = this.getCacheKey(date, timeScale)
const position = cumulativePosition + i * 30 // 小时视图每小时30px const position = cumulativePosition + i * 30 // 小时视图每小时30px
this.cache.set(key, position) this.cache.set(key, position)
@@ -143,8 +142,8 @@ export class PositionCache {
} }
} else if (timeScale === TimelineScale.MONTH) { } else if (timeScale === TimelineScale.MONTH) {
// 月视图:为每个月的每一天建立映射 // 月视图:为每个月的每一天建立映射
const startDate = new Date(periodData.startDate) const startDate = new Date((periodData as TimelineMonth).startDate)
const endDate = new Date(periodData.endDate) const endDate = new Date((periodData as TimelineMonth).endDate)
const daysInMonth = (periodData as TimelineMonth).monthData?.dayCount || 30 const daysInMonth = (periodData as TimelineMonth).monthData?.dayCount || 30
const monthWidth = 60 const monthWidth = 60
const dayWidth = monthWidth / daysInMonth const dayWidth = monthWidth / daysInMonth
@@ -222,14 +221,6 @@ export class PositionCache {
} }
} }
} }
const endTime = performance.now()
// Performance log for debugging
if (this.cache.size > 0) {
const duration = (endTime - startTime).toFixed(2)
// eslint-disable-next-line no-console
console.log(`[PositionCache] Built cache: ${this.cache.size} entries, took ${duration}ms`)
}
} }
/** /**
+1 -1
View File
@@ -29,7 +29,7 @@ function getTaskResourcePercent(task: Task, resourceId: string | number): number
return 100 // 默认100% return 100 // 默认100%
} }
const allocation = task.resources.find((r: any) => String(r.id) === String(resourceId)) const allocation = task.resources.find((r: any) => String(r.id) === String(resourceId))
return allocation?.percent ?? 100 return allocation?.capacity ?? 100
} }
/** /**
+3 -18
View File
@@ -33,20 +33,6 @@ export function hasTimeOverlap(task1: Task, task2: Task): boolean {
return end1Plus > start2 && end2Plus > start1 return end1Plus > start2 && end2Plus > start1
} }
/**
*
*/
function getTaskResourcePercent(task: Task, resourceId?: string | number): number {
if (!resourceId || !task.resources || task.resources.length === 0) {
return 100
}
const allocation = task.resources.find((r: any) => r.id === resourceId)
if (allocation && allocation.percent !== undefined) {
return Math.max(20, Math.min(100, allocation.percent))
}
return 100
}
/** /**
* *
* 使 * 使
@@ -57,7 +43,6 @@ function getTaskResourcePercent(task: Task, resourceId?: string | number): numbe
*/ */
export function assignTaskRows( export function assignTaskRows(
tasks: Task[], tasks: Task[],
resourceId?: string | number,
baseRowHeight = 51, baseRowHeight = 51,
): { ): {
taskRowMap: Map<string | number, number> taskRowMap: Map<string | number, number>
@@ -139,8 +124,8 @@ export function assignTaskRows(
/** /**
* *
*/ */
export function calculateMaxRows(tasks: Task[], resourceId?: string | number): number { export function calculateMaxRows(tasks: Task[]): number {
const result = assignTaskRows(tasks, resourceId) const result = assignTaskRows(tasks)
if (result.taskRowMap.size === 0) { if (result.taskRowMap.size === 0) {
return 1 return 1
} }
@@ -171,7 +156,7 @@ export function calculateResourceTaskLayout(
}) })
// 为当前资源的任务分配行 // 为当前资源的任务分配行
const result = assignTaskRows(resourceTasks, currentResourceId, baseRowHeight) const result = assignTaskRows(resourceTasks, baseRowHeight)
resourceLayoutMap.set(currentResourceId, result) resourceLayoutMap.set(currentResourceId, result)
return resourceLayoutMap return resourceLayoutMap
+21 -21
View File
@@ -146,14 +146,14 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-20', startDate: '2026-01-20',
endDate: '2026-01-25', endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 60 }], resources: [{ id: 'r1', capacity: 60 }],
} as Task, } as Task,
] ]
@@ -169,14 +169,14 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 60 }], resources: [{ id: 'r1', capacity: 60 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
] ]
@@ -195,14 +195,14 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 70 }], resources: [{ id: 'r1', capacity: 70 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 65 }], resources: [{ id: 'r1', capacity: 65 }],
} as Task, } as Task,
] ]
@@ -220,14 +220,14 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 80 }], resources: [{ id: 'r1', capacity: 80 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 90 }], resources: [{ id: 'r1', capacity: 90 }],
} as Task, } as Task,
] ]
@@ -245,21 +245,21 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 40 }], resources: [{ id: 'r1', capacity: 40 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-18', endDate: '2026-01-18',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
{ {
id: 3, id: 3,
name: '任务3', name: '任务3',
startDate: '2026-01-15', startDate: '2026-01-15',
endDate: '2026-01-25', endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 30 }], resources: [{ id: 'r1', capacity: 30 }],
} as Task, } as Task,
] ]
@@ -277,21 +277,21 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 60 }], resources: [{ id: 'r1', capacity: 60 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r2', percent: 80 }], // 不同资源 resources: [{ id: 'r2', capacity: 80 }], // 不同资源
} as Task, } as Task,
{ {
id: 3, id: 3,
name: '任务3', name: '任务3',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
] ]
@@ -317,7 +317,7 @@ describe('conflictUtils', () => {
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
] ]
@@ -333,14 +333,14 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 40 }], resources: [{ id: 'r1', capacity: 40 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
] ]
@@ -357,7 +357,7 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-15', endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 120 }], // 单个任务占比>100%也不算冲突 resources: [{ id: 'r1', capacity: 120 }], // 单个任务占比>100%也不算冲突
} as Task, } as Task,
] ]
@@ -373,21 +373,21 @@ describe('conflictUtils', () => {
name: '任务1', name: '任务1',
startDate: '2026-01-10', startDate: '2026-01-10',
endDate: '2026-01-20', endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 60 }], resources: [{ id: 'r1', capacity: 60 }],
} as Task, } as Task,
{ {
id: 2, id: 2,
name: '任务2', name: '任务2',
startDate: '2026-01-12', startDate: '2026-01-12',
endDate: '2026-01-18', endDate: '2026-01-18',
resources: [{ id: 'r1', percent: 50 }], resources: [{ id: 'r1', capacity: 50 }],
} as Task, } as Task,
{ {
id: 3, id: 3,
name: '任务3', name: '任务3',
startDate: '2026-01-15', startDate: '2026-01-15',
endDate: '2026-01-25', endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 55 }], resources: [{ id: 'r1', capacity: 55 }],
} as Task, } as Task,
] ]
+1 -1
View File
@@ -74,7 +74,7 @@ console.log(`function generateTestTasks(count, resourceId) {
resources: [{ resources: [{
id: resourceId, id: resourceId,
name: '资源1', name: '资源1',
percent: 60 // 60%投入 capacity: 60 // 60%投入
}] }]
}) })
} }