├── .fatherrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── docs
├── index.html
├── umi.css
└── umi.js
├── example
├── config
│ └── config.js
├── pages
│ ├── document.ejs
│ ├── index.css
│ └── index.js
└── snapshots
│ └── 1.jpg
├── package.json
├── src
├── index.less
├── index.tsx
└── locales
│ ├── en-US.js
│ ├── index.js
│ ├── pt-BR.js
│ └── zh-CN.js
├── tsconfig.json
└── typings
└── index.d.ts
/.fatherrc.js:
--------------------------------------------------------------------------------
1 | export default {
2 | cjs: 'rollup',
3 | esm: 'rollup',
4 | cssModules: {
5 | camelCase: true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /yarn.lock
2 | /package-lock.json
3 | /dist
4 | /.docz
5 | /node_modules
6 | .idea
7 | /example/dist/
8 | /example/pages/.umi/
9 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.mdx
3 | package.json
4 | .umi
5 | .umi-production
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 100,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019-present ()
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Ant Design Editable Table
2 |
3 | [](https://www.npmjs.org/package/antd-etable)
4 | [](https://www.npmjs.org/package/antd-etable)
5 | 
6 |
7 | 
8 |
9 | ## Online Demo
10 | https://guozhaolong.github.io/antd-etable/
11 |
12 | ## Usage
13 | ```
14 | import React, {useContext, useState} from "react";
15 | import EditableTable from 'antd-etable';
16 | import {Button} from 'antd';
17 | import styles from './index.css';
18 |
19 | const data = [
20 | {id:1,name:'测试1',title:'哈哈',status:0,desc:'描述1描述1描述1描述1',type:0,created_time:'2019-5-2'},
21 | {id:2,name:'测试2',title:'呵呵',status:1,desc:'描述2描述2描述2描述2',type:1,created_time:'2019-5-3'},
22 | {id:3,name:'测试3',title:'嘻嘻',status:2,desc:'描述3描述3描述3描述3',type:0,created_time:'2019-5-4'}
23 | ];
24 | const type = ['类型一','类型二'];
25 | const status = ['正常','异常','停止'];
26 | const cols = [
27 | {
28 | title: '名称',
29 | dataIndex: 'name',
30 | editable:true,
31 | editor: {
32 | required: true,
33 | },
34 | },
35 | {
36 | title: '类型',
37 | dataIndex: 'type',
38 | editable:true,
39 | editor: {
40 | type: 'select',
41 | options: [
42 | {key: 1, value: '类型一'},
43 | {key: 2, value: '类型二'},
44 | ]
45 | },
46 | render: (text, record) => (
47 | type[text]
48 | ),
49 | },
50 | {
51 | title: '日期',
52 | dataIndex: 'created_time',
53 | editable:true,
54 | editor: {
55 | type: 'datetime'
56 | }
57 | },
58 | ];
59 | const allCols = [
60 | ...cols.slice(0,2),
61 | {
62 | title: '标题',
63 | dataIndex: 'title',
64 | editable:true,
65 | width: 120,
66 | },
67 | ...cols.slice(2),
68 | {
69 | title: '状态',
70 | dataIndex: 'status',
71 | editable:true,
72 | width: 120,
73 | editor: {
74 | type: 'select',
75 | options: status.map((value,key) => ({key,value}))
76 | },
77 | render: (text, record) => (
78 | status[text]
79 | ),
80 | }
81 | ];
82 | export default function() {
83 | const [changedData,setChangedData] = useState([]);
84 | const fetch = (pager,filter,sorter) => {
85 | // Do Remote Fetch
86 | };
87 | return (
88 |
89 |
90 |
fetch()}
100 | onChangedDataUpdate={(d)=>{setChangedData(d)}}
101 | />
102 |
103 | );
104 | }
105 |
106 | ```
107 | ## API
108 | ##### EditableTable
109 | ###### 属性
110 | | 名称 | 描述 | 类型 | 默认值 |
111 | |:---|:---|:---:|:---:|
112 | | data | 初始化数据 | Array | [ ] |
113 | | [changedData](#changeddata) | 用于保存增删改的更新数据 | Array | [ ] |
114 | | [cols](#cols) | 表格列 | Array | [ ] |
115 | | allCols | 可显示表格列(格式同cols属性) | Array | [ ] |
116 | | [rowKey](#rowkey) | 唯一标识 | String | 'id' |
117 | | newRowKeyPrefix | 新增数据唯一标识的前缀 | String | 'new_' |
118 | | title | 标题 | String或Component | '' |
119 | | loading | 读取状态 | Boolean | false |
120 | | pageSize | 每页记录数 | Number | 10 |
121 | | total | 记录总数 | Number | 0 |
122 | | multiSelect | 可多选 | Boolean | false |
123 | | showHeader | 是否显示顶栏 | Boolean | true |
124 | | showFooter | 是否显示底栏 | Boolean | true |
125 | | showToolbar | 是否显示顶部工具栏 | Boolean | true |
126 | | showSelector | 是否显示选择按钮 | Boolean | false |
127 | | showAddBtn | 是否显示添加按钮 | Boolean | true |
128 | | showOpBtn | 是否显示编辑和删除按钮 | Boolean | true |
129 | | showTopPager | 是否显示顶部分页器 | Boolean | true |
130 | | showBottomPager | 是否显示底部分页器 | Boolean | false |
131 | | buttons | 自定义操作按钮组 | Component | 无 |
132 | | style | 样式 | Object | null |
133 | | expandedRowRender | 展开行时的渲染内容 | ReactNode | null |
134 | | expandedFirstRow | 默认展开第一行 | Boolean | false |
135 | | editOnSelected | 点击一行时编辑 | Boolean | false |
136 | | parentForm | 传入form | FormInstance | null |
137 |
138 | ###### 事件
139 | | 名称 | 描述 | 参数 | 返回值 |
140 | |:---|:---|:---:|:---:|
141 | | canEdit | 每行是否可编辑 | record | Boolean |
142 | | canRemove | 每行是否可删除 | record | Boolean |
143 | | beforeEdit | 编辑数据前触发 | 无 | 无 |
144 | | afterEdit | 编辑数据后触发 | 无 | 无 |
145 | | [onAdd](#onadd) | 新增数据的默认对象 | 无 | Object |
146 | | onFetch | 请求数据事件 | pager,filter,sorter | 无 |
147 | | [onChangedDataUpdate](#onchangeddataupdate) | 更新数据变化时触发 | arr | 无 |
148 | | [onSelectRow](#onselectrow) | 每页记录数 | rows | 无 |
149 | | [onDownload](#ondownload) | 每页记录数 | filter,sorter | 无 |
150 | | onExpandedRow | 展开一行时触发 | record | 无 |
151 |
152 | ###### 方法
153 | | 名称 | 描述 | 参数 | 返回值 |
154 | |:---|:---|:---:|:---:|
155 | | resetTable | 重置表格页码 | 无 | 无 |
156 |
157 | ## Config
158 | ##### changedData
159 | ###### 数组,用于保存变更后的数据,每条数据中会使用isNew、isUpdate、isDelete来标识该数据是新增、更新还是删除
160 |
161 | ##### cols
162 | ###### 参数例子
163 | ```
164 | [{
165 | title: 'ID',
166 | dataIndex: 'id',
167 | editable:false,
168 | },{
169 | title: '名称',
170 | dataIndex: 'name',
171 | sorter: true,
172 | editable:true,
173 | editor: {
174 | required: true,
175 | type: 'select',
176 | options: [
177 | {key: 1, value: '类型一'},
178 | {key: 2, value: '类型二'},
179 | ],
180 | validator: (rule,value,callback) => {
181 | if(data.find(d => d.name === value))
182 | callback('名称已存在!');
183 | else
184 | callback();
185 | },
186 | },
187 | }]
188 | ```
189 | ###### editable:设置可编辑状态
190 | ###### editor:对象默认类型为text,支持的类型包括select、number、datetime、checkbox,如果为select需传入options参数
191 |
192 | ##### rowKey
193 | ###### 数据的唯一标识,必须唯一,用于判断编辑状态和匹配数据
194 |
195 | ##### onAdd
196 | ###### 当点击新增时,可配置初始化数据的方法用于返回一个新数据对象,可用来设置一些默认值
197 |
198 | ##### onChangedDataUpdate
199 | ###### 每次新增、更新、删除都会触发该方法,并传入更新后的数组
200 |
201 | ##### onSelectRow
202 | ###### 该方法会传入一个已选对象的数组,如果为单选模式,该数组只包含当前点击行的对象
203 |
204 | ##### onDownload
205 | ###### 点击工具栏下载时触发,如果配置了方法,则该方法会接到filter和sorter两个参数,如果没有配置方法则默认生成当页的excel下载
206 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | antd-etable
--------------------------------------------------------------------------------
/example/config/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | base: '/antd-etable',
3 | publicPath: '/antd-etable/',
4 | }
5 |
--------------------------------------------------------------------------------
/example/pages/document.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | antd-etable
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/pages/index.css:
--------------------------------------------------------------------------------
1 | .root {
2 | padding: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/example/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState,useRef } from "react";
2 | import EditableTable from '../../dist';
3 | import { Button, Checkbox, Input, Tooltip, Form, Row,Col,InputNumber,DatePicker } from 'antd';
4 | import styles from './index.css';
5 | import moment from 'moment';
6 | import { SearchOutlined } from '@ant-design/icons';
7 | import _ from "lodash";
8 |
9 | const demoData = [
10 | {id:1,obj1:{a:1,},timeLimit:'10:10',name:'测试1',title:'哈哈',status:0,test1:'111',test2:'222',test3:'aaa',test4:'bbb',desc:'描述1描述1描述1描述1描述1描述1描述1描述1描述1描述1描述1',type:0,created_time:'2019-05-02 00:00:00'},
11 | {id:2,obj1:{a:3,},name:'测试2',title:'呵呵',status:1,test1:'333',test2:'444',test3:'ccc',test4:'ddd',desc:'描述2描述2描述2描述2描述2描述2描述2描述2描述2描述2描述2',type:1,created_time:'2019-05-03 00:00:00'},
12 | {id:3,obj1:{a:5,},name:'测试3',title:'嘻嘻',status:2,test1:'555',test2:'666',test3:'eee',test4:'fff',desc:'描述3描述3描述3描述3描述3描述3描述3描述3描述3描述3描述3',type:0,created_time:'2019-05-04 00:00:00'},
13 | {id:4,created_time:null},
14 | {id:5},
15 | ];
16 | const type = ['类型一','类型二'];
17 | const status = ['正常','异常','停止'];
18 | const cols = [
19 | {
20 | title: 'ID',
21 | dataIndex: 'id',
22 | editable: false,
23 | width: 120,
24 | },
25 | {
26 | title: '测试时间',
27 | dataIndex: 'timeLimit',
28 | editable: true,
29 | editor: { type: 'time' },
30 | width: 120,
31 | render: (text) => (text ? moment(text,'HH:mm').format('HH:mm') : '')
32 | },
33 | {
34 | title: '子属性',
35 | dataIndex: ['obj1','a'],
36 | editable:true,
37 | width: 120,
38 | editor: {
39 | type:'number',
40 | max: 400,
41 | }
42 | },
43 | {
44 | title: '名称',
45 | dataIndex: 'name',
46 | sorter: true,
47 | editable:true,
48 | width: 160,
49 | editor: {
50 | required: true,
51 | validator: (rule,value,callback,record) => {
52 | if(demoData.find(d => d.name === value && record.id !== d.id))
53 | callback('名称已存在!');
54 | else
55 | callback();
56 | },
57 | },
58 | },
59 | {
60 | title: '测试多列',
61 | children: [
62 | {title:'测试列1',children:[
63 | {
64 | title: '测试1',
65 | dataIndex: 'test1',
66 | width: 120,
67 | editable:true,
68 | },
69 | {
70 | title: '测试2',
71 | dataIndex: 'test2',
72 | width: 120,
73 | editable:true,
74 | },
75 | ]},
76 | {title:'测试列2',children:[
77 | {
78 | title: '测试3',
79 | dataIndex: 'test3',
80 | width: 120,
81 | editable:true,
82 | },
83 | {
84 | title: '测试4',
85 | dataIndex: 'test4',
86 | width: 120,
87 | editable:true,
88 | },
89 | ]}
90 | ],
91 | },
92 | {
93 | title: '描述',
94 | dataIndex: 'desc',
95 | editable:true,
96 | width: 200,
97 | editor: {
98 | max: 4,
99 | },
100 | render: (text) => {
101 | return {text}
102 | }
103 | },
104 | {
105 | title: '类型',
106 | dataIndex: 'type',
107 | sorter: true,
108 | editable:true,
109 | width: 120,
110 | editor: {
111 | type: 'select',
112 | options: type.map((value,key) => ({key,value}))
113 | },
114 | render: (text, record) => (
115 | type[text]
116 | ),
117 | },
118 | {
119 | title: '日期',
120 | dataIndex: 'created_time',
121 | editable:true,
122 | width: 180,
123 | editor: {
124 | type: 'datetime'
125 | }
126 | },
127 | ];
128 |
129 | const allCols = [
130 | ...cols.slice(0,2),
131 | {
132 | title: '标题',
133 | dataIndex: 'title',
134 | editable:true,
135 | width: 120,
136 | },
137 | ...cols.slice(2),
138 | {
139 | title: '状态',
140 | dataIndex: 'status',
141 | editable:true,
142 | width: 120,
143 | editor: {
144 | type: 'select',
145 | options: status.map((value,key) => ({key,value}))
146 | },
147 | render: (text, record) => (
148 | status[text]
149 | ),
150 | }
151 | ];
152 | let i = 10;
153 | export default function() {
154 | const [data,setData] = useState(demoData);
155 | const [changedData,setChangedData] = useState([]);
156 | const [showToolbar,setShowToolbar] = useState(true);
157 | const [showOpBtn,setShowOpBtn] = useState(true);
158 | const [showAddBtn,setShowAddBtn] = useState(true);
159 | const [showSelector,setShowSelector] = useState(false);
160 | const [multiSelect,setMultiSelect] = useState(false);
161 | const [showTopPager,setShowTopPager] = useState(true);
162 | const [currentPage,setCurrentPage] = useState(1);
163 | const [showBottomPager,setShowBottomPager] = useState(false);
164 | const [showHeader,setShowHeader] = useState(true);
165 | const [showFooter,setShowFooter] = useState(true);
166 | const [loading,setLoading] = useState(true);
167 | const [form] = Form.useForm();
168 | const [currentRow,setCurrentRow] = useState();
169 |
170 | const tableRef = useRef();
171 | const demoButtons = <>
172 |
173 |
174 | >;
175 | const [buttons,setButtons] = useState(demoButtons);
176 | setTimeout(()=> { setLoading(false); }, 500 );
177 | const fetch = (pager,filter,sorter) => {
178 | console.log('onFetch',pager,filter,sorter);
179 | setLoading(true);
180 | setTimeout(()=> {
181 | setLoading(false);
182 | }, 500 );
183 | };
184 | const handleChangeData = (record)=>{
185 | setChangedData(changedData.map(c => {
186 | if(c.id === record['id']){
187 | return {
188 | ...c,
189 | name: 'haha'
190 | }
191 | }else{
192 | return c;
193 | }
194 | }));
195 | };
196 | const handleFormChange = (values)=>{
197 | if(currentRow){
198 | const isExistRow = changedData.find(c => c.id === currentRow.id);
199 | if(!isExistRow){
200 | const updateData = [...changedData,{id:currentRow.id,...values,isUpdate: true}];
201 | setChangedData(updateData);
202 | }else {
203 | const updateData = changedData.map(c => {
204 | if (c.id === currentRow.id) {
205 | return _.merge({}, c, values);
206 | } else {
207 | return c;
208 | }
209 | });
210 | setChangedData(updateData);
211 | }
212 | }
213 | };
214 | const handleExpandRow = (row)=>{
215 | setData(data.map(c => {
216 | if(c.id === row.id){
217 | return _.merge({b:"haha"}, c);
218 | }else{
219 | return c;
220 | }
221 | }));
222 | };
223 | return (
224 |
305 | );
306 | }
307 |
--------------------------------------------------------------------------------
/example/snapshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guozhaolong/antd-etable/0deaa25b5b2caf5ae3e1de0481bd0f7227f8589e/example/snapshots/1.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "antd-etable",
3 | "version": "1.3.71",
4 | "description": "a editable table base on ant design",
5 | "main": "dist/index.js",
6 | "module": "dist/index.esm.js",
7 | "types": "src/index.d.ts",
8 | "authors": {
9 | "name": "guozhaolong",
10 | "email": "guozhaolong@gmail.com"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/guozhaolong/antd-etable.git"
15 | },
16 | "scripts": {
17 | "dev": "cd example && umi dev",
18 | "build": "father build"
19 | },
20 | "files": [
21 | "dist",
22 | "assets"
23 | ],
24 | "dependencies": {
25 | "@ant-design/icons": "^4.0.3",
26 | "react-resizable": "^1.10.1",
27 | "antd": "4.5.2"
28 | },
29 | "peerDependencies": {
30 | "react": "^16.12.0",
31 | "lodash": "^4.17.11",
32 | "moment": "^2.24.0"
33 | },
34 | "devDependencies": {
35 | "father": "^3.0.0-alpha.1",
36 | "lodash": "^4.17.11",
37 | "umi": "^2.12.9",
38 | "webpack": "^4.42.0"
39 | },
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/guozhaolong/antd-etable/issues"
43 | },
44 | "homepage": "https://github.com/guozhaolong/antd-etable#readme",
45 | "directories": {
46 | "example": "example"
47 | },
48 | "keywords": [
49 | "editable",
50 | "table",
51 | "antd"
52 | ],
53 | "author": "stevenkwok"
54 | }
55 |
--------------------------------------------------------------------------------
/src/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 | .antETable {
3 | .antETableHeader {
4 | display: flex;
5 | //justify-content: space-between;
6 | background-color: @background-color-base;
7 | padding: 4px 8px;
8 | border-bottom: 1px solid @border-color-split;
9 | .antETableTitleContainer {
10 | width: 80%;
11 | display: flex;
12 | }
13 | .antETableTitle {
14 | color: @heading-color;
15 | font-weight: 400;
16 | font-size: @font-size-base;
17 | white-space: nowrap;
18 | }
19 | .antETableTopPagerDisabled {
20 | :global {
21 | .ant-pagination-prev,.ant-pagination-next{
22 | curor: not-allow;
23 | .ant-pagination-item-link {
24 | color: rgba(0, 0, 0, 0.25);
25 | cursor: not-allowed;
26 | }
27 | }
28 | .ant-pagination-simple-pager {
29 | color: #ddd;
30 | input {
31 | border-color: #ddd;
32 | }
33 | }
34 | }
35 | }
36 | .antETableToolbar {
37 | margin-left: 8px;
38 | display: flex;
39 | justify-content: space-around;
40 | > div {
41 | line-height: 24px;
42 | }
43 | > span > svg {
44 | margin-top: 6px;
45 | margin-right: 16px;
46 | cursor: pointer;
47 | font-size: @font-size-sm;
48 | }
49 | :global {
50 | .ant-pagination-item-link {
51 | background-color: transparent;
52 | }
53 | }
54 | }
55 | .antETableToolbarRight {
56 | width: 20%;
57 | text-align: right;
58 | > span > svg {
59 | margin-top: 6px;
60 | margin-right: 16px;
61 | cursor: pointer;
62 | font-size: @font-size-sm;
63 | }
64 | }
65 | }
66 | .antETableFilter {
67 | min-height: 30px;
68 | td {
69 | border-bottom: 1px solid @border-color-base;
70 | &:last-of-type {
71 | border-right: 1px solid @border-color-base;
72 | }
73 | }
74 | :global {
75 | .ant-input {
76 | background-color: transparent;
77 | }
78 | .ant-input-affix-wrapper {
79 | background-color: transparent;
80 | }
81 | .ant-select-selector {
82 | background-color: transparent;
83 | }
84 | }
85 | }
86 | .antETableBottomBar {
87 | display: flex;
88 | justify-content: space-between;
89 | }
90 | .antETableContent {
91 | border: 1px solid @border-color-base;
92 | border-radius: 4px;
93 | }
94 | :global {
95 | .ant-table table {
96 | table-layout: fixed;
97 | }
98 | .ant-table tbody > tr .ant-table {
99 | margin: 0 !important;
100 | }
101 | .ant-table-tbody > tr .ant-table-wrapper:only-child .ant-table {
102 | margin: 0 !important;
103 | td {
104 | background-color: #fff !important;
105 | }
106 | }
107 | .ant-table-pagination {
108 | margin-top: 24px;
109 | }
110 | .ant-table-thead {
111 | .react-resizable {
112 | position: relative;
113 | }
114 | .react-resizable-handle {
115 | position: absolute;
116 | width: 10px;
117 | height: 100%;
118 | bottom: 0;
119 | right: -5px;
120 | cursor: col-resize;
121 | z-index: 1;
122 | background-image: none;
123 | }
124 | }
125 | .ant-table-tbody {
126 | tr {
127 | td {
128 | word-break: keep-all;
129 | white-space: nowrap;
130 | overflow: hidden;
131 | text-overflow: ellipsis;
132 | }
133 | }
134 | }
135 | .ant-table-footer {
136 | padding: 8px;
137 | .ant-btn-sm {
138 | line-height: unset;
139 | }
140 | }
141 | .ant-pagination-total-text {
142 | margin-right: 0;
143 | }
144 | .ant-table-placeholder {
145 | border-radius: 0;
146 | }
147 | .ant-input {
148 | transition: none;
149 | }
150 | .ant-table-cell {
151 | padding: 8px;
152 | }
153 | .ant-table-expanded-row {
154 | .ant-input {
155 | background-color: transparent;
156 | }
157 | .ant-input-affix-wrapper {
158 | background-color: transparent;
159 | }
160 | .ant-select-selector {
161 | background-color: transparent;
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | useState,
4 | useEffect,
5 | useMemo,
6 | Component,
7 | CSSProperties,
8 | PropsWithChildren,
9 | } from 'react';
10 | import {
11 | Form,
12 | Table,
13 | Input,
14 | Pagination,
15 | Tooltip,
16 | Button,
17 | Select,
18 | InputNumber,
19 | DatePicker,
20 | TimePicker,
21 | Checkbox,
22 | Divider,
23 | Popover,
24 | List,
25 | Row,
26 | Col,
27 | Empty,
28 | } from 'antd';
29 | import 'antd/lib/form/style';
30 | import 'antd/lib/table/style';
31 | import 'antd/lib/input/style';
32 | import 'antd/lib/pagination/style';
33 | import 'antd/lib/tooltip/style';
34 | import 'antd/lib/select/style';
35 | import 'antd/lib/input-number/style';
36 | import 'antd/lib/date-picker/style';
37 | import 'antd/lib/checkbox/style';
38 | import 'antd/lib/divider/style';
39 | import 'antd/lib/popover/style';
40 | import 'antd/lib/list/style';
41 | import moment from 'moment';
42 | import { Resizable } from 'react-resizable';
43 | import _ from 'lodash';
44 | import styles from './index.less';
45 | import locale from './locales';
46 | import { ColumnType } from 'antd/lib/table';
47 | import {
48 | CheckOutlined,
49 | CloseOutlined,
50 | EditOutlined,
51 | DeleteOutlined,
52 | DeleteFilled,
53 | FilterOutlined,
54 | FilterFilled,
55 | RestFilled,
56 | SearchOutlined,
57 | UnorderedListOutlined,
58 | DownloadOutlined,
59 | ColumnHeightOutlined,
60 | VerticalAlignMiddleOutlined,
61 | PlusOutlined,
62 | RightOutlined,
63 | DownOutlined,
64 | } from '@ant-design/icons';
65 | import { FormInstance } from 'antd/lib/form';
66 |
67 | interface ETableColEditorProps {
68 | type?: 'select' | 'datetime' | 'string' | 'checkbox' | 'number' | 'date' | 'time';
69 | required?: boolean;
70 | validator?: (...arg: any[]) => void;
71 | options?: any[];
72 | format?: string;
73 | max?: number;
74 | min?: number;
75 | regex?: RegExp;
76 | component?: ()=> React.ReactElement;
77 | }
78 |
79 | interface ETableColProps extends ColumnType {
80 | editable?: (...arg: any[]) => boolean | true | false;
81 | editor?: ETableColEditorProps;
82 | children?: ETableColProps[];
83 | }
84 |
85 | const { RangePicker } = DatePicker;
86 |
87 | interface EditableContextProps {
88 | rowKey?: string;
89 | changedData?: any[];
90 | filter?: any;
91 | filterVisible?: boolean;
92 | setFilter?: (...args: any[]) => void;
93 | selectedRowKeys?: string[];
94 | showSelector?: boolean;
95 | columns?: ETableColProps[];
96 | setColumns?: (cols: ETableColProps[]) => void;
97 | handleTableChange?: (p?: any, f?: any, s?: any) => void;
98 | expandedRowRender?: (record) => React.ReactNode;
99 | }
100 |
101 | const EditableContext = React.createContext({});
102 | const dateTimeFormat = 'YYYY-MM-DD HH:mm:ss';
103 | const dateFormat = 'YYYY-MM-DD';
104 | const timeFormat = 'HH:mm';
105 |
106 | function updateChangedData(changedData: any[], item: any, rowKey: string = 'id'): any[] {
107 | let result: any[];
108 | const idx = changedData.findIndex(d => item[rowKey] === d[rowKey]);
109 | const older = changedData.find(d => item[rowKey] === d[rowKey]);
110 | if (item.isDelete) {
111 | if (older && (older.isNew || !older.isUpdate)) {
112 | result = [...changedData.slice(0, idx), ...changedData.slice(idx + 1)];
113 | } else if (older && older.isDelete && older.isUpdate) {
114 | result = changedData.map(d => {
115 | if (item[rowKey] === d[rowKey]) {
116 | return { ...d, isDelete: false };
117 | }
118 | return d;
119 | });
120 | } else if (older && !older.isDelete) {
121 | result = changedData.map(d => {
122 | if (item[rowKey] === d[rowKey]) {
123 | return { ...d, isDelete: true };
124 | }
125 | return d;
126 | });
127 | } else {
128 | result = [...changedData, { ...item, isDelete: true }];
129 | }
130 | } else if (idx > -1) {
131 | result = changedData.map(d => {
132 | if (item[rowKey] === d[rowKey]) {
133 | return _.merge({},d,item);
134 | }
135 | return d;
136 | });
137 | } else {
138 | result = [...changedData, item];
139 | }
140 | return result;
141 | }
142 |
143 | function exportCSV(payload) {
144 | const { name, header, data } = payload;
145 | if (payload && data.length > 0) {
146 | let str = header.map(h => h.title).join(',') + '\n';
147 | str += data.map(d => header.map(h => _.get(d, [h.dataIndex])).join(',')).join('\n');
148 | const blob = new Blob(['\ufeff' + str], { type: 'text/csv;charset=utf-8;' });
149 | const filename = `${name}.csv`;
150 | let link = document.createElement('a');
151 | if (link.download !== undefined) {
152 | let url = URL.createObjectURL(blob);
153 | link.setAttribute('href', url);
154 | link.setAttribute('download', filename);
155 | link.style.visibility = 'hidden';
156 | document.body.appendChild(link);
157 | link.click();
158 | document.body.removeChild(link);
159 | }
160 | }
161 | }
162 |
163 | function flatCols(columns: ETableColProps[]) {
164 | return columns.flatMap(col => ('children' in col ? flatCols(col.children!) : col));
165 | }
166 |
167 | function initChildCols(col: ETableColProps, idx: string | number, editingKey: string, rowKey: string) {
168 | if (col.children) {
169 | return {
170 | ...col,
171 | children: col.children.map((child, i) => {
172 | return initChildCols(child, idx + '' + i, editingKey, rowKey);
173 | }),
174 | };
175 | } else if (!col.editable) {
176 | return col;
177 | } else {
178 | const isEditing = (record)=>{
179 | if(!col.editable || _.isFunction(col.editable) && !col.editable(record))
180 | return false;
181 | else
182 | return record[rowKey] === editingKey;
183 | };
184 | return {
185 | ...col,
186 | onCell: record => ({
187 | record,
188 | editor: col.editor,
189 | editing: isEditing(record),
190 | dataIndex: col.dataIndex,
191 | title: col.title,
192 | }),
193 | onHeaderCell: col => ({
194 | width: col.width,
195 | index: idx,
196 | }),
197 | };
198 | }
199 | }
200 |
201 | function isFixedHeader(children) {
202 | return !!children.find(c => !!c.props.fixed);
203 | }
204 |
205 | function setFormValue(form:FormInstance,record:any,columns:ETableColProps[]){
206 | const tmp = {};
207 | _.keys(record).map(k => {
208 | const col:ETableColProps = columns.find(c => c.dataIndex === k)!;
209 | if(col && col.editor && col.editor.type === 'datetime'){
210 | tmp[k] = moment(record[k],dateTimeFormat);
211 | }else if(col && col.editor && col.editor.type === 'date'){
212 | tmp[k] = moment(record[k],dateFormat);
213 | }else if(col && col.editor && col.editor.type === 'time'){
214 | tmp[k] = moment(record[k],timeFormat);
215 | }else{
216 | tmp[k] = record[k];
217 | }
218 | });
219 | form.setFieldsValue(tmp);
220 | }
221 |
222 | const EditableHWrapper: React.FC> = ({ className, children }) => {
223 | const { filter, filterVisible, setFilter, columns, handleTableChange, showSelector,expandedRowRender } = useContext(EditableContext);
224 | const flatColumns = useMemo(() => flatCols(columns!), [columns]);
225 | return (
226 |
227 | {children}
228 | {!isFixedHeader(children) && filterVisible && (
229 |
230 | {expandedRowRender && }
231 | {showSelector && | }
232 | {flatColumns.map((col, idx) => {
233 | const { editor = {}, align = 'left' } = col;
234 | if (col.dataIndex)
235 | return |
236 | {getFilterInput(editor, _.get(filter,col.dataIndex), value => {
237 | if(_.isArray(col.dataIndex)){
238 | setFilter!(_.merge({},filter,_.set({},col.dataIndex,value)));
239 | }else {
240 | setFilter!({ ...filter, [col.dataIndex]: value, })
241 | }
242 | }, handleTableChange)}
243 | | ;
244 | else
245 | return ;
246 | }).filter(a => a !== undefined)}
247 | |
248 | )}
249 |
250 | );
251 | };
252 |
253 | const getFilterInput = (editor, value, onChange, onSearch) => {
254 | const { type = 'string', options = [], format } = editor;
255 | switch (type) {
256 | case 'number':
257 | return onChange(value)}
259 | onKeyPress={(e) => e.nativeEvent.key === 'Enter' ? onSearch({ currentPage: 1 }) : null}/>;
260 | case 'select':
261 | return (
262 |
269 | );
270 | case 'datetime':
271 | return onChange(dates)}/>;
276 | case 'date':
277 | return onChange(dates)}/>;
281 | case 'time':
282 | return onChange(dates)}/>;
286 | case 'checkbox':
287 | return onChange(e.target.checked)}/>;
289 | case 'string':
290 | return onChange(e.target.value.trim())}
292 | onKeyPress={(e) => e.nativeEvent.key === 'Enter' ? onSearch({ currentPage: 1 }) : null}/>;
293 | default:
294 | return onChange(e.target.value.trim())}
296 | onKeyPress={(e) => e.nativeEvent.key === 'Enter' ? onSearch({ currentPage: 1 }) : null}/>;
297 | }
298 | };
299 |
300 | interface ResizeableCellProps {
301 | index: number;
302 | width?: number;
303 | }
304 |
305 | const ResizeableCell: React.FC = ({ index, width, ...restProps }) => {
306 | const { columns, setColumns } = useContext(EditableContext);
307 | if (!width) {
308 | return | ;
309 | }
310 | return (
311 | {
315 | const nextColumns = [...columns!];
316 | nextColumns[index] = {
317 | ...nextColumns[index],
318 | width: size.width,
319 | };
320 | setColumns!(nextColumns);
321 | e.stopPropagation();
322 | }}
323 | draggableOpts={{ enableUserSelectHack: false }}
324 | >
325 | |
326 |
327 | );
328 | };
329 |
330 | const EditableRow: React.FC> = props => {
331 | const { changedData, selectedRowKeys, rowKey } = useContext(EditableContext);
332 | const key = props['data-row-key'];
333 | const isDelete = changedData!.find(d => key === d[rowKey!] && d.isDelete);
334 | let style = props.style;
335 | const selected = selectedRowKeys!.find(i => key === i);
336 | if (selected) {
337 | style = {
338 | ...style,
339 | fontWeight: 800,
340 | };
341 | }
342 | const deleteStyle:any = {
343 | borderTop: '1px solid #000',
344 | position: 'absolute',
345 | height: 1,
346 | width: 'calc(100% - 4px)',
347 | marginTop: -20,
348 | };
349 | return <>
350 |
351 | { isDelete && }
352 | >;
353 | };
354 |
355 | interface EditableCellProps {
356 | editor?: ETableColEditorProps;
357 | editing?: boolean;
358 | dataIndex?: string | string[];
359 | title?: string;
360 | record?: any;
361 | index?: number;
362 | }
363 |
364 | const EditableCell: React.FC = ({ editor = { type: 'string' }, editing, dataIndex, title, record, index, children, ...restProps }) => {
365 | const rules: any[] = [];
366 | const { type = 'string',required, validator , min, max, regex, component } = editor;
367 | if (required) {
368 | rules.push({ required: editor.required, message: `${title}必填.` });
369 | }
370 | if (validator) {
371 | rules.push({ validator: (rule, value, callback) => editor.validator!(rule, value, callback, record) });
372 | }
373 | if(type === 'string' && max){
374 | rules.push({ type: 'string', max: max, message: `最多${max}个字符` });
375 | }
376 | if(type === 'number' && max){
377 | rules.push({ type: 'number', max, message: `不能大于${max}` });
378 | }
379 | if(type === 'number' && min){
380 | rules.push({ type: 'number', min, message: `不能小于${min}` });
381 | }
382 | if(type === 'string' && regex){
383 | rules.push({ type: 'string', pattern: regex, message: '内容不符合要求' });
384 | }
385 | return (
386 |
387 | {editing ? (
388 | component ?
389 | component() :
390 | {
394 | if((editor.type === 'datetime' || editor.type === 'date' || editor.type === 'time') && _.isObject(value)) {
395 | if(value.isValid()){
396 | return { value };
397 | }else{
398 | return { value: undefined};
399 | }
400 | }else if(editor.type === 'datetime' || editor.type === 'date' || editor.type === 'time'){
401 | if(!value || value === '')
402 | return { value: undefined };
403 | else if(editor.type === 'datetime')
404 | return { value: moment(value,"YYYY-MM-DD HH:mm:ss") };
405 | else if(editor.type === 'date')
406 | return { value: moment(value,"YYYY-MM-DD") };
407 | else if(editor.type === 'time')
408 | return { value: moment(value,"HH:mm") };
409 | }
410 | return { value }
411 | }}
412 | getValueFromEvent={(e)=>{
413 | if((editor.type === 'datetime' || editor.type === 'date' || editor.type === 'time') && !e)
414 | return '';
415 | if(editor.type === 'datetime')
416 | return moment(e).format("YYYY-MM-DD HH:mm:ss");
417 | else if(editor.type === 'date')
418 | return moment(e).format("YYYY-MM-DD");
419 | else if(editor.type === 'time')
420 | return moment(e).format("HH:mm");
421 | else if(editor.type === 'number' || editor.type === 'select')
422 | return e;
423 | else
424 | return e.target.value;
425 | }}
426 | valuePropName={editor.type === 'checkbox' ? 'checked' : 'value'}>
427 | {getInput(editor)}
428 |
429 | ) : (
430 | children
431 | )}
432 | |
433 | );
434 | };
435 |
436 | const getInput = (editor: ETableColEditorProps) => {
437 | const { type = 'string', options = [], format, } = editor;
438 | switch (type) {
439 | case 'number':
440 | return ;
441 | case 'select':
442 | return (
443 |
450 | );
451 | case 'datetime':
452 | return ;
453 | case 'date':
454 | return ;
455 | case 'time':
456 | return ;
457 | case 'checkbox':
458 | return ;
459 | case 'string':
460 | return ;
461 | default:
462 | return ;
463 | }
464 | };
465 |
466 | const defaultArr = [];
467 |
468 | export interface ETableProps {
469 | name?: string;
470 | bordered?: boolean;
471 | lang?: 'zh' | 'en' | 'pt_br';
472 | rowKey?: string;
473 | title?: string;
474 | style?: CSSProperties;
475 | newRowKeyPrefix?: string;
476 | cols?: ETableColProps[];
477 | allCols?: ETableColProps[];
478 | data?: any[];
479 | changedData?: any[];
480 | loading?: boolean;
481 | currentPage?: number;
482 | pageSize?: number;
483 | total?: number;
484 | scroll?: any;
485 | multiSelect?: boolean;
486 | showHeader?: boolean;
487 | showFooter?: boolean;
488 | showToolbar?: boolean;
489 | showAddBtn?: boolean;
490 | showOpBtn?: boolean;
491 | showSelectRecord?: boolean;
492 | showSelector?: boolean;
493 | showTopPager?: boolean;
494 | showBottomPager?: boolean;
495 | buttons?: React.ReactElement,
496 | canEdit?: (...args: any[]) => boolean;
497 | canRemove?: (...args: any[]) => boolean;
498 | beforeEdit?: (...args: any[]) => any;
499 | afterEdit?: (...args: any[]) => any;
500 | onAdd?: (...args: any[]) => any;
501 | onFetch?: (...args: any[]) => void;
502 | onChangedDataUpdate?: (...args: any[]) => void;
503 | onDownload?: (...args: any[]) => any;
504 | onSelectRow?: (...args: any[]) => void;
505 | expandedRowRender?: (record) => React.ReactNode;
506 | expandedFirstRow?: boolean;
507 | editOnSelected?: boolean;
508 | onExpandedRow?: (...args: any[]) => void;
509 | parentForm?: FormInstance;
510 | }
511 |
512 | const EditableTable: React.FC = ({
513 | name ,
514 | bordered = false,
515 | lang = 'zh',
516 | rowKey = 'id',
517 | title = '',
518 | style = {},
519 | newRowKeyPrefix = 'new_',
520 | cols = defaultArr,
521 | allCols = [],
522 | data = [],
523 | changedData = defaultArr,
524 | loading = false,
525 | currentPage = 1,
526 | pageSize = 10,
527 | total = 0,
528 | scroll = { x: null },
529 | multiSelect = true,
530 | showHeader = true,
531 | showFooter = true,
532 | showToolbar = true,
533 | showAddBtn = true,
534 | showOpBtn = true,
535 | showSelectRecord = true,
536 | showSelector: defaultShowSelecor = false,
537 | showTopPager = true,
538 | showBottomPager = false,
539 | buttons,
540 | canEdit = () => true,
541 | canRemove = () => true,
542 | beforeEdit = () => ({}),
543 | afterEdit = () => ({}),
544 | onAdd = () => ({}),
545 | onFetch = () => {},
546 | onChangedDataUpdate = () => {},
547 | onDownload,
548 | onSelectRow = () => {},
549 | expandedRowRender,
550 | expandedFirstRow = false,
551 | editOnSelected = false,
552 | onExpandedRow = () => {},
553 | parentForm,
554 | ...rest
555 | }) => {
556 | const [form] = Form.useForm(parentForm);
557 | const [showSelector, setShowSelector] = useState(defaultShowSelecor);
558 | const [editingKey, setEditingKey] = useState('');
559 | const [filterVisible, setFilterVisible] = useState(false);
560 | const [filter, setFilter] = useState({});
561 | const [sorter, setSorter] = useState({});
562 | const [pager, setPager] = useState({ currentPage, pageSize });
563 | const [selectedRowKeys, setSelectedRowKeys] = useState([]);
564 | const [columnSeq, setColumnSeq] = useState(cols.map((c, idx) => ({ ...c, idx, visible: true })));
565 | const [allColumnSeq, setAllColumnSeq] = useState[]>([]);
566 | const [columns, setColumns] = useState[]>(allCols);
567 | const [columnsPopVisible, setColumnsPopVisible] = useState(false);
568 | const [collapsed, setCollapsed] = useState(false);
569 | const [expandedRowKeys,setExpandedRowKeys] = useState([]);
570 |
571 | const i18n = locale[lang.toLowerCase()];
572 | const updateData = data.filter(d => !!d).map(d => {
573 | const updater = changedData.find(s => d[rowKey] === s[rowKey]);
574 | if (updater) {
575 | return _.merge({},d,updater);
576 | }
577 | return d;
578 | });
579 | const newData = changedData.filter(s => s.isNew);
580 | const temp:any[] = [];
581 | const dataSource = temp.concat(newData).reverse().concat(updateData);
582 | const handleTableChange = (p?: any, f?: any, s?: any) => {
583 | let current = pager.currentPage;
584 | let size = pager.pageSize;
585 | let filters = filter;
586 | let sorters = sorter;
587 | if (p && p.currentPage) {
588 | current = p.currentPage;
589 | size = p.pageSize || pager.pageSize;
590 | setPager({ currentPage: current, pageSize: size });
591 | }
592 | if (!_.isEmpty(f)) {
593 | if (f.clear) {
594 | setFilter({});
595 | filters = {};
596 | } else {
597 | filters = { ...filter, ...f };
598 | setFilter(f);
599 | }
600 | }
601 | if (!_.isEmpty(s)) {
602 | sorters = { [s.field]: s.order };
603 | setSorter(sorters);
604 | }
605 | filters = _.pickBy(filters, value => !_.isUndefined(value) && value !== '');
606 | sorters = _.pickBy(sorters, value => !_.isUndefined(value) && value !== '');
607 | onFetch({ currentPage: current, pageSize: size }, filters, sorters);
608 | };
609 |
610 | let rowSelection: any = useMemo(()=> ({
611 | selectedRowKeys,
612 | type: multiSelect ? 'checkbox' : 'radio',
613 | onChange: (keys, _rows) => setSelectedRowKeys(keys),
614 | onSelect: (record, _selected, rows, _e) => {
615 | handleSelect(record,rows);
616 | },
617 | onSelectAll: (_selected, rows, _changeRows) => onSelectRow(rows),
618 | }),[selectedRowKeys,multiSelect]);
619 |
620 | if (!showSelector) {
621 | rowSelection = undefined;
622 | }
623 |
624 | const handleSelect = (record,rows)=>{
625 | if (editOnSelected)
626 | setEditingKey(record[rowKey]);
627 | if (!selectedRowKeys.find(k => k === record[rowKey])) {
628 | if(selectedRowKeys.length > 0){
629 | const previousRow = dataSource.find(d => d[rowKey] === selectedRowKeys[0]);
630 | form.resetFields(_.keys(previousRow));
631 | }
632 | setSelectedRowKeys([record[rowKey]]);
633 | setFormValue(form, record, columns);
634 | if (expandedRowKeys.length > 0) {
635 | setExpandedRowKeys([record[rowKey]]);
636 | }
637 | }
638 | onSelectRow(rows);
639 | };
640 |
641 | const handleSelectRow = record => ({
642 | onClick: _event => {
643 | if (!showSelector && (editingKey === "" || editOnSelected)) {
644 | handleSelect(record,[record]);
645 | }
646 | },
647 | });
648 |
649 | const handleAdd = () => {
650 | if(editingKey === "" || editOnSelected) {
651 | let newObj = onAdd();
652 | let key = _.uniqueId(newRowKeyPrefix);
653 | if (newObj) {
654 | newObj.isNew = true;
655 | if (newObj[rowKey])
656 | key = newObj[rowKey];
657 | else
658 | newObj[rowKey] = key;
659 | } else {
660 | newObj = { [rowKey]: key, isNew: true };
661 | }
662 | setEditingKey(key);
663 | if(dataSource.length > 0) {
664 | if(selectedRowKeys.length > 0){
665 | const previousRow = dataSource.find(d => d[rowKey] === selectedRowKeys[0]);
666 | form.resetFields(_.keys(previousRow));
667 | }else {
668 | form.resetFields(_.keys(dataSource[0]));
669 | }
670 | }
671 | setFormValue(form, newObj, columns);
672 | setExpandedRowKeys([newObj[rowKey]]);
673 | setSelectedRowKeys([newObj[rowKey]]);
674 | onSelectRow([newObj]);
675 | const result = updateChangedData(changedData, newObj, rowKey);
676 | onChangedDataUpdate(result);
677 | }
678 | };
679 |
680 | const handleRemove = (item,isDelete) => {
681 | const result = updateChangedData(changedData, { ...item, isDelete }, rowKey);
682 | onChangedDataUpdate(result);
683 | if (item.isNew && item[rowKey] === editingKey)
684 | setEditingKey('');
685 | onSelectRow([{ ...item, isDelete }]);
686 | };
687 |
688 | const handleUpdate = (record,row)=>{
689 | let updateRow = _.pickBy(row, (value) => !_.isUndefined(value));
690 | for (let key in updateRow) {
691 | if (moment.isMoment(updateRow[key])) {
692 | updateRow[key] = updateRow[key].format(updateRow[key]._f);
693 | }
694 | }
695 | updateRow = _.pickBy(updateRow, (_value,key) => !_.isEqual(updateRow[key],record[key]) && !(_.isObject(updateRow[key]) && _.isMatch(record[key],updateRow[key])));
696 | const updateData = changedData;
697 | if (record.isNew && !record.isUpdate) {
698 | if(row[rowKey]) {
699 | record[rowKey] = row[rowKey];
700 | _.last(updateData)[rowKey] = row[rowKey];
701 | setSelectedRowKeys([row[rowKey]]);
702 | onSelectRow([record]);
703 | }
704 | }
705 | afterEdit({ [rowKey]: record[rowKey], ...updateRow, isUpdate: true });
706 | const result = updateChangedData(updateData, { [rowKey]: record[rowKey], ...updateRow, isUpdate: true }, rowKey);
707 | onChangedDataUpdate(result);
708 | if(!editOnSelected)
709 | setEditingKey('');
710 | };
711 |
712 | const handleEditOk = record => {
713 | form.validateFields().then(row => {
714 | handleUpdate(record, row);
715 | }).catch(errorInfo => {
716 | if (errorInfo.outOfDate) {
717 | handleEditOk(record);
718 | }
719 | return errorInfo;
720 | });
721 | };
722 |
723 | const handleDownload = () => {
724 | let allData = data;
725 | if (onDownload) {
726 | allData = onDownload(filter, sorter);
727 | }
728 | exportCSV({ name: 'table', header: flatCols(columnSeq), data: allData });
729 | };
730 |
731 | const handleFormChange = (values) => {
732 | if(editOnSelected || editingKey === "" && expandedRowKeys.length > 0){
733 | handleUpdate(dataSource.find(d => d[rowKey] === selectedRowKeys[0]),values);
734 | }
735 | };
736 |
737 | const handleFilterClear = () => {
738 | if (!_.isEmpty(filter)) {
739 | if(dataSource.length > 0) {
740 | form.resetFields(_.keys(dataSource[0]));
741 | }
742 | handleTableChange({ currentPage: 1 }, { clear: true });
743 | }
744 | };
745 |
746 | const getColumns = () => {
747 | let cols1 = columnSeq.map(c => {
748 | if (c.visible) {
749 | return c;
750 | }
751 | }).filter(c => c !== undefined);
752 | if (showOpBtn) {
753 | cols1 = cols1.concat({
754 | title: i18n['op'],
755 | align: 'center',
756 | fixed: scroll && scroll.x ? 'right' : null,
757 | width: editOnSelected ? 60 : 100,
758 | render: (_text, record) => {
759 | const editing = record[rowKey] === editingKey;
760 | return (
761 | <>
762 | {canEdit(record) && !editOnSelected &&
763 | (editing ? (
764 | <>
765 |
766 | {
767 | handleEditOk(record);
768 | e.stopPropagation();
769 | }} style={{ marginRight: 8 }}/>
770 |
771 | {
772 | (!record.isNew || record.isUpdate) &&
773 |
774 | {
775 | setEditingKey('');
776 | e.stopPropagation();
777 | }}/>
778 |
779 | }
780 | >
781 | ) : (
782 | !editOnSelected && {
783 | if (editingKey === '') {
784 | beforeEdit(record);
785 | const previousRow = dataSource.find(d => d[rowKey] === selectedRowKeys[0]);
786 | form.resetFields(_.keys(previousRow));
787 | setFormValue(form, record, columns);
788 | setSelectedRowKeys([record[rowKey]]);
789 | setEditingKey(record[rowKey]);
790 | setExpandedRowKeys([record[rowKey]]);
791 | }
792 | e.stopPropagation();
793 | }}>
794 |
795 |
796 |
797 |
798 | ))}
799 | {canEdit(record) && canRemove(record) && record[rowKey] && !editOnSelected && }
800 | {canRemove(record) && (record[rowKey] || !canEdit(record)) && (
801 |
802 | <>
803 | {!record.isDelete && {
804 | handleRemove(record,true);
805 | e.stopPropagation();
806 | }}/>}
807 | {record.isDelete && {
808 | handleRemove(record,false);
809 | e.stopPropagation();
810 | }}/>}
811 | >
812 |
813 | )}
814 | >
815 | );
816 | },
817 | });
818 | }
819 | return cols1.map((col, idx) => initChildCols(col, idx, editingKey, rowKey));
820 | };
821 | useEffect(() => {
822 | setColumnSeq(cols.map((c, idx) => ({ ...c, idx, visible: true })));
823 | if (!allCols || allCols.length === 0) {
824 | setAllColumnSeq(cols);
825 | } else {
826 | setAllColumnSeq(allCols);
827 | }
828 | }, [cols]);
829 | useEffect(() => {
830 | setColumns(getColumns());
831 | }, [editingKey, changedData, columnSeq]);
832 | useEffect(()=>{
833 | if(selectedRowKeys.length === 1){
834 | const updatedRow = changedData.find(c => c[rowKey] === selectedRowKeys[0]);
835 | if(updatedRow) {
836 | setFormValue(form, updatedRow, columns);
837 | }
838 | }
839 | },[changedData]);
840 | useEffect(() => setPager({ currentPage, pageSize }), [currentPage, pageSize]);
841 | useEffect(()=> {
842 | if(expandedRowKeys.length > 0 && data && data.length > 0){
843 | const updateData = data.find(d => d[rowKey] === expandedRowKeys[0]);
844 | setFormValue(form,updateData,columns);
845 | }else if(expandedFirstRow && data && data.length > 0){
846 | setExpandedRowKeys([data[0][rowKey]]);
847 | setFormValue(form,data[0],columns);
848 | setSelectedRowKeys([data[0][rowKey]]);
849 | onSelectRow([data[0]]);
850 | if(editOnSelected)
851 | setEditingKey(data[0][rowKey]);
852 | }
853 | },[data]);
854 |
855 | const expandable:any = useMemo(()=> {
856 | if(expandedRowRender){
857 | return {
858 | rowExpandable: () => editingKey === '' || expandedRowKeys.find(k=> k === editingKey),
859 | expandedRowRender,
860 | expandedRowKeys,
861 | expandIcon: ({ expanded, onExpand, record }) =>
862 | expanded ? (
863 | onExpand(record, e)} />
864 | ) : (
865 | onExpand(record, e)} />
866 | ),
867 | onExpand:(expanded, record) => {
868 | if(!editOnSelected && editingKey !== '' && record[rowKey] !== editingKey)
869 | return;
870 | if(expanded){
871 | setExpandedRowKeys([record[rowKey]]);
872 | onExpandedRow(record,true);
873 | } else {
874 | setExpandedRowKeys([]);
875 | onExpandedRow(record,false);
876 | }
877 | },
878 | }
879 | }else{
880 | return null;
881 | }
882 | },[expandedRowRender,expandedRowKeys]);
883 |
884 | const footer = () => (
885 |
886 |
887 | {showSelectRecord &&
888 | setShowSelector((e.target as HTMLInputElement).checked)}>{i18n['select']}}
890 |
891 |
892 | {
893 | !buttons && !showBottomPager && !showAddBtn ? null :
894 | (
895 | {!showBottomPager &&
}
896 |
897 | {showAddBtn && (
898 |
901 | )}
902 | {buttons}
903 |
904 | {
905 | showBottomPager &&
906 |
{
913 | return `${i18n['total.prefix']} ${t} ${i18n['total.suffix']}`;
914 | }}
915 | onChange={(current, size) => handleTableChange({ currentPage: current, pageSize: size })}
916 | onShowSizeChange={(current, size) => handleTableChange({ currentPage: current, pageSize: size })}
917 | current={pager.currentPage}
918 | pageSize={pager.pageSize}
919 | total={total}
920 | />
921 | }
922 | )
923 | }
924 |
925 |
926 | );
927 |
928 | const components = {
929 | header: {
930 | wrapper: EditableHWrapper,
931 | cell: ResizeableCell,
932 | },
933 | body: {
934 | row: EditableRow,
935 | cell: EditableCell,
936 | },
937 | };
938 | const columnsFilter = (
943 |
944 | c.dataIndex === item.dataIndex && c.visible)}
945 | onChange={(e) => {
946 | if (e.target.checked) {
947 | let flag = true;
948 | setColumnSeq(columnSeq.map(c => {
949 | if (c.dataIndex === item.dataIndex) {
950 | flag = false;
951 | c.visible = true;
952 | }
953 | return c;
954 | }));
955 | if (flag) { // Make sure to insert it in the current position
956 | let insertIdx = 0;
957 | const tempCols = columnSeq.map(c => {
958 | if (c.idx < idx) {
959 | insertIdx = c.idx;
960 | } else {
961 | c.idx++;
962 | }
963 | return c;
964 | });
965 | setColumnSeq([...tempCols.slice(0, insertIdx + 1), {
966 | ...item,
967 | idx,
968 | visible: true,
969 | }, ...tempCols.slice(insertIdx + 1)]);
970 | }
971 | } else {
972 | setColumnSeq(columnSeq.map(c => {
973 | if (c.dataIndex === item.dataIndex) {
974 | c.visible = false;
975 | }
976 | return c;
977 | }));
978 | }
979 | }}>{item.title}
980 |
981 | )}
982 | />;
983 |
984 | const table = }}
987 | bordered={bordered}
988 | size="middle"
989 | rowKey={rowKey}
990 | rowSelection={rowSelection}
991 | footer={showFooter ? footer : undefined}
992 | pagination={false}
993 | loading={loading}
994 | components={components}
995 | columns={columns}
996 | dataSource={dataSource}
997 | onChange={(p, f, s) => handleTableChange(p, f, s)}
998 | onRow={handleSelectRow}
999 | scroll={scroll}
1000 | expandable={expandable}
1001 | {...rest} />;
1002 |
1003 | const header =
1004 |
1005 |
{title}
1006 | {title &&
}
1007 |
1008 | {showToolbar &&
1009 | <>
1010 |
1011 | <>
1012 | {filterVisible && setFilterVisible(!filterVisible)}
1013 | style={{ cursor: loading ? 'not-allow' : 'pointer', color: loading ? '#ddd' : '#666', }} />}
1014 | {!filterVisible && setFilterVisible(!filterVisible)}
1015 | style={{ cursor: loading ? 'not-allow' : 'pointer', color: loading ? '#ddd' : '#666', }} />}
1016 | >
1017 |
1018 |
1019 |
1020 |
1021 |
1022 | {
1024 | if(!loading) {
1025 | handleTableChange({ currentPage: 1 })
1026 | }}}/>
1027 |
1028 |
1029 | setColumnsPopVisible(visible)}
1035 | >
1036 |
1037 |
1038 |
1039 | >
1040 | }
1041 | {showTopPager && (
1042 | <>
1043 | {showToolbar &&
}
1044 |
{
1053 | if(!loading) {
1054 | handleTableChange({ currentPage: current, pageSize: size })
1055 | }
1056 | }}
1057 | style={{ display: 'inline-block', marginRight: 4 }}
1058 | />
1059 | {`${i18n['total.prefix']} ${total} ${i18n['total.suffix']}`}
1060 | >
1061 | )}
1062 |
1063 |
1064 |
1065 |
1066 | handleDownload()}/>
1067 |
1068 |
1069 | <>
1070 | {collapsed && setCollapsed(!collapsed)}/>}
1071 | {!collapsed && setCollapsed(!collapsed)}/>}
1072 | >
1073 |
1074 |
1075 |
;
1076 | return (
1077 |
1090 |
1091 | {
1092 | showHeader && header
1093 | }
1094 | {
1095 | !collapsed &&
1096 | (parentForm ?
1097 | <>{table}> :
1098 |
1101 | )
1102 | }
1103 |
1104 |
1105 | );
1106 | };
1107 |
1108 | interface StateProps {
1109 | currentPage?: number;
1110 | }
1111 |
1112 | const ETableHOC = (ETableComponent) => (
1113 | class extends Component {
1114 | public resetTable: () => void;
1115 |
1116 | constructor(props) {
1117 | super(props);
1118 | this.state = {
1119 | currentPage: 1,
1120 | };
1121 | this.resetTable = () => {
1122 | this.setState({ currentPage: 0 });
1123 | this.setState({ currentPage: 1 });
1124 | };
1125 | }
1126 |
1127 | render() {
1128 | return ;
1129 | }
1130 | }
1131 | );
1132 |
1133 | export default ETableHOC(EditableTable);
1134 |
--------------------------------------------------------------------------------
/src/locales/en-US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'filter.expand': 'Expand Filter',
3 | 'filter.collapse': 'Collapse Filter',
4 | 'filter.clear': 'Clear Filter',
5 | 'total.prefix': 'Total',
6 | 'total.suffix': 'Rows',
7 | 'add': 'Add',
8 | 'edit': 'Edit',
9 | 'delete': 'Delete',
10 | 'undelete': 'UnDelete',
11 | 'ok': 'Ok',
12 | 'cancel': 'Cancel',
13 | 'op': 'Operation',
14 | 'search': 'Search',
15 | 'columns': 'Columns',
16 | 'download': 'Download',
17 | 'select': 'Select Record',
18 | 'collapse': 'Collapse Table',
19 | 'expand': 'Expand Table',
20 | 'empty': 'No Data',
21 | };
22 |
--------------------------------------------------------------------------------
/src/locales/index.js:
--------------------------------------------------------------------------------
1 | import en from './en-US'
2 | import zh from './zh-CN'
3 | import pt_br from './pt-BR'
4 | export default {
5 | en,
6 | pt_br,
7 | zh,
8 | }
--------------------------------------------------------------------------------
/src/locales/pt-BR.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'filter.expand': 'Expandir filtro',
3 | 'filter.collapse': 'Esconder filtro',
4 | 'filter.clear': 'Remover filtro',
5 | 'total.prefix': 'Total',
6 | 'total.suffix': 'Linhas',
7 | 'add': 'Adicionar',
8 | 'edit': 'Editar',
9 | 'delete': 'Excluir',
10 | 'undelete': 'Desfazer excluir',
11 | 'ok': 'Ok',
12 | 'cancel': 'Cancelar',
13 | 'op': 'Ação',
14 | 'search': 'Procurar',
15 | 'columns': 'Colunas',
16 | 'download': 'Baixar',
17 | 'select': 'Selecionar Registro',
18 | 'collapse': 'Esconder Mesa',
19 | 'expand': 'Expandir Mesa',
20 | 'empty': 'Sem Dados',
21 | };
22 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'filter.expand': '展开过滤器',
3 | 'filter.collapse': '收起过滤器',
4 | 'filter.clear': '清除过滤器',
5 | 'total.prefix': '共',
6 | 'total.suffix': '条',
7 | 'add': '添加',
8 | 'edit': '编辑',
9 | 'delete': '删除',
10 | 'undelete': '取消删除',
11 | 'ok': '确定',
12 | 'cancel': '取消',
13 | 'op': '操作',
14 | 'search': '查询',
15 | 'columns': '显示列',
16 | 'download': '下载',
17 | 'select': '选择记录',
18 | 'collapse': '收起',
19 | 'expand': '展开',
20 | 'empty': '无数据',
21 | };
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "strictNullChecks": true,
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "experimentalDecorators": true,
8 | "jsx": "preserve",
9 | "noUnusedParameters": true,
10 | "noUnusedLocals": false,
11 | "noImplicitAny": false,
12 | "target": "es6",
13 | "lib": ["dom", "es2017","esnext"],
14 | "skipLibCheck": true,
15 | "paths": {
16 | "etable": [
17 | "src/index.tsx"
18 | ]
19 | }
20 | },
21 | "include": ["src", "typings"],
22 | "exclude": ["node_modules", "lib", "es"]
23 | }
24 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 | declare module '*.svg';
4 | declare module '*.png';
5 | declare module "*.json" {
6 | const content: object;
7 | export default content;
8 | }
9 | declare module 'react-resizable';
10 | declare module 'lodash';
11 |
--------------------------------------------------------------------------------