v1.7.0-rc.1 Add TaskList and TaskBar Custom contextMenu

This commit is contained in:
LINING-PC\lining
2025-12-23 18:00:31 +08:00
parent 775a0d425c
commit 352f2dce16
6 changed files with 302 additions and 3 deletions
+44
View File
@@ -54,6 +54,7 @@ const props = withDefaults(defineProps<Props>(), {
assigneeOptions: () => [],
taskListRowClassName: undefined,
taskListRowStyle: undefined,
useDefaultContextMenu: true,
})
const emit = defineEmits([
@@ -92,6 +93,31 @@ const slots = useSlots()
// 提供 slots 给子组件(TaskList 和 TaskRow
provide('gantt-column-slots', slots)
// 提供右键菜单配置给子组件
provide('use-default-context-menu', computed(() => props.useDefaultContextMenu))
// 计算是否使用自定义 TaskList 右键菜单
const hasTaskListContextMenuSlot = computed(() => Boolean(slots['task-list-context-menu']))
const shouldUseCustomTaskListContextMenu = computed(() => {
if (!props.useDefaultContextMenu) {
return hasTaskListContextMenuSlot.value
}
return hasTaskListContextMenuSlot.value
})
// 计算是否使用自定义 TaskBar 右键菜单
const hasTaskBarContextMenuSlot = computed(() => Boolean(slots['task-bar-context-menu']))
const shouldUseCustomTaskBarContextMenu = computed(() => {
if (!props.useDefaultContextMenu) {
return hasTaskBarContextMenuSlot.value
}
return hasTaskBarContextMenuSlot.value
})
// 提供右键菜单 slot 给子组件
provide('task-list-context-menu-slot', computed(() => shouldUseCustomTaskListContextMenu.value))
provide('task-bar-context-menu-slot', computed(() => shouldUseCustomTaskBarContextMenu.value))
interface Props {
// 任务数据
tasks?: Task[]
@@ -155,6 +181,10 @@ interface Props {
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
// 函数形式:(row: Task, rowIndex: number) => StyleValue
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
// 是否使用默认的右键菜单(默认为 true)
// 当设置为 false 时,将关闭内置的 TaskRow 和 TaskBar 右键菜单
// 可以通过 task-list-context-menu 和 task-bar-context-menu 插槽自定义菜单
useDefaultContextMenu?: boolean
}
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
@@ -2359,6 +2389,13 @@ function handleMilestoneDialogDelete(milestoneId: number) {
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
<slot name="custom-task-content" v-bind="rowScope" />
</template>
<!-- 传递 task-list-context-menu slot -->
<template
v-if="$slots['task-list-context-menu']"
#task-list-context-menu="contextMenuScope"
>
<slot name="task-list-context-menu" v-bind="contextMenuScope" />
</template>
</TaskList>
</div>
<div class="gantt-splitter" @mousedown="onMouseDown">
@@ -2417,6 +2454,13 @@ function handleMilestoneDialogDelete(milestoneId: number) {
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
<slot name="custom-task-content" v-bind="barScope" />
</template>
<!-- 传递 task-bar-context-menu slot -->
<template
v-if="$slots['task-bar-context-menu']"
#task-bar-context-menu="contextMenuScope"
>
<slot name="task-bar-context-menu" v-bind="contextMenuScope" />
</template>
</Timeline>
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
+66 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
<script setup lang="ts">
import { ref, computed, onUnmounted, onMounted, nextTick, watch, useSlots } from 'vue'
import { ref, computed, onUnmounted, onMounted, nextTick, watch, useSlots, inject, type ComputedRef } from 'vue'
import type { Task } from '../models/classes/Task'
import { TimelineScale } from '../models/types/TimelineScale'
import TaskContextMenu from './TaskContextMenu.vue'
@@ -92,9 +92,49 @@ 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: (linkTask: Task) => void
}): unknown
}>()
const slots = useSlots()
// 注入右键菜单配置
const useDefaultContextMenu = inject<ComputedRef<boolean>>('use-default-context-menu', computed(() => true))
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
// 判断是否应该显示任何右键菜单
const shouldShowAnyContextMenu = computed(() => {
// 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单
if (!useDefaultContextMenu.value && !hasTaskBarContextMenuSlot.value) {
return false
}
return true
})
// 判断是否显示默认右键菜单(没有自定义 slot 时显示)
const shouldShowDefaultContextMenu = computed(() => {
if (!useDefaultContextMenu.value) {
return false
}
return !hasTaskBarContextMenuSlot.value
})
// 判断是否显示自定义右键菜单(有自定义 slot 时显示)
const shouldShowCustomContextMenu = computed(() => {
return hasTaskBarContextMenuSlot.value
})
const { getTranslation } = useI18n()
const t = (key: string): string => {
return getTranslation(key)
@@ -2249,6 +2289,12 @@ const contextMenuPosition = ref({ x: 0, y: 0 })
const contextMenuTask = computed(() => props.task)
function handleContextMenu(event: MouseEvent) {
// 如果不应该显示任何菜单,直接阻止
if (!shouldShowAnyContextMenu.value) {
event.preventDefault()
return
}
// 高亮模式下禁用右键菜单
if (props.isHighlighted || props.isPrimaryHighlight) {
event.preventDefault()
@@ -2532,7 +2578,9 @@ onUnmounted(() => {
@click.stop="handleBubbleClick"
></div>
<!-- 默认右键菜单 -->
<TaskContextMenu
v-if="shouldShowDefaultContextMenu"
:visible="contextMenuVisible"
:task="contextMenuTask"
:position="contextMenuPosition"
@@ -2545,6 +2593,23 @@ onUnmounted(() => {
@delete="handleTaskDelete"
@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"
/>
</div>
<!-- Tooltip 弹窗 -->
+3
View File
@@ -330,6 +330,9 @@ onUnmounted(() => {
<template v-if="hasRowSlot" #custom-task-content="rowScope">
<slot name="custom-task-content" v-bind="rowScope" />
</template>
<template v-if="slots['task-list-context-menu']" #task-list-context-menu="contextMenuScope">
<slot name="task-list-context-menu" v-bind="contextMenuScope" />
</template>
</TaskRow>
<div class="task-list-body-spacer" :style="{ height: `${endSpacerHeight}px` }"></div>
+65 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, useSlots, toRef } from 'vue'
import { ref, computed, useSlots, toRef, inject, type ComputedRef } from 'vue'
import type { StyleValue } from 'vue'
import { useI18n } from '../../../composables/useI18n'
import { formatPredecessorDisplay } from '../../../utils/predecessorUtils'
@@ -72,6 +72,17 @@ const emit = defineEmits([
defineSlots<{
'custom-task-content'(props: TaskRowSlotProps): unknown
'task-list-context-menu'(props: {
task: Task
position: { x: number; y: number }
visible: boolean
onClose: () => void
onStartTimer: () => void
onStopTimer: () => void
onAddPredecessor: () => void
onAddSuccessor: () => void
onDelete: (task: Task, deleteChildren?: boolean) => void
}): unknown
}>()
const { t } = useI18n()
@@ -82,6 +93,32 @@ const daysText = computed(() => t.value?.days ?? '')
const slots = useSlots()
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
// 注入右键菜单配置
const useDefaultContextMenu = inject<ComputedRef<boolean>>('use-default-context-menu', computed(() => true))
const hasTaskListContextMenuSlot = inject<ComputedRef<boolean>>('task-list-context-menu-slot', computed(() => false))
// 判断是否应该显示任何右键菜单
const shouldShowAnyContextMenu = computed(() => {
// 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单
if (!useDefaultContextMenu.value && !hasTaskListContextMenuSlot.value) {
return false
}
return true
})
// 判断是否显示默认右键菜单(没有自定义 slot 时显示)
const shouldShowDefaultContextMenu = computed(() => {
if (!useDefaultContextMenu.value) {
return false
}
return !hasTaskListContextMenuSlot.value
})
// 判断是否显示自定义右键菜单(有自定义 slot 时显示)
const shouldShowCustomContextMenu = computed(() => {
return hasTaskListContextMenuSlot.value
})
// 性能优化:只对会变化的props使用toRef,其他直接传值
const taskRef = toRef(props, 'task')
const levelRef = toRef(props, 'level')
@@ -112,7 +149,7 @@ const {
contextMenuVisible,
contextMenuPosition,
contextMenuTask,
handleContextMenu,
handleContextMenu: handleContextMenuBase,
closeContextMenu,
handleTaskDelete,
timerElapsed,
@@ -120,6 +157,15 @@ const {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = useTaskRowContextMenu(taskRef, emit as any)
// 包装 handleContextMenu 以添加权限检查
const handleContextMenu = (event: MouseEvent) => {
if (!shouldShowAnyContextMenu.value) {
event.preventDefault()
return
}
handleContextMenuBase(event)
}
// TaskRow拖拽功能
const taskRowRef = ref<HTMLElement | null>(null)
const isSplitterDragging = ref(false)
@@ -371,7 +417,9 @@ const slotPayload = computed(() => ({
TaskRow只负责渲染单个任务行不再递归渲染子任务
-->
<!-- 默认右键菜单 -->
<TaskContextMenu
v-if="shouldShowDefaultContextMenu"
:visible="contextMenuVisible"
:task="contextMenuTask"
:position="contextMenuPosition"
@@ -382,6 +430,21 @@ const slotPayload = computed(() => ({
@add-successor="$emit('add-successor', props.task)"
@delete="handleTaskDelete"
/>
<!-- 自定义右键菜单 Slot -->
<slot
v-if="shouldShowCustomContextMenu"
name="task-list-context-menu"
:task="contextMenuTask"
:position="contextMenuPosition"
:visible="contextMenuVisible"
: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"
/>
</div>
</template>
+6
View File
@@ -4359,6 +4359,12 @@ const handleAddSuccessor = (task: Task) => {
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
<slot name="custom-task-content" v-bind="barScope" />
</template>
<template
v-if="$slots['task-bar-context-menu']"
#task-bar-context-menu="contextMenuScope"
>
<slot name="task-bar-context-menu" v-bind="contextMenuScope" />
</template>
</TaskBar>
</div>
</div>