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
+216 -6
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">
<!-- <p align="center">
<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">
</a>
</p>
</p> -->
<p align="center">
<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
<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
<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 |
| `milestones` | `Task[]` | `[]` | Array of milestone data (Note: Type is Task[], must set type='milestone') |
| `resources` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `Resource[]` | `[]` | Array of resource data (used in resource planning view) |
| `viewMode` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `'task' \| 'resource'` | `'task'` | View mode: 'task' for task planning view \| 'resource' for resource planning view |
| `showToolbar` | `boolean` | `true` | Whether to show the toolbar |
| `useDefaultDrawer` | `boolean` | `true` | Whether to use the built-in task edit drawer (TaskDrawer) |
| `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use the built-in milestone edit dialog (MilestoneDialog) |
@@ -215,7 +221,9 @@ npm run dev
| `completeTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#909399'` | Background color for completed tasks' TaskBar. Supports hex color values (e.g., `'#909399'`). **Priority**: Higher than system default, lower than Task object's `barColor` property |
| `ongoingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#e6a23c'` | Background color for ongoing tasks' TaskBar. Supports hex color values (e.g., `'#e6a23c'`). **Priority**: Higher than system default, lower than Task object's `barColor` property |
| `showActualTaskbar` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | Whether to display actual TaskBar (shows actual execution progress below planned TaskBar) |
| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to enable TaskBar hover tooltip (shows task details on mouse hover) |
| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to enable TaskBar hover tooltip (shows task details on mouse hover) |
| `showConflicts` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to display resource conflict visualization layer (shows diagonal stripe background for overload zones in resource view) |
| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to display resource tab on TaskBar (shows resource allocation label on TaskBar in resource view) |
#### TaskListColumn Component Props
@@ -362,6 +370,7 @@ For complete configuration object documentation, see [⚙️ Configuration & Cus
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------------------- |
| `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration |
| `taskListConfig` | `TaskListConfig` | `undefined` | Task list configuration |
| `resourceListConfig` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `ResourceListConfig` | `undefined` | Resource list configuration |
| `taskBarConfig` | `TaskBarConfig` | `undefined` | Task bar style configuration |
| `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | Custom localization messages |
| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | Working hours configuration |
@@ -408,6 +417,7 @@ For complete event documentation, see:
| `milestone-icon-changed` | `{ milestoneId, icon }` | Milestone icon changed |
| `milestone-drag-end` | `(milestone: Task)` | Milestone drag ended |
| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | TaskRow drag ended (optional) |
| `taskbar-resource-change` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | Task moved across resources (dragging task to another resource row in resource view) |
#### Example 1: Simplest Gantt Chart
@@ -1100,7 +1110,207 @@ const handleTaskRowMoved = async (payload: {
- Cannot drag onto its own child tasks (avoid circular reference)
- Milestones and milestone groups cannot be dragged
### Milestone Management
### Resource Management ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF)
Resource management is used to manage human resources or equipment in a project, supporting task allocation, resource load analysis, and conflict detection in resource view. Switch to resource planning view using the `viewMode="resource"` prop.
> **Core Features**:
> - 📊 **Resource View**: Display task allocation by resource dimension
> - 🎯 **Load Analysis**: Real-time display of resource utilization and overload status
> - ⚠️ **Conflict Detection**: Automatically detect resource time conflicts (e.g., Task A:40% + Task B:40% + Task C:30% = 110% overload)
> - 🎨 **Visualization**: Diagonal stripe background marks conflict zones, resource tabs show utilization percentage
> - 🔄 **Cross-Resource Move**: Support dragging tasks to different resource rows for reallocation
>
> **View Limitations**:
> - ❌ **Task Links Disabled**: Resource view does not display predecessor/successor relationship lines between tasks, as resource view focuses on resource allocation rather than task dependencies
> - ❌ **No Actual TaskBar**: `showActualTaskbar` prop has no effect in resource view, actual execution progress bar will not be displayed
#### Resource Data Structure
| Field | Type | Required | Default | Description |
| --------------- | ------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `id` | `string \| number` | ✅ | - | Unique resource identifier |
| `name` | `string` | ✅ | - | Resource name (e.g., person name, device name) |
| `type` | `string` | - | - | Resource type (e.g., 'developer', 'designer', 'device') |
| `avatar` | `string` | - | - | Resource avatar URL |
| `description` | `string` | - | - | Resource description |
| `department` | `string` | - | - | Department |
| `skills` | `string[]` | - | - | Skill tags array (e.g., `['Vue', 'React', 'TypeScript']`) |
| `capacity` | `number` | - | - | Resource capacity/utilization (0-100), can represent overall load level |
| `color` | `string` | - | - | Custom resource row left border color (e.g., `'#ff5733'`), uses default color scheme if not set |
| `tasks` | `Task[]` | - | `[]` | Array of tasks assigned to this resource, **each task needs `resources` field to mark resource utilization** |
| `[key: string]` | `unknown` | - | - | Support custom property extension, can add any additional fields |
> **Custom Property Extension**: Resource interface supports adding any custom fields, e.g., `email`, `phone`, `location`, `workHours`, etc.
>
> **Task-Resource Association**:
>
> - Each Resource contains a `tasks` array storing tasks assigned to that resource
> - Each Task should include a `resources` field marking which resources are used and their utilization percentage
> - Resource utilization format: `task.resources = [{ id: 'resource1', capacity: 60 }, { id: 'resource2', capacity: 40 }]`
> - `capacity` range: 20-100, representing the percentage of resource used by that task
> - Conflict detection: When multiple tasks' `capacity` sum > 100% for the same resource in the same time period, a conflict warning is displayed
**Resource Data Example**:
```typescript
import type { Resource, Task } from 'jordium-gantt-vue3'
const resources: Resource[] = [
{
id: 'dev-001',
name: 'Zhang San',
type: 'developer',
avatar: '/avatars/zhangsan.jpg',
department: 'R&D',
skills: ['Vue', 'TypeScript', 'Node.js'],
capacity: 85, // Overall load level
color: '#409eff',
tasks: [
{
id: 1,
name: 'Frontend Development',
startDate: '2026-02-01',
endDate: '2026-02-10',
progress: 50,
resources: [
{ id: 'dev-001', capacity: 60 }, // This task uses 60% of Zhang San's time
{ id: 'dev-002', capacity: 40 } // Also uses 40% of Li Si's time
]
},
{
id: 2,
name: 'Code Review',
startDate: '2026-02-05',
endDate: '2026-02-08',
progress: 0,
resources: [
{ id: 'dev-001', capacity: 40 } // This task uses 40% of Zhang San's time
]
}
// Note: If two tasks overlap, Zhang San's total utilization on Feb 5-8 is 100% (60%+40%), at threshold
]
},
{
id: 'dev-002',
name: 'Li Si',
type: 'developer',
avatar: '/avatars/lisi.jpg',
department: 'R&D',
skills: ['React', 'TypeScript'],
tasks: []
}
]
```
**Resource Conflict Detection Example**:
```typescript
// Scenario: Zhang San is assigned 3 tasks in the same time period
const resource = {
id: 'dev-001',
name: 'Zhang San',
tasks: [
{
id: 1,
name: 'Task A',
startDate: '2026-02-10',
endDate: '2026-02-15',
resources: [{ id: 'dev-001', capacity: 40 }] // Uses 40%
},
{
id: 2,
name: 'Task B',
startDate: '2026-02-10',
endDate: '2026-02-20',
resources: [{ id: 'dev-001', capacity: 40 }] // Uses 40%
},
{
id: 3,
name: 'Task C',
startDate: '2026-02-12',
endDate: '2026-02-18',
resources: [{ id: 'dev-001', capacity: 30 }] // Uses 30%
}
]
}
// Conflict Analysis:
// - Feb 10-11: A(40%) + B(40%) = 80%, not overloaded
// - Feb 12-15: A(40%) + B(40%) + C(30%) = 110%, overloaded! Shows conflict warning
// - Feb 16-18: B(40%) + C(30%) = 70%, not overloaded
// - Feb 19-20: B(40%), not overloaded
```
#### Resource-Related Props
| Prop | Type | Default | Description |
| --------------------- | ---------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
| `resources` | `Resource[]` | `[]` | Array of resource data |
| `viewMode` | `'task' \| 'resource'` | `'task'` | View mode: 'task' for task planning view, 'resource' for resource planning view |
| `resourceListConfig` | `ResourceListConfig` | `undefined` | Resource list configuration, similar to TaskListConfig, for configuring resource list columns, width etc |
| `showConflicts` | `boolean` | `true` | Whether to display resource conflict visualization layer (diagonal stripe background in resource view) |
| `showTaskbarTab` | `boolean` | `true` | Whether to display resource tab on TaskBar (resource utilization label on TaskBar in resource view) |
#### Resource Events
| Event Name | Parameters | Trigger Timing | Description |
| -------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------- |
| `taskbar-resource-change` | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | When task drag across resources | Triggered when dragging task to another resource row in resource view, component auto-updates task's `resources` field |
**Data Sync Notes**:
-**Auto Update**: Resource-related operations (like task cross-resource move) are automatically updated by the component in `props.resources` and task's `resources` field
-**Events for Notification**: External event listeners are mainly used for: showing messages, calling backend APIs, updating other related data, etc.
-**Avoid Duplicate Operations**: Do not modify data again in event handlers, as this causes duplicate updates
#### Example: Basic Resource View Usage
```vue
<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
+215 -3
View File
@@ -29,11 +29,11 @@
<p align="center">现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案</p>
<p align="center">
<!-- <p align="center">
<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">
</a>
</p>
</p> -->
<p align="center">
<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-theme1.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[]` | `[]` | 任务数据数组 |
| `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone' |
| `resources` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `Resource[]` | `[]` | 资源数据数组(资源计划视图使用) |
| `viewMode` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `'task' \| 'resource'` | `'task'` | 视图模式:'task' 任务计划视图 \| 'resource' 资源计划视图 |
| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) |
| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog |
@@ -213,7 +219,9 @@ npm run dev
| `completeTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#909399'` | 已完成任务的TaskBar背景色。支持十六进制颜色值(如 `'#909399'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 |
| `ongoingTaskBackgroundColor` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `string` | `'#e6a23c'` | 进行中任务的TaskBar背景色。支持十六进制颜色值(如 `'#e6a23c'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 |
| `showActualTaskbar` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `false` | 是否显示实际TaskBar(在计划TaskBar下方显示实际执行进度) |
| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) |
| `enableTaskbarTooltip` ![v1.8.0](https://img.shields.io/badge/v1.8.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) |
| `showConflicts` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否显示资源冲突可视化层(资源视图下显示斜纹背景标识超载区域) |
| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) |
#### TaskListColumn 属性
@@ -360,6 +368,7 @@ npm run dev
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------- |
| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 |
| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置 |
| `resourceListConfig` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `ResourceListConfig` | `undefined` | 资源列表配置 |
| `taskBarConfig` | `TaskBarConfig` | `undefined` | 任务条样式配置 |
| `localeMessages` | `Partial<Messages['zh-CN']>` | `undefined` | 自定义多语言配置 |
| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | 工作时间配置 |
@@ -406,6 +415,7 @@ npm run dev
| `milestone-icon-changed` | `{ milestoneId, icon }` | 里程碑图标变更 |
| `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 |
| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | 拖拽TaskRow结束(可选) |
| `taskbar-resource-change` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | 任务跨资源移动事件(资源视图下拖拽任务到另一资源行) |
#### 示例1:最简单的甘特图
@@ -1098,6 +1108,208 @@ const handleTaskRowMoved = async (payload: {
- 不能拖拽到自己的子任务上(避免循环引用)
- 里程碑和里程碑分组不能被拖拽
### 资源管理 ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF)
资源管理用于管理项目中的人力或设备资源,支持资源视图下的任务分配、资源负载分析、冲突检测等功能。通过 `viewMode="resource"` 属性切换到资源计划视图。
> **核心特性**
> - 📊 **资源视图**:按资源维度展示任务分配情况
> - 🎯 **负载分析**:实时显示资源占用率和超载状态
> - ⚠️ **冲突检测**:自动检测资源时间冲突(如 A任务:40% + B任务:40% + C任务:30% = 110%超载)
> - 🎨 **可视化**:斜纹背景标识冲突区域,资源Tab显示占用比例
> - 🔄 **跨资源移动**:支持拖拽任务到不同资源行进行重新分配
>
> **视图限制**
> - ❌ **任务关系线禁用**:资源视图下不显示任务之间的前后置关系线,因为资源视图关注资源分配而非任务依赖关系
> - ❌ **不支持实际TaskBar**`showActualTaskbar` 属性在资源视图下无效,不会显示实际执行进度条
#### Resource 数据结构
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
| --------------- | ------------------- | ---- | ------ | ----------------------------------------------------------------------------------------------- |
| `id` | `string \| number` | ✅ | - | 资源唯一标识符 |
| `name` | `string` | ✅ | - | 资源名称(如人名、设备名) |
| `type` | `string` | - | - | 资源类型(如 'developer', 'designer', 'device' 等) |
| `avatar` | `string` | - | - | 资源头像 URL |
| `description` | `string` | - | - | 资源描述 |
| `department` | `string` | - | - | 所属部门 |
| `skills` | `string[]` | - | - | 技能标签数组(如 `['Vue', 'React', 'TypeScript']`) |
| `capacity` | `number` | - | - | 资源容量/利用率(0-100),可用于表示资源的整体负载水平 |
| `color` | `string` | - | - | 自定义资源行左边框颜色(如 `'#ff5733'`),若不设置则使用默认颜色方案 |
| `tasks` | `Task[]` | - | `[]` | 分配给该资源的任务数组,**每个任务需包含 `resources` 字段标注资源占用比例** |
| `[key: string]` | `unknown` | - | - | 支持自定义属性扩展,可添加任意额外字段 |
> **自定义属性扩展**:Resource 接口支持添加任意自定义字段,例如:`email`、`phone`、`location`、`workHours` 等业务相关字段。
>
> **任务资源关联说明**
>
> - 每个 Resource 包含一个 `tasks` 数组,存储分配给该资源的任务
> - 每个 Task 应包含 `resources` 字段,标注该任务使用了哪些资源及占用比例
> - 资源占用比例格式:`task.resources = [{ id: 'resource1', capacity: 60 }, { id: 'resource2', capacity: 40 }]`
> - `capacity` 范围:20-100,表示该任务占用该资源的百分比
> - 冲突检测:当同一资源在同一时间段的多个任务 `capacity` 总和 > 100% 时,会显示冲突警告
**Resource 数据示例**
```typescript
import type { Resource, Task } from 'jordium-gantt-vue3'
const resources: Resource[] = [
{
id: 'dev-001',
name: '张三',
type: 'developer',
avatar: '/avatars/zhangsan.jpg',
department: '研发部',
skills: ['Vue', 'TypeScript', 'Node.js'],
capacity: 85, // 整体负载水平
color: '#409eff',
tasks: [
{
id: 1,
name: '前端开发',
startDate: '2026-02-01',
endDate: '2026-02-10',
progress: 50,
resources: [
{ id: 'dev-001', capacity: 60 }, // 该任务占用张三60%的时间
{ id: 'dev-002', capacity: 40 } // 同时占用李四40%的时间
]
},
{
id: 2,
name: '代码审查',
startDate: '2026-02-05',
endDate: '2026-02-08',
progress: 0,
resources: [
{ id: 'dev-001', capacity: 40 } // 该任务占用张三40%的时间
]
}
// 注意:如果两个任务时间重叠,张三在2月5-8日的总占用率为100%(60%+40%),临界值
]
},
{
id: 'dev-002',
name: '李四',
type: 'developer',
avatar: '/avatars/lisi.jpg',
department: '研发部',
skills: ['React', 'TypeScript'],
tasks: []
}
]
```
**资源冲突检测示例**
```typescript
// 场景:张三在同一时间段被分配了3个任务
const resource = {
id: 'dev-001',
name: '张三',
tasks: [
{
id: 1,
name: '任务A',
startDate: '2026-02-10',
endDate: '2026-02-15',
resources: [{ id: 'dev-001', capacity: 40 }] // 占用40%
},
{
id: 2,
name: '任务B',
startDate: '2026-02-10',
endDate: '2026-02-20',
resources: [{ id: 'dev-001', capacity: 40 }] // 占用40%
},
{
id: 3,
name: '任务C',
startDate: '2026-02-12',
endDate: '2026-02-18',
resources: [{ id: 'dev-001', capacity: 30 }] // 占用30%
}
]
}
// 冲突分析:
// - 2月10-11日:A(40%) + B(40%) = 80%,未超载
// - 2月12-15日:A(40%) + B(40%) + C(30%) = 110%,超载!显示冲突警告
// - 2月16-18日:B(40%) + C(30%) = 70%,未超载
// - 2月19-20日:B(40%),未超载
```
#### 资源相关属性
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------- | --------------------- | ------------ | --------------------------------------------------------------------------- |
| `resources` | `Resource[]` | `[]` | 资源数据数组 |
| `viewMode` | `'task' \| 'resource'` | `'task'` | 视图模式:'task' 任务计划视图,'resource' 资源计划视图 |
| `resourceListConfig` | `ResourceListConfig` | `undefined` | 资源列表配置,类似 TaskListConfig,用于配置资源列表的列定义、宽度等 |
| `showConflicts` | `boolean` | `true` | 是否显示资源冲突可视化层(资源视图下显示斜纹背景标识超载区域) |
| `showTaskbarTab` | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar上的资源占用比例标签) |
#### 资源事件
| 事件名 | 参数 | 触发时机 | 说明 |
| -------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- |
| `taskbar-resource-change` | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | 任务跨资源拖拽结束时 | 资源视图下拖拽任务到另一资源行时触发,组件已自动更新任务的 `resources` 字段 |
**数据同步说明**
-**组件内部自动更新**:资源相关操作(如任务跨资源移动)组件会自动更新 `props.resources` 和任务的 `resources` 字段
-**事件仅做通知**:外部监听事件主要用于:显示提示消息、调用后端 API、更新其他相关数据等
-**避免重复操作**:不要在事件处理器中再次修改数据,否则会导致重复更新
#### 示例:资源视图基础用法
```vue
<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,也支持完全自定义编辑行为。
+129 -4
View File
@@ -39,6 +39,7 @@ const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
const resources = ref<Resource[]>([])
const viewMode = ref<'task' | 'resource'>('task')
const useDefaultDrawer = ref(true)
const rawDataSources = [
{
@@ -202,6 +203,16 @@ const isMilestoneEditMode = ref(false)
// Drawer
const showVersionDrawer = ref(false)
// v1.9.7 dialog
const resourceEditHintVisible = ref(false)
const clickedResource = ref<Resource | null>(null)
// v1.9.7 viewModeuseDefaultDrawer
// TaskDrawer
watch(viewMode, (newMode) => {
useDefaultDrawer.value = newMode !== 'resource'
})
const toolbarConfig = {
showAddTask: true,
showAddMilestone: true,
@@ -615,6 +626,28 @@ const handleTaskClick = (task: Task) => {
showTaskClickDialog.value = true
}
// v1.9.7
const handleTaskDoubleClick = (taskOrResource: Task | Resource) => {
// 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
const closeTaskClickDialog = () => {
showTaskClickDialog.value = false
@@ -911,6 +944,9 @@ onMounted(() => {
nextTick(() => {
updateStatus()
})
// v1.9.7
//window.addEventListener('task-row-double-click', handleResourceDoubleClick as EventListener)
})
// predecessor
@@ -1107,6 +1143,23 @@ const confirmResourceDrag = () => {
destResource.tasks.push(task)
}
// v1.9.7 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}` : ''
showMessage(`任务 "${task.name}" 已分配给资源 "${targetResource.name}"${dateInfo}`, 'success')
@@ -1147,6 +1200,58 @@ const cancelResourceDrag = () => {
}
}
// v1.9.7
// const handleResourceDoubleClick = (event: CustomEvent) => {
// //
// if (viewMode.value !== 'resource') {
// return
// }
// const item = event.detail
// // Resource
// if (item && typeof item === 'object' && 'tasks' in item) {
// clickedResource.value = item as Resource
// resourceEditHintVisible.value = true
// }
// }
// dialog
// const closeResourceEditHint = () => {
// resourceEditHintVisible.value = false
// clickedResource.value = null
// //
// resourceDragData.value = {
// task: null,
// sourceResourceIndex: -1,
// targetResourceIndex: -1,
// targetResource: null,
// newStartDate: undefined,
// newEndDate: undefined
// }
// }
// v1.9.7
// const handleResourceDoubleClick = (event: CustomEvent) => {
// //
// if (viewMode.value !== 'resource') {
// return
// }
// const item = event.detail
// // Resource
// if (item && typeof item === 'object' && 'tasks' in item) {
// clickedResource.value = item as Resource
// resourceEditHintVisible.value = true
// }
// }
// // dialog
// const closeResourceEditHint = () => {
// resourceEditHintVisible.value = false
// clickedResource.value = null
// }
//
const handleCustomMenuAction = (action: string, task: Task) => {
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
@@ -1175,7 +1280,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
packageInfo.version
}}</span>
</div>
<div class="title-center">
<!-- <div class="title-center">
<a
:href="demoMessages.giteeBadge?.url || 'https://gitee.com/activity/2025opensource?ident=IOUNZP'"
target="_blank"
@@ -1184,7 +1289,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
>
{{ demoMessages.giteeBadge?.text || '🥇 Gitee 2025 Open Source Awards 👉 Thanks for Your Vote' }}
</a>
</div>
</div> -->
<div class="title-right docs-links">
<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">
@@ -2114,6 +2219,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
:delay-task-background-color="delayTaskBackgroundColor"
:complete-task-background-color="completeTaskBackgroundColor"
:ongoing-task-background-color="ongoingTaskBackgroundColor"
:use-default-drawer="useDefaultDrawer"
@milestone-saved="handleMilestoneSaved"
@milestone-deleted="handleMilestoneDeleted"
@milestone-icon-changed="handleMilestoneIconChanged"
@@ -2122,6 +2228,8 @@ const handleCustomMenuAction = (action: string, task: Task) => {
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd"
@task-click="handleTaskClick"
@task-double-click="handleTaskDoubleClick"
@view-mode-changed="handleViewModeChanged"
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
@close="() => showMessage('已关闭任务编辑', 'info')"
@timer-started="onTimerStarted"
@@ -2138,8 +2246,6 @@ const handleCustomMenuAction = (action: string, task: Task) => {
@task-added="handleTaskAddEvent"
@task-updated="handleTaskUpdateEvent"
@task-row-moved="handleTaskRowMoved"
@view-mode-change="mode => viewMode = mode"
@resource-click="resource => showMessage(`资源点击: ${resource.name}`, 'info')"
@resource-drag-end="handleResourceDragEnd"
>
<!-- 自定义任务名称内容 (TaskRow TaskBar) -->
@@ -2339,6 +2445,25 @@ const handleCustomMenuAction = (action: string, task: Task) => {
</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 资源拖拽确认对话框 -->
<div v-if="resourceDragConfirmVisible" class="modal-overlay" @click.self="cancelResourceDrag">
<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,
"type": "story",
"resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 100 }
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 100 }
]
},
{
@@ -29,7 +29,7 @@
"progress": 100,
"type": "task",
"resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 75 }
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 75 }
]
},
{
@@ -40,7 +40,7 @@
"progress": 80,
"type": "task",
"resources": [
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "percent": 50 }
{ "id": "resource-chen", "name": "首席研究员 Dr. Chen", "capacity": 50 }
]
}
]
@@ -63,7 +63,7 @@
"progress": 65,
"type": "story",
"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,
"type": "task",
"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,
"type": "task",
"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,
"type": "task",
"resources": [
{ "id": "resource-li", "name": "方案设计师 李明", "percent": 100 }
{ "id": "resource-li", "name": "方案设计师 李明", "capacity": 100 }
]
},
{
@@ -119,7 +119,7 @@
"type": "task",
"barColor": "#7B3FF2",
"resources": [
{ "id": "resource-li", "name": "方案设计师 李明", "percent": 75 }
{ "id": "resource-li", "name": "方案设计师 李明", "capacity": 75 }
]
}
]
@@ -142,7 +142,7 @@
"progress": 50,
"type": "task",
"resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 100 }
{ "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 100 }
]
},
{
@@ -153,7 +153,7 @@
"progress": 70,
"type": "task",
"resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 50 }
{ "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 50 }
]
},
{
@@ -164,7 +164,7 @@
"progress": 25,
"type": "task",
"resources": [
{ "id": "resource-zhang", "name": "数据分析师 张伟", "percent": 75 }
{ "id": "resource-zhang", "name": "数据分析师 张伟", "capacity": 75 }
]
}
]
@@ -296,7 +296,7 @@
"progress": 100,
"type": "task",
"resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 100 }
{ "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 100 }
]
},
{
@@ -307,7 +307,7 @@
"progress": 75,
"type": "task",
"resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 75 }
{ "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 75 }
]
},
{
@@ -318,7 +318,7 @@
"progress": 85,
"type": "task",
"resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 50 }
{ "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 50 }
]
},
{
@@ -329,7 +329,7 @@
"progress": 30,
"type": "task",
"resources": [
{ "id": "resource-sun", "name": "生物统计师 孙丽", "percent": 100 }
{ "id": "resource-sun", "name": "生物统计师 孙丽", "capacity": 100 }
]
}
]
@@ -352,7 +352,7 @@
"progress": 100,
"type": "task",
"resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 100 }
{ "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 100 }
]
},
{
@@ -363,7 +363,7 @@
"progress": 100,
"type": "task",
"resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 75 }
{ "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 75 }
]
},
{
@@ -374,7 +374,7 @@
"progress": 90,
"type": "task",
"resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 50 }
{ "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 50 }
]
},
{
@@ -385,7 +385,7 @@
"progress": 70,
"type": "task",
"resources": [
{ "id": "resource-zhou", "name": "临床监查员 周杰", "percent": 25 }
{ "id": "resource-zhou", "name": "临床监查员 周杰", "capacity": 25 }
]
}
]
@@ -408,7 +408,7 @@
"progress": 100,
"type": "task",
"resources": [
{ "id": "resource-xu", "name": "医学撰写员 徐静", "percent": 80 }
{ "id": "resource-xu", "name": "医学撰写员 徐静", "capacity": 80 }
]
},
{
@@ -419,7 +419,7 @@
"progress": 60,
"type": "task",
"resources": [
{ "id": "resource-xu", "name": "医学撰写员 徐静", "percent": 70 }
{ "id": "resource-xu", "name": "医学撰写员 徐静", "capacity": 70 }
]
},
{
@@ -430,7 +430,7 @@
"progress": 20,
"type": "task",
"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",
"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": {
"title": "Confirm Task Move",
"confirmText": "Confirm",
+5
View File
@@ -3,6 +3,11 @@
"text": "🥇 Gitee 2025年度开源项目评选 👉 感谢您的参与投票",
"url": "https://gitee.com/activity/2025opensource?ident=IOUNZP"
},
"resourceEditHint": {
"title": "资源编辑提示",
"message": "资源条目被双击,此事件可以自定义完成资源的编辑需求。\n\n当前资源:{resourceName}\n\n您可以在此处实现资源属性编辑功能,例如修改资源名称、容量、成本等信息。",
"confirmText": "知道了"
},
"taskMoveConfirm": {
"title": "确认移动任务",
"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)
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({
id: taskId,
@@ -43,7 +43,7 @@ function generateLargeResourceData() {
{
id: resourceId,
name: `资源${i}`,
percent,
capacity,
},
],
})
+50 -8
View File
@@ -93,6 +93,7 @@ const emit = defineEmits([
'task-updated',
'task-collapse-change', //
'link-deleted', //
'view-mode-changed', //
//
'add-task',
'add-milestone',
@@ -106,8 +107,6 @@ const emit = defineEmits([
// TaskRow
'task-row-moved',
// v1.9.0
'view-mode-change', //
'resource-click', //
'taskbar-resource-change', //
'resource-drag-end', // v1.9.0
])
@@ -152,7 +151,7 @@ const resourceTaskLayouts = computed(() => {
resources.forEach(resource => {
const resourceId = String(resource.id)
if (resource.tasks && resource.tasks.length > 0) {
const layout = assignTaskRows(resource.tasks, resourceId, baseRowHeight)
const layout = assignTaskRows(resource.tasks, baseRowHeight)
layouts.set(resourceId, layout)
} else {
// 使
@@ -195,7 +194,7 @@ const resourceConflicts = computed(() => {
if (currentViewMode.value !== 'resource') return new Map()
const resources = currentDataSource.value as Resource[]
const conflictsMap = new Map<string, Set<number>>()
const conflictsMap = new Map<string, Set<number | string>>()
// updateTaskTrigger 便
if (updateTaskTrigger.value >= 0) {
@@ -208,7 +207,7 @@ const resourceConflicts = computed(() => {
const conflictZones = detectConflicts(tasks, resource.id)
if (conflictZones.length > 0) {
const conflicts = new Set<number>()
const conflicts = new Set<number | string>()
// ID
conflictZones.forEach(zone => {
@@ -548,7 +547,8 @@ const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
const handleViewModeChange = (newMode: 'task' | 'resource') => {
if (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 => {
if (newMode && currentViewMode.value !== newMode) {
currentViewMode.value = newMode
// v1.9.7 emit
emit('view-mode-changed', newMode)
}
},
)
@@ -2525,6 +2527,7 @@ function handleToolbarAddTask() {
description: '',
parentId: undefined,
children: [],
resources: [],
}
taskDrawerTask.value = newTask
taskDrawerEditMode.value = false
@@ -2581,7 +2584,8 @@ function handleTimelineEditTask(task: Task) {
emit('task-double-click', task)
// useDefaultDrawer TaskDrawer
if (props.useDefaultDrawer) {
// v1.9.7 TaskTaskDrawerResourceResourceid
if (props.useDefaultDrawer && typeof task.id === 'number') {
taskDrawerTask.value = task
taskDrawerEditMode.value = true
taskDrawerVisible.value = true
@@ -2614,6 +2618,7 @@ function handleAddPredecessor(targetTask: Task) {
description: '',
parentId: targetTask.parentId,
children: [],
resources: [],
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
@@ -2648,6 +2653,7 @@ function handleAddSuccessor(targetTask: Task) {
description: '',
parentId: targetTask.parentId,
children: [],
resources: [],
isTimerRunning: false,
timerStartTime: 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
const updateTaskStateInTree = (taskId: number, updateFn: (task: Task) => void): boolean => {
const updateInList = (tasks: Task[]): boolean => {
@@ -2779,6 +2819,9 @@ function handleTaskDrawerSubmit(task: Task) {
if (props.tasks) {
insertTask(props.tasks, task)
}
// v1.9.0
addTaskToResource(task)
// emit
emit('task-added', { task })
if (taskToAddPredecessorTo.value) {
@@ -2984,7 +3027,6 @@ defineExpose({
@add-milestone="milestoneAddHandler"
@expand-all="handleExpandAll"
@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') => {
if (currentViewMode.value !== mode) {
currentViewMode.value = mode
// GanttChart
if (props.onViewModeChange && typeof props.onViewModeChange === 'function') {
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="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
{{ t('resourceView') || '资源视图' }}
{{ t('resourceView.desc') || '资源视图' }}
</button>
</div>
+15 -22
View File
@@ -64,8 +64,8 @@ const resourcePercent = computed(() => {
return String(r.id) === String(props.currentResourceId)
})
if (allocation && allocation.percent !== undefined) {
const val = Number(allocation.percent)
if (allocation && allocation.capacity !== undefined) {
const val = Number(allocation.capacity)
if (Number.isFinite(val) && val >= 0) {
return Math.max(0, Math.min(100, val))
}
@@ -87,13 +87,6 @@ const currentResourceColor = computed(() => {
return '#85ce61'
})
// v1.9.2
const currentResourceTotalLoad = computed(() => {
// undefined
// Timelineprops
return undefined
})
// v1.9.2
const currentResourceName = computed(() => {
if (!props.currentResourceId) return ''
@@ -3357,7 +3350,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
'--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */,
'--handle-width': `${actualHandleWidth}px` /* 传递手柄宽度给CSS变量 */,
'--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-border-color': dynamicBorderColor, /* v1.9.2 使用动态边框颜色 */
boxShadow: isParent
@@ -3410,7 +3403,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
:task="task"
:current-resource-id="currentResourceId"
:resource-color="currentResourceColor"
:resource-percent="resourcePercent"
:resource-capacity="resourcePercent"
:resource-name="currentResourceName"
:task-bar-width="taskBarWidth"
:task-bar-left="taskBarLeft"
@@ -3505,7 +3498,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div v-else class="task-name">
{{ task.name }}
<!-- v1.9.0 资源视图显示占比文字 -->
<span v-if="shouldShowPercentText" class="resource-percent-text">
<span v-if="shouldShowPercentText" class="resource-capacity-text">
{{ resourcePercent }}%
</span>
</div>
@@ -3638,7 +3631,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div class="tooltip-content">
<!-- v1.9.0 资源视图显示利用率 -->
<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>
</div>
<div class="tooltip-row">
@@ -3663,7 +3656,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
</div>
<!-- v1.9.0 资源冲突警告 -->
<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>
@@ -3849,7 +3842,7 @@ class="hover-tooltip-arrow" :style="{
top: 0;
left: 0;
right: 0;
height: calc((1 - var(--allocation-percent, 1)) * 100%);
height: calc((1 - var(--allocation-capacity, 1)) * 100%);
border-top: 1.5px dashed currentColor;
border-left: 1.5px dashed currentColor;
border-right: 1.5px dashed currentColor;
@@ -3863,7 +3856,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,隐藏上半部分镂空区域 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::before {
.task-bar.resource-view[style*="--allocation-capacity: 1"]::before {
display: none;
}
@@ -3874,7 +3867,7 @@ class="hover-tooltip-arrow" :style="{
left: 0;
right: 0;
bottom: 0;
height: calc(var(--allocation-percent, 1) * 100%);
height: calc(var(--allocation-capacity, 1) * 100%);
background: var(--task-bar-bg-color, #e3f2fd);
/*border: 1px solid var(--task-bar-border-color, #90caf9);*/
border-radius: 0 0 4px 4px;
@@ -3885,7 +3878,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,整个TaskBar都是实心,四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::after {
.task-bar.resource-view[style*="--allocation-capacity: 1"]::after {
border-radius: 4px;
}
@@ -3895,7 +3888,7 @@ class="hover-tooltip-arrow" :style="{
bottom: 0;
left: 0;
top: auto;
height: calc(var(--allocation-percent, 1) * 100%);
height: calc(var(--allocation-capacity, 1) * 100%);
z-index: 1;
border-radius: 0 0 4px 4px;
pointer-events: none;
@@ -3903,7 +3896,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,进度条四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"] .progress-bar {
.task-bar.resource-view[style*="--allocation-capacity: 1"] .progress-bar {
border-radius: 4px;
}
@@ -4380,7 +4373,7 @@ class="hover-tooltip-arrow" :style="{
}
/* v1.9.0 资源占比文字样式 */
.resource-percent-text {
.resource-capacity-text {
display: inline-block;
margin-left: 6px;
font-size: 12px;
@@ -4399,7 +4392,7 @@ class="hover-tooltip-arrow" :style="{
.task-bar[style*="width: 28px"],
.task-bar[style*="width: 32px"],
.task-bar[style*="width: 36px"] {
.resource-percent-text {
.resource-capacity-text {
display: none;
}
}
+26 -22
View File
@@ -7,6 +7,7 @@ import GanttConfirmDialog from './GanttConfirmDialog.vue'
import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
import type { Task } from '../models/classes/Task'
import { Resource } from '../models/classes/Resource'
import '../styles/app.css'
interface AssigneeOption {
@@ -197,7 +198,7 @@ const getTaskTypeDisplay = (type: string): string => {
const progressSliderStyle = computed(() => {
const progressPercent = formData.progress || 0
return {
'--progress-percent': `${progressPercent}%`,
'--progress-capacity': `${progressPercent}%`,
}
})
@@ -500,6 +501,7 @@ const handleSubmit = async () => {
const taskData: Task = {
...formData,
id: props.isEdit && props.task ? props.task.id : Date.now(),
resources: formData.resources, // resources
}
emit('submit', taskData)
handleClose()
@@ -680,11 +682,12 @@ const addResource = () => {
if (!formData.resources) {
formData.resources = []
}
formData.resources.push({
formData.resources.push(new Resource({
id: '',
name: '',
percent: 100,
})
capacity: 100,
tasks: [],
}))
}
const removeResource = (index: number) => {
@@ -693,7 +696,7 @@ const removeResource = (index: number) => {
}
}
const handleResourceChange = (index: number, field: 'id' | 'percent', value: string | number) => {
const handleResourceChange = (index: number, field: 'id' | 'capacity', value: string | number) => {
if (!formData.resources || !formData.resources[index]) return
if (field === 'id') {
@@ -702,12 +705,12 @@ const handleResourceChange = (index: number, field: 'id' | 'percent', value: str
formData.resources[index].id = selected.value
formData.resources[index].name = selected.label
}
} else if (field === 'percent') {
let percent = typeof value === 'string' ? parseInt(value) : value
} else if (field === 'capacity') {
let capacity = typeof value === 'string' ? parseInt(value) : value
// 20-100
if (percent < 20) percent = 20
if (percent > 100) percent = 100
formData.resources[index].percent = percent
if (capacity < 20) capacity = 20
if (capacity > 100) capacity = 100
formData.resources[index].capacity = capacity
}
}
@@ -728,11 +731,12 @@ const mapAssigneeToResources = () => {
formData.resources = assignees.map((assigneeId) => {
// assigneeOption
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
return {
return new Resource({
id: assigneeId,
name: option?.label || String(assigneeId),
percent: 100, // 100%
}
capacity: 100, // 100%
tasks: [],
})
})
}
@@ -1026,9 +1030,9 @@ const taskStatus = computed(() => {
</select>
<select
v-model.number="resource.percent"
class="form-select percent-select"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)"
v-model.number="resource.capacity"
class="form-select capacity-select"
@change="handleResourceChange(index, 'capacity', ($event.target as HTMLSelectElement).value)"
>
<option :value="25">25%</option>
<option :value="50">50%</option>
@@ -1474,8 +1478,8 @@ const taskStatus = computed(() => {
background: linear-gradient(
to right,
var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%),
var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100%
);
}
@@ -1487,8 +1491,8 @@ const taskStatus = computed(() => {
background: linear-gradient(
to right,
var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%),
var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100%
);
border: none;
@@ -1508,7 +1512,7 @@ const taskStatus = computed(() => {
top: 0;
left: 0;
height: 6px;
width: var(--progress-percent, 0%);
width: var(--progress-capacity, 0%);
background: var(--gantt-primary, #409eff);
border-radius: 3px;
pointer-events: none;
@@ -1769,7 +1773,7 @@ const taskStatus = computed(() => {
padding: 8px 12px;
}
.percent-select {
.capacity-select {
width: 110px;
height: 36px;
flex-shrink: 0;
+50 -103
View File
@@ -13,7 +13,7 @@ import { getPredecessorIds } from '../utils/predecessorUtils'
import { perfMonitor } from '../utils/perfMonitor'
import { perfMonitor2 } from '../utils/perfMonitor2' // v1.9.6
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 { TimelineConfig } from '../models/configs/TimelineConfig'
import { TimelineScale } from '../models/types/TimelineScale'
@@ -115,7 +115,7 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n
if (viewMode.value !== 'resource') return []
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 resource = resources.find(r => String(r.id) === String(resourceId))
@@ -180,7 +180,6 @@ const layoutCache = new Map<string, {
let resourceTaskLayoutsCallCount = 0
const resourceTaskLayouts = computed(() => {
resourceTaskLayoutsCallCount++
const startTime = performance.now()
const layoutMap = new Map<string | 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
})
@@ -258,7 +249,7 @@ const getResourceLayout = (resource: Resource) => {
//
let result
if (resourceTasks.length > 0) {
result = assignTaskRows(resourceTasks, resource.id, 51)
result = assignTaskRows(resourceTasks, 51)
} else {
result = {
taskRowMap: new Map(),
@@ -275,7 +266,6 @@ const getResourceLayout = (resource: Resource) => {
let resourceRowPositionsCallCount = 0
const resourceRowPositions = computed(() => {
resourceRowPositionsCallCount++
const startTime = performance.now()
const positions = new Map<string | number, number>()
if (viewMode.value !== 'resource') {
@@ -305,7 +295,6 @@ const resourceRowPositions = computed(() => {
//
if (cumulativeTop > scrollBottom + ROW_HEIGHT * 20) {
// 51px
const remainingResources = resources.length - processedCount
for (let i = processedCount; i < resources.length; i++) {
positions.set(resources[i].id, cumulativeTop) //
cumulativeTop += 51 //
@@ -314,9 +303,6 @@ const resourceRowPositions = computed(() => {
}
}
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return positions
})
@@ -1599,12 +1585,6 @@ const visibleTimeRange = computed(() => {
//
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 }
})
@@ -1724,7 +1704,6 @@ const getDateByScrollPosition = (scrollPosition: number): Date => {
let visibleTaskRangeCallCount = 0
const visibleTaskRange = computed(() => {
visibleTaskRangeCallCount++
const startTime = performance.now()
const scrollTop = timelineBodyScrollTop.value
const containerHeight = timelineBodyHeight.value || 600
@@ -1762,9 +1741,6 @@ const visibleTaskRange = computed(() => {
}
}
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return {
startIndex: Math.max(0, startIndex),
endIndex: Math.min(resources.length, endIndex),
@@ -1856,8 +1832,6 @@ const scheduleResourceBatchRender = () => {
const frameStartTime = performance.now()
const limits = new Map(resourceTaskRenderLimits.value)
let hasMore = false
let hasBackground = false
let processedInFrame = 0
resourceTaskQueues.value.forEach((queue, resourceId) => {
//
@@ -1868,19 +1842,12 @@ const scheduleResourceBatchRender = () => {
const current = limits.get(resourceId) || 0
// v1.9.6 Sprint4 - 使 visibleCount background
// queue.totalCount queue.visibleCount rebuildResourceTaskQueues backgroundTasks
const phaseTarget = queue.visibleCount
// v1.9.6 Sprint4 - hasBackground background
// if (queue.totalCount > queue.visibleCount) {
// hasBackground = true
// }
if (current < phaseTarget) {
const next = Math.min(phaseTarget, current + RESOURCE_BATCH_SIZE)
limits.set(resourceId, next)
hasMore = true
processedInFrame++
}
})
@@ -1915,51 +1882,54 @@ const rebuildResourceTaskQueues = () => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR
// 1: -
const scrollLeft = timelineScrollLeft.value
const containerWidth = timelineContainerWidth.value
// v1.9.7 Bug//使visibleTimeRange
// 28-31使30
// 22830TaskBar
const scale = currentTimeScale.value
const timelineStart = timelineConfig.value.startDate.getTime()
const usePixelLevelFilter = scale === TimelineScale.HOUR || scale === TimelineScale.DAY || scale === TimelineScale.WEEK
//
let pixelPerMs = 0
if (scale === TimelineScale.HOUR) {
pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour
} else if (scale === TimelineScale.DAY) {
pixelPerMs = 30 / (24 * 60 * 60 * 1000) // 30px per day (5030)
} else if (scale === TimelineScale.WEEK) {
pixelPerMs = 60 / (7 * 24 * 60 * 60 * 1000) // 60px per week
} else if (scale === TimelineScale.MONTH) {
pixelPerMs = 60 / (30 * 24 * 60 * 60 * 1000) // 60px per month (approximate)
} else if (scale === TimelineScale.QUARTER) {
pixelPerMs = 90 / (90 * 24 * 60 * 60 * 1000) // 90px per quarter
} else if (scale === TimelineScale.YEAR) {
pixelPerMs = 120 / (365 * 24 * 60 * 60 * 1000) // 120px per year
let viewportStartTime: number
let viewportEndTime: number
if (usePixelLevelFilter) {
// 1: - - //
const scrollLeft = timelineScrollLeft.value
const containerWidth = timelineContainerWidth.value
const timelineStart = timelineConfig.value.startDate.getTime()
//
let pixelPerMs = 0
if (scale === TimelineScale.HOUR) {
pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour
} 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 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) -
const currentCache = new Map(taskBarRenderCache.value)
const newCache = new Map<string, TaskBarRenderCache>()
const currentTimestamp = Date.now()
let cachedCount = 0 // TaskBar
let totalVisibleTaskBars = 0 // v1.9.6 Sprint4 -
visibleResources.value.forEach(({ resource }) => {
const resourceId = resource.id as string | number
@@ -1995,7 +1965,11 @@ const rebuildResourceTaskQueues = () => {
taskId,
resourceId,
rendered: isRendered,
timestamp: isRendered ? existingCache.timestamp : currentTimestamp,
timestamp: isRendered ?
(existingCache ?
existingCache.timestamp
: currentTimestamp)
: currentTimestamp,
})
})
@@ -2008,8 +1982,6 @@ const rebuildResourceTaskQueues = () => {
originalTasks,
})
totalVisibleTaskBars += visibleTasks.length // v1.9.6 Sprint4 -
// v1.9.6 Sprint2(P5) - TaskBar
const previousLimit = resourceTaskRenderLimits.value.get(resourceId)
let initialLimit: number
@@ -2030,10 +2002,6 @@ const rebuildResourceTaskQueues = () => {
resourceRenderPhase.value = 'visible'
taskBarRenderCache.value = newCache //
// v1.9.6 Sprint2(P5) -
const totalTaskBars = newCache.size
const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0'
scheduleResourceBatchRender()
}
@@ -2090,11 +2058,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
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 totalFilteredTasks = 0
@@ -2131,21 +2094,16 @@ const visibleResourcesWithFilteredTasks = computed(() => {
totalFilteredTasks += renderTasks.length
return {
resource: {
resource: new Resource({
...resource,
tasks: renderTasks,
// GanttConflicts
allTasks: originalTasks,
} as Resource,
}),
originalIndex,
}
})
// v1.9.6 Sprint2(P1) - 10
if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) {
const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1)
}
return result
})
@@ -2524,7 +2482,6 @@ const handleTaskRowHover = (taskId: number | string | null) => {
// Timeline
const contentHeight = computed(() => {
const startTime = performance.now()
const minHeight = 400 //
// v1.9.0 使
@@ -2538,9 +2495,6 @@ const contentHeight = computed(() => {
totalHeight += layout?.totalHeight || 51
})
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return Math.max(totalHeight, minHeight, timelineBodyHeight.value)
}
@@ -2603,10 +2557,8 @@ const handleMilestoneUpdate = (updatedMilestone: Milestone) => {
//
const generateTimelineData = (): any => {
const startTime = performance.now()
// 使
const result = getCachedTimelineData()
const duration = (performance.now() - startTime).toFixed(2)
return result
}
@@ -2984,18 +2936,12 @@ watch(
[timelineData, currentTimeScale],
([newData, newScale]) => {
positionCacheWatchCount++
const watchStartTime = performance.now()
if (newData && newScale) {
//
const cacheStartTime = performance.now()
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/
@@ -5673,7 +5619,7 @@ const handleAddSuccessor = (task: Task) => {
<!-- 为资源下的每个任务渲染 TaskBar -->
<template v-if="(resource as any).tasks && (resource as any).tasks.length > 0">
<TaskBar
v-for="(task, taskIndex) in (resource as any).tasks"
v-for="(task) in (resource as any).tasks"
:key="`taskbar-${task.id}-${taskBarRenderKey}`"
:task="task"
:row-index="originalIndex"
@@ -5754,9 +5700,10 @@ const handleAddSuccessor = (task: Task) => {
<!-- v1.9.5 修复传递任务行号信息正确计算冲突区域高度 -->
<!-- v1.9.5 可通过 show-conflicts prop 控制是否显示 -->
<!-- v1.9.6 修复width使用totalTimelineWidth用于坐标计算containerWidth用于Canvas宽度 -->
<!-- v1.9.7 Bug修复使用渲染的tasks而不是allTasks避免滚动后显示已消失TaskBar的冲突 -->
<GanttConflicts
v-if="showConflicts"
:tasks="(resource as any).allTasks || (resource as any).tasks"
:tasks="(resource as any).tasks"
:resource-id="resource.id"
:day-width="dayWidth"
:start-date="
+16 -35
View File
@@ -50,9 +50,6 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
// containerWidth1900px0使1920使props.width30px
const canvasWidth = computed(() => {
const width = props.containerWidth || 1920
if (import.meta.env.DEV) {
console.log(`[GanttConflicts] canvasWidth: ${width}px (containerWidth: ${props.containerWidth}, totalWidth: ${props.width})`)
}
return width
})
const canvasHeight = computed(() => props.height)
@@ -161,8 +158,11 @@ watch([() => props.timelineData, () => props.currentTimeScale], () => {
if (import.meta.env.DEV) {}
// v1.9.4 BUG -
coordsCache.clear()
recalculateConflicts()
// v1.9.7 Bug - 使nextTick
texturePatterns.value = { light: null, medium: null, severe: null }
nextTick(() => {
recalculateConflicts()
})
}, { deep: true })
// v1.9.6 scrollLeft
@@ -186,8 +186,6 @@ watch(() => props.scrollLeft, () => {
// TaskBar
function recalculateConflictsIncremental(changedTaskId: string | number) {
const startTime = performance.now()
//
const changedTask = props.tasks.find(t => t.id === changedTaskId)
if (!changedTask) {
@@ -198,10 +196,14 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
//
const affectedTasks = props.tasks.filter(task => {
//
const taskStart = new Date(task.startDate)
const taskEnd = new Date(task.endDate)
const changedStart = new Date(changedTask.startDate)
const changedEnd = new Date(changedTask.endDate)
const taskStart = task.startDate ? new Date(task.startDate) : null
if (!taskStart) return false
const taskEnd = task.endDate ? new Date(task.endDate) : null
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)
})
@@ -213,7 +215,7 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
const affectedTaskIds = new Set(affectedTasks.map(t => t.id))
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,
height,
}
}).filter(Boolean)
const endTime = performance.now()
const elapsed = endTime - startTime
}).filter(z => z !== null) as ConflictZone[]
// 使
renderConflictsIncremental()
@@ -304,13 +303,6 @@ function recalculateConflicts() {
//
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 - 使
conflictZones.value = conflicts.map((zone) => {
// key
@@ -386,9 +378,6 @@ function recalculateConflicts() {
}
}
//
if (import.meta.env.DEV) {}
return {
...zone,
left: canvasLeft,
@@ -396,19 +385,11 @@ function recalculateConflicts() {
top,
height,
}
}).filter(Boolean) //
}).filter(z => z !== null) as ConflictZone[]
const endTime = performance.now()
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) {}
+23 -26
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import type { Task } from '../../models/classes/Task'
import { useI18n } from '../../composables/useI18n'
interface Props {
task: Task
currentResourceId: string | number
resourceColor: string
resourcePercent: number
resourceCapacity: number
resourceName: string
taskBarWidth?: number
taskBarLeft?: number
@@ -33,6 +34,8 @@ const emit = defineEmits<{
'hover-change': [isHovered: boolean]
}>()
const { t } = useI18n()
//
const isExpanded = ref(false)
const tabElement = ref<HTMLElement | null>(null)
@@ -43,7 +46,7 @@ let debounceTimer: number | null = null
const DEBOUNCE_DELAY = 50 // 50ms
//
const percentText = computed(() => `${Math.round(props.resourcePercent)}%`)
const percentText = computed(() => `${Math.round(props.resourceCapacity)}%`)
// Tab taskBarWidthtaskbar
// taskbartab
@@ -64,7 +67,6 @@ const tabLeftOffset = computed(() => {
}
const taskBarLeft = props.taskBarLeft
const taskBarRight = taskBarLeft + (props.taskBarWidth || 0)
const viewportLeft = props.scrollLeft
// TaskBarTab
@@ -146,7 +148,7 @@ const expandedStyle = computed(() => {
if (shouldExpandUpward) {
//
return {
position: 'fixed',
position: 'fixed' as const,
bottom: `${viewportHeight - rect.top + 2}px`,
left: `${rect.left}px`,
maxHeight: `${Math.min(spaceAbove - 10, 400)}px`, // 10px
@@ -156,7 +158,7 @@ const expandedStyle = computed(() => {
} else {
//
return {
position: 'fixed',
position: 'fixed' as const,
top: `${rect.bottom + 2}px`,
left: `${rect.left}px`,
maxHeight: `${Math.min(spaceBelow - 10, 400)}px`, // 10px
@@ -174,9 +176,10 @@ const formattedDateRange = computed(() => {
const end = new Date(props.task.endDate)
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
return `${year}/${month}/${day}`
}
return `${formatDate(start)} ~ ${formatDate(end)}`
@@ -194,9 +197,6 @@ const conflictInfoList = computed(() => {
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
//
const currentPercent = props.resourcePercent || 100
// v1.9.8
return props.conflictTasks.map(conflictTask => {
if (!conflictTask.startDate || !conflictTask.endDate) return null
@@ -210,8 +210,8 @@ const conflictInfoList = computed(() => {
const allocation = conflictTask.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
conflictPercent = Math.max(20, Math.min(100, allocation.percent))
if (allocation && allocation.capacity !== undefined) {
conflictPercent = Math.max(20, Math.min(100, allocation.capacity))
}
}
@@ -221,7 +221,7 @@ const conflictInfoList = computed(() => {
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}`
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
}
return {
@@ -241,10 +241,7 @@ const totalOverloadPercent = computed(() => {
const currentTask = props.task
if (!currentTask.startDate || !currentTask.endDate) return 0
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
const currentPercent = props.resourcePercent || 100
const currentPercent = props.resourceCapacity || 100
// v1.9.9 endDate +1
const DAY_MS = 24 * 60 * 60 * 1000
@@ -290,8 +287,8 @@ const totalOverloadPercent = computed(() => {
const allocation = task.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
taskPercent = allocation.percent
if (allocation && allocation.capacity !== undefined) {
taskPercent = allocation.capacity
}
}
intervalTotal += taskPercent
@@ -432,12 +429,12 @@ onUnmounted(() => {
<div class="expanded-body">
<!-- 利用率 -->
<div class="expanded-row">
<span class="info-label">利用率</span>
<span class="info-label">{{ t.resourceView.capacity }}</span>
<span class="info-value">{{ percentText }}</span>
</div>
<!-- 日期范围 -->
<div class="expanded-row">
<span class="info-label">时间范围</span>
<span class="info-label">{{ t.resourceView.duration }}</span>
<span class="info-value">{{ formattedDateRange }}</span>
</div>
<!-- 冲突预警有冲突时才显示 -->
@@ -447,20 +444,20 @@ onUnmounted(() => {
<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"/>
</svg>
<span class="conflict-title">资源超载警告</span>
<span class="conflict-title">{{ t.resourceView.overloadWarning }}</span>
<span class="total-overload"> {{ totalOverloadPercent }}%</span>
</div>
<!-- 可滚动的冲突列表 -->
<div class="conflict-list-container">
<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">
<span class="conflict-label">冲突时段</span>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span>
<span class="conflict-label">{{ t.resourceView.conflictDuration }}</span>
<span class="conflict-value">{{ info ? info.overlapStart : '' }} ~ {{ info ? info.overlapEnd : '' }}</span>
</div>
<div class="conflict-detail">
<span class="conflict-label">任务利用率</span>
<span class="conflict-value">{{ info.conflictPercent }}%</span>
<span class="conflict-label">{{ t.resourceView.capacity }}: </span>
<span class="conflict-value">{{ info ? info.conflictPercent : '' }}%</span>
</div>
</div>
</div>
+20 -2
View File
@@ -97,7 +97,6 @@ const messages = {
collapseAll: '全部折叠',
// v1.9.0 视图模式
taskView: '任务视图',
resourceView: '资源视图',
language: '中文',
languageTooltip: '选择语言',
lightMode: '明亮模式',
@@ -297,6 +296,16 @@ const messages = {
},
},
},
resourceView: {
desc: '资源视图',
capacity: '利用率',
overloaded: '超负荷',
duration: '时间周期',
overloadWarning: '资源超负荷警告',
conflictDuration: '冲突时段',
conflictWith: '与',
conflictSuffix: '冲突',
},
},
'en-US': {
dateNotSet: 'Not set',
@@ -389,7 +398,6 @@ const messages = {
collapseAll: 'Collapse All',
// v1.9.0 视图模式
taskView: 'Task View',
resourceView: 'Resource View',
language: 'English',
languageTooltip: 'Select language',
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
* @returns (20-100)100
*/
private getTaskAllocationPercent(task: any): number {
getTaskAllocationPercent(task: any): number {
if (!task.resources || !Array.isArray(task.resources)) {
return 100 // 未配置resources时,默认100%
}
@@ -159,7 +159,7 @@ export class Resource {
return 100 // 未找到当前资源的分配信息,默认100%
}
const percent = allocation.percent ?? 100
return Math.max(20, Math.min(100, percent)) // 限制范围 20-100
const capacity = allocation.capacity ?? 100
return Math.max(20, Math.min(100, capacity)) // 限制范围 20-100
}
}
+2 -12
View File
@@ -1,14 +1,4 @@
/**
* (Resource Allocation)
* @version 1.9.0
*/
export interface ResourceAllocation {
id: string | number // 资源ID
name: string // 资源名称
percent?: number // 投入精力占比 (20-100),默认100
}
// Task 类型定义
import { Resource } from './Resource'// Task 类型定义
export interface Task {
id: number
name: string
@@ -42,7 +32,7 @@ export interface Task {
// 自定义样式
barColor?: string // 自定义TaskBar颜色,如 '#ff5733',若不设置则使用默认颜色方案
// v1.9.0 资源占用比例
resources?: ResourceAllocation[] // 资源分配列表,包含占比信息
resources?: Resource[] // 资源分配列表,包含占比信息
// 支持自定义属性 - 使用 unknown 允许任意类型
[key: string]: unknown
}
+16 -51
View File
@@ -8,7 +8,6 @@
*/
import type { Task } from '../models/classes/Task'
import { perfMonitor } from './perfMonitor'
/**
*
@@ -26,7 +25,7 @@ export interface ConflictZone {
tasks: Array<{
id: number | string
name: string
percent: number
capacity: number
}>
/** Canvas渲染坐标(由GanttConflicts组件计算填充) */
left?: number
@@ -57,8 +56,6 @@ export function detectConflicts(
tasks: Task[],
resourceId: string | number,
): ConflictZone[] {
const startTime = performance.now()
// 过滤出包含指定资源的任务(没有resources字段时视为100%分配给该资源)
const resourceTasks = tasks.filter((task) => {
// 如果没有resources字段或为空,视为100%分配
@@ -82,9 +79,6 @@ export function detectConflicts(
result = detectConflictsBruteForce(resourceTasks, resourceId)
}
const duration = performance.now() - startTime
perfMonitor.recordConflictDetection(resourceTasks.length, duration)
return result
}
@@ -108,18 +102,6 @@ function detectConflictsBruteForce(
const intersection = getTimeIntersection(task1, task2)
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时区问题
const formatLocalDate = (date: Date): string => {
@@ -140,49 +122,32 @@ function detectConflictsBruteForce(
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
const taskDetails = overlappingTasks.map((task) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0)
totalPercent += percent
const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += capacity
return {
id: task.id!,
name: task.name || '未命名任务',
percent,
capacity,
}
})
// 只有超载(>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
}
// 🔍 调试:输出检测到的冲突
if (import.meta.env.DEV) {
console.log(`[ConflictDetect] ✓ Conflict detected: totalPercent=${totalPercent}% > 100%`,
`Tasks: ${taskDetails.map(t => `${t.name}(${t.percent}%)`).join(' + ')}`)
}
// 计算冲突范围:所有参与冲突的任务在intersection范围内的并集
// 过滤出有资源分配的任务(没有resources字段视为100%分配)
const tasksWithResource = overlappingTasks.filter((task) => {
if (!task.resources || task.resources.length === 0) return true
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 修复:精确计算真正超载的时间段
@@ -221,9 +186,9 @@ function detectConflictsBruteForce(
const taskEndInclusive = taskEnd.getTime() + 24 * 60 * 60 * 1000
if (taskStart.getTime() <= segmentStart && taskEndInclusive > segmentStart) {
const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0)
segmentPercent += percent
const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += capacity
}
}
@@ -327,7 +292,7 @@ function mergeTasks(
} else {
// 已存在,更新为更高的投入比例
const existing = taskMap.get(task.id)!
if (task.percent > existing.percent) {
if (task.capacity > existing.capacity) {
taskMap.set(task.id, task)
}
}
@@ -594,12 +559,12 @@ function detectConflictsWithIntervalTree(
const taskDetails = overlappingTasks.map((t) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0)
totalPercent += percent
const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += capacity
return {
id: t.id!,
name: t.name || '未命名任务',
percent,
capacity,
}
})
@@ -611,7 +576,7 @@ function detectConflictsWithIntervalTree(
const tasksWithResource = overlappingTasks.filter((t) => {
if (!t.resources || t.resources.length === 0) return true
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
if (tStart.getTime() <= segmentStart && tEndInclusive > segmentStart) {
const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0)
segmentPercent += percent
const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += capacity
}
}
+20 -84
View File
@@ -69,31 +69,24 @@ export const perfMonitor = {
/**
*
*/
log(tag: string, data: unknown) {
log() {
const now = Date.now()
if (now - lastLogTime < LOG_THROTTLE) {
return
}
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 result = fn()
const end = performance.now()
const duration = end - start
if (duration > 5) {
// 只记录耗时超过 5ms 的操作
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}: ${duration.toFixed(2)}ms`)
}
if (duration > 5) { }
return result
},
@@ -115,7 +108,7 @@ export const perfMonitor = {
fpsValues = []
// eslint-disable-next-line no-console
console.log('[Perf] FPS 监控已启动')
// console.log('[Perf] FPS 监控已启动')
const updateFps = () => {
if (!fpsMonitorEnabled) return
@@ -133,19 +126,6 @@ export const perfMonitor = {
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
fpsLastTime = currentTime
}
@@ -172,19 +152,6 @@ export const perfMonitor = {
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 = []
},
@@ -229,7 +196,7 @@ export const perfMonitor = {
printDrawStats() {
if (drawStats.totalDraws === 0) {
// eslint-disable-next-line no-console
console.log('[Perf] 暂无绘制数据')
// console.log('[Perf] 暂无绘制数据')
return
}
@@ -238,10 +205,10 @@ export const perfMonitor = {
recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length
// eslint-disable-next-line no-console
console.log(
'%c[Perf] Canvas 绘制统计',
'color: #409eff; font-weight: bold; font-size: 14px;',
)
// console.log(
// '%c[Perf] Canvas 绘制统计',
// 'color: #409eff; font-weight: bold; font-size: 14px;',
// )
// eslint-disable-next-line no-console
console.table({
总绘制次数: drawStats.totalDraws,
@@ -264,7 +231,7 @@ export const perfMonitor = {
drawStats.recentDraws = []
// eslint-disable-next-line no-console
console.log('[Perf] 绘制统计已重置')
// console.log('[Perf] 绘制统计已重置')
},
/**
@@ -273,10 +240,10 @@ export const perfMonitor = {
*/
startPerformanceTest(durationMs = 10000) {
// eslint-disable-next-line no-console
console.log(
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
'color: #409eff; font-weight: bold; font-size: 16px;',
)
// console.log(
// `%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
// 'color: #409eff; font-weight: bold; font-size: 16px;',
// )
this.resetDrawStats()
this.startFpsMonitor()
@@ -286,10 +253,10 @@ export const perfMonitor = {
this.printDrawStats()
// eslint-disable-next-line no-console
console.log(
'%c[Perf] 性能测试完成',
'color: #67c23a; font-weight: bold; font-size: 16px;',
)
// console.log(
// '%c[Perf] 性能测试完成',
// 'color: #67c23a; font-weight: bold; font-size: 16px;',
// )
}, durationMs)
},
@@ -306,21 +273,6 @@ export const perfMonitor = {
const now = Date.now()
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()
linkDragStats.lastReportTime = now
return true
@@ -380,7 +332,7 @@ export const perfMonitor = {
if (frameTime > longFrameThreshold) {
frameMonitorStats.longFrameCount++
// 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
@@ -397,17 +349,6 @@ export const perfMonitor = {
if (frameMonitorStats.frameMonitorId !== null) {
cancelAnimationFrame(frameMonitorStats.frameMonitorId)
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 duration
*/
recordConflictDetection(taskCount: number, duration: number) {
recordConflictDetection() {
const now = performance.now()
// 每秒最多输出一次日志
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
}
},
+4 -16
View File
@@ -14,7 +14,7 @@ class PerformanceMonitor {
*/
start(name: string): void {
this.marks.set(name, performance.now())
console.log(`[⏱️ START] ${name}`)
// console.log(`[⏱️ START] ${name}`)
}
/**
@@ -23,7 +23,7 @@ class PerformanceMonitor {
end(name: string): number {
const startTime = this.marks.get(name)
if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for: ${name}`)
// console.warn(`[⏱️ WARN] No start mark found for: ${name}`)
return 0
}
@@ -33,14 +33,6 @@ class PerformanceMonitor {
duration,
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)
return duration
}
@@ -48,15 +40,11 @@ class PerformanceMonitor {
/**
*
*/
checkpoint(name: string, message: string): void {
checkpoint(name: string): void {
const startTime = this.marks.get(name)
if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for checkpoint: ${name}`)
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),
'Max (ms)': stat.max.toFixed(2),
'Total (ms)': stat.total.toFixed(2),
}))
})),
)
console.groupEnd()
+3 -12
View File
@@ -92,7 +92,6 @@ export class PositionCache {
this.timelineDataHash = newHash
let cumulativePosition = 0
const startTime = performance.now()
// ⚠️ 关键优化:一次性遍历timelineData,构建完整的日期→位置映射表
for (const periodData of timelineData) {
@@ -102,7 +101,7 @@ export class PositionCache {
for (let i = 0; i < hours.length; 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 position = cumulativePosition + i * 30 // 小时视图每小时30px
this.cache.set(key, position)
@@ -143,8 +142,8 @@ export class PositionCache {
}
} else if (timeScale === TimelineScale.MONTH) {
// 月视图:为每个月的每一天建立映射
const startDate = new Date(periodData.startDate)
const endDate = new Date(periodData.endDate)
const startDate = new Date((periodData as TimelineMonth).startDate)
const endDate = new Date((periodData as TimelineMonth).endDate)
const daysInMonth = (periodData as TimelineMonth).monthData?.dayCount || 30
const monthWidth = 60
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%
}
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
}
/**
*
*/
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(
tasks: Task[],
resourceId?: string | number,
baseRowHeight = 51,
): {
taskRowMap: Map<string | number, number>
@@ -139,8 +124,8 @@ export function assignTaskRows(
/**
*
*/
export function calculateMaxRows(tasks: Task[], resourceId?: string | number): number {
const result = assignTaskRows(tasks, resourceId)
export function calculateMaxRows(tasks: Task[]): number {
const result = assignTaskRows(tasks)
if (result.taskRowMap.size === 0) {
return 1
}
@@ -171,7 +156,7 @@ export function calculateResourceTaskLayout(
})
// 为当前资源的任务分配行
const result = assignTaskRows(resourceTasks, currentResourceId, baseRowHeight)
const result = assignTaskRows(resourceTasks, baseRowHeight)
resourceLayoutMap.set(currentResourceId, result)
return resourceLayoutMap
+21 -21
View File
@@ -146,14 +146,14 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-20',
endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 60 }],
resources: [{ id: 'r1', capacity: 60 }],
} as Task,
]
@@ -169,14 +169,14 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 60 }],
resources: [{ id: 'r1', capacity: 60 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
]
@@ -195,14 +195,14 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 70 }],
resources: [{ id: 'r1', capacity: 70 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 65 }],
resources: [{ id: 'r1', capacity: 65 }],
} as Task,
]
@@ -220,14 +220,14 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 80 }],
resources: [{ id: 'r1', capacity: 80 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 90 }],
resources: [{ id: 'r1', capacity: 90 }],
} as Task,
]
@@ -245,21 +245,21 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 40 }],
resources: [{ id: 'r1', capacity: 40 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-18',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
{
id: 3,
name: '任务3',
startDate: '2026-01-15',
endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 30 }],
resources: [{ id: 'r1', capacity: 30 }],
} as Task,
]
@@ -277,21 +277,21 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 60 }],
resources: [{ id: 'r1', capacity: 60 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r2', percent: 80 }], // 不同资源
resources: [{ id: 'r2', capacity: 80 }], // 不同资源
} as Task,
{
id: 3,
name: '任务3',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
]
@@ -317,7 +317,7 @@ describe('conflictUtils', () => {
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
]
@@ -333,14 +333,14 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 40 }],
resources: [{ id: 'r1', capacity: 40 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
]
@@ -357,7 +357,7 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-15',
resources: [{ id: 'r1', percent: 120 }], // 单个任务占比>100%也不算冲突
resources: [{ id: 'r1', capacity: 120 }], // 单个任务占比>100%也不算冲突
} as Task,
]
@@ -373,21 +373,21 @@ describe('conflictUtils', () => {
name: '任务1',
startDate: '2026-01-10',
endDate: '2026-01-20',
resources: [{ id: 'r1', percent: 60 }],
resources: [{ id: 'r1', capacity: 60 }],
} as Task,
{
id: 2,
name: '任务2',
startDate: '2026-01-12',
endDate: '2026-01-18',
resources: [{ id: 'r1', percent: 50 }],
resources: [{ id: 'r1', capacity: 50 }],
} as Task,
{
id: 3,
name: '任务3',
startDate: '2026-01-15',
endDate: '2026-01-25',
resources: [{ id: 'r1', percent: 55 }],
resources: [{ id: 'r1', capacity: 55 }],
} as Task,
]
+1 -1
View File
@@ -74,7 +74,7 @@ console.log(`function generateTestTasks(count, resourceId) {
resources: [{
id: resourceId,
name: '资源1',
percent: 60 // 60%投入
capacity: 60 // 60%投入
}]
})
}