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
+8
View File
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.6] - 2025-07-03
### Changed
- 更新TaskDrawer组件的Progressbar样式
- 可通过拖拽进度条和文本修改的方式更改任务的进度
- Story进度根据子任务集进度改编重新计算
- 同时保留Story进度单独的调整方式
## [1.0.5] - 2025-07-02 ## [1.0.5] - 2025-07-02
### Changed ### Changed
+5 -7
View File
@@ -113,14 +113,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('milestone-deleted', { new CustomEvent('milestone-deleted', {
detail: { milestoneId }, detail: { milestoneId },
}) }),
) )
// 触发强制更新事件,确保Timeline重新渲染 // 触发强制更新事件,确保Timeline重新渲染
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('milestone-data-changed', { new CustomEvent('milestone-data-changed', {
detail: { milestones: milestones.value }, detail: { milestones: milestones.value },
}) }),
) )
} }
@@ -240,8 +240,6 @@ const handleTaskUpdate = (updatedTask: Task) => {
showMessage(`就地更新失败,未找到任务,ID ${updatedTask.id}`, 'warning', { closable: true }) showMessage(`就地更新失败,未找到任务,ID ${updatedTask.id}`, 'warning', { closable: true })
} }
} }
showMessage('任务更新完成', 'success', { closable: false })
} }
// 任务添加处理器 // 任务添加处理器
@@ -252,7 +250,7 @@ const handleTaskAdd = (newTask: Task) => {
const maxId = Math.max( const maxId = Math.max(
...tasks.value.map(t => t.id || 0), ...tasks.value.map(t => t.id || 0),
...milestones.value.map(m => m.id || 0), ...milestones.value.map(m => m.id || 0),
0 0,
) )
newTask.id = maxId + 1 newTask.id = maxId + 1
} }
@@ -383,7 +381,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
`开始: ${oldTask.startDate}${newTask.startDate}\n` + `开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`, `结束: ${oldTask.endDate}${newTask.endDate}`,
'info', 'info',
{ closable: true } { closable: true },
) )
} }
function handleMilestoneDragEnd(newMilestone) { function handleMilestoneDragEnd(newMilestone) {
@@ -393,7 +391,7 @@ function handleMilestoneDragEnd(newMilestone) {
`里程碑【${newMilestone.name}\n` + `里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`, `开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info', 'info',
{ closable: true } { closable: true },
) )
} }
+10
View File
@@ -105,5 +105,15 @@
"version": "1.0.5", "version": "1.0.5",
"date": "2025-07-02", "date": "2025-07-02",
"notes": ["更新README中所有NPM徽章CDN地址"] "notes": ["更新README中所有NPM徽章CDN地址"]
},
{
"version": "1.0.6",
"date": "2025-07-03",
"notes": [
"更新TaskDrawer组件的Progressbar样式",
"可通过拖拽进度条和文本修改的方式更改任务的进度",
"Story进度根据子任务集进度改编重新计算",
"同时保留Story进度单独的调整方式"
]
} }
] ]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jordium-gantt-vue3", "name": "jordium-gantt-vue3",
"version": "1.0.5", "version": "1.0.6",
"type": "module", "type": "module",
"main": "dist/jordium-gantt-vue3.cjs.js", "main": "dist/jordium-gantt-vue3.cjs.js",
"module": "dist/jordium-gantt-vue3.es.js", "module": "dist/jordium-gantt-vue3.es.js",
+6 -4
View File
@@ -1,11 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import GanttChart from '../src/components/GanttChart.vue'
import TaskDrawer from '../src/components/TaskDrawer.vue'
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
import demoData from './data.json' import demoData from './data.json'
import packageInfo from '../package.json' import packageInfo from '../package.json'
// 导入主题变量 // 导入主题变量
import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue' import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import { GanttChart, TaskDrawer, MilestoneDialog, useMessage, Task } from 'jordium-gantt-vue3' import { useMessage } from '../src/composables/useMessage'
import 'jordium-gantt-vue3/dist/jordium-gantt-vue3.css' import type { Task } from '../src/models/Task'
const { showMessage } = useMessage() const { showMessage } = useMessage()
@@ -236,8 +240,6 @@ const handleTaskUpdate = (updatedTask: Task) => {
showMessage(`就地更新失败,未找到任务,ID ${updatedTask.id}`, 'warning', { closable: true }) showMessage(`就地更新失败,未找到任务,ID ${updatedTask.id}`, 'warning', { closable: true })
} }
} }
showMessage('任务更新完成', 'success', { closable: false })
} }
// 任务添加处理器 // 任务添加处理器
+17 -8
View File
@@ -20,7 +20,8 @@
"progress": 100, "progress": 100,
"estimatedHours": 16, "estimatedHours": 16,
"actualHours": 15, "actualHours": 15,
"type": "task" "type": "task",
"parentId": 1
}, },
{ {
"id": 3, "id": 3,
@@ -32,6 +33,7 @@
"estimatedHours": 24, "estimatedHours": 24,
"actualHours": 28, "actualHours": 28,
"type": "story", "type": "story",
"parentId": 1,
"children": [ "children": [
{ {
"id": 4, "id": 4,
@@ -43,7 +45,8 @@
"progress": 80, "progress": 80,
"estimatedHours": 8, "estimatedHours": 8,
"actualHours": 10, "actualHours": 10,
"type": "task" "type": "task",
"parentId": 3
}, },
{ {
"id": 5, "id": 5,
@@ -55,7 +58,8 @@
"progress": 0, "progress": 0,
"estimatedHours": 16, "estimatedHours": 16,
"actualHours": 0, "actualHours": 0,
"type": "task" "type": "task",
"parentId": 3
} }
], ],
"collapsed": false "collapsed": false
@@ -84,7 +88,8 @@
"estimatedHours": 80, "estimatedHours": 80,
"actualHours": 35, "actualHours": 35,
"type": "task", "type": "task",
"predecessor": "5" "predecessor": "5",
"parentId": 7
}, },
{ {
"id": 11, "id": 11,
@@ -96,7 +101,8 @@
"estimatedHours": 80, "estimatedHours": 80,
"actualHours": 0, "actualHours": 0,
"type": "task", "type": "task",
"predecessor": "5" "predecessor": "5",
"parentId": 7
}, },
{ {
"id": 12, "id": 12,
@@ -108,7 +114,8 @@
"estimatedHours": 88, "estimatedHours": 88,
"actualHours": 0, "actualHours": 0,
"type": "task", "type": "task",
"predecessor": "5" "predecessor": "5",
"parentId": 7
} }
], ],
"collapsed": false "collapsed": false
@@ -134,7 +141,8 @@
"estimatedHours": 48, "estimatedHours": 48,
"actualHours": 0, "actualHours": 0,
"type": "task", "type": "task",
"predecessor": "7" "predecessor": "7",
"parentId": 13
}, },
{ {
"id": 15, "id": 15,
@@ -146,7 +154,8 @@
"estimatedHours": 24, "estimatedHours": 24,
"actualHours": 0, "actualHours": 0,
"type": "task", "type": "task",
"predecessor": "7" "predecessor": "7",
"parentId": 13
} }
], ],
"collapsed": false "collapsed": false
+60 -1
View File
@@ -902,13 +902,72 @@ const handleMilestoneSave = (milestone: Task) => {
// //
const handleTaskUpdate = (event: CustomEvent) => { const handleTaskUpdate = (event: CustomEvent) => {
const updatedTask = event.detail const updatedTask = event.detail
if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
// taskparentIdstory
if (updatedTask.type === 'task' && updatedTask.parentId) {
const updatedStory = calculateStoryProgress(updatedTask.parentId, updatedTask)
//
if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
props.onTaskUpdate(updatedTask)
}
// storystory
if (updatedStory && props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
props.onTaskUpdate(updatedStory)
}
} else if (props.onTaskUpdate && typeof props.onTaskUpdate === 'function') {
//
props.onTaskUpdate(updatedTask) props.onTaskUpdate(updatedTask)
} }
// Timeline // Timeline
updateTaskTrigger.value++ updateTaskTrigger.value++
} }
// storytask
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
// storytask
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 handleTaskAdd = (event: CustomEvent) => {
const newTask = event.detail const newTask = event.detail
+34 -8
View File
@@ -27,7 +27,19 @@ const dragStartLeft = ref(0)
const tempMilestoneData = ref<{ startDate?: string } | null>(null) 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) { if (props.milestone) {
emit('milestone-double-click', props.milestone) emit('milestone-double-click', props.milestone)
} else { } else {
@@ -57,10 +69,11 @@ const formatDateToLocalString = (date: Date): string => {
// //
const handleMouseDown = (e: MouseEvent) => { const handleMouseDown = (e: MouseEvent) => {
//
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
isDragging.value = true //
dragStartX.value = e.clientX dragStartX.value = e.clientX
dragStartLeft.value = parseInt(milestoneStyle.value.left) dragStartLeft.value = parseInt(milestoneStyle.value.left)
tempMilestoneData.value = null tempMilestoneData.value = null
@@ -71,8 +84,12 @@ const handleMouseDown = (e: MouseEvent) => {
} }
const handleMouseMove = (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 newLeft = Math.max(0, dragStartLeft.value + deltaX)
const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
@@ -84,17 +101,21 @@ const handleMouseMove = (e: MouseEvent) => {
} }
const handleMouseUp = () => { const handleMouseUp = () => {
// //
if (tempMilestoneData.value && props.milestone) { if (isDragging.value && tempMilestoneData.value && props.milestone) {
const updatedMilestone = { const updatedMilestone = {
...props.milestone, ...props.milestone,
...tempMilestoneData.value, ...tempMilestoneData.value,
} }
emit('update:milestone', updatedMilestone) emit('update:milestone', updatedMilestone)
emit('drag-end', updatedMilestone) // emit('drag-end', updatedMilestone)
tempMilestoneData.value = null
} }
//
isDragging.value = false isDragging.value = false
tempMilestoneData.value = null
//
document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp) document.removeEventListener('mouseup', handleMouseUp)
} }
@@ -144,6 +165,11 @@ const milestoneIcon = computed(() => {
// //
onUnmounted(() => { onUnmounted(() => {
//
isDragging.value = false
tempMilestoneData.value = null
//
document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp) document.removeEventListener('mouseup', handleMouseUp)
}) })
+10 -4
View File
@@ -306,11 +306,17 @@ watch(
const handleTaskBarDoubleClick = (e: MouseEvent) => { const handleTaskBarDoubleClick = (e: MouseEvent) => {
// //
e.stopPropagation() e.stopPropagation()
e.preventDefault()
// //
if (isDragging.value || isResizingLeft.value || isResizingRight.value) { isDragging.value = false
return isResizingLeft.value = false
} isResizingRight.value = false
tempTaskData.value = null
//
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
// //
if (props.onDoubleClick && typeof props.onDoubleClick === 'function') { if (props.onDoubleClick && typeof props.onDoubleClick === 'function') {
+7 -35
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch, computed, onMounted, onUnmounted } from 'vue' import { ref, reactive, watch, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from '../composables/useI18n' import { useI18n } from '../composables/useI18n'
import { useMessage } from '../composables/useMessage'
import DatePicker from './DatePicker.vue' import DatePicker from './DatePicker.vue'
import GanttConfirmDialog from './GanttConfirmDialog.vue' import GanttConfirmDialog from './GanttConfirmDialog.vue'
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
@@ -28,6 +29,7 @@ const emit = defineEmits<{
}>() }>()
const { t } = useI18n() const { t } = useI18n()
const { showMessage } = useMessage()
const submitting = ref(false) const submitting = ref(false)
const isVisible = ref(props.visible) const isVisible = ref(props.visible)
@@ -55,7 +57,7 @@ const allTasks = ref<Task[]>([])
// type="task" // type="task"
const availablePredecessorTasks = computed(() => { const availablePredecessorTasks = computed(() => {
return allTasks.value.filter( 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( .filter(
task => task =>
task.id !== props.task?.id && // task.id !== props.task?.id && //
(task.type === 'story' || task.type === 'task') // storytask (task.type === 'story' || task.type === 'task'), // storytask
) )
.map(task => ({ .map(task => ({
...task, ...task,
@@ -191,7 +193,7 @@ watch(
// //
window.dispatchEvent(new CustomEvent('request-task-list')) window.dispatchEvent(new CustomEvent('request-task-list'))
} }
} },
) )
// isVisible // isVisible
@@ -278,8 +280,7 @@ const handleSubmit = async () => {
showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success') showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success')
handleClose() handleClose()
} catch (error) { } catch (error) {
// lint //
console.error(error)
showMessage(t.value.operationFailed, 'error') showMessage(t.value.operationFailed, 'error')
} finally { } finally {
submitting.value = false submitting.value = false
@@ -327,42 +328,13 @@ onUnmounted(() => {
window.removeEventListener('task-list-updated', handleTasksChanged as EventListener) 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 // formData.progress
watch( watch(
() => formData.progress, () => formData.progress,
newValue => { newValue => {
progressDisplayValue.value = (newValue || 0).toString() progressDisplayValue.value = (newValue || 0).toString()
}, },
{ immediate: true } { immediate: true },
) )
</script> </script>
+25 -1
View File
@@ -31,6 +31,10 @@ function getContainer() {
return container return container
} }
// 防重复消息队列,存储最近的消息内容和时间
const recentMessages = new Map<string, number>()
const DUPLICATE_THRESHOLD = 1000 // 1秒内相同消息视为重复
export function useMessage() { export function useMessage() {
/** /**
* @param message * @param message
@@ -38,12 +42,32 @@ export function useMessage() {
* @param options * @param options
* - duration: 自动关闭的毫秒数30000 * - duration: 自动关闭的毫秒数30000
* - closable: 是否显示关闭按钮true=false= * - closable: 是否显示关闭按钮true=false=
* - allowDuplicate: 是否允许重复消息false
*/ */
const showMessage = ( const showMessage = (
message: string, message: string,
type: MessageType = 'success', type: MessageType = 'success',
options?: { duration?: number; closable?: boolean }, options?: { duration?: number; closable?: boolean; allowDuplicate?: boolean },
) => { ) => {
// 防重复检查
const now = Date.now()
const messageKey = `${type}:${message}`
const lastTime = recentMessages.get(messageKey)
if (!options?.allowDuplicate && lastTime && now - lastTime < DUPLICATE_THRESHOLD) {
// 重复消息,忽略
return
}
// 记录消息时间
recentMessages.set(messageKey, now)
// 清理过期的消息记录
for (const [key, time] of recentMessages.entries()) {
if (now - time > DUPLICATE_THRESHOLD * 2) {
recentMessages.delete(key)
}
}
const messageEl = document.createElement('div') const messageEl = document.createElement('div')
messageEl.className = `message ${type}` messageEl.className = `message ${type}`
messageEl.textContent = message messageEl.textContent = message