一、起因
1、需求: 由于業(yè)務(wù)需求在頁面一次性展示較多數(shù)據(jù),不低于上千,但是每條數(shù)據(jù)涉及樣式較多,數(shù)據(jù)渲染過多就會導致頁面卡頓
2、滿足: 大量數(shù)據(jù)加載;表格功能:列顯隱、列順序調(diào)整、固定、篩選、排序;表格調(diào)整存儲本地
3、技術(shù)框架: 若依、Element UI、vue2
二、umy-ui
1、umy-ui庫中的table表格組件,它不造輪子。它改造了element-ui等等庫的表格組件。只為了免費解決前端小伙伴的問題。
2、用前須知(這是關(guān)于表格的須知,你應(yīng)該認真讀完下面的內(nèi)容)
1. 表格解決卡頓問題,那么虛擬表格原理呢大概就是: 減少對DOM節(jié)點的渲染,通過滾動函數(shù)節(jié)流實現(xiàn)滾動后事件來動態(tài)渲染數(shù)據(jù)
2. 基礎(chǔ)表格其實就是element的表格的升級版,修改了ele的表格bug(如果你想使用個普通表格你無需安裝其他庫,就使用這個表格即可),你可以發(fā)現(xiàn)基礎(chǔ)表格里面的示例沒有配置:use-virtual 這個屬性。
3 基礎(chǔ)表格沒有使用use-virtual屬性,代表表格數(shù)據(jù)不多,只想要一個普通的表格。如果你表格卡。請你關(guān)注下虛擬表格部分。
4. 使用u-table 開啟use-virtual虛擬可以支持微小的合并行|列 如2列 2行,支持多級頭, 超過2行2列可能布局錯亂,因為虛擬滾動的原理導致某些節(jié)點并未渲染。
4.5 使用u-table 開啟use-virtual不支持開展行,如果需要展開行,你是要虛擬表格部分的ux展開行!
5. u-table不支持展開行,需要展開行使用ux-grid
6. ux-grid解決列多 行多導致卡的情況, u-table解決行多的情況,不解決列多的情況(如你的列超過70+,你可能就需要使用ux-grid了,因為此時你需要把列也虛擬)
7. 重點:虛擬表格集成了基礎(chǔ)表格的東西(如屬性/方法/事件)!
8. 虛擬表格在本文檔中呢, 意思就是解決了數(shù)據(jù)量多導致卡頓的情況! 基礎(chǔ)表格在文檔中呢,意思就是升級版的el-table(但是沒解決數(shù)據(jù)多卡的情況)!
9. 編輯型表格呢,是解決那種表格單元帶有輸入框或者選擇時間等等的情況,而導致卡頓的場景!意思就是表格單元格具有一定的操作,單元格有自定義組件或者UI庫組件等等
10. 有了表格,怎么導出表格數(shù)據(jù)為excel并且?guī)邮侥?,[請點擊](https://github.com/livelyPeng/pl-export-excel)
三、安裝引入
1.安裝
推薦使用 npm 的方式安裝,它能更好地和 webpack 打包工具配合使用
npm install umy-ui
2.引入
main.js文章來源:http://www.zghlxwxcb.cn/news/detail-759893.html
// 引入umy-ui
import UmyUi from 'umy-ui'
Vue.use(UmyUi);
三、封裝
以下代碼是基于若依框架封裝的主代碼,其余見附帶資源中,對應(yīng)表格中輸入或展示形式可自行封裝:文章來源地址http://www.zghlxwxcb.cn/news/detail-759893.html
<script>
export default {
name: "SuperUxTable",
props: {
// 數(shù)據(jù)
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(); // 關(guān)鍵
}
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)
);
}
});
},
// 凍結(jié)
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位小數(shù),解決小數(shù)合計列
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-sort
v-if="item.sortabled"
v-model="item.sort"
@sort="onSort(item)"
></icon-sort> -->
<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>
<!-- </el-table> -->
</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>
到了這里,關(guān)于Vue2虛擬列表,umy-ui封裝的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!