From de6ce4400066ac9d390c5386f8c576b48fb7e1e5 Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Thu, 29 Jan 2026 23:27:34 +0800 Subject: [PATCH] v1.9.0 - Add resource view --- demo/App.vue | 426 +++++++++++++++++ demo/data-resources.json | 244 ++++++++++ package.json | 2 +- src/components/GanttChart.vue | 61 +++ src/components/GanttLinks.vue | 2 +- src/components/GanttToolbar.vue | 199 ++++++++ src/components/LinkAnchor.vue | 10 +- src/components/TaskBar.vue | 448 ++++++++++++++++-- src/components/TaskList/TaskList.vue | 54 ++- .../taskList/useTaskListEventHandlers.ts | 4 +- src/components/TaskList/taskRow/TaskRow.vue | 123 +++-- src/components/Timeline.vue | 256 +++++++++- src/composables/useI18n.ts | 6 + src/index.ts | 7 + src/models/classes/Resource.ts | 91 ++++ src/models/configs/ResourceListConfig.ts | 80 ++++ src/utils/resourceUtils.ts | 98 ++++ 17 files changed, 2008 insertions(+), 103 deletions(-) create mode 100644 demo/data-resources.json create mode 100644 src/models/classes/Resource.ts create mode 100644 src/models/configs/ResourceListConfig.ts create mode 100644 src/utils/resourceUtils.ts diff --git a/demo/App.vue b/demo/App.vue index 9168f41..f8be33c 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -5,6 +5,7 @@ import { ref, computed, onMounted, nextTick, watch } from 'vue' import MilestoneDialog from '../src/components/MilestoneDialog.vue' import normalData from './data.json' import largeData from './data-large-1m.json' +import resourcesData from './data-resources.json' import packageInfo from '../package.json' // 导入主题变量 import '../src/styles/theme-variables.css' @@ -15,7 +16,9 @@ import { useI18n } from '../src/composables/useI18n' import { useDemoLocale } from './useDemoLocale' import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import type { Task } from '../src/models/Task' +import { Resource } from '../src/models/classes/Resource' import type { TaskListConfig, TaskListColumnConfig } from '../src/models/configs/TaskListConfig' +import type { ResourceListConfig } from '../src/models/configs/ResourceListConfig' import type { TaskBarConfig } from '../src/models/configs/TaskBarConfig' const { showMessage } = useMessage() @@ -33,6 +36,8 @@ const gantt = ref([]) const milestones = ref([]) +const resources = ref([]) +const viewMode = ref<'task' | 'resource'>('task') const rawDataSources = [ { @@ -80,6 +85,56 @@ const applyDataSource = (source: RawDataSource) => { const payload = source.payload as { tasks?: Task[]; milestones?: Task[] } tasks.value = cloneData(payload.tasks ?? []) milestones.value = cloneData(payload.milestones ?? []) + + // 从独立的资源数据文件加载资源数据 + const resourcePayload = resourcesData as { resources?: any[] } + if (resourcePayload.resources) { + resources.value = resourcePayload.resources.map(resData => { + // 使用Resource类创建资源实例 + return new Resource({ + id: resData.id, + name: resData.name, + type: resData.type, + avatar: resData.avatar, + description: resData.description, + department: resData.department, + skills: resData.skills, + utilization: resData.utilization, + tasks: resData.tasks || [] + }) + }) + } else { + // 向后兼容:如果没有独立的资源数据,从任务中生成 + const resourceMap = new Map() + tasks.value.forEach(task => { + if (task.assignee && !resourceMap.has(task.assignee)) { + resourceMap.set(task.assignee, new Resource({ + id: task.assignee, + name: task.assignee, + type: 'user', + avatar: task.assigneeAvatar || undefined, + tasks: [] + })) + } + }) + + // 将任务分配到对应资源 + tasks.value.forEach(task => { + if (task.assignee) { + const resource = resourceMap.get(task.assignee) + if (resource) { + resource.addTask(task) + } + } + }) + + // 计算每个资源的利用率 + resourceMap.forEach(resource => { + resource.updateUtilization() + }) + + resources.value = Array.from(resourceMap.values()) + } } const getSourceTexts = (key: DataSourceKey) => { @@ -216,12 +271,81 @@ const taskListConfig = computed(() => ({ widthUnit.value === '%' ? `${widthPercentage.value.maxWidth}%` : taskListWidth.value.maxWidth, })) +// 资源列表配置 +const resourceListConfig = computed(() => ({ + columns: [ + { + key: 'name', + label: '资源名称', + visible: true, + width: 150, + formatter: (resource: Resource) => resource.name || '-', + }, + { + key: 'type', + label: '类型', + visible: true, + width: 100, + formatter: (resource: Resource) => resource.type || '-', + }, + { + key: 'taskCount', + label: '任务数', + visible: true, + width: 100, + formatter: (resource: Resource) => resource.tasks?.length?.toString() || '0', + }, + { + key: 'utilization', + label: '利用率', + visible: true, + width: 100, + formatter: (resource: Resource) => { + if (resource.utilization != null) { + return `${resource.utilization}%` + } + // 简单计算:基于任务数量的利用率 + const taskCount = resource.tasks?.length || 0 + const utilizationPercent = Math.min(taskCount * 20, 100) // 假设每个任务占20% + return `${utilizationPercent}%` + }, + }, + ], + defaultWidth: + widthUnit.value === '%' + ? `${widthPercentage.value.defaultWidth}%` + : taskListWidth.value.defaultWidth, + minWidth: + widthUnit.value === '%' ? `${widthPercentage.value.minWidth}%` : taskListWidth.value.minWidth, + maxWidth: + widthUnit.value === '%' ? `${widthPercentage.value.maxWidth}%` : taskListWidth.value.maxWidth, +})) + + // 控制是否允许拖拽和拉伸 const allowDragAndResize = ref(true) // 控制是否启用TaskRow拖拽移动 const enableTaskRowMove = ref(true) +// v1.9.0 资源视图垂直拖拽确认对话框 +const resourceDragConfirmVisible = ref(false) +const resourceDragData = ref<{ + task: Task | null + sourceResourceIndex: number + targetResourceIndex: number + targetResource: Resource | null + newStartDate?: string + newEndDate?: string +}>({ + task: null, + sourceResourceIndex: -1, + targetResourceIndex: -1, + targetResource: null, + newStartDate: undefined, + newEndDate: undefined, +}) + // 指派人员选项列表 const assigneeOptions = ref([ { value: 'zhangsan', label: '张三' }, @@ -922,6 +1046,96 @@ const handleTaskRowMoved = async (payload: { // } } +// v1.9.0 处理资源视图垂直拖拽结束事件 +const handleResourceDragEnd = (event: { + task: Task + sourceResourceIndex: number + targetResourceIndex: number + targetResource: Resource + newStartDate?: string + newEndDate?: string +}) => { + // 保存拖拽数据并显示确认对话框 + resourceDragData.value = { + task: event.task, + sourceResourceIndex: event.sourceResourceIndex, + targetResourceIndex: event.targetResourceIndex, + targetResource: event.targetResource, + newStartDate: event.newStartDate, + newEndDate: event.newEndDate, + } + resourceDragConfirmVisible.value = true +} + +// 确认资源分配 +const confirmResourceDrag = () => { + const { task, sourceResourceIndex, targetResourceIndex, targetResource, newStartDate, newEndDate } = resourceDragData.value + if (!task || !targetResource) return + + // v1.9.0 跨行拖拽时需要更新任务的日期,使任务显示在正确的时间位置 + if (newStartDate && newEndDate) { + task.startDate = newStartDate + task.endDate = newEndDate + } + + // 从源资源中移除任务 + const sourceResource = resources.value[sourceResourceIndex] + if (sourceResource && sourceResource.tasks) { + const taskIndex = sourceResource.tasks.findIndex(t => t.id === task.id) + if (taskIndex !== -1) { + sourceResource.tasks.splice(taskIndex, 1) + } + } + + // 添加任务到目标资源 + const destResource = resources.value[targetResourceIndex] + if (destResource) { + if (!destResource.tasks) { + destResource.tasks = [] + } + destResource.tasks.push(task) + } + + // 显示资源分配和日期更新信息 + const dateInfo = newStartDate && newEndDate ? `,日期已调整为 ${newStartDate} ~ ${newEndDate}` : '' + showMessage(`任务 "${task.name}" 已分配给资源 "${targetResource.name}"${dateInfo}`, 'success') + resourceDragConfirmVisible.value = false + + // 重置拖拽数据 + resourceDragData.value = { + task: null, + sourceResourceIndex: -1, + targetResourceIndex: -1, + targetResource: null, + newStartDate: undefined, + newEndDate: undefined + } +} + +// 取消资源分配 +const cancelResourceDrag = () => { + resourceDragConfirmVisible.value = false + + // v1.9.0 通知TaskBar清除临时数据,恢复到原始位置 + window.dispatchEvent( + new CustomEvent('resource-drag-cancel', { + detail: { + taskId: resourceDragData.value.task?.id + } + }) + ) + + // 重置拖拽数据 + resourceDragData.value = { + task: null, + sourceResourceIndex: -1, + targetResourceIndex: -1, + targetResource: null, + newStartDate: undefined, + newEndDate: undefined + } +} + // 自定义右键菜单操作处理 const handleCustomMenuAction = (action: string, task: Task) => { showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true }) @@ -1863,6 +2077,9 @@ const handleCustomMenuAction = (action: string, task: Task) => { ref="gantt" :tasks="tasks" :milestones="milestones" + :resources="resources" + :view-mode="viewMode" + :resource-list-config="resourceListConfig" :locale="controlMode === 'props' ? propsLocale : undefined" :theme="controlMode === 'props' ? propsTheme : undefined" :time-scale="controlMode === 'props' ? propsTimeScale : undefined" @@ -1910,6 +2127,9 @@ const handleCustomMenuAction = (action: string, task: Task) => { @task-added="handleTaskAddEvent" @task-updated="handleTaskUpdateEvent" @task-row-moved="handleTaskRowMoved" + @view-mode-change="mode => viewMode = mode" + @resource-click="resource => showMessage(`资源点击: ${resource.name}`, 'info')" + @resource-drag-end="handleResourceDragEnd" >