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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
||||
+3
-1
@@ -188,6 +188,7 @@ interface ToolbarConfig {
|
||||
showLanguage?: boolean // Show language switch button
|
||||
showTheme?: boolean // Show theme switch 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,
|
||||
showExportCsv: true,
|
||||
showExportPdf: true,
|
||||
showFullscreen: true
|
||||
showFullscreen: true,
|
||||
showTimeScale: true // Control visibility of Day|Week|Month time scale toggle buttons
|
||||
}
|
||||
|
||||
// Custom locale messages
|
||||
|
||||
@@ -189,6 +189,7 @@ interface ToolbarConfig {
|
||||
showLanguage?: boolean // 是否显示语言切换按钮
|
||||
showTheme?: boolean // 是否显示主题切换按钮
|
||||
showFullscreen?: boolean // 是否显示全屏切换按钮
|
||||
showTimeScale?: boolean // 是否显示时间刻度切换按钮组(日|周|月)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -328,7 +329,8 @@ const toolbarConfig = {
|
||||
showTodayLocate: true,
|
||||
showExportCsv: true,
|
||||
showExportPdf: true,
|
||||
showFullscreen: true
|
||||
showFullscreen: true,
|
||||
showTimeScale: true // 控制日|周|月时间刻度按钮组的可见性
|
||||
}
|
||||
|
||||
// 自定义多语言配置
|
||||
|
||||
@@ -41,6 +41,7 @@ const toolbarConfig = {
|
||||
showLanguage: true,
|
||||
showTheme: true,
|
||||
showFullscreen: true,
|
||||
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
|
||||
}
|
||||
|
||||
// 自定义CSV导出处理器(可选)
|
||||
|
||||
@@ -135,5 +135,10 @@
|
||||
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
||||
"增强TaskBar可以存在多个前置任务"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"date": "2025-07-08",
|
||||
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
|
||||
}
|
||||
]
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.0.10",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
@@ -9,6 +9,7 @@ import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
import { useMessage } from '../composables/useMessage'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -91,6 +92,9 @@ const leftPanelWidth = ref(320)
|
||||
// Timeline组件的引用
|
||||
const timelineRef = ref<InstanceType<typeof Timeline> | null>(null)
|
||||
|
||||
// 时间刻度状态
|
||||
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||
|
||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||
// 列宽: 300+120+120+140+140+100+100+100 = 1120px
|
||||
// 边框: 7个列间边框 * 1px = 7px
|
||||
@@ -615,9 +619,18 @@ const timelineDateRange = computed(() => {
|
||||
const minDate = new Date(Math.min(...startDates.map(d => d.getTime())))
|
||||
const maxDate = new Date(Math.max(...endDates.map(d => d.getTime())))
|
||||
|
||||
// 前后各延伸6个月
|
||||
const min = new Date(minDate.getFullYear(), minDate.getMonth() - 6, 1)
|
||||
const max = new Date(maxDate.getFullYear(), maxDate.getMonth() + 6 + 1, 0)
|
||||
// 日视图前后各延伸6个月
|
||||
let min = new Date(minDate.getFullYear(), minDate.getMonth() - 6, 1)
|
||||
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 }
|
||||
})
|
||||
|
||||
@@ -638,6 +651,25 @@ const csvExportHandler = () => {
|
||||
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导出功能
|
||||
const defaultExportCsv = () => {
|
||||
try {
|
||||
@@ -1156,6 +1188,7 @@ watch(
|
||||
:on-language-change="props.onLanguageChange"
|
||||
:on-theme-change="props.onThemeChange"
|
||||
:on-fullscreen-change="props.onFullscreenChange"
|
||||
:on-time-scale-change="handleTimeScaleChange"
|
||||
/>
|
||||
|
||||
<!-- 甘特图主体 -->
|
||||
@@ -1212,6 +1245,7 @@ watch(
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
:on-task-delete="props.onTaskDelete"
|
||||
:on-milestone-save="handleMilestoneSave"
|
||||
@timeline-scale-changed="handleTimelineScaleChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
import '../styles/app.css'
|
||||
|
||||
// 语言定义 - 使用多语言系统的类型
|
||||
@@ -18,6 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
onThemeChange: undefined,
|
||||
onFullscreenChange: undefined,
|
||||
onSettingsConfirm: undefined,
|
||||
onTimeScaleChange: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -29,6 +31,7 @@ const emit = defineEmits<{
|
||||
'language-change': [lang: 'zh-CN' | 'en-US']
|
||||
'theme-change': [isDark: boolean]
|
||||
'fullscreen-change': [isFullscreen: boolean]
|
||||
'time-scale-change': [scale: TimelineScale]
|
||||
}>()
|
||||
|
||||
// LocalStorage keys
|
||||
@@ -52,6 +55,7 @@ interface Props {
|
||||
onLanguageChange?: (lang: 'zh-CN' | 'en-US') => void
|
||||
onThemeChange?: (isDark: boolean) => void
|
||||
onFullscreenChange?: (isFullscreen: boolean) => void
|
||||
onTimeScaleChange?: (scale: TimelineScale) => void
|
||||
// 外部确认接口
|
||||
onSettingsConfirm?: (
|
||||
type: 'theme' | 'language',
|
||||
@@ -78,6 +82,7 @@ const currentLanguage = ref<Language>('zh')
|
||||
const isDarkMode = ref(getInitialTheme())
|
||||
const isFullscreen = ref(false)
|
||||
const showLanguageDropdown = ref(false)
|
||||
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||
|
||||
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
|
||||
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 target = event.target as HTMLElement
|
||||
@@ -413,6 +445,36 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 右侧设置区 -->
|
||||
<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">
|
||||
<button
|
||||
@@ -587,7 +649,6 @@ onUnmounted(() => {
|
||||
background: var(--gantt-bg-toolbar, #f8f9fa);
|
||||
border-bottom: 1px solid var(--gantt-border-color, #ebeef5);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
/*transition: all 0.2s ease;*/
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
@@ -616,7 +677,6 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
color: var(--gantt-text-primary, #606266);
|
||||
cursor: pointer;
|
||||
/*transition: all 0.2s ease;*/
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -682,7 +742,6 @@ onUnmounted(() => {
|
||||
border: 1px solid var(--gantt-border-color, #dcdfe6);
|
||||
color: var(--gantt-text-primary, #606266);
|
||||
cursor: pointer;
|
||||
/*transition: all 0.2s ease;*/
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
@@ -1085,9 +1144,128 @@ onUnmounted(() => {
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .btn-group:not(.add-btn-group) .btn-group-item:hover {
|
||||
background: #404040;
|
||||
border-color: #66b1ff;
|
||||
color: #66b1ff;
|
||||
/* 分段控制器样式 - Element Plus Segmented 风格 */
|
||||
.segmented-control {
|
||||
position: relative;
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onUnmounted } from 'vue'
|
||||
import type { Milestone } from '../models/classes/Milestone'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
|
||||
interface Props {
|
||||
date: string // 里程碑日期
|
||||
@@ -22,6 +23,17 @@ interface Props {
|
||||
stickyPosition: 'left' | 'right' | 'none'
|
||||
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>()
|
||||
@@ -89,7 +101,7 @@ const formatDateToLocalString = (date: Date): string => {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 拖拽事件处理
|
||||
// 拖拽事件处理 - 使用相对位置拖拽方案
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 如果是停靠状态或被推出边界,禁止拖拽
|
||||
if (
|
||||
@@ -104,17 +116,44 @@ const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// 获取当前里程碑相对位置
|
||||
const timelineContainer = document.querySelector('.timeline') as HTMLElement
|
||||
if (!timelineContainer) return
|
||||
|
||||
// 设置拖拽状态,但不立即开始拖拽
|
||||
dragStartX.value = e.clientX
|
||||
dragStartLeft.value = parseInt(milestoneStyle.value.left)
|
||||
tempMilestoneData.value = null
|
||||
|
||||
// 监听自动滚动事件
|
||||
window.addEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||
|
||||
// 添加全局事件监听器
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
// 处理自动滚动事件
|
||||
const handleAutoScroll = (event: CustomEvent) => {
|
||||
const { scrollDelta } = event.detail
|
||||
|
||||
// 当Timeline滚动时,调整鼠标起始位置以保持相对位置
|
||||
if (isDragging.value) {
|
||||
dragStartX.value -= scrollDelta
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 只有在真正移动了一定距离后才开始拖拽(避免意外触发)
|
||||
@@ -132,6 +171,16 @@ const handleMouseMove = (e: MouseEvent) => {
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// 停止边界检测
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('drag-boundary-check', {
|
||||
detail: {
|
||||
mouseX: 0,
|
||||
isDragging: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 只有在真正拖拽了(有临时数据)且状态为拖拽中时才触发更新
|
||||
if (isDragging.value && tempMilestoneData.value && props.milestone) {
|
||||
const updatedMilestone = {
|
||||
@@ -142,6 +191,9 @@ const handleMouseUp = () => {
|
||||
emit('drag-end', updatedMilestone)
|
||||
}
|
||||
|
||||
// 清理自动滚动监听器
|
||||
window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||
|
||||
// 重置所有拖拽状态
|
||||
isDragging.value = false
|
||||
tempMilestoneData.value = null
|
||||
@@ -209,12 +261,32 @@ const milestoneStyle = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
const startDiff = Math.floor(
|
||||
(milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
let left = 0
|
||||
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 {
|
||||
left: `${startDiff * props.dayWidth + props.dayWidth / 2 - size / 2}px`,
|
||||
left: `${left}px`,
|
||||
top: `${(props.rowHeight - size) / 2}px`,
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
@@ -438,6 +510,87 @@ onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
+341
-7
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
|
||||
interface Props {
|
||||
task: Task
|
||||
@@ -14,6 +15,17 @@ interface Props {
|
||||
containerWidth?: number
|
||||
// 新增:外部控制半圆隐藏状态(用于Timeline初始化等场景)
|
||||
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>()
|
||||
@@ -70,6 +82,9 @@ const resizeStartX = ref(0)
|
||||
const resizeStartWidth = ref(0)
|
||||
const resizeStartLeft = ref(0)
|
||||
|
||||
// 相对位置拖拽方案:记录鼠标相对于TaskBar的位置
|
||||
const mouseOffsetX = ref(0) // 鼠标在TaskBar内的相对位置
|
||||
|
||||
// 缓存拖拽/拉伸过程中的临时数据,只在鼠标抬起时提交更新
|
||||
const tempTaskData = ref<{
|
||||
startDate?: string
|
||||
@@ -94,12 +109,60 @@ const taskBarStyle = computed(() => {
|
||||
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 {
|
||||
left: `${startDiff * props.dayWidth}px`,
|
||||
width: `${duration * props.dayWidth}px`,
|
||||
left: `${left}px`,
|
||||
width: `${width}px`,
|
||||
height: `${props.rowHeight - 10}px`,
|
||||
top: '4px',
|
||||
}
|
||||
@@ -172,7 +235,19 @@ const progressWidth = computed(() => {
|
||||
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') => {
|
||||
// 如果已完成或是父级任务,禁用所有交互
|
||||
if (isCompleted.value || props.isParent) {
|
||||
@@ -185,6 +260,15 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
|
||||
// 清空之前的临时数据
|
||||
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') {
|
||||
isDragging.value = true
|
||||
dragStartX.value = e.clientX
|
||||
@@ -202,10 +286,25 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
|
||||
resizeStartLeft.value = parseInt(taskBarStyle.value.left)
|
||||
}
|
||||
|
||||
// 监听自动滚动事件
|
||||
window.addEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
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() {
|
||||
if (barRef.value) {
|
||||
const rect = barRef.value.getBoundingClientRect()
|
||||
@@ -220,6 +319,16 @@ function reportBarPosition() {
|
||||
}
|
||||
|
||||
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) {
|
||||
const deltaX = e.clientX - dragStartX.value
|
||||
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
|
||||
@@ -260,6 +369,16 @@ const handleMouseMove = (e: MouseEvent) => {
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// 停止边界检测
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('drag-boundary-check', {
|
||||
detail: {
|
||||
mouseX: 0,
|
||||
isDragging: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 如果有临时数据,说明发生了拖拽或拉伸,提交数据更新
|
||||
if (tempTaskData.value) {
|
||||
const updatedTask = {
|
||||
@@ -284,6 +403,9 @@ const handleMouseUp = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 清理自动滚动监听器
|
||||
window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener)
|
||||
|
||||
isDragging.value = false
|
||||
isResizingLeft.value = false
|
||||
isResizingRight.value = false
|
||||
@@ -295,6 +417,32 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
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
|
||||
const getNameStyles = () => {
|
||||
const styles = stickyStyles.value
|
||||
@@ -731,6 +885,87 @@ const getProgressStyles = () => {
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -750,6 +985,9 @@ const getProgressStyles = () => {
|
||||
resizing: isResizingLeft || isResizingRight,
|
||||
completed: isCompleted,
|
||||
'parent-task': isParent,
|
||||
'week-view': isWeekView,
|
||||
'short-task-bar': isShortTaskBar,
|
||||
'overflow-effect': needsOverflowEffect,
|
||||
}"
|
||||
@dblclick="handleTaskBarDoubleClick"
|
||||
>
|
||||
@@ -781,8 +1019,8 @@ const getProgressStyles = () => {
|
||||
</div>
|
||||
|
||||
<!-- 进度百分比 -->
|
||||
<div v-if="task.progress !== undefined" class="task-progress" :style="getProgressStyles()">
|
||||
{{ task.progress }}%
|
||||
<div v-if="shouldShowProgress" class="task-progress" :style="getProgressStyles()">
|
||||
{{ task.progress || 0 }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1009,6 +1247,26 @@ const getProgressStyles = () => {
|
||||
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 {
|
||||
position: absolute;
|
||||
@@ -1349,4 +1607,80 @@ const getProgressStyles = () => {
|
||||
:global(html[data-theme='dark']) .resize-handle:hover {
|
||||
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>
|
||||
|
||||
+839
-65
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@ const messages = {
|
||||
// 日期格式
|
||||
yearMonthFormat: (year: number, month: number) =>
|
||||
`${year}年${String(month).padStart(2, '0')}月`,
|
||||
// 月份格式
|
||||
monthFormat: (month: number) => `${month}月`,
|
||||
|
||||
// 其他
|
||||
milestone: '里程碑',
|
||||
@@ -71,6 +73,11 @@ const messages = {
|
||||
exitFullscreen: '退出全屏',
|
||||
githubDocs: '查看Github文档',
|
||||
giteeDocs: '查看Gitee文档',
|
||||
// 时间刻度按钮
|
||||
timeScaleMonth: '月',
|
||||
timeScaleWeek: '周',
|
||||
timeScaleDay: '日',
|
||||
timeScaleTooltip: '切换时间刻度',
|
||||
// 确认对话框
|
||||
confirmDialogMessage: '是否需要保留该设置?',
|
||||
// 新建任务对话框
|
||||
@@ -178,6 +185,8 @@ const messages = {
|
||||
|
||||
// 日期格式
|
||||
yearMonthFormat: (year: number, month: number) => `${year}/${String(month).padStart(2, '0')}`,
|
||||
// 月份格式
|
||||
monthFormat: (month: number) => `M${String(month).padStart(2, '0')}`,
|
||||
|
||||
// 其他
|
||||
milestone: 'Milestone',
|
||||
@@ -215,6 +224,11 @@ const messages = {
|
||||
exitFullscreen: 'Exit Fullscreen',
|
||||
githubDocs: 'GitHub Docs',
|
||||
giteeDocs: 'Gitee Docs',
|
||||
// 时间刻度按钮
|
||||
timeScaleMonth: 'Month',
|
||||
timeScaleWeek: 'Week',
|
||||
timeScaleDay: 'Day',
|
||||
timeScaleTooltip: 'Switch Time Scale',
|
||||
// Confirm dialog
|
||||
confirmDialogMessage: 'Do you want to save this setting?',
|
||||
taskNamePlaceholder: 'Enter task name',
|
||||
@@ -363,6 +377,11 @@ export function useI18n() {
|
||||
return t.value.yearMonthFormat(year, month)
|
||||
}
|
||||
|
||||
// 格式化月份
|
||||
const formatMonth = (month: number) => {
|
||||
return t.value.monthFormat(month)
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
getTranslation,
|
||||
@@ -370,6 +389,7 @@ export function useI18n() {
|
||||
locale,
|
||||
setLocale,
|
||||
formatYearMonth,
|
||||
formatMonth,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,5 @@ export interface ToolbarConfig {
|
||||
showLanguage?: boolean
|
||||
showTheme?: 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