├── .gitignore ├── .npmrc ├── LICENSE ├── babel.config.build.js ├── babel.config.js ├── build.sh ├── config ├── dev.js ├── index.js └── prod.js ├── global.d.ts ├── package.json ├── preview.gif ├── project.config.json ├── project.private.config.json ├── readme.md ├── src ├── app.config.ts ├── app.less ├── app.ts ├── components │ └── Table │ │ ├── Table.tsx │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── style.css │ │ ├── style.less │ │ └── types.ts └── pages │ └── example │ ├── index.config.js │ └── index.tsx ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build 3 | .idea 4 | node_modules/ 5 | deploy_versions/ 6 | .temp/ 7 | .rn_temp/ 8 | .DS_Store 9 | .vscode -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /babel.config.build.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/env', 4 | '@babel/typescript', 5 | '@babel/react', 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel-preset-taro 更多选项和默认值: 2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md 3 | module.exports = { 4 | presets: [ 5 | [ 6 | 'taro', 7 | { 8 | framework: 'react', 9 | ts: true 10 | } 11 | ] 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "--- BUILD START ---" 4 | 5 | # reset 6 | rm -rf build 7 | mkdir build 8 | 9 | # move 10 | cp -r src/components/Table build 11 | 12 | # complie 13 | npx lessc build/Table/style.less build/Table/style.css -x 14 | npx babel --config-file $(pwd)/babel.config.build.js build/Table/Table.tsx --out-file build/Table/Table.js 15 | 16 | # clear 17 | rm -rf build/Table/style.less 18 | rm -rf build/Table/Table.tsx 19 | rm -rf build/Table/types.ts 20 | 21 | echo "--- BUILD FINISH ---" 22 | -------------------------------------------------------------------------------- /config/dev.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const exec = require('child_process').exec; 4 | 5 | // 监听 style.less 编译为 css 6 | const LESSC = path.resolve(__dirname, '../node_modules/.bin/lessc'); 7 | const INPUT = path.resolve(__dirname, '../src/components/Table/style.less'); 8 | const OUTPUT = path.resolve(__dirname, '../src/components/Table/style.css'); 9 | 10 | console.log('😁 正在监听 style.less 修改'); 11 | fs.watchFile(INPUT, { interval: 1000 }, (curr, prev) => { 12 | console.log('💨 开始编译 style.less'); 13 | exec(`${LESSC} ${INPUT} ${OUTPUT} -x`, (err) => { 14 | if (err) { 15 | console.log('💢 编译 style.less 出错'); 16 | return; 17 | } 18 | console.log('✅ 编译 style.less 成功'); 19 | }); 20 | }); 21 | 22 | module.exports = { 23 | env: { 24 | NODE_ENV: '"development"' 25 | }, 26 | defineConstants: {}, 27 | mini: {} 28 | }; 29 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'taro3-table', 3 | date: '2021-3-12', 4 | designWidth: 750, 5 | deviceRatio: { 6 | 640: 2.34 / 2, 7 | 750: 1, 8 | 828: 1.81 / 2 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: 'dist', 12 | plugins: [], 13 | defineConstants: {}, 14 | copy: { 15 | patterns: [], 16 | options: {} 17 | }, 18 | framework: 'react', 19 | mini: { 20 | postcss: { 21 | pxtransform: { 22 | enable: true, 23 | config: {} 24 | }, 25 | url: { 26 | enable: true, 27 | config: { 28 | limit: 1024 // 设定转换尺寸上限 29 | } 30 | }, 31 | cssModules: { 32 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 33 | config: { 34 | namingPattern: 'module', // 转换模式,取值为 global/module 35 | generateScopedName: '[name]__[local]___[hash:base64:5]' 36 | } 37 | } 38 | } 39 | } 40 | }; 41 | 42 | module.exports = function (merge) { 43 | if (process.env.NODE_ENV === 'development') { 44 | return merge({}, config, require('./dev')); 45 | } 46 | return merge({}, config, require('./prod')); 47 | }; 48 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: {}, 6 | mini: {} 7 | }; 8 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.gif'; 3 | declare module '*.jpg'; 4 | declare module '*.jpeg'; 5 | declare module '*.svg'; 6 | declare module '*.css'; 7 | declare module '*.less'; 8 | declare module '*.scss'; 9 | declare module '*.sass'; 10 | declare module '*.styl'; 11 | 12 | declare namespace NodeJS { 13 | interface ProcessEnv { 14 | TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taro3-table", 3 | "version": "1.1.2", 4 | "author": "qxtang", 5 | "main": "build/Table/index.js", 6 | "types": "build/Table/index.d.ts", 7 | "description": "基于 Taro3、React 的微信小程序端多功能表格组件", 8 | "templateInfo": { 9 | "name": "default", 10 | "typescript": true, 11 | "css": "less" 12 | }, 13 | "files": [ 14 | "build", 15 | "readme.md", 16 | "package.json" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:qxtang/taro3-table.git" 21 | }, 22 | "scripts": { 23 | "prepublishOnly": "npm run build", 24 | "build:weapp": "taro build --type weapp", 25 | "dev:weapp": "npm run build:weapp -- --watch", 26 | "build": "bash ./build.sh" 27 | }, 28 | "browserslist": [ 29 | "last 3 versions", 30 | "Android >= 4.1", 31 | "ios >= 8" 32 | ], 33 | "dependencies": { 34 | "classnames": "^2.2.6", 35 | "use-deep-compare-effect": "^1.6.1" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.13.10", 39 | "@babel/core": "^7.8.0", 40 | "@babel/runtime": "^7.7.7", 41 | "@tarojs/components": "3.0.22", 42 | "@tarojs/mini-runner": "3.0.22", 43 | "@tarojs/react": "3.0.22", 44 | "@tarojs/runtime": "3.0.22", 45 | "@tarojs/taro": "3.0.22", 46 | "@tarojs/webpack-runner": "3.0.22", 47 | "@types/react": "^16.0.0", 48 | "@types/webpack-env": "^1.13.6", 49 | "@typescript-eslint/eslint-plugin": "^2.x", 50 | "@typescript-eslint/parser": "^2.x", 51 | "babel-preset-taro": "3.0.22", 52 | "eslint": "^6.8.0", 53 | "eslint-config-taro": "3.0.22", 54 | "eslint-plugin-import": "^2.12.0", 55 | "eslint-plugin-react": "^7.8.2", 56 | "eslint-plugin-react-hooks": "^1.6.1", 57 | "less": "^4.1.2", 58 | "lessc": "^1.0.2", 59 | "react": "^16.10.0", 60 | "react-dom": "^16.10.0", 61 | "stylelint": "9.3.0", 62 | "typescript": "^3.7.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BugHunter7788/taro3-table/ed82bde9e85e8820d128d3c1353669c810889f85/preview.gif -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "dist/", 3 | "projectname": "taro3-table", 4 | "description": "项目配置文件,详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", 5 | "appid": "wxf085182fc9a94aec", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "enhance": true, 10 | "postcss": false, 11 | "preloadBackgroundData": false, 12 | "minified": false, 13 | "newFeature": false, 14 | "coverView": true, 15 | "nodeModules": false, 16 | "autoAudits": false, 17 | "showShadowRootInWxmlPanel": true, 18 | "scopeDataCheck": false, 19 | "uglifyFileName": false, 20 | "checkInvalidKey": true, 21 | "checkSiteMap": true, 22 | "uploadWithSourceMap": true, 23 | "compileHotReLoad": false, 24 | "lazyloadPlaceholderEnable": false, 25 | "useMultiFrameRuntime": true, 26 | "useApiHook": true, 27 | "useApiHostProcess": true, 28 | "babelSetting": { 29 | "ignore": [], 30 | "disablePlugins": [], 31 | "outputPath": "" 32 | }, 33 | "enableEngineNative": false, 34 | "useIsolateContext": false, 35 | "userConfirmedBundleSwitch": false, 36 | "packNpmManually": false, 37 | "packNpmRelationList": [], 38 | "minifyWXSS": true, 39 | "disableUseStrict": false, 40 | "showES6CompileOption": false, 41 | "useCompilerPlugins": false, 42 | "minifyWXML": true, 43 | "useStaticServer": true 44 | }, 45 | "compileType": "miniprogram", 46 | "condition": {}, 47 | "libVersion": "2.24.7", 48 | "srcMiniprogramRoot": "dist/", 49 | "packOptions": { 50 | "ignore": [], 51 | "include": [] 52 | }, 53 | "editorSetting": { 54 | "tabIndent": "insertSpaces", 55 | "tabSize": 2 56 | } 57 | } -------------------------------------------------------------------------------- /project.private.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectname": "taro3-table", 3 | "setting": { 4 | "compileHotReLoad": true 5 | }, 6 | "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html" 7 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # taro3-table 2 | 3 |

