v1.1.0 - Trigger timeline scroll when TaskBar dragging; Add 'Day|Week|Month' timeline scale; Issues fixed.
This commit is contained in:
@@ -5,6 +5,15 @@ 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.1.0] - 2025-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- TaskBar拖拽触发Timeline水平滑动
|
||||||
|
- 增加日|周|月视图切换
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 问题修复
|
||||||
|
|
||||||
## [1.0.10] - 2025-07-06
|
## [1.0.10] - 2025-07-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+3
-1
@@ -188,6 +188,7 @@ interface ToolbarConfig {
|
|||||||
showLanguage?: boolean // Show language switch button
|
showLanguage?: boolean // Show language switch button
|
||||||
showTheme?: boolean // Show theme switch button
|
showTheme?: boolean // Show theme switch button
|
||||||
showFullscreen?: boolean // Show fullscreen toggle button
|
showFullscreen?: boolean // Show fullscreen toggle button
|
||||||
|
showTimeScale?: boolean // Show time scale toggle buttons (Day|Week|Month)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -327,7 +328,8 @@ const toolbarConfig = {
|
|||||||
showTodayLocate: true,
|
showTodayLocate: true,
|
||||||
showExportCsv: true,
|
showExportCsv: true,
|
||||||
showExportPdf: true,
|
showExportPdf: true,
|
||||||
showFullscreen: true
|
showFullscreen: true,
|
||||||
|
showTimeScale: true // Control visibility of Day|Week|Month time scale toggle buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom locale messages
|
// Custom locale messages
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ interface ToolbarConfig {
|
|||||||
showLanguage?: boolean // 是否显示语言切换按钮
|
showLanguage?: boolean // 是否显示语言切换按钮
|
||||||
showTheme?: boolean // 是否显示主题切换按钮
|
showTheme?: boolean // 是否显示主题切换按钮
|
||||||
showFullscreen?: boolean // 是否显示全屏切换按钮
|
showFullscreen?: boolean // 是否显示全屏切换按钮
|
||||||
|
showTimeScale?: boolean // 是否显示时间刻度切换按钮组(日|周|月)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -328,7 +329,8 @@ const toolbarConfig = {
|
|||||||
showTodayLocate: true,
|
showTodayLocate: true,
|
||||||
showExportCsv: true,
|
showExportCsv: true,
|
||||||
showExportPdf: true,
|
showExportPdf: true,
|
||||||
showFullscreen: true
|
showFullscreen: true,
|
||||||
|
showTimeScale: true // 控制日|周|月时间刻度按钮组的可见性
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义多语言配置
|
// 自定义多语言配置
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const toolbarConfig = {
|
|||||||
showLanguage: true,
|
showLanguage: true,
|
||||||
showTheme: true,
|
showTheme: true,
|
||||||
showFullscreen: true,
|
showFullscreen: true,
|
||||||
|
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义CSV导出处理器(可选)
|
// 自定义CSV导出处理器(可选)
|
||||||
|
|||||||
@@ -135,5 +135,10 @@
|
|||||||
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
||||||
"增强TaskBar可以存在多个前置任务"
|
"增强TaskBar可以存在多个前置任务"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.1.0",
|
||||||
|
"date": "2025-07-08",
|
||||||
|
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import jsPDF from 'jspdf'
|
|||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||||
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
import { useMessage } from '../composables/useMessage'
|
import { useMessage } from '../composables/useMessage'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -91,6 +92,9 @@ const leftPanelWidth = ref(320)
|
|||||||
// Timeline组件的引用
|
// Timeline组件的引用
|
||||||
const timelineRef = ref<InstanceType<typeof Timeline> | null>(null)
|
const timelineRef = ref<InstanceType<typeof Timeline> | null>(null)
|
||||||
|
|
||||||
|
// 时间刻度状态
|
||||||
|
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
// 列宽: 300+120+120+140+140+100+100+100 = 1120px
|
// 列宽: 300+120+120+140+140+100+100+100 = 1120px
|
||||||
// 边框: 7个列间边框 * 1px = 7px
|
// 边框: 7个列间边框 * 1px = 7px
|
||||||
@@ -615,9 +619,18 @@ const timelineDateRange = computed(() => {
|
|||||||
const minDate = new Date(Math.min(...startDates.map(d => d.getTime())))
|
const minDate = new Date(Math.min(...startDates.map(d => d.getTime())))
|
||||||
const maxDate = new Date(Math.max(...endDates.map(d => d.getTime())))
|
const maxDate = new Date(Math.max(...endDates.map(d => d.getTime())))
|
||||||
|
|
||||||
// 前后各延伸6个月
|
// 日视图前后各延伸6个月
|
||||||
const min = new Date(minDate.getFullYear(), minDate.getMonth() - 6, 1)
|
let min = new Date(minDate.getFullYear(), minDate.getMonth() - 6, 1)
|
||||||
const max = new Date(maxDate.getFullYear(), maxDate.getMonth() + 6 + 1, 0)
|
let max = new Date(maxDate.getFullYear(), maxDate.getMonth() + 6 + 1, 0)
|
||||||
|
if (currentTimeScale.value === TimelineScale.WEEK) {
|
||||||
|
// 月视图Timeline周期为往前1年~往后1年
|
||||||
|
min = new Date(minDate.getFullYear() - 1, minDate.getMonth(), 1)
|
||||||
|
max = new Date(maxDate.getFullYear() + 1, maxDate.getMonth() + 1, 0)
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.MONTH) {
|
||||||
|
// 月视图Timeline周期为往前2年~往后2年
|
||||||
|
min = new Date(minDate.getFullYear() - 2, minDate.getMonth(), 1)
|
||||||
|
max = new Date(maxDate.getFullYear() + 2, maxDate.getMonth() + 1, 0)
|
||||||
|
}
|
||||||
return { min, max }
|
return { min, max }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -638,6 +651,25 @@ const csvExportHandler = () => {
|
|||||||
defaultExportCsv()
|
defaultExportCsv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 时间刻度变化处理函数
|
||||||
|
const handleTimeScaleChange = (scale: TimelineScale) => {
|
||||||
|
currentTimeScale.value = scale
|
||||||
|
// 通知 Timeline 组件更新时间刻度
|
||||||
|
if (timelineRef.value) {
|
||||||
|
timelineRef.value.updateTimeScale(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeline组件时间刻度变化完成后的处理函数
|
||||||
|
const handleTimelineScaleChanged = (scale: TimelineScale) => {
|
||||||
|
// 强制重新渲染所有TaskBar,触发位置重新计算
|
||||||
|
nextTick(() => {
|
||||||
|
// 触发强制更新,让所有TaskBar重新计算位置
|
||||||
|
const event = new CustomEvent('timeline-scale-updated', { detail: scale })
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 默认CSV导出功能
|
// 默认CSV导出功能
|
||||||
const defaultExportCsv = () => {
|
const defaultExportCsv = () => {
|
||||||
try {
|
try {
|
||||||
@@ -1156,6 +1188,7 @@ watch(
|
|||||||
:on-language-change="props.onLanguageChange"
|
:on-language-change="props.onLanguageChange"
|
||||||
:on-theme-change="props.onThemeChange"
|
:on-theme-change="props.onThemeChange"
|
||||||
:on-fullscreen-change="props.onFullscreenChange"
|
:on-fullscreen-change="props.onFullscreenChange"
|
||||||
|
:on-time-scale-change="handleTimeScaleChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 甘特图主体 -->
|
<!-- 甘特图主体 -->
|
||||||
@@ -1212,6 +1245,7 @@ watch(
|
|||||||
:use-default-drawer="props.useDefaultDrawer"
|
:use-default-drawer="props.useDefaultDrawer"
|
||||||
:on-task-delete="props.onTaskDelete"
|
:on-task-delete="props.onTaskDelete"
|
||||||
:on-milestone-save="handleMilestoneSave"
|
:on-milestone-save="handleMilestoneSave"
|
||||||
|
@timeline-scale-changed="handleTimelineScaleChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||||
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
import '../styles/app.css'
|
import '../styles/app.css'
|
||||||
|
|
||||||
// 语言定义 - 使用多语言系统的类型
|
// 语言定义 - 使用多语言系统的类型
|
||||||
@@ -18,6 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
onThemeChange: undefined,
|
onThemeChange: undefined,
|
||||||
onFullscreenChange: undefined,
|
onFullscreenChange: undefined,
|
||||||
onSettingsConfirm: undefined,
|
onSettingsConfirm: undefined,
|
||||||
|
onTimeScaleChange: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -29,6 +31,7 @@ const emit = defineEmits<{
|
|||||||
'language-change': [lang: 'zh-CN' | 'en-US']
|
'language-change': [lang: 'zh-CN' | 'en-US']
|
||||||
'theme-change': [isDark: boolean]
|
'theme-change': [isDark: boolean]
|
||||||
'fullscreen-change': [isFullscreen: boolean]
|
'fullscreen-change': [isFullscreen: boolean]
|
||||||
|
'time-scale-change': [scale: TimelineScale]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// LocalStorage keys
|
// LocalStorage keys
|
||||||
@@ -52,6 +55,7 @@ interface Props {
|
|||||||
onLanguageChange?: (lang: 'zh-CN' | 'en-US') => void
|
onLanguageChange?: (lang: 'zh-CN' | 'en-US') => void
|
||||||
onThemeChange?: (isDark: boolean) => void
|
onThemeChange?: (isDark: boolean) => void
|
||||||
onFullscreenChange?: (isFullscreen: boolean) => void
|
onFullscreenChange?: (isFullscreen: boolean) => void
|
||||||
|
onTimeScaleChange?: (scale: TimelineScale) => void
|
||||||
// 外部确认接口
|
// 外部确认接口
|
||||||
onSettingsConfirm?: (
|
onSettingsConfirm?: (
|
||||||
type: 'theme' | 'language',
|
type: 'theme' | 'language',
|
||||||
@@ -78,6 +82,7 @@ const currentLanguage = ref<Language>('zh')
|
|||||||
const isDarkMode = ref(getInitialTheme())
|
const isDarkMode = ref(getInitialTheme())
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
const showLanguageDropdown = ref(false)
|
const showLanguageDropdown = ref(false)
|
||||||
|
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||||
|
|
||||||
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
|
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
|
||||||
const t = (key: string): string => {
|
const t = (key: string): string => {
|
||||||
@@ -261,6 +266,33 @@ const handleFullscreenToggle = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 时间刻度切换处理
|
||||||
|
const handleTimeScaleChange = (scale: TimelineScale) => {
|
||||||
|
currentTimeScale.value = scale
|
||||||
|
|
||||||
|
if (props.onTimeScaleChange && typeof props.onTimeScaleChange === 'function') {
|
||||||
|
props.onTimeScaleChange(scale)
|
||||||
|
} else {
|
||||||
|
emit('time-scale-change', scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分段控制器滑块位置
|
||||||
|
const getThumbStyle = () => {
|
||||||
|
const scaleIndex = {
|
||||||
|
[TimelineScale.MONTH]: 0,
|
||||||
|
[TimelineScale.WEEK]: 1,
|
||||||
|
[TimelineScale.DAY]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = scaleIndex[currentTimeScale.value] || 0
|
||||||
|
const translateX = index * 100 // 每个选项占33.33%,所以移动100%的倍数
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `translateX(${translateX}%)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 点击外部关闭下拉菜单
|
// 点击外部关闭下拉菜单
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
@@ -413,6 +445,36 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 右侧设置区 -->
|
<!-- 右侧设置区 -->
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
|
<!-- 时间刻度分段控制器 (Segmented) -->
|
||||||
|
<div v-if="config.showTimeScale !== false" class="segmented-control time-scale-segmented">
|
||||||
|
<div class="segmented-track">
|
||||||
|
<div class="segmented-thumb" :style="getThumbStyle()"></div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="segmented-item"
|
||||||
|
:class="{ active: currentTimeScale === 'month' }"
|
||||||
|
:title="t('timeScaleTooltip')"
|
||||||
|
@click="handleTimeScaleChange(TimelineScale.MONTH)"
|
||||||
|
>
|
||||||
|
{{ t('timeScaleMonth') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="segmented-item"
|
||||||
|
:class="{ active: currentTimeScale === 'week' }"
|
||||||
|
:title="t('timeScaleTooltip')"
|
||||||
|
@click="handleTimeScaleChange(TimelineScale.WEEK)"
|
||||||
|
>
|
||||||
|
{{ t('timeScaleWeek') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="segmented-item"
|
||||||
|
:class="{ active: currentTimeScale === 'day' }"
|
||||||
|
:title="t('timeScaleTooltip')"
|
||||||
|
@click="handleTimeScaleChange(TimelineScale.DAY)"
|
||||||
|
>
|
||||||
|
{{ t('timeScaleDay') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<!-- 语言选择下拉菜单 -->
|
<!-- 语言选择下拉菜单 -->
|
||||||
<div v-if="config.showLanguage !== false" class="language-dropdown">
|
<div v-if="config.showLanguage !== false" class="language-dropdown">
|
||||||
<button
|
<button
|
||||||
@@ -587,7 +649,6 @@ onUnmounted(() => {
|
|||||||
background: var(--gantt-bg-toolbar, #f8f9fa);
|
background: var(--gantt-bg-toolbar, #f8f9fa);
|
||||||
border-bottom: 1px solid var(--gantt-border-color, #ebeef5);
|
border-bottom: 1px solid var(--gantt-border-color, #ebeef5);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
/*transition: all 0.2s ease;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-left {
|
.toolbar-left {
|
||||||
@@ -616,7 +677,6 @@ onUnmounted(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--gantt-text-primary, #606266);
|
color: var(--gantt-text-primary, #606266);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/*transition: all 0.2s ease;*/
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +742,6 @@ onUnmounted(() => {
|
|||||||
border: 1px solid var(--gantt-border-color, #dcdfe6);
|
border: 1px solid var(--gantt-border-color, #dcdfe6);
|
||||||
color: var(--gantt-text-primary, #606266);
|
color: var(--gantt-text-primary, #606266);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/*transition: all 0.2s ease;*/
|
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -1085,9 +1144,128 @@ onUnmounted(() => {
|
|||||||
color: #e5e5e5;
|
color: #e5e5e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html[data-theme='dark']) .btn-group:not(.add-btn-group) .btn-group-item:hover {
|
/* 分段控制器样式 - Element Plus Segmented 风格 */
|
||||||
background: #404040;
|
.segmented-control {
|
||||||
border-color: #66b1ff;
|
position: relative;
|
||||||
color: #66b1ff;
|
display: inline-flex;
|
||||||
|
background: var(--gantt-bg-primary, #ffffff);
|
||||||
|
border: 1px solid var(--gantt-border-color, #dcdfe6);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px;
|
||||||
|
margin-right: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-control:hover {
|
||||||
|
border-color: var(--gantt-primary-light, #79bbff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-track {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 33.333333%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--gantt-primary, #409eff);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
min-width: 40px;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item:hover:not(.active) {
|
||||||
|
color: var(--gantt-primary, #409eff);
|
||||||
|
background: var(--gantt-bg-hover, rgba(64, 158, 255, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item:active:not(.active) {
|
||||||
|
background: var(--gantt-bg-active, rgba(64, 158, 255, 0.12));
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item.active {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-scale-segmented {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-scale-segmented .segmented-item {
|
||||||
|
height: 34px;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式下的分段控制器样式 */
|
||||||
|
:global(html[data-theme='dark']) .segmented-control {
|
||||||
|
background: var(--gantt-bg-secondary, #4b4b4b);
|
||||||
|
border-color: var(--gantt-border-color, #808080);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-control:hover {
|
||||||
|
border-color: var(--gantt-primary, #3399ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-thumb {
|
||||||
|
background: var(--gantt-primary, #3399ff);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.3),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-item {
|
||||||
|
color: #ffffff !important; /* 强制使用纯白色,更加突出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特别针对时间刻度分段控制器的暗黑模式样式 */
|
||||||
|
:global(html[data-theme='dark']) .time-scale-segmented .segmented-item {
|
||||||
|
color: #ffffff !important; /* 确保时间刻度按钮也使用纯白色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-item:hover:not(.active) {
|
||||||
|
color: var(--gantt-primary, #3399ff); /* 使用主色调,更加鲜艳 */
|
||||||
|
background: rgba(51, 153, 255, 0.12); /* 调整背景透明度,与主色调匹配 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-item:active:not(.active) {
|
||||||
|
background: rgba(51, 153, 255, 0.2); /* 调整背景透明度,与主色调匹配 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .segmented-item.active {
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onUnmounted } from 'vue'
|
import { computed, ref, onUnmounted } from 'vue'
|
||||||
import type { Milestone } from '../models/classes/Milestone'
|
import type { Milestone } from '../models/classes/Milestone'
|
||||||
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: string // 里程碑日期
|
date: string // 里程碑日期
|
||||||
@@ -22,6 +23,17 @@ interface Props {
|
|||||||
stickyPosition: 'left' | 'right' | 'none'
|
stickyPosition: 'left' | 'right' | 'none'
|
||||||
priority: number // 推挤优先级
|
priority: number // 推挤优先级
|
||||||
}> // 其他里程碑的位置信息
|
}> // 其他里程碑的位置信息
|
||||||
|
// 新增:时间线数据,用于精确计算subDays定位
|
||||||
|
timelineData?: Array<{
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
subDays?: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
monthData?: { dayCount: number }
|
||||||
|
}>
|
||||||
|
// 新增:当前时间刻度
|
||||||
|
currentTimeScale?: TimelineScale
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -89,7 +101,7 @@ const formatDateToLocalString = (date: Date): string => {
|
|||||||
return `${year}-${month}-${day}`
|
return `${year}-${month}-${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽事件处理
|
// 拖拽事件处理 - 使用相对位置拖拽方案
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
// 如果是停靠状态或被推出边界,禁止拖拽
|
// 如果是停靠状态或被推出边界,禁止拖拽
|
||||||
if (
|
if (
|
||||||
@@ -104,17 +116,44 @@ const handleMouseDown = (e: MouseEvent) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
|
// 获取当前里程碑相对位置
|
||||||
|
const timelineContainer = document.querySelector('.timeline') as HTMLElement
|
||||||
|
if (!timelineContainer) return
|
||||||
|
|
||||||
// 设置拖拽状态,但不立即开始拖拽
|
// 设置拖拽状态,但不立即开始拖拽
|
||||||
dragStartX.value = e.clientX
|
dragStartX.value = e.clientX
|
||||||
dragStartLeft.value = parseInt(milestoneStyle.value.left)
|
dragStartLeft.value = parseInt(milestoneStyle.value.left)
|
||||||
tempMilestoneData.value = null
|
tempMilestoneData.value = null
|
||||||
|
|
||||||
|
// 监听自动滚动事件
|
||||||
|
window.addEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||||
|
|
||||||
// 添加全局事件监听器
|
// 添加全局事件监听器
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理自动滚动事件
|
||||||
|
const handleAutoScroll = (event: CustomEvent) => {
|
||||||
|
const { scrollDelta } = event.detail
|
||||||
|
|
||||||
|
// 当Timeline滚动时,调整鼠标起始位置以保持相对位置
|
||||||
|
if (isDragging.value) {
|
||||||
|
dragStartX.value -= scrollDelta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
// 发送边界检测事件给Timeline
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('drag-boundary-check', {
|
||||||
|
detail: {
|
||||||
|
mouseX: e.clientX,
|
||||||
|
isDragging: isDragging.value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const deltaX = e.clientX - dragStartX.value
|
const deltaX = e.clientX - dragStartX.value
|
||||||
|
|
||||||
// 只有在真正移动了一定距离后才开始拖拽(避免意外触发)
|
// 只有在真正移动了一定距离后才开始拖拽(避免意外触发)
|
||||||
@@ -132,6 +171,16 @@ const handleMouseMove = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
|
// 停止边界检测
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('drag-boundary-check', {
|
||||||
|
detail: {
|
||||||
|
mouseX: 0,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// 只有在真正拖拽了(有临时数据)且状态为拖拽中时才触发更新
|
// 只有在真正拖拽了(有临时数据)且状态为拖拽中时才触发更新
|
||||||
if (isDragging.value && tempMilestoneData.value && props.milestone) {
|
if (isDragging.value && tempMilestoneData.value && props.milestone) {
|
||||||
const updatedMilestone = {
|
const updatedMilestone = {
|
||||||
@@ -142,6 +191,9 @@ const handleMouseUp = () => {
|
|||||||
emit('drag-end', updatedMilestone)
|
emit('drag-end', updatedMilestone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理自动滚动监听器
|
||||||
|
window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||||
|
|
||||||
// 重置所有拖拽状态
|
// 重置所有拖拽状态
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
tempMilestoneData.value = null
|
tempMilestoneData.value = null
|
||||||
@@ -209,12 +261,32 @@ const milestoneStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDiff = Math.floor(
|
let left = 0
|
||||||
(milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24),
|
|
||||||
)
|
|
||||||
const size = Math.min(props.rowHeight, props.dayWidth * 1.2, 24)
|
const size = Math.min(props.rowHeight, props.dayWidth * 1.2, 24)
|
||||||
|
|
||||||
|
// 优先使用基于timelineData的精确定位(适用于周视图和月视图)
|
||||||
|
if (
|
||||||
|
props.timelineData &&
|
||||||
|
props.currentTimeScale &&
|
||||||
|
(props.currentTimeScale === TimelineScale.WEEK ||
|
||||||
|
props.currentTimeScale === TimelineScale.MONTH)
|
||||||
|
) {
|
||||||
|
const centerPosition = calculateMilestonePositionFromTimelineData(
|
||||||
|
milestoneDate,
|
||||||
|
props.timelineData,
|
||||||
|
props.currentTimeScale,
|
||||||
|
)
|
||||||
|
left = centerPosition - size / 2 // 从中心位置偏移到图标左上角
|
||||||
|
} else {
|
||||||
|
// 日视图:保持原有逻辑
|
||||||
|
const startDiff = Math.floor(
|
||||||
|
(milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
|
)
|
||||||
|
left = startDiff * props.dayWidth + props.dayWidth / 2 - size / 2
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: `${startDiff * props.dayWidth + props.dayWidth / 2 - size / 2}px`,
|
left: `${left}px`,
|
||||||
top: `${(props.rowHeight - size) / 2}px`,
|
top: `${(props.rowHeight - size) / 2}px`,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -438,6 +510,87 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('mousemove', handleMouseMove)
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 基于timelineData和subDays精确计算里程碑位置的函数
|
||||||
|
const calculateMilestonePositionFromTimelineData = (
|
||||||
|
targetDate: Date,
|
||||||
|
timelineData: Array<{
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
subDays?: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
monthData?: { dayCount: number }
|
||||||
|
weeks?: Array<{
|
||||||
|
weekStart: Date
|
||||||
|
weekEnd: Date
|
||||||
|
subDays: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
}>
|
||||||
|
}>,
|
||||||
|
timeScale: TimelineScale,
|
||||||
|
) => {
|
||||||
|
let cumulativePosition = 0
|
||||||
|
|
||||||
|
for (const periodData of timelineData) {
|
||||||
|
if (timeScale === TimelineScale.WEEK) {
|
||||||
|
// 周视图:处理嵌套的weeks结构,返回中心位置
|
||||||
|
const weeks = periodData.weeks || []
|
||||||
|
|
||||||
|
for (const week of weeks) {
|
||||||
|
const weekStart = new Date(week.weekStart)
|
||||||
|
const weekEnd = new Date(week.weekEnd)
|
||||||
|
|
||||||
|
if (targetDate >= weekStart && targetDate <= weekEnd) {
|
||||||
|
// 找到目标日期所在的周
|
||||||
|
const weekWidth = 60
|
||||||
|
const subDays = week.subDays || []
|
||||||
|
const dayWidth = weekWidth / 7
|
||||||
|
|
||||||
|
// 在subDays中查找目标日期的位置
|
||||||
|
for (let i = 0; i < subDays.length; i++) {
|
||||||
|
const subDay = subDays[i]
|
||||||
|
const subDayDate = new Date(subDay.date)
|
||||||
|
// 比较日期(忽略时分秒)
|
||||||
|
if (
|
||||||
|
subDayDate.getFullYear() === targetDate.getFullYear() &&
|
||||||
|
subDayDate.getMonth() === targetDate.getMonth() &&
|
||||||
|
subDayDate.getDate() === targetDate.getDate()
|
||||||
|
) {
|
||||||
|
return cumulativePosition + i * dayWidth + dayWidth / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到精确匹配,回退到dayOfWeek计算
|
||||||
|
const dayOfWeek = targetDate.getDay()
|
||||||
|
return cumulativePosition + dayOfWeek * dayWidth + dayWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累加每周的宽度
|
||||||
|
cumulativePosition += 60
|
||||||
|
}
|
||||||
|
} else if (timeScale === TimelineScale.MONTH) {
|
||||||
|
// 月视图:处理扁平化的subDays结构,返回中心位置
|
||||||
|
const periodStart = new Date(periodData.startDate)
|
||||||
|
const periodEnd = new Date(periodData.endDate)
|
||||||
|
|
||||||
|
if (targetDate >= periodStart && targetDate <= periodEnd) {
|
||||||
|
// 找到目标日期所在的时间段
|
||||||
|
const monthWidth = 60
|
||||||
|
const daysInMonth = periodData.monthData?.dayCount || 30
|
||||||
|
const dayWidth = monthWidth / daysInMonth
|
||||||
|
const dayInMonth = targetDate.getDate()
|
||||||
|
return cumulativePosition + (dayInMonth - 1) * dayWidth + dayWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累加每月的宽度
|
||||||
|
cumulativePosition += 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cumulativePosition // 如果没找到,返回累计位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...existing code...
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
+341
-7
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
task: Task
|
task: Task
|
||||||
@@ -14,6 +15,17 @@ interface Props {
|
|||||||
containerWidth?: number
|
containerWidth?: number
|
||||||
// 新增:外部控制半圆隐藏状态(用于Timeline初始化等场景)
|
// 新增:外部控制半圆隐藏状态(用于Timeline初始化等场景)
|
||||||
hideBubbles?: boolean
|
hideBubbles?: boolean
|
||||||
|
// 新增:时间线数据,用于精确计算subDays定位
|
||||||
|
timelineData?: Array<{
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
subDays?: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
monthData?: { dayCount: number }
|
||||||
|
}>
|
||||||
|
// 新增:当前时间刻度
|
||||||
|
currentTimeScale?: TimelineScale
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -70,6 +82,9 @@ const resizeStartX = ref(0)
|
|||||||
const resizeStartWidth = ref(0)
|
const resizeStartWidth = ref(0)
|
||||||
const resizeStartLeft = ref(0)
|
const resizeStartLeft = ref(0)
|
||||||
|
|
||||||
|
// 相对位置拖拽方案:记录鼠标相对于TaskBar的位置
|
||||||
|
const mouseOffsetX = ref(0) // 鼠标在TaskBar内的相对位置
|
||||||
|
|
||||||
// 缓存拖拽/拉伸过程中的临时数据,只在鼠标抬起时提交更新
|
// 缓存拖拽/拉伸过程中的临时数据,只在鼠标抬起时提交更新
|
||||||
const tempTaskData = ref<{
|
const tempTaskData = ref<{
|
||||||
startDate?: string
|
startDate?: string
|
||||||
@@ -94,12 +109,60 @@ const taskBarStyle = computed(() => {
|
|||||||
top: '4px',
|
top: '4px',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const startDiff = Math.floor((startDate.getTime() - baseStart.getTime()) / (1000 * 60 * 60 * 24))
|
|
||||||
const duration = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1
|
let left = 0
|
||||||
|
let width = 0
|
||||||
|
|
||||||
|
// 优先使用基于timelineData的精确定位(适用于周视图和月视图)
|
||||||
|
if (
|
||||||
|
props.timelineData &&
|
||||||
|
props.currentTimeScale &&
|
||||||
|
(props.currentTimeScale === TimelineScale.WEEK ||
|
||||||
|
props.currentTimeScale === TimelineScale.MONTH)
|
||||||
|
) {
|
||||||
|
// 计算开始位置
|
||||||
|
const startPosition = calculatePositionFromTimelineData(
|
||||||
|
startDate,
|
||||||
|
props.timelineData,
|
||||||
|
props.currentTimeScale,
|
||||||
|
)
|
||||||
|
// 计算结束位置:为结束日期添加一天来获取正确的结束位置
|
||||||
|
const nextDay = new Date(endDate)
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1)
|
||||||
|
let endPosition = calculatePositionFromTimelineData(
|
||||||
|
nextDay,
|
||||||
|
props.timelineData,
|
||||||
|
props.currentTimeScale,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果结束日期+1天超出范围,使用结束日期的位置+一天的宽度
|
||||||
|
if (endPosition === startPosition) {
|
||||||
|
const dayWidth = props.currentTimeScale === TimelineScale.WEEK ? 60 / 7 : 60 / 30
|
||||||
|
endPosition =
|
||||||
|
calculatePositionFromTimelineData(endDate, props.timelineData, props.currentTimeScale) +
|
||||||
|
dayWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
left = startPosition
|
||||||
|
width = Math.max(endPosition - startPosition, 4) // 确保最小4px宽度
|
||||||
|
} else {
|
||||||
|
// 日视图:保持原有逻辑
|
||||||
|
const startDiff = Math.floor(
|
||||||
|
(startDate.getTime() - baseStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
|
)
|
||||||
|
// 重新计算duration,确保包含结束日期当天
|
||||||
|
const timeDiffMs = endDate.getTime() - startDate.getTime()
|
||||||
|
const daysDiff = timeDiffMs / (1000 * 60 * 60 * 24)
|
||||||
|
// 对于跨天的任务,需要包含开始和结束两天
|
||||||
|
const duration = Math.floor(daysDiff) + 1
|
||||||
|
|
||||||
|
left = startDiff * props.dayWidth
|
||||||
|
width = duration * props.dayWidth
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: `${startDiff * props.dayWidth}px`,
|
left: `${left}px`,
|
||||||
width: `${duration * props.dayWidth}px`,
|
width: `${width}px`,
|
||||||
height: `${props.rowHeight - 10}px`,
|
height: `${props.rowHeight - 10}px`,
|
||||||
top: '4px',
|
top: '4px',
|
||||||
}
|
}
|
||||||
@@ -172,7 +235,19 @@ const progressWidth = computed(() => {
|
|||||||
return `${(progress / 100) * totalWidth}px`
|
return `${(progress / 100) * totalWidth}px`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 鼠标事件处理
|
// 判断是否为周视图(dayWidth小于等于9为周视图)
|
||||||
|
const isWeekView = computed(() => props.dayWidth <= 9)
|
||||||
|
|
||||||
|
// 判断是否为短TaskBar(宽度小于80px)
|
||||||
|
const isShortTaskBar = computed(() => {
|
||||||
|
const width = parseFloat(taskBarStyle.value.width || '0')
|
||||||
|
return width < 80
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否需要溢出效果(周视图且短TaskBar)
|
||||||
|
const needsOverflowEffect = computed(() => isWeekView.value && isShortTaskBar.value)
|
||||||
|
|
||||||
|
// 鼠标事件处理 - 使用相对位置拖拽方案
|
||||||
const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-right') => {
|
const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-right') => {
|
||||||
// 如果已完成或是父级任务,禁用所有交互
|
// 如果已完成或是父级任务,禁用所有交互
|
||||||
if (isCompleted.value || props.isParent) {
|
if (isCompleted.value || props.isParent) {
|
||||||
@@ -185,6 +260,15 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
|
|||||||
// 清空之前的临时数据
|
// 清空之前的临时数据
|
||||||
tempTaskData.value = null
|
tempTaskData.value = null
|
||||||
|
|
||||||
|
// 获取TaskBar相对于Timeline容器的位置
|
||||||
|
const timelineContainer = document.querySelector('.timeline') as HTMLElement
|
||||||
|
if (!timelineContainer || !barRef.value) return
|
||||||
|
|
||||||
|
const barRect = barRef.value.getBoundingClientRect()
|
||||||
|
|
||||||
|
// 计算鼠标相对于TaskBar的位置
|
||||||
|
mouseOffsetX.value = e.clientX - barRect.left
|
||||||
|
|
||||||
if (type === 'drag') {
|
if (type === 'drag') {
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
dragStartX.value = e.clientX
|
dragStartX.value = e.clientX
|
||||||
@@ -202,10 +286,25 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
|
|||||||
resizeStartLeft.value = parseInt(taskBarStyle.value.left)
|
resizeStartLeft.value = parseInt(taskBarStyle.value.left)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听自动滚动事件
|
||||||
|
window.addEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理自动滚动事件
|
||||||
|
const handleAutoScroll = (event: CustomEvent) => {
|
||||||
|
const { scrollDelta } = event.detail
|
||||||
|
|
||||||
|
// 当Timeline滚动时,调整鼠标起始位置以保持相对位置
|
||||||
|
if (isDragging.value) {
|
||||||
|
dragStartX.value -= scrollDelta
|
||||||
|
} else if (isResizingLeft.value || isResizingRight.value) {
|
||||||
|
resizeStartX.value -= scrollDelta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reportBarPosition() {
|
function reportBarPosition() {
|
||||||
if (barRef.value) {
|
if (barRef.value) {
|
||||||
const rect = barRef.value.getBoundingClientRect()
|
const rect = barRef.value.getBoundingClientRect()
|
||||||
@@ -220,6 +319,16 @@ function reportBarPosition() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
// 发送边界检测事件给Timeline
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('drag-boundary-check', {
|
||||||
|
detail: {
|
||||||
|
mouseX: e.clientX,
|
||||||
|
isDragging: isDragging.value || isResizingLeft.value || isResizingRight.value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if (isDragging.value) {
|
if (isDragging.value) {
|
||||||
const deltaX = e.clientX - dragStartX.value
|
const deltaX = e.clientX - dragStartX.value
|
||||||
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
|
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
|
||||||
@@ -260,6 +369,16 @@ const handleMouseMove = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
|
// 停止边界检测
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('drag-boundary-check', {
|
||||||
|
detail: {
|
||||||
|
mouseX: 0,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// 如果有临时数据,说明发生了拖拽或拉伸,提交数据更新
|
// 如果有临时数据,说明发生了拖拽或拉伸,提交数据更新
|
||||||
if (tempTaskData.value) {
|
if (tempTaskData.value) {
|
||||||
const updatedTask = {
|
const updatedTask = {
|
||||||
@@ -284,6 +403,9 @@ const handleMouseUp = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理自动滚动监听器
|
||||||
|
window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||||
|
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
isResizingLeft.value = false
|
isResizingLeft.value = false
|
||||||
isResizingRight.value = false
|
isResizingRight.value = false
|
||||||
@@ -295,6 +417,32 @@ onMounted(() => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
reportBarPosition()
|
reportBarPosition()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听时间刻度变化事件,重新计算位置
|
||||||
|
const handleTimelineScaleUpdate = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
reportBarPosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听强制重新计算事件
|
||||||
|
const handleForceRecalculate = () => {
|
||||||
|
// 延迟稍长一点,确保DOM完全更新
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reportBarPosition()
|
||||||
|
}, 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||||
|
window.addEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||||
|
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听任务数据变化,重新报告位置
|
// 监听任务数据变化,重新报告位置
|
||||||
@@ -709,6 +857,12 @@ const workHourInfo = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 判断是否应该显示进度百分比
|
||||||
|
const shouldShowProgress = computed(() => {
|
||||||
|
// 任何情况下都显示完成率(包括0%和undefined),确保始终展示
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
// Helper functions to create type-safe style objects
|
// Helper functions to create type-safe style objects
|
||||||
const getNameStyles = () => {
|
const getNameStyles = () => {
|
||||||
const styles = stickyStyles.value
|
const styles = stickyStyles.value
|
||||||
@@ -731,6 +885,87 @@ const getProgressStyles = () => {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 基于timelineData和subDays精确计算日期位置的函数
|
||||||
|
const calculatePositionFromTimelineData = (
|
||||||
|
targetDate: Date,
|
||||||
|
timelineData: Array<{
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
subDays?: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
monthData?: { dayCount: number }
|
||||||
|
weeks?: Array<{
|
||||||
|
weekStart: Date
|
||||||
|
weekEnd: Date
|
||||||
|
subDays: Array<{ date: Date; dayOfWeek?: number }>
|
||||||
|
}>
|
||||||
|
}>,
|
||||||
|
timeScale: TimelineScale,
|
||||||
|
) => {
|
||||||
|
let cumulativePosition = 0
|
||||||
|
|
||||||
|
for (const periodData of timelineData) {
|
||||||
|
if (timeScale === TimelineScale.WEEK) {
|
||||||
|
// 周视图:处理嵌套的weeks结构
|
||||||
|
const weeks = periodData.weeks || []
|
||||||
|
|
||||||
|
for (const week of weeks) {
|
||||||
|
const weekStart = new Date(week.weekStart)
|
||||||
|
const weekEnd = new Date(week.weekEnd)
|
||||||
|
|
||||||
|
if (targetDate >= weekStart && targetDate <= weekEnd) {
|
||||||
|
// 找到目标日期所在的周
|
||||||
|
const weekWidth = 60
|
||||||
|
const subDays = week.subDays || []
|
||||||
|
const dayWidth = weekWidth / 7
|
||||||
|
|
||||||
|
// 在subDays中查找目标日期的位置
|
||||||
|
for (let i = 0; i < subDays.length; i++) {
|
||||||
|
const subDay = subDays[i]
|
||||||
|
const subDayDate = new Date(subDay.date)
|
||||||
|
// 比较日期(忽略时分秒)
|
||||||
|
if (
|
||||||
|
subDayDate.getFullYear() === targetDate.getFullYear() &&
|
||||||
|
subDayDate.getMonth() === targetDate.getMonth() &&
|
||||||
|
subDayDate.getDate() === targetDate.getDate()
|
||||||
|
) {
|
||||||
|
return cumulativePosition + i * dayWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到精确匹配,回退到dayOfWeek计算
|
||||||
|
const dayOfWeek = targetDate.getDay()
|
||||||
|
return cumulativePosition + dayOfWeek * dayWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累加每周的宽度
|
||||||
|
cumulativePosition += 60
|
||||||
|
}
|
||||||
|
} else if (timeScale === TimelineScale.MONTH) {
|
||||||
|
// 月视图:处理扁平化的subDays结构
|
||||||
|
const periodStart = new Date(periodData.startDate)
|
||||||
|
const periodEnd = new Date(periodData.endDate)
|
||||||
|
|
||||||
|
if (targetDate >= periodStart && targetDate <= periodEnd) {
|
||||||
|
// 找到目标日期所在的时间段
|
||||||
|
const monthWidth = 60
|
||||||
|
const daysInMonth = periodData.monthData?.dayCount || 30
|
||||||
|
const dayWidth = monthWidth / daysInMonth
|
||||||
|
const dayInMonth = targetDate.getDate()
|
||||||
|
return cumulativePosition + (dayInMonth - 1) * dayWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累加每月的宽度
|
||||||
|
cumulativePosition += 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cumulativePosition // 如果没找到,返回累计位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...existing code...
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -750,6 +985,9 @@ const getProgressStyles = () => {
|
|||||||
resizing: isResizingLeft || isResizingRight,
|
resizing: isResizingLeft || isResizingRight,
|
||||||
completed: isCompleted,
|
completed: isCompleted,
|
||||||
'parent-task': isParent,
|
'parent-task': isParent,
|
||||||
|
'week-view': isWeekView,
|
||||||
|
'short-task-bar': isShortTaskBar,
|
||||||
|
'overflow-effect': needsOverflowEffect,
|
||||||
}"
|
}"
|
||||||
@dblclick="handleTaskBarDoubleClick"
|
@dblclick="handleTaskBarDoubleClick"
|
||||||
>
|
>
|
||||||
@@ -781,8 +1019,8 @@ const getProgressStyles = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 进度百分比 -->
|
<!-- 进度百分比 -->
|
||||||
<div v-if="task.progress !== undefined" class="task-progress" :style="getProgressStyles()">
|
<div v-if="shouldShowProgress" class="task-progress" :style="getProgressStyles()">
|
||||||
{{ task.progress }}%
|
{{ task.progress || 0 }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1009,6 +1247,26 @@ const getProgressStyles = () => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 溢出效果下的拉伸handle优化 */
|
||||||
|
.task-bar.overflow-effect .resize-handle {
|
||||||
|
z-index: 20; /* 确保handle在溢出内容之上 */
|
||||||
|
background: rgba(0, 0, 0, 0.15); /* 稍微加深以提高可见性 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-bar.overflow-effect .resize-handle:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
width: 8px; /* 悬停时稍微加宽 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 溢出模式下左右handle的位置调整 */
|
||||||
|
.task-bar.overflow-effect .resize-handle-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-bar.overflow-effect .resize-handle-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* === 半圆气泡指示器样式 === */
|
/* === 半圆气泡指示器样式 === */
|
||||||
.bubble-indicator {
|
.bubble-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1349,4 +1607,80 @@ const getProgressStyles = () => {
|
|||||||
:global(html[data-theme='dark']) .resize-handle:hover {
|
:global(html[data-theme='dark']) .resize-handle:hover {
|
||||||
background: rgba(255, 255, 255, 0.3) !important;
|
background: rgba(255, 255, 255, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 周视图下的短TaskBar样式优化 */
|
||||||
|
.task-bar.week-view.short-task-bar {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 周视图下短TaskBar的内容溢出效果 */
|
||||||
|
.task-bar.overflow-effect .task-bar-content {
|
||||||
|
/* 保持与日视图一致的布局 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
/* 确保主体内容区域仍可拖拽 */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-bar.overflow-effect .task-name {
|
||||||
|
/* 保持与日视图一致的样式 */
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
z-index: 15;
|
||||||
|
pointer-events: none;
|
||||||
|
/* 允许溢出显示,但保持原始样式 */
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 周视图下进度百分比保持与日视图一致 */
|
||||||
|
.task-bar.overflow-effect .task-progress {
|
||||||
|
/* 保持与日视图一致的基础样式 */
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
z-index: 16;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 1px 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 周视图下的TaskBar基础样式调整 */
|
||||||
|
.task-bar.week-view {
|
||||||
|
min-width: 4px; /* 确保即使很短的任务也有最小可见宽度 */
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的短TaskBar溢出效果 */
|
||||||
|
:global(html[data-theme='dark']) .task-bar.overflow-effect .resize-handle {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .task-bar.overflow-effect .resize-handle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的进度百分比样式 */
|
||||||
|
:global(html[data-theme='dark']) .task-bar.overflow-effect .task-progress {
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .task-bar.week-view {
|
||||||
|
border-color: var(--gantt-border-light, #555555);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+839
-65
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@ const messages = {
|
|||||||
// 日期格式
|
// 日期格式
|
||||||
yearMonthFormat: (year: number, month: number) =>
|
yearMonthFormat: (year: number, month: number) =>
|
||||||
`${year}年${String(month).padStart(2, '0')}月`,
|
`${year}年${String(month).padStart(2, '0')}月`,
|
||||||
|
// 月份格式
|
||||||
|
monthFormat: (month: number) => `${month}月`,
|
||||||
|
|
||||||
// 其他
|
// 其他
|
||||||
milestone: '里程碑',
|
milestone: '里程碑',
|
||||||
@@ -71,6 +73,11 @@ const messages = {
|
|||||||
exitFullscreen: '退出全屏',
|
exitFullscreen: '退出全屏',
|
||||||
githubDocs: '查看Github文档',
|
githubDocs: '查看Github文档',
|
||||||
giteeDocs: '查看Gitee文档',
|
giteeDocs: '查看Gitee文档',
|
||||||
|
// 时间刻度按钮
|
||||||
|
timeScaleMonth: '月',
|
||||||
|
timeScaleWeek: '周',
|
||||||
|
timeScaleDay: '日',
|
||||||
|
timeScaleTooltip: '切换时间刻度',
|
||||||
// 确认对话框
|
// 确认对话框
|
||||||
confirmDialogMessage: '是否需要保留该设置?',
|
confirmDialogMessage: '是否需要保留该设置?',
|
||||||
// 新建任务对话框
|
// 新建任务对话框
|
||||||
@@ -178,6 +185,8 @@ const messages = {
|
|||||||
|
|
||||||
// 日期格式
|
// 日期格式
|
||||||
yearMonthFormat: (year: number, month: number) => `${year}/${String(month).padStart(2, '0')}`,
|
yearMonthFormat: (year: number, month: number) => `${year}/${String(month).padStart(2, '0')}`,
|
||||||
|
// 月份格式
|
||||||
|
monthFormat: (month: number) => `M${String(month).padStart(2, '0')}`,
|
||||||
|
|
||||||
// 其他
|
// 其他
|
||||||
milestone: 'Milestone',
|
milestone: 'Milestone',
|
||||||
@@ -215,6 +224,11 @@ const messages = {
|
|||||||
exitFullscreen: 'Exit Fullscreen',
|
exitFullscreen: 'Exit Fullscreen',
|
||||||
githubDocs: 'GitHub Docs',
|
githubDocs: 'GitHub Docs',
|
||||||
giteeDocs: 'Gitee Docs',
|
giteeDocs: 'Gitee Docs',
|
||||||
|
// 时间刻度按钮
|
||||||
|
timeScaleMonth: 'Month',
|
||||||
|
timeScaleWeek: 'Week',
|
||||||
|
timeScaleDay: 'Day',
|
||||||
|
timeScaleTooltip: 'Switch Time Scale',
|
||||||
// Confirm dialog
|
// Confirm dialog
|
||||||
confirmDialogMessage: 'Do you want to save this setting?',
|
confirmDialogMessage: 'Do you want to save this setting?',
|
||||||
taskNamePlaceholder: 'Enter task name',
|
taskNamePlaceholder: 'Enter task name',
|
||||||
@@ -363,6 +377,11 @@ export function useI18n() {
|
|||||||
return t.value.yearMonthFormat(year, month)
|
return t.value.yearMonthFormat(year, month)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化月份
|
||||||
|
const formatMonth = (month: number) => {
|
||||||
|
return t.value.monthFormat(month)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
t,
|
t,
|
||||||
getTranslation,
|
getTranslation,
|
||||||
@@ -370,6 +389,7 @@ export function useI18n() {
|
|||||||
locale,
|
locale,
|
||||||
setLocale,
|
setLocale,
|
||||||
formatYearMonth,
|
formatYearMonth,
|
||||||
|
formatMonth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export interface ToolbarConfig {
|
|||||||
showLanguage?: boolean
|
showLanguage?: boolean
|
||||||
showTheme?: boolean
|
showTheme?: boolean
|
||||||
showFullscreen?: boolean
|
showFullscreen?: boolean
|
||||||
|
showTimeScale?: boolean // 显示时间刻度按钮组
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// 时间轴比例类型定义
|
||||||
|
|
||||||
|
// 使用字符串字面量类型代替enum,兼容erasableSyntaxOnly
|
||||||
|
export type TimelineScale = 'day' | 'week' | 'month'
|
||||||
|
|
||||||
|
// 导出常量值以便于使用
|
||||||
|
export const TimelineScale = {
|
||||||
|
DAY: 'day' as TimelineScale, // 日视图 - 每列显示一天
|
||||||
|
WEEK: 'week' as TimelineScale, // 周视图 - 每列显示一周
|
||||||
|
MONTH: 'month' as TimelineScale, // 月视图 - 每列显示一个月
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineScaleConfig {
|
||||||
|
scale: TimelineScale
|
||||||
|
cellWidth: number // 每个时间单元的宽度(px)
|
||||||
|
headerLevels: number // 表头层级数
|
||||||
|
formatters: {
|
||||||
|
primary: string // 主要时间标签格式
|
||||||
|
secondary?: string // 次要时间标签格式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预设配置
|
||||||
|
export const SCALE_CONFIGS = {
|
||||||
|
day: {
|
||||||
|
scale: TimelineScale.DAY,
|
||||||
|
cellWidth: 30,
|
||||||
|
headerLevels: 2,
|
||||||
|
formatters: { primary: 'yyyy年MM月', secondary: 'dd' },
|
||||||
|
},
|
||||||
|
week: {
|
||||||
|
scale: TimelineScale.WEEK,
|
||||||
|
cellWidth: 120,
|
||||||
|
headerLevels: 2,
|
||||||
|
formatters: { primary: 'yyyy年MM月', secondary: 'W周' },
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
scale: TimelineScale.MONTH,
|
||||||
|
cellWidth: 180,
|
||||||
|
headerLevels: 2,
|
||||||
|
formatters: { primary: 'yyyy年', secondary: 'MM月' },
|
||||||
|
},
|
||||||
|
} as Record<TimelineScale, TimelineScaleConfig>
|
||||||
|
|
||||||
|
// 时间单元数据接口
|
||||||
|
export interface TimelineUnit {
|
||||||
|
id: string
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
label: string
|
||||||
|
isToday?: boolean
|
||||||
|
isWeekend?: boolean
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间轴数据结构
|
||||||
|
export interface TimelineData {
|
||||||
|
scale: TimelineScale
|
||||||
|
units: TimelineUnit[]
|
||||||
|
months?: Array<{
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
yearMonthLabel: string
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
units: TimelineUnit[]
|
||||||
|
}>
|
||||||
|
weeks?: Array<{
|
||||||
|
weekNumber: number
|
||||||
|
year: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
units: TimelineUnit[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user