import { createTable, filterPlugin, paginationPlugin, selectionPlugin, sortPlugin, } from '../dist/index.js'; const users = [ { id: 1, name: 'Charlie', age: 35, city: 'Shenzhen' }, { id: 2, name: 'Alice', age: 28, city: 'Beijing' }, { id: 3, name: 'Bob', age: 31, city: 'Shanghai' }, { id: 4, name: 'Dave', age: 22, city: 'Beijing' }, { id: 5, name: 'Eve', age: 29, city: 'Shanghai' }, ]; const table = createTable({ data: users, columns: [ { key: 'name', header: 'Name', sortable: true }, { key: 'age', header: 'Age', sortable: true }, { key: 'city', header: 'City' }, ], rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin(), selectionPlugin()], initialState: { pagination: { page: 1, pageSize: 2 } }, }); /*** 断言工具:失败即退出 ***/ const assert = (cond, msg) => { if (!cond) { console.error('FAIL:', msg); process.exit(1); } console.log('ok -', msg); }; table.dispatch({ type: 'toggleSort', key: 'name' }); // 按名称升序 assert( JSON.stringify(table.getVisibleRows().rows.map((r) => r.name)) === JSON.stringify(['Alice', 'Bob']), 'sort by name asc + page 1 of size 2', ); table.dispatch({ type: 'setFilter', key: 'city', value: 'shang' }); // 筛选上海 const filtered = table.getVisibleRows(); assert( JSON.stringify(filtered.rows.map((r) => r.name)) === JSON.stringify(['Bob', 'Eve']) && filtered.total === 2, 'filter city~sh keeps Shanghai rows, total=2', ); assert(table.getPageInfo().totalPages === 1, 'totalPages computed from filtered total'); table.dispatch({ type: 'clearFilters' }); // 清筛选回到全量 table.dispatch({ type: 'setPage', page: 2 }); assert( JSON.stringify(table.getVisibleRows().rows.map((r) => r.name)) === JSON.stringify(['Charlie', 'Dave']), 'page 2 after clearing filter', ); assert(table.getPageInfo().page === 2 && table.getPageInfo().totalPages === 3, 'page info total=3 pages'); table.dispatch({ type: 'toggleSelect', key: 3 }); // 勾选行 3 assert(JSON.stringify(table.getState().selection) === JSON.stringify([3]), 'toggleSelect adds rowKey'); table.selectAllVisible(); // 全选当前页(替换语义) assert( JSON.stringify(table.getState().selection) === JSON.stringify([1, 4]), 'selectAllVisible replaces selection with visible rowKeys', ); table.dispatch({ type: 'setPage', page: 99 }); // 越界页码 const clamped = table.getVisibleRows(); assert( table.getPageInfo().page === 3 && clamped.rows.length === 1, 'page clamped to totalPages', ); const before = table.getVisibleRows(); // memo 基线 table.dispatch({ type: 'clearSelection' }); // 无关状态变更 assert(table.getVisibleRows() === before, 'derive stages memoized across unrelated state changes'); // —— 补充不变量 —— const fresh = createTable({ data: users, columns: [{ key: 'name', header: 'Name', sortable: true }], rowKey: 'id', plugins: [paginationPlugin()], initialState: { pagination: { page: 2, pageSize: 2 } }, }); assert( fresh.getPageInfo().page === 2 && fresh.getPageInfo().pageSize === 2, 'getPageInfo 无需先取可见行即返回 initialState 生效值', ); assert(fresh.getPageInfo().totalPages === 3, 'getPageInfo 的 totalPages 由上游 total 推导'); const marks = createTable({ data: users, columns: [{ key: 'name', header: 'Name' }, { key: 'city', header: 'City' }], rowKey: 'id', plugins: [filterPlugin(), paginationPlugin(), selectionPlugin()], initialState: { pagination: { page: 1, pageSize: 2 } }, }); marks.selectAllVisible(); // 选中第 1 页的 id 1 / 2 marks.dispatch({ type: 'setFilter', key: 'city', value: 'Beijing' }); // 可见行整体换掉 assert( JSON.stringify(marks.getState().selection) === JSON.stringify([1, 2]), 'selection 以 rowKey 记账,筛选换掉可见行后仍保留', ); const toggle = createTable({ data: users, columns: [{ key: 'age', header: 'Age', sortable: true }], rowKey: 'id', plugins: [sortPlugin()], }); toggle.dispatch({ type: 'toggleSort', key: 'age' }); toggle.dispatch({ type: 'toggleSort', key: 'age' }); toggle.dispatch({ type: 'toggleSort', key: 'age' }); assert(toggle.getState().sorting.length === 0, '同一列连点三次回到无序(asc → desc → 取消)'); const multi = createTable({ data: users, columns: [{ key: 'city', header: 'City' }, { key: 'age', header: 'Age', sortable: true }], rowKey: 'id', plugins: [sortPlugin({ multi: true })], }); multi.dispatch({ type: 'toggleSort', key: 'city', multi: true }); multi.dispatch({ type: 'toggleSort', key: 'age', multi: true }); assert(multi.getState().sorting.length === 2, 'multi 排序保留两列规则'); assert( JSON.stringify(multi.getVisibleRows().rows.map((r) => r.id)) === JSON.stringify([4, 2, 5, 3, 1]), '多列排序按规则顺序生效(city 升序,同城内 age 升序:Beijing 4→2、Shanghai 5→3)', ); const sizing = createTable({ data: users, columns: [{ key: 'name', header: 'Name' }], rowKey: 'id', plugins: [paginationPlugin()], initialState: { pagination: { page: 1, pageSize: 2 } }, }); sizing.dispatch({ type: 'setPage', page: 2 }); sizing.dispatch({ type: 'setPageSize', pageSize: 3 }); assert(sizing.getPageInfo().page === 1 && sizing.getPageInfo().pageSize === 3, '改页大小回到第 1 页'); sizing.dispatch({ type: 'toggleSort', key: 'name' }); sizing.dispatch({ type: 'reset' }); assert( sizing.getState().sorting.length === 0 && sizing.getState().filters.length === 0 && sizing.getState().pagination.page === 1 && sizing.getState().pagination.pageSize === 20, 'reset 回到默认态(排序/筛选清空,分页回 1/20)', ); // getAllRows:过筛选 + 排序但跳过分页,供导出用 const all = createTable({ data: users, columns: [{ key: 'name', header: 'Name' }, { key: 'city', header: 'City' }], rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin()], initialState: { pagination: { page: 1, pageSize: 2 } }, }); assert(all.getVisibleRows().rows.length === 2, 'getVisibleRows 受分页约束只给当页'); all.dispatch({ type: 'setFilter', key: 'city', value: 'Beijing' }); assert(all.getAllRows().rows.length === 2 && all.getVisibleRows().rows.length === 2, 'getAllRows 跳过分页但与筛选一致(Beijing 2 行)'); all.dispatch({ type: 'toggleSort', key: 'age' }); assert( JSON.stringify(all.getAllRows().rows.map((r) => r.id)) === JSON.stringify([4, 2]), 'getAllRows 仍按排序规则排(age 升序 4→2)', ); assert(all.getPageInfo().totalPages === 1, 'getAllRows 不影响分页快照(2 行 / 每页 2 = 1 页)'); // setRows:整体换数据源,选择状态按 rowKey 剔除失效行,页码越界由插件收敛 const swapped = createTable({ data: users, columns: [{ key: 'name', header: 'Name' }], rowKey: 'id', plugins: [selectionPlugin(), paginationPlugin()], initialState: { pagination: { page: 1, pageSize: 2 } }, }); swapped.dispatch({ type: 'toggleSelect', key: 1 }); // 选中两行,后面验证失效行被剔除 swapped.dispatch({ type: 'toggleSelect', key: 4 }); assert(swapped.isRowSelected(1) && swapped.isRowSelected(4), 'setRows 前:两行已选'); swapped.setRows(users.filter((u) => u.id !== 4)); assert(swapped.getVisibleRows().total === 4, 'setRows 后:total 跟随新数据(5 行 → 4 行)'); assert(swapped.isRowSelected(1) && !swapped.isRowSelected(4), 'setRows 后:失效行键从选择里剔除,有效行保留'); assert(swapped.getState().pagination.page === 1, 'setRows 不动页码(用户翻页意图不被打断)'); const sameRows = users.filter((u) => u.id !== 4); let notified = 0; swapped.subscribe(() => (notified += 1)); swapped.setRows(sameRows); assert(notified === 1 && swapped.getVisibleRows().total === 4, 'setRows 换引用:通知一次并重算 total'); // 页码收敛:数据缩水后越界页码范围回生效页,state 与视图保持一致(不再滞留旧页码) const clamp = createTable({ data: users, columns: [{ key: 'name', header: 'Name' }], rowKey: 'id', plugins: [paginationPlugin()], initialState: { pagination: { page: 3, pageSize: 2 } }, }); assert(clamp.getVisibleRows().rows.length === 1 && clamp.getPageInfo().page === 3, '收敛前:5 行 / 每页 2 = 3 页,停在第 3 页'); clamp.setRows(users.slice(0, 1)); // 只剩 1 行 → 总页数 1 assert(clamp.getPageInfo().page === 1 && clamp.getState().pagination.page === 1, '数据缩水后:越界页码回写为 1(state 与生效页一致)'); clamp.setRows(users); // 数据恢复 5 行 assert(clamp.getVisibleRows().rows.length === 2 && clamp.getState().pagination.page === 1, '数据恢复后:仍停第 1 页,不记忆已失效的旧页码'); clamp.dispatch({ type: 'setPage', page: 9 }); // 显式跳到越界页 assert(clamp.getVisibleRows().rows.length === 1 && clamp.getState().pagination.page === 3, '跳越界页:派生钳制后回写为末页 3'); assert(clamp.getPageInfo().totalPages === 3, '收敛不改分页快照语义(totalPages 仍按 total 推算)'); console.log('SMOKE OK');