├── .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 | [![NPM Version](http://img.shields.io/npm/v/antd-etable.svg?style=flat)](https://www.npmjs.org/package/antd-etable) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/antd-etable.svg?style=flat)](https://www.npmjs.org/package/antd-etable) 5 | ![](https://img.shields.io/badge/license-MIT-000000.svg) 6 | 7 | ![image](https://github.com/guozhaolong/antd-etable/raw/master/example/snapshots/1.jpg) 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 |
225 |
226 | setMultiSelect(e.target.checked)} checked={multiSelect}>多选 227 | setShowToolbar(e.target.checked)} checked={showToolbar}>显示工具栏按钮 228 | setShowAddBtn(e.target.checked)} checked={showAddBtn}>显示添加按钮 229 | setShowTopPager(e.target.checked)} checked={showTopPager}>显示顶部分页器 230 | setShowBottomPager(e.target.checked)} checked={showBottomPager}>显示底部分页器 231 | e.target.checked ? setButtons(demoButtons):setButtons(null)} checked={!!buttons}>显示底部自定义按钮 232 | setShowHeader(e.target.checked)} checked={showHeader}>显示顶栏 233 | setShowFooter(e.target.checked)} checked={showFooter}>显示底栏 234 | 235 |
236 |
237 | 238 | 239 | fetch(pager,filter,sorter)} 266 | onChangedDataUpdate={(d)=>{console.log(d);setChangedData(d)}} 267 | onAdd={()=>{console.log('onAdd');return {id:'test'+(i++)}}} 268 | onSelectRow={(rows)=>{console.log('onSelectRow',rows);setCurrentRow(rows[0])}} 269 | expandedFirstRow={false} 270 | onExpandedRow={handleExpandRow} 271 | expandedRowRender={ record => ( 272 | <> 273 | 274 | 275 | handleChangeData(record)} />} /> 276 | 277 | 278 | ({value:moment(value)})} 281 | getValueFromEvent={(e)=> moment(e).format("YYYY-MM-DD HH:mm:ss")}> 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | )} 302 | /> 303 | 304 |
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 |
1099 | {table} 1100 | 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 | --------------------------------------------------------------------------------