From 1fe624b93bfcac3ab1eb3a30c6baadae6c642c90 Mon Sep 17 00:00:00 2001 From: raotao <> Date: Mon, 29 Jan 2024 22:43:46 +0800 Subject: [PATCH 1/2] demo(antd): user-based prefix --- packages/antd/view/Layout.tsx | 4 +++- packages/antd/view/{global.css => global.less} | 3 +++ packages/antd/view/main.tsx | 9 ++++++--- packages/antd/vite.config.ts | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) rename packages/antd/view/{global.css => global.less} (65%) diff --git a/packages/antd/view/Layout.tsx b/packages/antd/view/Layout.tsx index adb2066..2ec7a26 100644 --- a/packages/antd/view/Layout.tsx +++ b/packages/antd/view/Layout.tsx @@ -7,6 +7,7 @@ import { UserOutlined, VideoCameraOutlined, } from '@ant-design/icons' +import { ConfigContext } from 'antd/es/config-provider' const { Header, Sider, Content } = Layout @@ -14,6 +15,7 @@ const AppLayout = () => { const [collapsed, setCollapsed] = useState(false) const navigate = useNavigate() const location = useLocation() + const { getPrefixCls } = React.useContext(ConfigContext); return ( @@ -81,7 +83,7 @@ const AppLayout = () => { backgroundColor: 'white', }} > -

Hey here! 👋

+

Hey here! 👋{getPrefixCls()}

diff --git a/packages/antd/view/global.css b/packages/antd/view/global.less similarity index 65% rename from packages/antd/view/global.css rename to packages/antd/view/global.less index 6a6d843..428b699 100644 --- a/packages/antd/view/global.css +++ b/packages/antd/view/global.less @@ -1,3 +1,6 @@ +@import 'antd/dist/antd.less'; +@ant-prefix: hb; + #root { height: 100%; } diff --git a/packages/antd/view/main.tsx b/packages/antd/view/main.tsx index 1a460da..6a38919 100644 --- a/packages/antd/view/main.tsx +++ b/packages/antd/view/main.tsx @@ -1,16 +1,19 @@ import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' +import { ConfigProvider } from 'antd' import App from './App' import 'antd/dist/antd.css' -import './global.css' +import './global.less' ReactDOM.render( - + + + , - document.getElementById('root') as HTMLElement, + document.getElementById('root') as HTMLElement ) diff --git a/packages/antd/vite.config.ts b/packages/antd/vite.config.ts index 59640a7..d03a5ce 100644 --- a/packages/antd/vite.config.ts +++ b/packages/antd/vite.config.ts @@ -11,6 +11,11 @@ export default defineConfig({ { find: 'root', replacement: __dirname }, ], }, + css: { + preprocessorOptions: { + less: { javascriptEnabled: true }, + }, + }, server: { host: '0.0.0.0' } From 850de533057f83e5da583ca4ce04fec27c926734 Mon Sep 17 00:00:00 2001 From: raotao <> Date: Fri, 9 Feb 2024 11:03:07 +0800 Subject: [PATCH 2/2] feat(antd): support antd table selection --- packages/antd/components/table/README.md | 1 + packages/antd/components/table/index.tsx | 21 +- .../antd/components/table/useSelections.ts | 274 ++++++++++++++++++ packages/antd/view/Layout.tsx | 8 + packages/antd/view/router.tsx | 5 + packages/antd/view/selectionTable/index.tsx | 109 +++++++ 6 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 packages/antd/components/table/useSelections.ts create mode 100644 packages/antd/view/selectionTable/index.tsx diff --git a/packages/antd/components/table/README.md b/packages/antd/components/table/README.md index dea27d0..4fc3977 100644 --- a/packages/antd/components/table/README.md +++ b/packages/antd/components/table/README.md @@ -92,6 +92,7 @@ export default = () => { const tableProps: TableProps = { columns: [], handle, + rowSelection: {} // 开启可选择 } return ( diff --git a/packages/antd/components/table/index.tsx b/packages/antd/components/table/index.tsx index fe4811c..09da6af 100644 --- a/packages/antd/components/table/index.tsx +++ b/packages/antd/components/table/index.tsx @@ -25,8 +25,10 @@ import type { FilterValue, SorterResult, TableCurrentDataSource, + TableRowSelection, } from 'antd/es/table/interface' import { TooltipProps } from 'antd/es/tooltip' +import useTableSelections, { AntdTableSelectionResult } from './useSelections' // 🚧-①: 屏蔽 React.StrictMode 副作用 // 🐞-①: 使用 render 实现的动态 Form.Item 会在表格增加、减少行时造成 Form 数据丢失!可以通过 🚧-② 绕开! @@ -62,7 +64,9 @@ export interface TableProps> extends Omit } }) => Promise<({ data: RecordType[] } & Partial>) | void> + rowSelection?: Partial & { disabled: boolean | ((row: RecordType) => boolean) }> handle?: { + selection: AntdTableSelectionResult['action'] & AntdTableSelectionResult['state'], query: (args?: Omit>[0], 'count'>) => void // React 单项数据流设计,遂抛出 dataSource data: RecordType[] @@ -77,6 +81,8 @@ export type TableColumn> = Required> = Required>['query'] export type TableHandle> = Required>['handle'] +const isObject = (any:any) => typeof any === 'object' && any !== null; + // Table 的可编辑表格的表单组件样式(对齐单元格) function formatStyle(prefixCls = 'ant') { const id = 'tr-form-item_style' @@ -98,6 +104,7 @@ function TableAntd, FormValues = Record, FormValues = Record() // NodeJS.Timeout 不一定会有 const editable = useMemo(() => columns?.find(col => col.formItem), [columns]) + const { state: selection, action: selectionAction, rowSelection } = useTableSelections( + data as Readonly[], + { ...props_rowSelection, rowKey: rest.rowKey as any } + ) useLayoutEffect(() => { unMounted.current = false // 🚧-① @@ -187,8 +198,15 @@ function TableAntd, FormValues = Record { @@ -234,6 +252,7 @@ function TableAntd, FormValues = Record String(index), // Expect to pass from props! pagination: page, + rowSelection: isObject(props_rowSelection) ? rowSelection : undefined, ...rest, }) diff --git a/packages/antd/components/table/useSelections.ts b/packages/antd/components/table/useSelections.ts new file mode 100644 index 0000000..e4db5e5 --- /dev/null +++ b/packages/antd/components/table/useSelections.ts @@ -0,0 +1,274 @@ +import React, { useMemo, useState, useRef } from 'react' + +const isUndef = (value: unknown): value is undefined => + typeof value === 'undefined' + +type noop = (this: any, ...args: any[]) => any + +type PickFunction = ( + this: ThisParameterType, + ...args: Parameters +) => ReturnType + +function useMemoizedFn(fn: T) { + if (typeof fn !== 'function') { + console.error( + `useMemoizedFn expected parameter is a function, got ${typeof fn}` + ) + } + + const fnRef = useRef(fn) + + // why not write `fnRef.current = fn`? + // https://github.com/alibaba/hooks/issues/728 + fnRef.current = useMemo(() => fn, [fn]) + + const memoizedFn = useRef>() + if (!memoizedFn.current) { + memoizedFn.current = function (this, ...args) { + return fnRef.current.apply(this, args) + } + } + + return memoizedFn.current as T +} + +export type RowSelectionType = 'checkbox' | 'radio' +export type RowSelectMethod = 'all' | 'none' | 'invert' | 'single' | 'multiple' +export type GetRowKey = (record: RecordType) => React.Key + +export interface AntdTableRowSelection { + type: RowSelectionType + selectedRowKeys: React.Key[] + defaultSelectedRowKeys: React.Key[] + getCheckboxProps: (row: RecordType) => Partial<{ disabled: boolean }> + onChange: ( + selectedRowKeys: React.Key[], + selectedRows: RecordType[], + info: { type: RowSelectMethod } + ) => void + rowKey: string | keyof RecordType | GetRowKey + disabled: boolean | ((row: RecordType) => boolean) + [key: string]: any +} + +export interface AntdTableSelectionResult { + state: { + allSelected: boolean + selectedRows: RecordType[] + selectedRowKeys: React.Key[] + } + action: { + select: (item: RecordType) => void + toggle: (item: RecordType) => void + unSelect: (item: RecordType) => void + toggleAll: () => void + selectAll: () => void + isSelected: (item: RecordType) => boolean + unSelectAll: () => void + setSelected: (keys: React.Key[]) => void + } + rowSelection: Omit, 'rowKey' | 'disabled'> +} + +function useAntdTableSelection( + rows: RecordType[] = [], + config?: Partial> +): AntdTableSelectionResult { + if (!isUndef(config?.rowKey)) { + console.error( + `useAntdTableSelection expected config.rowKey is a function|string|number, got undefined. + Will default to using "key" as rowKey. + The value must be the same as the rowKey of the Antd Table component` + ) + } + + const { + rowKey = 'key', + disabled = false, + type = 'checkbox', + defaultSelectedRowKeys = [], + ...rest + } = config || {} + + const getRowKey = React.useMemo>(() => { + if (typeof rowKey === 'function') { + return rowKey + } + + return (record: RecordType) => (record as any)?.[rowKey] + }, [rowKey]) + + const allRowKeys = useMemo(() => rows.map((t) => getRowKey(t)), [rows]) + + const rowKeyMapRow = React.useMemo( + () => + rows.reduce>((prev, cur) => { + prev[getRowKey(cur)] = cur + return prev + }, {}), + [rows] + ) + + // ========================= States ========================= + const [selectedRows, setSelectedRows] = useState( + defaultSelectedRowKeys + .map((key) => rowKeyMapRow[key]) + .filter((t) => !isUndef(t)) + ) + const [selectedRowKeys, setSelectedRowKeys] = useState( + defaultSelectedRowKeys + ) + + const getDisabled = React.useMemo(() => { + if (typeof disabled === 'function') { + return (row: RecordType) => disabled(row) + } + if (typeof disabled === 'boolean') { + return () => disabled + } + return () => false + }, [disabled]) + + const getCheckboxProps = useMemoizedFn((row: RecordType) => { + const checkboxProps = + typeof rest?.getCheckboxProps === 'function' + ? rest.getCheckboxProps?.(row) + : {} + + return Object.assign({ disabled: getDisabled(row) }, checkboxProps) + }) + + const isValidRow = useMemoizedFn( + (row: RecordType) => + !getCheckboxProps(row).disabled && allRowKeys.includes(getRowKey(row)) + ) + + // ========================= Funcs ========================= + const onSelectionChange: AntdTableRowSelection['onChange'] = + useMemoizedFn((rowKeys, records, info) => { + setSelectedRows(records) + setSelectedRowKeys(rowKeys) + rest?.onChange?.(rowKeys, records, info) + }) + + const onRowsChange = useMemoizedFn((records: RecordType[]) => { + /** based action change state, must have rowKey and row is't disabled. + * because of this, the config.rowKey is must + */ + const willSelected = records.filter((t) => isValidRow(t)) + setSelectedRows(willSelected) + setSelectedRowKeys(willSelected.map((t) => getRowKey(t))) + }) + + // ========================= Select Memo States ========================= + const selectedSet = useMemo(() => new Set(selectedRows), [selectedRows]) + + const noneSelected = useMemo( + () => rows.every((o) => !selectedSet.has(o)), + [rows, selectedSet] + ) + + const allSelected = useMemo( + () => + rows.filter((t) => !getCheckboxProps(t).disabled).length === + selectedRows.length && !noneSelected, + [rows, selectedSet, noneSelected] + ) + + // ========================= Select Action ========================= + const isSelected = useMemoizedFn((item: RecordType) => selectedSet.has(item)) + + const select = useMemoizedFn((item: RecordType) => { + if (isValidRow(item)) { + selectedSet.add(item) + onRowsChange(Array.from(selectedSet)) + } + }) + + const unSelect = useMemoizedFn((item: RecordType) => { + selectedSet.delete(item) + onRowsChange(Array.from(selectedSet)) + }) + + const selectAll = useMemoizedFn(() => { + selectedSet.clear() + + for (let i = 0; i < rows.length; i++) { + const row = rows[i] + if (isValidRow(row)) { + selectedSet.add(row) + if (type === 'radio') break + } + } + + onRowsChange(Array.from(selectedSet)) + }) + + const unSelectAll = useMemoizedFn(() => { + selectedSet.clear() + onRowsChange(Array.from(selectedSet)) + }) + + const toggle = useMemoizedFn((item: RecordType) => { + if (isSelected(item)) { + unSelect(item) + } else { + select(item) + } + }) + + const toggleAll = useMemoizedFn(() => + allSelected ? unSelectAll() : selectAll() + ) + + const setSelected = useMemoizedFn((rowKeys: React.Key[]) => { + selectedSet.clear() + + const willRowKeys: React.Key[] = [] + let key: any = null + let row: any = null + + for (let i = 0; i < rowKeys.length; i++) { + key = rowKeys[i] + row = rowKeyMapRow[key] + if (isValidRow(row)) { + selectedSet.add(rowKeyMapRow[key]) + willRowKeys.push(key) + /* if type is radio, should be used the first */ + if (type === 'radio') break + } + } + + setSelectedRows(Array.from(selectedSet)) + setSelectedRowKeys(willRowKeys) + }) + + return { + state: { + allSelected, + selectedRows, + selectedRowKeys, + }, + action: { + select, + toggle, + unSelect, + toggleAll, + selectAll, + isSelected, + unSelectAll, + setSelected, + }, + rowSelection: { + ...rest, + type, + onChange: onSelectionChange, + getCheckboxProps, + selectedRowKeys, + defaultSelectedRowKeys, + }, + } +} + +export default useAntdTableSelection diff --git a/packages/antd/view/Layout.tsx b/packages/antd/view/Layout.tsx index 2ec7a26..e8e5679 100644 --- a/packages/antd/view/Layout.tsx +++ b/packages/antd/view/Layout.tsx @@ -62,6 +62,14 @@ const AppLayout = () => { onClick() { navigate('/table') }, + }, + { + key: '/selection-table', + icon: , + label: 'Selection-table', + onClick() { + navigate('/selection-table') + }, }, { key: '/table-edit', diff --git a/packages/antd/view/router.tsx b/packages/antd/view/router.tsx index 41f2cf3..6eacc3d 100644 --- a/packages/antd/view/router.tsx +++ b/packages/antd/view/router.tsx @@ -6,6 +6,7 @@ import Layout from './Layout' import Form from './form' import Table from './table' import TableEdit from './table-edit' +import SelectionTable from './selectionTable' // https://stackblitz.com/github/remix-run/react-router/tree/main/examples/route-objects?file=src%2FApp.tsx export const routes: RouteObject[] = [ @@ -21,6 +22,10 @@ export const routes: RouteObject[] = [ path: '/table', element: , }, + { + path: '/selection-Table', + element: , + }, { path: '/table-edit', element: , diff --git a/packages/antd/view/selectionTable/index.tsx b/packages/antd/view/selectionTable/index.tsx new file mode 100644 index 0000000..9a0cd99 --- /dev/null +++ b/packages/antd/view/selectionTable/index.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react' +import { Button, Col, Row, Space, Tooltip, message } from 'antd' +import { + Table, + TableProps, + TableHandle, +} from 'root/components' + +export interface RecordType { + id: string + name: string + age: number + gender: 0 | 1 + date: number +} + +export default () => { + + const [data, setData] = useState([ + { id: '1', name: '张三', age: 23, gender: 1, date: Date.now() }, + { id: '2', name: '阿宁', age: 24, gender: 0, date: Date.now() }, + { id: '3', name: '阿波', age: 25, gender: 1, date: Date.now() }, + { id: '4', name: '菜菜', age: 26, gender: 1, date: Date.now() }, + { id: '5', name: '东哥', age: 27, gender: 0, date: Date.now() }, + { id: '6', name: '老李', age: 28, gender: 0, date: Date.now() }, + ]) + const [open, setOpen] = useState(true) + + const handle = {} as TableHandle + + const reload = () => { + handle?.query() + } + + const tableProps: TableProps = { + async query(args) { + console.log('-- 发起请求 --', JSON.parse(JSON.stringify(args))) + + return { + data: data.map(d => ({ ...d, date: Date.now() })), + total: 100, + pageSize: args.pagination.pageSize, + current: args.pagination.current, + } + }, + rowSelection: open ? { disabled:(row) => ["2","5"].includes(row.id) } : undefined, + columns: [ + { + title: '姓名', + dataIndex: 'name', + }, + { + title: '年龄', + dataIndex: 'age', + render: age => ( + 芳龄: {age}} + >{age} + ), + }, + { + title: '性别', + dataIndex: 'gender', + render: gender => gender === 1 ? '男' : '女', + }, + { + title: '时间', + dataIndex: 'date', + render: date => new Date(date).toLocaleString(), + }, + { + title: '操作', + dataIndex: '操作', + render: () => , + }, + ], + // 直接传递 data + // dataSource: data, + // 关闭分页 + // pagination: false, + handle, + rowKey: (row) => row.id, + } + + return ( + <> + + + + + + + + + + + + + + + + + + + +
+ + ) +}