From 3fb2a05101d5552550b356da0e74be798c41657c Mon Sep 17 00:00:00 2001
From: fangsmile <fangsmile@users.noreply.github.com>
Date: Wed, 18 Mar 2026 09:39:16 +0000
Subject: [PATCH 01/16] docs: generate changelog of release v1.24.0

---
 docs/assets/changelog/en/release.md | 20 ++++++++++++++++++++
 docs/assets/changelog/zh/release.md | 20 ++++++++++++++++++++
 2 files changed, 40 insertions(+)

diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md
index 89dc2f7f7..332816b72 100644
--- a/docs/assets/changelog/en/release.md
+++ b/docs/assets/changelog/en/release.md
@@ -1,3 +1,23 @@
+# v1.24.0
+
+2026-03-18
+
+
+**🆕 New feature**
+
+- **@visactor/vtable-gantt**: gantt weekend column width support setting
+- **@visactor/vtable-sheet**: add undo/redo to sheet
+
+**🐛 Bug fix**
+
+- **@visactor/vtable**: groupBy with frozenColCount and enableTreeStickCell usage problem
+- **@visactor/vtable**: when just has only one column groupTitle not show
+- **@visactor/vtable**: checkbox cell render error when set groupBy with rowSeriesNumber
+
+
+
+[more detail about v1.24.0](https://github.com/VisActor/VTable/releases/tag/v1.24.0)
+
 # v1.23.3
 
 2026-03-05
diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md
index 02cacd16e..c4d681481 100644
--- a/docs/assets/changelog/zh/release.md
+++ b/docs/assets/changelog/zh/release.md
@@ -1,3 +1,23 @@
+# v1.24.0
+
+2026-03-18
+
+
+**🆕 新增功能**
+
+- **@visactor/vtable-gantt**: gantt weekend column width support setting
+- **@visactor/vtable-sheet**: add undo/redo to sheet
+
+**🐛 功能修复**
+
+- **@visactor/vtable**: groupBy with frozenColCount and enableTreeStickCell usage problem
+- **@visactor/vtable**: when just has only one column groupTitle not show
+- **@visactor/vtable**: checkbox cell render error when set groupBy with rowSeriesNumber
+
+
+
+[更多详情请查看 v1.24.0](https://github.com/VisActor/VTable/releases/tag/v1.24.0)
+
 # v1.23.3
 
 2026-03-05

From 750a54da9b122bb76f98911bf770f98b9e0c1e87 Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Wed, 18 Mar 2026 17:55:33 +0800
Subject: [PATCH 02/16] docs: update release log

---
 docs/assets/changelog/zh/release.md | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md
index c4d681481..981902410 100644
--- a/docs/assets/changelog/zh/release.md
+++ b/docs/assets/changelog/zh/release.md
@@ -4,16 +4,16 @@
 
 
 **🆕 新增功能**
-
-- **@visactor/vtable-gantt**: gantt weekend column width support setting
-- **@visactor/vtable-sheet**: add undo/redo to sheet
+
+- **@visactor/vtable-gantt**: 甘特图配置支持设置周末列宽度
+- **@visactor/vtable-sheet**: 电子表格新增撤销/重做功能
 
 **🐛 功能修复**
-
-- **@visactor/vtable**: groupBy with frozenColCount and enableTreeStickCell usage problem
-- **@visactor/vtable**: when just has only one column groupTitle not show
-- **@visactor/vtable**: checkbox cell render error when set groupBy with rowSeriesNumber
-
+
+- **@visactor/vtable**: 修复 groupBy 分组表配合使用 frozenColCount 和 enableTreeStickCell 时滚动错位的问题
+- **@visactor/vtable**: 修复当只有一列时，分组标题不显示问题
+- **@visactor/vtable**: 修复当设置分组表时，复选框单元格渲染错误问题
+
 
 
 [更多详情请查看 v1.24.0](https://github.com/VisActor/VTable/releases/tag/v1.24.0)

From ad1ff5b51cb662e912bdc0425294fd2acbe60ffe Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Fri, 20 Mar 2026 19:06:32 +0800
Subject: [PATCH 03/16] feat: add option scrollFrozenCols

---
 .../demo/auto-fill/auto-fill.ts               |  33 +++
 .../list-table-scrollable-frozen-cols.ts      |  75 ++++++
 packages/vtable/examples/menu.ts              |   4 +
 packages/vtable/src/core/BaseTable.ts         |  66 ++++--
 packages/vtable/src/core/tableHelper.ts       |   4 +-
 .../src/core/utils/get-cell-position.ts       |  16 +-
 .../src/event/listener/container-dom.ts       |   7 +-
 .../vtable/src/event/listener/table-group.ts  |  38 ++--
 packages/vtable/src/event/scroll.ts           |  50 +++-
 packages/vtable/src/event/util.ts             |  41 ++++
 .../scenegraph/component/table-component.ts   |   8 +-
 .../vtable/src/scenegraph/component/util.ts   |   6 +-
 .../group-contribution-render.ts              | 130 ++++++++---
 .../group-creater/init-scenegraph.ts          |  55 +++++
 .../group-creater/progress/proxy.ts           |   4 +-
 packages/vtable/src/scenegraph/scenegraph.ts  | 213 +++++++++++++++++-
 .../scenegraph/select/create-select-border.ts |  57 +----
 .../scenegraph/select/delete-select-border.ts |   2 -
 .../select/update-custom-select-border.ts     |  28 +--
 .../scenegraph/select/update-select-border.ts | 190 +---------------
 packages/vtable/src/state/state.ts            |  45 +++-
 packages/vtable/src/ts-types/base-table.ts    |   4 +
 22 files changed, 722 insertions(+), 354 deletions(-)
 create mode 100644 packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts

diff --git a/packages/vtable-plugins/demo/auto-fill/auto-fill.ts b/packages/vtable-plugins/demo/auto-fill/auto-fill.ts
index 1b465791a..45ef83ea9 100644
--- a/packages/vtable-plugins/demo/auto-fill/auto-fill.ts
+++ b/packages/vtable-plugins/demo/auto-fill/auto-fill.ts
@@ -75,6 +75,36 @@ export function createTable() {
       field: 'otherDirection_2',
       title: '',
       width: 120
+    },
+    {
+      field: 'id',
+      title: 'ID',
+      width: 80
+    },
+    {
+      field: 'name',
+      title: '章节',
+      width: 150
+    },
+    {
+      field: 'arithmetic',
+      title: '等差',
+      width: 120
+    },
+    {
+      field: 'geometric',
+      title: '等比',
+      width: 120
+    },
+    {
+      field: 'date',
+      title: '日期',
+      width: 120
+    },
+    {
+      field: 'week',
+      title: '星期',
+      width: 120
     }
   ];
 
@@ -93,6 +123,9 @@ export function createTable() {
     excelOptions: {
       fillHandle: true // 启用填充炳功能
     },
+    frozenColCount: 4,
+    maxFrozenWidth: 320,
+    scrollFrozenCols: true,
     plugins: [autoFillPlugin]
   };
 
diff --git a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
new file mode 100644
index 000000000..f3ee040f5
--- /dev/null
+++ b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
@@ -0,0 +1,75 @@
+import * as VTable from '../../src';
+
+const CONTAINER_ID = 'vTable';
+
+const generatePersons = (count: number) => {
+  return Array.from(new Array(count)).map((_, i) => ({
+    id: i + 1,
+    email1: `${i + 1}@xxx.com`,
+    name: `小明${i + 1}`,
+    lastName: '王',
+    date1: '2022年9月1日',
+    tel: '000-0000-0000',
+    sex: i % 2 === 0 ? 'boy' : 'girl',
+    work: i % 2 === 0 ? 'back-end engineer' : 'front-end engineer',
+    city: 'beijing'
+  }));
+};
+
+export function createTable() {
+  const records = generatePersons(100);
+  const columns: VTable.ColumnsDefine = [
+    { field: 'id', title: 'ID', width: 120, sort: true },
+    { field: 'email1', title: 'email', width: 220, sort: true },
+    { field: 'name', title: 'First Name', width: 160 },
+    { field: 'lastName', title: 'Last Name', width: 160 },
+    { field: 'date1', title: 'birthday', width: 160 },
+    { field: 'sex', title: 'sex', width: 120 },
+    { field: 'tel', title: 'telephone', width: 180 },
+    { field: 'work', title: 'job', width: 200 },
+    { field: 'city', title: 'city', width: 160 },
+    { field: 'work', title: 'job2', width: 200 },
+    { field: 'city', title: 'city2', width: 160 },
+    { field: 'tel', title: 'telephone2', width: 180 },
+    { field: 'work', title: 'job', width: 200 },
+    { field: 'city', title: 'city', width: 160 },
+    { field: 'work', title: 'job2', width: 200 },
+    { field: 'city', title: 'city2', width: 160 },
+    { field: 'tel', title: 'telephone2', width: 180 }
+  ];
+
+  const option: VTable.TYPES.ListTableConstructorOptions = {
+    container: document.getElementById(CONTAINER_ID),
+    records,
+    columns,
+    frozenColCount: 6,
+    maxFrozenWidth: 320,
+    scrollFrozenCols: true,
+    excelOptions: {
+      fillHandle: args => {
+        const { selectRanges, table } = args;
+        if (selectRanges.length === 1) {
+          const { start, end } = selectRanges[0];
+          console.log('fillHandle', start, end);
+          const minCol = Math.min(start.col, end.col);
+          const maxCol = Math.max(start.col, end.col);
+          const minRow = Math.min(start.row, end.row);
+          const maxRow = Math.max(start.row, end.row);
+          //判断start到end 所有单元格有没有不能编辑的
+          for (let col = minCol; col <= maxCol; col++) {
+            for (let row = minRow; row <= maxRow; row++) {
+              if (row === 2 && col === 2) {
+                return false;
+              }
+            }
+          }
+          return true;
+        }
+        return false;
+      }
+    }
+  };
+
+  const tableInstance = new VTable.ListTable(option);
+  (window as any).tableInstance = tableInstance;
+}
diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts
index 855c29568..658a0acdf 100644
--- a/packages/vtable/examples/menu.ts
+++ b/packages/vtable/examples/menu.ts
@@ -1111,6 +1111,10 @@ export const menus = [
         path: 'frozen',
         name: 'list-table-scrollx-skip-frozen'
       },
+      {
+        path: 'frozen',
+        name: 'list-table-scrollable-frozen-cols'
+      },
       {
         path: 'frozen',
         name: 'pivot-table-right-frozen'
diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts
index ab99553bb..fa8076646 100644
--- a/packages/vtable/src/core/BaseTable.ts
+++ b/packages/vtable/src/core/BaseTable.ts
@@ -774,7 +774,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     // 纠正frozenColCount的值;
     const maxFrozenWidth = this._getMaxFrozenWidth();
     // if (this.tableNoFrameWidth - this.getColsWidth(0, frozenColCount - 1) <= 120) {
-    if (this.getColsWidth(0, frozenColCount - 1) > maxFrozenWidth) {
+    if (!this.options.scrollFrozenCols && this.getColsWidth(0, frozenColCount - 1) > maxFrozenWidth) {
       if (this.internalProps.unfreezeAllOnExceedsMaxWidth) {
         this.internalProps.frozenColCount = 0;
       } else {
@@ -795,7 +795,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     //纠正frozenColCount的值
     const maxFrozenWidth = this._getMaxFrozenWidth();
     // if (this.tableNoFrameWidth - this.getColsWidth(0, frozenColCount - 1) <= 120) {
-    if (this.getColsWidth(0, frozenColCount - 1) > maxFrozenWidth) {
+    if (!this.options.scrollFrozenCols && this.getColsWidth(0, frozenColCount - 1) > maxFrozenWidth) {
       if (this.internalProps.unfreezeAllOnExceedsMaxWidth) {
         this.internalProps.frozenColCount = 0;
       } else {
@@ -2079,7 +2079,11 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
       relativeY = false;
     }
     const cellRect = this.getCellRect(col, row);
-    return this._toRelativeRect(cellRect, relativeX, relativeY);
+    const rect = this._toRelativeRect(cellRect, relativeX, relativeY);
+    if (isFrozenCell?.col && !this.isRightFrozenColumn(col, row)) {
+      rect.offsetLeft(-this.getFrozenColsScrollLeft());
+    }
+    return rect;
   }
   /**
    * 获取的位置是相对表格显示界面的左上角
@@ -2099,7 +2103,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
       } else if (isFrozenCell?.row) {
         relativeY = false;
       }
-      return this._toRelativeRect(
+      const rect = this._toRelativeRect(
         this.getCellsRect(
           (<CellRange>range).start.col,
           (<CellRange>range).start.row,
@@ -2109,6 +2113,10 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
         relativeX,
         relativeY
       );
+      if (isFrozenCell?.col && !this.isRightFrozenColumn((<CellRange>range).start.col, (<CellRange>range).start.row)) {
+        rect.offsetLeft(-this.getFrozenColsScrollLeft());
+      }
+      return rect;
     }
     const cellRange = this.getCellRange((<CellAddress>range).col, (<CellAddress>range).row);
     const isFrozenCell = this.isFrozenCell((<CellAddress>range).col, (<CellAddress>range).row);
@@ -2122,11 +2130,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     } else if (isFrozenCell?.row) {
       relativeY = false;
     }
-    return this._toRelativeRect(
+    const rect = this._toRelativeRect(
       this.getCellsRect(cellRange.start.col, cellRange.start.row, cellRange.end.col, cellRange.end.row),
       relativeX,
       relativeY
     );
+    if (isFrozenCell?.col && !this.isRightFrozenColumn((<CellAddress>range).col, (<CellAddress>range).row)) {
+      rect.offsetLeft(-this.getFrozenColsScrollLeft());
+    }
+    return rect;
   }
   /**
    *  即仅视觉看到的位置 获取的位置是相对表格显示界面的左上角
@@ -2429,7 +2441,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     const { scrollTop, scrollLeft } = this;
     const width = this.tableNoFrameWidth;
     const height = this.tableNoFrameHeight;
-    return new Rect(scrollLeft, scrollTop, width, height);
+    return new Rect(scrollLeft + this.getFrozenColsOffset(), scrollTop, width, height);
   }
   /**
    * 获取网格中完全可见的可滚动行数。不包括表头及冻结的行
@@ -2464,19 +2476,20 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
   getBodyVisibleCellRange() {
     const { scrollTop, scrollLeft } = this;
     const frozenRowsHeight = this.getFrozenRowsHeight();
-    const frozenColsWidth = this.getFrozenColsWidth();
+    const frozenColsContentWidth = this.getFrozenColsContentWidth();
+    const frozenColsOffset = this.getFrozenColsOffset();
     const bottomFrozenRowsHeight = this.getBottomFrozenRowsHeight();
     const rightFrozenColsWidth = this.getRightFrozenColsWidth();
     // 计算非冻结
     const { row: rowStart } = this.getRowAt(scrollTop + frozenRowsHeight + 1);
-    const { col: colStart } = this.getColAt(scrollLeft + frozenColsWidth + 1);
+    const { col: colStart } = this.getColAt(scrollLeft + frozenColsContentWidth + 1);
     const rowEnd =
       this.getAllRowsHeight() > this.tableNoFrameHeight
         ? this.getRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight).row
         : this.rowCount - 1;
     const colEnd =
       this.getAllColsWidth() > this.tableNoFrameWidth
-        ? this.getColAt(scrollLeft + this.tableNoFrameWidth - 1 - rightFrozenColsWidth).col
+        ? this.getColAt(scrollLeft + frozenColsOffset + this.tableNoFrameWidth - 1 - rightFrozenColsWidth).col
         : this.colCount - 1;
     if (colEnd < 0 || rowEnd < 0) {
       return null;
@@ -2512,14 +2525,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
    */
   getBodyVisibleColRange(start_deltaX: number = 0, end_deltaX: number = 0) {
     const { scrollLeft } = this;
-    const frozenColsWidth = this.getFrozenColsWidth();
+    const frozenColsContentWidth = this.getFrozenColsContentWidth();
+    const frozenColsOffset = this.getFrozenColsOffset();
     const rightFrozenColsWidth = this.getRightFrozenColsWidth();
     // 计算非冻结
-    const { col: colStart } = this.getColAt(scrollLeft + frozenColsWidth + 1 + start_deltaX);
+    const { col: colStart } = this.getColAt(scrollLeft + frozenColsContentWidth + 1 + start_deltaX);
 
     const colEnd =
       this.getAllColsWidth() > this.tableNoFrameWidth
-        ? this.getColAt(scrollLeft + this.tableNoFrameWidth - 1 - rightFrozenColsWidth + end_deltaX).col
+        ? this.getColAt(scrollLeft + frozenColsOffset + this.tableNoFrameWidth - 1 - rightFrozenColsWidth + end_deltaX)
+            .col
         : this.colCount - 1;
     if (colEnd < 0) {
       return null;
@@ -2533,8 +2548,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
   get visibleColCount(): number {
     const { frozenColCount } = this;
     const visibleRect = this.getVisibleRect();
-    const visibleLeft =
-      frozenColCount > 0 ? visibleRect.left + this.getColsWidth(0, frozenColCount - 1) : visibleRect.left;
+    const visibleLeft = frozenColCount > 0 ? visibleRect.left + this.getFrozenColsWidth() : visibleRect.left;
 
     const initCol = this.getTargetColAt(visibleLeft);
     if (!initCol) {
@@ -2995,8 +3009,23 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
    * @returns
    */
   getFrozenColsWidth(): number {
-    const w = this.getColsWidth(0, this.frozenColCount - 1);
-    return w;
+    const contentWidth = this.getFrozenColsContentWidth();
+    if (!this.options.scrollFrozenCols) {
+      return contentWidth;
+    }
+    const maxFrozenWidth = this._getMaxFrozenWidth();
+    return Math.min(contentWidth, maxFrozenWidth);
+  }
+  getFrozenColsContentWidth(): number {
+    return this.getColsWidth(0, this.frozenColCount - 1);
+  }
+  getFrozenColsOffset(): number {
+    const contentWidth = this.getFrozenColsContentWidth();
+    const viewportWidth = this.getFrozenColsWidth();
+    return Math.max(0, contentWidth - viewportWidth);
+  }
+  getFrozenColsScrollLeft(): number {
+    return this.stateManager.scroll.frozenHorizontalBarPos ?? 0;
   }
   /**
    * 获取底部冻结固定列总宽
@@ -5192,7 +5221,10 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
       // This ensures dynamically-computed row heights (e.g. autoWrapText) are reflected
       // in the scroll position calculation.
       const top = this.rowHeightsMap.getSumInRange(0, cellAddr.row - 1);
-      this.scrollTop = Math.min(top - frozenHeight, this.rowHeightsMap.getSumInRange(0, this.rowCount - 1) - drawRange.height);
+      this.scrollTop = Math.min(
+        top - frozenHeight,
+        this.rowHeightsMap.getSumInRange(0, this.rowCount - 1) - drawRange.height
+      );
     }
     this.render();
   }
diff --git a/packages/vtable/src/core/tableHelper.ts b/packages/vtable/src/core/tableHelper.ts
index d146a1ce4..3cb077996 100644
--- a/packages/vtable/src/core/tableHelper.ts
+++ b/packages/vtable/src/core/tableHelper.ts
@@ -215,10 +215,12 @@ export function isAutoDefine(width: string | number): width is 'auto' {
 
 export function _getScrollableVisibleRect(table: BaseTableAPI): Rect {
   let frozenColsWidth = 0;
+  let frozenColsContentWidth = 0;
   let rightFrozenColsWidth = 0;
   if (table.frozenColCount > 0) {
     //有固定列时绘制固定列
     frozenColsWidth = table.getFrozenColsWidth();
+    frozenColsContentWidth = table.getFrozenColsContentWidth?.() ?? frozenColsWidth;
   }
   if (table.rightFrozenColCount > 0) {
     //有固定列时绘制固定列
@@ -235,7 +237,7 @@ export function _getScrollableVisibleRect(table: BaseTableAPI): Rect {
     bottomFrozenRowsHeight = table.getBottomFrozenRowsHeight();
   }
   return new Rect(
-    table.scrollLeft + frozenColsWidth,
+    table.scrollLeft + frozenColsContentWidth,
     table.scrollTop + frozenRowsHeight,
     table.tableNoFrameWidth - frozenColsWidth - rightFrozenColsWidth,
     table.tableNoFrameHeight - frozenRowsHeight - bottomFrozenRowsHeight
diff --git a/packages/vtable/src/core/utils/get-cell-position.ts b/packages/vtable/src/core/utils/get-cell-position.ts
index ac48ab8da..f55d9c936 100644
--- a/packages/vtable/src/core/utils/get-cell-position.ts
+++ b/packages/vtable/src/core/utils/get-cell-position.ts
@@ -305,6 +305,10 @@ export function getCellAtRelativePosition(x: number, y: number, _this: BaseTable
   x -= _this.tableX;
   y -= _this.tableY;
 
+  const frozenColsWidth = _this.getFrozenColsWidth();
+  const frozenColsOffset = _this.getFrozenColsOffset?.() ?? 0;
+  const frozenColsScrollLeft = _this.getFrozenColsScrollLeft?.() ?? 0;
+
   // top frozen
   let topFrozen = false;
   if (y > 0 && y < _this.getFrozenRowsHeight()) {
@@ -313,7 +317,7 @@ export function getCellAtRelativePosition(x: number, y: number, _this: BaseTable
 
   // left frozen
   let leftFrozen = false;
-  if (x > 0 && x < _this.getFrozenColsWidth()) {
+  if (x > 0 && x < frozenColsWidth) {
     leftFrozen = true;
   }
 
@@ -338,7 +342,7 @@ export function getCellAtRelativePosition(x: number, y: number, _this: BaseTable
 
   // 加上 tableX 和 tableY 是因为在考虑冻结列和冻结行时，需要将坐标转换为相对于表格左上角的坐标
   const colInfo = getTargetColAtConsiderRightFrozen(
-    (leftFrozen || rightFrozen ? x : x + _this.scrollLeft) + _this.tableX,
+    (leftFrozen ? x + frozenColsScrollLeft : rightFrozen ? x : x + _this.scrollLeft + frozenColsOffset) + _this.tableX,
     rightFrozen,
     _this
   );
@@ -372,9 +376,13 @@ export function getColAtRelativePosition(x: number, _this: BaseTableAPI): number
   // table border and outer component
   x -= _this.tableX;
 
+  const frozenColsWidth = _this.getFrozenColsWidth();
+  const frozenColsOffset = _this.getFrozenColsOffset?.() ?? 0;
+  const frozenColsScrollLeft = _this.getFrozenColsScrollLeft?.() ?? 0;
+
   // left frozen
   let leftFrozen = false;
-  if (x > 0 && x < _this.getFrozenColsWidth()) {
+  if (x > 0 && x < frozenColsWidth) {
     leftFrozen = true;
   }
 
@@ -390,7 +398,7 @@ export function getColAtRelativePosition(x: number, _this: BaseTableAPI): number
 
   // 加上 tableX 是因为在考虑冻结列时，需要将坐标转换为相对于表格左上角的坐标
   const colInfo = getTargetColAtConsiderRightFrozen(
-    (leftFrozen || rightFrozen ? x : x + _this.scrollLeft) + _this.tableX,
+    (leftFrozen ? x + frozenColsScrollLeft : rightFrozen ? x : x + _this.scrollLeft + frozenColsOffset) + _this.tableX,
     rightFrozen,
     _this
   );
diff --git a/packages/vtable/src/event/listener/container-dom.ts b/packages/vtable/src/event/listener/container-dom.ts
index cf8e2625a..269e5d0bd 100644
--- a/packages/vtable/src/event/listener/container-dom.ts
+++ b/packages/vtable/src/event/listener/container-dom.ts
@@ -571,6 +571,7 @@ export function bindContainerDomListener(eventManager: EventManager) {
 
           let selectX: number;
           let selectY: number;
+          const frozenOffset = table.getFrozenColsOffset?.() ?? 0;
 
           if (bottom) {
             selectY = table.scrollTop + drawRange.height - bottomFrozenRowHeight - 20;
@@ -579,9 +580,9 @@ export function bindContainerDomListener(eventManager: EventManager) {
           }
 
           if (right) {
-            selectX = table.scrollLeft + drawRange.width - rightFrozenColsWidth - 20;
+            selectX = table.scrollLeft + frozenOffset + drawRange.width - rightFrozenColsWidth - 20;
           } else if (left) {
-            selectX = table.scrollLeft + leftFrozenColsWidth + 20;
+            selectX = table.scrollLeft + frozenOffset + leftFrozenColsWidth + 20;
           }
 
           let considerFrozenY = false;
@@ -594,7 +595,7 @@ export function bindContainerDomListener(eventManager: EventManager) {
               selectX = x;
               considerFrozenX = true;
             } else {
-              selectX = table.scrollLeft + x;
+              selectX = table.scrollLeft + frozenOffset + x;
             }
           }
           if (!bottom && !top) {
diff --git a/packages/vtable/src/event/listener/table-group.ts b/packages/vtable/src/event/listener/table-group.ts
index affdca98f..11d5c86b4 100644
--- a/packages/vtable/src/event/listener/table-group.ts
+++ b/packages/vtable/src/event/listener/table-group.ts
@@ -9,7 +9,7 @@ import type {
 } from '../../ts-types';
 import { IconFuncTypeEnum, InteractionState } from '../../ts-types';
 import type { SceneEvent } from '../util';
-import { getCellEventArgsSet, regIndexReg } from '../util';
+import { getCellEventArgsSetWithTable, regIndexReg } from '../util';
 import { TABLE_EVENT_TYPE } from '../../core/TABLE_EVENT_TYPE';
 import type { Group } from '../../scenegraph/graphic/group';
 import { isValid } from '@visactor/vutils';
@@ -27,6 +27,7 @@ import { clearPageSelection } from '../../tools/env';
 export function bindTableGroupListener(eventManager: EventManager) {
   const table = eventManager.table;
   const stateManager = table.stateManager;
+  const getEventArgsSet = (e: FederatedPointerEvent) => getCellEventArgsSetWithTable(e, table);
 
   table.scenegraph.tableGroup.addEventListener('pointermove', (e: FederatedPointerEvent) => {
     const lastX = table.eventManager.LastPointerXY?.x ?? e.x;
@@ -38,7 +39,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
       clearTimeout(eventManager.touchSetTimeout);
       eventManager.touchSetTimeout = undefined;
     }
-    const eventArgsSet = getCellEventArgsSet(e);
+    const eventArgsSet = getEventArgsSet(e);
     // if (stateManager.interactionState === InteractionState.scrolling) {
     //   return;
     // }
@@ -181,7 +182,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
 
   table.scenegraph.tableGroup.addEventListener('pointerout', (e: FederatedPointerEvent) => {
-    const eventArgsSet = getCellEventArgsSet(e);
+    const eventArgsSet = getEventArgsSet(e);
     const cellGoup = eventArgsSet?.eventArgs?.target as unknown as Group;
     if (cellGoup?.role === 'table') {
       eventManager.dealTableHover();
@@ -189,7 +190,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
 
   table.scenegraph.tableGroup.addEventListener('pointerover', (e: FederatedPointerEvent) => {
-    const eventArgsSet = getCellEventArgsSet(e);
+    const eventArgsSet = getEventArgsSet(e);
     const cellGoup = eventArgsSet?.eventArgs?.target as unknown as Group;
     // console.log('pointerover', cellGoup);
 
@@ -415,7 +416,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
       // 只处理左键
       return;
     }
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     eventManager.downIcon = undefined;
     if (stateManager.interactionState !== InteractionState.default) {
       return;
@@ -564,7 +565,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
         stateManager.updateInteractionState(InteractionState.grabing);
       }
       if ((table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_CELL)) {
-        const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+        const eventArgsSet: SceneEvent = getEventArgsSet(e);
         if (eventArgsSet.eventArgs) {
           table.fireListeners(TABLE_EVENT_TYPE.MOUSEDOWN_CELL, {
             col: eventArgsSet.eventArgs.col,
@@ -601,7 +602,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
         if (table.stateManager.isFillHandle()) {
           table.stateManager.endFillSelect();
         }
-        const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+        const eventArgsSet: SceneEvent = getEventArgsSet(e);
         if (
           table.eventManager.isDraging &&
           eventArgsSet.eventArgs &&
@@ -627,7 +628,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
     }
     if (!table.eventManager.isDraging) {
       // 从pointertap中挪过来的这段逻辑
-      const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+      const eventArgsSet: SceneEvent = getEventArgsSet(e);
       if (
         !eventManager.isTouchMove &&
         e.button === 0 &&
@@ -667,7 +668,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
 
     // console.log('DRAG_SELECT_END');
     if ((table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEUP_CELL)) {
-      const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+      const eventArgsSet: SceneEvent = getEventArgsSet(e);
       if (eventArgsSet.eventArgs) {
         table.fireListeners(TABLE_EVENT_TYPE.MOUSEUP_CELL, {
           col: eventArgsSet.eventArgs.col,
@@ -688,7 +689,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
 
   table.scenegraph.tableGroup.addEventListener('rightdown', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     // 右键点击
     if (eventArgsSet.eventArgs) {
       stateManager.triggerContextMenu(
@@ -775,7 +776,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
     if (table.stateManager.columnResize.resizing) {
       return;
     }
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     // 触发click_cell事件的逻辑挪到了pointerup中
     // if (
     //   !eventManager.isTouchMove &&
@@ -828,7 +829,8 @@ export function bindTableGroupListener(eventManager: EventManager) {
       // 通过这个变量判断非drag鼠标拖拽状态，就不再增加其他变量isDrag了（touchSetTimeout如果拖拽过会变成undefined pointermove事件有置为undefined）
       if (e.pointerType === 'touch') {
         // 移动端事件特殊处理
-        const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+        const eventArgsSet: SceneEvent = getEventArgsSet(e);
+        // replaced by getEventArgsSet above
         if (eventManager.touchSetTimeout) {
           clearTimeout(eventManager.touchSetTimeout);
           const isHasSelected = !!stateManager.select.ranges?.length;
@@ -841,7 +843,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
   // stage 的pointerdown监听
   table.scenegraph.stage.addEventListener('pointerdown', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getCellEventArgsSetWithTable(e, table);
     if (
       !eventArgsSet.eventArgs?.target ||
       (eventArgsSet.eventArgs?.target as any) !== stateManager.residentHoverIcon?.icon
@@ -947,7 +949,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
     }
   });
   table.scenegraph.stage.addEventListener('pointermove', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getCellEventArgsSetWithTable(e, table);
 
     // 检查事件是否来自当前表格的有效区域
     const isEventFromCurrentTable = e.target?.isDescendantsOf?.(table.scenegraph.tableGroup) ?? false;
@@ -988,7 +990,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   // });
 
   table.scenegraph.tableGroup.addEventListener('checkbox_state_change', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     const { col, row } = eventArgsSet.eventArgs;
     const cellInfo = table.getCellInfo(col, row);
 
@@ -1058,7 +1060,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
 
   table.scenegraph.tableGroup.addEventListener('radio_checked', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     const { col, row, target } = eventArgsSet.eventArgs;
     const cellInfo = table.getCellInfo(col, row);
     const indexInCell: string | undefined = regIndexReg.exec(target.id as string)?.[1];
@@ -1143,7 +1145,7 @@ export function bindTableGroupListener(eventManager: EventManager) {
   });
 
   table.scenegraph.tableGroup.addEventListener('switch_state_change', (e: FederatedPointerEvent) => {
-    const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+    const eventArgsSet: SceneEvent = getEventArgsSet(e);
     const { col, row, target } = eventArgsSet.eventArgs;
     const cellInfo = table.getCellInfo(col, row);
 
@@ -1241,7 +1243,7 @@ export function endResizeRow(table: BaseTableAPI) {
 }
 
 function dblclickHandler(e: FederatedPointerEvent, table: BaseTableAPI) {
-  const eventArgsSet: SceneEvent = getCellEventArgsSet(e);
+  const eventArgsSet: SceneEvent = getCellEventArgsSetWithTable(e, table);
   let col = -1;
   let row = -1;
   if (eventArgsSet.eventArgs) {
diff --git a/packages/vtable/src/event/scroll.ts b/packages/vtable/src/event/scroll.ts
index 2f0bd27bf..e7c455cfc 100644
--- a/packages/vtable/src/event/scroll.ts
+++ b/packages/vtable/src/event/scroll.ts
@@ -23,9 +23,39 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
     }
   }
   const autoHide = state.table.options.theme?.scrollStyle?.visible === 'scrolling';
+  let usedFrozenHorizontal = false;
   if (optimizedDeltaX) {
-    state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
-    state.showHorizontalScrollBar(autoHide);
+    const px = (event as any).x ?? (state.table as any).eventManager?.LastBodyPointerXY?.x;
+    const py = (event as any).y ?? (state.table as any).eventManager?.LastBodyPointerXY?.y;
+    const relativeX = typeof px === 'number' ? px - state.table.tableX : NaN;
+    const relativeY = typeof py === 'number' ? py - state.table.tableY : NaN;
+    const isInTable =
+      isFinite(relativeX) &&
+      isFinite(relativeY) &&
+      relativeX >= 0 &&
+      relativeY >= 0 &&
+      relativeX <= state.table.tableNoFrameWidth &&
+      relativeY <= state.table.tableNoFrameHeight;
+    const frozenColsScrollable = state.table.options.scrollFrozenCols && state.table.getFrozenColsOffset() > 0;
+    const isInFrozenViewport = isInTable && relativeX > 0 && relativeX < state.table.getFrozenColsWidth();
+
+    if (frozenColsScrollable && isInFrozenViewport) {
+      const maxFrozenScrollLeft = state.table.getFrozenColsOffset();
+      const nextFrozenScrollLeft = state.scroll.frozenHorizontalBarPos + optimizedDeltaX;
+      const canScrollFrozen =
+        (optimizedDeltaX < 0 && state.scroll.frozenHorizontalBarPos > 0) ||
+        (optimizedDeltaX > 0 && state.scroll.frozenHorizontalBarPos < maxFrozenScrollLeft);
+      if (canScrollFrozen) {
+        state.setFrozenColsScrollLeft(nextFrozenScrollLeft);
+        usedFrozenHorizontal = true;
+      } else {
+        state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
+        state.showHorizontalScrollBar(autoHide);
+      }
+    } else {
+      state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
+      state.showHorizontalScrollBar(autoHide);
+    }
   }
   if (optimizedDeltaY) {
     state.setScrollTop(state.scroll.verticalBarPos + optimizedDeltaY, event);
@@ -37,7 +67,9 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
     ((state.table.internalProps.overscrollBehavior === 'none' &&
       ((deltaY && isVerticalExistScrollBar(state)) || (deltaX && isHorizontalExistScrollBar(state)))) ||
       (Math.abs(deltaY) >= Math.abs(deltaX) && deltaY !== 0 && isVerticalScrollable(deltaY, state)) ||
-      (Math.abs(deltaY) <= Math.abs(deltaX) && deltaX !== 0 && isHorizontalScrollable(deltaX, state)))
+      (Math.abs(deltaY) <= Math.abs(deltaX) &&
+        deltaX !== 0 &&
+        (usedFrozenHorizontal ? state.table.getFrozenColsOffset() > 0 : isHorizontalScrollable(deltaX, state))))
   ) {
     event.nativeEvent.preventDefault();
   }
@@ -74,7 +106,8 @@ export function isVerticalScrollable(deltaY: number, state: StateManager) {
 }
 
 export function isHorizontalScrollable(deltaX: number, state: StateManager) {
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width;
+  const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
   if (totalWidth === 0) {
     return false;
   }
@@ -90,7 +123,8 @@ export function isVerticalExistScrollBar(state: StateManager) {
 }
 
 export function isHorizontalExistScrollBar(state: StateManager) {
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width;
+  const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
   if (totalWidth <= 0) {
     return false;
   }
@@ -111,7 +145,8 @@ function isScrollToBottom(deltaY: number, state: StateManager) {
 }
 
 function isScrollToLeft(deltaX: number, state: StateManager) {
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width;
+  const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
   return totalWidth !== 0 && deltaX <= 0 && state.scroll.horizontalBarPos < 1;
 }
 
@@ -119,7 +154,8 @@ function isScrollToRight(deltaX: number, state: StateManager) {
   // 这里加入tolerance，避免出现无用滚动
   const sizeTolerance = state.table.options.customConfig?._disableColumnAndRowSizeRound ? 1 : 0;
 
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width;
+  const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
   return totalWidth !== 0 && deltaX >= 0 && Math.abs(state.scroll.horizontalBarPos - totalWidth) < 1 + sizeTolerance;
 }
 
diff --git a/packages/vtable/src/event/util.ts b/packages/vtable/src/event/util.ts
index b0ec78d6b..0469ac0a1 100644
--- a/packages/vtable/src/event/util.ts
+++ b/packages/vtable/src/event/util.ts
@@ -2,6 +2,7 @@ import type { FederatedPointerEvent, IEventTarget } from '@src/vrender';
 import type { Group } from '../scenegraph/graphic/group';
 import type { MergeCellInfo } from '../ts-types';
 import { isValid } from '@visactor/vutils';
+import type { BaseTableAPI } from '../ts-types/base-table';
 
 export interface SceneEvent {
   abstractPos: {
@@ -46,6 +47,46 @@ export function getCellEventArgsSet(e: FederatedPointerEvent): SceneEvent {
   return tableEvent;
 }
 
+export function getCellEventArgsSetWithTable(e: FederatedPointerEvent, table: BaseTableAPI): SceneEvent {
+  const eventArgsSet = getCellEventArgsSet(e);
+  if (!eventArgsSet.eventArgs) {
+    return eventArgsSet;
+  }
+  if (!table.options.scrollFrozenCols || table.getFrozenColsOffset?.() === 0) {
+    return eventArgsSet;
+  }
+
+  const xInTable = e.x - table.tableX;
+  const yInTable = e.y - table.tableY;
+  if (xInTable < 0 || yInTable < 0 || xInTable > table.tableNoFrameWidth || yInTable > table.tableNoFrameHeight) {
+    return eventArgsSet;
+  }
+
+  const frozenViewportWidth = table.getFrozenColsWidth();
+  const pickedCol = eventArgsSet.eventArgs.col;
+  const pickedRow = eventArgsSet.eventArgs.row;
+  const isPickedLeftFrozen =
+    pickedCol >= 0 && pickedCol < table.frozenColCount && !table.isRightFrozenColumn(pickedCol, pickedRow);
+
+  if (isPickedLeftFrozen && xInTable >= frozenViewportWidth) {
+    const cell = table.getCellAtRelativePosition(e.x, e.y);
+    if (cell.col === -1 || cell.row === -1) {
+      return eventArgsSet;
+    }
+    const targetCell = table.scenegraph.getCell(cell.col, cell.row);
+    eventArgsSet.eventArgs = {
+      col: cell.col,
+      row: cell.row,
+      event: e,
+      targetCell,
+      mergeInfo: getMergeCellInfo(targetCell),
+      target: targetCell as unknown as IEventTarget
+    };
+  }
+
+  return eventArgsSet;
+}
+
 export function getTargetCell(target: any) {
   while (target && target.parent) {
     if (target.role === 'cell') {
diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts
index b58f532e8..d522489a7 100644
--- a/packages/vtable/src/scenegraph/component/table-component.ts
+++ b/packages/vtable/src/scenegraph/component/table-component.ts
@@ -355,6 +355,7 @@ export class TableComponent {
     const totalWidth = this.table.getAllColsWidth();
     const frozenRowsHeight = this.table.getFrozenRowsHeight();
     const frozenColsWidth = this.table.getFrozenColsWidth();
+    const frozenColsContentWidth = this.table.getFrozenColsContentWidth?.() ?? frozenColsWidth;
     const bottomFrozenRowsHeight = this.table.getBottomFrozenRowsHeight();
     const rightFrozenColsWidth = this.table.getRightFrozenColsWidth();
     const hoverOn = this.table.theme.scrollStyle.hoverOn;
@@ -366,7 +367,7 @@ export class TableComponent {
 
     if (totalWidth > tableWidth + sizeTolerance) {
       const y = Math.min(tableHeight, totalHeight);
-      const rangeEnd = Math.max(0.05, (tableWidth - frozenColsWidth) / (totalWidth - frozenColsWidth));
+      const rangeEnd = Math.max(0.05, (tableWidth - frozenColsWidth) / (totalWidth - frozenColsContentWidth));
 
       let attrY = 0;
       if (this.table.theme.scrollStyle.barToSide) {
@@ -667,7 +668,10 @@ export class TableComponent {
    * @return {*}
    */
   setFrozenColumnShadow(col: number, isRightFrozen?: boolean) {
-    const colX = getColX(col, this.table, isRightFrozen);
+    const colX =
+      !isRightFrozen && col === this.table.frozenColCount - 1 && (this.table.getFrozenColsOffset?.() ?? 0) > 0
+        ? this.table.getFrozenColsWidth()
+        : getColX(col, this.table, isRightFrozen);
     if (col < 0 || this.table.theme.frozenColumnLine?.shadow?.visible !== 'always') {
       this.frozenShadowLine.setAttributes({
         visible: false,
diff --git a/packages/vtable/src/scenegraph/component/util.ts b/packages/vtable/src/scenegraph/component/util.ts
index 397614648..3ff8b38ed 100644
--- a/packages/vtable/src/scenegraph/component/util.ts
+++ b/packages/vtable/src/scenegraph/component/util.ts
@@ -4,9 +4,13 @@ export function getColX(col: number, table: BaseTableAPI, isRightFrozen?: boolea
   if (isRightFrozen) {
     return Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) - table.getColsWidth(col, table.colCount - 1);
   }
+  const frozenOffset = table.getFrozenColsOffset?.() ?? 0;
+  const frozenScrollLeft = table.getFrozenColsScrollLeft?.() ?? 0;
   let colX = table.getColsWidth(0, col);
   if (col >= table.frozenColCount) {
-    colX -= table.scrollLeft;
+    colX -= table.scrollLeft + frozenOffset;
+  } else {
+    colX -= frozenScrollLeft;
   }
   return colX;
 }
diff --git a/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts b/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
index e313877a2..454bc7c37 100644
--- a/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
+++ b/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
@@ -963,59 +963,91 @@ export class ClipBodyGroupBeforeRenderContribution implements IGroupRenderContri
       return;
     }
 
+    const clipInflate = getSelectOverlayClipInflate(group as Group, table);
+
     if ((group as Group).role === 'body') {
-      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth();
-      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight();
-      const width = group.parent.attribute.width - table.getFrozenColsWidth() - table.getRightFrozenColsWidth();
-      const height = group.parent.attribute.height - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
+      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth() - clipInflate.left;
+      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight() - clipInflate.top;
+      const width =
+        group.parent.attribute.width -
+        table.getFrozenColsWidth() -
+        table.getRightFrozenColsWidth() +
+        clipInflate.left +
+        clipInflate.right;
+      const height =
+        group.parent.attribute.height -
+        table.getFrozenRowsHeight() -
+        table.getBottomFrozenRowsHeight() +
+        clipInflate.top +
+        clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'row-header') {
-      const x = 0;
-      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight();
-      const width = table.getFrozenColsWidth();
-      const height = group.parent.attribute.height - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight() - clipInflate.top;
+      const width = table.getFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height =
+        group.parent.attribute.height -
+        table.getFrozenRowsHeight() -
+        table.getBottomFrozenRowsHeight() +
+        clipInflate.top +
+        clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'col-header') {
-      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth();
-      const y = 0;
-      const width = group.parent.attribute.width - table.getFrozenColsWidth() - table.getRightFrozenColsWidth();
-      const height = table.getFrozenRowsHeight();
+      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth() - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width =
+        group.parent.attribute.width -
+        table.getFrozenColsWidth() -
+        table.getRightFrozenColsWidth() +
+        clipInflate.left +
+        clipInflate.right;
+      const height = table.getFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'right-frozen') {
-      const x = 0;
-      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight();
-      const width = table.getRightFrozenColsWidth();
-      const height = group.parent.attribute.height - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = -(group.attribute.y ?? 0) + table.getFrozenRowsHeight() - clipInflate.top;
+      const width = table.getRightFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height =
+        group.parent.attribute.height -
+        table.getFrozenRowsHeight() -
+        table.getBottomFrozenRowsHeight() +
+        clipInflate.top +
+        clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'bottom-frozen') {
-      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth();
-      const y = 0;
-      const width = group.parent.attribute.width - table.getFrozenColsWidth() - table.getRightFrozenColsWidth();
-      const height = table.getBottomFrozenRowsHeight();
+      const x = -(group.attribute.x ?? 0) + table.getFrozenColsWidth() - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width =
+        group.parent.attribute.width -
+        table.getFrozenColsWidth() -
+        table.getRightFrozenColsWidth() +
+        clipInflate.left +
+        clipInflate.right;
+      const height = table.getBottomFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'corner-header') {
-      const x = 0;
-      const y = 0;
-      const width = table.getFrozenColsWidth();
-      const height = table.getFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width = table.getFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height = table.getFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'corner-right-top-header') {
-      const x = 0;
-      const y = 0;
-      const width = table.getRightFrozenColsWidth();
-      const height = table.getFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width = table.getRightFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height = table.getFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'corner-right-bottom-header') {
-      const x = 0;
-      const y = 0;
-      const width = table.getRightFrozenColsWidth();
-      const height = table.getBottomFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width = table.getRightFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height = table.getBottomFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     } else if ((group as Group).role === 'corner-left-bottom-header') {
-      const x = 0;
-      const y = 0;
-      const width = table.getFrozenColsWidth();
-      const height = table.getBottomFrozenRowsHeight();
+      const x = 0 - clipInflate.left;
+      const y = 0 - clipInflate.top;
+      const width = table.getFrozenColsWidth() + clipInflate.left + clipInflate.right;
+      const height = table.getBottomFrozenRowsHeight() + clipInflate.top + clipInflate.bottom;
       drawClipRect(context, x, y, width, height);
     }
   }
@@ -1023,6 +1055,32 @@ export class ClipBodyGroupBeforeRenderContribution implements IGroupRenderContri
 
 const precision = Math.pow(2, 24);
 
+function getSelectOverlayClipInflate(group: Group, table: BaseTableAPI) {
+  const isSelectOverlay = (group as any).name === 'select-overlay';
+  if (!isSelectOverlay) {
+    return { left: 0, top: 0, right: 0, bottom: 0 };
+  }
+
+  const lineWidth = table.theme.selectionStyle?.cellBorderLineWidth;
+  const maxLineWidth = Array.isArray(lineWidth)
+    ? Math.max(...lineWidth.filter(v => typeof v === 'number'))
+    : typeof lineWidth === 'number'
+    ? lineWidth
+    : 0;
+
+  const baseInflate = Math.max(1, Math.ceil(maxLineWidth / 2) + 1);
+  const shouldInflateForFillHandle =
+    !!table.options.excelOptions?.fillHandle && table.stateManager.select.ranges?.length === 1;
+  const handleInflate = shouldInflateForFillHandle ? 3 : 0;
+
+  return {
+    left: baseInflate,
+    top: baseInflate,
+    right: Math.max(baseInflate, handleInflate),
+    bottom: Math.max(baseInflate, handleInflate)
+  };
+}
+
 function drawClipRect(context: IContext2d, x: number, y: number, width: number, height: number) {
   context.beginPath();
 
diff --git a/packages/vtable/src/scenegraph/group-creater/init-scenegraph.ts b/packages/vtable/src/scenegraph/group-creater/init-scenegraph.ts
index 68ca0691f..b110d86e2 100644
--- a/packages/vtable/src/scenegraph/group-creater/init-scenegraph.ts
+++ b/packages/vtable/src/scenegraph/group-creater/init-scenegraph.ts
@@ -57,6 +57,51 @@ export function initSceneGraph(scene: Scenegraph) {
   leftBottomCornerGroup.role = 'corner-left-bottom-header';
   scene.leftBottomCornerGroup = leftBottomCornerGroup;
 
+  const bodySelectGroup = createContainerGroup(width, 0, true);
+  bodySelectGroup.role = 'body';
+  bodySelectGroup.name = 'select-overlay';
+  scene.bodySelectGroup = bodySelectGroup;
+
+  const rowHeaderSelectGroup = createContainerGroup(0, 0, true);
+  rowHeaderSelectGroup.role = 'row-header';
+  rowHeaderSelectGroup.name = 'select-overlay';
+  scene.rowHeaderSelectGroup = rowHeaderSelectGroup;
+
+  const bottomFrozenSelectGroup = createContainerGroup(0, 0, true);
+  bottomFrozenSelectGroup.role = 'bottom-frozen';
+  bottomFrozenSelectGroup.name = 'select-overlay';
+  scene.bottomFrozenSelectGroup = bottomFrozenSelectGroup;
+
+  const colHeaderSelectGroup = createContainerGroup(0, 0, true);
+  colHeaderSelectGroup.role = 'col-header';
+  colHeaderSelectGroup.name = 'select-overlay';
+  scene.colHeaderSelectGroup = colHeaderSelectGroup;
+
+  const rightFrozenSelectGroup = createContainerGroup(0, 0, true);
+  rightFrozenSelectGroup.role = 'right-frozen';
+  rightFrozenSelectGroup.name = 'select-overlay';
+  scene.rightFrozenSelectGroup = rightFrozenSelectGroup;
+
+  const rightTopCornerSelectGroup = createContainerGroup(0, 0, true);
+  rightTopCornerSelectGroup.role = 'corner-right-top-header';
+  rightTopCornerSelectGroup.name = 'select-overlay';
+  scene.rightTopCornerSelectGroup = rightTopCornerSelectGroup;
+
+  const rightBottomCornerSelectGroup = createContainerGroup(0, 0, true);
+  rightBottomCornerSelectGroup.role = 'corner-right-bottom-header';
+  rightBottomCornerSelectGroup.name = 'select-overlay';
+  scene.rightBottomCornerSelectGroup = rightBottomCornerSelectGroup;
+
+  const leftBottomCornerSelectGroup = createContainerGroup(0, 0, true);
+  leftBottomCornerSelectGroup.role = 'corner-left-bottom-header';
+  leftBottomCornerSelectGroup.name = 'select-overlay';
+  scene.leftBottomCornerSelectGroup = leftBottomCornerSelectGroup;
+
+  const cornerHeaderSelectGroup = createContainerGroup(0, 0, true);
+  cornerHeaderSelectGroup.role = 'corner-header';
+  cornerHeaderSelectGroup.name = 'select-overlay';
+  scene.cornerHeaderSelectGroup = cornerHeaderSelectGroup;
+
   scene.tableGroup.addChild(bodyGroup);
   //注意这块添加的顺序 会影响select框选效果 有可能引起框选框覆盖其他部分group的问题  具体问题出在update-select-border文件中的updateComponent方法
   scene.tableGroup.addChild(rowHeaderGroup);
@@ -69,6 +114,16 @@ export function initSceneGraph(scene: Scenegraph) {
   scene.tableGroup.addChild(rightTopCornerGroup);
   scene.tableGroup.addChild(leftBottomCornerGroup);
   scene.tableGroup.addChild(cornerHeaderGroup);
+
+  scene.tableGroup.addChild(bodySelectGroup);
+  scene.tableGroup.addChild(rowHeaderSelectGroup);
+  scene.tableGroup.addChild(bottomFrozenSelectGroup);
+  scene.tableGroup.addChild(colHeaderSelectGroup);
+  scene.tableGroup.addChild(rightFrozenSelectGroup);
+  scene.tableGroup.addChild(rightBottomCornerSelectGroup);
+  scene.tableGroup.addChild(rightTopCornerSelectGroup);
+  scene.tableGroup.addChild(leftBottomCornerSelectGroup);
+  scene.tableGroup.addChild(cornerHeaderSelectGroup);
   scene.tableGroup.addChild(componentGroup);
 }
 
diff --git a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
index 702e5c9d0..afb9fc251 100644
--- a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
+++ b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
@@ -526,7 +526,9 @@ export class SceneProxy {
       this.table.getColsWidth(this.bodyLeftCol, this.bodyLeftCol + (this.colEnd - this.colStart + 1)) / 2;
     const xLimitRight = this.table.getAllColsWidth() - xLimitLeft;
 
-    const screenLeft = this.table.getTargetColAt(x + this.table.scenegraph.rowHeaderGroup.attribute.width);
+    const screenLeft = this.table.getTargetColAt(
+      x + this.table.scenegraph.rowHeaderGroup.attribute.width + (this.table.getFrozenColsOffset?.() ?? 0)
+    );
     if (screenLeft) {
       this.screenLeftCol = screenLeft.col;
     }
diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts
index 344f6ff74..2530fdcba 100644
--- a/packages/vtable/src/scenegraph/scenegraph.ts
+++ b/packages/vtable/src/scenegraph/scenegraph.ts
@@ -126,6 +126,15 @@ export class Scenegraph {
   leftBottomCornerGroup: Group; // 左下角占位单元格Group,只在有下侧冻结行时使用
   rightBottomCornerGroup: Group; // 右下角占位单元格Group,只在有右侧下侧都有冻结行时使用
   componentGroup: Group; // 表格外组件Group
+  bodySelectGroup: Group;
+  rowHeaderSelectGroup: Group;
+  bottomFrozenSelectGroup: Group;
+  colHeaderSelectGroup: Group;
+  rightFrozenSelectGroup: Group;
+  rightTopCornerSelectGroup: Group;
+  leftBottomCornerSelectGroup: Group;
+  rightBottomCornerSelectGroup: Group;
+  cornerHeaderSelectGroup: Group;
   /** 所有选中区域对应的选框组件 */
   selectedRangeComponents: Map<string, { rect: IRect; fillhandle?: IRect; role: CellSubLocation }>;
   /** 当前正在选择区域对应的选框组件 为什么是map 以为可能一个选中区域会被拆分为多个rect组件 三块表头和body都分别对应不同组件*/
@@ -341,6 +350,15 @@ export class Scenegraph {
     delete this.rightBottomCornerGroup.border;
     this.leftBottomCornerGroup.clear();
     delete this.leftBottomCornerGroup.border;
+    this.bodySelectGroup?.clear();
+    this.rowHeaderSelectGroup?.clear();
+    this.bottomFrozenSelectGroup?.clear();
+    this.colHeaderSelectGroup?.clear();
+    this.rightFrozenSelectGroup?.clear();
+    this.rightTopCornerSelectGroup?.clear();
+    this.leftBottomCornerSelectGroup?.clear();
+    this.rightBottomCornerSelectGroup?.clear();
+    this.cornerHeaderSelectGroup?.clear();
 
     this.colHeaderGroup.setAttributes({
       x: 0,
@@ -399,6 +417,65 @@ export class Scenegraph {
       height: 0,
       visible: false
     });
+    this.bodySelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0
+    });
+    this.rowHeaderSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0
+    });
+    this.bottomFrozenSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      visible: false
+    });
+    this.colHeaderSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0
+    });
+    this.rightFrozenSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      visible: false
+    });
+    this.rightTopCornerSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      visible: false
+    });
+    this.leftBottomCornerSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      visible: false
+    });
+    this.rightBottomCornerSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      visible: false
+    });
+    this.cornerHeaderSelectGroup?.setAttributes({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0
+    });
 
     this.tableGroup.setAttributes({
       x: this.table.tableX,
@@ -1507,8 +1584,21 @@ export class Scenegraph {
     }
     this.bodyGroup.setAttribute('y', this.colHeaderGroup.attribute.height + y);
     this.rowHeaderGroup.setAttribute('y', this.cornerHeaderGroup.attribute.height + y);
+    this.bodySelectGroup.setAttribute('y', this.bodyGroup.attribute.y);
+    this.rowHeaderSelectGroup.setAttribute('y', this.rowHeaderGroup.attribute.y);
+    this.colHeaderSelectGroup.setAttribute('y', this.colHeaderGroup.attribute.y);
+    this.cornerHeaderSelectGroup.setAttribute('y', this.cornerHeaderGroup.attribute.y);
     if (this.table.rightFrozenColCount > 0) {
       this.rightFrozenGroup.setAttribute('y', this.rightTopCornerGroup.attribute.height + y);
+      this.rightFrozenSelectGroup.setAttribute('y', this.rightFrozenGroup.attribute.y);
+      this.rightTopCornerSelectGroup.setAttribute('y', this.rightTopCornerGroup.attribute.y);
+    }
+    if (this.table.bottomFrozenRowCount > 0) {
+      this.bottomFrozenSelectGroup.setAttribute('y', this.bottomFrozenGroup.attribute.y);
+      this.leftBottomCornerSelectGroup.setAttribute('y', this.leftBottomCornerGroup.attribute.y);
+    }
+    if (this.table.rightFrozenColCount > 0 && this.table.bottomFrozenRowCount > 0) {
+      this.rightBottomCornerSelectGroup.setAttribute('y', this.rightBottomCornerGroup.attribute.y);
     }
     // this.tableGroup.setAttribute('height', this.table.tableNoFrameHeight - y);
     // (this.tableGroup.lastChild as any).setAttribute('width', this.table.tableNoFrameWidth - x);
@@ -1545,8 +1635,21 @@ export class Scenegraph {
     }
     this.bodyGroup.setAttribute('x', this.table.getFrozenColsWidth() + x);
     this.colHeaderGroup.setAttribute('x', this.table.getFrozenColsWidth() + x);
+    this.bodySelectGroup.setAttribute('x', this.bodyGroup.attribute.x);
+    this.colHeaderSelectGroup.setAttribute('x', this.colHeaderGroup.attribute.x);
+    this.rowHeaderSelectGroup.setAttribute('x', this.rowHeaderGroup.attribute.x);
+    this.cornerHeaderSelectGroup.setAttribute('x', this.cornerHeaderGroup.attribute.x);
     if (this.table.bottomFrozenRowCount > 0) {
       this.bottomFrozenGroup.setAttribute('x', this.table.getFrozenColsWidth() + x);
+      this.bottomFrozenSelectGroup.setAttribute('x', this.bottomFrozenGroup.attribute.x);
+      this.leftBottomCornerSelectGroup.setAttribute('x', this.leftBottomCornerGroup.attribute.x);
+    }
+    if (this.table.rightFrozenColCount > 0) {
+      this.rightFrozenSelectGroup.setAttribute('x', this.rightFrozenGroup.attribute.x);
+      this.rightTopCornerSelectGroup.setAttribute('x', this.rightTopCornerGroup.attribute.x);
+    }
+    if (this.table.rightFrozenColCount > 0 && this.table.bottomFrozenRowCount > 0) {
+      this.rightBottomCornerSelectGroup.setAttribute('x', this.rightBottomCornerGroup.attribute.x);
     }
     this.updateNextFrame();
   }
@@ -1853,9 +1956,11 @@ export class Scenegraph {
   }
 
   updateContainerAttrWidthAndX() {
+    const frozenStartX = -(this.table.getFrozenColsScrollLeft?.() ?? 0);
+    const frozenViewportWidth = this.table.getFrozenColsWidth();
     // 更新各列x&col
-    const cornerX = updateContainerChildrenX(this.cornerHeaderGroup, 0);
-    const rowHeaderX = updateContainerChildrenX(this.rowHeaderGroup, 0);
+    updateContainerChildrenX(this.cornerHeaderGroup, frozenStartX);
+    updateContainerChildrenX(this.rowHeaderGroup, frozenStartX);
     const colHeaderX =
       this.colHeaderGroup.hasChildNodes() && this.colHeaderGroup.firstChild
         ? updateContainerChildrenX(
@@ -1887,17 +1992,17 @@ export class Scenegraph {
           ? this.table.getColsWidth(this.table.frozenColCount ?? 0, (this.bottomFrozenGroup.firstChild as any).col - 1)
           : 0
       );
-    updateContainerChildrenX(this.leftBottomCornerGroup, 0);
+    updateContainerChildrenX(this.leftBottomCornerGroup, frozenStartX);
     updateContainerChildrenX(this.rightTopCornerGroup, 0);
     updateContainerChildrenX(this.rightBottomCornerGroup, 0);
 
     // 更新容器
-    this.cornerHeaderGroup.setDeltaWidth(cornerX - this.cornerHeaderGroup.attribute.width);
-    this.leftBottomCornerGroup.setDeltaWidth(cornerX - this.leftBottomCornerGroup.attribute.width);
+    this.cornerHeaderGroup.setDeltaWidth(frozenViewportWidth - this.cornerHeaderGroup.attribute.width);
+    this.leftBottomCornerGroup.setDeltaWidth(frozenViewportWidth - this.leftBottomCornerGroup.attribute.width);
     //TODO 可能有影响
     this.colHeaderGroup.setDeltaWidth(colHeaderX - this.colHeaderGroup.attribute.width);
     // this.rightFrozenGroup.setDeltaWidth(colHeaderX - this.table.getRightFrozenColsWidth());
-    this.rowHeaderGroup.setDeltaWidth(rowHeaderX - this.rowHeaderGroup.attribute.width);
+    this.rowHeaderGroup.setDeltaWidth(frozenViewportWidth - this.rowHeaderGroup.attribute.width);
     this.bottomFrozenGroup.setDeltaWidth(colHeaderX - this.bottomFrozenGroup.attribute.width);
     this.rightFrozenGroup.setDeltaWidth(rightX - this.rightFrozenGroup.attribute.width);
     this.rightTopCornerGroup.setDeltaWidth(rightX - this.rightTopCornerGroup.attribute.width);
@@ -1908,6 +2013,14 @@ export class Scenegraph {
     this.bodyGroup.setAttribute('x', this.rowHeaderGroup.attribute.width);
   }
 
+  setFrozenColsScrollLeft(left: number) {
+    const frozenStartX = -left;
+    updateContainerChildrenX(this.cornerHeaderGroup, frozenStartX);
+    updateContainerChildrenX(this.rowHeaderGroup, frozenStartX);
+    updateContainerChildrenX(this.leftBottomCornerGroup, frozenStartX);
+    this.updateNextFrame();
+  }
+
   updateContainerAttrHeightAndY() {
     for (let i = 0; i < this.cornerHeaderGroup.children.length; i++) {
       updateContainerChildrenY(this.cornerHeaderGroup.children[i] as Group, 0);
@@ -1990,6 +2103,7 @@ export class Scenegraph {
       this.updateContainerAttrHeightAndY();
     }
     this.updateTableSize();
+    this.syncSelectOverlayGroups();
     this.component.updateScrollBar();
 
     // this.updateDomContainer();
@@ -1997,6 +2111,93 @@ export class Scenegraph {
     this.updateNextFrame();
   }
 
+  syncSelectOverlayGroups() {
+    this.bodySelectGroup.setAttributes({
+      x: this.bodyGroup.attribute.x,
+      y: this.bodyGroup.attribute.y,
+      width: this.bodyGroup.attribute.width,
+      height: this.bodyGroup.attribute.height
+    });
+    this.rowHeaderSelectGroup.setAttributes({
+      x: this.rowHeaderGroup.attribute.x,
+      y: this.rowHeaderGroup.attribute.y,
+      width: this.rowHeaderGroup.attribute.width,
+      height: this.rowHeaderGroup.attribute.height
+    });
+    this.colHeaderSelectGroup.setAttributes({
+      x: this.colHeaderGroup.attribute.x,
+      y: this.colHeaderGroup.attribute.y,
+      width: this.colHeaderGroup.attribute.width,
+      height: this.colHeaderGroup.attribute.height
+    });
+    this.cornerHeaderSelectGroup.setAttributes({
+      x: this.cornerHeaderGroup.attribute.x,
+      y: this.cornerHeaderGroup.attribute.y,
+      width: this.cornerHeaderGroup.attribute.width,
+      height: this.cornerHeaderGroup.attribute.height
+    });
+
+    this.rightFrozenSelectGroup.setAttributes({
+      x: this.rightFrozenGroup.attribute.x,
+      y: this.rightFrozenGroup.attribute.y,
+      width: this.rightFrozenGroup.attribute.width,
+      height: this.rightFrozenGroup.attribute.height,
+      visible: this.rightFrozenGroup.attribute.visible
+    });
+    this.bottomFrozenSelectGroup.setAttributes({
+      x: this.bottomFrozenGroup.attribute.x,
+      y: this.bottomFrozenGroup.attribute.y,
+      width: this.bottomFrozenGroup.attribute.width,
+      height: this.bottomFrozenGroup.attribute.height,
+      visible: this.bottomFrozenGroup.attribute.visible
+    });
+    this.rightTopCornerSelectGroup.setAttributes({
+      x: this.rightTopCornerGroup.attribute.x,
+      y: this.rightTopCornerGroup.attribute.y,
+      width: this.rightTopCornerGroup.attribute.width,
+      height: this.rightTopCornerGroup.attribute.height,
+      visible: this.rightTopCornerGroup.attribute.visible
+    });
+    this.leftBottomCornerSelectGroup.setAttributes({
+      x: this.leftBottomCornerGroup.attribute.x,
+      y: this.leftBottomCornerGroup.attribute.y,
+      width: this.leftBottomCornerGroup.attribute.width,
+      height: this.leftBottomCornerGroup.attribute.height,
+      visible: this.leftBottomCornerGroup.attribute.visible
+    });
+    this.rightBottomCornerSelectGroup.setAttributes({
+      x: this.rightBottomCornerGroup.attribute.x,
+      y: this.rightBottomCornerGroup.attribute.y,
+      width: this.rightBottomCornerGroup.attribute.width,
+      height: this.rightBottomCornerGroup.attribute.height,
+      visible: this.rightBottomCornerGroup.attribute.visible
+    });
+  }
+
+  getSelectOverlayGroup(selectRangeType: CellSubLocation): Group {
+    switch (selectRangeType) {
+      case 'body':
+        return this.bodySelectGroup;
+      case 'rowHeader':
+        return this.rowHeaderSelectGroup;
+      case 'bottomFrozen':
+        return this.bottomFrozenSelectGroup;
+      case 'columnHeader':
+        return this.colHeaderSelectGroup;
+      case 'rightFrozen':
+        return this.rightFrozenSelectGroup;
+      case 'rightTopCorner':
+        return this.rightTopCornerSelectGroup;
+      case 'leftBottomCorner':
+        return this.leftBottomCornerSelectGroup;
+      case 'rightBottomCorner':
+        return this.rightBottomCornerSelectGroup;
+      case 'cornerHeader':
+      default:
+        return this.cornerHeaderSelectGroup;
+    }
+  }
+
   updateCellContentWhileResize(col: number, row: number) {
     const isVtableMerge = this.table.getCellRawRecord(col, row)?.vtableMerge;
 
diff --git a/packages/vtable/src/scenegraph/select/create-select-border.ts b/packages/vtable/src/scenegraph/select/create-select-border.ts
index 265b67ac0..ac1cfee3f 100644
--- a/packages/vtable/src/scenegraph/select/create-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/create-select-border.ts
@@ -1,7 +1,6 @@
 import { createRect } from '@src/vrender';
 import type { CellSubLocation } from '../../ts-types';
 import type { Scenegraph } from '../scenegraph';
-import { table } from 'console';
 import type { BaseTableAPI } from '../../ts-types/base-table';
 
 export function createCellSelectBorder(
@@ -39,6 +38,9 @@ export function createCellSelectBorder(
   const startRow = Math.min(start_Row, end_Row);
   const endCol = Math.max(start_Col, end_Col);
   const endRow = Math.max(start_Row, end_Row);
+  const overlayGroup = scene.getSelectOverlayGroup(selectRangeType);
+  const offsetX = scene.tableGroup.attribute.x + (overlayGroup.attribute.x ?? 0);
+  const offsetY = scene.tableGroup.attribute.y + (overlayGroup.attribute.y ?? 0);
   const firstCellBound = scene.highPerformanceGetCell(startCol, startRow).globalAABBBounds;
   const lastCellBound = scene.highPerformanceGetCell(endCol, endRow).globalAABBBounds;
   const theme = scene.table.theme;
@@ -60,8 +62,8 @@ export function createCellSelectBorder(
       }
       return false;
     }),
-    x: firstCellBound.x1 - scene.tableGroup.attribute.x, // 坐标xy及宽高width height 不需要这里计算具体值 update-select-border文件中updateComponent方法中有逻辑  且该方法调用时间是render
-    y: firstCellBound.y1 - scene.tableGroup.attribute.y,
+    x: firstCellBound.x1 - offsetX,
+    y: firstCellBound.y1 - offsetY,
     width: 0,
     height: 0,
     visible: true,
@@ -83,8 +85,8 @@ export function createCellSelectBorder(
       fill: bodyClickBorderColor as string,
       // lineWidth: bodyClickLineWidth as number,
       stroke: bodyClickBorderColor as string, // 右下角小方块边框颜色
-      x: lastCellBound.x2 - 3, // 调整小方块位置
-      y: lastCellBound.y2 - 3, // 调整小方块位置
+      x: lastCellBound.x2 - offsetX - 3,
+      y: lastCellBound.y2 - offsetY - 3,
       width: 6,
       height: 6,
 
@@ -97,47 +99,10 @@ export function createCellSelectBorder(
     fillhandle,
     role: selectRangeType
   });
-  scene.tableGroup.insertAfter(
-    rect,
-    selectRangeType === 'body'
-      ? scene.bodyGroup
-      : selectRangeType === 'columnHeader'
-      ? scene.colHeaderGroup
-      : selectRangeType === 'rowHeader'
-      ? scene.rowHeaderGroup
-      : selectRangeType === 'cornerHeader'
-      ? scene.cornerHeaderGroup
-      : selectRangeType === 'rightTopCorner'
-      ? scene.rightTopCornerGroup
-      : selectRangeType === 'rightFrozen'
-      ? scene.rightFrozenGroup
-      : selectRangeType === 'leftBottomCorner'
-      ? scene.leftBottomCornerGroup
-      : selectRangeType === 'bottomFrozen'
-      ? scene.bottomFrozenGroup
-      : scene.rightBottomCornerGroup
-  );
-  isHasFillHandleRect &&
-    scene.tableGroup.insertAfter(
-      fillhandle,
-      selectRangeType === 'body'
-        ? scene.bodyGroup
-        : selectRangeType === 'columnHeader'
-        ? scene.colHeaderGroup
-        : selectRangeType === 'rowHeader'
-        ? scene.rowHeaderGroup
-        : selectRangeType === 'cornerHeader'
-        ? scene.cornerHeaderGroup
-        : selectRangeType === 'rightTopCorner'
-        ? scene.rightTopCornerGroup
-        : selectRangeType === 'rightFrozen'
-        ? scene.rightFrozenGroup
-        : selectRangeType === 'leftBottomCorner'
-        ? scene.leftBottomCornerGroup
-        : selectRangeType === 'bottomFrozen'
-        ? scene.bottomFrozenGroup
-        : scene.rightBottomCornerGroup
-    );
+  overlayGroup.addChild(rect);
+  if (isHasFillHandleRect) {
+    overlayGroup.addChild(fillhandle);
+  }
 }
 
 // set corner radius in select rect which covers the corner of the table
diff --git a/packages/vtable/src/scenegraph/select/delete-select-border.ts b/packages/vtable/src/scenegraph/select/delete-select-border.ts
index c55779f1c..eb262242a 100644
--- a/packages/vtable/src/scenegraph/select/delete-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/delete-select-border.ts
@@ -20,7 +20,6 @@ export function deleteAllSelectBorder(scene: Scenegraph) {
   scene.selectedRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       selectComp.rect.delete();
-
       selectComp.fillhandle?.delete();
     }
   );
@@ -31,7 +30,6 @@ export function deleteAllSelectingBorder(scene: Scenegraph) {
   scene.selectingRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       selectComp.rect.delete();
-
       selectComp.fillhandle?.delete();
     }
   );
diff --git a/packages/vtable/src/scenegraph/select/update-custom-select-border.ts b/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
index 95cb01dde..f0ddccd5e 100644
--- a/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
@@ -204,6 +204,9 @@ function createCustomCellSelectBorder(
   const startRow = Math.min(start_Row, end_Row);
   const endCol = Math.max(start_Col, end_Col);
   const endRow = Math.max(start_Row, end_Row);
+  const overlayGroup = scene.getSelectOverlayGroup(selectRangeType);
+  const offsetX = scene.tableGroup.attribute.x + (overlayGroup.attribute.x ?? 0);
+  const offsetY = scene.tableGroup.attribute.y + (overlayGroup.attribute.y ?? 0);
   const firstCellBound = scene.highPerformanceGetCell(startCol, startRow).globalAABBBounds;
   const rect = createRect({
     pickable: false,
@@ -216,8 +219,8 @@ function createCustomCellSelectBorder(
       }
       return false;
     }),
-    x: firstCellBound.x1 - scene.tableGroup.attribute.x, // 坐标xy及宽高width height 不需要这里计算具体值 update-select-border文件中updateComponent方法中有逻辑  且该方法调用时间是render
-    y: firstCellBound.y1 - scene.tableGroup.attribute.y,
+    x: firstCellBound.x1 - offsetX,
+    y: firstCellBound.y1 - offsetY,
     width: 0,
     height: 0,
     visible: true,
@@ -235,24 +238,5 @@ function createCustomCellSelectBorder(
     rect,
     role: selectRangeType
   });
-  scene.tableGroup.insertAfter(
-    rect,
-    selectRangeType === 'body'
-      ? scene.bodyGroup
-      : selectRangeType === 'columnHeader'
-      ? scene.colHeaderGroup
-      : selectRangeType === 'rowHeader'
-      ? scene.rowHeaderGroup
-      : selectRangeType === 'cornerHeader'
-      ? scene.cornerHeaderGroup
-      : selectRangeType === 'rightTopCorner'
-      ? scene.rightTopCornerGroup
-      : selectRangeType === 'rightFrozen'
-      ? scene.rightFrozenGroup
-      : selectRangeType === 'leftBottomCorner'
-      ? scene.leftBottomCornerGroup
-      : selectRangeType === 'bottomFrozen'
-      ? scene.bottomFrozenGroup
-      : scene.rightBottomCornerGroup
-  );
+  overlayGroup.addChild(rect);
 }
diff --git a/packages/vtable/src/scenegraph/select/update-select-border.ts b/packages/vtable/src/scenegraph/select/update-select-border.ts
index f840986b4..a27948099 100644
--- a/packages/vtable/src/scenegraph/select/update-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/update-select-border.ts
@@ -93,14 +93,18 @@ function updateComponent(
   const colsWidth = table.getColsWidth(computeRectCellRangeStartCol, computeRectCellRangeEndCol);
   const rowsHeight = table.getRowsHeight(computeRectCellRangeStartRow, computeRectCellRangeEndRow);
 
+  const overlayGroup = scene.getSelectOverlayGroup(selectComp.role);
+  const offsetX = scene.tableGroup.attribute.x + (overlayGroup.attribute.x ?? 0);
+  const offsetY = scene.tableGroup.attribute.y + (overlayGroup.attribute.y ?? 0);
+
   const firstCellBound = scene.highPerformanceGetCell(
     computeRectCellRangeStartCol,
     computeRectCellRangeStartRow
   ).globalAABBBounds;
 
   selectComp.rect.setAttributes({
-    x: firstCellBound.x1 - scene.tableGroup.attribute.x, //坐标xy在下面的逻辑中会做适当调整
-    y: firstCellBound.y1 - scene.tableGroup.attribute.y,
+    x: firstCellBound.x1 - offsetX,
+    y: firstCellBound.y1 - offsetY,
     width: colsWidth,
     height: rowsHeight,
     visible: true
@@ -127,17 +131,17 @@ function updateComponent(
     //当选择区域没有到到最后一列时
     if (endCol < table.colCount - 1) {
       lastCellBound = scene.highPerformanceGetCell(endCol, endRow).globalAABBBounds;
-      handlerX = lastCellBound.x2 - scene.tableGroup.attribute.x - 3;
+      handlerX = lastCellBound.x2 - offsetX - 3;
     } else {
       // 最后一列
       // computeRectCellRangeStartCol 而且是第一列时
       if (startCol === 0) {
         //解决issue #4376  但还是有问题当存在冻结的时候。以及需要处理类似情况下面逻辑最后一行的情况
         lastCellBound = scene.highPerformanceGetCell(0, endRow).globalAABBBounds;
-        handlerX = lastCellBound.x1 - scene.tableGroup.attribute.x;
+        handlerX = lastCellBound.x1 - offsetX;
       } else {
         lastCellBound = scene.highPerformanceGetCell(startCol - 1, endRow).globalAABBBounds;
-        handlerX = lastCellBound.x2 - scene.tableGroup.attribute.x - 3;
+        handlerX = lastCellBound.x2 - offsetX - 3;
       }
     }
     // const handlerX = lastCellBound.x2 - scene.tableGroup.attribute.x - 3;
@@ -148,7 +152,7 @@ function updateComponent(
       // 最后一行
       lastCellBound = scene.highPerformanceGetCell(endCol, startRow - 1).globalAABBBounds;
     }
-    const handlerY = lastCellBound.y2 - scene.tableGroup.attribute.y - 3;
+    const handlerY = lastCellBound.y2 - offsetY - 3;
     //#endregion
 
     selectComp.fillhandle?.setAttributes({
@@ -160,180 +164,6 @@ function updateComponent(
     });
   }
 
-  //#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半，因此需要动态调整层级
-  let isNearRowHeader = table.frozenColCount ? startCol === table.frozenColCount : false;
-  if (!isNearRowHeader && table.frozenColCount && table.scrollLeft > 0 && startCol >= table.frozenColCount) {
-    const startColRelativePosition = table.getColsWidth(0, startCol - 1) - table.scrollLeft;
-    if (startColRelativePosition < table.getFrozenColsWidth()) {
-      isNearRowHeader = true;
-    }
-  }
-
-  let isNearRightRowHeader = table.rightFrozenColCount
-    ? table.rightFrozenColCount > 0 && endCol === table.colCount - table.rightFrozenColCount - 1
-    : false;
-  if (!isNearRightRowHeader && table.rightFrozenColCount && endCol < table.colCount - table.rightFrozenColCount) {
-    const endColRelativePosition = table.getColsWidth(0, endCol) - table.scrollLeft;
-    if (endColRelativePosition > table.tableNoFrameWidth - table.getRightFrozenColsWidth()) {
-      isNearRightRowHeader = true;
-    }
-  }
-
-  let isNearColHeader = table.frozenRowCount ? startRow === table.frozenRowCount : true;
-  if (!isNearColHeader && table.frozenRowCount && table.scrollTop > 0 && startRow >= table.frozenRowCount) {
-    const startRowRelativePosition = table.getRowsHeight(0, startRow - 1) - table.scrollTop;
-    if (startRowRelativePosition < table.getFrozenRowsHeight()) {
-      isNearColHeader = true;
-    }
-  }
-
-  let isNearBottomColHeader = table.bottomFrozenRowCount
-    ? endRow === table.rowCount - table.bottomFrozenRowCount - 1
-    : false;
-  if (!isNearBottomColHeader && table.bottomFrozenRowCount && endRow < table.rowCount - table.bottomFrozenRowCount) {
-    const endRowRelativePosition = table.getRowsHeight(0, endRow) - table.scrollTop;
-    if (endRowRelativePosition > table.tableNoFrameHeight - table.getBottomFrozenRowsHeight()) {
-      isNearBottomColHeader = true;
-    }
-  }
-
-  const { dynamicUpdateSelectionSize } = table.theme.selectionStyle;
-  if (
-    (isNearRowHeader && (selectComp.rect.attribute.stroke[3] || dynamicUpdateSelectionSize)) ||
-    (isNearRightRowHeader && (selectComp.rect.attribute.stroke[1] || dynamicUpdateSelectionSize)) ||
-    (isNearColHeader && (selectComp.rect.attribute.stroke[0] || dynamicUpdateSelectionSize)) ||
-    (isNearBottomColHeader && (selectComp.rect.attribute.stroke[2] || dynamicUpdateSelectionSize))
-  ) {
-    if (isNearRowHeader && selectComp.rect.attribute.stroke[3]) {
-      scene.tableGroup.insertAfter(
-        selectComp.rect,
-        selectComp.role === 'columnHeader'
-          ? scene.cornerHeaderGroup
-          : selectComp.role === 'bottomFrozen'
-          ? scene.leftBottomCornerGroup
-          : scene.rowHeaderGroup
-      );
-    }
-
-    if (isNearBottomColHeader && selectComp.rect.attribute.stroke[2]) {
-      scene.tableGroup.insertAfter(
-        selectComp.rect,
-        selectComp.role === 'rowHeader'
-          ? scene.leftBottomCornerGroup
-          : selectComp.role === 'rightFrozen'
-          ? scene.rightBottomCornerGroup
-          : scene.bottomFrozenGroup
-      );
-    }
-
-    if (isNearColHeader && selectComp.rect.attribute.stroke[0]) {
-      scene.tableGroup.insertAfter(
-        selectComp.rect,
-        selectComp.role === 'rowHeader'
-          ? scene.cornerHeaderGroup
-          : selectComp.role === 'rightFrozen'
-          ? scene.rightTopCornerGroup
-          : scene.colHeaderGroup
-      );
-    }
-    if (isNearRightRowHeader && selectComp.rect.attribute.stroke[1]) {
-      scene.tableGroup.insertAfter(
-        selectComp.rect,
-        selectComp.role === 'columnHeader'
-          ? scene.rightTopCornerGroup
-          : selectComp.role === 'bottomFrozen'
-          ? scene.rightBottomCornerGroup
-          : scene.rightFrozenGroup
-      );
-    }
-
-    //#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面，所有需要调整选中框rect的 边界
-    if (
-      selectComp.rect.attribute.x < table.getFrozenColsWidth() &&
-      // selectComp.rect.attribute.x + selectComp.rect.attribute.width > scene.rowHeaderGroup.attribute.width &&
-      table.scrollLeft > 0 &&
-      (selectComp.role === 'body' || selectComp.role === 'columnHeader' || selectComp.role === 'bottomFrozen')
-    ) {
-      const width = selectComp.rect.attribute.width - (table.getFrozenColsWidth() - selectComp.rect.attribute.x);
-      selectComp.rect.setAttributes({
-        x: selectComp.rect.attribute.x + (table.getFrozenColsWidth() - selectComp.rect.attribute.x),
-        width: width > 0 ? width : 0
-      });
-      selectComp.fillhandle?.setAttributes({
-        visible: width > 0
-      });
-    }
-    if (
-      // selectComp.rect.attribute.x < scene.rightFrozenGroup.attribute.x &&
-      table.getRightFrozenColsWidth() > 0 && // right冻结列存在的情况下
-      scene.rightFrozenGroup.attribute.height > 0 &&
-      selectComp.rect.attribute.x + selectComp.rect.attribute.width > scene.rightFrozenGroup.attribute.x &&
-      (selectComp.role === 'body' || selectComp.role === 'columnHeader' || selectComp.role === 'bottomFrozen')
-    ) {
-      const width = scene.rightFrozenGroup.attribute.x - selectComp.rect.attribute.x;
-      selectComp.rect.setAttributes({
-        x: selectComp.rect.attribute.x,
-        width: width > 0 ? width : 0
-      });
-      selectComp.fillhandle?.setAttributes({
-        visible: width - colsWidth > 0
-      });
-    }
-    if (
-      selectComp.rect.attribute.y < scene.colHeaderGroup.attribute.height &&
-      table.scrollTop > 0 &&
-      (selectComp.role === 'body' || selectComp.role === 'rowHeader' || selectComp.role === 'rightFrozen')
-    ) {
-      const height =
-        selectComp.rect.attribute.height - (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y);
-      selectComp.rect.setAttributes({
-        y: selectComp.rect.attribute.y + (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y),
-        height: height > 0 ? height : 0
-      });
-      selectComp.fillhandle?.setAttributes({
-        visible: height > 0
-      });
-    }
-    if (
-      scene.bottomFrozenGroup.attribute.width > 0 &&
-      scene.bottomFrozenGroup.attribute.height > 0 &&
-      selectComp.rect.attribute.y + selectComp.rect.attribute.height > scene.bottomFrozenGroup.attribute.y &&
-      (selectComp.role === 'body' || selectComp.role === 'rowHeader' || selectComp.role === 'rightFrozen')
-    ) {
-      const height = scene.bottomFrozenGroup.attribute.y - selectComp.rect.attribute.y;
-      selectComp.rect.setAttributes({
-        y: selectComp.rect.attribute.y,
-        height: height > 0 ? height : 0
-      });
-      selectComp.fillhandle?.setAttributes({
-        visible: height - rowsHeight > 0
-      });
-    }
-    //#endregion
-  } else {
-    scene.tableGroup.insertAfter(
-      selectComp.rect,
-      selectComp.role === 'body'
-        ? scene.bodyGroup
-        : selectComp.role === 'columnHeader'
-        ? scene.colHeaderGroup
-        : selectComp.role === 'rowHeader'
-        ? scene.rowHeaderGroup
-        : selectComp.role === 'cornerHeader'
-        ? scene.cornerHeaderGroup
-        : selectComp.role === 'rightTopCorner'
-        ? scene.rightTopCornerGroup
-        : selectComp.role === 'rightFrozen'
-        ? scene.rightFrozenGroup
-        : selectComp.role === 'leftBottomCorner'
-        ? scene.leftBottomCornerGroup
-        : selectComp.role === 'bottomFrozen'
-        ? scene.bottomFrozenGroup
-        : scene.rightBottomCornerGroup
-    );
-  }
-  //#endregion
-
   //#region 处理边缘被截问题
   let diffSize = 0;
   if (typeof selectComp.rect.attribute.lineWidth === 'number') {
diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts
index ed89875b2..0112a3d23 100644
--- a/packages/vtable/src/state/state.ts
+++ b/packages/vtable/src/state/state.ts
@@ -179,6 +179,7 @@ export class StateManager {
   scroll: {
     horizontalBarPos: number;
     verticalBarPos: number;
+    frozenHorizontalBarPos: number;
   };
   tablePosition: {
     absoluteX: number;
@@ -398,7 +399,8 @@ export class StateManager {
     };
     this.scroll = {
       horizontalBarPos: 0,
-      verticalBarPos: 0
+      verticalBarPos: 0,
+      frozenHorizontalBarPos: 0
     };
     this.tablePosition = {
       absoluteX: 0,
@@ -982,7 +984,12 @@ export class StateManager {
       const maxFrozenWidth = this.table._getMaxFrozenWidth();
 
       if (frozenWidth > maxFrozenWidth) {
-        if (this.table.internalProps.unfreezeAllOnExceedsMaxWidth) {
+        if (this.table.options.scrollFrozenCols) {
+          if (this.table.frozenColCount !== originalFrozenColCount) {
+            this.table._setFrozenColCount(originalFrozenColCount);
+            this.setFrozenCol(originalFrozenColCount);
+          }
+        } else if (this.table.internalProps.unfreezeAllOnExceedsMaxWidth) {
           this.table._setFrozenColCount(0);
           this.setFrozenCol(-1);
         } else {
@@ -994,11 +1001,30 @@ export class StateManager {
         this.table._setFrozenColCount(originalFrozenColCount);
         this.setFrozenCol(originalFrozenColCount);
       }
+      if (!this.table.options.scrollFrozenCols || this.table.getFrozenColsOffset() === 0) {
+        this.setFrozenColsScrollLeft(0, false);
+      } else {
+        this.setFrozenColsScrollLeft(this.scroll.frozenHorizontalBarPos, false);
+      }
     } else {
       this.clearFrozenObserver();
     }
   }
 
+  setFrozenColsScrollLeft(left: number, triggerRender: boolean = true) {
+    if (!this.table || !this.table.scenegraph) {
+      return;
+    }
+    const maxScrollLeft = this.table.getFrozenColsOffset();
+    left = Math.max(0, Math.min(left, maxScrollLeft));
+    left = Math.ceil(left);
+    if (this.scroll.frozenHorizontalBarPos === left) {
+      return;
+    }
+    this.scroll.frozenHorizontalBarPos = left;
+    triggerRender && this.table.scenegraph.setFrozenColsScrollLeft(left);
+  }
+
   clearFrozenObserver() {
     // 清理观察器
     if (this._frozenObserver) {
@@ -1124,8 +1150,10 @@ export class StateManager {
   updateHorizontalScrollBar(xRatio: number) {
     const totalWidth = this.table.getAllColsWidth();
     const oldHorizontalBarPos = this.scroll.horizontalBarPos;
+    const frozenOffset = this.table.getFrozenColsOffset?.() ?? 0;
+    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset);
 
-    let horizontalBarPos = Math.ceil(xRatio * (totalWidth - this.table.scenegraph.width));
+    let horizontalBarPos = Math.ceil(xRatio * scrollRange);
     if (!isValid(horizontalBarPos) || isNaN(horizontalBarPos)) {
       horizontalBarPos = 0;
     }
@@ -1147,7 +1175,7 @@ export class StateManager {
 
     if (canScroll.some(value => value === false)) {
       // reset scrollbar pos
-      const xRatio = this.scroll.horizontalBarPos / (totalWidth - this.table.scenegraph.width);
+      const xRatio = scrollRange ? this.scroll.horizontalBarPos / scrollRange : 0;
       this.table.scenegraph.component.updateHorizontalScrollBarPos(xRatio);
       return;
     }
@@ -1273,7 +1301,8 @@ export class StateManager {
     const oldScrollLeft = this.table.scrollLeft;
     // 矫正left值范围
     const totalWidth = this.table.getAllColsWidth();
-    const frozenWidth = this.table.getFrozenColsWidth();
+    const frozenOffset = this.table.getFrozenColsOffset?.() ?? 0;
+    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset);
 
     // _disableColumnAndRowSizeRound环境中，可能出现
     // getAllColsWidth/getAllRowsHeight(A) + getAllColsWidth/getAllRowsHeight(B) < getAllColsWidth/getAllRowsHeight(A+B)
@@ -1281,10 +1310,10 @@ export class StateManager {
     // 这里加入tolerance，避免出现无用滚动
     const sizeTolerance = this.table.options.customConfig?._disableColumnAndRowSizeRound ? 1 : 0;
 
-    left = Math.max(0, Math.min(left, totalWidth - this.table.scenegraph.width - sizeTolerance));
+    left = Math.max(0, Math.min(left, scrollRange - sizeTolerance));
     left = Math.ceil(left);
     const oldHorizontalBarPos = this.scroll.horizontalBarPos;
-    const xRatio = left / (totalWidth - this.table.scenegraph.width);
+    const xRatio = scrollRange ? left / scrollRange : 0;
 
     // if (oldHorizontalBarPos !== left && triggerEvent) {
     if (
@@ -1311,7 +1340,7 @@ export class StateManager {
 
       if (canScroll.some(value => value === false)) {
         // reset scrollbar pos
-        const xRatio = this.scroll.horizontalBarPos / (totalWidth - this.table.scenegraph.width);
+        const xRatio = scrollRange ? this.scroll.horizontalBarPos / scrollRange : 0;
         this.table.scenegraph.component.updateHorizontalScrollBarPos(xRatio);
         return;
       }
diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts
index 932a8bfcb..eb1a476da 100644
--- a/packages/vtable/src/ts-types/base-table.ts
+++ b/packages/vtable/src/ts-types/base-table.ts
@@ -319,6 +319,7 @@ export interface BaseTableConstructorOptions {
   maxFrozenWidth?: number | string;
   /** 超过最大冻结宽度后是否全部解冻，默认true */
   unfreezeAllOnExceedsMaxWidth?: boolean;
+  scrollFrozenCols?: boolean;
 
   // /** 待实现 TODO */
   // frozenRowCount?: number;
@@ -851,6 +852,9 @@ export interface BaseTableAPI {
 
   getFrozenRowsHeight: () => number;
   getFrozenColsWidth: () => number;
+  getFrozenColsContentWidth: () => number;
+  getFrozenColsOffset: () => number;
+  getFrozenColsScrollLeft: () => number;
   getBottomFrozenRowsHeight: () => number;
   getRightFrozenColsWidth: () => number;
   selectCell: (

From 110f1da78a2376343c4960071bc492716d61867c Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Fri, 20 Mar 2026 19:09:47 +0800
Subject: [PATCH 04/16] docs: update changlog of rush

---
 .../feat-frozenColumn_scroll_2026-03-20-11-09.json    | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.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
new file mode 100644
index 000000000..4f2c6adbc
--- /dev/null
+++ b/common/changes/@visactor/vtable/feat-frozenColumn_scroll_2026-03-20-11-09.json
@@ -0,0 +1,11 @@
+{
+  "changes": [
+    {
+      "comment": "feat: add option scrollFrozenCols\n\n",
+      "type": "none",
+      "packageName": "@visactor/vtable"
+    }
+  ],
+  "packageName": "@visactor/vtable",
+  "email": "892739385@qq.com"
+}
\ No newline at end of file

From 2f6e00b564502176d04eb82173ab11321bbb1f5a Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Sat, 21 Mar 2026 20:22:25 +0800
Subject: [PATCH 05/16] fix: addRecords recreateSelectRangeComponent

---
 packages/vtable/src/scenegraph/scenegraph.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts
index 2530fdcba..efd8ee8ea 100644
--- a/packages/vtable/src/scenegraph/scenegraph.ts
+++ b/packages/vtable/src/scenegraph/scenegraph.ts
@@ -1709,6 +1709,9 @@ export class Scenegraph {
     if (this.table.options.menu?.contextMenuWorkOnlyCell === false) {
       this.canvasShowMenu();
     }
+    if (this.table.stateManager.select.ranges?.length) {
+      this.recreateAllSelectRangeComponents();
+    }
     this.updateNextFrame();
   }
 

From dc8f2836c660213ba5a455402a985a3fc56883fd Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Mon, 23 Mar 2026 14:49:35 +0800
Subject: [PATCH 06/16] feat: add left frozen and right frozen columns region
 scroll component

---
 .../list-table-scrollable-frozen-cols.ts      |   8 +
 packages/vtable/src/core/BaseTable.ts         |  21 +-
 .../src/core/utils/get-cell-position.ts       |   2 +
 .../vtable/src/event/listener/scroll-bar.ts   | 161 ++++++++++++++-
 .../vtable/src/event/listener/table-group.ts  |  33 ++-
 packages/vtable/src/event/scroll.ts           |  60 +++++-
 .../scenegraph/component/table-component.ts   | 192 +++++++++++++++++-
 .../vtable/src/scenegraph/component/util.ts   |   6 +-
 .../group-creater/progress/proxy.ts           |   4 +-
 packages/vtable/src/scenegraph/scenegraph.ts  |  28 ++-
 packages/vtable/src/state/state.ts            |  57 +++++-
 packages/vtable/src/ts-types/base-table.ts    |   5 +
 12 files changed, 541 insertions(+), 36 deletions(-)

diff --git a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
index f3ee040f5..c564326f7 100644
--- a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
+++ b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
@@ -43,8 +43,16 @@ export function createTable() {
     records,
     columns,
     frozenColCount: 6,
+    rightFrozenColCount: 4,
     maxFrozenWidth: 320,
     scrollFrozenCols: true,
+    maxRightFrozenWidth: 320,
+    scrollRightFrozenCols: true,
+    theme: VTable.themes.DEFAULT.extends({
+      scrollStyle: {
+        visible: 'none'
+      }
+    }),
     excelOptions: {
       fillHandle: args => {
         const { selectRanges, table } = args;
diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts
index fa8076646..b375443f4 100644
--- a/packages/vtable/src/core/BaseTable.ts
+++ b/packages/vtable/src/core/BaseTable.ts
@@ -1088,6 +1088,10 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     const maxFrozenWidth = this.options.maxFrozenWidth ?? '80%';
     return _toPxWidth(this, maxFrozenWidth);
   }
+  _getMaxRightFrozenWidth(): number {
+    const maxRightFrozenWidth = this.options.maxRightFrozenWidth ?? this.options.maxFrozenWidth ?? '80%';
+    return _toPxWidth(this, maxRightFrozenWidth);
+  }
   _getComputedFrozenColCount(frozenColCount: number): number {
     const maxFrozenWidth = this._getMaxFrozenWidth();
     let computedfrozenColCount = frozenColCount;
@@ -3047,8 +3051,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
    * @returns
    */
   getRightFrozenColsWidth(): number {
+    const contentWidth = this.getRightFrozenColsContentWidth();
+    if (!this.options.scrollRightFrozenCols) {
+      return contentWidth;
+    }
+    const maxRightFrozenWidth = this._getMaxRightFrozenWidth();
+    return Math.min(contentWidth, maxRightFrozenWidth);
+  }
+  getRightFrozenColsContentWidth(): number {
     if (this.rightFrozenColCount > 0) {
-      // const width = this.getColsWidth(this.colCount - this.rightFrozenColCount, this.colCount - 1); // 同getBottomFrozenRowsHeight的原因
       let width = 0;
       for (let col = this.colCount - this.rightFrozenColCount; col <= this.colCount - 1; col++) {
         width += this.getColWidth(col);
@@ -3057,6 +3068,14 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     }
     return 0;
   }
+  getRightFrozenColsOffset(): number {
+    const contentWidth = this.getRightFrozenColsContentWidth();
+    const viewportWidth = this.getRightFrozenColsWidth();
+    return Math.max(0, contentWidth - viewportWidth);
+  }
+  getRightFrozenColsScrollLeft(): number {
+    return this.stateManager.scroll.rightFrozenHorizontalBarPos ?? 0;
+  }
   /**
    * 获取实际绘制范围的宽高，而非可绘制画布大小
    * @param table
diff --git a/packages/vtable/src/core/utils/get-cell-position.ts b/packages/vtable/src/core/utils/get-cell-position.ts
index f55d9c936..7689e4762 100644
--- a/packages/vtable/src/core/utils/get-cell-position.ts
+++ b/packages/vtable/src/core/utils/get-cell-position.ts
@@ -183,12 +183,14 @@ export function getTargetColAtConsiderRightFrozen(
     return { left: 0, col: 0, right: 0, width: 0 };
   }
   absoluteX = absoluteX - _this.tableX;
+  const rightFrozenScrollLeft = _this.getRightFrozenColsScrollLeft?.() ?? 0;
   if (
     isConsider &&
     absoluteX > _this.tableNoFrameWidth - _this.getRightFrozenColsWidth() &&
     absoluteX < _this.tableNoFrameWidth &&
     absoluteX <= _this.getAllColsWidth()
   ) {
+    absoluteX -= rightFrozenScrollLeft;
     for (let i = 0; i < _this.rightFrozenColCount; i++) {
       if (absoluteX > _this.tableNoFrameWidth - _this.getColsWidth(_this.colCount - i - 1, _this.colCount - 1)) {
         return {
diff --git a/packages/vtable/src/event/listener/scroll-bar.ts b/packages/vtable/src/event/listener/scroll-bar.ts
index 84f888e1f..343411946 100644
--- a/packages/vtable/src/event/listener/scroll-bar.ts
+++ b/packages/vtable/src/event/listener/scroll-bar.ts
@@ -11,21 +11,62 @@ export function bindScrollBarListener(eventManager: EventManager) {
   const table = eventManager.table;
   const stateManager = table.stateManager;
   const scenegraph = table.scenegraph;
+  const visible1 = table.theme.scrollStyle?.visible as string;
+  const horizontalVisible = table.theme.scrollStyle?.horizontalVisible ?? visible1;
+  const verticalVisible = table.theme.scrollStyle?.verticalVisible ?? visible1;
 
   // 监听滚动条组件pointover事件
   scenegraph.component.vScrollBar.addEventListener('pointerover', (e: any) => {
-    stateManager.showVerticalScrollBar();
+    if (verticalVisible === 'focus') {
+      stateManager.showVerticalScrollBar();
+    }
   });
   scenegraph.component.hScrollBar.addEventListener('pointerover', (e: any) => {
-    stateManager.showHorizontalScrollBar();
+    if (horizontalVisible === 'focus') {
+      stateManager.showHorizontalScrollBar(false, 'body');
+    }
+  });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointerover', (e: any) => {
+    if (horizontalVisible === 'focus') {
+      stateManager.showHorizontalScrollBar(false, 'frozen');
+    }
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerover', (e: any) => {
+    if (horizontalVisible === 'focus') {
+      stateManager.showHorizontalScrollBar(false, 'rightFrozen');
+    }
   });
   scenegraph.component.vScrollBar.addEventListener('pointerout', (e: any) => {
+    if (verticalVisible !== 'focus') {
+      return;
+    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
     stateManager.hideVerticalScrollBar();
   });
   scenegraph.component.hScrollBar.addEventListener('pointerout', (e: any) => {
+    if (horizontalVisible !== 'focus') {
+      return;
+    }
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      return;
+    }
+    stateManager.hideHorizontalScrollBar();
+  });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointerout', (e: any) => {
+    if (horizontalVisible !== 'focus') {
+      return;
+    }
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      return;
+    }
+    stateManager.hideHorizontalScrollBar();
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerout', (e: any) => {
+    if (horizontalVisible !== 'focus') {
+      return;
+    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
@@ -78,6 +119,14 @@ export function bindScrollBarListener(eventManager: EventManager) {
     scenegraph.table.stateManager.updateCursor();
     e.stopPropagation(); //防止冒泡到stage上 检测到挨着列间隔线判断成可拖拽
   });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointermove', (e: FederatedPointerEvent) => {
+    scenegraph.table.stateManager.updateCursor();
+    e.stopPropagation();
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointermove', (e: FederatedPointerEvent) => {
+    scenegraph.table.stateManager.updateCursor();
+    e.stopPropagation();
+  });
   scenegraph.component.hScrollBar.addEventListener('pointerdown', (e: FederatedPointerEvent) => {
     e.stopPropagation(); //防止冒泡到stage上 检测到挨着列间隔线判断成拖拽状态
     if ((scenegraph.table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE)) {
@@ -86,6 +135,22 @@ export function bindScrollBarListener(eventManager: EventManager) {
       });
     }
   });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointerdown', (e: FederatedPointerEvent) => {
+    e.stopPropagation();
+    if ((scenegraph.table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE)) {
+      scenegraph.table.fireListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE, {
+        event: e.nativeEvent
+      });
+    }
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerdown', (e: FederatedPointerEvent) => {
+    e.stopPropagation();
+    if ((scenegraph.table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE)) {
+      scenegraph.table.fireListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE, {
+        event: e.nativeEvent
+      });
+    }
+  });
   scenegraph.component.hScrollBar.addEventListener('scrollDown', (e: FederatedPointerEvent) => {
     scenegraph.table.eventManager.LastBodyPointerXY = { x: e.x, y: e.y };
     scenegraph.table.eventManager.isDown = true;
@@ -100,6 +165,34 @@ export function bindScrollBarListener(eventManager: EventManager) {
       });
     }
   });
+  scenegraph.component.frozenHScrollBar.addEventListener('scrollDown', (e: FederatedPointerEvent) => {
+    scenegraph.table.eventManager.LastBodyPointerXY = { x: e.x, y: e.y };
+    scenegraph.table.eventManager.isDown = true;
+    if (stateManager.interactionState !== InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.scrolling);
+    }
+    scenegraph.table.stateManager.hideMenu();
+    (scenegraph.table as ListTableAPI).editorManager?.completeEdit();
+    if ((scenegraph.table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE)) {
+      scenegraph.table.fireListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE, {
+        event: e.nativeEvent
+      });
+    }
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('scrollDown', (e: FederatedPointerEvent) => {
+    scenegraph.table.eventManager.LastBodyPointerXY = { x: e.x, y: e.y };
+    scenegraph.table.eventManager.isDown = true;
+    if (stateManager.interactionState !== InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.scrolling);
+    }
+    scenegraph.table.stateManager.hideMenu();
+    (scenegraph.table as ListTableAPI).editorManager?.completeEdit();
+    if ((scenegraph.table as any).hasListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE)) {
+      scenegraph.table.fireListeners(TABLE_EVENT_TYPE.MOUSEDOWN_TABLE, {
+        event: e.nativeEvent
+      });
+    }
+  });
   scenegraph.component.hScrollBar.addEventListener('pointerup', () => {
     stateManager.fastScrolling = false;
     scenegraph.table.eventManager.isDraging = false;
@@ -116,8 +209,42 @@ export function bindScrollBarListener(eventManager: EventManager) {
   scenegraph.component.hScrollBar.addEventListener('scrollUp', (e: FederatedPointerEvent) => {
     scenegraph.table.eventManager.isDraging = false;
   });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointerup', () => {
+    stateManager.fastScrolling = false;
+    scenegraph.table.eventManager.isDraging = false;
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.default);
+    }
+  });
+  scenegraph.component.frozenHScrollBar.addEventListener('pointerupoutside', () => {
+    stateManager.fastScrolling = false;
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.default);
+    }
+  });
+  scenegraph.component.frozenHScrollBar.addEventListener('scrollUp', (e: FederatedPointerEvent) => {
+    scenegraph.table.eventManager.isDraging = false;
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerup', () => {
+    stateManager.fastScrolling = false;
+    scenegraph.table.eventManager.isDraging = false;
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.default);
+    }
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerupoutside', () => {
+    stateManager.fastScrolling = false;
+    if (stateManager.interactionState === InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.default);
+    }
+  });
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('scrollUp', (e: FederatedPointerEvent) => {
+    scenegraph.table.eventManager.isDraging = false;
+  });
   const throttleVerticalWheel = throttle(stateManager.updateVerticalScrollBar, 20);
   const throttleHorizontalWheel = throttle(stateManager.updateHorizontalScrollBar, 20);
+  const throttleFrozenHorizontalWheel = throttle(stateManager.updateFrozenHorizontalScrollBar, 20);
+  const throttleRightFrozenHorizontalWheel = throttle(stateManager.updateRightFrozenHorizontalScrollBar, 20);
 
   // 监听滚动条组件scroll事件
   scenegraph.component.vScrollBar.addEventListener('scrollDrag', (e: any) => {
@@ -158,4 +285,34 @@ export function bindScrollBarListener(eventManager: EventManager) {
     //   stateManager.table.scenegraph.proxy.isSkipProgress = false;
     // }, 10);
   });
+
+  scenegraph.component.frozenHScrollBar.addEventListener('scrollDrag', (e: any) => {
+    if (scenegraph.table.eventManager.isDown) {
+      scenegraph.table.eventManager.isDraging = true;
+    }
+    stateManager.fastScrolling = true;
+    if (stateManager.interactionState !== InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.scrolling);
+    }
+    scenegraph.table.stateManager.hideMenu();
+    (scenegraph.table as ListTableAPI).editorManager?.completeEdit();
+    table.scenegraph.deactivateChart(-1, -1, true);
+    const ratio = e.detail.value[0] / (1 - e.detail.value[1] + e.detail.value[0]);
+    throttleFrozenHorizontalWheel(ratio);
+  });
+
+  scenegraph.component.rightFrozenHScrollBar.addEventListener('scrollDrag', (e: any) => {
+    if (scenegraph.table.eventManager.isDown) {
+      scenegraph.table.eventManager.isDraging = true;
+    }
+    stateManager.fastScrolling = true;
+    if (stateManager.interactionState !== InteractionState.scrolling) {
+      stateManager.updateInteractionState(InteractionState.scrolling);
+    }
+    scenegraph.table.stateManager.hideMenu();
+    (scenegraph.table as ListTableAPI).editorManager?.completeEdit();
+    table.scenegraph.deactivateChart(-1, -1, true);
+    const ratio = e.detail.value[0] / (1 - e.detail.value[1] + e.detail.value[0]);
+    throttleRightFrozenHorizontalWheel(ratio);
+  });
 }
diff --git a/packages/vtable/src/event/listener/table-group.ts b/packages/vtable/src/event/listener/table-group.ts
index 11d5c86b4..f09b5fcfb 100644
--- a/packages/vtable/src/event/listener/table-group.ts
+++ b/packages/vtable/src/event/listener/table-group.ts
@@ -179,6 +179,25 @@ export function bindTableGroupListener(eventManager: EventManager) {
         mergeCellInfo: eventArgsSet.eventArgs?.mergeInfo
       });
     }
+
+    if (
+      (table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.horizontalVisible === 'focus') ||
+      (!table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.visible === 'focus')
+    ) {
+      const relativeX = e.x - table.tableX;
+      const target =
+        table.options.scrollFrozenCols &&
+        table.getFrozenColsOffset?.() > 0 &&
+        relativeX >= 0 &&
+        relativeX < table.getFrozenColsWidth()
+          ? 'frozen'
+          : table.options.scrollRightFrozenCols &&
+            table.getRightFrozenColsOffset?.() > 0 &&
+            relativeX > table.tableNoFrameWidth - table.getRightFrozenColsWidth()
+          ? 'rightFrozen'
+          : 'body';
+      stateManager.showHorizontalScrollBar(false, target);
+    }
   });
 
   table.scenegraph.tableGroup.addEventListener('pointerout', (e: FederatedPointerEvent) => {
@@ -267,7 +286,19 @@ export function bindTableGroupListener(eventManager: EventManager) {
       (table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.horizontalVisible === 'focus') ||
       (!table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.visible === 'focus')
     ) {
-      stateManager.showHorizontalScrollBar();
+      const relativeX = e.x - table.tableX;
+      const target =
+        table.options.scrollFrozenCols &&
+        table.getFrozenColsOffset?.() > 0 &&
+        relativeX >= 0 &&
+        relativeX < table.getFrozenColsWidth()
+          ? 'frozen'
+          : table.options.scrollRightFrozenCols &&
+            table.getRightFrozenColsOffset?.() > 0 &&
+            relativeX > table.tableNoFrameWidth - table.getRightFrozenColsWidth()
+          ? 'rightFrozen'
+          : 'body';
+      stateManager.showHorizontalScrollBar(false, target);
     }
     if (
       (table.theme.scrollStyle.verticalVisible && table.theme.scrollStyle.verticalVisible === 'focus') ||
diff --git a/packages/vtable/src/event/scroll.ts b/packages/vtable/src/event/scroll.ts
index e7c455cfc..3c014e49b 100644
--- a/packages/vtable/src/event/scroll.ts
+++ b/packages/vtable/src/event/scroll.ts
@@ -22,11 +22,21 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       state.updateInteractionState(InteractionState.scrolling);
     }
   }
-  const autoHide = state.table.options.theme?.scrollStyle?.visible === 'scrolling';
+  const visible1 = state.table.theme.scrollStyle?.visible;
+  const horizontalVisible = state.table.theme.scrollStyle?.horizontalVisible ?? visible1;
+  const verticalVisible = state.table.theme.scrollStyle?.verticalVisible ?? visible1;
+  const horizontalAutoHide = horizontalVisible === 'scrolling';
+  const verticalAutoHide = verticalVisible === 'scrolling';
   let usedFrozenHorizontal = false;
+  let usedRightFrozenHorizontal = false;
   if (optimizedDeltaX) {
-    const px = (event as any).x ?? (state.table as any).eventManager?.LastBodyPointerXY?.x;
-    const py = (event as any).y ?? (state.table as any).eventManager?.LastBodyPointerXY?.y;
+    const pxEvent = (event as any).x;
+    const pyEvent = (event as any).y;
+    const pxFallback = (state.table as any).eventManager?.LastBodyPointerXY?.x;
+    const pyFallback = (state.table as any).eventManager?.LastBodyPointerXY?.y;
+    const px = typeof pxEvent === 'number' ? pxEvent : pxFallback;
+    const py = typeof pyEvent === 'number' ? pyEvent : pyFallback;
+    const usedFallback = typeof pxEvent !== 'number' || typeof pyEvent !== 'number';
     const relativeX = typeof px === 'number' ? px - state.table.tableX : NaN;
     const relativeY = typeof py === 'number' ? py - state.table.tableY : NaN;
     const isInTable =
@@ -37,7 +47,11 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       relativeX <= state.table.tableNoFrameWidth &&
       relativeY <= state.table.tableNoFrameHeight;
     const frozenColsScrollable = state.table.options.scrollFrozenCols && state.table.getFrozenColsOffset() > 0;
+    const rightFrozenColsScrollable =
+      state.table.options.scrollRightFrozenCols && state.table.getRightFrozenColsOffset() > 0;
     const isInFrozenViewport = isInTable && relativeX > 0 && relativeX < state.table.getFrozenColsWidth();
+    const isInRightFrozenViewport =
+      isInTable && relativeX > state.table.tableNoFrameWidth - state.table.getRightFrozenColsWidth();
 
     if (frozenColsScrollable && isInFrozenViewport) {
       const maxFrozenScrollLeft = state.table.getFrozenColsOffset();
@@ -48,18 +62,34 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       if (canScrollFrozen) {
         state.setFrozenColsScrollLeft(nextFrozenScrollLeft);
         usedFrozenHorizontal = true;
+        state.showHorizontalScrollBar(horizontalAutoHide, 'frozen');
       } else {
         state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
-        state.showHorizontalScrollBar(autoHide);
+        state.showHorizontalScrollBar(horizontalAutoHide, 'body');
+      }
+    } else if (rightFrozenColsScrollable && isInRightFrozenViewport) {
+      const maxRightFrozenScrollLeft = state.table.getRightFrozenColsOffset();
+      const rightFrozenDelta = -optimizedDeltaX;
+      const nextRightFrozenScrollLeft = state.scroll.rightFrozenHorizontalBarPos + rightFrozenDelta;
+      const canScrollRightFrozen =
+        (rightFrozenDelta < 0 && state.scroll.rightFrozenHorizontalBarPos > 0) ||
+        (rightFrozenDelta > 0 && state.scroll.rightFrozenHorizontalBarPos < maxRightFrozenScrollLeft);
+      if (canScrollRightFrozen) {
+        state.setRightFrozenColsScrollLeft(nextRightFrozenScrollLeft);
+        usedRightFrozenHorizontal = true;
+        state.showHorizontalScrollBar(horizontalAutoHide, 'rightFrozen');
+      } else {
+        state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
+        state.showHorizontalScrollBar(horizontalAutoHide, 'body');
       }
     } else {
       state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
-      state.showHorizontalScrollBar(autoHide);
+      state.showHorizontalScrollBar(horizontalAutoHide, 'body');
     }
   }
   if (optimizedDeltaY) {
     state.setScrollTop(state.scroll.verticalBarPos + optimizedDeltaY, event);
-    state.showVerticalScrollBar(autoHide);
+    state.showVerticalScrollBar(verticalAutoHide);
   }
   isWheelEvent && state.resetInteractionState(state.interactionStateBeforeScroll);
   if (
@@ -69,7 +99,11 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       (Math.abs(deltaY) >= Math.abs(deltaX) && deltaY !== 0 && isVerticalScrollable(deltaY, state)) ||
       (Math.abs(deltaY) <= Math.abs(deltaX) &&
         deltaX !== 0 &&
-        (usedFrozenHorizontal ? state.table.getFrozenColsOffset() > 0 : isHorizontalScrollable(deltaX, state))))
+        (usedFrozenHorizontal
+          ? state.table.getFrozenColsOffset() > 0
+          : usedRightFrozenHorizontal
+          ? state.table.getRightFrozenColsOffset() > 0
+          : isHorizontalScrollable(deltaX, state))))
   ) {
     event.nativeEvent.preventDefault();
   }
@@ -107,7 +141,8 @@ export function isVerticalScrollable(deltaY: number, state: StateManager) {
 
 export function isHorizontalScrollable(deltaX: number, state: StateManager) {
   const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
+  const rightFrozenOffset = state.table.getRightFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset - rightFrozenOffset;
   if (totalWidth === 0) {
     return false;
   }
@@ -124,7 +159,8 @@ export function isVerticalExistScrollBar(state: StateManager) {
 
 export function isHorizontalExistScrollBar(state: StateManager) {
   const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
+  const rightFrozenOffset = state.table.getRightFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset - rightFrozenOffset;
   if (totalWidth <= 0) {
     return false;
   }
@@ -146,7 +182,8 @@ function isScrollToBottom(deltaY: number, state: StateManager) {
 
 function isScrollToLeft(deltaX: number, state: StateManager) {
   const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
+  const rightFrozenOffset = state.table.getRightFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset - rightFrozenOffset;
   return totalWidth !== 0 && deltaX <= 0 && state.scroll.horizontalBarPos < 1;
 }
 
@@ -155,7 +192,8 @@ function isScrollToRight(deltaX: number, state: StateManager) {
   const sizeTolerance = state.table.options.customConfig?._disableColumnAndRowSizeRound ? 1 : 0;
 
   const frozenOffset = state.table.getFrozenColsOffset?.() ?? 0;
-  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset;
+  const rightFrozenOffset = state.table.getRightFrozenColsOffset?.() ?? 0;
+  const totalWidth = state.table.getAllColsWidth() - state.table.scenegraph.width - frozenOffset - rightFrozenOffset;
   return totalWidth !== 0 && deltaX >= 0 && Math.abs(state.scroll.horizontalBarPos - totalWidth) < 1 + sizeTolerance;
 }
 
diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts
index d522489a7..37ea3538d 100644
--- a/packages/vtable/src/scenegraph/component/table-component.ts
+++ b/packages/vtable/src/scenegraph/component/table-component.ts
@@ -16,6 +16,7 @@ import { isValid } from '@visactor/vutils';
  */
 export class TableComponent {
   table: BaseTableAPI;
+  _horizontalScrollBarTarget?: 'body' | 'frozen' | 'rightFrozen' | 'all';
 
   border: IRect; // 表格外边框
   // selectBorder: IRect; // 表格选择区域边框
@@ -28,6 +29,8 @@ export class TableComponent {
   menu: MenuHandler; // 表格菜单
   vScrollBar: ScrollBar; // 表格横向滚动条
   hScrollBar: ScrollBar; // 表格纵向滚动条
+  frozenHScrollBar: ScrollBar; // 左冻结区横向滚动条
+  rightFrozenHScrollBar: ScrollBar; // 右冻结区横向滚动条
   frozenShadowLine: IRect; // 表格冻结列右侧阴影块
   rightFrozenShadowLine: IRect; // 表格右侧冻结列左侧阴影块
   drillIcon: DrillIcon; // drill icon
@@ -254,9 +257,13 @@ export class TableComponent {
     const hoverOn = this.table.theme.scrollStyle.hoverOn;
     if (hoverOn && !this.table.theme.scrollStyle.barToSide) {
       componentGroup.addChild(this.hScrollBar);
+      componentGroup.addChild(this.frozenHScrollBar);
+      componentGroup.addChild(this.rightFrozenHScrollBar);
       componentGroup.addChild(this.vScrollBar);
     } else {
       componentGroup.stage.defaultLayer.addChild(this.hScrollBar);
+      componentGroup.stage.defaultLayer.addChild(this.frozenHScrollBar);
+      componentGroup.stage.defaultLayer.addChild(this.rightFrozenHScrollBar);
       componentGroup.stage.defaultLayer.addChild(this.vScrollBar);
 
       // // add scroll bar before border, avoid scroll hide by border globalCompositeOperation
@@ -314,6 +321,40 @@ export class TableComponent {
     (this.hScrollBar as any).render();
     this.hScrollBar.hideAll();
 
+    this.frozenHScrollBar = new ScrollBar({
+      direction: 'horizontal',
+      x: -this.table.tableNoFrameWidth * 2,
+      y: -this.table.tableNoFrameHeight * 2,
+      width: this.table.tableNoFrameWidth,
+      height: width,
+      padding: horizontalPadding,
+      railStyle: {
+        fill: scrollRailColor
+      },
+      sliderStyle,
+      range: [0, 0.1],
+      visible: false
+    });
+    (this.frozenHScrollBar as any).render();
+    this.frozenHScrollBar.hideAll();
+
+    this.rightFrozenHScrollBar = new ScrollBar({
+      direction: 'horizontal',
+      x: -this.table.tableNoFrameWidth * 2,
+      y: -this.table.tableNoFrameHeight * 2,
+      width: this.table.tableNoFrameWidth,
+      height: width,
+      padding: horizontalPadding,
+      railStyle: {
+        fill: scrollRailColor
+      },
+      sliderStyle,
+      range: [0, 0.1],
+      visible: false
+    });
+    (this.rightFrozenHScrollBar as any).render();
+    this.rightFrozenHScrollBar.hideAll();
+
     this.vScrollBar = new ScrollBar({
       direction: 'vertical',
       x: -this.table.tableNoFrameWidth * 2,
@@ -339,6 +380,8 @@ export class TableComponent {
   updateScrollBar() {
     const oldHorizontalBarPos = this.table.stateManager.scroll.horizontalBarPos;
     const oldVerticalBarPos = this.table.stateManager.scroll.verticalBarPos;
+    const oldFrozenHorizontalBarPos = this.table.stateManager.scroll.frozenHorizontalBarPos;
+    const oldRightFrozenHorizontalBarPos = this.table.stateManager.scroll.rightFrozenHorizontalBarPos;
 
     const theme = this.table.theme;
     const width = theme.scrollStyle?.width as number;
@@ -358,6 +401,7 @@ export class TableComponent {
     const frozenColsContentWidth = this.table.getFrozenColsContentWidth?.() ?? frozenColsWidth;
     const bottomFrozenRowsHeight = this.table.getBottomFrozenRowsHeight();
     const rightFrozenColsWidth = this.table.getRightFrozenColsWidth();
+    const rightFrozenColsContentWidth = this.table.getRightFrozenColsContentWidth?.() ?? rightFrozenColsWidth;
     const hoverOn = this.table.theme.scrollStyle.hoverOn;
     // _disableColumnAndRowSizeRound环境中，可能出现
     // getAllColsWidth/getAllRowsHeight(A) + getAllColsWidth/getAllRowsHeight(B) < getAllColsWidth/getAllRowsHeight(A+B)
@@ -367,7 +411,9 @@ export class TableComponent {
 
     if (totalWidth > tableWidth + sizeTolerance) {
       const y = Math.min(tableHeight, totalHeight);
-      const rangeEnd = Math.max(0.05, (tableWidth - frozenColsWidth) / (totalWidth - frozenColsContentWidth));
+      const bodyViewportWidth = tableWidth - frozenColsWidth - rightFrozenColsWidth;
+      const bodyContentWidth = totalWidth - frozenColsContentWidth - rightFrozenColsContentWidth;
+      const rangeEnd = bodyContentWidth > 0 ? Math.max(0.05, bodyViewportWidth / bodyContentWidth) : 1;
 
       let attrY = 0;
       if (this.table.theme.scrollStyle.barToSide) {
@@ -409,6 +455,63 @@ export class TableComponent {
       if (horizontalVisible === 'always') {
         this.hScrollBar.showAll();
       }
+
+      const frozenScrollable = this.table.options.scrollFrozenCols && this.table.getFrozenColsOffset() > 0;
+      if (!ignoreFrozenCols && frozenScrollable) {
+        const frozenRangeEnd = Math.max(0.05, frozenColsWidth / frozenColsContentWidth);
+        const x = !hoverOn ? this.table.scenegraph.tableGroup.attribute.x : 0;
+        this.frozenHScrollBar.setAttributes({
+          x,
+          y: attrY,
+          width: frozenColsWidth,
+          range: [0, frozenRangeEnd],
+          visible: horizontalVisible === 'always'
+        });
+        const bounds = this.frozenHScrollBar.AABBBounds && this.frozenHScrollBar.globalAABBBounds;
+        (this.frozenHScrollBar as any)._viewPosition = {
+          x: bounds.x1,
+          y: bounds.y1
+        };
+        if (horizontalVisible === 'always') {
+          this.frozenHScrollBar.showAll();
+        }
+      } else {
+        this.frozenHScrollBar.setAttributes({
+          x: -this.table.tableNoFrameWidth * 2,
+          y: -this.table.tableNoFrameHeight * 2,
+          width: 0,
+          visible: false
+        });
+      }
+
+      const rightFrozenScrollable =
+        this.table.options.scrollRightFrozenCols && this.table.getRightFrozenColsOffset() > 0;
+      if (!ignoreFrozenCols && rightFrozenScrollable) {
+        const rightFrozenRangeEnd = Math.max(0.05, rightFrozenColsWidth / rightFrozenColsContentWidth);
+        const x = tableWidth - rightFrozenColsWidth + (!hoverOn ? this.table.scenegraph.tableGroup.attribute.x : 0);
+        this.rightFrozenHScrollBar.setAttributes({
+          x,
+          y: attrY,
+          width: rightFrozenColsWidth,
+          range: [0, rightFrozenRangeEnd],
+          visible: horizontalVisible === 'always'
+        });
+        const bounds = this.rightFrozenHScrollBar.AABBBounds && this.rightFrozenHScrollBar.globalAABBBounds;
+        (this.rightFrozenHScrollBar as any)._viewPosition = {
+          x: bounds.x1,
+          y: bounds.y1
+        };
+        if (horizontalVisible === 'always') {
+          this.rightFrozenHScrollBar.showAll();
+        }
+      } else {
+        this.rightFrozenHScrollBar.setAttributes({
+          x: -this.table.tableNoFrameWidth * 2,
+          y: -this.table.tableNoFrameHeight * 2,
+          width: 0,
+          visible: false
+        });
+      }
     } else {
       this.hScrollBar.setAttributes({
         x: -this.table.tableNoFrameWidth * 2,
@@ -416,6 +519,18 @@ export class TableComponent {
         width: 0,
         visible: false
       });
+      this.frozenHScrollBar.setAttributes({
+        x: -this.table.tableNoFrameWidth * 2,
+        y: -this.table.tableNoFrameHeight * 2,
+        width: 0,
+        visible: false
+      });
+      this.rightFrozenHScrollBar.setAttributes({
+        x: -this.table.tableNoFrameWidth * 2,
+        y: -this.table.tableNoFrameHeight * 2,
+        width: 0,
+        visible: false
+      });
     }
 
     if (totalHeight > tableHeight + sizeTolerance) {
@@ -459,6 +574,8 @@ export class TableComponent {
     }
 
     this.table.stateManager.setScrollLeft(oldHorizontalBarPos);
+    this.table.stateManager.setFrozenColsScrollLeft(oldFrozenHorizontalBarPos, false);
+    this.table.stateManager.setRightFrozenColsScrollLeft(oldRightFrozenHorizontalBarPos, false);
     this.table.stateManager.setScrollTop(oldVerticalBarPos);
   }
 
@@ -693,7 +810,11 @@ export class TableComponent {
    * @return {*}
    */
   setRightFrozenColumnShadow(col: number) {
-    const colX = getColX(col, this.table, true);
+    const shouldFixViewport =
+      this.table.options.scrollRightFrozenCols && (this.table.getRightFrozenColsOffset?.() ?? 0) > 0;
+    const colX = shouldFixViewport
+      ? this.table.tableNoFrameWidth - this.table.getRightFrozenColsWidth()
+      : getColX(col, this.table, true);
     if (col >= this.table.colCount || this.table.theme.frozenColumnLine?.shadow?.visible !== 'always') {
       this.rightFrozenShadowLine.setAttributes({
         visible: false,
@@ -756,16 +877,39 @@ export class TableComponent {
     }
     this.hScrollBar.setAttribute('visible', false);
     this.hScrollBar.hideAll();
+    this.frozenHScrollBar.setAttribute('visible', false);
+    this.frozenHScrollBar.hideAll();
+    this.rightFrozenHScrollBar.setAttribute('visible', false);
+    this.rightFrozenHScrollBar.hideAll();
+    this._horizontalScrollBarTarget = undefined;
     this.table.scenegraph.updateNextFrame();
   }
-  showHorizontalScrollBar() {
+  showHorizontalScrollBar(target: 'body' | 'frozen' | 'rightFrozen' | 'all' = 'all') {
     const visible1 = this.table.theme.scrollStyle.visible;
     const horizontalVisible = this.table.theme.scrollStyle.horizontalVisible ?? visible1;
     if (horizontalVisible !== 'focus' && horizontalVisible !== 'scrolling') {
       return;
     }
-    this.hScrollBar.setAttribute('visible', true);
-    this.hScrollBar.showAll();
+    if (this._horizontalScrollBarTarget === target) {
+      return;
+    }
+    this._horizontalScrollBarTarget = target;
+
+    const showBody = target === 'all' || target === 'body';
+    const showFrozen = target === 'all' || target === 'frozen';
+    const showRightFrozen = target === 'all' || target === 'rightFrozen';
+
+    const bodyVisible = showBody && this.hScrollBar.attribute.width > 0;
+    this.hScrollBar.setAttribute('visible', bodyVisible);
+    bodyVisible ? this.hScrollBar.showAll() : this.hScrollBar.hideAll();
+
+    const frozenVisible = showFrozen && this.frozenHScrollBar.attribute.width > 0;
+    this.frozenHScrollBar.setAttribute('visible', frozenVisible);
+    frozenVisible ? this.frozenHScrollBar.showAll() : this.frozenHScrollBar.hideAll();
+
+    const rightFrozenVisible = showRightFrozen && this.rightFrozenHScrollBar.attribute.width > 0;
+    this.rightFrozenHScrollBar.setAttribute('visible', rightFrozenVisible);
+    rightFrozenVisible ? this.rightFrozenHScrollBar.showAll() : this.rightFrozenHScrollBar.hideAll();
     this.table.scenegraph.updateNextFrame();
   }
   updateVerticalScrollBarPos(topRatio: number) {
@@ -790,6 +934,28 @@ export class TableComponent {
       y: bounds.y1
     };
   }
+  updateFrozenHorizontalScrollBarPos(leftRatio: number) {
+    const range = this.frozenHScrollBar.attribute.range;
+    const size = range[1] - range[0];
+    const range0 = leftRatio * (1 - size);
+    this.frozenHScrollBar.setAttribute('range', [range0, range0 + size]);
+    const bounds = this.frozenHScrollBar.AABBBounds && this.frozenHScrollBar.globalAABBBounds;
+    (this.frozenHScrollBar as any)._viewPosition = {
+      x: bounds.x1,
+      y: bounds.y1
+    };
+  }
+  updateRightFrozenHorizontalScrollBarPos(leftRatio: number) {
+    const range = this.rightFrozenHScrollBar.attribute.range;
+    const size = range[1] - range[0];
+    const range0 = leftRatio * (1 - size);
+    this.rightFrozenHScrollBar.setAttribute('range', [range0, range0 + size]);
+    const bounds = this.rightFrozenHScrollBar.AABBBounds && this.rightFrozenHScrollBar.globalAABBBounds;
+    (this.rightFrozenHScrollBar as any)._viewPosition = {
+      x: bounds.x1,
+      y: bounds.y1
+    };
+  }
 
   updateStyle() {
     const theme = this.table.theme;
@@ -830,6 +996,22 @@ export class TableComponent {
       },
       sliderStyle
     });
+    this.frozenHScrollBar.setAttributes({
+      height: width,
+      padding: horizontalPadding,
+      railStyle: {
+        fill: scrollRailColor
+      },
+      sliderStyle
+    });
+    this.rightFrozenHScrollBar.setAttributes({
+      height: width,
+      padding: horizontalPadding,
+      railStyle: {
+        fill: scrollRailColor
+      },
+      sliderStyle
+    });
 
     // columnResizeLine & columnResizeBgLine
     const columnResizeColor = theme.columnResize?.lineColor;
diff --git a/packages/vtable/src/scenegraph/component/util.ts b/packages/vtable/src/scenegraph/component/util.ts
index 3ff8b38ed..b5dbf3013 100644
--- a/packages/vtable/src/scenegraph/component/util.ts
+++ b/packages/vtable/src/scenegraph/component/util.ts
@@ -2,7 +2,11 @@ import type { BaseTableAPI } from '../../ts-types/base-table';
 
 export function getColX(col: number, table: BaseTableAPI, isRightFrozen?: boolean) {
   if (isRightFrozen) {
-    return Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) - table.getColsWidth(col, table.colCount - 1);
+    return (
+      Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) -
+      table.getColsWidth(col, table.colCount - 1) +
+      (table.getRightFrozenColsScrollLeft?.() ?? 0)
+    );
   }
   const frozenOffset = table.getFrozenColsOffset?.() ?? 0;
   const frozenScrollLeft = table.getFrozenColsScrollLeft?.() ?? 0;
diff --git a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
index afb9fc251..d044569fd 100644
--- a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
+++ b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts
@@ -838,7 +838,9 @@ export class SceneProxy {
         const deltaX =
           colGroup.attribute.x +
           colGroup.attribute.width -
-          (this.table.getAllColsWidth() - this.table.getFrozenColsWidth() - this.table.getRightFrozenColsWidth());
+          (this.table.getAllColsWidth() -
+            (this.table.getFrozenColsContentWidth?.() ?? this.table.getFrozenColsWidth()) -
+            (this.table.getRightFrozenColsContentWidth?.() ?? this.table.getRightFrozenColsWidth()));
         this.deltaX = -deltaX;
       }
     } else if (isValid(screenLeftX) && isValid(screenLeftCol)) {
diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts
index efd8ee8ea..3f99b2e38 100644
--- a/packages/vtable/src/scenegraph/scenegraph.ts
+++ b/packages/vtable/src/scenegraph/scenegraph.ts
@@ -1961,6 +1961,9 @@ export class Scenegraph {
   updateContainerAttrWidthAndX() {
     const frozenStartX = -(this.table.getFrozenColsScrollLeft?.() ?? 0);
     const frozenViewportWidth = this.table.getFrozenColsWidth();
+    const rightFrozenStartX =
+      -this.table.getRightFrozenColsOffset() + (this.table.getRightFrozenColsScrollLeft?.() ?? 0);
+    const rightFrozenContentWidth = this.table.getRightFrozenColsContentWidth();
     // 更新各列x&col
     updateContainerChildrenX(this.cornerHeaderGroup, frozenStartX);
     updateContainerChildrenX(this.rowHeaderGroup, frozenStartX);
@@ -1982,10 +1985,9 @@ export class Scenegraph {
               : 0
           )
         : 0;
-    const rightX = updateContainerChildrenX(
-      this.rightFrozenGroup.childrenCount > 0 ? this.rightFrozenGroup : this.rightTopCornerGroup,
-      0
-    );
+    if (this.rightFrozenGroup.childrenCount > 0) {
+      updateContainerChildrenX(this.rightFrozenGroup, rightFrozenStartX);
+    }
 
     this.bottomFrozenGroup.hasChildNodes() &&
       this.bottomFrozenGroup.firstChild &&
@@ -1996,8 +1998,8 @@ export class Scenegraph {
           : 0
       );
     updateContainerChildrenX(this.leftBottomCornerGroup, frozenStartX);
-    updateContainerChildrenX(this.rightTopCornerGroup, 0);
-    updateContainerChildrenX(this.rightBottomCornerGroup, 0);
+    updateContainerChildrenX(this.rightTopCornerGroup, rightFrozenStartX);
+    updateContainerChildrenX(this.rightBottomCornerGroup, rightFrozenStartX);
 
     // 更新容器
     this.cornerHeaderGroup.setDeltaWidth(frozenViewportWidth - this.cornerHeaderGroup.attribute.width);
@@ -2007,9 +2009,9 @@ export class Scenegraph {
     // this.rightFrozenGroup.setDeltaWidth(colHeaderX - this.table.getRightFrozenColsWidth());
     this.rowHeaderGroup.setDeltaWidth(frozenViewportWidth - this.rowHeaderGroup.attribute.width);
     this.bottomFrozenGroup.setDeltaWidth(colHeaderX - this.bottomFrozenGroup.attribute.width);
-    this.rightFrozenGroup.setDeltaWidth(rightX - this.rightFrozenGroup.attribute.width);
-    this.rightTopCornerGroup.setDeltaWidth(rightX - this.rightTopCornerGroup.attribute.width);
-    this.rightBottomCornerGroup.setDeltaWidth(rightX - this.rightBottomCornerGroup.attribute.width);
+    this.rightFrozenGroup.setDeltaWidth(rightFrozenContentWidth - this.rightFrozenGroup.attribute.width);
+    this.rightTopCornerGroup.setDeltaWidth(rightFrozenContentWidth - this.rightTopCornerGroup.attribute.width);
+    this.rightBottomCornerGroup.setDeltaWidth(rightFrozenContentWidth - this.rightBottomCornerGroup.attribute.width);
     this.bodyGroup.setDeltaWidth(bodyX - this.bodyGroup.attribute.width);
     this.colHeaderGroup.setAttribute('x', this.cornerHeaderGroup.attribute.width);
     this.bottomFrozenGroup.setAttribute('x', this.table.getFrozenColsWidth());
@@ -2024,6 +2026,14 @@ export class Scenegraph {
     this.updateNextFrame();
   }
 
+  setRightFrozenColsScrollLeft(left: number) {
+    const rightStartX = -this.table.getRightFrozenColsOffset() + left;
+    updateContainerChildrenX(this.rightFrozenGroup, rightStartX);
+    updateContainerChildrenX(this.rightTopCornerGroup, rightStartX);
+    updateContainerChildrenX(this.rightBottomCornerGroup, rightStartX);
+    this.updateNextFrame();
+  }
+
   updateContainerAttrHeightAndY() {
     for (let i = 0; i < this.cornerHeaderGroup.children.length; i++) {
       updateContainerChildrenY(this.cornerHeaderGroup.children[i] as Group, 0);
diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts
index 0112a3d23..3319b4a05 100644
--- a/packages/vtable/src/state/state.ts
+++ b/packages/vtable/src/state/state.ts
@@ -180,6 +180,7 @@ export class StateManager {
     horizontalBarPos: number;
     verticalBarPos: number;
     frozenHorizontalBarPos: number;
+    rightFrozenHorizontalBarPos: number;
   };
   tablePosition: {
     absoluteX: number;
@@ -230,6 +231,8 @@ export class StateManager {
 
     this.updateVerticalScrollBar = this.updateVerticalScrollBar.bind(this);
     this.updateHorizontalScrollBar = this.updateHorizontalScrollBar.bind(this);
+    this.updateFrozenHorizontalScrollBar = this.updateFrozenHorizontalScrollBar.bind(this);
+    this.updateRightFrozenHorizontalScrollBar = this.updateRightFrozenHorizontalScrollBar.bind(this);
   }
 
   initState() {
@@ -400,7 +403,8 @@ export class StateManager {
     this.scroll = {
       horizontalBarPos: 0,
       verticalBarPos: 0,
-      frozenHorizontalBarPos: 0
+      frozenHorizontalBarPos: 0,
+      rightFrozenHorizontalBarPos: 0
     };
     this.tablePosition = {
       absoluteX: 0,
@@ -1006,6 +1010,11 @@ export class StateManager {
       } else {
         this.setFrozenColsScrollLeft(this.scroll.frozenHorizontalBarPos, false);
       }
+      if (!this.table.options.scrollRightFrozenCols || this.table.getRightFrozenColsOffset() === 0) {
+        this.setRightFrozenColsScrollLeft(0, false);
+      } else {
+        this.setRightFrozenColsScrollLeft(this.scroll.rightFrozenHorizontalBarPos, false);
+      }
     } else {
       this.clearFrozenObserver();
     }
@@ -1022,9 +1031,45 @@ export class StateManager {
       return;
     }
     this.scroll.frozenHorizontalBarPos = left;
+    const ratio = maxScrollLeft ? left / maxScrollLeft : 0;
+    this.table.scenegraph.component.updateFrozenHorizontalScrollBarPos(ratio);
     triggerRender && this.table.scenegraph.setFrozenColsScrollLeft(left);
   }
 
+  setRightFrozenColsScrollLeft(left: number, triggerRender: boolean = true) {
+    if (!this.table || !this.table.scenegraph) {
+      return;
+    }
+    const maxScrollLeft = this.table.getRightFrozenColsOffset();
+    left = Math.max(0, Math.min(left, maxScrollLeft));
+    left = Math.ceil(left);
+    if (this.scroll.rightFrozenHorizontalBarPos === left) {
+      return;
+    }
+    this.scroll.rightFrozenHorizontalBarPos = left;
+    const ratio = maxScrollLeft ? 1 - left / maxScrollLeft : 1;
+    this.table.scenegraph.component.updateRightFrozenHorizontalScrollBarPos(ratio);
+    triggerRender && this.table.scenegraph.setRightFrozenColsScrollLeft(left);
+  }
+
+  updateFrozenHorizontalScrollBar(xRatio: number) {
+    const maxScrollLeft = this.table.getFrozenColsOffset?.() ?? 0;
+    let left = Math.ceil(xRatio * maxScrollLeft);
+    if (!isValid(left) || isNaN(left)) {
+      left = 0;
+    }
+    this.setFrozenColsScrollLeft(left, true);
+  }
+
+  updateRightFrozenHorizontalScrollBar(xRatio: number) {
+    const maxScrollLeft = this.table.getRightFrozenColsOffset?.() ?? 0;
+    let left = Math.ceil((1 - xRatio) * maxScrollLeft);
+    if (!isValid(left) || isNaN(left)) {
+      left = 0;
+    }
+    this.setRightFrozenColsScrollLeft(left, true);
+  }
+
   clearFrozenObserver() {
     // 清理观察器
     if (this._frozenObserver) {
@@ -1151,7 +1196,8 @@ export class StateManager {
     const totalWidth = this.table.getAllColsWidth();
     const oldHorizontalBarPos = this.scroll.horizontalBarPos;
     const frozenOffset = this.table.getFrozenColsOffset?.() ?? 0;
-    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset);
+    const rightFrozenOffset = this.table.getRightFrozenColsOffset?.() ?? 0;
+    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset - rightFrozenOffset);
 
     let horizontalBarPos = Math.ceil(xRatio * scrollRange);
     if (!isValid(horizontalBarPos) || isNaN(horizontalBarPos)) {
@@ -1302,7 +1348,8 @@ export class StateManager {
     // 矫正left值范围
     const totalWidth = this.table.getAllColsWidth();
     const frozenOffset = this.table.getFrozenColsOffset?.() ?? 0;
-    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset);
+    const rightFrozenOffset = this.table.getRightFrozenColsOffset?.() ?? 0;
+    const scrollRange = Math.max(0, totalWidth - this.table.scenegraph.width - frozenOffset - rightFrozenOffset);
 
     // _disableColumnAndRowSizeRound环境中，可能出现
     // getAllColsWidth/getAllRowsHeight(A) + getAllColsWidth/getAllRowsHeight(B) < getAllColsWidth/getAllRowsHeight(A+B)
@@ -1396,8 +1443,8 @@ export class StateManager {
   hideHorizontalScrollBar() {
     this.table.scenegraph.component.hideHorizontalScrollBar();
   }
-  showHorizontalScrollBar(autoHide?: boolean) {
-    this.table.scenegraph.component.showHorizontalScrollBar();
+  showHorizontalScrollBar(autoHide?: boolean, target: 'body' | 'frozen' | 'rightFrozen' | 'all' = 'all') {
+    this.table.scenegraph.component.showHorizontalScrollBar(target);
     this.table.scenegraph?.component.showFrozenColumnShadow();
     if (autoHide) {
       // 滚轮触发滚动条显示后，异步隐藏
diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts
index eb1a476da..794456d2c 100644
--- a/packages/vtable/src/ts-types/base-table.ts
+++ b/packages/vtable/src/ts-types/base-table.ts
@@ -317,9 +317,11 @@ export interface BaseTableConstructorOptions {
   bottomFrozenRowCount?: number;
   /** 最大冻结宽度，固定值 or 百分比。默认为'80%' */
   maxFrozenWidth?: number | string;
+  maxRightFrozenWidth?: number | string;
   /** 超过最大冻结宽度后是否全部解冻，默认true */
   unfreezeAllOnExceedsMaxWidth?: boolean;
   scrollFrozenCols?: boolean;
+  scrollRightFrozenCols?: boolean;
 
   // /** 待实现 TODO */
   // frozenRowCount?: number;
@@ -857,6 +859,9 @@ export interface BaseTableAPI {
   getFrozenColsScrollLeft: () => number;
   getBottomFrozenRowsHeight: () => number;
   getRightFrozenColsWidth: () => number;
+  getRightFrozenColsContentWidth: () => number;
+  getRightFrozenColsOffset: () => number;
+  getRightFrozenColsScrollLeft: () => number;
   selectCell: (
     col: number,
     row: number,

From 7c3379caf2ce2cf37ff8478251fef9e00063fd7e Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Mon, 23 Mar 2026 17:00:38 +0800
Subject: [PATCH 07/16] fix: scrolling mode can drag scrollbar

---
 packages/vtable/src/event/listener/scroll-bar.ts | 8 ++++----
 packages/vtable/src/state/state.ts               | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/vtable/src/event/listener/scroll-bar.ts b/packages/vtable/src/event/listener/scroll-bar.ts
index 343411946..df83208ec 100644
--- a/packages/vtable/src/event/listener/scroll-bar.ts
+++ b/packages/vtable/src/event/listener/scroll-bar.ts
@@ -17,22 +17,22 @@ export function bindScrollBarListener(eventManager: EventManager) {
 
   // 监听滚动条组件pointover事件
   scenegraph.component.vScrollBar.addEventListener('pointerover', (e: any) => {
-    if (verticalVisible === 'focus') {
+    if (verticalVisible === 'focus' || verticalVisible === 'scrolling') {
       stateManager.showVerticalScrollBar();
     }
   });
   scenegraph.component.hScrollBar.addEventListener('pointerover', (e: any) => {
-    if (horizontalVisible === 'focus') {
+    if (horizontalVisible === 'focus' || horizontalVisible === 'scrolling') {
       stateManager.showHorizontalScrollBar(false, 'body');
     }
   });
   scenegraph.component.frozenHScrollBar.addEventListener('pointerover', (e: any) => {
-    if (horizontalVisible === 'focus') {
+    if (horizontalVisible === 'focus' || horizontalVisible === 'scrolling') {
       stateManager.showHorizontalScrollBar(false, 'frozen');
     }
   });
   scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerover', (e: any) => {
-    if (horizontalVisible === 'focus') {
+    if (horizontalVisible === 'focus' || horizontalVisible === 'scrolling') {
       stateManager.showHorizontalScrollBar(false, 'rightFrozen');
     }
   });
diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts
index 3319b4a05..30a6cf88f 100644
--- a/packages/vtable/src/state/state.ts
+++ b/packages/vtable/src/state/state.ts
@@ -1432,9 +1432,9 @@ export class StateManager {
   }
   showVerticalScrollBar(autoHide?: boolean) {
     this.table.scenegraph.component.showVerticalScrollBar();
+    clearTimeout(this._clearVerticalScrollBar);
     if (autoHide) {
       // 滚轮触发滚动条显示后，异步隐藏
-      clearTimeout(this._clearVerticalScrollBar);
       this._clearVerticalScrollBar = setTimeout(() => {
         this.table.scenegraph?.component.hideVerticalScrollBar();
       }, 1000);
@@ -1446,9 +1446,9 @@ export class StateManager {
   showHorizontalScrollBar(autoHide?: boolean, target: 'body' | 'frozen' | 'rightFrozen' | 'all' = 'all') {
     this.table.scenegraph.component.showHorizontalScrollBar(target);
     this.table.scenegraph?.component.showFrozenColumnShadow();
+    clearTimeout(this._clearHorizontalScrollBar);
     if (autoHide) {
       // 滚轮触发滚动条显示后，异步隐藏
-      clearTimeout(this._clearHorizontalScrollBar);
       this._clearHorizontalScrollBar = setTimeout(() => {
         this.table.scenegraph?.component.hideFrozenColumnShadow();
         this.table.scenegraph?.component.hideHorizontalScrollBar();

From 5d3400f6abf3aeb2d966ae7183c760ad3eee613e Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Mon, 23 Mar 2026 17:17:10 +0800
Subject: [PATCH 08/16] fix: scrolling mode can hide autoly

---
 .../vtable/src/event/listener/scroll-bar.ts   | 36 ++++++++++---------
 1 file changed, 20 insertions(+), 16 deletions(-)

diff --git a/packages/vtable/src/event/listener/scroll-bar.ts b/packages/vtable/src/event/listener/scroll-bar.ts
index df83208ec..307411a3d 100644
--- a/packages/vtable/src/event/listener/scroll-bar.ts
+++ b/packages/vtable/src/event/listener/scroll-bar.ts
@@ -37,40 +37,44 @@ export function bindScrollBarListener(eventManager: EventManager) {
     }
   });
   scenegraph.component.vScrollBar.addEventListener('pointerout', (e: any) => {
-    if (verticalVisible !== 'focus') {
-      return;
-    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
-    stateManager.hideVerticalScrollBar();
+    if (verticalVisible === 'focus') {
+      stateManager.hideVerticalScrollBar();
+    } else if (verticalVisible === 'scrolling') {
+      stateManager.showVerticalScrollBar(true);
+    }
   });
   scenegraph.component.hScrollBar.addEventListener('pointerout', (e: any) => {
-    if (horizontalVisible !== 'focus') {
-      return;
-    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
-    stateManager.hideHorizontalScrollBar();
+    if (horizontalVisible === 'focus') {
+      stateManager.hideHorizontalScrollBar();
+    } else if (horizontalVisible === 'scrolling') {
+      stateManager.showHorizontalScrollBar(true, 'body');
+    }
   });
   scenegraph.component.frozenHScrollBar.addEventListener('pointerout', (e: any) => {
-    if (horizontalVisible !== 'focus') {
-      return;
-    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
-    stateManager.hideHorizontalScrollBar();
+    if (horizontalVisible === 'focus') {
+      stateManager.hideHorizontalScrollBar();
+    } else if (horizontalVisible === 'scrolling') {
+      stateManager.showHorizontalScrollBar(true, 'frozen');
+    }
   });
   scenegraph.component.rightFrozenHScrollBar.addEventListener('pointerout', (e: any) => {
-    if (horizontalVisible !== 'focus') {
-      return;
-    }
     if (stateManager.interactionState === InteractionState.scrolling) {
       return;
     }
-    stateManager.hideHorizontalScrollBar();
+    if (horizontalVisible === 'focus') {
+      stateManager.hideHorizontalScrollBar();
+    } else if (horizontalVisible === 'scrolling') {
+      stateManager.showHorizontalScrollBar(true, 'rightFrozen');
+    }
   });
   scenegraph.component.vScrollBar.addEventListener('pointermove', (e: FederatedPointerEvent) => {
     scenegraph.table.stateManager.updateCursor('default');

From 9b4d77bee3fcf2f3ce2560c9bc2e0bcdc7997d3a Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Mon, 23 Mar 2026 18:16:49 +0800
Subject: [PATCH 09/16] docs: add scrollFrozenCols

---
 .../demo/en/interaction/scroll-frozen.md      | 62 +++++++++++++++++++
 docs/assets/demo/menu.json                    | 12 +++-
 .../demo/zh/interaction/scroll-frozen.md      | 62 +++++++++++++++++++
 .../en/basic_function/frozen_column_row.md    | 25 +++++++-
 docs/assets/guide/en/interaction/scroll.md    | 30 +++++++++
 .../zh/basic_function/frozen_column_row.md    | 25 +++++++-
 docs/assets/guide/zh/interaction/scroll.md    | 32 ++++++++++
 .../option/en/common/option-secondary.md      | 14 ++++-
 .../option/zh/common/option-secondary.md      | 14 ++++-
 .../list-table-scrollable-frozen-cols.ts      |  2 +-
 10 files changed, 270 insertions(+), 8 deletions(-)
 create mode 100644 docs/assets/demo/en/interaction/scroll-frozen.md
 create mode 100644 docs/assets/demo/zh/interaction/scroll-frozen.md

diff --git a/docs/assets/demo/en/interaction/scroll-frozen.md b/docs/assets/demo/en/interaction/scroll-frozen.md
new file mode 100644
index 000000000..f77be79bc
--- /dev/null
+++ b/docs/assets/demo/en/interaction/scroll-frozen.md
@@ -0,0 +1,62 @@
+---
+category: examples
+group: Interaction
+title: Scrollbars In Frozen Areas
+cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/scroll-frozen.gif
+link: interaction/scroll-frozen
+option: ListTable#scrollFrozenCols
+---
+
+# Scrollbars In Frozen Areas
+
+This example shows horizontal scrolling inside frozen areas (both left and right) when the total width of frozen columns exceeds the maximum frozen width.
+
+## Key Options
+
+- `frozenColCount` / `rightFrozenColCount` set left/right frozen columns count
+- `maxFrozenWidth` / `maxRightFrozenWidth` set the maximum frozen area width
+- `scrollFrozenCols` / `scrollRightFrozenCols` enable horizontal scrolling inside frozen areas
+- `theme.scrollStyle.visible` can be used to observe scrollbar visibility behavior across multiple scrollable regions (e.g. `scrolling` / `focus`)
+
+## Code
+
+```javascript livedemo template=vtable
+let tableInstance;
+fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_data.json')
+  .then(res => res.json())
+  .then(data => {
+    const columns = [
+      { field: 'Order ID', title: 'Order ID', width: 160 },
+      { field: 'Customer ID', title: 'Customer ID', width: 160 },
+      { field: 'Product Name', title: 'Product Name', width: 220 },
+      { field: 'Category', title: 'Category', width: 140 },
+      { field: 'Sub-Category', title: 'Sub-Category', width: 160 },
+      { field: 'Region', title: 'Region', width: 120 },
+      { field: 'City', title: 'City', width: 140 },
+      { field: 'Order Date', title: 'Order Date', width: 140 },
+      { field: 'Region', title: 'Region', width: 120 },
+      { field: 'City', title: 'City', width: 140 },
+      { field: 'Order Date', title: 'Order Date', width: 140 },
+      { field: 'Quantity', title: 'Quantity', width: 120 },
+      { field: 'Sales', title: 'Sales', width: 120 },
+      { field: 'Profit', title: 'Profit', width: 120 },
+      { field: 'Segment', title: 'Segment', width: 140 },
+      { field: 'Ship Mode', title: 'Ship Mode', width: 140 }
+    ];
+
+    const option = {
+      records: data,
+      columns,
+      widthMode: 'standard',
+      frozenColCount: 4,
+      rightFrozenColCount: 4,
+      maxFrozenWidth: 320,
+      maxRightFrozenWidth: 320,
+      scrollFrozenCols: true,
+      scrollRightFrozenCols: true,
+      overscrollBehavior: 'none'
+    };
+    tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option);
+    window['tableInstance'] = tableInstance;
+  });
+```
diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json
index 467d6ecf8..107327efa 100644
--- a/docs/assets/demo/menu.json
+++ b/docs/assets/demo/menu.json
@@ -439,8 +439,7 @@
             "zh": "甘特图缩放滚动条",
             "en": "Gantt DataZoomAxis Scrollbar"
           }
-        }
-        ,
+        },
         {
           "path": "gantt-baseline",
           "title": {
@@ -920,6 +919,13 @@
             "en": "scroll"
           }
         },
+        {
+          "path": "scroll-frozen",
+          "title": {
+            "zh": "冻结区可滚动",
+            "en": "Scroll frozen area"
+          }
+        },
         {
           "path": "arrowkeys-move-select",
           "title": {
@@ -1845,4 +1851,4 @@
       ]
     }
   ]
-}
+}
\ No newline at end of file
diff --git a/docs/assets/demo/zh/interaction/scroll-frozen.md b/docs/assets/demo/zh/interaction/scroll-frozen.md
new file mode 100644
index 000000000..cdf8e0985
--- /dev/null
+++ b/docs/assets/demo/zh/interaction/scroll-frozen.md
@@ -0,0 +1,62 @@
+---
+category: examples
+group: Interaction
+title: 冻结区域滚动条
+cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/scroll-frozen.gif
+link: interaction/scroll-frozen
+option: ListTable#scrollFrozenCols
+---
+
+# 冻结区域滚动条
+
+该示例展示了当冻结区域的列宽总和超过最大冻结宽度时，开启冻结区域内部横向滚动的效果（左冻结与右冻结）。
+
+## 关键配置
+
+- `frozenColCount` / `rightFrozenColCount` 设置左右冻结列数
+- `maxFrozenWidth` / `maxRightFrozenWidth` 设置左右冻结区域最大宽度
+- `scrollFrozenCols` / `scrollRightFrozenCols` 开启冻结区域内部横向滚动
+- `theme.scrollStyle.visible` 可配合观察滚动条在多滚动区域下的显隐行为（如 `scrolling` / `focus`）
+
+## 代码演示
+
+```javascript livedemo template=vtable
+let tableInstance;
+fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_data.json')
+  .then(res => res.json())
+  .then(data => {
+    const columns = [
+      { field: 'Order ID', title: 'Order ID', width: 160 },
+      { field: 'Customer ID', title: 'Customer ID', width: 160 },
+      { field: 'Product Name', title: 'Product Name', width: 220 },
+      { field: 'Category', title: 'Category', width: 140 },
+      { field: 'Sub-Category', title: 'Sub-Category', width: 160 },
+      { field: 'Region', title: 'Region', width: 120 },
+      { field: 'City', title: 'City', width: 140 },
+      { field: 'Order Date', title: 'Order Date', width: 140 },
+      { field: 'Region', title: 'Region', width: 120 },
+      { field: 'City', title: 'City', width: 140 },
+      { field: 'Order Date', title: 'Order Date', width: 140 },
+      { field: 'Quantity', title: 'Quantity', width: 120 },
+      { field: 'Sales', title: 'Sales', width: 120 },
+      { field: 'Profit', title: 'Profit', width: 120 },
+      { field: 'Segment', title: 'Segment', width: 140 },
+      { field: 'Ship Mode', title: 'Ship Mode', width: 140 }
+    ];
+
+    const option = {
+      records: data,
+      columns,
+      widthMode: 'standard',
+      frozenColCount: 4,
+      rightFrozenColCount: 4,
+      maxFrozenWidth: 320,
+      maxRightFrozenWidth: 320,
+      scrollFrozenCols: true,
+      scrollRightFrozenCols: true,
+      overscrollBehavior: 'none'
+    };
+    tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option);
+    window['tableInstance'] = tableInstance;
+  });
+```
diff --git a/docs/assets/guide/en/basic_function/frozen_column_row.md b/docs/assets/guide/en/basic_function/frozen_column_row.md
index 23d3832f6..cc9a20a34 100644
--- a/docs/assets/guide/en/basic_function/frozen_column_row.md
+++ b/docs/assets/guide/en/basic_function/frozen_column_row.md
@@ -7,13 +7,13 @@ Note: This function is only supported in the basic table ListTable.
 ## Set Left Frozen Columns
 
 Freezing the left column is the most common freezing requirement. Compared with the freezing in other directions, this is also the most comprehensive freezing ability supported by VTable.
-f
 The relevant configuration items are as follows:
 
 - `frozenColCount`: Number of frozen columns, default is 0.
 - `allowFrozenColCount`: Number of columns allowed to be operated, that is, the number of columns before which the freeze operation button will appear, default is 0.
 - `showFrozenIcon`: Whether to display the fixed column icon, default is `true`.
 - `maxFrozenWidth`: Maximum freeze width, default is '80%'.
+- `scrollFrozenCols`: When the total width of frozen columns exceeds `maxFrozenWidth`, the left frozen area becomes horizontally scrollable, default is `false`. When enabled, all frozen columns are kept and you can use trackpad horizontal scrolling or drag the scrollbar inside the frozen area.
 - `unfreezeAllOnExceedsMaxWidth`: When the column width exceeds the maximum freeze width, whether to automatically unfreeze all, default is `true`. If set to false, it will not unfreeze all columns, but will determine the number of columns to be unfrozen according to the value of maxFrozenWidth.
 
 Here is a configuration example:
@@ -36,6 +36,29 @@ A common scenario for freezing the right column in a table is to place operation
 The configuration items are as follows:
 
 - `rightFrozenColCount`: Number of right frozen columns, default is 0.
+- `maxRightFrozenWidth`: Maximum freeze width for right frozen columns (fixed value or percentage). Defaults to `maxFrozenWidth`.
+- `scrollRightFrozenCols`: When the total width of right frozen columns exceeds `maxRightFrozenWidth`, the right frozen area becomes horizontally scrollable, default is `false`.
+
+## Horizontal Scrolling Inside Frozen Areas
+
+When the frozen area's content width exceeds the maximum freeze width, you can enable horizontal scrolling inside the frozen area to keep all frozen columns.
+
+```javascript
+const listTable = new ListTable({
+  // ...other configuration items
+  frozenColCount: 6,
+  maxFrozenWidth: 320,
+  scrollFrozenCols: true,
+
+  rightFrozenColCount: 4,
+  maxRightFrozenWidth: 320,
+  scrollRightFrozenCols: true
+});
+```
+
+After enabling:
+- The left/right frozen areas respond to trackpad horizontal scrolling within their own regions.
+- When scrollbars are visible, independent horizontal scrollbars for frozen areas will appear at the bottom.
 
 ## Set Top Frozen Rows
 
diff --git a/docs/assets/guide/en/interaction/scroll.md b/docs/assets/guide/en/interaction/scroll.md
index c17db31b5..b61ee53df 100644
--- a/docs/assets/guide/en/interaction/scroll.md
+++ b/docs/assets/guide/en/interaction/scroll.md
@@ -21,6 +21,12 @@ VTable provides rich scroll style configuration items, and users can customize t
 - barToSide: Whether to display to the edge of the container even though the contents are not full. Default false
 - ignoreFrozenCols: Ignore the width of frozen columns, default false
 
+When horizontal scrolling inside frozen areas is enabled (for example `scrollFrozenCols` / `scrollRightFrozenCols`), there may be multiple horizontal scrollable regions (body / left frozen / right frozen). In this case:
+
+- `visible: 'always'`: Scrollbars of all regions are always visible (only if the region is actually scrollable).
+- `visible: 'scrolling'`: Scrollbars are shown when scrolling happens or when hovering over the scrollbar area, and will auto-hide after leaving.
+- `visible: 'focus'`: Only the scrollbar of the region under the pointer is shown (to avoid multiple scrollbars showing at the same time).
+
 Below we show the effect of these configurations with an example:
 
 ```javascript livedemo  template=vtable
@@ -109,6 +115,30 @@ fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American
 
 VTable supports horizontal scrolling while holding down the Shift key, or directly dragging the horizontal scroll bar to make it easier for users to browse table data. Of course, if your computer has a touchpad, you can swipe left and right directly on the touchpad to achieve horizontal scrolling.
 
+## Horizontal scrolling inside frozen areas
+
+After enabling frozen columns, if the frozen area's total frozen width exceeds the maximum frozen width (`maxFrozenWidth` / `maxRightFrozenWidth`), besides auto-unfreezing columns to fit the viewport, you can enable horizontal scrolling inside frozen areas to keep all frozen columns:
+
+- Left frozen area: enable with `scrollFrozenCols: true`
+- Right frozen area: enable with `scrollRightFrozenCols: true`
+
+After enabling, trackpad horizontal scrolling works inside the corresponding frozen area (without scrolling the body first). When scrollbars are visible, independent horizontal scrollbars for frozen areas will appear at the bottom, supporting dragging the thumb or clicking the track.
+
+Example:
+
+```javascript
+const option = {
+  // ...other configuration items
+  frozenColCount: 6,
+  maxFrozenWidth: 320,
+  scrollFrozenCols: true,
+
+  rightFrozenColCount: 4,
+  maxRightFrozenWidth: 320,
+  scrollRightFrozenCols: true
+};
+```
+
 ## scroll interface
 
 VTable provides the scrollToCell interface for scrolling to the specified cell location. The method accepts the cellAddr parameter to specify the cell location to scroll to. Example code is as follows:
diff --git a/docs/assets/guide/zh/basic_function/frozen_column_row.md b/docs/assets/guide/zh/basic_function/frozen_column_row.md
index 8201bf5e3..edaffbc4b 100644
--- a/docs/assets/guide/zh/basic_function/frozen_column_row.md
+++ b/docs/assets/guide/zh/basic_function/frozen_column_row.md
@@ -7,13 +7,13 @@
 ## 左侧冻结列设置
 
 针对左侧列冻结是最常见的冻结需求，相比其他方向的冻结这也是 VTable 支持功能最全面的冻结能力。
-f
 相关配置项如下：
 
 - `frozenColCount`: 冻结列数，默认为 0。
 - `allowFrozenColCount`: 允许可操作冻结列数，即前多少列会出现冻结操作按钮，默认为 0。
 - `showFrozenIcon`: 是否显示固定列图标，默认为 `true`。
 - `maxFrozenWidth`: 最大冻结宽度，默认为'80%'。
+- `scrollFrozenCols`: 当冻结列总宽度超过 `maxFrozenWidth` 时，左侧冻结区域可横向滚动，默认为 `false`。开启后会保留全部冻结列，并在冻结区域内通过触摸板横向滚动或拖拽滚动条查看超出部分。
 - `unfreezeAllOnExceedsMaxWidth`: 当列宽超过最大冻结宽度时，是否全部自动解冻，默认为 `true`。如果设置为false，则不会解冻全部列，而是根据 maxFrozenWidth 的值来决定最终解冻的列数。
 
 以下是一个配置示例：
@@ -36,6 +36,29 @@ const listTable = new ListTable({
 配置项如下：
 
 - `rightFrozenColCount`: 右侧冻结列数，默认为 0。
+- `maxRightFrozenWidth`: 右侧最大冻结宽度，固定值 or 百分比。默认与 `maxFrozenWidth` 一致。
+- `scrollRightFrozenCols`: 当右侧冻结列总宽度超过 `maxRightFrozenWidth` 时，右侧冻结区域可横向滚动，默认为 `false`。
+
+## 冻结区域横向滚动
+
+当冻结区域的“内容总宽度”超过最大冻结宽度时，可以开启冻结区域内部横向滚动来保留全部冻结列。
+
+```javascript
+const listTable = new ListTable({
+  // ...其他配置项
+  frozenColCount: 6,
+  maxFrozenWidth: 320,
+  scrollFrozenCols: true,
+
+  rightFrozenColCount: 4,
+  maxRightFrozenWidth: 320,
+  scrollRightFrozenCols: true
+});
+```
+
+开启后：
+- 左/右冻结区域会在自身区域内响应触摸板横向滚动。
+- 当滚动条可见时，底部会出现对应冻结区域的独立横向滚动条。
 
 ## 顶部冻结行
 
diff --git a/docs/assets/guide/zh/interaction/scroll.md b/docs/assets/guide/zh/interaction/scroll.md
index c966990ff..094de00cd 100644
--- a/docs/assets/guide/zh/interaction/scroll.md
+++ b/docs/assets/guide/zh/interaction/scroll.md
@@ -21,6 +21,12 @@ VTable 提供了丰富的滚动样式配置项，用户可以按照自己的需
 - barToSide ：是否显示到容器的边缘 尽管内容没有撑满的情况下. 默认 false
 - ignoreFrozenCols ：横向滚动条是否忽略冻结列宽度，默认false
 
+当表格启用了冻结区域内部横向滚动（如 `scrollFrozenCols` / `scrollRightFrozenCols`）时，表格横向会同时存在多个可滚动区域（body / 左冻结 / 右冻结）。此时：
+
+- `visible: 'always'`：所有区域的滚动条都会常驻显示（若该区域确实可滚动）。
+- `visible: 'scrolling'`：仅在“滚动发生”或“鼠标悬停在滚动条区域”时显示，离开后会自动隐藏。
+- `visible: 'focus'`：鼠标指针聚焦到某个区域时，仅显示该区域对应的滚动条（避免多个滚动条同时展示）。
+
 下面我们通过示例来展示这些配置的效果：
 
 ```javascript livedemo   template=vtable
@@ -109,6 +115,32 @@ fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American
 
 VTable 支持在按住 Shift 键时进行横向滚动，或者直接拖拽横向滚动条，以便用户更方便地浏览表格数据。当然，如果您的电脑有触摸板，可以直接在触摸板上直接左右滑动来实现横向滚动。
 
+## 冻结区域内横向滚动
+
+在开启列冻结后，如果冻结区域的“冻结列总宽度”超过最大冻结宽度（`maxFrozenWidth` / `maxRightFrozenWidth`），除了将冻结列自动解冻以适配可视宽度外，也可以开启冻结区域内部横向滚动来“保留全部冻结列”：
+
+- 左侧冻结区域：通过 `scrollFrozenCols: true` 开启
+- 右侧冻结区域：通过 `scrollRightFrozenCols: true` 开启
+
+开启后，触摸板的横向滚动会在对应冻结区域内生效（无需先滚动 body），并且在滚动条可见时底部会出现对应冻结区域的独立横向滚动条，支持拖拽滑块或点击轨道进行滚动。
+
+示例：
+
+```javascript
+const option = {
+  // ...其他配置项
+  frozenColCount: 6,
+  maxFrozenWidth: 320,
+  scrollFrozenCols: true,
+
+  rightFrozenColCount: 4,
+  maxRightFrozenWidth: 320,
+  scrollRightFrozenCols: true
+};
+```
+
+更多冻结相关配置可参考《行列冻结》章节。
+
 ## 滚动接口
 
 VTable 提供了 scrollToCell 接口，用于滚到指定的单元格位置。该方法接受 cellAddr 参数用于指定要滚动到的单元位置。示例代码如下：
diff --git a/docs/assets/option/en/common/option-secondary.md b/docs/assets/option/en/common/option-secondary.md
index bc439188e..bf13145fa 100644
--- a/docs/assets/option/en/common/option-secondary.md
+++ b/docs/assets/option/en/common/option-secondary.md
@@ -104,6 +104,18 @@ number of frozen rows at the bottom
 
 Maximum freezing width, fixed value or percentage. Default is '80%'
 
+#${prefix} scrollFrozenCols(boolean) = false
+
+When the total width of frozen columns exceeds maxFrozenWidth, the frozen area becomes horizontally scrollable. When enabled, all frozen columns are kept and you can use trackpad horizontal scrolling or drag the scrollbar inside the frozen area to view the overflow part.
+
+#${prefix} maxRightFrozenWidth(number | string) = maxFrozenWidth
+
+Maximum freeze width for right frozen columns (fixed value or percentage). Defaults to maxFrozenWidth.
+
+#${prefix} scrollRightFrozenCols(boolean) = false
+
+When the total width of right frozen columns exceeds maxRightFrozenWidth, the right frozen area becomes horizontally scrollable. When enabled, all right frozen columns are kept and you can use trackpad horizontal scrolling or drag the scrollbar inside the frozen area to view the overflow part.
+
 #${prefix} unfreezeAllOnExceedsMaxWidth(boolean) = true
 
 Whether to defrost after the maximum freezing width is exceeded. The default value is true. If set to false, it will not unfreeze all columns, but will determine the number of columns to be unfrozen according to the value of maxFrozenWidth.
@@ -762,4 +774,4 @@ Component layout order, default is ['legend', 'title'].
 
 ```
 componentLayoutOrder?: ('legend' | 'title')[];
-```
\ No newline at end of file
+```
diff --git a/docs/assets/option/zh/common/option-secondary.md b/docs/assets/option/zh/common/option-secondary.md
index d6bf09405..991c3d3f2 100644
--- a/docs/assets/option/zh/common/option-secondary.md
+++ b/docs/assets/option/zh/common/option-secondary.md
@@ -108,6 +108,18 @@ containerFit: {
 
 最大冻结宽度，固定值 or 百分比。默认为'80%'
 
+#${prefix} scrollFrozenCols(boolean) = false
+
+当冻结列总宽度超过 maxFrozenWidth 时，冻结区域可横向滚动。开启后会保留全部冻结列，并在冻结区域内通过触摸板横向滚动或拖拽滚动条查看超出部分。
+
+#${prefix} maxRightFrozenWidth(number | string) = maxFrozenWidth
+
+右侧最大冻结宽度，固定值 or 百分比。默认与 maxFrozenWidth 一致。
+
+#${prefix} scrollRightFrozenCols(boolean) = false
+
+当右侧冻结列总宽度超过 maxRightFrozenWidth 时，右侧冻结区域可横向滚动。开启后会保留全部右侧冻结列，并在冻结区域内通过触摸板横向滚动或拖拽滚动条查看超出部分。
+
 #${prefix} unfreezeAllOnExceedsMaxWidth(boolean) = true
 
 超过最大冻结宽度后是否全部解冻，默认 true。如果设置为 false，则不会解冻全部列，而是根据 maxFrozenWidth 的值来决定最终解冻的列数。
@@ -758,4 +770,4 @@ defaultCursor?: 'default' | 'cell' | 'pointer' | 'text' | 'wait' | 'help' | 'cro
 
 ```
 componentLayoutOrder?: ('legend' | 'title')[];
-```
\ No newline at end of file
+```
diff --git a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
index c564326f7..3540aa758 100644
--- a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
+++ b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
@@ -50,7 +50,7 @@ export function createTable() {
     scrollRightFrozenCols: true,
     theme: VTable.themes.DEFAULT.extends({
       scrollStyle: {
-        visible: 'none'
+        visible: 'scrolling'
       }
     }),
     excelOptions: {

From d57599a917e70dd7ddabc94c6670caaa7f53c63a Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Tue, 24 Mar 2026 14:43:09 +0800
Subject: [PATCH 10/16] docs: add code abbitation and technical docs

---
 .../vtable/docs/frozen-area-scroll-design.md  | 176 ++++++++++++++++++
 packages/vtable/docs/select-border-design.md  | 171 +++++++++++++++++
 packages/vtable/src/core/BaseTable.ts         |   9 +
 .../src/core/utils/get-cell-position.ts       |   2 +
 .../vtable/src/event/listener/scroll-bar.ts   |   5 +-
 .../vtable/src/event/listener/table-group.ts  |   2 +
 packages/vtable/src/event/scroll.ts           |   8 +
 .../scenegraph/component/table-component.ts   |  20 +-
 .../vtable/src/scenegraph/component/util.ts   |   2 +
 .../group-contribution-render.ts              |   7 +
 packages/vtable/src/scenegraph/scenegraph.ts  |   6 +
 .../scenegraph/select/create-select-border.ts |  21 +++
 .../scenegraph/select/delete-select-border.ts |   5 +
 .../scenegraph/select/move-select-border.ts   |   4 +
 .../select/update-custom-select-border.ts     |   4 +
 .../scenegraph/select/update-select-border.ts |  21 +++
 .../scenegraph/select/update-select-style.ts  |   2 +
 packages/vtable/src/state/state.ts            |   8 +
 packages/vtable/src/ts-types/base-table.ts    |  25 +++
 19 files changed, 495 insertions(+), 3 deletions(-)
 create mode 100644 packages/vtable/docs/frozen-area-scroll-design.md
 create mode 100644 packages/vtable/docs/select-border-design.md

diff --git a/packages/vtable/docs/frozen-area-scroll-design.md b/packages/vtable/docs/frozen-area-scroll-design.md
new file mode 100644
index 000000000..a468ee9e2
--- /dev/null
+++ b/packages/vtable/docs/frozen-area-scroll-design.md
@@ -0,0 +1,176 @@
+# 冻结区域独立横向滚动：技术设计
+
+本文总结 VTable 中“冻结区域（左冻结/右冻结）支持独立横向滚动”的实现设计、核心数据结构、关键链路改动点与边界行为。
+
+> 对应实现主要落在 packages/vtable/src 内；PR 参考：#5063（Feat/frozen column scroll）。
+
+## 1. 背景与问题
+
+在 ListTable 中，冻结列用于保持关键列常驻可见。但当冻结列总宽度超过最大冻结宽度（`maxFrozenWidth`）时，既有策略通常是“自动解冻部分列”以满足视口宽度限制。
+
+在一些业务场景中，冻结列必须保留（例如左侧关键标识列、右侧操作列），因此需要：
+
+- 冻结区域在宽度受限时仍保留全部冻结列
+- 在冻结区域内部提供横向滚动能力（与 body 横向滚动相互独立）
+- 在多滚动域场景下，滚动条显隐与交互需要“按区域”工作，避免混乱
+
+## 2. 目标与非目标
+
+### 2.1 目标
+
+- 新增左/右冻结区域内部横向滚动能力（trackpad/wheel 与滚动条拖拽/点击轨道）
+- 保持冻结区域“视口宽度”可控（左：`maxFrozenWidth`，右：`maxRightFrozenWidth`）
+- 多滚动域并存时（body / leftFrozen / rightFrozen），保证：
+  - 渲染坐标系正确（内容随各自 scrollLeft 平移）
+  - 命中坐标正确（getCellAt、事件 target 等与渲染一致）
+  - 主滚动条与冻结滚动条互不干扰
+  - `scrollStyle.visible` 为 `focus/scrolling/always/none` 时行为一致
+
+### 2.2 非目标
+
+- 本设计不覆盖 PivotTable/Gantt 的冻结滚动扩展（当前以 ListTable 为主）
+- 不引入插件化实现（该能力需要穿透状态/布局/命中/scenegraph 多层链路）
+
+## 3. 概念与术语
+
+- **冻结内容宽（content width）**：冻结列本身的总宽度，不受 maxFrozenWidth 限制。
+- **冻结视口宽（viewport width）**：冻结区域在画布上占用的宽度，受 maxFrozenWidth / maxRightFrozenWidth 限制。
+- **冻结溢出量（offset）**：`max(0, contentWidth - viewportWidth)`，表示冻结区域内部最大可滚动距离。
+- **滚动域（scroll domain）**：
+  - `body`：主横向滚动域（影响非冻结列）
+  - `frozen`：左冻结区域内部横向滚动域
+  - `rightFrozen`：右冻结区域内部横向滚动域
+
+## 4. 对外配置与 API
+
+### 4.1 新增配置（ListTableConstructorOptions）
+
+- `scrollFrozenCols?: boolean`
+  - `false`（默认）：冻结列超出最大冻结宽度时遵循原策略（可能解冻）
+  - `true`：冻结区域内部可横向滚动，保留全部冻结列
+- `maxRightFrozenWidth?: number | string`
+  - 右侧最大冻结宽度，默认与 `maxFrozenWidth` 对齐
+- `scrollRightFrozenCols?: boolean`
+  - `false`（默认）：右冻结区域宽度 = 内容宽度（无内部滚动）
+  - `true`：右冻结区域内部可横向滚动
+
+### 4.2 相关方法（BaseTable）
+
+左冻结：
+
+- `getFrozenColsContentWidth()`：冻结内容宽
+- `getFrozenColsWidth()`：冻结视口宽（scrollFrozenCols 开启时受 maxFrozenWidth 限制）
+- `getFrozenColsOffset()`：溢出量（最大可滚动距离）
+- `getFrozenColsScrollLeft()`：当前左冻结 scrollLeft（px）
+
+右冻结：
+
+- `getRightFrozenColsContentWidth()`
+- `getRightFrozenColsWidth()`（scrollRightFrozenCols 开启时受 maxRightFrozenWidth 限制）
+- `getRightFrozenColsOffset()`
+- `getRightFrozenColsScrollLeft()`
+
+## 5. 数据结构（StateManager）
+
+在 StateManager 中新增两个横向位置用于维护冻结域滚动：
+
+- `scroll.frozenHorizontalBarPos`：左冻结 scrollLeft（px）
+- `scroll.rightFrozenHorizontalBarPos`：右冻结 scrollLeft（px）
+
+并提供两类接口：
+
+1) 外部“设置滚动位置”（用于 wheel / click 轨道 / 拖拽）
+- `setFrozenColsScrollLeft(left, triggerRender?)`
+- `setRightFrozenColsScrollLeft(left, triggerRender?)`
+
+2) 外部“按滚动条 ratio 更新”（scrollDrag 回调给的是 range，需要映射回 scrollLeft）
+- `updateFrozenHorizontalScrollBar(xRatio)`
+- `updateRightFrozenHorizontalScrollBar(xRatio)`
+
+右冻结的 ratio 与 left 做了反向映射（`ratio = 1 - left/maxScrollLeft`），以更符合“右冻结内容从右向左展开”的视觉直觉。
+
+## 6. 渲染与布局（Scenegraph）
+
+### 6.1 左冻结
+
+左冻结的平移相对直观：冻结区域内部滚动时，对应 group 的 childrenX 直接使用 `-scrollLeft`。
+
+### 6.2 右冻结
+
+右冻结的布局基准是“内容右对齐视口”，因此需要同时考虑溢出量 offset 与 scrollLeft：
+
+- `rightFrozenStartX = -rightFrozenOffset + rightFrozenScrollLeft`
+
+含义：
+
+- `-offset`：使右冻结内容尾部对齐到视口右侧（把超出部分整体向左移出视口）
+- `+scrollLeft`：在视口内左右移动查看隐藏的列
+
+对应更新点：
+
+- `Scenegraph.updateContainerAttrWidthAndX()` 在布局刷新时更新 rightFrozenGroup / corner group 的 childrenX
+- `Scenegraph.setRightFrozenColsScrollLeft()` 在右冻结滚动变化时更新 rightFrozenGroup / rightTopCorner / rightBottomCorner 的 childrenX
+
+### 6.3 Clip（裁剪）
+
+多区域 overlay/内容组均依赖 clipRect 进行裁剪。右冻结视口宽度在开启 scrollRightFrozenCols 时不再等于内容宽度，需要使用 `getRightFrozenColsWidth()` 作为 clip 宽度来源，保证“内容滚动但不越界绘制”。
+
+## 7. 命中与坐标映射（HitTest）
+
+冻结区域内部滚动会改变“可视坐标 ↔ 内容坐标”的映射关系，因此需要在命中链路中补偿：
+
+- 右冻结命中：当 x 落在右冻结视口范围内时，先将 `absoluteX -= rightFrozenScrollLeft` 再计算 target col
+- 右冻结列 x 计算：`getColX(col, table, true)` 叠加 `getRightFrozenColsScrollLeft()`，保证渲染坐标与 hitTest 一致
+
+## 8. 事件分发（Wheel/Trackpad）
+
+横向 wheel 需要判断“滚动意图属于哪个滚动域”：
+
+- 优先冻结域：当指针坐标落在左冻结/右冻结视口范围内且该域可滚动（offset>0）
+- 否则落入 body 域
+
+注意：部分环境 wheel 事件可能没有可靠的 x/y，因此引入 LastBodyPointerXY 作为回退坐标。
+
+右冻结域的 delta 需要反向映射：
+
+- `rightFrozenDelta = -optimizedDeltaX`
+
+原因是右冻结内容的“展开方向”与 body/左冻结相反（内容从右向左展开）。
+
+## 9. 滚动条系统（ScrollBar UI）
+
+### 9.1 多段横向滚动条
+
+当左右冻结域启用内部滚动且存在溢出时，底部会出现三段横向滚动条：
+
+- body 主滚动条（hScrollBar）
+- 左冻结横向滚动条（frozenHScrollBar）
+- 右冻结横向滚动条（rightFrozenHScrollBar）
+
+各段的 range（滑块长度）分别反映其域的“视口宽 / 内容宽”。
+
+### 9.2 显隐策略与交互
+
+`scrollStyle.visible` 在多滚动域场景下的定义：
+
+- `always`：所有可滚动域的滚动条同时显示
+- `focus`：只显示指针所在域的滚动条（避免干扰）
+- `scrolling`：滚动发生时显示；hover 到滚动条区域时显示以支持交互；离开后延迟隐藏
+
+实现上通过 `TableComponent.showHorizontalScrollBar(target)` 控制显示目标域，并在事件监听中根据 hover/scrolling 规则维护 autoHide。
+
+## 10. 关键边界与已处理问题
+
+- **右冻结分割线（shadow line）错位**：当右冻结内容可滚动时，分割线应固定在“右冻结视口左边界”而不是随内容滚动
+- **选区 overlay 被裁切**：当选区贴边或存在 fill handle 时，需要对 overlay 的 clipRect 进行外扩（详见选框技术设计文档）
+- **拖拽滚动条不生效**：throttle 绑定函数需要 bind(this)，否则 this.table 不可用
+
+## 11. 可观测性与测试建议
+
+建议覆盖以下用例：
+
+- 左冻结溢出：trackpad 横向滚动仅影响左冻结内容；body 不动
+- 右冻结溢出：trackpad 横向滚动方向符合预期；命中列与渲染一致
+- 滚动条：三段滚动条的滑块比例正确；拖拽与点击轨道能驱动对应域滚动
+- visible 策略：focus/scolling 下仅显示目标域滚动条且可自动隐藏
+
diff --git a/packages/vtable/docs/select-border-design.md b/packages/vtable/docs/select-border-design.md
new file mode 100644
index 000000000..91eca726a
--- /dev/null
+++ b/packages/vtable/docs/select-border-design.md
@@ -0,0 +1,171 @@
+# 选框（Select Border）改造：技术设计（前/后对比）
+
+本文总结 VTable 选框（selection border + fill handle）的设计背景、改造前的问题、改造后的结构与关键实现点，便于后续维护与扩展。
+
+> 涉及代码：packages/vtable/src/scenegraph/select/* 及 overlay clip 相关贡献逻辑。
+
+## 1. 背景
+
+VTable 的“选框”由两个主要视觉元素组成：
+
+- **selection border**：选区外边框（可配置边框色、线宽、虚线等）
+- **fill handle**：右下角 6x6 小方块（Excel 类填充柄），受 `excelOptions.fillHandle` 控制
+
+在引入“冻结区域独立滚动”后，表格的可视区域被拆成多个区域（body/headers/leftFrozen/rightFrozen/bottomFrozen），选框需要：
+
+- 正确出现在对应区域的 overlay 层并被 clip 裁剪
+- 支持跨区域选择（需要拆分多段绘制）
+- 避免在跨区域边界重复描边（导致边框变粗）
+- 在贴边/裁剪边界时不出现“半条线”或 fill handle 被切掉
+
+## 2. 改造前设计（概念层面）
+
+### 2.1 选框的基本组织方式
+
+- 选框图元挂在 overlayGroup 下（select-overlay），overlayGroup 会被各区域 clipRect 裁剪。
+- 对于跨区域选区，会拆分为多个选框段落（每段一个 rect，必要时一个 fill handle）。
+
+### 2.2 主要痛点
+
+1) **贴边裁剪导致的边框缺失**
+- 选框的描边线宽是以 rect 边界为中心绘制；当选区贴着表格边缘或区域 clip 边界时，一半线宽会被裁剪，呈现“半条线”。
+
+2) **fill handle 被裁剪或定位异常**
+- 在最后一行/最后一列等边界场景，fill handle 可能落到 clip 外，导致不可见或难以命中。
+
+3) **跨区域选框的重复描边**
+- 选区跨越多个 overlay（例如 columnHeader + body，或 body + rightFrozen）时，如果各段都绘制相邻边，会在边界处叠加出更粗的边框。
+
+4) **场景树重建导致选区丢失**
+- 数据更新导致 scenegraph 重建时，overlay 下的选区图元会被清空，需要从 state 恢复。
+
+## 3. 改造后设计（实现层面）
+
+改造后的原则：
+
+- **跨区域必拆分**：按区域拆分为多段选框，每段只负责自己的绘制与裁剪
+- **边界不重复描边**：通过 strokeArray 控制每段四边是否绘制
+- **贴边不裁半线**：在选框更新时对 rect 做“半线宽补偿”，同时对 overlay clipRect 做“外扩”
+- **fill handle 只在可解释场景出现**：单选区 + 非表头 + 边不被禁用时显示
+
+## 4. 关键实现点
+
+### 4.1 选框创建：overlay 坐标换算与 fill handle 显示条件
+
+文件：
+
+- packages/vtable/src/scenegraph/select/create-select-border.ts
+
+要点：
+
+- 使用 `highPerformanceGetCell(...).globalAABBBounds` 获取单元格全局边界，再减去 `tableGroup + overlayGroup` 的偏移，换算成 overlay 本地坐标。
+- fill handle 的显示需要满足：
+  - `excelOptions.fillHandle` 开启
+  - 当前仅 1 个选区（多选区时移除所有 handle）
+  - 选区不包含表头（header 不允许填充）
+  - strokes 中右边或下边被关闭时不显示（避免 handle 由其它段负责时重复出现）
+
+### 4.2 选框更新：可视范围裁剪与 fill handle 边界定位
+
+文件：
+
+- packages/vtable/src/scenegraph/select/update-select-border.ts
+
+要点：
+
+1) **按 role 裁剪计算范围**
+
+不同区域的选框更新策略不同：
+
+- `rowHeader`：只裁剪行范围（跟随 body 可视行）
+- `columnHeader` / `bottomFrozen`：只裁剪列范围（跟随 body 可视列）
+- `rightFrozen`：只裁剪行范围（跟随 body 可视行）
+- `body`：裁剪行列范围
+
+目的是避免更新不可见区域的选框段落，降低滚动过程的更新开销。
+
+2) **fill handle 边界推导**
+
+当选区触达最后一列/最后一行时，直接取 “end cell bound” 可能导致 handle 超出 clip。实现上通过相邻单元格的 bound 推导 `handlerX/handlerY`，让 handle 保持在可见边界附近。
+
+3) **贴边半线宽裁切修正**
+
+当选区贴着表格外边界时，通过根据 lineWidth 计算 diffSize，对 rect 做 x/y/width/height 的微调，避免“半条线”现象。
+
+### 4.3 跨区域拆分：calculateCellRangeDistribution + strokeArray
+
+文件：
+
+- packages/vtable/src/scenegraph/select/update-select-border.ts
+- packages/vtable/src/scenegraph/select/update-custom-select-border.ts
+
+流程：
+
+1) `calculateCellRangeDistribution(startCol, startRow, endCol, endRow, table)` 判断选区跨越哪些区域。
+2) 针对每个需要的区域创建一段选框，传入该段负责的范围。
+3) 通过 `strokeArray=[top,right,bottom,left]` 控制该段四边是否绘制，避免跨区域边界重复描边。
+
+自定义选框（CustomSelectionStyle）沿用相同的拆分策略，但只绘制 rect，不包含 fill handle。
+
+### 4.4 selecting → selected 的提交语义
+
+文件：
+
+- packages/vtable/src/scenegraph/select/move-select-border.ts
+
+语义：
+
+- `selectingRangeComponents` 表示“拖拽中”的临时选框
+- 鼠标松开后需要迁移到 `selectedRangeComponents`，作为稳定的选中态
+- 若同 key 已存在历史段落，先 delete 避免泄漏与重复绘制
+
+### 4.5 删除逻辑：shift 续选与 fill handle 清理
+
+文件：
+
+- packages/vtable/src/scenegraph/select/delete-select-border.ts
+
+要点：
+
+- 通过 `scene.lastSelectId` 识别“上一次选择动作”产生的所有段落（跨区域拆分时 selectId 相同）
+- shift 续选需要删除上一次选择段落，再追加新的段落
+- 多选区时需要统一移除 fill handle
+
+### 4.6 overlay 裁剪外扩：避免边框与 fill handle 被 clip 截断
+
+文件：
+
+- packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
+
+要点：
+
+- overlay（select-overlay）组会被各区域 clipRect 裁剪
+- 对 overlay 的 clipRect 进行“外扩”（inflate）：
+  - baseInflate：覆盖 selection border 的线宽
+  - handleInflate：当开启 fill handle 且只有一个选区时，为 6x6 handle 预留空间（3px）
+
+这一层和 4.2 的贴边修正配合，解决“线宽/handle 被裁切”的可见性问题。
+
+### 4.7 场景树重建后的选区恢复
+
+文件：
+
+- packages/vtable/src/scenegraph/scenegraph.ts
+
+要点：
+
+- 数据更新触发 scenegraph 重建会清空 overlay 下的选区图元
+- 若 state 中仍存在 select ranges，需要在场景树重建完成后重新创建选区组件，保证选中态不丢失
+
+## 5. 行为对比（摘要）
+
+- 改造前：选区贴边时边框/handle 可能被裁，跨区域容易重复描边，scenegraph 重建后可能丢失选区图元。
+- 改造后：通过“跨区域拆分 + strokeArray 去重 + overlay clip 外扩 + 贴边半线宽修正 + 重建后恢复”保证一致性与可维护性。
+
+## 6. 测试建议
+
+- 单选区 + fill handle：拖拽到最后一行/最后一列仍可见且可命中
+- 多选区：fill handle 不出现，且历史 handle 会被清理
+- 跨区域选择：跨表头/左冻结/右冻结/底部冻结时边框不加粗
+- scrollFrozenCols/scrollRightFrozenCols 开启：滚动冻结域时选框与 handle 不被裁切、不漂移
+
diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts
index b375443f4..42f3169dd 100644
--- a/packages/vtable/src/core/BaseTable.ts
+++ b/packages/vtable/src/core/BaseTable.ts
@@ -3014,6 +3014,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
    */
   getFrozenColsWidth(): number {
     const contentWidth = this.getFrozenColsContentWidth();
+    // frozenColsWidth 表示“冻结区域视口宽度”，可能小于冻结列内容总宽。
+    // 当开启 scrollFrozenCols 时，冻结区域会限制到 maxFrozenWidth，并允许在冻结区域内部横向滚动来查看超出部分。
     if (!this.options.scrollFrozenCols) {
       return contentWidth;
     }
@@ -3021,14 +3023,17 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     return Math.min(contentWidth, maxFrozenWidth);
   }
   getFrozenColsContentWidth(): number {
+    // 冻结列内容总宽（不受 maxFrozenWidth 限制）
     return this.getColsWidth(0, this.frozenColCount - 1);
   }
   getFrozenColsOffset(): number {
+    // 冻结区域可滚动的最大距离（内容宽 - 视口宽），用于计算滚动条范围与边界判断
     const contentWidth = this.getFrozenColsContentWidth();
     const viewportWidth = this.getFrozenColsWidth();
     return Math.max(0, contentWidth - viewportWidth);
   }
   getFrozenColsScrollLeft(): number {
+    // 左冻结区域内部的横向滚动位置（像素值）
     return this.stateManager.scroll.frozenHorizontalBarPos ?? 0;
   }
   /**
@@ -3052,6 +3057,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
    */
   getRightFrozenColsWidth(): number {
     const contentWidth = this.getRightFrozenColsContentWidth();
+    // rightFrozenColsWidth 表示“右侧冻结区域视口宽度”。
+    // 当开启 scrollRightFrozenCols 时，右侧冻结区域会限制到 maxRightFrozenWidth，并允许在右冻结区域内部横向滚动。
     if (!this.options.scrollRightFrozenCols) {
       return contentWidth;
     }
@@ -3069,11 +3076,13 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
     return 0;
   }
   getRightFrozenColsOffset(): number {
+    // 右侧冻结区域可滚动的最大距离（内容宽 - 视口宽）
     const contentWidth = this.getRightFrozenColsContentWidth();
     const viewportWidth = this.getRightFrozenColsWidth();
     return Math.max(0, contentWidth - viewportWidth);
   }
   getRightFrozenColsScrollLeft(): number {
+    // 右冻结区域内部的横向滚动位置（像素值）
     return this.stateManager.scroll.rightFrozenHorizontalBarPos ?? 0;
   }
   /**
diff --git a/packages/vtable/src/core/utils/get-cell-position.ts b/packages/vtable/src/core/utils/get-cell-position.ts
index 7689e4762..618b0aaae 100644
--- a/packages/vtable/src/core/utils/get-cell-position.ts
+++ b/packages/vtable/src/core/utils/get-cell-position.ts
@@ -190,6 +190,8 @@ export function getTargetColAtConsiderRightFrozen(
     absoluteX < _this.tableNoFrameWidth &&
     absoluteX <= _this.getAllColsWidth()
   ) {
+    // 命中测试需要将“右冻结视口坐标”映射到“右冻结内容坐标”：
+    // 右冻结内容在视口内可滚动时，实际的列位置会整体随 scrollLeft 平移。
     absoluteX -= rightFrozenScrollLeft;
     for (let i = 0; i < _this.rightFrozenColCount; i++) {
       if (absoluteX > _this.tableNoFrameWidth - _this.getColsWidth(_this.colCount - i - 1, _this.colCount - 1)) {
diff --git a/packages/vtable/src/event/listener/scroll-bar.ts b/packages/vtable/src/event/listener/scroll-bar.ts
index 307411a3d..bc321cd1f 100644
--- a/packages/vtable/src/event/listener/scroll-bar.ts
+++ b/packages/vtable/src/event/listener/scroll-bar.ts
@@ -15,7 +15,9 @@ export function bindScrollBarListener(eventManager: EventManager) {
   const horizontalVisible = table.theme.scrollStyle?.horizontalVisible ?? visible1;
   const verticalVisible = table.theme.scrollStyle?.verticalVisible ?? visible1;
 
-  // 监听滚动条组件pointover事件
+  // 监听滚动条 hover：
+  // - focus：hover 显示，pointerout 立即隐藏
+  // - scrolling：hover 显示（用于支持“点轨道/拖拽”交互），pointerout 时重新触发 autoHide 计时
   scenegraph.component.vScrollBar.addEventListener('pointerover', (e: any) => {
     if (verticalVisible === 'focus' || verticalVisible === 'scrolling') {
       stateManager.showVerticalScrollBar();
@@ -53,6 +55,7 @@ export function bindScrollBarListener(eventManager: EventManager) {
     if (horizontalVisible === 'focus') {
       stateManager.hideHorizontalScrollBar();
     } else if (horizontalVisible === 'scrolling') {
+      // 鼠标离开滚动条后，滚动条应当回到“滚动时显示”的策略：延迟隐藏
       stateManager.showHorizontalScrollBar(true, 'body');
     }
   });
diff --git a/packages/vtable/src/event/listener/table-group.ts b/packages/vtable/src/event/listener/table-group.ts
index f09b5fcfb..3c466dc35 100644
--- a/packages/vtable/src/event/listener/table-group.ts
+++ b/packages/vtable/src/event/listener/table-group.ts
@@ -184,6 +184,8 @@ export function bindTableGroupListener(eventManager: EventManager) {
       (table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.horizontalVisible === 'focus') ||
       (!table.theme.scrollStyle.horizontalVisible && table.theme.scrollStyle.visible === 'focus')
     ) {
+      // focus 模式下：根据鼠标当前所在的区域，仅显示该区域对应的横向滚动条，
+      // 避免 body/左冻结/右冻结三段滚动条同时展示造成干扰。
       const relativeX = e.x - table.tableX;
       const target =
         table.options.scrollFrozenCols &&
diff --git a/packages/vtable/src/event/scroll.ts b/packages/vtable/src/event/scroll.ts
index 3c014e49b..f3aec7e4a 100644
--- a/packages/vtable/src/event/scroll.ts
+++ b/packages/vtable/src/event/scroll.ts
@@ -30,6 +30,12 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
   let usedFrozenHorizontal = false;
   let usedRightFrozenHorizontal = false;
   if (optimizedDeltaX) {
+    // 横向滚动需要判断“滚动意图属于哪个区域”：
+    // - body：主滚动域（影响非冻结列）
+    // - frozen：左侧冻结区域内部滚动（scrollFrozenCols 开启且溢出时）
+    // - rightFrozen：右侧冻结区域内部滚动（scrollRightFrozenCols 开启且溢出时）
+    //
+    // 部分浏览器/输入设备的 wheel 事件不一定带坐标（event.x/y），因此这里会回退使用 LastBodyPointerXY。
     const pxEvent = (event as any).x;
     const pyEvent = (event as any).y;
     const pxFallback = (state.table as any).eventManager?.LastBodyPointerXY?.x;
@@ -69,6 +75,8 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       }
     } else if (rightFrozenColsScrollable && isInRightFrozenViewport) {
       const maxRightFrozenScrollLeft = state.table.getRightFrozenColsOffset();
+      // 右冻结区域的内容从右向左展开。为了让触摸板的“向左滑”表现为“展开更多右冻结列”，
+      // 这里将 deltaX 反向映射到 rightFrozen 的 scrollLeft。
       const rightFrozenDelta = -optimizedDeltaX;
       const nextRightFrozenScrollLeft = state.scroll.rightFrozenHorizontalBarPos + rightFrozenDelta;
       const canScrollRightFrozen =
diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts
index 37ea3538d..9a9040b9c 100644
--- a/packages/vtable/src/scenegraph/component/table-component.ts
+++ b/packages/vtable/src/scenegraph/component/table-component.ts
@@ -16,6 +16,11 @@ import { isValid } from '@visactor/vutils';
  */
 export class TableComponent {
   table: BaseTableAPI;
+  // 横向滚动条在多滚动域场景下需要“只显示当前目标区域”的那一段：
+  // - body：主滚动域
+  // - frozen：左冻结内部滚动域
+  // - rightFrozen：右冻结内部滚动域
+  // - all：全部显示（visible: 'always' 或兼容场景）
   _horizontalScrollBarTarget?: 'body' | 'frozen' | 'rightFrozen' | 'all';
 
   border: IRect; // 表格外边框
@@ -27,8 +32,8 @@ export class TableComponent {
   rowResizeBgLine: ILine; // 表格列宽调整基准线背景
   rowResizeLabel: IGroup; // 表格列宽调整标记
   menu: MenuHandler; // 表格菜单
-  vScrollBar: ScrollBar; // 表格横向滚动条
-  hScrollBar: ScrollBar; // 表格纵向滚动条
+  vScrollBar: ScrollBar; // 表格纵向滚动条
+  hScrollBar: ScrollBar; // 表格横向滚动条（body 主滚动域）
   frozenHScrollBar: ScrollBar; // 左冻结区横向滚动条
   rightFrozenHScrollBar: ScrollBar; // 右冻结区横向滚动条
   frozenShadowLine: IRect; // 表格冻结列右侧阴影块
@@ -321,6 +326,8 @@ export class TableComponent {
     (this.hScrollBar as any).render();
     this.hScrollBar.hideAll();
 
+    // 左冻结/右冻结滚动条默认不展示，仅在对应区域开启内部滚动（scrollFrozenCols/scrollRightFrozenCols）
+    // 且该区域存在溢出（offset > 0）时显示。
     this.frozenHScrollBar = new ScrollBar({
       direction: 'horizontal',
       x: -this.table.tableNoFrameWidth * 2,
@@ -411,6 +418,9 @@ export class TableComponent {
 
     if (totalWidth > tableWidth + sizeTolerance) {
       const y = Math.min(tableHeight, totalHeight);
+      // 多滚动域下，body 的可视区域需要扣除左右冻结占用的视口宽度；
+      // body 的可滚动内容宽度也需要扣除左右冻结列的内容宽度。
+      // 这样主滚动条的滑块长度能准确反映“body 可视宽 / body 内容宽”的比例。
       const bodyViewportWidth = tableWidth - frozenColsWidth - rightFrozenColsWidth;
       const bodyContentWidth = totalWidth - frozenColsContentWidth - rightFrozenColsContentWidth;
       const rangeEnd = bodyContentWidth > 0 ? Math.max(0.05, bodyViewportWidth / bodyContentWidth) : 1;
@@ -458,6 +468,7 @@ export class TableComponent {
 
       const frozenScrollable = this.table.options.scrollFrozenCols && this.table.getFrozenColsOffset() > 0;
       if (!ignoreFrozenCols && frozenScrollable) {
+        // 左冻结滚动条的滑块长度 = 冻结视口宽 / 冻结内容宽
         const frozenRangeEnd = Math.max(0.05, frozenColsWidth / frozenColsContentWidth);
         const x = !hoverOn ? this.table.scenegraph.tableGroup.attribute.x : 0;
         this.frozenHScrollBar.setAttributes({
@@ -487,6 +498,7 @@ export class TableComponent {
       const rightFrozenScrollable =
         this.table.options.scrollRightFrozenCols && this.table.getRightFrozenColsOffset() > 0;
       if (!ignoreFrozenCols && rightFrozenScrollable) {
+        // 右冻结滚动条的滑块长度 = 右冻结视口宽 / 右冻结内容宽
         const rightFrozenRangeEnd = Math.max(0.05, rightFrozenColsWidth / rightFrozenColsContentWidth);
         const x = tableWidth - rightFrozenColsWidth + (!hoverOn ? this.table.scenegraph.tableGroup.attribute.x : 0);
         this.rightFrozenHScrollBar.setAttributes({
@@ -810,6 +822,9 @@ export class TableComponent {
    * @return {*}
    */
   setRightFrozenColumnShadow(col: number) {
+    // 右冻结阴影线应当绘制在“右冻结视口的左边界”。
+    // 在 scrollRightFrozenCols 开启且存在溢出时，右冻结内容会在视口内滚动，
+    // 因此不能用 getColX（受内容滚动影响）来定位分界线，而是固定在 viewport 边界。
     const shouldFixViewport =
       this.table.options.scrollRightFrozenCols && (this.table.getRightFrozenColsOffset?.() ?? 0) > 0;
     const colX = shouldFixViewport
@@ -881,6 +896,7 @@ export class TableComponent {
     this.frozenHScrollBar.hideAll();
     this.rightFrozenHScrollBar.setAttribute('visible', false);
     this.rightFrozenHScrollBar.hideAll();
+    // 清空当前显示目标，确保下一次 show 时能正确切换显示区域
     this._horizontalScrollBarTarget = undefined;
     this.table.scenegraph.updateNextFrame();
   }
diff --git a/packages/vtable/src/scenegraph/component/util.ts b/packages/vtable/src/scenegraph/component/util.ts
index b5dbf3013..29b098a0d 100644
--- a/packages/vtable/src/scenegraph/component/util.ts
+++ b/packages/vtable/src/scenegraph/component/util.ts
@@ -2,6 +2,8 @@ import type { BaseTableAPI } from '../../ts-types/base-table';
 
 export function getColX(col: number, table: BaseTableAPI, isRightFrozen?: boolean) {
   if (isRightFrozen) {
+    // 右冻结列的 x 位置以“表格最右侧”为基准向左累加。
+    // 当开启右冻结区域内部滚动时，需要叠加右冻结的 scrollLeft，使得列在右冻结视口内可左右移动。
     return (
       Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) -
       table.getColsWidth(col, table.colCount - 1) +
diff --git a/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts b/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
index 454bc7c37..5ff051bf4 100644
--- a/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
+++ b/packages/vtable/src/scenegraph/graphic/contributions/group-contribution-render.ts
@@ -1061,6 +1061,13 @@ function getSelectOverlayClipInflate(group: Group, table: BaseTableAPI) {
     return { left: 0, top: 0, right: 0, bottom: 0 };
   }
 
+  // 选区 overlay 组会被各个区域的 clipRect 裁剪。
+  // 当选区贴边（表格边缘/冻结分区边缘）时，边框外描边与 fill handle（右下角小方块）
+  // 可能被 clipRect 截断，因此对 overlay 的 clipRect 做“外扩”处理。
+  //
+  // 其中：
+  // - baseInflate：覆盖 selection border 的线宽（避免只显示一半）
+  // - handleInflate：当开启 fillHandle 且只有一个选区时，为 6x6 的 handle 预留溢出空间（半径 3px）
   const lineWidth = table.theme.selectionStyle?.cellBorderLineWidth;
   const maxLineWidth = Array.isArray(lineWidth)
     ? Math.max(...lineWidth.filter(v => typeof v === 'number'))
diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts
index 3f99b2e38..022f2db9b 100644
--- a/packages/vtable/src/scenegraph/scenegraph.ts
+++ b/packages/vtable/src/scenegraph/scenegraph.ts
@@ -1709,6 +1709,8 @@ export class Scenegraph {
     if (this.table.options.menu?.contextMenuWorkOnlyCell === false) {
       this.canvasShowMenu();
     }
+    // addRecords / setRecords 等数据变更路径可能会 clearCells + recreate scenegraph，
+    // 选区组件挂在 overlay 下会被清空，因此需要在场景树重建完成后按 state 重新创建选区图元。
     if (this.table.stateManager.select.ranges?.length) {
       this.recreateAllSelectRangeComponents();
     }
@@ -1964,6 +1966,9 @@ export class Scenegraph {
     const rightFrozenStartX =
       -this.table.getRightFrozenColsOffset() + (this.table.getRightFrozenColsScrollLeft?.() ?? 0);
     const rightFrozenContentWidth = this.table.getRightFrozenColsContentWidth();
+    // rightFrozenStartX 需要同时考虑“右冻结内容溢出量”与“右冻结滚动位置”：
+    // - 右冻结内容默认是贴在最右侧的，因此需要先整体向左偏移 offset（使右侧内容尾部对齐视口）
+    // - 再加上 scrollLeft（在视口内左右移动查看隐藏的列）
     // 更新各列x&col
     updateContainerChildrenX(this.cornerHeaderGroup, frozenStartX);
     updateContainerChildrenX(this.rowHeaderGroup, frozenStartX);
@@ -2027,6 +2032,7 @@ export class Scenegraph {
   }
 
   setRightFrozenColsScrollLeft(left: number) {
+    // rightStartX 以“右冻结内容右对齐”为基准，再叠加 scrollLeft 在视口内平移
     const rightStartX = -this.table.getRightFrozenColsOffset() + left;
     updateContainerChildrenX(this.rightFrozenGroup, rightStartX);
     updateContainerChildrenX(this.rightTopCornerGroup, rightStartX);
diff --git a/packages/vtable/src/scenegraph/select/create-select-border.ts b/packages/vtable/src/scenegraph/select/create-select-border.ts
index ac1cfee3f..8a731ca19 100644
--- a/packages/vtable/src/scenegraph/select/create-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/create-select-border.ts
@@ -3,6 +3,18 @@ import type { CellSubLocation } from '../../ts-types';
 import type { Scenegraph } from '../scenegraph';
 import type { BaseTableAPI } from '../../ts-types/base-table';
 
+/**
+ * 选框（select border）的创建入口。
+ *
+ * 选框图元并不是直接挂在 tableGroup 下，而是挂在各区域对应的 overlayGroup（select-overlay）下：
+ * - body / columnHeader / rowHeader / cornerHeader
+ * - rightFrozen / rightTopCorner / rightBottomCorner
+ * - bottomFrozen / leftBottomCorner
+ *
+ * 这样做的原因：
+ * - overlay 会参与各区域的 clipRect 裁剪，避免选框越界绘制
+ * - 当选择范围跨越多个区域（例如跨表头+body、跨左冻结+body、跨右冻结），需要拆成多段选框分别绘制
+ */
 export function createCellSelectBorder(
   scene: Scenegraph,
   start_Col: number,
@@ -14,6 +26,10 @@ export function createCellSelectBorder(
   strokes: boolean[]
   // isHasFillHandleRect: boolean
 ) {
+  // fill handle（右下角小方块）只在“单一选区 + body 区域 + 允许显示”的场景生效：
+  // - 多选区时避免出现多个 handle（交互语义不明确），并清理历史 handle
+  // - 选区包含表头时禁止显示（表头不是可填充的目标）
+  // - strokes 关闭底边/右边时（例如跨区域拆分后该边由其它段负责），也不应显示 handle
   let isHasFillHandleRect = !!scene.table.options.excelOptions?.fillHandle;
   if (scene.table.stateManager.select.ranges?.length > 1) {
     isHasFillHandleRect = false;
@@ -38,6 +54,8 @@ export function createCellSelectBorder(
   const startRow = Math.min(start_Row, end_Row);
   const endCol = Math.max(start_Col, end_Col);
   const endRow = Math.max(start_Row, end_Row);
+  // overlayGroup 在不同区域会有各自的坐标偏移（例如 columnHeader / body / rightFrozen），
+  // 选框需要用 global bounds（全局 AABB）减去 tableGroup + overlayGroup 的偏移，换算为 overlay 的本地坐标。
   const overlayGroup = scene.getSelectOverlayGroup(selectRangeType);
   const offsetX = scene.tableGroup.attribute.x + (overlayGroup.attribute.x ?? 0);
   const offsetY = scene.tableGroup.attribute.y + (overlayGroup.attribute.y ?? 0);
@@ -80,6 +98,7 @@ export function createCellSelectBorder(
   // 创建右下角小方块
   let fillhandle;
   if (isHasFillHandleRect) {
+    // 6x6 的 handle，定位在选区右下角（右/下各内缩 3px），避免压住边框线
     fillhandle = createRect({
       pickable: false,
       fill: bodyClickBorderColor as string,
@@ -94,6 +113,8 @@ export function createCellSelectBorder(
     });
   }
   scene.lastSelectId = selectId;
+  // key 以“标准化后的 start/end + selectId”组成：同一选择范围在不同区域拆分后 key 不同，
+  // 但 selectId 相同，可用于 shift 续选等场景的删除/替换。
   scene.selectingRangeComponents.set(`${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`, {
     rect,
     fillhandle,
diff --git a/packages/vtable/src/scenegraph/select/delete-select-border.ts b/packages/vtable/src/scenegraph/select/delete-select-border.ts
index eb262242a..cb44172f9 100644
--- a/packages/vtable/src/scenegraph/select/delete-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/delete-select-border.ts
@@ -4,6 +4,8 @@ import type { CellSubLocation } from '../../ts-types';
 
 /** 按住shift 则继续上次选中范围 需要将现有的删除掉 */
 export function deleteLastSelectedRangeComponents(scene: Scenegraph) {
+  // lastSelectId 用于标识“上一次选择动作”的选区段落（可能拆分为多段，selectId 相同）。
+  // shift 续选时，需要移除上一次的选区图元，再将本次选区追加到 selectedRangeComponents。
   scene.selectedRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       const lastSelectId = key.split('-')[4];
@@ -17,6 +19,7 @@ export function deleteLastSelectedRangeComponents(scene: Scenegraph) {
 }
 
 export function deleteAllSelectBorder(scene: Scenegraph) {
+  // 清理最终选中态选区（包含 fill handle）
   scene.selectedRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       selectComp.rect.delete();
@@ -27,6 +30,7 @@ export function deleteAllSelectBorder(scene: Scenegraph) {
 }
 
 export function deleteAllSelectingBorder(scene: Scenegraph) {
+  // 清理拖拽/框选过程中的临时选区
   scene.selectingRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       selectComp.rect.delete();
@@ -37,6 +41,7 @@ export function deleteAllSelectingBorder(scene: Scenegraph) {
 }
 
 export function removeFillHandleFromSelectComponents(scene: Scenegraph) {
+  // 多选区时不展示 fill handle，需要从所有 selectedRangeComponents 中移除
   scene.selectedRangeComponents.forEach(
     (selectComp: { rect: IRect; fillhandle?: IRect; role: CellSubLocation }, key: string) => {
       selectComp.fillhandle?.delete();
diff --git a/packages/vtable/src/scenegraph/select/move-select-border.ts b/packages/vtable/src/scenegraph/select/move-select-border.ts
index cdcb58cab..4ead094d5 100644
--- a/packages/vtable/src/scenegraph/select/move-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/move-select-border.ts
@@ -1,6 +1,10 @@
 import type { Scenegraph } from '../scenegraph';
 
 export function moveSelectingRangeComponentsToSelectedRangeComponents(scene: Scenegraph) {
+  // selectingRangeComponents 是拖拽/框选过程中的临时选区；
+  // 鼠标松开后需要将其“提交”为 selectedRangeComponents，用于后续 hover/滚动时持续更新位置。
+  //
+  // 若 selectedRangeComponents 已存在同 key 的段落（例如重复选择同一区域），先删除旧图元避免泄漏与重复绘制。
   scene.selectingRangeComponents.forEach((rangeComponent, key) => {
     if (scene.selectedRangeComponents.get(key)) {
       scene.selectingRangeComponents.get(key).rect.delete();
diff --git a/packages/vtable/src/scenegraph/select/update-custom-select-border.ts b/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
index f0ddccd5e..b4ae967d3 100644
--- a/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/update-custom-select-border.ts
@@ -12,6 +12,9 @@ export function updateCustomSelectBorder(
   selectRange: CellRange & { skipBodyMerge?: boolean },
   style: CustomSelectionStyle
 ) {
+  // 自定义选区（CustomSelectionStyle）与默认选区的拆分方式一致：
+  // 仍然需要按 corner/headers/body/rightFrozen/bottomFrozen 等区域拆分绘制，
+  // 以便配合各区域的 clipRect，避免跨区域绘制导致的越界或重复描边。
   const table = scene.table;
   const newStartCol = selectRange.start.col;
   const newStartRow = selectRange.start.row;
@@ -200,6 +203,7 @@ function createCustomCellSelectBorder(
   strokes: boolean[],
   style: CustomSelectionStyle
 ) {
+  // 自定义选区只负责绘制 rect（不包含 fill handle），其 key 与默认选区保持一致，便于统一更新/删除。
   const startCol = Math.min(start_Col, end_Col);
   const startRow = Math.min(start_Row, end_Row);
   const endCol = Math.max(start_Col, end_Col);
diff --git a/packages/vtable/src/scenegraph/select/update-select-border.ts b/packages/vtable/src/scenegraph/select/update-select-border.ts
index a27948099..23685e1b1 100644
--- a/packages/vtable/src/scenegraph/select/update-select-border.ts
+++ b/packages/vtable/src/scenegraph/select/update-select-border.ts
@@ -6,6 +6,10 @@ import { calculateCellRangeDistribution } from '../utils/cell-pos';
 import { TABLE_EVENT_TYPE } from '../../core/TABLE_EVENT_TYPE';
 
 export function updateAllSelectComponent(scene: Scenegraph) {
+  // 三类选区组件需要统一更新：
+  // - customSelectedRangeComponents：自定义样式选区（独立于默认 selectionStyle）
+  // - selectingRangeComponents：拖拽/框选过程中的临时选区
+  // - selectedRangeComponents：最终选中态选区
   scene.customSelectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellSubLocation }, key: string) => {
     updateComponent(selectComp, key, scene);
   });
@@ -28,6 +32,7 @@ function updateComponent(
   scene: Scenegraph
 ) {
   const table = scene.table;
+  // key: `${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`
   const [startColStr, startRowStr, endColStr, endRowStr] = key.split('-');
   const startCol = parseInt(startColStr, 10);
   const startRow = parseInt(startRowStr, 10);
@@ -42,6 +47,7 @@ function updateComponent(
   let visibleCellRange;
   switch (selectComp.role) {
     case 'rowHeader':
+      // rowHeader 的选区高度跟随 body 可视行范围（避免选框覆盖不可见区域造成性能浪费）
       visibleCellRange = table.getBodyVisibleRowRange();
       if (visibleCellRange) {
         computeRectCellRangeStartRow = Math.max(startRow, visibleCellRange.rowStart - 1);
@@ -49,6 +55,7 @@ function updateComponent(
       }
       break;
     case 'columnHeader':
+      // columnHeader 的选区宽度跟随 body 可视列范围
       visibleCellRange = table.getBodyVisibleCellRange();
       if (visibleCellRange) {
         computeRectCellRangeStartCol = Math.max(startCol, visibleCellRange.colStart - 1);
@@ -58,6 +65,7 @@ function updateComponent(
     case 'cornerHeader':
       break;
     case 'bottomFrozen':
+      // bottomFrozen 的横向范围依赖 body 的可视列范围
       visibleCellRange = table.getBodyVisibleCellRange();
       if (visibleCellRange) {
         computeRectCellRangeStartCol = Math.max(startCol, visibleCellRange.colStart - 1);
@@ -65,6 +73,7 @@ function updateComponent(
       }
       break;
     case 'rightFrozen':
+      // rightFrozen 的纵向范围依赖 body 的可视行范围
       visibleCellRange = table.getBodyVisibleCellRange();
       if (visibleCellRange) {
         computeRectCellRangeStartRow = Math.max(startRow, visibleCellRange.rowStart - 1);
@@ -78,6 +87,7 @@ function updateComponent(
     case 'rightBottomCorner':
       break;
     default:
+      // body 区选框默认同时裁剪行列范围
       visibleCellRange = table.getBodyVisibleCellRange();
       if (visibleCellRange) {
         computeRectCellRangeStartRow = Math.max(startRow, visibleCellRange.rowStart - 1);
@@ -97,6 +107,7 @@ function updateComponent(
   const offsetX = scene.tableGroup.attribute.x + (overlayGroup.attribute.x ?? 0);
   const offsetY = scene.tableGroup.attribute.y + (overlayGroup.attribute.y ?? 0);
 
+  // 使用第一个单元格的 global bounds 来确定选框左上角，换算为 overlay 本地坐标。
   const firstCellBound = scene.highPerformanceGetCell(
     computeRectCellRangeStartCol,
     computeRectCellRangeStartRow
@@ -126,6 +137,9 @@ function updateComponent(
     }
     //#region 计算填充柄小方块的位置
 
+    // fill handle 的定位基于“选区右下角单元格”的 global bounds。
+    // 当选区贴边（最后一列/最后一行）时，需要用相邻单元格 bounds 来推导一个合理的 handle 位置，
+    // 否则 handle 会超出 table/clip 区域导致不可见或命中异常。
     let lastCellBound;
     let handlerX;
     //当选择区域没有到到最后一列时
@@ -165,6 +179,8 @@ function updateComponent(
   }
 
   //#region 处理边缘被截问题
+  // 选框边框线绘制在 rect 边缘的中心（half line width），当选区贴着表格最外边界时，
+  // 有一半线宽会被 clip 或 canvas 边界裁掉。这里通过收缩/偏移 rect 的宽高来避免“半条线”效果。
   let diffSize = 0;
   if (typeof selectComp.rect.attribute.lineWidth === 'number') {
     diffSize = Math.ceil(selectComp.rect.attribute.lineWidth / 2);
@@ -238,6 +254,8 @@ export function updateCellSelectBorder(
   let endRow = Math.min(Math.max(newEndRow, newStartRow), table.rowCount - 1);
   //#region region 校验四周的单元格有没有合并的情况，如有则扩大范围
   const extendSelectRange = () => {
+    // 若选区边缘触达合并单元格的一部分，需要扩大选区以覆盖整个 merge block。
+    // 这里采用递归扩展：一旦发生扩展，继续检查新边界直到收敛。
     let isExtend = false;
     for (let col = startCol; col <= endCol; col++) {
       if (col === startCol) {
@@ -347,6 +365,9 @@ export function updateCellSelectBorder(
     needRightBottomCornerHeader
   } = calculateCellRangeDistribution(startCol, startRow, endCol, endRow, table);
 
+  // 选区可能跨越多个区域（例如 cornerHeader/columnHeader/body/rightFrozen/bottomFrozen 等），
+  // 需要按区域拆分成多个 select border。每段的 strokeArray 用来控制四条边是否绘制，
+  // 避免跨区域边界处出现“重复描边”（两段都画同一条边会显得更粗）。
   // TODO 可以尝试不拆分三个表头和body【前提是theme中合并配置】 用一个SelectBorder 需要结合clip，并动态设置border的范围【依据区域范围 已经是否跨表头及body】
   if (needCornerHeader) {
     const cornerEndCol = Math.min(endCol, table.frozenColCount - 1);
diff --git a/packages/vtable/src/scenegraph/select/update-select-style.ts b/packages/vtable/src/scenegraph/select/update-select-style.ts
index 072562f45..a14c86987 100644
--- a/packages/vtable/src/scenegraph/select/update-select-style.ts
+++ b/packages/vtable/src/scenegraph/select/update-select-style.ts
@@ -3,6 +3,8 @@ import type { Scenegraph } from '../scenegraph';
 
 // for fs big screen
 export function temporarilyUpdateSelectRectStyle(rectAttribute: IRectGraphicAttribute, scene: Scenegraph) {
+  // 临时覆盖选区样式（例如大屏模式下增强可见性）。
+  // 该方法只更新最终选中态（selectedRangeComponents），不会影响 selectingRangeComponents（拖拽过程态）。
   const { selectedRangeComponents } = scene;
   selectedRangeComponents.forEach((selectComp: { rect: IRect }, key: string) => {
     selectComp.rect.setAttributes(rectAttribute);
diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts
index 30a6cf88f..51ae5ba3c 100644
--- a/packages/vtable/src/state/state.ts
+++ b/packages/vtable/src/state/state.ts
@@ -179,7 +179,9 @@ export class StateManager {
   scroll: {
     horizontalBarPos: number;
     verticalBarPos: number;
+    // 左侧冻结区域内部横向滚动位置（单位：px）。仅在 scrollFrozenCols 开启且存在溢出时生效。
     frozenHorizontalBarPos: number;
+    // 右侧冻结区域内部横向滚动位置（单位：px）。仅在 scrollRightFrozenCols 开启且存在溢出时生效。
     rightFrozenHorizontalBarPos: number;
   };
   tablePosition: {
@@ -1031,6 +1033,7 @@ export class StateManager {
       return;
     }
     this.scroll.frozenHorizontalBarPos = left;
+    // 左冻结滚动条的 0~1 比例与 scrollLeft 同向：ratio = left / maxScrollLeft
     const ratio = maxScrollLeft ? left / maxScrollLeft : 0;
     this.table.scenegraph.component.updateFrozenHorizontalScrollBarPos(ratio);
     triggerRender && this.table.scenegraph.setFrozenColsScrollLeft(left);
@@ -1047,6 +1050,9 @@ export class StateManager {
       return;
     }
     this.scroll.rightFrozenHorizontalBarPos = left;
+    // 右冻结的视觉“展开方向”与 left 值相反（right frozen 的内容从右往左展开）。
+    // 为了让滚动条 thumb 的移动方向更符合直觉，这里将滚动条 ratio 做反向映射：
+    // ratio = 1 - left / maxScrollLeft
     const ratio = maxScrollLeft ? 1 - left / maxScrollLeft : 1;
     this.table.scenegraph.component.updateRightFrozenHorizontalScrollBarPos(ratio);
     triggerRender && this.table.scenegraph.setRightFrozenColsScrollLeft(left);
@@ -1054,6 +1060,7 @@ export class StateManager {
 
   updateFrozenHorizontalScrollBar(xRatio: number) {
     const maxScrollLeft = this.table.getFrozenColsOffset?.() ?? 0;
+    // 由滚动条 ratio 反推左冻结 scrollLeft（同向）
     let left = Math.ceil(xRatio * maxScrollLeft);
     if (!isValid(left) || isNaN(left)) {
       left = 0;
@@ -1063,6 +1070,7 @@ export class StateManager {
 
   updateRightFrozenHorizontalScrollBar(xRatio: number) {
     const maxScrollLeft = this.table.getRightFrozenColsOffset?.() ?? 0;
+    // 由滚动条 ratio 反推右冻结 scrollLeft（反向）
     let left = Math.ceil((1 - xRatio) * maxScrollLeft);
     if (!isValid(left) || isNaN(left)) {
       left = 0;
diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts
index 794456d2c..23faf72f6 100644
--- a/packages/vtable/src/ts-types/base-table.ts
+++ b/packages/vtable/src/ts-types/base-table.ts
@@ -317,10 +317,35 @@ export interface BaseTableConstructorOptions {
   bottomFrozenRowCount?: number;
   /** 最大冻结宽度，固定值 or 百分比。默认为'80%' */
   maxFrozenWidth?: number | string;
+  /**
+   * 右侧最大冻结宽度，固定值 or 百分比。
+   *
+   * - 仅在 `rightFrozenColCount > 0` 时有意义
+   * - 默认与 `maxFrozenWidth` 保持一致（便于左右冻结行为对齐）
+   * - 当 `scrollRightFrozenCols` 开启时，该值决定右侧冻结区域的“视口宽度上限”
+   */
   maxRightFrozenWidth?: number | string;
   /** 超过最大冻结宽度后是否全部解冻，默认true */
   unfreezeAllOnExceedsMaxWidth?: boolean;
+  /**
+   * 是否允许左侧冻结区域内部横向滚动。
+   *
+   * 当左侧冻结列的“内容总宽度”超过 `maxFrozenWidth` 时：
+   * - `false`（默认）：冻结列会按 `unfreezeAllOnExceedsMaxWidth` 的策略自动解冻以适配视口
+   * - `true`：保留全部冻结列，并在左侧冻结区域内通过触摸板横向滚动/滚动条查看超出部分
+   *
+   * 该能力会引入一个独立的滚动域（frozen），对应 `getFrozenColsScrollLeft/getFrozenColsOffset`。
+   */
   scrollFrozenCols?: boolean;
+  /**
+   * 是否允许右侧冻结区域内部横向滚动。
+   *
+   * 当右侧冻结列的“内容总宽度”超过 `maxRightFrozenWidth` 时：
+   * - `false`（默认）：右侧冻结区域宽度等于内容宽度（不会出现内部横向滚动）
+   * - `true`：保留全部右侧冻结列，并在右侧冻结区域内通过触摸板横向滚动/滚动条查看超出部分
+   *
+   * 该能力会引入一个独立的滚动域（rightFrozen），对应 `getRightFrozenColsScrollLeft/getRightFrozenColsOffset`。
+   */
   scrollRightFrozenCols?: boolean;
 
   // /** 待实现 TODO */

From c2cd599be3992e06e2c6b1d7f128b5c6938b08f1 Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Tue, 24 Mar 2026 19:01:58 +0800
Subject: [PATCH 11/16] feat: add option scrollFrozenColsPassThroughToBody

---
 docs/assets/guide/en/interaction/scroll.md    |  5 +++-
 docs/assets/guide/zh/interaction/scroll.md    |  5 +++-
 .../option/en/common/option-secondary.md      |  4 ++++
 .../option/zh/common/option-secondary.md      |  4 ++++
 .../list-table-scrollable-frozen-cols.ts      |  1 +
 packages/vtable/src/event/scroll.ts           | 24 +++++++++++++++----
 packages/vtable/src/ts-types/base-table.ts    | 12 ++++++++++
 7 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/docs/assets/guide/en/interaction/scroll.md b/docs/assets/guide/en/interaction/scroll.md
index b61ee53df..929d6cc27 100644
--- a/docs/assets/guide/en/interaction/scroll.md
+++ b/docs/assets/guide/en/interaction/scroll.md
@@ -124,6 +124,8 @@ After enabling frozen columns, if the frozen area's total frozen width exceeds t
 
 After enabling, trackpad horizontal scrolling works inside the corresponding frozen area (without scrolling the body first). When scrollbars are visible, independent horizontal scrollbars for frozen areas will appear at the bottom, supporting dragging the thumb or clicking the track.
 
+By default (`scrollFrozenColsPassThroughToBody: false`), when you reach the start/end of a frozen area, the body will not scroll. If you want to continue scrolling the body after the frozen area hits its boundary, enable this option.
+
 Example:
 
 ```javascript
@@ -135,7 +137,8 @@ const option = {
 
   rightFrozenColCount: 4,
   maxRightFrozenWidth: 320,
-  scrollRightFrozenCols: true
+  scrollRightFrozenCols: true,
+  scrollFrozenColsPassThroughToBody: false
 };
 ```
 
diff --git a/docs/assets/guide/zh/interaction/scroll.md b/docs/assets/guide/zh/interaction/scroll.md
index 094de00cd..38e987767 100644
--- a/docs/assets/guide/zh/interaction/scroll.md
+++ b/docs/assets/guide/zh/interaction/scroll.md
@@ -124,6 +124,8 @@ VTable 支持在按住 Shift 键时进行横向滚动，或者直接拖拽横向
 
 开启后，触摸板的横向滚动会在对应冻结区域内生效（无需先滚动 body），并且在滚动条可见时底部会出现对应冻结区域的独立横向滚动条，支持拖拽滑块或点击轨道进行滚动。
 
+默认情况下（`scrollFrozenColsPassThroughToBody: false`），在冻结区域滚动到头/尾时不会联动滚动 body；如希望在冻结区域到边界后继续滚动 body，可开启该配置。
+
 示例：
 
 ```javascript
@@ -135,7 +137,8 @@ const option = {
 
   rightFrozenColCount: 4,
   maxRightFrozenWidth: 320,
-  scrollRightFrozenCols: true
+  scrollRightFrozenCols: true,
+  scrollFrozenColsPassThroughToBody: false
 };
 ```
 
diff --git a/docs/assets/option/en/common/option-secondary.md b/docs/assets/option/en/common/option-secondary.md
index bf13145fa..abc43c208 100644
--- a/docs/assets/option/en/common/option-secondary.md
+++ b/docs/assets/option/en/common/option-secondary.md
@@ -116,6 +116,10 @@ Maximum freeze width for right frozen columns (fixed value or percentage). Defau
 
 When the total width of right frozen columns exceeds maxRightFrozenWidth, the right frozen area becomes horizontally scrollable. When enabled, all right frozen columns are kept and you can use trackpad horizontal scrolling or drag the scrollbar inside the frozen area to view the overflow part.
 
+#${prefix} scrollFrozenColsPassThroughToBody(boolean) = false
+
+When horizontally scrolling inside a frozen area (left frozen / right frozen) reaches its boundary, whether to pass the scroll intent through to the body horizontal scrolling. Default is false, which means the body will not scroll when the frozen area is already at its start/end. When enabled, body horizontal scrolling will be triggered if the frozen area can no longer scroll.
+
 #${prefix} unfreezeAllOnExceedsMaxWidth(boolean) = true
 
 Whether to defrost after the maximum freezing width is exceeded. The default value is true. If set to false, it will not unfreeze all columns, but will determine the number of columns to be unfrozen according to the value of maxFrozenWidth.
diff --git a/docs/assets/option/zh/common/option-secondary.md b/docs/assets/option/zh/common/option-secondary.md
index 991c3d3f2..cc560547b 100644
--- a/docs/assets/option/zh/common/option-secondary.md
+++ b/docs/assets/option/zh/common/option-secondary.md
@@ -120,6 +120,10 @@ containerFit: {
 
 当右侧冻结列总宽度超过 maxRightFrozenWidth 时，右侧冻结区域可横向滚动。开启后会保留全部右侧冻结列，并在冻结区域内通过触摸板横向滚动或拖拽滚动条查看超出部分。
 
+#${prefix} scrollFrozenColsPassThroughToBody(boolean) = false
+
+当在冻结区域（左冻结/右冻结）内横向滚动到边界时，是否将滚动意图“透传”给 body 横向滚动。默认 false，即在冻结区域滚动到头/尾也不会触发 body 滚动；开启后在冻结区域无法继续滚动时会自动联动滚动 body。
+
 #${prefix} unfreezeAllOnExceedsMaxWidth(boolean) = true
 
 超过最大冻结宽度后是否全部解冻，默认 true。如果设置为 false，则不会解冻全部列，而是根据 maxFrozenWidth 的值来决定最终解冻的列数。
diff --git a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
index 3540aa758..9afcba0f6 100644
--- a/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
+++ b/packages/vtable/examples/frozen/list-table-scrollable-frozen-cols.ts
@@ -48,6 +48,7 @@ export function createTable() {
     scrollFrozenCols: true,
     maxRightFrozenWidth: 320,
     scrollRightFrozenCols: true,
+    scrollFrozenColsPassThroughToBody: true,
     theme: VTable.themes.DEFAULT.extends({
       scrollStyle: {
         visible: 'scrolling'
diff --git a/packages/vtable/src/event/scroll.ts b/packages/vtable/src/event/scroll.ts
index f3aec7e4a..6932f05df 100644
--- a/packages/vtable/src/event/scroll.ts
+++ b/packages/vtable/src/event/scroll.ts
@@ -29,6 +29,8 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
   const verticalAutoHide = verticalVisible === 'scrolling';
   let usedFrozenHorizontal = false;
   let usedRightFrozenHorizontal = false;
+  let blockedFrozenHorizontal = false;
+  let blockedRightFrozenHorizontal = false;
   if (optimizedDeltaX) {
     // 横向滚动需要判断“滚动意图属于哪个区域”：
     // - body：主滚动域（影响非冻结列）
@@ -70,8 +72,13 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
         usedFrozenHorizontal = true;
         state.showHorizontalScrollBar(horizontalAutoHide, 'frozen');
       } else {
-        state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
-        state.showHorizontalScrollBar(horizontalAutoHide, 'body');
+        if (state.table.options.scrollFrozenColsPassThroughToBody) {
+          state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
+          state.showHorizontalScrollBar(horizontalAutoHide, 'body');
+        } else {
+          blockedFrozenHorizontal = true;
+          state.showHorizontalScrollBar(horizontalAutoHide, 'frozen');
+        }
       }
     } else if (rightFrozenColsScrollable && isInRightFrozenViewport) {
       const maxRightFrozenScrollLeft = state.table.getRightFrozenColsOffset();
@@ -87,8 +94,13 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
         usedRightFrozenHorizontal = true;
         state.showHorizontalScrollBar(horizontalAutoHide, 'rightFrozen');
       } else {
-        state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
-        state.showHorizontalScrollBar(horizontalAutoHide, 'body');
+        if (state.table.options.scrollFrozenColsPassThroughToBody) {
+          state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
+          state.showHorizontalScrollBar(horizontalAutoHide, 'body');
+        } else {
+          blockedRightFrozenHorizontal = true;
+          state.showHorizontalScrollBar(horizontalAutoHide, 'rightFrozen');
+        }
       }
     } else {
       state.setScrollLeft(state.scroll.horizontalBarPos + optimizedDeltaX, event);
@@ -107,7 +119,9 @@ export function handleWhell(event: FederatedWheelEvent, state: StateManager, isW
       (Math.abs(deltaY) >= Math.abs(deltaX) && deltaY !== 0 && isVerticalScrollable(deltaY, state)) ||
       (Math.abs(deltaY) <= Math.abs(deltaX) &&
         deltaX !== 0 &&
-        (usedFrozenHorizontal
+        (blockedFrozenHorizontal || blockedRightFrozenHorizontal
+          ? false
+          : usedFrozenHorizontal
           ? state.table.getFrozenColsOffset() > 0
           : usedRightFrozenHorizontal
           ? state.table.getRightFrozenColsOffset() > 0
diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts
index 23faf72f6..4c9811d85 100644
--- a/packages/vtable/src/ts-types/base-table.ts
+++ b/packages/vtable/src/ts-types/base-table.ts
@@ -348,6 +348,18 @@ export interface BaseTableConstructorOptions {
    */
   scrollRightFrozenCols?: boolean;
 
+  /**
+   * 冻结区域滚动到边界时，是否自动“透传”给 body 横向滚动。
+   *
+   * - `false`（默认）：在冻结区域内滚动时，即使滚动到头/尾也不会触发 body 横向滚动
+   * - `true`：当冻结区域无法继续滚动时，将剩余滚动意图交由 body 横向滚动处理
+   *
+   * 说明：
+   * - 仅对鼠标滚轮/触摸板触发的横向滚动（wheel）生效
+   * - 仅在 `scrollFrozenCols` / `scrollRightFrozenCols` 开启且对应区域存在溢出（offset > 0）时才有意义
+   */
+  scrollFrozenColsPassThroughToBody?: boolean;
+
   // /** 待实现 TODO */
   // frozenRowCount?: number;
   /** 可冻结列数，表示前多少列会出现冻结操作按钮 基本表格生效 */

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 12/16] 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 = `<svg width="100" height="200" xmlns="http://www.w3.o
   <line x1="70" y1="10" x2="70" y2="190" stroke="black" stroke-width="4"/>
 </svg>`;
 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 13/16] 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 14/16] 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 15/16] 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]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 08:26:48 +0000
Subject: [PATCH 16/16] 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",
