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