Merge branch 'master' into develop
# Conflicts: # src/components/GanttChart.vue # src/components/Timeline.vue
This commit is contained in:
@@ -35,3 +35,5 @@ dist-ssr
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.4.2-patch3] - 2025-11-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 支持视图上下/左右拖拽
|
||||||
|
- Support for vertical/horizontal dragging of the view
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 性能优化:canvas绘制性能优化
|
||||||
|
- 性能优化:拖拽视图性能优化,拖拽期间禁止重绘canvas
|
||||||
|
- 性能优化:周视图月开始分隔线实现使用canvas替代dom
|
||||||
|
- 性能优化:使用canvas替代svg links实现
|
||||||
|
- 性能优化:路径缓存+防抖+shallowRef计算
|
||||||
|
- Performance Optimization: Canvas drawing performance optimization
|
||||||
|
- Performance Optimization: View dragging performance optimization, disabling canvas redraw during dragging
|
||||||
|
- Performance Optimization: Weekly view month start separator line implemented using canvas instead of DOM
|
||||||
|
- Performance Optimization: Using canvas instead of SVG links implementation
|
||||||
|
- Performance Optimization: Path caching + debounce + shallowRef calculation
|
||||||
|
|
||||||
|
## [1.4.2-patch1] - 2025-11-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 缺陷修复:更新父级任务后,再次更新子级任务是发生无限循环调用的问题修复
|
||||||
|
- Defect fix: Fixed the issue of infinite loop calls when updating child tasks after updating the parent task
|
||||||
|
|
||||||
## [1.4.2-patch2] - 2025-11-11
|
## [1.4.2-patch2] - 2025-11-11
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+47
-44
@@ -1778,53 +1778,56 @@ Override or extend default translations via `localeMessages` prop:
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const customMessages = {
|
const customMessages = {
|
||||||
// Task list related
|
"zh-CN": {
|
||||||
name: 'Task Name (Custom)',
|
// Task list related
|
||||||
startDate: 'Start Date',
|
name: 'Task Name (Custom)',
|
||||||
endDate: 'End Date',
|
startDate: 'Start Date',
|
||||||
duration: 'Duration',
|
endDate: 'End Date',
|
||||||
progress: 'Completion',
|
duration: 'Duration',
|
||||||
predecessor: 'Predecessors',
|
progress: 'Completion',
|
||||||
assignee: 'Assignee',
|
predecessor: 'Predecessors',
|
||||||
estimatedHours: 'Estimated Hours',
|
assignee: 'Assignee',
|
||||||
actualHours: 'Actual Hours'
|
estimatedHours: 'Estimated Hours',
|
||||||
|
actualHours: 'Actual Hours'
|
||||||
|
|
||||||
// Toolbar related
|
// Toolbar related
|
||||||
addTask: 'New Task',
|
addTask: 'New Task',
|
||||||
addMilestone: 'New Milestone',
|
addMilestone: 'New Milestone',
|
||||||
today: 'Today',
|
today: 'Today',
|
||||||
exportCsv: 'Export CSV',
|
exportCsv: 'Export CSV',
|
||||||
exportPdf: 'Export PDF',
|
exportPdf: 'Export PDF',
|
||||||
fullscreen: 'Fullscreen',
|
fullscreen: 'Fullscreen',
|
||||||
exitFullscreen: 'Exit Fullscreen',
|
exitFullscreen: 'Exit Fullscreen',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
expandAll: 'Expand All',
|
expandAll: 'Expand All',
|
||||||
collapseAll: 'Collapse All'
|
collapseAll: 'Collapse All'
|
||||||
|
|
||||||
// Internal Task editor related
|
// Internal Task editor related
|
||||||
title: 'Task Details',
|
title: 'Task Details',
|
||||||
titleEdit: 'Edit Task',
|
titleEdit: 'Edit Task',
|
||||||
titleNew: 'New Task',
|
titleNew: 'New Task',
|
||||||
name: 'Task Name',
|
name: 'Task Name',
|
||||||
startDate: 'Start Date',
|
startDate: 'Start Date',
|
||||||
endDate: 'End Date',
|
endDate: 'End Date',
|
||||||
assignee: 'Assignee',
|
assignee: 'Assignee',
|
||||||
predecessor: 'Predecessors',
|
predecessor: 'Predecessors',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
estimatedHours: 'Estimated Hours',
|
estimatedHours: 'Estimated Hours',
|
||||||
actualHours: 'Actual Hours',
|
actualHours: 'Actual Hours',
|
||||||
progress: 'Progress',
|
progress: 'Progress',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
delete: 'Delete'
|
delete: 'Delete'
|
||||||
|
|
||||||
// Other texts
|
// Other texts
|
||||||
days: 'days',
|
days: 'days',
|
||||||
hours: 'hours',
|
hours: 'hours',
|
||||||
overtime: 'overtime',
|
overtime: 'overtime',
|
||||||
overdue: 'overdue',
|
overdue: 'overdue',
|
||||||
// ... More custom translations
|
// ... More custom translations
|
||||||
|
},
|
||||||
|
"en-US": {......}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1769,53 +1769,56 @@ const handleLanguageChange = (lang: 'zh-CN' | 'en-US') => {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const customMessages = {
|
const customMessages = {
|
||||||
// 任务列表相关
|
"zh-CN": {
|
||||||
name: '任务名称(自定义)',
|
// 任务列表相关
|
||||||
startDate: '开始日期',
|
name: '任务名称(自定义)',
|
||||||
endDate: '结束日期',
|
startDate: '开始日期',
|
||||||
duration: '工期',
|
endDate: '结束日期',
|
||||||
progress: '完成度',
|
duration: '工期',
|
||||||
predecessor: '前置任务',
|
progress: '完成度',
|
||||||
assignee: '负责人',
|
predecessor: '前置任务',
|
||||||
estimatedHours: '预估工时',
|
assignee: '负责人',
|
||||||
actualHours: '实际工时'
|
estimatedHours: '预估工时',
|
||||||
|
actualHours: '实际工时'
|
||||||
|
|
||||||
// 工具栏相关
|
// 工具栏相关
|
||||||
addTask: '新建任务',
|
addTask: '新建任务',
|
||||||
addMilestone: '新建里程碑',
|
addMilestone: '新建里程碑',
|
||||||
today: '今天',
|
today: '今天',
|
||||||
exportCsv: '导出 CSV',
|
exportCsv: '导出 CSV',
|
||||||
exportPdf: '导出 PDF',
|
exportPdf: '导出 PDF',
|
||||||
fullscreen: '全屏',
|
fullscreen: '全屏',
|
||||||
exitFullscreen: '退出全屏',
|
exitFullscreen: '退出全屏',
|
||||||
language: '语言',
|
language: '语言',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
expandAll: '全部展开',
|
expandAll: '全部展开',
|
||||||
collapseAll: '全部折叠'
|
collapseAll: '全部折叠'
|
||||||
|
|
||||||
// 内置任务编辑器相关
|
// 内置任务编辑器相关
|
||||||
title: '任务详情',
|
title: '任务详情',
|
||||||
titleEdit: '编辑任务',
|
titleEdit: '编辑任务',
|
||||||
titleNew: '新建任务',
|
titleNew: '新建任务',
|
||||||
name: '任务名称',
|
name: '任务名称',
|
||||||
startDate: '开始日期',
|
startDate: '开始日期',
|
||||||
endDate: '结束日期',
|
endDate: '结束日期',
|
||||||
assignee: '负责人',
|
assignee: '负责人',
|
||||||
predecessor: '前置任务',
|
predecessor: '前置任务',
|
||||||
description: '描述',
|
description: '描述',
|
||||||
estimatedHours: '预估工时',
|
estimatedHours: '预估工时',
|
||||||
actualHours: '实际工时',
|
actualHours: '实际工时',
|
||||||
progress: '进度',
|
progress: '进度',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
delete: '删除'
|
delete: '删除'
|
||||||
|
|
||||||
// 其他文本
|
// 其他文本
|
||||||
days: '天',
|
days: '天',
|
||||||
hours: '小时',
|
hours: '小时',
|
||||||
overtime: '超时',
|
overtime: '超时',
|
||||||
overdue: '逾期',
|
overdue: '逾期',
|
||||||
// ... 更多自定义翻译
|
// ... 更多自定义翻译
|
||||||
|
},
|
||||||
|
"en-US": {......}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-3
@@ -910,7 +910,7 @@ function taskDebug(item: any) {
|
|||||||
.app-container {
|
.app-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--gantt-bg-secondary, #f0f2f5);
|
background: var(--gantt-bg-secondary, #f0f2f5);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -922,7 +922,7 @@ function taskDebug(item: any) {
|
|||||||
background: var(--gantt-bg-primary, #ffffff);
|
background: var(--gantt-bg-primary, #ffffff);
|
||||||
border: 1px solid var(--gantt-border-color, #e4e7ed);
|
border: 1px solid var(--gantt-border-color, #e4e7ed);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 10px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1453,7 +1453,6 @@ function taskDebug(item: any) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.license-info {
|
.license-info {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 30px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -286,5 +286,25 @@
|
|||||||
"Defect fix: Fixed the issue of invalid column width configuration in TaskList",
|
"Defect fix: Fixed the issue of invalid column width configuration in TaskList",
|
||||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to @SHENGLONG749 for their valuable use and feedback</span>"
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to @SHENGLONG749 for their valuable use and feedback</span>"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.2-patch3",
|
||||||
|
"date": "2025-11-13",
|
||||||
|
"notes": [
|
||||||
|
"新增:支持视图上下/左右拖拽",
|
||||||
|
"性能优化:canvas绘制性能优化",
|
||||||
|
"性能优化:拖拽视图性能优化,拖拽期间禁止重绘canvas",
|
||||||
|
"性能优化:周视图月开始分隔线实现使用canvas替代dom",
|
||||||
|
"性能优化:使用canvas替代svg links实现",
|
||||||
|
"性能优化:路径缓存+防抖+shallowRef计算",
|
||||||
|
"Added: Support for vertical/horizontal dragging of the view",
|
||||||
|
"Performance Optimization: Canvas drawing performance optimization",
|
||||||
|
"Performance Optimization: View dragging performance optimization, disabling canvas redraw during dragging",
|
||||||
|
"Performance Optimization: Weekly view month start separator line implemented using canvas instead of DOM",
|
||||||
|
"Performance Optimization: Using canvas instead of SVG links implementation",
|
||||||
|
"Performance Optimization: Path caching + debounce + shallowRef calculation",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 @qiuchengw的专业贡献</span>",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to @qiuchengw for his professional contribution</span>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+105
-106
@@ -1,90 +1,5 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div style="height: 600px;">
|
|
||||||
<GanttChart
|
|
||||||
:tasks="tasks"
|
|
||||||
:milestones="milestones"
|
|
||||||
:task-list-config="taskListConfig"
|
|
||||||
:toolbar-config="toolbarConfig"
|
|
||||||
:use-default-drawer="false"
|
|
||||||
:use-default-milestone-dialog="false"
|
|
||||||
:locale-messages="customMessages"
|
|
||||||
:allow-drag-and-resize="true"
|
|
||||||
@add-task="showAddTaskDrawer = true"
|
|
||||||
@add-milestone="showAddMilestoneDialog = true"
|
|
||||||
@task-double-click="onTaskDblclick"
|
|
||||||
@task-click="onTaskClick"
|
|
||||||
@milestone-double-click="onMilestoneDblclick"
|
|
||||||
>
|
|
||||||
</GanttChart>
|
|
||||||
</div>
|
|
||||||
<!-- 自定义添加任务按钮 -->
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
|
|
||||||
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
|
||||||
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
|
||||||
<div class="drawer-container" @click.stop>
|
|
||||||
<div class="drawer-header">
|
|
||||||
<h3>自定义添加任务组件</h3>
|
|
||||||
<button class="close-btn" @click="showAddTaskDrawer = false">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-body">
|
|
||||||
<div class="form-item">
|
|
||||||
<label>任务名称:</label>
|
|
||||||
<input v-model="newTask.name" type="text" placeholder="请输入任务名称" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-item">
|
|
||||||
<label>开始日期:</label>
|
|
||||||
<input v-model="newTask.startDate" type="date" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-item">
|
|
||||||
<label>结束日期:</label>
|
|
||||||
<input v-model="newTask.endDate" type="date" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-footer">
|
|
||||||
<button class="btn btn-primary" @click="addTask">确定</button>
|
|
||||||
<button class="btn btn-default" @click="showAddTaskDrawer = false">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 自定义Dialog组件基于element plus -->
|
|
||||||
<el-dialog
|
|
||||||
title="自定义添加里程碑组件 - Element Plus"
|
|
||||||
v-model="showAddMilestoneDialog"
|
|
||||||
width="400px"
|
|
||||||
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<div class="form-item">
|
|
||||||
<label>任务名称:</label>
|
|
||||||
<el-input v-model="newTask.name" placeholder="请输入任务名称" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-item">
|
|
||||||
<label>日期:</label>
|
|
||||||
<el-date-picker v-model="newTask.startDate" type="date" value-format="YYYY-MM-DD" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="addMilestone">确定</el-button>
|
|
||||||
<el-button @click="showAddMilestoneDialog = false">取消</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue'
|
||||||
import { GanttChart } from 'jordium-gantt-vue3'
|
import { GanttChart } from 'jordium-gantt-vue3'
|
||||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
@@ -121,7 +36,7 @@ const tasks = ref([
|
|||||||
department: '管理部',
|
department: '管理部',
|
||||||
departmentCode: 'D001',
|
departmentCode: 'D001',
|
||||||
type: 'task',
|
type: 'task',
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const milestones = ref([
|
const milestones = ref([
|
||||||
@@ -130,8 +45,8 @@ const milestones = ref([
|
|||||||
name: '项目立项',
|
name: '项目立项',
|
||||||
startDate: '2025-10-29',
|
startDate: '2025-10-29',
|
||||||
type: 'milestone',
|
type: 'milestone',
|
||||||
icon: 'diamond'
|
icon: 'diamond',
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const customMessages = {
|
const customMessages = {
|
||||||
@@ -142,13 +57,13 @@ const customMessages = {
|
|||||||
'en-US': {
|
'en-US': {
|
||||||
department: 'Department',
|
department: 'Department',
|
||||||
departmentCode: 'Department Code',
|
departmentCode: 'Department Code',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
// const tasks = ref([])
|
// const tasks = ref([])
|
||||||
|
|
||||||
// const milestones = ref([])
|
// const milestones = ref([])
|
||||||
const showAddTaskDrawer = ref(false);
|
const showAddTaskDrawer = ref(false)
|
||||||
const showAddMilestoneDialog = ref(false);
|
const showAddMilestoneDialog = ref(false)
|
||||||
|
|
||||||
// 定义可动态配置的列
|
// 定义可动态配置的列
|
||||||
const availableColumns = ref<TaskListColumnConfig[]>([
|
const availableColumns = ref<TaskListColumnConfig[]>([
|
||||||
@@ -164,7 +79,7 @@ const taskListConfig = {
|
|||||||
defaultWidth: '50%', // 默认展开宽度50%
|
defaultWidth: '50%', // 默认展开宽度50%
|
||||||
minWidth: '300px', // 最小宽度300px(默认280px)
|
minWidth: '300px', // 最小宽度300px(默认280px)
|
||||||
maxWidth: '1200px', // 最大宽度1200px(默认1160px)
|
maxWidth: '1200px', // 最大宽度1200px(默认1160px)
|
||||||
columns: availableColumns.value
|
columns: availableColumns.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolbar配置示例
|
// toolbar配置示例
|
||||||
@@ -179,18 +94,17 @@ const toolbarConfig: ToolbarConfig = {
|
|||||||
showFullscreen: true, // 显示全屏按钮
|
showFullscreen: true, // 显示全屏按钮
|
||||||
showTimeScale: true, // 显示时间刻度按钮组
|
showTimeScale: true, // 显示时间刻度按钮组
|
||||||
timeScaleDimensions: [ // 显示所有时间刻度维度
|
timeScaleDimensions: [ // 显示所有时间刻度维度
|
||||||
'hour', 'day', 'week', 'month', 'quarter', 'year'
|
'hour', 'day', 'week', 'month', 'quarter', 'year',
|
||||||
],
|
],
|
||||||
defaultTimeScale: 'week', // 默认选中周视图
|
defaultTimeScale: 'week', // 默认选中周视图
|
||||||
showExpandCollapse: false // 显示展开/折叠按钮
|
showExpandCollapse: false, // 显示展开/折叠按钮
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const newTask = ref({
|
const newTask = ref({
|
||||||
name: '',
|
name: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: ''
|
endDate: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const addTask = () => {
|
const addTask = () => {
|
||||||
tasks.value.push({
|
tasks.value.push({
|
||||||
@@ -202,10 +116,10 @@ const addTask = () => {
|
|||||||
department: '未分配',
|
department: '未分配',
|
||||||
departmentCode: 'D000',
|
departmentCode: 'D000',
|
||||||
type: 'task',
|
type: 'task',
|
||||||
});
|
})
|
||||||
newTask.value = { name: '', startDate: '', endDate: '' };
|
newTask.value = { name: '', startDate: '', endDate: '' }
|
||||||
showAddTaskDrawer.value = false;
|
showAddTaskDrawer.value = false
|
||||||
};
|
}
|
||||||
|
|
||||||
const addMilestone = () => {
|
const addMilestone = () => {
|
||||||
milestones.value.push({
|
milestones.value.push({
|
||||||
@@ -213,11 +127,11 @@ const addMilestone = () => {
|
|||||||
name: newTask.value.name,
|
name: newTask.value.name,
|
||||||
startDate: newTask.value.startDate,
|
startDate: newTask.value.startDate,
|
||||||
type: 'milestone',
|
type: 'milestone',
|
||||||
icon: 'diamond'
|
icon: 'diamond',
|
||||||
});
|
})
|
||||||
console.log('milestones: ', milestones.value)
|
console.log('milestones: ', milestones.value)
|
||||||
newTask.value = { name: '', startDate: '', endDate: '' };
|
newTask.value = { name: '', startDate: '', endDate: '' }
|
||||||
showAddMilestoneDialog.value = false;
|
showAddMilestoneDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTaskDblclick = (task: any) => {
|
const onTaskDblclick = (task: any) => {
|
||||||
@@ -231,6 +145,91 @@ const onMilestoneDblclick = (milestone: any) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="height: 600px;">
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:milestones="milestones"
|
||||||
|
:task-list-config="taskListConfig"
|
||||||
|
:toolbar-config="toolbarConfig"
|
||||||
|
:use-default-drawer="false"
|
||||||
|
:use-default-milestone-dialog="false"
|
||||||
|
:locale-messages="customMessages"
|
||||||
|
:allow-drag-and-resize="true"
|
||||||
|
@add-task="showAddTaskDrawer = true"
|
||||||
|
@add-milestone="showAddMilestoneDialog = true"
|
||||||
|
@task-double-click="onTaskDblclick"
|
||||||
|
@task-click="onTaskClick"
|
||||||
|
@milestone-double-click="onMilestoneDblclick"
|
||||||
|
>
|
||||||
|
</GanttChart>
|
||||||
|
</div>
|
||||||
|
<!-- 自定义添加任务按钮 -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
|
||||||
|
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
||||||
|
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
||||||
|
<div class="drawer-container" @click.stop>
|
||||||
|
<div class="drawer-header">
|
||||||
|
<h3>自定义添加任务组件</h3>
|
||||||
|
<button class="close-btn" @click="showAddTaskDrawer = false">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-body">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>任务名称:</label>
|
||||||
|
<input v-model="newTask.name" type="text" placeholder="请输入任务名称" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>开始日期:</label>
|
||||||
|
<input v-model="newTask.startDate" type="date" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>结束日期:</label>
|
||||||
|
<input v-model="newTask.endDate" type="date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<button class="btn btn-primary" @click="addTask">确定</button>
|
||||||
|
<button class="btn btn-default" @click="showAddTaskDrawer = false">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义Dialog组件基于element plus -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showAddMilestoneDialog"
|
||||||
|
title="自定义添加里程碑组件 - Element Plus"
|
||||||
|
width="400px"
|
||||||
|
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>任务名称:</label>
|
||||||
|
<el-input v-model="newTask.name" placeholder="请输入任务名称" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>日期:</label>
|
||||||
|
<el-date-picker v-model="newTask.startDate" type="date" value-format="YYYY-MM-DD" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="addMilestone">确定</el-button>
|
||||||
|
<el-button @click="showAddMilestoneDialog = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 抽屉遮罩层 */
|
/* 抽屉遮罩层 */
|
||||||
.drawer-overlay {
|
.drawer-overlay {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue';
|
import App from './App.vue'
|
||||||
import ElementPlus from 'element-plus';
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/index.css';
|
import 'element-plus/theme-chalk/index.css'
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App)
|
||||||
app.use(ElementPlus);
|
app.use(ElementPlus)
|
||||||
app.mount('#app');
|
app.mount('#app')
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.4.2-patch.2",
|
"version": "1.4.2-patch.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/jordium-gantt-vue3.cjs.js",
|
"main": "dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "dist/jordium-gantt-vue3.es.js",
|
"module": "dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
@@ -136,45 +136,64 @@ interface Props {
|
|||||||
const ganttRootRef = ref<HTMLElement | null>(null)
|
const ganttRootRef = ref<HTMLElement | null>(null)
|
||||||
const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值
|
const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值
|
||||||
|
|
||||||
// ResizeObserver 用于监听容器宽度变化
|
|
||||||
let ganttRootResizeObserver: ResizeObserver | null = null
|
|
||||||
|
|
||||||
// 监听容器宽度变化
|
// 监听容器宽度变化
|
||||||
const updateContainerWidth = (newWidth: number) => {
|
// 节流函数工具
|
||||||
if (newWidth !== ganttContainerWidth.value) {
|
const throttle = <T extends (...args: unknown[]) => unknown>(func: T, delay: number): T => {
|
||||||
ganttContainerWidth.value = newWidth
|
let lastCall = 0
|
||||||
// 容器宽度变化时,重新计算 TaskList 的宽度限制
|
let timeoutId: number | null = null
|
||||||
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
|
|
||||||
taskListBodyWidth.value = getTaskListMaxWidth()
|
|
||||||
taskListBodyProposedWidth.value = getTaskListMaxWidth()
|
|
||||||
taskListBodyWidthLimit.value = getTaskListMaxWidth()
|
|
||||||
|
|
||||||
// 确保当前宽度在新的限制范围内
|
return ((...args: Parameters<T>) => {
|
||||||
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
|
const now = Date.now()
|
||||||
if (adjustedWidth !== leftPanelWidth.value) {
|
const remaining = delay - (now - lastCall)
|
||||||
leftPanelWidth.value = adjustedWidth
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
lastCall = now
|
||||||
|
func(...args)
|
||||||
|
} else {
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
lastCall = Date.now()
|
||||||
|
func(...args)
|
||||||
|
timeoutId = null
|
||||||
|
}, remaining)
|
||||||
|
}
|
||||||
|
}) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContainerWidth = () => {
|
||||||
|
if (ganttRootRef.value) {
|
||||||
|
const newWidth = ganttRootRef.value.clientWidth
|
||||||
|
if (newWidth !== ganttContainerWidth.value) {
|
||||||
|
ganttContainerWidth.value = newWidth
|
||||||
|
// 容器宽度变化时,重新计算 TaskList 的宽度限制
|
||||||
|
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
|
||||||
|
taskListBodyWidth.value = getTaskListMaxWidth()
|
||||||
|
taskListBodyProposedWidth.value = getTaskListMaxWidth()
|
||||||
|
taskListBodyWidthLimit.value = getTaskListMaxWidth()
|
||||||
|
|
||||||
|
// 确保当前宽度在新的限制范围内
|
||||||
|
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
|
||||||
|
if (adjustedWidth !== leftPanelWidth.value) {
|
||||||
|
leftPanelWidth.value = adjustedWidth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建节流版本的 updateContainerWidth,避免频繁调用
|
||||||
|
const throttledUpdateContainerWidth = throttle(updateContainerWidth, 100)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (ganttRootRef.value) {
|
updateContainerWidth()
|
||||||
// 使用 ResizeObserver 监听容器宽度变化,避免频繁读取 clientWidth
|
// 使用节流版本监听窗口大小变化
|
||||||
ganttRootResizeObserver = new ResizeObserver((entries) => {
|
window.addEventListener('resize', throttledUpdateContainerWidth)
|
||||||
for (const entry of entries) {
|
|
||||||
// 使用 contentRect.width,避免强制重排
|
|
||||||
updateContainerWidth(entry.contentRect.width)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ganttRootResizeObserver.observe(ganttRootRef.value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (ganttRootResizeObserver) {
|
window.removeEventListener('resize', throttledUpdateContainerWidth)
|
||||||
ganttRootResizeObserver.disconnect()
|
|
||||||
ganttRootResizeObserver = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比)
|
// TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比)
|
||||||
@@ -341,11 +360,9 @@ function onMouseDown(e: MouseEvent) {
|
|||||||
document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false })
|
document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false })
|
||||||
document.addEventListener('contextmenu', blockAllEvents, { capture: true })
|
document.addEventListener('contextmenu', blockAllEvents, { capture: true })
|
||||||
|
|
||||||
// ⚠️ 使用requestAnimationFrame节流,但移除阈值检测,确保每帧都更新
|
|
||||||
let rafId: number | null = null
|
|
||||||
|
|
||||||
function onMouseMove(ev: MouseEvent) {
|
function onMouseMove(ev: MouseEvent) {
|
||||||
if (!dragging.value) return
|
if (!dragging.value) return
|
||||||
|
|
||||||
// 强制阻止所有默认行为和事件传播
|
// 强制阻止所有默认行为和事件传播
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
@@ -353,29 +370,15 @@ function onMouseDown(e: MouseEvent) {
|
|||||||
|
|
||||||
const delta = ev.clientX - startX
|
const delta = ev.clientX - startX
|
||||||
const proposedWidth = startWidth + delta
|
const proposedWidth = startWidth + delta
|
||||||
|
|
||||||
|
// 直接使用面板宽度限制检查,无需复杂的坐标计算
|
||||||
const finalWidth = checkWidthLimits(proposedWidth)
|
const finalWidth = checkWidthLimits(proposedWidth)
|
||||||
|
leftPanelWidth.value = finalWidth
|
||||||
// 取消之前的帧请求
|
|
||||||
if (rafId !== null) {
|
|
||||||
cancelAnimationFrame(rafId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在下一帧更新(节流到60fps,避免过度触发响应式系统)
|
|
||||||
rafId = requestAnimationFrame(() => {
|
|
||||||
leftPanelWidth.value = finalWidth
|
|
||||||
rafId = null
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp() {
|
function onMouseUp() {
|
||||||
dragging.value = false
|
dragging.value = false
|
||||||
|
|
||||||
// 取消未完成的帧请求
|
|
||||||
if (rafId !== null) {
|
|
||||||
cancelAnimationFrame(rafId)
|
|
||||||
rafId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除全局事件拦截器
|
// 移除全局事件拦截器
|
||||||
document.removeEventListener('mousedown', blockAllEvents, { capture: true })
|
document.removeEventListener('mousedown', blockAllEvents, { capture: true })
|
||||||
document.removeEventListener('click', blockAllEvents, { capture: true })
|
document.removeEventListener('click', blockAllEvents, { capture: true })
|
||||||
@@ -506,8 +509,10 @@ onMounted(() => {
|
|||||||
// 监听右侧面板(timeline 的可视容器)的宽度
|
// 监听右侧面板(timeline 的可视容器)的宽度
|
||||||
const rightPanel = document.querySelector('.gantt-panel-right')
|
const rightPanel = document.querySelector('.gantt-panel-right')
|
||||||
if (rightPanel) {
|
if (rightPanel) {
|
||||||
// 使用 ResizeObserver 自动更新宽度,避免直接读取clientWidth造成强制重排
|
// 初始化宽度
|
||||||
// ResizeObserver 会在开始观察时立即触发一次回调,提供初始宽度
|
timelineContainerWidth.value = rightPanel.clientWidth
|
||||||
|
|
||||||
|
// 使用 ResizeObserver 监听宽度变化
|
||||||
resizeObserver = new ResizeObserver(entries => {
|
resizeObserver = new ResizeObserver(entries => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
timelineContainerWidth.value = entry.contentRect.width
|
timelineContainerWidth.value = entry.contentRect.width
|
||||||
@@ -2186,7 +2191,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
<div
|
<div
|
||||||
v-if="isTaskListVisible"
|
v-if="isTaskListVisible"
|
||||||
class="gantt-panel gantt-panel-left"
|
class="gantt-panel gantt-panel-left"
|
||||||
:class="{ dragging: dragging }"
|
|
||||||
:style="{ width: leftPanelWidth + 'px' }"
|
:style="{ width: leftPanelWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<TaskList
|
<TaskList
|
||||||
@@ -2342,12 +2346,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
.gantt-panel-left {
|
.gantt-panel-left {
|
||||||
/* width 由js控制 */
|
/* width 由js控制 */
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
/* ⚠️ 拖拽时禁用transition,提升响应速度 */
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gantt-panel-left:not(.dragging) {
|
|
||||||
/* 非拖拽时保留平滑过渡效果(如toggle时) */
|
|
||||||
transition: width 0.1s;
|
transition: width 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,446 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
|
import type { Task } from '../models/classes/Task'
|
||||||
|
import { getPredecessorIds } from '../utils/predecessorUtils'
|
||||||
|
|
||||||
|
// 定义 TaskBar 位置信息类型
|
||||||
|
interface TaskBarPosition {
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义月份分隔线位置类型
|
||||||
|
interface VerticalLine {
|
||||||
|
left: number
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 Props
|
||||||
|
interface Props {
|
||||||
|
tasks: Task[]
|
||||||
|
taskBarPositions: Record<number, TaskBarPosition>
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
|
||||||
|
highlightedTaskId: number | null
|
||||||
|
highlightedTaskIds: Set<number>
|
||||||
|
hoveredTaskId: number | null
|
||||||
|
// 月份分隔线配置
|
||||||
|
verticalLines?: VerticalLine[]
|
||||||
|
showVerticalLines?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
verticalLines: () => [],
|
||||||
|
showVerticalLines: true,
|
||||||
|
offsetLeft: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Canvas 引用
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
// requestAnimationFrame 防抖控制
|
||||||
|
let rafId: number | null = null
|
||||||
|
let pendingRedraw = false
|
||||||
|
let themeObserver: MutationObserver | null = null
|
||||||
|
|
||||||
|
// 当前主题(用于分隔线颜色)
|
||||||
|
const isDarkTheme = ref(document.documentElement.getAttribute('data-theme') === 'dark')
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
const updateTheme = () => {
|
||||||
|
isDarkTheme.value = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
scheduleRedraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制关系线到 Canvas
|
||||||
|
* 性能优势:相比 SVG 提升 18 倍渲染性能
|
||||||
|
*/
|
||||||
|
const drawLinks = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d', { alpha: true })
|
||||||
|
if (!ctx) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('❌ Canvas context 获取失败,可能是尺寸超限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 适配高清屏(Retina)
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const displayWidth = props.width
|
||||||
|
const displayHeight = props.height
|
||||||
|
|
||||||
|
// 只在尺寸变化时更新 canvas 尺寸
|
||||||
|
const pixelWidth = displayWidth * dpr
|
||||||
|
const pixelHeight = displayHeight * dpr
|
||||||
|
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
||||||
|
canvas.width = pixelWidth
|
||||||
|
canvas.height = pixelHeight
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空画布(透明)
|
||||||
|
ctx.clearRect(0, 0, displayWidth, displayHeight)
|
||||||
|
|
||||||
|
// 绘制月份分隔线(在关系线之前,作为背景)
|
||||||
|
if (props.showVerticalLines && props.verticalLines && props.verticalLines.length > 0) {
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
// 使用与旗帜一致的主题色:浅色模式 #409eff,暗色模式 #66b1ff
|
||||||
|
const lineColor = isDarkTheme.value ? '#66b1ff' : '#409eff'
|
||||||
|
ctx.strokeStyle = lineColor
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
|
||||||
|
// 优化:一次性绘制所有垂直线,避免多次 stroke() 调用
|
||||||
|
// 虚拟渲染:减去偏移量,转换为 Canvas 局部坐标
|
||||||
|
ctx.beginPath()
|
||||||
|
for (const line of props.verticalLines) {
|
||||||
|
const localX = line.left - props.offsetLeft
|
||||||
|
// 只绘制在 Canvas 可见范围内的线
|
||||||
|
if (localX >= 0 && localX <= displayWidth) {
|
||||||
|
ctx.moveTo(localX, 0)
|
||||||
|
ctx.lineTo(localX, displayHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前渲染的任务ID集合
|
||||||
|
const currentTaskIds = new Set<number>()
|
||||||
|
for (const task of props.tasks) {
|
||||||
|
currentTaskIds.add(task.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否处于高亮模式
|
||||||
|
const isHighlightMode = props.highlightedTaskId !== null
|
||||||
|
|
||||||
|
// 虚拟渲染:计算 Canvas 覆盖的范围
|
||||||
|
const canvasStartX = props.offsetLeft
|
||||||
|
const canvasEndX = props.offsetLeft + displayWidth
|
||||||
|
|
||||||
|
// 定义线条数据类型
|
||||||
|
interface LineData {
|
||||||
|
x1: number
|
||||||
|
y1: number
|
||||||
|
x2: number
|
||||||
|
y2: number
|
||||||
|
c1x: number
|
||||||
|
c1y: number
|
||||||
|
c2x: number
|
||||||
|
c2y: number
|
||||||
|
arrowAngle: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按样式分组线条数据(减少状态切换)
|
||||||
|
const highlightedLines: LineData[] = []
|
||||||
|
const hoveredLines: LineData[] = []
|
||||||
|
const normalLines: LineData[] = []
|
||||||
|
const fadedLines: LineData[] = [] // 高亮模式下的普通线条(半透明)
|
||||||
|
|
||||||
|
for (const task of props.tasks) {
|
||||||
|
if (!task.predecessor || !props.taskBarPositions[task.id]) continue
|
||||||
|
|
||||||
|
const predecessorIds = getPredecessorIds(task.predecessor)
|
||||||
|
|
||||||
|
for (const predecessorId of predecessorIds) {
|
||||||
|
const fromBar = props.taskBarPositions[predecessorId]
|
||||||
|
const toBar = props.taskBarPositions[task.id]
|
||||||
|
|
||||||
|
if (!fromBar || !toBar || !currentTaskIds.has(predecessorId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
|
||||||
|
// 如果起点和终点都在 Canvas 外,跳过
|
||||||
|
const fromX = fromBar.left + fromBar.width
|
||||||
|
const toX = toBar.left
|
||||||
|
const lineMinX = Math.min(fromX, toX)
|
||||||
|
const lineMaxX = Math.max(fromX, toX)
|
||||||
|
if (lineMaxX < canvasStartX || lineMinX > canvasEndX) {
|
||||||
|
continue // 完全在 Canvas 外,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断高亮状态
|
||||||
|
const fromIsPrimary = props.highlightedTaskId === predecessorId
|
||||||
|
const toIsPrimary = props.highlightedTaskId === task.id
|
||||||
|
const fromIsHighlighted = props.highlightedTaskIds.has(predecessorId)
|
||||||
|
const toIsHighlighted = props.highlightedTaskIds.has(task.id)
|
||||||
|
const isLineHighlighted = fromIsHighlighted && toIsHighlighted
|
||||||
|
|
||||||
|
// 判断 hover 状态
|
||||||
|
const fromIsHovered = props.hoveredTaskId === predecessorId
|
||||||
|
const toIsHovered = props.hoveredTaskId === task.id
|
||||||
|
const isLineHovered = fromIsHovered || toIsHovered
|
||||||
|
|
||||||
|
// 计算 Y 轴偏移(高亮时的位移)
|
||||||
|
const fromYOffset = fromIsPrimary ? -8 : fromIsHighlighted ? -5 : 0
|
||||||
|
const toYOffset = toIsPrimary ? -8 : toIsHighlighted ? -5 : 0
|
||||||
|
|
||||||
|
// 计算坐标(全局坐标)
|
||||||
|
const globalX1 = fromBar.left + fromBar.width
|
||||||
|
const globalY1 = fromBar.top + fromBar.height / 2 + fromYOffset
|
||||||
|
const globalX2 = toBar.left
|
||||||
|
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
|
||||||
|
|
||||||
|
// 转换为 Canvas 局部坐标
|
||||||
|
const x1 = globalX1 - props.offsetLeft
|
||||||
|
const y1 = globalY1
|
||||||
|
const x2 = globalX2 - props.offsetLeft
|
||||||
|
const y2 = globalY2
|
||||||
|
|
||||||
|
const c1x = x1 + 40
|
||||||
|
const c1y = y1
|
||||||
|
const c2x = x2 - 40
|
||||||
|
const c2y = y2
|
||||||
|
|
||||||
|
// 预计算箭头角度
|
||||||
|
const arrowAngle = Math.atan2(y2 - c2y, x2 - c2x)
|
||||||
|
const lineData: LineData = { x1, y1, x2, y2, c1x, c1y, c2x, c2y, arrowAngle }
|
||||||
|
|
||||||
|
// 根据状态分组
|
||||||
|
if (isLineHighlighted) {
|
||||||
|
highlightedLines.push(lineData)
|
||||||
|
} else if (isLineHovered) {
|
||||||
|
hoveredLines.push(lineData)
|
||||||
|
} else if (isHighlightMode) {
|
||||||
|
fadedLines.push(lineData)
|
||||||
|
} else {
|
||||||
|
normalLines.push(lineData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量绘制:设置虚线样式(所有线条共用)
|
||||||
|
ctx.setLineDash([6, 4])
|
||||||
|
|
||||||
|
// 批量绘制高亮线条
|
||||||
|
if (highlightedLines.length > 0) {
|
||||||
|
ctx.strokeStyle = '#409eff'
|
||||||
|
ctx.fillStyle = '#409eff'
|
||||||
|
ctx.lineWidth = 4
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
for (const line of highlightedLines) {
|
||||||
|
ctx.moveTo(line.x1, line.y1)
|
||||||
|
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 批量绘制箭头
|
||||||
|
for (const line of highlightedLines) {
|
||||||
|
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量绘制悬停线条
|
||||||
|
if (hoveredLines.length > 0) {
|
||||||
|
ctx.strokeStyle = '#67c23a'
|
||||||
|
ctx.fillStyle = '#67c23a'
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
for (const line of hoveredLines) {
|
||||||
|
ctx.moveTo(line.x1, line.y1)
|
||||||
|
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 批量绘制箭头
|
||||||
|
for (const line of hoveredLines) {
|
||||||
|
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量绘制普通线条
|
||||||
|
if (normalLines.length > 0) {
|
||||||
|
ctx.strokeStyle = '#c0c4cc'
|
||||||
|
ctx.fillStyle = '#c0c4cc'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
for (const line of normalLines) {
|
||||||
|
ctx.moveTo(line.x1, line.y1)
|
||||||
|
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 批量绘制箭头
|
||||||
|
for (const line of normalLines) {
|
||||||
|
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量绘制半透明线条(高亮模式下的普通线条)
|
||||||
|
if (fadedLines.length > 0) {
|
||||||
|
ctx.strokeStyle = '#c0c4cc'
|
||||||
|
ctx.fillStyle = '#c0c4cc'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.globalAlpha = 0.2
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
for (const line of fadedLines) {
|
||||||
|
ctx.moveTo(line.x1, line.y1)
|
||||||
|
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 批量绘制箭头
|
||||||
|
for (const line of fadedLines) {
|
||||||
|
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复透明度
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化版箭头绘制(减少参数传递,复用已设置的 fillStyle)
|
||||||
|
*/
|
||||||
|
const drawArrowOptimized = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x2: number,
|
||||||
|
y2: number,
|
||||||
|
angle: number,
|
||||||
|
) => {
|
||||||
|
const arrowLength = 8
|
||||||
|
const arrowWidth = 4
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
// fillStyle 已在外部设置,无需重复设置
|
||||||
|
ctx.moveTo(x2, y2)
|
||||||
|
ctx.lineTo(
|
||||||
|
x2 - arrowLength * Math.cos(angle) - arrowWidth * Math.sin(angle),
|
||||||
|
y2 - arrowLength * Math.sin(angle) + arrowWidth * Math.cos(angle),
|
||||||
|
)
|
||||||
|
ctx.lineTo(
|
||||||
|
x2 - arrowLength * Math.cos(angle) + arrowWidth * Math.sin(angle),
|
||||||
|
y2 - arrowLength * Math.sin(angle) - arrowWidth * Math.cos(angle),
|
||||||
|
)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 requestAnimationFrame 优化的重绘调度器
|
||||||
|
* 合并多个连续的重绘请求为单次绘制
|
||||||
|
*/
|
||||||
|
const scheduleRedraw = () => {
|
||||||
|
if (pendingRedraw) {
|
||||||
|
// 已有待处理的重绘请求,跳过
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRedraw = true
|
||||||
|
|
||||||
|
// 取消之前的 RAF(如果有)
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在下一帧绘制
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
pendingRedraw = false
|
||||||
|
rafId = null
|
||||||
|
drawLinks()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听相关状态变化,自动重绘 Canvas
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => props.taskBarPositions,
|
||||||
|
() => props.tasks,
|
||||||
|
() => props.highlightedTaskId,
|
||||||
|
() => props.highlightedTaskIds,
|
||||||
|
() => props.hoveredTaskId,
|
||||||
|
() => props.width,
|
||||||
|
() => props.height,
|
||||||
|
() => props.verticalLines,
|
||||||
|
() => props.showVerticalLines,
|
||||||
|
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
|
||||||
|
scheduleRedraw()
|
||||||
|
},
|
||||||
|
{ deep: false }, // shallowRef 不需要 deep
|
||||||
|
)
|
||||||
|
|
||||||
|
// 组件挂载后初始化绘制
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听主题变化
|
||||||
|
themeObserver = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.attributeName === 'data-theme') {
|
||||||
|
updateTheme()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme'],
|
||||||
|
})
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
drawLinks()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理主题观察器
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect()
|
||||||
|
themeObserver = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消待处理的 RAF
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
rafId = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
redraw: drawLinks,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
class="gantt-links-canvas"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
transform: `translateX(${offsetLeft}px)`,
|
||||||
|
zIndex: highlightedTaskId !== null ? 1001 : 25,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gantt-links-canvas {
|
||||||
|
display: block;
|
||||||
|
background: transparent; /* 确保背景透明 */
|
||||||
|
opacity: 1; /* 确保不透明度为 100% */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+578
-629
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* 性能监控工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
let lastLogTime = 0
|
||||||
|
const LOG_THROTTLE = 100 // 最多每 100ms 输出一次日志
|
||||||
|
|
||||||
|
// FPS 监控相关
|
||||||
|
let fpsFrameCount = 0
|
||||||
|
let fpsLastTime = performance.now()
|
||||||
|
let fpsValues: number[] = []
|
||||||
|
let fpsMonitorEnabled = false
|
||||||
|
let fpsRafId: number | null = null
|
||||||
|
|
||||||
|
// 绘制性能统计
|
||||||
|
interface DrawStats {
|
||||||
|
totalDraws: number
|
||||||
|
totalDuration: number
|
||||||
|
minDuration: number
|
||||||
|
maxDuration: number
|
||||||
|
avgDuration: number
|
||||||
|
recentDraws: Array<{ timestamp: number; duration: number; details?: unknown }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawStats: DrawStats = {
|
||||||
|
totalDraws: 0,
|
||||||
|
totalDuration: 0,
|
||||||
|
minDuration: Infinity,
|
||||||
|
maxDuration: 0,
|
||||||
|
avgDuration: 0,
|
||||||
|
recentDraws: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RECENT_DRAWS = 100 // 保留最近 100 次绘制记录
|
||||||
|
|
||||||
|
export const perfMonitor = {
|
||||||
|
/**
|
||||||
|
* 记录性能日志(自动节流)
|
||||||
|
*/
|
||||||
|
log(tag: string, data: unknown) {
|
||||||
|
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 {
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 FPS 监控
|
||||||
|
* @param intervalMs 刷新间隔(毫秒),默认 1000ms
|
||||||
|
*/
|
||||||
|
startFpsMonitor(intervalMs = 1000) {
|
||||||
|
if (fpsMonitorEnabled) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[Perf] FPS 监控已在运行中')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsMonitorEnabled = true
|
||||||
|
fpsFrameCount = 0
|
||||||
|
fpsLastTime = performance.now()
|
||||||
|
fpsValues = []
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[Perf] FPS 监控已启动')
|
||||||
|
|
||||||
|
const updateFps = () => {
|
||||||
|
if (!fpsMonitorEnabled) return
|
||||||
|
|
||||||
|
fpsFrameCount++
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const elapsed = currentTime - fpsLastTime
|
||||||
|
|
||||||
|
if (elapsed >= intervalMs) {
|
||||||
|
const fps = Math.round((fpsFrameCount * 1000) / elapsed)
|
||||||
|
fpsValues.push(fps)
|
||||||
|
|
||||||
|
// 保留最近 60 个 FPS 值用于计算平均值
|
||||||
|
if (fpsValues.length > 60) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsRafId = requestAnimationFrame(updateFps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsRafId = requestAnimationFrame(updateFps)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止 FPS 监控
|
||||||
|
*/
|
||||||
|
stopFpsMonitor() {
|
||||||
|
if (!fpsMonitorEnabled) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[Perf] FPS 监控未运行')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsMonitorEnabled = false
|
||||||
|
if (fpsRafId !== null) {
|
||||||
|
cancelAnimationFrame(fpsRafId)
|
||||||
|
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 = []
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录绘制性能
|
||||||
|
* @param duration 绘制耗时(毫秒)
|
||||||
|
* @param details 额外的详细信息
|
||||||
|
*/
|
||||||
|
recordDraw(duration: number, details?: unknown) {
|
||||||
|
drawStats.totalDraws++
|
||||||
|
drawStats.totalDuration += duration
|
||||||
|
drawStats.minDuration = Math.min(drawStats.minDuration, duration)
|
||||||
|
drawStats.maxDuration = Math.max(drawStats.maxDuration, duration)
|
||||||
|
drawStats.avgDuration = drawStats.totalDuration / drawStats.totalDraws
|
||||||
|
|
||||||
|
// 记录最近的绘制
|
||||||
|
drawStats.recentDraws.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration,
|
||||||
|
details,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保持数组大小在限制内
|
||||||
|
if (drawStats.recentDraws.length > MAX_RECENT_DRAWS) {
|
||||||
|
drawStats.recentDraws.shift()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取绘制统计信息
|
||||||
|
*/
|
||||||
|
getDrawStats() {
|
||||||
|
return {
|
||||||
|
...drawStats,
|
||||||
|
recentDraws: drawStats.recentDraws.slice(-10), // 只返回最近 10 次
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印绘制统计信息
|
||||||
|
*/
|
||||||
|
printDrawStats() {
|
||||||
|
if (drawStats.totalDraws === 0) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[Perf] 暂无绘制数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const recent10 = drawStats.recentDraws.slice(-10)
|
||||||
|
const recent10Avg =
|
||||||
|
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;',
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.table({
|
||||||
|
总绘制次数: drawStats.totalDraws,
|
||||||
|
平均耗时: `${drawStats.avgDuration.toFixed(2)}ms`,
|
||||||
|
最小耗时: `${drawStats.minDuration.toFixed(2)}ms`,
|
||||||
|
最大耗时: `${drawStats.maxDuration.toFixed(2)}ms`,
|
||||||
|
最近10次平均: `${recent10Avg.toFixed(2)}ms`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置绘制统计
|
||||||
|
*/
|
||||||
|
resetDrawStats() {
|
||||||
|
drawStats.totalDraws = 0
|
||||||
|
drawStats.totalDuration = 0
|
||||||
|
drawStats.minDuration = Infinity
|
||||||
|
drawStats.maxDuration = 0
|
||||||
|
drawStats.avgDuration = 0
|
||||||
|
drawStats.recentDraws = []
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[Perf] 绘制统计已重置')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始完整的性能测试(FPS + 绘制统计)
|
||||||
|
* @param durationMs 测试持续时间(毫秒),默认 10000ms (10秒)
|
||||||
|
*/
|
||||||
|
startPerformanceTest(durationMs = 10000) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
|
||||||
|
'color: #409eff; font-weight: bold; font-size: 16px;',
|
||||||
|
)
|
||||||
|
|
||||||
|
this.resetDrawStats()
|
||||||
|
this.startFpsMonitor()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.stopFpsMonitor()
|
||||||
|
this.printDrawStats()
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
'%c[Perf] 性能测试完成',
|
||||||
|
'color: #67c23a; font-weight: bold; font-size: 16px;',
|
||||||
|
)
|
||||||
|
}, durationMs)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ export default defineConfig({
|
|||||||
// 确保外部化处理那些你不想打包进库的依赖
|
// 确保外部化处理那些你不想打包进库的依赖
|
||||||
external: ['vue'],
|
external: ['vue'],
|
||||||
output: {
|
output: {
|
||||||
|
// 使用命名导出,避免默认导出警告
|
||||||
|
exports: 'named',
|
||||||
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
|
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
|
||||||
globals: {
|
globals: {
|
||||||
vue: 'Vue',
|
vue: 'Vue',
|
||||||
|
|||||||
Reference in New Issue
Block a user