Files
jordium-gantt-vue3/src/components/MilestonePoint.vue
T

365 lines
9.6 KiB
Vue

<script setup lang="ts">
import { computed, ref, onUnmounted } from 'vue'
import type { Milestone } from '../models/classes/Milestone'
interface Props {
date: string // 里程碑日期
rowHeight: number
dayWidth: number
startDate: Date
name?: string
milestone?: Milestone // 完整的里程碑数据
}
const props = defineProps<Props>()
// 添加事件定义
const emit = defineEmits<{
'milestone-double-click': [milestone: Milestone]
'update:milestone': [milestone: Milestone] // 新增里程碑更新事件
'drag-end': [milestone: Milestone] // 新增
}>()
// 拖拽相关状态
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartLeft = ref(0)
const tempMilestoneData = ref<{ startDate?: string } | null>(null)
// 双击事件处理
const handleDoubleClick = (e: MouseEvent) => {
// 阻止事件冒泡和默认行为
e.preventDefault()
e.stopPropagation()
// 清理任何可能残留的拖拽状态
isDragging.value = false
tempMilestoneData.value = null
// 移除可能残留的事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
if (props.milestone) {
emit('milestone-double-click', props.milestone)
} else {
// 如果没有完整数据,构造基本的里程碑对象
const basicMilestone: Milestone = {
name: props.name || '里程碑',
startDate: props.date,
type: 'milestone',
}
emit('milestone-double-click', basicMilestone)
}
}
// 日期工具函数
const addDaysToLocalDate = (date: Date, days: number): Date => {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
const formatDateToLocalString = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 拖拽事件处理
const handleMouseDown = (e: MouseEvent) => {
// 如果正在双击过程中,不启动拖拽
e.preventDefault()
e.stopPropagation()
// 设置拖拽状态,但不立即开始拖拽
dragStartX.value = e.clientX
dragStartLeft.value = parseInt(milestoneStyle.value.left)
tempMilestoneData.value = null
// 添加全局事件监听器
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
const handleMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - dragStartX.value
// 只有在真正移动了一定距离后才开始拖拽(避免意外触发)
if (Math.abs(deltaX) > 3) {
isDragging.value = true
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
// 只更新临时数据,不触发事件
tempMilestoneData.value = {
startDate: formatDateToLocalString(newStartDate),
}
}
}
const handleMouseUp = () => {
// 只有在真正拖拽了(有临时数据)且状态为拖拽中时才触发更新
if (isDragging.value && tempMilestoneData.value && props.milestone) {
const updatedMilestone = {
...props.milestone,
...tempMilestoneData.value,
}
emit('update:milestone', updatedMilestone)
emit('drag-end', updatedMilestone)
}
// 重置所有拖拽状态
isDragging.value = false
tempMilestoneData.value = null
// 移除事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
// 计算菱形位置 - 考虑拖拽临时数据
const milestoneStyle = computed(() => {
const milestoneDate = tempMilestoneData.value?.startDate
? new Date(tempMilestoneData.value.startDate)
: new Date(props.date)
// 修正:props.startDate 可能为 undefined,需防御性处理
if (!props.startDate || isNaN(new Date(props.date).getTime())) {
return {
left: '0px',
top: '0px',
width: 'auto',
height: 'auto',
}
}
const startDiff = Math.floor(
(milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24),
)
const size = Math.min(props.rowHeight, props.dayWidth * 1.2, 24)
return {
left: `${startDiff * props.dayWidth + props.dayWidth / 2 - size / 2}px`,
top: `${(props.rowHeight - size) / 2}px`,
width: 'auto',
height: 'auto',
}
})
// 里程碑统一使用红色配色
const milestoneColor = computed(() => {
// 使用危险色(红色)统一里程碑配色
return 'var(--gantt-danger, #f56c6c)'
})
const milestoneBorder = computed(() => {
// 稍浅的红色作为边框
return 'var(--gantt-danger-light, #fab6b6)'
})
// 计算里程碑图标类型
const milestoneIcon = computed(() => {
return props.milestone?.icon || 'diamond' // 默认为菱形
})
// 组件销毁时清理事件监听器
onUnmounted(() => {
// 清理拖拽状态
isDragging.value = false
tempMilestoneData.value = null
// 移除事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
})
</script>
<template>
<div
class="milestone"
:style="milestoneStyle"
:title="props.name || '里程碑'"
:class="{ dragging: isDragging }"
@dblclick="handleDoubleClick"
@mousedown="handleMouseDown"
>
<svg :width="24" :height="24" :viewBox="`0 0 24 24`">
<!-- 菱形图标 -->
<g v-if="milestoneIcon === 'diamond'" transform="rotate(45 16 16)">
<rect
x="4"
y="8"
width="15"
height="15"
rx="6"
ry="6"
:fill="milestoneColor"
:stroke="milestoneBorder"
stroke-width="2"
/>
</g>
<!-- 火箭图标 -->
<g v-else-if="milestoneIcon === 'rocket'">
<foreignObject x="0" y="0" width="24" height="24">
<div class="rocket-emoji">🚀</div>
</foreignObject>
</g>
<!-- 默认菱形图标 -->
<g v-else transform="rotate(45 16 16)">
<rect
x="4"
y="8"
width="15"
height="15"
rx="6"
ry="6"
:fill="milestoneColor"
:stroke="milestoneBorder"
stroke-width="2"
/>
</g>
</svg>
<span v-if="props.name" class="milestone-label milestone-label-right">{{ props.name }}</span>
</div>
</template>
<style scoped>
@import '../styles/theme-variables.css';
.milestone {
position: absolute;
z-index: 120;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
user-select: none;
}
/* 里程碑SVG发光效果 */
.milestone svg {
filter: drop-shadow(0 0 8px var(--gantt-danger, #f56c6c));
animation: milestone-glow 2s ease-in-out infinite alternate;
}
/* 里程碑发光动画 */
@keyframes milestone-glow {
from {
filter: drop-shadow(0 0 4px var(--gantt-danger, #f56c6c));
}
to {
filter: drop-shadow(0 0 12px var(--gantt-danger, #f56c6c))
drop-shadow(0 0 20px rgba(245, 108, 108, 0.3));
}
}
/* 悬停时增强发光效果 */
.milestone:hover svg {
filter: drop-shadow(0 0 16px var(--gantt-danger, #f56c6c))
drop-shadow(0 0 24px rgba(245, 108, 108, 0.4));
animation: milestone-glow-intense 1.5s ease-in-out infinite alternate;
}
@keyframes milestone-glow-intense {
from {
filter: drop-shadow(0 0 12px var(--gantt-danger, #f56c6c))
drop-shadow(0 0 20px rgba(245, 108, 108, 0.4));
}
to {
filter: drop-shadow(0 0 20px var(--gantt-danger, #f56c6c))
drop-shadow(0 0 32px rgba(245, 108, 108, 0.6));
}
}
.milestone-label {
font-size: 12px;
font-weight: bold;
color: var(--gantt-text-primary, #222);
white-space: nowrap;
}
.milestone-label-right {
margin-left: 5px;
align-self: center;
}
/* 火箭emoji样式 */
.rocket-emoji {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
line-height: 1;
transform: rotate(-45deg);
transition: transform 0.3s ease;
}
/* 火箭emoji悬停效果 */
.milestone:hover .rocket-emoji {
transform: rotate(-45deg) scale(1.1);
}
/* 暗黑模式下的适配 */
:global(html[data-theme='dark']) .milestone-label {
color: var(--gantt-text-white, #ffffff) !important;
}
:global(html[data-theme='dark']) .milestone svg {
filter: drop-shadow(0 0 8px var(--gantt-danger, #f67c7c));
animation: milestone-glow-dark 2s ease-in-out infinite alternate;
}
:global(html[data-theme='dark']) .milestone:hover svg {
filter: drop-shadow(0 0 16px var(--gantt-danger, #f67c7c))
drop-shadow(0 0 24px rgba(246, 124, 124, 0.4));
animation: milestone-glow-intense-dark 1.5s ease-in-out infinite alternate;
}
/* 暗黑模式发光动画 */
@keyframes milestone-glow-dark {
from {
filter: drop-shadow(0 0 4px var(--gantt-danger, #f67c7c));
}
to {
filter: drop-shadow(0 0 12px var(--gantt-danger, #f67c7c))
drop-shadow(0 0 20px rgba(246, 124, 124, 0.3));
}
}
@keyframes milestone-glow-intense-dark {
from {
filter: drop-shadow(0 0 12px var(--gantt-danger, #f67c7c))
drop-shadow(0 0 20px rgba(246, 124, 124, 0.4));
}
to {
filter: drop-shadow(0 0 20px var(--gantt-danger, #f67c7c))
drop-shadow(0 0 32px rgba(246, 124, 124, 0.6));
}
}
/* 拖拽状态样式 */
.milestone.dragging {
z-index: 1000;
opacity: 0.8;
transform: scale(1.1);
cursor: grabbing;
}
.milestone.dragging svg {
filter: drop-shadow(0 0 20px var(--gantt-danger, #f56c6c))
drop-shadow(0 0 32px rgba(245, 108, 108, 0.6));
animation: none;
}
:global(html[data-theme='dark']) .milestone.dragging svg {
filter: drop-shadow(0 0 20px var(--gantt-danger, #f67c7c))
drop-shadow(0 0 32px rgba(246, 124, 124, 0.6));
}
</style>