diff --git a/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json b/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json new file mode 100644 index 0000000000..b9ca3c5c70 --- /dev/null +++ b/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-gantt", + "comment": "fix: return null for missing gantt task bars and support sub task rect lookup", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-gantt", + "email": "892739385@qq.com" +} diff --git a/docs/assets/api/en/GanttAPI.md b/docs/assets/api/en/GanttAPI.md index 1b7ab7b384..6bb5f5e492 100644 --- a/docs/assets/api/en/GanttAPI.md +++ b/docs/assets/api/en/GanttAPI.md @@ -149,12 +149,12 @@ Get or set the horizontal scroll value to a specified position. Get the position of the task bar. The position relative to the top-left corner of the Gantt chart. ``` - getTaskBarRelativeRect:(index: number) =>{ + getTaskBarRelativeRect:(index: number, sub_task_index?: number) =>{ left: number; top: number; width: number; height: number; - } + } | null ``` ## Events diff --git a/docs/assets/api/zh/GanttAPI.md b/docs/assets/api/zh/GanttAPI.md index be2c0b889f..13d90f4c1b 100644 --- a/docs/assets/api/zh/GanttAPI.md +++ b/docs/assets/api/zh/GanttAPI.md @@ -150,12 +150,12 @@ const info = ganttInstance.getBaselineInfoByTaskListIndex(0); 获取任务条的位置。相对应甘特图表左上角的位置。 ``` - getTaskBarRelativeRect:(index: number) =>{ + getTaskBarRelativeRect:(index: number, sub_task_index?: number) =>{ left: number; top: number; width: number; height: number; - } + } | null ``` ## Events diff --git a/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts new file mode 100644 index 0000000000..c580d86d64 --- /dev/null +++ b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts @@ -0,0 +1,118 @@ +import type { ColumnsDefine } from '@visactor/vtable'; +import type { GanttConstructorOptions } from '../../src/index'; +import { Gantt } from '../../src/index'; +import { TasksShowMode, TaskType } from '../../src/ts-types'; + +const CONTAINER_ID = 'vTable'; + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue4778Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check getTaskBarRelativeRect" to verify issue #4778.'; + + const button = document.createElement('button'); + button.textContent = 'Check getTaskBarRelativeRect'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkRects(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const isValidRect = (rect: any) => + rect && + Number.isFinite(rect.left) && + Number.isFinite(rect.top) && + Number.isFinite(rect.width) && + Number.isFinite(rect.height); + +const checkRects = () => { + const ganttInstance = (window as any).ganttInstance as Gantt; + const status = document.getElementById('issue4778Status')!; + let emptyRect: any; + let firstChildRect: any; + let secondChildRect: any; + + try { + emptyRect = ganttInstance.getTaskBarRelativeRect(1); + firstChildRect = ganttInstance.getTaskBarRelativeRect(0, [0, 0]); + secondChildRect = ganttInstance.getTaskBarRelativeRect(0, [0, 1]); + } catch (err) { + status.textContent = `FAIL | ${(err as Error).message}`; + return status.textContent; + } + + const pass = + emptyRect === null && + isValidRect(firstChildRect) && + isValidRect(secondChildRect) && + firstChildRect.left !== secondChildRect.left; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | empty=${JSON.stringify(emptyRect)}, first=${JSON.stringify( + firstChildRect + )}, second=${JSON.stringify(secondChildRect)}`; + return status.textContent; +}; + +export function createTable() { + const records = [ + { + id: 1, + title: 'Collapsed project with inline subtasks', + type: TaskType.PROJECT, + children: [ + { id: 11, title: 'Sub task A', start: '2024-07-01', end: '2024-07-04', progress: 30 }, + { id: 12, title: 'Sub task B', start: '2024-07-08', end: '2024-07-12', progress: 60 } + ] + }, + { + id: 2, + title: 'Empty task row' + } + ]; + + const columns: ColumnsDefine = [ + { field: 'title', title: 'title', width: 220 }, + { field: 'start', title: 'start', width: 120 }, + { field: 'end', title: 'end', width: 120 }, + { field: 'progress', title: 'progress', width: 100 } + ]; + + const option: GanttConstructorOptions = { + records, + taskListTable: { + columns, + tableWidth: 260 + }, + taskKeyField: 'id', + tasksShowMode: TasksShowMode.Project_Sub_Tasks_Inline, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress' + }, + minDate: '2024-07-01', + maxDate: '2024-07-20', + timelineHeader: { + colWidth: 36, + scales: [{ unit: 'day', step: 1 }] + }, + grid: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + } + }; + + createStatusBar(); + const ganttInstance = new Gantt(document.getElementById(CONTAINER_ID)!, option); + (window as any).ganttInstance = ganttInstance; + (window as any).issue4778Run = checkRects; +} diff --git a/packages/vtable-gantt/examples/menu.ts b/packages/vtable-gantt/examples/menu.ts index c5e88f7d7a..1bcfe17a99 100644 --- a/packages/vtable-gantt/examples/menu.ts +++ b/packages/vtable-gantt/examples/menu.ts @@ -178,6 +178,10 @@ export const menus = [ { path: 'gantt', name: 'gantt-issue-5162-sort-drag' + }, + { + path: 'gantt', + name: 'issue-4778-taskbar-relative-rect' } // ] // } diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index bf77edc652..0cfcca00c1 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -1027,12 +1027,12 @@ export class Gantt extends EventTarget { return this.records[taskShowIndex]; } - _refreshTaskBar(taskShowIndex: number, sub_task_index?: number) { + _refreshTaskBar(taskShowIndex: number, sub_task_index?: number | number[]) { // this.taskListTableInstance.updateRecords([record], [index]); this.scenegraph.taskBar.updateTaskBarNode(taskShowIndex, sub_task_index); this.scenegraph.refreshRecordLinkNodes( taskShowIndex, - undefined, + sub_task_index, this.scenegraph.taskBar.getTaskBarNodeByIndex(taskShowIndex, sub_task_index) as GanttTaskBarNode ); this.scenegraph.updateNextFrame(); @@ -1262,12 +1262,13 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } } - _updateEndDateToTaskRecord(endDate: Date, index: number, sub_task_index?: number) { + _updateEndDateToTaskRecord(endDate: Date, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const endDateField = this.parsedOptions.endDateField; const dateFormat = this.parsedOptions.dateFormat ?? parseDateFormat(taskRecord[endDateField]); @@ -1284,12 +1285,13 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } } - _updateStartEndDateToTaskRecord(startDate: Date, endDate: Date, index: number, sub_task_index?: number) { + _updateStartEndDateToTaskRecord(startDate: Date, endDate: Date, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const startDateField = this.parsedOptions.startDateField; const endDateField = this.parsedOptions.endDateField; @@ -1308,6 +1310,7 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } @@ -1319,11 +1322,16 @@ export class Gantt extends EventTarget { * @param index 对应的一定是左侧表格body的index * @param sub_task_index 子任务的index, 当taskShowMode是sub_tasks_*模式时,会传入sub_task_index。如果是tasks_separate模式,sub_task_index传入undefined。 */ - _updateProgressToTaskRecord(progress: number, index: number, sub_task_index?: number) { + _updateProgressToTaskRecord(progress: number, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const progressField = this.parsedOptions.progressField; if (progressField) { taskRecord[progressField] = progress; + if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); + this._refreshTaskBar(index, sub_task_index); + return; + } const recordIndex = this.getRecordIndexByTaskShowIndex(index); this._updateRecordToListTable(taskRecord, Array.isArray(recordIndex) ? recordIndex : index); if (!this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index)) { @@ -1353,11 +1361,11 @@ export class Gantt extends EventTarget { * 如果TasksShowModes是 sub_tasks_*** 模式 则需要传入task_index和sub_task_index */ updateTaskRecord(record: any, task_index: number | number[]): void; - updateTaskRecord(record: any, task_index: number, sub_task_index: number): void; - updateTaskRecord(record: any, task_index: number | number[], sub_task_index?: number) { + updateTaskRecord(record: any, task_index: number, sub_task_index: number | number[]): void; + updateTaskRecord(record: any, task_index: number | number[], sub_task_index?: number | number[]) { if (isValid(sub_task_index)) { const index = typeof task_index === 'number' ? task_index : task_index[0]; - this._updateRecordToListTable(record, [index, sub_task_index]); + this._updateRecordToListTable(record, Array.isArray(sub_task_index) ? sub_task_index : [index, sub_task_index]); this._refreshTaskBar(index, sub_task_index); return; } @@ -1637,8 +1645,11 @@ export class Gantt extends EventTarget { this.stateManager.setScrollLeft(value); } /** 获取任务条的位置。相对应甘特图表左上角的位置。 */ - getTaskBarRelativeRect(index: number) { - const taskBarNode = this.scenegraph.taskBar.getTaskBarNodeByIndex(index); + getTaskBarRelativeRect(index: number, sub_task_index?: number | number[]) { + const taskBarNode = this.scenegraph.taskBar.getTaskBarNodeByIndex(index, sub_task_index); + if (!taskBarNode) { + return null; + } const left = taskBarNode.attribute.x + this.taskListTableInstance.tableNoFrameWidth + diff --git a/packages/vtable-gantt/src/scenegraph/gantt-node.ts b/packages/vtable-gantt/src/scenegraph/gantt-node.ts index e92027d0f0..6a37d207e6 100644 --- a/packages/vtable-gantt/src/scenegraph/gantt-node.ts +++ b/packages/vtable-gantt/src/scenegraph/gantt-node.ts @@ -15,7 +15,7 @@ export class GanttTaskBarNode extends Group { textLabel?: IText; declare name: string; task_index: number; - sub_task_index?: number; + sub_task_index?: number | number[]; record?: any; labelStyle?: ITaskBarLabelTextStyle; diff --git a/packages/vtable-gantt/src/scenegraph/scenegraph.ts b/packages/vtable-gantt/src/scenegraph/scenegraph.ts index d68465655d..88c98dbc29 100644 --- a/packages/vtable-gantt/src/scenegraph/scenegraph.ts +++ b/packages/vtable-gantt/src/scenegraph/scenegraph.ts @@ -338,7 +338,12 @@ export class Scenegraph { this.toolTip.hide(); } - refreshRecordLinkNodes(taskIndex: number, sub_task_index: number, target: GanttTaskBarNode, dy: number = 0) { + refreshRecordLinkNodes( + taskIndex: number, + sub_task_index: number | number[], + target: GanttTaskBarNode, + dy: number = 0 + ) { const gantt: Gantt = this._gantt; const record = gantt.getRecordByIndex(taskIndex, sub_task_index); const vtable_gantt_linkedTo = record.vtable_gantt_linkedTo; diff --git a/packages/vtable-gantt/src/scenegraph/task-bar.ts b/packages/vtable-gantt/src/scenegraph/task-bar.ts index 244f477370..850632174b 100644 --- a/packages/vtable-gantt/src/scenegraph/task-bar.ts +++ b/packages/vtable-gantt/src/scenegraph/task-bar.ts @@ -19,6 +19,23 @@ const LOCATE_ICON_BG_HOVER = '#4080ff'; const LOCATE_ICON_ARROW = '#4e5969'; const LOCATE_ICON_ARROW_HOVER = '#ffffff'; +const isSameSubTaskIndex = (source?: number | number[], target?: number | number[]) => { + if (!isValid(target)) { + return true; + } + + if (Array.isArray(source) || Array.isArray(target)) { + return ( + Array.isArray(source) && + Array.isArray(target) && + source.length === target.length && + source.every((value, index) => value === target[index]) + ); + } + + return source === target; +}; + export class TaskBar { formatMilestoneText(text: string, record: any): string { if (!text) { @@ -524,7 +541,7 @@ export class TaskBar { } return { barGroupBox, baselineBar }; } - updateTaskBarNode(index: number, sub_task_index?: number) { + updateTaskBarNode(index: number, sub_task_index?: number | number[]) { const taskbarGroup = this.getTaskBarNodeByIndex(index, sub_task_index); if (taskbarGroup) { this.barContainer.removeChild(taskbarGroup); @@ -1054,16 +1071,13 @@ export class TaskBar { this.selectedBorders[0].appendChild(line); } - getTaskBarNodeByIndex(index: number, sub_task_index?: number): GanttTaskBarNode { + getTaskBarNodeByIndex(index: number, sub_task_index?: number | number[]): GanttTaskBarNode { let c = this.barContainer.firstChild as GanttTaskBarNode; if (!c) { return null; } for (let i = 0; i < this.barContainer.childrenCount; i++) { - if ( - c.task_index === index && - (!isValid(sub_task_index) || (isValid(sub_task_index) && c.sub_task_index === sub_task_index)) - ) { + if (c.task_index === index && isSameSubTaskIndex(c.sub_task_index, sub_task_index)) { return c; } c = c._next as GanttTaskBarNode; diff --git a/packages/vtable-gantt/src/state/state-manager.ts b/packages/vtable-gantt/src/state/state-manager.ts index f2255b642b..47f5560c3e 100644 --- a/packages/vtable-gantt/src/state/state-manager.ts +++ b/packages/vtable-gantt/src/state/state-manager.ts @@ -479,9 +479,9 @@ export class StateManager { const indexs = getTaskIndexsByTaskY(targetEndY, this._gantt); this._gantt._dragOrderTaskRecord( target.task_index, - target.sub_task_index, + target.sub_task_index as number, indexs.task_index, - indexs.sub_task_index + indexs.sub_task_index as number ); clearRecordShowIndex(this._gantt.records); this._gantt.taskListTableInstance.renderWithRecreateCells(); @@ -494,18 +494,20 @@ export class StateManager { Math.abs(Math.round(deltaY / this._gantt.parsedOptions.rowHeight)) >= 1 ) { const indexs = getTaskIndexsByTaskY(targetEndY, this._gantt); - this._gantt._dragOrderTaskRecord( - target.task_index, - target.sub_task_index, - indexs.task_index, - indexs.sub_task_index - ); - if (this._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate) { - this._gantt.taskListTableInstance.renderWithRecreateCells(); - this._gantt.scenegraph.refreshTaskBarsAndGrid(); - } else { - this._gantt.scenegraph.taskBar.refresh(); - this._gantt.scenegraph.dependencyLink.refresh(); + if (!Array.isArray(target.sub_task_index) && !Array.isArray(indexs.sub_task_index)) { + this._gantt._dragOrderTaskRecord( + target.task_index, + target.sub_task_index, + indexs.task_index, + indexs.sub_task_index + ); + if (this._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate) { + this._gantt.taskListTableInstance.renderWithRecreateCells(); + this._gantt.scenegraph.refreshTaskBarsAndGrid(); + } else { + this._gantt.scenegraph.taskBar.refresh(); + this._gantt.scenegraph.dependencyLink.refresh(); + } } // target = this._gantt.scenegraph.taskBar.getTaskBarNodeByIndex(indexs.task_index, indexs.sub_task_index); } else { @@ -1364,6 +1366,8 @@ export class StateManager { // 更新父任务的时间范围 parent[startDateField] = formatDateValue(earliestStart); parent[endDateField] = formatDateValue(latestEnd); + const parentRecordIndex = parentPath.length === 0 ? childIndex : [...parentPath, childIndex]; + this._gantt._updateRecordToListTable(parent, parentRecordIndex); } } } @@ -1377,7 +1381,7 @@ export class StateManager { } } -function reCreateCustomNode(gantt: Gantt, taskBarGroup: Group, taskIndex: number, sub_task_index?: number) { +function reCreateCustomNode(gantt: Gantt, taskBarGroup: Group, taskIndex: number, sub_task_index?: number | number[]) { const taskBarCustomLayout = gantt.parsedOptions.taskBarCustomLayout; if (taskBarCustomLayout) { let customLayoutObj; diff --git a/packages/vtable-gantt/src/ts-types/events.ts b/packages/vtable-gantt/src/ts-types/events.ts index 55f33ecc80..6f75fe741a 100644 --- a/packages/vtable-gantt/src/ts-types/events.ts +++ b/packages/vtable-gantt/src/ts-types/events.ts @@ -3,6 +3,8 @@ import type { IMarkLine, ITaskLink, ITimelineDateInfo } from './gantt-engine'; import type { IPosition } from './common'; import type { IZoomEventArgs } from './zoom-scale'; +type SubTaskIndex = number | number[]; + export type TableEventListener = ( args: TableEventHandlersEventArgumentMap[TYPE] ) => TableEventHandlersReturnMap[TYPE]; //AnyFunction; @@ -19,7 +21,7 @@ export interface TableEventHandlersEventArgumentMap { mouseenter_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -27,7 +29,7 @@ export interface TableEventHandlersEventArgumentMap { mouseleave_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -35,7 +37,7 @@ export interface TableEventHandlersEventArgumentMap { click_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -43,7 +45,7 @@ export interface TableEventHandlersEventArgumentMap { contextmenu_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -51,7 +53,7 @@ export interface TableEventHandlersEventArgumentMap { change_date_range: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: Date; /** 改变后的结束日期 */ @@ -66,7 +68,7 @@ export interface TableEventHandlersEventArgumentMap { move_end_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: Date; /** 改变后的结束日期 */ @@ -87,7 +89,7 @@ export interface TableEventHandlersEventArgumentMap { event: Event; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: string; /** 改变后的结束日期 */ @@ -114,7 +116,7 @@ export interface TableEventHandlersEventArgumentMap { point: 'start' | 'end'; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; }; contextmenu_dependency_link: { @@ -138,7 +140,7 @@ export interface TableEventHandlersEventArgumentMap { event: Event; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 新的进度值 */ progress: number; /** 原来的进度值 */