├── .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 | 
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 |
2 |
3 |
4 |
36 |
37 |
你可以点击序列号试试
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
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 |
2 |
6 |
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 |
2 |
3 |
4 |
20 |
46 |
--------------------------------------------------------------------------------
/src/vue-bigdata-table/components/item-table.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
20 |
21 |
22 | |
23 |
36 |
37 | {{ (typeof td === 'object' && td !== null) ? td.value : td }}
38 |
39 |
40 |
41 |
42 |
43 | |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
63 |
64 |
65 | |
66 |
79 |
80 | {{ (typeof td === 'object' && td !== null) ? td.value : td }}
81 |
82 |
83 |
84 |
85 |
86 | |
87 |
88 |
89 |
90 |
91 |
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 |
2 |
3 |
4 |
5 |
6 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
23 | {{ col.title }}
24 |
25 | |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
39 |
40 | {{ col.title }}
41 |
42 | |
43 |
44 |
45 |
46 |
51 |
52 |
53 |
54 |
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 |
--------------------------------------------------------------------------------