├── .eslintignore ├── .gitignore ├── .editorconfig ├── index.html ├── .postcssrc.js ├── src ├── vue-element-bigdata-table │ ├── index.js │ ├── dom │ │ └── renderDom.js │ ├── dropdown.js │ ├── layout-observer.js │ ├── util.js │ ├── util │ │ └── index.js │ ├── table-footer.js │ ├── filter-panel.vue │ ├── table-layout.js │ ├── mixins │ │ └── data-handle.js │ ├── table-column.js │ ├── table-store.js │ ├── table-header.js │ ├── table-body.js │ └── table.vue ├── main.js └── App.vue ├── .babelrc ├── .eslintrc.js ├── README.md ├── package.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-elementui-bigdata-table 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/index.js: -------------------------------------------------------------------------------- 1 | import ElBigdataTable from './table.vue'; 2 | const install = (Vue, opts = {}) => { 3 | Vue.component('ElBigdataTable', ElBigdataTable); 4 | }; 5 | if (typeof window !== 'undefined' && window.Vue) { 6 | install(window.Vue); 7 | } 8 | export default Object.assign(ElBigdataTable, {install}); 9 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | import ElementUI from 'element-ui'; 5 | import 'element-ui/lib/theme-chalk/index.css'; 6 | import ElBigdataTable from '../dist/vue-elementui-bigdata-table.js'; 7 | Vue.use(ElementUI); 8 | Vue.use(ElBigdataTable); 9 | 10 | new Vue({ 11 | el: '#app', 12 | render: h => h(App) 13 | }); 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/dom/renderDom.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'renderDom', 3 | functional: true, 4 | props: { 5 | render: Function, 6 | backValue: [Number, Object], 7 | fixed: String, 8 | store: Object, 9 | stripe: Boolean, 10 | rowClassName: [String, Function], 11 | rowStyle: [Object, Function], 12 | highlight: Boolean, 13 | context: {} 14 | }, 15 | render: (h, ctx) => { 16 | return ctx.props.render(h, { 17 | fixed: ctx.props.fixed, 18 | store: ctx.props.store, 19 | stripe: ctx.props.stripe, 20 | rowClassName: ctx.props.rowClassName, 21 | rowStyle: ctx.props.rowStyle, 22 | highlight: ctx.props.highlight, 23 | context: ctx.props.context, 24 | style: ctx.data.style 25 | }, ctx.parent); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/dropdown.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | var dropdowns = []; 3 | 4 | !Vue.prototype.$isServer && document.addEventListener('click', function (event) { 5 | dropdowns.forEach(function (dropdown) { 6 | var target = event.target; 7 | if (!dropdown || !dropdown.$el) return; 8 | if (target === dropdown.$el || dropdown.$el.contains(target)) { 9 | return; 10 | } 11 | dropdown.handleOutsideClick && dropdown.handleOutsideClick(event); 12 | }); 13 | }); 14 | 15 | export default { 16 | open (instance) { 17 | if (instance) { 18 | dropdowns.push(instance); 19 | } 20 | }, 21 | 22 | close (instance) { 23 | var index = dropdowns.indexOf(instance); 24 | if (index !== -1) { 25 | dropdowns.splice(instance, 1); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 28 | 'indent': 0, 29 | 'no-tabs': 0, 30 | 'no-new': 0, 31 | 'semi': [1, 'always'] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/layout-observer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | created () { 3 | this.tableLayout.addObserver(this); 4 | }, 5 | 6 | destroyed () { 7 | this.tableLayout.removeObserver(this); 8 | }, 9 | 10 | computed: { 11 | tableLayout () { 12 | let layout = this.layout; 13 | if (!layout && this.table) { 14 | layout = this.table.layout; 15 | } 16 | if (!layout) { 17 | throw new Error('Can not find table layout.'); 18 | } 19 | return layout; 20 | } 21 | }, 22 | 23 | mounted () { 24 | this.onColumnsChange(this.tableLayout); 25 | this.onScrollableChange(this.tableLayout); 26 | }, 27 | 28 | updated () { 29 | if (this.__updated__) return; 30 | this.onColumnsChange(this.tableLayout); 31 | this.onScrollableChange(this.tableLayout); 32 | this.__updated__ = true; 33 | }, 34 | 35 | methods: { 36 | onColumnsChange () { 37 | const cols = this.$el.querySelectorAll('colgroup > col'); 38 | if (!cols.length) return; 39 | const flattenColumns = this.tableLayout.getFlattenColumns(); 40 | const columnsMap = {}; 41 | flattenColumns.forEach((column) => { 42 | columnsMap[column.id] = column; 43 | }); 44 | for (let i = 0, j = cols.length; i < j; i++) { 45 | const col = cols[i]; 46 | const name = col.getAttribute('name'); 47 | const column = columnsMap[name]; 48 | if (column) { 49 | col.setAttribute('width', column.realWidth || column.width); 50 | } 51 | } 52 | }, 53 | 54 | onScrollableChange (layout) { 55 | const cols = this.$el.querySelectorAll('colgroup > col[name=gutter]'); 56 | for (let i = 0, j = cols.length; i < j; i++) { 57 | const col = cols[i]; 58 | col.setAttribute('width', layout.scrollY ? layout.gutterWidth : '0'); 59 | } 60 | const ths = this.$el.querySelectorAll('th.gutter'); 61 | for (let i = 0, j = ths.length; i < j; i++) { 62 | const th = ths[i]; 63 | th.style.width = layout.scrollY ? layout.gutterWidth + 'px' : '0'; 64 | th.style.display = layout.scrollY ? '' : 'none'; 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-elementui-bigdata-table 2 | 3 | > Vue2 elementUI table 组件扩展,大量数据表格。 4 | 5 | ## Build Setup 6 | 7 | ```bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run dist 16 | ``` 17 | 18 | ## Feature 19 | 20 | 采用虚拟渲染方案,解决大数据量 DOM 渲染性能瓶颈。 21 | 1、基于 elementUI table,结合 vue-bigdata-table。 22 | 23 | 参考 [vue-bigdata-table](https://github.com/lison16/vue-bigdata-table) 24 | 参考 elementUI [table 组件](http://element-cn.eleme.io/#/zh-CN/component/table) 25 | 26 | ## API 27 | 28 | ### props: 29 | 30 | > 参考 elementUI [table 组件](http://element-cn.eleme.io/#/zh-CN/component/table) 31 | 32 | > props 添加行高 33 | 34 | | 属性 | 说明 |   类型 |   默认值 | 35 | | :-------: | ---- | :----: | :------: | 36 | | rowHeight | 行高 | Number | 32 | 37 | 38 | ## 使用 39 | 40 | > npm i vue-elementui-bigdata-table --save 41 | > 42 | > import ElBigdataTable from 'vue-elementui-bigdata-table.js' 43 | > 44 | > Vue.use(ElBigdataTable) 45 | 46 | ### 报错(忽略【已修复】) 47 | 48 | ```shell 49 | [//]: # (error in ./node_modules/_vue-elementui-bigdata-table@1.2.0@vue-elementui-bigdata-table/src/vue-elementui-bigdata-table/) 50 | [//]: # (table-body.js) 51 | [//]: # (Module parse failed: Unexpected token (36:6)) 52 | [//]: # (You may need an appropriate loader to handle this file type.) 53 | [//]: # (| const columnsHidden = this.columns.map((column, index) => this.isColumnHidden(index));) 54 | [//]: # (| return () 55 | [//]: # (| 配置 webpack, 添加 vue-elementui-bigdata-table 参与 jsx 解析) 65 | [//]: # (```javascript) 66 | [//]: # (// function resolve (dir) {) 67 | [//]: # (// return path.join(__dirname, '..', dir)) 68 | [//]: # (// }) 69 | [//]: # ({) 70 | [//]: # ( test: /\.js$/,) 71 | [//]: # ( loader: 'babel-loader',) 72 | [//]: # ( include: [) 73 | [//]: # ( resolve('src'),) 74 | [//]: # ( resolve('test'),) 75 | [//]: # ( resolve('node_modules/webpack-dev-server/client'),) 76 | [//]: # ( resolve('node_modules/vue-elementui-bigdata-table') // add) 77 | [//]: # ( ]) 78 | [//]: # (}) 79 | [//]: # (```) 80 | 81 | ```` 82 | 83 | ### 问题 84 | 85 | 由于动态加载数据 props 事件中 \$index 可能不准确。可使用 row 数据查找。 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-elementui-bigdata-table", 3 | "version": "1.3.2", 4 | "description": "vue element ui bigdata table", 5 | "author": { 6 | "name": "ArcherTrister", 7 | "email": "ArcherTrister@outlook.com" 8 | }, 9 | "license": "MIT", 10 | "main": "dist/vue-elementui-bigdata-table.js", 11 | "scripts": { 12 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 13 | "dist": "cross-env NODE_ENV=production webpack --progress --hide-modules", 14 | "lint": "eslint --ext .js,.vue src" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ArcherTrister/vue-element-bigdata-table.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/ArcherTrister/vue-element-bigdata-table/issues" 22 | }, 23 | "homepage": "https://github.com/ArcherTrister/vue-element-bigdata-table#readme", 24 | "dependencies": { 25 | "element-ui": "^2.4.4", 26 | "vue": "^2.5.11" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions", 31 | "not ie <= 8" 32 | ], 33 | "devDependencies": { 34 | "autoprefixer": "^8.3.0", 35 | "babel-core": "^6.22.1", 36 | "babel-eslint": "^7.1.1", 37 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 38 | "babel-jest": "^21.0.2", 39 | "babel-loader": "^7.1.1", 40 | "babel-plugin-dynamic-import-node": "^1.2.0", 41 | "babel-plugin-syntax-jsx": "^6.18.0", 42 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 43 | "babel-plugin-transform-runtime": "^6.22.0", 44 | "babel-plugin-transform-vue-jsx": "^3.5.0", 45 | "babel-preset-env": "^1.3.2", 46 | "babel-preset-stage-2": "^6.22.0", 47 | "babel-register": "^6.22.0", 48 | "cross-env": "^5.0.5", 49 | "css-loader": "^0.28.7", 50 | "eslint": "^3.19.0", 51 | "eslint-config-standard": "^10.2.1", 52 | "eslint-friendly-formatter": "^3.0.0", 53 | "eslint-loader": "^1.7.1", 54 | "eslint-plugin-html": "^3.0.0", 55 | "eslint-plugin-import": "^2.7.0", 56 | "eslint-plugin-node": "^5.2.0", 57 | "eslint-plugin-promise": "^3.4.0", 58 | "eslint-plugin-standard": "^3.0.1", 59 | "eslint-plugin-vue": "^4.0.0", 60 | "extract-text-webpack-plugin": "^3.0.2", 61 | "file-loader": "^1.1.4", 62 | "hoek": "^5.0.3", 63 | "less": "^3.0.1", 64 | "less-loader": "^4.1.0", 65 | "postcss-import": "^11.1.0", 66 | "postcss-url": "^7.3.2", 67 | "url-loader": "^1.0.1", 68 | "vue-loader": "^13.0.5", 69 | "vue-template-compiler": "^2.4.4", 70 | "webpack": "^3.6.0", 71 | "webpack-dev-server": "^2.9.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: 7 | process.env.NODE_ENV === 'production' 8 | ? './src/vue-element-bigdata-table/index.js' 9 | : './src/main.js', 10 | output: { 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: '/dist/', 13 | filename: 14 | process.env.NODE_ENV === 'production' 15 | ? 'vue-elementui-bigdata-table.js' 16 | : 'build.js', // 打包后输出的文件名 17 | library: 'ElBigdataTable', // library指定的就是你使用require时的模块名,这里便是require("ElBigdataTable") 18 | libraryTarget: 'umd', // libraryTarget会生成不同umd的代码,可以只是commonjs标准的,也可以是指amd标准的,也可以只是通过script标签引入 19 | umdNamedDefine: true /// / 会对 UMD 的构建过程中的 AMD 模块进行命名。否则就使用匿名的 define。 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/, 25 | use: ['vue-style-loader', 'css-loader'] 26 | }, 27 | { 28 | test: /\.less$/, 29 | use: ExtractTextPlugin.extract({ 30 | use: ['css-loader?minimize', 'autoprefixer-loader', 'less-loader'], 31 | fallback: 'style-loader' 32 | }) 33 | }, 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: { 38 | loaders: { 39 | css: 'vue-style-loader!css-loader', 40 | less: 'vue-style-loader!css-loader!less-loader' 41 | }, 42 | postLoaders: { 43 | html: 'babel-loader' 44 | } 45 | } 46 | }, 47 | { 48 | test: /\.js$/, 49 | loader: 'babel-loader', 50 | include: [ 51 | path.resolve(__dirname, 'src'), 52 | path.resolve(__dirname, 'node_modules/element-ui/src') 53 | ] 54 | }, 55 | { 56 | test: /\.(png|jpg|gif|svg)$/, 57 | loader: 'file-loader', 58 | options: { 59 | name: '[name].[ext]?[hash]' 60 | } 61 | }, 62 | { 63 | test: /\.(woff|svg|eot|ttf)\??.*$/, 64 | loader: 'url-loader' 65 | } 66 | ] 67 | }, 68 | resolve: { 69 | alias: { 70 | vue$: 'vue/dist/vue.esm.js' 71 | }, 72 | extensions: ['*', '.js', '.vue', '.json'] 73 | }, 74 | devServer: { 75 | historyApiFallback: true, 76 | noInfo: true, 77 | overlay: true 78 | }, 79 | performance: { 80 | hints: false 81 | }, 82 | devtool: '#eval-source-map' 83 | }; 84 | 85 | if (process.env.NODE_ENV === 'production') { 86 | module.exports.devtool = '#source-map'; 87 | // http://vue-loader.vuejs.org/en/workflow/production.html 88 | module.exports.plugins = (module.exports.plugins || []).concat([ 89 | new webpack.DefinePlugin({ 90 | 'process.env': { 91 | NODE_ENV: '"production"' 92 | } 93 | }), 94 | new webpack.optimize.UglifyJsPlugin({ 95 | sourceMap: true, 96 | compress: { 97 | warnings: false 98 | } 99 | }), 100 | new webpack.LoaderOptionsPlugin({ 101 | minimize: true 102 | }), 103 | new ExtractTextPlugin('./vue-element-bigdata-table.css', { 104 | allChunks: true 105 | }), 106 | new webpack.optimize.CommonsChunkPlugin({ 107 | async: true, 108 | children: true 109 | }) 110 | ]); 111 | } 112 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/util.js: -------------------------------------------------------------------------------- 1 | import { getValueByPath } from 'element-ui/src/utils/util'; 2 | 3 | export const getCell = function (event) { 4 | let cell = event.target; 5 | 6 | while (cell && cell.tagName.toUpperCase() !== 'HTML') { 7 | if (cell.tagName.toUpperCase() === 'TD') { 8 | return cell; 9 | } 10 | cell = cell.parentNode; 11 | } 12 | 13 | return null; 14 | }; 15 | 16 | const isObject = function (obj) { 17 | return obj !== null && typeof obj === 'object'; 18 | }; 19 | 20 | export const orderBy = function (array, sortKey, reverse, sortMethod, sortBy) { 21 | if (!sortKey && !sortMethod && (!sortBy || (Array.isArray(sortBy) && !sortBy.length))) { 22 | return array; 23 | } 24 | if (typeof reverse === 'string') { 25 | reverse = reverse === 'descending' ? -1 : 1; 26 | } else { 27 | reverse = (reverse && reverse < 0) ? -1 : 1; 28 | } 29 | const getKey = sortMethod ? null : function (value, index) { 30 | if (sortBy) { 31 | if (!Array.isArray(sortBy)) { 32 | sortBy = [sortBy]; 33 | } 34 | return sortBy.map(function (by) { 35 | if (typeof by === 'string') { 36 | return getValueByPath(value, by); 37 | } else { 38 | return by(value, index, array); 39 | } 40 | }); 41 | } 42 | if (sortKey !== '$key') { 43 | if (isObject(value) && '$value' in value) value = value.$value; 44 | } 45 | return [isObject(value) ? getValueByPath(value, sortKey) : value]; 46 | }; 47 | const compare = function (a, b) { 48 | if (sortMethod) { 49 | return sortMethod(a.value, b.value); 50 | } 51 | for (let i = 0, len = a.key.length; i < len; i++) { 52 | if (a.key[i] < b.key[i]) { 53 | return -1; 54 | } 55 | if (a.key[i] > b.key[i]) { 56 | return 1; 57 | } 58 | } 59 | return 0; 60 | }; 61 | return array.map(function (value, index) { 62 | return { 63 | value: value, 64 | index: index, 65 | key: getKey ? getKey(value, index) : null 66 | }; 67 | }).sort(function (a, b) { 68 | let order = compare(a, b); 69 | if (!order) { 70 | // make stable https://en.wikipedia.org/wiki/Sorting_algorithm#Stability 71 | order = a.index - b.index; 72 | } 73 | return order * reverse; 74 | }).map(item => item.value); 75 | }; 76 | 77 | export const getColumnById = function (table, columnId) { 78 | let column = null; 79 | table.columns.forEach(function (item) { 80 | if (item.id === columnId) { 81 | column = item; 82 | } 83 | }); 84 | return column; 85 | }; 86 | 87 | export const getColumnByCell = function (table, cell) { 88 | const matches = (cell.className || '').match(/el-table_[^\s]+/gm); 89 | if (matches) { 90 | return getColumnById(table, matches[0]); 91 | } 92 | return null; 93 | }; 94 | 95 | export const getRowIdentity = (row, rowKey) => { 96 | if (!row) throw new Error('row is required when get row identity'); 97 | if (typeof rowKey === 'string') { 98 | if (rowKey.indexOf('.') < 0) { 99 | return row[rowKey]; 100 | } 101 | let key = rowKey.split('.'); 102 | let current = row; 103 | for (let i = 0; i < key.length; i++) { 104 | current = current[key[i]]; 105 | } 106 | return current; 107 | } else if (typeof rowKey === 'function') { 108 | return rowKey.call(this, row); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 105 | 111 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/util/index.js: -------------------------------------------------------------------------------- 1 | export const findNodeUpper = (ele, tag) => { 2 | if (ele.parentNode) { 3 | if (ele.parentNode.tagName === tag.toUpperCase()) { 4 | return ele.parentNode; 5 | } else { 6 | if (ele.parentNode) return findNodeUpper(ele.parentNode, tag); 7 | else return false; 8 | } 9 | } 10 | }; 11 | 12 | export const getScrollbarWidth = () => { 13 | let oP = document.createElement('p'); 14 | let styles = { 15 | width: '100px', 16 | height: '100px', 17 | overflowY: 'scroll' 18 | }; 19 | for (let i in styles) { 20 | oP.style[i] = styles[i]; 21 | } 22 | document.body.appendChild(oP); 23 | let scrollbarWidth = oP.offsetWidth - oP.clientWidth; 24 | oP.remove(); 25 | return scrollbarWidth; 26 | }; 27 | 28 | export const createNewArray = (length, content = undefined) => { 29 | let i = -1; 30 | let arr = []; 31 | while (++i < length) { 32 | let con = Array.isArray(content) ? content[i] : content; 33 | arr.push(con); 34 | } 35 | return arr; 36 | }; 37 | 38 | export const iteratorByTimes = (times, fn) => { 39 | let i = -1; 40 | while (++i < times) { 41 | fn(i); 42 | } 43 | }; 44 | 45 | export const getHeaderWords = (length) => { 46 | let wordsArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 47 | let headerArr = []; 48 | if (length <= 26) { 49 | headerArr = wordsArr.slice(0, length); 50 | } else { 51 | headerArr = [...wordsArr]; 52 | let num = length - 26; 53 | let firstWordIndex = 0; 54 | let secondWordIndex = 0; 55 | let i = -1; 56 | while (++i < num) { 57 | firstWordIndex = Math.floor(i / 26); 58 | secondWordIndex = i % 26; 59 | let sumWord = `${wordsArr[firstWordIndex]}${wordsArr[secondWordIndex]}`; 60 | headerArr.push(sumWord); 61 | } 62 | } 63 | return headerArr; 64 | }; 65 | 66 | // 获取数组中第一个不为空的值 67 | export const getFirstNotNullValue = (array, index) => { 68 | if (!(array && array.length)) return false; 69 | let r = -1; 70 | let rowLength = array.length; 71 | while (++r < rowLength) { 72 | let item = array[r][index]; 73 | if (item || item === 0) return item; 74 | } 75 | return false; 76 | }; 77 | 78 | const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); 79 | export const sortArr = (arr, index) => { 80 | if (arr.length <= 1) return; 81 | const firstNotNullValue = getFirstNotNullValue(arr, index); 82 | if (!firstNotNullValue && firstNotNullValue !== 0) return; 83 | if (!isChineseReg.test(firstNotNullValue)) { 84 | if (isNaN(Number(firstNotNullValue))) { 85 | // 非中文非数值 86 | arr.sort(); 87 | } else { 88 | // 数值型 89 | arr.sort((a, b) => { 90 | return a[index] - b[index]; 91 | }); 92 | } 93 | } else { 94 | arr.sort((a, b) => { 95 | return a[index].localeCompare(b[index], 'zh'); 96 | }); 97 | } 98 | }; 99 | 100 | // 倒序 101 | export const sortDesArr = (arr, index) => { 102 | if (arr.length <= 1) return; 103 | const firstNotNullValue = getFirstNotNullValue(arr, index); 104 | if (!firstNotNullValue && firstNotNullValue !== 0) return; 105 | if (!isChineseReg.test(firstNotNullValue)) { 106 | if (isNaN(Number(firstNotNullValue))) { 107 | // 非中文非数值 108 | arr.sort().reverse(); 109 | } else { 110 | // 数值型 111 | arr.sort((a, b) => { 112 | return b[index] - a[index]; 113 | }); 114 | } 115 | } else { 116 | arr.sort((a, b) => { 117 | return b[index].localeCompare(a[index], 'zh'); 118 | }); 119 | } 120 | }; 121 | 122 | export const on = (ele, event, callback) => { 123 | ele.addEventListener(event, callback); 124 | }; 125 | 126 | export const off = (ele, event, callback) => { 127 | ele.removeEventListener(event, callback); 128 | }; 129 | 130 | export const attr = (ele, attribution, value) => { 131 | if (value || value === 0) { 132 | ele.setAttribute(attribution, value); 133 | } else { 134 | return ele.getAttribute(attribution); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-footer.js: -------------------------------------------------------------------------------- 1 | import LayoutObserver from './layout-observer'; 2 | 3 | export default { 4 | name: 'ElTableFooter', 5 | 6 | mixins: [LayoutObserver], 7 | 8 | render (h) { 9 | if (h); 10 | const sums = []; 11 | this.columns.forEach((column, index) => { 12 | if (index === 0) { 13 | sums[index] = this.sumText; 14 | return; 15 | } 16 | const values = this.store.states.data.map(item => Number(item[column.property])); 17 | const precisions = []; 18 | let notNumber = true; 19 | values.forEach(value => { 20 | if (!isNaN(value)) { 21 | notNumber = false; 22 | let decimal = ('' + value).split('.')[1]; 23 | precisions.push(decimal ? decimal.length : 0); 24 | } 25 | }); 26 | const precision = Math.max.apply(null, precisions); 27 | if (!notNumber) { 28 | sums[index] = values.reduce((prev, curr) => { 29 | const value = Number(curr); 30 | if (!isNaN(value)) { 31 | return parseFloat((prev + curr).toFixed(Math.min(precision, 20))); 32 | } else { 33 | return prev; 34 | } 35 | }, 0); 36 | } else { 37 | sums[index] = ''; 38 | } 39 | }); 40 | 41 | return ( 42 | 47 | 48 | { 49 | this._l(this.columns, column => ) 50 | } 51 | { 52 | this.hasGutter ? : '' 53 | } 54 | 55 | 56 | 57 | { 58 | this._l(this.columns, (column, cellIndex) => 59 | 69 | ) 70 | } 71 | { 72 | this.hasGutter ? : '' 73 | } 74 | 75 | 76 | 77 | ); 78 | }, 79 | 80 | props: { 81 | fixed: String, 82 | store: { 83 | required: true 84 | }, 85 | summaryMethod: Function, 86 | sumText: String, 87 | border: Boolean, 88 | defaultSort: { 89 | type: Object, 90 | default () { 91 | return { 92 | prop: '', 93 | order: '' 94 | }; 95 | } 96 | } 97 | }, 98 | 99 | computed: { 100 | table () { 101 | return this.$parent; 102 | }, 103 | 104 | isAllSelected () { 105 | return this.store.states.isAllSelected; 106 | }, 107 | 108 | columnsCount () { 109 | return this.store.states.columns.length; 110 | }, 111 | 112 | leftFixedCount () { 113 | return this.store.states.fixedColumns.length; 114 | }, 115 | 116 | rightFixedCount () { 117 | return this.store.states.rightFixedColumns.length; 118 | }, 119 | 120 | columns () { 121 | return this.store.states.columns; 122 | }, 123 | 124 | hasGutter () { 125 | return !this.fixed && this.tableLayout.gutterWidth; 126 | } 127 | }, 128 | 129 | methods: { 130 | isCellHidden (index, columns) { 131 | if (this.fixed === true || this.fixed === 'left') { 132 | return index >= this.leftFixedCount; 133 | } else if (this.fixed === 'right') { 134 | let before = 0; 135 | for (let i = 0; i < index; i++) { 136 | before += columns[i].colSpan; 137 | } 138 | return before < this.columnsCount - this.rightFixedCount; 139 | } else { 140 | return (index < this.leftFixedCount) || (index >= this.columnsCount - this.rightFixedCount); 141 | } 142 | } 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/filter-panel.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 239 | 245 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-layout.js: -------------------------------------------------------------------------------- 1 | import scrollbarWidth from 'element-ui/src/utils/scrollbar-width'; 2 | import Vue from 'vue'; 3 | 4 | class TableLayout { 5 | constructor (options) { 6 | this.observers = []; 7 | this.table = null; 8 | this.store = null; 9 | this.columns = null; 10 | this.fit = true; 11 | this.showHeader = true; 12 | 13 | this.height = null; 14 | this.scrollX = false; 15 | this.scrollY = false; 16 | this.bodyWidth = null; 17 | this.fixedWidth = null; 18 | this.rightFixedWidth = null; 19 | this.tableHeight = null; 20 | this.headerHeight = 44; // Table Header Height 21 | this.appendHeight = 0; // Append Slot Height 22 | this.footerHeight = 44; // Table Footer Height 23 | this.viewportHeight = null; // Table Height - Scroll Bar Height 24 | this.bodyHeight = null; // Table Height - Table Header Height 25 | this.fixedBodyHeight = null; // Table Height - Table Header Height - Scroll Bar Height 26 | this.gutterWidth = scrollbarWidth(); 27 | 28 | for (let name in options) { 29 | if (options.hasOwnProperty(name)) { 30 | this[name] = options[name]; 31 | } 32 | } 33 | 34 | if (!this.table) { 35 | throw new Error('table is required for Table Layout'); 36 | } 37 | if (!this.store) { 38 | throw new Error('store is required for Table Layout'); 39 | } 40 | } 41 | 42 | updateScrollY () { 43 | const height = this.height; 44 | if (typeof height !== 'string' && typeof height !== 'number') return; 45 | const bodyWrapper = this.table.bodyWrapper; 46 | if (this.table.$el && bodyWrapper) { 47 | const body = bodyWrapper.querySelector('.el-table__body'); 48 | this.scrollY = body.offsetHeight > this.bodyHeight; 49 | } 50 | } 51 | 52 | setHeight (value, prop = 'height') { 53 | if (Vue.prototype.$isServer) return; 54 | const el = this.table.$el; 55 | if (typeof value === 'string' && /^\d+$/.test(value)) { 56 | value = Number(value); 57 | } 58 | this.height = value; 59 | 60 | if (!el && (value || value === 0)) return Vue.nextTick(() => this.setHeight(value, prop)); 61 | 62 | if (typeof value === 'number') { 63 | el.style[prop] = value + 'px'; 64 | 65 | this.updateElsHeight(); 66 | } else if (typeof value === 'string') { 67 | el.style[prop] = value; 68 | this.updateElsHeight(); 69 | } 70 | } 71 | 72 | setMaxHeight (value) { 73 | return this.setHeight(value, 'max-height'); 74 | } 75 | 76 | updateElsHeight () { 77 | if (!this.table.$ready) return Vue.nextTick(() => this.updateElsHeight()); 78 | const { headerWrapper, appendWrapper, footerWrapper } = this.table.$refs; 79 | this.appendHeight = appendWrapper ? appendWrapper.offsetHeight : 0; 80 | 81 | if (this.showHeader && !headerWrapper) return; 82 | const headerHeight = this.headerHeight = !this.showHeader ? 0 : headerWrapper.offsetHeight; 83 | if (this.showHeader && headerWrapper.offsetWidth > 0 && (this.table.columns || []).length > 0 && headerHeight < 2) { 84 | return Vue.nextTick(() => this.updateElsHeight()); 85 | } 86 | const tableHeight = this.tableHeight = this.table.$el.clientHeight; 87 | if (this.height !== null && (!isNaN(this.height) || typeof this.height === 'string')) { 88 | const footerHeight = this.footerHeight = footerWrapper ? footerWrapper.offsetHeight : 0; 89 | this.bodyHeight = tableHeight - headerHeight - footerHeight + (footerWrapper ? 1 : 0); 90 | } 91 | this.fixedBodyHeight = this.scrollX ? this.bodyHeight - this.gutterWidth : this.bodyHeight; 92 | 93 | const noData = !this.table.data || this.table.data.length === 0; 94 | this.viewportHeight = this.scrollX ? tableHeight - (noData ? 0 : this.gutterWidth) : tableHeight; 95 | 96 | this.updateScrollY(); 97 | this.notifyObservers('scrollable'); 98 | } 99 | 100 | getFlattenColumns () { 101 | const flattenColumns = []; 102 | const columns = this.table.columns; 103 | columns.forEach((column) => { 104 | if (column.isColumnGroup) { 105 | flattenColumns.push.apply(flattenColumns, column.columns); 106 | } else { 107 | flattenColumns.push(column); 108 | } 109 | }); 110 | 111 | return flattenColumns; 112 | } 113 | 114 | updateColumnsWidth () { 115 | const fit = this.fit; 116 | const bodyWidth = this.table.$el.clientWidth; 117 | let bodyMinWidth = 0; 118 | 119 | const flattenColumns = this.getFlattenColumns(); 120 | let flexColumns = flattenColumns.filter((column) => typeof column.width !== 'number'); 121 | 122 | flattenColumns.forEach((column) => { // Clean those columns whose width changed from flex to unflex 123 | if (typeof column.width === 'number' && column.realWidth) column.realWidth = null; 124 | }); 125 | 126 | if (flexColumns.length > 0 && fit) { 127 | flattenColumns.forEach((column) => { 128 | bodyMinWidth += column.width || column.minWidth || 80; 129 | }); 130 | 131 | const scrollYWidth = this.scrollY ? this.gutterWidth : 0; 132 | 133 | if (bodyMinWidth <= bodyWidth - scrollYWidth) { // DON'T HAVE SCROLL BAR 134 | this.scrollX = false; 135 | 136 | const totalFlexWidth = bodyWidth - scrollYWidth - bodyMinWidth; 137 | 138 | if (flexColumns.length === 1) { 139 | flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth; 140 | } else { 141 | const allColumnsWidth = flexColumns.reduce((prev, column) => prev + (column.minWidth || 80), 0); 142 | const flexWidthPerPixel = totalFlexWidth / allColumnsWidth; 143 | let noneFirstWidth = 0; 144 | 145 | flexColumns.forEach((column, index) => { 146 | if (index === 0) return; 147 | const flexWidth = Math.floor((column.minWidth || 80) * flexWidthPerPixel); 148 | noneFirstWidth += flexWidth; 149 | column.realWidth = (column.minWidth || 80) + flexWidth; 150 | }); 151 | 152 | flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth - noneFirstWidth; 153 | } 154 | } else { // HAVE HORIZONTAL SCROLL BAR 155 | this.scrollX = true; 156 | flexColumns.forEach(function (column) { 157 | column.realWidth = column.minWidth; 158 | }); 159 | } 160 | // fix scrollY theader and tbody does not match 161 | this.bodyWidth = Math.max(bodyMinWidth, bodyWidth - scrollYWidth); 162 | } else { 163 | flattenColumns.forEach((column) => { 164 | if (!column.width && !column.minWidth) { 165 | column.realWidth = 80; 166 | } else { 167 | column.realWidth = column.width || column.minWidth; 168 | } 169 | 170 | bodyMinWidth += column.realWidth; 171 | }); 172 | this.scrollX = bodyMinWidth > bodyWidth; 173 | 174 | this.bodyWidth = bodyMinWidth; 175 | } 176 | 177 | const fixedColumns = this.store.states.fixedColumns; 178 | 179 | if (fixedColumns.length > 0) { 180 | let fixedWidth = 0; 181 | fixedColumns.forEach(function (column) { 182 | fixedWidth += column.realWidth || column.width; 183 | }); 184 | 185 | this.fixedWidth = fixedWidth; 186 | } 187 | 188 | const rightFixedColumns = this.store.states.rightFixedColumns; 189 | if (rightFixedColumns.length > 0) { 190 | let rightFixedWidth = 0; 191 | rightFixedColumns.forEach(function (column) { 192 | rightFixedWidth += column.realWidth || column.width; 193 | }); 194 | 195 | this.rightFixedWidth = rightFixedWidth; 196 | } 197 | 198 | this.notifyObservers('columns'); 199 | } 200 | 201 | addObserver (observer) { 202 | this.observers.push(observer); 203 | } 204 | 205 | removeObserver (observer) { 206 | const index = this.observers.indexOf(observer); 207 | if (index !== -1) { 208 | this.observers.splice(index, 1); 209 | } 210 | } 211 | 212 | notifyObservers (event) { 213 | const observers = this.observers; 214 | observers.forEach((observer) => { 215 | switch (event) { 216 | case 'columns': 217 | observer.onColumnsChange(this); 218 | break; 219 | case 'scrollable': 220 | observer.onScrollableChange(this); 221 | break; 222 | default: 223 | throw new Error(`Table Layout don't have event ${event}.`); 224 | } 225 | }); 226 | } 227 | } 228 | 229 | export default TableLayout; 230 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/mixins/data-handle.js: -------------------------------------------------------------------------------- 1 | // import TableStore from '../table-store'; 2 | // import TableLayout from '../table-layout'; 3 | import ElTableBody from '../table-body.js'; 4 | import { getScrollbarWidth } from '../util/index.js'; 5 | 6 | export default { 7 | data () { 8 | // 9 | return { 10 | // layout, 11 | // store, 12 | scrollLeft: 0, 13 | scrollTop: 0, 14 | // 三个tr块中的一块的高度 15 | moduleHeight: 0, 16 | /** 17 | * @description 根据表格容器高度计算内置单个表格(1/3)渲染的行数基础上额外渲染的行数,行数越多表格接替渲染效果越好,但越耗性能 18 | */ 19 | appendNum: 15, 20 | // 一块数据显示的数据条数 21 | itemNum: 0, 22 | /** 23 | * @description 表头高度 24 | */ 25 | headerHeight: 0, 26 | /** 27 | * @description 表格行高 28 | */ 29 | // rowHeight: 32, 30 | // 表格滚动区域高度 31 | wrapperHeight: 0, 32 | // 当前展示的表格是第几个 33 | totalRowHeight: 0, // 如果全量渲染应该是多高,用于计算占位 34 | currentIndex: 0, 35 | times0: 0, // 当前是第几轮 36 | times1: 0, 37 | times2: -1, 38 | topPlaceholderHeight: 0, // 顶部占位容器高度 39 | table1Data: [], 40 | table2Data: [], 41 | table3Data: [], 42 | outerWidth: 0, // 外面容器宽度 43 | groupHeight: {}, 44 | groupIndex: 0 45 | }; 46 | }, 47 | methods: { 48 | // 滚动条拖动 49 | handleScroll (e) { 50 | const ele = e.srcElement || e.target; 51 | const { scrollTop, scrollLeft } = ele; 52 | this.scrollLeft = scrollLeft; 53 | this.scrollTop = scrollTop; 54 | }, 55 | // 获取高度与数量 56 | updateHeight () { 57 | this.itemNum = Math.ceil(this.height / this.rowHeight) + this.appendNum; 58 | this.wrapperHeight = this.$refs.bodyWrapper.offsetHeight; 59 | this.setTopPlace(); 60 | }, 61 | // 62 | initGroupHeight (data) { 63 | // 分组数据 64 | let moduleNb = Math.ceil(this.height / this.rowHeight) + this.appendNum; 65 | console.log('moduleNb', moduleNb) 66 | let groupHeight = {}; 67 | if (data.length > moduleNb) { 68 | for (let i in data) { 69 | let nb = (+i + 1) * moduleNb; 70 | if (nb > data.length) { 71 | groupHeight[i] = (data.length % moduleNb) * this.rowHeight; 72 | break; 73 | } 74 | groupHeight[i] = moduleNb * this.rowHeight; 75 | } 76 | } else { 77 | groupHeight[0] = data.length * this.rowHeight; 78 | } 79 | return groupHeight; 80 | }, 81 | // 设置顶部定位 82 | setTopPlace () { 83 | let scrollTop = this.scrollTop; 84 | let t0 = 0; 85 | let t1 = 0; 86 | let t2 = 0; 87 | if (scrollTop > this.groupHeight[0]) { 88 | switch (this.currentIndex) { 89 | case 0: t0 = parseInt(this.groupIndex / 3); t1 = t2 = t0; break; 90 | case 1: t1 = parseInt(this.groupIndex / 3); t0 = t1 + 1; t2 = t1; break; 91 | case 2: t2 = parseInt(this.groupIndex / 3); t0 = t1 = t2 + 1; 92 | } 93 | } 94 | this.times0 = t0; 95 | this.times1 = t1; 96 | this.times2 = t2; 97 | // 98 | let height = 0; 99 | for (let i in this.groupHeight) { 100 | if (+i === +this.groupIndex) { 101 | break; 102 | } 103 | height += this.groupHeight[i]; 104 | } 105 | this.topPlaceholderHeight = height; 106 | }, 107 | // 给表格数据添加行号,用于排序后正确修改数据 108 | setIndex (tableData) { 109 | return tableData.map((item, i) => { 110 | let row = item; 111 | row.initRowIndex = i; 112 | return row; 113 | }); 114 | }, 115 | setComputedProps () { 116 | let height = 0; 117 | for (let i in this.groupHeight) { 118 | height += this.groupHeight[i]; 119 | } 120 | this.totalRowHeight = height; 121 | }, 122 | // 123 | _tableResize () { 124 | this.$nextTick(() => { 125 | this.updateHeight(); 126 | this.setComputedProps(); 127 | let scrollBarWidth = this.totalRowHeight > this.wrapperHeight ? getScrollbarWidth() : 0; 128 | this.outerWidth = this.$refs.bodyWrapper.offsetWidth - 2 - scrollBarWidth; 129 | // let width = this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0); 130 | // this.tableWidth = width > this.outerWidth ? width : this.outerWidth; 131 | // this.tableWidth = this.fixedWrapperWidth ? this.outerWidth : (width > this.outerWidth ? width : this.outerWidth); 132 | // if (width < this.outerWidth) this._setColWidthArr(); 133 | // this.widthArr = this.colWidthArr; 134 | }); 135 | }, 136 | // 生成三个Vnode的数组 137 | getTables (h, prop) { 138 | let table1 = this.getItemTable(h, this.table1Data, 1, prop); 139 | let table2 = this.getItemTable(h, this.table2Data, 2, prop); 140 | let table3 = this.getItemTable(h, this.table3Data, 3, prop); 141 | if (this.currentIndex === 0) return [table1, table2, table3]; 142 | else if (this.currentIndex === 1) return [table2, table3, table1]; 143 | else return [table3, table1, table2]; 144 | }, 145 | // 三个Vnode的外包装 146 | renderTable (h, prop) { 147 | return h('div', { 148 | class: 'vue-element-bigdata-table-div', 149 | style: prop.style 150 | }, this.getTables(h, prop)); 151 | }, 152 | // 生成body 153 | getItemTable (h, data, index, prop) { 154 | return h(ElTableBody, { 155 | style: {width: '100%'}, 156 | props: { 157 | store: this.store, 158 | tableData: data, 159 | stripe: prop.stripe, 160 | context: prop.context, 161 | rowClassName: prop.rowClassName, 162 | rowStyle: prop.rowStyle, 163 | fixed: prop.fixed, 164 | highlight: prop.highlight, 165 | times0: this.times0, 166 | times1: this.times1, 167 | times2: this.times2, 168 | itemNum: this.itemNum, 169 | tableIndex: index, 170 | groupIndex: +this.groupIndex, 171 | itemRowHeight: this.rowHeight 172 | }, 173 | on: { 174 | // 175 | 'changeHeight': (index, height) => { 176 | this.groupHeight[index] = height; 177 | // 178 | this.setComputedProps(); 179 | } 180 | }, 181 | key: 'table-item-key' + index, 182 | ref: 'itemTable' + index, 183 | attrs: { 184 | 'data-index': index 185 | } 186 | }); 187 | }, 188 | 189 | // 190 | // 涉及到表格容器尺寸变化或数据变化的情况调用此方法重新计算相关值 191 | resize () { 192 | this._tableResize(); 193 | }, 194 | // 获取表格横向滚动的距离 195 | getScrollLeft () { 196 | return this.$refs.outer.scrollLeft; 197 | }, 198 | // 调用此方法跳转到某条数据 199 | scrollToRow (index) { 200 | this._scrollToIndexRow(index); 201 | }, 202 | // canEdit为true时调用此方法使第row+1行第col+1列变为编辑状态,这里的行列指的是表格显示的行和除序列号列的列 203 | editCell (row, col) { 204 | this._editCell(row, col); 205 | } 206 | }, 207 | computed: { 208 | bottomPlaceholderHeight () { 209 | return (this.placeholderHeight - this.topPlaceholderHeight) < 0 ? 0 : this.placeholderHeight - this.topPlaceholderHeight; 210 | }, 211 | placeholderHeight () { 212 | // 当前三块总高度 213 | let mdHeight = 0; 214 | let arr = []; 215 | for (let i in this.groupHeight) { 216 | arr[i] = this.groupHeight[i]; 217 | } 218 | arr = arr.slice(this.groupIndex, +this.groupIndex + 3); 219 | for (let n of arr) { 220 | mdHeight += n; 221 | } 222 | return this.totalRowHeight - mdHeight; // 占位容器的总高度(上 + 下) 223 | } 224 | }, 225 | watch: { 226 | scrollTop (top) { 227 | // 当前滚动条是在三个table中的哪一个 228 | let height = 0; 229 | // 通过当前上部站位得到三个table最上一个table的是哪个组 230 | // let index = 0; 231 | for (let i in this.groupHeight) { 232 | if (top >= height && top < (height + (this.groupHeight[i] ? this.groupHeight[i] : 0))) { 233 | this.groupIndex = +i; 234 | break; 235 | } 236 | height += this.groupHeight[i]; 237 | } 238 | // 239 | this.currentIndex = this.groupIndex % 3; 240 | this.$nextTick(() => { 241 | this.setTopPlace(); 242 | }); 243 | }, 244 | data: { 245 | immediate: true, 246 | handler (value) { 247 | this.insideTableData = this.setIndex(value); 248 | this.groupHeight = this.initGroupHeight(value); 249 | this.resize(); 250 | // this.store.commit('setData', value); 251 | if (this.$ready) { 252 | this.$nextTick(() => { 253 | // this.doLayout(); 254 | // 设置滚动条高度与左边度 255 | // this.scrollLeft = 0; 256 | // this.scrollTop = 0; 257 | // 更新 258 | this.setComputedProps(); 259 | // 延迟 计算滚动条是否显示 260 | setTimeout(() => { 261 | this.updateScrollY(); 262 | }, 100); 263 | }); 264 | } 265 | } 266 | }, 267 | insideTableData () { 268 | this.resize(); 269 | }, 270 | tableData (newValue) { 271 | // 重置高度分组 272 | this.groupHeight = this.initGroupHeight(newValue); 273 | this.resize(); 274 | } 275 | 276 | }, 277 | mounted () { 278 | this.$nextTick(() => { 279 | this.insideTableData = this.setIndex(this.tableData); 280 | // this._initM(); 281 | this.resize(); 282 | }); 283 | } 284 | }; 285 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-column.js: -------------------------------------------------------------------------------- 1 | import ElCheckbox from 'element-ui/packages/checkbox'; 2 | import ElTag from 'element-ui/packages/tag'; 3 | import objectAssign from 'element-ui/src/utils/merge'; 4 | import { getPropByPath } from 'element-ui/src/utils/util'; 5 | 6 | let columnIdSeed = 1; 7 | 8 | const defaults = { 9 | default: { 10 | order: '' 11 | }, 12 | selection: { 13 | width: 48, 14 | minWidth: 48, 15 | realWidth: 48, 16 | order: '', 17 | className: 'el-table-column--selection' 18 | }, 19 | expand: { 20 | width: 48, 21 | minWidth: 48, 22 | realWidth: 48, 23 | order: '' 24 | }, 25 | index: { 26 | width: 48, 27 | minWidth: 48, 28 | realWidth: 48, 29 | order: '' 30 | } 31 | }; 32 | 33 | const forced = { 34 | selection: { 35 | renderHeader: function (h, { store }) { 36 | return 0 && !this.isAllSelected } 39 | nativeOn-click={ this.toggleAllSelection } 40 | value={ this.isAllSelected } />; 41 | }, 42 | renderCell: function (h, { row, column, store, $index }) { 43 | return event.stopPropagation() } 45 | value={ store.isSelected(row) } 46 | disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false } 47 | on-input={ () => { store.commit('rowSelectedChanged', row); } } />; 48 | }, 49 | sortable: false, 50 | resizable: false 51 | }, 52 | index: { 53 | renderHeader: function (h, { column }) { 54 | return column.label || '#'; 55 | }, 56 | renderCell: function (h, { $index, column }) { 57 | let i = $index + 1; 58 | const index = column.index; 59 | 60 | if (typeof index === 'number') { 61 | i = $index + index; 62 | } else if (typeof index === 'function') { 63 | i = index($index); 64 | } 65 | 66 | return
{ i }
; 67 | }, 68 | sortable: false 69 | }, 70 | expand: { 71 | renderHeader: function (h, { column }) { 72 | return column.label || ''; 73 | }, 74 | renderCell: function (h, { row, store }, proxy) { 75 | const expanded = store.states.expandRows.indexOf(row) > -1; 76 | return
proxy.handleExpandClick(row, e) }> 78 | 79 |
; 80 | }, 81 | sortable: false, 82 | resizable: false, 83 | className: 'el-table__expand-column' 84 | } 85 | }; 86 | 87 | const getDefaultColumn = function (type, options) { 88 | const column = {}; 89 | 90 | objectAssign(column, defaults[type || 'default']); 91 | 92 | for (let name in options) { 93 | if (options.hasOwnProperty(name)) { 94 | const value = options[name]; 95 | if (typeof value !== 'undefined') { 96 | column[name] = value; 97 | } 98 | } 99 | } 100 | 101 | if (!column.minWidth) { 102 | column.minWidth = 80; 103 | } 104 | 105 | column.realWidth = column.width === undefined ? column.minWidth : column.width; 106 | 107 | return column; 108 | }; 109 | 110 | const DEFAULT_RENDER_CELL = function (h, { row, column }) { 111 | const property = column.property; 112 | const value = property && getPropByPath(row, property).v; 113 | if (column && column.formatter) { 114 | return column.formatter(row, column, value); 115 | } 116 | return value; 117 | }; 118 | 119 | const parseWidth = (width) => { 120 | if (width !== undefined) { 121 | width = parseInt(width, 10); 122 | if (isNaN(width)) { 123 | width = null; 124 | } 125 | } 126 | return width; 127 | }; 128 | 129 | const parseMinWidth = (minWidth) => { 130 | if (minWidth !== undefined) { 131 | minWidth = parseInt(minWidth, 10); 132 | if (isNaN(minWidth)) { 133 | minWidth = 80; 134 | } 135 | } 136 | return minWidth; 137 | }; 138 | 139 | export default { 140 | name: 'ElTableColumn', 141 | 142 | props: { 143 | type: { 144 | type: String, 145 | default: 'default' 146 | }, 147 | label: String, 148 | className: String, 149 | labelClassName: String, 150 | property: String, 151 | prop: String, 152 | width: {}, 153 | minWidth: {}, 154 | renderHeader: Function, 155 | sortable: { 156 | type: [String, Boolean], 157 | default: false 158 | }, 159 | sortMethod: Function, 160 | sortBy: [String, Function, Array], 161 | resizable: { 162 | type: Boolean, 163 | default: true 164 | }, 165 | context: {}, 166 | columnKey: String, 167 | align: String, 168 | headerAlign: String, 169 | showTooltipWhenOverflow: Boolean, 170 | showOverflowTooltip: Boolean, 171 | fixed: [Boolean, String], 172 | formatter: Function, 173 | selectable: Function, 174 | reserveSelection: Boolean, 175 | filterMethod: Function, 176 | filteredValue: Array, 177 | filters: Array, 178 | filterPlacement: String, 179 | filterMultiple: { 180 | type: Boolean, 181 | default: true 182 | }, 183 | index: [Number, Function] 184 | }, 185 | 186 | data () { 187 | return { 188 | isSubColumn: false, 189 | columns: [] 190 | }; 191 | }, 192 | 193 | beforeCreate () { 194 | this.row = {}; 195 | this.column = {}; 196 | this.$index = 0; 197 | }, 198 | 199 | components: { 200 | ElCheckbox, 201 | ElTag 202 | }, 203 | 204 | computed: { 205 | owner () { 206 | let parent = this.$parent; 207 | while (parent && !parent.tableId) { 208 | parent = parent.$parent; 209 | } 210 | return parent; 211 | }, 212 | columnOrTableParent () { 213 | let parent = this.$parent; 214 | while (parent && !parent.tableId && !parent.columnId) { 215 | parent = parent.$parent; 216 | } 217 | return parent; 218 | } 219 | }, 220 | 221 | created () { 222 | this.customRender = this.$options.render; 223 | this.$options.render = h => h('div', this.$slots.default); 224 | 225 | let parent = this.columnOrTableParent; 226 | let owner = this.owner; 227 | this.isSubColumn = owner !== parent; 228 | this.columnId = (parent.tableId || parent.columnId) + '_column_' + columnIdSeed++; 229 | 230 | let type = this.type; 231 | 232 | const width = parseWidth(this.width); 233 | const minWidth = parseMinWidth(this.minWidth); 234 | 235 | let isColumnGroup = false; 236 | 237 | let column = getDefaultColumn(type, { 238 | id: this.columnId, 239 | columnKey: this.columnKey, 240 | label: this.label, 241 | className: this.className, 242 | labelClassName: this.labelClassName, 243 | property: this.prop || this.property, 244 | type, 245 | renderCell: null, 246 | renderHeader: this.renderHeader, 247 | minWidth, 248 | width, 249 | isColumnGroup, 250 | context: this.context, 251 | align: this.align ? 'is-' + this.align : null, 252 | headerAlign: this.headerAlign ? 'is-' + this.headerAlign : (this.align ? 'is-' + this.align : null), 253 | sortable: this.sortable === '' ? true : this.sortable, 254 | sortMethod: this.sortMethod, 255 | sortBy: this.sortBy, 256 | resizable: this.resizable, 257 | showOverflowTooltip: this.showOverflowTooltip || this.showTooltipWhenOverflow, 258 | formatter: this.formatter, 259 | selectable: this.selectable, 260 | reserveSelection: this.reserveSelection, 261 | fixed: this.fixed === '' ? true : this.fixed, 262 | filterMethod: this.filterMethod, 263 | filters: this.filters, 264 | filterable: this.filters || this.filterMethod, 265 | filterMultiple: this.filterMultiple, 266 | filterOpened: false, 267 | filteredValue: this.filteredValue || [], 268 | filterPlacement: this.filterPlacement || '', 269 | index: this.index 270 | }); 271 | 272 | objectAssign(column, forced[type] || {}); 273 | 274 | this.columnConfig = column; 275 | 276 | let renderCell = column.renderCell; 277 | let _self = this; 278 | 279 | if (type === 'expand') { 280 | owner.renderExpanded = function (h, data) { 281 | return _self.$scopedSlots.default 282 | ? _self.$scopedSlots.default(data) 283 | : _self.$slots.default; 284 | }; 285 | 286 | column.renderCell = function (h, data) { 287 | return
{ renderCell(h, data, this._renderProxy) }
; 288 | }; 289 | 290 | return; 291 | } 292 | 293 | column.renderCell = function (h, data) { 294 | if (_self.$scopedSlots.default) { 295 | renderCell = () => _self.$scopedSlots.default(data); 296 | } 297 | 298 | if (!renderCell) { 299 | renderCell = DEFAULT_RENDER_CELL; 300 | } 301 | 302 | return _self.showOverflowTooltip || _self.showTooltipWhenOverflow 303 | ?
{ renderCell(h, data) }
304 | :
{ renderCell(h, data) }
; 305 | }; 306 | }, 307 | 308 | destroyed () { 309 | if (!this.$parent) return; 310 | const parent = this.$parent; 311 | this.owner.store.commit('removeColumn', this.columnConfig, this.isSubColumn ? parent.columnConfig : null); 312 | }, 313 | 314 | watch: { 315 | label (newVal) { 316 | if (this.columnConfig) { 317 | this.columnConfig.label = newVal; 318 | } 319 | }, 320 | 321 | prop (newVal) { 322 | if (this.columnConfig) { 323 | this.columnConfig.property = newVal; 324 | } 325 | }, 326 | 327 | property (newVal) { 328 | if (this.columnConfig) { 329 | this.columnConfig.property = newVal; 330 | } 331 | }, 332 | 333 | filters (newVal) { 334 | if (this.columnConfig) { 335 | this.columnConfig.filters = newVal; 336 | } 337 | }, 338 | 339 | filterMultiple (newVal) { 340 | if (this.columnConfig) { 341 | this.columnConfig.filterMultiple = newVal; 342 | } 343 | }, 344 | 345 | align (newVal) { 346 | if (this.columnConfig) { 347 | this.columnConfig.align = newVal ? 'is-' + newVal : null; 348 | 349 | if (!this.headerAlign) { 350 | this.columnConfig.headerAlign = newVal ? 'is-' + newVal : null; 351 | } 352 | } 353 | }, 354 | 355 | headerAlign (newVal) { 356 | if (this.columnConfig) { 357 | this.columnConfig.headerAlign = 'is-' + (newVal ? newVal + '' : this.align); 358 | } 359 | }, 360 | 361 | width (newVal) { 362 | if (this.columnConfig) { 363 | this.columnConfig.width = parseWidth(newVal); 364 | this.owner.store.scheduleLayout(); 365 | } 366 | }, 367 | 368 | minWidth (newVal) { 369 | if (this.columnConfig) { 370 | this.columnConfig.minWidth = parseMinWidth(newVal); 371 | this.owner.store.scheduleLayout(); 372 | } 373 | }, 374 | 375 | fixed (newVal) { 376 | if (this.columnConfig) { 377 | this.columnConfig.fixed = newVal; 378 | this.owner.store.scheduleLayout(true); 379 | } 380 | }, 381 | 382 | sortable (newVal) { 383 | if (this.columnConfig) { 384 | this.columnConfig.sortable = newVal; 385 | } 386 | }, 387 | 388 | index (newVal) { 389 | if (this.columnConfig) { 390 | this.columnConfig.index = newVal; 391 | } 392 | } 393 | }, 394 | 395 | mounted () { 396 | const owner = this.owner; 397 | const parent = this.columnOrTableParent; 398 | let columnIndex; 399 | 400 | if (!this.isSubColumn) { 401 | columnIndex = [].indexOf.call(parent.$refs.hiddenColumns.children, this.$el); 402 | } else { 403 | columnIndex = [].indexOf.call(parent.$el.children, this.$el); 404 | } 405 | 406 | owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null); 407 | } 408 | }; 409 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import debounce from 'throttle-debounce/debounce'; 3 | import merge from 'element-ui/src/utils/merge'; 4 | import { orderBy, getColumnById, getRowIdentity } from './util'; 5 | 6 | const sortData = (data, states) => { 7 | const sortingColumn = states.sortingColumn; 8 | if (!sortingColumn || typeof sortingColumn.sortable === 'string') { 9 | return data; 10 | } 11 | return orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy); 12 | }; 13 | 14 | const getKeysMap = function (array, rowKey) { 15 | const arrayMap = {}; 16 | (array || []).forEach((row, index) => { 17 | arrayMap[getRowIdentity(row, rowKey)] = { row, index }; 18 | }); 19 | return arrayMap; 20 | }; 21 | 22 | const toggleRowSelection = function (states, row, selected) { 23 | let changed = false; 24 | const selection = states.selection; 25 | const index = selection.indexOf(row); 26 | if (typeof selected === 'undefined') { 27 | if (index === -1) { 28 | selection.push(row); 29 | changed = true; 30 | } else { 31 | selection.splice(index, 1); 32 | changed = true; 33 | } 34 | } else { 35 | if (selected && index === -1) { 36 | selection.push(row); 37 | changed = true; 38 | } else if (!selected && index > -1) { 39 | selection.splice(index, 1); 40 | changed = true; 41 | } 42 | } 43 | 44 | return changed; 45 | }; 46 | 47 | const toggleRowExpansion = function (states, row, expanded) { 48 | let changed = false; 49 | const expandRows = states.expandRows; 50 | if (typeof expanded !== 'undefined') { 51 | const index = expandRows.indexOf(row); 52 | if (expanded) { 53 | if (index === -1) { 54 | expandRows.push(row); 55 | changed = true; 56 | } 57 | } else { 58 | if (index !== -1) { 59 | expandRows.splice(index, 1); 60 | changed = true; 61 | } 62 | } 63 | } else { 64 | const index = expandRows.indexOf(row); 65 | if (index === -1) { 66 | expandRows.push(row); 67 | changed = true; 68 | } else { 69 | expandRows.splice(index, 1); 70 | changed = true; 71 | } 72 | } 73 | 74 | return changed; 75 | }; 76 | 77 | const TableStore = function (table, initialState = {}) { 78 | if (!table) { 79 | throw new Error('Table is required.'); 80 | } 81 | this.table = table; 82 | 83 | this.states = { 84 | rowKey: null, 85 | _columns: [], 86 | originColumns: [], 87 | columns: [], 88 | fixedColumns: [], 89 | rightFixedColumns: [], 90 | leafColumns: [], 91 | fixedLeafColumns: [], 92 | rightFixedLeafColumns: [], 93 | leafColumnsLength: 0, 94 | fixedLeafColumnsLength: 0, 95 | rightFixedLeafColumnsLength: 0, 96 | isComplex: false, 97 | filteredData: null, 98 | data: null, 99 | sortingColumn: null, 100 | sortProp: null, 101 | sortOrder: null, 102 | isAllSelected: false, 103 | selection: [], 104 | reserveSelection: false, 105 | selectable: null, 106 | currentRow: null, 107 | hoverRow: null, 108 | filters: {}, 109 | expandRows: [], 110 | defaultExpandAll: false 111 | }; 112 | 113 | for (let prop in initialState) { 114 | if (initialState.hasOwnProperty(prop) && this.states.hasOwnProperty(prop)) { 115 | this.states[prop] = initialState[prop]; 116 | } 117 | } 118 | }; 119 | 120 | TableStore.prototype.mutations = { 121 | setData (states, data) { 122 | const dataInstanceChanged = states._data !== data; 123 | states._data = data; 124 | 125 | Object.keys(states.filters).forEach((columnId) => { 126 | const values = states.filters[columnId]; 127 | if (!values || values.length === 0) return; 128 | const column = getColumnById(this.states, columnId); 129 | if (column && column.filterMethod) { 130 | data = data.filter((row) => { 131 | return values.some(value => column.filterMethod.call(null, value, row, column)); 132 | }); 133 | } 134 | }); 135 | 136 | states.filteredData = data; 137 | states.data = sortData((data || []), states); 138 | 139 | this.updateCurrentRow(); 140 | 141 | if (!states.reserveSelection) { 142 | if (dataInstanceChanged) { 143 | this.clearSelection(); 144 | } else { 145 | this.cleanSelection(); 146 | } 147 | this.updateAllSelected(); 148 | } else { 149 | const rowKey = states.rowKey; 150 | if (rowKey) { 151 | const selection = states.selection; 152 | const selectedMap = getKeysMap(selection, rowKey); 153 | 154 | states.data.forEach((row) => { 155 | const rowId = getRowIdentity(row, rowKey); 156 | const rowInfo = selectedMap[rowId]; 157 | if (rowInfo) { 158 | selection[rowInfo.index] = row; 159 | } 160 | }); 161 | 162 | this.updateAllSelected(); 163 | } else { 164 | // console.warn('WARN: rowKey is required when reserve-selection is enabled.'); 165 | } 166 | } 167 | 168 | const defaultExpandAll = states.defaultExpandAll; 169 | if (defaultExpandAll) { 170 | this.states.expandRows = (states.data || []).slice(0); 171 | } 172 | 173 | Vue.nextTick(() => this.table.updateScrollY()); 174 | }, 175 | 176 | changeSortCondition (states, options) { 177 | states.data = sortData((states.filteredData || states._data || []), states); 178 | 179 | if (!options || !options.silent) { 180 | this.table.$emit('sort-change', { 181 | column: this.states.sortingColumn, 182 | prop: this.states.sortProp, 183 | order: this.states.sortOrder 184 | }); 185 | } 186 | 187 | Vue.nextTick(() => this.table.updateScrollY()); 188 | }, 189 | 190 | filterChange (states, options) { 191 | let { column, values, silent } = options; 192 | if (values && !Array.isArray(values)) { 193 | values = [values]; 194 | } 195 | 196 | const prop = column.property; 197 | const filters = {}; 198 | 199 | if (prop) { 200 | states.filters[column.id] = values; 201 | filters[column.columnKey || column.id] = values; 202 | } 203 | 204 | let data = states._data; 205 | 206 | Object.keys(states.filters).forEach((columnId) => { 207 | const values = states.filters[columnId]; 208 | if (!values || values.length === 0) return; 209 | const column = getColumnById(this.states, columnId); 210 | if (column && column.filterMethod) { 211 | data = data.filter((row) => { 212 | return values.some(value => column.filterMethod.call(null, value, row, column)); 213 | }); 214 | } 215 | }); 216 | 217 | states.filteredData = data; 218 | states.data = sortData(data, states); 219 | 220 | if (!silent) { 221 | this.table.$emit('filter-change', filters); 222 | } 223 | 224 | Vue.nextTick(() => this.table.updateScrollY()); 225 | }, 226 | 227 | insertColumn (states, column, index, parent) { 228 | let array = states._columns; 229 | if (parent) { 230 | array = parent.children; 231 | if (!array) array = parent.children = []; 232 | } 233 | 234 | if (typeof index !== 'undefined') { 235 | array.splice(index, 0, column); 236 | } else { 237 | array.push(column); 238 | } 239 | 240 | if (column.type === 'selection') { 241 | states.selectable = column.selectable; 242 | states.reserveSelection = column.reserveSelection; 243 | } 244 | 245 | if (this.table.$ready) { 246 | this.updateColumns(); // hack for dynamics insert column 247 | this.scheduleLayout(); 248 | } 249 | }, 250 | 251 | removeColumn (states, column, parent) { 252 | let array = states._columns; 253 | if (parent) { 254 | array = parent.children; 255 | if (!array) array = parent.children = []; 256 | } 257 | if (array) { 258 | array.splice(array.indexOf(column), 1); 259 | } 260 | 261 | if (this.table.$ready) { 262 | this.updateColumns(); // hack for dynamics remove column 263 | this.scheduleLayout(); 264 | } 265 | }, 266 | 267 | setHoverRow (states, row) { 268 | states.hoverRow = row; 269 | }, 270 | 271 | setCurrentRow (states, row) { 272 | const oldCurrentRow = states.currentRow; 273 | states.currentRow = row; 274 | 275 | if (oldCurrentRow !== row) { 276 | this.table.$emit('current-change', row, oldCurrentRow); 277 | } 278 | }, 279 | 280 | rowSelectedChanged (states, row) { 281 | const changed = toggleRowSelection(states, row); 282 | const selection = states.selection; 283 | 284 | if (changed) { 285 | const table = this.table; 286 | table.$emit('selection-change', selection ? selection.slice() : []); 287 | table.$emit('select', selection, row); 288 | } 289 | 290 | this.updateAllSelected(); 291 | }, 292 | 293 | toggleAllSelection: debounce(10, function (states) { 294 | const data = states.data || []; 295 | if (data.length === 0) return; 296 | const value = !states.isAllSelected; 297 | const selection = this.states.selection; 298 | let selectionChanged = false; 299 | 300 | data.forEach((item, index) => { 301 | if (states.selectable) { 302 | if (states.selectable.call(null, item, index) && toggleRowSelection(states, item, value)) { 303 | selectionChanged = true; 304 | } 305 | } else { 306 | if (toggleRowSelection(states, item, value)) { 307 | selectionChanged = true; 308 | } 309 | } 310 | }); 311 | 312 | const table = this.table; 313 | if (selectionChanged) { 314 | table.$emit('selection-change', selection ? selection.slice() : []); 315 | } 316 | table.$emit('select-all', selection); 317 | states.isAllSelected = value; 318 | }) 319 | }; 320 | 321 | const doFlattenColumns = (columns) => { 322 | const result = []; 323 | columns.forEach((column) => { 324 | if (column.children) { 325 | result.push.apply(result, doFlattenColumns(column.children)); 326 | } else { 327 | result.push(column); 328 | } 329 | }); 330 | return result; 331 | }; 332 | 333 | TableStore.prototype.updateColumns = function () { 334 | const states = this.states; 335 | const _columns = states._columns || []; 336 | states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left'); 337 | states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right'); 338 | 339 | if (states.fixedColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) { 340 | _columns[0].fixed = true; 341 | states.fixedColumns.unshift(_columns[0]); 342 | } 343 | 344 | const notFixedColumns = _columns.filter(column => !column.fixed); 345 | states.originColumns = [].concat(states.fixedColumns).concat(notFixedColumns).concat(states.rightFixedColumns); 346 | 347 | const leafColumns = doFlattenColumns(notFixedColumns); 348 | const fixedLeafColumns = doFlattenColumns(states.fixedColumns); 349 | const rightFixedLeafColumns = doFlattenColumns(states.rightFixedColumns); 350 | 351 | states.leafColumnsLength = leafColumns.length; 352 | states.fixedLeafColumnsLength = fixedLeafColumns.length; 353 | states.rightFixedLeafColumnsLength = rightFixedLeafColumns.length; 354 | 355 | states.columns = [].concat(fixedLeafColumns).concat(leafColumns).concat(rightFixedLeafColumns); 356 | states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0; 357 | }; 358 | 359 | TableStore.prototype.isSelected = function (row) { 360 | return (this.states.selection || []).indexOf(row) > -1; 361 | }; 362 | 363 | TableStore.prototype.clearSelection = function () { 364 | const states = this.states; 365 | states.isAllSelected = false; 366 | const oldSelection = states.selection; 367 | if (states.selection.length) { 368 | states.selection = []; 369 | } 370 | if (oldSelection.length > 0) { 371 | this.table.$emit('selection-change', states.selection ? states.selection.slice() : []); 372 | } 373 | }; 374 | 375 | TableStore.prototype.setExpandRowKeys = function (rowKeys) { 376 | const expandRows = []; 377 | const data = this.states.data; 378 | const rowKey = this.states.rowKey; 379 | if (!rowKey) throw new Error('[Table] prop row-key should not be empty.'); 380 | const keysMap = getKeysMap(data, rowKey); 381 | rowKeys.forEach((key) => { 382 | const info = keysMap[key]; 383 | if (info) { 384 | expandRows.push(info.row); 385 | } 386 | }); 387 | 388 | this.states.expandRows = expandRows; 389 | }; 390 | 391 | TableStore.prototype.toggleRowSelection = function (row, selected) { 392 | const changed = toggleRowSelection(this.states, row, selected); 393 | if (changed) { 394 | this.table.$emit('selection-change', this.states.selection ? this.states.selection.slice() : []); 395 | } 396 | }; 397 | 398 | TableStore.prototype.toggleRowExpansion = function (row, expanded) { 399 | const changed = toggleRowExpansion(this.states, row, expanded); 400 | if (changed) { 401 | this.table.$emit('expand-change', row, this.states.expandRows); 402 | this.scheduleLayout(); 403 | } 404 | }; 405 | 406 | TableStore.prototype.isRowExpanded = function (row) { 407 | const { expandRows = [], rowKey } = this.states; 408 | if (rowKey) { 409 | const expandMap = getKeysMap(expandRows, rowKey); 410 | return !!expandMap[getRowIdentity(row, rowKey)]; 411 | } 412 | return expandRows.indexOf(row) !== -1; 413 | }; 414 | 415 | TableStore.prototype.cleanSelection = function () { 416 | const selection = this.states.selection || []; 417 | const data = this.states.data; 418 | const rowKey = this.states.rowKey; 419 | let deleted; 420 | if (rowKey) { 421 | deleted = []; 422 | const selectedMap = getKeysMap(selection, rowKey); 423 | const dataMap = getKeysMap(data, rowKey); 424 | for (let key in selectedMap) { 425 | if (selectedMap.hasOwnProperty(key) && !dataMap[key]) { 426 | deleted.push(selectedMap[key].row); 427 | } 428 | } 429 | } else { 430 | deleted = selection.filter((item) => { 431 | return data.indexOf(item) === -1; 432 | }); 433 | } 434 | 435 | deleted.forEach((deletedItem) => { 436 | selection.splice(selection.indexOf(deletedItem), 1); 437 | }); 438 | 439 | if (deleted.length) { 440 | this.table.$emit('selection-change', selection ? selection.slice() : []); 441 | } 442 | }; 443 | 444 | TableStore.prototype.clearFilter = function () { 445 | const states = this.states; 446 | const { tableHeader, fixedTableHeader, rightFixedTableHeader } = this.table.$refs; 447 | let panels = {}; 448 | 449 | if (tableHeader) panels = merge(panels, tableHeader.filterPanels); 450 | if (fixedTableHeader) panels = merge(panels, fixedTableHeader.filterPanels); 451 | if (rightFixedTableHeader) panels = merge(panels, rightFixedTableHeader.filterPanels); 452 | 453 | const keys = Object.keys(panels); 454 | if (!keys.length) return; 455 | 456 | keys.forEach(key => { 457 | panels[key].filteredValue = []; 458 | }); 459 | 460 | states.filters = {}; 461 | 462 | this.commit('filterChange', { 463 | column: {}, 464 | values: [], 465 | silent: true 466 | }); 467 | }; 468 | 469 | TableStore.prototype.clearSort = function () { 470 | const states = this.states; 471 | if (!states.sortingColumn) return; 472 | states.sortingColumn.order = null; 473 | states.sortProp = null; 474 | states.sortOrder = null; 475 | 476 | this.commit('changeSortCondition', { 477 | silent: true 478 | }); 479 | }; 480 | 481 | TableStore.prototype.updateAllSelected = function () { 482 | const states = this.states; 483 | const { selection, rowKey, selectable, data } = states; 484 | if (!data || data.length === 0) { 485 | states.isAllSelected = false; 486 | return; 487 | } 488 | 489 | let selectedMap; 490 | if (rowKey) { 491 | selectedMap = getKeysMap(states.selection, rowKey); 492 | } 493 | 494 | const isSelected = function (row) { 495 | if (selectedMap) { 496 | return !!selectedMap[getRowIdentity(row, rowKey)]; 497 | } else { 498 | return selection.indexOf(row) !== -1; 499 | } 500 | }; 501 | 502 | let isAllSelected = true; 503 | let selectedCount = 0; 504 | for (let i = 0, j = data.length; i < j; i++) { 505 | const item = data[i]; 506 | const isRowSelectable = selectable && selectable.call(this, item, i); 507 | if (!isSelected(item)) { 508 | if (!selectable || isRowSelectable) { 509 | isAllSelected = false; 510 | break; 511 | } 512 | } else { 513 | selectedCount++; 514 | } 515 | } 516 | 517 | if (selectedCount === 0) isAllSelected = false; 518 | 519 | states.isAllSelected = isAllSelected; 520 | }; 521 | 522 | TableStore.prototype.scheduleLayout = function (updateColumns) { 523 | if (updateColumns) { 524 | this.updateColumns(); 525 | } 526 | this.table.debouncedUpdateLayout(); 527 | }; 528 | 529 | TableStore.prototype.setCurrentRowKey = function (key) { 530 | const states = this.states; 531 | const rowKey = states.rowKey; 532 | if (!rowKey) throw new Error('[Table] row-key should not be empty.'); 533 | const data = states.data || []; 534 | const keysMap = getKeysMap(data, rowKey); 535 | const info = keysMap[key]; 536 | if (info) { 537 | states.currentRow = info.row; 538 | } 539 | }; 540 | 541 | TableStore.prototype.updateCurrentRow = function () { 542 | const states = this.states; 543 | const table = this.table; 544 | const data = states.data || []; 545 | const oldCurrentRow = states.currentRow; 546 | 547 | if (data.indexOf(oldCurrentRow) === -1) { 548 | states.currentRow = null; 549 | 550 | if (states.currentRow !== oldCurrentRow) { 551 | table.$emit('current-change', null, oldCurrentRow); 552 | } 553 | } 554 | }; 555 | 556 | TableStore.prototype.commit = function (name, ...args) { 557 | const mutations = this.mutations; 558 | if (mutations[name]) { 559 | mutations[name].apply(this, [this.states].concat(args)); 560 | } else { 561 | throw new Error(`Action not found: ${name}`); 562 | } 563 | }; 564 | 565 | export default TableStore; 566 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-header.js: -------------------------------------------------------------------------------- 1 | import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom'; 2 | import ElCheckbox from 'element-ui/packages/checkbox'; 3 | import ElTag from 'element-ui/packages/tag'; 4 | import Vue from 'vue'; 5 | import FilterPanel from './filter-panel.vue'; 6 | import LayoutObserver from './layout-observer'; 7 | 8 | const getAllColumns = (columns) => { 9 | const result = []; 10 | columns.forEach((column) => { 11 | if (column.children) { 12 | result.push(column); 13 | result.push.apply(result, getAllColumns(column.children)); 14 | } else { 15 | result.push(column); 16 | } 17 | }); 18 | return result; 19 | }; 20 | 21 | const convertToRows = (originColumns) => { 22 | let maxLevel = 1; 23 | const traverse = (column, parent) => { 24 | if (parent) { 25 | column.level = parent.level + 1; 26 | if (maxLevel < column.level) { 27 | maxLevel = column.level; 28 | } 29 | } 30 | if (column.children) { 31 | let colSpan = 0; 32 | column.children.forEach((subColumn) => { 33 | traverse(subColumn, column); 34 | colSpan += subColumn.colSpan; 35 | }); 36 | column.colSpan = colSpan; 37 | } else { 38 | column.colSpan = 1; 39 | } 40 | }; 41 | 42 | originColumns.forEach((column) => { 43 | column.level = 1; 44 | traverse(column); 45 | }); 46 | 47 | const rows = []; 48 | for (let i = 0; i < maxLevel; i++) { 49 | rows.push([]); 50 | } 51 | 52 | const allColumns = getAllColumns(originColumns); 53 | 54 | allColumns.forEach((column) => { 55 | if (!column.children) { 56 | column.rowSpan = maxLevel - column.level + 1; 57 | } else { 58 | column.rowSpan = 1; 59 | } 60 | rows[column.level - 1].push(column); 61 | }); 62 | 63 | return rows; 64 | }; 65 | 66 | export default { 67 | name: 'ElTableHeader', 68 | 69 | mixins: [LayoutObserver], 70 | 71 | render (h) { 72 | const originColumns = this.store.states.originColumns; 73 | const columnRows = convertToRows(originColumns, this.columns); 74 | // 是否拥有多级表头 75 | const isGroup = columnRows.length > 1; 76 | if (isGroup) this.$parent.isGroup = true; 77 | return ( 78 | 83 | 84 | { 85 | this._l(this.columns, column => ) 86 | } 87 | { 88 | this.hasGutter ? : '' 89 | } 90 | 91 | 92 | { 93 | this._l(columnRows, (columns, rowIndex) => 94 | 98 | { 99 | this._l(columns, (column, cellIndex) => 100 | 133 | ) 134 | } 135 | { 136 | this.hasGutter ? : '' 137 | } 138 | 139 | ) 140 | } 141 | 142 |
this.handleMouseMove($event, column) } 104 | on-mouseout={ this.handleMouseOut } 105 | on-mousedown={ ($event) => this.handleMouseDown($event, column) } 106 | on-click={ ($event) => this.handleHeaderClick($event, column) } 107 | on-contextmenu={ ($event) => this.handleHeaderContextMenu($event, column) } 108 | style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) } 109 | class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) }> 110 |
0 ? 'highlight' : '', column.labelClassName] }> 111 | { 112 | column.renderHeader 113 | ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }) 114 | : column.label 115 | } 116 | { 117 | column.sortable 118 | ? this.handleSortClick($event, column) }> 119 | this.handleSortClick($event, column, 'ascending') }> 120 | 121 | this.handleSortClick($event, column, 'descending') }> 122 | 123 | 124 | : '' 125 | } 126 | { 127 | column.filterable 128 | ? this.handleFilterClick($event, column) }> 129 | : '' 130 | } 131 |
132 |
143 | ); 144 | }, 145 | 146 | props: { 147 | fixed: String, 148 | store: { 149 | required: true 150 | }, 151 | border: Boolean, 152 | defaultSort: { 153 | type: Object, 154 | default () { 155 | return { 156 | prop: '', 157 | order: '' 158 | }; 159 | } 160 | } 161 | }, 162 | 163 | components: { 164 | ElCheckbox, 165 | ElTag 166 | }, 167 | 168 | computed: { 169 | table () { 170 | return this.$parent; 171 | }, 172 | 173 | isAllSelected () { 174 | return this.store.states.isAllSelected; 175 | }, 176 | 177 | columnsCount () { 178 | return this.store.states.columns.length; 179 | }, 180 | 181 | leftFixedCount () { 182 | return this.store.states.fixedColumns.length; 183 | }, 184 | 185 | rightFixedCount () { 186 | return this.store.states.rightFixedColumns.length; 187 | }, 188 | 189 | leftFixedLeafCount () { 190 | return this.store.states.fixedLeafColumnsLength; 191 | }, 192 | 193 | rightFixedLeafCount () { 194 | return this.store.states.rightFixedLeafColumnsLength; 195 | }, 196 | 197 | columns () { 198 | return this.store.states.columns; 199 | }, 200 | 201 | hasGutter () { 202 | return !this.fixed && this.tableLayout.gutterWidth; 203 | } 204 | }, 205 | 206 | created () { 207 | this.filterPanels = {}; 208 | }, 209 | 210 | mounted () { 211 | if (this.defaultSort.prop) { 212 | const states = this.store.states; 213 | states.sortProp = this.defaultSort.prop; 214 | states.sortOrder = this.defaultSort.order || 'ascending'; 215 | this.$nextTick(() => { 216 | for (let i = 0, length = this.columns.length; i < length; i++) { 217 | let column = this.columns[i]; 218 | if (column.property === states.sortProp) { 219 | column.order = states.sortOrder; 220 | states.sortingColumn = column; 221 | break; 222 | } 223 | } 224 | 225 | if (states.sortingColumn) { 226 | this.store.commit('changeSortCondition'); 227 | } 228 | }); 229 | } 230 | }, 231 | 232 | beforeDestroy () { 233 | const panels = this.filterPanels; 234 | for (let prop in panels) { 235 | if (panels.hasOwnProperty(prop) && panels[prop]) { 236 | panels[prop].$destroy(true); 237 | } 238 | } 239 | }, 240 | 241 | methods: { 242 | isCellHidden (index, columns) { 243 | let start = 0; 244 | for (let i = 0; i < index; i++) { 245 | start += columns[i].colSpan; 246 | } 247 | const after = start + columns[index].colSpan - 1; 248 | if (this.fixed === true || this.fixed === 'left') { 249 | return after >= this.leftFixedLeafCount; 250 | } else if (this.fixed === 'right') { 251 | return start < this.columnsCount - this.rightFixedLeafCount; 252 | } else { 253 | return (after < this.leftFixedLeafCount) || (start >= this.columnsCount - this.rightFixedLeafCount); 254 | } 255 | }, 256 | 257 | getHeaderRowStyle (rowIndex) { 258 | const headerRowStyle = this.table.headerRowStyle; 259 | if (typeof headerRowStyle === 'function') { 260 | return headerRowStyle.call(this, { rowIndex }); 261 | } 262 | return headerRowStyle; 263 | }, 264 | 265 | getHeaderRowClass (rowIndex) { 266 | const classes = []; 267 | 268 | const headerRowClassName = this.table.headerRowClassName; 269 | if (typeof headerRowClassName === 'string') { 270 | classes.push(headerRowClassName); 271 | } else if (typeof headerRowClassName === 'function') { 272 | classes.push(headerRowClassName.call(this, { rowIndex })); 273 | } 274 | 275 | return classes.join(' '); 276 | }, 277 | 278 | getHeaderCellStyle (rowIndex, columnIndex, row, column) { 279 | const headerCellStyle = this.table.headerCellStyle; 280 | if (typeof headerCellStyle === 'function') { 281 | return headerCellStyle.call(this, { 282 | rowIndex, 283 | columnIndex, 284 | row, 285 | column 286 | }); 287 | } 288 | return headerCellStyle; 289 | }, 290 | 291 | getHeaderCellClass (rowIndex, columnIndex, row, column) { 292 | const classes = [column.id, column.order, column.headerAlign, column.className, column.labelClassName]; 293 | 294 | if (rowIndex === 0 && this.isCellHidden(columnIndex, row)) { 295 | classes.push('is-hidden'); 296 | } 297 | 298 | if (!column.children) { 299 | classes.push('is-leaf'); 300 | } 301 | 302 | if (column.sortable) { 303 | classes.push('is-sortable'); 304 | } 305 | 306 | const headerCellClassName = this.table.headerCellClassName; 307 | if (typeof headerCellClassName === 'string') { 308 | classes.push(headerCellClassName); 309 | } else if (typeof headerCellClassName === 'function') { 310 | classes.push(headerCellClassName.call(this, { 311 | rowIndex, 312 | columnIndex, 313 | row, 314 | column 315 | })); 316 | } 317 | 318 | return classes.join(' '); 319 | }, 320 | 321 | toggleAllSelection () { 322 | this.store.commit('toggleAllSelection'); 323 | }, 324 | 325 | handleFilterClick (event, column) { 326 | event.stopPropagation(); 327 | const target = event.target; 328 | const cell = target.parentNode; 329 | const table = this.$parent; 330 | 331 | let filterPanel = this.filterPanels[column.id]; 332 | 333 | if (filterPanel && column.filterOpened) { 334 | filterPanel.showPopper = false; 335 | return; 336 | } 337 | 338 | if (!filterPanel) { 339 | filterPanel = new Vue(FilterPanel); 340 | this.filterPanels[column.id] = filterPanel; 341 | if (column.filterPlacement) { 342 | filterPanel.placement = column.filterPlacement; 343 | } 344 | filterPanel.table = table; 345 | filterPanel.cell = cell; 346 | filterPanel.column = column; 347 | !this.$isServer && filterPanel.$mount(document.createElement('div')); 348 | } 349 | 350 | setTimeout(() => { 351 | filterPanel.showPopper = true; 352 | }, 16); 353 | }, 354 | 355 | handleHeaderClick (event, column) { 356 | if (!column.filters && column.sortable) { 357 | this.handleSortClick(event, column); 358 | } else if (column.filters && !column.sortable) { 359 | this.handleFilterClick(event, column); 360 | } 361 | 362 | this.$parent.$emit('header-click', column, event); 363 | }, 364 | 365 | handleHeaderContextMenu (event, column) { 366 | this.$parent.$emit('header-contextmenu', column, event); 367 | }, 368 | 369 | handleMouseDown (event, column) { 370 | if (this.$isServer) return; 371 | if (column.children && column.children.length > 0) return; 372 | /* istanbul ignore if */ 373 | if (this.draggingColumn && this.border) { 374 | this.dragging = true; 375 | 376 | this.$parent.resizeProxyVisible = true; 377 | 378 | const table = this.$parent; 379 | const tableEl = table.$el; 380 | const tableLeft = tableEl.getBoundingClientRect().left; 381 | const columnEl = this.$el.querySelector(`th.${column.id}`); 382 | const columnRect = columnEl.getBoundingClientRect(); 383 | const minLeft = columnRect.left - tableLeft + 30; 384 | 385 | addClass(columnEl, 'noclick'); 386 | 387 | this.dragState = { 388 | startMouseLeft: event.clientX, 389 | startLeft: columnRect.right - tableLeft, 390 | startColumnLeft: columnRect.left - tableLeft, 391 | tableLeft 392 | }; 393 | 394 | const resizeProxy = table.$refs.resizeProxy; 395 | resizeProxy.style.left = this.dragState.startLeft + 'px'; 396 | 397 | document.onselectstart = function () { return false; }; 398 | document.ondragstart = function () { return false; }; 399 | 400 | const handleMouseMove = (event) => { 401 | const deltaLeft = event.clientX - this.dragState.startMouseLeft; 402 | const proxyLeft = this.dragState.startLeft + deltaLeft; 403 | 404 | resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px'; 405 | }; 406 | 407 | const handleMouseUp = () => { 408 | if (this.dragging) { 409 | const { 410 | startColumnLeft, 411 | startLeft 412 | } = this.dragState; 413 | const finalLeft = parseInt(resizeProxy.style.left, 10); 414 | const columnWidth = finalLeft - startColumnLeft; 415 | column.width = column.realWidth = columnWidth; 416 | table.$emit('header-dragend', column.width, startLeft - startColumnLeft, column, event); 417 | 418 | this.store.scheduleLayout(); 419 | 420 | document.body.style.cursor = ''; 421 | this.dragging = false; 422 | this.draggingColumn = null; 423 | this.dragState = {}; 424 | 425 | table.resizeProxyVisible = false; 426 | } 427 | 428 | document.removeEventListener('mousemove', handleMouseMove); 429 | document.removeEventListener('mouseup', handleMouseUp); 430 | document.onselectstart = null; 431 | document.ondragstart = null; 432 | 433 | setTimeout(function () { 434 | removeClass(columnEl, 'noclick'); 435 | }, 0); 436 | }; 437 | 438 | document.addEventListener('mousemove', handleMouseMove); 439 | document.addEventListener('mouseup', handleMouseUp); 440 | } 441 | }, 442 | 443 | handleMouseMove (event, column) { 444 | if (column.children && column.children.length > 0) return; 445 | let target = event.target; 446 | while (target && target.tagName !== 'TH') { 447 | target = target.parentNode; 448 | } 449 | 450 | if (!column || !column.resizable) return; 451 | 452 | if (!this.dragging && this.border) { 453 | let rect = target.getBoundingClientRect(); 454 | 455 | const bodyStyle = document.body.style; 456 | if (rect.width > 12 && rect.right - event.pageX < 8) { 457 | bodyStyle.cursor = 'col-resize'; 458 | if (hasClass(target, 'is-sortable')) { 459 | target.style.cursor = 'col-resize'; 460 | } 461 | this.draggingColumn = column; 462 | } else if (!this.dragging) { 463 | bodyStyle.cursor = ''; 464 | if (hasClass(target, 'is-sortable')) { 465 | target.style.cursor = 'pointer'; 466 | } 467 | this.draggingColumn = null; 468 | } 469 | } 470 | }, 471 | 472 | handleMouseOut () { 473 | if (this.$isServer) return; 474 | document.body.style.cursor = ''; 475 | }, 476 | 477 | toggleOrder (order) { 478 | return !order ? 'ascending' : order === 'ascending' ? 'descending' : null; 479 | }, 480 | 481 | handleSortClick (event, column, givenOrder) { 482 | event.stopPropagation(); 483 | let order = givenOrder || this.toggleOrder(column.order); 484 | 485 | let target = event.target; 486 | while (target && target.tagName !== 'TH') { 487 | target = target.parentNode; 488 | } 489 | 490 | if (target && target.tagName === 'TH') { 491 | if (hasClass(target, 'noclick')) { 492 | removeClass(target, 'noclick'); 493 | return; 494 | } 495 | } 496 | 497 | if (!column.sortable) return; 498 | 499 | const states = this.store.states; 500 | let sortProp = states.sortProp; 501 | let sortOrder; 502 | const sortingColumn = states.sortingColumn; 503 | 504 | if (sortingColumn !== column || (sortingColumn === column && sortingColumn.order === null)) { 505 | if (sortingColumn) { 506 | sortingColumn.order = null; 507 | } 508 | states.sortingColumn = column; 509 | sortProp = column.property; 510 | } 511 | 512 | if (!order) { 513 | sortOrder = column.order = null; 514 | states.sortingColumn = null; 515 | sortProp = null; 516 | } else { 517 | sortOrder = column.order = order; 518 | } 519 | 520 | states.sortProp = sortProp; 521 | states.sortOrder = sortOrder; 522 | 523 | this.store.commit('changeSortCondition'); 524 | } 525 | }, 526 | 527 | data () { 528 | return { 529 | draggingColumn: null, 530 | dragging: false, 531 | dragState: {} 532 | }; 533 | } 534 | }; 535 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table-body.js: -------------------------------------------------------------------------------- 1 | import { getCell, getColumnByCell, getRowIdentity } from './util'; 2 | import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom'; 3 | import ElCheckbox from 'element-ui/packages/checkbox'; 4 | import debounce from 'throttle-debounce/debounce'; 5 | import LayoutObserver from './layout-observer'; 6 | 7 | export default { 8 | name: 'ElTableBody', 9 | 10 | mixins: [LayoutObserver], 11 | 12 | components: { 13 | ElCheckbox 14 | }, 15 | 16 | props: { 17 | store: { 18 | required: true 19 | }, 20 | stripe: Boolean, 21 | context: {}, 22 | rowClassName: [String, Function], 23 | rowStyle: [Object, Function], 24 | fixed: String, 25 | highlight: Boolean, 26 | // 27 | tableData: Array, 28 | times0: Number, 29 | times1: Number, 30 | times2: Number, 31 | groupIndex: Number, 32 | itemNum: Number, 33 | tableIndex: Number, 34 | itemRowHeight: { 35 | type: Number, 36 | default: 28 37 | } 38 | }, 39 | 40 | render (h) { 41 | const columnsHidden = this.columns.map((column, index) => this.isColumnHidden(index)); 42 | return ( 43 | 49 | 50 | { 51 | this._l(this.columns, column => ) 52 | } 53 | 54 | 55 | { 56 | this._l(this.timesTableData, (row, $index) => 57 | [ this.handleDoubleClick($event, row) } 61 | on-click={ ($event) => this.handleClick($event, row) } 62 | on-contextmenu={ ($event) => this.handleContextMenu($event, row) } 63 | on-mouseenter={ () => this.handleMouseEnter(this.fvIndex($index, row)) } 64 | on-mouseleave={ () => this.handleMouseLeave() } 65 | class={ [this.getRowClass(row, this.fvIndex($index, row))] }> 66 | { 67 | this._l(this.columns, (column, cellIndex) => { 68 | const { rowspan, colspan } = this.getSpan(row, column, this.fvIndex($index, row), cellIndex); 69 | if (!rowspan || !colspan) { 70 | return ''; 71 | } else { 72 | if (rowspan === 1 && colspan === 1) { 73 | return ( 74 | 94 | ); 95 | } else { 96 | return ( 97 | 119 | ); 120 | } 121 | } 122 | }) 123 | } 124 | , 125 | this.store.isRowExpanded(row) 126 | ? ( 127 | 130 | ) 131 | : '' 132 | ] 133 | ).concat( 134 | 135 | ) 136 | } 137 | 138 |
this.handleCellMouseEnter($event, row) } 78 | on-mouseleave={ this.handleCellMouseLeave }> 79 | { 80 | column.renderCell.call( 81 | this._renderProxy, 82 | h, 83 | { 84 | row, 85 | column, 86 | $index: this.fvIndex($index, row), 87 | store: this.store, 88 | _self: this.context || this.table.$vnode.context 89 | }, 90 | columnsHidden[cellIndex] 91 | ) 92 | } 93 | this.handleCellMouseEnter($event, row) } 103 | on-mouseleave={ this.handleCellMouseLeave }> 104 | { 105 | column.renderCell.call( 106 | this._renderProxy, 107 | h, 108 | { 109 | row, 110 | column, 111 | $index: this.fvIndex($index, row), 112 | store: this.store, 113 | _self: this.context || this.table.$vnode.context 114 | }, 115 | columnsHidden[cellIndex] 116 | ) 117 | } 118 |
128 | { this.table.renderExpanded ? this.table.renderExpanded(h, { row, $index: this.fvIndex($index, row), store: this.store }) : ''} 129 |
139 | ); 140 | }, 141 | 142 | watch: { 143 | 'store.states.hoverRow' (newVal, oldVal) { 144 | // if (!this.store.states.isComplex) return; 145 | const el = this.$el; 146 | if (!el) return; 147 | const tr = el.querySelector('tbody').children; 148 | const rows = [].filter.call(tr, row => hasClass(row, 'el-table__row')); 149 | if (this.itemNum) { 150 | newVal %= this.itemNum; 151 | oldVal %= this.itemNum; 152 | } 153 | const oldRow = rows[oldVal]; 154 | const newRow = rows[newVal]; 155 | if (oldRow) { 156 | removeClass(oldRow, 'hover-row'); 157 | } 158 | if (newRow) { 159 | addClass(newRow, 'hover-row'); 160 | } 161 | }, 162 | 'store.states.currentRow' (newVal, oldVal) { 163 | if (!this.highlight) return; 164 | const el = this.$el; 165 | if (!el) return; 166 | // const data = this.store.states.data; 167 | const data = this.timesTableData; 168 | const tr = el.querySelector('tbody').children; 169 | const rows = [].filter.call(tr, row => hasClass(row, 'el-table__row')); 170 | const oldRow = rows[data.indexOf(oldVal)]; 171 | const newRow = rows[data.indexOf(newVal)]; 172 | if (oldRow) { 173 | removeClass(oldRow, 'current-row'); 174 | } else { 175 | [].forEach.call(rows, row => removeClass(row, 'current-row')); 176 | } 177 | if (newRow) { 178 | addClass(newRow, 'current-row'); 179 | } 180 | } 181 | }, 182 | 183 | computed: { 184 | table () { 185 | return this.$parent; 186 | }, 187 | 188 | data () { 189 | return this.store.states.data; 190 | }, 191 | 192 | columnsCount () { 193 | return this.store.states.columns.length; 194 | }, 195 | 196 | leftFixedLeafCount () { 197 | return this.store.states.fixedLeafColumnsLength; 198 | }, 199 | 200 | rightFixedLeafCount () { 201 | return this.store.states.rightFixedLeafColumnsLength; 202 | }, 203 | 204 | leftFixedCount () { 205 | return this.store.states.fixedColumns.length; 206 | }, 207 | 208 | rightFixedCount () { 209 | return this.store.states.rightFixedColumns.length; 210 | }, 211 | 212 | columns () { 213 | return this.store.states.columns; 214 | }, 215 | // 取数据 216 | timesTableData () { 217 | let data = []; 218 | let count1 = 0; 219 | let count2 = 0; 220 | let count3 = 0; 221 | this.data.filter((e, i) => { 222 | e.initRowIndex = i; 223 | }); 224 | switch (this.tableIndex) { 225 | case 1: 226 | count1 = this.times0 * this.itemNum * 3; 227 | data = this.data.slice(count1, count1 + this.itemNum); 228 | break; 229 | case 2: 230 | count2 = this.times1 * this.itemNum * 3; 231 | data = this.data.slice(count2 + this.itemNum, count2 + this.itemNum * 2); 232 | break; 233 | case 3: 234 | count3 = this.times2 * this.itemNum * 3; 235 | data = this.data.slice(count3 + this.itemNum * 2, count3 + this.itemNum * 3); 236 | break; 237 | } 238 | // 选中行 239 | this.$nextTick(() => { 240 | this.dfCurrentRow(this.store.states.currentRow); 241 | }); 242 | 243 | return data; 244 | } 245 | }, 246 | 247 | data () { 248 | return { 249 | tooltipContent: '', 250 | // 定时器监听table高度改变 251 | intervalId: null, 252 | // 当前table高度 253 | tableBodyHeight: 0 254 | }; 255 | }, 256 | 257 | created () { 258 | this.activateTooltip = debounce(50, tooltip => tooltip.handleShowPopper()); 259 | }, 260 | mounted () { 261 | this.$nextTick(() => { 262 | this.tableBodyHeight = this.$refs.tableBody.offsetHeight; 263 | // 定时器监听当前表格高度改变 264 | this.intervalId = setInterval(() => { 265 | if (this.$refs.tableBody.offsetHeight > 0 && this.tableBodyHeight !== this.$refs.tableBody.offsetHeight) { 266 | // 有改变 267 | this.tableBodyHeight = this.$refs.tableBody.offsetHeight; 268 | // 当前块取当前分组 269 | let groupIndex = this.times0; 270 | switch (this.tableIndex) { 271 | case 1: 272 | groupIndex = this.times0 * 3 + this.tableIndex - 1; 273 | break; 274 | case 2: 275 | groupIndex = this.times1 * 3 + this.tableIndex - 1; 276 | break; 277 | case 3: 278 | groupIndex = this.times2 * 3 + this.tableIndex - 1; 279 | break; 280 | } 281 | // 执行修改高度与总高度 282 | this.$emit('changeHeight', groupIndex, this.tableBodyHeight); 283 | } 284 | }, 80); 285 | }); 286 | }, 287 | destroyed () { 288 | if (this.intervalId) { 289 | clearInterval(this.intervalId); 290 | } 291 | }, 292 | methods: { 293 | // _l (data, cb) { 294 | // for (let i in data) { 295 | // cb(data[i], i); 296 | // } 297 | // }, 298 | getKeyOfRow (row, index) { 299 | const rowKey = this.table.rowKey; 300 | if (rowKey) { 301 | return getRowIdentity(row, rowKey); 302 | } 303 | return index; 304 | }, 305 | 306 | isColumnHidden (index) { 307 | if (this.fixed === true || this.fixed === 'left') { 308 | return index >= this.leftFixedLeafCount; 309 | } else if (this.fixed === 'right') { 310 | return index < this.columnsCount - this.rightFixedLeafCount; 311 | } else { 312 | return (index < this.leftFixedLeafCount) || (index >= this.columnsCount - this.rightFixedLeafCount); 313 | } 314 | }, 315 | 316 | getSpan (row, column, rowIndex, columnIndex) { 317 | let rowspan = 1; 318 | let colspan = 1; 319 | 320 | const fn = this.table.spanMethod; 321 | if (typeof fn === 'function') { 322 | const result = fn({ 323 | row, 324 | column, 325 | rowIndex, 326 | columnIndex 327 | }); 328 | 329 | if (Array.isArray(result)) { 330 | rowspan = result[0]; 331 | colspan = result[1]; 332 | } else if (typeof result === 'object') { 333 | rowspan = result.rowspan; 334 | colspan = result.colspan; 335 | } 336 | } 337 | 338 | return { 339 | rowspan, 340 | colspan 341 | }; 342 | }, 343 | 344 | getRowStyle (row, rowIndex) { 345 | const rowStyle = this.table.rowStyle; 346 | if (typeof rowStyle === 'function') { 347 | return rowStyle.call(this, { 348 | row, 349 | rowIndex 350 | }); 351 | } 352 | return rowStyle; 353 | }, 354 | 355 | getRowClass (row, rowIndex) { 356 | const classes = ['el-table__row']; 357 | 358 | if (this.stripe && rowIndex % 2 === 1) { 359 | classes.push('el-table__row--striped'); 360 | } 361 | const rowClassName = this.table.rowClassName; 362 | if (typeof rowClassName === 'string') { 363 | classes.push(rowClassName); 364 | } else if (typeof rowClassName === 'function') { 365 | classes.push(rowClassName.call(this, { 366 | row, 367 | rowIndex 368 | })); 369 | } 370 | 371 | if (this.store.states.expandRows.indexOf(row) > -1) { 372 | classes.push('expanded'); 373 | } 374 | 375 | return classes.join(' '); 376 | }, 377 | 378 | getCellStyle (rowIndex, columnIndex, row, column) { 379 | const cellStyle = this.table.cellStyle; 380 | if (typeof cellStyle === 'function') { 381 | return cellStyle.call(this, { 382 | rowIndex, 383 | columnIndex, 384 | row, 385 | column 386 | }); 387 | } 388 | return cellStyle; 389 | }, 390 | 391 | getCellClass (rowIndex, columnIndex, row, column) { 392 | const classes = [column.id, column.align, column.className]; 393 | 394 | if (this.isColumnHidden(columnIndex)) { 395 | classes.push('is-hidden'); 396 | } 397 | 398 | const cellClassName = this.table.cellClassName; 399 | if (typeof cellClassName === 'string') { 400 | classes.push(cellClassName); 401 | } else if (typeof cellClassName === 'function') { 402 | classes.push(cellClassName.call(this, { 403 | rowIndex, 404 | columnIndex, 405 | row, 406 | column 407 | })); 408 | } 409 | 410 | return classes.join(' '); 411 | }, 412 | 413 | handleCellMouseEnter (event, row) { 414 | const table = this.table; 415 | const cell = getCell(event); 416 | 417 | if (cell) { 418 | const column = getColumnByCell(table, cell); 419 | const hoverState = table.hoverState = {cell, column, row}; 420 | table.$emit('cell-mouse-enter', hoverState.row, hoverState.column, hoverState.cell, event); 421 | } 422 | 423 | // 判断是否text-overflow, 如果是就显示tooltip 424 | const cellChild = event.target.querySelector('.cell'); 425 | 426 | if (hasClass(cellChild, 'el-tooltip') && cellChild.scrollWidth > cellChild.offsetWidth && this.$refs.tooltip) { 427 | const tooltip = this.$refs.tooltip; 428 | // TODO 会引起整个 Table 的重新渲染,需要优化 429 | this.tooltipContent = cell.textContent || cell.innerText; 430 | tooltip.referenceElm = cell; 431 | tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none'); 432 | tooltip.doDestroy(); 433 | tooltip.setExpectedState(true); 434 | this.activateTooltip(tooltip); 435 | } 436 | }, 437 | 438 | handleCellMouseLeave (event) { 439 | const tooltip = this.$refs.tooltip; 440 | if (tooltip) { 441 | tooltip.setExpectedState(false); 442 | tooltip.handleClosePopper(); 443 | } 444 | const cell = getCell(event); 445 | if (!cell) return; 446 | 447 | const oldHoverState = this.table.hoverState || {}; 448 | this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event); 449 | }, 450 | 451 | handleMouseEnter (index) { 452 | this.store.commit('setHoverRow', index); 453 | }, 454 | 455 | handleMouseLeave () { 456 | this.store.commit('setHoverRow', null); 457 | }, 458 | 459 | handleContextMenu (event, row) { 460 | this.handleEvent(event, row, 'contextmenu'); 461 | }, 462 | 463 | handleDoubleClick (event, row) { 464 | this.handleEvent(event, row, 'dblclick'); 465 | }, 466 | 467 | handleClick (event, row) { 468 | this.store.commit('setCurrentRow', row); 469 | this.handleEvent(event, row, 'click'); 470 | }, 471 | 472 | handleEvent (event, row, name) { 473 | const table = this.table; 474 | const cell = getCell(event); 475 | let column; 476 | if (cell) { 477 | column = getColumnByCell(table, cell); 478 | if (column) { 479 | table.$emit(`cell-${name}`, row, column, cell, event); 480 | } 481 | } 482 | table.$emit(`row-${name}`, row, event, column); 483 | }, 484 | 485 | handleExpandClick (row, e) { 486 | e.stopPropagation(); 487 | this.store.toggleRowExpansion(row); 488 | }, 489 | // $index 处理 490 | fvIndex (index, item) { 491 | let _index = index; 492 | if (item && 'initRowIndex' in item) { 493 | _index = item['initRowIndex']; 494 | } 495 | return _index; 496 | }, 497 | // 默认选中行处理 498 | dfCurrentRow (currentRow) { 499 | if (!this.highlight) return; 500 | const el = this.$el; 501 | if (!el) return; 502 | // const data = this.store.states.data; 503 | const data = this.timesTableData; 504 | const tr = el.querySelector('tbody').children; 505 | const rows = [].filter.call(tr, row => hasClass(row, 'el-table__row')); 506 | const oldRow = rows[data.indexOf(currentRow)]; 507 | const newRow = rows[data.indexOf(currentRow)]; 508 | if (oldRow) { 509 | removeClass(oldRow, 'current-row'); 510 | } else { 511 | [].forEach.call(rows, row => removeClass(row, 'current-row')); 512 | } 513 | if (newRow) { 514 | addClass(newRow, 'current-row'); 515 | } 516 | } 517 | } 518 | }; 519 | -------------------------------------------------------------------------------- /src/vue-element-bigdata-table/table.vue: -------------------------------------------------------------------------------- 1 | 227 | 228 | 695 | 700 | --------------------------------------------------------------------------------