组件二次封装-移动端分页加载

diy-vant-list 此组件可使用 van-list 的任意 api,van-pull-refresh 中的部分 api 与 List 组件命名冲突,增加前缀 [pull-]。 如: [pull-loading-text][pull-loading]
学习本文档可参考 vant 官方文档

引入(全局注入组件)

1
2
3
import diyVantList from "@/components/diy-vant-list";

Vue.component("diyVantList", diyVantList);

组件使用

1
2
3
4
5
<diy-vant-list postUrl="">
<template slot="content" slot-scope="{listItem}">
<div>{{ listItem }}</div>
</template>
</diy-vant-list>

API

Props

主题

参数说明类型默认值
theme是否定制其他风格的空状态、列表请求失败图片以及列表加载完成样式Stringnormal

请求接口

参数说明类型默认值
postUrl请求接口路径(必传)String-
apiData请求参数(除了 pageNum,和 pageSize,还需向接口传递的参数)Object-
startName当前页码的属性名( pageNum/pageNo)StringpageNum
totalPageName接口返回总页数字段名StringtotalPageCount
listName接口返回 list 的位置Booleandata.list
pageSize每页条数(默认 10 条)String,Number10

列表相关

参数说明类型默认值
changeButton是否展示切换加载按钮(测试使用)Booleanfalse
condition展示列表需要的额外条件,如列表只展示排名大于 3 的数据Booleantrue
noPaging是否不需要分页加载数据Stringfalse
autoLoad是否需要上拉加载Booleantrue(当 autoLoad 为 false 时,列表为点击加载更多)
immediate-check是否在初始化时立即执行滚动位置检查Booleantrue
loading-text加载过程中的提示文案String请求失败,点击重新加载
finished-text列表加载完成时显示的文案String没有更多了
readmore-text点击查看更能多显示的文案String查看更多
error-text加载失败后的提示文案String请求失败,点击重新加载
list-item-class订制每条数据样式类名String-
offset滚动条与底部距离小于 offset 时触发 load 事件String, Number300
minHeight为了避免列表数据过少,下拉刷新时列表区域被遮挡String, Number0
direction滚动触发加载的方向,可选值为 upStringdown

list-item-class 的使用

1
2
3
4
5
<diy-vant-list postUrl="" list-item-class="content-item">
<template slot="content" slot-scope="{listItem}">
<div>{{ listItem }}</div>
</template>
</diy-vant-list>
1
2
3
4
5
6
7
8
/deep/ .content-item {
width: 351px;
height: 124px;
background: rgba(255, 255, 255, 1);
box-shadow: 0px 1px 8px 0px rgba(210, 216, 241, 0.6);
border-radius: 2px;
margin: 0 auto 8px;
}

下拉加载

参数说明类型默认值
pullRefresh是否需要开启下拉刷新Booleanfalse
pullingText下拉过程提示文案String下拉即可刷新…
loosingText下拉释放过程提示文案String释放即可刷新…
pull-loading-text下拉加载过程提示文案String加载中…
success-text下拉刷新成功提示文案String-
success-duration刷新成功提示展示时长(ms)String, Number500
animation-duration动画时长(ms)String, Number300
head-height(v2.4.2)下拉顶部内容高度String, Number50

Slots 插槽

名称说明
内容填充
content自定义列表每一项
列表相关
loading自定义底部加载中提示
error自定义加载失败后的提示内容
finished自定义加载完成后的提示内容
readmore手动点击加载下一页提示内容
empty列表数据为空时显示内容
下拉加载
default下拉自定义内容
normal非下拉状态时顶部内容
pulling下拉过程中顶部内容
loosing下拉释放提示
pull-loading下拉加载提示 [pull-loading] 用以区分下拉刷新和列表加载 loading
success下拉成功提示

Events

当页面有筛选条件或者 tab 切换需要更新列表数据时,需要主动调用 reset 方法重置数据

1
2
3
4
5
<diy-vant-list postUrl="" ref="diyVantList">
<template slot="content" slot-scope="{listItem}">
<div>{{ listItem }}</div>
</template>
</diy-vant-list>
1
this.$refs.diyVantList.reset(); // 重置数据

父组件需要获取数据长度等相关的属性时,可调用 setData 获取列表数据

