├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── deploy.yml │ ├── rebase.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc.js ├── .umirc.js ├── LICENSE ├── README.md ├── docs ├── demo │ ├── base.tsx │ ├── complex.tsx │ ├── expand.tsx │ ├── group.tsx │ ├── headerRender.tsx │ ├── layout.tsx │ ├── minMenu.tsx │ ├── selectedRow.tsx │ ├── size.tsx │ └── special.tsx └── index.md ├── jest.config.js ├── package.json ├── public ├── CNAME └── favicon.ico ├── src ├── Item.tsx ├── hooks │ ├── useLazyKVMap.ts │ ├── usePagination.ts │ └── useSelection.tsx ├── index.less ├── index.tsx ├── toolBar │ ├── index.less │ └── index.tsx └── util │ └── getPrefixCls.ts ├── tests ├── __snapshots__ │ └── demo.test.tsx.snap └── demo.test.tsx ├── tsconfig.json ├── typings.d.ts └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | ./**/demo -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 'import/no-extraneous-dependencies': 0, 'import/no-unresolved': 0 }, 4 | }; 5 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: 'src/index.tsx', 3 | esm: { 4 | type: 'babel', 5 | importLibToEs: true, 6 | }, 7 | cjs: 'babel', 8 | extraBabelPlugins: [['import', { libraryName: 'antd', style: true }]], 9 | }; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 Ant Design Pro List 的 bug 4 | title: '🐛[BUG]' 5 | labels: '🐛 BUG' 6 | assignees: '' 7 | --- 8 | 9 | ### 🐛 bug 描述 [详细地描述 bug,让大家都能理解] 10 | 11 | ### 📷 复现步骤 [清晰描述复现步骤,让别人也能看到问题] 12 | 13 | ### 🏞 期望结果 [描述你原本期望看到的结果] 14 | 15 | ### 💻 复现代码 [提供可复现的代码,仓库,或线上示例] 16 | 17 | ### © 版本信息 18 | 19 | - Ant Design Pro 版本: [e.g. 4.0.0] 20 | - umi 版本 21 | - 浏览器环境 22 | - 开发环境 [e.g. mac OS] 23 | 24 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 Ant Design Pro List 的需求或建议 4 | title: '👑 [需求]' 5 | labels: '👑 Feature' 6 | assignees: '' 7 | --- 8 | 9 | ### 🥰 需求描述 [详细地描述需求,让大家都能理解] 10 | 11 | ### 🧐 解决方案 [如果你有解决方案,在这里清晰地阐述] 12 | 13 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '疑问或需要帮助 ❓' 3 | about: 对 Ant Design Pro List 使用的疑问或需要帮助 4 | title: '🧐[问题]' 5 | labels: '🧐 Question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 问题描述 [详细地描述问题,让大家都能理解] 10 | 11 | ### 💻 示例代码 [如果有必要,展示代码,线上示例,或仓库] 12 | 13 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@master 13 | - run: yarn 14 | - run: yarn run lint 15 | # - run: yarn run tsc 16 | - name: Build and Deploy 17 | uses: JamesIves/github-pages-deploy-action@master 18 | env: 19 | CI: true 20 | GIT_CONFIG_NAME: qixian.cs 21 | GIT_CONFIG_EMAIL: qixian.cs@outlook.com 22 | NODE_OPTIONS: --max_old_space_size=4096 23 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 24 | BRANCH: gh-pages 25 | FOLDER: 'dist/' 26 | BUILD_SCRIPT: yarn && npm uninstall husky && npm run site_build && git checkout . && git clean -df 27 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Automatic Rebase 5 | jobs: 6 | rebase: 7 | name: Rebase 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Automatic Rebase 12 | uses: cirrus-actions/rebase@master 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | 6 | steps: 7 | - name: checkout 8 | uses: actions/checkout@master 9 | 10 | - name: install 11 | run: npm install 12 | 13 | - name: lint 14 | run: npm run lint 15 | 16 | - name: test 17 | run: npm run test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /.docz 5 | .storybook 6 | **/node_modules 7 | # roadhog-api-doc ignore 8 | /src/utils/request-temp.js 9 | _roadhog-api-doc 10 | 11 | # production 12 | /dist 13 | /.vscode 14 | /es 15 | /lib 16 | 17 | # misc 18 | .DS_Store 19 | storybook-static 20 | npm-debug.log* 21 | yarn-error.log 22 | 23 | /coverage 24 | .idea 25 | yarn.lock 26 | package-lock.json 27 | *bak 28 | .vscode 29 | 30 | # visual studio code 31 | .history 32 | *.log 33 | functions/* 34 | lambda/mock/index.js 35 | .temp/** 36 | 37 | # umi 38 | .umi 39 | .umi-production 40 | 41 | # screenshot 42 | screenshot 43 | .firebase 44 | example/.temp/* 45 | .eslintcache 46 | lib/** 47 | es/** 48 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | .umi 3 | .umi-production 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "proseWrap": "never", 6 | "overrides": [ 7 | { 8 | "files": ".prettierrc", 9 | "options": { 10 | "parser": "json" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Pro-List', 3 | mode: 'site', 4 | logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 5 | extraBabelPlugins: [ 6 | [ 7 | 'import', 8 | { 9 | libraryName: 'antd', 10 | libraryDirectory: 'es', 11 | style: 'css', 12 | }, 13 | ], 14 | ], 15 | hash: true, 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present chenshuai2144 (qixian.cs@outlook.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 此仓库已废弃 2 | 3 | **重要:** 此仓库后续不再维护,也不再接受更多的特性更新。`ant-design/pro-list` 将会迁移至 `ant-design/pro-components` 仓库进行后续的维护,访问 https://procomponent.ant.design/ 了解更多。此变更不影响继续使用 `@ant-design/pro-list` 这个 npm 包名安装使用此组件。 4 | 5 | # ProList (高级列表) 6 | 7 | ProList 在 antd 的 list 支持了一些功能,比如 多选,展开等功能,使用体验贴近 table。 8 | 9 | ## 何时使用 10 | 11 | 在完成一个标准的列表时即可使用。 12 | 13 | ## API 14 | 15 | ### ProList 16 | 17 | ProList 与 antd 的 [List](https://ant.design/components/list-cn/) 相比,主要增加了 rowSelection 和 expandable 来支持选中与筛选 18 | 19 | | 参数 | 说明 | 类型 | 默认值 | 20 | | :-- | :-- | :-- | :-- | 21 | | rowSelection | 与 antd 相同的[配置](https://ant.design/components/table-cn/#rowSelection) | object \|boolean | false | 22 | | expandable | 与 antd 相同的[配置](https://ant.design/components/table-cn/#expandable) | object \| false | - | 23 | | showActions | 何时展示 actions | 'hover' \| 'always' | always | 24 | | rowKey | 行的 key,一般是行 id | string \| (row,index)=>string | "id" | 25 | | renderItem | 现在的 renderItem 需要返回 ProList.Item 的 props,而不是 dom | ItemProps | - | 26 | | listRenderItem | 这是 antd 的 renderItem 的别名 | (row,index)=> Node | - | 27 | 28 | ### ProList.Item 29 | 30 | 如果你的 dataSource 包含 children,我们会将其打平传入到 renderItem 中,但是包含 children 的项会转化了 group 的样式,只支持 title 和 actions 的属性。 31 | 32 | | 参数 | 说明 | 类型 | 默认值 | 33 | | :-- | :-- | :-- | :-- | 34 | | type | 列表项的预设样式 | new \| top | - | 35 | | title | 列表项的主标题 | ReactNode | - | 36 | | subTitle | 列表项的副标题 | ReactNode | - | 37 | | checkbox | 列表的选择框 | React.ReactNode | - | 38 | | loading | 列表项是否在加载中 | React.ReactNode | - | 39 | | avatar | 列表项的头像 | AvatarProps \| string | - | 40 | | actions | 操作列表项 | React.ReactNode[] | - | 41 | | description | 列表项的描述,与 title 不在一行 | React.ReactNode[] | - | 42 | | expandedRowClassName | 多余展开的 css | string | - | 43 | | expand | 列表项是否展开 | boolean | - | 44 | | onExpand | 列表项展开收起的回调 | (expand: boolean) => void | - | 45 | | expandable | 列表项展开配置 | [object](https://ant.design/components/table-cn/#expandable) | - | 46 | -------------------------------------------------------------------------------- /docs/demo/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Tag, Space } from 'antd'; 3 | // @ts-ignore 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import ProList from '@ant-design/pro-list'; 6 | 7 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 8 | 9 | export default () => ( 10 | <> 11 | 12 | actions={[ 13 | , 16 | ]} 17 | rowKey="id" 18 | title="基础列表" 19 | showActions="hover" 20 | dataSource={dataSource} 21 | renderItem={(item) => ({ 22 | title: item, 23 | subTitle: ( 24 | 25 | Ant Design 26 | TechUI 27 | 28 | ), 29 | actions: [邀请], 30 | description: 31 | 'Ant Design, a design language for background applications, is refined by Ant UED Team', 32 | avatar: 33 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 34 | })} 35 | /> 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /docs/demo/complex.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactText } from 'react'; 2 | import { Button, Progress, Tag } from 'antd'; 3 | import { EllipsisOutlined } from '@ant-design/icons'; 4 | // @ts-ignore 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import ProList from '@ant-design/pro-list'; 7 | 8 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 9 | 10 | export default () => { 11 | const [expandedRowKeys, setExpandedRowKeys] = useState([]); 12 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 13 | const rowSelection = { 14 | selectedRowKeys, 15 | onChange: (keys: ReactText[]) => setSelectedRowKeys(keys), 16 | }; 17 | return ( 18 | <> 19 | 20 | actions={[ 21 | , 24 | ]} 25 | rowKey="id" 26 | title="复杂的例子" 27 | rowSelection={rowSelection} 28 | dataSource={dataSource} 29 | expandable={{ 30 | expandedRowKeys, 31 | expandedRowRender: () => { 32 | return ( 33 |
40 |
45 |
发布中
46 | 47 |
48 |
49 | ); 50 | }, 51 | expandedRowClassName: () => 'qixian', 52 | onExpandedRowsChange: setExpandedRowKeys, 53 | }} 54 | renderItem={(item) => ({ 55 | title: item, 56 | subTitle: 语雀专栏, 57 | actions: [ 58 | 邀请, 59 | 操作, 60 | 61 | 62 | , 63 | ], 64 | description: ( 65 |
66 |
一个 UI 设计体系
67 |
林外发布于 2019-06-25
68 |
69 | ), 70 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/UCSiy1j6jx/xingzhuang.svg', 71 | })} 72 | /> 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /docs/demo/expand.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactText } from 'react'; 2 | import { Button, Progress, Tag, Space } from 'antd'; 3 | // @ts-ignore 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import ProList from '@ant-design/pro-list'; 6 | 7 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 8 | 9 | export default () => { 10 | const [expandedRowKeys, setExpandedRowKeys] = useState([]); 11 | 12 | return ( 13 | <> 14 | 15 | actions={[ 16 | , 19 | ]} 20 | rowKey="id" 21 | title="支持展开的列表" 22 | expandable={{ expandedRowKeys, onExpandedRowsChange: setExpandedRowKeys }} 23 | dataSource={dataSource} 24 | renderItem={(item) => ({ 25 | title: item, 26 | subTitle: ( 27 | 28 | Ant Design 29 | TechUI 30 | 31 | ), 32 | actions: [邀请], 33 | description: 34 | 'Ant Design, a design language for background applications, is refined by Ant UED Team', 35 | avatar: 36 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 37 | children: ( 38 |
46 |
51 |
发布中
52 | 53 |
54 |
55 | ), 56 | })} 57 | /> 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /docs/demo/group.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Tag } from 'antd'; 3 | import { MessageOutlined, LikeOutlined, StarOutlined, EllipsisOutlined } from '@ant-design/icons'; 4 | // @ts-ignore 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import ProList from '@ant-design/pro-list'; 7 | 8 | const IconText = ({ icon, text }: { icon: any; text: string }) => ( 9 | 10 | {React.createElement(icon, { style: { marginRight: 8 } })} 11 | {text} 12 | 13 | ); 14 | 15 | const dataSource = [ 16 | { 17 | title: '分组标题', 18 | children: [ 19 | { title: '语雀的天空' }, 20 | { title: 'Ant Design' }, 21 | { title: '蚂蚁金服体验科技' }, 22 | { title: 'TechUI' }, 23 | ], 24 | }, 25 | ]; 26 | 27 | export default () => { 28 | return ( 29 | <> 30 | 36 | actions={[ 37 | , 40 | ]} 41 | itemLayout="vertical" 42 | rowKey="id" 43 | title="复杂的例子" 44 | dataSource={dataSource} 45 | renderItem={(item) => { 46 | if (item.children) { 47 | return { 48 | title: '分组标题', 49 | actions: [ 50 | 邀请, 51 | 操作, 52 | 53 | 54 | , 55 | ], 56 | }; 57 | } 58 | return { 59 | title: item.title, 60 | actions: [ 61 | , 62 | , 63 | , 64 | ], 65 | description: ( 66 | <> 67 | 语雀专栏 68 | 设计语言 69 | 蚂蚁金服 70 | 71 | ), 72 | extra: ( 73 | logo 78 | ), 79 | // avatar: 'https://gw.alipayobjects.com/zos/antfincdn/UCSiy1j6jx/xingzhuang.svg', 80 | children: ( 81 |
82 | 段落示意:蚂蚁金服设计平台 83 | design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 84 | design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态提供跨越设计与开发的体验解决方案。 85 |
86 | ), 87 | }; 88 | }} 89 | /> 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /docs/demo/headerRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Tag, Space } from 'antd'; 3 | // @ts-ignore 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import ProList from '@ant-design/pro-list'; 6 | 7 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 8 | 9 | export default () => ( 10 | <> 11 | 12 | actions={[ 13 | , 16 | ]} 17 | rowKey="id" 18 | title="基础列表" 19 | headerRender={({ title }, defaultDom) => { 20 | return ( 21 |
22 | {defaultDom} 23 |
24 | 这是自定义的第二行: {title} 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | ); 33 | }} 34 | showActions="hover" 35 | dataSource={dataSource} 36 | renderItem={(item) => ({ 37 | title: item, 38 | subTitle: ( 39 |
40 | Ant Design 41 | 47 | TechUI 48 | 49 |
50 | ), 51 | actions: [邀请], 52 | description: 53 | 'Ant Design, a design language for background applications, is refined by Ant UED Team', 54 | avatar: 55 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 56 | })} 57 | /> 58 | 59 | ); 60 | -------------------------------------------------------------------------------- /docs/demo/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Tag } from 'antd'; 3 | import { MessageOutlined, LikeOutlined, StarOutlined } from '@ant-design/icons'; 4 | 5 | // @ts-ignore 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import ProList from '@ant-design/pro-list'; 8 | 9 | const IconText = ({ icon, text }: { icon: any; text: string }) => ( 10 | 11 | {React.createElement(icon, { style: { marginRight: 8 } })} 12 | {text} 13 | 14 | ); 15 | 16 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 17 | 18 | export default () => { 19 | return ( 20 | <> 21 | 22 | actions={[ 23 | , 26 | ]} 27 | itemLayout="vertical" 28 | rowKey="id" 29 | title="竖排样式" 30 | dataSource={dataSource} 31 | renderItem={(item) => ({ 32 | title: item, 33 | actions: [ 34 | , 35 | , 36 | , 37 | ], 38 | description: ( 39 | <> 40 | 语雀专栏 41 | 设计语言 42 | 蚂蚁金服 43 | 44 | ), 45 | extra: ( 46 | logo 51 | ), 52 | // avatar: 'https://gw.alipayobjects.com/zos/antfincdn/UCSiy1j6jx/xingzhuang.svg', 53 | children: ( 54 |
55 | 段落示意:蚂蚁金服设计平台 56 | design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 57 | design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态提供跨越设计与开发的体验解决方案。 58 |
59 | ), 60 | })} 61 | /> 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /docs/demo/minMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EllipsisOutlined } from '@ant-design/icons'; 3 | // @ts-ignore 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import ProList from '@ant-design/pro-list'; 6 | 7 | const dataSource = [ 8 | { title: '分组标题', children: [{ title: '语雀的天空' }, { title: 'Ant Design' }] }, 9 | { title: '分组标题', children: [{ title: '蚂蚁金服体验科技' }, { title: 'TechUI' }] }, 10 | ]; 11 | 12 | export default () => ( 13 |
20 | 26 | rowKey="id" 27 | dataSource={dataSource} 28 | split={false} 29 | style={{ 30 | background: '#FFF', 31 | }} 32 | renderItem={(item) => ({ 33 | title: item.title, 34 | actions: item.children && [ 35 | 邀请, 36 | 操作, 37 | 38 | 39 | , 40 | ], 41 | type: 'inline', 42 | avatar: 43 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 44 | })} 45 | /> 46 |
47 | ); 48 | -------------------------------------------------------------------------------- /docs/demo/selectedRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactText } from 'react'; 2 | import { Button, Progress, Tag } from 'antd'; 3 | // @ts-ignore 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import ProList from '@ant-design/pro-list'; 6 | 7 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 8 | 9 | export default () => { 10 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 11 | const rowSelection = { 12 | selectedRowKeys, 13 | onChange: (keys: ReactText[]) => setSelectedRowKeys(keys), 14 | }; 15 | return ( 16 | <> 17 | 18 | actions={[ 19 | , 22 | ]} 23 | rowKey="id" 24 | title="支持选中的列表" 25 | rowSelection={rowSelection} 26 | dataSource={dataSource} 27 | renderItem={(item) => ({ 28 | title: item, 29 | subTitle: ( 30 |
31 | Ant Design 32 | 38 | TechUI 39 | 40 |
41 | ), 42 | actions: [邀请], 43 | description: 44 | 'Ant Design, a design language for background applications, is refined by Ant UED Team', 45 | avatar: 46 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 47 | children: ( 48 |
55 |
60 |
发布中
61 | 62 |
63 |
64 | ), 65 | })} 66 | /> 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /docs/demo/size.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactText } from 'react'; 2 | import { Button, Select, Progress, Tag } from 'antd'; 3 | import { EllipsisOutlined } from '@ant-design/icons'; 4 | 5 | // @ts-ignore 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import ProList from '@ant-design/pro-list'; 8 | 9 | const dataSource = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI']; 10 | 11 | export default () => { 12 | const [size, setSize] = useState<'small' | 'default' | 'large' | undefined>('default'); 13 | const [split, setSplit] = useState('有'); 14 | const [bordered, setBordered] = useState('有'); 15 | const [expandedRowKeys, setExpandedRowKeys] = useState([]); 16 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 17 | const rowSelection = { 18 | selectedRowKeys, 19 | onChange: (keys: ReactText[]) => setSelectedRowKeys(keys), 20 | }; 21 | 22 | return ( 23 | <> 24 | 大小: 25 | 26 | value={size} 27 | onChange={(value) => setSize(value as any)} 28 | options={['small', 'default', 'large'].map((selectSize) => ({ 29 | value: selectSize, 30 | label: selectSize, 31 | }))} 32 | />{' '} 33 | 分割线: 34 | 35 | value={split} 36 | onChange={(value) => setSplit(value)} 37 | options={['有', '无'].map((selectSize) => ({ 38 | value: selectSize, 39 | label: selectSize, 40 | }))} 41 | />{' '} 42 | 边框线: 43 | 44 | value={bordered} 45 | onChange={(value) => setBordered(value)} 46 | options={['有', '无'].map((selectSize) => ({ 47 | value: selectSize, 48 | label: selectSize, 49 | }))} 50 | /> 51 |
52 |
53 | 54 | size={size} 55 | split={split === '有'} 56 | actions={[ 57 | , 60 | ]} 61 | bordered={bordered === '有'} 62 | rowKey="id" 63 | title="复杂的例子" 64 | rowSelection={rowSelection} 65 | dataSource={dataSource} 66 | expandable={{ expandedRowKeys, onExpandedRowsChange: setExpandedRowKeys }} 67 | renderItem={(item) => ({ 68 | title: item, 69 | subTitle: 语雀专栏, 70 | actions: [ 71 | 邀请, 72 | 操作, 73 | 74 | 75 | , 76 | ], 77 | description: ( 78 |
79 |
一个 UI 设计体系
80 |
林外发布于 2019-06-25
81 |
82 | ), 83 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/UCSiy1j6jx/xingzhuang.svg', 84 | children: ( 85 |
92 |
97 |
发布中
98 | 99 |
100 |
101 | ), 102 | })} 103 | /> 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /docs/demo/special.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactText } from 'react'; 2 | import { Button, Progress, Tag } from 'antd'; 3 | import { EllipsisOutlined } from '@ant-design/icons'; 4 | 5 | // @ts-ignore 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import ProList from '@ant-design/pro-list'; 8 | 9 | const data = ['语雀的天空', 'Ant Design', '蚂蚁金服体验科技', 'TechUI'].map((item, index) => ({ 10 | title: item, 11 | subTitle: 语雀专栏, 12 | actions: [ 13 | 邀请, 14 | 操作, 15 | 16 | 17 | , 18 | ], 19 | description: ( 20 |
21 |
一个 UI 设计体系
22 |
林外发布于 2019-06-25
23 |
24 | ), 25 | type: index === 0 ? 'top' : undefined, 26 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/UCSiy1j6jx/xingzhuang.svg', 27 | children: ( 28 |
35 |
40 |
发布中
41 | 42 |
43 |
44 | ), 45 | })); 46 | 47 | export default () => { 48 | const [expandedRowKeys, setExpandedRowKeys] = useState([]); 49 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 50 | const rowSelection = { 51 | selectedRowKeys, 52 | onChange: (keys: ReactText[]) => setSelectedRowKeys(keys), 53 | }; 54 | const [dataSource, setDataSource] = useState([...data] as any[]); 55 | 56 | return ( 57 | <> 58 | 67 | actions={[ 68 | , 82 | ]} 83 | rowKey="id" 84 | title="预设的列状态" 85 | rowSelection={rowSelection} 86 | dataSource={dataSource} 87 | renderItem={(item) => item} 88 | expandable={{ expandedRowKeys, onExpandedRowsChange: setExpandedRowKeys }} 89 | /> 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ProList 高级列表 3 | order: 10 4 | sidemenu: false 5 | --- 6 | 7 |

@ant-design/pro-list

8 | 9 |
10 | 11 | 🏆 Use Ant Design List like a Pro! 12 | 13 |
14 | 15 | ProList 在 antd 的 list 支持了一些功能,比如 多选,展开等功能,使用体验贴近 table。 16 | 17 | ## 何时使用 18 | 19 | 在完成一个标准的 表格列表时即可使用。 20 | 21 | ## 代码演示 22 | 23 | ### 基本使用 24 | 25 | 26 | 27 | ### 支持展开的列表 28 | 29 | 30 | 31 | ### 支持选中的列表 32 | 33 | 34 | 35 | ### 复杂的列表 36 | 37 | 38 | 39 | ### 各种 size 40 | 41 | 42 | 43 | ### 竖排样式 44 | 45 | 46 | 47 | ### 文段式场景 48 | 49 | 50 | 51 | ### 一些预设的模式 52 | 53 | 54 | 55 | ### 自定义表头 56 | 57 | 58 | 59 | ### 小菜单 60 | 61 | 62 | 63 | ## API 64 | 65 | ### ProList 66 | 67 | ProList 与 antd 的 [List](https://ant.design/components/list-cn/) 相比,主要增加了 rowSelection 和 expandable 来支持选中与筛选 68 | 69 | | 参数 | 说明 | 类型 | 默认值 | 70 | | :-- | :-- | :-- | :-- | 71 | | rowSelection | 与 antd 相同的[配置](https://ant.design/components/table-cn/#rowSelection) | object \|boolean | false | 72 | | expandable | 与 antd 相同的[配置](https://ant.design/components/table-cn/#expandable) | object \| false | - | 73 | | showActions | 何时展示 actions | 'hover' \| 'always' | always | 74 | | rowKey | 行的 key,一般是行 id | string \| (row,index)=>string | "id" | 75 | | renderItem | 现在的 renderItem 需要返回 ProList.Item 的 props,而不是 dom | ItemProps | - | 76 | | title | 列表头部主标题 | ReactNode | - | 77 | | actions | 列表头部操作项 | React.ReactNode[] | - | 78 | | headerRender | 自定义列表头的 render 方法,替代 `` 的 header 属性 | (props: {title, actions}, defaultDom: React.ReactNode) => ReactNode | - | 79 | | listRenderItem | 这是 antd 的 renderItem 的别名 | (row,index)=> Node | - | 80 | 81 | ### ProList.Item 82 | 83 | 如果你的 dataSource 包含 children,我们会将其打平传入到 renderItem 中,但是包含 children 的项会转化了 group 的样式,只支持 title 和 actions 的属性。 84 | 85 | | 参数 | 说明 | 类型 | 默认值 | 86 | | :-- | :-- | :-- | :-- | 87 | | type | 列表项的预设样式 | new \| top | - | 88 | | title | 列表项的主标题 | ReactNode | - | 89 | | subTitle | 列表项的副标题 | ReactNode | - | 90 | | checkbox | 列表的选择框 | React.ReactNode | - | 91 | | loading | 列表项是否在加载中 | React.ReactNode | - | 92 | | avatar | 列表项的头像 | AvatarProps \| string | - | 93 | | actions | 操作列表项 | React.ReactNode[] | - | 94 | | description | 列表项的描述,与 title 不在一行 | React.ReactNode[] | - | 95 | | expandedRowClassName | 多余展开的 css | string | - | 96 | | expand | 列表项是否展开 | boolean | - | 97 | | onExpand | 列表项展开收起的回调 | (expand: boolean) => void | - | 98 | | expandable | 列表项展开配置 | [object](https://ant.design/components/table-cn/#expandable) | - | 99 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | module.exports = { 4 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 5 | moduleNameMapper: { 6 | '@ant-design/pro-list': join(__dirname, '/src/index.tsx'), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design/pro-list", 3 | "version": "0.0.5", 4 | "description": "🏆 Use Ant Design List like a Pro!", 5 | "repository": "https://github.com/ant-design/pro-list", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "module": "es/index.js", 9 | "types": "lib/index.d.ts", 10 | "files": [ 11 | "/lib", 12 | "/es", 13 | "/dist" 14 | ], 15 | "scripts": { 16 | "build": "father build && webpack", 17 | "lint": "npm run lint-eslint && npm run lint:style && tsc --noEmit", 18 | "lint-eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", 19 | "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty \"./src\" ./tests && npm run lint:style", 20 | "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", 21 | "prepublishOnly": "npm run test && npm run build && np --no-cleanup --yolo --no-publish", 22 | "prettier": "prettier -c --write \"**/*\"", 23 | "site": "dumi build && gh-pages -d ./dist", 24 | "start": "dumi dev", 25 | "test": "npm run lint && father test" 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "pretty-quick --staged" 30 | } 31 | }, 32 | "browserslist": [ 33 | "> 1%", 34 | "last 2 versions", 35 | "not ie <= 10" 36 | ], 37 | "dependencies": { 38 | "@ant-design/icons": "^4.0.0", 39 | "antd": "^4.0.0", 40 | "classnames": "^2.2.6", 41 | "dnd-core": "^10.0.2", 42 | "lodash.isequal": "^4.5.0", 43 | "lodash.tonumber": "^4.0.3", 44 | "moment": "^2.24.0", 45 | "rc-resize-observer": "^0.1.3", 46 | "rc-util": "^4.19.0", 47 | "react-dnd": "^10.0.2", 48 | "react-dnd-html5-backend": "^10.0.2", 49 | "unstated-next": "^1.1.0", 50 | "use-json-comparison": "^1.0.5", 51 | "use-media-antd-query": "1.0.2", 52 | "use-merge-value": "^1.0.1" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.8.3", 56 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 57 | "@babel/preset-env": "^7.8.3", 58 | "@babel/preset-react": "^7.8.3", 59 | "@babel/preset-typescript": "^7.8.3", 60 | "@types/classnames": "^2.2.9", 61 | "@types/enzyme": "^3.10.3", 62 | "@types/jest": "^24.0.23", 63 | "@types/lodash.isequal": "^4.5.5", 64 | "@types/lodash.tonumber": "^4.0.3", 65 | "@types/node": "^12.12.8", 66 | "@types/react": "^16.9.11", 67 | "@types/react-responsive": "^8.0.2", 68 | "@umijs/babel-preset-umi": "^3.0.14", 69 | "@umijs/fabric": "^2.0.0", 70 | "babel-plugin-import": "^1.12.2", 71 | "css-loader": "^3.4.2", 72 | "dumi": "^1.0.0", 73 | "enzyme": "^3.10.0", 74 | "enzyme-to-json": "^3.4.3", 75 | "eslint": "^7.2.0", 76 | "father": "^2.26.0", 77 | "husky": "^4.2.3", 78 | "jsdom-global": "^3.0.2", 79 | "less-loader": "^5.0.0", 80 | "np": "^5.1.3", 81 | "prettier": "^2.0.4", 82 | "pretty-quick": "^2.0.1", 83 | "react-github-btn": "^1.1.1", 84 | "style-loader": "^1.1.3", 85 | "stylelint": "^13.4.1", 86 | "typescript": "^3.3.3", 87 | "umi": "^3.0.0-beta.32", 88 | "umi-plugin-githubpages": "^2.0.1", 89 | "umi-request": "^1.2.15", 90 | "webpack-bundle-analyzer": "^3.6.0", 91 | "webpack-cli": "^3.3.10" 92 | }, 93 | "peerDependencies": { 94 | "react": "16.x" 95 | }, 96 | "authors": { 97 | "name": "chenshuai2144", 98 | "email": "qixian.cs@outlook.com" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | prolist.ant.design -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/pro-list/59284aecc8191f625a0a3a45491a81766c886c8f/public/favicon.ico -------------------------------------------------------------------------------- /src/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, Skeleton, Avatar } from 'antd'; 3 | import { RightOutlined } from '@ant-design/icons'; 4 | import useMergedState from 'rc-util/lib/hooks/useMergedState'; 5 | import { AvatarProps } from 'antd/es/avatar'; 6 | import { ListGridType } from 'antd/es/list'; 7 | import { ExpandableConfig } from 'antd/es/table/interface'; 8 | import classNames from 'classnames'; 9 | 10 | import getPrefixCls from './util/getPrefixCls'; 11 | 12 | export interface RenderExpandIconProps { 13 | prefixCls: string; 14 | expanded: boolean; 15 | expandIcon: React.ReactNode; 16 | onExpand: (expanded: boolean) => void; 17 | } 18 | 19 | export function renderExpandIcon({ 20 | prefixCls, 21 | expandIcon = , 22 | onExpand, 23 | expanded, 24 | }: RenderExpandIconProps) { 25 | const expandClassName = `${prefixCls}-row-expand-icon`; 26 | 27 | const onClick: React.MouseEventHandler = (event) => { 28 | onExpand(!expanded); 29 | event.stopPropagation(); 30 | }; 31 | 32 | return ( 33 | 40 | {expandIcon} 41 | 42 | ); 43 | } 44 | 45 | export interface ItemProps { 46 | title?: React.ReactNode; 47 | subTitle?: React.ReactNode; 48 | checkbox?: React.ReactNode; 49 | className?: string; 50 | prefixCls?: string; 51 | item?: any; 52 | subheader?: { 53 | title: React.ReactNode; 54 | actions: React.ReactNode[]; 55 | }; 56 | index?: number; 57 | selected?: boolean; 58 | avatar?: string | AvatarProps; 59 | children?: React.ReactNode; 60 | actions?: React.ReactNode[]; 61 | description?: React.ReactNode; 62 | loading?: boolean; 63 | style?: React.CSSProperties; 64 | extra?: React.ReactNode; 65 | grid?: ListGridType; 66 | expand?: boolean; 67 | rowSupportExpand?: boolean; 68 | onExpand?: (expand: boolean) => void; 69 | expandable?: ExpandableConfig; 70 | showActions?: 'hover' | 'always'; 71 | type?: 'new' | 'top' | 'inline' | 'subheader'; 72 | } 73 | 74 | /** 75 | * 头像的语法糖,支持之传入 Avatar 76 | * @param param 77 | */ 78 | const ProListItemAvatar: React.FC<{ 79 | className: string; 80 | avatar: string | AvatarProps; 81 | }> = ({ className, avatar }) => { 82 | if (!avatar) { 83 | return null; 84 | } 85 | if (typeof avatar === 'string') { 86 | return ( 87 |
88 | 89 |
90 | ); 91 | } 92 | return ( 93 |
94 | 95 |
96 | ); 97 | }; 98 | 99 | function ProListItem(props: ItemProps) { 100 | const { prefixCls: customizePrefixCls } = props; 101 | const prefixCls = getPrefixCls('list', customizePrefixCls); 102 | const defaultClassName = `${prefixCls}-row`; 103 | 104 | const { 105 | title, 106 | subTitle, 107 | children, 108 | prefixCls: restPrefixCls, 109 | actions, 110 | item, 111 | avatar, 112 | description, 113 | checkbox, 114 | index = 0, 115 | selected, 116 | loading, 117 | expand: propsExpand, 118 | onExpand: propsOnExpand, 119 | expandable: expandableConfig, 120 | rowSupportExpand, 121 | showActions, 122 | type, 123 | style, 124 | className: propsClassName = defaultClassName, 125 | ...rest 126 | } = props; 127 | 128 | const { expandedRowRender, expandIcon, expandRowByClick, indentSize = 8, expandedRowClassName } = 129 | expandableConfig || {}; 130 | 131 | const [expanded, onExpand] = useMergedState(!!propsExpand, { 132 | value: propsExpand, 133 | onChange: propsOnExpand, 134 | }); 135 | 136 | const className = classNames( 137 | { 138 | [`${propsClassName}-selected`]: selected, 139 | [`${propsClassName}-show-action-hover`]: showActions === 'hover', 140 | [`${propsClassName}-type-${type}`]: type, 141 | }, 142 | propsClassName, 143 | ); 144 | const needExpanded = !expanded || Object.values(expandableConfig || {}).length === 0; 145 | const expandedRowDom = expandedRowRender && expandedRowRender(item, index, indentSize, expanded); 146 | return ( 147 |
148 | { 152 | if (expandRowByClick) { 153 | onExpand(!expanded); 154 | } 155 | }} 156 | > 157 | 158 |
159 |
160 | {checkbox &&
{checkbox}
} 161 | {Object.values(expandableConfig || {}).length > 0 && 162 | rowSupportExpand && 163 | renderExpandIcon({ 164 | prefixCls, 165 | expandIcon, 166 | onExpand, 167 | expanded, 168 | })} 169 |
170 | } 172 | title={ 173 |
174 | {title &&
{title}
} 175 | {subTitle &&
{subTitle}
} 176 |
177 | } 178 | description={ 179 | description && 180 | needExpanded &&
{description}
181 | } 182 | /> 183 |
184 | {needExpanded && (children || expandedRowDom) && ( 185 |
186 | {children} 187 | {expandedRowRender && rowSupportExpand && ( 188 |
191 | {expandedRowDom} 192 |
193 | )} 194 |
195 | )} 196 |
197 |
198 |
199 | ); 200 | } 201 | 202 | /** 203 | * 简单的,只包含 title 和 actions 的分组标题 204 | * @param param0 205 | */ 206 | const ProListSubItem: React.FC<{ 207 | title?: React.ReactNode; 208 | actions?: React.ReactNode[]; 209 | className?: string; 210 | prefixCls?: string; 211 | style?: React.CSSProperties; 212 | }> = ({ style, prefixCls, title, actions, ...rest }) => { 213 | const defaultClassName = `${prefixCls}-row`; 214 | const { className = defaultClassName } = rest; 215 | return ( 216 |
217 |
{title}
218 |
{actions}
219 |
220 | ); 221 | }; 222 | export { ProListSubItem }; 223 | 224 | export default ProListItem; 225 | -------------------------------------------------------------------------------- /src/hooks/useLazyKVMap.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Key, GetRowKey } from 'antd/es/table/interface'; 3 | 4 | interface MapCache { 5 | data?: RecordType[]; 6 | childrenColumnName?: string; 7 | kvMap?: Map; 8 | getRowKey?: Function; 9 | } 10 | 11 | export function findAllChildrenKeys( 12 | data: RecordType[], 13 | getRowKey: GetRowKey, 14 | childrenColumnName: string, 15 | ): Key[] { 16 | const keys: Key[] = []; 17 | 18 | function dig(list: RecordType[]) { 19 | if (!Array.isArray(list)) { 20 | return; 21 | } 22 | (list || []).forEach((item, index) => { 23 | keys.push(getRowKey(item, index)); 24 | 25 | dig((item as any)[childrenColumnName]); 26 | }); 27 | } 28 | 29 | dig(data); 30 | 31 | return keys; 32 | } 33 | 34 | export default function useLazyKVMap( 35 | data: RecordType[], 36 | childrenColumnName: string, 37 | getRowKey: GetRowKey, 38 | ) { 39 | const mapCacheRef = React.useRef>({}); 40 | 41 | function getRecordByKey(key: Key): RecordType { 42 | if ( 43 | !mapCacheRef.current || 44 | mapCacheRef.current.data !== data || 45 | mapCacheRef.current.childrenColumnName !== childrenColumnName || 46 | mapCacheRef.current.getRowKey !== getRowKey 47 | ) { 48 | const kvMap = new Map(); 49 | 50 | /* eslint-disable no-inner-declarations */ 51 | function dig(records: RecordType[]) { 52 | records.forEach((record, index) => { 53 | const rowKey = getRowKey(record, index); 54 | kvMap.set(rowKey, record); 55 | }); 56 | } 57 | /* eslint-enable */ 58 | 59 | dig(data); 60 | 61 | mapCacheRef.current = { 62 | data, 63 | childrenColumnName, 64 | kvMap, 65 | getRowKey, 66 | }; 67 | } 68 | 69 | return mapCacheRef.current.kvMap!.get(key)!; 70 | } 71 | 72 | return [getRecordByKey]; 73 | } 74 | -------------------------------------------------------------------------------- /src/hooks/usePagination.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { PaginationProps, PaginationConfig } from 'antd/es/pagination'; 3 | 4 | export const DEFAULT_PAGE_SIZE = 10; 5 | 6 | export function getPaginationParam( 7 | pagination: PaginationConfig | boolean | undefined, 8 | mergedPagination: PaginationConfig, 9 | ) { 10 | const param: any = { 11 | current: mergedPagination.current, 12 | pageSize: mergedPagination.pageSize, 13 | }; 14 | const paginationObj = pagination && typeof pagination === 'object' ? pagination : {}; 15 | 16 | Object.keys(paginationObj).forEach((pageProp) => { 17 | const value = (mergedPagination as any)[pageProp]; 18 | 19 | if (typeof value !== 'function') { 20 | param[pageProp] = value; 21 | } 22 | }); 23 | 24 | return param; 25 | } 26 | 27 | export default function usePagination( 28 | total: number, 29 | pagination: PaginationConfig | false | undefined, 30 | onChange: (current: number, pageSize: number) => void, 31 | ): [PaginationConfig, () => void] { 32 | const { total: paginationTotal = 0, ...paginationObj } = 33 | pagination && typeof pagination === 'object' ? pagination : {}; 34 | 35 | const [innerPagination, setInnerPagination] = useState(() => { 36 | return { 37 | current: 'defaultCurrent' in paginationObj ? paginationObj.defaultCurrent : 1, 38 | pageSize: 39 | 'defaultPageSize' in paginationObj ? paginationObj.defaultPageSize : DEFAULT_PAGE_SIZE, 40 | }; 41 | }); 42 | 43 | // ============ Basic Pagination Config ============ 44 | const mergedPagination = { 45 | ...innerPagination, 46 | ...paginationObj, 47 | total: paginationTotal > 0 ? paginationTotal : total, 48 | }; 49 | 50 | if (!paginationTotal) { 51 | // Reset `current` if data length changed. Only reset when paginationObj do not have total 52 | const maxPage = Math.ceil(total / mergedPagination.pageSize!); 53 | if (maxPage < mergedPagination.current!) { 54 | mergedPagination.current = 1; 55 | } 56 | } 57 | 58 | const refreshPagination = (current: number = 1) => { 59 | setInnerPagination({ 60 | ...mergedPagination, 61 | current, 62 | }); 63 | }; 64 | 65 | const onInternalChange: PaginationProps['onChange'] = (...args) => { 66 | const [current] = args; 67 | refreshPagination(current); 68 | 69 | onChange(current, args[1] || mergedPagination.pageSize!); 70 | 71 | if (pagination && pagination.onChange) { 72 | pagination.onChange(...args); 73 | } 74 | }; 75 | 76 | const onInternalShowSizeChange: PaginationProps['onShowSizeChange'] = (...args) => { 77 | const [, pageSize] = args; 78 | setInnerPagination({ 79 | ...mergedPagination, 80 | current: 1, 81 | pageSize, 82 | }); 83 | 84 | onChange(1, pageSize); 85 | 86 | if (pagination && pagination.onShowSizeChange) { 87 | pagination.onShowSizeChange(...args); 88 | } 89 | }; 90 | 91 | if (pagination === false) { 92 | return [{}, () => {}]; 93 | } 94 | 95 | return [ 96 | { 97 | ...mergedPagination, 98 | onChange: onInternalChange, 99 | onShowSizeChange: onInternalShowSizeChange, 100 | }, 101 | refreshPagination, 102 | ]; 103 | } 104 | -------------------------------------------------------------------------------- /src/hooks/useSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import DownOutlined from '@ant-design/icons/DownOutlined'; 3 | import { noteOnce as warning } from 'rc-util/lib/warning'; 4 | import { 5 | TableRowSelection, 6 | Key, 7 | ColumnsType, 8 | GetRowKey, 9 | TableLocale, 10 | SelectionItem, 11 | ExpandType, 12 | ColumnType, 13 | } from 'antd/es/table/interface'; 14 | import { Checkbox, Dropdown, Menu, Radio } from 'antd'; 15 | import { CheckboxProps } from 'antd/es/checkbox'; 16 | 17 | const EMPTY_LIST: any[] = []; 18 | 19 | // TODO: warning if use ajax!!! 20 | export const SELECTION_ALL = 'SELECT_ALL'; 21 | export const SELECTION_INVERT = 'SELECT_INVERT'; 22 | 23 | interface UseSelectionConfig { 24 | prefixCls: string; 25 | pageData: RecordType[]; 26 | data: RecordType[]; 27 | getRowKey: GetRowKey; 28 | getRecordByKey: (key: Key) => RecordType; 29 | expandType: ExpandType; 30 | childrenColumnName: string; 31 | expandIconColumnIndex?: number; 32 | locale: TableLocale; 33 | } 34 | 35 | type InternalSelectionItem = SelectionItem | typeof SELECTION_ALL | typeof SELECTION_INVERT; 36 | 37 | function flattenData(data: RecordType[] | undefined): RecordType[] { 38 | const list: RecordType[] = []; 39 | (data || []).forEach((record) => { 40 | list.push(record); 41 | }); 42 | 43 | return list; 44 | } 45 | 46 | export default function useSelection( 47 | rowSelection: TableRowSelection | undefined, 48 | config: UseSelectionConfig, 49 | ): [() => ColumnType | null, Set] { 50 | const { 51 | selectedRowKeys, 52 | getCheckboxProps, 53 | onChange: onSelectionChange, 54 | onSelect, 55 | onSelectAll, 56 | onSelectInvert, 57 | onSelectMultiple, 58 | columnWidth: selectionColWidth = 60, 59 | type: selectionType, 60 | selections, 61 | } = rowSelection || {}; 62 | 63 | const { 64 | prefixCls, 65 | data, 66 | pageData, 67 | getRecordByKey, 68 | getRowKey, 69 | expandType, 70 | locale: tableLocale, 71 | } = config; 72 | 73 | const [innerSelectedKeys, setInnerSelectedKeys] = React.useState(); 74 | const mergedSelectedKeys = selectedRowKeys || innerSelectedKeys || EMPTY_LIST; 75 | const mergedSelectedKeySet = React.useMemo(() => { 76 | const keys = selectionType === 'radio' ? mergedSelectedKeys.slice(0, 1) : mergedSelectedKeys; 77 | return new Set(keys); 78 | }, [mergedSelectedKeys, selectionType]); 79 | 80 | // Save last selected key to enable range selection 81 | const [lastSelectedKey, setLastSelectedKey] = React.useState(null); 82 | 83 | // Reset if rowSelection reset 84 | React.useEffect(() => { 85 | if (!rowSelection) { 86 | setInnerSelectedKeys([]); 87 | } 88 | }, [!!rowSelection]); 89 | 90 | const setSelectedKeys = React.useCallback( 91 | (keys: Key[]) => { 92 | setInnerSelectedKeys(keys); 93 | 94 | const records = keys.map((key) => getRecordByKey(key)); 95 | 96 | if (onSelectionChange) { 97 | onSelectionChange(keys, records); 98 | } 99 | }, 100 | [setInnerSelectedKeys, getRecordByKey, onSelectionChange], 101 | ); 102 | 103 | // Trigger single `onSelect` event 104 | const triggerSingleSelection = React.useCallback( 105 | (key: Key, selected: boolean, keys: Key[], event: Event) => { 106 | if (onSelect) { 107 | const rows = keys.map((k) => getRecordByKey(k)); 108 | onSelect(getRecordByKey(key), selected, rows, event); 109 | } 110 | 111 | setSelectedKeys(keys); 112 | }, 113 | [onSelect, getRecordByKey, setSelectedKeys], 114 | ); 115 | 116 | const mergedSelections = React.useMemo(() => { 117 | if (!selections) { 118 | return null; 119 | } 120 | 121 | const selectionList: InternalSelectionItem[] = 122 | selections === true ? [SELECTION_ALL, SELECTION_INVERT] : selections; 123 | 124 | return selectionList.map((selection: InternalSelectionItem) => { 125 | if (selection === SELECTION_ALL) { 126 | return { 127 | key: 'all', 128 | text: tableLocale.selectionAll, 129 | onSelect() { 130 | setSelectedKeys(data.map((record, index) => getRowKey(record, index))); 131 | }, 132 | }; 133 | } 134 | if (selection === SELECTION_INVERT) { 135 | return { 136 | key: 'invert', 137 | text: tableLocale.selectInvert, 138 | onSelect() { 139 | const keySet = new Set(mergedSelectedKeySet); 140 | pageData.forEach((record, index) => { 141 | const key = getRowKey(record, index); 142 | 143 | if (keySet.has(key)) { 144 | keySet.delete(key); 145 | } else { 146 | keySet.add(key); 147 | } 148 | }); 149 | 150 | const keys = Array.from(keySet); 151 | setSelectedKeys(keys); 152 | if (onSelectInvert) { 153 | warning( 154 | false, 155 | '`onSelectInvert` will be removed in future. Please use `onChange` instead.', 156 | ); 157 | onSelectInvert(keys); 158 | } 159 | }, 160 | }; 161 | } 162 | return selection as SelectionItem; 163 | }); 164 | }, [selections, mergedSelectedKeySet, pageData, getRowKey]); 165 | 166 | const transformColumns = React.useCallback((): ColumnsType[number] | null => { 167 | if (!rowSelection) { 168 | return null; 169 | } 170 | 171 | // Get flatten data 172 | const flattedData = flattenData(pageData); 173 | 174 | // Support selection 175 | const keySet = new Set(mergedSelectedKeySet); 176 | 177 | // Get all checkbox props 178 | const checkboxPropsMap = new Map>(); 179 | flattedData.forEach((record, index) => { 180 | const key = getRowKey(record, index); 181 | const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {}; 182 | checkboxPropsMap.set(key, checkboxProps); 183 | 184 | if ('checked' in checkboxProps || 'defaultChecked' in checkboxProps) { 185 | warning( 186 | false, 187 | 'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.', 188 | ); 189 | } 190 | }); 191 | 192 | // Record key only need check with enabled 193 | const recordKeys = flattedData 194 | .map(getRowKey) 195 | .filter((key) => !checkboxPropsMap.get(key)!.disabled); 196 | const checkedCurrentAll = recordKeys.every((key) => keySet.has(key)); 197 | const checkedCurrentSome = recordKeys.some((key) => keySet.has(key)); 198 | 199 | const onSelectAllChange = () => { 200 | const changeKeys: Key[] = []; 201 | 202 | if (checkedCurrentAll) { 203 | recordKeys.forEach((key) => { 204 | keySet.delete(key); 205 | changeKeys.push(key); 206 | }); 207 | } else { 208 | recordKeys.forEach((key) => { 209 | keySet.add(key); 210 | changeKeys.push(key); 211 | }); 212 | } 213 | 214 | const keys = Array.from(keySet); 215 | setSelectedKeys(keys); 216 | 217 | if (onSelectAll) { 218 | onSelectAll( 219 | !checkedCurrentAll, 220 | keys.map((k) => getRecordByKey(k)), 221 | changeKeys.map((k) => getRecordByKey(k)), 222 | ); 223 | } 224 | }; 225 | 226 | // ===================== Render ===================== 227 | // Title Cell 228 | let title: React.ReactNode; 229 | if (selectionType !== 'radio') { 230 | let customizeSelections: React.ReactNode; 231 | if (mergedSelections) { 232 | const menu = ( 233 | 234 | {mergedSelections.map((selection, index) => { 235 | const { key, text, onSelect: onSelectionClick } = selection; 236 | return ( 237 | { 240 | if (onSelectionClick) { 241 | onSelectionClick(recordKeys); 242 | } 243 | }} 244 | > 245 | {text} 246 | 247 | ); 248 | })} 249 | 250 | ); 251 | customizeSelections = ( 252 |
253 | 254 | 255 | 256 | 257 | 258 |
259 | ); 260 | } 261 | 262 | const allDisabled = flattedData.every((record, index) => { 263 | const key = getRowKey(record, index); 264 | const checkboxProps = checkboxPropsMap.get(key) || {}; 265 | return checkboxProps.disabled; 266 | }); 267 | 268 | title = ( 269 |
270 | 276 | {customizeSelections} 277 |
278 | ); 279 | } 280 | 281 | // Body Cell 282 | let renderCell: (_: RecordType, record: RecordType, index: number) => React.ReactNode; 283 | if (selectionType === 'radio') { 284 | renderCell = (_, record, index) => { 285 | const key = getRowKey(record, index); 286 | 287 | return ( 288 | { 292 | if (!keySet.has(key)) { 293 | triggerSingleSelection(key, true, [key], event.nativeEvent); 294 | } 295 | }} 296 | /> 297 | ); 298 | }; 299 | } else { 300 | renderCell = (_, record, index) => { 301 | const key = getRowKey(record, index) || index; 302 | const hasKey = keySet.has(key); 303 | // Record checked 304 | return ( 305 | { 309 | const { shiftKey } = nativeEvent; 310 | 311 | let startIndex: number = -1; 312 | let endIndex: number = -1; 313 | 314 | // Get range of this 315 | if (shiftKey) { 316 | const pointKeys = new Set([lastSelectedKey, key]); 317 | 318 | recordKeys.some((recordKey, recordIndex) => { 319 | if (pointKeys.has(recordKey)) { 320 | if (startIndex === -1) { 321 | startIndex = recordIndex; 322 | } else { 323 | endIndex = recordIndex; 324 | return true; 325 | } 326 | } 327 | 328 | return false; 329 | }); 330 | } 331 | 332 | if (endIndex !== -1 && startIndex !== endIndex) { 333 | // Batch update selections 334 | const rangeKeys = recordKeys.slice(startIndex, endIndex + 1); 335 | const changedKeys: Key[] = []; 336 | 337 | if (hasKey) { 338 | rangeKeys.forEach((recordKey) => { 339 | if (keySet.has(recordKey)) { 340 | changedKeys.push(recordKey); 341 | keySet.delete(recordKey); 342 | } 343 | }); 344 | } else { 345 | rangeKeys.forEach((recordKey) => { 346 | if (!keySet.has(recordKey)) { 347 | changedKeys.push(recordKey); 348 | keySet.add(recordKey); 349 | } 350 | }); 351 | } 352 | 353 | const keys = Array.from(keySet); 354 | setSelectedKeys(keys); 355 | if (onSelectMultiple) { 356 | onSelectMultiple( 357 | !hasKey, 358 | keys.map((recordKey) => getRecordByKey(recordKey)), 359 | changedKeys.map((recordKey) => getRecordByKey(recordKey)), 360 | ); 361 | } 362 | } else { 363 | // Single record selected 364 | if (hasKey) { 365 | keySet.delete(key); 366 | } else { 367 | keySet.add(key); 368 | } 369 | 370 | triggerSingleSelection(key, !hasKey, Array.from(keySet), nativeEvent); 371 | } 372 | 373 | setLastSelectedKey(key); 374 | }} 375 | /> 376 | ); 377 | }; 378 | } 379 | 380 | // Columns 381 | const selectionColumn = { 382 | width: selectionColWidth, 383 | className: `${prefixCls}-selection-column`, 384 | title: rowSelection.columnTitle || title, 385 | render: renderCell, 386 | }; 387 | 388 | return selectionColumn; 389 | }, [ 390 | getRowKey, 391 | pageData, 392 | rowSelection, 393 | innerSelectedKeys, 394 | mergedSelectedKeys, 395 | selectionColWidth, 396 | mergedSelections, 397 | expandType, 398 | lastSelectedKey, 399 | onSelectMultiple, 400 | triggerSingleSelection, 401 | ]); 402 | 403 | return [transformColumns, mergedSelectedKeySet]; 404 | } 405 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | @import '~antd/lib/list/style/index.less'; 3 | 4 | @pro-list-prefix-cls: ant-pro-list; 5 | 6 | .@{pro-list-prefix-cls} { 7 | .@{pro-list-prefix-cls}-row { 8 | padding: 0 18px 0 24px; 9 | 10 | &.@{pro-list-prefix-cls}-row-selected { 11 | background-color: @table-selected-row-bg; 12 | } 13 | 14 | &.@{pro-list-prefix-cls}-row-type-new { 15 | animation: techUiListActive 3s; 16 | } 17 | 18 | &.@{pro-list-prefix-cls}-row-type-inline { 19 | .@{pro-list-prefix-cls}-row-title { 20 | font-weight: normal; 21 | } 22 | } 23 | 24 | &.@{pro-list-prefix-cls}-row-type-top { 25 | background-image: url('https://gw.alipayobjects.com/zos/antfincdn/DehQfMbOJb/icon.svg'); 26 | background-repeat: no-repeat; 27 | background-position: left top; 28 | background-size: 12px 12px; 29 | } 30 | 31 | &:hover { 32 | background-color: rgba(0, 0, 0, 0.02); 33 | transition: background-color 0.3s; 34 | .@{ant-prefix}-list-item-action { 35 | display: block; 36 | } 37 | 38 | .@{pro-list-prefix-cls}-row-subheader-actions { 39 | display: block; 40 | } 41 | } 42 | 43 | &-show-action-hover { 44 | .@{ant-prefix}-list-item-action { 45 | display: none; 46 | } 47 | } 48 | 49 | &-subheader { 50 | display: flex; 51 | align-items: center; 52 | justify-content: space-between; 53 | height: 44px; 54 | padding: 0 24px; 55 | color: rgba(0, 0, 0, 0.45); 56 | line-height: 44px; 57 | background: rgba(0, 0, 0, 0.02); 58 | 59 | &-actions { 60 | display: none; 61 | } 62 | 63 | &-actions > * { 64 | margin-right: 8px; 65 | 66 | &:last-child { 67 | margin-right: 0; 68 | } 69 | } 70 | } 71 | 72 | &-header { 73 | display: flex; 74 | flex: 1; 75 | justify-content: flex-start; 76 | } 77 | 78 | &-header-title { 79 | display: flex; 80 | align-items: center; 81 | justify-content: flex-start; 82 | } 83 | 84 | &-header-option { 85 | display: flex; 86 | } 87 | 88 | &-checkbox { 89 | width: 16px; 90 | margin-right: 12px; 91 | } 92 | 93 | &-expand-icon { 94 | margin-right: 8; 95 | margin-right: 8px; 96 | color: rgba(0, 0, 0, 0.45); 97 | > .anticon > svg { 98 | transition: 0.3s; 99 | } 100 | } 101 | 102 | &-collapsed { 103 | > .anticon > svg { 104 | transform: rotate(90deg); 105 | } 106 | } 107 | 108 | &-title { 109 | margin-right: 16px; 110 | cursor: pointer; 111 | &:hover { 112 | color: @primary-color; 113 | } 114 | } 115 | 116 | &-content { 117 | position: relative; 118 | display: flex; 119 | flex: 1; 120 | flex-direction: column; 121 | margin: 0 32px; 122 | } 123 | 124 | &-subTitle { 125 | color: rgba(0, 0, 0, 0.45); 126 | } 127 | 128 | &-description { 129 | margin-top: 4px; 130 | } 131 | 132 | &:last-child { 133 | border-bottom: none; 134 | .@{ant-prefix}-list-item { 135 | border-bottom: none; 136 | } 137 | } 138 | 139 | &-avatar { 140 | display: flex; 141 | } 142 | } 143 | 144 | .@{ant-prefix}-list { 145 | &-header { 146 | padding: 0; 147 | border-bottom: none; 148 | } 149 | 150 | .@{ant-prefix}-list-item { 151 | width: 100%; 152 | border-bottom: 1px solid @border-color-split; 153 | 154 | &-meta-avatar { 155 | display: flex; 156 | align-items: center; 157 | margin-right: 8px; 158 | } 159 | &-action-split { 160 | display: none; 161 | } 162 | &-meta-title { 163 | margin: 0; 164 | } 165 | } 166 | } 167 | 168 | &-no-split { 169 | .@{ant-prefix}-list .@{ant-prefix}-list-item { 170 | border-bottom: none; 171 | } 172 | } 173 | 174 | &-bordered { 175 | .@{pro-list-prefix-cls}-toolbar { 176 | border-bottom: 1px solid @border-color-split; 177 | } 178 | } 179 | 180 | .@{ant-prefix}-list-vertical { 181 | .@{pro-list-prefix-cls}-row { 182 | padding: 0 24px; 183 | &-header-title { 184 | display: flex; 185 | flex-direction: column; 186 | align-items: flex-start; 187 | justify-content: center; 188 | } 189 | 190 | &-content { 191 | margin: 0; 192 | } 193 | 194 | &-subTitle { 195 | margin-top: 8px; 196 | } 197 | } 198 | 199 | .@{ant-prefix}-list-item-extra { 200 | display: flex; 201 | align-items: center; 202 | margin-left: 32px; 203 | } 204 | 205 | .@{pro-list-prefix-cls}-row-description { 206 | margin-top: 16px; 207 | } 208 | } 209 | 210 | .@{ant-prefix}-list-bordered .@{ant-prefix}-list-item { 211 | padding-right: 0; 212 | padding-left: 0; 213 | } 214 | } 215 | 216 | @keyframes techUiListActive { 217 | 0% { 218 | background-color: unset; 219 | } 220 | 30% { 221 | background: #fefbe6; 222 | } 223 | 100% { 224 | background-color: unset; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List } from 'antd'; 3 | import classNames from 'classnames'; 4 | import { noteOnce } from 'rc-util/lib/warning'; 5 | import { TableRowSelection, GetRowKey, ExpandableConfig } from 'antd/es/table/interface'; 6 | import { ListProps } from 'antd/es/list'; 7 | import ToolBar, { ToolBarProps } from './toolBar'; 8 | import useSelection from './hooks/useSelection'; 9 | import useLazyKVMap, { findAllChildrenKeys } from './hooks/useLazyKVMap'; 10 | import usePagination from './hooks/usePagination'; 11 | import getPrefixCls from './util/getPrefixCls'; 12 | import ProListItem, { ItemProps, ProListSubItem } from './Item'; 13 | 14 | import './index.less'; 15 | 16 | type AntdListProps = Omit, 'rowKey'>; 17 | 18 | type WithFalse = T | false; 19 | 20 | export interface HeaderViewProps { 21 | title?: React.ReactNode; 22 | actions?: React.ReactNode[]; 23 | } 24 | 25 | export interface ProListProps 26 | extends Omit, 27 | AntdListProps { 28 | rowSelection?: TableRowSelection; 29 | rowKey?: string | GetRowKey; 30 | renderItem: (row: RecordType, index: number) => ItemProps; 31 | listRenderItem?: (row: RecordType, index: number) => React.ReactNode; 32 | headerRender?: WithFalse< 33 | (props: HeaderViewProps, defaultDom: React.ReactNode) => React.ReactNode 34 | >; 35 | expandable?: ExpandableConfig; 36 | showActions?: 'hover' | 'always'; 37 | } 38 | 39 | export type Key = React.Key; 40 | 41 | export type TriggerEventHandler = (record: RecordType) => void; 42 | 43 | function ProList(props: ProListProps) { 44 | const { 45 | rowSelection, 46 | prefixCls: customizePrefixCls, 47 | pagination, 48 | dataSource = [], 49 | rowKey, 50 | showActions = 'always', 51 | bordered, 52 | headerRender, 53 | split = true, 54 | expandable: expandableConfig, 55 | ...rest 56 | } = props; 57 | const prefixCls = getPrefixCls('list', customizePrefixCls); 58 | 59 | const getRowKey = React.useMemo>((): GetRowKey => { 60 | if (typeof rowKey === 'function' && rowKey) { 61 | return rowKey; 62 | } 63 | 64 | return (record: RecordType, index?: number) => (record as any)[rowKey as string] || index; 65 | }, [rowKey]); 66 | 67 | const mergedData = dataSource.flatMap((item) => { 68 | // @ts-ignore 69 | if (item.children && Array.isArray(item.children)) { 70 | // @ts-ignore 71 | return [{ ...item }, ...item.children]; 72 | } 73 | return item; 74 | }); 75 | 76 | const [getRecordByKey] = useLazyKVMap(mergedData, 'children', getRowKey); 77 | 78 | // 合并分页的的配置 79 | const [mergedPagination] = usePagination(mergedData.length, pagination, () => { 80 | // console.log('run'); 81 | }); 82 | 83 | /** 84 | * 根据分页来回去不同的数据,模拟 table 85 | */ 86 | const pageData = React.useMemo(() => { 87 | if ( 88 | pagination === false || 89 | !mergedPagination.pageSize || 90 | mergedData.length < mergedPagination.total! 91 | ) { 92 | return mergedData; 93 | } 94 | 95 | const { current = 1, pageSize = 10 } = mergedPagination; 96 | const currentPageData = mergedData.slice((current - 1) * pageSize, current * pageSize); 97 | return currentPageData; 98 | }, [ 99 | !!pagination, 100 | mergedData, 101 | mergedPagination && mergedPagination.current, 102 | mergedPagination && mergedPagination.pageSize, 103 | mergedPagination && mergedPagination.total, 104 | ]); 105 | 106 | /** 107 | * 提供和 table 一样的 rowSelection 配置 108 | */ 109 | const [selectItemRender, selectedKeySet] = useSelection(rowSelection, { 110 | getRowKey, 111 | getRecordByKey, 112 | prefixCls, 113 | data: dataSource, 114 | pageData, 115 | expandType: 'row', 116 | childrenColumnName: 'children', 117 | locale: {}, 118 | expandIconColumnIndex: 0, 119 | }); 120 | 121 | const { 122 | expandedRowKeys, 123 | defaultExpandedRowKeys, 124 | defaultExpandAllRows = true, 125 | onExpand, 126 | onExpandedRowsChange, 127 | } = expandableConfig || {}; 128 | 129 | const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => { 130 | if (defaultExpandedRowKeys) { 131 | return defaultExpandedRowKeys; 132 | } 133 | if (defaultExpandAllRows !== false) { 134 | const keys = findAllChildrenKeys(mergedData, getRowKey, 'children'); 135 | if (onExpandedRowsChange) { 136 | onExpandedRowsChange(keys); 137 | } 138 | return keys; 139 | } 140 | return []; 141 | }); 142 | 143 | const mergedExpandedKeys = React.useMemo( 144 | () => new Set(expandedRowKeys || innerExpandedKeys || []), 145 | [expandedRowKeys, innerExpandedKeys], 146 | ); 147 | 148 | const onTriggerExpand: TriggerEventHandler = React.useCallback( 149 | (record: RecordType) => { 150 | const key = getRowKey(record, mergedData.indexOf(record)); 151 | let newExpandedKeys: Key[]; 152 | const hasKey = mergedExpandedKeys.has(key); 153 | if (hasKey) { 154 | mergedExpandedKeys.delete(key); 155 | newExpandedKeys = [...mergedExpandedKeys]; 156 | } else { 157 | newExpandedKeys = [...mergedExpandedKeys, key]; 158 | } 159 | 160 | setInnerExpandedKeys(newExpandedKeys); 161 | if (onExpand) { 162 | onExpand(!hasKey, record); 163 | } 164 | if (onExpandedRowsChange) { 165 | onExpandedRowsChange(newExpandedKeys); 166 | } 167 | }, 168 | [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange], 169 | ); 170 | 171 | /** 172 | * 这个是 选择框的 render 方法 173 | * 为了兼容 antd 的 table,用了同样的渲染逻辑 174 | * 所以看起来有点奇怪 175 | */ 176 | const selectItemDom = selectItemRender(); 177 | 178 | const defaultRenderItem = () => { 179 | const { rowExpandable } = expandableConfig || {}; 180 | const { renderItem } = props; 181 | 182 | if (renderItem) { 183 | return (item: RecordType, index: number) => { 184 | const ProListItemProps = renderItem(item, index); 185 | // @ts-ignore 186 | if (item.children && Array.isArray(item.children)) { 187 | return ( 188 | 193 | ); 194 | } 195 | if (!ProListItemProps) { 196 | return undefined; 197 | } 198 | return ( 199 | { 205 | onTriggerExpand(item); 206 | }} 207 | showActions={showActions} 208 | rowSupportExpand={!rowExpandable || (rowExpandable && rowExpandable(item))} 209 | selected={selectedKeySet.has(getRowKey(item, index))} 210 | checkbox={ 211 | selectItemDom && selectItemDom.render && selectItemDom?.render(item, item, index) 212 | } 213 | item={item} 214 | {...ProListItemProps} 215 | /> 216 | ); 217 | }; 218 | } 219 | if (props.listRenderItem) { 220 | return props.listRenderItem; 221 | } 222 | 223 | noteOnce(!!props.listRenderItem, 'list need renderItem'); 224 | 225 | return (item: RecordType, index: number) => ( 226 | { 231 | onTriggerExpand(item); 232 | }} 233 | showActions={showActions} 234 | rowSupportExpand={!rowExpandable || (rowExpandable && rowExpandable(item))} 235 | selected={selectedKeySet.has(getRowKey(item, index))} 236 | checkbox={selectItemDom && selectItemDom.render && selectItemDom.render(item, item, index)} 237 | {...item} 238 | /> 239 | ); 240 | }; 241 | const listClassName = classNames(prefixCls, { 242 | [`${prefixCls}-bordered`]: bordered, 243 | [`${prefixCls}-no-split`]: !split, 244 | }); 245 | 246 | const renderHeader = () => { 247 | if (headerRender === false) { 248 | return null; 249 | } 250 | 251 | const defaultDom = (rest.title || rest.actions) && ( 252 | 253 | ); 254 | 255 | if (headerRender) { 256 | return headerRender({ title: rest.title, actions: rest.actions }, defaultDom); 257 | } 258 | 259 | return defaultDom; 260 | }; 261 | 262 | return ( 263 |
264 | 265 | {...rest} 266 | split={false} 267 | header={renderHeader()} 268 | bordered={bordered} 269 | dataSource={pageData} 270 | renderItem={defaultRenderItem()} 271 | pagination={pagination && mergedPagination} 272 | /> 273 |
274 | ); 275 | } 276 | 277 | export default ProList; 278 | -------------------------------------------------------------------------------- /src/toolBar/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import '../index'; 3 | 4 | .@{pro-list-prefix-cls}-toolbar { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 64px; 9 | padding: 0 24px; 10 | line-height: 64px; 11 | &-option { 12 | display: flex; 13 | align-items: center; 14 | justify-content: flex-end; 15 | } 16 | 17 | &-item-icon { 18 | margin-left: 16px; 19 | font-size: 16px; 20 | cursor: pointer; 21 | &:first-child { 22 | margin-left: 8px; 23 | } 24 | } 25 | 26 | &-title { 27 | flex: 1; 28 | color: @label-color; 29 | font-size: 16px; 30 | line-height: 24px; 31 | opacity: 0.85; 32 | } 33 | } 34 | 35 | @media (max-width: 575px) { 36 | .@{pro-list-prefix-cls}-toolbar { 37 | flex-direction: column; 38 | align-items: flex-start; 39 | justify-content: flex-start; 40 | margin-bottom: 16px; 41 | margin-left: 16px; 42 | padding: 0; 43 | line-height: normal; 44 | 45 | &-option { 46 | display: flex; 47 | justify-content: space-between; 48 | width: 100%; 49 | } 50 | 51 | &-default-option { 52 | display: flex; 53 | flex: 1; 54 | align-items: center; 55 | justify-content: flex-end; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/toolBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Space } from 'antd'; 3 | import './index.less'; 4 | 5 | export interface ToolBarProps { 6 | title?: React.ReactNode; 7 | actions?: React.ReactNode[]; 8 | className?: string; 9 | } 10 | 11 | const ToolBar = ({ title, actions, className }: ToolBarProps) => { 12 | return ( 13 |
14 |
{title}
15 |
16 | {actions && ( 17 | 18 | {actions 19 | .filter((item) => item) 20 | .map((node, index) => ( 21 |
25 | {node} 26 |
27 | ))} 28 |
29 | )} 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default ToolBar; 36 | -------------------------------------------------------------------------------- /src/util/getPrefixCls.ts: -------------------------------------------------------------------------------- 1 | const getPrefixCls = (suffixCls: string, customizePrefixCls?: string) => { 2 | const prefixCls = 'ant-pro'; 3 | 4 | if (customizePrefixCls) return customizePrefixCls; 5 | 6 | return suffixCls ? `${prefixCls}-${suffixCls}` : prefixCls; 7 | }; 8 | 9 | export default getPrefixCls; 10 | -------------------------------------------------------------------------------- /tests/demo.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import { render } from 'enzyme'; 3 | import * as React from 'react'; 4 | import { readdirSync } from 'fs'; 5 | import { join } from 'path'; 6 | 7 | const demoPath = join(__dirname, '../docs/demo/'); 8 | const demos = readdirSync(demoPath); 9 | 10 | describe('ProList render', () => { 11 | demos.forEach((file) => { 12 | test(`${file} should match snapshot `, () => { 13 | // eslint-disable-next-line import/no-dynamic-require 14 | const Demo = require(join(demoPath, file)).default; 15 | const wrapper = render(); 16 | expect(wrapper).toMatchSnapshot(); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "experimentalDecorators": true, 17 | "strict": true, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | "paths": { 21 | "@ant-design/pro-list": ["./src"] 22 | } 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "build", 27 | "scripts", 28 | "acceptance-tests", 29 | "webpack", 30 | "jest", 31 | "tslint:latest", 32 | "tslint-config-prettier", 33 | "example", 34 | "_test_", 35 | "tests" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.png'; 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.tsx', 5 | output: { 6 | library: 'ProList', 7 | libraryExport: 'default', 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'pro-list.umd.js', 10 | globalObject: 'this', 11 | }, 12 | mode: 'production', 13 | resolve: { 14 | extensions: ['.ts', '.tsx', '.json', '.css', '.js'], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | exclude: /(node_modules|bower_components)/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/typescript', '@babel/env', '@babel/react'], 25 | plugins: ['@babel/proposal-class-properties', '@babel/proposal-object-rest-spread'], 26 | }, 27 | }, 28 | }, 29 | { 30 | test: /\.tsx?$/, 31 | exclude: /(node_modules|bower_components)/, 32 | use: { 33 | loader: 'babel-loader', 34 | options: { 35 | presets: [ 36 | '@babel/typescript', 37 | [ 38 | '@babel/env', 39 | { 40 | loose: true, 41 | modules: false, 42 | }, 43 | ], 44 | '@babel/react', 45 | ], 46 | plugins: ['@babel/proposal-class-properties', '@babel/proposal-object-rest-spread'], 47 | }, 48 | }, 49 | }, 50 | { 51 | test: /\.less$/, 52 | use: [ 53 | { 54 | loader: 'style-loader', // creates style nodes from JS strings 55 | }, 56 | { 57 | loader: 'css-loader', // translates CSS into CommonJS 58 | }, 59 | { 60 | loader: 'less-loader', 61 | options: { 62 | javascriptEnabled: true, 63 | }, 64 | }, 65 | ], 66 | }, 67 | { 68 | test: /\.css$/, 69 | use: [ 70 | { 71 | loader: 'style-loader', // creates style nodes from JS strings 72 | }, 73 | { 74 | loader: 'css-loader', // translates CSS into CommonJS 75 | }, 76 | ], 77 | }, 78 | ], 79 | }, 80 | externals: [ 81 | { 82 | react: 'React', 83 | 'react-dom': 'ReactDOM', 84 | antd: 'antd', 85 | moment: 'moment', 86 | }, 87 | /^antd/, 88 | ], 89 | }; 90 | --------------------------------------------------------------------------------