v1.1.0 - Trigger timeline scroll when TaskBar dragging; Add 'Day|Week|Month' timeline scale; Issues fixed.

This commit is contained in:
LINING-PC\lining
2025-07-08 20:03:04 +08:00
parent 11ee864d36
commit 9cc769c622
15 changed files with 1680 additions and 92 deletions
+341 -7
View File
@@ -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>