├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── index-render.js ├── main.js └── vue-bigdata-table │ ├── components │ ├── button.vue │ ├── input-render.js │ ├── input.vue │ ├── item-table.vue │ ├── renderDom.js │ └── sort-button.vue │ ├── index.js │ ├── mixins │ ├── data-handle.js │ ├── edit.js │ ├── empty-table.js │ ├── filter.js │ ├── header-move.js │ ├── index.js │ ├── sort.js │ └── style-compute.js │ ├── util │ └── index.js │ ├── vue-bigdata-table.less │ └── vue-bigdata-table.vue └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-3" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-bigdata-table 2 | 3 | > Powerful, table components optimized for large amounts of data, based on Vue2.0 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 | ## Feature 18 | 19 | 采用虚拟渲染方案,解决大数据量DOM渲染性能瓶颈,原理请看文章[实战Vue百万条数据渲染表格组件开发](https://juejin.im/post/5ad876a36fb9a045df1720b9?utm_source=gold_browser_extension) 20 | 21 | #### 作者系列视频课程: 22 | 23 | [Vue技术栈开发实战(26课时)](https://segmentfault.com/ls/1650000016221751?utm_source=recommend_web-live-new) 24 | 25 | [TypeScript完全解读(26课时)](https://segmentfault.com/ls/1650000018455856?utm_source=recommend_web-live-new) 26 | 27 | #### 进群和4000+前后端开发者交流学习 28 | 29 | ![image](https://github.com/lison16/v-org-tree/blob/master/group.png) 30 | 31 | ## API 32 | 33 | ### props: 34 | 35 | 属性 | 说明 |  类型 |  默认值 36 | :-------: | ------- | :-------: | :-------: 37 | showIndex | 是否显示序列号列 |  Boolean |  false 38 | value |  表格数据,可以使用v-model双向绑定 |  Array |  - 39 | rowHeight | 表格行高 |  Number |  48 40 | fixed |  固定表头,设为true后表头不随表格滚动 |  Boolean |  false 41 | fixedWrapperWidth | 设为true后表格列宽总是平分容器宽度减去indexWidth后的宽度 |  Boolean |  false 42 | disabledHover |  是否取消鼠标悬浮高亮效果 |  Boolean |  true 43 | columns | 表头数组,元素为单个表头的对象,{title: 'xxx', width: 该列宽度(number), render: (h) => {}, cellRender: (h, params) => {}},默认只需要title属性,render是表头渲染函数,cellRender是列单元格渲染函数 | Array | - 44 | colWidth |  列宽,如果单独列中指定了宽度则按单独列,如果所有宽度加起来比容器宽度小,则平分宽度,否则用colWidth |  Number |  100 45 | headerHeight |  表头高度 |  Number |  52 46 | highlightRow | 点击一行是否高亮 | Boolean | false 47 | headerTrStyle | 表头tr样式 |  Object |  {} 48 | indexWidth |  序列号列宽,如果没有设置,则会根据数据行数自动计算合适的宽度 |  Number |  - 49 | indexRender |  序列号渲染render |  Function |  (h, index) => {return h('span', index + 1);} 50 | indexRenderParams | indexRender的第三个参数 | Object | default: () => ({}) 51 | stripe |  是否显示斑马线 |  Boolean |  false 52 | atLeftCellPosi |  指定当前鼠标在表头单元格左侧atLeftCellPosi像素处 |  Number |  80 53 | atRightCellPosi |  指定当前鼠标在表头单元格右侧atRightCellPosi像素处 |  Number |  80 54 | fixedCol |  固定的列的范围,[0, fixedCol],设为2即固定0,1,2列,这三列横向不滚动,固定后列横向不随表格滚动 |  Number |  -1 55 | appendNum |  根据表格容器高度计算内置单个表格(1/3)渲染的行数基础上额外渲染的行数,行数越多表格接替渲染效果越好,但越耗性能 |  Number |  15 56 | canEdit |  是否可编辑 |  Boolean |  false 57 | startEditType |  触发编辑单元格的方式,目前只支持dblclick一种,即鼠标双击单元格 |  String |  'dblclick' 58 | editCellRender |  自定义编辑单元格的render函数,如果不指定则使用默认内置的editRender,可参考components/input-render.js |  Funciton |  editRender 59 | beforeSave |  保存修改的单元格内容之前的钩子,如果该函数返回false,则阻止保存 |  Function |  ({ row, col, value, initRowIndex }) => {return true} 60 | selectable |  是否可选择单元格,开启后效果就像excel点击一个单元格然后拖动选择 |  Boolean |  false 61 | paste |  是否可粘贴,设为true后可划选要粘贴的位置,然后ctrl+v粘贴从其他地方复制的表格数据,设为true则selectable将开启 |  Boolean |  false 62 | sortable |  是否可排序 |  Boolean |  false 63 | sortIndex |  开启排序的列序号数组或序号 |  Array, Number |  - 64 | defaultSort |  数据默认排序方式,是一个包含一对键值对的对象,键是要按其排序的序号,值是'up'(升序)或'down'(降序)(为方便记忆,并没有使用'asc'和'desc') |  Object |  - 65 | 66 | ### Event: 67 | 68 | 事件名 | 说明 |  返回值 69 | :-------: | ------- | :-------: 70 | on-success-save |  编辑保存成功时触发 |  row(当前行号,指当前在表格中的行号), col(列号,序列号列列号为0), value(该单元格修改后的值), initRowIndex(初始行号,即改行数据原本在数据二维数组中的索引,不受排序等影响) 71 | on-fail-save |  编辑保存失败时触发 |  同on-success-save 72 | on-click-tr |  点击行时触发 |  index(当前行号) 73 | on-click-td | 点击单元格时触发 |  {row, col},是个对象 74 | on-moving-on-header |  鼠标在表头移动时触发 |  鼠标事件对象,其中还添加了一些属性:colIndex(当前所在的列的索引号), atRightGivenArea(是否在当前单元格右侧atRightCellPosi指定的距离内), atLeftGivenArea(是否在当前单元格左侧atLeftCellPosi指定的距离内) 75 | on-click-tr |  点击行时触发 |  index(当前行号) 76 | 77 | ### Methods: 78 | 79 | 方法 | 说明 |  参数 80 | :-------: | ------- | :-------: 81 | resize | 涉及到表格容器尺寸变化或数据变化的情况需要调用此方法,如果设changeInitIndex为false,则不会重新为数据设置原始行号 |  changeInitIndex 82 | getScrollLeft | 用于获取当前横向滚动的距离 |  - 83 | scrollToRow | 跳转到指定行号的一行,这里的行号是从0开始的 |  index 84 | editCell | canEdit为true时调用此方法使第row+1行第col+1列变为编辑状态,这里的行列指的是表格显示的行和除序列号列的列 |  row, col 85 | selectCell | canEdit为true时调用此方法使指定单元格被选中 | row, col 86 | setHighlightRow | 手动设置高亮行 | row 87 | filter | 按照某一列的指定关键词进行筛选 | col: 要按哪一列筛选的列号, queryArr: 筛选关键字数组 88 | cancelFilter | 取消筛选 | - 89 | clearCurrentRow | 清除高亮项目 | - 90 | getInitRowIndexByIndex | 获取指定行的初始行号 | row 91 | getIndexByInitRowIndex | 获取指定初始行号的当前行号 | initRow 92 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-bigdata-table 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-bigdata-table", 3 | "description": "Powerful, table components optimized for large amounts of data, based on Vue2.0", 4 | "version": "1.0.0", 5 | "author": "lison", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 10 | "dist": "cross-env NODE_ENV=production webpack --progress --hide-modules", 11 | "lint": "eslint --ext .js,.vue src" 12 | }, 13 | "dependencies": { 14 | "vue": "^2.5.11" 15 | }, 16 | "browserslist": [ 17 | "> 1%", 18 | "last 2 versions", 19 | "not ie <= 8" 20 | ], 21 | "devDependencies": { 22 | "autoprefixer": "^8.3.0", 23 | "babel-core": "^6.22.1", 24 | "babel-eslint": "^7.1.1", 25 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 26 | "babel-jest": "^21.0.2", 27 | "babel-loader": "^7.1.1", 28 | "babel-plugin-dynamic-import-node": "^1.2.0", 29 | "babel-plugin-syntax-jsx": "^6.18.0", 30 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-plugin-transform-vue-jsx": "^3.5.0", 33 | "babel-preset-env": "^1.3.2", 34 | "babel-preset-stage-2": "^6.22.0", 35 | "babel-register": "^6.22.0", 36 | "cross-env": "^5.0.5", 37 | "css-loader": "^0.28.7", 38 | "eslint": "^3.19.0", 39 | "eslint-config-standard": "^10.2.1", 40 | "eslint-friendly-formatter": "^3.0.0", 41 | "eslint-loader": "^1.7.1", 42 | "eslint-plugin-html": "^3.0.0", 43 | "eslint-plugin-import": "^2.7.0", 44 | "eslint-plugin-node": "^5.2.0", 45 | "eslint-plugin-promise": "^3.4.0", 46 | "eslint-plugin-standard": "^3.0.1", 47 | "eslint-plugin-vue": "^4.0.0", 48 | "extract-text-webpack-plugin": "^3.0.2", 49 | "file-loader": "^1.1.4", 50 | "less": "^3.0.1", 51 | "less-loader": "^4.1.0", 52 | "postcss-import": "^11.1.0", 53 | "postcss-url": "^7.3.2", 54 | "vue-loader": "^13.0.5", 55 | "vue-template-compiler": "^2.4.4", 56 | "webpack": "^3.6.0", 57 | "webpack-dev-server": "^2.9.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 50 | 169 | 170 | 248 | -------------------------------------------------------------------------------- /src/index-render.js: -------------------------------------------------------------------------------- 1 | export default (h, { index, params }, table) => { 2 | return h('div', { 3 | 'class': 'index-render-item-wrapper' 4 | }, [ 5 | h('div', { 6 | 'class': [ 7 | 'index-slide-wrapper', 8 | params.index === index ? 'slide-to-left' : '' 9 | ], 10 | on: { 11 | click (event) { 12 | table.$parent.$emit('on-index-change', params.index === index ? -1 : index); 13 | event.stopPropagation(); 14 | } 15 | } 16 | }, [ 17 | h('div', { 18 | 'class': 'index-render-num-con' 19 | }, [ 20 | h('span', index + 1) 21 | ]), 22 | h('div', { 23 | 'class': 'index-render-tip-con' 24 | }, [ 25 | h('span', { 26 | 'class': 'is-paintting-tip' 27 | }, `第${index + 1}行`) 28 | ]) 29 | ]) 30 | ]); 31 | }; 32 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | new Vue({ 5 | el: '#app', 6 | render: h => h(App) 7 | }); 8 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/button.vue: -------------------------------------------------------------------------------- 1 | 7 | 20 | 76 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/input-render.js: -------------------------------------------------------------------------------- 1 | import Input from './input.vue'; 2 | import Button from './button.vue'; 3 | export default (h, {row, col, value, beforeSave, initRowIndex}, table) => { 4 | return h('div', { 5 | 'class': 'edit-item-con' 6 | }, [ 7 | h(Input, { 8 | 'class': 'edit-item-input', 9 | props: { 10 | value: value 11 | }, 12 | on: { 13 | input (res) { 14 | table.editContent = res; 15 | } 16 | } 17 | }), 18 | h('div', { 19 | 'class': 'edit-item-btn-con' 20 | }, [ 21 | h(Button, { 22 | 'class': 'edit-btn', 23 | props: { 24 | type: 'confirm' 25 | }, 26 | on: { 27 | click () { 28 | if (beforeSave({ row, col, value, initRowIndex })) { 29 | table.$emit('on-success-save', { 30 | row: row, 31 | col: col, 32 | value: table.editContent, 33 | initRowIndex: initRowIndex 34 | }); 35 | } else { 36 | table.$emit('on-fail-save', { 37 | row: row, 38 | col: col, 39 | value: table.editContent, 40 | initRowIndex: initRowIndex 41 | }); 42 | } 43 | } 44 | } 45 | }), 46 | h(Button, { 47 | 'class': 'edit-btn', 48 | props: { 49 | type: 'cancel' 50 | }, 51 | on: { 52 | click () { 53 | table.$emit('on-cancel-edit'); 54 | } 55 | } 56 | }) 57 | ]) 58 | ]); 59 | }; 60 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/input.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | 46 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/item-table.vue: -------------------------------------------------------------------------------- 1 | 92 | 213 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/renderDom.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'RenderCell', 3 | functional: true, 4 | props: { 5 | render: Function, 6 | backValue: [Number, Object] 7 | }, 8 | render: (h, ctx) => { 9 | return ctx.props.render(h, ctx.props.backValue, ctx.parent); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/components/sort-button.vue: -------------------------------------------------------------------------------- 1 | 7 | 44 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/index.js: -------------------------------------------------------------------------------- 1 | import bigdataTable from './vue-bigdata-table.vue'; 2 | export default bigdataTable; 3 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/data-handle.js: -------------------------------------------------------------------------------- 1 | import ItemTable from '../components/item-table.vue'; 2 | import { iteratorByTimes, getHeaderWords } from '../util'; 3 | export default { 4 | data () { 5 | return { 6 | times0: 0, // 当前是第几轮 7 | times1: 0, 8 | times2: -1, 9 | table1Data: [], 10 | table2Data: [], 11 | table3Data: [], 12 | currentIndex: 0, // 当前展示的表格是第几个 13 | itemNum: 0, // 一块数据显示的数据条数 14 | timer: null, 15 | scrollLeft: 0, 16 | insideTableData: [], 17 | initTableData: [] // 初始表格数据,用于恢复搜索和筛选, 18 | }; 19 | }, 20 | computed: { 21 | cellNum () { // 表格列数 22 | return this.columnsHandled.length; 23 | }, 24 | columnsHandled () { 25 | let columns = [...this.columns]; 26 | if (this.colNum > this.columns.length) { 27 | let colLength = this.colNum - this.columns.length; 28 | let headerWordsArr = getHeaderWords(colLength); 29 | iteratorByTimes(colLength, (i) => { 30 | columns.push({ 31 | title: headerWordsArr[i] 32 | }); 33 | }); 34 | } 35 | if (this.showIndex) { 36 | columns.unshift({ 37 | title: 'No', 38 | align: 'center', 39 | width: this.indexWidthInside 40 | }); 41 | } 42 | return columns; 43 | } 44 | }, 45 | methods: { 46 | getComputedTableDataIndex (colIndex) { 47 | return this.showIndex ? (colIndex - 1) : colIndex; 48 | }, 49 | handleScroll (e) { 50 | let ele = e.srcElement || e.target; 51 | let { scrollTop, scrollLeft } = ele; 52 | this.scrollLeft = scrollLeft; 53 | // let direction = (scrollTop - this.scrollTop) > 0 ? 1 : ((scrollTop - this.scrollTop) < 0 ? -1 : 0); // 1 => down -1 => up 0 => stop 54 | this.currentIndex = parseInt((scrollTop % (this.moduleHeight * 3)) / this.moduleHeight); 55 | this.scrollTop = scrollTop; 56 | this.$nextTick(() => { 57 | this.setTopPlace(); 58 | }); 59 | }, 60 | setTableData () { 61 | let count1 = this.times0 * this.itemNum * 3; 62 | this.table1Data = this.insideTableData.slice(count1, count1 + this.itemNum); 63 | let count2 = this.times1 * this.itemNum * 3; 64 | this.table2Data = this.insideTableData.slice(count2 + this.itemNum, count2 + this.itemNum * 2); 65 | let count3 = this.times2 * this.itemNum * 3; 66 | this.table3Data = this.insideTableData.slice(count3 + this.itemNum * 2, count3 + this.itemNum * 3); 67 | }, 68 | getTables (h) { 69 | let table1 = this.getItemTable(h, this.table1Data, 1); 70 | let table2 = this.getItemTable(h, this.table2Data, 2); 71 | let table3 = this.getItemTable(h, this.table3Data, 3); 72 | if (this.currentIndex === 0) { 73 | return [table1, table2, table3]; 74 | } else if (this.currentIndex === 1) { 75 | return [table2, table3, table1]; 76 | } else { 77 | return [table3, table1, table2]; 78 | } 79 | }, 80 | renderTable (h) { 81 | return h('div', { 82 | style: this.tableWidthStyles 83 | }, this.getTables(h)); 84 | }, 85 | getItemTable (h, data, index) { 86 | return h(ItemTable, { 87 | props: { 88 | times: this['times' + (index - 1)], 89 | tableIndex: index, 90 | itemData: data, 91 | itemNum: this.itemNum, 92 | rowStyles: this.rowStyles, 93 | widthArr: this.colWidthArr, 94 | columns: this.columnsHandled, 95 | showIndex: this.showIndex, 96 | indexRender: this.indexRender, 97 | stripe: this.stripe, 98 | fixedCol: this.fixedCol, 99 | currentScrollToRowIndex: this.currentScrollToRowIndex, 100 | canEdit: this.canEdit, 101 | edittingTd: this.edittingTd, 102 | startEditType: this.startEditType, 103 | showFixedBoxShadow: this.showFixedBoxShadow, 104 | editCellRender: this.editCellRender, 105 | beforeSave: this.beforeSave, 106 | canSelectText: this.canSelectText, 107 | startSelect: this.startSelect, 108 | endSelect: this.endSelect, 109 | disabledHover: this.disabledHover, 110 | highlightRow: this.highlightRow, 111 | highlightRowIndex: this.highlightRowIndex, 112 | indexRenderParams: this.indexRenderParams 113 | }, 114 | on: { 115 | 'on-click-tr': (index, initRowIndex) => { 116 | if (this.highlightRow) this.highlightRowIndex = index; 117 | this.$emit('on-click-tr', index, initRowIndex); 118 | }, 119 | 'on-click-td': (params) => { 120 | this.$emit('on-click-td', params); 121 | }, 122 | 'on-edit-cell': (row, col) => { 123 | // this.edittingTd = `${row}-${col}`; 124 | this._editCell(row, col, false) 125 | }, 126 | 'on-success-save': ({ row, col, value, initRowIndex, oldValue }) => { 127 | let data = [...this.value]; 128 | data[initRowIndex][col] = value; 129 | this.$emit('input', data); 130 | this.$emit('on-success-save', { row, col, value, initRowIndex, oldValue }); 131 | this.edittingTd = ''; 132 | }, 133 | 'on-fail-save': ({ row, col, value, initRowIndex }) => { 134 | this.$emit('on-fail-save', { row, col, value, initRowIndex }); 135 | }, 136 | 'on-cancel-edit': () => { 137 | this.edittingTd = ''; 138 | }, 139 | 'on-paste': (data) => { 140 | if (!this.paste) return; 141 | let value = [...this.value]; 142 | let rowLength = data.length; 143 | let startSelect = this.startSelect; 144 | let endSelect = this.endSelect; 145 | let startRow = startSelect.row; 146 | let startCol = startSelect.col; 147 | let endRow = endSelect.row; 148 | let endCol = endSelect.col; 149 | let selectRow = endRow - startRow + 1; 150 | let selectCol = endCol - startCol + 1; 151 | // let lastColLength = value[0].length - startCol; 152 | // let lastRowLength = value.length - startRow; 153 | if (rowLength === 0) return; 154 | let colLength = data[0].length; 155 | if (colLength === 0) return; 156 | // 使用复制的数据替换原数据 157 | for (let r = 0; r < rowLength && r < selectRow; r++) { 158 | for (let c = 0; c < colLength && c < selectCol; c++) { 159 | let valueRow = startRow + r; 160 | let valueCol = startCol + c; 161 | if (typeof value[valueRow][valueCol] === 'object') { 162 | value[valueRow][valueCol].value = data[r][c]; 163 | } else { 164 | value[valueRow][valueCol] = data[r][c]; 165 | } 166 | } 167 | } 168 | // for (let r = startRow; r < selectRow; r++) { 169 | // for (let c = startCol; c < selectCol; c++) { 170 | // // 171 | // } 172 | // } 173 | this.$emit('input', value); 174 | this.$emit('on-paste', data); 175 | this._tableResize(); 176 | } 177 | }, 178 | key: 'table-item-key' + index, 179 | ref: 'itemTable' + index, 180 | attrs: { 181 | 'data-index': index 182 | } 183 | }); 184 | }, 185 | _scrollToIndexRow (index) { 186 | index = parseInt(index); 187 | if (isNaN(index) || index >= this.insideTableData.length || index < 0) return; 188 | let scrollTop = index * this.itemRowHeight; 189 | this.$refs.outer.scrollTop = scrollTop; 190 | this.currentScrollToRowIndex = index; 191 | clearTimeout(this.timer); 192 | this.timer = setTimeout(() => { 193 | this.currentScrollToRowIndex = -1; 194 | }, 1800); 195 | }, 196 | // 给表格数据添加行号,用于排序后正确修改数据 197 | setInitIndex (tableData) { 198 | return tableData.map((item, i) => { 199 | let row = item; 200 | row.initRowIndex = i; 201 | return row; 202 | }); 203 | }, 204 | // 获取指定行的初始行号 205 | _getInitRowIndexByIndex (index) { 206 | return this.insideTableData[index].initRowIndex; 207 | }, 208 | _getIndexByInitRowIndex (index) { 209 | let i = -1; 210 | let len = this.insideTableData.length; 211 | while(++i < len) { 212 | let row = this.insideTableData[i]; 213 | if (row.initRowIndex === index) return i; 214 | } 215 | } 216 | } 217 | }; 218 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/edit.js: -------------------------------------------------------------------------------- 1 | import { findNodeUpper } from '../util'; 2 | export default { 3 | data () { 4 | return { 5 | edittingTd: '', // 正在编辑的单元格的行号和列号拼接的字符串 `${row}-${col}` 6 | editContent: '', // 用来保存编辑的内容 7 | selectCellsStart: {}, // 编辑模式下可选中多行多列,此用来保存其实单元格行列号 8 | selectCellsEnd: {}, 9 | selectTotalColStartIndex: -1, // 选取整列起始序列号 10 | selectTotalColEndIndex: -1 11 | }; 12 | }, 13 | computed: { 14 | startSelect () { 15 | return { 16 | row: Math.min(this.selectCellsStart.row, this.selectCellsEnd.row), 17 | col: Math.min(this.selectCellsStart.col, this.selectCellsEnd.col) 18 | }; 19 | }, 20 | endSelect () { 21 | return { 22 | row: Math.max(this.selectCellsStart.row, this.selectCellsEnd.row), 23 | col: Math.max(this.selectCellsStart.col, this.selectCellsEnd.col) 24 | }; 25 | } 26 | }, 27 | watch: { 28 | selectable () { 29 | this.selectCellsStart = { 30 | row: -1, 31 | col: -1 32 | }; 33 | this.selectCellsEnd = { 34 | row: -1, 35 | col: -1 36 | }; 37 | } 38 | }, 39 | methods: { 40 | _editCell (row, col, scrollToView = true) { 41 | if (!this.canEdit || row < 0 || row > this.insideTableData.length || col < 0 || col > this.columns.length || this.edittingTd === `${row}-${col}`) return; 42 | if (scrollToView && parseInt(this.edittingTd.split('-')[0]) !== row) this.scrollToRow(row); 43 | this.edittingTd = `${row}-${col}`; 44 | }, 45 | handleMousedownOnTable (e) { 46 | if (e.button !== 0 || (!this.paste && !this.selectable)) return; 47 | let currentTd = e.target.tagName === 'TD' ? e.target : findNodeUpper(e.target, 'td'); 48 | this.selectCellsStart = { 49 | row: currentTd.getAttribute('data-row'), 50 | col: currentTd.getAttribute('data-col') 51 | }; 52 | this.selectCellsEnd = { 53 | row: currentTd.getAttribute('data-row'), 54 | col: currentTd.getAttribute('data-col') 55 | }; 56 | this.canSelectText = false; 57 | document.addEventListener('mousemove', this.handleMoveOnTable); 58 | document.addEventListener('mouseup', this.handleUpOnTable); 59 | }, 60 | handleMoveOnTable (e) { 61 | if (!(e.target.tagName === 'TD' || findNodeUpper(e.target, 'td'))) return; 62 | let currentTd = e.target.tagName === 'TD' ? e.target : findNodeUpper(e.target, 'td'); 63 | this.selectCellsEnd = { 64 | row: currentTd.getAttribute('data-row'), 65 | col: currentTd.getAttribute('data-col') 66 | }; 67 | }, 68 | handleUpOnTable (e) { 69 | if (!this.paste && !this.selectable) return; 70 | this.canSelectText = true; 71 | this.handleMoveOnTable(e); 72 | document.removeEventListener('mousemove', this.handleMoveOnTable); 73 | document.removeEventListener('mouseup', this.handleUpOnTable); 74 | this.$emit('on-select-cells', { 75 | start: { 76 | row: this.startSelect.row, 77 | col: this.startSelect.col 78 | }, 79 | end: { 80 | row: this.endSelect.row, 81 | col: this.endSelect.col 82 | } 83 | }); 84 | }, 85 | _selectCell (row, col) { 86 | this.selectCellsStart = { row, col }; 87 | this.selectCellsEnd = { row, col }; 88 | } 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/empty-table.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | _createEmptyData () { 4 | // this.$nextTick(() => { 5 | let rowNum = this.rowNum; 6 | let colNum = this.colNum; 7 | if (this.rowNum && this.colNum) { 8 | console.log(this.value.length, this.rowNum, this.colNum); 9 | let valueRowNum = this.value.length; 10 | let valueColNum = this.value[0] ? this.value[0].length : 0; 11 | let totalRowNum = valueRowNum + rowNum; 12 | let totalColNum = valueColNum + colNum; 13 | let value = [...this.value]; 14 | console.log(totalRowNum, valueRowNum); 15 | for (let r = valueRowNum; r < totalRowNum; r++) { 16 | value.push([]); 17 | for (let c = valueColNum; c < totalColNum; c++) { 18 | value[r].push(''); 19 | } 20 | } 21 | // this. 22 | console.log(value); 23 | this.$emit('input', value); 24 | // this.$nextTick(() => { 25 | // this._tableResize(); 26 | // }) 27 | } 28 | // }); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/filter.js: -------------------------------------------------------------------------------- 1 | import { hasOneOf } from '../util'; 2 | export default { 3 | methods: { 4 | _filter (col, queryArr) { 5 | let value = [...this.value]; 6 | this.insideTableData = value.filter(item => hasOneOf(item[col], queryArr)); 7 | this._tableResize(); 8 | }, 9 | _cancelFilter () { 10 | this.insideTableData = [...this.value]; 11 | this._tableResize(); 12 | } 13 | } 14 | }; -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/header-move.js: -------------------------------------------------------------------------------- 1 | import { findNodeUpper } from '../util'; 2 | export default { 3 | data () { 4 | return { 5 | isOnCellEdge: false, // 鼠标是否在表头的两个单元格之间的边框上 6 | canResizeCell: false, 7 | initCellX: 0, // 用于计算鼠标移动的距离 8 | scrollLeft: 0, 9 | colIndex: 0, // 在表头上移动时鼠标所在列的序号, 10 | atLeftGivenArea: false, // 是否在表头单元格指定区域(距左侧) 11 | atRightGivenArea: false // 是否在表头单元格指定区域(距右侧) 12 | }; 13 | }, 14 | methods: { 15 | handleMousemove (e) { 16 | const target = e.srcElement || e.target; 17 | let cell = target.tagName.toUpperCase() === 'TH' ? target : findNodeUpper(target, 'th'); 18 | let cellDomRect = cell.getBoundingClientRect(); 19 | let atLeft = (e.pageX - cellDomRect.left) < (cellDomRect.width / 2); 20 | let atLeftGivenArea = (cellDomRect.left + this.atLeftCellPosi) >= e.pageX; 21 | let atRightGivenArea = (cellDomRect.right - e.pageX) <= this.atRightCellPosi; 22 | let cellIndex = parseInt(cell.getAttribute('data-index')); // 当前单元格的序号 23 | if (atLeft && cellIndex !== 0) { 24 | this.isOnCellEdge = (e.pageX - cellDomRect.left) <= 1 && cellIndex - 1 !== this.fixedCol; 25 | } else if (!atLeft && cellIndex !== this.cellNum - 1) { 26 | this.isOnCellEdge = (cellDomRect.right - e.pageX) <= 1 && cellIndex !== this.fixedCol; 27 | } 28 | e.atRightGivenArea = atRightGivenArea; 29 | e.atLeftGivenArea = atLeftGivenArea; 30 | this.atRightGivenArea = atRightGivenArea; 31 | this.atLeftGivenArea = atLeftGivenArea; 32 | let index = 0; // 调整表格列宽的左侧的表格的序列 33 | e.colIndex = cellIndex; 34 | this.colIndex = cellIndex; 35 | this.$emit('on-moving-on-header', e); 36 | if (this.canResizeCell) { 37 | if (atLeft) { 38 | index = cellIndex - 1; 39 | } else { 40 | index = cellIndex; 41 | } 42 | if (index === this.fixedCol) return; 43 | let widthLeft = this.widthArr[index] + e.pageX - this.initCellX; 44 | let widthRight = this.widthArr[index + 1] + this.initCellX - e.pageX; 45 | this.widthArr.splice(index, 2, widthLeft, widthRight); 46 | this.initCellX = e.pageX; 47 | } 48 | }, 49 | handleMousedown (e) { 50 | e.colIndex = this.cellIndex; 51 | this.$emit('on-mousedown-on-header', e); 52 | if (this.isOnCellEdge) { 53 | this.canResizeCell = true; 54 | this.initCellX = e.pageX; 55 | } 56 | }, 57 | canNotMove (e) { 58 | this.canResizeCell = false; 59 | e.colIndex = this.colIndex; 60 | e.atLeftGivenArea = this.atLeftGivenArea; 61 | e.atRightGivenArea = this.atRightGivenArea; 62 | this.$emit('on-mouseup-on-header', e); 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/index.js: -------------------------------------------------------------------------------- 1 | import headerMove from './header-move'; 2 | import styleComputed from './style-compute'; 3 | import dataHandle from './data-handle'; 4 | import edit from './edit'; 5 | import emptyTable from './empty-table'; 6 | import sort from './sort'; 7 | import filter from './filter'; 8 | 9 | export default [ headerMove, styleComputed, dataHandle, edit, emptyTable, sort, filter ]; 10 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/sort.js: -------------------------------------------------------------------------------- 1 | import { sortArr, sortDesArr } from '../util'; 2 | export default { 3 | data () { 4 | return { 5 | sortedByColIndex: -1, 6 | sortedType: '' 7 | }; 8 | }, 9 | methods: { 10 | showSortBtn (colIndex) { 11 | const sortable = this.sortable ? true : this.sortIndex !== undefined; 12 | return (sortable && !(this.showIndex && colIndex === 0) && (typeof this.sortIndex === 'number' ? colIndex <= this.sortIndex : this.sortIndex.indexOf(colIndex) >= 0)) || this.columnsHandled[colIndex].sortable; 13 | }, 14 | handleSort (colIndex, sortType) { 15 | this.sortedByColIndex = colIndex; 16 | this.sortedType = sortType; 17 | let valueArr = [...this.value]; 18 | colIndex = this.showIndex ? colIndex - 1 : colIndex; 19 | if (sortType === 'up') { 20 | sortArr(valueArr, colIndex); 21 | } else { 22 | sortDesArr(valueArr, colIndex); 23 | } 24 | this.insideTableData = [...valueArr]; 25 | }, 26 | handleCancelSort () { 27 | this.sortedByColIndex = -1; 28 | this.sortedType = ''; 29 | this.insideTableData = [...this.value]; 30 | }, 31 | initSort () { 32 | if (this.defaultSort) { 33 | const colIndex = parseInt(Object.keys(this.defaultSort)[0]); 34 | if (!(colIndex || colIndex === 0)) return; 35 | const sortType = this.defaultSort[colIndex]; 36 | this.handleSort(colIndex, sortType); 37 | } 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/mixins/style-compute.js: -------------------------------------------------------------------------------- 1 | import { getScrollbarWidth } from '../util'; 2 | export default { 3 | data () { 4 | return { 5 | wrapperHeight: 0, 6 | scrollTop: 0, 7 | moduleHeight: 0, // 三个tr块中的一块的高度 8 | topPlaceholderHeight: 0, // 顶部占位容器高度 9 | tableWidth: 0, 10 | widthArr: [], // 用于给数据表格传递列宽 11 | totalRowHeight: 0, // 如果全量渲染应该是多高,用于计算占位 12 | currentScrollToRowIndex: -1, // 当前跳转到的行号,用于做闪烁提示 13 | canSelectText: true, // 用于控制是否可选中表格文字 14 | indexWidthInside: 0, 15 | outerWidth: 0, // 外面容器宽度 16 | oldTableWidth: 0, // 旧的表格宽度,用于重新计算列宽 17 | highlightRowIndex: -1, // 高亮行号 18 | updateID: 0 19 | }; 20 | }, 21 | computed: { 22 | fixedColCom () { 23 | return this.showIndex ? (this.fixedCol + 1) : this.fixedCol; 24 | }, 25 | wrapperClasses () { 26 | return [ 27 | this.prefix, 28 | this.fixed ? `${this.prefix}-fixed` : '' 29 | ]; 30 | }, 31 | headerStyle () { 32 | return { 33 | height: this.headerHeight + 'px', 34 | transform: 'translateX(0)' 35 | }; 36 | }, 37 | showFixedBoxShadow () { 38 | return this.scrollLeft !== 0; 39 | }, 40 | tableWidthStyles () { 41 | return {width: this.tableWidth + 'px'}; 42 | }, 43 | rowStyles () { 44 | return this.rowHeight !== undefined ? {height: `${this.rowHeight}px`} : {}; 45 | }, 46 | placeholderHeight () { 47 | return this.totalRowHeight - this.moduleHeight * 3; // 占位容器的总高度(上 + 下) 48 | }, 49 | bottomPlaceholderHeight () { 50 | return (this.placeholderHeight - this.topPlaceholderHeight) < 0 ? 0 : this.placeholderHeight - this.topPlaceholderHeight; 51 | }, 52 | itemRowHeight () { 53 | return this.rowHeight === undefined ? 48 : this.rowHeight; 54 | }, 55 | colWidthArr () { 56 | let len = this.cellNum; 57 | let colWidthArr = []; 58 | if (this.fixedWrapperWidth) { 59 | let width = this.outerWidth; 60 | let num = this.cellNum; 61 | if (this.showIndex) { 62 | colWidthArr.push(this.indexWidth); 63 | width -= this.indexWidth; 64 | num -= 1; 65 | } 66 | let i = -1; 67 | let itemColWidth = width / num; 68 | while (++i < num) { 69 | colWidthArr.push(itemColWidth); 70 | } 71 | } else { 72 | let i = 0; 73 | let hasWidthCellCount = 0; // 统计设置了width的列的数量,从而为没有设置width的列分配宽度 74 | let noWidthCellIndexArr = []; // 没有设置宽度的列的序列 75 | let hasWidthCellTotalWidth = 0; // 设置了width的列一共多宽 76 | while (i < len) { 77 | if (this.columnsHandled[i].width) { 78 | hasWidthCellCount++; 79 | hasWidthCellTotalWidth += this.columnsHandled[i].width; 80 | colWidthArr.push(this.columnsHandled[i].width); 81 | } else { 82 | noWidthCellIndexArr.push(i); 83 | colWidthArr.push(0); 84 | } 85 | i++; 86 | } 87 | let noWidthCellWidth = (this.tableWidth - hasWidthCellTotalWidth) / (len - hasWidthCellCount); 88 | let w = 0; 89 | let indexArrLen = noWidthCellIndexArr.length; 90 | while (w < indexArrLen) { 91 | colWidthArr[noWidthCellIndexArr[w]] = noWidthCellWidth; 92 | w++; 93 | } 94 | // this.widthArr = colWidthArr; 95 | } 96 | return colWidthArr; 97 | }, 98 | cursorOnHeader () { 99 | return this.headerTrStyle.cursor ? this.headerTrStyle.cursor : ((this.isOnCellEdge || this.canResizeCell) ? 'col-resize' : 'default'); 100 | } 101 | }, 102 | watch: { 103 | highlightRow () { 104 | this._clearCurrentRow(); 105 | } 106 | }, 107 | methods: { 108 | _tableResize () { 109 | this.$nextTick(() => { 110 | this.updateHeight(); 111 | this.setComputedProps(); 112 | let scrollBarWidth = this.totalRowHeight > this.wrapperHeight ? getScrollbarWidth() : 0; 113 | this.outerWidth = this.$refs.outer.offsetWidth - 2 - scrollBarWidth; 114 | let width = this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0); 115 | // this.tableWidth = width > this.outerWidth ? width : this.outerWidth; 116 | this.tableWidth = this.fixedWrapperWidth ? this.outerWidth : (width > this.outerWidth ? width : this.outerWidth); 117 | if (width < this.outerWidth) this._setColWidthArr(); 118 | this.widthArr = this.colWidthArr; 119 | }); 120 | }, 121 | updateHeight () { 122 | this.$nextTick(() => { 123 | let wrapperHeight = this.$refs.outer.offsetHeight; 124 | this.itemNum = Math.ceil((wrapperHeight - this.headerHeight) / this.itemRowHeight) + this.appendNum; 125 | this.moduleHeight = this.itemNum * this.itemRowHeight; 126 | this.wrapperHeight = wrapperHeight; 127 | this.setTopPlace(); 128 | }); 129 | }, 130 | setComputedProps () { 131 | const len = this.insideTableData.length; 132 | this.totalRowHeight = len * this.itemRowHeight; 133 | }, 134 | setIndexWidth (len) { 135 | let width = 70; 136 | if (len <= 99) { 137 | width = 50; 138 | } else if (len > 99 && len <= 1000) { 139 | width = 60; 140 | } else if (len > 1000 && len <= 10000) { 141 | width = 70; 142 | } else if (len > 10000 && len <= 100000) { 143 | width = 90; 144 | } else { 145 | width = 100; 146 | } 147 | return width; 148 | }, 149 | setTopPlace () { 150 | let scrollTop = this.scrollTop; 151 | let t0 = 0; 152 | let t1 = 0; 153 | let t2 = 0; 154 | if (scrollTop > this.moduleHeight) { 155 | switch (this.currentIndex) { 156 | case 0: t0 = parseInt(scrollTop / (this.moduleHeight * 3)); t1 = t2 = t0; break; 157 | case 1: t1 = parseInt((scrollTop - this.moduleHeight) / (this.moduleHeight * 3)); t0 = t1 + 1; t2 = t1; break; 158 | case 2: t2 = parseInt((scrollTop - this.moduleHeight * 2) / (this.moduleHeight * 3)); t0 = t1 = t2 + 1; 159 | } 160 | } 161 | this.times0 = t0; 162 | this.times1 = t1; 163 | this.times2 = t2; 164 | this.topPlaceholderHeight = parseInt(scrollTop / this.moduleHeight) * this.moduleHeight; 165 | this.setTableData(); 166 | }, 167 | _initMountedHandle () { 168 | if (this.indexWidth === undefined) this.indexWidthInside = this.setIndexWidth(this.insideTableData.length); 169 | else this.indexWidthInside = this.indexWidth; 170 | this.oldTableWidth = this.colWidthArr.reduce((sum, b) => { 171 | return sum + b; 172 | }, 0); 173 | this.widthArr = this.colWidthArr; 174 | if ((this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0)) < this.outerWidth) this._setColWidthArr(); 175 | }, 176 | _setColWidthArr () { 177 | let widthArr = this.widthArr.map(width => { 178 | return width / this.oldTableWidth * this.tableWidth; 179 | }); 180 | this.oldTableWidth = this.tableWidth; 181 | this.widthArr = widthArr; 182 | }, 183 | _clearCurrentRow () { 184 | this.highlightRowIndex = -1; 185 | }, 186 | refreshHeader () { 187 | this.updateID++; 188 | }, 189 | _setHighlightRow (row) { 190 | this.highlightRowIndex = row; 191 | } 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /src/vue-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 | export const sortArr = (arr, index) => { 79 | const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); 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 | const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); 103 | if (arr.length <= 1) return; 104 | const firstNotNullValue = getFirstNotNullValue(arr, index); 105 | if (!firstNotNullValue && firstNotNullValue !== 0) return; 106 | if (!isChineseReg.test(firstNotNullValue)) { 107 | if (isNaN(Number(firstNotNullValue))) { 108 | // 非中文非数值 109 | arr.sort().reverse(); 110 | } else { 111 | // 数值型 112 | arr.sort((a, b) => { 113 | return b[index] - a[index]; 114 | }); 115 | } 116 | } else { 117 | arr.sort((a, b) => { 118 | return b[index].localeCompare(a[index], 'zh'); 119 | }); 120 | } 121 | }; 122 | 123 | export const hasOneOf = (str, targetArr) => { 124 | let len = targetArr.length; 125 | let i = -1; 126 | while (++i < len) { 127 | if (str.indexOf(targetArr[i]) >= 0) { 128 | return true; 129 | } 130 | } 131 | return false; 132 | }; 133 | 134 | export const oneOf = (ele, targetArr) => { 135 | return targetArr.indexOf(ele) > -1 136 | } -------------------------------------------------------------------------------- /src/vue-bigdata-table/vue-bigdata-table.less: -------------------------------------------------------------------------------- 1 | @prefix: ~"vue-bigdata-table"; 2 | @select-border-color: #3695FE; 3 | 4 | @keyframes scroll-tip{ 5 | 0% { 6 | background: #fff; 7 | } 8 | 50% { 9 | background: #d0e8ff; 10 | } 11 | } 12 | 13 | .@{prefix}{ 14 | width: 100%; 15 | box-sizing: border-box; 16 | *{ 17 | font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; 18 | color: #495060; 19 | font-size: 12px; 20 | font-weight: 400; 21 | } 22 | &-outer{ 23 | width: 100%; 24 | height: 100%; 25 | overflow: auto; 26 | border: 1px solid #e9eaec; 27 | box-sizing: border-box; 28 | position: relative; 29 | .@{prefix}-header-wrapper{ 30 | box-sizing: border-box; 31 | z-index: 70; 32 | &.header-wrapper-fixed{ 33 | position: -webkit-sticky; 34 | position: sticky; 35 | } 36 | table{ 37 | table-layout: fixed; 38 | height: 100%; 39 | tr th{ 40 | border-right: 1px solid #e9eaec; 41 | border-bottom: 1px solid #e9eaec; 42 | background: #fff; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | white-space: nowrap; 46 | word-break: break-all; 47 | // &:last-child{ 48 | // border-right: none; 49 | // } 50 | .@{prefix}-header-inside-wrapper{ 51 | box-sizing: border-box; 52 | padding: 0 8px; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | &-fixed-header{ 60 | position: -webkit-sticky; 61 | position: sticky; 62 | transform: translateX(0); 63 | left: 0; 64 | z-index: 110; 65 | transition: box-shadow .2s ease; 66 | &.box-shadow{ 67 | box-shadow: 2px 0 6px -2px rgba(0,0,0,.2); 68 | transition: box-shadow .2s ease; 69 | } 70 | } 71 | 72 | &-wrapper{ 73 | width: 100%; 74 | border-bottom: none; 75 | 76 | .@{prefix}-content{ 77 | width: 100%; 78 | height: auto; 79 | // &.noselect-text{ 80 | // -webkit-touch-callout: none; 81 | // -webkit-user-select: none; 82 | // -khtml-user-select: none; 83 | // -moz-user-select: none; 84 | // -ms-user-select: none; 85 | // user-select: none; 86 | // } 87 | } 88 | 89 | &:nth-child(2){ 90 | border-top: 1px solid #e9eaec; 91 | } 92 | &:nth-child(4){ 93 | border-bottom: 1px solid #e9eaec; 94 | } 95 | .@{prefix}-data-table{ 96 | &.@{prefix}-content-table{ 97 | 98 | left: 0; 99 | top: 0; 100 | } 101 | border-bottom: none; 102 | border-top: none; 103 | table-layout: fixed; 104 | tr{ 105 | background: #fff; 106 | &.scroll-to-row-tip{ 107 | animation: scroll-tip .6s 3; 108 | } 109 | td{ 110 | min-width: 0; 111 | height: 48px; 112 | box-sizing: border-box; 113 | overflow: hidden; 114 | text-overflow: ellipsis; 115 | vertical-align: middle; 116 | border-bottom: 1px solid #e9eaec; 117 | border-right: 1px solid #e9eaec; 118 | 119 | border-left: 1px solid transparent; 120 | border-top: 1px solid transparent; // 表格选中 121 | .@{prefix}-cell{ 122 | box-sizing: border-box; 123 | padding: 0 18px; 124 | overflow: hidden; 125 | text-overflow: ellipsis; 126 | white-space: nowrap; 127 | word-break: break-all; 128 | } 129 | // &:last-child{ 130 | // border-right: none; 131 | // } 132 | .edit-item-con{ 133 | width: 100%; 134 | text-align: left; 135 | padding: 0 6px; 136 | box-sizing: border-box; 137 | .edit-item{ 138 | &-input{ 139 | width: ~"calc(100% - 50px)"; 140 | float: left; 141 | } 142 | &-btn-con{ 143 | float: left; 144 | .edit-btn{ 145 | width: 20px; 146 | margin: 7px 4px 0 0; 147 | } 148 | } 149 | } 150 | } 151 | &.start-select-cell{ 152 | border-left: 1px solid @select-border-color; 153 | border-top: 1px solid @select-border-color; 154 | } 155 | &.end-select-cell{ 156 | border-right: 1px solid @select-border-color; 157 | border-bottom: 1px solid @select-border-color; 158 | } 159 | &.right-top-select-cell{ 160 | border-right: 1px solid @select-border-color; 161 | border-top: 1px solid @select-border-color; 162 | } 163 | &.left-bottom-select-cell{ 164 | border-left: 1px solid @select-border-color; 165 | border-bottom: 1px solid @select-border-color; 166 | } 167 | &.top-center-select-cell{ 168 | border-top: 1px solid @select-border-color; 169 | } 170 | &.bottom-center-select-cell{ 171 | border-bottom: 1px solid @select-border-color; 172 | } 173 | &.left-center-select-cell{ 174 | border-left: 1px solid @select-border-color; 175 | } 176 | &.right-center-select-cell{ 177 | border-right: 1px solid @select-border-color; 178 | } 179 | } 180 | &.stripe-gray{ 181 | background: #f8f8f9; 182 | } 183 | &.highlight-row{ 184 | background: #ebf7ff; 185 | } 186 | } 187 | &-left{ 188 | text-align: left; 189 | } 190 | &-center{ 191 | text-align: center; 192 | } 193 | &-right{ 194 | text-align: right; 195 | } 196 | } 197 | } 198 | 199 | &-fixed{ 200 | .@{prefix}-header-wrapper{ 201 | top: 0; 202 | left: 0; 203 | box-sizing: border-box; 204 | } 205 | } 206 | 207 | &-fixed-table{ 208 | position: -webkit-sticky; 209 | position: sticky; 210 | left: 0; 211 | z-index: 60; 212 | transition: box-shadow .2s ease; 213 | &.box-shadow{ 214 | box-shadow: 2px 0 6px -2px rgba(0,0,0,.2); 215 | transition: box-shadow .2s ease; 216 | } 217 | td{ 218 | border-right: 1px solid #e9eaec; 219 | // &:last-child{ 220 | // border-right: 1px solid #e9eaec !important; 221 | // } 222 | } 223 | } 224 | 225 | &-item-table{ 226 | position: relative; 227 | } 228 | 229 | .sort-button{ 230 | &-wrapper{ 231 | display: inline-block; 232 | position: relative; 233 | width: 10px; 234 | height: 11px; 235 | transform: translateY(1px); 236 | } 237 | &-item{ 238 | position: absolute; 239 | display: inline-block; 240 | width: 0; 241 | height: 0; 242 | border: 4px solid transparent; 243 | margin: 0; 244 | padding: 0; 245 | cursor: pointer; 246 | transition: border-color .2s ease; 247 | &-up{ 248 | top: -4px; 249 | border-bottom: 4px solid #bbbec4; 250 | &:hover{ 251 | border-bottom: 4px solid #495060; 252 | } 253 | &-active{ 254 | border-bottom: 4px solid #2d8cf0; 255 | } 256 | } 257 | &-down{ 258 | bottom: -4px; 259 | border-top: 4px solid #bbbec4; 260 | &:hover{ 261 | border-top: 4px solid #495060; 262 | } 263 | &-active{ 264 | border-top: 4px solid #2d8cf0; 265 | } 266 | } 267 | } 268 | } 269 | } 270 | 271 | -------------------------------------------------------------------------------- /src/vue-bigdata-table/vue-bigdata-table.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 55 | 376 | -------------------------------------------------------------------------------- /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: process.env.NODE_ENV === 'production' ? './src/vue-bigdata-table/index.js' : './src/main.js', 7 | output: { 8 | path: path.resolve(__dirname, './dist'), 9 | publicPath: '/dist/', 10 | filename: process.env.NODE_ENV === 'production' ? 'vue-bigdata-table.min.js' : 'build.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | use: [ 17 | 'vue-style-loader', 18 | 'css-loader' 19 | ], 20 | }, 21 | { 22 | test: /\.less$/, 23 | use: ExtractTextPlugin.extract({ 24 | use: ['css-loader?minimize', 'autoprefixer-loader', 'less-loader'], 25 | fallback: 'style-loader' 26 | }) 27 | }, 28 | { 29 | test: /\.vue$/, 30 | loader: 'vue-loader', 31 | options: { 32 | loaders: { 33 | css: 'vue-style-loader!css-loader', 34 | less: 'vue-style-loader!css-loader!less-loader' 35 | }, 36 | postLoaders: { 37 | html: 'babel-loader' 38 | } 39 | } 40 | }, 41 | { 42 | test: /\.js$/, 43 | loader: 'babel-loader', 44 | exclude: /node_modules/ 45 | }, 46 | { 47 | test: /\.(png|jpg|gif|svg)$/, 48 | loader: 'file-loader', 49 | options: { 50 | name: '[name].[ext]?[hash]' 51 | } 52 | } 53 | ] 54 | }, 55 | resolve: { 56 | alias: { 57 | 'vue$': 'vue/dist/vue.esm.js' 58 | }, 59 | extensions: ['*', '.js', '.vue', '.json'] 60 | }, 61 | devServer: { 62 | historyApiFallback: true, 63 | noInfo: true, 64 | overlay: true 65 | }, 66 | performance: { 67 | hints: false 68 | }, 69 | devtool: '#eval-source-map' 70 | } 71 | 72 | if (process.env.NODE_ENV === 'production') { 73 | module.exports.devtool = '#source-map' 74 | // http://vue-loader.vuejs.org/en/workflow/production.html 75 | module.exports.plugins = (module.exports.plugins || []).concat([ 76 | new webpack.DefinePlugin({ 77 | 'process.env': { 78 | NODE_ENV: '"production"' 79 | } 80 | }), 81 | new webpack.optimize.UglifyJsPlugin({ 82 | sourceMap: true, 83 | compress: { 84 | warnings: false 85 | } 86 | }), 87 | new webpack.LoaderOptionsPlugin({ 88 | minimize: true 89 | }), 90 | new ExtractTextPlugin('./vue-bigdata-table.css', { 91 | allChunks: true 92 | }), 93 | new webpack.optimize.CommonsChunkPlugin({ 94 | async: true, 95 | children: true 96 | }) 97 | ]) 98 | } 99 | --------------------------------------------------------------------------------