merge src/master
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
|
||||
interface DragState {
|
||||
isDragging: boolean
|
||||
draggedTask: Task | null
|
||||
draggedElement: HTMLElement | null
|
||||
ghostElement: HTMLElement | null
|
||||
dropTargetTask: Task | null
|
||||
dropPosition: 'after' | 'child' | null
|
||||
}
|
||||
|
||||
interface UseDragOptions {
|
||||
enabled: boolean
|
||||
onDragStart?: (task: Task) => void
|
||||
onDragOver?: (task: Task, position: 'after' | 'child') => void
|
||||
onDrop?: (draggedTask: Task, targetTask: Task, position: 'after' | 'child') => void
|
||||
}
|
||||
|
||||
export function useTaskRowDrag(options: UseDragOptions) {
|
||||
const dragState = ref<DragState>({
|
||||
isDragging: false,
|
||||
draggedTask: null,
|
||||
draggedElement: null,
|
||||
ghostElement: null,
|
||||
dropTargetTask: null,
|
||||
dropPosition: null,
|
||||
})
|
||||
|
||||
const startDrag = (task: Task, element: HTMLElement, event: MouseEvent) => {
|
||||
if (!options.enabled) return
|
||||
|
||||
// 防止里程碑分组、里程碑被拖拽
|
||||
if (task.type === 'milestone-group' || task.type === 'milestone') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
dragState.value.isDragging = true
|
||||
dragState.value.draggedTask = task
|
||||
dragState.value.draggedElement = element
|
||||
|
||||
// 创建拖拽的幽灵元素
|
||||
const ghost = element.cloneNode(true) as HTMLElement
|
||||
ghost.style.position = 'fixed'
|
||||
ghost.style.left = '-9999px'
|
||||
ghost.style.top = '-9999px'
|
||||
ghost.style.width = `${element.offsetWidth}px`
|
||||
ghost.style.height = `${element.offsetHeight}px`
|
||||
ghost.style.opacity = '0.6'
|
||||
ghost.style.pointerEvents = 'none'
|
||||
ghost.style.zIndex = '999999'
|
||||
ghost.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.3)'
|
||||
ghost.classList.add('task-row-dragging')
|
||||
|
||||
// 查找全屏容器或使用body
|
||||
const fullscreenContainer = document.querySelector('.gantt-fullscreen') || document.body
|
||||
fullscreenContainer.appendChild(ghost)
|
||||
dragState.value.ghostElement = ghost
|
||||
|
||||
// 设置原始元素为半透明
|
||||
element.style.opacity = '0.4'
|
||||
|
||||
// 禁用页面选择
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.webkitUserSelect = 'none'
|
||||
document.body.style.cursor = 'move'
|
||||
|
||||
// 广播拖拽开始事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('task-row-drag-start', {
|
||||
detail: { task },
|
||||
}),
|
||||
)
|
||||
|
||||
if (options.onDragStart) {
|
||||
options.onDragStart(task)
|
||||
}
|
||||
|
||||
// 初始定位幽灵元素
|
||||
updateGhostPosition(event)
|
||||
}
|
||||
|
||||
const updateGhostPosition = (event: MouseEvent) => {
|
||||
if (dragState.value.ghostElement) {
|
||||
const ghost = dragState.value.ghostElement
|
||||
ghost.style.left = `${event.clientX + 10}px`
|
||||
ghost.style.top = `${event.clientY - 25}px`
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (task: Task, element: HTMLElement, event: MouseEvent) => {
|
||||
if (!dragState.value.isDragging || !dragState.value.draggedTask) return
|
||||
|
||||
// 不能拖拽到自己身上
|
||||
if (task.id === dragState.value.draggedTask.id) {
|
||||
clearDropTarget()
|
||||
return
|
||||
}
|
||||
|
||||
// 不能拖拽到自己的子任务上
|
||||
if (isDescendant(dragState.value.draggedTask, task)) {
|
||||
clearDropTarget()
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// 确定放置位置:根据是否有子任务
|
||||
const hasChildren = task.children && task.children.length > 0
|
||||
const position: 'after' | 'child' = hasChildren ? 'child' : 'after'
|
||||
|
||||
dragState.value.dropTargetTask = task
|
||||
dragState.value.dropPosition = position
|
||||
|
||||
// 移除之前的高亮
|
||||
document.querySelectorAll('.task-row-drop-target').forEach(el => {
|
||||
el.classList.remove('task-row-drop-target', 'drop-after', 'drop-child')
|
||||
})
|
||||
|
||||
// 添加新的高亮
|
||||
element.classList.add('task-row-drop-target')
|
||||
if (position === 'after') {
|
||||
element.classList.add('drop-after')
|
||||
} else {
|
||||
element.classList.add('drop-child')
|
||||
}
|
||||
|
||||
if (options.onDragOver) {
|
||||
options.onDragOver(task, position)
|
||||
}
|
||||
}
|
||||
|
||||
const clearDropTarget = () => {
|
||||
dragState.value.dropTargetTask = null
|
||||
dragState.value.dropPosition = null
|
||||
document.querySelectorAll('.task-row-drop-target').forEach(el => {
|
||||
el.classList.remove('task-row-drop-target', 'drop-after', 'drop-child')
|
||||
})
|
||||
}
|
||||
|
||||
const endDrag = () => {
|
||||
if (!dragState.value.isDragging) return
|
||||
|
||||
const draggedTask = dragState.value.draggedTask
|
||||
const dropTargetTask = dragState.value.dropTargetTask
|
||||
const dropPosition = dragState.value.dropPosition
|
||||
|
||||
// 恢复原始元素样式
|
||||
if (dragState.value.draggedElement) {
|
||||
dragState.value.draggedElement.style.opacity = ''
|
||||
}
|
||||
|
||||
// 移除幽灵元素
|
||||
if (dragState.value.ghostElement && dragState.value.ghostElement.parentElement) {
|
||||
dragState.value.ghostElement.parentElement.removeChild(dragState.value.ghostElement)
|
||||
}
|
||||
|
||||
// 清除高亮
|
||||
clearDropTarget()
|
||||
|
||||
// 恢复页面选择
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.webkitUserSelect = ''
|
||||
document.body.style.cursor = ''
|
||||
|
||||
// 广播拖拽结束事件
|
||||
window.dispatchEvent(new CustomEvent('task-row-drag-end'))
|
||||
|
||||
// 如果有有效的放置目标,触发drop回调
|
||||
if (draggedTask && dropTargetTask && dropPosition && options.onDrop) {
|
||||
options.onDrop(draggedTask, dropTargetTask, dropPosition)
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
dragState.value = {
|
||||
isDragging: false,
|
||||
draggedTask: null,
|
||||
draggedElement: null,
|
||||
ghostElement: null,
|
||||
dropTargetTask: null,
|
||||
dropPosition: null,
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (dragState.value.isDragging) {
|
||||
updateGhostPosition(event)
|
||||
|
||||
// 临时隐藏幽灵元素以检测下方元素
|
||||
const ghost = dragState.value.ghostElement
|
||||
if (ghost) {
|
||||
ghost.style.display = 'none'
|
||||
}
|
||||
|
||||
// 检测鼠标下的元素
|
||||
const elementUnderMouse = document.elementFromPoint(event.clientX, event.clientY)
|
||||
|
||||
// 恢复幽灵元素显示
|
||||
if (ghost) {
|
||||
ghost.style.display = ''
|
||||
}
|
||||
|
||||
if (elementUnderMouse) {
|
||||
// 查找最近的task-row元素
|
||||
const taskRow = elementUnderMouse.closest('.task-row') as HTMLElement
|
||||
if (taskRow && taskRow.dataset.taskId) {
|
||||
const taskId = Number(taskRow.dataset.taskId)
|
||||
|
||||
// 触发全局事件,让TaskRow组件处理
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('task-row-drag-over', {
|
||||
detail: { taskId, event },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (dragState.value.isDragging) {
|
||||
endDrag()
|
||||
}
|
||||
}
|
||||
|
||||
// 检查task2是否是task1的后代
|
||||
const isDescendant = (ancestor: Task, descendant: Task): boolean => {
|
||||
if (!ancestor.children) return false
|
||||
|
||||
for (const child of ancestor.children) {
|
||||
if (child.id === descendant.id) return true
|
||||
if (isDescendant(child, descendant)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
|
||||
// 清理可能残留的幽灵元素
|
||||
if (dragState.value.ghostElement && dragState.value.ghostElement.parentElement) {
|
||||
dragState.value.ghostElement.parentElement.removeChild(dragState.value.ghostElement)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
dragState,
|
||||
startDrag,
|
||||
handleDragOver,
|
||||
endDrag,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user