From 045162848c107e139ae9187c5457f7c04cf5d6e1 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 25 Mar 2026 11:55:18 +0800 Subject: [PATCH 1/9] feat: gantt add locateIcon for taskbar --- .../demo/en/gantt/gantt-locate-taskbar.md | 78 ++++++++ docs/assets/demo/menu.json | 7 + .../demo/zh/gantt/gantt-locate-taskbar.md | 78 ++++++++ docs/assets/guide/en/gantt/Getting_Started.md | 16 ++ docs/assets/guide/zh/gantt/Getting_Started.md | 16 ++ .../assets/option/en/common/gantt/task-bar.md | 7 +- .../assets/option/zh/common/gantt/task-bar.md | 7 +- .../examples/gantt/gantt-locate-taskbar.ts | 67 +++++++ packages/vtable-gantt/examples/menu.ts | 4 + packages/vtable-gantt/src/Gantt.ts | 1 + .../vtable-gantt/src/event/event-manager.ts | 43 +++- packages/vtable-gantt/src/gantt-helper.ts | 2 + .../vtable-gantt/src/scenegraph/task-bar.ts | 189 ++++++++++++++++++ .../vtable-gantt/src/ts-types/gantt-engine.ts | 2 + 14 files changed, 512 insertions(+), 5 deletions(-) create mode 100644 docs/assets/demo/en/gantt/gantt-locate-taskbar.md create mode 100644 docs/assets/demo/zh/gantt/gantt-locate-taskbar.md create mode 100644 packages/vtable-gantt/examples/gantt/gantt-locate-taskbar.ts diff --git a/docs/assets/demo/en/gantt/gantt-locate-taskbar.md b/docs/assets/demo/en/gantt/gantt-locate-taskbar.md new file mode 100644 index 000000000..b999735dd --- /dev/null +++ b/docs/assets/demo/en/gantt/gantt-locate-taskbar.md @@ -0,0 +1,78 @@ +--- +category: examples +group: gantt +title: Task Bar Locate (Offscreen Indicator) +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-basic-preview.png +link: gantt/Getting_Started +option: Gantt#taskBar +--- + +# Task Bar Locate (Offscreen Indicator) + +When the timeline is long, task bars may be outside the current viewport. This demo shows how to enable the locate icon feature: when a task bar is horizontally outside the viewport, an icon is displayed at the left/right edge of the gantt view; hover highlights it, and click scrolls the task bar into view. + +## Key Option + +- `taskBar.locateIcon: true` + +## Live Demo + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; +const CONTAINER_ID = 'vTable'; + +const records = [ + { id: 1, title: 'Offscreen on the left', start: '2024-02-05', end: '2024-02-20', progress: 20 }, + { id: 2, title: 'Offscreen on the left', start: '2024-03-10', end: '2024-03-18', progress: 60 }, + { id: 5, title: 'Visible in viewport', start: '2024-05-28', end: '2024-06-05', progress: 50 }, + { id: 3, title: 'Offscreen on the right', start: '2024-10-05', end: '2024-10-20', progress: 40 }, + { id: 4, title: 'Offscreen on the right', start: '2024-11-10', end: '2024-11-25', progress: 80 } +]; + +const columns = [ + { field: 'title', title: 'title', width: 160, sort: true }, + { field: 'start', title: 'start', width: 120, sort: true }, + { field: 'end', title: 'end', width: 120, sort: true }, + { field: 'progress', title: 'progress', width: 100, sort: true } +]; + +const option = { + records, + taskKeyField: 'id', + taskListTable: { + columns, + tableWidth: 280, + minTableWidth: 240, + maxTableWidth: 600 + }, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + locateIcon: true + }, + minDate: '2024-01-01', + maxDate: '2024-12-31', + timelineHeader: { + colWidth: 30, + scales: [{ unit: 'day', step: 1 }] + }, + scrollStyle: { + visible: 'scrolling' + }, + grid: { + verticalLine: { lineWidth: 1, lineColor: '#e1e4e8' }, + horizontalLine: { lineWidth: 1, lineColor: '#e1e4e8' } + } +}; + +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; + +setTimeout(() => { + const x = ganttInstance.getXByTime(new Date('2024-06-01 00:00:00').getTime()); + ganttInstance.scrollLeft = x; +}, 0); +``` + diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json index 467d6ecf8..4da642d8c 100644 --- a/docs/assets/demo/menu.json +++ b/docs/assets/demo/menu.json @@ -314,6 +314,13 @@ "en": "Gantt Basic" } }, + { + "path": "gantt-locate-taskbar", + "title": { + "zh": "任务条定位(超出可视区)", + "en": "Task Bar Locate" + } + }, { "path": "gantt-customLayout", "title": { diff --git a/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md b/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md new file mode 100644 index 000000000..11ac227e5 --- /dev/null +++ b/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md @@ -0,0 +1,78 @@ +--- +category: examples +group: gantt +title: 甘特图任务条定位(超出可视区提示) +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-basic-preview.png +link: gantt/Getting_Started +option: Gantt#taskBar +--- + +# 甘特图任务条定位(超出可视区提示) + +当时间轴很长时,任务条可能不在当前可视区域内。本示例展示如何开启定位图标能力:当任务条横向超出可视区域时,在甘特图左右边缘显示定位图标;鼠标 hover 会高亮,点击后会自动滚动将任务条带入可视区域。 + +## 关键配置 + +- `taskBar.locateIcon: true` + +## 代码演示 + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; +const CONTAINER_ID = 'vTable'; + +const records = [ + { id: 1, title: '任务条在左侧不可见', start: '2024-02-05', end: '2024-02-20', progress: 20 }, + { id: 2, title: '任务条在左侧不可见', start: '2024-03-10', end: '2024-03-18', progress: 60 }, + { id: 5, title: '任务条在可见区', start: '2024-05-28', end: '2024-06-05', progress: 50 }, + { id: 3, title: '任务条在右侧不可见', start: '2024-10-05', end: '2024-10-20', progress: 40 }, + { id: 4, title: '任务条在右侧不可见', start: '2024-11-10', end: '2024-11-25', progress: 80 } +]; + +const columns = [ + { field: 'title', title: 'title', width: 160, sort: true }, + { field: 'start', title: 'start', width: 120, sort: true }, + { field: 'end', title: 'end', width: 120, sort: true }, + { field: 'progress', title: 'progress', width: 100, sort: true } +]; + +const option = { + records, + taskKeyField: 'id', + taskListTable: { + columns, + tableWidth: 280, + minTableWidth: 240, + maxTableWidth: 600 + }, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + locateIcon: true + }, + minDate: '2024-01-01', + maxDate: '2024-12-31', + timelineHeader: { + colWidth: 30, + scales: [{ unit: 'day', step: 1 }] + }, + scrollStyle: { + visible: 'scrolling' + }, + grid: { + verticalLine: { lineWidth: 1, lineColor: '#e1e4e8' }, + horizontalLine: { lineWidth: 1, lineColor: '#e1e4e8' } + } +}; + +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; + +setTimeout(() => { + const x = ganttInstance.getXByTime(new Date('2024-06-01 00:00:00').getTime()); + ganttInstance.scrollLeft = x; +}, 0); +``` + diff --git a/docs/assets/guide/en/gantt/Getting_Started.md b/docs/assets/guide/en/gantt/Getting_Started.md index cb39c61bb..d444eb973 100644 --- a/docs/assets/guide/en/gantt/Getting_Started.md +++ b/docs/assets/guide/en/gantt/Getting_Started.md @@ -351,4 +351,20 @@ At this point, you have successfully drawn a simple Gantt chart! When the smallest time scale is `unit: 'day'` and `step: 1`, you can use `timelineHeader.weekendColWidth` to override weekend column width (or set `timelineHeader.hideWeekend` to hide weekend columns). +## Task Bar Locate (Optional) + +When the timeline is long and the task bar is outside the current viewport, you can enable the “locate icon” feature: an icon is shown on the left/right edge of the gantt view, and clicking it scrolls the task bar into the viewport. + +Key option: + +```javascript +const option = { + taskBar: { + locateIcon: true + } +}; +``` + +Demo: `gantt-locate-taskbar`. + I hope this tutorial helps you learn how to use Gantt. Next, you can delve into the various configuration options of vtable-gantt to customize more diverse table effects. diff --git a/docs/assets/guide/zh/gantt/Getting_Started.md b/docs/assets/guide/zh/gantt/Getting_Started.md index d2e0912ea..462d68c62 100644 --- a/docs/assets/guide/zh/gantt/Getting_Started.md +++ b/docs/assets/guide/zh/gantt/Getting_Started.md @@ -351,4 +351,20 @@ window['ganttInstance'] = ganttInstance; 当最小粒度为天且 `unit: 'day'`、`step: 1` 时,可以通过 `timelineHeader.weekendColWidth` 覆盖周末列宽度(或通过 `timelineHeader.hideWeekend` 隐藏周末列)。 +## 任务条定位(可选) + +当时间轴较长、任务条不在当前可视区域内时,可以开启“定位图标”能力:在甘特图左右边缘显示图标,点击后自动滚动到该任务条的可视区域。 + +关键配置: + +```javascript +const option = { + taskBar: { + locateIcon: true + } +}; +``` + +可参考示例:`gantt-locate-taskbar`。 + 希望这篇教程对你学习如何使用 Gantt 有所帮助。接下来可以深入了解 vtable-gantt 的各种配置选项,定制出更加丰富多样的表格效果。 diff --git a/docs/assets/option/en/common/gantt/task-bar.md b/docs/assets/option/en/common/gantt/task-bar.md index 447f27152..6a9b6e72f 100644 --- a/docs/assets/option/en/common/gantt/task-bar.md +++ b/docs/assets/option/en/common/gantt/task-bar.md @@ -171,6 +171,12 @@ Whether the service clause is optional, the default is true Not required +${prefix} locateIcon(boolean) = false + +When the task bar is horizontally outside the current viewport, show a “locate icon” at the left/right edge of the gantt view. Hover highlights the icon, and click scrolls the task bar into the viewport. + +Optional + ${prefix} scheduleCreatable(boolean | Function) = true When there is no scheduling data, scheduling can be done by creating a task bar. When `tasksShowMode` is `TasksShowMode.Tasks_Separate` or `TasksShowMode.Sub_Tasks_Separate`, `scheduleCreatable` defaults to `true`, otherwise, when `tasksShowMode` is `TasksShowMode.Sub_Tasks_Inline`, `TasksShowMode.Sub_Tasks_Arrange`, or `TasksShowMode.Sub_Tasks_Compact`, `scheduleCreatable` defaults to `false`. @@ -267,4 +273,3 @@ Vertical position of the baseline bar relative to the main task bar: - `overlap`: baseline overlaps and is centered with the task bar. Optional - diff --git a/docs/assets/option/zh/common/gantt/task-bar.md b/docs/assets/option/zh/common/gantt/task-bar.md index d27d462d7..446434e89 100644 --- a/docs/assets/option/zh/common/gantt/task-bar.md +++ b/docs/assets/option/zh/common/gantt/task-bar.md @@ -176,6 +176,12 @@ ${prefix} selectable(boolean) 非必填 +${prefix} locateIcon(boolean) = false + +当任务条在横向上不在当前可视区域内时,在甘特图左右边缘展示“定位图标”;鼠标 hover 会高亮,点击后会一键滚动到任务条可视区域内。 + +非必填 + ${prefix} scheduleCreatable(boolean | Function) = true 数据没有排期时,可通过创建任务条排期。当 tasksShowMode 为 TasksShowMode.Tasks_Separate 或 TasksShowMode.Sub_Tasks_Separate 时 `scheduleCreatable` 默认为 true,其他情况即当 tasksShowMode 为 TasksShowMode.Sub_Tasks_Inline 或 TasksShowMode.Sub_Tasks_Arrange 或 TasksShowMode.Sub_Tasks_Compact 时 `scheduleCreatable` 默认为 false @@ -273,4 +279,3 @@ ${prefix} baselinePosition('top' | 'bottom' | 'overlap') = 'bottom' - `overlap`:基线与主任务条重叠居中。 非必填 - diff --git a/packages/vtable-gantt/examples/gantt/gantt-locate-taskbar.ts b/packages/vtable-gantt/examples/gantt/gantt-locate-taskbar.ts new file mode 100644 index 000000000..20a933396 --- /dev/null +++ b/packages/vtable-gantt/examples/gantt/gantt-locate-taskbar.ts @@ -0,0 +1,67 @@ +import type { ColumnsDefine } from '@visactor/vtable'; +import type { GanttConstructorOptions } from '../../src/index'; +import { Gantt } from '../../src/index'; + +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const records = [ + { id: 1, title: '任务条在左侧不可见', start: '2024-02-05', end: '2024-02-20', progress: 20 }, + { id: 2, title: '任务条在左侧不可见', start: '2024-03-10', end: '2024-03-18', progress: 60 }, + { id: 5, title: '任务条在可见区', start: '2024-05-28', end: '2024-06-05', progress: 50 }, + { id: 3, title: '任务条在右侧不可见', start: '2024-10-05', end: '2024-10-20', progress: 40 }, + { id: 4, title: '任务条在右侧不可见', start: '2024-11-10', end: '2024-11-25', progress: 80 } + ]; + + const columns: ColumnsDefine = [ + { field: 'title', title: 'title', width: 160, sort: true }, + { field: 'start', title: 'start', width: 120, sort: true }, + { field: 'end', title: 'end', width: 120, sort: true }, + { field: 'progress', title: 'progress', width: 100, sort: true } + ]; + + const option: GanttConstructorOptions = { + records, + taskListTable: { + columns, + tableWidth: 280, + minTableWidth: 240, + maxTableWidth: 600 + }, + taskKeyField: 'id', + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + locateIcon: true + }, + minDate: '2024-01-01', + maxDate: '2024-12-31', + timelineHeader: { + colWidth: 30, + scales: [{ unit: 'day', step: 1 }] + }, + scrollStyle: { + visible: 'scrolling' + }, + grid: { + // backgroundColor: 'gray', + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + } + }; + + const ganttInstance = new Gantt(document.getElementById(CONTAINER_ID)!, option); + (window as any).ganttInstance = ganttInstance; + + setTimeout(() => { + const x = ganttInstance.getXByTime(new Date('2024-06-01 00:00:00').getTime()); + ganttInstance.scrollLeft = x; + }, 0); +} diff --git a/packages/vtable-gantt/examples/menu.ts b/packages/vtable-gantt/examples/menu.ts index ce9d8a3b6..727abebef 100644 --- a/packages/vtable-gantt/examples/menu.ts +++ b/packages/vtable-gantt/examples/menu.ts @@ -170,6 +170,10 @@ export const menus = [ { path: 'gantt', name: 'project-sub-tasks-inline' + }, + { + path: 'gantt', + name: 'gantt-locate-taskbar' } // ] // } diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index efb1bbced..3e45afe08 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -165,6 +165,7 @@ export class Gantt extends EventTarget { tasksShowMode: TasksShowMode; projectSubTasksExpandable: boolean; taskBarClip: boolean; + taskBarLocateIcon: boolean; startDateField: string; endDateField: string; diff --git a/packages/vtable-gantt/src/event/event-manager.ts b/packages/vtable-gantt/src/event/event-manager.ts index dec71970d..b02f464ef 100644 --- a/packages/vtable-gantt/src/event/event-manager.ts +++ b/packages/vtable-gantt/src/event/event-manager.ts @@ -1,5 +1,5 @@ import { vglobal } from '@visactor/vtable/es/vrender'; -import type { FederatedPointerEvent, FederatedWheelEvent } from '@visactor/vtable/es/vrender'; +import type { FederatedPointerEvent, FederatedWheelEvent, Group } from '@visactor/vtable/es/vrender'; import type { Gantt } from '../Gantt'; import { EventHandler } from '../event/EventHandler'; import { handleWhell } from '../event/scroll'; @@ -220,6 +220,19 @@ function bindTableGroupListener(event: EventManager) { event.touchSetTimeout = undefined; } if (stateManager.interactionState === InteractionState.default) { + let locateIconTarget: Group | null = null; + if (gantt.parsedOptions.taskBarLocateIcon) { + // 优先处理定位图标 hover:避免与任务条 hover 互相抢占状态 + locateIconTarget = e.detailPath.find((pathNode: any) => { + return pathNode.name === 'task-bar-locate-icon-left' || pathNode.name === 'task-bar-locate-icon-right'; + }) as any as Group; + if (locateIconTarget) { + scene._gantt.scenegraph.taskBar.setLocateIconHover(locateIconTarget); + } else if (scene._gantt.scenegraph.taskBar.currentHoverLocateIcon) { + scene._gantt.scenegraph.taskBar.setLocateIconHover(null); + } + } + const taskBarTarget = e.detailPath.find((pathNode: any) => { return pathNode.name === 'task-bar'; // || pathNode.name === 'task-bar-hover-shadow'; }); @@ -267,7 +280,7 @@ function bindTableGroupListener(event: EventManager) { }); } } - } else { + } else if (!locateIconTarget) { if (scene._gantt.stateManager.hoverTaskBar.target) { if (scene._gantt.hasListeners(GANTT_EVENT_TYPE.MOUSELEAVE_TASK_BAR)) { // const taskIndex = getTaskIndexByY(e.offset.y, scene._gantt); @@ -445,8 +458,10 @@ function bindTableGroupListener(event: EventManager) { let depedencyLink; let isClickMarklineIcon = false; let isClickMarklineContent = false; + let isClickLocateIcon = false; let markLineContentTarget: any; let markLineIconTarget: any; + let locateIconTarget: any; const taskBarTarget = e.detailPath.find((pathNode: any) => { if (pathNode.name === 'task-bar') { @@ -476,11 +491,30 @@ function bindTableGroupListener(event: EventManager) { isClickMarklineContent = true; markLineContentTarget = pathNode; return false; + } else if ( + gantt.parsedOptions.taskBarLocateIcon && + (pathNode.name === 'task-bar-locate-icon-left' || pathNode.name === 'task-bar-locate-icon-right') + ) { + isClickLocateIcon = true; + locateIconTarget = pathNode; + return false; } return false; }); - if (isClickBar && scene._gantt.parsedOptions.taskBarSelectable && event.poniterState === 'down') { + if (isClickLocateIcon && event.poniterState === 'down') { + // 点击定位图标:将任务条滚动到可视区(左右边缘附近),便于快速定位长时间轴任务 + const barNode = locateIconTarget?.attachedToTaskBarNode as GanttTaskBarNode; + const side = locateIconTarget?.side as 'left' | 'right'; + if (barNode) { + const barLeft = barNode.attribute.x; + const barRight = barLeft + barNode.attribute.width; + const viewWidth = gantt.tableNoFrameWidth; + const padding = 12; + const targetLeft = side === 'left' ? barLeft - padding : barRight - viewWidth + padding; + gantt.stateManager.setScrollLeft(targetLeft); + } + } else if (isClickBar && scene._gantt.parsedOptions.taskBarSelectable && event.poniterState === 'down') { stateManager.hideDependencyLinkSelectedLine(); const taskBarNode = taskBarTarget as any as GanttTaskBarNode; stateManager.showTaskBarSelectedBorder(taskBarNode); @@ -678,6 +712,9 @@ function bindTableGroupListener(event: EventManager) { }); scene.ganttGroup.addEventListener('pointerleave', (e: FederatedPointerEvent) => { + if (scene._gantt.scenegraph.taskBar.currentHoverLocateIcon) { + scene._gantt.scenegraph.taskBar.setLocateIconHover(null); + } if ( (gantt.parsedOptions.scrollStyle.horizontalVisible && gantt.parsedOptions.scrollStyle.horizontalVisible === 'focus') || diff --git a/packages/vtable-gantt/src/gantt-helper.ts b/packages/vtable-gantt/src/gantt-helper.ts index 87f347345..47f09f056 100644 --- a/packages/vtable-gantt/src/gantt-helper.ts +++ b/packages/vtable-gantt/src/gantt-helper.ts @@ -136,6 +136,8 @@ export function initOptions(gantt: Gantt) { gantt.parsedOptions.baselineEndDateField = options.taskBar?.baselineEndDateField; gantt.parsedOptions.baselinePosition = options.taskBar?.baselinePosition ?? 'bottom'; gantt.parsedOptions.taskBarClip = options?.taskBar?.clip ?? true; + // 是否开启“任务条超出可视区”的定位图标能力(默认关闭) + gantt.parsedOptions.taskBarLocateIcon = options?.taskBar?.locateIcon ?? false; gantt.parsedOptions.projectSubTasksExpandable = options?.projectSubTasksExpandable ?? true; // gantt.parsedOptions.minDate = options?.minDate // ? gantt.parsedOptions.timeScaleIncludeHour diff --git a/packages/vtable-gantt/src/scenegraph/task-bar.ts b/packages/vtable-gantt/src/scenegraph/task-bar.ts index 4e957d2b0..244f47737 100644 --- a/packages/vtable-gantt/src/scenegraph/task-bar.ts +++ b/packages/vtable-gantt/src/scenegraph/task-bar.ts @@ -12,6 +12,12 @@ const TASKBAR_HOVER_ICON = ` `; export const TASKBAR_HOVER_ICON_WIDTH = 10; +const LOCATE_ICON_SIZE = 22; +const LOCATE_ICON_PADDING = 4; +const LOCATE_ICON_BG = '#f2f3f5'; +const LOCATE_ICON_BG_HOVER = '#4080ff'; +const LOCATE_ICON_ARROW = '#4e5969'; +const LOCATE_ICON_ARROW_HOVER = '#ffffff'; export class TaskBar { formatMilestoneText(text: string, record: any): string { @@ -98,6 +104,8 @@ export class TaskBar { hoverBarLeftIcon: Image; hoverBarRightIcon: Image; hoverBarProgressHandle: Group; + locateIconsGroup?: Group; + currentHoverLocateIcon: Group | null; _scene: Scenegraph; width: number; height: number; @@ -119,6 +127,10 @@ export class TaskBar { scene.ganttGroup.addChild(this.group); this.initBars(); this.initHoverBarIcons(); + if (scene._gantt.parsedOptions.taskBarLocateIcon) { + // 定位图标层:用于提示“任务条在当前可视区外”,并支持一键滚动定位 + this.initLocateIconsGroup(); + } } initBars() { @@ -525,6 +537,7 @@ export class TaskBar { if (baselineBar) { this.barContainer.insertBefore(baselineBar, barGroupBox); } + this.updateOffscreenIndicators(); } initHoverBarIcons() { const hoverBarGroup = new Group({ @@ -602,11 +615,176 @@ export class TaskBar { hoverBarGroup.appendChild(progressHandle); } + initLocateIconsGroup() { + // 覆盖在任务条区域之上(clip = true),仅用于绘制定位图标,避免受任务条容器滚动影响 + const locateIconsGroup = new Group({ + x: 0, + y: 0, + width: this.width, + height: this.height, + clip: true, + pickable: false + }); + this.locateIconsGroup = locateIconsGroup; + locateIconsGroup.name = 'task-bar-locate-icons'; + this.group.appendChild(locateIconsGroup); + } + + applyLocateIconStyle(icon: Group, hover: boolean) { + const background = (icon as any).background; + const arrow = (icon as any).arrow; + if (background) { + background.setAttribute('fill', hover ? LOCATE_ICON_BG_HOVER : LOCATE_ICON_BG); + } + if (arrow) { + arrow.setAttribute('fill', hover ? LOCATE_ICON_ARROW_HOVER : LOCATE_ICON_ARROW); + } + } + + createLocateIcon(side: 'left' | 'right', target: GanttTaskBarNode) { + const iconGroup = new Group({ + x: 0, + y: 0, + width: LOCATE_ICON_SIZE, + height: LOCATE_ICON_SIZE, + pickable: true, + cursor: 'pointer', + visibleAll: false + }); + iconGroup.name = side === 'left' ? 'task-bar-locate-icon-left' : 'task-bar-locate-icon-right'; + (iconGroup as any).attachedToTaskBarNode = target; + (iconGroup as any).side = side; + const background = createRect({ + x: 0, + y: 0, + width: LOCATE_ICON_SIZE, + height: LOCATE_ICON_SIZE, + cornerRadius: 4, + fill: LOCATE_ICON_BG, + pickable: false + }); + const arrowSize = 6; + const center = LOCATE_ICON_SIZE / 2; + const arrow = + side === 'left' + ? new Polygon({ + points: [ + { x: center + arrowSize / 2, y: center - arrowSize }, + { x: center - arrowSize / 2, y: center }, + { x: center + arrowSize / 2, y: center + arrowSize } + ], + fill: LOCATE_ICON_ARROW, + pickable: false + }) + : new Polygon({ + points: [ + { x: center - arrowSize / 2, y: center - arrowSize }, + { x: center + arrowSize / 2, y: center }, + { x: center - arrowSize / 2, y: center + arrowSize } + ], + fill: LOCATE_ICON_ARROW, + pickable: false + }); + iconGroup.appendChild(background); + iconGroup.appendChild(arrow); + (iconGroup as any).background = background; + (iconGroup as any).arrow = arrow; + this.applyLocateIconStyle(iconGroup, false); + return iconGroup; + } + + setLocateIconHover(icon: Group | null) { + if (this.currentHoverLocateIcon && this.currentHoverLocateIcon !== icon) { + this.applyLocateIconStyle(this.currentHoverLocateIcon, false); + } + if (icon) { + this.applyLocateIconStyle(icon, true); + } + this.currentHoverLocateIcon = icon; + this._scene.updateNextFrame(); + } + + updateOffscreenIndicators() { + if (!this.locateIconsGroup) { + return; + } + // 任务条相对 barContainer 的坐标系:与滚动值一致(scrollLeft / scrollTop) + const gantt = this._scene._gantt; + const scrollLeft = gantt.stateManager.scrollLeft; + const scrollTop = gantt.stateManager.scrollTop; + const viewWidth = gantt.tableNoFrameWidth; + const viewHeight = this.height; + const visibleLeft = scrollLeft; + const visibleRight = scrollLeft + viewWidth; + const visibleTop = scrollTop; + const visibleBottom = scrollTop + viewHeight; + + let child = this.barContainer.firstChild as any; + while (child) { + if (child.name === 'task-bar') { + const bar = child as GanttTaskBarNode; + const barLeft = bar.attribute.x; + const barRight = barLeft + bar.attribute.width; + const barTop = bar.attribute.y; + const barBottom = barTop + bar.attribute.height; + // 仅当该行在纵向可视范围内时,才展示横向定位图标 + const verticalVisible = barBottom >= visibleTop && barTop <= visibleBottom; + let side: 'left' | 'right' | null = null; + if (verticalVisible) { + if (barRight < visibleLeft) { + side = 'left'; + } else if (barLeft > visibleRight) { + side = 'right'; + } + } + const leftIcon = (bar as any).locateLeftIcon as Group; + const rightIcon = (bar as any).locateRightIcon as Group; + if (!side) { + // 使用 visibleAll 关闭整组显隐(包含子图形),避免只隐藏 group 导致残留 + leftIcon?.setAttribute('visibleAll', false); + rightIcon?.setAttribute('visibleAll', false); + if (this.currentHoverLocateIcon === leftIcon || this.currentHoverLocateIcon === rightIcon) { + this.setLocateIconHover(null); + } + } else { + let icon = side === 'left' ? leftIcon : rightIcon; + if (!icon) { + icon = this.createLocateIcon(side, bar); + if (side === 'left') { + (bar as any).locateLeftIcon = icon; + } else { + (bar as any).locateRightIcon = icon; + } + this.locateIconsGroup.appendChild(icon); + } else if (icon.parent !== this.locateIconsGroup) { + this.locateIconsGroup.appendChild(icon); + } + const iconX = side === 'left' ? LOCATE_ICON_PADDING : viewWidth - LOCATE_ICON_SIZE - LOCATE_ICON_PADDING; + // 图标固定在左右边缘,y 跟随任务条行,并转换到“可视区坐标系” + const iconY = barTop - scrollTop + (bar.attribute.height - LOCATE_ICON_SIZE) / 2; + icon.setAttributes({ + x: iconX, + y: iconY, + visibleAll: true + }); + const otherIcon = side === 'left' ? rightIcon : leftIcon; + otherIcon?.setAttribute('visibleAll', false); + if (this.currentHoverLocateIcon === otherIcon) { + this.setLocateIconHover(null); + } + } + } + child = child._next; + } + } + setX(x: number) { this.barContainer.setAttribute('x', x); + this.updateOffscreenIndicators(); } setY(y: number) { this.barContainer.setAttribute('y', y); + this.updateOffscreenIndicators(); } /** 重新创建任务条节点 */ refresh() { @@ -617,6 +795,11 @@ export class TaskBar { width: this.width, y: this._scene._gantt.getAllHeaderRowsHeight() }); + this.locateIconsGroup?.setAttributes({ + width: this.width, + height: this.height + }); + this.locateIconsGroup?.removeAllChild(); const x = this.barContainer.attribute.x; const y = this.barContainer.attribute.y; this.barContainer.removeAllChild(); @@ -624,12 +807,18 @@ export class TaskBar { this.initBars(); this.setX(x); this.setY(y); + this.updateOffscreenIndicators(); } resize() { this.width = this._scene._gantt.tableNoFrameWidth; this.height = this._scene._gantt.gridHeight; this.group.setAttribute('width', this.width); this.group.setAttribute('height', this.height); + this.locateIconsGroup?.setAttributes({ + width: this.width, + height: this.height + }); + this.updateOffscreenIndicators(); } showHoverBar(x: number, y: number, width: number, height: number, target?: GanttTaskBarNode) { diff --git a/packages/vtable-gantt/src/ts-types/gantt-engine.ts b/packages/vtable-gantt/src/ts-types/gantt-engine.ts index 2bf81c6a5..cb33f7b05 100644 --- a/packages/vtable-gantt/src/ts-types/gantt-engine.ts +++ b/packages/vtable-gantt/src/ts-types/gantt-engine.ts @@ -150,6 +150,8 @@ export interface GanttConstructorOptions { }; /** 数据没有排期时,可通过创建任务条排期。默认为true */ scheduleCreatable?: boolean | ((interactionArgs: TaskBarInteractionArgumentType) => boolean); + /** 是否开启“任务条超出可视区”定位图标能力。默认 false */ + locateIcon?: boolean; /** 针对没有分配日期的任务,可以显示出创建按钮 */ scheduleCreation?: { buttonStyle?: ILineStyle & { From 2538030cd9fbbb8afc2a13d843d5f0ade275cdb1 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 25 Mar 2026 11:56:26 +0800 Subject: [PATCH 2/9] docs: update changlog of rush --- .../feat-gantt-autoLocationIcon_2026-03-25-03-56.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json diff --git a/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json b/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json new file mode 100644 index 000000000..9f994c0b8 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: gantt add locateIcon for taskbar\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file From 85c8ebd8294dd5846d57b2ab184102fb1142f0ff Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 25 Mar 2026 15:38:02 +0800 Subject: [PATCH 3/9] docs: update gantt locate icon --- .../assets/demo/en/gantt/gantt-locate-taskbar.md | 3 +-- .../assets/demo/zh/gantt/gantt-locate-taskbar.md | 3 +-- docs/assets/guide/en/gantt/Getting_Started.md | 16 ---------------- docs/assets/guide/en/gantt/introduction.md | 14 ++++++++++++++ docs/assets/guide/zh/gantt/Getting_Started.md | 16 ---------------- docs/assets/guide/zh/gantt/introduction.md | 14 ++++++++++++++ 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/docs/assets/demo/en/gantt/gantt-locate-taskbar.md b/docs/assets/demo/en/gantt/gantt-locate-taskbar.md index b999735dd..6fad2d3b8 100644 --- a/docs/assets/demo/en/gantt/gantt-locate-taskbar.md +++ b/docs/assets/demo/en/gantt/gantt-locate-taskbar.md @@ -2,7 +2,7 @@ category: examples group: gantt title: Task Bar Locate (Offscreen Indicator) -cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-basic-preview.png +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-locate-taskbar.gif link: gantt/Getting_Started option: Gantt#taskBar --- @@ -20,7 +20,6 @@ When the timeline is long, task bars may be outside the current viewport. This d ```javascript livedemo template=vtable // import * as VTableGantt from '@visactor/vtable-gantt'; let ganttInstance; -const CONTAINER_ID = 'vTable'; const records = [ { id: 1, title: 'Offscreen on the left', start: '2024-02-05', end: '2024-02-20', progress: 20 }, diff --git a/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md b/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md index 11ac227e5..3a5710661 100644 --- a/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md +++ b/docs/assets/demo/zh/gantt/gantt-locate-taskbar.md @@ -2,7 +2,7 @@ category: examples group: gantt title: 甘特图任务条定位(超出可视区提示) -cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-basic-preview.png +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-locate-taskbar.gif link: gantt/Getting_Started option: Gantt#taskBar --- @@ -20,7 +20,6 @@ option: Gantt#taskBar ```javascript livedemo template=vtable // import * as VTableGantt from '@visactor/vtable-gantt'; let ganttInstance; -const CONTAINER_ID = 'vTable'; const records = [ { id: 1, title: '任务条在左侧不可见', start: '2024-02-05', end: '2024-02-20', progress: 20 }, diff --git a/docs/assets/guide/en/gantt/Getting_Started.md b/docs/assets/guide/en/gantt/Getting_Started.md index d444eb973..cb39c61bb 100644 --- a/docs/assets/guide/en/gantt/Getting_Started.md +++ b/docs/assets/guide/en/gantt/Getting_Started.md @@ -351,20 +351,4 @@ At this point, you have successfully drawn a simple Gantt chart! When the smallest time scale is `unit: 'day'` and `step: 1`, you can use `timelineHeader.weekendColWidth` to override weekend column width (or set `timelineHeader.hideWeekend` to hide weekend columns). -## Task Bar Locate (Optional) - -When the timeline is long and the task bar is outside the current viewport, you can enable the “locate icon” feature: an icon is shown on the left/right edge of the gantt view, and clicking it scrolls the task bar into the viewport. - -Key option: - -```javascript -const option = { - taskBar: { - locateIcon: true - } -}; -``` - -Demo: `gantt-locate-taskbar`. - I hope this tutorial helps you learn how to use Gantt. Next, you can delve into the various configuration options of vtable-gantt to customize more diverse table effects. diff --git a/docs/assets/guide/en/gantt/introduction.md b/docs/assets/guide/en/gantt/introduction.md index 8e8f87143..9042c1dc8 100644 --- a/docs/assets/guide/en/gantt/introduction.md +++ b/docs/assets/guide/en/gantt/introduction.md @@ -133,6 +133,20 @@ You can set whether task bars are resizable through the `taskBar.resizable` conf You can set whether task bars are adjustable through the `taskBar.progressAdjustable` configuration item. +#### Task Bar Locate + +When the timeline is long and the task bar is outside the current viewport, you can enable the “locate icon” feature: an icon is shown on the left/right edge of the gantt view, and clicking it scrolls the task bar into the viewport. + +Key option: + +```javascript +const option = { + taskBar: { + locateIcon: true + } +}; +``` + #### Adjusting the Width of the Left Table You can set the divider line to be draggable by configuring `frame.verticalSplitLineMoveable` to true. diff --git a/docs/assets/guide/zh/gantt/Getting_Started.md b/docs/assets/guide/zh/gantt/Getting_Started.md index 462d68c62..d2e0912ea 100644 --- a/docs/assets/guide/zh/gantt/Getting_Started.md +++ b/docs/assets/guide/zh/gantt/Getting_Started.md @@ -351,20 +351,4 @@ window['ganttInstance'] = ganttInstance; 当最小粒度为天且 `unit: 'day'`、`step: 1` 时,可以通过 `timelineHeader.weekendColWidth` 覆盖周末列宽度(或通过 `timelineHeader.hideWeekend` 隐藏周末列)。 -## 任务条定位(可选) - -当时间轴较长、任务条不在当前可视区域内时,可以开启“定位图标”能力:在甘特图左右边缘显示图标,点击后自动滚动到该任务条的可视区域。 - -关键配置: - -```javascript -const option = { - taskBar: { - locateIcon: true - } -}; -``` - -可参考示例:`gantt-locate-taskbar`。 - 希望这篇教程对你学习如何使用 Gantt 有所帮助。接下来可以深入了解 vtable-gantt 的各种配置选项,定制出更加丰富多样的表格效果。 diff --git a/docs/assets/guide/zh/gantt/introduction.md b/docs/assets/guide/zh/gantt/introduction.md index e9836d0cc..401699ccf 100644 --- a/docs/assets/guide/zh/gantt/introduction.md +++ b/docs/assets/guide/zh/gantt/introduction.md @@ -133,6 +133,20 @@ links:[ 通过 `taskBar.progressAdjustable` 配置项,可以设置任务条是否可调整进度。 +#### 任务条定位 + +当时间轴较长、任务条不在当前可视区域内时,可以开启“定位图标”能力:在甘特图左右边缘显示图标,点击后自动滚动到该任务条的可视区域。 + +关键配置: + +```javascript +const option = { + taskBar: { + locateIcon: true + } +}; +``` + #### 调整左侧表格宽度 通过 `frame.verticalSplitLineMoveable` 配置为 true,可以设置分割线可拖拽。 From 083ce18d0746b2f84beec4c060d770192694ef33 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 25 Mar 2026 16:11:11 +0800 Subject: [PATCH 4/9] docs: update change log --- .../vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json b/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json index 4f2c6adbc..f3bc869cb 100644 --- a/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json +++ b/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json @@ -2,7 +2,7 @@ "changes": [ { "comment": "feat: add option scrollFrozenCols\n\n", - "type": "none", + "type": "minor", "packageName": "@visactor/vtable" } ], From eba665b7be231440bd8d9e1b405184b66432d25b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 08:26:48 +0000 Subject: [PATCH 5/9] build: prelease version 1.25.0 --- ...at-frozenColumn_scroll_2026-03-20-11-09.json | 11 ----------- ...gantt-autoLocationIcon_2026-03-25-03-56.json | 11 ----------- common/config/rush/version-policies.json | 2 +- packages/openinula-vtable/package.json | 2 +- packages/react-vtable/package.json | 2 +- packages/vtable-calendar/package.json | 2 +- packages/vtable-editors/package.json | 2 +- packages/vtable-export/package.json | 2 +- packages/vtable-gantt/package.json | 2 +- packages/vtable-plugins/package.json | 2 +- packages/vtable-search/package.json | 2 +- packages/vtable-sheet/package.json | 2 +- packages/vtable/CHANGELOG.json | 17 +++++++++++++++++ packages/vtable/CHANGELOG.md | 17 ++++++++++++++++- packages/vtable/package.json | 2 +- packages/vue-vtable/package.json | 2 +- 16 files changed, 45 insertions(+), 35 deletions(-) delete mode 100644 common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json delete mode 100644 common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json diff --git a/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json b/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json deleted file mode 100644 index f3bc869cb..000000000 --- a/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "feat: add option scrollFrozenCols\n\n", - "type": "minor", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "892739385@qq.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json b/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json deleted file mode 100644 index 9f994c0b8..000000000 --- a/common/changes/@visactor/vtable/feat-gantt-autoLocationIcon_2026-03-25-03-56.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "feat: gantt add locateIcon for taskbar\n\n", - "type": "none", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "892739385@qq.com" -} \ No newline at end of file diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index bead9d4a6..9d40f100f 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.24.0","mainProject":"@visactor/vtable","nextBump":"minor"}] +[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.25.0","mainProject":"@visactor/vtable","nextBump":"minor"}] diff --git a/packages/openinula-vtable/package.json b/packages/openinula-vtable/package.json index 4785ce163..1b1f8db29 100644 --- a/packages/openinula-vtable/package.json +++ b/packages/openinula-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/openinula-vtable", - "version": "1.24.0", + "version": "1.25.0", "description": "The openinula version of VTable", "keywords": [ "openinula", diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index 9c9510b4e..c8e770151 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vtable", - "version": "1.24.0", + "version": "1.25.0", "description": "The react version of VTable", "keywords": [ "react", diff --git a/packages/vtable-calendar/package.json b/packages/vtable-calendar/package.json index 58bf93956..bc2031524 100644 --- a/packages/vtable-calendar/package.json +++ b/packages/vtable-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-calendar", - "version": "1.24.0", + "version": "1.25.0", "description": "The calendar component of VTable", "author": { "name": "VisActor", diff --git a/packages/vtable-editors/package.json b/packages/vtable-editors/package.json index 5f2ba92b5..76fca6389 100644 --- a/packages/vtable-editors/package.json +++ b/packages/vtable-editors/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-editors", - "version": "1.24.0", + "version": "1.25.0", "description": "", "sideEffects": false, "main": "cjs/index.js", diff --git a/packages/vtable-export/package.json b/packages/vtable-export/package.json index 2423db425..1b4f16171 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-export", - "version": "1.24.0", + "version": "1.25.0", "description": "The export util of VTable", "author": { "name": "VisActor", diff --git a/packages/vtable-gantt/package.json b/packages/vtable-gantt/package.json index d8d68f521..918c054b8 100644 --- a/packages/vtable-gantt/package.json +++ b/packages/vtable-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-gantt", - "version": "1.24.0", + "version": "1.25.0", "description": "canvas table width high performance", "keywords": [ "vtable-gantt", diff --git a/packages/vtable-plugins/package.json b/packages/vtable-plugins/package.json index f4ff591d8..8427cc664 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-plugins", - "version": "1.24.0", + "version": "1.25.0", "description": "The search util of VTable", "author": { "name": "VisActor", diff --git a/packages/vtable-search/package.json b/packages/vtable-search/package.json index 69ecaf65c..8b03e0d76 100644 --- a/packages/vtable-search/package.json +++ b/packages/vtable-search/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-search", - "version": "1.24.0", + "version": "1.25.0", "description": "The search util of VTable", "author": { "name": "VisActor", diff --git a/packages/vtable-sheet/package.json b/packages/vtable-sheet/package.json index 2c5bd6edf..b63978105 100644 --- a/packages/vtable-sheet/package.json +++ b/packages/vtable-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-sheet", - "version": "1.24.0", + "version": "1.25.0", "description": "Lightweight editable spreadsheet component based on VTable", "keywords": [ "vtable-sheet", diff --git a/packages/vtable/CHANGELOG.json b/packages/vtable/CHANGELOG.json index 72814fca2..1addba71d 100644 --- a/packages/vtable/CHANGELOG.json +++ b/packages/vtable/CHANGELOG.json @@ -1,6 +1,23 @@ { "name": "@visactor/vtable", "entries": [ + { + "version": "1.25.0", + "tag": "@visactor/vtable_v1.25.0", + "date": "Wed, 25 Mar 2026 08:15:12 GMT", + "comments": { + "minor": [ + { + "comment": "feat: add option scrollFrozenCols\n\n" + } + ], + "none": [ + { + "comment": "feat: gantt add locateIcon for taskbar\n\n" + } + ] + } + }, { "version": "1.24.0", "tag": "@visactor/vtable_v1.24.0", diff --git a/packages/vtable/CHANGELOG.md b/packages/vtable/CHANGELOG.md index b3a961882..790fba79f 100644 --- a/packages/vtable/CHANGELOG.md +++ b/packages/vtable/CHANGELOG.md @@ -1,6 +1,21 @@ # Change Log - @visactor/vtable -This log was last generated on Wed, 18 Mar 2026 09:15:31 GMT and should not be manually modified. +This log was last generated on Wed, 25 Mar 2026 08:15:12 GMT and should not be manually modified. + +## 1.25.0 +Wed, 25 Mar 2026 08:15:12 GMT + +### Minor changes + +- feat: add option scrollFrozenCols + + + +### Updates + +- feat: gantt add locateIcon for taskbar + + ## 1.24.0 Wed, 18 Mar 2026 09:15:31 GMT diff --git a/packages/vtable/package.json b/packages/vtable/package.json index fdcb05724..2dfcda655 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable", - "version": "1.24.0", + "version": "1.25.0", "description": "canvas table width high performance", "keywords": [ "grid", diff --git a/packages/vue-vtable/package.json b/packages/vue-vtable/package.json index 0267260bb..f315fa844 100644 --- a/packages/vue-vtable/package.json +++ b/packages/vue-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vue-vtable", - "version": "1.24.0", + "version": "1.25.0", "description": "The vue version of VTable", "keywords": [ "vue", From c6b222d4a6291f1aef8da2b5944ff3481352fe6b Mon Sep 17 00:00:00 2001 From: fangsmile Date: Wed, 25 Mar 2026 08:49:27 +0000 Subject: [PATCH 6/9] docs: generate changelog of release v1.25.0 --- docs/assets/changelog/en/release.md | 14 ++++++++++++++ docs/assets/changelog/zh/release.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index 332816b72..6c202bbb7 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,17 @@ +# v1.25.0 + +2026-03-25 + + +**🆕 New feature** + +- **@visactor/vtable**: gantt add locateIcon for taskbar +- **@visactor/vtable**: add option scrollFrozenCols support frozen columns can be scrolled + + + +[more detail about v1.25.0](https://github.com/VisActor/VTable/releases/tag/v1.25.0) + # v1.24.0 2026-03-18 diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 981902410..6d9980afb 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,17 @@ +# v1.25.0 + +2026-03-25 + + +**🆕 新增功能** + +- **@visactor/vtable**: gantt add locateIcon for taskbar +- **@visactor/vtable**: add option scrollFrozenCols support frozen columns can be scrolled + + + +[更多详情请查看 v1.25.0](https://github.com/VisActor/VTable/releases/tag/v1.25.0) + # v1.24.0 2026-03-18 From 24d2342509b206e7681aa0be0021a8c4f9c2c6af Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 25 Mar 2026 17:17:41 +0800 Subject: [PATCH 7/9] docs: update release log --- docs/assets/changelog/zh/release.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 6d9980afb..3a7d14567 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -4,10 +4,10 @@ **🆕 新增功能** - -- **@visactor/vtable**: gantt add locateIcon for taskbar -- **@visactor/vtable**: add option scrollFrozenCols support frozen columns can be scrolled - + +- **@visactor/vtable**: 甘特图新增定位图标用于当任务时间不在可视范围内时,快速定位任务条 +- **@visactor/vtable**: 左右冻结列区域支持单独滚动 + [更多详情请查看 v1.25.0](https://github.com/VisActor/VTable/releases/tag/v1.25.0) From fe9df4e71be2f6fa9d7cd43931d3868964f71870 Mon Sep 17 00:00:00 2001 From: Erica-cod <2779428708@qq.com> Date: Sun, 29 Mar 2026 22:25:48 -0400 Subject: [PATCH 8/9] fix: degrade radio/checkbox cells to text in aggregation rows #4027 Aggregation rows should display plain text instead of radio/checkbox controls, as interactive controls are meaningless for aggregated data. Changed the condition from isAggregation && isSeriesNumber to isAggregation for both checkbox and radio cell types. Added demo case for verification. Made-with: Cursor --- packages/vtable/examples/menu.ts | 4 + .../vtable/examples/type/radio-aggregation.ts | 110 ++++++++++++++++++ .../scenegraph/group-creater/cell-helper.ts | 68 +++++++---- 3 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 packages/vtable/examples/type/radio-aggregation.ts diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 658a0acdf..7868a6d2a 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -580,6 +580,10 @@ export const menus = [ path: 'type', name: 'radio' }, + { + path: 'type', + name: 'radio-aggregation' + }, { path: 'type', name: 'switch' diff --git a/packages/vtable/examples/type/radio-aggregation.ts b/packages/vtable/examples/type/radio-aggregation.ts new file mode 100644 index 000000000..dba773498 --- /dev/null +++ b/packages/vtable/examples/type/radio-aggregation.ts @@ -0,0 +1,110 @@ +import * as VTable from '../../src'; +import { AggregationType } from '../../src/ts-types'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const ListTable = VTable.ListTable; +const CONTAINER_ID = 'vTable'; + +/** + * 验证场景:合计行中 radio 单元格应降级为文本单元格,与 checkbox 行为保持一致。 + * 对应 issue: https://github.com/VisActor/VTable/issues/4027 + * + * 复现步骤(修复前): + * 1. 表格配置了 bottomFrozenRowCount 和 aggregation(合计行) + * 2. 某列配置 cellType: 'radio' + * 3. 修复前:合计行中 radio 渲染异常,可能导致表格整体空白 + * 4. 修复后:合计行中 radio 列正确降级为纯文本显示,表格正常渲染 + */ +export function createTable() { + const data = [ + { percent: '100%', value: 20, check: { text: 'unchecked', checked: false, disable: false } }, + { percent: '80%', value: 18, check: { text: 'checked', checked: true, disable: false } }, + { percent: '20%', value: 12, check: { text: 'unchecked', checked: false, disable: false } }, + { percent: '0%', value: 10, check: { text: 'checked', checked: false, disable: false } }, + { percent: '60%', value: 16, check: { text: 'disable', checked: true, disable: true } }, + { percent: '40%', value: 14, check: { text: 'disable', checked: false, disable: true } }, + { percent: '0%', value: -10, check: true }, + { percent: '0%', value: -10, check: ['选中', '选中'] } + ]; + let records: any[] = []; + for (let i = 0; i < 10; i++) { + records = records.concat(data); + } + + const columns: VTable.ColumnsDefine = [ + { + field: 'percent', + title: 'percent', + width: 120, + sort: true + }, + { + field: 'value', + title: 'value', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.SUM, + formatFun(value) { + return '合计: ' + Math.round(value); + } + } + ] + }, + { + field: 'percent', + title: 'column radio', + width: 120, + cellType: 'radio' + }, + { + field: 'check', + title: 'cell radio', + width: 200, + cellType: 'radio', + radioCheckType: 'cell', + radioDirectionInCell: 'vertical', + style: { + spaceBetweenRadio: 10 + } + }, + { + field: 'percent', + title: 'checkbox', + width: 120, + cellType: 'checkbox', + disable: true, + checked: true + } + ]; + + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + columns, + records, + widthMode: 'standard', + heightMode: 'autoHeight', + bottomFrozenRowCount: 1, + rowSeriesNumber: { + width: 50, + cellType: 'checkbox' + }, + theme: VTable.themes.DEFAULT.extends({ + bottomFrozenStyle: { + fontWeight: 500 + } + }) + }; + + const instance = new ListTable(option); + + bindDebugTool(instance.scenegraph.stage as any, { + customGrapicKeys: ['role', '_updateTag'] + }); + + const { RADIO_STATE_CHANGE } = VTable.ListTable.EVENT_TYPE; + instance.on(RADIO_STATE_CHANGE, e => { + console.log('Radio state changed:', e.col, e.row, e.radioIndexInCell); + }); + + (window as any).tableInstance = instance; +} diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 88217769e..4268b776d 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -374,8 +374,7 @@ export function createCell( } else if (type === 'checkbox') { const isAggregation = 'isAggregation' in table.internalProps.layoutMap && table.internalProps.layoutMap.isAggregation(col, row); - const isSeriesNumber = table.internalProps.layoutMap.isSeriesNumber(col, row); - if (isAggregation && isSeriesNumber) { + if (isAggregation) { const createTextCellGroup = Factory.getFunction('createTextCellGroup') as CreateTextCellGroup; cellGroup = createTextCellGroup( table, @@ -423,25 +422,52 @@ export function createCell( ); } } else if (type === 'radio') { - const createRadioCellGroup = Factory.getFunction('createRadioCellGroup') as CreateRadioCellGroup; - cellGroup = createRadioCellGroup( - null, - columnGroup, - 0, - y, - col, - row, - colWidth, - cellWidth, - cellHeight, - padding, - textAlign, - textBaseline, - table, - cellTheme, - define as RadioColumnDefine, - range - ); + const isAggregation = + 'isAggregation' in table.internalProps.layoutMap && table.internalProps.layoutMap.isAggregation(col, row); + if (isAggregation) { + const createTextCellGroup = Factory.getFunction('createTextCellGroup') as CreateTextCellGroup; + cellGroup = createTextCellGroup( + table, + value, + columnGroup, + 0, + y, + col, + row, + colWidth, + cellWidth, + cellHeight, + padding, + textAlign, + textBaseline, + false, + undefined, + true, + cellTheme, + range, + isAsync + ); + } else { + const createRadioCellGroup = Factory.getFunction('createRadioCellGroup') as CreateRadioCellGroup; + cellGroup = createRadioCellGroup( + null, + columnGroup, + 0, + y, + col, + row, + colWidth, + cellWidth, + cellHeight, + padding, + textAlign, + textBaseline, + table, + cellTheme, + define as RadioColumnDefine, + range + ); + } } else if (type === 'switch') { const createSwitchCellGroup = Factory.getFunction('createSwitchCellGroup') as CreateSwitchCellGroup; cellGroup = createSwitchCellGroup( From c98725a25b9b1e006583e3d63c23dd8109ffe0cb Mon Sep 17 00:00:00 2001 From: Erica-cod <2779428708@qq.com> Date: Sun, 29 Mar 2026 22:28:53 -0400 Subject: [PATCH 9/9] docs: update changelog of rush Made-with: Cursor --- ...adio-aggregation-row-4027-v2_2026-03-30-02-28.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable/fix-radio-aggregation-row-4027-v2_2026-03-30-02-28.json diff --git a/common/changes/@visactor/vtable/fix-radio-aggregation-row-4027-v2_2026-03-30-02-28.json b/common/changes/@visactor/vtable/fix-radio-aggregation-row-4027-v2_2026-03-30-02-28.json new file mode 100644 index 000000000..cee3e82dd --- /dev/null +++ b/common/changes/@visactor/vtable/fix-radio-aggregation-row-4027-v2_2026-03-30-02-28.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: degrade radio/checkbox cells to text in aggregation rows", + "type": "patch", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "2779428708@qq.com" +} \ No newline at end of file