v1.7.0 - add TaskListContextMenu and TaskBarContextMenu components & slots

This commit is contained in:
LINING-PC\lining
2026-01-10 23:16:56 +08:00
parent 84aabe53d7
commit 396c7b24da
20 changed files with 1338 additions and 151 deletions
+68 -50
View File
@@ -17,6 +17,7 @@ interface Props {
startDate: Date
isParent?: boolean
onClick?: (task: Task, event: MouseEvent) => void
rowIndex?: number
// 新增:用于粘性文字显示的滚动位置信息
scrollLeft?: number
containerWidth?: number
@@ -92,47 +93,50 @@ const emit = defineEmits([
defineSlots<{
'custom-task-content'(props: TaskBarSlotProps): unknown
'task-bar-context-menu'(props: {
task: Task
position: { x: number; y: number }
visible: boolean
allTasks?: Task[]
onClose: () => void
onStartTimer: () => void
onStopTimer: () => void
onAddPredecessor: () => void
onAddSuccessor: () => void
onDelete: (task: Task, deleteChildren?: boolean) => void
onDeleteLink: (event: { sourceTaskId: number; targetTaskId: number }) => void
}): unknown
}>()
const slots = useSlots()
// 注入右键菜单配置
const useDefaultContextMenu = inject<ComputedRef<boolean>>('use-default-context-menu', computed(() => true))
const enableTaskBarContextMenu = inject<ComputedRef<boolean>>('enable-task-bar-context-menu', computed(() => true))
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
const declarativeTaskBarContextMenu = inject<ComputedRef<any>>('declarative-task-bar-context-menu', computed(() => null))
// 判断是否应该显示任何右键菜单
const shouldShowAnyContextMenu = computed(() => {
// 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单
if (!useDefaultContextMenu.value && !hasTaskBarContextMenuSlot.value) {
// 如果 enableTaskBarContextMenu 为 false,则不显示任何菜单
if (!enableTaskBarContextMenu.value) {
return false
}
return true
})
// 判断是否显示默认右键菜单(没有自定义 slot 时显示)
// 判断是否显示默认右键菜单(enableTaskBarContextMenu=true 且没有自定义 slot 时显示)
const shouldShowDefaultContextMenu = computed(() => {
if (!useDefaultContextMenu.value) {
if (!enableTaskBarContextMenu.value) {
return false
}
return !hasTaskBarContextMenuSlot.value
})
// 判断是否显示自定义右键菜单(有自定义 slot 时显示)
// 判断是否显示自定义右键菜单(enableTaskBarContextMenu=true 且有自定义 slot 时显示)
const shouldShowCustomContextMenu = computed(() => {
return hasTaskBarContextMenuSlot.value
if (!enableTaskBarContextMenu.value) {
return false
}
if (!hasTaskBarContextMenuSlot.value) {
return false
}
// 检查 taskType 过滤
const config = declarativeTaskBarContextMenu.value
if (config?.taskType !== undefined) {
const taskType = props.task.type || 'task'
const allowedTypes = Array.isArray(config.taskType) ? config.taskType : [config.taskType]
return allowedTypes.includes(taskType)
}
return true
})
const { getTranslation } = useI18n()
@@ -1330,6 +1334,7 @@ onMounted(() => {
// 监听全局关闭菜单事件
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
document.addEventListener('click', handleDocumentClick)
// 清理函数
onUnmounted(() => {
@@ -1342,12 +1347,19 @@ onMounted(() => {
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
document.removeEventListener('click', handleDocumentClick)
// 清除长按定时器
if (longPressTimer.value !== null) {
clearTimeout(longPressTimer.value)
longPressTimer.value = null
}
// 清理单击定时器
if (clickTimer !== null) {
clearTimeout(clickTimer)
clickTimer = null
}
})
})
@@ -2303,7 +2315,8 @@ function handleContextMenu(event: MouseEvent) {
// 先广播关闭所有TaskBar菜单
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
if (props.task.type !== 'task' && props.task.type !== 'story') {
const taskType = props.task.type || 'task'
if (taskType !== 'task' && taskType !== 'story') {
// 为了排除里程碑类型
event.preventDefault()
contextMenuVisible.value = false
@@ -2317,6 +2330,20 @@ function closeContextMenu() {
contextMenuVisible.value = false
}
// 处理文档点击事件,点击菜单外部时关闭
function handleDocumentClick(event: MouseEvent) {
if (!contextMenuVisible.value) return
const target = event.target as HTMLElement
// 检查点击是否在右键菜单内部
const contextMenuElement = document.querySelector('.task-context-menu')
if (contextMenuElement && contextMenuElement.contains(target)) {
return
}
closeContextMenu()
}
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
// 触发删除事件
emit('delete', task, deleteChildren)
@@ -2370,19 +2397,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
task: props.task,
type: anchorEvent.type,
})
}// 监听全局关闭菜单事件
onMounted(() => {
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
})
onUnmounted(() => {
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
// 清理单击定时器
if (clickTimer !== null) {
clearTimeout(clickTimer)
clickTimer = null
}
})
}
</script>
<template>
@@ -2594,22 +2609,25 @@ onUnmounted(() => {
@delete-link="handleDeleteLink"
/>
<!-- 自定义右键菜单 Slot -->
<slot
v-if="shouldShowCustomContextMenu"
name="task-bar-context-menu"
:task="contextMenuTask"
:position="contextMenuPosition"
:visible="contextMenuVisible"
:all-tasks="allTasks"
:on-close="closeContextMenu"
:on-start-timer="() => $emit('start-timer', props.task)"
:on-stop-timer="() => $emit('stop-timer', props.task)"
:on-add-predecessor="() => $emit('add-predecessor', props.task)"
:on-add-successor="() => $emit('add-successor', props.task)"
:on-delete="handleTaskDelete"
:on-delete-link="handleDeleteLink"
/>
<!-- 声明式右键菜单 -->
<Teleport to="body">
<div
v-if="shouldShowCustomContextMenu && contextMenuVisible && declarativeTaskBarContextMenu?.defaultSlot"
class="gantt-context-menu-wrapper"
:style="{
position: 'fixed',
left: `${contextMenuPosition.x}px`,
top: `${contextMenuPosition.y}px`,
zIndex: 9999,
}"
>
<component
:is="declarativeTaskBarContextMenu.defaultSlot"
:row="contextMenuTask"
:$index="props.rowIndex ?? -1"
/>
</div>
</Teleport>
</div>
<!-- Tooltip 弹窗 -->