4 | 5 | stars 6 | 7 | 8 | forks 9 | 10 | 11 | version 12 | 13 | 14 | downloads 15 | 16 |

17 | 18 | 基于 Taro3、React 的微信小程序端多功能表格组件 19 | 20 | - 自定义样式 21 | - 单列多列排序 22 | - 自定义排序 23 | - 服务端排序 24 | - 固定表头、固定列 25 | 26 | ![](https://raw.githubusercontent.com/qxtang/taro3-table/master/preview.gif) 27 | 28 | # 注意 29 | 30 | 只能在基于 Taro3.x 和 React 的微信小程序项目中使用。 31 | 32 | # 安装 33 | 34 | ```sh 35 | npm install taro3-table 36 | ``` 37 | 38 | # 使用 39 | 40 | ```jsx 41 | import React from 'react'; 42 | import Table from 'taro3-table'; 43 | 44 | export default () => { 45 | const dataSource = [ 46 | { 47 | username: '小红', 48 | telephone: '123', 49 | }, 50 | { 51 | username: '小明', 52 | telephone: '456', 53 | }, 54 | ]; 55 | 56 | const columns = [ 57 | { 58 | title: '用户名', 59 | dataIndex: 'username', 60 | }, 61 | 62 | { 63 | title: '手机号', 64 | dataIndex: 'telephone', 65 | }, 66 | ]; 67 | 68 | return ( 69 | 74 | ); 75 | }; 76 | ``` 77 | 78 | # 示例 79 | 80 | - [使用示例](https://github.com/qxtang/taro3-table/blob/master/src/pages/example/index.tsx) 81 | 82 | # 参数说明 83 | 84 | | 参数 | 描述 | 类型 | 必传 | 默认值 | 85 | | -------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ | ---- | ------ | 86 | | columns | 表格列的配置描述,详见下方 | IColumns[] | 是 | [] | 87 | | dataSource | 数据源 | any[] | 是 | [] | 88 | | rowKey | 表格行 key 的取值 | string | 是 | | 89 | | className | 最外层包裹节点 css 类名 | string | 否 | | 90 | | style | 最外层包裹节点内联样式 | CSSProperties | 否 | | 91 | | colStyle | 单元格统一样式 | CSSProperties | 否 | | 92 | | colClassName | 单元格统一类名 | string | 否 | | 93 | | rowStyle | 行统一样式 | CSSProperties | 否 | | 94 | | rowClassName | 行统一 css 类名 | string | 否 | | 95 | | titleStyle | 统一设置表头样式 | CSSProperties | 否 | | 96 | | titleClassName | 统一设置表头单元格 css 类名 | string | 否 | | 97 | | loading | 是否加载中 | boolean | 否 | | 98 | | onChange | 表格数据变化钩子 | (dataSource: any[]) => void | 否 | | 99 | | multipleSort | 是否开启多列排序 | boolean | 否 | false | 100 | | scroll | 表格是否可滚动,也可以指定滚动区域的宽、高 | { x?: number | string | boolean, y?: number | string | boolean } | 否 | | 101 | 102 | ## column 103 | 104 | 表格列的配置描述,是 columns 中的一项: 105 | 106 | | 参数 | 描述 | 类型 | 必传 | 默认值 | 107 | | -------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---- | -------- | 108 | | title | 标题 | string | JSX.Element | 是 | | 109 | | dataIndex | 列数据在数据项中对应的路径 | string | 是 | | 110 | | key | React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 | string | 否 | | 111 | | align | 设置该列文本对齐方式 | 'left' | 'right' | 'center' | 否 | 'center' | 112 | | style | 该列单元格内联样式 | CSSProperties | 否 | | 113 | | titleStyle | 该列表头内联样式 | CSSProperties | 否 | | 114 | | className | 该列单元格 css 类名 | string | 否 | | 115 | | titleClassName | 设置该列表头单元格 css 类名 | string | 否 | | 116 | | render | 渲染函数 | (text?: any, record?: AnyOpt, index?: number) => JSX.Element | string | 否 | | 117 | | width | 列宽,单位 px,默认 100 | number | 否 | 100 | 118 | | sort | 表头是否显示排序按钮 | boolean | 否 | | 119 | | sortOrder | 排序的受控属性 | SortOrder | 否 | | 120 | | sorter | 自定义排序函数,相当于 Array.sort 的 compareFunction,需要服务端排序可设为 true | CompareFn | boolean | 否 | | 121 | | sortLevel | 多列排序优先级 | number | 否 | 0 | 122 | | onSort | 点击排序按钮钩子,常用于服务端排序 | (sortOrder: SortOrder) => void | 否 | | 123 | | fixed | 固定列 | 'left' | 'right' | 否 | | 124 | | expandable | 该列是否启用点击展开收起功能,默认 true | boolean | 否 | true | 125 | 126 | # TODO 127 | 128 | - 支持虚拟列表 129 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | import { name } from '../package.json'; 2 | 3 | export default { 4 | pages: [ 5 | 'pages/example/index', 6 | ], 7 | window: { 8 | backgroundTextStyle: 'light', 9 | navigationBarBackgroundColor: '#fbfbfb', 10 | navigationBarTitleText: name, 11 | navigationBarTextStyle: 'black' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/app.less: -------------------------------------------------------------------------------- 1 | .example { 2 | min-height: 100vh; 3 | min-width: 100vw; 4 | background-color: #fbfbfb; 5 | 6 | > .btns { 7 | width: 92vw; 8 | margin: 0 auto; 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: flex-start; 12 | align-items: center; 13 | padding: 40px 0; 14 | 15 | > button { 16 | width: 200px; 17 | padding: 0 20px; 18 | margin-right: 10px; 19 | margin-left: 0; 20 | margin-bottom: 10px; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import './app.less'; 2 | 3 | export default props => props.children; 4 | -------------------------------------------------------------------------------- /src/components/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | // base 2 | import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; 3 | import Taro from '@tarojs/taro'; 4 | import classnames from 'classnames'; 5 | import useDeepCompareEffect from 'use-deep-compare-effect'; 6 | 7 | // components 8 | import { ScrollView, Text, View } from '@tarojs/components'; 9 | 10 | // styles 11 | import './style.css'; 12 | 13 | // types 14 | import { AnyOpt, CompareFn, FixedType, IColumns, Props, SortOrder } from './types'; 15 | 16 | // constants 17 | const DEFAULT_COL_WIDTH = 100; // 默认列宽 18 | const JC_TA_MAP = { 19 | 'left': 'flex-start', 20 | 'center': 'center', 21 | 'right': 'flex-end', 22 | }; 23 | 24 | const getSize = (size: string | number): string => { 25 | if (typeof size === 'number') { 26 | return Taro.pxTransform((size as number) * 2); 27 | } else { 28 | return String(size); 29 | } 30 | }; 31 | 32 | const compare = (a, b, sortOrder: SortOrder = 'ascend'): number => { 33 | if (isNaN(Number(a)) || isNaN(Number(b))) { 34 | if (sortOrder === 'ascend') { 35 | return a.localeCompare(b); 36 | } else { 37 | return b.localeCompare(a); 38 | } 39 | } 40 | if (sortOrder === 'ascend') { 41 | return (Number(a || 0) - Number(b || 0)) || 0; 42 | } else { 43 | return (Number(b || 0) - Number(a || 0)) || 0; 44 | } 45 | }; 46 | 47 | const doSort = (opts: { columns: IColumns[]; dataSource: AnyOpt[] }) => { 48 | const { columns, dataSource } = opts; 49 | 50 | // 查找需要排序的列 51 | const sortColumns: IColumns[] = columns.filter(item => item.sortOrder) || []; 52 | 53 | if (sortColumns.length === 0) { 54 | return dataSource; 55 | } 56 | 57 | // 根据多列排序优先级对 sortColumns 进行排序,优先级高的放在最后 58 | sortColumns.sort((a, b): number => { 59 | return (a.sortLevel || 0) - (b.sortLevel || 0); 60 | }); 61 | 62 | // 计算排序结果 63 | let result: AnyOpt[] = dataSource; 64 | 65 | sortColumns.forEach((column: IColumns) => { 66 | const dataIndex: string = column.dataIndex; 67 | const sortOrder: SortOrder = column.sortOrder; 68 | const sorter: CompareFn | boolean | undefined = column.sorter; 69 | 70 | const temp: AnyOpt[] = [...result]; 71 | 72 | temp.sort((a, b): number => { 73 | if (sorter) { 74 | if (typeof sorter === 'function') { 75 | return sorter(a, b, sortOrder); 76 | } else { 77 | return 0; 78 | } 79 | } 80 | 81 | return compare(a[dataIndex], b[dataIndex], sortOrder); 82 | }); 83 | 84 | result = temp; 85 | }); 86 | 87 | return result; 88 | }; 89 | 90 | // 固定列的时候计算偏移量 91 | const calculateFixedDistance = (opt: { fixedType: FixedType; index: number; columns: IColumns[] }) => { 92 | const { fixedType, index, columns } = opt; 93 | let result: number; 94 | if (fixedType === 'left') { 95 | result = columns.reduce(function (prev, cur, i) { 96 | if ((i + 1) <= index) { 97 | return prev + (cur.width || DEFAULT_COL_WIDTH); 98 | } else { 99 | return prev; 100 | } 101 | }, 0); 102 | } else { 103 | result = columns.reduceRight(function (prev, cur, i) { 104 | if ((i - 1) >= index) { 105 | return prev + (cur.width || DEFAULT_COL_WIDTH); 106 | } else { 107 | return prev; 108 | } 109 | }, 0); 110 | } 111 | 112 | return getSize(result); 113 | }; 114 | 115 | const Loading = () => { 116 | return ( 117 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | const Empty = () => { 124 | return ( 125 | 126 | 暂无数据 127 | 128 | ); 129 | }; 130 | 131 | const Table = (props: Props): JSX.Element | null => { 132 | const { 133 | columns: pColumns = [], 134 | dataSource: pDataSource = [], 135 | rowKey = '', 136 | loading = false, 137 | className = '', 138 | style = {}, 139 | titleClassName = '', 140 | titleStyle = {}, 141 | rowClassName = '', 142 | rowStyle = {}, 143 | colStyle = {}, 144 | colClassName = '', 145 | onChange = (): void => { 146 | }, 147 | multipleSort = false, 148 | scroll = {} 149 | } = props; 150 | 151 | const [dataSource, setDataSource] = useState(pDataSource); 152 | const [columns, setColumns] = useState(pColumns); 153 | const [expansion, setExpansion] = useState(false); // 是否展开 154 | 155 | useEffect(() => { 156 | onChange(dataSource); 157 | }, [dataSource]); 158 | 159 | useDeepCompareEffect(() => { 160 | setColumns(pColumns); 161 | }, [pColumns]); 162 | 163 | // 排序 164 | useEffect(() => { 165 | const result = doSort({ columns, dataSource: pDataSource }); 166 | setDataSource(result); 167 | }, [columns, pColumns, pDataSource]); 168 | 169 | // 表头点击事件 170 | const handleClickTitle = useCallback((item: IColumns, index: number): void => { 171 | if (!item.sort || loading) { 172 | return; 173 | } 174 | 175 | const temp: IColumns[] = [...columns]; 176 | 177 | if (!multipleSort) { 178 | temp.forEach((j: IColumns, i: number): void => { 179 | if (i !== index) { 180 | delete j.sortOrder; 181 | } 182 | }); 183 | } 184 | 185 | // 连续点击循环设置排序方式 186 | const array: SortOrder[] = ['ascend', 'descend', undefined]; 187 | const curr: number = array.indexOf(temp[index].sortOrder); 188 | const next: SortOrder = temp[index].sortOrder = array[(curr + 1) % array.length]; 189 | item.onSort && item.onSort(next); 190 | setColumns(temp); 191 | }, [columns, loading]); 192 | 193 | const Title = (props: { key: any, column: IColumns, index: number }): JSX.Element => { 194 | const { 195 | column, 196 | index, 197 | } = props; 198 | 199 | return ( 200 | 217 | {column.title} 218 | { 219 | column.sort && ( 220 | 221 | 227 | 233 | 234 | ) 235 | } 236 | 237 | ); 238 | }; 239 | 240 | const Row = (props: { key: any, dataSourceItem: AnyOpt, index: number }): JSX.Element => { 241 | const { 242 | dataSourceItem, 243 | index 244 | } = props; 245 | 246 | return ( 247 | 255 | { 256 | columns.map((columnItem: IColumns, colIndex: number): JSX.Element => { 257 | const text = dataSourceItem[columnItem.dataIndex]; 258 | const expandable = columnItem.expandable !== false; 259 | let result; 260 | 261 | if (columnItem.render) { 262 | const render = columnItem.render(text, dataSourceItem, index); 263 | 264 | if (typeof render !== 'object') { 265 | result = ({render}); 266 | } else { 267 | result = render; 268 | } 269 | } else { 270 | result = ({String(text)}); 271 | } 272 | 273 | return ( 274 | {result} 292 | ); 293 | }) 294 | } 295 | 296 | ); 297 | }; 298 | 299 | const wrapWidth = useMemo((): number => { 300 | return columns.reduce(function (prev, cur) { 301 | return prev + (cur.width || DEFAULT_COL_WIDTH); 302 | }, 0); 303 | }, [columns]); 304 | 305 | return ( 306 | 313 | {loading && ()} 314 | 323 | 329 | { 330 | (columns.length === 0) ? ( 331 | 332 | ) : columns.map((item: IColumns, index: number): JSX.Element => { 333 | return ( 334 | 339 | ); 340 | }) 341 | } 342 | </View> 343 | <View className="taro3table_body"> 344 | { 345 | (dataSource.length > 0) ? dataSource.map((dataSourceItem: AnyOpt, index: number): JSX.Element => { 346 | return ( 347 | <Row 348 | key={dataSourceItem[rowKey]} 349 | dataSourceItem={dataSourceItem} 350 | index={index} 351 | /> 352 | ); 353 | }) : (<Empty/>) 354 | } 355 | </View> 356 | </ScrollView> 357 | </View> 358 | ); 359 | }; 360 | 361 | export default memo(Table); 362 | -------------------------------------------------------------------------------- /src/components/Table/index.d.ts: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, PropsWithChildren } from 'react'; 2 | 3 | export interface AnyOpt { 4 | [prop: string]: any; 5 | } 6 | 7 | export type FixedType = 'left' | 'right'; 8 | export type SortOrder = 'ascend' | 'descend' | undefined; 9 | export type CompareFn<T = AnyOpt> = (a: T, b: T, sortOrder: SortOrder) => number; 10 | 11 | export interface IColumns { 12 | title: string | JSX.Element; // 标题 13 | dataIndex: string; // 列数据在数据项中对应的路径 14 | key?: string; // React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 15 | align?: 'left' | 'right' | 'center'; // 设置该列文本对齐方式 16 | style?: CSSProperties; // 该列单元格内联样式 17 | titleStyle?: CSSProperties; // 该列表头内联样式 18 | className?: string; // 该列单元格 css 类名 19 | titleClassName?: string; // 设置该列表头单元格 css 类名 20 | render?: (text?: any, record?: AnyOpt, index?: number) => JSX.Element | string; // 渲染函数 21 | width?: number; // 列宽,单位px,默认100 22 | sort?: boolean; // 表头是否显示排序按钮 23 | sortOrder?: SortOrder; // 排序的受控属性 24 | sorter?: CompareFn | boolean; // 自定义排序函数,相当于 Array.sort 的 compareFunction,需要服务端排序可设为 true 25 | sortLevel?: number; // 多列排序优先级 26 | onSort?: (sortOrder: SortOrder) => void; // 点击排序按钮钩子,常用于服务端排序 27 | fixed?: FixedType; // 固定列 28 | expandable?: boolean; // 该列是否启用点击展开收起功能,默认 true 29 | } 30 | 31 | export interface Props extends PropsWithChildren<any> { 32 | columns: IColumns[]; // 表格列的配置描述 33 | dataSource: AnyOpt[]; // 数据源 34 | rowKey: string; // 表格行 key 的取值 35 | className?: string; // 最外层包裹节点 css 类名 36 | style?: CSSProperties; // 最外层包裹节点内联样式 37 | colStyle?: CSSProperties; // 单元格统一样式 38 | colClassName?: string; // 单元格统一类名 39 | rowStyle?: CSSProperties; // 行统一样式 40 | rowClassName?: string; // 表格行 css 类名 41 | titleStyle?: CSSProperties; // 统一设置表头样式 42 | titleClassName?: string; // 统一设置表头单元格 css 类名 43 | loading?: boolean; // 是否加载中 44 | onChange?: (dataSource: AnyOpt[]) => void; // 表格数据变化钩子 45 | multipleSort?: boolean; // 是否开启多列排序 46 | 47 | // 表格是否可滚动,也可以指定滚动区域的宽、高 48 | scroll?: { 49 | x?: number | string | boolean, 50 | y?: number | string | boolean, 51 | }; 52 | } 53 | 54 | declare const Table: React.FC<Props>; 55 | 56 | export default Table; 57 | -------------------------------------------------------------------------------- /src/components/Table/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Table'); 2 | -------------------------------------------------------------------------------- /src/components/Table/style.css: -------------------------------------------------------------------------------- 1 | @keyframes rotate{from{transform:rotate(0deg)}to{transform:rotate(359deg)}}.taro3table{box-sizing:border-box;position:relative;max-width:100vw;border-radius:8rpx;border:2rpx solid rgba(67,120,190,0.1)}.taro3table .taro3table_loading{position:absolute;height:calc(100% + 4rpx);width:calc(100% + 4rpx);left:0;right:0;top:0;bottom:0;background-color:rgba(255,255,255,0.3);backdrop-filter:blur(6rpx);z-index:94;color:#ffffff;display:flex;align-items:center;justify-content:center;font-size:28rpx}.taro3table .taro3table_loading>.taro3table_circle{height:60rpx;width:60rpx;border-top:8rpx solid rgba(153,153,153,0.7);border-right:8rpx solid rgba(153,153,153,0.7);border-bottom:8rpx solid rgba(153,153,153,0.7);border-left:8rpx solid transparent;border-radius:50%;animation:rotate 1s linear infinite}.taro3table .taro3table_table{min-height:100%;min-width:100%}.taro3table .taro3table_table .taro3table_empty{width:100%;text-align:center;font-size:24rpx;color:#cccccc;min-height:88rpx;line-height:88rpx}.taro3table .taro3table_table .taro3table_head{display:flex;align-items:center;justify-content:flex-start;width:max-content;min-width:100%;height:88rpx;background-color:#f7fbff}.taro3table .taro3table_table .taro3table_head.taro3table_scroll{position:sticky;top:0;z-index:93}.taro3table .taro3table_table .taro3table_head>.taro3table_title{display:flex;align-items:center;justify-content:center;height:88rpx;color:#3975c6;font-size:24rpx;font-family:PingFangSC-Regular,PingFang SC,sans-serif;font-weight:400;background-color:#f7fbff;box-sizing:border-box}.taro3table .taro3table_table .taro3table_head>.taro3table_title:active{opacity:.7}.taro3table .taro3table_table .taro3table_head>.taro3table_title.taro3table_fixed{position:sticky;top:0}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn{display:flex;flex-direction:column;align-items:center;justify-content:center;margin-left:10rpx}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn>.taro3table_btn{height:0;width:0}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn>.taro3table_btn.taro3table_ascend{border-top:8rpx solid transparent;border-left:8rpx solid transparent;border-right:8rpx solid transparent;border-bottom:8rpx solid #e2e2e2;margin-bottom:4rpx}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn>.taro3table_btn.taro3table_ascend.taro3table_active{border-bottom-color:#3975c6}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn>.taro3table_btn.taro3table_descend{border-top:8rpx solid #e2e2e2;border-left:8rpx solid transparent;border-right:8rpx solid transparent;border-bottom:8rpx solid transparent}.taro3table .taro3table_table .taro3table_head>.taro3table_title>.taro3table_sortBtn>.taro3table_btn.taro3table_descend.taro3table_active{border-top-color:#3975c6}.taro3table .taro3table_table .taro3table_body{min-height:120rpx}.taro3table .taro3table_table .taro3table_body>.taro3table_row{min-height:120rpx;position:relative;display:flex;align-items:center;justify-content:flex-start;height:max-content;width:max-content;min-width:100%;background-color:#ffffff;border-top:1rpx solid rgba(67,120,190,0.1)}.taro3table .taro3table_table .taro3table_body>.taro3table_row:first-child{border-top:none}.taro3table .taro3table_table .taro3table_body>.taro3table_row>.taro3table_col{min-height:120rpx;background-color:#ffffff;display:flex;align-items:center;box-sizing:border-box}.taro3table .taro3table_table .taro3table_body>.taro3table_row>.taro3table_col text{font-size:28rpx;font-family:PingFangSC-Regular,PingFang SC,sans-serif;font-weight:400;color:#222222;min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.taro3table .taro3table_table .taro3table_body>.taro3table_row>.taro3table_col.taro3table_expansion{height:100%}.taro3table .taro3table_table .taro3table_body>.taro3table_row>.taro3table_col.taro3table_expansion text{overflow:auto;text-overflow:unset;white-space:normal;word-wrap:break-word;word-break:break-all}.taro3table .taro3table_table .taro3table_body>.taro3table_row>.taro3table_col.taro3table_fixed{position:sticky;top:0;z-index:92} -------------------------------------------------------------------------------- /src/components/Table/style.less: -------------------------------------------------------------------------------- 1 | @head_height: unit(88, rpx); 2 | 3 | @keyframes rotate { 4 | from { 5 | transform: rotate(0deg); 6 | } 7 | to { 8 | transform: rotate(359deg); 9 | } 10 | } 11 | 12 | .taro3table { 13 | box-sizing: border-box; 14 | position: relative; 15 | max-width: 100vw; 16 | border-radius: unit(8, rpx); 17 | border: unit(2, rpx) solid rgba(67, 120, 190, 0.1); 18 | 19 | .taro3table_loading { 20 | position: absolute; 21 | height: calc(100% + unit(4, rpx)); 22 | width: calc(100% + unit(4, rpx)); 23 | left: 0; 24 | right: 0; 25 | top: 0; 26 | bottom: 0; 27 | background-color: rgba(255, 255, 255, 0.3); 28 | backdrop-filter: blur(unit(6, rpx)); 29 | z-index: 94; 30 | color: #ffffff; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | font-size: unit(28, rpx); 35 | 36 | > .taro3table_circle { 37 | height: unit(60, rpx); 38 | width: unit(60, rpx); 39 | border-top: unit(8, rpx) solid rgba(153, 153, 153, 0.7); 40 | border-right: unit(8, rpx) solid rgba(153, 153, 153, 0.7); 41 | border-bottom: unit(8, rpx) solid rgba(153, 153, 153, 0.7); 42 | border-left: unit(8, rpx) solid transparent; 43 | border-radius: 50%; 44 | animation: rotate 1s linear infinite; 45 | } 46 | } 47 | 48 | .taro3table_table { 49 | min-height: 100%; 50 | min-width: 100%; 51 | 52 | .taro3table_empty { 53 | width: 100%; 54 | text-align: center; 55 | font-size: unit(24, rpx); 56 | color: #cccccc; 57 | min-height: unit(88, rpx); 58 | line-height: unit(88, rpx); 59 | } 60 | 61 | .taro3table_head { 62 | display: flex; 63 | align-items: center; 64 | justify-content: flex-start; 65 | width: max-content; 66 | min-width: 100%; 67 | height: @head_height; 68 | background-color: #f7fbff; 69 | 70 | &.taro3table_scroll { 71 | position: sticky; 72 | top: 0; 73 | z-index: 93; 74 | } 75 | 76 | > .taro3table_title { 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | height: @head_height; 81 | color: #3975c6; 82 | font-size: unit(24, rpx); 83 | font-family: PingFangSC-Regular, PingFang SC, sans-serif; 84 | font-weight: 400; 85 | background-color: #f7fbff; 86 | box-sizing: border-box; 87 | 88 | &:active { 89 | opacity: 0.7; 90 | } 91 | 92 | &.taro3table_fixed { 93 | position: sticky; 94 | top: 0; 95 | } 96 | 97 | > .taro3table_sortBtn { 98 | display: flex; 99 | flex-direction: column; 100 | align-items: center; 101 | justify-content: center; 102 | margin-left: unit(10, rpx); 103 | 104 | > .taro3table_btn { 105 | @color: #e2e2e2; 106 | @active-color: #3975c6; 107 | @size: unit(8, rpx); 108 | height: 0; 109 | width: 0; 110 | 111 | &.taro3table_ascend { 112 | border-top: @size solid transparent; 113 | border-left: @size solid transparent; 114 | border-right: @size solid transparent; 115 | border-bottom: @size solid @color; 116 | margin-bottom: unit(4, rpx); 117 | 118 | &.taro3table_active { 119 | border-bottom-color: @active-color; 120 | } 121 | } 122 | 123 | &.taro3table_descend { 124 | border-top: @size solid @color; 125 | border-left: @size solid transparent; 126 | border-right: @size solid transparent; 127 | border-bottom: @size solid transparent; 128 | 129 | &.taro3table_active { 130 | border-top-color: @active-color; 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | .taro3table_body { 139 | @min-height: unit(120, rpx); 140 | min-height: @min-height; 141 | 142 | > .taro3table_row { 143 | min-height: @min-height; 144 | position: relative; 145 | display: flex; 146 | align-items: center; 147 | justify-content: flex-start; 148 | height: max-content; 149 | width: max-content; 150 | min-width: 100%; 151 | background-color: #ffffff; 152 | border-top: unit(1, rpx) solid rgba(67, 120, 190, 0.1); 153 | 154 | &:first-child { 155 | border-top: none; 156 | } 157 | 158 | > .taro3table_col { 159 | min-height: @min-height; 160 | background-color: #ffffff; 161 | display: flex; 162 | align-items: center; 163 | box-sizing: border-box; 164 | 165 | text { 166 | font-size: unit(28, rpx); 167 | font-family: PingFangSC-Regular, PingFang SC, sans-serif; 168 | font-weight: 400; 169 | color: #222222; 170 | min-width: 100%; 171 | 172 | // 收起 173 | overflow: hidden; 174 | text-overflow: ellipsis; 175 | white-space: nowrap; 176 | } 177 | 178 | // 展开 179 | &.taro3table_expansion { 180 | height: 100%; 181 | 182 | text { 183 | overflow: auto; 184 | text-overflow: unset; 185 | white-space: normal; 186 | word-wrap: break-word; 187 | word-break: break-all; 188 | } 189 | } 190 | 191 | &.taro3table_fixed { 192 | position: sticky; 193 | top: 0; 194 | z-index: 92; 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/components/Table/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, PropsWithChildren } from 'react'; 2 | 3 | export interface AnyOpt { 4 | [prop: string]: any; 5 | } 6 | 7 | export type FixedType = 'left' | 'right'; 8 | export type SortOrder = 'ascend' | 'descend' | undefined; 9 | export type CompareFn<T = AnyOpt> = (a: T, b: T, sortOrder: SortOrder) => number; 10 | 11 | export interface IColumns { 12 | title: string | JSX.Element; // 标题 13 | dataIndex: string; // 列数据在数据项中对应的路径 14 | key?: string; // React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 15 | align?: 'left' | 'right' | 'center'; // 设置该列文本对齐方式 16 | style?: CSSProperties; // 该列单元格内联样式 17 | titleStyle?: CSSProperties; // 该列表头内联样式 18 | className?: string; // 该列单元格 css 类名 19 | titleClassName?: string; // 设置该列表头单元格 css 类名 20 | render?: (text?: any, record?: AnyOpt, index?: number) => JSX.Element | string; // 渲染函数 21 | width?: number; // 列宽,单位px,默认100 22 | sort?: boolean; // 表头是否显示排序按钮 23 | sortOrder?: SortOrder; // 排序的受控属性 24 | sorter?: CompareFn | boolean; // 自定义排序函数,相当于 Array.sort 的 compareFunction,需要服务端排序可设为 true 25 | sortLevel?: number; // 多列排序优先级 26 | onSort?: (sortOrder: SortOrder) => void; // 点击排序按钮钩子,常用于服务端排序 27 | fixed?: FixedType; // 固定列 28 | expandable?: boolean; // 该列是否启用点击展开收起功能,默认 true 29 | } 30 | 31 | export interface Props extends PropsWithChildren<any> { 32 | columns: IColumns[]; // 表格列的配置描述 33 | dataSource: AnyOpt[]; // 数据源 34 | rowKey: string; // 表格行 key 的取值 35 | className?: string; // 最外层包裹节点 css 类名 36 | style?: CSSProperties; // 最外层包裹节点内联样式 37 | colStyle?: CSSProperties; // 单元格统一样式 38 | colClassName?: string; // 单元格统一类名 39 | rowStyle?: CSSProperties; // 行统一样式 40 | rowClassName?: string; // 表格行 css 类名 41 | titleStyle?: CSSProperties; // 统一设置表头样式 42 | titleClassName?: string; // 统一设置表头单元格 css 类名 43 | loading?: boolean; // 是否加载中 44 | onChange?: (dataSource: AnyOpt[]) => void; // 表格数据变化钩子 45 | multipleSort?: boolean; // 是否开启多列排序 46 | 47 | // 表格是否可滚动,也可以指定滚动区域的宽、高 48 | scroll?: { 49 | x?: number | string | boolean, 50 | y?: number | string | boolean, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/example/index.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | enablePullDownRefresh: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/pages/example/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { View, Text, Button } from '@tarojs/components'; 3 | import Taro, { usePullDownRefresh } from '@tarojs/taro'; 4 | // import Table, { IColumns } from 'taro3-table'; // 走 link 5 | import Table, { IColumns } from '../../components/Table'; 6 | // import Table, { IColumns } from '../../../build/Table'; 7 | 8 | const sleep = (s = 1000) => new Promise((r) => setTimeout(r, s)); 9 | 10 | // 模拟请求假数据 11 | const queryData = async (opt?: { page: number; page_size: number }): Promise<any> => { 12 | await sleep(1000); 13 | 14 | const { page = 1, page_size = 5 } = opt || {}; 15 | const total_rows = 53; 16 | const size = (() => { 17 | const max_page = Math.ceil(total_rows / Number(page_size)); 18 | if (Number(page) < max_page) { 19 | return page_size; 20 | } 21 | if (Number(page) === max_page) { 22 | return total_rows % Number(page_size); 23 | } 24 | return 0; 25 | })(); 26 | 27 | const list = new Array(Number(size)).fill(null).map((_, index) => { 28 | const key = String(Math.ceil(Math.random() * 1e5)); 29 | return { 30 | user_id: key, 31 | username: `name_${page}_${index}`, 32 | telephone: Math.ceil(Math.random() * 1e11), 33 | price: (Math.random() * 1e3).toFixed(2), 34 | sex: Number(Math.random() > 0.5), 35 | address: `地址_${page}_${key}`, 36 | orderInfo: { 37 | price: (Math.random() * 1e3).toFixed(2), 38 | orderName: `orderName_${key}`, 39 | createTime: `createTime_${key}`, 40 | }, 41 | createTime: new Date().toLocaleString(), 42 | status: Math.random() > 0.5, 43 | }; 44 | }); 45 | 46 | return { 47 | data: list, 48 | pager: { 49 | page, 50 | page_size, 51 | total_rows, 52 | }, 53 | }; 54 | }; 55 | 56 | export default () => { 57 | const [loading, setLoading] = useState<boolean>(false); 58 | const [dataSource, setDataSource] = useState<any[]>([]); 59 | const [columns, setColumns] = useState<IColumns[]>([ 60 | { 61 | title: '用户编号', 62 | width: 70, 63 | dataIndex: 'user_id', 64 | 65 | // 左固定列示例 66 | fixed: 'left', 67 | }, 68 | { 69 | title: '用户名', 70 | dataIndex: 'username', 71 | 72 | // 自定义render 73 | render: (t) => { 74 | return <Text style={{ color: 'red' }}>{t}</Text>; 75 | }, 76 | }, 77 | { 78 | title: '性别', 79 | dataIndex: 'sex', 80 | width: 60, 81 | render: (t) => { 82 | switch (String(t)) { 83 | case '0': 84 | return '男'; 85 | case '1': 86 | return '女'; 87 | default: 88 | return '-'; 89 | } 90 | }, 91 | }, 92 | 93 | // 服务端排序示例,结合 onSort 钩子请求后端数据 94 | { 95 | title: '手机号', 96 | dataIndex: 'telephone', 97 | sort: true, 98 | sorter: true, 99 | onSort: async (sortOrder) => { 100 | console.log('onSort -', sortOrder); 101 | 102 | setLoading(true); 103 | const { data } = await queryData(); 104 | setDataSource(data); 105 | setLoading(false); 106 | }, 107 | }, 108 | { 109 | title: '余额', 110 | dataIndex: 'price', 111 | sort: true, 112 | render: (t) => '¥' + t, 113 | }, 114 | { 115 | title: '地址', 116 | dataIndex: 'address', 117 | }, 118 | { 119 | title: '订单金额', 120 | dataIndex: 'orderInfo', 121 | render: (_, record) => record?.orderInfo?.price, 122 | 123 | // 自定义排序方式示例 124 | sort: true, 125 | sorter: (a, b, sortOrder) => { 126 | if (sortOrder === 'ascend') { 127 | return a.orderInfo.price - b.orderInfo.price; 128 | } else { 129 | return b.orderInfo.price - a.orderInfo.price; 130 | } 131 | }, 132 | }, 133 | { 134 | title: '创建时间', 135 | dataIndex: 'createTime', 136 | }, 137 | { 138 | title: '操作', 139 | dataIndex: 'status', 140 | 141 | // 右固定列示例 142 | fixed: 'right', 143 | 144 | // 禁用点击展开功能 145 | expandable: false, 146 | 147 | render: (t) => { 148 | return ( 149 | <Button type={t ? 'default' : 'warn'} size="mini"> 150 | {t ? '启用' : '禁用'} 151 | </Button> 152 | ); 153 | }, 154 | }, 155 | ]); 156 | 157 | useEffect(() => { 158 | fetchData(); 159 | }, []); 160 | 161 | // 下拉刷新示例 162 | usePullDownRefresh(() => { 163 | fetchData().then(() => { 164 | Taro.stopPullDownRefresh(); 165 | }); 166 | }); 167 | 168 | const fetchData = async (): Promise<any[]> => { 169 | setLoading(true); 170 | const { data } = await queryData(); 171 | setDataSource(data); 172 | setLoading(false); 173 | return data; 174 | }; 175 | 176 | return ( 177 | <View className="example"> 178 | <View className="btns"> 179 | <Button 180 | size="mini" 181 | onClick={(): void => { 182 | const temp = [...dataSource]; 183 | temp[0].username = `修改姓名_${Math.ceil(Math.random() * 100)}`; 184 | setDataSource(temp); 185 | }} 186 | > 187 | 修改数据 188 | </Button> 189 | <Button 190 | size="mini" 191 | onClick={(): void => { 192 | const temp = [...columns]; 193 | temp[0].title = `修改标题_${Math.ceil(Math.random() * 100)}`; 194 | setColumns(temp); 195 | }} 196 | > 197 | 修改columns 198 | </Button> 199 | <Button size="mini" onClick={fetchData}> 200 | 刷新数据 201 | </Button> 202 | <Button size="mini" onClick={setDataSource.bind(this, [])}> 203 | 清空数据 204 | </Button> 205 | <Button size="mini" onClick={setLoading.bind(this, !loading)}> 206 | loading 开关 207 | </Button> 208 | </View> 209 | 210 | <Table 211 | onChange={(v) => { 212 | console.log('onChange -', v); 213 | }} 214 | colStyle={{ padding: '0 5px' }} 215 | columns={columns} 216 | dataSource={dataSource} 217 | rowKey="user_id" 218 | loading={loading} 219 | style={{ 220 | margin: '0 auto', 221 | width: '92vw', 222 | }} 223 | // 固定表头、横向滚动 示例 224 | scroll={{ 225 | x: '100vw', 226 | y: 400, 227 | }} 228 | /> 229 | </View> 230 | ); 231 | }; 232 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "react", 19 | "jsxFactory": "React.createElement", 20 | "allowJs": true, 21 | "resolveJsonModule": true, 22 | "typeRoots": [ 23 | "node_modules/@types", 24 | "global.d.ts" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "dist" 30 | ], 31 | "compileOnSave": false 32 | } 33 | --------------------------------------------------------------------------------