├── public ├── CNAME └── favicon.ico ├── .prettierignore ├── .eslintignore ├── typings.d.ts ├── .stylelintrc.js ├── jest.config.js ├── .eslintrc.js ├── .babelrc ├── src ├── component │ ├── status │ │ ├── index.less │ │ └── index.tsx │ ├── dropdown │ │ ├── index.less │ │ └── index.tsx │ ├── alert │ │ ├── index.less │ │ └── index.tsx │ ├── indexColumn │ │ ├── index.less │ │ └── index.tsx │ ├── percent │ │ ├── util.ts │ │ └── index.tsx │ ├── toolBar │ │ ├── FullscreenIcon.tsx │ │ ├── DensityIcon.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── ErrorBoundary.tsx │ ├── columnSetting │ │ ├── index.less │ │ ├── DndItem.tsx │ │ └── index.tsx │ ├── intlContext │ │ └── index.tsx │ └── util.tsx ├── locale │ ├── ja_JP.tsx │ ├── zh_CN.tsx │ ├── zh_TW.tsx │ ├── en_US.tsx │ ├── ms_MY.tsx │ ├── vi_VN.tsx │ ├── it_IT.tsx │ ├── ru_RU.tsx │ ├── es_ES.tsx │ └── fr_FR.tsx ├── index.less ├── index.tsx ├── form │ ├── FormOption.tsx │ └── index.less ├── container.tsx ├── useFetchData.tsx └── defaultRender.tsx ├── .fatherrc.ts ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md └── workflows │ ├── rebase.yml │ ├── test.yml │ └── deploy.yml ├── .umirc.js ├── .gitignore ├── docs ├── example.md ├── intl.md ├── index.md ├── demo │ ├── valueTypeDate.tsx │ ├── valueTypeNumber.tsx │ ├── search.tsx │ ├── batchOption.tsx │ ├── search_option.tsx │ ├── valueType.tsx │ ├── linkage_form.tsx │ └── single.tsx ├── valueType.md ├── example │ ├── form.tsx │ ├── search.tsx │ ├── dataSource.tsx │ ├── columnsStateMap.tsx │ ├── pollinga.tsx │ ├── nested-table.tsx │ ├── renderTable.tsx │ └── intl.tsx ├── search.md ├── api.md └── getting-started.md ├── tsconfig.json ├── LICENSE ├── tests └── __tests__ │ ├── index.test.tsx │ ├── demo.tsx │ └── __snapshots__ │ └── index.test.tsx.snap ├── package.json ├── webpack.config.js ├── README.md └── README.en_US.md /public/CNAME: -------------------------------------------------------------------------------- 1 | protable.ant.design -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | .umi 3 | .umi-production 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.png'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/pro-table/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 3 | }; 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "import", 5 | { 6 | "libraryName": "antd", 7 | "style": "css" // or 'css' 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/component/status/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-dropdown-prefix-cls: ~'@{ant-prefix}-pro-table-dropdown'; 4 | 5 | .@{pro-dropdown-prefix-cls} { 6 | width: auto; 7 | } 8 | -------------------------------------------------------------------------------- /src/component/dropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-dropdown-prefix-cls: ~'@{ant-prefix}-pro-table-dropdown'; 4 | 5 | .@{pro-dropdown-prefix-cls} { 6 | width: auto; 7 | } 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 Ant Design Pro Table 的需求或建议 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 Table 使用的疑问或需要帮助 4 | title: '🧐[问题]' 5 | labels: '🧐 Question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 问题描述 [详细地描述问题,让大家都能理解] 10 | 11 | ### 💻 示例代码 [如果有必要,展示代码,线上示例,或仓库] 12 | 13 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/component/alert/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-table-alert-prefix-cls: ~'@{ant-prefix}-pro-table-alert'; 4 | 5 | .@{pro-table-alert-prefix-cls} { 6 | margin-bottom: 16px; 7 | 8 | &-info { 9 | display: flex; 10 | align-items: center; 11 | transition: 0.3 all; 12 | &-content { 13 | flex: 1; 14 | } 15 | &-option { 16 | min-width: 48px; 17 | padding-left: 16px; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.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:coverage 18 | 19 | - name: Generate coverage 20 | run: bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 Ant Design Pro Table 的 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 | -------------------------------------------------------------------------------- /src/component/indexColumn/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-table-index-prefix-cls: ~'@{ant-prefix}-pro-table-index-column'; 4 | 5 | .@{pro-table-index-prefix-cls} { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | width: 18px; 10 | height: 18px; 11 | 12 | &-border { 13 | color: #fff; 14 | font-size: 12px; 15 | line-height: 12px; 16 | background-color: #314659; 17 | border-radius: 9px; 18 | &.top-three { 19 | background-color: #979797; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'ProTable', 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 | navs: [ 16 | null, 17 | { 18 | title: 'GitHub', 19 | path: 'https://github.com/ant-design/pro-table', 20 | }, 21 | ], 22 | hash: true, 23 | dynamicImport: { 24 | loading: '@ant-design/pro-skeleton', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/component/percent/util.ts: -------------------------------------------------------------------------------- 1 | /** 获取展示符号 */ 2 | export function getSymbolByRealValue(realValue: number) { 3 | return realValue > 0 ? '+' : ''; 4 | } 5 | 6 | /** 获取颜色 */ 7 | export function getColorByRealValue(realValue: number /** ,color: string */) { 8 | if (realValue === 0) { 9 | return '#595959'; 10 | } 11 | return realValue > 0 ? '#ff4d4f' : '#52c41a'; 12 | } 13 | 14 | /** 获取到最后展示的数字 */ 15 | export function getRealTextWithPrecision(realValue: number, precision: number = 2) { 16 | if (precision && precision <= 0) { 17 | throw new Error('precision must be more the zero'); 18 | } 19 | 20 | return precision && precision > 0 ? realValue.toFixed(precision) : realValue; 21 | } 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 例子 3 | order: 0 4 | sidemenu: false 5 | nav: 6 | title: 例子 7 | order: 9 8 | --- 9 | 10 | # 各种示例 11 | 12 | 这里将会提供一些常用的功能示例,方便大家直接拷贝。 13 | 14 | ## 批量操作 15 | 16 | 17 | 18 | ## toolbar 搜索 19 | 20 | 21 | 22 | ## form 操作 23 | 24 | 25 | 26 | ## 使用 dataSource 和 loading 27 | 28 | 29 | 30 | ## 受控的列显示隐藏 31 | 32 | 可以默认隐藏某些栏,但是在操作栏中可以选择 33 | 34 | 35 | 36 | ## 轮询 37 | 38 | 39 | 40 | ## 嵌套表格 41 | 42 | 43 | 44 | ## 自定义表格的主体 45 | 46 | 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/component/indexColumn/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context'; 4 | import './index.less'; 5 | 6 | /** 7 | * 默认的 index 列容器,提供一个好看的 index 8 | * @param param0 9 | */ 10 | const IndexColumn: React.FC<{ border?: boolean }> = ({ border = false, children }) => ( 11 | 12 | {({ getPrefixCls }: ConfigConsumerProps) => { 13 | const className = getPrefixCls('pro-table-index-column'); 14 | return ( 15 |
2, 19 | })} 20 | > 21 | {children} 22 |
23 | ); 24 | }} 25 |
26 | ); 27 | 28 | export default IndexColumn; 29 | -------------------------------------------------------------------------------- /src/component/toolBar/FullscreenIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; 3 | import { Tooltip } from 'antd'; 4 | import { useIntl } from '../intlContext'; 5 | 6 | const FullScreenIcon = () => { 7 | const intl = useIntl(); 8 | const [fullscreen, setFullscreen] = useState(false); 9 | useEffect(() => { 10 | document.onfullscreenchange = () => { 11 | setFullscreen(!!document.fullscreenElement); 12 | }; 13 | }, []); 14 | return fullscreen ? ( 15 | 16 | 17 | 18 | ) : ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default FullScreenIcon; 26 | -------------------------------------------------------------------------------- /src/component/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo } from 'react'; 2 | import { Result } from 'antd'; 3 | 4 | class ErrorBoundary extends React.Component<{}, { hasError: boolean; errorInfo: string }> { 5 | state = { hasError: false, errorInfo: '' }; 6 | 7 | static getDerivedStateFromError(error: Error) { 8 | return { hasError: true, errorInfo: error.message }; 9 | } 10 | 11 | componentDidCatch(error: any, errorInfo: ErrorInfo) { 12 | // You can also log the error to an error reporting service 13 | // eslint-disable-next-line no-console 14 | console.log(error, errorInfo); 15 | } 16 | 17 | render() { 18 | if (this.state.hasError) { 19 | // You can render any custom fallback UI 20 | return ; 21 | } 22 | return this.props.children; 23 | } 24 | } 25 | 26 | export default ErrorBoundary; 27 | -------------------------------------------------------------------------------- /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-table": ["./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 | -------------------------------------------------------------------------------- /src/locale/ja_JP.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '¥', 3 | tableForm: { 4 | search: '検索', 5 | reset: 'リセット', 6 | submit: '提交', 7 | collapsed: '展開', 8 | expand: '収納', 9 | inputPlaceholder: '入力してください', 10 | selectPlaceholder: '選択してください', 11 | }, 12 | alert: { 13 | clear: 'クリア', 14 | selected: '選択した', 15 | item: '項目', 16 | }, 17 | pagination: { 18 | total: { 19 | range: '記事', 20 | total: '/合計', 21 | item: ' ', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: '左に固定', 26 | rightPin: '右に固定', 27 | noPin: 'キャンセル', 28 | leftFixedTitle: '左に固定された項目', 29 | rightFixedTitle: '右に固定された項目', 30 | noFixedTitle: '固定されてない項目', 31 | reset: 'リセット', 32 | columnDisplay: '表示列', 33 | columnSetting: '列表示設定', 34 | fullScreen: 'フルスクリーン', 35 | exitFullScreen: '終了', 36 | reload: '更新', 37 | density: '行高', 38 | densityLarger: '默认', 39 | densityMiddle: '中', 40 | densitySmall: '小', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/locale/zh_CN.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '¥', 3 | tableForm: { 4 | search: '查询', 5 | reset: '重置', 6 | submit: '提交', 7 | collapsed: '展开', 8 | expand: '收起', 9 | inputPlaceholder: '请输入', 10 | selectPlaceholder: '请选择', 11 | }, 12 | alert: { 13 | clear: '清空', 14 | selected: '已选择', 15 | item: '项', 16 | }, 17 | pagination: { 18 | total: { 19 | range: '第', 20 | total: '条/总共', 21 | item: '条', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: '固定到左边', 26 | rightPin: '固定到右边', 27 | noPin: '取消固定', 28 | leftFixedTitle: '固定在左侧', 29 | rightFixedTitle: '固定在右侧', 30 | noFixedTitle: '不固定', 31 | reset: '重置', 32 | columnDisplay: '列展示', 33 | columnSetting: '列设置', 34 | fullScreen: '全屏', 35 | exitFullScreen: '退出全屏', 36 | reload: '刷新', 37 | density: '密度', 38 | densityDefault: '正常', 39 | densityLarger: '默认', 40 | densityMiddle: '中等', 41 | densitySmall: '紧凑', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/locale/zh_TW.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '¥', 3 | tableForm: { 4 | search: '查詢', 5 | reset: '重置', 6 | submit: '提交', 7 | collapsed: '展開', 8 | expand: '收起', 9 | inputPlaceholder: '請輸入', 10 | selectPlaceholder: '請選擇', 11 | }, 12 | alert: { 13 | clear: '清空', 14 | selected: '已選擇', 15 | item: '項', 16 | }, 17 | pagination: { 18 | total: { 19 | range: '第', 20 | total: '條/總共', 21 | item: '條', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: '固定到左邊', 26 | rightPin: '固定到右邊', 27 | noPin: '取消固定', 28 | leftFixedTitle: '固定在左側', 29 | rightFixedTitle: '固定在右側', 30 | noFixedTitle: '不固定', 31 | reset: '重置', 32 | columnDisplay: '列展示', 33 | columnSetting: '列設置', 34 | fullScreen: '全屏', 35 | exitFullScreen: '退出全屏', 36 | reload: '刷新', 37 | density: '密度', 38 | densityDefault: '正常', 39 | densityLarger: '默認', 40 | densityMiddle: '中等', 41 | densitySmall: '緊湊', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/component/status/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Badge } from 'antd'; 3 | import './index.less'; 4 | 5 | interface StatusProps { 6 | className?: string; 7 | style?: CSSProperties; 8 | } 9 | 10 | /** 11 | * 快捷操作,用于快速的展示一个状态 12 | */ 13 | const Status: { 14 | success: React.FC; 15 | error: React.FC; 16 | processing: React.FC; 17 | default: React.FC; 18 | warning: React.FC; 19 | } = { 20 | success: ({ children }) => , 21 | error: ({ children }) => , 22 | default: ({ children }) => , 23 | processing: ({ children }) => , 24 | warning: ({ children }) => , 25 | }; 26 | 27 | export type StatusType = keyof typeof Status; 28 | 29 | export default Status; 30 | export const Color: React.FC = ({ color, children }) => ( 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /src/locale/en_US.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '$', 3 | tableForm: { 4 | search: 'Query', 5 | reset: 'Reset', 6 | submit: 'Submit', 7 | collapsed: 'Expand', 8 | expand: 'Collapse', 9 | inputPlaceholder: 'Please enter', 10 | selectPlaceholder: 'Please select', 11 | }, 12 | alert: { 13 | clear: 'Clear', 14 | selected: 'Selected', 15 | item: 'Item', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'of', 21 | item: 'items', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Pin to left', 26 | rightPin: 'Pin to right', 27 | noPin: 'Unpinned', 28 | leftFixedTitle: 'Fixed the left', 29 | rightFixedTitle: 'Fixed the right', 30 | noFixedTitle: 'Not Fixed', 31 | reset: 'Reset', 32 | columnDisplay: 'Column Display', 33 | columnSetting: 'Settings', 34 | fullScreen: 'Full Screen', 35 | exitFullScreen: 'Exit Full Screen', 36 | reload: 'Refresh', 37 | density: 'Density', 38 | densityDefault: 'Default', 39 | densityLarger: 'Larger', 40 | densityMiddle: 'Middle', 41 | densitySmall: 'Compact', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-table-prefix-cls: ~'@{ant-prefix}-pro-table'; 4 | 5 | .@{pro-table-prefix-cls} { 6 | z-index: 1; 7 | &:not(:root):fullscreen { 8 | min-height: 100vh; 9 | overflow: auto; 10 | background: @component-background; 11 | } 12 | 13 | &-extra { 14 | margin-bottom: 16px; 15 | } 16 | 17 | .@{ant-prefix}-pagination { 18 | padding: 0 24px; 19 | } 20 | 21 | .@{ant-prefix}-table-row .@{ant-prefix}-table-cell:first-child { 22 | padding-left: 24px; 23 | a { 24 | padding: 0; 25 | } 26 | } 27 | 28 | .@{ant-prefix}-table-content tr:first-child > th:first-child { 29 | padding-left: 24px; 30 | a { 31 | padding: 0; 32 | } 33 | } 34 | 35 | td.@{ant-prefix}-table-cell { 36 | > a { 37 | font-size: 14px; 38 | } 39 | } 40 | 41 | .@{ant-prefix}-table 42 | .@{ant-prefix}-table-tbody 43 | .@{ant-prefix}-table-wrapper:only-child 44 | .@{ant-prefix}-table { 45 | margin: 0; 46 | } 47 | 48 | .@{ant-prefix}-table.@{ant-prefix}-table-middle .@{pro-table-prefix-cls} { 49 | margin: -12px -8px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/locale/ms_MY.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: 'RM', 3 | tableForm: { 4 | search: 'Cari', 5 | reset: 'Menetapkan semula', 6 | submit: 'Hantar', 7 | collapsed: 'Kembang', 8 | expand: 'Kuncup', 9 | inputPlaceholder: 'Sila masuk', 10 | selectPlaceholder: 'Sila pilih', 11 | }, 12 | alert: { 13 | clear: 'Padam', 14 | selected: 'Dipilih', 15 | item: 'Item', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'daripada', 21 | item: 'item', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Pin ke kiri', 26 | rightPin: 'Pin ke kanan', 27 | noPin: 'Tidak pin', 28 | leftFixedTitle: 'Tetap ke kiri', 29 | rightFixedTitle: 'Tetap ke kanan', 30 | noFixedTitle: 'Tidak Tetap', 31 | reset: 'Menetapkan semula', 32 | columnDisplay: 'Lajur', 33 | columnSetting: 'Settings', 34 | fullScreen: 'Full Screen', 35 | exitFullScreen: 'Keluar Full Screen', 36 | reload: 'Muat Semula', 37 | density: 'Densiti', 38 | densityDefault: 'Biasa', 39 | densityLarger: 'Besar', 40 | densityMiddle: 'Tengah', 41 | densitySmall: 'Kecil', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/locale/vi_VN.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '₫', 3 | tableForm: { 4 | search: 'Tìm kiếm', 5 | reset: 'Làm lại', 6 | submit: 'Gửi đi', 7 | collapsed: 'Mở rộng', 8 | expand: 'Thu gọn', 9 | inputPlaceholder: 'nhập dữ liệu', 10 | selectPlaceholder: 'Vui lòng chọn', 11 | }, 12 | alert: { 13 | clear: 'Xóa', 14 | selected: 'đã chọn', 15 | item: 'mục', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'trên', 21 | item: 'mặt hàng', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Ghim trái', 26 | rightPin: 'Ghim phải', 27 | noPin: 'Bỏ ghim', 28 | leftFixedTitle: 'Cố định trái', 29 | rightFixedTitle: 'Cố định phải', 30 | noFixedTitle: 'Chưa cố định', 31 | reset: 'Làm lại', 32 | columnDisplay: 'Cột hiển thị', 33 | columnSetting: 'Cấu hình', 34 | fullScreen: 'Chế độ toàn màn hình', 35 | exitFullScreen: 'Thoát chế độ toàn màn hình', 36 | reload: 'Làm mới', 37 | density: 'Mật độ hiển thị', 38 | densityDefault: 'Mặc định', 39 | densityLarger: 'Mặc định', 40 | densityMiddle: 'Trung bình', 41 | densitySmall: 'Chật', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/locale/it_IT.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '€', 3 | tableForm: { 4 | search: 'Filtra', 5 | reset: 'Pulisci', 6 | submit: 'Invia', 7 | collapsed: 'Espandi', 8 | expand: 'Contrai', 9 | inputPlaceholder: 'Digita', 10 | selectPlaceholder: 'Seleziona', 11 | }, 12 | alert: { 13 | clear: 'Rimuovi', 14 | selected: 'Selezionati', 15 | item: 'elementi', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'di', 21 | item: 'elementi', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Fissa a sinistra', 26 | rightPin: 'Fissa a destra', 27 | noPin: 'Ripristina posizione', 28 | leftFixedTitle: 'Fissato a sinistra', 29 | rightFixedTitle: 'Fissato a destra', 30 | noFixedTitle: 'Non fissato', 31 | reset: 'Ripristina', 32 | columnDisplay: 'Disposizione colonne', 33 | columnSetting: 'Impostazioni', 34 | fullScreen: 'Modalità schermo intero', 35 | exitFullScreen: 'Esci da modalità schermo intero', 36 | reload: 'Ricarica', 37 | density: 'Grandezza tabella', 38 | densityLarger: 'Grande', 39 | densityMiddle: 'Media', 40 | densitySmall: 'Compatta', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/locale/ru_RU.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '₽', 3 | tableForm: { 4 | search: 'Найти', 5 | reset: 'Сброс', 6 | submit: 'Отправить', 7 | collapsed: 'Развернуть', 8 | expand: 'Свернуть', 9 | inputPlaceholder: 'Введите значение', 10 | selectPlaceholder: 'Выберите значение', 11 | }, 12 | alert: { 13 | clear: 'Очистить', 14 | selected: 'выбранный', 15 | item: 'предмет', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'из', 21 | item: 'предметов', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Закрепить слева', 26 | rightPin: 'Закрепить справа', 27 | noPin: 'Открепить', 28 | leftFixedTitle: 'Закреплено слева', 29 | rightFixedTitle: 'Закреплено справа', 30 | noFixedTitle: 'Не закреплено', 31 | reset: 'Сброс', 32 | columnDisplay: 'Отображение столбца', 33 | columnSetting: 'Настройки', 34 | fullScreen: 'Полный экран', 35 | exitFullScreen: 'Выйти из полноэкранного режима', 36 | reload: 'Обновить', 37 | density: 'Размер', 38 | densityDefault: 'По умолчанию', 39 | densityLarger: 'Большой', 40 | densityMiddle: 'Средний', 41 | densitySmall: 'Сжатый', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ProTable, { ProColumns, ActionType, ProTableProps, ColumnsState } from './Table'; 2 | import IndexColumn from './component/indexColumn'; 3 | import { RequestData } from './useFetchData'; 4 | import TableDropdown from './component/dropdown'; 5 | import TableStatus from './component/status'; 6 | import { 7 | IntlProvider, 8 | IntlConsumer, 9 | createIntl, 10 | IntlType, 11 | zhCNIntl, 12 | enUSIntl, 13 | viVNIntl, 14 | itITIntl, 15 | jaJPIntl, 16 | esESIntl, 17 | ruRUIntl, 18 | msMYIntl, 19 | zhTWIntl, 20 | } from './component/intlContext'; 21 | import Search from './form'; 22 | import defaultRenderText, { ProColumnsValueType } from './defaultRender'; 23 | 24 | export type { 25 | ProTableProps, 26 | IntlType, 27 | ColumnsState, 28 | ProColumnsValueType, 29 | ProColumns, 30 | ActionType, 31 | RequestData, 32 | }; 33 | 34 | export { 35 | IndexColumn, 36 | TableDropdown, 37 | TableStatus, 38 | Search, 39 | IntlProvider, 40 | IntlConsumer, 41 | zhCNIntl, 42 | defaultRenderText, 43 | createIntl, 44 | enUSIntl, 45 | viVNIntl, 46 | itITIntl, 47 | jaJPIntl, 48 | esESIntl, 49 | ruRUIntl, 50 | msMYIntl, 51 | zhTWIntl, 52 | }; 53 | 54 | export default ProTable; 55 | -------------------------------------------------------------------------------- /src/locale/es_ES.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '€', 3 | tableForm: { 4 | search: 'Buscar', 5 | reset: 'Limpiar', 6 | submit: 'Submit', 7 | collapsed: 'Expandir', 8 | expand: 'Colapsar', 9 | inputPlaceholder: 'Ingrese valor', 10 | selectPlaceholder: 'Seleccione valor', 11 | }, 12 | alert: { 13 | clear: 'Limpiar', 14 | selected: 'Seleccionado', 15 | item: 'Articulo', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'de', 21 | item: 'artículos', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Pin a la izquierda', 26 | rightPin: 'Pin a la derecha', 27 | noPin: 'Sin Pin', 28 | leftFixedTitle: 'Fijado a la izquierda', 29 | rightFixedTitle: 'Fijado a la derecha', 30 | noFixedTitle: 'Sin Fijar', 31 | reset: 'Reiniciar', 32 | columnDisplay: 'Mostrar Columna', 33 | columnSetting: 'Configuración', 34 | fullScreen: 'Pantalla Completa', 35 | exitFullScreen: 'Salir Pantalla Completa', 36 | reload: 'Refrescar', 37 | density: 'Densidad', 38 | densityDefault: 'Por Defecto', 39 | densityLarger: 'Largo', 40 | densityMiddle: 'Medio', 41 | densitySmall: 'Compacto', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/locale/fr_FR.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | moneySymbol: '€', 3 | tableForm: { 4 | search: 'Rechercher', 5 | reset: 'Réinitialiser', 6 | submit: 'Envoyer', 7 | collapsed: 'Agrandir', 8 | expand: 'Réduire', 9 | inputPlaceholder: 'Entrer une valeur', 10 | selectPlaceholder: 'Sélectionner une valeur', 11 | }, 12 | alert: { 13 | clear: 'Réinitialiser', 14 | selected: 'Sélectionné', 15 | item: 'Item', 16 | }, 17 | pagination: { 18 | total: { 19 | range: ' ', 20 | total: 'sur', 21 | item: 'éléments', 22 | }, 23 | }, 24 | tableToolBar: { 25 | leftPin: 'Épingler à gauche', 26 | rightPin: 'Épingler à gauche', 27 | noPin: 'Sans épingle', 28 | leftFixedTitle: 'Fixer à gauche', 29 | rightFixedTitle: 'Fixer à droite', 30 | noFixedTitle: 'Non fixé', 31 | reset: 'Réinitialiser', 32 | columnDisplay: 'Affichage colonne', 33 | columnSetting: 'Réglages', 34 | fullScreen: 'Plein écran', 35 | exitFullScreen: 'Quitter Plein écran', 36 | reload: 'Rafraichir', 37 | density: 'Densité', 38 | densityDefault: 'Par défaut', 39 | densityLarger: 'Larger', 40 | densityMiddle: 'Moyenne', 41 | densitySmall: 'Compacte', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/component/percent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactNode, useMemo } from 'react'; 2 | import toNumber from 'lodash.tonumber'; 3 | 4 | import { getColorByRealValue, getSymbolByRealValue, getRealTextWithPrecision } from './util'; 5 | 6 | export interface PercentPropInt { 7 | prefix?: ReactNode; 8 | suffix?: ReactNode; 9 | value?: number | string; 10 | precision?: number; 11 | showColor?: boolean; 12 | showSymbol?: boolean; 13 | } 14 | 15 | const Percent: React.SFC = ({ 16 | value, 17 | prefix, 18 | precision, 19 | showSymbol, 20 | suffix = '%', 21 | showColor = false, 22 | }) => { 23 | const realValue = useMemo( 24 | () => 25 | typeof value === 'string' && value.includes('%') 26 | ? toNumber(value.replace('%', '')) 27 | : toNumber(value), 28 | [value], 29 | ); 30 | /** 颜色有待确定, 根据提供 colors: ['正', '负'] | boolean */ 31 | const style = showColor ? { color: getColorByRealValue(realValue) } : {}; 32 | 33 | return ( 34 | 35 | {prefix && {prefix}} 36 | {showSymbol && {getSymbolByRealValue(realValue)} } 37 | {getRealTextWithPrecision(realValue, precision)} 38 | {suffix && suffix} 39 | 40 | ); 41 | }; 42 | 43 | export default Percent; 44 | -------------------------------------------------------------------------------- /src/component/columnSetting/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-column-setting-prefix-cls: ~'@{ant-prefix}-pro-table-column-setting'; 4 | 5 | .@{pro-column-setting-prefix-cls} { 6 | width: auto; 7 | &-title { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | height: 32px; 12 | } 13 | &-overlay { 14 | .ant-popover-inner-content { 15 | padding: 12px 0; 16 | } 17 | } 18 | } 19 | .@{pro-column-setting-prefix-cls}-list { 20 | display: flex; 21 | flex-direction: column; 22 | width: 100%; 23 | &-title { 24 | color: #8c8c8c; 25 | font-size: 12px; 26 | } 27 | &-item { 28 | display: flex; 29 | align-items: center; 30 | width: 100%; 31 | padding: 4px 16px; 32 | &-option { 33 | display: none; 34 | float: right; 35 | cursor: pointer; 36 | > span { 37 | > i.anticon { 38 | color: @primary-color; 39 | } 40 | } 41 | > span + span { 42 | margin-left: 8px; 43 | } 44 | } 45 | &:hover { 46 | background-color: @item-active-bg; 47 | .@{pro-column-setting-prefix-cls}-list-item-option { 48 | display: block; 49 | } 50 | } 51 | } 52 | .@{ant-prefix}-checkbox-wrapper { 53 | flex: 1; 54 | margin: 0; 55 | :last-child { 56 | margin-bottom: 0; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/intl.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 国际化 3 | order: 8 4 | sidemenu: false 5 | nav: 6 | title: 国际化 7 | order: 2 8 | --- 9 | 10 | # 国际化 11 | 12 | ProTable 内置了国际化的支持,作为一个文本量比较少的组件,我们可以自行实现国际化,成本也很低。 13 | 14 | ## 代码示例 15 | 16 | 全量的文本 17 | 18 | ```typescript | prue 19 | const enLocale = { 20 | tableFrom: { 21 | search: 'Query', 22 | reset: 'Reset', 23 | submit: 'Submit', 24 | collapsed: 'Expand', 25 | expand: 'Collapse', 26 | inputPlaceholder: 'Please enter', 27 | selectPlaceholder: 'Please select', 28 | }, 29 | alert: { 30 | clear: 'Clear', 31 | }, 32 | tableToolBar: { 33 | leftPin: 'Pin to left', 34 | rightPin: 'Pin to right', 35 | noPin: 'Unpinned', 36 | leftFixedTitle: 'Fixed the left', 37 | rightFixedTitle: 'Fixed the right', 38 | noFixedTitle: 'Not Fixed', 39 | reset: 'Reset', 40 | columnDisplay: 'Column Display', 41 | columnSetting: 'Settings', 42 | fullScreen: 'Full Screen', 43 | exitFullScreen: 'Exit Full Screen', 44 | reload: 'Refresh', 45 | density: 'Density', 46 | densityDefault: 'Default', 47 | densityLarger: 'Larger', 48 | densityMiddle: 'Middle', 49 | densitySmall: 'Compact', 50 | }, 51 | }; 52 | 53 | // 生成 intl 对象 54 | const enUSIntl = createIntl('en_US', enUS); 55 | 56 | // 使用 57 | 58 | 59 | ; 60 | ``` 61 | 62 | # Demo 列表 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/component/toolBar/DensityIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ColumnHeightOutlined } from '@ant-design/icons'; 3 | import { Menu, Dropdown, Tooltip } from 'antd'; 4 | import Container from '../../container'; 5 | import { useIntl } from '../intlContext'; 6 | 7 | export type DensitySize = 'middle' | 'small' | 'large' | undefined; 8 | 9 | const DensityIcon: React.ForwardRefRenderFunction = (_, ref) => { 10 | const counter = Container.useContainer(); 11 | const intl = useIntl(); 12 | return ( 13 | { 18 | if (counter.setTableSize) { 19 | counter.setTableSize(key as DensitySize); 20 | } 21 | }} 22 | style={{ 23 | width: 80, 24 | }} 25 | > 26 | {intl.getMessage('tableToolBar.densityLarger', '默认')} 27 | 28 | {intl.getMessage('tableToolBar.densityMiddle', '中等')} 29 | 30 | {intl.getMessage('tableToolBar.densitySmall', '紧凑')} 31 | 32 | } 33 | trigger={['click']} 34 | > 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default React.forwardRef(DensityIcon); 43 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍 - ProTable 3 | order: 10 4 | sidebar: false 5 | hero: 6 | title: ProTable 7 | desc: 🏆 Use Ant Design Table like a Pro! 8 | actions: 9 | - text: 快速开始 → 10 | link: /getting-started 11 | features: 12 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziitmp/13668549-b393-42a2-97c3-a6365ba87ac2_w96_h96.png 13 | title: 简单易用 14 | desc: 开箱即用的 Table 组件,在antd Table 之上扩展了更多便捷易用的功能 15 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziik0f/487a2685-8f68-4c34-824f-e34c171d0dfd_w96_h96.png 16 | title: Ant Design 17 | desc: 与 Ant Design 设计体系一脉相承,无缝对接 antd 项目,兼容 antd 3.x & 4.x 18 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziip85/89434dcf-5f1d-4362-9ce0-ab8012a85924_w96_h96.png 19 | title: 国际化 20 | desc: 提供完备的国际化语言支持,与 Ant Design 体系打通 21 | - icon: https://gw.alipayobjects.com/mdn/rms_05efff/afts/img/A*-3XMTrwP85wAAAAAAAAAAABkARQnAQ 22 | title: 预设样式 23 | desc: 样式风格与 antd 一脉相承,无需魔改,浑然天成 24 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziieuq/decadf3f-b53a-4c48-83f3-a2faaccf9ff7_w96_h96.png 25 | title: 预设行为 26 | desc: 内置搜索、筛选、刷新等常用表格行为,并为多种类型数据展示提供了内置格式化 27 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9zij2bh/67f75d56-0d62-47d6-a8a5-dbd0cb79a401_w96_h96.png 28 | title: Typescript 29 | desc: 使用 TypeScript 开发,提供完整的类型定义文件 30 | 31 | footer: Open-source MIT Licensed | Copyright © 2017-present 32 | --- 33 | 34 | ## 使用 35 | 36 | ```bash 37 | npm install @ant-design/pro-table 38 | # or 39 | yarn add @ant-design/pro-table 40 | ``` 41 | 42 | ## 示例 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/form/FormOption.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormInstance } from 'antd/es/form'; 3 | import { Button, Space } from 'antd'; 4 | import { ProTableTypes } from '../Table'; 5 | import { SearchConfig } from './index'; 6 | 7 | export interface FormOptionProps { 8 | searchConfig: SearchConfig; 9 | type?: ProTableTypes; 10 | form: FormInstance; 11 | submit: () => void; 12 | collapse: boolean; 13 | setCollapse: (collapse: boolean) => void; 14 | showCollapseButton: boolean; 15 | onReset?: () => void; 16 | } 17 | 18 | /** 19 | * FormFooter 的组件,可以自动进行一些配置 20 | * @param props 21 | */ 22 | const FormOption: React.FC = (props) => { 23 | const { 24 | searchConfig, 25 | setCollapse, 26 | collapse, 27 | type, 28 | form, 29 | submit, 30 | showCollapseButton, 31 | onReset = () => {}, 32 | } = props; 33 | const isForm = type === 'form'; 34 | const { searchText, submitText, resetText, collapseRender, optionRender } = searchConfig; 35 | if (optionRender === false) { 36 | return null; 37 | } 38 | if (optionRender) { 39 | return <>{optionRender(searchConfig, props)}; 40 | } 41 | return ( 42 | 43 | 54 | 57 | {!isForm && showCollapseButton && ( 58 | { 60 | setCollapse(!collapse); 61 | }} 62 | > 63 | {collapseRender && collapseRender(collapse)} 64 | 65 | )} 66 | 67 | ); 68 | }; 69 | 70 | export default FormOption; 71 | -------------------------------------------------------------------------------- /src/form/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import '~antd/es/style/mixins/index.less'; 3 | 4 | @pro-table-search-prefix-cls: ~'@{ant-prefix}-pro-table-search'; 5 | 6 | @pro-table-form-prefix-cls: ~'@{ant-prefix}-pro-table-form'; 7 | 8 | .@{pro-table-search-prefix-cls} { 9 | margin-bottom: 16px; 10 | padding: 24px; 11 | padding-bottom: 0; 12 | background: @component-background; 13 | 14 | .clearfix; 15 | 16 | &.@{pro-table-form-prefix-cls} { 17 | margin: 0; 18 | padding: 0 16px; 19 | overflow: unset; 20 | } 21 | 22 | .@{ant-prefix}-form-item-label { 23 | min-width: 40px; 24 | text-align: right; 25 | } 26 | .@{ant-prefix}-row:not(.ant-form-item) { 27 | > .ant-col { 28 | padding: 4px 0; 29 | } 30 | } 31 | 32 | .@{ant-prefix}-form-item { 33 | display: flex; 34 | } 35 | 36 | .@{ant-prefix}-form-item-control { 37 | flex: 1; 38 | } 39 | 40 | &-option { 41 | .@{ant-prefix}-form-item-control-input { 42 | display: flex; 43 | align-items: center; 44 | justify-content: flex-end; 45 | text-align: right; 46 | } 47 | } 48 | 49 | &-form-option { 50 | .@{ant-prefix}-form-item { 51 | margin: 0; 52 | } 53 | .@{ant-prefix}-form-item-label { 54 | opacity: 0; 55 | } 56 | .@{ant-prefix}-form-item-control-input { 57 | justify-content: flex-start; 58 | } 59 | } 60 | } 61 | 62 | @media (max-width: 575px) { 63 | .@{pro-table-search-prefix-cls} { 64 | height: auto !important; 65 | padding-bottom: 24px; 66 | .@{ant-prefix}-form-item { 67 | display: inline; 68 | } 69 | .@{ant-prefix}-legacy-form-item { 70 | display: inline; 71 | } 72 | .@{ant-prefix}-form-item-label { 73 | min-width: 80px; 74 | text-align: left; 75 | } 76 | .@{ant-prefix}-legacy-form-item-label { 77 | min-width: 80px; 78 | text-align: left; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/component/toolBar/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import '../../index'; 3 | 4 | .@{pro-table-prefix-cls}-toolbar { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 64px; 9 | padding: 0 24px; 10 | 11 | // 与 listtoolbar 配合时产生更好的效果 12 | .tech-listtoolbar-container { 13 | padding-right: 0; 14 | padding-left: 0; 15 | } 16 | 17 | &-option { 18 | display: flex; 19 | align-items: center; 20 | justify-content: flex-end; 21 | } 22 | 23 | &-item-icon { 24 | margin-left: 16px; 25 | font-size: 16px; 26 | cursor: pointer; 27 | &:first-child { 28 | margin-left: 8px; 29 | } 30 | } 31 | 32 | &-title { 33 | flex: 1; 34 | color: @label-color; 35 | font-weight: 500; 36 | font-size: 16px; 37 | line-height: 24px; 38 | opacity: 0.85; 39 | } 40 | } 41 | 42 | @media (max-width: @screen-xs) { 43 | .@{pro-table-prefix-cls} { 44 | .ant-table { 45 | width: 100%; 46 | overflow-x: auto; 47 | &-thead > tr, 48 | &-tbody > tr { 49 | > th, 50 | > td { 51 | white-space: pre; 52 | > span { 53 | display: block; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | @media (max-width: 575px) { 62 | .@{pro-table-prefix-cls}-toolbar { 63 | flex-direction: column; 64 | align-items: flex-start; 65 | justify-content: flex-start; 66 | height: auto; 67 | margin-bottom: 16px; 68 | margin-left: 16px; 69 | padding: 8px; 70 | padding-top: 16px; 71 | line-height: normal; 72 | 73 | &-title { 74 | margin-bottom: 16px; 75 | } 76 | 77 | &-option { 78 | display: flex; 79 | justify-content: space-between; 80 | width: 100%; 81 | } 82 | 83 | &-default-option { 84 | display: flex; 85 | flex: 1; 86 | align-items: center; 87 | justify-content: flex-end; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/container.tsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from 'unstated-next'; 2 | import { useState, useRef } from 'react'; 3 | import { ColumnType } from 'antd/es/table'; 4 | import useMergeValue from 'use-merge-value'; 5 | 6 | import { RequestData, ProColumns } from './index'; 7 | import { UseFetchDataAction } from './useFetchData'; 8 | import { DensitySize } from './component/toolBar/DensityIcon'; 9 | import { ColumnsState } from './Table'; 10 | 11 | export interface UseCounterProps { 12 | columnsStateMap?: { 13 | [key: string]: ColumnsState; 14 | }; 15 | onColumnsStateChange?: (map: { [key: string]: ColumnsState }) => void; 16 | size?: DensitySize; 17 | onSizeChange?: (size: DensitySize) => void; 18 | } 19 | 20 | function useCounter(props: UseCounterProps = {}) { 21 | const actionRef = useRef>>(); 22 | const [columns, setColumns] = useState[]>([]); 23 | // 用于排序的数组 24 | const [sortKeyColumns, setSortKeyColumns] = useState<(string | number)[]>([]); 25 | const [proColumns, setProColumns] = useState[]>([]); 26 | 27 | const [tableSize, setTableSize] = useMergeValue(props.size || 'middle', { 28 | value: props.size, 29 | onChange: props.onSizeChange, 30 | }); 31 | 32 | const [columnsMap, setColumnsMap] = useMergeValue<{ 33 | [key: string]: ColumnsState; 34 | }>(props.columnsStateMap || {}, { 35 | value: props.columnsStateMap, 36 | onChange: props.onColumnsStateChange, 37 | }); 38 | return { 39 | action: actionRef, 40 | setAction: (newAction: UseFetchDataAction>) => { 41 | actionRef.current = newAction; 42 | }, 43 | sortKeyColumns, 44 | setSortKeyColumns, 45 | columns, 46 | setColumns, 47 | columnsMap, 48 | setTableSize, 49 | tableSize, 50 | setColumnsMap, 51 | proColumns, 52 | setProColumns, 53 | }; 54 | } 55 | 56 | const Counter = createContainer, UseCounterProps>(useCounter); 57 | 58 | export { useCounter }; 59 | 60 | export default Counter; 61 | -------------------------------------------------------------------------------- /src/component/dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import { DownOutlined, EllipsisOutlined } from '@ant-design/icons'; 4 | import { Dropdown, Menu, Button } from 'antd'; 5 | import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context'; 6 | import './index.less'; 7 | 8 | export interface DropdownProps { 9 | className?: string; 10 | style?: React.CSSProperties; 11 | menus?: { 12 | name: React.ReactNode; 13 | key: string; 14 | }[]; 15 | onSelect?: (key: string) => void; 16 | } 17 | 18 | /** 19 | * 默认的 index 列容器,提供一个好看的 index 20 | * @param param0 21 | */ 22 | const DropdownButton: React.FC = ({ 23 | children, 24 | menus = [], 25 | onSelect, 26 | className, 27 | style, 28 | }) => ( 29 | 30 | {({ getPrefixCls }: ConfigConsumerProps) => { 31 | const tempClassName = getPrefixCls('pro-table-dropdown'); 32 | const menu = ( 33 | onSelect && onSelect(params.key as string)}> 34 | {menus.map((item) => ( 35 | {item.name} 36 | ))} 37 | 38 | ); 39 | return ( 40 | 41 | 44 | 45 | ); 46 | }} 47 | 48 | ); 49 | 50 | const TableDropdown: React.FC & { Button: typeof DropdownButton } = ({ 51 | className: propsClassName, 52 | style, 53 | onSelect, 54 | menus = [], 55 | }) => ( 56 | 57 | {({ getPrefixCls }: ConfigConsumerProps) => { 58 | const className = getPrefixCls('pro-table-dropdown'); 59 | const menu = ( 60 | onSelect && onSelect(params.key as string)}> 61 | {menus.map((item) => ( 62 | {item.name} 63 | ))} 64 | 65 | ); 66 | return ( 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }} 74 | 75 | ); 76 | 77 | TableDropdown.Button = DropdownButton; 78 | 79 | export default TableDropdown; 80 | -------------------------------------------------------------------------------- /docs/demo/valueTypeDate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProTable from '@ant-design/pro-table'; 3 | 4 | const valueEnum = { 5 | 0: 'close', 6 | 1: 'running', 7 | 2: 'online', 8 | 3: 'error', 9 | }; 10 | 11 | export interface TableListItem { 12 | key: number; 13 | name: string; 14 | status: string; 15 | updatedAt: number; 16 | createdAt: number; 17 | progress: number; 18 | money: number; 19 | percent: number | string; 20 | createdAtRange: number[]; 21 | code: string; 22 | } 23 | const tableListDataSource: TableListItem[] = []; 24 | 25 | for (let i = 0; i < 2; i += 1) { 26 | tableListDataSource.push({ 27 | key: i, 28 | name: `TradeCode ${i}`, 29 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 30 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 31 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 32 | createdAtRange: [ 33 | Date.now() - Math.floor(Math.random() * 2000), 34 | Date.now() - Math.floor(Math.random() * 2000), 35 | ], 36 | money: Math.floor(Math.random() * 2000) * i, 37 | progress: Math.ceil(Math.random() * 100) + 1, 38 | percent: 39 | Math.random() > 0.5 40 | ? ((i + 1) * 10 + Math.random()).toFixed(3) 41 | : -((i + 1) * 10 + Math.random()).toFixed(2), 42 | code: `const getData = async params => { 43 | const data = await getData(params); 44 | return { list: data.data, ...data }; 45 | };`, 46 | }); 47 | } 48 | 49 | export default () => ( 50 | <> 51 | 52 | columns={[ 53 | { 54 | title: '创建时间', 55 | key: 'since', 56 | dataIndex: 'createdAt', 57 | valueType: 'dateTime', 58 | }, 59 | { 60 | title: '日期区间', 61 | key: 'dateRange', 62 | dataIndex: 'createdAtRange', 63 | valueType: 'dateRange', 64 | }, 65 | { 66 | title: '时间区间', 67 | key: 'dateTimeRange', 68 | dataIndex: 'createdAtRange', 69 | valueType: 'dateTimeRange', 70 | }, 71 | { 72 | title: '更新时间', 73 | key: 'since2', 74 | dataIndex: 'createdAt', 75 | valueType: 'date', 76 | }, 77 | { 78 | title: '关闭时间', 79 | key: 'since3', 80 | dataIndex: 'updatedAt', 81 | valueType: 'time', 82 | }, 83 | ]} 84 | request={() => { 85 | return Promise.resolve({ 86 | total: 200, 87 | data: tableListDataSource, 88 | success: true, 89 | }); 90 | }} 91 | rowKey="key" 92 | headerTitle="日期类" 93 | /> 94 | 95 | ); 96 | -------------------------------------------------------------------------------- /src/component/alert/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context'; 3 | import { Alert, Space } from 'antd'; 4 | import './index.less'; 5 | import { useIntl, IntlType } from '../intlContext'; 6 | 7 | export interface TableAlertProps { 8 | selectedRowKeys: (number | string)[]; 9 | selectedRows: T[]; 10 | alertInfoRender?: 11 | | ((props: { 12 | intl: IntlType; 13 | selectedRowKeys: (number | string)[]; 14 | selectedRows: T[]; 15 | }) => React.ReactNode) 16 | | false; 17 | onCleanSelected: () => void; 18 | alertOptionRender?: 19 | | false 20 | | ((props: { intl: IntlType; onCleanSelected: () => void }) => React.ReactNode); 21 | } 22 | 23 | const defaultAlertOptionRender = (props: { intl: IntlType; onCleanSelected: () => void }) => { 24 | const { intl, onCleanSelected } = props; 25 | return [ 26 | 27 | {intl.getMessage('alert.clear', '清空')} 28 | , 29 | ]; 30 | }; 31 | 32 | const TableAlert = ({ 33 | selectedRowKeys = [], 34 | onCleanSelected, 35 | selectedRows = [], 36 | alertInfoRender = ({ intl }) => ( 37 | 38 | {intl.getMessage('alert.selected', '已选择')} 39 | {selectedRowKeys.length} 40 | {intl.getMessage('alert.item', '项')}   41 | 42 | ), 43 | alertOptionRender = defaultAlertOptionRender, 44 | }: TableAlertProps) => { 45 | const intl = useIntl(); 46 | 47 | const option = 48 | alertOptionRender && 49 | alertOptionRender({ 50 | onCleanSelected, 51 | intl, 52 | }); 53 | return ( 54 | 55 | {({ getPrefixCls }: ConfigConsumerProps) => { 56 | const className = getPrefixCls('pro-table-alert'); 57 | if (alertInfoRender === false) { 58 | return null; 59 | } 60 | const dom = alertInfoRender({ intl, selectedRowKeys, selectedRows }); 61 | if (dom === false) { 62 | return null; 63 | } 64 | return ( 65 |
66 | 69 |
{dom}
70 | {option &&
{option}
} 71 |
72 | } 73 | type="info" 74 | showIcon 75 | /> 76 | 77 | ); 78 | }} 79 |
80 | ); 81 | }; 82 | 83 | export default TableAlert; 84 | -------------------------------------------------------------------------------- /tests/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'enzyme'; 2 | import React from 'react'; 3 | import { Input } from 'antd'; 4 | import ProTable, { TableDropdown } from '../../src/index'; 5 | import { columns, request } from './demo'; 6 | 7 | describe('BasicTable', () => { 8 | it('🎏 base use', () => { 9 | const html = render( 10 | [ 20 | , 25 | 31 | 更多操作 32 | , 33 | ]} 34 | />, 35 | ); 36 | expect(html).toMatchSnapshot(); 37 | }); 38 | 39 | it('🎏 do not render Search ', () => { 40 | const html = render( 41 | , 52 | ); 53 | expect(html).toMatchSnapshot(); 54 | }); 55 | 56 | it('🎏 do not render default option', () => { 57 | const html = render( 58 | , 74 | ); 75 | expect(html).toMatchSnapshot(); 76 | }); 77 | 78 | it('🎏 do not render setting', () => { 79 | const html = render( 80 | , 96 | ); 97 | expect(html).toMatchSnapshot(); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /docs/demo/valueTypeNumber.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProTable from '@ant-design/pro-table'; 3 | 4 | const valueEnum = { 5 | 0: 'close', 6 | 1: 'running', 7 | 2: 'online', 8 | 3: 'error', 9 | }; 10 | 11 | export interface TableListItem { 12 | key: number; 13 | name: string; 14 | status: string; 15 | updatedAt: number; 16 | createdAt: number; 17 | progress: number; 18 | money: number; 19 | percent: number | string; 20 | createdAtRange: number[]; 21 | code: string; 22 | } 23 | const tableListDataSource: TableListItem[] = []; 24 | 25 | for (let i = 0; i < 2; i += 1) { 26 | tableListDataSource.push({ 27 | key: i, 28 | name: `TradeCode ${i}`, 29 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 30 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 31 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 32 | createdAtRange: [ 33 | Date.now() - Math.floor(Math.random() * 2000), 34 | Date.now() - Math.floor(Math.random() * 2000), 35 | ], 36 | money: Math.floor(Math.random() * 2000) * i, 37 | progress: Math.ceil(Math.random() * 100) + 1, 38 | percent: 39 | Math.random() > 0.5 40 | ? ((i + 1) * 10 + Math.random()).toFixed(3) 41 | : -((i + 1) * 10 + Math.random()).toFixed(2), 42 | code: `const getData = async params => { 43 | const data = await getData(params); 44 | return { list: data.data, ...data }; 45 | };`, 46 | }); 47 | } 48 | 49 | export default () => ( 50 | <> 51 | 52 | columns={[ 53 | { 54 | title: '进度', 55 | key: 'progress', 56 | dataIndex: 'progress', 57 | valueType: (item) => ({ 58 | type: 'progress', 59 | status: item.status !== 'error' ? 'active' : 'exception', 60 | }), 61 | width: 200, 62 | }, 63 | { 64 | title: '金额', 65 | dataIndex: 'money', 66 | valueType: 'money', 67 | width: 150, 68 | }, 69 | { 70 | title: '数字', 71 | dataIndex: 'money', 72 | key: 'digit', 73 | valueType: 'digit', 74 | width: 150, 75 | }, 76 | { 77 | title: '百分比', 78 | key: 'percent', 79 | width: 120, 80 | dataIndex: 'percent', 81 | valueType: () => ({ 82 | type: 'percent', 83 | }), 84 | }, 85 | ]} 86 | request={() => { 87 | return Promise.resolve({ 88 | total: 200, 89 | data: tableListDataSource, 90 | success: true, 91 | }); 92 | }} 93 | rowKey="key" 94 | headerTitle="数字类" 95 | /> 96 | 97 | ); 98 | -------------------------------------------------------------------------------- /src/component/intlContext/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import zhCN from '../../locale/zh_CN'; 3 | import enUS from '../../locale/en_US'; 4 | import viVN from '../../locale/vi_VN'; 5 | import itIT from '../../locale/it_IT'; 6 | import jaJP from '../../locale/ja_JP'; 7 | import esES from '../../locale/es_ES'; 8 | import ruRU from '../../locale/ru_RU'; 9 | import msMY from '../../locale/ms_MY'; 10 | import zhTW from '../../locale/zh_TW'; 11 | import frFR from '../../locale/fr_FR'; 12 | import { getLang } from '../util'; 13 | 14 | export interface IntlType { 15 | locale: string; 16 | getMessage: (id: string, defaultMessage: string) => string; 17 | } 18 | 19 | function get(source: object, path: string, defaultValue?: string): string | undefined { 20 | // a[3].b -> a.3.b 21 | const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.'); 22 | let result = source; 23 | let message = defaultValue; 24 | // eslint-disable-next-line no-restricted-syntax 25 | for (const p of paths) { 26 | message = Object(result)[p]; 27 | result = Object(result)[p]; 28 | if (message === undefined) { 29 | return defaultValue; 30 | } 31 | } 32 | return message; 33 | } 34 | 35 | /** 36 | * 创建一个操作函数 37 | * @param locale 38 | * @param localeMap 39 | */ 40 | const createIntl = (locale: string, localeMap: { [key: string]: any }): IntlType => ({ 41 | getMessage: (id: string, defaultMessage: string) => 42 | get(localeMap, id, defaultMessage) || defaultMessage, 43 | locale, 44 | }); 45 | 46 | const zhCNIntl = createIntl('zh_CN', zhCN); 47 | const enUSIntl = createIntl('en_US', enUS); 48 | const viVNIntl = createIntl('vi_VN', viVN); 49 | const itITIntl = createIntl('it_IT', itIT); 50 | const jaJPIntl = createIntl('ja_JP', jaJP); 51 | const esESIntl = createIntl('es_ES', esES); 52 | const ruRUIntl = createIntl('ru_RU', ruRU); 53 | const msMYIntl = createIntl('ms_MY', msMY); 54 | const zhTWIntl = createIntl('zh_TW', zhTW); 55 | const frFRIntl = createIntl('fr_FR', frFR); 56 | 57 | const intlMap = { 58 | 'zh-CN': zhCNIntl, 59 | 'en-US': enUSIntl, 60 | 'vi-VN': viVNIntl, 61 | 'it-IT': itITIntl, 62 | 'js-JP': jaJPIntl, 63 | 'es-ES': esESIntl, 64 | 'ru-RU': ruRUIntl, 65 | 'ms-MY': msMYIntl, 66 | 'zh-TW': zhTWIntl, 67 | 'fr-FR': frFRIntl, 68 | }; 69 | 70 | export { enUSIntl, zhCNIntl, viVNIntl, itITIntl, jaJPIntl, esESIntl, ruRUIntl, msMYIntl, zhTWIntl }; 71 | 72 | const IntlContext = React.createContext(intlMap[getLang() || ''] || zhCNIntl); 73 | 74 | const { Consumer: IntlConsumer, Provider: IntlProvider } = IntlContext; 75 | 76 | export { IntlConsumer, IntlProvider, createIntl }; 77 | 78 | export function useIntl(): IntlType { 79 | const intl = useContext(IntlContext); 80 | return intl; 81 | } 82 | 83 | export default IntlContext; 84 | -------------------------------------------------------------------------------- /docs/valueType.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 预设样式 3 | order: 7 4 | sidemenu: false 5 | nav: 6 | title: 预设样式 7 | order: 5 8 | --- 9 | 10 | # 值类型 11 | 12 | ProTable 封装了一些常用的值类型来减少重复的 `render` 操作,配置一个`valueType` 即可展示格式化响应的数据。 13 | 14 | ## valueType 15 | 16 | 现在支持的值如下 17 | 18 | | 类型 | 描述 | 示例 | 19 | | --- | --- | --- | 20 | | money | 转化值为金额 | ¥10,000.26 | 21 | | date | 日期 | 2019-11-16 | 22 | | dateRange | 日期区间 | 2019-11-16 2019-11-18 | 23 | | dateTime | 日期和时间 | 2019-11-16 12:50:00 | 24 | | dateTimeRange | 日期和时间区间 | 2019-11-16 12:50:00 2019-11-18 12:50:00 | 25 | | time | 时间 | 12:50:00 | 26 | | option | 操作项,会自动增加 marginRight,只支持一个数组,表单中会自动忽略 | `[操作a,操作b]` | 27 | | text | 默认值,不做任何处理 | - | 28 | | textarea | 与 text 相同, form 转化时会转为 textarea 组件 | - | 29 | | index | 序号列 | - | 30 | | indexBorder | 带 border 的序号列 | - | 31 | | progress | 进度条 | - | 32 | | digit | 单纯的数字,form 转化时会转为 inputNumber | - | 33 | | percent | 百分比 | +1.12 | 34 | | code | 代码块 | `const a = b` | 35 | | avatar | 头像 | 展示一个头像 | 36 | 37 | ## 传入 function 38 | 39 | 只有一个值并不能表现很多类型,`progress` 就是一个很好的例子。所以我们支持传入一个 function。你可以这样使用: 40 | 41 | ```tsx |pure 42 | const columns = { 43 | title: '进度', 44 | key: 'progress', 45 | dataIndex: 'progress', 46 | valueType: (item: T) => ({ 47 | type: 'progress', 48 | status: item.status !== 'error' ? 'active' : 'exception', 49 | }), 50 | }; 51 | ``` 52 | 53 | ### 支持的返回值 54 | 55 | ### progress 56 | 57 | #### progress 58 | 59 | ```js 60 | return { type: 'progress', status: 'success' | 'exception' | 'normal' | 'active' }; 61 | ``` 62 | 63 | ### money 64 | 65 | ```js 66 | return { type: 'money', locale: 'en-Us' }; 67 | ``` 68 | 69 | ### percent 70 | 71 | ```js 72 | return { type: 'percent', showSymbol: true | false, precision: 2 }; 73 | ``` 74 | 75 | ## valueEnum 76 | 77 | valueEnum 需要传入一个枚举,ProTable 会自动根据值获取响应的枚举,并且在 from 中生成一个下拉框。看起来是这样的: 78 | 79 | ```ts | pure 80 | const valueEnum = { 81 | open: { 82 | text: '未解决', 83 | status: 'Error', 84 | }, 85 | closed: { 86 | text: '已解决', 87 | status: 'Success', 88 | }, 89 | }; 90 | 91 | // 也可以设置为一个function 92 | const valueEnum = (row) => 93 | row.isMe 94 | ? { 95 | open: { 96 | text: '未解决', 97 | status: 'Error', 98 | }, 99 | closed: { 100 | text: '已解决', 101 | status: 'Success', 102 | }, 103 | } 104 | : { 105 | open: { 106 | text: '等待解决', 107 | status: 'Error', 108 | }, 109 | closed: { 110 | text: '已回应', 111 | status: 'Success', 112 | }, 113 | }; 114 | ``` 115 | 116 | > 这里值得注意的是在 from 中并没有 row,所以传入了一个 null,你可以根据这个来判断要在 from 中显示什么选项。 117 | 118 | ## 示例 119 | 120 | ### 日期类 121 | 122 | 123 | 124 | ### 数字类 125 | 126 | 127 | 128 | ### 样式类 129 | 130 | 131 | -------------------------------------------------------------------------------- /docs/example/form.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Button } from 'antd'; 3 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 4 | import { FormInstance } from 'antd/lib/form'; 5 | 6 | const valueEnum = { 7 | 0: 'close', 8 | 1: 'running', 9 | 2: 'online', 10 | 3: 'error', 11 | }; 12 | 13 | export interface TableListItem { 14 | key: number; 15 | name: string; 16 | status: string; 17 | updatedAt: number; 18 | createdAt: number; 19 | progress: number; 20 | money: number; 21 | } 22 | const tableListDataSource: TableListItem[] = []; 23 | 24 | for (let i = 0; i < 100; i += 1) { 25 | tableListDataSource.push({ 26 | key: i, 27 | name: `TradeCode ${i}`, 28 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 29 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 30 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 31 | money: Math.floor(Math.random() * 2000) * i, 32 | progress: Math.ceil(Math.random() * 100) + 1, 33 | }); 34 | } 35 | 36 | const columns: ProColumns[] = [ 37 | { 38 | title: '标题', 39 | dataIndex: 'name', 40 | }, 41 | { 42 | title: '状态', 43 | dataIndex: 'status', 44 | initialValue: 'all', 45 | filters: true, 46 | valueEnum: { 47 | all: { text: '全部', status: 'Default' }, 48 | close: { text: '关闭', status: 'Default' }, 49 | running: { text: '运行中', status: 'Processing' }, 50 | online: { text: '已上线', status: 'Success' }, 51 | error: { text: '异常', status: 'Error' }, 52 | }, 53 | }, 54 | { 55 | title: '创建时间', 56 | key: 'since', 57 | dataIndex: 'createdAt', 58 | valueType: 'dateTime', 59 | }, 60 | { 61 | title: '操作', 62 | key: 'option', 63 | width: 120, 64 | valueType: 'option', 65 | render: () => [操作, 删除], 66 | }, 67 | ]; 68 | 69 | export default () => { 70 | const ref = useRef(); 71 | const [collapsed, setCollapsed] = useState(false); 72 | 73 | return ( 74 | 75 | columns={columns} 76 | request={() => 77 | Promise.resolve({ 78 | data: tableListDataSource, 79 | success: true, 80 | }) 81 | } 82 | rowKey="key" 83 | pagination={{ 84 | showSizeChanger: true, 85 | }} 86 | search={{ 87 | collapsed, 88 | onCollapse: setCollapsed, 89 | }} 90 | formRef={ref} 91 | toolBarRender={() => [ 92 | , 103 | ]} 104 | options={false} 105 | dateFormatter="string" 106 | headerTitle="表单赋值" 107 | /> 108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/component/columnSetting/DndItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { XYCoord } from 'dnd-core'; 3 | import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; 4 | 5 | export interface CardProps { 6 | id: any; 7 | index: number; 8 | move?: (dragIndex: number, hoverIndex: number) => void; 9 | end: (id: string, dragIndex: number) => void; 10 | } 11 | 12 | interface DragItem { 13 | index: number; 14 | id: string; 15 | type: string; 16 | } 17 | const ItemTypes = { 18 | CARD: 'card', 19 | }; 20 | 21 | const Card: React.FC = ({ id, end, move, children, index }) => { 22 | const ref = useRef(null); 23 | const [, drop] = useDrop({ 24 | accept: ItemTypes.CARD, 25 | hover(item: DragItem, monitor: DropTargetMonitor) { 26 | if (!ref.current) { 27 | return; 28 | } 29 | const dragIndex = item.index; 30 | const hoverIndex = index; 31 | 32 | // Don't replace items with themselves 33 | if (dragIndex === hoverIndex) { 34 | return; 35 | } 36 | 37 | // Determine rectangle on screen 38 | const hoverBoundingRect = ref.current!.getBoundingClientRect(); 39 | 40 | // Get vertical middle 41 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 42 | 43 | // Determine mouse position 44 | const clientOffset = monitor.getClientOffset(); 45 | 46 | // Get pixels to the top 47 | const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; 48 | 49 | // Only perform the move when the mouse has crossed half of the items height 50 | // When dragging downwards, only move when the cursor is below 50% 51 | // When dragging upwards, only move when the cursor is above 50% 52 | 53 | // Dragging downwards 54 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 55 | return; 56 | } 57 | 58 | // Dragging upwards 59 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 60 | return; 61 | } 62 | // Time to actually perform the action 63 | 64 | if (move) { 65 | move(dragIndex, hoverIndex); 66 | } 67 | 68 | // Note: we're mutating the monitor item here! 69 | // Generally it's better to avoid mutations, 70 | // but it's good here for the sake of performance 71 | // to avoid expensive index searches. 72 | // eslint-disable-next-line no-param-reassign 73 | item.index = hoverIndex; 74 | }, 75 | }); 76 | 77 | const [{ isDragging }, drag] = useDrag({ 78 | item: { type: ItemTypes.CARD, id, index }, 79 | collect: (monitor: any) => ({ 80 | isDragging: monitor.isDragging(), 81 | }), 82 | end: (item?: { id: string; index: number }) => { 83 | if (!item) { 84 | return; 85 | } 86 | end(item.id, item.index); 87 | }, 88 | }); 89 | 90 | const opacity = isDragging ? 0 : 1; 91 | drag(drop(ref)); 92 | return ( 93 |
94 | {children} 95 |
96 | ); 97 | }; 98 | 99 | export default Card; 100 | -------------------------------------------------------------------------------- /docs/example/search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import { Button } from 'antd'; 5 | 6 | const valueEnum = { 7 | 0: 'close', 8 | 1: 'running', 9 | 2: 'online', 10 | 3: 'error', 11 | }; 12 | 13 | export interface TableListItem { 14 | key: number; 15 | name: string; 16 | status: string; 17 | updatedAt: number; 18 | createdAt: number; 19 | progress: number; 20 | money: number; 21 | } 22 | const tableListDataSource: TableListItem[] = []; 23 | 24 | for (let i = 0; i < 100; i += 1) { 25 | tableListDataSource.push({ 26 | key: i, 27 | name: `TradeCode ${i}`, 28 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 29 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 30 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 31 | money: Math.floor(Math.random() * 2000) * i, 32 | progress: Math.ceil(Math.random() * 100) + 1, 33 | }); 34 | } 35 | 36 | const columns: ProColumns[] = [ 37 | { 38 | title: '标题', 39 | dataIndex: 'name', 40 | render: (_) => {_}, 41 | }, 42 | { 43 | title: '状态', 44 | dataIndex: 'status', 45 | initialValue: 'all', 46 | width: 100, 47 | filters: true, 48 | valueEnum: { 49 | all: { text: '全部', status: 'Default' }, 50 | close: { text: '关闭', status: 'Default' }, 51 | running: { text: '运行中', status: 'Processing' }, 52 | online: { text: '已上线', status: 'Success' }, 53 | error: { text: '异常', status: 'Error' }, 54 | }, 55 | }, 56 | { 57 | title: '创建时间', 58 | key: 'since', 59 | dataIndex: 'createdAt', 60 | width: 200, 61 | valueType: 'dateTime', 62 | }, 63 | { 64 | title: '更新时间', 65 | key: 'since2', 66 | width: 120, 67 | dataIndex: 'createdAt', 68 | valueType: 'date', 69 | }, 70 | 71 | { 72 | title: '操作', 73 | key: 'option', 74 | width: 120, 75 | valueType: 'option', 76 | render: () => [操作, 删除], 77 | }, 78 | ]; 79 | 80 | export default () => { 81 | return ( 82 | 83 | columns={columns} 84 | request={(params = {}) => 85 | Promise.resolve({ 86 | data: tableListDataSource.filter((item) => { 87 | if (!params.keyWord) { 88 | return true; 89 | } 90 | return item.name.includes(params.keyWord) || item.status.includes(params.keyWord); 91 | }), 92 | success: true, 93 | }) 94 | } 95 | options={{ 96 | search: { 97 | name: 'keyWord', 98 | }, 99 | }} 100 | rowKey="key" 101 | pagination={{ 102 | showSizeChanger: true, 103 | }} 104 | size="middle" 105 | search={false} 106 | toolBarRender={() => [ 107 | , 110 | ]} 111 | dateFormatter="string" 112 | headerTitle="简单搜索" 113 | /> 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /docs/example/dataSource.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button } from 'antd'; 4 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 5 | 6 | const valueEnum = { 7 | 0: 'close', 8 | 1: 'running', 9 | 2: 'online', 10 | 3: 'error', 11 | }; 12 | 13 | export interface TableListItem { 14 | key: number; 15 | name: string; 16 | status: string; 17 | updatedAt: number; 18 | createdAt: number; 19 | progress: number; 20 | money: number; 21 | } 22 | const tableListDataSource: TableListItem[] = []; 23 | 24 | for (let i = 0; i < 20; i += 1) { 25 | tableListDataSource.push({ 26 | key: i, 27 | name: `TradeCode ${i}`, 28 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 29 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 30 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 31 | money: Math.floor(Math.random() * 2000) * i, 32 | progress: Math.ceil(Math.random() * 100) + 1, 33 | }); 34 | } 35 | 36 | const columns: ProColumns[] = [ 37 | { 38 | title: '序号', 39 | dataIndex: 'index', 40 | valueType: 'index', 41 | width: 80, 42 | }, 43 | { 44 | title: '状态', 45 | dataIndex: 'status', 46 | initialValue: 'all', 47 | width: 100, 48 | filters: true, 49 | valueEnum: { 50 | all: { text: '全部', status: 'Default' }, 51 | close: { text: '关闭', status: 'Default' }, 52 | running: { text: '运行中', status: 'Processing' }, 53 | online: { text: '已上线', status: 'Success' }, 54 | error: { text: '异常', status: 'Error' }, 55 | }, 56 | }, 57 | { 58 | title: '进度', 59 | key: 'progress', 60 | dataIndex: 'progress', 61 | valueType: (item) => ({ 62 | type: 'progress', 63 | status: item.status !== 'error' ? 'active' : 'exception', 64 | }), 65 | width: 200, 66 | }, 67 | { 68 | title: '更新时间', 69 | key: 'since2', 70 | width: 120, 71 | dataIndex: 'createdAt', 72 | valueType: 'date', 73 | }, 74 | ]; 75 | 76 | export default () => { 77 | const [loading, setLoading] = useState(true); 78 | const [dataSource, setDataSource] = useState([]); 79 | useEffect(() => { 80 | setTimeout(() => { 81 | setLoading(false); 82 | setDataSource(tableListDataSource); 83 | }, 5000); 84 | }, []); 85 | 86 | return ( 87 | 88 | columns={columns} 89 | rowKey="key" 90 | pagination={{ 91 | showSizeChanger: true, 92 | }} 93 | loading={loading} 94 | dataSource={dataSource} 95 | options={{ 96 | density: true, 97 | reload: () => { 98 | setLoading(true); 99 | setTimeout(() => { 100 | setLoading(false); 101 | }, 1000); 102 | }, 103 | fullScreen: true, 104 | setting: true, 105 | }} 106 | dateFormatter="string" 107 | headerTitle="dataSource 和 loading" 108 | toolBarRender={() => [ 109 | , 113 | ]} 114 | /> 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /docs/example/columnsStateMap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input } from 'antd'; 3 | import ProTable, { ProColumns, ColumnsState } from '@ant-design/pro-table'; 4 | 5 | const valueEnum = { 6 | 0: 'close', 7 | 1: 'running', 8 | 2: 'online', 9 | 3: 'error', 10 | }; 11 | 12 | export interface TableListItem { 13 | key: number; 14 | name: string; 15 | status: string; 16 | updatedAt: number; 17 | createdAt: number; 18 | progress: number; 19 | money: number; 20 | } 21 | const tableListDataSource: TableListItem[] = []; 22 | 23 | for (let i = 0; i < 100; i += 1) { 24 | tableListDataSource.push({ 25 | key: i, 26 | name: `TradeCode ${i}`, 27 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 28 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 29 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 30 | money: Math.floor(Math.random() * 2000) * i, 31 | progress: Math.ceil(Math.random() * 100) + 1, 32 | }); 33 | } 34 | 35 | const columns: ProColumns[] = [ 36 | { 37 | title: '标题', 38 | dataIndex: 'name', 39 | }, 40 | { 41 | title: '状态', 42 | dataIndex: 'status', 43 | initialValue: 'all', 44 | width: 100, 45 | filters: true, 46 | valueEnum: { 47 | all: { text: '全部', status: 'Default' }, 48 | close: { text: '关闭', status: 'Default' }, 49 | running: { text: '运行中', status: 'Processing' }, 50 | online: { text: '已上线', status: 'Success' }, 51 | error: { text: '异常', status: 'Error' }, 52 | }, 53 | }, 54 | { 55 | title: '创建时间', 56 | key: 'since', 57 | dataIndex: 'createdAt', 58 | width: 200, 59 | valueType: 'dateTime', 60 | }, 61 | { 62 | title: '更新时间', 63 | key: 'since2', 64 | width: 120, 65 | dataIndex: 'createdAt', 66 | valueType: 'date', 67 | }, 68 | 69 | { 70 | title: '操作', 71 | key: 'option', 72 | width: 120, 73 | valueType: 'option', 74 | render: () => [操作, 删除], 75 | }, 76 | ]; 77 | 78 | export default () => { 79 | const [columnsStateMap, setColumnsStateMap] = useState<{ 80 | [key: string]: ColumnsState; 81 | }>({ 82 | name: { 83 | show: false, 84 | }, 85 | }); 86 | return ( 87 | <> 88 | {JSON.stringify(columnsStateMap)} 89 | 90 | columns={columns} 91 | request={(params = {}) => 92 | Promise.resolve({ 93 | data: tableListDataSource.filter((item) => { 94 | if (!params.keyWord) { 95 | return true; 96 | } 97 | if (item.name.includes(params.keyWord) || item.status.includes(params.keyWord)) { 98 | return true; 99 | } 100 | return false; 101 | }), 102 | success: true, 103 | }) 104 | } 105 | rowKey="key" 106 | pagination={{ 107 | showSizeChanger: true, 108 | }} 109 | columnsStateMap={columnsStateMap} 110 | onColumnsStateChange={(map) => setColumnsStateMap(map)} 111 | search={false} 112 | dateFormatter="string" 113 | headerTitle="受控模式" 114 | toolBarRender={() => []} 115 | /> 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /tests/__tests__/demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { ProColumns, TableStatus, TableDropdown } from '../../src'; 4 | 5 | const data: { 6 | key: string | number; 7 | name: string; 8 | age: string | number; 9 | address: string; 10 | money: number; 11 | date: number; 12 | }[] = []; 13 | for (let i = 0; i < 46; i += 1) { 14 | data.push({ 15 | key: i, 16 | name: `Edward King ${i}`, 17 | age: 10 + i, 18 | money: parseFloat((10000.26 * (i + 1)).toFixed(2)), 19 | date: moment('2019-11-16 12:50:26').valueOf() + i * 1000 * 60 * 2, 20 | address: `London, Park Lane no. ${i}`, 21 | }); 22 | } 23 | 24 | export const columns: ProColumns[] = [ 25 | { 26 | title: '序号', 27 | dataIndex: 'index', 28 | valueType: 'index', 29 | width: 72, 30 | }, 31 | { 32 | title: '边框序号', 33 | dataIndex: 'indexBorder', 34 | valueType: 'indexBorder', 35 | width: 72, 36 | }, 37 | { 38 | title: 'Name', 39 | dataIndex: 'name', 40 | copyable: true, 41 | }, 42 | { 43 | title: 'sex', 44 | dataIndex: 'sex', 45 | copyable: true, 46 | filters: true, 47 | valueEnum: { 48 | man: '男', 49 | woman: '女', 50 | }, 51 | }, 52 | { 53 | title: 'Age', 54 | dataIndex: 'age', 55 | }, 56 | { 57 | title: 'Address', 58 | dataIndex: 'address', 59 | ellipsis: true, 60 | width: 100, 61 | }, 62 | { 63 | title: 'money', 64 | dataIndex: 'money', 65 | valueType: 'money', 66 | }, 67 | { 68 | title: 'date', 69 | key: 'date', 70 | dataIndex: 'date', 71 | valueType: 'date', 72 | }, 73 | { 74 | title: 'dateTime', 75 | key: 'dateTime', 76 | dataIndex: 'date', 77 | valueType: 'dateTime', 78 | }, 79 | { 80 | title: 'time', 81 | key: 'time', 82 | dataIndex: 'date', 83 | valueType: 'time', 84 | }, 85 | { 86 | title: '状态', 87 | dataIndex: 'status', 88 | render: () => ( 89 |
90 | 上线成功 91 |
92 | 上线失败 93 |
94 | 正在部署 95 |
96 | 正在初始化 97 |
98 | ), 99 | }, 100 | { 101 | title: 'option', 102 | valueType: 'option', 103 | dataIndex: 'id', 104 | render: (text, row, index, action) => [ 105 | { 107 | window.alert('确认删除?'); 108 | action.reload(); 109 | }} 110 | > 111 | delete 112 | , 113 | { 115 | window.alert('确认刷新?'); 116 | action.reload(); 117 | }} 118 | > 119 | reload 120 | , 121 | window.alert(key)} 123 | menus={[ 124 | { key: 'copy', name: '复制' }, 125 | { key: 'delete', name: '删除' }, 126 | ]} 127 | />, 128 | ], 129 | }, 130 | ]; 131 | 132 | export const request = (): Promise<{ 133 | data: { 134 | key: string | number; 135 | name: string; 136 | age: string | number; 137 | address: string; 138 | }[]; 139 | success: true; 140 | }> => 141 | Promise.resolve({ 142 | data, 143 | success: true, 144 | }); 145 | -------------------------------------------------------------------------------- /docs/example/pollinga.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button } from 'antd'; 4 | import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; 5 | 6 | const valueEnum = { 7 | 0: 'close', 8 | 1: 'running', 9 | 2: 'online', 10 | 3: 'error', 11 | }; 12 | 13 | export interface TableListItem { 14 | key: number; 15 | name: string; 16 | status: string; 17 | updatedAt: number; 18 | createdAt: number; 19 | progress: number; 20 | money: number; 21 | } 22 | const tableListDataSource: TableListItem[] = []; 23 | 24 | for (let i = 0; i < 20; i += 1) { 25 | tableListDataSource.push({ 26 | key: i, 27 | name: `TradeCode ${i}`, 28 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 29 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 30 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 31 | money: Math.floor(Math.random() * 2000) * i, 32 | progress: Math.ceil(Math.random() * 100) + 1, 33 | }); 34 | } 35 | 36 | const timeAwait = (waitTime: number) => 37 | new Promise((res) => 38 | window.setTimeout(() => { 39 | res(); 40 | }, waitTime), 41 | ); 42 | 43 | const columns: ProColumns[] = [ 44 | { 45 | title: '序号', 46 | dataIndex: 'index', 47 | valueType: 'index', 48 | width: 80, 49 | }, 50 | { 51 | title: '状态', 52 | dataIndex: 'status', 53 | initialValue: 'all', 54 | width: 100, 55 | filters: true, 56 | valueEnum: { 57 | all: { text: '全部', status: 'Default' }, 58 | close: { text: '关闭', status: 'Default' }, 59 | running: { text: '运行中', status: 'Processing' }, 60 | online: { text: '已上线', status: 'Success' }, 61 | error: { text: '异常', status: 'Error' }, 62 | }, 63 | }, 64 | 65 | { 66 | title: '进度', 67 | key: 'progress', 68 | dataIndex: 'progress', 69 | valueType: (item) => ({ 70 | type: 'progress', 71 | status: item.status !== 'error' ? 'active' : 'exception', 72 | }), 73 | width: 200, 74 | }, 75 | { 76 | title: '更新时间', 77 | key: 'since2', 78 | width: 120, 79 | dataIndex: 'createdAt', 80 | valueType: 'date', 81 | }, 82 | ]; 83 | 84 | export default () => { 85 | const actionRef = useRef(undefined); 86 | useEffect(() => { 87 | let id = 0; 88 | const loop = () => { 89 | id = window.setTimeout(() => { 90 | const { current } = actionRef; 91 | if (current) { 92 | current.reload(); 93 | } 94 | loop(); 95 | }, 1000); 96 | }; 97 | loop(); 98 | return () => { 99 | window.clearTimeout(id); 100 | }; 101 | }, []); 102 | 103 | return ( 104 | 105 | columns={columns} 106 | rowKey="key" 107 | pagination={{ 108 | showSizeChanger: true, 109 | }} 110 | actionRef={actionRef} 111 | request={async () => { 112 | await timeAwait(500); 113 | return { 114 | data: tableListDataSource, 115 | success: true, 116 | total: tableListDataSource.length, 117 | }; 118 | }} 119 | dateFormatter="string" 120 | headerTitle="轮询" 121 | toolBarRender={() => [ 122 | , 126 | ]} 127 | /> 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /docs/example/nested-table.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 3 | 4 | const valueEnum = { 5 | 0: 'close', 6 | 1: 'running', 7 | 2: 'online', 8 | 3: 'error', 9 | }; 10 | 11 | export interface TableListItem { 12 | key: number; 13 | name: string; 14 | status: string; 15 | updatedAt: number; 16 | createdAt: number; 17 | progress: number; 18 | money: number; 19 | } 20 | const tableListDataSource: TableListItem[] = []; 21 | 22 | for (let i = 0; i < 1; i += 1) { 23 | tableListDataSource.push({ 24 | key: i, 25 | name: `TradeCode ${i}`, 26 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 27 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 28 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 29 | money: Math.floor(Math.random() * 2000) * i, 30 | progress: Math.ceil(Math.random() * 100) + 1, 31 | }); 32 | } 33 | 34 | const columns: ProColumns[] = [ 35 | { 36 | title: '序号', 37 | dataIndex: 'index', 38 | valueType: 'index', 39 | width: 80, 40 | }, 41 | { 42 | title: '状态', 43 | dataIndex: 'status', 44 | initialValue: 'all', 45 | width: 100, 46 | filters: true, 47 | valueEnum: { 48 | all: { text: '全部', status: 'Default' }, 49 | close: { text: '关闭', status: 'Default' }, 50 | running: { text: '运行中', status: 'Processing' }, 51 | online: { text: '已上线', status: 'Success' }, 52 | error: { text: '异常', status: 'Error' }, 53 | }, 54 | }, 55 | { 56 | title: '进度', 57 | key: 'progress', 58 | dataIndex: 'progress', 59 | valueType: (item) => ({ 60 | type: 'progress', 61 | status: item.status !== 'error' ? 'active' : 'exception', 62 | }), 63 | width: 200, 64 | }, 65 | { 66 | title: '更新时间', 67 | key: 'since2', 68 | width: 120, 69 | dataIndex: 'createdAt', 70 | valueType: 'date', 71 | }, 72 | ]; 73 | 74 | const expandedRowRender = () => { 75 | const data = []; 76 | for (let i = 0; i < 3; i += 1) { 77 | data.push({ 78 | key: i, 79 | date: '2014-12-24 23:12:00', 80 | name: 'This is production name', 81 | upgradeNum: 'Upgraded: 56', 82 | }); 83 | } 84 | return ( 85 | [Pause, Stop], 99 | }, 100 | ]} 101 | headerTitle={false} 102 | search={false} 103 | options={false} 104 | dataSource={data} 105 | pagination={false} 106 | /> 107 | ); 108 | }; 109 | 110 | export default () => { 111 | const [dataSource, setDataSource] = useState([]); 112 | useEffect(() => { 113 | setDataSource(tableListDataSource); 114 | }, []); 115 | return ( 116 | 117 | columns={columns} 118 | rowKey="key" 119 | pagination={{ 120 | showSizeChanger: true, 121 | }} 122 | dataSource={dataSource} 123 | expandable={{ expandedRowRender }} 124 | dateFormatter="string" 125 | headerTitle="嵌套表格" 126 | /> 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design/pro-table", 3 | "version": "2.4.4", 4 | "description": "🏆 Use Ant Design Table like a Pro!", 5 | "repository": "https://github.com/ant-design/pro-table", 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 && umi-test", 26 | "test:coverage": "umi-test --coverage" 27 | }, 28 | "husky": { 29 | "hooks": { 30 | "pre-commit": "pretty-quick --staged" 31 | } 32 | }, 33 | "browserslist": [ 34 | "> 1%", 35 | "last 2 versions", 36 | "not ie <= 10" 37 | ], 38 | "dependencies": { 39 | "@ant-design/icons": "^4.1.0", 40 | "antd": "^4.1.5", 41 | "classnames": "^2.2.6", 42 | "dnd-core": "^10.0.2", 43 | "lodash.isequal": "^4.5.0", 44 | "lodash.tonumber": "^4.0.3", 45 | "moment": "^2.24.0", 46 | "rc-resize-observer": "^0.2.1", 47 | "rc-util": "^5.0.1", 48 | "react-dnd": "^10.0.2", 49 | "react-dnd-html5-backend": "^10.0.2", 50 | "unstated-next": "^1.1.0", 51 | "use-json-comparison": "^1.0.5", 52 | "use-media-antd-query": "1.0.2", 53 | "use-merge-value": "^1.0.1" 54 | }, 55 | "devDependencies": { 56 | "@ant-design/pro-skeleton": "^1.0.0-beta.2", 57 | "@babel/core": "^7.8.3", 58 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 59 | "@babel/preset-env": "^7.8.3", 60 | "@babel/preset-react": "^7.8.3", 61 | "@babel/preset-typescript": "^7.8.3", 62 | "@types/classnames": "^2.2.9", 63 | "@types/enzyme": "^3.10.3", 64 | "@types/jest": "^26.0.0", 65 | "@types/lodash.isequal": "^4.5.5", 66 | "@types/lodash.tonumber": "^4.0.3", 67 | "@types/node": "^14.0.5", 68 | "@types/react": "^16.9.34", 69 | "@types/react-responsive": "^8.0.2", 70 | "@umijs/babel-preset-umi": "^3.1.2", 71 | "@umijs/fabric": "^2.0.0", 72 | "babel-loader": "^8.1.0", 73 | "babel-plugin-import": "^1.12.2", 74 | "css-loader": "^3.5.3", 75 | "dumi": "^1.0.20", 76 | "enzyme": "^3.10.0", 77 | "enzyme-to-json": "^3.4.3", 78 | "eslint": "^7.3.1", 79 | "father-build": "^1.18.1", 80 | "husky": "^4.2.5", 81 | "jsdom-global": "^3.0.2", 82 | "less-loader": "^6.0.0", 83 | "np": "^6.2.3", 84 | "prettier": "^2.0.5", 85 | "pretty-quick": "^2.0.1", 86 | "react-github-btn": "^1.1.1", 87 | "style-loader": "^1.2.1", 88 | "stylelint": "^13.3.3", 89 | "typescript": "^3.3.3", 90 | "umi": "^3.1.2", 91 | "umi-plugin-githubpages": "^2.0.1", 92 | "umi-request": "^1.2.15", 93 | "umi-test": "^1.9.6", 94 | "webpack": "^4.43.0", 95 | "webpack-bundle-analyzer": "^3.7.0", 96 | "webpack-cli": "^3.3.12" 97 | }, 98 | "peerDependencies": { 99 | "react": "16.x" 100 | }, 101 | "authors": { 102 | "name": "chenshuai2144", 103 | "email": "qixian.cs@outlook.com" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | protable: './src/index.tsx', 9 | 'protable.min': './src/index.tsx', 10 | }, 11 | output: { 12 | filename: '[name].js', 13 | library: 'ProTable', 14 | libraryExport: 'default', 15 | path: path.resolve(__dirname, 'dist'), 16 | globalObject: 'this', 17 | }, 18 | mode: 'production', 19 | resolve: { 20 | extensions: ['.ts', '.tsx', '.json', '.css', '.js', '.less'], 21 | }, 22 | optimization: { 23 | minimize: true, 24 | minimizer: [ 25 | new TerserPlugin({ 26 | include: /\.min\.js$/, 27 | }), 28 | new OptimizeCSSAssetsPlugin({ 29 | include: /\.min\.js$/, 30 | }), 31 | ], 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.(png|jpg|gif|svg)$/i, 37 | use: [ 38 | { 39 | loader: 'url-loader', 40 | options: { 41 | limit: 8192, 42 | }, 43 | }, 44 | ], 45 | }, 46 | { 47 | test: /\.jsx?$/, 48 | use: { 49 | loader: 'babel-loader', 50 | options: { 51 | presets: ['@babel/typescript', '@babel/env', '@babel/react'], 52 | plugins: [ 53 | ['@babel/plugin-proposal-decorators', { legacy: true }], 54 | ['@babel/plugin-proposal-class-properties', { loose: true }], 55 | '@babel/proposal-object-rest-spread', 56 | ], 57 | }, 58 | }, 59 | }, 60 | { 61 | test: /\.tsx?$/, 62 | exclude: /(node_modules|bower_components)/, 63 | use: { 64 | loader: 'babel-loader', 65 | options: { 66 | presets: [ 67 | '@babel/typescript', 68 | [ 69 | '@babel/env', 70 | { 71 | loose: true, 72 | modules: false, 73 | }, 74 | ], 75 | '@babel/react', 76 | ], 77 | plugins: [ 78 | ['@babel/plugin-proposal-decorators', { legacy: true }], 79 | ['@babel/plugin-proposal-class-properties', { loose: true }], 80 | '@babel/proposal-object-rest-spread', 81 | ], 82 | }, 83 | }, 84 | }, 85 | { 86 | test: /\.css$/, 87 | use: [ 88 | { 89 | loader: 'style-loader', // creates style nodes from JS strings 90 | }, 91 | { 92 | loader: 'css-loader', // translates CSS into CommonJS 93 | }, 94 | ], 95 | }, 96 | { 97 | test: /\.less$/, 98 | use: [ 99 | { 100 | loader: MiniCssExtractPlugin.loader, 101 | options: { 102 | publicPath: (resourcePath, context) => 103 | `${path.relative(path.dirname(resourcePath), context)}/`, 104 | }, 105 | }, 106 | { 107 | loader: 'css-loader', // translates CSS into CommonJS 108 | }, 109 | { 110 | loader: 'less-loader', 111 | options: { 112 | lessOptions: { 113 | javascriptEnabled: true, 114 | }, 115 | }, 116 | }, 117 | ], 118 | }, 119 | ], 120 | }, 121 | externals: [ 122 | { 123 | react: 'React', 124 | 'react-dom': 'ReactDOM', 125 | antd: 'antd', 126 | moment: 'moment', 127 | }, 128 | /^antd/, 129 | ], 130 | plugins: [ 131 | new MiniCssExtractPlugin({ 132 | // Options similar to the same options in webpackOptions.output 133 | // both options are optional 134 | filename: '[name].css', 135 | chunkFilename: '[id].css', 136 | }), 137 | ], 138 | }; 139 | -------------------------------------------------------------------------------- /docs/demo/search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Tag, Space } from 'antd'; 4 | import ProTable, { ProColumns, TableDropdown } from '@ant-design/pro-table'; 5 | import request from 'umi-request'; 6 | 7 | interface GithubIssueItem { 8 | url: string; 9 | repository_url: string; 10 | labels_url: string; 11 | comments_url: string; 12 | events_url: string; 13 | html_url: string; 14 | id: number; 15 | node_id: string; 16 | number: number; 17 | title: string; 18 | user: User; 19 | labels: Label[]; 20 | state: string; 21 | locked: boolean; 22 | assignee?: any; 23 | assignees: any[]; 24 | milestone?: any; 25 | comments: number; 26 | created_at: string; 27 | updated_at: string; 28 | closed_at?: any; 29 | author_association: string; 30 | body: string; 31 | } 32 | 33 | interface Label { 34 | id: number; 35 | node_id: string; 36 | url: string; 37 | name: string; 38 | color: string; 39 | default: boolean; 40 | description: string; 41 | } 42 | 43 | interface User { 44 | login: string; 45 | id: number; 46 | node_id: string; 47 | avatar_url: string; 48 | gravatar_id: string; 49 | url: string; 50 | html_url: string; 51 | followers_url: string; 52 | following_url: string; 53 | gists_url: string; 54 | starred_url: string; 55 | subscriptions_url: string; 56 | organizations_url: string; 57 | repos_url: string; 58 | events_url: string; 59 | received_events_url: string; 60 | type: string; 61 | site_admin: boolean; 62 | } 63 | 64 | const columns: ProColumns[] = [ 65 | { 66 | title: '序号', 67 | dataIndex: 'index', 68 | valueType: 'indexBorder', 69 | width: 72, 70 | }, 71 | { 72 | title: '标题', 73 | dataIndex: 'title', 74 | copyable: true, 75 | ellipsis: true, 76 | width: 200, 77 | hideInSearch: true, 78 | }, 79 | { 80 | title: (_, type) => (type === 'table' ? '状态' : '列表状态'), 81 | dataIndex: 'state', 82 | initialValue: 'all', 83 | filters: true, 84 | valueEnum: { 85 | all: { text: '全部', status: 'Default' }, 86 | open: { 87 | text: '未解决', 88 | status: 'Error', 89 | }, 90 | closed: { 91 | text: '已解决', 92 | status: 'Success', 93 | }, 94 | }, 95 | }, 96 | { 97 | title: '排序方式', 98 | key: 'direction', 99 | hideInTable: true, 100 | dataIndex: 'direction', 101 | filters: true, 102 | valueEnum: { 103 | asc: '正序', 104 | desc: '倒序', 105 | }, 106 | }, 107 | { 108 | title: '标签', 109 | dataIndex: 'labels', 110 | width: 120, 111 | render: (_, row) => ( 112 | 113 | {row.labels.map(({ name, id, color }) => ( 114 | 115 | {name} 116 | 117 | ))} 118 | 119 | ), 120 | }, 121 | { 122 | title: '创建时间', 123 | key: 'since', 124 | dataIndex: 'created_at', 125 | valueType: 'dateTime', 126 | }, 127 | { 128 | title: 'option', 129 | valueType: 'option', 130 | dataIndex: 'id', 131 | render: (text, row) => [ 132 | 133 | 查看 134 | , 135 | window.alert(key)} 137 | menus={[ 138 | { key: 'copy', name: '复制' }, 139 | { key: 'delete', name: '删除' }, 140 | ]} 141 | />, 142 | ], 143 | }, 144 | ]; 145 | 146 | export default () => ( 147 | 148 | columns={columns} 149 | request={async (params = {}) => 150 | request<{ 151 | data: GithubIssueItem[]; 152 | }>('https://proapi.azurewebsites.net/github/issues', { 153 | params, 154 | }) 155 | } 156 | rowKey="id" 157 | dateFormatter="string" 158 | headerTitle="查询 Table" 159 | toolBarRender={() => [ 160 | , 164 | ]} 165 | /> 166 | ); 167 | -------------------------------------------------------------------------------- /docs/example/renderTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Result, Card, Descriptions } from 'antd'; 4 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 5 | 6 | const valueEnum = { 7 | 0: 'close', 8 | 1: 'running', 9 | 2: 'online', 10 | 3: 'error', 11 | }; 12 | 13 | export interface TableListItem { 14 | key: number; 15 | name: string; 16 | status: string; 17 | updatedAt: number; 18 | createdAt: number; 19 | progress: number; 20 | money: number; 21 | } 22 | const tableListDataSource: TableListItem[] = []; 23 | 24 | for (let i = 0; i < 20; i += 1) { 25 | tableListDataSource.push({ 26 | key: i, 27 | name: `TradeCode ${i}`, 28 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 29 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 30 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 31 | money: Math.floor(Math.random() * 2000) * i, 32 | progress: Math.ceil(Math.random() * 100) + 1, 33 | }); 34 | } 35 | 36 | const columns: ProColumns[] = [ 37 | { 38 | title: '序号', 39 | dataIndex: 'index', 40 | valueType: 'index', 41 | width: 80, 42 | }, 43 | { 44 | title: '状态', 45 | dataIndex: 'status', 46 | initialValue: 'all', 47 | width: 100, 48 | filters: true, 49 | valueEnum: { 50 | all: { text: '全部', status: 'Default' }, 51 | close: { text: '关闭', status: 'Default' }, 52 | running: { text: '运行中', status: 'Processing' }, 53 | online: { text: '已上线', status: 'Success' }, 54 | error: { text: '异常', status: 'Error' }, 55 | }, 56 | }, 57 | { 58 | title: '进度', 59 | key: 'progress', 60 | dataIndex: 'progress', 61 | valueType: (item) => ({ 62 | type: 'progress', 63 | status: item.status !== 'error' ? 'active' : 'exception', 64 | }), 65 | width: 200, 66 | }, 67 | { 68 | title: '更新时间', 69 | key: 'since2', 70 | width: 120, 71 | dataIndex: 'createdAt', 72 | valueType: 'date', 73 | }, 74 | ]; 75 | 76 | export default () => { 77 | const [loading, setLoading] = useState(true); 78 | const [dataSource, setDataSource] = useState([]); 79 | useEffect(() => { 80 | setTimeout(() => { 81 | setLoading(false); 82 | setDataSource(tableListDataSource); 83 | }, 5000); 84 | }, []); 85 | 86 | return ( 87 | 88 | columns={columns} 89 | rowKey="key" 90 | pagination={{ 91 | showSizeChanger: true, 92 | }} 93 | tableRender={(_, dom) => ( 94 |
100 | 101 |
106 | {dom} 107 |
108 |
109 | )} 110 | tableExtraRender={(_, data) => ( 111 | 112 | 113 | {data.length} 114 | Lili Qu 115 | 116 | 421421 117 | 118 | 2017-01-10 119 | 2017-10-10 120 | 121 | Gonghu Road, Xihu District, Hangzhou, Zhejiang, China 122 | 123 | 124 | 125 | )} 126 | loading={loading} 127 | dataSource={dataSource} 128 | options={{ 129 | density: true, 130 | reload: () => { 131 | setLoading(true); 132 | setTimeout(() => { 133 | setLoading(false); 134 | }, 1000); 135 | }, 136 | fullScreen: true, 137 | setting: true, 138 | }} 139 | dateFormatter="string" 140 | headerTitle="dataSource 和 loading" 141 | toolBarRender={() => [ 142 | , 146 | ]} 147 | /> 148 | ); 149 | }; 150 | -------------------------------------------------------------------------------- /docs/demo/batchOption.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Tag, Space } from 'antd'; 4 | import ProTable, { ProColumns, TableDropdown } from '@ant-design/pro-table'; 5 | import request from 'umi-request'; 6 | 7 | interface GithubIssueItem { 8 | url: string; 9 | repository_url: string; 10 | labels_url: string; 11 | comments_url: string; 12 | events_url: string; 13 | html_url: string; 14 | id: number; 15 | node_id: string; 16 | number: number; 17 | title: string; 18 | user: User; 19 | labels: Label[]; 20 | state: string; 21 | locked: boolean; 22 | assignee?: any; 23 | assignees: any[]; 24 | milestone?: any; 25 | comments: number; 26 | created_at: string; 27 | updated_at: string; 28 | closed_at?: any; 29 | author_association: string; 30 | body: string; 31 | } 32 | 33 | interface Label { 34 | id: number; 35 | node_id: string; 36 | url: string; 37 | name: string; 38 | color: string; 39 | default: boolean; 40 | description: string; 41 | } 42 | 43 | interface User { 44 | login: string; 45 | id: number; 46 | node_id: string; 47 | avatar_url: string; 48 | gravatar_id: string; 49 | url: string; 50 | html_url: string; 51 | followers_url: string; 52 | following_url: string; 53 | gists_url: string; 54 | starred_url: string; 55 | subscriptions_url: string; 56 | organizations_url: string; 57 | repos_url: string; 58 | events_url: string; 59 | received_events_url: string; 60 | type: string; 61 | site_admin: boolean; 62 | } 63 | 64 | const columns: ProColumns[] = [ 65 | { 66 | title: '标题', 67 | dataIndex: 'title', 68 | copyable: true, 69 | ellipsis: true, 70 | width: 200, 71 | hideInSearch: true, 72 | }, 73 | { 74 | title: '状态', 75 | dataIndex: 'state', 76 | initialValue: 'all', 77 | filters: true, 78 | valueEnum: { 79 | all: { text: '全部', status: 'Default' }, 80 | open: { 81 | text: '未解决', 82 | status: 'Error', 83 | }, 84 | closed: { 85 | text: '已解决', 86 | status: 'Success', 87 | }, 88 | }, 89 | }, 90 | { 91 | title: '标签', 92 | dataIndex: 'labels', 93 | width: 120, 94 | render: (_, row) => ( 95 | 96 | {row.labels.map(({ name, id, color }) => ( 97 | 98 | {name} 99 | 100 | ))} 101 | 102 | ), 103 | }, 104 | { 105 | title: '创建时间', 106 | key: 'since', 107 | dataIndex: 'created_at', 108 | valueType: 'dateTime', 109 | }, 110 | 111 | { 112 | title: 'option', 113 | valueType: 'option', 114 | dataIndex: 'id', 115 | render: (text, row) => [ 116 | 117 | 查看 118 | , 119 | window.alert(key)} 122 | menus={[ 123 | { key: 'copy', name: '复制' }, 124 | { key: 'delete', name: '删除' }, 125 | ]} 126 | />, 127 | ], 128 | }, 129 | ]; 130 | 131 | export default () => ( 132 | 133 | columns={columns} 134 | request={async (params = {}) => 135 | request<{ 136 | data: GithubIssueItem[]; 137 | }>('https://proapi.azurewebsites.net/github/issues', { 138 | params, 139 | }) 140 | } 141 | rowKey="id" 142 | rowSelection={{}} 143 | tableAlertRender={({ selectedRowKeys, selectedRows }) => 144 | `当前共选中 ${selectedRowKeys.length} 项,共有 ${selectedRows.reduce((pre, item) => { 145 | if (item.state === 'open') { 146 | return pre + 1; 147 | } 148 | return pre; 149 | }, 0)} 项未解决 ` 150 | } 151 | tableAlertOptionRender={(props) => { 152 | const { onCleanSelected } = props; 153 | return ( 154 | 155 | 自定义 156 | 清空 157 | 158 | ); 159 | }} 160 | dateFormatter="string" 161 | headerTitle="批量操作" 162 | toolBarRender={(_, { selectedRowKeys }) => [ 163 | , 167 | selectedRowKeys && selectedRowKeys.length && ( 168 | 176 | ), 177 | ]} 178 | /> 179 | ); 180 | -------------------------------------------------------------------------------- /docs/search.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 表单 3 | order: 8 4 | sidemenu: false 5 | nav: 6 | title: 表单 7 | order: 2 8 | --- 9 | 10 | # Table 搜索 11 | 12 | ProTable 会根据列来生成一个 Form,用于筛选列表数据,最后的值会根据通过 `request` 的第一个参数返回,看起来就像。 13 | 14 | ```jsx | pure 15 | { all params}}> 16 | ``` 17 | 18 | 按照规范,table 的表单不需要任何的必选参数,所有点击搜索和重置都会触发 `request`来发起一次查询。 19 | 20 | Form 的列是根据 `valueType` 来生成不同的类型。 21 | 22 | > valueType 为 index indexBorder option 和没有 dataIndex 和 key 的列将会忽略。 23 | 24 | | 类型 | 对应的组件 | 25 | | --- | --- | 26 | | text | [Input](https://ant.design/components/input-cn/) | 27 | | textarea | [Input.TextArea](https://ant.design/components/input-cn/#components-input-demo-textarea) | 28 | | date | [DatePicker](https://ant.design/components/date-picker-cn/) | 29 | | dateTime | [DatePicker](https://ant.design/components/date-picker-cn/#components-date-picker-demo-time) | 30 | | time | [TimePicker](https://ant.design/components/time-picker-cn/) | 31 | | dateTimeRange | [RangePicker](https://ant.design/components/time-picker-cn/#components-time-picker-demo-range-picker) | 32 | | dateRange | [RangePicker](https://ant.design/components/time-picker-cn/#components-time-picker-demo-range-picker) | 33 | | money | [InputNumber](https://ant.design/components/input-number-cn/) | 34 | | digit | [InputNumber](https://ant.design/components/input-number-cn/) | 35 | | option | 不展示 | 36 | | index | 不展示 | 37 | | progress | 不展示 | 38 | 39 | 设置了 `valueEnum` 的列将会生成 Select,Select 会自动插入一个全部选项,并且默认选中,但是值为 `all` 在查询时会被丢弃。 40 | 41 | ## 相关 API 42 | 43 | ### ProTable 44 | 45 | | 属性 | 描述 | 类型 | 默认值 | 46 | | --- | --- | --- | --- | 47 | | onLoad | 数据加载完成后触发,会多次触发 | `(dataSource: T[]) => void` | - | 48 | | onRequestError | 数据加载失败时触发 | `(e: Error) => void` | - | 49 | | beforeSearchSubmit | 搜索之前进行一些修改 | `(params:T)=>T` | - | 50 | | search | 是否显示搜索表单,传入对象时为搜索表单的配置 | `boolean \| { span?: number \| DefaultColConfig,searchText?: string, resetText?: string, collapseRender?: (collapsed: boolean) => React.ReactNode, collapsed:boolean, onCollapse: (collapsed:boolean)=> void }` | true | 51 | | dateFormatter | moment 的格式化方式,默认会转化成 string | `"string" \| "number" \| false` | string | 52 | 53 | ### search 54 | 55 | | 属性 | 描述 | 类型 | 默认值 | 56 | | --- | --- | --- | --- | 57 | | searchText | 查询按钮的文本 | string | 查询 | 58 | | resetText | 重置按钮的文本 | string | 重置 | 59 | | submitText | 提交按钮的文本 | string | 提交 | 60 | | collapseRender | 收起按钮的 render | `(collapsed: boolean,showCollapseButton?: boolean,) => React.ReactNode` | - | 61 | | collapsed | 是否收起 | boolean | - | 62 | | onCollapse | 收起按钮的事件 | `(collapsed: boolean) => void;` | - | 63 | | optionRender | 操作栏的 render | `(( searchConfig: Omit, props: Omit, ) => React.ReactNode) \| false;` | - | 64 | 65 | ### Columns 66 | 67 | | 属性 | 描述 | 类型 | 默认值 | 68 | | --- | --- | --- | --- | 69 | | valueEnum | 值的枚举,会自动转化把值当成 key 来取出要显示的内容 | [valueEnum](#valueEnum) | - | 70 | | valueType | 值的类型 | `'money' \| 'option' \| 'date' \| 'dateTime' \| 'time' \| 'text'\| 'index' \| 'indexBorder'` | 'text' | 71 | | hideInSearch | 在查询表单中不展示此项 | boolean | - | 72 | | hideInTable | 在 Table 中不展示此列 | boolean | - | 73 | | showFilters | 开启该列的表头的筛选菜单项,配合 valueEnum 使用 | boolean | false | 74 | | formItemProps | 查询表单的 props,会透传给表单项 | `{ [prop: string]: any }` | - | 75 | | renderFormItem | 渲染查询表单的输入组件 | `(item,props:{value,onChange}) => React.ReactNode` | - | 76 | 77 | ## 自定义表单项 78 | 79 | 很多时候内置的表单项无法满足我们的基本需求,这时候我们就需要来自定义一下默认的组件,我们可以通过 `formItemProps` 和 `renderFormItem` 配合来使用。 80 | 81 | `formItemProps` 可以把 props 透传,可以设置 select 的样式和多选等问题。 82 | 83 | `renderFormItem` 可以完成重写渲染逻辑,传入 item 和 props 来进行渲染,需要注意的是我们必须要将 props 中的 `value` 和 `onChange` 必须要被赋值,否则 form 无法拿到参数。 84 | 85 | ```tsx | pure 86 | renderFormItem: (_, { type, defaultRender, ...rest }, form) => { 87 | if (type === 'form') { 88 | return null; 89 | } 90 | const status = form.getFieldValue('state'); 91 | if (status !== 'open') { 92 | return ; 93 | } 94 | return defaultRender(_); 95 | }; 96 | ``` 97 | 98 | `renderFormItem` 的定义, 具体的值可以打开控制台查看。 99 | 100 | ```tsx | pure 101 | renderFormItem?: ( 102 | item: ProColumns, 103 | config: { 104 | value?: any; 105 | onChange?: (value: any) => void; 106 | onSelect?: (value: any) => void; 107 | type: ProTableTypes; 108 | defaultRender: (newItem: ProColumns) => JSX.Element | null; 109 | }, 110 | form: FormInstance, 111 | ) => JSX.Element | false | null; 112 | ``` 113 | 114 | 115 | 116 | ## 基本使用 117 | 118 | 119 | 120 | ## 操作栏 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/demo/search_option.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Tag, Space } from 'antd'; 4 | import ProTable, { ProColumns, TableDropdown } from '@ant-design/pro-table'; 5 | import request from 'umi-request'; 6 | 7 | interface GithubIssueItem { 8 | url: string; 9 | repository_url: string; 10 | labels_url: string; 11 | comments_url: string; 12 | events_url: string; 13 | html_url: string; 14 | id: number; 15 | node_id: string; 16 | number: number; 17 | title: string; 18 | user: User; 19 | labels: Label[]; 20 | state: string; 21 | locked: boolean; 22 | assignee?: any; 23 | assignees: any[]; 24 | milestone?: any; 25 | comments: number; 26 | created_at: string; 27 | updated_at: string; 28 | closed_at?: any; 29 | author_association: string; 30 | body: string; 31 | } 32 | 33 | interface Label { 34 | id: number; 35 | node_id: string; 36 | url: string; 37 | name: string; 38 | color: string; 39 | default: boolean; 40 | description: string; 41 | } 42 | 43 | interface User { 44 | login: string; 45 | id: number; 46 | node_id: string; 47 | avatar_url: string; 48 | gravatar_id: string; 49 | url: string; 50 | html_url: string; 51 | followers_url: string; 52 | following_url: string; 53 | gists_url: string; 54 | starred_url: string; 55 | subscriptions_url: string; 56 | organizations_url: string; 57 | repos_url: string; 58 | events_url: string; 59 | received_events_url: string; 60 | type: string; 61 | site_admin: boolean; 62 | } 63 | 64 | const columns: ProColumns[] = [ 65 | { 66 | title: '序号', 67 | dataIndex: 'index', 68 | valueType: 'indexBorder', 69 | width: 72, 70 | }, 71 | { 72 | title: '标题', 73 | dataIndex: 'title', 74 | copyable: true, 75 | ellipsis: true, 76 | width: 200, 77 | hideInSearch: true, 78 | }, 79 | { 80 | title: '状态', 81 | dataIndex: 'state', 82 | initialValue: 'all', 83 | filters: true, 84 | valueEnum: { 85 | all: { text: '全部', status: 'Default' }, 86 | open: { 87 | text: '未解决', 88 | status: 'Error', 89 | }, 90 | closed: { 91 | text: '已解决', 92 | status: 'Success', 93 | }, 94 | }, 95 | }, 96 | { 97 | title: '排序方式', 98 | key: 'direction', 99 | hideInTable: true, 100 | dataIndex: 'direction', 101 | filters: true, 102 | valueEnum: { 103 | asc: '正序', 104 | desc: '倒序', 105 | }, 106 | }, 107 | { 108 | title: '标签', 109 | dataIndex: 'labels', 110 | width: 120, 111 | render: (_, row) => ( 112 | 113 | {row.labels.map(({ name, id, color }) => ( 114 | 115 | {name} 116 | 117 | ))} 118 | 119 | ), 120 | }, 121 | { 122 | title: '创建时间', 123 | key: 'since', 124 | dataIndex: 'created_at', 125 | valueType: 'dateTime', 126 | }, 127 | { 128 | title: 'option', 129 | valueType: 'option', 130 | dataIndex: 'id', 131 | render: (text, row) => [ 132 | 133 | 查看 134 | , 135 | window.alert(key)} 137 | menus={[ 138 | { key: 'copy', name: '复制' }, 139 | { key: 'delete', name: '删除' }, 140 | ]} 141 | />, 142 | ], 143 | }, 144 | ]; 145 | 146 | export default () => ( 147 | 148 | columns={columns} 149 | request={async (params = {}) => 150 | request<{ 151 | data: GithubIssueItem[]; 152 | }>('https://proapi.azurewebsites.net/github/issues', { 153 | params, 154 | }) 155 | } 156 | rowKey="id" 157 | dateFormatter="string" 158 | headerTitle="查询 Table" 159 | search={{ 160 | collapsed: false, 161 | optionRender: ({ searchText, resetText }, { form }) => { 162 | return ( 163 | <> 164 | { 166 | form.submit(); 167 | }} 168 | > 169 | {searchText} 170 | {' '} 171 | { 173 | form.resetFields(); 174 | }} 175 | > 176 | {resetText} 177 | {' '} 178 | 导出 179 | 180 | ); 181 | }, 182 | }} 183 | toolBarRender={() => [ 184 | , 188 | ]} 189 | /> 190 | ); 191 | -------------------------------------------------------------------------------- /docs/demo/valueType.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 3 | import { Space } from 'antd'; 4 | 5 | const valueEnum = { 6 | 0: 'close', 7 | 1: 'running', 8 | 2: 'online', 9 | 3: 'error', 10 | }; 11 | 12 | export interface TableListItem { 13 | key: number; 14 | name: string; 15 | status: string | number; 16 | updatedAt: number; 17 | createdAt: number; 18 | progress: number; 19 | money: number; 20 | percent: number | string; 21 | createdAtRange: number[]; 22 | code: string; 23 | avatar: string; 24 | } 25 | const tableListDataSource: TableListItem[] = []; 26 | 27 | for (let i = 0; i < 2; i += 1) { 28 | tableListDataSource.push({ 29 | key: i, 30 | avatar: 31 | 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 32 | name: `TradeCode ${i}`, 33 | status: valueEnum[Math.floor(Math.random() * 10) % 4], 34 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 35 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 36 | createdAtRange: [ 37 | Date.now() - Math.floor(Math.random() * 2000), 38 | Date.now() - Math.floor(Math.random() * 2000), 39 | ], 40 | money: Math.floor(Math.random() * 2000) * i, 41 | progress: Math.ceil(Math.random() * 100) + 1, 42 | percent: 43 | Math.random() > 0.5 44 | ? ((i + 1) * 10 + Math.random()).toFixed(3) 45 | : -((i + 1) * 10 + Math.random()).toFixed(2), 46 | code: `const getData = async params => { 47 | const data = await getData(params); 48 | return { list: data.data, ...data }; 49 | };`, 50 | }); 51 | } 52 | 53 | tableListDataSource.push({ 54 | key: 3, 55 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', 56 | name: `TradeCode ${3}`, 57 | status: 0, 58 | updatedAt: Date.now() - Math.floor(Math.random() * 1000), 59 | createdAt: Date.now() - Math.floor(Math.random() * 2000), 60 | createdAtRange: [ 61 | Date.now() - Math.floor(Math.random() * 2000), 62 | Date.now() - Math.floor(Math.random() * 2000), 63 | ], 64 | money: Math.floor(Math.random() * 2000) * 3, 65 | progress: Math.ceil(Math.random() * 100) + 1, 66 | percent: 67 | Math.random() > 0.5 68 | ? ((3 + 1) * 10 + Math.random()).toFixed(3) 69 | : -((3 + 1) * 10 + Math.random()).toFixed(2), 70 | code: `const getData = async params => { 71 | const data = await getData(params); 72 | return { list: data.data, ...data }; 73 | };`, 74 | }); 75 | 76 | const columns: ProColumns[] = [ 77 | { 78 | title: '序号', 79 | dataIndex: 'index', 80 | valueType: 'index', 81 | width: 72, 82 | }, 83 | { 84 | title: 'border 序号', 85 | dataIndex: 'index', 86 | key: 'indexBorder', 87 | valueType: 'indexBorder', 88 | width: 72, 89 | // @ts-ignore 90 | sorter: { 91 | multiple: 3, 92 | }, 93 | }, 94 | { 95 | title: '状态', 96 | dataIndex: 'status', 97 | initialValue: 'all', 98 | // @ts-ignore 99 | sorter: { 100 | multiple: 3, 101 | }, 102 | width: 100, 103 | ellipsis: true, 104 | filters: true, 105 | valueEnum: { 106 | all: { text: '全部', status: 'Default' }, 107 | close: { text: '关闭', status: 'Default' }, 108 | running: { text: '运行中', status: 'Processing' }, 109 | online: { text: '已上线', status: 'Success' }, 110 | error: { text: '异常', status: 'Error' }, 111 | 0: { text: '0异常', status: 'Error' }, 112 | }, 113 | }, 114 | { 115 | title: '代码', 116 | key: 'code', 117 | width: 120, 118 | dataIndex: 'code', 119 | valueType: 'code', 120 | }, 121 | { 122 | title: '头像', 123 | dataIndex: 'avatar', 124 | key: 'avatar', 125 | valueType: 'avatar', 126 | width: 150, 127 | render: (dom) => ( 128 | 129 | {dom} 130 | 131 | chenshuai2144 132 | 133 | 134 | ), 135 | }, 136 | { 137 | title: '操作', 138 | key: 'option', 139 | width: 120, 140 | valueType: 'option', 141 | render: () => [操作, 删除], 142 | }, 143 | ]; 144 | 145 | export default () => ( 146 | <> 147 | 148 | columns={columns} 149 | request={(params, sorter, filter) => { 150 | console.log(params, sorter, filter); 151 | return Promise.resolve({ 152 | total: 200, 153 | data: tableListDataSource, 154 | success: true, 155 | }); 156 | }} 157 | rowKey="key" 158 | headerTitle="样式类" 159 | /> 160 | 161 | ); 162 | -------------------------------------------------------------------------------- /docs/demo/linkage_form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Tag, Space, Input } from 'antd'; 4 | import ProTable, { ProColumns, TableDropdown } from '@ant-design/pro-table'; 5 | import request from 'umi-request'; 6 | 7 | interface GithubIssueItem { 8 | url: string; 9 | repository_url: string; 10 | labels_url: string; 11 | comments_url: string; 12 | events_url: string; 13 | html_url: string; 14 | id: number; 15 | node_id: string; 16 | number: number; 17 | title: string; 18 | user: User; 19 | labels: Label[]; 20 | state: string; 21 | locked: boolean; 22 | assignee?: any; 23 | assignees: any[]; 24 | milestone?: any; 25 | comments: number; 26 | created_at: string; 27 | updated_at: string; 28 | closed_at?: any; 29 | author_association: string; 30 | body: string; 31 | } 32 | 33 | interface Label { 34 | id: number; 35 | node_id: string; 36 | url: string; 37 | name: string; 38 | color: string; 39 | default: boolean; 40 | description: string; 41 | } 42 | 43 | interface User { 44 | login: string; 45 | id: number; 46 | node_id: string; 47 | avatar_url: string; 48 | gravatar_id: string; 49 | url: string; 50 | html_url: string; 51 | followers_url: string; 52 | following_url: string; 53 | gists_url: string; 54 | starred_url: string; 55 | subscriptions_url: string; 56 | organizations_url: string; 57 | repos_url: string; 58 | events_url: string; 59 | received_events_url: string; 60 | type: string; 61 | site_admin: boolean; 62 | } 63 | 64 | const columns: ProColumns[] = [ 65 | { 66 | title: '序号', 67 | dataIndex: 'index', 68 | valueType: 'indexBorder', 69 | width: 72, 70 | }, 71 | { 72 | title: '标题', 73 | dataIndex: 'title', 74 | copyable: true, 75 | ellipsis: true, 76 | width: 200, 77 | hideInSearch: true, 78 | }, 79 | { 80 | title: '状态', 81 | dataIndex: 'state', 82 | initialValue: 'all', 83 | filters: true, 84 | valueEnum: { 85 | all: { text: '全部', status: 'Default' }, 86 | open: { 87 | text: '未解决', 88 | status: 'Error', 89 | }, 90 | closed: { 91 | text: '已解决', 92 | status: 'Success', 93 | }, 94 | }, 95 | }, 96 | { 97 | title: '排序方式', 98 | key: 'direction', 99 | hideInTable: true, 100 | dataIndex: 'direction', 101 | filters: true, 102 | valueEnum: { 103 | asc: '正序', 104 | desc: '倒序', 105 | }, 106 | renderFormItem: (_, { type, defaultRender, ...rest }, form) => { 107 | console.log('item:', _); 108 | console.log('config:', { type, defaultRender, ...rest }); 109 | console.log('form:', form); 110 | if (type === 'form') { 111 | return null; 112 | } 113 | const status = form.getFieldValue('state'); 114 | if (status !== 'open') { 115 | return ; 116 | } 117 | return defaultRender(_); 118 | }, 119 | }, 120 | { 121 | title: '标签', 122 | dataIndex: 'labels', 123 | width: 120, 124 | render: (_, row) => ( 125 | 126 | {row.labels.map(({ name, id, color }) => ( 127 | 128 | {name} 129 | 130 | ))} 131 | 132 | ), 133 | }, 134 | { 135 | title: '创建时间', 136 | key: 'since', 137 | dataIndex: 'created_at', 138 | valueType: 'dateTime', 139 | }, 140 | { 141 | title: 'option', 142 | valueType: 'option', 143 | dataIndex: 'id', 144 | render: (text, row) => [ 145 | 146 | 查看 147 | , 148 | window.alert(key)} 150 | menus={[ 151 | { key: 'copy', name: '复制' }, 152 | { key: 'delete', name: '删除' }, 153 | ]} 154 | />, 155 | ], 156 | }, 157 | ]; 158 | 159 | export default () => ( 160 | 161 | columns={columns} 162 | request={async (params = {}) => 163 | request<{ 164 | data: GithubIssueItem[]; 165 | }>('https://proapi.azurewebsites.net/github/issues', { 166 | params, 167 | }) 168 | } 169 | rowKey="id" 170 | dateFormatter="string" 171 | headerTitle="查询 Table" 172 | search={{ 173 | collapsed: false, 174 | optionRender: ({ searchText, resetText }, { form }) => ( 175 | <> 176 | { 178 | form.submit(); 179 | }} 180 | > 181 | {searchText} 182 | {' '} 183 | { 185 | form.resetFields(); 186 | }} 187 | > 188 | {resetText} 189 | {' '} 190 | 导出 191 | 192 | ), 193 | }} 194 | toolBarRender={() => [ 195 | , 199 | ]} 200 | /> 201 | ); 202 | -------------------------------------------------------------------------------- /docs/example/intl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Tag, Select } from 'antd'; 4 | import ProTable, { 5 | ProColumns, 6 | TableDropdown, 7 | IntlProvider, 8 | zhCNIntl, 9 | enUSIntl, 10 | viVNIntl, 11 | itITIntl, 12 | jaJPIntl, 13 | esESIntl, 14 | ruRUIntl, 15 | msMYIntl, 16 | ActionType, 17 | // @ts-ignore 18 | } from '@ant-design/pro-table'; 19 | import request from 'umi-request'; 20 | 21 | const intlMap = { 22 | zhCNIntl, 23 | enUSIntl, 24 | viVNIntl, 25 | itITIntl, 26 | jaJPIntl, 27 | esESIntl, 28 | ruRUIntl, 29 | msMYIntl, 30 | }; 31 | 32 | interface GithubIssueItem { 33 | url: string; 34 | repository_url: string; 35 | labels_url: string; 36 | comments_url: string; 37 | events_url: string; 38 | html_url: string; 39 | id: number; 40 | node_id: string; 41 | number: number; 42 | title: string; 43 | user: User; 44 | labels: Label[]; 45 | state: string; 46 | locked: boolean; 47 | assignee?: any; 48 | assignees: any[]; 49 | milestone?: any; 50 | comments: number; 51 | created_at: string; 52 | updated_at: string; 53 | closed_at?: any; 54 | author_association: string; 55 | body: string; 56 | } 57 | 58 | interface Label { 59 | id: number; 60 | node_id: string; 61 | url: string; 62 | name: string; 63 | color: string; 64 | default: boolean; 65 | description: string; 66 | } 67 | 68 | interface User { 69 | login: string; 70 | id: number; 71 | node_id: string; 72 | avatar_url: string; 73 | gravatar_id: string; 74 | url: string; 75 | html_url: string; 76 | followers_url: string; 77 | following_url: string; 78 | gists_url: string; 79 | starred_url: string; 80 | subscriptions_url: string; 81 | organizations_url: string; 82 | repos_url: string; 83 | events_url: string; 84 | received_events_url: string; 85 | type: string; 86 | site_admin: boolean; 87 | } 88 | 89 | const columns: ProColumns[] = [ 90 | { 91 | title: 'index', 92 | dataIndex: 'index', 93 | valueType: 'indexBorder', 94 | width: 72, 95 | }, 96 | { 97 | title: 'Title', 98 | dataIndex: 'title', 99 | copyable: true, 100 | ellipsis: true, 101 | width: 200, 102 | hideInSearch: true, 103 | }, 104 | { 105 | title: 'Money', 106 | dataIndex: 'title', 107 | width: 100, 108 | valueType: 'money', 109 | render: () => (Math.random() * 100).toFixed(2), 110 | }, 111 | { 112 | title: 'Status', 113 | dataIndex: 'state', 114 | initialValue: 'all', 115 | filters: true, 116 | valueEnum: { 117 | all: { text: 'ALL', status: 'Default' }, 118 | open: { 119 | text: 'Error', 120 | status: 'Error', 121 | }, 122 | closed: { 123 | text: 'Success', 124 | status: 'Success', 125 | }, 126 | }, 127 | }, 128 | { 129 | title: 'Labels', 130 | dataIndex: 'labels', 131 | width: 80, 132 | render: (_, row) => 133 | row.labels.map(({ name, id, color }) => ( 134 | 141 | {name} 142 | 143 | )), 144 | }, 145 | { 146 | title: 'Created Time', 147 | key: 'since', 148 | dataIndex: 'created_at', 149 | valueType: 'dateTime', 150 | }, 151 | { 152 | title: 'option', 153 | valueType: 'option', 154 | dataIndex: 'id', 155 | render: (text, row, _, action) => [ 156 | 157 | show 158 | , 159 | action.reload()} 161 | menus={[ 162 | { key: 'copy', name: 'copy' }, 163 | { key: 'delete', name: 'delete' }, 164 | ]} 165 | />, 166 | ], 167 | }, 168 | ]; 169 | 170 | export default () => { 171 | const actionRef = useRef(); 172 | const [intl, setIntl] = useState('zhCNIntl'); 173 | return ( 174 | <> 175 | 176 | style={{ 177 | width: 200, 178 | }} 179 | value={intl} 180 | onChange={(value) => setIntl(value)} 181 | options={Object.keys(intlMap).map((value) => ({ value, label: value }))} 182 | /> 183 | 184 | 185 | columns={columns} 186 | actionRef={actionRef} 187 | request={async (params = {}) => 188 | request<{ 189 | data: GithubIssueItem[]; 190 | }>('https://proapi.azurewebsites.net/github/issues', { 191 | params, 192 | }) 193 | } 194 | rowKey="id" 195 | rowSelection={{}} 196 | pagination={{ 197 | showSizeChanger: true, 198 | }} 199 | dateFormatter="string" 200 | headerTitle="Basic Table" 201 | toolBarRender={() => [ 202 | , 206 | ]} 207 | /> 208 | 209 | 210 | ); 211 | }; 212 | -------------------------------------------------------------------------------- /src/useFetchData.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { usePrevious, useDebounceFn } from './component/util'; 3 | 4 | export interface RequestData { 5 | data: T[]; 6 | success?: boolean; 7 | total?: number; 8 | [key: string]: any; 9 | } 10 | export interface UseFetchDataAction> { 11 | dataSource: T['data'] | T; 12 | loading: boolean | undefined; 13 | hasMore: boolean; 14 | current: number; 15 | pageSize: number; 16 | total: number; 17 | cancel: () => void; 18 | reload: () => Promise; 19 | fetchMore: () => void; 20 | fullScreen?: () => void; 21 | resetPageIndex: () => void; 22 | reset: () => void; 23 | setPageInfo: (pageInfo: Partial) => void; 24 | } 25 | 26 | interface PageInfo { 27 | hasMore: boolean; 28 | page: number; 29 | pageSize: number; 30 | total: number; 31 | } 32 | 33 | const useFetchData = >( 34 | getData: (params: { pageSize: number; current: number }) => Promise, 35 | defaultData?: Partial, 36 | options?: { 37 | defaultCurrent?: number; 38 | defaultPageSize?: number; 39 | effects?: any[]; 40 | onLoad?: (dataSource: T['data']) => void; 41 | onRequestError?: (e: Error) => void; 42 | }, 43 | ): UseFetchDataAction => { 44 | let isMount = true; 45 | const { defaultPageSize = 20, defaultCurrent = 1, onLoad = () => null, onRequestError } = 46 | options || {}; 47 | 48 | const [list, setList] = useState(defaultData as any); 49 | const [loading, setLoading] = useState(undefined); 50 | 51 | const [pageInfo, setPageInfo] = useState({ 52 | hasMore: false, 53 | page: defaultCurrent || 1, 54 | total: 0, 55 | pageSize: defaultPageSize, 56 | }); 57 | 58 | // pre state 59 | const prePage = usePrevious(pageInfo.page); 60 | const prePageSize = usePrevious(pageInfo.pageSize); 61 | 62 | const { effects = [] } = options || {}; 63 | 64 | /** 65 | * 请求数据 66 | * @param isAppend 是否添加数据到后面 67 | */ 68 | const fetchList = async (isAppend?: boolean) => { 69 | if (loading || !isMount) { 70 | return; 71 | } 72 | setLoading(true); 73 | const { pageSize, page } = pageInfo; 74 | 75 | try { 76 | const { data, success, total: dataTotal = 0 } = 77 | (await getData({ 78 | current: page, 79 | pageSize, 80 | })) || {}; 81 | if (success !== false) { 82 | if (isAppend && list) { 83 | setList([...list, ...data]); 84 | } else { 85 | setList(data); 86 | } 87 | // 判断是否可以继续翻页 88 | setPageInfo({ ...pageInfo, total: dataTotal, hasMore: dataTotal > pageSize * page }); 89 | } 90 | if (onLoad) { 91 | onLoad(data); 92 | } 93 | } catch (e) { 94 | // 如果没有传递这个方法的话,需要把错误抛出去,以免吞掉错误 95 | if (onRequestError === undefined) { 96 | throw new Error(e); 97 | } else { 98 | onRequestError(e); 99 | } 100 | } finally { 101 | setLoading(false); 102 | } 103 | }; 104 | 105 | const fetchListDebounce = useDebounceFn(fetchList, [], 200); 106 | 107 | const fetchMore = () => { 108 | // 如果没有更多的就忽略掉 109 | if (pageInfo.hasMore) { 110 | setPageInfo({ ...pageInfo, page: pageInfo.page + 1 }); 111 | } 112 | }; 113 | 114 | /** 115 | * pageIndex 改变的时候自动刷新 116 | */ 117 | useEffect(() => { 118 | const { page, pageSize } = pageInfo; 119 | // 如果上次的页码为空或者两次页码等于是没必要查询的 120 | // 如果 pageSize 发生变化是需要查询的,所以又加了 prePageSize 121 | if ((!prePage || prePage === page) && (!prePageSize || prePageSize === pageSize)) { 122 | return () => undefined; 123 | } 124 | // 如果 list 的长度大于 pageSize 的长度 125 | // 说明是一个假分页 126 | // (pageIndex - 1 || 1) 至少要第一页 127 | // 在第一页大于 10 128 | // 第二页也应该是大于 10 129 | if (page !== undefined && list.length <= pageSize) { 130 | fetchListDebounce.run(); 131 | return () => fetchListDebounce.cancel(); 132 | } 133 | return () => undefined; 134 | }, [pageInfo.page]); 135 | 136 | // pageSize 修改后返回第一页 137 | useEffect(() => { 138 | if (!prePageSize) { 139 | return () => undefined; 140 | } 141 | /** 142 | * 切换页面的时候清空一下数据,不然会造成判断失误。 143 | * 会认为是本地分页而不是服务器分页从而不请求数据 144 | */ 145 | setList([]); 146 | setPageInfo({ ...pageInfo, page: 1 }); 147 | fetchListDebounce.run(); 148 | return () => fetchListDebounce.cancel(); 149 | }, [pageInfo.pageSize]); 150 | 151 | /** 152 | * 重置pageIndex 到 1 153 | */ 154 | const resetPageIndex = () => { 155 | setPageInfo({ ...pageInfo, page: 1 }); 156 | }; 157 | 158 | useEffect(() => { 159 | fetchListDebounce.run(); 160 | return () => { 161 | fetchListDebounce.cancel(); 162 | isMount = false; 163 | }; 164 | }, effects); 165 | 166 | return { 167 | dataSource: list, 168 | loading, 169 | reload: async () => fetchListDebounce.run(), 170 | fetchMore, 171 | total: pageInfo.total, 172 | hasMore: pageInfo.hasMore, 173 | resetPageIndex, 174 | current: pageInfo.page, 175 | reset: () => { 176 | setPageInfo({ 177 | hasMore: false, 178 | page: defaultCurrent || 1, 179 | total: 0, 180 | pageSize: defaultPageSize, 181 | }); 182 | }, 183 | cancel: fetchListDebounce.cancel, 184 | pageSize: pageInfo.pageSize, 185 | setPageInfo: (info) => 186 | setPageInfo({ 187 | ...pageInfo, 188 | ...info, 189 | }), 190 | }; 191 | }; 192 | 193 | export default useFetchData; 194 | -------------------------------------------------------------------------------- /docs/demo/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { PlusOutlined } from '@ant-design/icons'; 3 | import { Button, Drawer, Tag, Space } from 'antd'; 4 | import ProTable, { ProColumns, TableDropdown, ActionType } from '@ant-design/pro-table'; 5 | import request from 'umi-request'; 6 | 7 | interface GithubIssueItem { 8 | url: string; 9 | repository_url: string; 10 | labels_url: string; 11 | comments_url: string; 12 | events_url: string; 13 | html_url: string; 14 | id: number; 15 | node_id: string; 16 | number: number; 17 | title: string; 18 | user: User; 19 | labels: Label[]; 20 | state: string; 21 | locked: boolean; 22 | assignee?: any; 23 | assignees: any[]; 24 | milestone?: any; 25 | comments: number; 26 | created_at: string; 27 | updated_at: string; 28 | closed_at?: any; 29 | author_association: string; 30 | body: string; 31 | } 32 | 33 | interface Label { 34 | id: number; 35 | node_id: string; 36 | url: string; 37 | name: string; 38 | color: string; 39 | default: boolean; 40 | description: string; 41 | } 42 | 43 | interface User { 44 | login: string; 45 | id: number; 46 | node_id: string; 47 | avatar_url: string; 48 | gravatar_id: string; 49 | url: string; 50 | html_url: string; 51 | followers_url: string; 52 | following_url: string; 53 | gists_url: string; 54 | starred_url: string; 55 | subscriptions_url: string; 56 | organizations_url: string; 57 | repos_url: string; 58 | events_url: string; 59 | received_events_url: string; 60 | type: string; 61 | site_admin: boolean; 62 | } 63 | 64 | const columns: ProColumns[] = [ 65 | { 66 | title: '序号', 67 | dataIndex: 'index', 68 | valueType: 'indexBorder', 69 | width: 72, 70 | }, 71 | { 72 | title: '标题', 73 | dataIndex: 'title', 74 | copyable: true, 75 | ellipsis: true, 76 | rules: [ 77 | { 78 | required: true, 79 | message: '此项为必填项', 80 | }, 81 | ], 82 | width: '30%', 83 | hideInSearch: true, 84 | }, 85 | { 86 | title: '状态', 87 | dataIndex: 'state', 88 | initialValue: 'all', 89 | filters: true, 90 | valueEnum: { 91 | all: { text: '全部', status: 'Default' }, 92 | open: { 93 | text: '未解决', 94 | status: 'Error', 95 | }, 96 | closed: { 97 | text: '已解决', 98 | status: 'Success', 99 | }, 100 | processing: { 101 | text: '解决中', 102 | status: 'Processing', 103 | }, 104 | }, 105 | width: '10%', 106 | }, 107 | { 108 | title: '标签', 109 | dataIndex: 'labels', 110 | width: '10%', 111 | render: (_, row) => ( 112 | 113 | {row.labels.map(({ name, id, color }) => ( 114 | 115 | {name} 116 | 117 | ))} 118 | 119 | ), 120 | }, 121 | { 122 | title: '创建时间', 123 | key: 'since', 124 | dataIndex: 'created_at', 125 | valueType: 'dateTime', 126 | width: '20%', 127 | }, 128 | { 129 | title: '操作', 130 | valueType: 'option', 131 | render: (text, row, _, action) => [ 132 | 133 | 链路 134 | , 135 | 136 | 报警 137 | , 138 | 139 | 查看 140 | , 141 | action.reload()} 143 | menus={[ 144 | { key: 'copy', name: '复制' }, 145 | { key: 'delete', name: '删除' }, 146 | ]} 147 | />, 148 | ], 149 | }, 150 | ]; 151 | 152 | export default () => { 153 | const actionRef = useRef(); 154 | const [visible, setVisible] = useState(false); 155 | return ( 156 |
163 | setVisible(false)} visible={visible}> 164 | 176 | 185 | 186 | columns={columns} 187 | type="form" 188 | onSubmit={(params) => console.log(params)} 189 | /> 190 | 191 | 192 | columns={columns} 193 | pagination={{ 194 | showQuickJumper: true, 195 | }} 196 | actionRef={actionRef} 197 | request={async (params = {}) => 198 | request<{ 199 | data: GithubIssueItem[]; 200 | }>('https://proapi.azurewebsites.net/github/issues', { 201 | params, 202 | }) 203 | } 204 | rowKey="id" 205 | dateFormatter="string" 206 | headerTitle="高级表格" 207 | toolBarRender={() => [ 208 | , 212 | ]} 213 | /> 214 |
215 | ); 216 | }; 217 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | order: 9 4 | sidemenu: false 5 | nav: 6 | title: API 7 | order: 2 8 | --- 9 | 10 | # API 11 | 12 | pro-table 在 antd 的 table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 antd table 不同的 api。 13 | 14 | ## Table 15 | 16 | | 属性 | 描述 | 类型 | 默认值 | 17 | | --- | --- | --- | --- | 18 | | request | 一个获得 dataSource 的方法 | `(params?: {pageSize: number;current: number;[key: string]: any;},sort,filter) => Promise>` | - | 19 | | postData | 对通过 url 获取的数据进行一些处理 | `(data: T[]) => T[]` | - | 20 | | defaultData | 默认的数据 | `T[]` | - | 21 | | actionRef | get table action | `React.MutableRefObject \| ((actionRef: ActionType) => void)` | - | 22 | | toolBarRender | 渲染工具栏,支持返回一个 dom 数组,会自动增加 margin-right | `(action: UseFetchDataAction>) => React.ReactNode[]` | - | 23 | | onLoad | 数据加载完成后触发,会多次触发 | `(dataSource: T[]) => void` | - | 24 | | onRequestError | 数据加载失败时触发 | `(e: Error) => void` | - | 25 | | tableClassName | 封装的 table 的 className | string | - | 26 | | tableStyle | 封装的 table 的 style | CSSProperties | - | 27 | | options | table 的工具栏,设置为 false 可以关闭它 | `{{ fullScreen: boolean \| function, reload: boolean \| function,setting: true }}` | `{ fullScreen: true, reload:true, setting: true}` | 28 | | search | 是否显示搜索表单,传入对象时为搜索表单的配置 | [search config](#search) | true | 29 | | dateFormatter | moment 的格式化方式 | `"string" \| "number" \| false` | string | 30 | | beforeSearchSubmit | 搜索之前进行一些修改 | `(params:T)=>T` | - | 31 | | onSizeChange | table 尺寸发生改变 | `(size: 'default' | 'middle' | 'small' | undefined) => void` | - | 32 | | columnsStateMap | columns 的状态枚举 | `{[key: string]: { show:boolean, fixed: "right"|"left"} }` | - | 33 | | onColumnsStateChange | columns 状态发生改变 | `(props: {[key: string]: { show:boolean, fixed: "right"|"left"} }) => void` | - | 34 | | type | pro-table 类型 | `"form"` | - | 35 | | form | antd form 的配置 | `FormProps` | - | 36 | | onSubmit | 提交表单时触发 | `(params: U) => void` | - | 37 | | onReset | 重置表单时触发 | `() => void` | - | 38 | | columnEmptyText | 空值时显示 | `"string" \| false` | false | 39 | 40 | ### search 41 | 42 | | 属性 | 描述 | 类型 | 默认值 | 43 | | --- | --- | --- | --- | 44 | | searchText | 查询按钮的文本 | string | 查询 | 45 | | resetText | 重置按钮的文本 | string | 重置 | 46 | | submitText | 提交按钮的文本 | string | 提交 | 47 | | collapseRender | 收起按钮的 render | `(collapsed: boolean,showCollapseButton?: boolean,) => React.ReactNode` | - | 48 | | collapsed | 是否收起 | boolean | - | 49 | | onCollapse | 收起按钮的事件 | `(collapsed: boolean) => void;` | - | 50 | | optionRender | 操作栏的 render | `(( searchConfig: Omit, props: Omit, ) => React.ReactNode) \| false;` | - | 51 | 52 | ## Columns 53 | 54 | | 属性 | 描述 | 类型 | 默认值 | 55 | | --- | --- | --- | --- | 56 | | title | 与 antd 中基本相同,但是支持通过传入一个方法 | `ReactNode \| ((config: ProColumnType, type: ProTableTypes) => ReactNode)` | - | 57 | | renderText | 类似 table 的 render,但是必须返回 string,如果只是希望转化枚举,可以使用 [valueEnum](#valueEnum) | `(text: any,record: T,index: number,action: UseFetchDataAction>) => string` | - | 58 | | render | 类似 table 的 render,第一个参数变成了 dom,增加了第四个参数 action | `(text: React.ReactNode,record: T,index: number,action: UseFetchDataAction>) => React.ReactNode \| React.ReactNode[]` | - | 59 | | ellipsis | 是否自动缩略 | boolean | - | 60 | | copyable | 是否支持复制 | boolean | - | 61 | | valueEnum | 值的枚举,会自动转化把值当成 key 来取出要显示的内容 | [valueEnum](#valueEnum) | - | 62 | | valueType | 值的类型 | `'money' \| 'option' \| 'date' \| 'dateTime' \| 'time' \| 'text'\| 'index' \| 'indexBorder'` | 'text' | 63 | | hideInSearch | 在查询表单中不展示此项 | boolean | - | 64 | | hideInTable | 在 Table 中不展示此列 | boolean | - | 65 | | hideInForm | 在 Form 模式下 中不展示此列 | boolean | - | 66 | | filters | 表头的筛选菜单项,当值为 true 时,自动使用 valueEnum 生成 | `boolean \| object[]` | false | 67 | | order | 决定在 查询表单中的顺序,越大越在前面 | number | - | 68 | | renderFormItem | 渲染查询表单的输入组件 | `(item,props:{value,onChange}) => React.ReactNode` | - | 69 | | formItemProps | 查询表单的 props,会透传给表单项 | `{ [prop: string]: any }` | - | 70 | 71 | ### ActionType 72 | 73 | 有些时候我们要触发 table 的 reload 等操作,action 可以帮助我们做到这一点。 74 | 75 | ```tsx | pure 76 | interface ActionType { 77 | reload: () => void; 78 | fetchMore: () => void; 79 | reset: () => void; 80 | } 81 | 82 | const ref = useRef(); 83 | 84 | ; 85 | 86 | // 刷新 87 | ref.current.reload(); 88 | 89 | // 加载更多 90 | ref.current.fetchMore(); 91 | 92 | // 重置到默认值 93 | ref.current.reset(); 94 | 95 | // 清空选中项 96 | ref.current.clearSelected(); 97 | ``` 98 | 99 | ## valueType 100 | 101 | 现在支持的值如下 102 | 103 | | 类型 | 描述 | 示例 | 104 | | --- | --- | --- | 105 | | money | 转化值为金额 | ¥10,000.26 | 106 | | date | 日期 | 2019-11-16 | 107 | | dateRange | 日期区间 | 2019-11-16 2019-11-18 | 108 | | dateTime | 日期和时间 | 2019-11-16 12:50:00 | 109 | | dateTimeRange | 日期和时间区间 | 2019-11-16 12:50:00 2019-11-18 12:50:00 | 110 | | time | 时间 | 12:50:00 | 111 | | option | 操作项,会自动增加 marginRight,只支持一个数组,表单中会自动忽略 | `[操作a,操作b]` | 112 | | text | 默认值,不做任何处理 | - | 113 | | textarea | 与 text 相同, form 转化时会转为 textarea 组件 | - | 114 | | index | 序号列 | - | 115 | | indexBorder | 带 border 的序号列 | - | 116 | | progress | 进度条 | - | 117 | | digit | 单纯的数字,form 转化时会转为 inputNumber | - | 118 | 119 | ## valueEnum 120 | 121 | 当前列值的枚举 122 | 123 | ```typescript | pure 124 | interface IValueEnum { 125 | [key: string]: 126 | | React.ReactNode 127 | | { 128 | text: React.ReactNode; 129 | status: 'Success' | 'Error' | 'Processing' | 'Warning' | 'Default'; 130 | }; 131 | } 132 | ``` 133 | 134 | ## 批量操作 135 | 136 | 与 antd 相同,批量操作需要设置 `rowSelection` 来开启,与 antd 不同的是,pro-table 提供了一个 alert 用于承载一些信息。你可以通过 `tableAlertRender` 来对它进行自定义。设置或者返回 false 即可关闭。 137 | 138 | | 属性 | 描述 | 类型 | 默认值 | 139 | | --- | --- | --- | --- | 140 | | tableAlertRender | 渲染 alert,当配置 `rowSelection`打开。 | `(keys:string[],rows:T[]) => React.ReactNode[]` | `已选择 ${selectedRowKeys.length} 项` | 141 | | rowSelection | 表格行是否可选择,[配置项](https://ant.design/components/table-cn/#rowSelection) | object | false | 142 | -------------------------------------------------------------------------------- /src/component/toolBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReloadOutlined, SettingOutlined } from '@ant-design/icons'; 3 | import { Divider, Space, Tooltip, Input } from 'antd'; 4 | import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context'; 5 | import { SearchProps } from 'antd/lib/input'; 6 | 7 | import ColumnSetting from '../columnSetting'; 8 | import { useIntl, IntlType } from '../intlContext'; 9 | import { UseFetchDataAction, RequestData } from '../../useFetchData'; 10 | import './index.less'; 11 | import FullScreenIcon from './FullscreenIcon'; 12 | import DensityIcon from './DensityIcon'; 13 | 14 | export interface OptionConfig { 15 | density?: boolean; 16 | fullScreen?: OptionsType; 17 | reload?: OptionsType; 18 | setting?: boolean; 19 | search?: (SearchProps & { name?: string }) | boolean; 20 | } 21 | 22 | export type OptionsType = 23 | | ((e: React.MouseEvent, action: UseFetchDataAction>) => void) 24 | | boolean; 25 | 26 | export interface ToolBarProps { 27 | headerTitle?: React.ReactNode; 28 | toolBarRender?: ( 29 | action: UseFetchDataAction>, 30 | rows: { 31 | selectedRowKeys?: (string | number)[]; 32 | selectedRows?: T[]; 33 | }, 34 | ) => React.ReactNode[]; 35 | action: UseFetchDataAction>; 36 | options?: OptionConfig | false; 37 | selectedRowKeys?: (string | number)[]; 38 | selectedRows?: T[]; 39 | className?: string; 40 | onSearch?: (keyWords: string) => void; 41 | } 42 | 43 | const getButtonText = ({ 44 | intl, 45 | }: OptionConfig & { 46 | intl: IntlType; 47 | }) => ({ 48 | fullScreen: { 49 | text: intl.getMessage('tableToolBar.fullScreen', '全屏'), 50 | icon: , 51 | }, 52 | reload: { 53 | text: intl.getMessage('tableToolBar.reload', '刷新'), 54 | icon: , 55 | }, 56 | setting: { 57 | text: intl.getMessage('tableToolBar.columnSetting', '列设置'), 58 | icon: , 59 | }, 60 | density: { 61 | text: intl.getMessage('tableToolBar.density', '表格密度'), 62 | icon: , 63 | }, 64 | }); 65 | 66 | /** 67 | * 渲染默认的 工具栏 68 | * @param options 69 | * @param className 70 | */ 71 | const renderDefaultOption = ( 72 | options: ToolBarProps['options'], 73 | className: string, 74 | defaultOptions: OptionConfig & { 75 | intl: IntlType; 76 | }, 77 | ) => 78 | options && 79 | Object.keys(options) 80 | .filter((item) => item) 81 | .map((key) => { 82 | const value = options[key]; 83 | if (!value) { 84 | return null; 85 | } 86 | if (key === 'setting') { 87 | return ; 88 | } 89 | if (key === 'fullScreen') { 90 | return ( 91 | 96 | 97 | 98 | ); 99 | } 100 | const optionItem = getButtonText(defaultOptions)[key]; 101 | if (optionItem) { 102 | return ( 103 | { 107 | if (value && defaultOptions[key] !== true) { 108 | if (value !== true) { 109 | value(); 110 | return; 111 | } 112 | defaultOptions[key](); 113 | } 114 | }} 115 | > 116 | {optionItem.icon} 117 | 118 | ); 119 | } 120 | return null; 121 | }) 122 | .filter((item) => item); 123 | 124 | const ToolBar = ({ 125 | headerTitle, 126 | toolBarRender, 127 | action, 128 | options: propsOptions = { 129 | density: true, 130 | fullScreen: () => action.fullScreen && action.fullScreen(), 131 | reload: () => action.reload(), 132 | setting: true, 133 | search: false, 134 | }, 135 | selectedRowKeys, 136 | selectedRows, 137 | className, 138 | onSearch, 139 | }: ToolBarProps) => { 140 | const options = propsOptions 141 | ? { 142 | density: true, 143 | fullScreen: () => action.fullScreen && action.fullScreen(), 144 | reload: () => action.reload(), 145 | setting: true, 146 | search: false, 147 | ...propsOptions, 148 | } 149 | : false; 150 | const intl = useIntl(); 151 | const optionDom = 152 | renderDefaultOption(options, `${className}-item-icon`, { 153 | fullScreen: () => action.fullScreen && action.fullScreen(), 154 | reload: () => action.reload(), 155 | density: true, 156 | setting: true, 157 | search: false, 158 | intl, 159 | }) || []; 160 | // 操作列表 161 | const actions = toolBarRender ? toolBarRender(action, { selectedRowKeys, selectedRows }) : []; 162 | const renderDivider = () => { 163 | if (optionDom.length < 1) { 164 | return false; 165 | } 166 | if (actions.length < 1 && options && options.search === false) { 167 | return false; 168 | } 169 | return ; 170 | }; 171 | return ( 172 |
173 |
{headerTitle}
174 |
175 | 176 | {options && options.search && ( 177 | 185 | )} 186 | {actions 187 | .filter((item) => item) 188 | .map((node, index) => ( 189 |
193 | {node} 194 |
195 | ))} 196 |
197 |
198 | {renderDivider()} 199 | {optionDom} 200 |
201 |
202 |
203 | ); 204 | }; 205 | 206 | const WarpToolBar = (props: ToolBarProps) => ( 207 | 208 | {({ getPrefixCls }: ConfigConsumerProps) => { 209 | const className = getPrefixCls('pro-table-toolbar'); 210 | return ; 211 | }} 212 | 213 | ); 214 | 215 | export default WarpToolBar; 216 | -------------------------------------------------------------------------------- /src/defaultRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Progress, Avatar } from 'antd'; 3 | import moment from 'moment'; 4 | import Percent from './component/percent'; 5 | import IndexColumn from './component/indexColumn'; 6 | import { getProgressStatus } from './component/util'; 7 | import { ColumnEmptyText } from './Table'; 8 | 9 | /** 10 | * money 金额 11 | * option 操作 需要返回一个数组 12 | * date 日期 YYYY-MM-DD 13 | * dateRange 日期范围 YYYY-MM-DD[] 14 | * dateTime 日期和时间 YYYY-MM-DD HH:mm:ss 15 | * dateTimeRange 范围日期和时间 YYYY-MM-DD HH:mm:ss[] 16 | * time: 时间 HH:mm:ss 17 | * index:序列 18 | * progress: 进度条 19 | * percent: 百分比 20 | */ 21 | export type ProColumnsValueType = 22 | | 'money' 23 | | 'textarea' 24 | | 'option' 25 | | 'date' 26 | | 'dateRange' 27 | | 'dateTimeRange' 28 | | 'dateTime' 29 | | 'time' 30 | | 'text' 31 | | 'index' 32 | | 'indexBorder' 33 | | 'progress' 34 | | 'percent' 35 | | 'digit' 36 | | 'avatar' 37 | | 'code'; 38 | 39 | // function return type 40 | export type ProColumnsValueObjectType = { 41 | type: 'progress' | 'money' | 'percent'; 42 | status?: 'normal' | 'active' | 'success' | 'exception' | undefined; 43 | locale?: string; 44 | /** percent */ 45 | showSymbol?: boolean; 46 | precision?: number; 47 | }; 48 | 49 | /** 50 | * value type by function 51 | */ 52 | export type ProColumnsValueTypeFunction = ( 53 | item: T, 54 | ) => ProColumnsValueType | ProColumnsValueObjectType; 55 | 56 | const moneyIntl = new Intl.NumberFormat('zh-Hans-CN', { 57 | currency: 'CNY', 58 | style: 'currency', 59 | minimumFractionDigits: 2, 60 | }); 61 | 62 | const enMoneyIntl = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }); 63 | const ruMoneyIntl = new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }); 64 | const msMoneyIntl = new Intl.NumberFormat('ms-MY', { style: 'currency', currency: 'MYR' }); 65 | 66 | /** 67 | * render valueType object 68 | * @param text string | number 69 | * @param value ProColumnsValueObjectType 70 | */ 71 | const defaultRenderTextByObject = (text: string | number, value: ProColumnsValueObjectType) => { 72 | if (value.type === 'progress') { 73 | return ( 74 | 79 | ); 80 | } 81 | if (value.type === 'money') { 82 | // english 83 | if (value.locale === 'en_US') { 84 | return enMoneyIntl.format(text as number); 85 | } 86 | // russian 87 | if (value.locale === 'ru_RU') { 88 | return ruMoneyIntl.format(text as number); 89 | } 90 | // malay 91 | if (value.locale === 'ms_MY') { 92 | return msMoneyIntl.format(text as number); 93 | } 94 | return moneyIntl.format(text as number); 95 | } 96 | if (value.type === 'percent') { 97 | return ; 98 | } 99 | return text; 100 | }; 101 | 102 | /** 103 | * 根据不同的类型来转化数值 104 | * @param text 105 | * @param valueType 106 | */ 107 | const defaultRenderText = ( 108 | text: string | number | React.ReactText[], 109 | valueType: ProColumnsValueType | ProColumnsValueTypeFunction, 110 | index: number, 111 | item?: T, 112 | columnEmptyText?: ColumnEmptyText, 113 | ): React.ReactNode => { 114 | // when valueType == function 115 | // item always not null 116 | if (typeof valueType === 'function' && item) { 117 | const value = valueType(item); 118 | if (typeof value === 'string') { 119 | return defaultRenderText(text, value, index); 120 | } 121 | if (typeof value === 'object') { 122 | return defaultRenderTextByObject(text as string, value); 123 | } 124 | } 125 | 126 | /** 127 | * 如果是金额的值 128 | */ 129 | if (valueType === 'money' && (text || text === 0)) { 130 | /** 131 | * 这个 api 支持三星和华为的手机 132 | */ 133 | if (typeof text === 'string') { 134 | return moneyIntl.format(parseFloat(text)); 135 | } 136 | return moneyIntl.format(text as number); 137 | } 138 | 139 | /** 140 | *如果是日期的值 141 | */ 142 | if (valueType === 'date' && text) { 143 | return moment(text).format('YYYY-MM-DD'); 144 | } 145 | 146 | /** 147 | *如果是日期范围的值 148 | */ 149 | if (valueType === 'dateRange' && text && Array.isArray(text) && text.length === 2) { 150 | // 值不存在的时候显示 "-" 151 | const [startText, endText] = text; 152 | return ( 153 |
154 |
{startText ? moment(startText).format('YYYY-MM-DD') : '-'}
155 |
{endText ? moment(endText).format('YYYY-MM-DD') : '-'}
156 |
157 | ); 158 | } 159 | 160 | /** 161 | *如果是日期加时间类型的值 162 | */ 163 | if (valueType === 'dateTime' && text) { 164 | return moment(text).format('YYYY-MM-DD HH:mm:ss'); 165 | } 166 | 167 | /** 168 | *如果是日期加时间类型的值的值 169 | */ 170 | if (valueType === 'dateTimeRange' && text && Array.isArray(text) && text.length === 2) { 171 | // 值不存在的时候显示 "-" 172 | const [startText, endText] = text; 173 | return ( 174 |
175 |
{startText ? moment(startText).format('YYYY-MM-DD HH:mm:ss') : '-'}
176 |
{endText ? moment(endText).format('YYYY-MM-DD HH:mm:ss') : '-'}
177 |
178 | ); 179 | } 180 | 181 | /** 182 | *如果是时间类型的值 183 | */ 184 | if (valueType === 'time' && text) { 185 | return moment(text).format('HH:mm:ss'); 186 | } 187 | 188 | if (valueType === 'index') { 189 | return {index + 1}; 190 | } 191 | 192 | if (valueType === 'indexBorder') { 193 | return {index + 1}; 194 | } 195 | 196 | if (valueType === 'progress') { 197 | return ( 198 | 199 | ); 200 | } 201 | /** 百分比, 默认展示符号, 不展示小数位 */ 202 | if (valueType === 'percent') { 203 | return ; 204 | } 205 | 206 | if (valueType === 'avatar' && typeof text === 'string') { 207 | return ; 208 | } 209 | 210 | if (valueType === 'code' && text) { 211 | return ( 212 |
222 |         {text}
223 |       
224 | ); 225 | } 226 | 227 | if (columnEmptyText) { 228 | if (typeof text !== 'boolean' && typeof text !== 'number' && !text) { 229 | return typeof columnEmptyText === 'string' ? columnEmptyText : '-'; 230 | } 231 | } 232 | 233 | return text; 234 | }; 235 | 236 | export default defaultRenderText; 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 此仓库已废弃 2 | 3 | **重要:** 此仓库后续不再维护,也不再接受更多的特性更新。`ant-design/pro-table` 将会迁移至 `ant-design/pro-components` 仓库进行后续的维护,访问 https://procomponents.ant.design/table 了解更多。此变更不影响继续使用 `@ant-design/pro-table` 这个 npm 包名安装使用此组件。 4 | 5 | 6 | [English](./README.en_US.md) 7 | 8 |

