├── app.js
├── pages
├── index
│ ├── index.wxss
│ ├── index.wxml
│ └── index.js
└── myCoupon
│ ├── myCoupon.json
│ ├── myCoupon.wxml
│ ├── myCoupon.wxss
│ └── myCoupon.js
├── assets
└── image
│ ├── coupon_item_bg.png
│ ├── coupon_item_icon.png
│ ├── coupon_item_icon_no.png
│ ├── coupon_item_icon_used.png
│ └── coupon_item_icon_default.png
├── app.wxss
├── app.json
├── README.md
├── utils
└── util.js
├── project.config.json
└── LICENSE
/app.js:
--------------------------------------------------------------------------------
1 | //app.js
--------------------------------------------------------------------------------
/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | /**index.wxss**/
--------------------------------------------------------------------------------
/pages/myCoupon/myCoupon.json:
--------------------------------------------------------------------------------
1 | {
2 | "onReachBottomDistance": 0,
3 | "navigationBarTitleText": "我的优惠券"
4 | }
--------------------------------------------------------------------------------
/assets/image/coupon_item_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhipingYang/WX_MultiTabList/HEAD/assets/image/coupon_item_bg.png
--------------------------------------------------------------------------------
/assets/image/coupon_item_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhipingYang/WX_MultiTabList/HEAD/assets/image/coupon_item_icon.png
--------------------------------------------------------------------------------
/assets/image/coupon_item_icon_no.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhipingYang/WX_MultiTabList/HEAD/assets/image/coupon_item_icon_no.png
--------------------------------------------------------------------------------
/assets/image/coupon_item_icon_used.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhipingYang/WX_MultiTabList/HEAD/assets/image/coupon_item_icon_used.png
--------------------------------------------------------------------------------
/assets/image/coupon_item_icon_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhipingYang/WX_MultiTabList/HEAD/assets/image/coupon_item_icon_default.png
--------------------------------------------------------------------------------
/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{motto}}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app.wxss:
--------------------------------------------------------------------------------
1 | /**app.wxss**/
2 | .container {
3 | height: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: space-between;
8 | padding: 200rpx 0;
9 | box-sizing: border-box;
10 | }
11 |
--------------------------------------------------------------------------------
/pages/index/index.js:
--------------------------------------------------------------------------------
1 | //index.js
2 | //获取应用实例
3 | const app = getApp()
4 |
5 | Page({
6 | data: {
7 | motto: '点击查看',
8 | },
9 | //事件处理函数
10 | goto_multi_list(e) {
11 | wx.navigateTo({
12 | url: '../myCoupon/myCoupon',
13 | })
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages":[
3 | "pages/index/index",
4 | "pages/myCoupon/myCoupon"
5 | ],
6 | "window":{
7 | "backgroundTextStyle":"light",
8 | "navigationBarBackgroundColor": "#fff",
9 | "navigationBarTitleText": "WeChat",
10 | "navigationBarTextStyle":"black"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MultiTabList
2 |
3 | 微信小程序,多个Tab列表的上下拉刷新方案
4 |
5 | > 由于顶部多Tab视图,原生的下拉刷新不适合(用小程序原生下拉交互会很奇怪,更像是全局刷新而不是当前list刷新)
6 |
7 | 
8 |
9 | ### 支持功能
10 |
11 | - [x] 等间距Tabs展示
12 | - [ ] 顶部多个Tab可滚动展示
13 | - [x] 左右滑动手势切换List
14 | - [x] 网络错误、数据空白占位提示
15 | - [x] 刷新 & 分页懒加载
16 | - [x] 上拉刷新(三个state)
17 |
--------------------------------------------------------------------------------
/utils/util.js:
--------------------------------------------------------------------------------
1 | const formatTime = date => {
2 | const year = date.getFullYear()
3 | const month = date.getMonth() + 1
4 | const day = date.getDate()
5 | const hour = date.getHours()
6 | const minute = date.getMinutes()
7 | const second = date.getSeconds()
8 |
9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
10 | }
11 |
12 | const formatNumber = n => {
13 | n = n.toString()
14 | return n[1] ? n : '0' + n
15 | }
16 |
17 | module.exports = {
18 | formatTime: formatTime
19 | }
20 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件。",
3 | "setting": {
4 | "urlCheck": true,
5 | "es6": true,
6 | "postcss": true,
7 | "minified": true,
8 | "newFeature": true
9 | },
10 | "compileType": "miniprogram",
11 | "libVersion": "1.9.94",
12 | "appid": "touristappid",
13 | "projectname": "MultiTapList",
14 | "condition": {
15 | "search": {
16 | "current": -1,
17 | "list": []
18 | },
19 | "conversation": {
20 | "current": -1,
21 | "list": []
22 | },
23 | "plugin": {
24 | "current": -1,
25 | "list": []
26 | },
27 | "game": {
28 | "currentL": -1,
29 | "list": []
30 | },
31 | "miniprogram": {
32 | "current": 0,
33 | "list": [
34 | {
35 | "id": -1,
36 | "name": "list",
37 | "pathName": "pages/myCoupon/myCoupon"
38 | }
39 | ]
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 穷端-杨
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pages/myCoupon/myCoupon.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 | {{item.title}}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{item.title}}
28 | 到期时间:{{item.date}}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{item.price}}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{tabs[tab_index].placeholder}}
45 |
46 |
47 |
48 | 上拉加载更多
49 | 正在载入更多...
50 | 已加载全部
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/pages/myCoupon/myCoupon.wxss:
--------------------------------------------------------------------------------
1 | .stv-container {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | display: flex;
8 | flex-direction: column;
9 | }
10 | .withAnimate {
11 | transition: all 100ms ease;
12 | -webkit-transform: translate3d(0, 0, 0);
13 | -moz-transform: translate3d(0, 0, 0);
14 | -ms-transform: translate3d(0, 0, 0);
15 | transform: translate3d(0, 0, 0);
16 | -webkit-backface-visibility: hidden;
17 | -moz-backface-visibility: hidden;
18 | -ms-backface-visibility: hidden;
19 | backface-visibility: hidden;
20 | -webkit-perspective: 1000;
21 | -moz-perspective: 1000;
22 | -ms-perspective: 1000;
23 | perspective: 1000;
24 | }
25 | .stv-container .tab-bar {
26 | position: relative;
27 | display: flex;
28 | font-size: 30rpx;
29 | color: #666666;
30 | background-color: white;
31 | border-top: 0.5rpx #dcdcdc solid;
32 | border-bottom: 0.5rpx #dcdcdc solid;
33 | }
34 | .stv-container .tab-bar .tab-active {
35 | color: #2d80ff;
36 | }
37 | .stv-container .tab-bar .tab {
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | padding-top: 16rpx;
42 | padding-bottom: 20rpx;
43 | }
44 | .stv-container .tab-bar .under-line {
45 | position: absolute;
46 | bottom: 0;
47 | height: 6rpx;
48 | background-color: #2d80ff;
49 | }
50 | .stv-container .scroll-view {
51 | position: relative;
52 | width: 100%;
53 | flex: 1;
54 | background: #e7eaef;
55 | }
56 | .stv-container .scroll-view .scroll-view-wrapper {
57 | position: absolute;
58 | top: 0;
59 | bottom: 0;
60 | display: flex;
61 | }
62 | .stv-container .scroll-view .scroll-view-wrapper .one-scene {
63 | height: 100%;
64 | }
65 |
66 | /* itemView */
67 | .item_view {
68 | border-bottom: 1rpx solid #dcdcdc;
69 | margin: 10rpx 40rpx 0rpx 40rpx;
70 | height: 120rpx;
71 | display: flex;
72 | align-items: center;
73 | background-color: white;
74 | }
75 | .item_view .item_back_image {
76 | margin-left: -20rpx;
77 | height: 100%;
78 | width: 20rpx;
79 | }
80 | .item_view .item_image {
81 | padding-left: 20rpx;
82 | width: 80rpx;
83 | height: 80rpx;
84 | }
85 | .item_view .item_flex-clomn {
86 | padding-left: 30rpx;
87 | display: flex;
88 | flex-direction: column;
89 | justify-content: center;
90 | height: 100%;
91 | }
92 | .item_view .item_title{
93 | font-size: 25rpx;
94 | color: darkgray;
95 | }
96 | .item_view .item_date {
97 | font-size: 20rpx;
98 | color: lightgray;
99 | margin-top: 10rpx;
100 | }
101 | .item_view .item_price_contain {
102 | margin-left: auto;
103 | min-width: 120rpx;
104 | height: 100%;
105 | position: relative;
106 | margin-right: -10rpx;
107 | }
108 | .item_view .item_price_contain image {
109 | position: absolute;
110 | width: 100%;
111 | height: 100%;
112 | }
113 | .item_view .item_price {
114 | color: white;
115 | font-size: 40rpx;
116 | position: relative;
117 | height: 100%;
118 | margin: 0rpx 15rpx 0rpx 10rpx;
119 | text-align: center;
120 | line-height: 120rpx;
121 | }
122 |
123 | .placeholder {
124 | display: inline-flex;
125 | text-align: center;
126 | align-items: center;
127 | justify-content: center;
128 | height: 90%;
129 | width: 100%;
130 | font-size: 40rpx;
131 | color: gray;
132 | }
133 |
134 | .loading_more {
135 | display: inline-flex;
136 | text-align: center;
137 | align-items: center;
138 | justify-content: center;
139 | height: 80rpx;
140 | width: 100%;
141 | font-size: 30rpx;
142 | color: lightgray;
143 | }
--------------------------------------------------------------------------------
/pages/myCoupon/myCoupon.js:
--------------------------------------------------------------------------------
1 | // pages/myCoupon/myCoupon.js
2 |
3 | let staticPageNumber = 20;
4 |
5 | // 分页 list
6 | class TabItem {
7 | constructor(title) {
8 | this.title = title; // 标题
9 | this.list = []; // 数据列表
10 | this.placeholder = "点击刷新"; // 占位提示(刷新、网络错误、空白)
11 | this.load_type = 0; // 0表示不显示,1表示加载中,2表示已加载全部
12 | }
13 | }
14 |
15 | // 数据item
16 | class ListItem {
17 | constructor() {
18 | this.image_url = "../../assets/image/coupon_item_icon.png";
19 | this.price = ListItem.randomNumber(5, 100);
20 | this.title = this.price + "元红包";
21 | this.date = ListItem.randomNumber(2017, 2020) + "-03-02";
22 | }
23 | static randomNumber(min, max) {
24 | return parseInt(Math.random() * (max - min) + min);
25 | }
26 | }
27 |
28 | // 空白页tip
29 | function getEmptyTip(index) {
30 | return ["无可使用优惠券", "无已使用优惠券", "无已失效的优惠券"][index % 3];
31 | }
32 |
33 | // 假数据
34 | function getFakeData(num = staticPageNumber) {
35 | var list = [];
36 | for (let i = 0; i < num; i++) {
37 | let item = new ListItem()
38 | list.push(item)
39 | }
40 | return list;
41 | }
42 |
43 | Page({
44 | data: {
45 | tabs: [
46 | new TabItem("可使用的"),
47 | new TabItem("已使用的"),
48 | new TabItem("已失效的")
49 | ],
50 | stv: {
51 | windowWidth: 0,
52 | lineWidth: 0,
53 | offset: 0,
54 | tStart: false
55 | },
56 | activeTab: 0,
57 | },
58 |
59 | onLoad: function (options) {
60 | try {
61 | let { tabs } = this.data;
62 | var res = wx.getSystemInfoSync()
63 | this.windowWidth = res.windowWidth;
64 | this.data.stv.lineWidth = res.windowWidth / this.data.tabs.length;
65 | this.data.stv.windowWidth = res.windowWidth;
66 | this.setData({ stv: this.data.stv })
67 | this.tabsCount = tabs.length;
68 | } catch (e) {
69 | //
70 | }
71 | },
72 |
73 | onShow: function () {
74 | this.loadCouponsAtIndexRefresh();
75 | },
76 |
77 | loadCouponsAtIndexRefresh(index = 0, isRefresh = true) {
78 |
79 | // loading
80 | wx.showLoading({
81 | title: '加载中',
82 | });
83 |
84 | // 显示加载更多
85 | if (!isRefresh) {
86 | // 已经加载全部,则不再请求
87 | let config = this.data.tabs[index];
88 |
89 | // 已经全部加载完毕
90 | if (!config.load_type == 2) {
91 | return;
92 | }
93 |
94 | var tabs = this.data.tabs;
95 | tabs[index].load_type = 1;
96 | this.setData({
97 | tabs: tabs
98 | })
99 | }
100 |
101 | setTimeout(() => {
102 | let res = {
103 | data: { code: 1 }
104 | };
105 |
106 | // fake
107 | res.list = getFakeData(Math.random() > 0.5 ? 6 : staticPageNumber);
108 |
109 | wx.hideLoading();
110 |
111 | let that = this;
112 | let item = that.data.tabs[index];
113 | var tips = item.placeholder;
114 | var list = item.list;
115 |
116 | // 请求成功
117 | if (res.data.code == 1) {
118 | if (res.list && res.list.length > 0) {
119 | if (isRefresh) {
120 | list = res.list;
121 | } else {
122 | list.push(...res.list);
123 | }
124 |
125 | // 加载更多
126 | var tabs = this.data.tabs;
127 | tabs[index].load_type = res.list.length < staticPageNumber ? 2 : 0;
128 | tabs[index].list = list;
129 |
130 | that.setData({
131 | tabs: tabs
132 | })
133 | return;
134 | } else {
135 | tips = getEmptyTip(index);
136 | }
137 | } else {
138 | tips = res.msg.length > 0 ? res.msg : "网络错误";
139 | }
140 | tips += " 点击刷新";
141 |
142 | var tabs = this.data.tabs;
143 | tabs[index].placeholder = tips;
144 |
145 | that.setData({
146 | tabs: tabs
147 | })
148 |
149 | }, 600);
150 | },
151 |
152 | // 加载更多
153 | loadMore(e) {
154 | let currentIndex = e.currentTarget.dataset.index;
155 | let currentTab = this.data.tabs[currentIndex];
156 | if (currentTab.list.length > 0 && currentTab.load_type != 2) {
157 | this.loadCouponsAtIndexRefresh(currentIndex, false);
158 | }
159 | },
160 |
161 | // 刷新
162 | refresh(e) {
163 | let currentIndex = e.currentTarget.dataset.index;
164 | let currentTab = this.data.tabs[currentIndex];
165 | if (currentTab.list.length <= 0) {
166 | this.loadCouponsAtIndexRefresh(currentIndex);
167 | }
168 | },
169 |
170 | // 手势开始
171 | handlerStart(e) {
172 | let { clientX, clientY } = e.touches[0];
173 | this.startX = clientX;
174 | this.tapStartX = clientX;
175 | this.tapStartY = clientY;
176 | this.data.stv.tStart = true;
177 | this.tapStartTime = e.timeStamp;
178 | this.setData({ stv: this.data.stv })
179 | },
180 |
181 | // 手势移动
182 | handlerMove(e) {
183 | let { clientX, clientY } = e.touches[0];
184 | let { stv } = this.data;
185 | let offsetX = this.startX - clientX;
186 | this.startX = clientX;
187 | stv.offset += offsetX;
188 | if (stv.offset <= 0) {
189 | stv.offset = 0;
190 | } else if (stv.offset >= stv.windowWidth * (this.tabsCount - 1)) {
191 | stv.offset = stv.windowWidth * (this.tabsCount - 1);
192 | }
193 | this.setData({ stv: stv });
194 | },
195 |
196 | // 手势取消
197 | handlerCancel(e) {
198 |
199 | },
200 |
201 | // 滑动手势完成
202 | handlerEnd(e) {
203 | let { clientX, clientY } = e.changedTouches[0];
204 | // 如果是点击手势,则屏蔽当前手势的事件处理
205 | if (Math.abs(this.tapStartX - clientX) < 1 && Math.abs(this.tapStartY - clientY) < 1) {
206 | return;
207 | }
208 | // 阻止干预scrollview的上下滑动体验
209 | if (Math.abs(this.data.stv.offset - 0) < 1 || Math.abs(this.data.stv.offset - this.data.windowWidth) < 1) {
210 | return;
211 | }
212 | let endTime = e.timeStamp;
213 | let { tabs, stv, activeTab } = this.data;
214 | let { offset, windowWidth } = stv;
215 |
216 | //快速滑动
217 | if (endTime - this.tapStartTime <= 300) {
218 | //向左
219 | if (Math.abs(this.tapStartY - clientY) < 50) {
220 | if (this.tapStartX - clientX > 5) {
221 | if (activeTab < this.tabsCount - 1) {
222 | let page = ++activeTab;
223 | this.reloadPageIfEmpty(page);
224 | this.setData({ activeTab: page })
225 | }
226 | } else {
227 | if (activeTab > 0) {
228 | let page = --activeTab;
229 | this.reloadPageIfEmpty(page);
230 | this.setData({ activeTab: page })
231 | }
232 | }
233 | stv.offset = stv.windowWidth * activeTab;
234 | } else {
235 | //快速滑动 但是Y距离大于50 所以用户是左右滚动
236 | let page = Math.round(offset / windowWidth);
237 | if (activeTab != page) {
238 | this.setData({ activeTab: page })
239 | this.reloadPageIfEmpty(page);
240 | }
241 | stv.offset = stv.windowWidth * page;
242 | }
243 | } else {
244 | let page = Math.round(offset / windowWidth);
245 |
246 | if (activeTab != page) {
247 | this.setData({ activeTab: page })
248 | this.reloadPageIfEmpty(page);
249 | }
250 | stv.offset = stv.windowWidth * page;
251 | }
252 |
253 | stv.tStart = false;
254 | this.setData({ stv: this.data.stv })
255 | },
256 |
257 | // item点击
258 | itemTap(e) {
259 | console.log(e);
260 | },
261 |
262 | // 更新选中的page
263 | updateSelectedPage(page) {
264 | // 屏蔽重复选中
265 | if (this.data.activeTab == page) {
266 | return;
267 | }
268 | let { tabs, stv, activeTab } = this.data;
269 | activeTab = page;
270 | this.setData({ activeTab: activeTab })
271 | stv.offset = stv.windowWidth * activeTab;
272 | this.setData({ stv: this.data.stv })
273 | this.reloadPageIfEmpty(page);
274 | },
275 |
276 | reloadPageIfEmpty(page) {
277 | // 重新请求
278 | if (this.data.tabs[page].list.length <= 0) {
279 | this.loadCouponsAtIndexRefresh(page);
280 | }
281 | },
282 |
283 | // item view 点击
284 | handlerTabTap(e) {
285 | this.updateSelectedPage(e.currentTarget.dataset.index);
286 | },
287 |
288 | /**
289 | * 用户点击右上角分享
290 | */
291 | onShareAppMessage: function () {
292 | return {
293 | title: `准确极速、支持全国`,
294 | desc: '准确极速、覆盖全国、1.4亿车主都在用',
295 | path: '/pages/myCoupon/myCoupon'
296 | }
297 | },
298 | })
--------------------------------------------------------------------------------