Merge remote-tracking branch 'src'
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -5,6 +5,24 @@ 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/),
|
||||
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
|
||||
|
||||
### Fixed
|
||||
- jspdf弱点升级
|
||||
- jspdf vulnerabilies
|
||||
|
||||
## [1.4.2-patch3] - 2025-11-13
|
||||
|
||||
### 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 TaskDrawer from '../src/components/TaskDrawer.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 '../src/styles/theme-variables.css'
|
||||
@@ -22,6 +23,100 @@ const { t, formatTranslation } = useI18n()
|
||||
const tasks = 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状态管理
|
||||
const showMilestoneDialog = ref(false)
|
||||
const currentMilestone = ref<Task | null>(null)
|
||||
@@ -110,6 +205,7 @@ const taskBarConfig = computed<TaskBarConfig>(() => ({
|
||||
|
||||
// 配置面板折叠状态
|
||||
const isConfigPanelCollapsed = ref(false)
|
||||
const isDataSourcePanelCollapsed = ref(false)
|
||||
|
||||
// TaskList 配置区域折叠状态(默认收起)
|
||||
const isTaskListConfigCollapsed = ref(true)
|
||||
@@ -122,6 +218,10 @@ const toggleConfigPanel = () => {
|
||||
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
|
||||
}
|
||||
|
||||
const toggleDataSourcePanel = () => {
|
||||
isDataSourcePanelCollapsed.value = !isDataSourcePanelCollapsed.value
|
||||
}
|
||||
|
||||
// 切换 TaskList 配置区域
|
||||
const toggleTaskListConfig = () => {
|
||||
isTaskListConfigCollapsed.value = !isTaskListConfigCollapsed.value
|
||||
@@ -377,8 +477,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tasks.value = demoData.tasks as Task[]
|
||||
milestones.value = demoData.milestones as Task[]
|
||||
switchDataSource(currentDataSource.value, { silent: true, force: true })
|
||||
})
|
||||
|
||||
// 清理被删除任务的predecessor依赖关系
|
||||
@@ -480,6 +579,64 @@ function taskDebug(item: any) {
|
||||
</h1>
|
||||
<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-header" @click="toggleConfigPanel">
|
||||
@@ -505,7 +662,7 @@ function taskDebug(item: any) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 10l5 5 5-5"
|
||||
:d="isConfigPanelCollapsed ? 'M7 14l5-5 5 5' : 'M7 10l5 5 5-5'"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
@@ -917,6 +1074,106 @@ function taskDebug(item: any) {
|
||||
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列配置面板样式 - 可折叠 */
|
||||
.config-panel {
|
||||
background: var(--gantt-bg-primary, #ffffff);
|
||||
@@ -964,8 +1221,6 @@ function taskDebug(item: any) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
@@ -974,6 +1229,10 @@ function taskDebug(item: any) {
|
||||
color: var(--gantt-text-secondary, #666);
|
||||
}
|
||||
|
||||
.collapse-button.collapsed {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.collapse-button:hover {
|
||||
background-color: var(--gantt-hover-bg, #e8f4fd);
|
||||
color: var(--gantt-primary-color, #409eff);
|
||||
@@ -985,10 +1244,6 @@ function taskDebug(item: any) {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.collapse-button.collapsed .collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.config-content-enter-active,
|
||||
.config-content-leave-active {
|
||||
@@ -1479,6 +1734,36 @@ function taskDebug(item: any) {
|
||||
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 {
|
||||
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
@@ -31,7 +31,7 @@
|
||||
"id": 1101,
|
||||
"name": "试验方案设计与<span style='font-weight: bold;color:red;'>伦理审查</span>",
|
||||
"assignee": "方案设计师 李明",
|
||||
"avatar": "https://i.pravatar.cc/150?img=1",
|
||||
"avatar": "https://i.pravatar.cc/50?img=1",
|
||||
"startDate": "2025-01-01",
|
||||
"endDate": "2025-02-28",
|
||||
"progress": 100,
|
||||
@@ -46,7 +46,7 @@
|
||||
"id": 1102,
|
||||
"name": "受试者招募与筛选",
|
||||
"assignee": "招募专员 张丽",
|
||||
"avatar": "https://i.pravatar.cc/150?img=5",
|
||||
"avatar": "https://i.pravatar.cc/50?img=5",
|
||||
"startDate": "2025-03-01",
|
||||
"endDate": "2025-04-30",
|
||||
"progress": 90,
|
||||
@@ -62,7 +62,7 @@
|
||||
"id": 1103,
|
||||
"name": "药物给药与安全性监测",
|
||||
"assignee": "临床医生 Dr. Liu",
|
||||
"avatar": "https://i.pravatar.cc/150?img=8",
|
||||
"avatar": "https://i.pravatar.cc/50?img=8",
|
||||
"startDate": "2025-05-01",
|
||||
"endDate": "2025-08-31",
|
||||
"progress": 40,
|
||||
@@ -95,7 +95,7 @@
|
||||
"id": 1201,
|
||||
"name": "多中心试验<span style='font-weight: bold; color: blue;'>启动</span>",
|
||||
"assignee": "项目经理 王芳",
|
||||
"avatar": "https://i.pravatar.cc/150?img=10",
|
||||
"avatar": "https://i.pravatar.cc/50?img=10",
|
||||
"startDate": "2025-09-01",
|
||||
"endDate": "2025-11-30",
|
||||
"progress": 25,
|
||||
@@ -111,7 +111,7 @@
|
||||
"id": 1202,
|
||||
"name": "患者入组与随机化",
|
||||
"assignee": "数据管理员 陈静",
|
||||
"avatar": "https://i.pravatar.cc/150?img=20",
|
||||
"avatar": "https://i.pravatar.cc/50?img=20",
|
||||
"startDate": "2025-12-01",
|
||||
"endDate": "2026-03-31",
|
||||
"progress": 0,
|
||||
@@ -127,7 +127,7 @@
|
||||
"id": 1203,
|
||||
"name": "疗效评估与数据收集",
|
||||
"assignee": "统计师 赵磊",
|
||||
"avatar": "https://i.pravatar.cc/150?img=15",
|
||||
"avatar": "https://i.pravatar.cc/50?img=15",
|
||||
"startDate": "2026-01-01",
|
||||
"endDate": "2026-08-31",
|
||||
"progress": 0,
|
||||
|
||||
@@ -306,5 +306,21 @@
|
||||
"<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>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.4.3",
|
||||
"date": "2025-11-28",
|
||||
"notes": [
|
||||
"性能优化:TaskList和Timeline增加虚拟滚动支持,大幅降低节点数和重绘开销",
|
||||
"性能优化:大数据量任务渲染性能优化",
|
||||
"性能优化:虚拟渲染优化",
|
||||
"新增:大数据加载演示",
|
||||
"修复:jspdf弱点升级",
|
||||
"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",
|
||||
"Fix: jspdf vulnerabilies"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
80
package-lock.json
generated
80
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -60,9 +60,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -957,6 +957,11 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz",
|
||||
@@ -1488,17 +1493,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1540,17 +1534,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -2090,6 +2073,16 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
|
||||
@@ -2375,6 +2368,11 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -2421,9 +2419,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -2451,13 +2449,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-3.0.1.tgz",
|
||||
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -2676,6 +2673,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3213,9 +3215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.4.2-patch.3",
|
||||
"version": "1.4.3",
|
||||
"type": "module",
|
||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
@@ -24,6 +24,7 @@ interface Props {
|
||||
width: number
|
||||
height: number
|
||||
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
|
||||
offsetTop?: number // Canvas 在垂直方向的偏移量(用于虚拟渲染)
|
||||
highlightedTaskId: number | null
|
||||
highlightedTaskIds: Set<number>
|
||||
hoveredTaskId: number | null
|
||||
@@ -36,6 +37,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
verticalLines: () => [],
|
||||
showVerticalLines: true,
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
})
|
||||
|
||||
// Canvas 引用
|
||||
@@ -124,6 +126,8 @@ const drawLinks = () => {
|
||||
// 虚拟渲染:计算 Canvas 覆盖的范围
|
||||
const canvasStartX = props.offsetLeft
|
||||
const canvasEndX = props.offsetLeft + displayWidth
|
||||
const canvasStartY = props.offsetTop
|
||||
const canvasEndY = props.offsetTop + displayHeight
|
||||
|
||||
// 定义线条数据类型
|
||||
interface LineData {
|
||||
@@ -157,16 +161,6 @@ const drawLinks = () => {
|
||||
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
|
||||
@@ -189,11 +183,26 @@ const drawLinks = () => {
|
||||
const globalX2 = toBar.left
|
||||
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
|
||||
|
||||
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
|
||||
// 如果起点和终点都在 Canvas 外,跳过
|
||||
const lineMinX = Math.min(globalX1, globalX2)
|
||||
const lineMaxX = Math.max(globalX1, globalX2)
|
||||
const lineMinY = Math.min(globalY1, globalY2)
|
||||
const lineMaxY = Math.max(globalY1, globalY2)
|
||||
if (
|
||||
lineMaxX < canvasStartX ||
|
||||
lineMinX > canvasEndX ||
|
||||
lineMaxY < canvasStartY ||
|
||||
lineMinY > canvasEndY
|
||||
) {
|
||||
continue // 完全在 Canvas 外,跳过
|
||||
}
|
||||
|
||||
// 转换为 Canvas 局部坐标
|
||||
const x1 = globalX1 - props.offsetLeft
|
||||
const y1 = globalY1
|
||||
const y1 = globalY1 - props.offsetTop
|
||||
const x2 = globalX2 - props.offsetLeft
|
||||
const y2 = globalY2
|
||||
const y2 = globalY2 - props.offsetTop
|
||||
|
||||
const c1x = x1 + 40
|
||||
const c1y = y1
|
||||
@@ -369,6 +378,7 @@ watch(
|
||||
() => props.verticalLines,
|
||||
() => props.showVerticalLines,
|
||||
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
|
||||
() => props.offsetTop,
|
||||
],
|
||||
() => {
|
||||
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
|
||||
@@ -430,7 +440,7 @@ defineExpose({
|
||||
top: 0,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
transform: `translateX(${offsetLeft}px)`,
|
||||
transform: `translate(${offsetLeft}px, ${offsetTop}px)`,
|
||||
zIndex: highlightedTaskId !== null ? 1001 : 25,
|
||||
pointerEvents: 'none',
|
||||
}"
|
||||
|
||||
@@ -119,6 +119,25 @@ const createLocalToday = (): Date => {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
}
|
||||
|
||||
// 缓存今天的日期,避免频繁创建
|
||||
// 每分钟更新一次缓存(对于日期判断来说足够了)
|
||||
const cachedToday = ref(createLocalToday())
|
||||
let todayCacheTimer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// 每60秒更新一次今天的日期缓存
|
||||
todayCacheTimer = window.setInterval(() => {
|
||||
cachedToday.value = createLocalToday()
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (todayCacheTimer !== null) {
|
||||
clearInterval(todayCacheTimer)
|
||||
todayCacheTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
const formatDateToLocalString = (date: Date): string => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
@@ -223,7 +242,7 @@ const taskBarStyle = computed(() => {
|
||||
|
||||
const startDate = createLocalDate(currentStartDate)
|
||||
const endDate = createLocalDate(currentEndDate)
|
||||
const baseStart = createLocalDate(props.startDate)
|
||||
const baseStart = parsedBaseStartDate.value
|
||||
if (!startDate || !endDate || !baseStart) {
|
||||
return {
|
||||
left: '0px',
|
||||
@@ -392,6 +411,24 @@ const taskBarStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 缓存 TaskBar 的位置信息,减少 DOM 读取频率
|
||||
const cachedPosition = ref({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
timestamp: 0,
|
||||
})
|
||||
|
||||
// 位置缓存有效期(毫秒)
|
||||
const POSITION_CACHE_TTL = 100 // 100ms 内使用缓存值
|
||||
|
||||
// 缓存解析后的结束日期,避免在 taskStatus 中重复解析
|
||||
const parsedEndDate = computed(() => createLocalDate(props.task.endDate || ''))
|
||||
|
||||
// 缓存解析后的基准开始日期
|
||||
const parsedBaseStartDate = computed(() => createLocalDate(props.startDate))
|
||||
|
||||
// 计算任务状态和颜色
|
||||
const taskStatus = computed(() => {
|
||||
// 父级任务(Story类型)使用与新建按钮一致的配色
|
||||
@@ -404,8 +441,9 @@ const taskStatus = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
const today = createLocalToday()
|
||||
const endDate = createLocalDate(props.task.endDate || '')
|
||||
// 使用缓存的今天日期,避免频繁创建日期对象
|
||||
const today = cachedToday.value
|
||||
const endDate = parsedEndDate.value
|
||||
const progress = props.task.progress || 0
|
||||
|
||||
// 已完成
|
||||
@@ -556,6 +594,8 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
|
||||
const timelineContainer = document.querySelector('.timeline') as HTMLElement
|
||||
if (!timelineContainer || !barRef.value) return
|
||||
|
||||
// 在 mousedown 事件中读取位置是合理的(不是高频操作)
|
||||
// 这个值用于计算拖拽偏移量,只在开始拖拽时读取一次
|
||||
const barRect = barRef.value.getBoundingClientRect()
|
||||
|
||||
// 计算鼠标相对于TaskBar的位置
|
||||
@@ -595,17 +635,57 @@ const handleAutoScroll = (event: CustomEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用缓存机制减少 DOM 读取频率,但保证位置准确性
|
||||
let reportPositionScheduled = false
|
||||
|
||||
function reportBarPosition() {
|
||||
if (barRef.value) {
|
||||
// 如果已经安排了本帧的位置报告,则跳过
|
||||
if (reportPositionScheduled) return
|
||||
|
||||
reportPositionScheduled = true
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
reportPositionScheduled = false
|
||||
|
||||
if (!barRef.value) return
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
// 如果缓存未过期,使用缓存值
|
||||
if (now - cachedPosition.value.timestamp < POSITION_CACHE_TTL) {
|
||||
emit('bar-mounted', {
|
||||
id: props.task.id,
|
||||
left: cachedPosition.value.left,
|
||||
top: cachedPosition.value.top,
|
||||
width: cachedPosition.value.width,
|
||||
height: cachedPosition.value.height,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存过期或首次调用,读取 DOM 并更新缓存
|
||||
// TaskBar 传递绝对位置(相对于视口),Timeline 会将其转换为相对位置
|
||||
const rect = barRef.value.getBoundingClientRect()
|
||||
emit('bar-mounted', {
|
||||
id: props.task.id,
|
||||
|
||||
// 计算并缓存位置
|
||||
const position = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
cachedPosition.value = {
|
||||
...position,
|
||||
timestamp: now,
|
||||
}
|
||||
|
||||
emit('bar-mounted', {
|
||||
id: props.task.id,
|
||||
...position,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拖拽时的实时日期提示框状态
|
||||
@@ -1123,8 +1203,20 @@ const handleMouseUp = () => {
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
reportBarPosition()
|
||||
|
||||
// 使用 ResizeObserver 监听任务名称宽度变化
|
||||
if (taskBarNameRef.value) {
|
||||
nameTextWidth.value = taskBarNameRef.value.getBoundingClientRect().width
|
||||
const nameResizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
nameTextWidth.value = entry.contentRect.width
|
||||
}
|
||||
})
|
||||
nameResizeObserver.observe(taskBarNameRef.value)
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
nameResizeObserver.disconnect()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1395,7 +1487,7 @@ const stickyStyles = computed(() => {
|
||||
} else if (nameNeedsRightSticky) {
|
||||
const offset = rightBoundary - taskLeft - nameWidth
|
||||
// name 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
|
||||
nameLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距
|
||||
nameLeft = `${offset - handleWidth - 10}px` // 考虑手柄宽度 + 间距
|
||||
namePosition = 'absolute'
|
||||
nameTop = '2px'
|
||||
}
|
||||
@@ -1431,7 +1523,7 @@ const stickyStyles = computed(() => {
|
||||
progressTop = '18px'
|
||||
} else if (progressNeedsRightSticky) {
|
||||
const offset = rightBoundary - taskLeft - progressWidth
|
||||
// progress 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
|
||||
// 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
|
||||
progressLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距
|
||||
progressPosition = 'absolute'
|
||||
progressTop = '18px'
|
||||
|
||||
@@ -35,6 +35,20 @@ const { t } = useI18n()
|
||||
|
||||
// TaskList 容器引用
|
||||
const taskListRef = ref<HTMLElement | null>(null)
|
||||
const taskListBodyRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 缓存容器宽度,避免频繁读取 offsetWidth 导致强制重排
|
||||
const cachedContainerWidth = ref(0)
|
||||
|
||||
// 使用 ResizeObserver 监听容器宽度变化
|
||||
let containerResizeObserver: ResizeObserver | null = null
|
||||
let bodyResizeObserver: ResizeObserver | null = null
|
||||
|
||||
// 纵向虚拟滚动相关状态
|
||||
const ROW_HEIGHT = 51 // 每行高度(与TaskList Row一致)
|
||||
const VERTICAL_BUFFER = 5 // 上下额外渲染的缓冲行数
|
||||
const taskListScrollTop = ref(0)
|
||||
const taskListBodyHeight = ref(0)
|
||||
|
||||
// 获取列宽度样式(百分比转像素)
|
||||
const getColumnWidthStyle = (column: { width?: number | string }) => {
|
||||
@@ -44,7 +58,7 @@ const getColumnWidthStyle = (column: { width?: number | string }) => {
|
||||
|
||||
// 如果是百分比,转换为像素
|
||||
if (typeof column.width === 'string' && column.width.includes('%')) {
|
||||
const containerWidth = taskListRef.value?.offsetWidth || 0
|
||||
const containerWidth = cachedContainerWidth.value || 0
|
||||
if (containerWidth > 0) {
|
||||
const percentage = parseFloat(column.width) / 100
|
||||
const pixels = Math.floor(containerWidth * percentage)
|
||||
@@ -89,6 +103,14 @@ const handleSplitterDragStart = () => {
|
||||
// 处理拖拽结束事件
|
||||
const handleSplitterDragEnd = () => {
|
||||
isSplitterDragging.value = false
|
||||
|
||||
// ⚠️ 拖拽结束后,手动更新一次容器宽度,触发列宽重新计算
|
||||
if (taskListRef.value) {
|
||||
const newWidth = taskListRef.value.offsetWidth
|
||||
if (Math.abs(newWidth - cachedContainerWidth.value) > 1) {
|
||||
cachedContainerWidth.value = newWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理任务行悬停事件
|
||||
@@ -228,6 +250,61 @@ const getAllTasks = (taskList: Task[]): Task[] => {
|
||||
return allTasks
|
||||
}
|
||||
|
||||
// 获取当前折叠状态下的可见任务列表
|
||||
const getFlattenedVisibleTasks = (
|
||||
taskList: Task[],
|
||||
level = 0,
|
||||
): Array<{ task: Task; level: number }> => {
|
||||
const result: Array<{ task: Task; level: number }> = []
|
||||
|
||||
for (const task of taskList) {
|
||||
result.push({ task, level })
|
||||
|
||||
const isMilestoneGroup = task.type === 'milestone-group'
|
||||
|
||||
if (!isMilestoneGroup && task.children && task.children.length > 0 && !task.collapsed) {
|
||||
result.push(...getFlattenedVisibleTasks(task.children, level + 1))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 扁平化后的可见任务列表
|
||||
const flattenedTasks = computed(() => getFlattenedVisibleTasks(localTasks.value))
|
||||
|
||||
// 计算可视区域任务范围
|
||||
const visibleTaskRange = computed(() => {
|
||||
const scrollTop = taskListScrollTop.value
|
||||
const containerHeight = taskListBodyHeight.value || 600
|
||||
|
||||
const startIndex = Math.floor(scrollTop / ROW_HEIGHT) - VERTICAL_BUFFER
|
||||
const endIndex = Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + VERTICAL_BUFFER
|
||||
|
||||
const total = flattenedTasks.value.length
|
||||
const clampedStart = Math.min(Math.max(0, startIndex), total)
|
||||
const clampedEnd = Math.min(total, Math.max(clampedStart + 1, endIndex))
|
||||
|
||||
return {
|
||||
startIndex: clampedStart,
|
||||
endIndex: clampedEnd,
|
||||
}
|
||||
})
|
||||
|
||||
// 虚拟列表中需要渲染的任务
|
||||
const visibleTasks = computed(() => {
|
||||
const { startIndex, endIndex } = visibleTaskRange.value
|
||||
return flattenedTasks.value.slice(startIndex, endIndex)
|
||||
})
|
||||
|
||||
// Spacer 高度用于撑起滚动区域
|
||||
const totalContentHeight = computed(() => flattenedTasks.value.length * ROW_HEIGHT)
|
||||
const startSpacerHeight = computed(() => visibleTaskRange.value.startIndex * ROW_HEIGHT)
|
||||
const endSpacerHeight = computed(() => {
|
||||
const visibleHeight = visibleTasks.value.length * ROW_HEIGHT
|
||||
return Math.max(0, totalContentHeight.value - startSpacerHeight.value - visibleHeight)
|
||||
})
|
||||
|
||||
// 监听外部传入的 tasks 数据变化
|
||||
watch(
|
||||
() => props.tasks,
|
||||
@@ -331,16 +408,23 @@ const handleTaskListScroll = (event: Event) => {
|
||||
|
||||
const scrollTop = target.scrollTop
|
||||
|
||||
taskListScrollTop.value = scrollTop
|
||||
|
||||
// 同步垂直滚动到Timeline
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('task-list-vertical-scroll', {
|
||||
detail: { scrollTop },
|
||||
}),
|
||||
)
|
||||
} // 处理Timeline垂直滚动同步
|
||||
}
|
||||
|
||||
// 处理Timeline垂直滚动同步
|
||||
const handleTimelineVerticalScroll = (event: CustomEvent) => {
|
||||
const { scrollTop } = event.detail
|
||||
const taskListBodyElement = document.querySelector('.task-list-body') as HTMLElement
|
||||
const taskListBodyElement = taskListBodyRef.value
|
||||
|
||||
taskListScrollTop.value = scrollTop
|
||||
|
||||
if (taskListBodyElement && Math.abs(taskListBodyElement.scrollTop - scrollTop) > 1) {
|
||||
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
|
||||
taskListBodyElement.scrollTop = scrollTop
|
||||
@@ -358,6 +442,7 @@ const handleTaskRowContextMenu = (event: { task: Task; position: { x: number; y:
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('TaskList - Failed to dispatch context-menu event', error)
|
||||
}
|
||||
}
|
||||
@@ -386,6 +471,37 @@ const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化 ResizeObserver 监听容器宽度变化
|
||||
if (taskListRef.value) {
|
||||
containerResizeObserver = new ResizeObserver((entries) => {
|
||||
// ⚠️ 拖拽期间跳过更新,避免高频重新计算列宽
|
||||
// TaskList的列宽不需要在拖拽期间实时更新,只需在拖拽结束后更新一次
|
||||
if (isSplitterDragging.value) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// 使用 contentRect.width 避免强制重排
|
||||
cachedContainerWidth.value = entry.contentRect.width
|
||||
}
|
||||
})
|
||||
containerResizeObserver.observe(taskListRef.value)
|
||||
}
|
||||
|
||||
// 监听TaskList body高度变化,提供虚拟滚动所需尺寸
|
||||
if (taskListBodyRef.value) {
|
||||
taskListBodyHeight.value = taskListBodyRef.value.clientHeight
|
||||
taskListScrollTop.value = taskListBodyRef.value.scrollTop
|
||||
|
||||
bodyResizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
taskListBodyHeight.value = entry.contentRect.height
|
||||
}
|
||||
})
|
||||
|
||||
bodyResizeObserver.observe(taskListBodyRef.value)
|
||||
}
|
||||
|
||||
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
|
||||
window.addEventListener('task-added', handleTaskAdded as EventListener)
|
||||
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||
@@ -401,6 +517,17 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理 ResizeObserver
|
||||
if (containerResizeObserver) {
|
||||
containerResizeObserver.disconnect()
|
||||
containerResizeObserver = null
|
||||
}
|
||||
|
||||
if (bodyResizeObserver) {
|
||||
bodyResizeObserver.disconnect()
|
||||
bodyResizeObserver = null
|
||||
}
|
||||
|
||||
window.removeEventListener('task-updated', handleTaskUpdated as EventListener)
|
||||
window.removeEventListener('task-added', handleTaskAdded as EventListener)
|
||||
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||
@@ -433,17 +560,20 @@ onUnmounted(() => {
|
||||
{{ (t as any)[column.key] || column.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-body" @scroll="handleTaskListScroll">
|
||||
<TaskRow
|
||||
v-for="task in localTasks"
|
||||
<div ref="taskListBodyRef" class="task-list-body" @scroll="handleTaskListScroll">
|
||||
<div class="task-list-body-spacer" :style="{ height: `${startSpacerHeight}px` }"></div>
|
||||
|
||||
<TaskRow
|
||||
v-for="{ task, level } in visibleTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
:level="0"
|
||||
:level="level"
|
||||
:is-hovered="hoveredTaskId === task.id"
|
||||
:hovered-task-id="hoveredTaskId"
|
||||
:on-hover="handleTaskRowHover"
|
||||
:columns="visibleColumns"
|
||||
:get-column-width-style="getColumnWidthStyle"
|
||||
:disable-children-render="true"
|
||||
@toggle="toggleCollapse"
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@contextmenu="handleTaskRowContextMenu"
|
||||
@@ -457,6 +587,8 @@ onUnmounted(() => {
|
||||
<slot name="custom-task-content" v-bind="rowScope" />
|
||||
</template>
|
||||
</TaskRow>
|
||||
|
||||
<div class="task-list-body-spacer" :style="{ height: `${endSpacerHeight}px` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -509,14 +641,18 @@ onUnmounted(() => {
|
||||
width: max-content;
|
||||
background: var(--gantt-bg-primary);
|
||||
flex: 1;
|
||||
overflow-x: hidden; /* 让body部分可以滚动 */
|
||||
overflow-y: auto; /* 允许垂直滚动 */
|
||||
overflow-x: hidden; /* 允许横向滚动,确保列完整展示 */
|
||||
overflow-y: auto;
|
||||
|
||||
/* Webkit浏览器滚动条样式 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--gantt-scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.task-list-body-spacer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-list-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch, useSlots } from 'vue'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
@@ -36,7 +36,8 @@ interface Props {
|
||||
hoveredTaskId?: number | null
|
||||
onHover?: (taskId: number | null) => void
|
||||
columns: TaskListColumnConfig[]
|
||||
getColumnWidthStyle?: (column: { width?: number | string }) => CSSProperties
|
||||
getColumnWidthStyle?: (column: { width?: number | string }) => StyleValue
|
||||
disableChildrenRender?: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits([
|
||||
@@ -412,7 +413,7 @@ onUnmounted(() => {
|
||||
:key="column.key"
|
||||
class="col"
|
||||
:class="column.cssClass || `col-${column.key}`"
|
||||
:style="getColumnWidthStyle ? getColumnWidthStyle(column) : {}"
|
||||
:style="getColumnWidthStyle ? getColumnWidthStyle(column) : undefined"
|
||||
>
|
||||
<!-- 里程碑分组显示空列 -->
|
||||
<template v-if="isMilestoneGroup">
|
||||
@@ -469,7 +470,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="hasChildren && !props.task.collapsed && !isMilestoneGroup">
|
||||
<template v-if="!props.disableChildrenRender && hasChildren && !props.task.collapsed && !isMilestoneGroup">
|
||||
<TaskRow
|
||||
v-for="child in props.task.children"
|
||||
:key="child.id"
|
||||
@@ -480,6 +481,7 @@ onUnmounted(() => {
|
||||
:on-hover="props.onHover"
|
||||
:columns="props.columns"
|
||||
:get-column-width-style="props.getColumnWidthStyle"
|
||||
:disable-children-render="props.disableChildrenRender"
|
||||
@toggle="emit('toggle', $event)"
|
||||
@dblclick="emit('dblclick', $event)"
|
||||
@start-timer="emit('start-timer', $event)"
|
||||
|
||||
@@ -157,15 +157,20 @@ const dayWidth = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 获取任务数据的日期范围(用于月度视图时间轴范围计算)
|
||||
const getTasksDateRange = () => {
|
||||
type TaskDateRange = { minDate: Date; maxDate: Date } | null
|
||||
let cachedTaskDateRange: TaskDateRange = null
|
||||
|
||||
const invalidateTaskDateRangeCache = () => {
|
||||
cachedTaskDateRange = null
|
||||
}
|
||||
|
||||
const computeTasksDateRange = (): TaskDateRange => {
|
||||
if (!tasks.value || tasks.value.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const dates: Date[] = []
|
||||
|
||||
// 收集所有任务的开始和结束日期
|
||||
const collectDatesFromTask = (task: Task) => {
|
||||
if (task.startDate) {
|
||||
dates.push(new Date(task.startDate))
|
||||
@@ -174,7 +179,6 @@ const getTasksDateRange = () => {
|
||||
dates.push(new Date(task.endDate))
|
||||
}
|
||||
|
||||
// 递归处理子任务 - 使用 for 循环代替 forEach
|
||||
if (task.children && task.children.length > 0) {
|
||||
for (const child of task.children) {
|
||||
collectDatesFromTask(child)
|
||||
@@ -182,7 +186,6 @@ const getTasksDateRange = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 for...of 循环代替 forEach
|
||||
for (const task of tasks.value) {
|
||||
collectDatesFromTask(task)
|
||||
}
|
||||
@@ -191,7 +194,6 @@ const getTasksDateRange = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// 过滤有效日期并直接获取最小/最大时间戳
|
||||
let minTime = Infinity
|
||||
let maxTime = -Infinity
|
||||
for (const date of dates) {
|
||||
@@ -206,10 +208,20 @@ const getTasksDateRange = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const minDate = new Date(minTime)
|
||||
const maxDate = new Date(maxTime)
|
||||
return {
|
||||
minDate: new Date(minTime),
|
||||
maxDate: new Date(maxTime),
|
||||
}
|
||||
}
|
||||
|
||||
return { minDate, maxDate }
|
||||
// 获取任务数据的日期范围(用于月度/年度视图时间轴范围计算)
|
||||
const getTasksDateRange = () => {
|
||||
if (cachedTaskDateRange) {
|
||||
return cachedTaskDateRange
|
||||
}
|
||||
|
||||
cachedTaskDateRange = computeTasksDateRange()
|
||||
return cachedTaskDateRange
|
||||
}
|
||||
|
||||
// 获取小时视图的时间范围
|
||||
@@ -657,6 +669,11 @@ let hideBubblesTimeout: number | null = null // 半圆显示恢复定时器
|
||||
const HOUR_WIDTH = 40 // 每小时40px
|
||||
const VIRTUAL_BUFFER = 10 // 减少缓冲区以提升滑动性能
|
||||
|
||||
// 纵向虚拟滚动相关状态
|
||||
const ROW_HEIGHT = 51 // 每行高度51px (50px + 1px border)
|
||||
const VERTICAL_BUFFER = 5 // 纵向缓冲区行数
|
||||
const timelineBodyScrollTop = ref(0) // 纵向滚动位置
|
||||
|
||||
// 数据缓存
|
||||
const timelineDataCache = new Map<string, unknown>()
|
||||
|
||||
@@ -701,6 +718,30 @@ const visibleHourRange = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 计算纵向可视区域的任务范围
|
||||
const visibleTaskRange = computed(() => {
|
||||
const scrollTop = timelineBodyScrollTop.value
|
||||
const containerHeight = timelineBodyHeight.value || 600
|
||||
|
||||
// 计算可视区域的开始和结束任务索引
|
||||
const startIndex = Math.floor(scrollTop / ROW_HEIGHT) - VERTICAL_BUFFER
|
||||
const endIndex = Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + VERTICAL_BUFFER
|
||||
|
||||
return {
|
||||
startIndex: Math.max(0, startIndex),
|
||||
endIndex: Math.min(tasks.value.length, Math.max(startIndex + 1, endIndex)),
|
||||
}
|
||||
})
|
||||
|
||||
// 获取虚拟滚动优化后的可见任务列表
|
||||
const visibleTasks = computed(() => {
|
||||
const { startIndex, endIndex } = visibleTaskRange.value
|
||||
return tasks.value.slice(startIndex, endIndex).map((task, index) => ({
|
||||
task,
|
||||
originalIndex: startIndex + index,
|
||||
}))
|
||||
})
|
||||
|
||||
// 防抖处理滚动事件(优化:增加防抖时间)
|
||||
const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number): T => {
|
||||
let timeout: number | null = null
|
||||
@@ -717,12 +758,12 @@ const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number)
|
||||
// 优化的滚动处理器(增加防抖时间到 50ms)
|
||||
const debouncedUpdatePositions = debounce(() => {
|
||||
computeAllMilestonesPositions()
|
||||
}, 50)
|
||||
}, 200)
|
||||
|
||||
// 虚拟渲染:防抖更新 Canvas 位置(滚动时触发)
|
||||
const debouncedUpdateCanvasPosition = debounce(() => {
|
||||
updateSvgSize() // 重新计算 Canvas 位置和尺寸
|
||||
}, 50)
|
||||
}, 200)
|
||||
|
||||
// 缓存时间轴数据的函数
|
||||
const getCachedTimelineData = (): unknown => {
|
||||
@@ -1938,17 +1979,20 @@ const svgHeight = ref(0)
|
||||
const canvasWidth = ref(0)
|
||||
const canvasHeight = ref(0)
|
||||
const canvasOffsetLeft = ref(0) // Canvas 在全局坐标系中的偏移量
|
||||
const canvasOffsetTop = ref(0)
|
||||
|
||||
// 虚拟渲染 Canvas 的安全宽度(防止超过浏览器限制)
|
||||
// 可根据实际需求调整:
|
||||
// - 5000: 最小内存 (~30MB),适合低端设备,但滚动时更频繁更新
|
||||
// - 10000: 平衡选择 (~60MB),覆盖小时视图 10 天,周视图 2 年
|
||||
const SAFE_CANVAS_WIDTH = 5000 // 平衡性能和覆盖范围
|
||||
const SAFE_CANVAS_HEIGHT = 5000
|
||||
|
||||
function updateSvgSize() {
|
||||
if (bodyContentRef.value) {
|
||||
// 获取 bodyContent 的总宽度和可视区域宽度
|
||||
const totalWidth = bodyContentRef.value.offsetWidth
|
||||
const totalHeight = contentHeight.value
|
||||
|
||||
// 使用已经维护的 timelineScrollLeft,而不是从 DOM 重新读取
|
||||
// 因为 handleTimelineScroll 已经实时更新了这个值
|
||||
@@ -1975,9 +2019,23 @@ function updateSvgSize() {
|
||||
|
||||
canvasOffsetLeft.value = idealOffsetLeft
|
||||
|
||||
const clampedHeight = Math.min(totalHeight, SAFE_CANVAS_HEIGHT)
|
||||
canvasHeight.value = clampedHeight
|
||||
svgWidth.value = canvasWidth.value
|
||||
svgHeight.value = contentHeight.value
|
||||
canvasHeight.value = contentHeight.value
|
||||
svgHeight.value = clampedHeight
|
||||
|
||||
const scrollTop = timelineBodyScrollTop.value
|
||||
const bufferTop = clampedHeight / 3
|
||||
let idealOffsetTop = Math.max(0, scrollTop - bufferTop)
|
||||
|
||||
if (totalHeight <= clampedHeight) {
|
||||
idealOffsetTop = 0
|
||||
} else {
|
||||
const maxOffsetTop = totalHeight - clampedHeight
|
||||
idealOffsetTop = Math.min(idealOffsetTop, maxOffsetTop)
|
||||
}
|
||||
|
||||
canvasOffsetTop.value = idealOffsetTop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2001,7 +2059,9 @@ function handleBarMounted(payload: {
|
||||
height: payload.height,
|
||||
},
|
||||
}
|
||||
updateSvgSize()
|
||||
setTimeout(() => {
|
||||
updateSvgSize()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 向上传递 TaskBar 拖拽/拉伸事件
|
||||
@@ -2162,6 +2222,12 @@ onMounted(() => {
|
||||
// 处理TaskList垂直滚动同步
|
||||
const handleTaskListVerticalScroll = (event: CustomEvent) => {
|
||||
const { scrollTop } = event.detail
|
||||
|
||||
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
||||
timelineBodyScrollTop.value = scrollTop
|
||||
|
||||
debouncedUpdateCanvasPosition()
|
||||
|
||||
if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) {
|
||||
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
|
||||
timelineBodyElement.value.scrollTop = scrollTop
|
||||
@@ -2175,6 +2241,11 @@ const handleTimelineBodyScroll = (event: Event) => {
|
||||
|
||||
const scrollTop = target.scrollTop
|
||||
|
||||
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
||||
timelineBodyScrollTop.value = scrollTop
|
||||
|
||||
debouncedUpdateCanvasPosition()
|
||||
|
||||
// 拖拽时不同步滚动事件,避免性能问题
|
||||
if (isDragging.value) return
|
||||
|
||||
@@ -2701,6 +2772,7 @@ const convertTaskToMilestone = (task: Task): Milestone => {
|
||||
watch(
|
||||
() => tasks.value.length,
|
||||
() => {
|
||||
invalidateTaskDateRangeCache()
|
||||
computeAllMilestonesPositions()
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -2749,6 +2821,9 @@ const updateTimelineRange = () => {
|
||||
watch(
|
||||
() => tasks.value?.length,
|
||||
(newLength, oldLength) => {
|
||||
if (newLength !== oldLength) {
|
||||
invalidateTaskDateRangeCache()
|
||||
}
|
||||
// 当任务从无到有时,重新计算时间范围
|
||||
if (oldLength === 0 && newLength > 0) {
|
||||
debouncedUpdateTimelineRange(50)
|
||||
@@ -2789,7 +2864,7 @@ watch([timelineData, timelineContainerWidth], () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
updateSvgSize()
|
||||
}, 50)
|
||||
}, 200)
|
||||
})
|
||||
taskBarRenderTimer = null
|
||||
}, 100)
|
||||
@@ -2799,6 +2874,7 @@ watch([timelineData, timelineContainerWidth], () => {
|
||||
watch(
|
||||
() => tasks.value,
|
||||
newTasks => {
|
||||
invalidateTaskDateRangeCache()
|
||||
// 优化:使用 for 循环直接构建 Set,避免 map 创建临时数组
|
||||
const currentTaskIds = new Set<number>()
|
||||
for (const task of newTasks) {
|
||||
@@ -3327,6 +3403,7 @@ const handleAddSuccessor = (task: Task) => {
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:offset-left="canvasOffsetLeft"
|
||||
:offset-top="canvasOffsetTop"
|
||||
:highlighted-task-id="highlightedTaskId"
|
||||
:highlighted-task-ids="highlightedTaskIds"
|
||||
:hovered-task-id="hoveredTaskId"
|
||||
@@ -3510,13 +3587,13 @@ const handleAddSuccessor = (task: Task) => {
|
||||
<!-- 同时需要考虑左侧TaskList包含1px的bottom border -->
|
||||
<div class="task-bar-container" :style="{ height: `${contentHeight}px` }">
|
||||
<div class="task-rows" :style="{ height: `${contentHeight}px` }">
|
||||
<!-- 使用v-memo减少渲染 -->
|
||||
<!-- 使用虚拟滚动渲染可见任务 -->
|
||||
<div
|
||||
v-for="(task, index) in tasks"
|
||||
v-for="{ task, originalIndex } in visibleTasks"
|
||||
:key="task.id"
|
||||
class="task-row"
|
||||
:class="{ 'task-row-hovered': hoveredTaskId === task.id }"
|
||||
:style="{ top: `${index * 51}px` }"
|
||||
:style="{ top: `${originalIndex * 51}px` }"
|
||||
@mouseenter="handleTaskRowHover(task.id)"
|
||||
@mouseleave="handleTaskRowHover(null)"
|
||||
>
|
||||
|
||||
@@ -240,6 +240,29 @@ const messages = {
|
||||
},
|
||||
},
|
||||
disableTaskbarFocusMode: '关闭聚焦功能',
|
||||
dataSourceAlreadyLoaded: '{name} 已是当前数据源',
|
||||
dataSourceLoadSuccess: '已加载 {name}',
|
||||
dataSourceLoadFailed: '{name} 加载失败',
|
||||
dataSourceSwitch: {
|
||||
title: '数据源切换',
|
||||
subtitle: '对比常规与超大数据集的初始化体验',
|
||||
loading: '数据加载中,请稍候...',
|
||||
alreadyLoaded: '{name} 已是当前数据源',
|
||||
loadSuccess: '已加载 {name}',
|
||||
loadFailed: '{name} 加载失败',
|
||||
sources: {
|
||||
normal: {
|
||||
label: '常规数据源',
|
||||
description: 'data.json · 含完整前/后置依赖,适合功能演示',
|
||||
badge: 'data.json',
|
||||
},
|
||||
large: {
|
||||
label: '超大数据源',
|
||||
description: 'data-large-1m.json · 百万级任务,验证虚拟渲染性能',
|
||||
badge: 'data-large-1m.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'en-US': {
|
||||
dateNotSet: 'Not set',
|
||||
@@ -478,6 +501,29 @@ const messages = {
|
||||
},
|
||||
},
|
||||
disableTaskbarFocusMode: 'Disable Focus Mode',
|
||||
dataSourceAlreadyLoaded: '{name} is already active',
|
||||
dataSourceLoadSuccess: '{name} loaded successfully',
|
||||
dataSourceLoadFailed: '{name} failed to load',
|
||||
dataSourceSwitch: {
|
||||
title: 'Data Sources',
|
||||
subtitle: 'Compare default vs. mega dataset initialization',
|
||||
loading: 'Loading data, please wait…',
|
||||
alreadyLoaded: '{name} is already active',
|
||||
loadSuccess: '{name} loaded successfully',
|
||||
loadFailed: '{name} failed to load',
|
||||
sources: {
|
||||
normal: {
|
||||
label: 'Standard Dataset',
|
||||
description: 'data.json · Full predecessor graph for feature demos',
|
||||
badge: 'data.json',
|
||||
},
|
||||
large: {
|
||||
label: 'Massive Dataset',
|
||||
description: 'data-large-1m.json · Million-level tasks to stress virtual rendering',
|
||||
badge: 'data-large-1m.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user