v1.9.0-rc.6 - SIT bugfix
This commit is contained in:
+216
-6
@@ -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`  | `Resource[]` | `[]` | Array of resource data (used in resource planning view) |
|
||||
| `viewMode`  | `'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`  | `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`  | `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`  | `boolean` | `false` | Whether to display actual TaskBar (shows actual execution progress below planned TaskBar) |
|
||||
| `enableTaskbarTooltip`  | `boolean` | `true` | Whether to enable TaskBar hover tooltip (shows task details on mouse hover) |
|
||||
| `enableTaskbarTooltip`  | `boolean` | `true` | Whether to enable TaskBar hover tooltip (shows task details on mouse hover) |
|
||||
| `showConflicts`  | `boolean` | `true` | Whether to display resource conflict visualization layer (shows diagonal stripe background for overload zones in resource view) |
|
||||
| `showTaskbarTab`  | `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`  | `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`  | `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 
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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`  | `Resource[]` | `[]` | 资源数据数组(资源计划视图使用) |
|
||||
| `viewMode`  | `'task' \| 'resource'` | `'task'` | 视图模式:'task' 任务计划视图 \| 'resource' 资源计划视图 |
|
||||
| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
|
||||
| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) |
|
||||
| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) |
|
||||
@@ -213,7 +219,9 @@ npm run dev
|
||||
| `completeTaskBackgroundColor`  | `string` | `'#909399'` | 已完成任务的TaskBar背景色。支持十六进制颜色值(如 `'#909399'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 |
|
||||
| `ongoingTaskBackgroundColor`  | `string` | `'#e6a23c'` | 进行中任务的TaskBar背景色。支持十六进制颜色值(如 `'#e6a23c'`)。**优先级**:高于系统默认,低于 Task 对象的 `barColor` 属性 |
|
||||
| `showActualTaskbar`  | `boolean` | `false` | 是否显示实际TaskBar(在计划TaskBar下方显示实际执行进度) |
|
||||
| `enableTaskbarTooltip`  | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) |
|
||||
| `enableTaskbarTooltip`  | `boolean` | `true` | 是否启用TaskBar悬停提示框(鼠标悬停显示任务详情) |
|
||||
| `showConflicts`  | `boolean` | `true` | 是否显示资源冲突可视化层(资源视图下显示斜纹背景标识超载区域) |
|
||||
| `showTaskbarTab`  | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) |
|
||||
|
||||
#### TaskListColumn 属性
|
||||
|
||||
@@ -360,6 +368,7 @@ npm run dev
|
||||
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- | ---------------- |
|
||||
| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 |
|
||||
| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置 |
|
||||
| `resourceListConfig`  | `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`  | `payload: { task: Task, oldResourceId: string \| number, newResourceId: string \| number }` | 任务跨资源移动事件(资源视图下拖拽任务到另一资源行) |
|
||||
|
||||
#### 示例1:最简单的甘特图
|
||||
|
||||
@@ -1098,6 +1108,208 @@ const handleTaskRowMoved = async (payload: {
|
||||
- 不能拖拽到自己的子任务上(避免循环引用)
|
||||
- 里程碑和里程碑分组不能被拖拽
|
||||
|
||||
### 资源管理 
|
||||
|
||||
资源管理用于管理项目中的人力或设备资源,支持资源视图下的任务分配、资源负载分析、冲突检测等功能。通过 `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
@@ -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 监听viewMode变化,自动同步useDefaultDrawer
|
||||
// 资源视图下关闭默认TaskDrawer,任务视图下开启
|
||||
watch(viewMode, (newMode) => {
|
||||
useDefaultDrawer.value = newMode !== 'resource'
|
||||
})
|
||||
|
||||
const toolbarConfig = {
|
||||
showAddTask: true,
|
||||
showAddMilestone: true,
|
||||
@@ -615,6 +626,28 @@ const handleTaskClick = (task: Task) => {
|
||||
showTaskClickDialog.value = true
|
||||
}
|
||||
|
||||
// v1.9.7 处理任务双击事件(资源视图下显示资源编辑提示)
|
||||
const handleTaskDoubleClick = (taskOrResource: Task | Resource) => {
|
||||
// 在资源视图下,检查是否为资源对象(Resource类型有tasks属性)
|
||||
if (viewMode.value === 'resource' && taskOrResource && typeof taskOrResource === 'object' && 'tasks' in taskOrResource) {
|
||||
clickedResource.value = taskOrResource as Resource
|
||||
resourceEditHintVisible.value = true
|
||||
}
|
||||
// 注意:useDefaultDrawer由watch(viewMode)自动管理,无需在此处手动设置
|
||||
}
|
||||
|
||||
// 关闭资源编辑提示dialog
|
||||
const closeResourceEditHint = () => {
|
||||
resourceEditHintVisible.value = false
|
||||
clickedResource.value = null
|
||||
}
|
||||
|
||||
// v1.9.7 处理视图模式变化事件,同步GanttChart内部状态
|
||||
const handleViewModeChanged = (newMode: 'task' | 'resource') => {
|
||||
viewMode.value = newMode
|
||||
// useDefaultDrawer会由watch(viewMode)自动更新
|
||||
}
|
||||
|
||||
// 关闭 Task Click Dialog
|
||||
const closeTaskClickDialog = () => {
|
||||
showTaskClickDialog.value = false
|
||||
@@ -911,6 +944,9 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateStatus()
|
||||
})
|
||||
|
||||
// v1.9.7 监听资源双击事件
|
||||
//window.addEventListener('task-row-double-click', handleResourceDoubleClick as EventListener)
|
||||
})
|
||||
|
||||
// 清理被删除任务的predecessor依赖关系
|
||||
@@ -1107,6 +1143,23 @@ const confirmResourceDrag = () => {
|
||||
destResource.tasks.push(task)
|
||||
}
|
||||
|
||||
// v1.9.7 Bug修复:同步更新task.resources数组,确保资源分配信息正确显示
|
||||
if (task.resources) {
|
||||
// 移除旧的资源分配
|
||||
const oldResourceIndex = task.resources.findIndex((r: any) => String(r.id) === String(sourceResource.id))
|
||||
if (oldResourceIndex !== -1) {
|
||||
const oldCapacity = task.resources[oldResourceIndex].capacity || 100
|
||||
task.resources.splice(oldResourceIndex, 1)
|
||||
|
||||
// 添加新的资源分配(保持原有的capacity)
|
||||
task.resources.push({
|
||||
id: targetResource.id,
|
||||
name: targetResource.name,
|
||||
capacity: oldCapacity
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示资源分配和日期更新信息
|
||||
const dateInfo = newStartDate && newEndDate ? `,日期已调整为 ${newStartDate} ~ ${newEndDate}` : ''
|
||||
showMessage(`任务 "${task.name}" 已分配给资源 "${targetResource.name}"${dateInfo}`, 'success')
|
||||
@@ -1147,6 +1200,58 @@ const cancelResourceDrag = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// v1.9.7 处理资源双击事件(仅在资源视图下触发)
|
||||
// const handleResourceDoubleClick = (event: CustomEvent) => {
|
||||
// // 只在资源视图下处理
|
||||
// if (viewMode.value !== 'resource') {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const item = event.detail
|
||||
// // 检查是否为资源(Resource类型)
|
||||
// if (item && typeof item === 'object' && 'tasks' in item) {
|
||||
// clickedResource.value = item as Resource
|
||||
// resourceEditHintVisible.value = true
|
||||
// }
|
||||
// }
|
||||
|
||||
// 关闭资源编辑提示dialog
|
||||
// const closeResourceEditHint = () => {
|
||||
// resourceEditHintVisible.value = false
|
||||
// clickedResource.value = null
|
||||
|
||||
// // 重置拖拽数据
|
||||
// resourceDragData.value = {
|
||||
// task: null,
|
||||
// sourceResourceIndex: -1,
|
||||
// targetResourceIndex: -1,
|
||||
// targetResource: null,
|
||||
// newStartDate: undefined,
|
||||
// newEndDate: undefined
|
||||
// }
|
||||
// }
|
||||
|
||||
// v1.9.7 处理资源双击事件(仅在资源视图下触发)
|
||||
// const handleResourceDoubleClick = (event: CustomEvent) => {
|
||||
// // 只在资源视图下处理
|
||||
// if (viewMode.value !== 'resource') {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const item = event.detail
|
||||
// // 检查是否为资源(Resource类型)
|
||||
// if (item && typeof item === 'object' && 'tasks' in item) {
|
||||
// clickedResource.value = item as Resource
|
||||
// resourceEditHintVisible.value = true
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 关闭资源编辑提示dialog
|
||||
// const closeResourceEditHint = () => {
|
||||
// resourceEditHintVisible.value = false
|
||||
// clickedResource.value = null
|
||||
// }
|
||||
|
||||
// 自定义右键菜单操作处理
|
||||
const handleCustomMenuAction = (action: string, task: Task) => {
|
||||
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
|
||||
@@ -1175,7 +1280,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
||||
packageInfo.version
|
||||
}}</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">
|
||||
|
||||
+2506
-2506
File diff suppressed because it is too large
Load Diff
+22
-22
@@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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 只为真正的Task对象打开TaskDrawer,Resource对象不打开(Resource的id是字符串格式)
|
||||
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"
|
||||
/>
|
||||
|
||||
<!-- 甘特图主体 -->
|
||||
|
||||
@@ -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
@@ -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
|
||||
// 实际应该在Timeline层计算好并通过props传递
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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天计算会导致跨月时间范围误差
|
||||
// 例如:2月只有28天,但计算时用30天,会导致部分TaskBar被错误过滤
|
||||
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 (修复:之前是50,应该是30)
|
||||
} 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 修复:月度/季度/年度视图直接使用visibleTimeRange(已通过timelineData精确计算)
|
||||
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="
|
||||
|
||||
@@ -50,9 +50,6 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
// containerWidth优先(约1900px),如果为0则使用1920作为默认值,绝不使用props.width(可能30万px)
|
||||
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) {}
|
||||
|
||||
|
||||
@@ -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 宽度:基于 taskBarWidth,严格不超过taskbar宽度
|
||||
// 参考截图,对于窄taskbar需要严格保证tab不超出
|
||||
@@ -64,7 +67,6 @@ const tabLeftOffset = computed(() => {
|
||||
}
|
||||
|
||||
const taskBarLeft = props.taskBarLeft
|
||||
const taskBarRight = taskBarLeft + (props.taskBarWidth || 0)
|
||||
const viewportLeft = props.scrollLeft
|
||||
|
||||
// TaskBar完全在可视区域右侧,不显示Tab
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ console.log(`function generateTestTasks(count, resourceId) {
|
||||
resources: [{
|
||||
id: resourceId,
|
||||
name: '资源1',
|
||||
percent: 60 // 60%投入
|
||||
capacity: 60 // 60%投入
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user