diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f7159d..5afa6ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,17 @@ 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.0.10] - 2025-07-06
+
+### Added
+- 支持多前置任务功能
+- 增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar
+- 点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar
+
+### Changed
+- 支持多前置任务功能
+- Timeline中sub-task隐藏后,关系线也随之隐藏
+
## [1.0.9] - 2025-07-04
### Changed
diff --git a/demo/App.vue b/demo/App.vue
index 92d9faf..32dcd33 100644
--- a/demo/App.vue
+++ b/demo/App.vue
@@ -10,6 +10,7 @@ import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import { useMessage } from '../src/composables/useMessage'
import { useI18n } from '../src/composables/useI18n'
+import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
import type { Task } from '../src/models/Task'
const { showMessage } = useMessage()
@@ -550,11 +551,8 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
const cleanupTaskArray = (taskArray: Task[]) => {
taskArray.forEach(task => {
if (task.predecessor) {
- // 将predecessor字符串解析为ID数组
- const predecessorIds = task.predecessor
- .split(',')
- .map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id))
+ // 使用工具函数获取前置任务ID数组
+ const predecessorIds = getPredecessorIds(task.predecessor)
// 过滤掉被删除的任务ID
const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id))
@@ -563,7 +561,7 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
if (validPredecessorIds.length === 0) {
delete task.predecessor
} else {
- task.predecessor = validPredecessorIds.join(',')
+ task.predecessor = predecessorIdsToString(validPredecessorIds)
}
}
diff --git a/demo/data.json b/demo/data.json
index 8da5a0a..376d687 100644
--- a/demo/data.json
+++ b/demo/data.json
@@ -38,7 +38,7 @@
{
"id": 4,
"name": "调研A",
- "predecessor": "2",
+ "predecessor": [2],
"assignee": "赵六",
"startDate": "2025-06-21",
"endDate": "2025-06-23",
@@ -51,7 +51,7 @@
{
"id": 5,
"name": "调研B",
- "predecessor": "4",
+ "predecessor": [4],
"assignee": "钱七",
"startDate": "2025-06-24",
"endDate": "2025-06-28",
@@ -88,7 +88,7 @@
"estimatedHours": 80,
"actualHours": 35,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
},
{
@@ -101,7 +101,7 @@
"estimatedHours": 80,
"actualHours": 0,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
},
{
@@ -114,7 +114,7 @@
"estimatedHours": 88,
"actualHours": 0,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
}
],
@@ -141,7 +141,7 @@
"estimatedHours": 48,
"actualHours": 0,
"type": "task",
- "predecessor": "7",
+ "predecessor": [8, 11, 12],
"parentId": 13
},
{
@@ -154,7 +154,7 @@
"estimatedHours": 24,
"actualHours": 0,
"type": "task",
- "predecessor": "7",
+ "predecessor": [14],
"parentId": 13
}
],
diff --git a/demo/version-history.json b/demo/version-history.json
index 5b39e5b..4daf2be 100644
--- a/demo/version-history.json
+++ b/demo/version-history.json
@@ -124,5 +124,16 @@
"增加Story删除时选择是否删除子任务的选项",
"删除Story或Task时同时删除前后的关系线路"
]
+ },
+ {
+ "version": "1.0.10",
+ "date": "2025-07-06",
+ "notes": [
+ "升级用户体验",
+ "增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar",
+ "点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar",
+ "Timeline中sub-task隐藏后,关系线也随之隐藏",
+ "增强TaskBar可以存在多个前置任务"
+ ]
}
]
diff --git a/package.json b/package.json
index 5aafb53..97fe18e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jordium-gantt-vue3",
- "version": "1.0.9",
+ "version": "1.0.10",
"type": "module",
"main": "dist/jordium-gantt-vue3.cjs.js",
"module": "dist/jordium-gantt-vue3.es.js",
diff --git a/packageDemo/App.vue b/packageDemo/App.vue
index 92d9faf..32dcd33 100644
--- a/packageDemo/App.vue
+++ b/packageDemo/App.vue
@@ -10,6 +10,7 @@ import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import { useMessage } from '../src/composables/useMessage'
import { useI18n } from '../src/composables/useI18n'
+import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
import type { Task } from '../src/models/Task'
const { showMessage } = useMessage()
@@ -550,11 +551,8 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
const cleanupTaskArray = (taskArray: Task[]) => {
taskArray.forEach(task => {
if (task.predecessor) {
- // 将predecessor字符串解析为ID数组
- const predecessorIds = task.predecessor
- .split(',')
- .map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id))
+ // 使用工具函数获取前置任务ID数组
+ const predecessorIds = getPredecessorIds(task.predecessor)
// 过滤掉被删除的任务ID
const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id))
@@ -563,7 +561,7 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
if (validPredecessorIds.length === 0) {
delete task.predecessor
} else {
- task.predecessor = validPredecessorIds.join(',')
+ task.predecessor = predecessorIdsToString(validPredecessorIds)
}
}
diff --git a/packageDemo/data.json b/packageDemo/data.json
index 8da5a0a..376d687 100644
--- a/packageDemo/data.json
+++ b/packageDemo/data.json
@@ -38,7 +38,7 @@
{
"id": 4,
"name": "调研A",
- "predecessor": "2",
+ "predecessor": [2],
"assignee": "赵六",
"startDate": "2025-06-21",
"endDate": "2025-06-23",
@@ -51,7 +51,7 @@
{
"id": 5,
"name": "调研B",
- "predecessor": "4",
+ "predecessor": [4],
"assignee": "钱七",
"startDate": "2025-06-24",
"endDate": "2025-06-28",
@@ -88,7 +88,7 @@
"estimatedHours": 80,
"actualHours": 35,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
},
{
@@ -101,7 +101,7 @@
"estimatedHours": 80,
"actualHours": 0,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
},
{
@@ -114,7 +114,7 @@
"estimatedHours": 88,
"actualHours": 0,
"type": "task",
- "predecessor": "5",
+ "predecessor": [5],
"parentId": 7
}
],
@@ -141,7 +141,7 @@
"estimatedHours": 48,
"actualHours": 0,
"type": "task",
- "predecessor": "7",
+ "predecessor": [8, 11, 12],
"parentId": 13
},
{
@@ -154,7 +154,7 @@
"estimatedHours": 24,
"actualHours": 0,
"type": "task",
- "predecessor": "7",
+ "predecessor": [14],
"parentId": 13
}
],
diff --git a/packageDemo/version-history.json b/packageDemo/version-history.json
index 5b39e5b..4daf2be 100644
--- a/packageDemo/version-history.json
+++ b/packageDemo/version-history.json
@@ -124,5 +124,16 @@
"增加Story删除时选择是否删除子任务的选项",
"删除Story或Task时同时删除前后的关系线路"
]
+ },
+ {
+ "version": "1.0.10",
+ "date": "2025-07-06",
+ "notes": [
+ "升级用户体验",
+ "增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar",
+ "点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar",
+ "Timeline中sub-task隐藏后,关系线也随之隐藏",
+ "增强TaskBar可以存在多个前置任务"
+ ]
}
]
diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue
index 6a7d2c7..6380190 100644
--- a/src/components/GanttChart.vue
+++ b/src/components/GanttChart.vue
@@ -4,6 +4,7 @@ import TaskList from './TaskList.vue'
import Timeline from './Timeline.vue'
import GanttToolbar from './GanttToolbar.vue'
import { useI18n, setCustomMessages } from '../composables/useI18n'
+import { formatPredecessorDisplay } from '../utils/predecessorUtils'
import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
import type { Task } from '../models/classes/Task'
@@ -222,16 +223,35 @@ const toggleTaskList = () => {
isTaskListVisible.value = !isTaskListVisible.value
}, 200)
- // 动画结束后清理状态
+ // 动画结束后清理状态,并通知Timeline容器变化
setTimeout(() => {
isAnimating.value = false
animationClass.value = ''
+
+ // 手动切换TaskList后,通知Timeline重新计算半圆
+ nextTick(() => {
+ window.dispatchEvent(
+ new CustomEvent('timeline-container-resized', {
+ detail: { source: 'manual-task-list-toggle' },
+ }),
+ )
+ })
}, 400)
}
// 监听Timeline的TaskList切换事件
const handleToggleTaskList = (event: CustomEvent) => {
isTaskListVisible.value = event.detail
+
+ // TaskList切换会改变Timeline容器宽度,需要通知Timeline重新计算半圆
+ // 派发事件通知Timeline容器宽度发生了变化
+ nextTick(() => {
+ window.dispatchEvent(
+ new CustomEvent('timeline-container-resized', {
+ detail: { source: 'task-list-toggle' },
+ }),
+ )
+ })
}
// --- 事件链路:监听 Timeline 传递上来的拖拽/拉伸事件,并通过 props 回调暴露 ---
@@ -716,7 +736,7 @@ const generateCsvContent = (tasks: Task[]): string => {
result.push({
id: task.id,
name: task.name || '',
- predecessor: task.predecessor || '',
+ predecessor: formatPredecessorDisplay(task.predecessor),
assignee: task.assignee || '',
startDate: task.startDate || '',
endDate: task.endDate || '',
@@ -747,7 +767,7 @@ const generateCsvContent = (tasks: Task[]): string => {
return [
escapeCSVField(task.id),
escapeCSVField(task.name),
- escapeCSVField(task.predecessor),
+ escapeCSVField(formatPredecessorDisplay(task.predecessor)),
escapeCSVField(task.assignee),
escapeCSVField(task.startDate),
escapeCSVField(task.endDate),
@@ -1181,12 +1201,6 @@ watch(
-
-
-
-
-
-
-
-
-
diff --git a/src/components/MultiSelectPredecessor.vue b/src/components/MultiSelectPredecessor.vue
new file mode 100644
index 0000000..1da4ff5
--- /dev/null
+++ b/src/components/MultiSelectPredecessor.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+ {{ pred.name }} ({{ pred.id }})
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue
index 81090c3..870ac6b 100644
--- a/src/components/TaskBar.vue
+++ b/src/components/TaskBar.vue
@@ -9,6 +9,11 @@ interface Props {
startDate: Date
isParent?: boolean
onDoubleClick?: (task: Task) => void
+ // 新增:用于粘性文字显示的滚动位置信息
+ scrollLeft?: number
+ containerWidth?: number
+ // 新增:外部控制半圆隐藏状态(用于Timeline初始化等场景)
+ hideBubbles?: boolean
}
const props = defineProps()
@@ -19,6 +24,7 @@ const emit = defineEmits([
'dblclick',
'drag-end', // 新增
'resize-end', // 新增
+ 'scroll-to-position', // 新增:半圆点击定位事件
])
// 日期工具函数 - 处理时区安全的日期创建和操作
@@ -327,10 +333,404 @@ const handleTaskBarDoubleClick = (e: MouseEvent) => {
}
}
+// 计算粘性样式 - 支持左右边界的精细控制
+const stickyStyles = computed(() => {
+ const scrollLeft = props.scrollLeft || 0
+ const containerWidth = props.containerWidth || 0
+
+ if (!scrollLeft && !containerWidth) {
+ return {
+ nameLeft: '',
+ namePosition: '',
+ nameTop: '',
+ progressLeft: '',
+ progressPosition: '',
+ progressTop: '',
+ }
+ }
+
+ const taskLeft = parseInt(taskBarStyle.value.left)
+ const taskWidth = parseInt(taskBarStyle.value.width)
+ const taskRight = taskLeft + taskWidth
+ const taskCenterX = taskLeft + taskWidth / 2
+ const leftBoundary = scrollLeft
+ const rightBoundary = scrollLeft + containerWidth
+
+ // 默认样式
+ let nameLeft = ''
+ let namePosition = ''
+ let nameTop = ''
+ let progressLeft = ''
+ let progressPosition = ''
+ let progressTop = ''
+
+ // 估算文字内容的实际位置
+ const nameText = props.task.name || ''
+ const nameWidth = Math.max(nameText.length * 7, 40)
+ const progressWidth = 35
+
+ // 计算名称和进度在默认居中状态下的位置
+ const nameLeftPos = taskCenterX - nameWidth / 2
+ const nameRightPos = taskCenterX + nameWidth / 2
+ const progressLeftPos = taskCenterX - progressWidth / 2
+ const progressRightPos = taskCenterX + progressWidth / 2
+
+ // 左侧边界粘性逻辑
+ const nameNeedsLeftSticky =
+ nameLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary
+
+ // 右侧边界粘性逻辑
+ const nameNeedsRightSticky =
+ nameRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary
+
+ // 名称粘性处理
+ if (nameNeedsLeftSticky) {
+ const offset = leftBoundary - taskLeft
+ nameLeft = `${offset + 8}px`
+ namePosition = 'absolute'
+ nameTop = '6px'
+ } else if (nameNeedsRightSticky) {
+ const offset = rightBoundary - taskLeft - nameWidth
+ nameLeft = `${offset - 8}px`
+ namePosition = 'absolute'
+ nameTop = '6px'
+ }
+
+ // 进度左侧边界粘性逻辑
+ const progressNeedsLeftSticky =
+ progressLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary
+
+ // 进度右侧边界粘性逻辑
+ const progressNeedsRightSticky =
+ progressRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary
+
+ // 进度粘性处理
+ if (progressNeedsLeftSticky) {
+ const offset = leftBoundary - taskLeft
+ progressLeft = `${offset + 8}px`
+ progressPosition = 'absolute'
+ progressTop = '24px'
+ } else if (progressNeedsRightSticky) {
+ const offset = rightBoundary - taskLeft - progressWidth
+ progressLeft = `${offset - 8}px`
+ progressPosition = 'absolute'
+ progressTop = '24px'
+ }
+
+ return {
+ nameLeft,
+ namePosition,
+ nameTop,
+ progressLeft,
+ progressPosition,
+ progressTop,
+ }
+})
+
+// 计算气泡指示器的显示状态和位置
+const bubbleIndicator = computed(() => {
+ const scrollLeft = props.scrollLeft || 0
+ const containerWidth = props.containerWidth || 0
+
+ // 如果没有有效的滚动信息,不显示气泡
+ if (!containerWidth || containerWidth <= 0) {
+ return {
+ show: false,
+ left: '0px',
+ side: 'left',
+ color: '#409eff',
+ animationType: 'none',
+ }
+ }
+
+ // 如果正在初始化、强制隐藏状态或外部要求隐藏,不显示气泡
+ if (isInitializing.value || bubbleHidden.value || props.hideBubbles) {
+ return {
+ show: false,
+ left: '0px',
+ side: 'left',
+ color: taskStatus.value.color,
+ animationType: 'none',
+ }
+ }
+
+ // 获取实际的DOM位置(考虑缩放等因素)
+ const taskLeft = parseInt(taskBarStyle.value.left)
+ const taskWidth = parseInt(taskBarStyle.value.width)
+ const taskRight = taskLeft + taskWidth
+ const leftBoundary = scrollLeft
+ const rightBoundary = scrollLeft + containerWidth
+
+ // 检查边界状态
+ const isCompletelyOutOfLeft = taskRight <= leftBoundary
+ const isCompletelyOutOfRight = taskLeft >= rightBoundary
+
+ // 只有完全超出边界时才显示半圆
+ if (isCompletelyOutOfLeft) {
+ return {
+ show: true,
+ left: `${leftBoundary - taskLeft - 4}px`, // 圆心在边界上,向左偏移半径(4px)
+ side: 'left',
+ color: taskStatus.value.color,
+ animationType: 'morphToSemiCircle',
+ }
+ }
+
+ if (isCompletelyOutOfRight) {
+ return {
+ show: true,
+ left: `${rightBoundary - taskLeft - 8}px`, // 右侧半圆位置调整,减少偏移量
+ side: 'right',
+ color: taskStatus.value.color,
+ animationType: 'morphToSemiCircle',
+ }
+ }
+
+ // 部分可见或完全可见时不显示
+ return {
+ show: false,
+ left: '0px',
+ side: 'left',
+ color: taskStatus.value.color,
+ animationType: 'none',
+ }
+})
+
+// 气泡 tooltip 状态
+const showTooltip = ref(false)
+const tooltipPosition = ref({ x: 0, y: 0 })
+
+// 跟踪滚动状态,避免非滚动时的动画
+const isScrollingContext = ref(false)
+const scrollTimeout = ref(null)
+const hasManualResize = ref(false) // 跟踪是否有手动resize事件
+const isInitializing = ref(true) // 跟踪初始化状态
+const isAutoScrolling = ref(false) // 跟踪自动滚动状态(如定位到今日、点击半圆定位等)
+const bubbleHidden = ref(false) // 控制半圆的强制隐藏状态
+
+// 气泡点击事件 - 将TaskBar定位到Timeline中间
+const handleBubbleClick = () => {
+ const scrollLeft = props.scrollLeft || 0
+ const containerWidth = props.containerWidth || 0
+
+ if (!scrollLeft && !containerWidth) return
+
+ const taskLeft = parseInt(taskBarStyle.value.left)
+ const taskWidth = parseInt(taskBarStyle.value.width)
+ const taskCenterX = taskLeft + taskWidth / 2
+
+ // 计算将TaskBar中心定位到Timeline中心所需的滚动位置
+ const targetScrollLeft = taskCenterX - containerWidth / 2
+
+ // 标记为自动滚动状态,隐藏所有半圆
+ isAutoScrolling.value = true
+ bubbleHidden.value = true
+
+ // 立即隐藏tooltip
+ showTooltip.value = false
+
+ // 通过事件向Timeline发送滚动请求
+ emit('scroll-to-position', targetScrollLeft)
+}
+
+// 监听滚动相关的props变化,判断是否在滚动
+watch(
+ () => [props.scrollLeft, props.containerWidth],
+ (newValues, oldValues) => {
+ // 检查是否是真实的滚动变化(而非初始化或resize)
+ const [newScrollLeft, newContainerWidth] = newValues
+ const [oldScrollLeft, oldContainerWidth] = oldValues || [0, 0]
+
+ // 确保数值有效
+ const safeNewScrollLeft = newScrollLeft || 0
+ const safeNewContainerWidth = newContainerWidth || 0
+ const safeOldScrollLeft = oldScrollLeft || 0
+ const safeOldContainerWidth = oldContainerWidth || 0
+
+ // 如果容器宽度发生变化(包括Splitter拖拽、TaskList展开收起、窗口resize等)
+ if (Math.abs(safeNewContainerWidth - safeOldContainerWidth) > 1 && safeOldContainerWidth > 0) {
+ hasManualResize.value = true
+
+ // 容器宽度变化时,强制重新计算气泡显示状态
+ // 立即触发bubbleIndicator重新计算
+ nextTick(() => {
+ // computed会自动重新计算
+ })
+
+ // 延长禁用动画的时间,确保各种resize操作稳定
+ setTimeout(() => {
+ hasManualResize.value = false
+ }, 500) // 给容器变化留足够时间稳定
+ return
+ }
+
+ // 首次接收到有效的滚动数据,标记初始化完成
+ if (isInitializing.value && safeNewScrollLeft >= 0 && safeNewContainerWidth > 0) {
+ // 延迟标记初始化完成,等待初始滚动动画结束
+ setTimeout(() => {
+ isInitializing.value = false
+ }, 1000) // 给初始化滚动留足够时间
+ }
+
+ // 只有在scrollLeft变化且没有resize时才认为是滚动
+ if (safeNewScrollLeft !== safeOldScrollLeft && !hasManualResize.value) {
+ isScrollingContext.value = true
+
+ // 清除之前的超时
+ if (scrollTimeout.value) {
+ clearTimeout(scrollTimeout.value)
+ }
+
+ // 滚动结束后的处理
+ scrollTimeout.value = setTimeout(() => {
+ isScrollingContext.value = false
+
+ // 如果是自动滚动结束,恢复半圆显示
+ if (isAutoScrolling.value) {
+ isAutoScrolling.value = false
+ // 延迟一点时间再显示半圆,确保滚动完全停止
+ setTimeout(() => {
+ bubbleHidden.value = false
+ }, 300)
+ }
+ }, 200)
+ }
+ },
+)
+
+// 监听外部hideBubbles属性变化,确保Timeline的容器变化能及时反应
+watch(
+ () => props.hideBubbles,
+ (newHidden, oldHidden) => {
+ // 当Timeline设置hideBubbles从true变为false时,强制重新计算半圆状态
+ if (oldHidden && !newHidden) {
+ nextTick(() => {
+ // 强制重新计算bubbleIndicator,确保容器宽度变化后正确显示半圆
+ })
+ }
+ },
+)
+
+// 监听TaskBar可见性变化,只在滚动时实现重新出现动画
+watch(
+ () => bubbleIndicator.value.show,
+ () => {
+ // TaskBar重新出现时,不需要动画效果
+ // 半圆会自然消失,TaskBar会立即显示
+ },
+)
+
+// 监听页面缩放和大小变化,重新计算气泡位置
+const handleResize = () => {
+ // timeline区域resize时,立即重新计算半圆显示状态
+ hasManualResize.value = true
+
+ // 强制重新计算bubbleIndicator
+ nextTick(() => {
+ // computed会自动重新计算
+ })
+
+ // 短时间后恢复正常状态,允许动画
+ setTimeout(() => {
+ hasManualResize.value = false
+ }, 300) // 缩短时间,快速恢复
+}
+
+onMounted(() => {
+ // 监听窗口大小变化和缩放
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('zoom', handleResize) // 某些浏览器支持
+})
+
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('zoom', handleResize)
})
+
+// 处理气泡悬停
+const handleBubbleMouseEnter = (event: MouseEvent) => {
+ showTooltip.value = true
+
+ // 智能定位:右侧气泡在左侧显示tooltip,左侧气泡在右侧显示
+ const isRightBubble = bubbleIndicator.value.side === 'right'
+ const offsetX = isRightBubble ? -180 : 15 // 右侧气泡向左偏移距离调整,与左侧距离一致
+
+ tooltipPosition.value = {
+ x: event.clientX + offsetX,
+ y: event.clientY - 10,
+ }
+}
+
+const handleBubbleMouseLeave = () => {
+ showTooltip.value = false
+}
+
+// 处理气泡点击事件 - 点击时隐藏tooltip,但不影响定位功能
+const handleBubbleMouseDown = (event: MouseEvent) => {
+ // 阻止mousedown事件冒泡,防止影响其他功能
+ event.stopPropagation()
+ // 点击时隐藏tooltip
+ showTooltip.value = false
+}
+
+// 格式化日期显示
+const formatDisplayDate = (dateStr: string | undefined): string => {
+ if (!dateStr) return '未设置'
+ const date = createLocalDate(dateStr)
+ if (!date) return '未设置'
+
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+// 计算工时信息
+const workHourInfo = computed(() => {
+ // 这里可以根据实际的数据结构调整
+ const startDate = createLocalDate(props.task.startDate)
+ const endDate = createLocalDate(props.task.endDate)
+
+ let totalHours = 0
+ if (startDate && endDate) {
+ const diffTime = Math.abs(endDate.getTime() - startDate.getTime())
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
+ totalHours = diffDays * 8 // 假设每天8小时
+ }
+
+ const progress = props.task.progress || 0
+ const usedHours = Math.round((totalHours * progress) / 100)
+
+ return {
+ total: totalHours,
+ used: usedHours,
+ }
+})
+
+// Helper functions to create type-safe style objects
+const getNameStyles = () => {
+ const styles = stickyStyles.value
+ const result: Record = {}
+
+ if (styles.nameLeft) result.left = styles.nameLeft
+ if (styles.namePosition) result.position = styles.namePosition
+ if (styles.nameTop) result.top = styles.nameTop
+
+ return result
+}
+
+const getProgressStyles = () => {
+ const styles = stickyStyles.value
+ const result: Record = {}
+
+ if (styles.progressLeft) result.left = styles.progressLeft
+ if (styles.progressPosition) result.position = styles.progressPosition
+ if (styles.progressTop) result.top = styles.progressTop
+
+ return result
+}
@@ -343,6 +743,7 @@ onUnmounted(() => {
borderColor: taskStatus.borderColor,
color: taskStatus.color,
cursor: isCompleted || isParent ? 'default' : 'move',
+ '--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */,
}"
:class="{
dragging: isDragging,
@@ -374,8 +775,15 @@ onUnmounted(() => {
handleMouseDown(e, 'drag')">
-
{{ task.name }}
-
{{ task.progress }}%
+
+
+ {{ task.name }}
+
+
+
+
+ {{ task.progress }}%
+
@@ -384,7 +792,63 @@ onUnmounted(() => {
class="resize-handle resize-handle-right"
@mousedown="e => handleMouseDown(e, 'resize-right')"
>
+
+
+
+
+
+
+
+