v1.7.0-rc.1 Add TaskList and TaskBar Custom contextMenu
This commit is contained in:
+118
@@ -733,6 +733,12 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
// showMessage('保存失败,请刷新页面', 'error', { closable: true })
|
// showMessage('保存失败,请刷新页面', 'error', { closable: true })
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义右键菜单操作处理
|
||||||
|
const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) => {
|
||||||
|
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1233,6 +1239,7 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
:on-language-change="handleLanguageChange"
|
:on-language-change="handleLanguageChange"
|
||||||
:on-theme-change="handleThemeChange"
|
:on-theme-change="handleThemeChange"
|
||||||
:task-list-column-render-mode="taskListColumnRenderMode"
|
:task-list-column-render-mode="taskListColumnRenderMode"
|
||||||
|
:use-default-context-menu="true"
|
||||||
@milestone-saved="handleMilestoneSaved"
|
@milestone-saved="handleMilestoneSaved"
|
||||||
@milestone-deleted="handleMilestoneDeleted"
|
@milestone-deleted="handleMilestoneDeleted"
|
||||||
@milestone-icon-changed="handleMilestoneIconChanged"
|
@milestone-icon-changed="handleMilestoneIconChanged"
|
||||||
@@ -1363,8 +1370,39 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
</TaskListColumn>
|
</TaskListColumn>
|
||||||
<TaskListColumn prop="startDate" :label="t.startDate" width="200" align="center" />
|
<TaskListColumn prop="startDate" :label="t.startDate" width="200" align="center" />
|
||||||
<TaskListColumn prop="endDate" :label="t.endDate" width="200" align="center" />
|
<TaskListColumn prop="endDate" :label="t.endDate" width="200" align="center" />
|
||||||
|
|
||||||
|
<!-- 自定义 TaskBar 右键菜单 - 声明此 slot 将使用自定义菜单 -->
|
||||||
|
<template #task-bar-context-menu="{ task, position, visible, onClose }">
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="visible && task"
|
||||||
|
class="custom-menu"
|
||||||
|
:style="{
|
||||||
|
left: `${position.x}px`,
|
||||||
|
top: `${position.y}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="custom-menu-header">自定义甘特图菜单</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', task, onClose)">
|
||||||
|
➡️ 延长任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('move', task, onClose)">
|
||||||
|
📅 移动任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-divider"></div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', task, onClose)">
|
||||||
|
📄 复制任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-divider"></div>
|
||||||
|
<div class="custom-menu-item" @click="onClose">❌ 取消</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- TaskList 不声明 slot,将使用内置默认菜单 -->
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="license-info">
|
<div class="license-info">
|
||||||
<span>MIT License @ 2025 JORDIUM.COM</span>
|
<span>MIT License @ 2025 JORDIUM.COM</span>
|
||||||
<a href="https://opensource.org/licenses/MIT">
|
<a href="https://opensource.org/licenses/MIT">
|
||||||
@@ -2739,4 +2777,84 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
:global(html[data-theme='dark']) :deep(.task-row-info:hover) {
|
:global(html[data-theme='dark']) :deep(.task-row-info:hover) {
|
||||||
background: linear-gradient(90deg, #353535 0%, #3d3d3d 100%) !important;
|
background: linear-gradient(90deg, #353535 0%, #3d3d3d 100%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义右键菜单样式 */
|
||||||
|
.custom-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999999 !important;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-header {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.danger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.danger:hover {
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的自定义菜单 */
|
||||||
|
:global(html[data-theme='dark']) .custom-menu {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-header {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-bottom-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item:hover {
|
||||||
|
background: #353535;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item.danger {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item.danger:hover {
|
||||||
|
background: #3a2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-divider {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
assigneeOptions: () => [],
|
assigneeOptions: () => [],
|
||||||
taskListRowClassName: undefined,
|
taskListRowClassName: undefined,
|
||||||
taskListRowStyle: undefined,
|
taskListRowStyle: undefined,
|
||||||
|
useDefaultContextMenu: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -92,6 +93,31 @@ const slots = useSlots()
|
|||||||
// 提供 slots 给子组件(TaskList 和 TaskRow)
|
// 提供 slots 给子组件(TaskList 和 TaskRow)
|
||||||
provide('gantt-column-slots', slots)
|
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 {
|
interface Props {
|
||||||
// 任务数据
|
// 任务数据
|
||||||
tasks?: Task[]
|
tasks?: Task[]
|
||||||
@@ -155,6 +181,10 @@ interface Props {
|
|||||||
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
|
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
|
||||||
// 函数形式:(row: Task, rowIndex: number) => StyleValue
|
// 函数形式:(row: Task, rowIndex: number) => StyleValue
|
||||||
taskListRowStyle?: StyleValue | ((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的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -2359,6 +2389,13 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
||||||
<slot name="custom-task-content" v-bind="rowScope" />
|
<slot name="custom-task-content" v-bind="rowScope" />
|
||||||
</template>
|
</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>
|
</TaskList>
|
||||||
</div>
|
</div>
|
||||||
<div class="gantt-splitter" @mousedown="onMouseDown">
|
<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">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
</template>
|
</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>
|
</Timeline>
|
||||||
|
|
||||||
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
<script setup lang="ts">
|
<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 type { Task } from '../models/classes/Task'
|
||||||
import { TimelineScale } from '../models/types/TimelineScale'
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
import TaskContextMenu from './TaskContextMenu.vue'
|
import TaskContextMenu from './TaskContextMenu.vue'
|
||||||
@@ -92,9 +92,49 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
'custom-task-content'(props: TaskBarSlotProps): unknown
|
'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 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 { getTranslation } = useI18n()
|
||||||
const t = (key: string): string => {
|
const t = (key: string): string => {
|
||||||
return getTranslation(key)
|
return getTranslation(key)
|
||||||
@@ -2249,6 +2289,12 @@ const contextMenuPosition = ref({ x: 0, y: 0 })
|
|||||||
const contextMenuTask = computed(() => props.task)
|
const contextMenuTask = computed(() => props.task)
|
||||||
|
|
||||||
function handleContextMenu(event: MouseEvent) {
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
// 如果不应该显示任何菜单,直接阻止
|
||||||
|
if (!shouldShowAnyContextMenu.value) {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 高亮模式下禁用右键菜单
|
// 高亮模式下禁用右键菜单
|
||||||
if (props.isHighlighted || props.isPrimaryHighlight) {
|
if (props.isHighlighted || props.isPrimaryHighlight) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -2532,7 +2578,9 @@ onUnmounted(() => {
|
|||||||
@click.stop="handleBubbleClick"
|
@click.stop="handleBubbleClick"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- 默认右键菜单 -->
|
||||||
<TaskContextMenu
|
<TaskContextMenu
|
||||||
|
v-if="shouldShowDefaultContextMenu"
|
||||||
:visible="contextMenuVisible"
|
:visible="contextMenuVisible"
|
||||||
:task="contextMenuTask"
|
:task="contextMenuTask"
|
||||||
:position="contextMenuPosition"
|
:position="contextMenuPosition"
|
||||||
@@ -2545,6 +2593,23 @@ onUnmounted(() => {
|
|||||||
@delete="handleTaskDelete"
|
@delete="handleTaskDelete"
|
||||||
@delete-link="handleDeleteLink"
|
@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>
|
</div>
|
||||||
|
|
||||||
<!-- Tooltip 弹窗 -->
|
<!-- Tooltip 弹窗 -->
|
||||||
|
|||||||
@@ -330,6 +330,9 @@ onUnmounted(() => {
|
|||||||
<template v-if="hasRowSlot" #custom-task-content="rowScope">
|
<template v-if="hasRowSlot" #custom-task-content="rowScope">
|
||||||
<slot name="custom-task-content" v-bind="rowScope" />
|
<slot name="custom-task-content" v-bind="rowScope" />
|
||||||
</template>
|
</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>
|
</TaskRow>
|
||||||
|
|
||||||
<div class="task-list-body-spacer" :style="{ height: `${endSpacerHeight}px` }"></div>
|
<div class="task-list-body-spacer" :style="{ height: `${endSpacerHeight}px` }"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 type { StyleValue } from 'vue'
|
||||||
import { useI18n } from '../../../composables/useI18n'
|
import { useI18n } from '../../../composables/useI18n'
|
||||||
import { formatPredecessorDisplay } from '../../../utils/predecessorUtils'
|
import { formatPredecessorDisplay } from '../../../utils/predecessorUtils'
|
||||||
@@ -72,6 +72,17 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
'custom-task-content'(props: TaskRowSlotProps): unknown
|
'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()
|
const { t } = useI18n()
|
||||||
@@ -82,6 +93,32 @@ const daysText = computed(() => t.value?.days ?? '')
|
|||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
|
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,其他直接传值
|
// 性能优化:只对会变化的props使用toRef,其他直接传值
|
||||||
const taskRef = toRef(props, 'task')
|
const taskRef = toRef(props, 'task')
|
||||||
const levelRef = toRef(props, 'level')
|
const levelRef = toRef(props, 'level')
|
||||||
@@ -112,7 +149,7 @@ const {
|
|||||||
contextMenuVisible,
|
contextMenuVisible,
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
contextMenuTask,
|
contextMenuTask,
|
||||||
handleContextMenu,
|
handleContextMenu: handleContextMenuBase,
|
||||||
closeContextMenu,
|
closeContextMenu,
|
||||||
handleTaskDelete,
|
handleTaskDelete,
|
||||||
timerElapsed,
|
timerElapsed,
|
||||||
@@ -120,6 +157,15 @@ const {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} = useTaskRowContextMenu(taskRef, emit as any)
|
} = useTaskRowContextMenu(taskRef, emit as any)
|
||||||
|
|
||||||
|
// 包装 handleContextMenu 以添加权限检查
|
||||||
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
|
if (!shouldShowAnyContextMenu.value) {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleContextMenuBase(event)
|
||||||
|
}
|
||||||
|
|
||||||
// TaskRow拖拽功能
|
// TaskRow拖拽功能
|
||||||
const taskRowRef = ref<HTMLElement | null>(null)
|
const taskRowRef = ref<HTMLElement | null>(null)
|
||||||
const isSplitterDragging = ref(false)
|
const isSplitterDragging = ref(false)
|
||||||
@@ -371,7 +417,9 @@ const slotPayload = computed(() => ({
|
|||||||
TaskRow只负责渲染单个任务行,不再递归渲染子任务
|
TaskRow只负责渲染单个任务行,不再递归渲染子任务
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- 默认右键菜单 -->
|
||||||
<TaskContextMenu
|
<TaskContextMenu
|
||||||
|
v-if="shouldShowDefaultContextMenu"
|
||||||
:visible="contextMenuVisible"
|
:visible="contextMenuVisible"
|
||||||
:task="contextMenuTask"
|
:task="contextMenuTask"
|
||||||
:position="contextMenuPosition"
|
:position="contextMenuPosition"
|
||||||
@@ -382,6 +430,21 @@ const slotPayload = computed(() => ({
|
|||||||
@add-successor="$emit('add-successor', props.task)"
|
@add-successor="$emit('add-successor', props.task)"
|
||||||
@delete="handleTaskDelete"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4359,6 +4359,12 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
</template>
|
</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>
|
</TaskBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user