diff --git a/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json b/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json
new file mode 100644
index 0000000000..3857992a67
--- /dev/null
+++ b/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json
@@ -0,0 +1,11 @@
+{
+ "changes": [
+ {
+ "comment": "fix: delete reverse-selected rows from context menu",
+ "type": "none",
+ "packageName": "@visactor/vtable"
+ }
+ ],
+ "packageName": "@visactor/vtable",
+ "email": "biukam.w@gmail.com"
+}
\ No newline at end of file
diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts
new file mode 100644
index 0000000000..1c777b352a
--- /dev/null
+++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts
@@ -0,0 +1,65 @@
+// @ts-nocheck
+import { ListTable } from '@visactor/vtable';
+import { createDiv } from '../../../vtable/__tests__/dom';
+import { MenuHandler } from '../../src/contextmenu/handle-menu-helper';
+import { TableSeriesNumber } from '../../src/table-series-number';
+
+global.__VERSION__ = 'none';
+
+describe('Context menu row deletion', () => {
+ let table: ListTable;
+
+ afterEach(() => {
+ table?.release();
+ document.body.innerHTML = '';
+ });
+
+ test('deletes all rows in a reverse-dragged row selection', () => {
+ const container = createDiv();
+ container.style.width = '600px';
+ container.style.height = '400px';
+
+ const seriesNumberPlugin = new TableSeriesNumber({
+ rowCount: 5,
+ colCount: 2
+ });
+ table = new ListTable({
+ container,
+ showHeader: false,
+ columns: [
+ { field: 'id', title: 'ID' },
+ { field: 'name', title: 'Name' }
+ ],
+ records: [
+ { id: 0, name: 'A' },
+ { id: 1, name: 'B' },
+ { id: 2, name: 'C' },
+ { id: 3, name: 'D' },
+ { id: 4, name: 'E' }
+ ],
+ syncRecordOperationsToSourceRecords: true,
+ plugins: [seriesNumberPlugin]
+ });
+
+ table.stateManager.select.ranges = [
+ {
+ start: { col: 0, row: 3 },
+ end: { col: table.colCount - 1, row: 1 }
+ }
+ ];
+
+ const selectCells = jest.spyOn(table, 'selectCells');
+ seriesNumberPlugin['handleSeriesNumberCellRightClick']({
+ detail: {
+ seriesNumberCell: { id: 2, name: 'row-series-number-cell' },
+ event: new MouseEvent('contextmenu')
+ }
+ });
+
+ expect(selectCells).not.toHaveBeenCalled();
+
+ new MenuHandler().handleDeleteRow(table);
+
+ expect(table.records.map(record => record.id)).toEqual([0, 4]);
+ });
+});
diff --git a/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts b/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts
new file mode 100644
index 0000000000..b0cdfcc0d7
--- /dev/null
+++ b/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts
@@ -0,0 +1,111 @@
+import * as VTable from '@visactor/vtable';
+import { MenuHandler } from '../../src/contextmenu/handle-menu-helper';
+import { TableSeriesNumber } from '../../src/table-series-number';
+
+const CONTAINER_ID = 'vTable';
+
+const initialRecords = [
+ { id: 0, name: 'A' },
+ { id: 1, name: 'B' },
+ { id: 2, name: 'C' },
+ { id: 3, name: 'D' },
+ { id: 4, name: 'E' }
+];
+
+const removeDemoToolbar = () => {
+ document.getElementById('issue5214Toolbar')?.remove();
+};
+
+const setStatus = (message: string, pass: boolean) => {
+ const statusNode = document.getElementById('issue5214Status');
+ if (!statusNode) {
+ return;
+ }
+ statusNode.textContent = message;
+ statusNode.style.color = pass ? '#237804' : '#cf1322';
+};
+
+export function createTable() {
+ removeDemoToolbar();
+
+ const container = document.getElementById(CONTAINER_ID)!;
+ container.style.width = '640px';
+ container.style.height = '360px';
+
+ const toolbar = document.createElement('div');
+ toolbar.id = 'issue5214Toolbar';
+ toolbar.style.cssText = [
+ 'display: flex',
+ 'gap: 8px',
+ 'align-items: center',
+ 'height: 48px',
+ 'font-size: 12px'
+ ].join(';');
+ toolbar.innerHTML = `
+
+
+ 预期:删除 id=1,2,3,仅保留 0 和 4。
+
+ `;
+ container.before(toolbar);
+
+ const seriesNumberPlugin = new TableSeriesNumber({
+ rowCount: 5,
+ colCount: 2
+ });
+
+ const tableInstance = new VTable.ListTable({
+ container,
+ showHeader: false,
+ columns: [
+ { field: 'id', title: 'ID', width: 120 },
+ { field: 'name', title: 'Name', width: 160 }
+ ],
+ records: initialRecords.map(record => ({ ...record })),
+ syncRecordOperationsToSourceRecords: true,
+ plugins: [seriesNumberPlugin],
+ defaultRowHeight: 36
+ });
+
+ const getRecordIds = () => tableInstance.records.map((record: { id: number }) => record.id);
+ const updateStatusFromRecords = (prefix: string, pass: boolean) => {
+ setStatus(`${prefix} | records=[${getRecordIds().join(',')}]`, pass);
+ };
+
+ document.getElementById('issue5214Reset')?.addEventListener('click', () => {
+ tableInstance.setRecords(initialRecords.map(record => ({ ...record })));
+ updateStatusFromRecords('RESET', true);
+ });
+
+ document.getElementById('issue5214Reproduce')?.addEventListener('click', () => {
+ tableInstance.stateManager.select.ranges = [
+ {
+ start: { col: 0, row: 3 },
+ end: { col: tableInstance.colCount - 1, row: 1 }
+ }
+ ];
+
+ seriesNumberPlugin['handleSeriesNumberCellRightClick']({
+ detail: {
+ seriesNumberCell: { id: 2, name: 'row-series-number-cell' },
+ event: new MouseEvent('contextmenu')
+ }
+ });
+
+ new MenuHandler().handleDeleteRow(tableInstance);
+
+ const ids = getRecordIds();
+ const pass = ids.length === 2 && ids[0] === 0 && ids[1] === 4;
+ updateStatusFromRecords(pass ? 'PASS' : 'FAIL', pass);
+ });
+
+ updateStatusFromRecords('READY', true);
+
+ const release = tableInstance.release.bind(tableInstance);
+ tableInstance.release = () => {
+ removeDemoToolbar();
+ release();
+ };
+
+ (window as any).tableInstance = tableInstance;
+}
diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts
index f5e19298ca..91bd566855 100644
--- a/packages/vtable-plugins/demo/menu.ts
+++ b/packages/vtable-plugins/demo/menu.ts
@@ -140,6 +140,10 @@ export const menus = [
path: 'context-menu',
name: 'context-menu'
},
+ {
+ path: 'context-menu',
+ name: 'issue-5214-reverse-selected-row-delete'
+ },
{
path: 'table-export',
name: 'table-export'
diff --git a/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts b/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts
index 1528a775e9..94fdf081e4 100644
--- a/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts
+++ b/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts
@@ -105,7 +105,9 @@ export class MenuHandler {
const deleteRowIndexs: number[] = [];
for (let i = 0; i < selectRanges.length; i++) {
const range = selectRanges[i];
- for (let j = range.start.row; j <= range.end.row; j++) {
+ const startRow = Math.min(range.start.row, range.end.row);
+ const endRow = Math.max(range.start.row, range.end.row);
+ for (let j = startRow; j <= endRow; j++) {
if (!deleteRowIndexs.includes(j)) {
deleteRowIndexs.push(j);
}
diff --git a/packages/vtable-plugins/src/table-series-number.ts b/packages/vtable-plugins/src/table-series-number.ts
index ea87ff781a..1c05719d6c 100644
--- a/packages/vtable-plugins/src/table-series-number.ts
+++ b/packages/vtable-plugins/src/table-series-number.ts
@@ -348,9 +348,11 @@ export class TableSeriesNumber implements pluginsDefinition.IVTablePlugin {
const rowIndex = seriesNumberCell.id;
//判断rowIndex整行是否被选中
const isRowSelected = this.table.stateManager.select.ranges.some(range => {
+ const startRow = Math.min(range.start.row, range.end.row);
+ const endRow = Math.max(range.start.row, range.end.row);
return (
- range.start.row <= rowIndex &&
- rowIndex <= range.end.row &&
+ startRow <= rowIndex &&
+ rowIndex <= endRow &&
range.start.col === 0 &&
range.end.col === this.table.colCount - 1
);