v1.0.6 - TaskDrawer.Progressbar style update; story progress cal based on sub-tasks' progress;

This commit is contained in:
LINING-PC\lining
2025-07-03 14:31:49 +08:00
parent 97c1d0e36c
commit 06c049f49c
11 changed files with 183 additions and 69 deletions
+60 -1
View File
@@ -902,13 +902,72 @@ const handleMilestoneSave = (milestone: Task) => {
// 处理任务更新事件
const handleTaskUpdate = (event: CustomEvent) => {
const updatedTask = event.detail
if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
// 如果更新的是task类型且有parentId,需要同时更新对应的story进度
if (updatedTask.type === 'task' && updatedTask.parentId) {
const updatedStory = calculateStoryProgress(updatedTask.parentId, updatedTask)
// 先调用外部的任务更新处理器更新子任务
if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
props.onTaskUpdate(updatedTask)
}
// 如果story进度有变化,也更新story
if (updatedStory && props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
props.onTaskUpdate(updatedStory)
}
} else if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
// 普通任务更新
props.onTaskUpdate(updatedTask)
}
// 关键:任务更新后强制刷新Timeline时间轴
updateTaskTrigger.value++
}
// 计算story的进度(根据其下所有task的进度计算)
const calculateStoryProgress = (storyId: number, updatedTask?: Task): Task | null => {
// 获取所有任务的扁平列表
const allTasks = [...(props.tasks || [])]
const flatTasks: Task[] = []
const flattenTasks = (tasks: Task[]) => {
tasks.forEach(task => {
flatTasks.push(task)
if (task.children && task.children.length > 0) {
flattenTasks(task.children)
}
})
}
flattenTasks(allTasks)
// 找到对应的story
const storyTask = flatTasks.find(task => task.id === storyId && task.type === 'story')
if (!storyTask) return null
// 获取该story下所有的task
let childTasks = flatTasks.filter(task => task.parentId === storyId && task.type === 'task')
if (childTasks.length === 0) return null
// 如果有正在更新的task,使用最新的数据替换旧数据
if (updatedTask && updatedTask.type === 'task' && updatedTask.parentId === storyId) {
childTasks = childTasks.map(task => (task.id === updatedTask.id ? updatedTask : task))
}
// 计算平均进度
const totalProgress = childTasks.reduce((sum, task) => sum + (task.progress || 0), 0)
const avgProgress = Math.round(totalProgress / childTasks.length)
// 如果进度有变化,返回更新后的story
if (storyTask.progress !== avgProgress) {
return { ...storyTask, progress: avgProgress }
}
// 进度没有变化,返回null
return null
}
// 处理任务添加事件
const handleTaskAdd = (event: CustomEvent) => {
const newTask = event.detail
+34 -8
View File
@@ -27,7 +27,19 @@ const dragStartLeft = ref(0)
const tempMilestoneData = ref<{ startDate?: string } | null>(null)
// 双击事件处理
const handleDoubleClick = () => {
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 {
@@ -57,10 +69,11 @@ const formatDateToLocalString = (date: Date): string => {
// 拖拽事件处理
const handleMouseDown = (e: MouseEvent) => {
// 如果正在双击过程中,不启动拖拽
e.preventDefault()
e.stopPropagation()
isDragging.value = true
// 设置拖拽状态,但不立即开始拖拽
dragStartX.value = e.clientX
dragStartLeft.value = parseInt(milestoneStyle.value.left)
tempMilestoneData.value = null
@@ -71,8 +84,12 @@ const handleMouseDown = (e: MouseEvent) => {
}
const handleMouseMove = (e: MouseEvent) => {
if (isDragging.value) {
const deltaX = e.clientX - dragStartX.value
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)
@@ -84,17 +101,21 @@ const handleMouseMove = (e: MouseEvent) => {
}
const handleMouseUp = () => {
// 如果有临时数据,说明发生了拖拽,提交数据更新
if (tempMilestoneData.value && props.milestone) {
// 只有在真正拖拽了(有临时数据)且状态为拖拽中时才触发更新
if (isDragging.value && tempMilestoneData.value && props.milestone) {
const updatedMilestone = {
...props.milestone,
...tempMilestoneData.value,
}
emit('update:milestone', updatedMilestone)
emit('drag-end', updatedMilestone) // 新增
tempMilestoneData.value = null
emit('drag-end', updatedMilestone)
}
// 重置所有拖拽状态
isDragging.value = false
tempMilestoneData.value = null
// 移除事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
@@ -144,6 +165,11 @@ const milestoneIcon = computed(() => {
// 组件销毁时清理事件监听器
onUnmounted(() => {
// 清理拖拽状态
isDragging.value = false
tempMilestoneData.value = null
// 移除事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
})
+10 -4
View File
@@ -306,11 +306,17 @@ watch(
const handleTaskBarDoubleClick = (e: MouseEvent) => {
// 阻止事件冒泡,避免触发拖拽等其他事件
e.stopPropagation()
e.preventDefault()
// 如果正在拖拽或调整大小,不触发双击事件
if (isDragging.value || isResizingLeft.value || isResizingRight.value) {
return
}
// 清理任何可能残留的拖拽状态和临时数据
isDragging.value = false
isResizingLeft.value = false
isResizingRight.value = false
tempTaskData.value = null
// 移除可能残留的事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
// 优先调用外部传入的双击处理器
if (props.onDoubleClick && typeof props.onDoubleClick === 'function') {
+7 -35
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, reactive, watch, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from '../composables/useI18n'
import { useMessage } from '../composables/useMessage'
import DatePicker from './DatePicker.vue'
import GanttConfirmDialog from './GanttConfirmDialog.vue'
import type { Task } from '../models/classes/Task'
@@ -28,6 +29,7 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { showMessage } = useMessage()
const submitting = ref(false)
const isVisible = ref(props.visible)
@@ -55,7 +57,7 @@ const allTasks = ref<Task[]>([])
// 获取可作为前置任务的任务列表(只包含type="task"的任务,且不包含当前任务)
const availablePredecessorTasks = computed(() => {
return allTasks.value.filter(
task => task.type === 'task' && task.id !== props.task?.id // 排除当前任务自己
task => task.type === 'task' && task.id !== props.task?.id, // 排除当前任务自己
)
})
@@ -65,7 +67,7 @@ const availableParentTasks = computed(() => {
.filter(
task =>
task.id !== props.task?.id && // 排除当前任务自己
(task.type === 'story' || task.type === 'task') // 只显示story和task类型
(task.type === 'story' || task.type === 'task'), // 只显示story和task类型
)
.map(task => ({
...task,
@@ -191,7 +193,7 @@ watch(
// 抽屉显示时重新请求任务数据,确保前置任务列表是最新的
window.dispatchEvent(new CustomEvent('request-task-list'))
}
}
},
)
// 监听 isVisible 变化,同步到父组件
@@ -278,8 +280,7 @@ const handleSubmit = async () => {
showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success')
handleClose()
} catch (error) {
// 记录异常,保证 lint 通过
console.error(error)
// 处理错误但不在控制台输出
showMessage(t.value.operationFailed, 'error')
} finally {
submitting.value = false
@@ -327,42 +328,13 @@ onUnmounted(() => {
window.removeEventListener('task-list-updated', handleTasksChanged as EventListener)
})
// 简单的消息提示函数
const showMessage = (message: string, type: 'success' | 'error') => {
// 创建消息元素
const messageEl = document.createElement('div')
messageEl.className = `message ${type}`
messageEl.textContent = message
messageEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
font-size: 14px;
z-index: 9999;
background: ${type === 'success' ? '#67c23a' : '#f56c6c'};
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
`
document.body.appendChild(messageEl)
// 3秒后自动移除
setTimeout(() => {
if (messageEl.parentNode) {
document.body.removeChild(messageEl)
}
}, 3000)
}
// 监听 formData.progress 变化,同步更新显示值
watch(
() => formData.progress,
newValue => {
progressDisplayValue.value = (newValue || 0).toString()
},
{ immediate: true }
{ immediate: true },
)
</script>