From 2fe71666ac44ac093e00790d0313a8f3bce05690 Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 21 Nov 2025 19:29:27 +0800 Subject: [PATCH 1/5] fix(Table): `disabled` state incorrectly modified by `checkAll` --- .../components/table/hooks/usePagination.tsx | 14 ++- .../components/table/hooks/useRowSelect.tsx | 116 +++++++++++------- 2 files changed, 79 insertions(+), 51 deletions(-) diff --git a/packages/components/table/hooks/usePagination.tsx b/packages/components/table/hooks/usePagination.tsx index 97c1068c97..18325796ec 100644 --- a/packages/components/table/hooks/usePagination.tsx +++ b/packages/components/table/hooks/usePagination.tsx @@ -3,6 +3,9 @@ import useConfig from '../../hooks/useConfig'; import Pagination, { PageInfo, PaginationProps } from '../../pagination'; import type { TableRowData, TdBaseTableProps } from '../type'; +export const DEFAULT_CURRENT = 1; +export const DEFAULT_PAGE_SIZE = 10; + // 分页功能包含:远程数据排序受控、远程数据排序非受控、本地数据排序受控、本地数据排序非受控 等 4 类功能 export default function usePagination(props: TdBaseTableProps, tableContentRef: React.RefObject) { const { pagination, data, disableDataPage } = props; @@ -15,7 +18,7 @@ export default function usePagination(props: TdBaseTableProps, tableContentRef: const isControlled = pagination?.current !== undefined; const calculatePaginatedData = useCallback( - (current = 1, pageSize = 10) => { + (current = DEFAULT_CURRENT, pageSize = DEFAULT_PAGE_SIZE) => { // data 数据数量超出分页大小时,则自动启动本地数据分页 const shouldPaginate = Boolean(!disableDataPage && data.length > pageSize); let newData: TableRowData[] = []; @@ -32,7 +35,7 @@ export default function usePagination(props: TdBaseTableProps, tableContentRef: ); const updateDataSourceAndPaginate = useCallback( - (current = 1, pageSize = 10) => { + (current = DEFAULT_CURRENT, pageSize = DEFAULT_PAGE_SIZE) => { const { newData, shouldPaginate } = calculatePaginatedData(current, pageSize); setIsPaginateData(shouldPaginate); setDataSource(newData); @@ -50,7 +53,7 @@ export default function usePagination(props: TdBaseTableProps, tableContentRef: // 受控情况 useEffect(() => { if (!pagination || !isControlled) return; - const [current, pageSize] = [pagination?.current || 1, pagination?.pageSize ?? 10]; + const [current, pageSize] = [pagination?.current || DEFAULT_CURRENT, pagination?.pageSize || DEFAULT_PAGE_SIZE]; updateDataSourceAndPaginate(current, pageSize); setInnerPagination({ current, pageSize }); }, [pagination, isControlled, updateDataSourceAndPaginate]); @@ -58,7 +61,10 @@ export default function usePagination(props: TdBaseTableProps, tableContentRef: // 非受控情况 useEffect(() => { if (!pagination || isControlled) return; - const [current, pageSize] = [pagination?.defaultCurrent || 1, pagination?.defaultPageSize ?? 10]; + const [current, pageSize] = [ + pagination?.defaultCurrent || DEFAULT_CURRENT, + pagination?.defaultPageSize || DEFAULT_PAGE_SIZE, + ]; updateDataSourceAndPaginate(current, pageSize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isControlled, updateDataSourceAndPaginate]); diff --git a/packages/components/table/hooks/useRowSelect.tsx b/packages/components/table/hooks/useRowSelect.tsx index b540333c6b..c0b6ea6ebd 100644 --- a/packages/components/table/hooks/useRowSelect.tsx +++ b/packages/components/table/hooks/useRowSelect.tsx @@ -1,11 +1,16 @@ // 行选中相关功能:单选 + 多选 -import React, { useEffect, useState, MouseEvent, useMemo } from 'react'; -import { intersection, get, isFunction } from 'lodash-es'; -import { isRowSelectedDisabled } from '@tdesign/common-js/table/utils'; +import React, { MouseEvent, useEffect, useMemo, useState } from 'react'; +import { get, isFunction } from 'lodash-es'; + import log from '@tdesign/common-js/log/index'; +import { isRowSelectedDisabled } from '@tdesign/common-js/table/utils'; +import Checkbox from '../../checkbox'; import useControlled from '../../hooks/useControlled'; -import { +import Radio from '../../radio'; + +import type { ClassName } from '../../common'; +import type { PrimaryTableCellParams, PrimaryTableCol, RowClassNameParams, @@ -13,10 +18,8 @@ import { TdBaseTableProps, TdPrimaryTableProps, } from '../type'; -import { TableClassName } from './useClassName'; -import Checkbox from '../../checkbox'; -import Radio from '../../radio'; -import { ClassName } from '../../common'; +import type { TableClassName } from './useClassName'; +import { DEFAULT_CURRENT, DEFAULT_PAGE_SIZE } from './usePagination'; const selectedRowDataMap = new Map(); @@ -24,8 +27,15 @@ export default function useRowSelect( props: TdPrimaryTableProps, tableSelectedClasses: TableClassName['tableSelectedClasses'], ) { - const { selectedRowKeys, columns, data, rowKey, indeterminateSelectedRowKeys } = props; - const { pagination, reserveSelectedRowOnPaginate } = props; + const { + selectedRowKeys, + columns, + data, + rowKey, + indeterminateSelectedRowKeys, + pagination, + reserveSelectedRowOnPaginate, + } = props; const [currentPaginateData, setCurrentPaginateData] = useState(data); const [selectedRowClassNames, setSelectedRowClassNames] = useState(); const [tSelectedRowKeys, setTSelectedRowKeys] = useControlled(props, 'selectedRowKeys', props.onSelectChange, { @@ -39,24 +49,22 @@ export default function useRowSelect( // eslint-disable-next-line }, [reserveSelectedRowOnPaginate, data, currentPaginateData]); - // 选中的行,和所有可以选择的行,交集,用于计算 isSelectedAll 和 isIndeterminate - const intersectionKeys = intersection( - tSelectedRowKeys, - canSelectedRows.map((t) => get(t, rowKey || 'id')), - ); - useEffect( () => { if (reserveSelectedRowOnPaginate) return; // 分页变化时,在 onPageChange 中设置 setCurrentPaginateData,PrimaryTable 中 + if (!pagination) { + setCurrentPaginateData(data); + return; + } const { pageSize, current, defaultPageSize, defaultCurrent } = pagination; - const tPageSize = pageSize || defaultPageSize; - const tCurrent = current || defaultCurrent; + const tPageSize = pageSize || defaultPageSize || DEFAULT_PAGE_SIZE; + const tCurrent = current || defaultCurrent || DEFAULT_CURRENT; const newData = data.slice(tPageSize * (tCurrent - 1), tPageSize * tCurrent); setCurrentPaginateData(newData); }, // eslint-disable-next-line - [data, reserveSelectedRowOnPaginate], + [data, reserveSelectedRowOnPaginate, pagination], ); useEffect( @@ -81,22 +89,48 @@ export default function useRowSelect( return isRowSelectedDisabled(selectColumn, row, rowIndex); } - function getSelectedHeader() { - return () => { - const isIndeterminate = intersectionKeys.length > 0 && intersectionKeys.length < canSelectedRows.length; - const isChecked = - intersectionKeys.length !== 0 && - canSelectedRows.length !== 0 && - intersectionKeys.length === canSelectedRows.length; - return ( - - ); + function renderCheckAll() { + const currentData = reserveSelectedRowOnPaginate ? data : currentPaginateData; + const totalRowCount = currentData?.length || 0; + + const currentPageRowKeys = currentData?.map((row) => get(row, rowKey)) || []; + const selectedInCurrentPage = tSelectedRowKeys.filter((key) => currentPageRowKeys.includes(key)); + + const isChecked = totalRowCount > 0 && selectedInCurrentPage.length === totalRowCount; + const isIndeterminate = selectedInCurrentPage.length > 0 && selectedInCurrentPage.length < totalRowCount; + + const handleSelectAll = () => { + const canSelectedRowKeys = canSelectedRows.map((record) => get(record, rowKey)); + + const currentData = reserveSelectedRowOnPaginate ? data : currentPaginateData; + const disabledRowKeys = + currentData?.filter((row, rowIndex) => isDisabled(row, rowIndex)).map((row) => get(row, rowKey)) || []; + + const disabledSelectedRowKeys = selectedRowKeys?.filter((id) => disabledRowKeys.includes(id)) || []; + const allSelectableRowsSelected = canSelectedRowKeys.every((key) => tSelectedRowKeys.includes(key)); + const shouldSelectAll = !allSelectableRowsSelected; + + const allIds = shouldSelectAll + ? [...disabledSelectedRowKeys, ...canSelectedRowKeys] + : [...disabledSelectedRowKeys]; + + setTSelectedRowKeys(allIds, { + selectedRowData: shouldSelectAll + ? allIds.map((t) => selectedRowDataMap.get(t)) + : disabledSelectedRowKeys.map((t) => selectedRowDataMap.get(t)), + type: shouldSelectAll ? 'check' : 'uncheck', + currentRowKey: 'CHECK_ALL_BOX', + }); }; + + return ( + + ); } function getRowSelectDisabledData(p: PrimaryTableCellParams) { @@ -165,18 +199,6 @@ export default function useRowSelect( }); } - function handleSelectAll(checked: boolean) { - const reRowKey = rowKey || 'id'; - const canSelectedRowKeys = canSelectedRows.map((record) => get(record, reRowKey)); - const disabledSelectedRowKeys = selectedRowKeys?.filter((id) => !canSelectedRowKeys.includes(id)) || []; - const allIds = checked ? [...disabledSelectedRowKeys, ...canSelectedRowKeys] : [...disabledSelectedRowKeys]; - setTSelectedRowKeys(allIds, { - selectedRowData: checked ? allIds.map((t) => selectedRowDataMap.get(t)) : [], - type: checked ? 'check' : 'uncheck', - currentRowKey: 'CHECK_ALL_BOX', - }); - } - function formatToRowSelectColumn(col: PrimaryTableCol) { const isSelection = ['multiple', 'single'].includes(col.type); if (!isSelection) return col; @@ -185,7 +207,7 @@ export default function useRowSelect( width: col.width || 64, className: tableSelectedClasses.checkCell, cell: (p: PrimaryTableCellParams) => renderSelectCell(p), - title: col.type === 'multiple' ? getSelectedHeader() : col.title, + title: col.type === 'multiple' ? renderCheckAll() : col.title, }; } From a405e327b4dc1e14776a71e43b465b626c9f0dbe Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 21 Nov 2025 21:54:56 +0800 Subject: [PATCH 2/5] docs: update demo --- .../table/_example/pagination-ajax.tsx | 4 +- .../components/table/_example/pagination.tsx | 28 ++-- .../table/_example/select-multiple.tsx | 8 +- test/snap/__snapshots__/csr.test.jsx.snap | 126 +++++++++--------- test/snap/__snapshots__/ssr.test.jsx.snap | 4 +- 5 files changed, 92 insertions(+), 78 deletions(-) diff --git a/packages/components/table/_example/pagination-ajax.tsx b/packages/components/table/_example/pagination-ajax.tsx index 8346dfe922..d9c7e591cc 100644 --- a/packages/components/table/_example/pagination-ajax.tsx +++ b/packages/components/table/_example/pagination-ajax.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; +import { CheckCircleFilledIcon, CloseCircleFilledIcon, ErrorCircleFilledIcon } from 'tdesign-icons-react'; import { Table, Tag } from 'tdesign-react'; -import { ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react'; import type { PrimaryTableProps, TableProps } from 'tdesign-react'; @@ -81,7 +81,7 @@ export default function TableBasic() { const { current, pageSize } = pageInfo; // 请求可能存在跨域问题 const url = new URL('https://randomuser.me/api'); - const params = { page: current, results: pageSize }; + const params = { page: current, results: pageSize, seed: 'tdesign' }; Object.keys(params).forEach((key) => url.searchParams.append(key, params[key])); const response = await fetch(url.toString()).then((x) => x.json()); setData(response.results); diff --git a/packages/components/table/_example/pagination.tsx b/packages/components/table/_example/pagination.tsx index 9805be89da..0d793e8da0 100644 --- a/packages/components/table/_example/pagination.tsx +++ b/packages/components/table/_example/pagination.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Table, Space, Radio, Tag } from 'tdesign-react'; -import { ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react'; +import { CheckCircleFilledIcon, CloseCircleFilledIcon, ErrorCircleFilledIcon } from 'tdesign-icons-react'; +import { Radio, Space, Table, Tag } from 'tdesign-react'; import type { TableProps } from 'tdesign-react'; @@ -10,11 +10,11 @@ const statusNameListMap = { 2: { label: '审批过期', theme: 'warning', icon: }, }; +const TOTAL = 60; const data: TableProps['data'] = []; -const total = 59; -for (let i = 0; i < total; i++) { +for (let i = 0; i < TOTAL; i++) { data.push({ - index: i, + index: i + 1, applicant: ['贾明', '张三', '王芳'][i % 3], status: i % 3, channel: ['电子签署', '纸质签署', '纸质签署'][i % 3], @@ -28,6 +28,12 @@ for (let i = 0; i < total; i++) { } const columns: TableProps['columns'] = [ + { + colKey: 'row-select', + type: 'multiple', + width: 46, + disabled: ({ row }) => row.index % 4 === 0, + }, { colKey: 'serial-number', width: 80, title: '序号' }, { colKey: 'applicant', title: '申请人', width: '100' }, { @@ -51,12 +57,14 @@ const columns: TableProps['columns'] = [ { colKey: 'channel', title: '签署方式', width: '120' }, // { colKey: 'detail.email', title: '邮箱地址', ellipsis: true }, { colKey: 'createTime', title: '申请时间' }, - { colKey: 'row-select', type: 'multiple', width: 46 }, ]; export default function TableBasic() { const [reserveSelectedRowOnPaginate, setReserveSelectedRowOnPaginate] = useState(true); - const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState( + Array.from({ length: TOTAL }, (_, i) => i + 1).filter((n) => n % 8 === 0), + ); + // const [current, setCurrent] = useState(2); // const [pageSize, setPageSize] = useState(5); @@ -82,7 +90,7 @@ export default function TableBasic() { pagination={{ defaultCurrent: 2, defaultPageSize: 5, - total, + total: TOTAL, showJumper: true, onChange(pageInfo) { console.log(pageInfo, 'onChange pageInfo'); @@ -123,8 +131,8 @@ export default function TableBasic() { }} selectedRowKeys={selectedRowKeys} onSelectChange={(val, context) => { - setSelectedRowKeys(val); - console.log(val, context); + setSelectedRowKeys(val as number[]); + console.log('onSelectChange', val, context); }} reserveSelectedRowOnPaginate={reserveSelectedRowOnPaginate} /> diff --git a/packages/components/table/_example/select-multiple.tsx b/packages/components/table/_example/select-multiple.tsx index 02cafd044f..3fef3d5811 100644 --- a/packages/components/table/_example/select-multiple.tsx +++ b/packages/components/table/_example/select-multiple.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; +import { CheckCircleFilledIcon, CloseCircleFilledIcon, ErrorCircleFilledIcon } from 'tdesign-icons-react'; import { Table, Tag } from 'tdesign-react'; -import { ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react'; import type { TableProps } from 'tdesign-react'; @@ -48,7 +48,7 @@ const columns: TableProps['columns'] = [ const initData: TableProps['data'] = []; for (let i = 0; i < 5; i++) { initData.push({ - index: i + 100, + index: i + 1, applicant: ['贾明', '张三', '王芳'][i % 3], status: i % 3, channel: ['电子签署', '纸质签署', '纸质签署'][i % 3], @@ -63,11 +63,11 @@ for (let i = 0; i < 5; i++) { export default function TableSingleSort() { const [data] = useState([...initData]); - const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([4]); const onSelectChange: TableProps['onSelectChange'] = (value, { selectedRowData }) => { console.log(value, selectedRowData); - setSelectedRowKeys(value); + setSelectedRowKeys(value as number[]); }; return ( diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 06e29da366..b9bcf3e33c 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -120593,6 +120593,9 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat class="t-table--layout-fixed" > + @@ -120608,14 +120611,34 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat - + +
+ +
+ csr test packages/components/table/_example/paginat 申请时间 - -
- -
- + + + 6 @@ -120731,6 +120749,8 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat 2022-02-01 + + @@ -120749,8 +120769,6 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat /> - - 7 @@ -120787,15 +120805,20 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat 2022-03-01 + + - - 8 @@ -120843,6 +120864,8 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat 2022-04-01 + + @@ -120861,8 +120884,6 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat /> - - 9 @@ -120899,6 +120920,8 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat 2022-01-01 + + @@ -120917,8 +120940,6 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat /> - - 10 @@ -120955,24 +120976,6 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat 2022-02-01 - - - @@ -120990,7 +120993,7 @@ exports[`csr snapshot test > csr test packages/components/table/_example/paginat
- 共 59 条数据 + 共 60 条数据
csr test packages/components/table/_example/select- class="t-table__th-cell-inner" >