1
2
3
4
5
<diy-vant-list postUrl="" @setData="setData">
<template slot="content" slot-scope="{listItem}">
<div>{{ listItem }}</div>
</template>
</diy-vant-list>
1
2
3
setData(listData, resData) {
this.dataListlength = listData.length;
}

当接口请求发生错误时,父组件可调用 error 捕捉错误信息

1
2
3
4
5
<diy-vant-list postUrl="" @error="error">
<template slot="content" slot-scope="{listItem}">
<div>{{ listItem }}</div>
</template>
</diy-vant-list>
1
2
3
error(error) {
console.log(error)
}

组件完整代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<div class="diy-vant-list" :class="theme">
<template v-if="changeButton">
<van-button type="primary" @click="autoLoad = !autoLoad; reset();">
切换加载模式: {{ autoLoad ? "自动" : "手动" }}
</van-button>
<van-button type="primary" @click="pullRefresh = !pullRefresh; reset();">
是否下拉刷新: {{ pullRefresh ? "是" : "否" }}
</van-button>
</template>
<template v-if="listData && listData.length && condition">
<van-pull-refresh
v-model="refreshing"
:disabled="!pullRefresh"
:pulling-text="pullingText"
:loosing-text="loosingText"
:loading-text="pullLoadingText"
:success-text="successText"
:success-duration="successDuration"
:animation-duration="animationDuration"
:head-height="headHeight"
@refresh="reset"
>
<!-- 下拉自定义内容 -->
<slot slot="default" name="default" />
<!-- 非下拉状态时顶部内容 -->
<slot slot="normal" name="normal" />
<!-- 下拉过程中顶部内容 -->
<slot slot="pulling" name="pulling" />
<!-- 下拉释放提示 -->
<slot slot="loosing" name="loosing" />
<!-- 下拉加载提示 [pull-loading]用以区分下拉刷新和列表加载loading -->
<slot slot="loading" name="pull-loading" />
<!-- 下拉成功提示 -->
<slot slot="success" name="success" />
<van-list
v-if="autoLoad && !noPaging"
v-model="listLoading"
v-bind="$attrs"
:finished="listFinished"
:loading-text="loadingText"
:finished-text="finishedText"
:error.sync="error"
:error-text="errorText"
:direction="direction"
:immediate-check="immediateCheck"
:offset="offset"
class="list-container"
:style="{ minHeight: minHeight + 'px' }"
@load="getListData"
>
<div
v-for="(item, index) in listData"
:key="index"
:class="listItemClass"
>
<slot slot name="content" :listItem="item" :index="index" />
</div>
<!-- 自定义底部加载中提示 -->
<slot slot="loading" name="loading" />
<!-- 自定义加载失败后的提示文案 -->
<slot slot="error" name="error" />
<!-- 自定义加载完成后的提示文案 -->
<slot slot="finished" name="finished">
<van-divider v-if="theme === 'lucky'" class="finished">
我是有底线的
</van-divider>
<template v-else>{{ finishedText }}</template>
</slot>
</van-list>
<div
v-else
class="list-container"
:style="{ minHeight: minHeight + 'px' }"
>
<div
v-for="(item, index) in listData"
:key="index"
:class="listItemClass"
>
<slot name="content" :listItem="item" :index="index" />
</div>
<div
v-if="!pageFirstLoading && listData.length > 0 && !noPaging"
class="readmore"
:class="{ nomore: !pageLoading && totalPageCount <= pageNo }"
@click="getListData"
>
<template v-if="pageLoading">
<slot name="loading">{{ loadingText }}</slot>
</template>
<template v-else-if="totalPageCount > pageNo">
<slot name="readmore">{{ readmoreText }}</slot>
</template>
<slot v-else name="finished">
<van-divider v-if="theme === 'lucky'" class="finished">
我是有底线的
</van-divider>
<template v-else>{{ finishedText }}</template>
</slot>
</div>
</div>
</van-pull-refresh>
</template>
<div v-else-if="pageFirstLoading" class="loading">
<van-loading />
</div>
<div v-else-if="networdError" class="network-error">
<slot name="error">
<img class="error-img" src="@/assets/images/common/network-error.png" />
<div class="error-mes">数据加载失败,请刷新重试</div>
<div class="refresh-btn" @click="reset">刷新</div>
</slot>
</div>
<div v-else class="no-data">
<slot name="empty">
<img class="empty-img" src="@/assets/images/common/empty.png" />
<div>
<slot name="empty-mes">暂无数据</slot>
</div>
</slot>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
export default {
props: {
condition: {
type: Boolean,
default: true
},
noPaging: {
type: Boolean,
default: false
},
// 是否展示切换加载按钮(测试使用)
changeButton: {
type: Boolean,
default: false
},
// 定制游戏主题lucky
theme: {
type: String,
default: "normal"
},
// 请求接口路径(必传)
postUrl: {
type: String,
required: true
},
/**
* 请求参数(除了pageNum,和pageSize,还需向接口传递的参数)
* !!!注:当父组件中的 apiData 不是直接绑定时,必须在父组件的 created 中初始化 apiData,以便子组件可以及时取到请求参数
* 扩展知识:vue父子组件生命周期加载顺序:父组件beforeCreated ->父组件created ->父组件beforeMounted ->子组件beforeCreated ->子组件created ->子组件beforeMounted ->子组件mounted -> 父组件mounted
*/
apiData: {
type: Object,
default: () => {
return {};
}
},
// 当前页码的属性名(例如pageNum/pageNo,默认为pageNum)
startName: {
type: String,
default: "pageNum"
},
// 每页条数(默认10条)
pageSize: {
type: [String, Number],
default: 10
},
// 为了避免列表数据过少,下拉刷新时列表区域被遮挡
minHeight: { type: [String, Number], default: 0 },
// 是否需要上拉加载(默认true,当 autoLoad 为 false 时,列表为点击加载)
autoLoad: {
type: Boolean,
default: true
},
// 总页数
totalPageName: {
type: String,
default: "data.totalPageCount"
},
// 滚动条与底部距离小于 offset 时触发 load 事件
offset: {
type: [String, Number],
default: 300
},
// 加载过程中的提示文案
loadingText: {
type: String,
default: "加载中..."
},
// 列表加载完成时显示的文案
finishedText: {
type: String,
default: "没有更多了"
},
// 点击查看更能多显示的文案
readmoreText: {
type: String,
default: "查看更多"
},
// 加载失败后的提示文案
errorText: {
type: String,
default: "请求失败,点击重新加载"
},
// 是否在初始化时立即执行滚动位置检查
immediateCheck: {
type: Boolean,
default: true
},
// 滚动触发加载的方向,可选值为 up
direction: {
type: String,
default: "down"
},
// 订制每条数据样式类名 可传 list-item-class
listItemClass: {
type: String,
default: ""
},
// 是否需要下拉刷新
pullRefresh: {
type: Boolean,
default: false
},
// 刷新成功提示展示时长(ms)
successDuration: { type: [String, Number], default: 500 },
// 动画时长(ms)
animationDuration: { type: [String, Number], default: 300 },
// 顶部内容高度
headHeight: { type: [String, Number], default: 50 },
pullingText: { type: String, default: "下拉即可刷新..." }, // 下拉过程提示文案
loosingText: { type: String, default: "释放即可刷新..." }, // 下拉释放过程提示文案
pullLoadingText: { type: String, default: "加载中..." }, // 下拉加载过程提示文案
successText: { type: String, default: "刷新成功" }, // 下拉刷新成功提示文案
listName: { type: String, default: "data.list" }
},
data() {
return {
listData: [], // 列表数据
listLoading: false, // 列表加载
listFinished: false, // 列表加载完成
pageFirstLoading: true, // 页面首次加载
pageLoading: false, // 防重复请求
rowCount: "", // 总条数
totalPageCount: 1, // 总页数
pageNo: 0, // 当前页数
refreshing: false, // 下拉刷新状态
error: false, // 列表加载失败
networdError: false // 接口请求失败,报错
};
},
created() {
this.getListData();
},
methods: {
/**
* 列表重置(在请求参数 apiData 有变化时,父组件需要主动调用此方法充值列表)
*/
reset() {
if (!this.refreshing) {
this.listData = [];
this.pageFirstLoading = true;
}
this.pageNo = 0;
this.totalPageCount = 1;
this.listFinished = false;
this.networdError = false;
this.pageLoading = false;
this.listLoading = true; // 将 loading 设置为 true,表示处于加载状态
this.getListData();
},
splitPath(str = "", obj = {}) {
const strArr = str.split(".");
let _obj = { ...obj };
strArr.forEach(item => {
_obj = _obj[item];
});
return _obj;
},
/**
* 请求列表数据(分页)
* @[startName] {Number} 当前页码
* @pageSize {Number} 显示条数
* 此方法向父组件
* 1. $emit("setData", data); 根据业务需求,父组件可调用 setData 获取列表数据
* 2. $emit("error", res); 根据业务需求,父组件可调用 error 处理请求错误
* @return: Object [data]
*/
getListData() {
if (this.listFinished || this.pageLoading) {
return false;
}
// 判断页码
this.pageNo++;
// 如果当前页数 > 总页数,则已经没有数据 停止加载
if (this.pageNo > this.totalPageCount) {
this.listFinished = true;
this.pageLoading = false;
return false;
}
const extendsParams = { ...this.apiData } || {};
const data = {
[this.startName]: this.pageNo,
pageSize: this.pageSize,
...extendsParams
};
const url = this.postUrl;
const pageNo = data[this.startName];

this.pageLoading = true;
this.$request.post({
url,
data,
done: res => {
this.pageFirstLoading = false;
this.pageLoading = false;
this.listLoading = false;
if (this.refreshing) {
// 接口请求成功,停止刷新
this.refreshing = false;
}
if (res.code === 0) {
const resData = res.data;
const listData = this.splitPath(this.listName, res);
// 如果当前请求返回list为空,list停止加载 listFinished 为 true
if (pageNo > this.totalPageCount || listData.length == 0) {
this.listFinished = true;
}
// pageNo 当前页数 pageSize 每页条数 rowCount 总条数 totalPageCount 总页数
this.totalPageCount = this.splitPath(this.totalPageName, res);
this.rowCount = resData.rowCount;
// 判断是否为当前请求的数据(url是否一致|扩展参数是否一致)
let isCurrentData =
url === this.postUrl &&
JSON.stringify(extendsParams) === JSON.stringify(this.apiData);
if (isCurrentData) {
/* 上拉一下 添加一次数据 */
if (pageNo === 1) {
this.listData = listData;
} else {
this.listData = this.listData.concat(listData);
}

this.$forceUpdate();
this.$emit("setData", this.listData, resData); // 父组件可调用 setData 获取列表数据
}
} else {
if (pageNo === 1) {
this.networdError = true;
this.pageLoading = false;
this.pageFirstLoading = false;
this.listLoading = false;
} else {
this.networdError = false;
this.error = true;
}
this.$emit("error", res);
}
},
fail: () => {
this.pageLoading = false;
this.pageFirstLoading = false;
this.listLoading = false;
if (this.pageNo === 1) {
this.networdError = true;
this.error = false;
} else {
this.networdError = false;
this.error = true;
}
}
});
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
.diy-vant-list {
position: relative;
min-height: 300px;
/deep/ .list-container {
&::after {
content: "";
display: table;
}
&::before {
content: "";
display: table;
}
}
.loading {
position: absolute;
left: 50%;
top: 20%;
transform: translateX(-50%);
width: 100%;
text-align: center;
}

.readmore {
color: #517cf1;
font-size: 12px;
text-align: center;
margin: 20px 0px;
&.nomore {
color: #a2a2a2;
}
}

.network-error {
width: 100%;
text-align: center;
position: absolute;
left: 50%;
top: 20%;
transform: translateX(-50%);
.error-img {
width: 138px;
}
.error-mes {
font-size: 16px;
color: #737373;
line-height: 21px;
}
.refresh-btn {
width: 114px;
line-height: 34px;
background: rgba(247, 139, 68, 1);
border-radius: 20px;
font-size: 16px;
font-weight: bold;
color: #ffffff;
margin: 16px auto;
}
}

.no-data {
width: 100%;
text-align: center;
position: absolute;
left: 50%;
top: 40%;
transform: translateX(-50%);
font-size: 13px;
color: #666666;
line-height: 19px;
.empty-img {
width: 70px;
margin: 0 auto 12px;
}
}
}