From df61ac1dad9b2a77d3157a70b7accc17f00a434c Mon Sep 17 00:00:00 2001 From: liyang <1711467488@qq.com> Date: Wed, 13 Jun 2018 15:13:27 +0800 Subject: [PATCH] no message Former-commit-id: 57424383590df6ac10d7c441098e16c31f7cff1a [formerly 491c2e4aa0fddd61f39cb879e9a953d40969ac0e] [formerly 57424383590df6ac10d7c441098e16c31f7cff1a [formerly 491c2e4aa0fddd61f39cb879e9a953d40969ac0e] [formerly 57424383590df6ac10d7c441098e16c31f7cff1a [formerly 491c2e4aa0fddd61f39cb879e9a953d40969ac0e] [formerly 491c2e4aa0fddd61f39cb879e9a953d40969ac0e [formerly 074d0a76168918e736973ea5caa315a1c2700ab2 [formerly e16c3f634a73cbfcc14d2c7e889220d769071da0]]]]] Former-commit-id: a97ea8612927180ae591ef7022c593e1c725cb20 Former-commit-id: 2a035bf971462a1f003bac5cbb6345e79869a953 Former-commit-id: 4fc0025144b2f70a8c7939934beb870fbfbc1bce [formerly ce26783e57c59a1237163a54c176d840d2976aca] Former-commit-id: e2019be83f67e22bd7a4330cf1dfc544e57e3e88 Former-commit-id: 80d2ff652ae9c2291451095f7c772aedaefa0ced Former-commit-id: 08577da4bb6d2ca67cb07fadcac2b63b1872f9dd Former-commit-id: 61fee4b6be26a14b5803c98a83bb3cd4f38f39a2 Former-commit-id: 44d02a765b61bed416e8e924e22c91251fbfcaff --- .eslintignore | 1 + build/webpack.base.conf.js | 4 + package.json | 2 + src/main.js | 3 + .../vue-bigdata-table/components/button.vue | 76 +++++ .../components/input-render.js | 55 ++++ .../vue-bigdata-table/components/input.vue | 46 +++ .../components/item-table.vue | 209 +++++++++++++ .../vue-bigdata-table/components/renderDom.js | 11 + .../components/sort-button.vue | 44 +++ src/plugin/vue-bigdata-table/index.js | 8 + .../vue-bigdata-table/mixins/data-handle.js | 199 ++++++++++++ src/plugin/vue-bigdata-table/mixins/edit.js | 77 +++++ .../vue-bigdata-table/mixins/empty-table.js | 31 ++ .../vue-bigdata-table/mixins/header-move.js | 64 ++++ src/plugin/vue-bigdata-table/mixins/index.js | 8 + src/plugin/vue-bigdata-table/mixins/sort.js | 40 +++ .../vue-bigdata-table/mixins/style-compute.js | 177 +++++++++++ .../vue-bigdata-table/styles/common.less | 29 ++ .../styles/vue-bigdata-table.less | 232 ++++++++++++++ src/plugin/vue-bigdata-table/util/index.js | 136 ++++++++ .../vue-bigdata-table/vue-bigdata-table.vue | 294 ++++++++++++++++++ 22 files changed, 1746 insertions(+) create mode 100755 src/plugin/vue-bigdata-table/components/button.vue create mode 100755 src/plugin/vue-bigdata-table/components/input-render.js create mode 100755 src/plugin/vue-bigdata-table/components/input.vue create mode 100755 src/plugin/vue-bigdata-table/components/item-table.vue create mode 100755 src/plugin/vue-bigdata-table/components/renderDom.js create mode 100755 src/plugin/vue-bigdata-table/components/sort-button.vue create mode 100755 src/plugin/vue-bigdata-table/index.js create mode 100755 src/plugin/vue-bigdata-table/mixins/data-handle.js create mode 100755 src/plugin/vue-bigdata-table/mixins/edit.js create mode 100755 src/plugin/vue-bigdata-table/mixins/empty-table.js create mode 100755 src/plugin/vue-bigdata-table/mixins/header-move.js create mode 100755 src/plugin/vue-bigdata-table/mixins/index.js create mode 100755 src/plugin/vue-bigdata-table/mixins/sort.js create mode 100755 src/plugin/vue-bigdata-table/mixins/style-compute.js create mode 100755 src/plugin/vue-bigdata-table/styles/common.less create mode 100755 src/plugin/vue-bigdata-table/styles/vue-bigdata-table.less create mode 100755 src/plugin/vue-bigdata-table/util/index.js create mode 100755 src/plugin/vue-bigdata-table/vue-bigdata-table.vue diff --git a/.eslintignore b/.eslintignore index e1fcc9c4..2bc76f59 100755 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ /config/ /dist/ /*.js +/src/plugin/vue-bigdata-table diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 0a43f3a4..e4101024 100755 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -81,6 +81,10 @@ module.exports = { test: /\.scss$/, loaders: ["style", "css", "sass"] }, + { + test: /\.less$/, + loaders: ["style", "css", "less"] + }, { test: /\.md$/, loaders: ["text-loader"] diff --git a/package.json b/package.json index 380540b8..b030ffcc 100755 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "file-loader": "^1.1.4", "friendly-errors-webpack-plugin": "^1.6.1", "html-webpack-plugin": "^2.30.1", + "less": "^3.0.4", + "less-loader": "^4.1.0", "node-notifier": "^5.1.2", "node-sass": "^4.7.2", "optimize-css-assets-webpack-plugin": "^3.2.0", diff --git a/src/main.js b/src/main.js index b712e985..4be272ea 100755 --- a/src/main.js +++ b/src/main.js @@ -41,6 +41,8 @@ import pluginExport from '@/plugin/export' import pluginLog from '@/plugin/log' // 插件 打开页面 import pluginOpen from '@/plugin/open' +// 插件 支持百万级数据的表格 +import bigdataTable from '@/plugin/vue-bigdata-table' Vue.use(ElementUI) @@ -48,6 +50,7 @@ Vue.use(pluginImport) Vue.use(pluginExport) Vue.use(pluginLog) Vue.use(pluginOpen) +Vue.use(bigdataTable) Vue.config.productionTip = false diff --git a/src/plugin/vue-bigdata-table/components/button.vue b/src/plugin/vue-bigdata-table/components/button.vue new file mode 100755 index 00000000..57ed5f84 --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/button.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/plugin/vue-bigdata-table/components/input-render.js b/src/plugin/vue-bigdata-table/components/input-render.js new file mode 100755 index 00000000..9719d48e --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/input-render.js @@ -0,0 +1,55 @@ +import Input from './input.vue'; +import Button from './button.vue'; +export default (h, {row, col, value, beforeSave, initRowIndex}, table) => { + return h('div', { + 'class': 'edit-item-con' + }, [ + h(Input, { + 'class': 'edit-item-input', + props: { + value: value + }, + on: { + input (res) { + table.editContent = res; + } + } + }), + h('div', { + 'class': 'edit-item-btn-con' + }, [ + h(Button, { + 'class': 'edit-btn', + props: { + type: 'confirm' + }, + on: { + click () { + const params = { + row, + col, + value: table.editContent, + initRowIndex + }; + if (beforeSave({ row, col, value, initRowIndex })) { + table.$emit('on-success-save', params); + } else { + table.$emit('on-fail-save', params); + } + } + } + }), + h(Button, { + 'class': 'edit-btn', + props: { + type: 'cancel' + }, + on: { + click () { + table.$emit('on-cancel-edit'); + } + } + }) + ]) + ]); +}; diff --git a/src/plugin/vue-bigdata-table/components/input.vue b/src/plugin/vue-bigdata-table/components/input.vue new file mode 100755 index 00000000..f64988da --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/input.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/plugin/vue-bigdata-table/components/item-table.vue b/src/plugin/vue-bigdata-table/components/item-table.vue new file mode 100755 index 00000000..84cbe66e --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/item-table.vue @@ -0,0 +1,209 @@ + + diff --git a/src/plugin/vue-bigdata-table/components/renderDom.js b/src/plugin/vue-bigdata-table/components/renderDom.js new file mode 100755 index 00000000..e494e8b6 --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/renderDom.js @@ -0,0 +1,11 @@ +export default { + name: 'RenderCell', + functional: true, + props: { + render: Function, + backValue: [Number, Object] + }, + render: (h, ctx) => { + return ctx.props.render(h, ctx.props.backValue, ctx.parent); + } +}; diff --git a/src/plugin/vue-bigdata-table/components/sort-button.vue b/src/plugin/vue-bigdata-table/components/sort-button.vue new file mode 100755 index 00000000..70ec9ab5 --- /dev/null +++ b/src/plugin/vue-bigdata-table/components/sort-button.vue @@ -0,0 +1,44 @@ + + diff --git a/src/plugin/vue-bigdata-table/index.js b/src/plugin/vue-bigdata-table/index.js new file mode 100755 index 00000000..7c06a58a --- /dev/null +++ b/src/plugin/vue-bigdata-table/index.js @@ -0,0 +1,8 @@ +import bigdataTable from './vue-bigdata-table.vue'; +const install = (Vue, opts = {}) => { + Vue.component('bigdataTable', bigdataTable); +}; +if (typeof window !== 'undefined' && window.Vue) { + install(window.Vue); +} +export default Object.assign(bigdataTable, {install}); diff --git a/src/plugin/vue-bigdata-table/mixins/data-handle.js b/src/plugin/vue-bigdata-table/mixins/data-handle.js new file mode 100755 index 00000000..5f5dfacc --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/data-handle.js @@ -0,0 +1,199 @@ +import ItemTable from '../components/item-table.vue'; +import { iteratorByTimes, getHeaderWords } from '../util'; +export default { + data () { + return { + times0: 0, // 当前是第几轮 + times1: 0, + times2: -1, + table1Data: [], + table2Data: [], + table3Data: [], + currentIndex: 0, // 当前展示的表格是第几个 + itemNum: 0, // 一块数据显示的数据条数 + timer: null, + scrollLeft: 0, + insideTableData: [], + initTableData: [] // 初始表格数据,用于恢复搜索和筛选 + }; + }, + computed: { + cellNum () { // 表格列数 + return this.columnsHandled.length; + }, + columnsHandled () { + let columns = [...this.columns]; + if (this.colNum > this.columns.length) { + let colLength = this.colNum - this.columns.length; + let headerWordsArr = getHeaderWords(colLength); + iteratorByTimes(colLength, (i) => { + columns.push({ + title: headerWordsArr[i] + }); + }); + } + if (this.showIndex) { + columns.unshift({ + title: 'No', + align: 'center', + width: this.indexWidthInside + }); + } + return columns; + } + }, + watch: { + scrollTop (top) { + this.currentIndex = parseInt((top % (this.moduleHeight * 3)) / this.moduleHeight); + this.$nextTick(() => { + this.setTopPlace(); + }); + } + }, + methods: { + getComputedTableDataIndex (colIndex) { + return this.showIndex ? (colIndex - 1) : colIndex; + }, + handleScroll (e) { + const ele = e.srcElement || e.target; + const { scrollTop, scrollLeft } = ele; + this.scrollLeft = scrollLeft; + this.scrollTop = scrollTop; + }, + setTableData () { + const count1 = this.times0 * this.itemNum * 3; + this.table1Data = this.insideTableData.slice(count1, count1 + this.itemNum); + const count2 = this.times1 * this.itemNum * 3; + this.table2Data = this.insideTableData.slice(count2 + this.itemNum, count2 + this.itemNum * 2); + const count3 = this.times2 * this.itemNum * 3; + this.table3Data = this.insideTableData.slice(count3 + this.itemNum * 2, count3 + this.itemNum * 3); + }, + getTables (h) { + let table1 = this.getItemTable(h, this.table1Data, 1); + let table2 = this.getItemTable(h, this.table2Data, 2); + let table3 = this.getItemTable(h, this.table3Data, 3); + if (this.currentIndex === 0) return [table1, table2, table3]; + else if (this.currentIndex === 1) return [table2, table3, table1]; + else return [table3, table1, table2]; + }, + renderTable (h) { + return h('div', { + style: this.tableWidthStyles + }, this.getTables(h)); + }, + getItemTable (h, data, index) { + return h(ItemTable, { + props: { + times: this['times' + (index - 1)], + tableIndex: index, + itemData: data, + itemNum: this.itemNum, + rowStyles: this.rowStyles, + widthArr: this.colWidthArr, + columns: this.columnsHandled, + showIndex: this.showIndex, + indexRender: this.indexRender, + stripe: this.stripe, + fixedCol: this.fixedCol, + currentScrollToRowIndex: this.currentScrollToRowIndex, + canEdit: this.canEdit, + edittingTd: this.edittingTd, + startEditType: this.startEditType, + showFixedBoxShadow: this.showFixedBoxShadow, + editCellRender: this.editCellRender, + beforeSave: this.beforeSave, + canSelectText: this.canSelectText, + startSelect: this.startSelect, + endSelect: this.endSelect, + disabledHover: this.disabledHover + }, + on: { + 'on-click-tr': (index) => { + this.$emit('on-click-tr', index); + }, + 'on-click-td': (params) => { + this.$emit('on-click-td', params); + }, + 'on-edit-cell': (row, col) => { + this.edittingTd = `${row}-${col}`; + }, + 'on-success-save': ({ row, col, value, initRowIndex }) => { + let data = [...this.value]; + data[initRowIndex][col] = value; + this.$emit('input', data); + this.$emit('on-success-save', { row, col, value, initRowIndex }); + this.edittingTd = ''; + }, + 'on-fail-save': ({ row, col, value, initRowIndex }) => { + this.$emit('on-fail-save', { row, col, value, initRowIndex }); + }, + 'on-cancel-edit': () => { + this.edittingTd = ''; + }, + 'on-paste': (data) => { + if (!this.paste) return; + let value = [...this.value]; + let rowLength = data.length; + let startSelect = this.startSelect; + let endSelect = this.endSelect; + let startRow = startSelect.row; + let startCol = startSelect.col; + let endRow = endSelect.row; + let endCol = endSelect.col; + let selectRow = endRow - startRow + 1; + let selectCol = endCol - startCol + 1; + // let lastColLength = value[0].length - startCol; + // let lastRowLength = value.length - startRow; + if (rowLength === 0) return; + let colLength = data[0].length; + if (colLength === 0) return; + // 使用复制的数据替换原数据 + for (let r = 0; r < rowLength && r < selectRow; r++) { + for (let c = 0; c < colLength && c < selectCol; c++) { + let valueRow = startRow + r; + let valueCol = startCol + c; + if (typeof value[valueRow][valueCol] === 'object') { + value[valueRow][valueCol].value = data[r][c]; + } else { + value[valueRow][valueCol] = data[r][c]; + } + } + } + // for (let r = startRow; r < selectRow; r++) { + // for (let c = startCol; c < selectCol; c++) { + // // + // } + // } + this.$emit('input', value); + this.$emit('on-paste', data); + this._tableResize(); + } + }, + key: 'table-item-key' + index, + ref: 'itemTable' + index, + attrs: { + 'data-index': index + } + }); + }, + _scrollToIndexRow (index) { + index = parseInt(index); + if (isNaN(index) || index >= this.insideTableData.length || index < 0) return; + let scrollTop = index * this.itemRowHeight; + this.$refs.outer.scrollTop = scrollTop; + this.currentScrollToRowIndex = index; + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.currentScrollToRowIndex = -1; + }, 1800); + }, + // 给表格数据添加行号,用于排序后正确修改数据 + setIndex (tableData) { + return tableData.map((item, i) => { + let row = item; + row.initRowIndex = i; + return row; + }); + } + } +}; diff --git a/src/plugin/vue-bigdata-table/mixins/edit.js b/src/plugin/vue-bigdata-table/mixins/edit.js new file mode 100755 index 00000000..0932c226 --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/edit.js @@ -0,0 +1,77 @@ +import { findNodeUpper, on, off, attr } from '../util'; +export default { + data () { + return { + edittingTd: '', // 正在编辑的单元格的行号和列号拼接的字符串 `${row}-${col}` + editContent: '', // 用来保存编辑的内容 + selectCellsStart: {}, // 编辑模式下可选中多行多列,此用来保存其实单元格行列号 + selectCellsEnd: {}, + selectTotalColStartIndex: -1, // 选取整列起始序列号 + selectTotalColEndIndex: -1 + }; + }, + computed: { + startSelect () { + return { + row: Math.min(this.selectCellsStart.row, this.selectCellsEnd.row), + col: Math.min(this.selectCellsStart.col, this.selectCellsEnd.col) + }; + }, + endSelect () { + return { + row: Math.max(this.selectCellsStart.row, this.selectCellsEnd.row), + col: Math.max(this.selectCellsStart.col, this.selectCellsEnd.col) + }; + } + }, + methods: { + _editCell (row, col) { + if (!this.canEdit || row < 0 || row > this.insideTableData.length || col < 0 || col > this.columns.length || this.edittingTd === `${row}-${col}`) return; + if (parseInt(this.edittingTd.split('-')[0]) !== row) this.scrollToRow(row); + this.edittingTd = `${row}-${col}`; + }, + getCurrentTd (e) { + return e.target.tagName === 'TD' ? e.target : findNodeUpper(e.target, 'td'); + }, + handleMousedownOnTable (e) { + if (e.button !== 0 || (!this.paste && !this.selectable)) return; + let currentTd = this.getCurrentTd(e); + this.selectCellsStart = { + row: attr(currentTd, 'data-row'), + col: attr(currentTd, 'data-col') + }; + this.selectCellsEnd = { + row: attr(currentTd, 'data-row'), + col: attr(currentTd, 'data-col') + }; + this.canSelectText = false; + on(document, 'mousemove', this.handleMoveOnTable); + on(document, 'mouseup', this.handleUpOnTable); + }, + handleMoveOnTable (e) { + if (!(e.target.tagName === 'TD' || findNodeUpper(e.target, 'td'))) return; + let currentTd = this.getCurrentTd(e); + this.selectCellsEnd = { + row: attr(currentTd, 'data-row'), + col: attr(currentTd, 'data-col') + }; + }, + handleUpOnTable (e) { + if (!this.paste && !this.selectable) return; + this.canSelectText = true; + this.handleMoveOnTable(e); + off(document, 'mousemove', this.handleMoveOnTable); + off(document, 'mouseup', this.handleUpOnTable); + this.$emit('on-select-cells', { + start: { + row: this.startSelect.row, + col: this.startSelect.col + }, + end: { + row: this.endSelect.row, + col: this.endSelect.col + } + }); + } + } +}; diff --git a/src/plugin/vue-bigdata-table/mixins/empty-table.js b/src/plugin/vue-bigdata-table/mixins/empty-table.js new file mode 100755 index 00000000..c03650dc --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/empty-table.js @@ -0,0 +1,31 @@ +export default { + methods: { + _createEmptyData () { + // this.$nextTick(() => { + let rowNum = this.rowNum; + let colNum = this.colNum; + if (this.rowNum && this.colNum) { + console.log(this.value.length, this.rowNum, this.colNum); + let valueRowNum = this.value.length; + let valueColNum = this.value[0] ? this.value[0].length : 0; + let totalRowNum = valueRowNum + rowNum; + let totalColNum = valueColNum + colNum; + let value = [...this.value]; + console.log(totalRowNum, valueRowNum); + for (let r = valueRowNum; r < totalRowNum; r++) { + value.push([]); + for (let c = valueColNum; c < totalColNum; c++) { + value[r].push(''); + } + } + // this. + console.log(value); + this.$emit('input', value); + // this.$nextTick(() => { + // this._tableResize(); + // }) + } + // }); + } + } +}; diff --git a/src/plugin/vue-bigdata-table/mixins/header-move.js b/src/plugin/vue-bigdata-table/mixins/header-move.js new file mode 100755 index 00000000..041502f8 --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/header-move.js @@ -0,0 +1,64 @@ +import { findNodeUpper, attr } from '../util'; +export default { + data () { + return { + isOnCellEdge: false, // 鼠标是否在表头的两个单元格之间的边框上 + canResizeCell: false, + initCellX: 0, // 用于计算鼠标移动的距离 + scrollLeft: 0, + colIndex: 0, // 在表头上移动时鼠标所在列的序号, + atLeftGivenArea: false, // 是否在表头单元格指定区域(距左侧) + atRightGivenArea: false // 是否在表头单元格指定区域(距右侧) + }; + }, + methods: { + handleMousemove (e) { + let cell = e.srcElement.tagName.toUpperCase() === 'TH' ? e.srcElement : findNodeUpper(e.srcElement, 'th'); + let cellDomRect = cell.getBoundingClientRect(); + let atLeft = (e.pageX - cellDomRect.left) < (cellDomRect.width / 2); + let atLeftGivenArea = (cellDomRect.left + this.atLeftCellPosi) >= e.pageX; + let atRightGivenArea = (cellDomRect.right - e.pageX) <= this.atRightCellPosi; + let cellIndex = parseInt(attr(cell, 'data-index')); // 当前单元格的序号 + if (atLeft && cellIndex !== 0) { + this.isOnCellEdge = (e.pageX - cellDomRect.left) <= 1 && cellIndex - 1 !== this.fixedCol; + } else if (!atLeft && cellIndex !== this.cellNum - 1) { + this.isOnCellEdge = (cellDomRect.right - e.pageX) <= 1 && cellIndex !== this.fixedCol; + } + e.atRightGivenArea = atRightGivenArea; + e.atLeftGivenArea = atLeftGivenArea; + this.atRightGivenArea = atRightGivenArea; + this.atLeftGivenArea = atLeftGivenArea; + let index = 0; // 调整表格列宽的左侧的表格的序列 + e.colIndex = cellIndex; + this.colIndex = cellIndex; + this.$emit('on-moving-on-header', e); + if (this.canResizeCell) { + if (atLeft) { + index = cellIndex - 1; + } else { + index = cellIndex; + } + if (index === this.fixedCol) return; + let widthLeft = this.widthArr[index] + e.pageX - this.initCellX; + let widthRight = this.widthArr[index + 1] + this.initCellX - e.pageX; + this.widthArr.splice(index, 2, widthLeft, widthRight); + this.initCellX = e.pageX; + } + }, + handleMousedown (e) { + e.colIndex = this.cellIndex; + this.$emit('on-mousedown-on-header', e); + if (this.isOnCellEdge) { + this.canResizeCell = true; + this.initCellX = e.pageX; + } + }, + canNotMove (e) { + this.canResizeCell = false; + e.colIndex = this.colIndex; + e.atLeftGivenArea = this.atLeftGivenArea; + e.atRightGivenArea = this.atRightGivenArea; + this.$emit('on-mouseup-on-header', e); + } + } +}; diff --git a/src/plugin/vue-bigdata-table/mixins/index.js b/src/plugin/vue-bigdata-table/mixins/index.js new file mode 100755 index 00000000..f9f54e38 --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/index.js @@ -0,0 +1,8 @@ +import headerMove from './header-move'; +import styleComputed from './style-compute'; +import dataHandle from './data-handle'; +import edit from './edit'; +import emptyTable from './empty-table'; +import sort from './sort'; + +export default [ headerMove, styleComputed, dataHandle, edit, emptyTable, sort ]; diff --git a/src/plugin/vue-bigdata-table/mixins/sort.js b/src/plugin/vue-bigdata-table/mixins/sort.js new file mode 100755 index 00000000..c88bb5e9 --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/sort.js @@ -0,0 +1,40 @@ +import { sortArr, sortDesArr } from '../util'; +export default { + data () { + return { + sortedByColIndex: -1, + sortedType: '' + }; + }, + methods: { + showSortBtn (colIndex) { + const sortable = this.sortable ? true : this.sortIndex !== undefined; + return (sortable && !(this.showIndex && colIndex === 0) && (typeof this.sortIndex === 'number' ? colIndex <= this.sortIndex : this.sortIndex.indexOf(colIndex) >= 0)) || this.columnsHandled[colIndex].sortable; + }, + handleSort (colIndex, sortType) { + this.sortedByColIndex = colIndex; + this.sortedType = sortType; + let valueArr = [...this.value]; + colIndex = this.showIndex ? colIndex - 1 : colIndex; + if (sortType === 'up') { + sortArr(valueArr, colIndex); + } else { + sortDesArr(valueArr, colIndex); + } + this.insideTableData = [...valueArr]; + }, + handleCancelSort () { + this.sortedByColIndex = -1; + this.sortedType = ''; + this.insideTableData = [...this.value]; + }, + initSort () { + if (this.defaultSort) { + const colIndex = parseInt(Object.keys(this.defaultSort)[0]); + if (!(colIndex || colIndex === 0)) return; + const sortType = this.defaultSort[colIndex]; + this.handleSort(colIndex, sortType); + } + } + } +}; diff --git a/src/plugin/vue-bigdata-table/mixins/style-compute.js b/src/plugin/vue-bigdata-table/mixins/style-compute.js new file mode 100755 index 00000000..6eae57b6 --- /dev/null +++ b/src/plugin/vue-bigdata-table/mixins/style-compute.js @@ -0,0 +1,177 @@ +import { getScrollbarWidth } from '../util'; +export default { + data () { + return { + wrapperHeight: 0, + scrollTop: 0, + moduleHeight: 0, // 三个tr块中的一块的高度 + topPlaceholderHeight: 0, // 顶部占位容器高度 + tableWidth: 0, + widthArr: [], // 用于给数据表格传递列宽 + totalRowHeight: 0, // 如果全量渲染应该是多高,用于计算占位 + currentScrollToRowIndex: -1, // 当前跳转到的行号,用于做闪烁提示 + canSelectText: true, // 用于控制是否可选中表格文字 + indexWidthInside: 0, + outerWidth: 0, // 外面容器宽度 + oldTableWidth: 0 // 旧的表格宽度,用于重新计算列宽 + }; + }, + computed: { + fixedColCom () { + return this.showIndex ? (this.fixedCol + 1) : this.fixedCol; + }, + wrapperClasses () { + return [ + this.prefix, + this.fixed ? `${this.prefix}-fixed` : '' + ]; + }, + headerStyle () { + return { + height: this.headerHeight + 'px', + transform: 'translateX(0)' + }; + }, + showFixedBoxShadow () { + return this.scrollLeft !== 0; + }, + tableWidthStyles () { + return {width: this.tableWidth + 'px'}; + }, + rowStyles () { + return this.rowHeight !== undefined ? {height: `${this.rowHeight}px`} : {}; + }, + placeholderHeight () { + return this.totalRowHeight - this.moduleHeight * 3; // 占位容器的总高度(上 + 下) + }, + bottomPlaceholderHeight () { + return (this.placeholderHeight - this.topPlaceholderHeight) < 0 ? 0 : this.placeholderHeight - this.topPlaceholderHeight; + }, + itemRowHeight () { + return this.rowHeight === undefined ? 48 : this.rowHeight; + }, + colWidthArr () { + let len = this.cellNum; + let colWidthArr = []; + if (this.fixedWrapperWidth) { + let width = this.outerWidth; + let num = this.cellNum; + if (this.showIndex) { + colWidthArr.push(this.indexWidth); + width -= this.indexWidth; + num -= 1; + } + let i = -1; + let itemColWidth = width / num; + while (++i < num) { + colWidthArr.push(itemColWidth); + } + } else { + let i = 0; + let hasWidthCellCount = 0; // 统计设置了width的列的数量,从而为没有设置width的列分配宽度 + let noWidthCellIndexArr = []; // 没有设置宽度的列的序列 + let hasWidthCellTotalWidth = 0; // 设置了width的列一共多宽 + while (i < len) { + if (this.columnsHandled[i].width) { + hasWidthCellCount++; + hasWidthCellTotalWidth += this.columnsHandled[i].width; + colWidthArr.push(this.columnsHandled[i].width); + } else { + noWidthCellIndexArr.push(i); + colWidthArr.push(0); + } + i++; + } + let noWidthCellWidth = (this.tableWidth - hasWidthCellTotalWidth) / (len - hasWidthCellCount); + let w = 0; + let indexArrLen = noWidthCellIndexArr.length; + while (w < indexArrLen) { + colWidthArr[noWidthCellIndexArr[w]] = noWidthCellWidth; + w++; + } + // this.widthArr = colWidthArr; + } + return colWidthArr; + }, + cursorOnHeader () { + return this.headerTrStyle.cursor ? this.headerTrStyle.cursor : ((this.isOnCellEdge || this.canResizeCell) ? 'col-resize' : 'default'); + } + }, + methods: { + _tableResize () { + this.$nextTick(() => { + this.updateHeight(); + this.setComputedProps(); + let scrollBarWidth = this.totalRowHeight > this.wrapperHeight ? getScrollbarWidth() : 0; + this.outerWidth = this.$refs.outer.offsetWidth - 2 - scrollBarWidth; + let width = this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0); + // this.tableWidth = width > this.outerWidth ? width : this.outerWidth; + this.tableWidth = this.fixedWrapperWidth ? this.outerWidth : (width > this.outerWidth ? width : this.outerWidth); + if (width < this.outerWidth) this._setColWidthArr(); + this.widthArr = this.colWidthArr; + }); + }, + updateHeight () { + this.$nextTick(() => { + let wrapperHeight = this.$refs.outer.offsetHeight; + this.itemNum = Math.ceil((wrapperHeight - this.headerHeight) / this.itemRowHeight) + this.appendNum; + this.moduleHeight = this.itemNum * this.itemRowHeight; + this.wrapperHeight = wrapperHeight; + this.setTopPlace(); + }); + }, + setComputedProps () { + const len = this.insideTableData.length; + this.totalRowHeight = len * this.itemRowHeight; + }, + setIndexWidth (len) { + let width = 70; + if (len <= 99) { + width = 50; + } else if (len > 99 && len <= 1000) { + width = 60; + } else if (len > 1000 && len <= 10000) { + width = 70; + } else if (len > 10000 && len <= 100000) { + width = 90; + } else { + width = 100; + } + return width; + }, + setTopPlace () { + let scrollTop = this.scrollTop; + let t0 = 0; + let t1 = 0; + let t2 = 0; + if (scrollTop > this.moduleHeight) { + switch (this.currentIndex) { + case 0: t0 = parseInt(scrollTop / (this.moduleHeight * 3)); t1 = t2 = t0; break; + case 1: t1 = parseInt((scrollTop - this.moduleHeight) / (this.moduleHeight * 3)); t0 = t1 + 1; t2 = t1; break; + case 2: t2 = parseInt((scrollTop - this.moduleHeight * 2) / (this.moduleHeight * 3)); t0 = t1 = t2 + 1; + } + } + this.times0 = t0; + this.times1 = t1; + this.times2 = t2; + this.topPlaceholderHeight = parseInt(scrollTop / this.moduleHeight) * this.moduleHeight; + this.setTableData(); + }, + _initM () { + if (this.indexWidth === undefined) this.indexWidthInside = this.setIndexWidth(this.insideTableData.length); + else this.indexWidthInside = this.indexWidth; + this.oldTableWidth = this.colWidthArr.reduce((sum, b) => { + return sum + b; + }, 0); + this.widthArr = this.colWidthArr; + if ((this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0)) < this.outerWidth) this._setColWidthArr(); + }, + _setColWidthArr () { + let widthArr = this.widthArr.map(width => { + return width / this.oldTableWidth * this.tableWidth; + }); + this.oldTableWidth = this.tableWidth; + this.widthArr = widthArr; + } + } +}; diff --git a/src/plugin/vue-bigdata-table/styles/common.less b/src/plugin/vue-bigdata-table/styles/common.less new file mode 100755 index 00000000..bfaae59c --- /dev/null +++ b/src/plugin/vue-bigdata-table/styles/common.less @@ -0,0 +1,29 @@ +@basic-blue: #3695FE; +@border-color: #e9eaec; +@sort-border-width: 4px; +@sort-border-default: #bbbec4; +@sort-border-hover: #495060; +@sort-border-active: @basic-blue; +// 超出用省略号代替 +.text-ellipsis{ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; +} +// 选择单元格表格 +.select-border (@direction) { + border-@{direction}: 1px solid @basic-blue; +} +.no-select{ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.sticky{ + position: -webkit-sticky; + position: sticky; +} diff --git a/src/plugin/vue-bigdata-table/styles/vue-bigdata-table.less b/src/plugin/vue-bigdata-table/styles/vue-bigdata-table.less new file mode 100755 index 00000000..2a341f23 --- /dev/null +++ b/src/plugin/vue-bigdata-table/styles/vue-bigdata-table.less @@ -0,0 +1,232 @@ +@import './common.less'; +@prefix: ~"v-bt"; +@keyframes scroll-tip { + 0% { + background: #fff; + } + 50% { + background: #d0e8ff; + } +} + +.@{prefix} { + width: 100%; + box-sizing: border-box; + * { + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; + color: #495060; + font-size: 12px; + font-weight: 400; + } + &-outer { + width: 100%; + height: 100%; + overflow: auto; + border: 1px solid @border-color; + box-sizing: border-box; + position: relative; + .@{prefix}-header-wrapper { + box-sizing: border-box; + z-index: 70; + &.header-wrapper-fixed { + .sticky; + } + table { + table-layout: fixed; + height: 100%; + tr th { + border-right: 1px solid @border-color; + border-bottom: 1px solid @border-color; + background: #fff; + .text-ellipsis; + .@{prefix}-header-inside-wrapper { + box-sizing: border-box; + padding: 0 8px; + } + } + } + } + } + &-fixed-header { + .sticky; + transform: translateX(0); + left: 0; + z-index: 110; + transition: box-shadow .2s ease; + &.box-shadow { + box-shadow: 2px 0 6px -2px rgba(0, 0, 0, .2); + transition: box-shadow .2s ease; + } + } + &-wrapper { + width: 100%; + border-bottom: none; + .@{prefix}-content { + width: 100%; + height: auto; + &.noselect-text { + .no-select; + } + } + &:nth-child(2) { + border-top: 1px solid @border-color; + } + &:nth-child(4) { + border-bottom: 1px solid @border-color; + } + .@{prefix}-data-table { + &.@{prefix}-content-table { + left: 0; + top: 0; + } + border-bottom: none; + border-top: none; + table-layout: fixed; + tr { + background: #fff; + &.scroll-to-row-tip { + animation: scroll-tip .6s 3; + } + td{ + min-width: 0; + height: 48px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + border-bottom: 1px solid @border-color; + border-right: 1px solid @border-color; + border-left: 1px solid transparent; + border-top: 1px solid transparent; // 表格选中 + .@{prefix}-cell { + box-sizing: border-box; + padding: 0 18px; + .text-ellipsis; + } + .edit-item-con { + width: 100%; + text-align: left; + padding: 0 6px; + box-sizing: border-box; + .edit-item { + &-input { + width: ~"calc(100% - 50px)"; + float: left; + } + &-btn-con { + float: left; + .edit-btn { + width: 20px; + margin: 7px 4px 0 0; + } + } + } + } + &.start-select-cell { + .select-border(left); + .select-border(top); + } + &.end-select-cell { + .select-border(right); + .select-border(bottom); + } + &.right-top-select-cell { + .select-border(right); + .select-border(top); + } + &.left-bottom-select-cell { + .select-border(left); + .select-border(bottom); + } + &.top-center-select-cell { + .select-border(top); + } + &.bottom-center-select-cell { + .select-border(bottom); + } + &.left-center-select-cell { + .select-border(left); + } + &.right-center-select-cell { + .select-border(right); + } + } + &.stripe-gray { + background: #f8f8f9; + } + } + &-left { + text-align: left; + } + &-center { + text-align: center; + } + &-right { + text-align: right; + } + } + } + &-fixed { + .@{prefix}-header-wrapper { + top: 0; + left: 0; + box-sizing: border-box; + } + } + &-fixed-table { + .sticky; + left: 0; + z-index: 60; + transition: box-shadow .2s ease; + &.box-shadow { + box-shadow: 2px 0 6px -2px rgba(0, 0, 0, .2); + transition: box-shadow .2s ease; + } + td { + border-right: 1px solid @border-color; + } + } + &-item-table { + position: relative; + } + .sort-button { + &-wrapper { + display: inline-block; + position: relative; + width: 10px; + height: 11px; + transform: translateY(1px); + } + &-item { + position: absolute; + display: inline-block; + width: 0; + height: 0; + border: @sort-border-width solid transparent; + margin: 0; + padding: 0; + cursor: pointer; + transition: border-color .2s ease; + &-up { + top: -4px; + border-bottom: @sort-border-width solid @sort-border-default; + &:hover { + border-bottom: @sort-border-width solid @sort-border-hover; + } + &-active { + border-bottom: @sort-border-width solid @sort-border-active; + } + } + &-down { + bottom: -4px; + border-top: @sort-border-width solid @sort-border-default; + &:hover { + border-top: @sort-border-width solid @sort-border-hover; + } + &-active { + border-top: @sort-border-width solid @sort-border-active; + } + } + } + } +} diff --git a/src/plugin/vue-bigdata-table/util/index.js b/src/plugin/vue-bigdata-table/util/index.js new file mode 100755 index 00000000..defa6ee8 --- /dev/null +++ b/src/plugin/vue-bigdata-table/util/index.js @@ -0,0 +1,136 @@ +export const findNodeUpper = (ele, tag) => { + if (ele.parentNode) { + if (ele.parentNode.tagName === tag.toUpperCase()) { + return ele.parentNode; + } else { + if (ele.parentNode) return findNodeUpper(ele.parentNode, tag); + else return false; + } + } +}; + +export const getScrollbarWidth = () => { + let oP = document.createElement('p'); + let styles = { + width: '100px', + height: '100px', + overflowY: 'scroll' + }; + for (let i in styles) { + oP.style[i] = styles[i]; + } + document.body.appendChild(oP); + let scrollbarWidth = oP.offsetWidth - oP.clientWidth; + oP.remove(); + return scrollbarWidth; +}; + +export const createNewArray = (length, content = undefined) => { + let i = -1; + let arr = []; + while (++i < length) { + let con = Array.isArray(content) ? content[i] : content; + arr.push(con); + } + return arr; +}; + +export const iteratorByTimes = (times, fn) => { + let i = -1; + while (++i < times) { + fn(i); + } +}; + +export const getHeaderWords = (length) => { + 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']; + let headerArr = []; + if (length <= 26) { + headerArr = wordsArr.slice(0, length); + } else { + headerArr = [...wordsArr]; + let num = length - 26; + let firstWordIndex = 0; + let secondWordIndex = 0; + let i = -1; + while (++i < num) { + firstWordIndex = Math.floor(i / 26); + secondWordIndex = i % 26; + let sumWord = `${wordsArr[firstWordIndex]}${wordsArr[secondWordIndex]}`; + headerArr.push(sumWord); + } + } + return headerArr; +}; + +// 获取数组中第一个不为空的值 +export const getFirstNotNullValue = (array, index) => { + if (!(array && array.length)) return false; + let r = -1; + let rowLength = array.length; + while (++r < rowLength) { + let item = array[r][index]; + if (item || item === 0) return item; + } + return false; +}; + +const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); +export const sortArr = (arr, index) => { + if (arr.length <= 1) return; + const firstNotNullValue = getFirstNotNullValue(arr, index); + if (!firstNotNullValue && firstNotNullValue !== 0) return; + if (!isChineseReg.test(firstNotNullValue)) { + if (isNaN(Number(firstNotNullValue))) { + // 非中文非数值 + arr.sort(); + } else { + // 数值型 + arr.sort((a, b) => { + return a[index] - b[index]; + }); + } + } else { + arr.sort((a, b) => { + return a[index].localeCompare(b[index], 'zh'); + }); + } +}; + +// 倒序 +export const sortDesArr = (arr, index) => { + if (arr.length <= 1) return; + const firstNotNullValue = getFirstNotNullValue(arr, index); + if (!firstNotNullValue && firstNotNullValue !== 0) return; + if (!isChineseReg.test(firstNotNullValue)) { + if (isNaN(Number(firstNotNullValue))) { + // 非中文非数值 + arr.sort().reverse(); + } else { + // 数值型 + arr.sort((a, b) => { + return b[index] - a[index]; + }); + } + } else { + arr.sort((a, b) => { + return b[index].localeCompare(a[index], 'zh'); + }); + } +}; + +export const on = (ele, event, callback) => { + ele.addEventListener(event, callback); +}; + +export const off = (ele, event, callback) => { + ele.removeEventListener(event, callback); +}; + +export const attr = (ele, attribution, value) => { + if (value || value === 0) { + ele.setAttribute(attribution, value); + } else { + return ele.getAttribute(attribution); + } +} diff --git a/src/plugin/vue-bigdata-table/vue-bigdata-table.vue b/src/plugin/vue-bigdata-table/vue-bigdata-table.vue new file mode 100755 index 00000000..1d6a10cc --- /dev/null +++ b/src/plugin/vue-bigdata-table/vue-bigdata-table.vue @@ -0,0 +1,294 @@ + + + +