@ant-design/pro-table

9 | 10 |
11 | 12 | 🏆 Use Ant Design Table like a Pro! 13 | 14 |
15 | 16 | ### Demo 17 | 18 | [codesandbox](https://codesandbox.io/embed/dreamy-river-q7v6s?fontsize=14&hidenavigation=1&theme=dark&view=preview) 19 | 20 | ## API 21 | 22 | pro-table 在 antd 的 table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 antd table 不同的 api。 23 | 24 | ## Table 25 | 26 | | 属性 | 描述 | 类型 | 默认值 | 27 | | --- | --- | --- | --- | 28 | | request | 一个获得 dataSource 的方法 | `(params?: {pageSize: number;current: number;[key: string]: any;}) => Promise>` | - | 29 | | postData | 对通过 url 获取的数据进行一些处理 | `(data: T[]) => T[]` | - | 30 | | defaultData | 默认的数据 | `T[]` | - | 31 | | actionRef | get table action | `React.MutableRefObject \| ((actionRef: ActionType) => void)` | - | 32 | | toolBarRender | 渲染工具栏,支持返回一个 dom 数组,会自动增加 margin-right | `(action: UseFetchDataAction>) => React.ReactNode[]` | - | 33 | | onLoad | 数据加载完成后触发,会多次触发 | `(dataSource: T[]) => void` | - | 34 | | onRequestError | 数据加载失败时触发 | `(e: Error) => void` | - | 35 | | tableClassName | 封装的 table 的 className | string | - | 36 | | tableStyle | 封装的 table 的 style | CSSProperties | - | 37 | | headerTitle | 左上角的 title | React.ReactNode | - | 38 | | options | table 的工具栏,设置为 false 可以关闭它 | `{{ fullScreen: boolean \| function, reload: boolean \| function,setting: true }}` | `{ fullScreen: true, reload:true, setting: true}` | 39 | | search | 是否显示搜索表单,传入对象时为搜索表单的配置 | [search config](#search) | true | 40 | | dateFormatter | moment 的格式化方式 | `"string" \| "number" \| false` | string | 41 | | beforeSearchSubmit | 搜索之前进行一些修改 | `(params:T)=>T` | - | 42 | | onSizeChange | table 尺寸发生改变 | `(size: 'default' | 'middle' | 'small' | undefined) => void` | - | 43 | | columnsStateMap | columns 的状态枚举 | `{[key: string]: { show:boolean, fixed: "right"|"left"} }` | - | 44 | | onColumnsStateChange | columns 状态发生改变 | `(props: {[key: string]: { show:boolean, fixed: "right"|"left"} }) => void` | - | 45 | | type | pro-table 类型 | `"form"` | - | 46 | | form | antd form 的配置 | `FormProps` | - | 47 | 48 | ### search 49 | 50 | | 属性 | 描述 | 类型 | 默认值 | 51 | | --- | --- | --- | --- | 52 | | searchText | 查询按钮的文本 | string | 查询 | 53 | | resetText | 重置按钮的文本 | string | 重置 | 54 | | submitText | 提交按钮的文本 | string | 提交 | 55 | | collapseRender | 收起按钮的 render | `(collapsed: boolean,showCollapseButton?: boolean,) => React.ReactNode` | - | 56 | | collapsed | 是否收起 | boolean | - | 57 | | onCollapse | 收起按钮的事件 | `(collapsed: boolean) => void;` | - | 58 | | optionRender | 操作栏的 render | `(( searchConfig: Omit, props: Omit, ) => React.ReactNode) \| false;` | - | 59 | 60 | ## Columns 61 | 62 | | 属性 | 描述 | 类型 | 默认值 | 63 | | --- | --- | --- | --- | 64 | | renderText | 类似 table 的 render,但是必须返回 string,如果只是希望转化枚举,可以使用 [valueEnum](#valueEnum) | `(text: any,record: T,index: number,action: UseFetchDataAction>) => string` | - | 65 | | render | 类似 table 的 render,第一个参数变成了 dom,增加了第四个参数 action | `(text: React.ReactNode,record: T,index: number,action: UseFetchDataAction>) => React.ReactNode \| React.ReactNode[]` | - | 66 | | renderFormItem | 渲染查询表单的输入组件 | `(item,props:{value,onChange}) => React.ReactNode` | - | 67 | | ellipsis | 是否自动缩略 | boolean | - | 68 | | copyable | 是否支持复制 | boolean | - | 69 | | valueEnum | 值的枚举,会自动转化把值当成 key 来取出要显示的内容 | [valueEnum](#valueEnum) | - | 70 | | valueType | 值的类型 | `'money' \| 'option' \| 'date' \| 'dateTime' \| 'time' \| 'text'\| 'index' \| 'indexBorder'` | 'text' | 71 | | hideInSearch | 在查询表单中不展示此项 | boolean | - | 72 | | hideInTable | 在 Table 中不展示此列 | boolean | - | 73 | | hideInForm | 在 Form 模式下 中不展示此列 | boolean | - | 74 | | filters | 表头的筛选菜单项,当值为 true 时,自动使用 valueEnum 生成 | `boolean \| object[]` | false | 75 | | order | 决定在 查询表单中的顺序,越大越在前面 | number | - | 76 | | formItemProps | 查询表单的 props,会透传给表单项 | `{ [prop: string]: any }` | - | 77 | 78 | ### ActionType 79 | 80 | 有些时候我们要触发 table 的 reload 等操作,action 可以帮助我们做到这一点。 81 | 82 | ```tsx | pure 83 | interface ActionType { 84 | reload: () => void; 85 | fetchMore: () => void; 86 | reset: () => void; 87 | reloadAndRest: () => void; 88 | } 89 | 90 | const ref = useRef(); 91 | 92 | ; 93 | 94 | // 刷新 95 | ref.current.reload(); 96 | 97 | // 重置所有项并刷新 98 | ref.current.reloadAndRest(); 99 | 100 | // 加载更多 101 | ref.current.fetchMore(); 102 | 103 | // 重置到默认值 104 | ref.current.reset(); 105 | 106 | // 清空选中项 107 | ref.current.clearSelected(); 108 | ``` 109 | 110 | ## valueType 111 | 112 | 现在支持的值如下 113 | 114 | | 类型 | 描述 | 示例 | 115 | | --- | --- | --- | 116 | | money | 转化值为金额 | ¥10,000.26 | 117 | | date | 日期 | 2019-11-16 | 118 | | dateRange | 日期区间 | 2019-11-16 2019-11-18 | 119 | | dateTime | 日期和时间 | 2019-11-16 12:50:00 | 120 | | dateTimeRange | 日期和时间区间 | 2019-11-16 12:50:00 2019-11-18 12:50:00 | 121 | | time | 时间 | 12:50:00 | 122 | | option | 操作项,会自动增加 marginRight,只支持一个数组,表单中会自动忽略 | `[操作a,操作b]` | 123 | | text | 默认值,不做任何处理 | - | 124 | | textarea | 与 text 相同, form 转化时会转为 textarea 组件 | - | 125 | | index | 序号列 | - | 126 | | indexBorder | 带 border 的序号列 | - | 127 | | progress | 进度条 | - | 128 | | digit | 单纯的数字,form 转化时会转为 inputNumber | - | 129 | 130 | ## valueEnum 131 | 132 | 当前列值的枚举 133 | 134 | ```typescript | pure 135 | interface IValueEnum { 136 | [key: string]: 137 | | React.ReactNode 138 | | { 139 | text: React.ReactNode; 140 | status: 'Success' | 'Error' | 'Processing' | 'Warning' | 'Default'; 141 | }; 142 | } 143 | ``` 144 | 145 | ## Usage 146 | 147 | ```bash 148 | npm install @ant-design/pro-table 149 | # or 150 | yarn add @ant-design/pro-table 151 | ``` 152 | 153 | ```tsx 154 | import React, { useState } from 'react'; 155 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 156 | import { Input, Button } from 'antd'; 157 | 158 | const columns: ProColumns[] = [ 159 | { 160 | title: 'Name', 161 | dataIndex: 'name', 162 | copyable: true, 163 | }, 164 | { 165 | title: 'Age', 166 | dataIndex: 'age', 167 | }, 168 | { 169 | title: 'date', 170 | dataIndex: 'date', 171 | valueType: 'date', 172 | }, 173 | { 174 | title: 'option', 175 | valueType: 'option', 176 | dataIndex: 'id', 177 | render: (text, row, index, action) => [ 178 | { 180 | window.alert('确认删除?'); 181 | action.reload(); 182 | }} 183 | > 184 | delete 185 | , 186 | { 188 | window.alert('确认刷新?'); 189 | action.reload(); 190 | }} 191 | > 192 | reload 193 | , 194 | ], 195 | }, 196 | ]; 197 | 198 | export default () => { 199 | const [keywords, setKeywords] = useState(''); 200 | return ( 201 | 202 | size="small" 203 | columns={columns} 204 | request={() => ({ 205 | data: [ 206 | { 207 | name: 'Jack', 208 | age: 12, 209 | date: '2020-01-02', 210 | }, 211 | ], 212 | success: true, 213 | })} 214 | rowKey="name" 215 | params={{ keywords }} 216 | toolBarRender={(action) => [ 217 | setKeywords(value)} 222 | />, 223 | ]} 224 | pagination={{ 225 | defaultCurrent: 10, 226 | }} 227 | /> 228 | ); 229 | }; 230 | ``` 231 | 232 | ## LICENSE 233 | 234 | MIT 235 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | order: 9 4 | sidemenu: false 5 | nav: 6 | title: 快速开始 7 | order: 0 8 | --- 9 | 10 | ProTable 的诞生是为了解决项目中需要写很多 table 的样板代码的问题,所以在其中做了封装了很多常用的逻辑。这些封装可以简单的分类为预设行为与预设逻辑。 11 | 12 | 在 react 的中写一个 table 免不了需要定义一些 state,比如 page,pageNumber,pageSize。如果使用 dva 等数据流方案可能还需要写很多样板代码来请求数据。但是很多时候这些行为是高度雷同的,所以 ProTable 默认封装了请求网络,翻页,搜索和筛选的逻辑。 13 | 14 | ## 请求数据 15 | 16 | request 中封装了请求网络的行为,ProTable 会将 props.params 中的数据默认带入到请求中,如果接口恰好与我们的定义相同,实现一个查询会非常简单。 17 | 18 | ```tsx | pure 19 | import request from 'umi-request'; 20 | 21 | const fetchData = (params, sort, filter) => 22 | request<{ 23 | data: T[]; 24 | }>('https://proapi.azurewebsites.net/github/issues', { 25 | params, 26 | sort, 27 | filter, 28 | }); 29 | 30 | const keyWords = "Ant Design" 31 | 32 | params={{ keyWords }} request={fetchData} />; 33 | ``` 34 | 35 | 我们约定 request 拥有三个参数,第一个 `params` 会自带 `pageSize` 和 `current`,并且将 props 中的 `params` 也会带入其中,第二个参数 `sort` 用与排序,第三个参数 `filter` 用于多选。他们的类型分别如下: 36 | 37 | ```tsx | pure 38 | ( 39 | params: U & { 40 | pageSize?: number; 41 | current?: number; 42 | }, 43 | sort: { 44 | [key: string]: 'ascend' | 'descend'; 45 | }, 46 | filter: { [key: string]: React.ReactText[] }, 47 | ) => RequestData; 48 | ``` 49 | 50 | > ProTable 会将第二个泛型认为是 `params` 的类型,保证各个环节都要完善的类型支持。 51 | 52 | 对与请求回来的结果的 ProTable 也有一些约定,类型如下: 53 | 54 | ```tsx | pure 55 | interface RequestData { 56 | data: Datum[]; 57 | success: boolean; 58 | total: number; 59 | } 60 | ``` 61 | 62 | 如果我们恰巧属性不同,也是可以做自定义的。request 只要是一个 `Promise` 接口,同样是上面的代码,我们可以自定义参数和返回值。看起来就像这样: 63 | 64 | ```tsx | pure 65 | const fetchData =async (params, sort, filter) =>{ 66 | const msg =await request<{ 67 | data: T[]; 68 | }>('https://proapi.azurewebsites.net/github/issues', { 69 | params:{ 70 | pageNum:params.current, 71 | size:params.pageSize 72 | }, 73 | sort, 74 | filter, 75 | }); 76 | return { 77 | data:msg.list, 78 | total:msg.sum, 79 | success:!msg.errorCode 80 | } 81 | } 82 | 83 | const keyWords = "Ant Design" 84 | 85 | params={{ keyWords }} request={fetchData} />; 86 | ``` 87 | 88 | ## 列配置 89 | 90 | 列配置复杂把数据映射成为具体的 dom, ProTable 在 antd 的基础上进行了一些封装,支持了一些默认的行为作为 render 的语法糖,我们可以在列中配置 valueType 配置一个字符串。现在支持的值如下: 91 | 92 | > 如果你的值的不是下面的类型,可以用 renderText 来进行修改,render 会覆盖掉 valueType。 93 | 94 | | 类型 | 描述 | 示例 | 95 | | --- | --- | --- | 96 | | money | 转化值为金额 | ¥10,000.26 | 97 | | date | 日期 | 2019-11-16 | 98 | | dateRange | 日期区间 | 2019-11-16 2019-11-18 | 99 | | dateTime | 日期和时间 | 2019-11-16 12:50:00 | 100 | | dateTimeRange | 日期和时间区间 | 2019-11-16 12:50:00 2019-11-18 12:50:00 | 101 | | time | 时间 | 12:50:00 | 102 | | option | 操作项,会自动增加 marginRight,只支持一个数组,表单中会自动忽略 | `[操作a,操作b]` | 103 | | text | 默认值,不做任何处理 | - | 104 | | textarea | 与 text 相同, form 转化时会转为 textarea 组件 | - | 105 | | index | 序号列 | - | 106 | | indexBorder | 带 border 的序号列 | - | 107 | | progress | 进度条 | - | 108 | | digit | 单纯的数字,form 转化时会转为 inputNumber | - | 109 | 110 | valueType 还会影响查询表单的生成,不同的 valueType 对应不同的 antd 组件,对应关系如下: 111 | 112 | | 类型 | 对应的组件 | 113 | | --- | --- | 114 | | text | [Input](https://ant.design/components/input-cn/) | 115 | | textarea | [Input.TextArea](https://ant.design/components/input-cn/#components-input-demo-textarea) | 116 | | date | [DatePicker](https://ant.design/components/date-picker-cn/) | 117 | | dateTime | [DatePicker](https://ant.design/components/date-picker-cn/#components-date-picker-demo-time) | 118 | | time | [TimePicker](https://ant.design/components/time-picker-cn/) | 119 | | dateTimeRange | [RangePicker](https://ant.design/components/time-picker-cn/#components-time-picker-demo-range-picker) | 120 | | dateRange | [RangePicker](https://ant.design/components/time-picker-cn/#components-time-picker-demo-range-picker) | 121 | | money | [InputNumber](https://ant.design/components/input-number-cn/) | 122 | | digit | [InputNumber](https://ant.design/components/input-number-cn/) | 123 | | option | 不展示 | 124 | | index | 不展示 | 125 | | progress | 不展示 | 126 | 127 | `valueType` 虽然解决了部分问题,但是枚举的情况他无法满足,所以 ProTable 还支持了 `valueEnum` 来支持枚举类型的数据。`valueEnum`是一个`Object`或者`Map`,如果你用数字当 key,或者对顺序有要求建议使用的`Map`。数据结构如下: 128 | 129 | ```tsx | pure 130 | const valueEnum = { 131 | open: '未解决', 132 | closed: { 133 | text: '已解决', 134 | status: 'Success', 135 | }, 136 | }; 137 | ``` 138 | 139 | 配合为 `valueEnum` 的字段会被展示为下拉框。 140 | 141 | ## ActionRef 142 | 143 | 在进行了操作,或者 tab 切换等时候我们需要手动触发一下表单的更新,纯粹的 props 很难解决这个问题,所以我们提供一个 ref 来支持一些默认的操作。 144 | 145 | ```tsx | pure 146 | const ref = useRef(); 147 | 148 | // 两秒刷新一次表格 149 | useEffect(() => { 150 | setInterval(() => { 151 | ref.current.reload(); 152 | }, 2000); 153 | }, []); 154 | 155 | // hooks 绑定 156 | ; 157 | 158 | // class 159 | (this.ref = ref)} />; 160 | ``` 161 | 162 | `ActionRef` 还支持了一些别的行为,某些时候会减少的你的编码成本,但是 ref 会脱离 react 的生命周期,所以这些 action 都是不受控的。 163 | 164 | ```tsx | pure 165 | // 刷新 166 | ref.current.reload(); 167 | 168 | // 重置所有项并刷新 169 | ref.current.reloadAndRest(); 170 | 171 | // 重置到默认值 172 | ref.current.reset(); 173 | 174 | // 清空选中项 175 | ref.current.clearSelected(); 176 | ``` 177 | 178 | ## 查询表单 179 | 180 | 查询表单是 ProTable 的默认行为中最为复杂的一个,我们为其提供了部分配置和预设。如果你的查询表单非常复杂,或者其中使用了一些业务逻辑,建议使用 antd 的进行排版,并把数据通过 params 交给 ProTable,默认的查询表单是高度标准化的。 181 | 182 | ![tableDemo](https://gw.alipayobjects.com/zos/antfincdn/P7jDHJ323a/4febb542-739c-49b7-8bb9-6a5fc2ca631c.png) 183 | 184 | ### 控制展示 185 | 186 | 很多时候查询表单是有一些配置的,默认的逻辑不能满足需求,我们支持通过 `formItemProps` 来进行一些简单的配置。比如 `placeholder` 或者增加一个 `addonAfter` 的。 187 | 188 | ```tsx | pure 189 | { 190 | formItemProps: { 191 | placeholder:"请输入表格名", 192 | addonAfter: ; 193 | } 194 | } 195 | ``` 196 | 197 | > value 和 onChange 有特殊的含义,用于表单绑定,所以不能覆盖。 198 | 199 | 有些时候表单中和 table 中的 title 也是不同的,我们支持配置 title 为 `function` 来支持根据情况显示不同 title。 200 | 201 | ```tsx | pure 202 | title: (_, type) => (type === 'table' ? '状态' : '列表状态'), 203 | ``` 204 | 205 | 我们可以在 props 中设置 form 配置来自定义表单的操作,比如说默认值。 206 | 207 | ```tsx | pure 208 | form={{ initialValues: {...data}, labelCol: { span: 6 }, }} 209 | ``` 210 | 211 | ### 自定义表单项 212 | 213 | 很多时候内置的表单项无法满足我们的需求,这时候我们就需要来自定义一下默认的组件,`renderFormItem` 可以完成重写渲染逻辑,它会传入 item 和 props 来进行渲染,需要注意的是我们必须要将 props 中的 `value` 和 `onChange` 必须要被赋值,否则 form 无法绑定数据。 214 | 215 | 为了做表单的联动 `renderFormItem` 增加了第三个参数,可以用 name 获得别的表单项数据并且做一些定制。 216 | 217 | ```tsx | pure 218 | renderFormItem: (_, { type, defaultRender, ...rest }, form) => { 219 | if (type === 'form') { 220 | return null; 221 | } 222 | const status = form.getFieldValue('state'); 223 | if (status !== 'open') { 224 | return ; 225 | } 226 | return defaultRender(_); 227 | }; 228 | ``` 229 | 230 | > renderFormItem 的性能不是很好,使用时要注意不要再其中做耗费时间较长的事情。 231 | 232 | ## 操作栏 233 | 234 | 操作栏可以承载一些常用的操作或者表格的标题,为了不与 antd 的 Table 的属性冲突,我们使用了 `headerTitle` 来定义了操作栏的标题,操作栏的标题是一个 ReactNode 你可以自定义它,如果需要可以放入一个 Tabs。 235 | 236 | `toolBarRender` 支持返回一个 ReactNode 的数组,我们会自动加入间距,toolBarRender 类型定义如下: 237 | 238 | ```tsx |pure 239 | toolBarRender: (action, { selectedRowKeys, selectedRows }) => ReactNode[]; 240 | ``` 241 | 242 | 默认会返回当前选中的所有行和他们的 keys,用于批量操作。 243 | 244 | 操作栏还自定义了一些默认的行为,默认支持了 `density` 密度调整, `fullScreen` 全屏,`reload` 刷新,`setting` table 设置。 245 | 246 | ```tsx | pure 247 | export interface OptionConfig { 248 | density: boolean; 249 | fullScreen: OptionsType; 250 | reload: OptionsType; 251 | setting: boolean; 252 | } 253 | ``` 254 | 255 | 我们可以在 props 中配置 options={false} 来关掉操作栏。也可以分别设置,只保留你想要的。 256 | 257 | ```tsx | pure 258 | options = { 259 | fullScreen: false, 260 | reload: false, 261 | setting: false, 262 | density: true, 263 | }; 264 | ``` 265 | 266 | 更多的功能查看查看具体的说明: 267 | 268 | - [API](/api) 269 | - [国际化](/intl) 270 | - [查询表单](/search) 271 | - [预设样式](/value-type) 272 | - [例子](/example) 273 | -------------------------------------------------------------------------------- /README.en_US.md: -------------------------------------------------------------------------------- 1 | # Deperecated Repository 2 | 3 | **important:** This repository is no longer maintained and will not receive any future updates. `ant-design/pro-table` will migrate to `ant-design/pro-components` repository,visit https://procomponent.ant.design/table to know more. This will not affect your continued use of the `@ant-design/pro-table` package. 4 | 5 | [中文](./README.md) 6 | 7 |

