├── .all-contributorsrc ├── .babelrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── badge.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── assets ├── image-20181106222453747.png ├── image-20181106224258372.png ├── image-20181106224933515.png ├── image-20181106225421654.png ├── image-20181106230058138.png └── image-20181106231010055.png ├── build.sh ├── build └── rollup.config.js ├── docs ├── axios-config.md ├── basic.md ├── before-confirm.md ├── before-search.md ├── can-search-collapse.md ├── clear-selection.md ├── custom-text.md ├── expand.md ├── extra-body.md ├── extra-buttons.md ├── extra-query.sync.md ├── faq.md ├── first-page.md ├── formatter.md ├── guide-contributing-correctly.md ├── has-pagination=false.md ├── header-buttons.md ├── id.md ├── is-tree.md ├── multi-level-header.md ├── on-default-delete.md ├── on-new-edit-delete.md ├── on-response.md ├── on-success.md ├── persist-selection.md ├── save-query.md ├── search-immediately.md ├── search-item-collapsible.md ├── slot-form.md ├── slot-header.md ├── slot-no-data.md ├── slot-operation.md ├── slot-search.md └── table-props-events.md ├── netlify.sh ├── notify.sh ├── package.json ├── src ├── components │ ├── el-data-table-column.js │ ├── self-loading-button.vue │ ├── text-button.vue │ └── the-dialog.vue ├── el-data-table.d.ts ├── el-data-table.md ├── el-data-table.vue ├── index.js └── utils │ ├── extract-keys.js │ ├── query.js │ ├── search-immediately-item.js │ ├── select-strategy.js │ └── utils.js ├── styleguide.config.js ├── styleguide ├── axios.js ├── el-form-renderer.js └── element.js ├── test ├── extract-keys.test.js ├── query.test.js ├── select-strategy.test.js └── utils.test.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "el-data-table", 3 | "projectOwner": "FEMessage", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "levy9527", 15 | "name": "levy", 16 | "avatar_url": "https://avatars3.githubusercontent.com/u/9384365?v=4", 17 | "profile": "http://levy.work", 18 | "contributions": [ 19 | "code", 20 | "review", 21 | "doc", 22 | "infra", 23 | "ideas" 24 | ] 25 | }, 26 | { 27 | "login": "donaldshen", 28 | "name": "Donald Shen", 29 | "avatar_url": "https://avatars3.githubusercontent.com/u/19591950?v=4", 30 | "profile": "https://donaldshen.github.io/portfolio", 31 | "contributions": [ 32 | "code", 33 | "test", 34 | "doc" 35 | ] 36 | }, 37 | { 38 | "login": "MiffyCooper", 39 | "name": "MiffyCooper", 40 | "avatar_url": "https://avatars1.githubusercontent.com/u/20179564?v=4", 41 | "profile": "https://github.com/MiffyCooper", 42 | "contributions": [ 43 | "code", 44 | "doc" 45 | ] 46 | }, 47 | { 48 | "login": "prisbre", 49 | "name": "Huanfeng Chen", 50 | "avatar_url": "https://avatars1.githubusercontent.com/u/13557397?v=4", 51 | "profile": "https://github.com/prisbre", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "evillt", 58 | "name": "EVILLT", 59 | "avatar_url": "https://avatars3.githubusercontent.com/u/19513289?v=4", 60 | "profile": "https://evila.me", 61 | "contributions": [ 62 | "code", 63 | "bug" 64 | ] 65 | }, 66 | { 67 | "login": "Alvin-Liu", 68 | "name": "Alvin", 69 | "avatar_url": "https://avatars0.githubusercontent.com/u/11909145?v=4", 70 | "profile": "https://github.com/Alvin-Liu", 71 | "contributions": [ 72 | "code", 73 | "bug" 74 | ] 75 | }, 76 | { 77 | "login": "lianghx-319", 78 | "name": "Han", 79 | "avatar_url": "https://avatars2.githubusercontent.com/u/27187946?v=4", 80 | "profile": "https://github.com/lianghx-319", 81 | "contributions": [ 82 | "code", 83 | "bug", 84 | "doc" 85 | ] 86 | }, 87 | { 88 | "login": "kunzhijia", 89 | "name": "kunzhijia", 90 | "avatar_url": "https://avatars2.githubusercontent.com/u/4848041?v=4", 91 | "profile": "https://github.com/kunzhijia", 92 | "contributions": [ 93 | "code", 94 | "tool", 95 | "example" 96 | ] 97 | }, 98 | { 99 | "login": "chenEdgar", 100 | "name": "Edgar", 101 | "avatar_url": "https://avatars3.githubusercontent.com/u/12596622?v=4", 102 | "profile": "https://github.com/chenEdgar", 103 | "contributions": [ 104 | "code", 105 | "bug" 106 | ] 107 | }, 108 | { 109 | "login": "Barretem", 110 | "name": "Barretem", 111 | "avatar_url": "https://avatars2.githubusercontent.com/u/47966933?v=4", 112 | "profile": "https://github.com/Barretem", 113 | "contributions": [ 114 | "code" 115 | ] 116 | }, 117 | { 118 | "login": "GaryHjy", 119 | "name": "阿禹。", 120 | "avatar_url": "https://avatars1.githubusercontent.com/u/32995274?v=4", 121 | "profile": "https://github.com/GaryHjy", 122 | "contributions": [ 123 | "doc" 124 | ] 125 | }, 126 | { 127 | "login": "lujunwei", 128 | "name": "lujunwei", 129 | "avatar_url": "https://avatars0.githubusercontent.com/u/7427200?v=4", 130 | "profile": "https://github.com/lujunwei", 131 | "contributions": [ 132 | "code" 133 | ] 134 | }, 135 | { 136 | "login": "cjfff", 137 | "name": "cjf", 138 | "avatar_url": "https://avatars1.githubusercontent.com/u/20502762?v=4", 139 | "profile": "http://www.ccc1996.cn", 140 | "contributions": [ 141 | "bug", 142 | "code" 143 | ] 144 | }, 145 | { 146 | "login": "Jack-rainbow", 147 | "name": "Jack-rainbow", 148 | "avatar_url": "https://avatars1.githubusercontent.com/u/20368037?v=4", 149 | "profile": "https://github.com/Jack-rainbow", 150 | "contributions": [ 151 | "bug" 152 | ] 153 | }, 154 | { 155 | "login": "colmugx", 156 | "name": "ColMugX", 157 | "avatar_url": "https://avatars1.githubusercontent.com/u/21327913?v=4", 158 | "profile": "https://colmugx.github.io", 159 | "contributions": [ 160 | "code" 161 | ] 162 | }, 163 | { 164 | "login": "snowlocked", 165 | "name": "snowlocked", 166 | "avatar_url": "https://avatars0.githubusercontent.com/u/19562649?v=4", 167 | "profile": "https://github.com/snowlocked", 168 | "contributions": [ 169 | "code", 170 | "doc" 171 | ] 172 | }, 173 | { 174 | "login": "zcqno1", 175 | "name": "Sponge", 176 | "avatar_url": "https://avatars0.githubusercontent.com/u/11766057?v=4", 177 | "profile": "https://github.com/zcqno1", 178 | "contributions": [ 179 | "bug", 180 | "code" 181 | ] 182 | }, 183 | { 184 | "login": "gd4Ark", 185 | "name": "4Ark", 186 | "avatar_url": "https://avatars0.githubusercontent.com/u/27952659?v=4", 187 | "profile": "https://4ark.me", 188 | "contributions": [ 189 | "code", 190 | "doc" 191 | ] 192 | }, 193 | { 194 | "login": "Htongbing", 195 | "name": "Htongbing", 196 | "avatar_url": "https://avatars2.githubusercontent.com/u/36433396?v=4", 197 | "profile": "https://github.com/Htongbing", 198 | "contributions": [ 199 | "code" 200 | ] 201 | }, 202 | { 203 | "login": "PPPenny", 204 | "name": "PPPenny", 205 | "avatar_url": "https://avatars3.githubusercontent.com/u/20984729?v=4", 206 | "profile": "https://github.com/PPPenny", 207 | "contributions": [ 208 | "code" 209 | ] 210 | }, 211 | { 212 | "login": "zenghao1203", 213 | "name": "PopupDialog", 214 | "avatar_url": "https://avatars0.githubusercontent.com/u/22702128?v=4", 215 | "profile": "https://github.com/zenghao1203", 216 | "contributions": [ 217 | "bug" 218 | ] 219 | }, 220 | { 221 | "login": "Jogiter", 222 | "name": "Jogiter", 223 | "avatar_url": "https://avatars0.githubusercontent.com/u/9711036?v=4", 224 | "profile": "https://www.jogiter.cn/", 225 | "contributions": [ 226 | "code" 227 | ] 228 | }, 229 | { 230 | "login": "yolofit", 231 | "name": "yolofit", 232 | "avatar_url": "https://avatars.githubusercontent.com/u/20294811?v=4", 233 | "profile": "https://github.com/yolofit", 234 | "contributions": [ 235 | "bug" 236 | ] 237 | }, 238 | { 239 | "login": "huazaili", 240 | "name": "huazaili", 241 | "avatar_url": "https://avatars.githubusercontent.com/u/13534495?v=4", 242 | "profile": "https://github.com/huazaili", 243 | "contributions": [ 244 | "bug" 245 | ] 246 | } 247 | ], 248 | "contributorsPerLine": 7, 249 | "skipCi": true 250 | } 251 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | return { 3 | presets: [['@babel/env', {modules: api.env('test') ? 'commonjs' : false}]], 4 | plugins: [['@babel/transform-runtime']] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:jest/recommended', 13 | 'plugin:vue/recommended', 14 | 'plugin:prettier/recommended', 15 | 'prettier/vue' 16 | ], 17 | plugins: ['vue', 'prettier'], 18 | rules: { 19 | 'no-console': [ 20 | 'error', 21 | { 22 | allow: ['warn', 'error'] 23 | } 24 | ], 25 | 'no-debugger': 'error', 26 | 'prettier/prettier': 'error' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/badge.yml: -------------------------------------------------------------------------------- 1 | types: 2 | feat: 'enhancement' 3 | fix: 4 | hack: 'hack' 5 | default: 'bug' 6 | hack: 'hack' 7 | docs: 'documentation' 8 | refactor: 'refactor' 9 | style: 'style' 10 | test: 'test' 11 | perf: 'performance' 12 | chore: 13 | deps: 'dependencies' 14 | default: 'chore' 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | dist 7 | docs/build 8 | docs/index.html 9 | docs/*.woff 10 | docs/*.ttf 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | .env 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | bracketSpacing: false 4 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "no-empty-source": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | language: node_js 5 | node_js: 6 | - lts/* 7 | git: 8 | depth: 30 9 | install: 10 | - yarn --frozen-lockfile 11 | - yarn test 12 | script: 13 | - ./build.sh 14 | after_script: 15 | - ./notify.sh 16 | cache: yarn 17 | deploy: 18 | - provider: pages 19 | local-dir: docs 20 | github-token: $GITHUB_TOKEN 21 | skip-cleanup: true 22 | keep-history: true 23 | - provider: npm 24 | email: levy9527@qq.com 25 | api_key: $NPM_TOKEN 26 | skip-cleanup: true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 FEMessage 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # el-data-table 2 | 3 | [![Build Status](https://badgen.net/travis/FEMessage/el-data-table/master)](https://travis-ci.com/FEMessage/el-data-table) 4 | [![NPM Download](https://badgen.net/npm/dm/@femessage/el-data-table)](https://www.npmjs.com/package/@femessage/el-data-table) 5 | [![NPM Version](https://badgen.net/npm/v/@femessage/el-data-table)](https://www.npmjs.com/package/@femessage/el-data-table) 6 | [![NPM License](https://badgen.net/npm/license/@femessage/el-data-table)](https://github.com/FEMessage/el-data-table/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/el-data-table/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github-tools.github.io/github-release-notes/) 9 | 10 | 使用`axios`自动发送请求,支持树形结构,支持分页,支持自定义查询, 自定义操作列, 让 RESTful 风格的 CRUD 更简单 👏 11 | 12 | 内置[@femessage/el-form-renderer](https://github.com/FEMessage/el-form-renderer)用于渲染表单 13 | 14 | ![](https://i.loli.net/2019/11/14/KxfWjch5F62lwyR.jpg) 15 | 16 | ## Table of Contents 17 | 18 | - [Introduction](#introduction) 19 | - [CRUD](#crud) 20 | - [数据驱动](#数据驱动) 21 | - [Why](#why) 22 | - [Feature](#feature) 23 | - [Links](#links) 24 | - [Install](#install) 25 | - [Quick Start](#quick-start) 26 | - [Global Register Component](#global-register-component) 27 | - [Template](#template) 28 | - [Reference](#reference) 29 | - [Contributing](#contributing) 30 | - [Contributors](#contributors) 31 | - [License](#license) 32 | 33 | ## Introduction 34 | 35 | ### CRUD 36 | 37 | el-data-table 就是为了解决业务问题而生的,故而封装了 CRUD 的逻辑在里面。 38 | 39 | 以用户接口示例,设其相对路径为: 40 | 41 | ```sh 42 | /api/v1/users 43 | ``` 44 | 45 | 则其 restful CRUD 接口如下: 46 | 47 | - 查询 48 | 49 | ```sh 50 | GET /api/v1/users?page=1&size=10 51 | ``` 52 | 53 | 默认数据结构 54 | 55 | ```js 56 | { 57 | "code":0, 58 | "msg":"ok", 59 | "payload":{ 60 | "content":[], // dataPath 61 | "totalElements":2, // totalPath 62 | } 63 | } 64 | ``` 65 | 66 | 可根据实际情况设置 dataPath/totalPath 两个字段的路径 67 | 如果接口不分页, 则传 hasPagination=false, 此时默认 dataPath 为 payload, 当然此时仍可以自定义 68 | 69 | - 新增 70 | 71 | ```sh 72 | POST /api/v1/users 73 | ``` 74 | 75 | - 修改(编辑) 76 | 77 | ```sh 78 | PUT /api/v1/users/:id 79 | ``` 80 | 81 | - 删除 82 | 83 | ```sh 84 | DELETE /api/v1/users/:id 85 | ``` 86 | 87 | 则只需要使用以下代码,即可完成 CRUD 功能 88 | 89 | ```vue 90 | 93 | ``` 94 | 95 | ```js 96 | 140 | ``` 141 | 142 | 效果如下: 143 | 144 | - 查询 145 | 146 | ![](https://i.loli.net/2019/11/14/GIN79blaoPKhHmp.jpg) 147 | 148 | ![](https://i.loli.net/2019/11/14/lasJ7NhzFi4rY6n.jpg) 149 | 150 | - 新增 151 | 152 | ![](https://i.loli.net/2019/11/14/NjvFqDV24Y9QJi5.jpg) 153 | 154 | - 修改 155 | 156 | ![](https://i.loli.net/2019/11/14/5pBfd4KtMDIbOX9.jpg) 157 | 158 | - 删除 159 | 160 | ![](https://i.loli.net/2019/11/14/tEqsjIHKMbRpOXP.jpg) 161 | 162 | [⬆ Back to Top](#table-of-contents) 163 | 164 | ### 数据驱动 165 | 166 | 把 template 的内容移动到 script 中, 意味着 template 可以精简,js 可以抽取出来,方便复用;同时,js 里的数据其实就是一段 json,这也让代码生成工具有了用武之地。 167 | 168 | [⬆ Back to Top](#table-of-contents) 169 | 170 | ### Why 171 | 172 | 为什么要在 element-ui 的 el-table 的基础上封装一个 el-data-table? 173 | 174 | 我常听到有以下几种声音: 175 | 176 | 1. el-table 已可以覆盖大部分场景,暂无扩展需求 177 | 2. 封装了这么多东西,耦合太严重了 178 | 3. 涉及过多的业务逻辑,有点僵化,业务操作还是交给开发者去处理 179 | 180 | 首先 el-table 的确很灵活,只不过,在实现分页请求的时候,仅有 el-table 还不够,还需要组合 el-pagination 组件来实现。而分页处理的内容大多都是重复的,如果不封装,只会产生冗余的代码。 181 | 182 | 而中后台太多都是 CRUD 的操作,结合 restful API,使用得只传一个 url 让组件做 CRUD 成为了可能。 183 | 184 | 其次,很多有经验的“老手”觉得组件越灵活越好。 185 | 186 | 但对于经验尚浅的“新手”,他们并不熟悉常见的业务场景,对一些基本操作,如果表单校验,空格过滤,添加 loading,异常处理,他们只会漏掉,而这正是产生 bug 的源头。 187 | 188 | 对于一线的业务开发人员而言,面对做不完的业务,其实他们并不想去处理重复的业务逻辑,他们只想解放双手,早点下班。 189 | 190 | 正是在这样的背景下,产生了 el-data-table。 191 | 192 | [⬆ Back to Top](#table-of-contents) 193 | 194 | ## Feature 195 | 196 | - 只需进行 json 配置,即可实现 restful 风格的 CRUD 四个接口的对接 197 | - 支持表格内展示树形结构数据(该功能 element-ui 官方是不支持的) 198 | - 自带分页逻辑 199 | - 可扩展自定义列按钮,以及自定义操作函数 200 | - 支持分页查询后,点击详情再返回,恢复上一次的查询状态 201 | 202 | [⬆ Back to Top](#table-of-contents) 203 | 204 | ## Links 205 | 206 | - [docs](https://FEMessage.github.io/el-data-table/) 207 | - [fem-vscode-helper](https://marketplace.visualstudio.com/items?itemName=FEMessage.fem-vscode-helper) 208 | 209 | [⬆ Back to Top](#table-of-contents) 210 | 211 | ## Install 212 | 213 | encourage using [yarn](https://yarnpkg.com/en/docs/install#mac-stable) to install 214 | 215 | ```sh 216 | yarn add @femessage/el-data-table 217 | ``` 218 | 219 | [⬆ Back to Top](#table-of-contents) 220 | 221 | ## Quick Start 222 | 223 | ### Global Register Component 224 | 225 | this is for minification reason: in this way building your app, 226 | 227 | webpack or other bundler just bundle the dependencies into one vendor for all pages which using this component, 228 | 229 | instead of one vendor for one page 230 | 231 | ```js 232 | import Vue from 'vue' 233 | 234 | // register component and loading directive 235 | import ElDataTable from '@femessage/el-data-table' 236 | import ElFormRenderer from '@femessage/el-form-renderer' 237 | import { 238 | Button, 239 | Dialog, 240 | Form, 241 | FormItem, 242 | Loading, 243 | Pagination, 244 | Table, 245 | TableColumn, 246 | Message, 247 | MessageBox 248 | } from 'element-ui' 249 | 250 | Vue.use(Button) 251 | Vue.use(Dialog) 252 | Vue.use(Form) 253 | Vue.use(FormItem) 254 | Vue.use(Loading.directive) 255 | Vue.use(Pagination) 256 | Vue.use(Table) 257 | Vue.use(TableColumn) 258 | Vue.component('el-form-renderer', ElFormRenderer) 259 | Vue.component('el-data-table', ElDataTable) 260 | 261 | // to show confirm before delete 262 | Vue.prototype.$confirm = MessageBox.confirm 263 | 264 | // show tips 265 | Vue.prototype.$message = Message 266 | 267 | // if the table component cannot access `this.$axios`, it cannot send request 268 | import axios from 'axios' 269 | Vue.prototype.$axios = axios 270 | ``` 271 | 272 | ### Template 273 | 274 | ```vue 275 | 278 | ``` 279 | 280 | [⬆ Back to Top](#table-of-contents) 281 | 282 | ## Reference 283 | 284 | - [form rules detail see async-validator](https://github.com/yiminghe/async-validator) 285 | - [el-input enter to submit](https://github.com/ElemeFE/element/pull/5920) 286 | - [html spec form submission](https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2) 287 | - [What_is_a_URL](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL) 288 | - [History_API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) 289 | - [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) 290 | - [RegExp](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp) 291 | - [从 vue-router 看前端路由的两种实现](https://zhuanlan.zhihu.com/p/27588422) 292 | - [peer-dependencies](https://nodejs.org/en/blog/npm/peer-dependencies/) 293 | 294 | [⬆ Back to Top](#table-of-contents) 295 | 296 | ## Contributing 297 | 298 | For those who are interested in contributing to this project, such as: 299 | 300 | - report a bug 301 | - request new feature 302 | - fix a bug 303 | - implement a new feature 304 | 305 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md). 306 | 307 | [⬆ Back to Top](#table-of-contents) 308 | 309 | ## Contributors 310 | 311 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 312 | 313 | 314 | 315 | 316 |
levy
levy

💻 👀 📖 🚇 🤔
Donald Shen
Donald Shen

💻 ⚠️ 📖
MiffyCooper
MiffyCooper

💻 📖
Huanfeng Chen
Huanfeng Chen

💻
EVILLT
EVILLT

💻 🐛
Alvin
Alvin

💻 🐛
Han
Han

💻 🐛
kunzhijia
kunzhijia

💻 🔧 💡
Edgar
Edgar

💻 🐛
Barretem
Barretem

💻
阿禹。
阿禹。

📖
lujunwei
lujunwei

💻
cjf
cjf

🐛
Jack-rainbow
Jack-rainbow

🐛
ColMugX
ColMugX

💻
snowlocked
snowlocked

💻 📖
317 | 318 | 319 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 320 | 321 | ## License 322 | 323 | [MIT](./LICENSE) 324 | 325 | [⬆ Back to Top](#table-of-contents) 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # el-data-table 2 | 3 | [![Build Status](https://badgen.net/travis/FEMessage/el-data-table/master)](https://travis-ci.com/FEMessage/el-data-table) 4 | [![NPM Download](https://badgen.net/npm/dm/@femessage/el-data-table)](https://www.npmjs.com/package/@femessage/el-data-table) 5 | [![NPM Version](https://badgen.net/npm/v/@femessage/el-data-table)](https://www.npmjs.com/package/@femessage/el-data-table) 6 | [![NPM License](https://badgen.net/npm/license/@femessage/el-data-table)](https://github.com/FEMessage/el-data-table/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/el-data-table/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github-tools.github.io/github-release-notes/) 9 | 10 | Auto requesting by `axios`, supports pagination, tree data structure, custom search, custom operation column, which makes rest api easily👏 11 | 12 | The table uses [@femessage/el-form-renderer](https://github.com/FEMessage/el-form-renderer) to render form. 13 | 14 | ![](https://i.loli.net/2019/11/14/KxfWjch5F62lwyR.jpg) 15 | 16 | [中文文档](./README-zh.md) 17 | 18 | ## Table of Contents 19 | 20 | - [Introduction](#introduction) 21 | - [CRUD](#crud) 22 | - [Data Driven](#data-driven) 23 | - [Why](#why) 24 | - [Feature](#feature) 25 | - [Links](#links) 26 | - [Install](#install) 27 | - [Quick Start](#quick-start) 28 | - [Global Register Component](#global-register-component) 29 | - [Template](#template) 30 | - [Reference](#reference) 31 | - [Contributing](#contributing) 32 | - [Contributors](#contributors) 33 | - [License](#license) 34 | 35 | ## Introduction 36 | 37 | ### CRUD 38 | 39 | el-data-table is created to solve business problems, so CRUD logic is set inside.
For example, to develop `user` api, suppose its relative path like this: 40 | 41 | ```javascript 42 | /api/v1/users 43 | ``` 44 | 45 | The restful CRUD api should be: 46 | 47 | - Retrieve 48 | 49 | ```javascript 50 | GET /api/v1/users?page=1&size=10 51 | ``` 52 | 53 | default data structure 54 | 55 | ```js 56 | { 57 | "code":0, 58 | "msg":"ok", 59 | "payload":{ 60 | "content":[], // dataPath 61 | "totalElements":2, // totalPath 62 | } 63 | } 64 | ``` 65 | 66 | You can customize dataPath/totalPath. 67 | 68 | If `hasPagination=false`, default dataPath is `payload` 69 | 70 | - Create 71 | 72 | ```javascript 73 | POST / api / v1 / users 74 | ``` 75 | 76 | - Update 77 | 78 | ```javascript 79 | PUT /api/v1/users/:id 80 | ``` 81 | 82 | - Delete 83 | 84 | ```javascript 85 | DELETE /api/v1/users/:id 86 | ``` 87 | 88 | Then only need to use the following code to complete CRUD functions 89 | 90 | ```html 91 | 94 | ``` 95 | 96 | ```javascript 97 | 141 | ``` 142 | 143 | The results are as follows: 144 | 145 | - Retrieve 146 | ![](https://i.loli.net/2019/11/14/GIN79blaoPKhHmp.jpg)
![](https://i.loli.net/2019/11/14/lasJ7NhzFi4rY6n.jpg) 147 | 148 | - Create 149 | ![](https://i.loli.net/2019/11/14/NjvFqDV24Y9QJi5.jpg) 150 | 151 | - Update 152 | ![](https://i.loli.net/2019/11/14/5pBfd4KtMDIbOX9.jpg) 153 | 154 | - Delete 155 | ![](https://i.loli.net/2019/11/14/tEqsjIHKMbRpOXP.jpg) 156 | 157 | [⬆Back to Top](#table-of-contents) 158 | 159 | ### Data Driven 160 | 161 | Moving the content of the template to the script means that the template can be reduced and js can be extracted to another file to reuse. 162 | At the same time, the data in js is actually a piece of json, this means code generation tool can help. 163 | 164 | ![](https://i.loli.net/2019/11/14/1jpJdiNMhPHoZmF.jpg)
![](https://i.loli.net/2019/11/14/hfTaURHEOYAkoSr.jpg)
![9.jpeg](https://i.loli.net/2019/11/14/uaNq3mbWRXPk1gs.jpg) 165 | 166 | [⬆Back to Top](#table-of-contents) 167 | 168 | ### Why 169 | 170 | Why do you create el-data-table based on el-table of element-ui? 171 | 172 | I often hear the following sounds: 173 | 174 | 1. el-table can cover most scenarios without extended requirements 175 | 2. wrap up so many things, it's heavy and high coupling 176 | 3. bound with too many business logic, it's not flexible; business logic should handle by developers 177 | 178 | First of all, I have to say, el-table is really flexible, but when implementing paging requests, only el-table is not enough, and the el-pagination component needs to be combined. Most of the content of paging processing is repeated. Without a high level business component, we get duplicate code everywhere. 179 | 180 | In fact, in the admin or dashboard web app, there are many CRUD operations, using restful API. It is possible to use only one url to make a component to complete CRUD functions. 181 | 182 | Secondly, many experienced developers think that components are the more flexible the better. 183 | 184 | However, for the "newbees" who lack of experience, they are not familiar with common business scenarios. Some basic operations, like form validation, space filtering, adding loading, exception handling, they may forget, which result in bugs. 185 | 186 | For front-line business developers, in the face of endless developing task, in fact, they don't want to deal with repeated business logic. they just want to free their hands and get off work early. 187 | 188 | In such situation, el-data-table was born. 189 | 190 | [⬆Back to Top](#table-of-contents) 191 | 192 | ## Feature 193 | 194 | - Use configuration to call restful api to complete CRUD functions 195 | - Support table display tree structure data 196 | - Bound with pagination logic 197 | - Support custom column buttons, and custom operation functions 198 | - Support saving query on url, which can resotre search status after history.go(-1) or location.reload() 199 | 200 | [⬆Back to Top](#table-of-contents) 201 | 202 | ## Links 203 | 204 | - [docs](https://FEMessage.github.io/el-data-table/) 205 | - [fem-vscode-helper](https://marketplace.visualstudio.com/items?itemName=FEMessage.fem-vscode-helper) 206 | 207 | [⬆ Back to Top](#table-of-contents) 208 | 209 | ## Install 210 | 211 | Encourage using [Yarn](https://yarnpkg.com/en/docs/install#mac-stable) to install 212 | 213 | ```sh 214 | yarn add @femessage/el-data-table 215 | ``` 216 | 217 | [⬆Back to Top](#table-of-contents) 218 | 219 | ## Quick Start 220 | 221 | ### Global Register Component 222 | 223 | This is for minification reason: in this way building your app, webpack or other bundler just bundle the dependencies into one vendor for all pages which using this component, instead of one vendor for one page 224 | 225 | ```javascript 226 | import Vue from 'vue' 227 | // register component and loading directive 228 | import ElDataTable from '@femessage/el-data-table' 229 | import ElFormRenderer from '@femessage/el-form-renderer' 230 | import { 231 | Button, 232 | Dialog, 233 | Form, 234 | FormItem, 235 | Loading, 236 | Pagination, 237 | Table, 238 | TableColumn, 239 | Message, 240 | MessageBox 241 | } from 'element-ui' 242 | Vue.use(Button) 243 | Vue.use(Dialog) 244 | Vue.use(Form) 245 | Vue.use(FormItem) 246 | Vue.use(Loading.directive) 247 | Vue.use(Pagination) 248 | Vue.use(Table) 249 | Vue.use(TableColumn) 250 | Vue.component('el-form-renderer', ElFormRenderer) 251 | Vue.component('el-data-table', ElDataTable) 252 | // to show confirm before delete 253 | Vue.prototype.$confirm = MessageBox.confirm 254 | // show tips 255 | Vue.prototype.$message = Message 256 | // if the table component cannot access `this.$axios`, it cannot send request 257 | import axios from 'axios' 258 | Vue.prototype.$axios = axios 259 | ``` 260 | 261 | ### Template 262 | 263 | ```vue 264 | 267 | ``` 268 | 269 | [⬆Back to Top](#table-of-contents) 270 | 271 | ## Reference 272 | 273 | - [form rules detail see asynchronc-veriator](https://github.com/yiminghe/async-validator) 274 | - [ll-input enter to submit](https://github.com/ElemeFE/element/pull/5920) 275 | - [html spec form submission](https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2) 276 | - [what_is_a_URL](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL) 277 | - [history_API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) 278 | - [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) 279 | - [regExp](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp) 280 | - [routing implementations details in vue-router](https://zhuanlan.zhihu.com/p/27588422) 281 | - [peer-dependencies](https://nodejs.org/en/blog/npm/peer-dependencies/) 282 | 283 | [⬆Back to Top](#table-of-contents) 284 | 285 | ## Contributing 286 | 287 | For those who are interested in contributing to this project, such as: 288 | 289 | - report a bug 290 | - request new feature 291 | - fix a bug 292 | - implement a new feature 293 | 294 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md). 295 | 296 | [⬆ Back to Top](#table-of-contents) 297 | 298 | ## Contributors 299 | 300 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |

levy

💻 👀 📖 🚇 🤔

Donald Shen

💻 ⚠️ 📖

MiffyCooper

💻 📖

Huanfeng Chen

💻

EVILLT

💻 🐛

Alvin

💻 🐛

Han

💻 🐛 📖

kunzhijia

💻 🔧 💡

Edgar

💻 🐛

Barretem

💻

阿禹。

📖

lujunwei

💻

cjf

🐛 💻

Jack-rainbow

🐛

ColMugX

💻

snowlocked

💻 📖

Sponge

🐛 💻

4Ark

💻 📖

Htongbing

💻

PPPenny

💻

PopupDialog

🐛

Jogiter

💻

yolofit

🐛

huazaili

🐛
339 | 340 | 341 | 342 | 343 | 344 | 345 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 346 | 347 | ## License 348 | 349 | [MIT](https://www.yuque.com/deepexi-serverless/onx52o/LICENSE) 350 | 351 | [⬆Back to Top](#table-of-contents) 352 | -------------------------------------------------------------------------------- /assets/image-20181106222453747.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106222453747.png -------------------------------------------------------------------------------- /assets/image-20181106224258372.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106224258372.png -------------------------------------------------------------------------------- /assets/image-20181106224933515.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106224933515.png -------------------------------------------------------------------------------- /assets/image-20181106225421654.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106225421654.png -------------------------------------------------------------------------------- /assets/image-20181106230058138.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106230058138.png -------------------------------------------------------------------------------- /assets/image-20181106231010055.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEMessage/el-data-table/899acd50f84a9878c539298a0f34fe617afd4973/assets/image-20181106231010055.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | yarn stdver 3 | 4 | yarn build 5 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import vue from 'rollup-plugin-vue' 3 | import babel from 'rollup-plugin-babel' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import {terser} from 'rollup-plugin-terser' 6 | import minimist from 'minimist' 7 | 8 | const argv = minimist(process.argv.slice(2)) 9 | 10 | const config = { 11 | input: 'src/index.js', 12 | output: { 13 | name: 'ElDataTable', 14 | exports: 'named' 15 | }, 16 | plugins: [ 17 | commonjs(), 18 | vue({ 19 | css: true, 20 | compileTemplate: true 21 | }), 22 | babel({ 23 | runtimeHelpers: true, 24 | exclude: 'node_modules/**' 25 | }) 26 | ] 27 | } 28 | 29 | // Only minify browser (iife) version 30 | if (argv.format === 'iife') { 31 | config.plugins.push(terser()) 32 | } 33 | 34 | export default config 35 | -------------------------------------------------------------------------------- /docs/axios-config.md: -------------------------------------------------------------------------------- 1 | axiosConfig 示例代码,打开控制台,可以看到请求中带上了自定义的头 2 | 3 | ```vue 4 | 7 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/basic.md: -------------------------------------------------------------------------------- 1 | 基本用法,包含crud 2 | 3 | ```vue 4 | 7 | 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/before-confirm.md: -------------------------------------------------------------------------------- 1 | 自定义新增、修改请求前的操作 2 | 3 | ```vue 4 | 7 | 8 | 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/before-search.md: -------------------------------------------------------------------------------- 1 | 搜索前的操作 2 | 3 | ```vue 4 | 7 | 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/can-search-collapse.md: -------------------------------------------------------------------------------- 1 | search表单可以折叠、展开 2 | 3 | ```vue 4 | 9 | 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/clear-selection.md: -------------------------------------------------------------------------------- 1 | 使用clearSelection清空多选项;使用toggleRowSelection切换、设置行是否选中 2 | 3 | ```vue 4 | 12 | 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/custom-text.md: -------------------------------------------------------------------------------- 1 | 自定义新增、修改、删除按钮以及删除提示框的文案 2 | 3 | ```vue 4 | 7 | 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/expand.md: -------------------------------------------------------------------------------- 1 | 展开表格列 2 | 3 | ```vue 4 | 25 | 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/extra-body.md: -------------------------------------------------------------------------------- 1 | 新增、修改请求附带额外参数 2 | 3 | ```vue 4 | 7 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/extra-buttons.md: -------------------------------------------------------------------------------- 1 | 自定义操作列按钮 2 | 3 | ```vue 4 | 7 | 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/extra-query.sync.md: -------------------------------------------------------------------------------- 1 | extraQuery用sync修饰时,点击重置按钮也会重置extraQuery 2 | 3 | ```vue 4 | 22 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## 获取组件内的el-table实例 2 | 3 | ```html 4 | 7 | 14 | ``` 15 | 16 | ## 手动刷新el-data-table 17 | 18 | ```javascript 19 | this.$refs.dataTable.getList() 20 | ``` 21 | 22 | ## 自己实现 delete 操作,同时实现删除最后一页时,返回正确的最后一页 23 | 24 | ### 场景 25 | 1. 因为业务需求,data-table 默认的删除确认框不满足需求 26 | 2. 自己在外部使用 extra-buttons 实现 delete 操作 27 | 3. 遇到了[最后一页删到空,页码出现错误](https://github.com/FEMessage/el-data-table/issues/223)的问题 28 | 29 | ### 解决方案 30 | 1. 自己实现删除操作 31 | 2. 调用 correctPage 判断是否返回正确的最后一页 32 | 3. 调用 getList 刷新列表 33 | ```html 34 | 37 | 38 | 63 | ``` 64 | 65 | ## 查询后刷新 66 | 67 | ### 场景 68 | ![](https://i.loli.net/2019/11/14/7qL8sAWzPchbKnJ.png)
![](https://i.loli.net/2019/11/14/f3o76bJCZKOk5Gr.png) 69 | 70 | 71 | ### 解决方案 72 | 把select option的value变成字符串类型 73 | 74 | ### 原有代码 75 | 76 | ```javascript 77 | searchForm: [ 78 | { 79 | el: {placeholder: '请选择'}, 80 | label: '状态', 81 | id: 'status', 82 | type: 'select', 83 | options: [ 84 | { 85 | value: 1, 86 | label: '待处理' 87 | }, 88 | ] 89 | } 90 | ], 91 | ``` 92 | 93 | ### 改动后代码 94 | 95 | ```javascript 96 | searchForm: [ 97 | { 98 | el: {placeholder: '请选择'}, 99 | label: '状态', 100 | id: 'status', 101 | type: 'select', 102 | options: [ 103 | { 104 | value: '1', // 修改了这一行 105 | label: '待处理' 106 | }, 107 | ] 108 | } 109 | ], 110 | ``` 111 | 112 | ## 弹窗关闭时清空选中状态 113 | 114 | ### 场景 115 | el-data-table 在 el-dialog 中展示,在 el-dialog 关闭后,清空 el-data-table 的选中状态。 116 | 117 | ### 解决方案 118 | ![](https://i.loli.net/2019/11/14/913MoeQ6cnIOlSk.png) 119 | 120 | ```javascript 121 | cancelRelation() { 122 | this.showRelationDialog = false 123 | this.$refs.outsideModuleTable.clearSelection() 124 | } 125 | ``` 126 | 127 | ## searchForm 部分内容重置失效 128 | ### 场景 129 | 两个 data-table 在同一个页面使用时,搜索栏的重置按钮不起作用 130 | ```html 131 | 132 | 133 | ``` 134 | ### 解决方案 135 | 在el-data-table上使用key属性规避此问题。 136 | ```html 137 | 138 | 139 | ``` 140 | ### 原始issue 141 | [https://github.com/FEMessage/el-data-table/issues/119](https://github.com/FEMessage/el-data-table/issues/119) 142 | 143 | ## 点击 el-form 内部按钮触发页面刷新 144 | 145 | ### 场景 146 | 147 | el-form 包裹 el-data-table 时,点击 el-data-table 的操作列按钮会触发 form submit 的默认行为,导致页面刷新一次。 148 | 149 | ```html 150 | 151 | 152 | 153 | ``` 154 | 155 | ### 解决方案 156 | 157 | 在 el-form 上增加一个禁用 submit 默认行为的事件。 158 | 159 | ```html 160 | 163 | 164 | 165 | ``` 166 | 167 | ### 参考链接 168 | [W3C Form Submission](https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2) 169 | 170 | [element-ui el-form 文档](https://element.eleme.cn/#/zh-CN/component/form) 171 | 172 | [issue #224](https://github.com/FEMessage/el-data-table/issues/224) 173 | 174 | ## 在 TypeScript 中指定组件的类型 175 | 176 | ```html 177 | 186 | ``` 187 | > 关于更多可用类型请参考:[el-data-table.d.ts](https://github.com/FEMessage/el-data-table/blob/dev/src/el-data-table.d.ts) 188 | -------------------------------------------------------------------------------- /docs/first-page.md: -------------------------------------------------------------------------------- 1 | 接口第一页页数为0 2 | 3 | ```vue 4 | 7 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/formatter.md: -------------------------------------------------------------------------------- 1 | formatter 可以返回 jsx 渲染功能更丰富的单元格 2 | 3 | ```vue 4 | 17 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/guide-contributing-correctly.md: -------------------------------------------------------------------------------- 1 | # 正确地为组件贡献 2 | 3 | ## 关于按钮大小 4 | 5 | 我们使用 `prop.buttonSize` 统一控制按钮大小. 6 | 7 | 因此, 在开发或贡献的时候有需要添加新的按钮的情况, 需要使用 `buttonSize` 来控制, 使得风格统一. 8 | 9 | 例如这样写: 10 | 11 | ```html 12 | 新增的按钮 13 | ``` -------------------------------------------------------------------------------- /docs/has-pagination=false.md: -------------------------------------------------------------------------------- 1 | 不分页,设置has-pagination="false"即可,此时接口传参page=-1 2 | 3 | ```vue 4 | 7 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/header-buttons.md: -------------------------------------------------------------------------------- 1 | 自定义头部按钮 2 | 3 | ```vue 4 | 7 | 8 | 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/id.md: -------------------------------------------------------------------------------- 1 | 自定义主键,点击删除按钮,查看网络请求 2 | 3 | ```vue 4 | 10 | 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/is-tree.md: -------------------------------------------------------------------------------- 1 | 树形结构 2 | 3 | ```vue 4 | 7 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/multi-level-header.md: -------------------------------------------------------------------------------- 1 | 只需要在columns子项里嵌套columns数组,就可以实现多级表头 2 | 3 | **二级** 4 | ```vue 5 | 8 | 52 | ``` 53 | 54 | **更多级** 55 | ```vue 56 | 59 | 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/on-default-delete.md: -------------------------------------------------------------------------------- 1 | 有时候,el-data-table 内置删除按钮的形式、数量不满足用户需求。那么用户可以在合适的时机自行调用内部方法来删除。 2 | 下面的例子就是在多选的情况补回行删除按钮 3 | 4 | ```vue 5 | 8 | 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/on-new-edit-delete.md: -------------------------------------------------------------------------------- 1 | 新增、编辑、删除钩子 2 | 3 | ```vue 4 | 21 | 22 | 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/on-response.md: -------------------------------------------------------------------------------- 1 | 处理请求返回的数据,需要返回 total 与 data, 如果有此函数,则会忽略 totalPath/dataPath 2 | 3 | ```vue 4 | 7 | 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/on-success.md: -------------------------------------------------------------------------------- 1 | 自定义操作成功后的逻辑 2 | 3 | ```vue 4 | 7 | 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/persist-selection.md: -------------------------------------------------------------------------------- 1 | 选中效果在翻页后仍维持 2 | 3 | ```vue 4 | 7 | 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/save-query.md: -------------------------------------------------------------------------------- 1 | 通过saveQuery控制页面中只有一个table使用url来保存query参数。saveQuery默认为true。 2 | 以下示例中,第一个table会saveQuery为true, 第二个saveQuery为false 3 | 4 | ```vue 5 | 11 | 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/search-immediately.md: -------------------------------------------------------------------------------- 1 | 表单项变更时, 立即获取新的表格数据 2 | 3 | ```vue 4 | 7 | 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/search-item-collapsible.md: -------------------------------------------------------------------------------- 1 | search表单折叠并且设置部分总是可见 2 | 3 | ```vue 4 | 7 | 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/slot-form.md: -------------------------------------------------------------------------------- 1 | form插槽用法 2 | 3 | ```vue 4 | 11 | 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/slot-header.md: -------------------------------------------------------------------------------- 1 | header插槽,作用域传入selected数组 2 | 3 | ```vue 4 | 25 | 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/slot-no-data.md: -------------------------------------------------------------------------------- 1 | slot=no-data, 只在第一次请求获取数据为空时显示 2 | 3 | ```vue 4 | 11 | 25 | 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/slot-operation.md: -------------------------------------------------------------------------------- 1 | operation插槽,作用域传入当前行 row 2 | 3 | ```vue 4 | 25 | 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/slot-search.md: -------------------------------------------------------------------------------- 1 | search插槽 2 | 3 | ```vue 4 | 18 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/table-props-events.md: -------------------------------------------------------------------------------- 1 | 通过 tableAttrs & tableEventHandlers 属性,可以直接传 prop 和 eventHandler 到 el-table 本体上 2 | 3 | ```vue 4 | 7 | 33 | ``` 34 | -------------------------------------------------------------------------------- /netlify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "is netlify: $NETLIFY" 3 | echo "in branch: $BRANCH" 4 | echo "head: $HEAD" 5 | 6 | if [ "$NETLIFY" != "true" ] 7 | then 8 | echo "this script only runs in netlify, bye" 9 | exit 1 10 | fi 11 | 12 | if [ "$BRANCH" != "dev" ] && [ "$HEAD" != "dev" ] 13 | then 14 | yarn doc 15 | else 16 | echo "this script only runs in targeting dev's PR deploy preview, bye" 17 | fi 18 | -------------------------------------------------------------------------------- /notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ "$TRAVIS_TEST_RESULT" != "0" ] 3 | then 4 | echo "build not success, bye" 5 | exit 1 6 | fi 7 | 8 | ORG_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 1) 9 | REPO_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 2) 10 | 11 | git remote add github https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git > /dev/null 2>&1 12 | git push github HEAD:master --follow-tags 13 | 14 | GREN_GITHUB_TOKEN=$GITHUB_TOKEN yarn release 15 | 16 | url=https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/latest 17 | resp_tmp_file=resp.tmp 18 | 19 | curl -H "Authorization: token $GITHUB_TOKEN" $url > $resp_tmp_file 20 | 21 | html_url=$(sed -n 5p $resp_tmp_file | sed 's/\"html_url\"://g' | awk -F '"' '{print $2}') 22 | body=$(grep body < $resp_tmp_file | sed 's/\"body\"://g;s/\"//g') 23 | version=$(echo $html_url | awk -F '/' '{print $NF}') 24 | 25 | msg='{"msgtype": "markdown", "markdown": {"title": "'$REPO_NAME'更新", "text": "@所有人\n# ['$REPO_NAME'('$version')]('$html_url')\n'$body'"}}' 26 | 27 | curl -X POST https://oapi.dingtalk.com/robot/send\?access_token\=$DINGTALK_ROBOT_TOKEN -H 'Content-Type: application/json' -d "$msg" 28 | 29 | rm $resp_tmp_file 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@femessage/el-data-table", 3 | "version": "1.2.1", 4 | "description": "base on element-ui, makes crud easily", 5 | "author": "https://github.com/FEMessage", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/FEMessage/el-data-table.git" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "element-ui", 14 | "table", 15 | "data-table", 16 | "element table" 17 | ], 18 | "files": [ 19 | "src", 20 | "dist" 21 | ], 22 | "main": "dist/el-data-table.umd.js", 23 | "module": "dist/el-data-table.esm.js", 24 | "unpkg": "dist/el-data-table.min.js", 25 | "browser": { 26 | "./sfc": "src/el-data-table.vue" 27 | }, 28 | "types": "src/el-data-table.d.ts", 29 | "scripts": { 30 | "test": "jest --verbose", 31 | "dev": "vue-styleguidist server", 32 | "doc": "vue-styleguidist build", 33 | "build": "npm run build:unpkg & npm run build:es & npm run build:umd & npm run doc", 34 | "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/el-data-table.umd.js", 35 | "build:es": "rollup --config build/rollup.config.js --format es --file dist/el-data-table.esm.js", 36 | "build:unpkg": "rollup --config build/rollup.config.js --format iife --file dist/el-data-table.min.js", 37 | "stdver": "standard-version -m '[skip ci] chore(release): v%s'", 38 | "lint": "eslint \"**/*.@(js|vue)\" --fix", 39 | "release": "gren release --override" 40 | }, 41 | "dependencies": { 42 | "lodash.get": "^4.4.2", 43 | "lodash.isempty": "^4.4.0", 44 | "lodash.kebabcase": "^4.1.1", 45 | "lodash.values": "^4.3.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.4.3", 49 | "@babel/plugin-transform-runtime": "^7.4.3", 50 | "@babel/preset-env": "^7.4.3", 51 | "@femessage/el-form-renderer": "^1.6.0", 52 | "@femessage/github-release-notes": "^0.19.0", 53 | "@femessage/types": "^1.0.3", 54 | "axios": "^0.19.0", 55 | "babel-eslint": "^10.0.3", 56 | "babel-loader": "^8.0.5", 57 | "element-ui": "2.4.11", 58 | "eslint": "^6.6.0", 59 | "eslint-config-prettier": "^6.5.0", 60 | "eslint-plugin-jest": "^23.0.2", 61 | "eslint-plugin-prettier": "^3.1.1", 62 | "eslint-plugin-vue": "^6.0.0", 63 | "file-loader": "^3.0.1", 64 | "glob": "^7.1.3", 65 | "husky": "1.3.1", 66 | "jest": "^24.8.0", 67 | "less": "^3.9.0", 68 | "less-loader": "^5.0.0", 69 | "lint-staged": "^8.1.0", 70 | "minimist": "^1.2.0", 71 | "prettier": "1.18.2", 72 | "rollup": "^1.9.0", 73 | "rollup-plugin-babel": "^4.3.2", 74 | "rollup-plugin-commonjs": "^9.3.4", 75 | "rollup-plugin-terser": "^4.0.4", 76 | "rollup-plugin-vue": "^4.7.2", 77 | "standard-version": "^6.0.1", 78 | "stylelint": "^9.10.0", 79 | "stylelint-config-standard": "^18.2.0", 80 | "vue": "^2.6.10", 81 | "vue-loader": "^15.7.1", 82 | "vue-styleguidist": "^3.16.3", 83 | "vue-template-compiler": "^2.5.16", 84 | "webpack": "^4.29.6" 85 | }, 86 | "publishConfig": { 87 | "access": "public" 88 | }, 89 | "vue-sfc-cli": "1.11.4", 90 | "engines": { 91 | "node": ">= 8.3.0", 92 | "npm": ">= 3.0.0" 93 | }, 94 | "husky": { 95 | "hooks": { 96 | "pre-commit": "lint-staged", 97 | "post-commit": "git update-index --again", 98 | "pre-push": "yarn test" 99 | } 100 | }, 101 | "lint-staged": { 102 | "*.@(md|json)": [ 103 | "prettier --write", 104 | "git add" 105 | ], 106 | "*.js": [ 107 | "eslint --fix", 108 | "prettier --write", 109 | "git add" 110 | ], 111 | "*.vue": [ 112 | "eslint --fix", 113 | "prettier --write", 114 | "stylelint --fix", 115 | "git add" 116 | ] 117 | }, 118 | "gren": "@femessage/grenrc" 119 | } 120 | -------------------------------------------------------------------------------- /src/components/el-data-table-column.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'el-data-table-column', 3 | functional: true, 4 | render(h, {data, props}) { 5 | let children = [] 6 | const align = props.align 7 | if (props.columns) { 8 | children = props.columns.map(column => 9 | h('el-data-table-column', { 10 | props: Object.assign({}, {align}, column) 11 | }) 12 | ) 13 | } 14 | data.props = { 15 | ...data.props 16 | } 17 | return h('el-table-column', data, children) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/self-loading-button.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 74 | -------------------------------------------------------------------------------- /src/components/text-button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 44 | -------------------------------------------------------------------------------- /src/components/the-dialog.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/el-data-table.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, {VueConstructor, VNode} from 'vue' 2 | 3 | import {AxiosRequestConfig} from 'axios' 4 | 5 | import {Table,Form, Dialog} from '@femessage/types/element-ui' 6 | import {TableColumn} from '@femessage/types/element-ui/table-column' 7 | 8 | declare module '@femessage/el-data-table' { 9 | class FemessageComponent extends Vue { 10 | static install(vue: typeof Vue): void 11 | } 12 | 13 | type CombinedVueInstance< 14 | Instance extends Vue, 15 | Data, 16 | Methods, 17 | Computed, 18 | Props 19 | > = Data & Methods & Computed & Props & Instance 20 | 21 | type ExtendedVue< 22 | Instance extends Vue, 23 | Data, 24 | Methods, 25 | Computed, 26 | Props 27 | > = VueConstructor< 28 | CombinedVueInstance & Vue 29 | > 30 | 31 | type Combined = Data & 32 | Methods & 33 | Computed & 34 | Props 35 | 36 | type ElDataTableData = { 37 | data: any[] 38 | size: number 39 | page: number 40 | total: null | number 41 | loading: boolean 42 | selected: any[] 43 | row: any 44 | initExtraQuery: any 45 | isSearchCollapse: boolean 46 | showNoData: boolean 47 | } 48 | 49 | type ElDataTableMethods = { 50 | getList: ({loading}?: {loading?: boolean}) => void 51 | 52 | resetSearch: () => void 53 | 54 | toggleRowSelection: (row: any, isSelected?: boolean) => any 55 | 56 | clearSelection: () => void 57 | 58 | onDefaultDelete: (data: any) => void 59 | 60 | correctPage: () => void 61 | } 62 | 63 | type ElDataTableComputed = { 64 | tableEventHandlersInner: any 65 | hasSelect: boolean 66 | selectable: () => boolean 67 | columnsAlign: string 68 | routerMode: string 69 | hasSearchForm: boolean 70 | hasHeader: boolean 71 | _extraBody: object 72 | _extraQuery: object 73 | selectStrategy: any 74 | searchLocatedSlotKeys: any 75 | collapseForm: any 76 | _searchForm: any 77 | } 78 | 79 | export interface FormContentItem { 80 | id: string 81 | label?: string | VNode 82 | [key: string]: any 83 | } 84 | 85 | export type FormContent = Array 86 | 87 | export interface DataTableColumn { 88 | label: string 89 | prop: string 90 | formatter?: (row: any, column: TableColumn) => any 91 | [key: string]: any 92 | } 93 | 94 | export type DataTableColumns = Array 95 | 96 | export type OperationButton = { 97 | type?: string 98 | text: string 99 | atClick: (row: any) => Promise 100 | show?: (row: any) => boolean 101 | disabled?: (row: any) => boolean 102 | } 103 | 104 | export type OperationButtons = OperationButton[] 105 | 106 | // props 都有默认值,都是可选的 107 | export type ElDataTableProps = Partial<{ 108 | url: string 109 | id: string 110 | firstPage: number 111 | dataPath: string 112 | totalPath: string 113 | pageKey: string 114 | pageSizeKey: string 115 | columns: DataTableColumns 116 | searchForm: FormContent 117 | canSearchCollapse: boolean 118 | beforeSearch: (formValue: any) => any 119 | single: boolean 120 | persistSelection: boolean 121 | hasOperation: boolean 122 | extraButtons: OperationButtons 123 | headerButtons: OperationButtons 124 | hasNew: boolean 125 | hasEdit: boolean 126 | hasView: boolean 127 | hasDelete: boolean 128 | newText: string 129 | editText: string 130 | viewText: string 131 | deleteText: string 132 | deleteMessage: (data: any) => string 133 | canDelete: (row: any) => boolean 134 | onNew: (data: any, row?: any) => Promise 135 | onEdit: (data: any, row: any) => Promise 136 | onDelete: (data: any) => Promise 137 | onSuccess: (type: 'new' | 'edit' | 'delete', data: any) => Promise 138 | hasPagination: boolean 139 | paginationLayout: string 140 | paginationSizes: number[] 141 | paginationSize: number 142 | noPaginationSize: number 143 | isTree: boolean 144 | treeChildKey: string 145 | treeParentKey: string 146 | treeParentValue: string 147 | expandAll: boolean 148 | tableAttrs: Partial 149 | tableEventHandlers: object 150 | operationAttrs: object 151 | dialogNewTitle: string 152 | dialogEditTitle: string 153 | dialogViewTitle: string 154 | form: FormContent 155 | formAttrs: Partial 156 | dialogAttrs: Partial 157 | extraParams: object 158 | extraBody: object 159 | beforeConfirm: (data: any, isNew: boolean) => Promise 160 | customQuery: object 161 | extraQuery: object 162 | saveQuery: boolean 163 | operationButtonType: string 164 | buttonSize: string 165 | axiosConfig: AxiosRequestConfig 166 | }> 167 | 168 | type ElDataTable = Combined< 169 | ElDataTableData, 170 | ElDataTableMethods, 171 | ElDataTableComputed, 172 | Required 173 | > 174 | 175 | export interface ElDataTableType extends FemessageComponent, ElDataTable {} 176 | 177 | const ElDataTableConstruction: ExtendedVue< 178 | Vue, 179 | ElDataTableData, 180 | ElDataTableMethods, 181 | ElDataTableComputed, 182 | Required 183 | > 184 | 185 | export default ElDataTableConstruction 186 | } 187 | -------------------------------------------------------------------------------- /src/el-data-table.md: -------------------------------------------------------------------------------- 1 | ## Extra feature 2 | 3 | ### searchForm 4 | 5 | 除了原有的 el-form-renderer 的表单项配置, 每个表单项还可以添加如下配置, 6 | 7 | #### searchImmediately 8 | 9 | 表单项变更时, 立即获取新的表格数据。 10 | 11 | ```diff 12 | const content = [ 13 | { 14 | id: 'name', 15 | type: 'input', 16 | label: 'name', 17 | + searchImmediately: true 18 | } 19 | ] 20 | ``` 21 | 22 | #### collapsible 23 | 24 | 当开启 `canSearchCollapse` 折叠表单的时候, 可以设置特定表单项不可折叠(无论折叠与否, 始终显示)。 25 | 26 | ```diff 27 | const content = [ 28 | { 29 | id: 'name', 30 | type: 'input', 31 | label: 'name', 32 | + collapsible: false 33 | } 34 | ] 35 | ``` 36 | -------------------------------------------------------------------------------- /src/el-data-table.vue: -------------------------------------------------------------------------------- 1 | 287 | 288 | 1322 | 1367 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Import vue component 2 | import component from './el-data-table.vue' 3 | 4 | // `Vue.use` automatically prevents you from using the same plugin more than once, 5 | // so calling it multiple times on the same plugin will install the plugin only once 6 | component.install = Vue => { 7 | Vue.component(component.name, component) 8 | } 9 | 10 | // To auto-install when vue is found 11 | let GlobalVue = null 12 | if (typeof window !== 'undefined') { 13 | GlobalVue = window.Vue 14 | } else if (typeof global !== 'undefined') { 15 | GlobalVue = global.Vue 16 | } 17 | if (GlobalVue) { 18 | GlobalVue.use(component) 19 | } 20 | 21 | // To allow use as module (npm/webpack/etc.) export component 22 | export default component 23 | 24 | // It's possible to expose named exports when writing components that can 25 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; 26 | // export const RollupDemoDirective = component; 27 | -------------------------------------------------------------------------------- /src/utils/extract-keys.js: -------------------------------------------------------------------------------- 1 | export default ($slots, name) => { 2 | const keys = Object.keys($slots) 3 | return keys.filter(key => key.indexOf(name) > -1) 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/query.js: -------------------------------------------------------------------------------- 1 | export const valueSeparator = '~' 2 | export const paramSeparator = ',' 3 | export const paramInnerSeparator = '|' 4 | export const queryFlag = 'q=' 5 | export const queryPattern = new RegExp(queryFlag + '.*' + paramSeparator) 6 | 7 | /** 8 | * 在浏览器地址栏的 URL 需要做额外处理:兼容持久化数组 9 | * 直接将数组 encode 时,数组[1,2]会先转成'1,2'再 encode 成 '1%2C2'。但 vue-router 会自动转义 url 参数,导致数组的 ',' 无法和键值对分隔符 ',' 区分开。解决方法:改变数组的分隔符 10 | * {a: 'a&b', b: true, d: [1,2,3]} => 'a~a%26b,b~true,d~%5B1%7C2%7C3%5D' 11 | * @param {object} query 12 | * @param {string} equal - 键和值的分隔符 13 | * @param {string} delimiter - 键值对之间的分隔符 14 | * @param {string} arrayDelimiter - 数组项之间的分隔符 15 | * @return {string} 16 | */ 17 | export function stringify( 18 | query, 19 | equal = valueSeparator, 20 | delimiter = paramSeparator, 21 | arrayDelimiter = paramInnerSeparator 22 | ) { 23 | return Object.keys(query) 24 | .map(k => { 25 | // 数组即便 encode 也会被自动转义,使用别的分割符 26 | const v = JSON.stringify(query[k]) 27 | .split(',') 28 | .join(arrayDelimiter) 29 | return `${k}${equal}${encodeURIComponent(v)}` 30 | }) 31 | .join(delimiter) 32 | } 33 | 34 | /** 35 | * 转换附在url上的字符串成query对象 36 | * qs.parse只能自定义delimiter,不能自定义equal 37 | * 'a~a%26b,b~true' => {a: 'a&b', b: true} 38 | * 39 | * @param {string} query 40 | * @param {string} equal - 键和值的分隔符 41 | * @param {string} delimiter - 键值对之间的分隔符 42 | * @return {object} 43 | */ 44 | export function parse( 45 | query, 46 | equal = valueSeparator, 47 | delimiter = paramSeparator, 48 | arrayDelimiter = paramInnerSeparator 49 | ) { 50 | return query 51 | .split(delimiter) 52 | .map(param => param.split(equal)) 53 | .reduce((obj, [k, v]) => { 54 | // 替换回逗号 55 | const value = decodeURIComponent(v) 56 | .split(arrayDelimiter) 57 | .join(',') 58 | obj[k] = JSON.parse(value) 59 | return obj 60 | }, {}) 61 | } 62 | 63 | /** 64 | * 将query对象转换成str插入到url上 65 | * 66 | * @param {string} url 67 | * @param {object} query 68 | * @param {'history'|'hash'} routerMode 69 | * @returns {string} 插入了query的url 70 | */ 71 | export function set(url, query, routerMode) { 72 | const queryStr = queryFlag + stringify(query) + paramSeparator 73 | const queryPrefix = str => (str.indexOf('?') > -1 ? '&' : '?') 74 | 75 | if (queryPattern.test(url)) { 76 | return url.replace(queryPattern, queryStr) 77 | } else if (url.indexOf('#') === -1) { 78 | return url + queryPrefix(url) + queryStr 79 | } else { 80 | const [path, hash] = url.split('#') 81 | if (routerMode === 'history') { 82 | return path + queryPrefix(path) + queryStr + '#' + hash 83 | } else { 84 | return url + queryPrefix(hash) + queryStr 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * 从url中取出query对象,如果没有,返回null 91 | * 92 | * @param {string} url 93 | * @return {object|null} 对象类型的query参数 94 | */ 95 | export function get(url) { 96 | const found = url.match(queryPattern) 97 | if (!found) return null 98 | const queryStr = found[0].replace(queryFlag, '').slice(0, -1) // 移除末尾的paramSeparator 99 | return parse(queryStr) 100 | } 101 | 102 | /** 103 | * 从url中移除(?||&)queryPattern 104 | * @param {string} url 105 | */ 106 | export function clear(url) { 107 | if (queryPattern.test(url)) { 108 | const replacePattern = RegExp('[?&]' + queryPattern.source) 109 | return url.replace(replacePattern, '') 110 | } else { 111 | return url 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/utils/search-immediately-item.js: -------------------------------------------------------------------------------- 1 | export default function(content, vm) { 2 | return content.map(item => { 3 | const origOnInput = item.on && item.on.input 4 | 5 | if (item.searchImmediately) { 6 | return Object.assign(item, { 7 | on: Object.assign({}, item.on, { 8 | input: (...args) => { 9 | if (typeof origOnInput === 'function') { 10 | origOnInput.call(item, ...args) 11 | } 12 | vm.search() 13 | } 14 | }) 15 | }) 16 | } 17 | 18 | return item 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/select-strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 两种多选策略:Normal 和 PersistSelection 3 | */ 4 | 5 | /** 6 | * 多选策略接口 7 | */ 8 | class StrategyAbstract { 9 | constructor(elDataTable) { 10 | this.elDataTable = elDataTable 11 | // 绑定this后可直接在template中使用 12 | this.onSelectionChange = this.onSelectionChange.bind(this) 13 | this.onSelect = this.onSelect.bind(this) 14 | this.onSelectAll = this.onSelectAll.bind(this) 15 | } 16 | get elTable() { 17 | return ( 18 | this.elDataTable.$refs.table || { 19 | onSelectionChange() {}, 20 | onSelect() {}, 21 | onSelectAll() {}, 22 | toggleRowSelection() {}, 23 | clearSelection() {}, 24 | updateElTableSelection() {} 25 | } 26 | ) 27 | } 28 | onSelectionChange() {} 29 | onSelect() {} 30 | onSelectAll() {} 31 | toggleRowSelection() {} 32 | clearSelection() {} 33 | updateElTableSelection() {} 34 | } 35 | 36 | /** 37 | * 普通策略。由el-table维护selected 38 | */ 39 | class StrategyNormal extends StrategyAbstract { 40 | /** 41 | * normal模式下只需要监听selection-change事件 42 | */ 43 | onSelectionChange(val) { 44 | this.elDataTable.selected = val 45 | } 46 | /** 47 | * toggleRowSelection和clearSelection的表现与el-table一致 48 | */ 49 | toggleRowSelection(...args) { 50 | return this.elTable.toggleRowSelection(...args) 51 | } 52 | clearSelection() { 53 | return this.elTable.clearSelection() 54 | } 55 | } 56 | 57 | /** 58 | * 跨页保存多选策略。手动维护selected数组 59 | */ 60 | class StrategyPersistSelection extends StrategyAbstract { 61 | /** 62 | * el-table的selection-change事件不适用于开启跨页保存的情况。 63 | * 比如,当开启persistSelection时,发生以下两个场景: 64 | * 1. 用户点击翻页 65 | * 2. 用户点击行首的切换全选项按钮,清空当前页多选项数据 66 | * 其中场景1应该保持selected不变;而场景2只应该从selected移除当前页所有行,保留其他页面的多选状态。 67 | * 但el-table的selection-change事件在两个场景中无差别发生,所以这里不处理这个事件 68 | */ 69 | 70 | /** 71 | * 用户切换某一行的多选 72 | */ 73 | onSelect(selection, row) { 74 | const isChosen = selection.indexOf(row) > -1 75 | this.toggleRowSelection(row, isChosen) 76 | } 77 | /** 78 | * 用户切换当前页的多选 79 | */ 80 | onSelectAll(selection, selectable = () => true) { 81 | const isSelected = !!selection.length 82 | this.elDataTable.data.forEach(r => { 83 | if (selectable(r)) { 84 | this.toggleRowSelection(r, isSelected) 85 | } 86 | }) 87 | } 88 | /** 89 | * toggleRowSelection和clearSelection管理elDataTable的selected数组 90 | * 记得最后要将状态同步到el-table中 91 | */ 92 | toggleRowSelection(row, isSelected) { 93 | const {id, selected} = this.elDataTable 94 | const foundIndex = selected.findIndex(r => r[id] === row[id]) 95 | if (typeof isSelected === 'undefined') { 96 | if (foundIndex > -1) { 97 | selected.splice(foundIndex, 1) 98 | } else { 99 | selected.push(row) 100 | } 101 | } else if (isSelected && foundIndex === -1) { 102 | selected.push(row) 103 | } else if (!isSelected && foundIndex > -1) { 104 | selected.splice(foundIndex, 1) 105 | } 106 | this.updateElTableSelection() 107 | } 108 | clearSelection() { 109 | this.elDataTable.selected = [] 110 | this.updateElTableSelection() 111 | } 112 | /** 113 | * 将selected状态同步到el-table中 114 | */ 115 | updateElTableSelection() { 116 | const {data, id, selected} = this.elDataTable 117 | data.forEach(r => { 118 | const isSelected = !!selected.find(r2 => r[id] === r2[id]) 119 | this.elTable.toggleRowSelection(r, isSelected) 120 | }) 121 | } 122 | } 123 | 124 | export default function getSelectStrategy(elDataTable) { 125 | return elDataTable.persistSelection 126 | ? new StrategyPersistSelection(elDataTable) 127 | : new StrategyNormal(elDataTable) 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | export const isFalsey = value => 2 | ['', undefined, null].indexOf(value) > -1 || 3 | (Array.isArray(value) && value.length === 0) 4 | 5 | export const removeEmptyKeys = query => 6 | Object.keys(query) 7 | .filter(k => { 8 | const value = query[k] 9 | return Array.isArray(value) 10 | ? value.length !== 0 11 | : !['', undefined, null].includes(value) 12 | }) 13 | .reduce((obj, k) => ((obj[k] = query[k]), obj), {}) 14 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const {VueLoaderPlugin} = require('vue-loader') 2 | const path = require('path') 3 | const glob = require('glob') 4 | 5 | const sections = (() => { 6 | const docs = glob 7 | .sync('docs/*.md') 8 | .map(p => ({name: path.basename(p, '.md'), content: p})) 9 | const demos = [] 10 | let faq = '' // 约定至多只有一个faq.md 11 | const guides = [] 12 | docs.forEach(d => { 13 | if (/^faq$/i.test(d.name)) { 14 | d.name = d.name.toUpperCase() 15 | faq = d 16 | } else if (/^guide-/.test(d.name)) { 17 | guides.push(d) 18 | } else { 19 | demos.push(d) 20 | } 21 | }) 22 | return [ 23 | { 24 | name: 'Components', 25 | components: 'src/el-data-table.vue', 26 | usageMode: 'expand' 27 | }, 28 | { 29 | name: 'Demo', 30 | sections: demos, 31 | sectionDepth: 2 32 | }, 33 | ...(faq ? [faq] : []), 34 | ...(guides.length ? [{name: 'Guide', sections: guides}] : []) 35 | ] 36 | })() 37 | 38 | module.exports = { 39 | styleguideDir: 'docs', 40 | pagePerSection: true, 41 | jsxInExamples: true, 42 | ribbon: { 43 | url: 'https://github.com/FEMessage/el-data-table' 44 | }, 45 | sections, 46 | require: [ 47 | './styleguide/element.js', 48 | './styleguide/axios.js', 49 | './styleguide/el-form-renderer.js' 50 | ], 51 | webpackConfig: { 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.vue$/, 56 | loader: 'vue-loader' 57 | }, 58 | { 59 | test: /\.js?$/, 60 | exclude: /node_modules/, 61 | loader: 'babel-loader' 62 | }, 63 | { 64 | test: /\.css$/, 65 | loaders: ['style-loader', 'css-loader'] 66 | }, 67 | { 68 | test: /\.less$/, 69 | loaders: ['vue-style-loader', 'css-loader', 'less-loader'] 70 | }, 71 | { 72 | test: /\.(woff2?|eot|[ot]tf)(\?.*)?$/, 73 | loader: 'file-loader' 74 | } 75 | ] 76 | }, 77 | plugins: [new VueLoaderPlugin()] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /styleguide/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Axios from 'axios' 3 | 4 | Vue.prototype.$axios = Axios 5 | -------------------------------------------------------------------------------- /styleguide/el-form-renderer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import FormRenderer from '@femessage/el-form-renderer' 3 | 4 | Vue.component('ElFormRenderer', FormRenderer) 5 | -------------------------------------------------------------------------------- /styleguide/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Elm from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | 5 | Vue.use(Elm) 6 | -------------------------------------------------------------------------------- /test/extract-keys.test.js: -------------------------------------------------------------------------------- 1 | import extractKeys from '../src/utils/extract-keys' 2 | 3 | const slotsObj = { 4 | search: '', 5 | 'search:name': '', 6 | 'search:cute': '' 7 | } 8 | 9 | test('提取指定 key `search:`', () => { 10 | expect( 11 | extractKeys(slotsObj, 'search:') 12 | ).toEqual(['search:name', 'search:cute']) 13 | }) 14 | 15 | test('提取符合全部 key `search`', () => { 16 | expect( 17 | extractKeys(slotsObj, 'search') 18 | ).toEqual(['search', 'search:name', 'search:cute']) 19 | }) 20 | 21 | test('提取不存在的 key', () => { 22 | expect( 23 | extractKeys(slotsObj, 'form') 24 | ).toEqual([]) 25 | }) 26 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | valueSeparator, 3 | paramSeparator, 4 | paramInnerSeparator, 5 | queryFlag, 6 | stringify, 7 | parse, 8 | set, 9 | get, 10 | clear 11 | } from '../src/utils/query' 12 | 13 | const query = { 14 | obj: {a: '1', b: 'b&c', d: ['1', '2', '3']}, 15 | str( 16 | equal = valueSeparator, 17 | delimiter = paramSeparator, 18 | arrayDelimiter = paramInnerSeparator 19 | ) { 20 | return [ 21 | `a${equal}${encodeURIComponent(JSON.stringify('1'))}`, 22 | `b${equal}${encodeURIComponent(JSON.stringify('b&c'))}`, 23 | `d${equal}${encodeURIComponent( 24 | JSON.stringify(['1', '2', '3']) 25 | .split(',') 26 | .join(arrayDelimiter) 27 | )}` 28 | ].join(`${delimiter}`) 29 | } 30 | } 31 | 32 | const routerModes = ['history', 'hash'] 33 | const locations = (() => { 34 | const origin = 'https://a.b' 35 | const search = '?c=1' 36 | const hash = '#d' 37 | const q = `${queryFlag}${query.str()}${paramSeparator}` 38 | return [ 39 | { 40 | href: origin + hash, 41 | result: { 42 | history: origin + `?${q}` + hash, 43 | hash: origin + hash + `?${q}` 44 | } 45 | }, 46 | { 47 | href: origin + hash + search, 48 | result: { 49 | history: origin + `?${q}` + hash + search, 50 | hash: origin + hash + search + `&${q}` 51 | } 52 | }, 53 | { 54 | href: origin + search + hash, 55 | result: { 56 | history: origin + search + `&${q}` + hash, 57 | hash: origin + search + hash + `?${q}` 58 | } 59 | }, 60 | { 61 | href: origin + search + hash + search, 62 | result: { 63 | history: origin + search + `&${q}` + hash + search, 64 | hash: origin + search + hash + search + `&${q}` 65 | } 66 | } 67 | ] 68 | })() 69 | 70 | describe('测试 stringify', () => { 71 | test('基本功能', () => { 72 | expect(stringify(query.obj)).toBe(query.str()) 73 | }) 74 | test('自定义 equal & delimiter & arrayDelimiter', () => { 75 | const equal = '=' 76 | const delimiter = '&' 77 | const arrayDelimiter = '$' 78 | const str = query.str(equal, delimiter, arrayDelimiter) 79 | expect(stringify(query.obj, equal, delimiter, arrayDelimiter)).toBe(str) 80 | }) 81 | }) 82 | 83 | describe('测试 parse', () => { 84 | test('基本功能', () => { 85 | expect(parse(query.str())).toEqual(query.obj) 86 | }) 87 | test('自定义 equal & delimiter', () => { 88 | const equal = '=' 89 | const delimiter = '&' 90 | const str = query.str(equal, delimiter) 91 | expect(parse(str, equal, delimiter)).toEqual(query.obj) 92 | }) 93 | }) 94 | 95 | describe('测试 set', () => { 96 | test('通过所有用例', () => { 97 | locations.forEach(({href, result}) => { 98 | routerModes.forEach(routerMode => { 99 | expect(set(href, query.obj, routerMode)).toBe(result[routerMode]) 100 | }) 101 | }) 102 | }) 103 | test('多次set仍是幂等操作', () => { 104 | locations.forEach(({href, result}) => { 105 | routerModes.forEach(routerMode => { 106 | const t = set(href, query.obj, routerMode) 107 | expect(set(t, query.obj, routerMode)).toBe(result[routerMode]) 108 | }) 109 | }) 110 | }) 111 | test('url已经有query时会被替换成新的', () => { 112 | locations.forEach(({href, result}) => { 113 | routerModes.forEach(routerMode => { 114 | const t = set(href, {x: 1}, routerMode) 115 | expect(set(t, query.obj, routerMode)).toBe(result[routerMode]) 116 | }) 117 | }) 118 | }) 119 | }) 120 | 121 | describe('测试 get', () => { 122 | test('通过所有用例', () => { 123 | locations.forEach(({href}) => { 124 | routerModes.forEach(routerMode => { 125 | expect(get(set(href, query.obj, routerMode))).toEqual(query.obj) 126 | }) 127 | }) 128 | }) 129 | }) 130 | 131 | describe('测试 clear', () => { 132 | test('通过所有用例', () => { 133 | locations.forEach(({href}) => { 134 | routerModes.forEach(routerMode => { 135 | expect(clear(set(href, query.obj, routerMode))).toBe(href) 136 | }) 137 | }) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /test/select-strategy.test.js: -------------------------------------------------------------------------------- 1 | import getSelectStrategy from '../src/utils/select-strategy' 2 | 3 | const elDataTableMock = { 4 | persistSelection: true, 5 | selected: [], 6 | data: [{id: 1}, {id: 2}], 7 | id: 'id', 8 | $refs: { 9 | table: { 10 | toggleRowSelection() { 11 | return "calling el-table's toggleRowSelection" 12 | }, 13 | clearSelection() { 14 | return "calling el-table's clearSelection" 15 | } 16 | } 17 | }, 18 | get selectStrategy() { 19 | return getSelectStrategy(this) 20 | }, 21 | toggleRowSelection(...args) { 22 | return this.selectStrategy.toggleRowSelection(...args) 23 | }, 24 | clearSelection() { 25 | return this.selectStrategy.clearSelection() 26 | } 27 | } 28 | 29 | describe('测试 normal 模式', () => { 30 | beforeAll(() => { 31 | elDataTableMock.persistSelection = false 32 | }) 33 | 34 | test('onSelectionChange', () => { 35 | const selected = [elDataTableMock.data[0]] 36 | elDataTableMock.selectStrategy.onSelectionChange(selected) 37 | expect(elDataTableMock.selected).toBe(selected) 38 | }) 39 | test('toggleRowSelection', () => { 40 | expect(elDataTableMock.toggleRowSelection()).toBe( 41 | elDataTableMock.$refs.table.toggleRowSelection() 42 | ) 43 | }) 44 | test('clearSelection', () => { 45 | expect(elDataTableMock.clearSelection()).toBe( 46 | elDataTableMock.$refs.table.clearSelection() 47 | ) 48 | }) 49 | }) 50 | 51 | describe('测试 persistSelection 模式', () => { 52 | beforeAll(() => { 53 | elDataTableMock.persistSelection = true 54 | }) 55 | 56 | test('onSelect', () => { 57 | const row = elDataTableMock.data[0] 58 | // 选中 59 | elDataTableMock.selectStrategy.onSelect([row], row) 60 | expect(elDataTableMock.selected).toContain(row) 61 | // 取消选中 62 | elDataTableMock.selectStrategy.onSelect([], row) 63 | expect(elDataTableMock.selected).not.toContain(row) 64 | }) 65 | test('onSelectAll', () => { 66 | const selection = elDataTableMock.data 67 | // 选中 68 | elDataTableMock.selectStrategy.onSelectAll(selection) 69 | selection.forEach(row => { 70 | expect(elDataTableMock.selected).toContain(row) 71 | }) 72 | // 取消选中 73 | elDataTableMock.selectStrategy.onSelectAll([]) 74 | expect(elDataTableMock.selected.length).toBe(0) 75 | }) 76 | test('toggleRowSelection', () => { 77 | const row = elDataTableMock.data[0] 78 | // 切换 79 | const init = elDataTableMock.selected.includes(row) 80 | elDataTableMock.toggleRowSelection(row) 81 | const toggle = elDataTableMock.selected.includes(row) 82 | expect(init).not.toBe(toggle) 83 | elDataTableMock.toggleRowSelection(row) 84 | const toggleAgain = elDataTableMock.selected.includes(row) 85 | expect(init).toBe(toggleAgain) 86 | // 选中 87 | elDataTableMock.toggleRowSelection(row, true) 88 | expect(elDataTableMock.selected).toContain(row) 89 | // 取消选中 90 | elDataTableMock.toggleRowSelection(row, false) 91 | expect(elDataTableMock.selected).not.toContain(row) 92 | }) 93 | test('clearSelection', () => { 94 | elDataTableMock.clearSelection() 95 | expect(elDataTableMock.selected.length).toBe(0) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import {isFalsey, removeEmptyKeys} from '../src/utils/utils' 2 | 3 | describe('test isFalsey', () => { 4 | test('判断为 "",undefined或者 null', () => { 5 | expect(isFalsey(undefined)).toBe(true) 6 | expect(isFalsey(null)).toBe(true) 7 | expect(isFalsey('')).toBe(true) 8 | expect(isFalsey()).toBe(true) 9 | expect(isFalsey(0)).toBe(false) 10 | expect(isFalsey({})).toBe(false) 11 | expect(isFalsey([])).toBe(true) 12 | }) 13 | }) 14 | 15 | describe('test removeEmptyKeys', () => { 16 | test('判断 "", undefined, null, 空数组', () => { 17 | expect( 18 | removeEmptyKeys({ 19 | a: 1, 20 | b: '', 21 | c: undefined, 22 | d: null, 23 | e: [], 24 | f: [2] 25 | }) 26 | ).toStrictEqual({a: 1, f: [2]}) 27 | }) 28 | }) 29 | --------------------------------------------------------------------------------