1、需求: 由于业务需求在页面一次性展示较多数据,不低于上千,但是每条数据涉及样式较多,数据渲染过多就会导致页面卡顿
2、满足: 大量数据加载;表格功能:列显隐、列顺序调整、固定、筛选、排序;表格调整存储本地
3、技术框架: 若依、Element UI、vue2
1、umy-ui库中的table表格组件,它不造轮子。它改造了element-ui等等库的表格组件。只为了免费解决前端小伙伴的问题。
2、用前须知(这是关于表格的须知,你应该认真读完下面的内容)
1. 表格解决卡顿问题,那么虚拟表格原理呢大概就是: 减少对DOM节点的渲染,通过滚动函数节流实现滚动后事件来动态渲染数据
2. 基础表格其实就是element的表格的升级版,修改了ele的表格bug(如果你想使用个普通表格你无需安装其他库,就使用这个表格即可),你可以发现基础表格里面的示例没有配置:use-virtual 这个属性。
3 基础表格没有使用use-virtual属性,代表表格数据不多,只想要一个普通的表格。如果你表格卡。请你关注下虚拟表格部分。
4. 使用u-table 开启use-virtual虚拟可以支持微小的合并行|列 如2列 2行,支持多级头, 超过2行2列可能布局错乱,因为虚拟滚动的原理导致某些节点并未渲染。
4.5 使用u-table 开启use-virtual不支持开展行,如果需要展开行,你是要虚拟表格部分的ux展开行!
5. u-table不支持展开行,需要展开行使用ux-grid
6. ux-grid解决列多 行多导致卡的情况, u-table解决行多的情况,不解决列多的情况(如你的列超过70+,你可能就需要使用ux-grid了,因为此时你需要把列也虚拟)
7. 重点:虚拟表格集成了基础表格的东西(如属性/方法/事件)!
8. 虚拟表格在本文档中呢, 意思就是解决了数据量多导致卡顿的情况! 基础表格在文档中呢,意思就是升级版的el-table(但是没解决数据多卡的情况)!
9. 编辑型表格呢,是解决那种表格单元带有输入框或者选择时间等等的情况,而导致卡顿的场景!意思就是表格单元格具有一定的操作,单元格有自定义组件或者UI库组件等等
10. 有了表格,怎么导出表格数据为excel并且带样式呢?,[请点击](https://github.com/livelyPeng/pl-export-excel)
1.安装
推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用
npm install umy-ui
2.引入
main.js
// 引入umy-ui
import UmyUi from 'umy-ui'
Vue.use(UmyUi);
以下代码是基于若依框架封装的主代码,其余见附带资源中,对应表格中输入或展示形式可自行封装:
<script>
export default {
name: "SuperUxTable",
props: {
// 数据
value: {
type: [Array],
require: true,
},
// 字典
dict: {
type: [Object],
require: true,
},
// 分页
page: {
type: [Object],
require: false,
},
// 模板
columns: {
type: [Array],
require: true,
},
// 是否显示序号
index: {
type: Boolean,
default: false,
},
// 是否显示单选
radio: {
type: Boolean,
default: false,
},
// 是否显示多选
checkbox: {
type: Boolean,
default: false,
},
// 是否显示分页
pagination: {
type: Boolean,
default: false,
},
// 是否列操作
convenitentOperation: {
type: Boolean,
default: false,
},
// 是否禁止选择
selectable: {
type: Function,
default: () => {},
},
//
storageKey: {
type: String,
},
showSummary: {
type: Boolean,
default: false,
},
height: {
type: [String, Number],
require: false,
},
firstSummary: {
type: Boolean,
default: false,
},
},
components: {
ElDictTag: () => import("@/components/DictTag/index.vue"),
ElDraggable: () => import("@/components/draggable/index.vue"),
ElFilePreview: () => import("@/components/file-preview/index.vue"),
ElComputedInput: () => import("@/components/computed-input/index.vue"),
ElPopoverSelectV2: () => import("@/components/popover-select-v2/index.vue"),
ElPopoverMultipleSelectV2: () =>
import("@/components/popover-select-v2/multiple.vue"),
ElComputedInputV2: () => import("@/components/computed-input-v2/index.vue"),
ElPopoverTreeSelect: () =>
import("@/components/popover-tree-select/index.vue"),
ButtonHide: () => import("./hide.vue"),
ButtonFreeze: () => import("./freeze.vue"),
IconHide: () => import("./once/hide.vue"),
IconSort: () => import("./once/sort.vue"),
IconFreeze: () => import("./once/freeze.vue"),
IconFilter: () => import("./once/filters.vue"),
},
data() {
const { columns, storageKey } = this.$props;
const localColumns = localStorage.getItem(storageKey);
const innerColumns =
storageKey && localColumns
? JSON.parse(localColumns)
: columns.map(({ item, attr }) => ({
attr,
item: { hidden: true, ...item },
}));
return {
innerColumns: innerColumns,
rowKey: "id",
// 选择
selectData: [],
selectState: false,
// 过滤
filterData: [],
filterState: false,
count: 0,
scrollTop: 0,
resizeHeight: 0,
};
},
computed: {
innerValue: {
get() {
if (this.filterState) {
return this.filterData;
} else if (this.selectState) {
return this.selectData;
} else {
return this.$props.value;
}
},
set(value) {
this.$emit("input", value);
},
},
showColumns: {
get() {
return this.innerColumns.filter(({ item }) => item.hidden);
},
set() {},
},
filterRules: {
get() {
return Object.fromEntries(
this.innerColumns
.filter(({ item }) => item.filter && !!item.filter.length)
.map(({ item }) => [item.key, item.filter])
);
},
set() {},
},
tableHeight: {
get() {
let { height } = this.$props;
return height ? height : this.resizeHeight;
},
set() {},
},
},
watch: {
filterRules: {
handler: function (newValue) {
function multiFilter(array, filters) {
const filterKeys = Object.keys(filters);
// filters all elements passing the criteria
return array.filter((item) => {
// dynamically validate all filter criteria
return filterKeys.every((key) => {
//ignore when the filter is empty Anne
if (!filters[key].length) return true;
return !!~filters[key].indexOf(item[key]);
});
});
}
this.filterState = JSON.stringify(newValue) !== "{}";
this.filterData = multiFilter(this.$props.value, newValue);
},
},
value: {
handler: function (newValue) {
if (this.value.length > 0) {
this.$refs.superUxTable && this.$refs.superUxTable.clearSelection();
}
},
immediate: true,
deep: true,
},
},
directives: {
// 使用局部注册指令的方式
resize: {
// 指令的名称
bind(el, binding) {
// el为绑定的元素,binding为绑定给指令的对象
let width = "",
height = "";
function isReize() {
const style = document.defaultView.getComputedStyle(el);
if (width !== style.width || height !== style.height) {
binding.value(); // 关键
}
width = style.width;
height = style.height;
}
el.__vueSetInterval__ = setInterval(isReize, 300);
},
unbind(el) {
clearInterval(el.__vueSetInterval__);
},
},
},
methods: {
resize() {
this.resizeHeight =
document.getElementsByClassName("el-super-ux-table")[0].offsetHeight -
55;
},
//
onSelectionChange(value) {
this.selectData = value;
this.$emit("row-select", this.selectData);
},
//
onRowClick(row, column, event) {
const { radio, checkbox } = this.$props;
// 单选
if (radio) {
this.$emit("row-select", [row]);
}
// 多选
if (checkbox) {
this.$refs.superUxTable.toggleRowSelection([
this.innerValue.find((item) => item.id === row.id),
]);
}
},
// 宽度
onWidth({ column }) {
this.innerColumns = this.innerColumns.map(({ item, attr }) => ({
attr,
item: {
...item,
width: item.key === column.property ? column.resizeWidth : item.width,
},
}));
if (this.$props.storageKey) {
localStorage.setItem(
this.$props.storageKey,
JSON.stringify(this.innerColumns)
);
}
},
// 隐藏
onHide(prop) {
this.$nextTick(() => {
this.$refs.superUxTable.doLayout();
if (this.$props.storageKey) {
localStorage.setItem(
this.$props.storageKey,
JSON.stringify(this.innerColumns)
);
}
});
},
// 排序
onSort(prop) {
const { key, sort } = prop;
console.log(key, "key", sort, "sort");
this.$nextTick(() => {
this.$refs.superUxTable.sort(key, sort);
this.$refs.superUxTable.doLayout();
if (this.$props.storageKey) {
localStorage.setItem(
this.$props.storageKey,
JSON.stringify(this.innerColumns)
);
}
});
},
// 冻结
onFreeze() {
this.$nextTick(() => {
this.$refs.superUxTable.doLayout();
if (this.$props.storageKey) {
localStorage.setItem(
this.$props.storageKey,
JSON.stringify(this.innerColumns)
);
}
this.count++;
});
},
// 过滤
onFilter() {
this.$nextTick(() => {
this.$refs.superUxTable.doLayout();
if (this.$props.storageKey) {
localStorage.setItem(
this.$props.storageKey,
JSON.stringify(this.innerColumns)
);
}
});
},
onFilters(value) {
const {
item: { key },
attr: { dictName },
} = value;
let dataList = [];
const dict = this.dict.type[dictName];
dataList = Array.from(
new Set(this.innerValue.map((item) => item[key]).filter((item) => item))
).map((item) => ({
text: dictName
? (dict.find((dictItem) => dictItem.value == item) || {}).label
: item,
value: item,
}));
return dataList;
},
// 继承el-table的Method
extendMethod() {
const refMethod = Object.entries(this.$refs["superUxTable"]);
for (const [key, value] of refMethod) {
if (!(key.includes("$") || key.includes("_"))) {
this[key] = value;
}
}
},
getSummaries({ columns, data }) {
const means = []; // 合计
let { firstSummary } = this.$props;
columns.forEach((column, columnIndex) => {
if (!firstSummary && columnIndex === 0) {
means.push("合计");
} else {
const values = data.map((item) => Number(item[column.property]));
let sumColumn = this.showColumns.filter(
({ item, attr }) => attr.isSummary && item.key === column.property
);
// 合计
// if (!values.every(value => isNaN(value))) {
if (sumColumn.length) {
means[columnIndex] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return prev + curr;
} else {
return prev;
}
}, 0);
means[columnIndex] = means[columnIndex].toFixed(2);
} else {
means[columnIndex] = "";
}
}
});
// sums[index] = sums[index] && sums[index].toFixed(2); // 保留2位小数,解决小数合计列
return [means];
},
},
created() {},
mounted() {
this.extendMethod();
},
updated() {
this.$nextTick(() => {
this.$refs.superUxTable.doLayout();
});
},
destroyed() {},
};
script>
<template>
<div class="el-super-ux-table" :key="count" v-resize="resize">
<ux-grid
border
row-key
use-virtual
keep-source
show-overflow
beautify-table
ref="superUxTable"
v-bind="$attrs"
:height="tableHeight"
v-on="$listeners"
:data="innerValue"
:show-summary="showSummary"
:summary-method="getSummaries"
@row-click="onRowClick"
@header-dragend="onWidth"
@selection-change="onSelectionChange"
:header-row-style="{
color: '#515a6e',
}"
style="flex: 1"
>
<ux-table-column
v-if="checkbox"
fixed="left"
width="50"
align="center"
type="checkbox"
resizable
reserve-selection
:column-key="rowKey"
>ux-table-column>
<ux-table-column
v-if="index"
fixed="left"
width="50"
title="序号"
type="index"
align="center"
class="is-index"
resizable
>ux-table-column>
<ux-table-column
v-for="({ item, attr }, index) in showColumns"
:key="item.key + index"
:field="item.key"
:title="item.title"
:fixed="item.fixed ? 'left' : undefined"
:width="item.width || 180"
:sortable="item.sortabled"
resizable
show-overflow
>
<template slot="header" slot-scope="scope">
<template>
<span v-if="item.require" style="color: #ff4949">*span>
<span
:style="{
color:
item.sort ||
item.fixed ||
(item.filter && !!item.filter.length)
? '#1890ff'
: '',
}"
>
{{ item.title }}
span>
<template>
<icon-freeze
v-if="item.fixedabled"
v-model="item.fixed"
@freeze="onFreeze"
>icon-freeze>
<icon-filter
v-if="item.filterabled"
v-model="item.filter"
:filters="onFilters({ item, attr })"
@filter="onFilter"
>icon-filter>
<icon-hide
v-if="item.hiddenabled"
v-model="item.hidden"
@hide="onHide"
>icon-hide>
template>
template>
template>
<template slot-scope="scope">
<slot :name="item.key" v-bind="scope" :item="item" :attr="attr">
<template v-if="attr.is">
<component
v-if="attr.is === 'el-dict-tag'"
v-bind="attr"
:size="$attrs.size"
:value="scope.row[item.key]"
:options="dict.type[attr.dictName]"
>component>
<component
v-else-if="attr.is === 'el-popover-select-v2'"
v-bind="attr"
v-model="scope.row[item.key]"
:title="item.title"
:size="$attrs.size"
:source.sync="scope.row"
>
component>
<component
v-else-if="attr.is === 'el-popover-multiple-select-v2'"
v-bind="attr"
v-model="scope.row[item.key]"
:title="item.title"
:size="$attrs.size"
:source.sync="scope.row"
>
component>
<component
v-else-if="attr.is === 'el-select'"
v-bind="attr"
v-model="scope.row[item.key]"
:size="$attrs.size"
>
<template>
<el-option
v-for="item in dict.type[attr.dictName]"
:key="item.value"
:label="item.label"
:value="item.value"
>
el-option>
template>
component>
<component
v-else
v-bind="attr"
v-model="scope.row[item.key]"
:size="$attrs.size"
style="width: 100%"
>
component
>template>
<template v-else>
<component v-if="attr.formatter" is="span">{{
attr.formatter(scope.row)
}}component>
<component v-else is="span">{{
scope.row[item.key] || "--"
}}component>
template>
slot>
template>
ux-table-column>
<slot>slot>
ux-grid>
<div
style="
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
"
:style="{
height: checkbox || pagination ? '50px' : '0px',
}"
>
<div class="mr-4">
<template v-if="convenitentOperation">
<button-hide v-model="innerColumns" @change="onHide">button-hide>
template>
div>
<pagination
v-if="pagination"
v-show="!selectState"
:total="page.total"
:page.sync="page.pageNum"
:limit.sync="page.pageSize"
@pagination="$emit('pagination', { ...$event })"
style="height: 32px; padding: 0 !important; flex: 1; overflow-x: auto"
/>
div>
div>
template>
<style lang="scss" scoped>
.el-super-ux-table {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
::v-deep.el-super-ux-table .elx-cell {
word-break: keep-all;
white-space: nowrap;
.icon-sort {
display: none;
}
&:hover .icon-sort {
display: inline-block;
}
.icon-freeze {
display: none;
}
&:hover .icon-freeze {
display: inline-block;
}
.icon-filter {
display: none;
}
&:hover .icon-filter {
display: inline-block;
}
.icon-hide {
display: none;
}
&:hover .icon-hide {
display: inline-block;
}
.elx-cell--sort {
display: none;
}
&:hover .elx-cell--sort {
display: inline-block;
}
}
::v-deep.uxbeautifyTableClass
.elx-header--column
.elx-resizable.is--line:before {
height: 100%;
background-color: #dfe6ec;
}
style>
<el-super-ux-table
index
v-model="materialInfo[item.key]"
:dict="dict"
:ref="tabName"
:columns="columns"
:size="$attrs.size"
:height="420"
>
<template slot="drug" slot-scope="scope">
<component
v-bind="scope.attr"
v-model="scope.row[scope.item.key]"
:size="$attrs.size"
:source.sync="scope.row"
:disabled="!(scope.row.medicineMaterial === '0')"
>
<el-option
v-for="item in dict.type[scope.attr.dictName]"
:key="item.value"
:label="item.label"
:value="item.value"
>
el-option>
component>
template>
<template slot="registrationNo" slot-scope="scope">
<component
v-bind="scope.attr"
v-model="scope.row[scope.item.key]"
:size="$attrs.size"
:source.sync="scope.row"
:disabled="!(scope.row.medicineMaterial === '0')"
>
component>
template>
<ux-table-column
fixed="right"
title="操作"
width="120"
align="center"
>
<template slot="header" slot-scope="scope">
<el-button
type="text"
:size="$attrs.size"
@click="useRowAdd(tabName)"
>
增行
el-button>
template>
<template slot-scope="scope">
<el-button
type="text"
:size="$attrs.size"
@click.native.prevent="useRowRemove(tabName, scope)"
>
删除
el-button>
<AmendantRecord
v-if="
tabName === 'materialBasic' &&
addType === 'edit' &&
scope.row.id
"
v-model="scope.row"
>AmendantRecord>
template>
ux-table-column>
el-super-ux-table>