v1.8.1 # Conflicts: # .gitignore # package.json # src/components/LinkAnchor.vue # src/components/TaskList/taskRow/TaskRow.vue # src/components/Timeline.vue
270 lines
6.4 KiB
Vue
270 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, onUnmounted, inject } from 'vue'
|
|
|
|
interface Props {
|
|
// 触点类型
|
|
type: 'predecessor' | 'successor' // left触点=前置, right触点=后置
|
|
taskId: number
|
|
// 显示控制
|
|
visible?: boolean // 是否显示(默认在 hover 时显示)
|
|
// 拖拽状态
|
|
isDragSource?: boolean // 是否是拖拽源
|
|
isDragTarget?: boolean // 是否是拖拽目标
|
|
isValidTarget?: boolean // 是否是合法目标
|
|
// 功能开关
|
|
enabled?: boolean // 是否启用连接功能(默认 true)
|
|
// 全局拖拽状态(用于优化显示逻辑)
|
|
globalDragging?: boolean // 是否有全局拖拽进行中
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
visible: false,
|
|
isDragSource: false,
|
|
isDragTarget: false,
|
|
isValidTarget: true,
|
|
enabled: true,
|
|
globalDragging: false,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'drag-start': [{ taskId: number; type: 'predecessor' | 'successor'; x: number; y: number }]
|
|
'drag-move': [{ x: number; y: number }]
|
|
'drag-end': [{ taskId: number; type: 'predecessor' | 'successor' }]
|
|
}>()
|
|
|
|
// 注入视图模式,在资源视图下禁用LinkAnchor
|
|
const viewMode = inject<any>('gantt-view-mode', { value: 'task' })
|
|
|
|
// 触点自身悬停状态
|
|
const isHoveredAnchor = ref(false)
|
|
|
|
// 优化的显示逻辑
|
|
const shouldShow = computed(() => {
|
|
// 在资源视图下禁用LinkAnchor
|
|
if (viewMode.value === 'resource') {
|
|
return false
|
|
}
|
|
|
|
// 在以下情况显示触点:
|
|
// 1. TaskBar 被悬停
|
|
// 2. 全局有拖拽操作进行中(其他任务正在拖拽连接线)
|
|
// 3. 当前是拖拽的源任务
|
|
// 4. 当前是拖拽的目标任务
|
|
// 5. 触点本身被悬停
|
|
return (
|
|
props.visible || // TaskBar 悬停
|
|
props.globalDragging || // 全局拖拽进行中
|
|
props.isDragSource || // 是拖拽的源任务
|
|
props.isDragTarget || // 是拖拽的目标任务
|
|
isHoveredAnchor.value // 触点本身被悬停
|
|
)
|
|
})
|
|
|
|
// 全局拖拽监听器管理(组件自治)
|
|
let globalMouseMoveListener: ((e: MouseEvent) => void) | null = null
|
|
let globalMouseUpListener: ((e: MouseEvent) => void) | null = null
|
|
let isDragging = false
|
|
|
|
// 开始拖拽
|
|
function handleMouseDown(event: MouseEvent) {
|
|
if (!props.enabled) return
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
|
|
isDragging = true
|
|
|
|
// 发射拖拽开始事件
|
|
emit('drag-start', {
|
|
taskId: props.taskId,
|
|
type: props.type,
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
})
|
|
|
|
// 添加全局监听器(组件自治)
|
|
globalMouseMoveListener = (e: MouseEvent) => {
|
|
if (!isDragging) return
|
|
// 发射拖拽移动事件
|
|
emit('drag-move', {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
})
|
|
}
|
|
|
|
globalMouseUpListener = () => {
|
|
if (!isDragging) return
|
|
// 发射拖拽结束事件
|
|
emit('drag-end', {
|
|
taskId: props.taskId,
|
|
type: props.type,
|
|
})
|
|
cleanup()
|
|
}
|
|
|
|
document.addEventListener('mousemove', globalMouseMoveListener)
|
|
document.addEventListener('mouseup', globalMouseUpListener)
|
|
}
|
|
|
|
function handleMouseEnter() {
|
|
if (!props.enabled) return
|
|
isHoveredAnchor.value = true
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
if (!props.enabled) return
|
|
isHoveredAnchor.value = false
|
|
}
|
|
|
|
// 清理全局监听器
|
|
function cleanup() {
|
|
isDragging = false
|
|
if (globalMouseMoveListener) {
|
|
document.removeEventListener('mousemove', globalMouseMoveListener)
|
|
globalMouseMoveListener = null
|
|
}
|
|
if (globalMouseUpListener) {
|
|
document.removeEventListener('mouseup', globalMouseUpListener)
|
|
globalMouseUpListener = null
|
|
}
|
|
}
|
|
|
|
// 组件卸载时清理
|
|
onUnmounted(() => {
|
|
cleanup()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="enabled && shouldShow"
|
|
class="link-anchor"
|
|
:class="{
|
|
'anchor-predecessor': type === 'predecessor',
|
|
'anchor-successor': type === 'successor',
|
|
'drag-source': isDragSource,
|
|
'drag-target': isDragTarget,
|
|
'valid-target': isDragTarget && isValidTarget,
|
|
'invalid-target': isDragTarget && !isValidTarget,
|
|
'visible': shouldShow,
|
|
}"
|
|
@mousedown="handleMouseDown"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
>
|
|
<!-- Tooltip -->
|
|
<div v-if="!isDragSource && !isDragTarget && !globalDragging" class="anchor-tooltip">
|
|
{{ type === 'predecessor' ? '添加前置任务' : '添加后置任务' }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.link-anchor {
|
|
position: absolute;
|
|
top: 50%;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #409eff;
|
|
border: 2px solid #fff;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition:
|
|
opacity 0.2s,
|
|
transform 0.2s;
|
|
z-index: 200;
|
|
/* 默认状态:垂直居中 */
|
|
transform: translateY(-50%) scale(1);
|
|
}
|
|
|
|
/* 前置任务触点(左侧) */
|
|
.link-anchor.anchor-predecessor {
|
|
left: -4px;
|
|
}
|
|
|
|
/* 后置任务触点(右侧) */
|
|
.link-anchor.anchor-successor {
|
|
right: -4px;
|
|
}
|
|
|
|
/* 当应该显示时,设置可见 */
|
|
.link-anchor.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
.link-anchor:hover {
|
|
/* 保持垂直居中的同时放大 */
|
|
transform: translateY(-50%) scale(1.3);
|
|
box-shadow: 0 0 8px rgba(64, 158, 255, 0.6);
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
.link-anchor.drag-source {
|
|
opacity: 1;
|
|
/* 拖拽源也保持垂直居中 */
|
|
transform: translateY(-50%) scale(1);
|
|
}
|
|
|
|
.link-anchor.drag-target {
|
|
opacity: 1;
|
|
/* 拖拽目标:保持垂直居中并放大 */
|
|
transform: translateY(-50%) scale(1.5);
|
|
animation: pulse 0.8s infinite;
|
|
}
|
|
|
|
.link-anchor.valid-target {
|
|
background: #67c23a;
|
|
border-color: #fff;
|
|
}
|
|
|
|
.link-anchor.invalid-target {
|
|
background: #f56c6c;
|
|
border-color: #fff;
|
|
}
|
|
|
|
.anchor-tooltip {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(-4px);
|
|
padding: 4px 8px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.link-anchor:hover .anchor-tooltip {
|
|
opacity: 1;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 8px rgba(64, 158, 255, 0.6);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 16px rgba(64, 158, 255, 1);
|
|
}
|
|
}
|
|
|
|
/* 暗色主题支持 */
|
|
:global(.gantt-root[data-theme='dark']) .link-anchor {
|
|
border-color: #1a1a1a;
|
|
}
|
|
|
|
:global(.gantt-root[data-theme='dark']) .link-anchor.valid-target,
|
|
:global(.gantt-root[data-theme='dark']) .link-anchor.invalid-target {
|
|
border-color: #1a1a1a;
|
|
}
|
|
|
|
:global(.gantt-root[data-theme='dark']) .anchor-tooltip {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
color: #1a1a1a;
|
|
}
|
|
</style>
|