本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1、前言
- 列表查询
Table用于展示多条结构类似的数据载体,经常出现在统计、排序、筛选、对比等场景中,对此已非常熟悉,对于一维表格通常都是行和列组成,每一行是一个实体,每一列是实体相关的一个属性。(在数据分析中常用矩阵向量来建模。)一般由于实体数据都比较多,出于查询性能和渲染压力等方面考虑,往往会采取分页(携带当前页和每页显示实体数)查询。
- 详情查询
实体属性太多,列表属性可展示有限(太多辣眼睛),那么解决矛盾的办法就是,通常增加点击单个实体,侧面板会弹出实体相关更详(rong)细(yu)的信息,但是矛盾又出现了,每次查看实体详情还得关了面板,回到列表再点下一条。怎么省去这些繁琐的操作?借鉴列表查询的交互能否在侧面板实现下一页或者上一页的功能呢?我们尝试借鉴mysql游标思路实现一下。
2、简图
Tips:
1、实体详情页 “->” 可以查看列表中的下一个实体详情;“<-” 可以查看列表中的上一个实体详情。
2、如果下一页或者上一页超出列表边界,下一页会自 动查接口翻页,而列表保持不变。
3、分析
整体以一般到特殊再到边界分析(前提假设:数据默认都以第二页,每页数据10条为例)
3.1 常规操作
初始化首先定位游标位置,在游标指针指向正确后,当有下一页(或者上一页)操作时,先移动游标位置,再根据游标指针获取队列中的实体属性。
3.2 特殊操作
假设实体总条数28,恰好游标已移动到队列的尾部也就是20的位置上,此时队列中只有20条数据,那接下来需要先扩容队列数据(请求第三页的数据);同理游标位置出于11的位置,上一页时操作同下一页先扩容再移动游标。如下图:
(往下翻操作)
(往上翻操作)
3.3 边界操作
作为一个洁癖程序猿,在交互细节也得考虑下。假设实体总数28,往下翻页,刚好游标移动到队列的尾部(注意此时是所有数据都已拉回)那么下一页的按钮应该禁用。一点是详情翻页用户并不像列表翻页很清楚某个实体数据在的位置(哪一页的哪一条,前面和后面各有多少条一目了然),然而详情翻页用户只清楚当前这一条数据,并不清楚上下位置关系,此时禁用会给用户很好的归属感。另外一点可以减少不必要的请求和详情重绘。细品,还真是那个味~
4、整体交互流程图
5、Code
基于整体分析,思路比较清晰了,接下来我们开始codeing。
5.1 Core
在js中我们使用数组索引模拟游标,使用数组的concat 和 unshift 来模拟队列。游标配合队列需要一些状态和方法,我们封装成一个类里。
/**
* @file 详情翻页
* @return 当前实体信息和翻页检查结果
* @params id 用来定位游标初始化位置
* @param innerPageCfg 初始化分页数据
*/
export default class PaginationSet {
constructor(props) {
this.values = [];
this.currIndex = 0; // 模拟游标
this.totalCount = 0;
this.currPage = 1;
this.pageSize = 20;
this.init(props);
}
init(props) {
const {
sourceData,
innerPageCfg
} = props;
this.initInnerPageCfg(innerPageCfg);
this.initPotCustIdList(sourceData);
}
filterId(sourceData) {
return sourceData.map(item => item.potCustId);
}
concat(sourceData, page) {
const potCustIdList = this.filterId(sourceData);
const values = potCustIdList.map(id => {
return {
currPage: page,
potCustId: id,
pageSize: this.pageSize,
totalCount: this.totalCount
};
});
this.values = this.values.concat(values);
}
unshift(sourceData, page) {
this.currIndex = sourceData.length - 1; // 修正当前索引
const potCustIdList = this.filterId(sourceData);
const values = potCustIdList.map(id => {
return {
currPage: page,
potCustId: id,
pageSize: this.pageSize,
totalCount: this.totalCount
};
});
this.values = values.concat(this.values);
}
has(val) {
return this.values.includes(val);
}
add(val) {
if (!this.has(val)) {
this.values.push(val);
}
}
clear() {
this.values = [];
this.currIndex = 0;
}
initInnerPageCfg(innerPageCfg) {
const {
currPage,
totalCount,
pageSize
} = innerPageCfg;
this.totalCount = totalCount || 0;
this.currPage = currPage || 1;
this.pageSize = pageSize || 20;
}
initPotCustIdList(sourceData) {
const potCustIdList = this.filterId(sourceData);
this.values = potCustIdList.map(id => {
return {
currPage: this.currPage,
potCustId: id,
pageSize: this.pageSize,
totalCount: this.totalCount
};
});
}
findCurrindex(potCustId) {
this.values.forEach((item, i) => {
if (item.potCustId === potCustId) {
this.currIndex = i;
}
});
}
hasNextItem() {
const current = this.values[this.currIndex];
return (this.currIndex === this.values.length - 1) && current.currPage * current.pageSize >= current.totalCount;
}
hasPrevItem() {
const current = this.values[this.currIndex];
return this.currIndex === 0 && current.currPage === 1;
}
enable() {
return this.hasPrevItem() || this.hasNextItem();
}
next() {
if (this.currIndex < this.values.length) {
this.currIndex++;
}
let temp = {
value: this.values[this.currIndex] && this.values[this.currIndex].potCustId,
done: this.currIndex >= this.values.length, // 翻页检查器
pageInfo: {
page: this.values[this.values.length - 1].currPage + 1,
pageSize: this.values[this.values.length - 1].pageSize
},
tail: this.hasNextItem()
};
return temp;
}
prev() {
if (this.currIndex >= 0) {
this.currIndex--;
}
let temp = {
value: this.values[this.currIndex] && this.values[this.currIndex].potCustId,
done: this.currIndex < 0, // 翻页检查器
pageInfo: {
page: this.values[0].currPage - 1,
pageSize: this.values[0].pageSize
},
tail: this.hasPrevItem()
};
return temp;
}
}
5.2 pagination.vue
<template>
<span class="pagination-wrapper">
<span :class="[prev && 'forbidde']">
<i class="el-icon-arrow-left"
:title="prev ? '暂无上一条' : '上一条'"
@click.stop="flipPagination('prev')"
>
</i>
</span>
<span :class="[next && 'forbidde']">
<i class="el-icon-arrow-right"
:title="next ? '暂无下一条' : '下一条'"
@click.stop="flipPagination('next')"
>
</i>
</span>
</span>
</template>
<script>
export default {
data() {
return {
next: false,
prev: false,
}
}
methods: {
initPagination() {
// this.detailpageInfo 为父组件传来上面👆🏻PaginationSet的实例
this.detailpageInfo.findCurrindex(this.potCustId); // 定位游标
this.isTail();
},
isTail() {
// 翻页检查
this.prev = this.detailpageInfo.hasPrevItem();
this.next = this.detailpageInfo.hasNextItem();
},
flipPagination(type) {
let temp = this.detailpageInfo[type](); // 返回的实体信息和翻页检查结果
if (temp.done) { // 扩容
const pageInfo = temp.pageInfo;
this.getData(type, pageInfo)
} else {
this.$emit('更新详情页', temp);
}
},
getData() {
new Promise().then(res => {
type === 'next' ?
this.detailpageInfo.concat(res.dataList, res.page.currPage) :this.detailpageInfo.unshift(res.dataList, res.page.currPage);
this.$emit('更新详情页', temp);
})
}
}
}
</script>
5.3 list.vue
最外层组件,mounted以后等list数据回来初始化PaginationSet,传给detail组件。
this.detailpageInfo = new PaginationSet({
sourceData: dataList,
innerPageCfg: pageCfg
});
6、总结
1、外层列表翻页每次都会重新实例化PaginationSet,可以保证队列里的游标起始位置总是正确的。
2、PaginationSet中队列翻页innerPageCfg配置总是和外层翻页配置同步,可以保证内外展现顺序表现一致。
3、如果不关闭详情页,队列其实起到了数据缓存的作用,提升用户体验,减少服务器压力。