v1.4.3 - performance enhancement - virtualized table & scroll
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -5,6 +5,20 @@ 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.3] - 2025-11-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 大数据加载演示
|
||||||
|
- Large data loading demonstration
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 性能优化:TaskList和Timeline增加虚拟滚动支持,大幅降低节点数和重绘开销
|
||||||
|
- 性能优化:大数据量任务渲染性能优化
|
||||||
|
- 性能优化:虚拟渲染优化
|
||||||
|
- Performance Optimization: TaskList and Timeline add virtualized scrolling support, greatly reducing node count and redraw overhead
|
||||||
|
- Performance Optimization: Large data volume task rendering performance optimization
|
||||||
|
- Performance Optimization: Virtualized rendering optimization
|
||||||
|
|
||||||
## [1.4.2-patch3] - 2025-11-13
|
## [1.4.2-patch3] - 2025-11-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
305
demo/App.vue
305
demo/App.vue
@@ -3,7 +3,8 @@ import { ref, computed, onMounted, nextTick } from 'vue'
|
|||||||
import GanttChart from '../src/components/GanttChart.vue'
|
import GanttChart from '../src/components/GanttChart.vue'
|
||||||
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
||||||
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
||||||
import demoData from './data.json'
|
import normalData from './data.json'
|
||||||
|
import largeData from './data-large-1m.json'
|
||||||
import packageInfo from '../package.json'
|
import packageInfo from '../package.json'
|
||||||
// 导入主题变量
|
// 导入主题变量
|
||||||
import '../src/styles/theme-variables.css'
|
import '../src/styles/theme-variables.css'
|
||||||
@@ -22,6 +23,100 @@ const { t, formatTranslation } = useI18n()
|
|||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const milestones = ref<Task[]>([])
|
const milestones = ref<Task[]>([])
|
||||||
|
|
||||||
|
const rawDataSources = [
|
||||||
|
{
|
||||||
|
key: 'normal',
|
||||||
|
fileName: 'data.json',
|
||||||
|
payload: normalData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'large',
|
||||||
|
fileName: 'data-large-1m.json',
|
||||||
|
payload: largeData,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type DataSourceKey = (typeof rawDataSources)[number]['key']
|
||||||
|
type RawDataSource = (typeof rawDataSources)[number]
|
||||||
|
|
||||||
|
const dataSourceOptions = computed(() => {
|
||||||
|
const dsTranslations = t.value.dataSourceSwitch?.sources ?? {}
|
||||||
|
return rawDataSources.map(source => {
|
||||||
|
const optionTexts = dsTranslations[source.key as keyof typeof dsTranslations] || {}
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
label: optionTexts.label || source.fileName,
|
||||||
|
description: optionTexts.description || '',
|
||||||
|
badge: optionTexts.badge || source.fileName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDataSource = ref<DataSourceKey>('large')
|
||||||
|
const dataLoading = ref(false)
|
||||||
|
|
||||||
|
const cloneData = <T>(data: T): T => {
|
||||||
|
const globalClone = (globalThis as unknown as { structuredClone?: <K>(value: K) => K }).structuredClone
|
||||||
|
if (typeof globalClone === 'function') {
|
||||||
|
return globalClone(data)
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(data)) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const findRawDataSource = (key: DataSourceKey) => rawDataSources.find(source => source.key === key)
|
||||||
|
|
||||||
|
const applyDataSource = (source: RawDataSource) => {
|
||||||
|
const payload = source.payload as { tasks?: Task[]; milestones?: Task[] }
|
||||||
|
tasks.value = cloneData(payload.tasks ?? [])
|
||||||
|
milestones.value = cloneData(payload.milestones ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSourceTexts = (key: DataSourceKey) => {
|
||||||
|
const dsTranslations = t.value.dataSourceSwitch?.sources ?? {}
|
||||||
|
return dsTranslations[key as keyof typeof dsTranslations] || { label: key }
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchDataSource = async (
|
||||||
|
key: DataSourceKey,
|
||||||
|
options: { silent?: boolean; force?: boolean } = {},
|
||||||
|
) => {
|
||||||
|
if (dataLoading.value) return
|
||||||
|
|
||||||
|
const source = findRawDataSource(key)
|
||||||
|
if (!source) return
|
||||||
|
|
||||||
|
const sourceTexts = getSourceTexts(key)
|
||||||
|
const displayName = sourceTexts.label || source.fileName
|
||||||
|
|
||||||
|
if (!options.force && currentDataSource.value === key && tasks.value.length > 0) {
|
||||||
|
showMessage(formatTranslation('dataSourceAlreadyLoaded', { name: displayName }), 'info', {
|
||||||
|
closable: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDataSource.value = key
|
||||||
|
dataLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nextTick()
|
||||||
|
applyDataSource(source)
|
||||||
|
if (!options.silent) {
|
||||||
|
showMessage(formatTranslation('dataSourceLoadSuccess', { name: displayName }), 'success', {
|
||||||
|
closable: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('切换数据源失败', error)
|
||||||
|
showMessage(formatTranslation('dataSourceLoadFailed', { name: displayName }), 'error', {
|
||||||
|
closable: true,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
dataLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MilestoneDialog状态管理
|
// MilestoneDialog状态管理
|
||||||
const showMilestoneDialog = ref(false)
|
const showMilestoneDialog = ref(false)
|
||||||
const currentMilestone = ref<Task | null>(null)
|
const currentMilestone = ref<Task | null>(null)
|
||||||
@@ -110,6 +205,7 @@ const taskBarConfig = computed<TaskBarConfig>(() => ({
|
|||||||
|
|
||||||
// 配置面板折叠状态
|
// 配置面板折叠状态
|
||||||
const isConfigPanelCollapsed = ref(false)
|
const isConfigPanelCollapsed = ref(false)
|
||||||
|
const isDataSourcePanelCollapsed = ref(false)
|
||||||
|
|
||||||
// TaskList 配置区域折叠状态(默认收起)
|
// TaskList 配置区域折叠状态(默认收起)
|
||||||
const isTaskListConfigCollapsed = ref(true)
|
const isTaskListConfigCollapsed = ref(true)
|
||||||
@@ -122,6 +218,10 @@ const toggleConfigPanel = () => {
|
|||||||
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
|
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleDataSourcePanel = () => {
|
||||||
|
isDataSourcePanelCollapsed.value = !isDataSourcePanelCollapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
// 切换 TaskList 配置区域
|
// 切换 TaskList 配置区域
|
||||||
const toggleTaskListConfig = () => {
|
const toggleTaskListConfig = () => {
|
||||||
isTaskListConfigCollapsed.value = !isTaskListConfigCollapsed.value
|
isTaskListConfigCollapsed.value = !isTaskListConfigCollapsed.value
|
||||||
@@ -377,8 +477,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
tasks.value = demoData.tasks as Task[]
|
switchDataSource(currentDataSource.value, { silent: true, force: true })
|
||||||
milestones.value = demoData.milestones as Task[]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清理被删除任务的predecessor依赖关系
|
// 清理被删除任务的predecessor依赖关系
|
||||||
@@ -480,6 +579,64 @@ function taskDebug(item: any) {
|
|||||||
</h1>
|
</h1>
|
||||||
<VersionHistoryDrawer :visible="showVersionDrawer" @close="showVersionDrawer = false" />
|
<VersionHistoryDrawer :visible="showVersionDrawer" @close="showVersionDrawer = false" />
|
||||||
|
|
||||||
|
<div class="data-source-panel" :class="{ collapsed: isDataSourcePanelCollapsed }">
|
||||||
|
<div class="data-source-header" @click="toggleDataSourcePanel">
|
||||||
|
<h3 class="config-title">
|
||||||
|
<svg
|
||||||
|
class="data-source-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 4h16a2 2 0 0 1 2 2v2H2V6a2 2 0 0 1 2-2zm16 6H4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10zm-5 4h4v4h-4v-4z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ t.dataSourceSwitch?.title }} - {{ t.dataSourceSwitch?.subtitle }}
|
||||||
|
</h3>
|
||||||
|
<button class="collapse-button" :class="{ collapsed: isDataSourcePanelCollapsed }">
|
||||||
|
<svg
|
||||||
|
class="collapse-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="isDataSourcePanelCollapsed ? 'M7 14l5-5 5 5' : 'M7 10l5 5 5-5'"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<transition name="config-content">
|
||||||
|
<div v-show="!isDataSourcePanelCollapsed" class="data-source-content">
|
||||||
|
<div class="data-source-buttons">
|
||||||
|
<button
|
||||||
|
v-for="source in dataSourceOptions"
|
||||||
|
:key="source.key"
|
||||||
|
class="data-source-button"
|
||||||
|
:class="{ active: currentDataSource === source.key }"
|
||||||
|
:disabled="dataLoading && currentDataSource === source.key"
|
||||||
|
@click="switchDataSource(source.key)"
|
||||||
|
>
|
||||||
|
<div class="ds-label-row">
|
||||||
|
<span class="ds-label">{{ source.label }}</span>
|
||||||
|
<span class="ds-file">{{ source.badge }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ds-desc">{{ source.description }}</div>
|
||||||
|
</button>
|
||||||
|
<span v-if="dataLoading" class="data-loading-hint">
|
||||||
|
{{ t.dataSourceSwitch?.loading }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 配置说明面板 - 可折叠 -->
|
<!-- 配置说明面板 - 可折叠 -->
|
||||||
<div class="config-panel" :class="{ collapsed: isConfigPanelCollapsed }">
|
<div class="config-panel" :class="{ collapsed: isConfigPanelCollapsed }">
|
||||||
<div class="config-header" @click="toggleConfigPanel">
|
<div class="config-header" @click="toggleConfigPanel">
|
||||||
@@ -505,7 +662,7 @@ function taskDebug(item: any) {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M7 10l5 5 5-5"
|
:d="isConfigPanelCollapsed ? 'M7 14l5-5 5 5' : 'M7 10l5 5 5-5'"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -917,6 +1074,106 @@ function taskDebug(item: any) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-source-panel {
|
||||||
|
background: var(--gantt-bg-primary, #ffffff);
|
||||||
|
border: 1px solid var(--gantt-border-color, #e4e7ed);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-panel.collapsed {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--gantt-primary-color, #409eff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-content {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-button {
|
||||||
|
flex: 1 1 220px;
|
||||||
|
min-width: 200px;
|
||||||
|
border: 1px solid var(--gantt-border-color, #e4e7ed);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--gantt-bg-secondary, #f9fafc);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-button:hover {
|
||||||
|
border-color: var(--gantt-primary-color, #409eff);
|
||||||
|
box-shadow: 0 2px 10px rgba(64, 158, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-button.active {
|
||||||
|
border-color: var(--gantt-primary-color, #409eff);
|
||||||
|
background: rgba(64, 158, 255, 0.08);
|
||||||
|
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gantt-text-primary, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-file {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gantt-text-muted, #909399);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gantt-text-secondary, #666);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-loading-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gantt-primary-color, #409eff);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/* TaskList列配置面板样式 - 可折叠 */
|
/* TaskList列配置面板样式 - 可折叠 */
|
||||||
.config-panel {
|
.config-panel {
|
||||||
background: var(--gantt-bg-primary, #ffffff);
|
background: var(--gantt-bg-primary, #ffffff);
|
||||||
@@ -964,8 +1221,6 @@ function taskDebug(item: any) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -974,6 +1229,10 @@ function taskDebug(item: any) {
|
|||||||
color: var(--gantt-text-secondary, #666);
|
color: var(--gantt-text-secondary, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collapse-button.collapsed {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.collapse-button:hover {
|
.collapse-button:hover {
|
||||||
background-color: var(--gantt-hover-bg, #e8f4fd);
|
background-color: var(--gantt-hover-bg, #e8f4fd);
|
||||||
color: var(--gantt-primary-color, #409eff);
|
color: var(--gantt-primary-color, #409eff);
|
||||||
@@ -985,10 +1244,6 @@ function taskDebug(item: any) {
|
|||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-button.collapsed .collapse-icon {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 过渡动画 */
|
/* 过渡动画 */
|
||||||
.config-content-enter-active,
|
.config-content-enter-active,
|
||||||
.config-content-leave-active {
|
.config-content-leave-active {
|
||||||
@@ -1479,6 +1734,36 @@ function taskDebug(item: any) {
|
|||||||
border-color: var(--gantt-border-color, #4a5568);
|
border-color: var(--gantt-border-color, #4a5568);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .data-source-panel {
|
||||||
|
background: var(--gantt-bg-primary, #2d3748);
|
||||||
|
border-color: var(--gantt-border-color, #4a5568);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .data-source-sub {
|
||||||
|
color: var(--gantt-text-secondary, #a0aec0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .data-source-button {
|
||||||
|
background: var(--gantt-bg-secondary, #1a202c);
|
||||||
|
border-color: var(--gantt-border-color, #4a5568);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .data-source-button.active {
|
||||||
|
background: rgba(64, 158, 255, 0.18);
|
||||||
|
border-color: var(--gantt-primary-color, #66b3ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .ds-label,
|
||||||
|
:global(html[data-theme='dark']) .ds-desc,
|
||||||
|
:global(html[data-theme='dark']) .ds-file {
|
||||||
|
color: var(--gantt-text-primary, #e2e8f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .ds-file {
|
||||||
|
color: var(--gantt-text-muted, #a0aec0);
|
||||||
|
}
|
||||||
|
|
||||||
:global(html[data-theme='dark']) .config-title {
|
:global(html[data-theme='dark']) .config-title {
|
||||||
color: var(--gantt-text-primary, #e2e8f0);
|
color: var(--gantt-text-primary, #e2e8f0);
|
||||||
}
|
}
|
||||||
|
|||||||
1
demo/data-large-1m.json
Normal file
1
demo/data-large-1m.json
Normal file
File diff suppressed because one or more lines are too long
@@ -306,5 +306,19 @@
|
|||||||
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 @qiuchengw的专业贡献</span>",
|
"<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>"
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to @qiuchengw for his professional contribution</span>"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.3",
|
||||||
|
"date": "2025-11-28",
|
||||||
|
"notes": [
|
||||||
|
"性能优化:TaskList和Timeline增加虚拟滚动支持,大幅降低节点数和重绘开销",
|
||||||
|
"性能优化:大数据量任务渲染性能优化",
|
||||||
|
"性能优化:虚拟渲染优化",
|
||||||
|
"新增:大数据加载演示",
|
||||||
|
"Performance Optimization: TaskList and Timeline add virtualized scrolling support, greatly reducing node count and redraw overhead",
|
||||||
|
"Performance Optimization: Large data volume task rendering performance optimization",
|
||||||
|
"Performance Optimization: Virtualized rendering optimization",
|
||||||
|
"Added: Large data loading demonstration"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.4.2-patch.3",
|
"version": "1.4.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",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed, watch, useSlots } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, watch, useSlots } from 'vue'
|
||||||
|
import type { StyleValue } from 'vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
@@ -35,7 +36,7 @@ interface Props {
|
|||||||
hoveredTaskId?: number | null
|
hoveredTaskId?: number | null
|
||||||
onHover?: (taskId: number | null) => void
|
onHover?: (taskId: number | null) => void
|
||||||
columns: TaskListColumnConfig[]
|
columns: TaskListColumnConfig[]
|
||||||
getColumnWidthStyle?: (column: { width?: number | string }) => object
|
getColumnWidthStyle?: (column: { width?: number | string }) => StyleValue
|
||||||
disableChildrenRender?: boolean
|
disableChildrenRender?: boolean
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -412,7 +413,7 @@ onUnmounted(() => {
|
|||||||
:key="column.key"
|
:key="column.key"
|
||||||
class="col"
|
class="col"
|
||||||
:class="column.cssClass || `col-${column.key}`"
|
:class="column.cssClass || `col-${column.key}`"
|
||||||
:style="getColumnWidthStyle ? getColumnWidthStyle(column) : {}"
|
:style="getColumnWidthStyle ? getColumnWidthStyle(column) : undefined"
|
||||||
>
|
>
|
||||||
<!-- 里程碑分组显示空列 -->
|
<!-- 里程碑分组显示空列 -->
|
||||||
<template v-if="isMilestoneGroup">
|
<template v-if="isMilestoneGroup">
|
||||||
|
|||||||
@@ -157,15 +157,20 @@ const dayWidth = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取任务数据的日期范围(用于月度视图时间轴范围计算)
|
type TaskDateRange = { minDate: Date; maxDate: Date } | null
|
||||||
const getTasksDateRange = () => {
|
let cachedTaskDateRange: TaskDateRange = null
|
||||||
|
|
||||||
|
const invalidateTaskDateRangeCache = () => {
|
||||||
|
cachedTaskDateRange = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeTasksDateRange = (): TaskDateRange => {
|
||||||
if (!tasks.value || tasks.value.length === 0) {
|
if (!tasks.value || tasks.value.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const dates: Date[] = []
|
const dates: Date[] = []
|
||||||
|
|
||||||
// 收集所有任务的开始和结束日期
|
|
||||||
const collectDatesFromTask = (task: Task) => {
|
const collectDatesFromTask = (task: Task) => {
|
||||||
if (task.startDate) {
|
if (task.startDate) {
|
||||||
dates.push(new Date(task.startDate))
|
dates.push(new Date(task.startDate))
|
||||||
@@ -174,7 +179,6 @@ const getTasksDateRange = () => {
|
|||||||
dates.push(new Date(task.endDate))
|
dates.push(new Date(task.endDate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理子任务 - 使用 for 循环代替 forEach
|
|
||||||
if (task.children && task.children.length > 0) {
|
if (task.children && task.children.length > 0) {
|
||||||
for (const child of task.children) {
|
for (const child of task.children) {
|
||||||
collectDatesFromTask(child)
|
collectDatesFromTask(child)
|
||||||
@@ -182,7 +186,6 @@ const getTasksDateRange = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 for...of 循环代替 forEach
|
|
||||||
for (const task of tasks.value) {
|
for (const task of tasks.value) {
|
||||||
collectDatesFromTask(task)
|
collectDatesFromTask(task)
|
||||||
}
|
}
|
||||||
@@ -191,7 +194,6 @@ const getTasksDateRange = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤有效日期并直接获取最小/最大时间戳
|
|
||||||
let minTime = Infinity
|
let minTime = Infinity
|
||||||
let maxTime = -Infinity
|
let maxTime = -Infinity
|
||||||
for (const date of dates) {
|
for (const date of dates) {
|
||||||
@@ -206,10 +208,20 @@ const getTasksDateRange = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const minDate = new Date(minTime)
|
return {
|
||||||
const maxDate = new Date(maxTime)
|
minDate: new Date(minTime),
|
||||||
|
maxDate: new Date(maxTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { minDate, maxDate }
|
// 获取任务数据的日期范围(用于月度/年度视图时间轴范围计算)
|
||||||
|
const getTasksDateRange = () => {
|
||||||
|
if (cachedTaskDateRange) {
|
||||||
|
return cachedTaskDateRange
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedTaskDateRange = computeTasksDateRange()
|
||||||
|
return cachedTaskDateRange
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取小时视图的时间范围
|
// 获取小时视图的时间范围
|
||||||
@@ -753,11 +765,6 @@ const debouncedUpdateCanvasPosition = debounce(() => {
|
|||||||
updateSvgSize() // 重新计算 Canvas 位置和尺寸
|
updateSvgSize() // 重新计算 Canvas 位置和尺寸
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
// 防抖更新纵向滚动位置
|
|
||||||
const debouncedUpdateVerticalScroll = debounce((scrollTop: number) => {
|
|
||||||
timelineBodyScrollTop.value = scrollTop
|
|
||||||
}, 16) // 使用16ms约等于60fps,保证流畅性
|
|
||||||
|
|
||||||
// 缓存时间轴数据的函数
|
// 缓存时间轴数据的函数
|
||||||
const getCachedTimelineData = (): unknown => {
|
const getCachedTimelineData = (): unknown => {
|
||||||
const scale = currentTimeScale.value
|
const scale = currentTimeScale.value
|
||||||
@@ -2765,6 +2772,7 @@ const convertTaskToMilestone = (task: Task): Milestone => {
|
|||||||
watch(
|
watch(
|
||||||
() => tasks.value.length,
|
() => tasks.value.length,
|
||||||
() => {
|
() => {
|
||||||
|
invalidateTaskDateRangeCache()
|
||||||
computeAllMilestonesPositions()
|
computeAllMilestonesPositions()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -2813,6 +2821,9 @@ const updateTimelineRange = () => {
|
|||||||
watch(
|
watch(
|
||||||
() => tasks.value?.length,
|
() => tasks.value?.length,
|
||||||
(newLength, oldLength) => {
|
(newLength, oldLength) => {
|
||||||
|
if (newLength !== oldLength) {
|
||||||
|
invalidateTaskDateRangeCache()
|
||||||
|
}
|
||||||
// 当任务从无到有时,重新计算时间范围
|
// 当任务从无到有时,重新计算时间范围
|
||||||
if (oldLength === 0 && newLength > 0) {
|
if (oldLength === 0 && newLength > 0) {
|
||||||
debouncedUpdateTimelineRange(50)
|
debouncedUpdateTimelineRange(50)
|
||||||
@@ -2863,6 +2874,7 @@ watch([timelineData, timelineContainerWidth], () => {
|
|||||||
watch(
|
watch(
|
||||||
() => tasks.value,
|
() => tasks.value,
|
||||||
newTasks => {
|
newTasks => {
|
||||||
|
invalidateTaskDateRangeCache()
|
||||||
// 优化:使用 for 循环直接构建 Set,避免 map 创建临时数组
|
// 优化:使用 for 循环直接构建 Set,避免 map 创建临时数组
|
||||||
const currentTaskIds = new Set<number>()
|
const currentTaskIds = new Set<number>()
|
||||||
for (const task of newTasks) {
|
for (const task of newTasks) {
|
||||||
|
|||||||
Reference in New Issue
Block a user