@ant-design/pro-table

8 | 9 |
10 | 11 | 🏆 Use Ant Design Table like a Pro! 12 | 13 |
14 | 15 | ### Demo 16 | 17 | [codesandbox](https://codesandbox.io/embed/dreamy-river-q7v6s?fontsize=14&hidenavigation=1&theme=dark&view=preview) 18 | 19 | ## API 20 | 21 | pro-table is encapsulated in an antd table, supports some presets, and encapsulates some behavior. Only apis different from antd table are listed here. 22 | 23 | ### Table 24 | 25 | | Property | Description | Type | Default Value | 26 | | --- | --- | --- | --- | 27 | | request | a method to get the dataSource. | `(params?: {pageSize: number;current: number;[key: string]: any;}) => Promise>` | - | 28 | | postData | Do some processing on the data obtained through the url. | `(data: T[]) => T[]` | - | 29 | | defaultData | Default data array. | `T[]` | - | 30 | | actionRef | Triggered after the table data is successfully initialized, it will be triggered multiple times. | `React.MutableRefObject \| ((actionRef: ActionType) => void)` | [] | 31 | | toolBarRender | Render toolbar, support for returning a dom array, will automatically increase margin-right. | `(action: UseFetchDataAction>) => React.ReactNode[]` | - | 32 | | onLoad | Triggered after the data is loaded, it will be triggered multiple times. | `(dataSource: T[]) => void` | - | 33 | | onRequestError | Triggered when fetching data failed | `(e: Error) => void` | - | 34 | | tableClassName | The className of the packaged table | string | - | 35 | | tableStyle | The style of the packaged table | CSSProperties | - | 36 | | headerTitle | The title on left-top | React.ReactNode | - | 37 | | options | table's default operation, set to false to close it | `{{ fullScreen: boolean \| function, reload: boolean \| function,setting: true }}` | `{{ fullScreen: true, reload:true,setting: true }}` | 38 | | search | Whether to search the form. It can also be a query form config when passing an object. | `boolean \| { span?: number \| DefaultColConfig,searchText?: string, resetText?: string, collapseRender?: (collapsed: boolean) => React.ReactNode, collapsed:boolean, onCollapse: (collapsed:boolean)=> void }` | true | 39 | | dateFormatter | formatting moment type | `"string" \| "number" \| false` | string | 40 | | beforeSearchSubmit | Make some changes before searching | `(params:T)=>T` | - | 41 | | onSizeChange | table size changes | `(size: 'default' | 'middle' | 'small' | undefined) => void` | - | 42 | | columnsStateMap | columns status | `{[key: string]: { show:boolean, fixed: "right"|"left"} }` | - | 43 | | onColumnsStateChange | columns status changed | `(props: {[key: string]: { show:boolean, fixed: "right"|"left"} }) => void` | - | 44 | | form | search From config type="form" and search form's Form config ,the config data like antd Form | `Omit` | - | 45 | 46 | ### Columns 47 | 48 | | Property | Description | Type | Default Value | 49 | | --- | --- | --- | --- | 50 | | renderText | Similar to table render, but must return string, if you just want to convert the enumeration, you can use this scheme | `(text: any,record: T,index: number,action: UseFetchDataAction>) => string` | - | 51 | | render | Like table's render, the first argument becomes dom, and the fourth argument is added. | `(text: React.ReactNode,record: T,index: number,action: UseFetchDataAction>) => React.ReactNode \| React.ReactNode[]` | - | 52 | | renderFormItem | input component for rendering query form | `(item, props: {value, onChange}) => React.ReactNode` | - | 53 | | ellipsis | Whether to automatically abbreviate | boolean | - | 54 | | copyable | Whether to support replication | boolean | - | 55 | | valueEnum | The enumeration of values will automatically convert the value as a key to get the content to be displayed | {[key: string]: React.ReactNode} | - | 56 | | valueType | Type of value | `'money' \| 'option' \| 'date' \| 'dateTime' \| 'time' \| 'text'\| 'index' \| 'indexBorder'` | 'text' | 57 | | hideInSearch | Do not show this in the query form | boolean | - | 58 | | filters | Filter menu config,when the value is true, it is generated automatically using valueEnum | `boolean \| object[]` | false | 59 | | hideInTable | Do not show this column in Table | boolean | - | 60 | | formItemProps | Props passed into query form item | `{ [prop: string]: any }` | - | 61 | 62 | ### ActionType 63 | 64 | Sometimes we need to trigger the reload of the table and other actions, and actions can help us do this. 65 | 66 | ```tsx | pure 67 | interface ActionType { 68 | reload: () => void; 69 | fetchMore: () => void; 70 | reset: () => void; 71 | } 72 | 73 | const ref = useRef(); 74 | 75 | ; 76 | 77 | // Load more 78 | ref.current.reload(); 79 | 80 | // 加载更多 81 | ref.current.fetchMore(); 82 | 83 | // Reset to reset value 84 | ref.current.reset(); 85 | 86 | // clear selected 87 | ref.current.clearSelected(); 88 | ``` 89 | 90 | ### valueType 91 | 92 | | Type | Description | Examples | 93 | | --- | --- | --- | 94 | | money | Conversion value is amount | ¥ 10,000.26 | 95 | | date | Date | 2019-11-16 | 96 | | dateTime | Date and Time | 2019-11-16 12:50:00 | 97 | | time | time | 12:50:00 | 98 | | option | The operation item will automatically increase the marginRight. Only one array is supported, and it will be automatically ignored in the form. | 99 | | text | Default value, no processing | - | 100 | | textarea | Same as text, form will be converted to textarea component when converting | - | 101 | | index | serial number | - | 102 | | indexBorder | ordinal column with border | - | 103 | | progress | progress bar | - | 104 | | digit | Simple digit, which will be converted to inputNumber when form is converted | - | 105 | | progress | progress bar | - | 106 | 107 | ### valueEnums 108 | 109 | ```typescript | pure 110 | interface IValueEnum { 111 | [key: string]: 112 | | React.ReactNode 113 | | { 114 | text: React.ReactNode; 115 | status: 'Success' | 'Error' | 'Processing' | 'Warning' | 'Default'; 116 | }; 117 | } 118 | ``` 119 | 120 | ## Usage 121 | 122 | ```bash 123 | npm install @ant-design/pro-table 124 | # or 125 | yarn add @ant-design/pro-table 126 | ``` 127 | 128 | ```tsx 129 | import React, { useState } from 'react'; 130 | import ProTable, { ProColumns } from '@ant-design/pro-table'; 131 | import { Input, Button } from 'antd'; 132 | 133 | const columns: ProColumns[] = [ 134 | { 135 | title: 'Name', 136 | dataIndex: 'name', 137 | copyable: true, 138 | }, 139 | { 140 | title: 'Age', 141 | dataIndex: 'age', 142 | }, 143 | { 144 | title: 'date', 145 | dataIndex: 'date', 146 | valueType: 'date', 147 | }, 148 | { 149 | title: 'option', 150 | valueType: 'option', 151 | dataIndex: 'id', 152 | render: (text, row, index, action) => [ 153 | { 155 | window.alert('确认删除?'); 156 | action.reload(); 157 | }} 158 | > 159 | delete 160 | , 161 | { 163 | window.alert('确认刷新?'); 164 | action.reload(); 165 | }} 166 | > 167 | reload 168 | , 169 | ], 170 | }, 171 | ]; 172 | 173 | export default () => { 174 | const [keywords, setKeywords] = useState(''); 175 | return ( 176 | 177 | size="small" 178 | columns={columns} 179 | request={() => ({ 180 | data: [ 181 | { 182 | name: 'Jack', 183 | age: 12, 184 | date: '2020-01-02', 185 | }, 186 | ], 187 | success: true, 188 | })} 189 | rowKey="name" 190 | params={{ keywords }} 191 | toolBarRender={(action) => [ 192 | setKeywords(value)} 197 | />, 198 | ]} 199 | pagination={{ 200 | defaultCurrent: 10, 201 | }} 202 | /> 203 | ); 204 | }; 205 | ``` 206 | 207 | ## LICENSE 208 | 209 | MIT 210 | -------------------------------------------------------------------------------- /src/component/util.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React, { ReactNode, useEffect, useRef, ReactText, DependencyList, useCallback } from 'react'; 3 | import isEqual from 'lodash.isequal'; 4 | import { DataIndex } from 'rc-table/lib/interface'; 5 | import TableStatus, { Color, StatusType } from './status'; 6 | import { ValueEnumObj, ValueEnumMap } from '../Table'; 7 | 8 | /** 9 | * 转化 text 和 valueEnum 10 | * 通过 type 来添加 Status 11 | * @param text 12 | * @param valueEnum 13 | * @param prue 纯净模式,不增加 status 14 | */ 15 | export const parsingText = (text: string | number, valueEnum?: ValueEnumMap, pure?: boolean) => { 16 | if (!valueEnum) { 17 | return text; 18 | } 19 | 20 | if (!valueEnum.has(text) && !valueEnum.has(`${text}`)) { 21 | return text; 22 | } 23 | 24 | const domText = (valueEnum.get(text) || valueEnum.get(`${text}`)) as { 25 | text: ReactNode; 26 | status: StatusType; 27 | }; 28 | if (domText.status) { 29 | if (pure) { 30 | return domText.text; 31 | } 32 | const { status } = domText; 33 | const Status = TableStatus[status.toLowerCase() || 'Init']; 34 | if (Status) return {domText.text}; 35 | return {domText.text}; 36 | } 37 | return domText.text || domText; 38 | }; 39 | 40 | /** 41 | * 把 value 的枚举转化为数组 42 | * @param valueEnum 43 | */ 44 | export const parsingValueEnumToArray = ( 45 | valueEnum: ValueEnumMap | undefined = new Map(), 46 | ): { 47 | value: string | number; 48 | text: string; 49 | }[] => { 50 | const enumArray: { 51 | value: string | number; 52 | text: string; 53 | }[] = []; 54 | valueEnum.forEach((_, key) => { 55 | if (!valueEnum.has(key) && !valueEnum.has(`${key}`)) { 56 | return; 57 | } 58 | const value = (valueEnum.get(key) || valueEnum.get(`${key}`)) as { 59 | text: string; 60 | }; 61 | if (!value) { 62 | return; 63 | } 64 | 65 | if (typeof value === 'object' && value?.text) { 66 | enumArray.push({ 67 | text: (value?.text as unknown) as string, 68 | value: key, 69 | }); 70 | return; 71 | } 72 | enumArray.push({ 73 | text: ((value || '') as unknown) as string, 74 | value: key, 75 | }); 76 | }); 77 | return enumArray; 78 | }; 79 | 80 | /** 81 | * 检查值是否存在 82 | * 为了 避开 0 和 false 83 | * @param value 84 | */ 85 | export const checkUndefinedOrNull = (value: any) => value !== undefined && value !== null; 86 | 87 | function deepCompareEquals(a: any, b: any) { 88 | return isEqual(a, b); 89 | } 90 | 91 | function useDeepCompareMemoize(value: any) { 92 | const ref = useRef(); 93 | // it can be done by using useMemo as well 94 | // but useRef is rather cleaner and easier 95 | if (!deepCompareEquals(value, ref.current)) { 96 | ref.current = value; 97 | } 98 | 99 | return ref.current; 100 | } 101 | 102 | export function useDeepCompareEffect(effect: React.EffectCallback, dependencies?: Object) { 103 | useEffect(effect, useDeepCompareMemoize(dependencies)); 104 | } 105 | 106 | export function getProgressStatus(text: number): 'success' | 'exception' | 'normal' | 'active' { 107 | if (typeof text !== 'number') { 108 | return 'exception'; 109 | } 110 | if (text === 100) { 111 | return 'success'; 112 | } 113 | if (text < 100) { 114 | return 'active'; 115 | } 116 | 117 | // magic 118 | if (text < 0) { 119 | return 'exception'; 120 | } 121 | return 'normal'; 122 | } 123 | 124 | /** 125 | * 根据 key 和 dataIndex 生成唯一 id 126 | * @param key 用户设置的 key 127 | * @param dataIndex 在对象中的数据 128 | * @param index 序列号,理论上唯一 129 | */ 130 | export const genColumnKey = ( 131 | key?: React.ReactText | undefined, 132 | dataIndex?: DataIndex, 133 | index?: number, 134 | ) => { 135 | if (key) { 136 | return key; 137 | } 138 | if (!key && dataIndex) { 139 | if (Array.isArray(dataIndex)) { 140 | return dataIndex.join('-'); 141 | } 142 | return dataIndex; 143 | } 144 | return `${index}`; 145 | }; 146 | 147 | export default function get(entity: any, path: ReactText | ReactText[]) { 148 | let tempPath: ReactText[] = ['']; 149 | if (typeof path === 'string') { 150 | if (path.includes('.')) { 151 | tempPath = path.split('.'); 152 | } else { 153 | tempPath = [path]; 154 | } 155 | } 156 | if (Array.isArray(path)) { 157 | tempPath = path; 158 | } 159 | let current = entity; 160 | 161 | for (let i = 0; i < tempPath.length; i += 1) { 162 | if (current === null || current === undefined) { 163 | return undefined; 164 | } 165 | 166 | current = current[tempPath[i]]; 167 | } 168 | 169 | return current; 170 | } 171 | 172 | export const usePrevious = (state: T): T | undefined => { 173 | const ref = useRef(); 174 | 175 | useEffect(() => { 176 | ref.current = state; 177 | }); 178 | 179 | return ref.current; 180 | }; 181 | export interface ReturnValue { 182 | run: (...args: T) => void; 183 | cancel: () => void; 184 | } 185 | const useUpdateEffect: typeof useEffect = (effect, deps) => { 186 | const isMounted = useRef(false); 187 | 188 | useEffect(() => { 189 | if (!isMounted.current) { 190 | isMounted.current = true; 191 | } else { 192 | return effect(); 193 | } 194 | return () => undefined; 195 | }, deps); 196 | }; 197 | 198 | export function useDebounceFn( 199 | fn: (...args: T) => any, 200 | deps: DependencyList | number, 201 | wait?: number, 202 | ): ReturnValue { 203 | // eslint-disable-next-line no-underscore-dangle 204 | const _deps: DependencyList = (Array.isArray(deps) ? deps : []) as DependencyList; 205 | // eslint-disable-next-line no-underscore-dangle 206 | const _wait: number = typeof deps === 'number' ? deps : wait || 0; 207 | const timer = useRef(); 208 | 209 | const fnRef = useRef(fn); 210 | fnRef.current = fn; 211 | 212 | const cancel = useCallback(() => { 213 | if (timer.current) { 214 | clearTimeout(timer.current); 215 | } 216 | }, []); 217 | 218 | const run = useCallback( 219 | (...args: any) => { 220 | cancel(); 221 | timer.current = setTimeout(() => { 222 | fnRef.current(...args); 223 | }, _wait); 224 | }, 225 | [_wait, cancel], 226 | ); 227 | 228 | useUpdateEffect(() => { 229 | run(); 230 | return cancel; 231 | }, [..._deps, run]); 232 | 233 | useEffect(() => cancel, []); 234 | 235 | return { 236 | run, 237 | cancel, 238 | }; 239 | } 240 | 241 | export const getLang = (): string => { 242 | const isNavigatorLanguageValid = 243 | typeof navigator !== 'undefined' && typeof navigator.language === 'string'; 244 | const browserLang = isNavigatorLanguageValid 245 | ? navigator.language.split('-').join('{{BaseSeparator}}') 246 | : ''; 247 | const lang = typeof localStorage !== 'undefined' ? window.localStorage.getItem('umi_locale') : ''; 248 | return lang || browserLang || ''; 249 | }; 250 | 251 | /** 252 | * 删除对象中所有的空值 253 | * @param obj 254 | */ 255 | export const removeObjectNull = (obj: { [key: string]: any }) => { 256 | const newObj = {}; 257 | Object.keys(obj).forEach((key) => { 258 | if (obj[key]) { 259 | newObj[key] = obj[key]; 260 | } 261 | }); 262 | return newObj; 263 | }; 264 | 265 | /** 266 | * 获取类型的 type 267 | * @param obj 268 | */ 269 | function getType(obj: any) { 270 | // @ts-ignore 271 | const type = Object.prototype.toString 272 | .call(obj) 273 | .match(/^\[object (.*)\]$/)[1] 274 | .toLowerCase(); 275 | if (type === 'string' && typeof obj === 'object') return 'object'; // Let "new String('')" return 'object' 276 | if (obj === null) return 'null'; // PhantomJS has type "DOMWindow" for null 277 | if (obj === undefined) return 'undefined'; // PhantomJS has type "DOMWindow" for undefined 278 | return type; 279 | } 280 | 281 | export const ObjToMap = ( 282 | value: ValueEnumObj | ValueEnumMap | undefined, 283 | ): ValueEnumMap | undefined => { 284 | if (!value) { 285 | return value; 286 | } 287 | if (getType(value) === 'map') { 288 | return value as ValueEnumMap; 289 | } 290 | return new Map(Object.entries(value)); 291 | }; 292 | 293 | /** 294 | * 减少 width,支持 string 和 number 295 | */ 296 | export const reduceWidth = (width?: string | number): string | number | undefined => { 297 | if (width === undefined) { 298 | return width; 299 | } 300 | if (typeof width === 'string') { 301 | if (!width.includes('calc')) { 302 | return `calc(100% - ${width})`; 303 | } 304 | return width; 305 | } 306 | if (typeof width === 'number') { 307 | return (width as number) - 32; 308 | } 309 | return width; 310 | }; 311 | -------------------------------------------------------------------------------- /src/component/columnSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context'; 3 | import { PushpinOutlined, SettingOutlined, VerticalAlignMiddleOutlined } from '@ant-design/icons'; 4 | import { Checkbox, Popover, Tooltip } from 'antd'; 5 | import { DndProvider } from 'react-dnd'; 6 | import Backend from 'react-dnd-html5-backend'; 7 | import Container from '../../container'; 8 | import { ProColumns, ColumnsState } from '../../Table'; 9 | import DnDItem from './DndItem'; 10 | import { useIntl } from '../intlContext'; 11 | import './index.less'; 12 | import { genColumnKey } from '../util'; 13 | 14 | interface ColumnSettingProps { 15 | columns?: ProColumns[]; 16 | } 17 | 18 | const ToolTipIcon: React.FC<{ 19 | title: string; 20 | columnKey: string | number; 21 | show: boolean; 22 | fixed: 'left' | 'right' | undefined; 23 | }> = ({ title, show, children, columnKey, fixed }) => { 24 | const { columnsMap, setColumnsMap } = Container.useContainer(); 25 | if (show) { 26 | return ( 27 | 28 | { 30 | const config = columnsMap[columnKey || ''] || {}; 31 | const columnKeyMap = { 32 | ...columnsMap, 33 | [columnKey]: { ...config, fixed } as ColumnsState, 34 | }; 35 | setColumnsMap(columnKeyMap); 36 | }} 37 | > 38 | {children} 39 | 40 | 41 | ); 42 | } 43 | return null; 44 | }; 45 | 46 | const CheckboxListItem: React.FC<{ 47 | columnKey: string | number; 48 | className?: string; 49 | title?: React.ReactNode; 50 | columnsMap: { 51 | [key: string]: ColumnsState; 52 | }; 53 | fixed?: boolean | 'left' | 'right'; 54 | setColumnsMap: (map: { [key: string]: ColumnsState }) => void; 55 | }> = ({ columnKey, className, columnsMap, title, setColumnsMap, fixed }) => { 56 | const intl = useIntl(); 57 | const config = columnsMap[columnKey || 'null'] || { show: true }; 58 | return ( 59 | 60 | { 62 | if (columnKey) { 63 | const tempConfig = columnsMap[columnKey || ''] || {}; 64 | const newSetting = { ...tempConfig }; 65 | if (e.target.checked) { 66 | delete newSetting.show; 67 | } else { 68 | newSetting.show = false; 69 | } 70 | const columnKeyMap = { 71 | ...columnsMap, 72 | [columnKey]: newSetting as ColumnsState, 73 | }; 74 | setColumnsMap(columnKeyMap); 75 | } 76 | }} 77 | checked={config.show !== false} 78 | > 79 | {title} 80 | 81 | 82 | 88 | 93 | 94 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | ); 113 | }; 114 | 115 | const CheckboxList: React.FC<{ 116 | list: (ProColumns & { index?: number })[]; 117 | className?: string; 118 | title: string; 119 | showTitle?: boolean; 120 | }> = ({ list, className, showTitle = true, title: listTitle }) => { 121 | const { columnsMap, setColumnsMap, sortKeyColumns, setSortKeyColumns } = Container.useContainer(); 122 | const show = list && list.length > 0; 123 | if (!show) { 124 | return null; 125 | } 126 | const move = (id: string, targetIndex: number) => { 127 | const newColumns = [...sortKeyColumns]; 128 | 129 | const findIndex = newColumns.findIndex((columnKey) => columnKey === id); 130 | 131 | const key = newColumns[findIndex]; 132 | newColumns.splice(findIndex, 1); 133 | if (targetIndex === 0) { 134 | newColumns.unshift(key); 135 | } else { 136 | newColumns.splice(targetIndex, 0, key); 137 | } 138 | setSortKeyColumns(newColumns); 139 | }; 140 | 141 | const listDom = list.map(({ key, dataIndex, title, fixed, ...rest }, index) => { 142 | const columnKey = genColumnKey(key, dataIndex, rest.index); 143 | return ( 144 | { 149 | move(id, targetIndex); 150 | }} 151 | > 152 | 160 | 161 | ); 162 | }); 163 | return ( 164 | 165 | {showTitle && {listTitle}} 166 | {listDom} 167 | 168 | ); 169 | }; 170 | 171 | const GroupCheckboxList: React.FC<{ 172 | localColumns: (ProColumns & { index?: number })[]; 173 | className?: string; 174 | }> = ({ localColumns, className }) => { 175 | const rightList: (ProColumns & { index?: number })[] = []; 176 | const leftList: (ProColumns & { index?: number })[] = []; 177 | const list: (ProColumns & { index?: number })[] = []; 178 | const intl = useIntl(); 179 | 180 | localColumns.forEach((item) => { 181 | const { fixed } = item; 182 | if (fixed === 'left') { 183 | leftList.push(item); 184 | return; 185 | } 186 | if (fixed === 'right') { 187 | rightList.push(item); 188 | return; 189 | } 190 | list.push(item); 191 | }); 192 | 193 | const showRight = rightList && rightList.length > 0; 194 | const showLeft = leftList && leftList.length > 0; 195 | 196 | return ( 197 |
198 | 203 | {/* 如果没有任何固定,不需要显示title */} 204 | 210 | 215 |
216 | ); 217 | }; 218 | 219 | const ColumnSetting = (props: ColumnSettingProps) => { 220 | const counter = Container.useContainer(); 221 | const localColumns: Omit & { index?: number }, 'ellipsis'>[] = 222 | props.columns || counter.columns || []; 223 | const { columnsMap, setColumnsMap, setSortKeyColumns } = counter; 224 | /** 225 | * 设置全部选中,或全部未选中 226 | * @param show 227 | */ 228 | const setAllSelectAction = (show: boolean = true) => { 229 | const columnKeyMap = {}; 230 | localColumns.forEach(({ key, fixed, dataIndex, index }) => { 231 | const columnKey = genColumnKey(key, dataIndex, index); 232 | if (columnKey) { 233 | columnKeyMap[columnKey] = { 234 | show, 235 | fixed, 236 | }; 237 | } 238 | }); 239 | setColumnsMap(columnKeyMap); 240 | }; 241 | 242 | // 选中的 key 列表 243 | const selectedKeys = Object.values(columnsMap).filter((value) => !value || value.show === false); 244 | 245 | // 是否已经选中 246 | const indeterminate = selectedKeys.length > 0 && selectedKeys.length !== localColumns.length; 247 | 248 | const intl = useIntl(); 249 | return ( 250 | 251 | {({ getPrefixCls }: ConfigConsumerProps) => { 252 | const className = getPrefixCls('pro-table-column-setting'); 253 | const toolBarClassName = getPrefixCls('pro-table-toolbar'); 254 | return ( 255 | 259 | { 263 | if (e.target.checked) { 264 | setAllSelectAction(); 265 | } else { 266 | setAllSelectAction(false); 267 | } 268 | }} 269 | > 270 | {intl.getMessage('tableToolBar.columnDisplay', '列展示')} 271 | 272 | { 274 | setColumnsMap({}); 275 | setSortKeyColumns([]); 276 | }} 277 | > 278 | {intl.getMessage('tableToolBar.reset', '重置')} 279 | 280 | 281 | } 282 | overlayClassName={`${className}-overlay`} 283 | trigger="click" 284 | placement="bottomRight" 285 | content={} 286 | > 287 | 288 | 294 | 295 | 296 | ); 297 | }} 298 | 299 | ); 300 | }; 301 | 302 | export default ColumnSetting; 303 | -------------------------------------------------------------------------------- /tests/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BasicTable 🎏 do not render default option 1`] = ` 4 |
7 |
11 |
14 |
17 | 24 | 28 | 31 | 38 | 42 | 47 | 51 | 55 | 56 | 60 | 64 | 70 | 73 | 74 | 75 | 76 |
77 |

80 | No Data 81 |

82 |
83 |
84 |
85 | `; 86 | 87 | exports[`BasicTable 🎏 do not render setting 1`] = ` 88 |
91 |
95 |
98 |
101 | 108 | 112 | 115 | 122 | 126 | 131 | 135 | 139 | 140 | 144 | 148 | 154 | 157 | 158 | 159 | 160 |
161 |

164 | No Data 165 |

166 |
167 |
168 |
169 | `; 170 | 171 | exports[`BasicTable 🎏 base use 1`] = ` 172 |
175 |
179 |
182 |
185 | 192 | 196 | 199 | 206 | 210 | 215 | 219 | 223 | 224 | 228 | 232 | 238 | 241 | 242 | 243 | 244 |
245 |

248 | No Data 249 |

250 |
251 |
252 |
253 | `; 254 | 255 | exports[`BasicTable 🎏 do not render Search 1`] = ` 256 |
259 |
263 |
266 |
269 | 276 | 280 | 283 | 290 | 294 | 299 | 303 | 307 | 308 | 312 | 316 | 322 | 325 | 326 | 327 | 328 |
329 |

332 | No Data 333 |

334 |
335 |
336 |
337 | `; 338 | --------------------------------------------------------------------------------