├── README.md ├── app.js ├── app.json ├── app.wxss ├── components ├── card │ ├── card.js │ ├── card.json │ ├── card.wxml │ └── card.wxss └── cardSwipe │ ├── cardSwipe.js │ ├── cardSwipe.json │ ├── cardSwipe.wxml │ ├── cardSwipe.wxs │ └── cardSwipe.wxss ├── pages ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── logs │ ├── logs.js │ ├── logs.json │ ├── logs.wxml │ └── logs.wxss ├── project.config.json ├── sitemap.json └── utils └── util.js /README.md: -------------------------------------------------------------------------------- 1 | ## cardSwipe - 小程序卡片滑动组件 2 | ### 介绍 3 | 此组件是使用原生微信小程序代码开发的一款高性能的卡片滑动组件,无外部依赖,导入即可使用。其主要功能效果类似探探的卡片滑动,支持循环,新增,删除,以及替换卡片。 4 | 5 | ![卡片展示图](http://p3.pstatp.com/large/pgc-image/9293be49347c4b60b0e928ca76dc8e08) 6 | 7 | ### 用法 8 | ##### 获取: 9 | ``` 10 | git clone https://github.com/1esse/cardSwipe.git 11 | ``` 12 | ##### 相关文件介绍: 13 | - /components 14 | - /card 15 | - /cardSwipe 16 | - /pages 17 | - /index 18 | 19 | **其中,components文件夹下的card组件是cardSwipe组件的[抽象节点](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/generics.html),放置卡片内容,需要调用者自己实现。而cardSwipe组件为卡片功能的具体实现。pages下的index为调用组件的页面,可供参考。** 20 | 21 | ### 功能介绍 22 | ##### 亮点: 23 | - 支持热循环(循环与不循环动态切换),动态新增,动态删除以及动态替换卡片 24 | - 卡片的wxml节点数不受卡片列表的大小影响,只等于卡片展示数,如果每次只展示三张卡片,那么卡片所代表的节点数只有三个 25 | - 支持调节各种属性(滑动速度,旋转角度,卡片展示数...等等) 26 | - 节点数少,可配置属性多,自由化程度高,容易嵌入各种页面且性能高 27 | ##### 实现方式: 28 | 循环/不循环: 属性circling(true/false)控制 29 | 30 | 新增: 向卡片的循环数组中添加(不推荐新增,具体原因[后面分析](#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AE%E6%96%B0%E5%A2%9E%E5%8D%A1%E7%89%87)。硬要新增的话,如果卡片列表不大,并且需要新增多张卡片,可以直接将数据push到卡片列表中然后setData整个数组。如果是每次只增加一张卡片,推荐使用下面这种方式,以数据路径的形式赋值) 31 | ```js 32 | this.setData({ 33 | [`cards[${cards.length}]`]: { 34 | title: `新增卡片${cards.length + 1}`, 35 | // ... 36 | } 37 | }) 38 | ``` 39 | 删除: 40 | ```js 41 | // removeIndex: Number,需要删除的卡片的索引,将删除的卡片的值设置为null 42 | // removed_cards: Array,收集已删除的卡片的索引,每次删除卡片都需要更新 43 | this.setData({ 44 | [`cards[${removeIndex}]`]: null, 45 | removed_cards 46 | }) 47 | ``` 48 | 替换: 49 | ```js 50 | // replaceIndex:Number,需要替换的卡片的索引 51 | // removed_cards: Array,收集已删除的卡片的索引,如果replaceIndex的卡片是已删除的卡片的话,需要将该卡片索引移出removed_cards 52 | this.setData({ 53 | [`cards[${replaceIndex}]`]: { 54 | title: `替换卡片${replaceIndex}`, 55 | // ... 56 | } 57 | // removed_cards 58 | }) 59 | ``` 60 | **注意:删除和替换操作都需要同步removed_cards** 61 | ##### why?为什么使用removed_cards而不直接删除数组中的元素 62 | 1. 由于小程序的机制,逻辑层和视图层的通信需要使用setData来完成。这对大数组以及对象特别不友好。如果setData的数据太多,会导致通信时间过长。又碰巧数组删除元素的操作无法通过数据路径的形式给出,这会导致每次删除都需要重新setData整张卡片列表,如果列表数据过大,将会引发[性能](https://developers.weixin.qq.com/miniprogram/dev/framework/audits/performance.html)问题。 63 | 2. 删除的时候,如果删除的卡片索引在当前索引之前,那么当前索引所代表的卡片将会是原来的下一张,导致混乱。 64 | 3. 保留删除元素,为卡片列表的替换以及删除提供更方便,快捷,稳定的操作。 65 | ### 优化 66 | ##### 由于组件支持动态的删除以及替换,这使得我们可以以很小的卡片列表来展示超多(or 无限)的卡片 67 | 场景1:实现一个超多的卡片展示(比如1000张) 68 | 69 | 实现思路:以分页的形式每次从后台获取数据,先获取第一页和第二页的数据。在逻辑层(js)创建一个卡片列表,把第一页数据赋值给它,用于跟视图层(wxml)通信。开启循环,用户每滑动一次,将划过的卡片替换成第二页相同索引的卡片,第一页卡片全部划过,第二页的内容也已经同步替换完毕,再次请求后台,获取第三页的内容,以此类推。到最后一页的时候,当前索引为0时,关闭循环,删除最后一页替换不到的上一页剩余的卡片 70 | 71 | 场景2:实现一个无限循环的卡片 72 | 73 | 实现思路:类似场景1。不关闭循环。 74 | ##### 为什么不建议新增卡片 75 | 新增卡片会增加卡片列表的长度,由于每次滑动都要重新计算卡片列表中所有卡片的状态,卡片列表越大,预示着每次滑动卡片需要计算状态的卡片越多,越消耗性能。在完全可以开启循环然后用替换卡片操作代替的情况下,不推荐新增卡片。建议卡片列表大小保持在10以内以保证最佳性能。 76 | ##### 以下为卡片列表大小在每次滑动时对性能的影响(指再次渲染耗时) 77 | 注:不同手机可能结果不同,但是耗时差距的原因是一样的 78 | | 耗时(ms/毫秒) | 10张卡片 | 100张卡片 | 1000张卡片 | 79 | |-----------|-------|--------|---------| 80 | | 再次渲染1 | 10 | 12 | 116 | 81 | | 再次渲染2 | 12 | 10 | 87 | 82 | | 再次渲染3 | 17 | 16 | 145 | 83 | | 再次渲染4 | 9 | 16 | 112 | 84 | | 再次渲染5 | 9 | 18 | 90 | 85 | | 平均 | 11\.4 | 14\.4 | 110 | 86 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | const res = wx.getSystemInfoSync() 10 | this.globalData.screenWidth = res.screenWidth 11 | 12 | // 登录 13 | wx.login({ 14 | success: res => { 15 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 16 | } 17 | }) 18 | // 获取用户信息 19 | wx.getSetting({ 20 | success: res => { 21 | if (res.authSetting['scope.userInfo']) { 22 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 23 | wx.getUserInfo({ 24 | success: res => { 25 | // 可以将 res 发送给后台解码出 unionId 26 | this.globalData.userInfo = res.userInfo 27 | 28 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 29 | // 所以此处加入 callback 以防止这种情况 30 | if (this.userInfoReadyCallback) { 31 | this.userInfoReadyCallback(res) 32 | } 33 | } 34 | }) 35 | } 36 | } 37 | }) 38 | }, 39 | globalData: { 40 | userInfo: null 41 | } 42 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/logs/logs" 5 | ], 6 | "window": { 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle": "black" 11 | }, 12 | "style": "v2", 13 | "sitemapLocation": "sitemap.json" 14 | } -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: space-between; 7 | box-sizing: border-box; 8 | } 9 | -------------------------------------------------------------------------------- /components/card/card.js: -------------------------------------------------------------------------------- 1 | // components/card/card.js 2 | Component({ 3 | /** 4 | * 组件的属性列表 5 | */ 6 | properties: { 7 | card: Object 8 | }, 9 | 10 | /** 11 | * 组件的初始数据 12 | */ 13 | data: { 14 | 15 | }, 16 | 17 | /** 18 | * 组件的方法列表 19 | */ 20 | methods: { 21 | 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /components/card/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /components/card/card.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{card.title}} 7 | {{card.desc}} 8 | 9 | -------------------------------------------------------------------------------- /components/card/card.wxss: -------------------------------------------------------------------------------- 1 | 2 | .card{ 3 | width: 100%; 4 | height: 740rpx; 5 | background-color: #ffffff; 6 | box-sizing: border-box; 7 | border: 1rpx solid #f9f9f9; 8 | box-shadow: 0 2rpx 20rpx #999999; 9 | border-radius: 10rpx; 10 | margin-top: 50rpx; 11 | } 12 | 13 | .card .top{ 14 | width: 100%; 15 | } 16 | .card .bottom{ 17 | width: 100%; 18 | padding: 20rpx; 19 | box-sizing: border-box; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | } 24 | -------------------------------------------------------------------------------- /components/cardSwipe/cardSwipe.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: jesse zhao 3 | * @Date: 2020-04-07 02:41:53 4 | * @Last Modified by: jesse zhao 5 | * @Last Modified time: 2020-04-20 03:13:52 6 | * @github: https://github.com/1esse/cardSwipe 7 | */ 8 | 9 | Component({ 10 | properties: { 11 | cards: Array, // 卡片数据,一个包含所有卡片对象的数组 12 | removedCards: Array, // 存放已经移除的卡片的索引数据,如果索引填充了其他卡片,需要将该索引移出 13 | transition: Boolean, // 是否开启过渡动画 14 | circling: Boolean, // 是否列表循环 15 | rotateDeg: Number, // 整个滑动过程旋转角度 16 | showCards: { // 显示几张卡片 17 | type: Number, 18 | value: 3 19 | }, 20 | slideDuration: { // 手指离开屏幕后滑出界面时长,单位(ms)毫秒 21 | type: Number, 22 | value: 200 23 | }, 24 | slideThershold: { // 松手后滑出界面阈值,单位px 25 | type: Number, 26 | value: 60 27 | }, 28 | upHeight: { // 下层卡片下移高度,单位px 29 | type: Number, 30 | value: 40 31 | }, 32 | scaleRatio: { // 下层卡片收缩力度 33 | type: Number, 34 | value: 0.05 35 | }, 36 | }, 37 | 38 | observers: { 39 | cards(nc, oc) { 40 | if (!nc) return 41 | this.cardReflect() 42 | }, 43 | showCards(nc, oc) { // 用于展示调节用,一般情况下展示卡片数量是固定的,不需要监听变化。 44 | if (!nc) return 45 | this.cardReflect() 46 | } 47 | }, 48 | 49 | data: { 50 | just_shown: -1, // 如果显示卡片的数量和卡片总数量一样,那么开启循环的时候,被设置过transform的节点不会重新渲染,这会导致已经滑出界面的卡片无法回归原位,这个字段就是用来控制滑出卡片重新渲染的 51 | }, 52 | 53 | attached() { 54 | // 给每张卡片设置层级 55 | const { cards } = this.data 56 | this.setData({ 57 | current_cursor: cards.findIndex(item => item) 58 | }) 59 | this.getContextWidth() 60 | }, 61 | 62 | methods: { 63 | cardReflect() { 64 | let { cards, showCards } = this.data 65 | let sc = showCards 66 | if (showCards < 1) sc = 1 67 | else if (showCards > cards.filter(item => item).length) sc = cards.filter(item => item).length 68 | this.setData({ 69 | current_z_index: new Array(sc).fill(0).map((_, index) => index + 1).reverse(), 70 | sc: sc 71 | }) 72 | }, 73 | 74 | getContextWidth() { 75 | const query = this.createSelectorQuery() 76 | query.select('.wrapper').boundingClientRect() 77 | query.exec((res) => { 78 | const contextWidth = res[0].width 79 | this.setData({ 80 | contextWidth 81 | }) 82 | }) 83 | }, 84 | nextCard(e) { 85 | let { current_cursor, just_shown, slideDuration } = this.data 86 | just_shown = current_cursor 87 | current_cursor = this.countCurrentCursor(current_cursor) 88 | Object.assign(e, { 89 | swiped_card_index: just_shown, 90 | current_cursor 91 | }) 92 | setTimeout(() => { 93 | this.setData({ 94 | just_shown 95 | }, () => { 96 | this.setData({ 97 | just_shown: -1, 98 | current_cursor, 99 | }) 100 | }) 101 | }, slideDuration) 102 | this.triggerEvent('cardSwipe', e) 103 | }, 104 | 105 | countCurrentCursor(current_cursor) { 106 | const { circling, cards, removedCards } = this.data 107 | if (circling) // 如果开启循环 108 | current_cursor = current_cursor + 1 === cards.length ? 0 : current_cursor + 1 109 | else 110 | current_cursor += 1 111 | if (!removedCards.includes(current_cursor)) return current_cursor 112 | return this.countCurrentCursor(current_cursor) 113 | } 114 | } 115 | }) 116 | -------------------------------------------------------------------------------- /components/cardSwipe/cardSwipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {}, 4 | "componentGenerics": { 5 | "card": true 6 | } 7 | } -------------------------------------------------------------------------------- /components/cardSwipe/cardSwipe.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/cardSwipe/cardSwipe.wxs: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: jesse zhao 3 | * @Date: 2020-04-07 02:41:53 4 | * @Last Modified by: jesse zhao 5 | * @Last Modified time: 2020-04-20 01:38:33 6 | * @github: https://github.com/1esse/cardSwipe 7 | */ 8 | var SLIDE_THRESHOLD = 0 // 松手后滑出界面阈值 9 | var CONTEXT_WIDTH = 0 // 组件宽度 10 | var instance = {} // 卡片实例 11 | var startX = 0 // 手指刚接触时屏幕X轴位置 12 | var moveX = 0 // 移动距离 13 | var cardIndex = 0 // 滑动卡片的层级索引 14 | var cardZindex = 0 // 滑动卡片的层级索引 15 | var currentCursor = 0 // 当前最上层卡片索引 16 | var lock = false 17 | var ROTATE_DEG = 0 // 移出屏幕过程旋转角度 18 | 19 | function touchstart(event, ownerinstance) { 20 | if (lock) return 21 | lock = true 22 | instance = event.instance 23 | var dataset = instance.getDataset() 24 | cardIndex = dataset.cardIndex 25 | currentCursor = dataset.currentCursor 26 | if (cardIndex !== currentCursor) { 27 | instance = {} 28 | return 29 | } 30 | ROTATE_DEG = dataset.rotateDeg 31 | SLIDE_DURATION = dataset.slideDuration 32 | SLIDE_THRESHOLD = dataset.slideThershold 33 | CONTEXT_WIDTH = dataset.contextWidth + Math.floor((Math.abs(ROTATE_DEG) % 360) * dataset.contextWidth / 180) 34 | cardZindex = dataset.cardZindex 35 | startX = event.touches[0].clientX 36 | } 37 | 38 | function touchmove(event, ownerinstance) { 39 | if (!lock) return 40 | if (!instance || !startX || cardIndex !== currentCursor) { 41 | lock = false 42 | return 43 | } 44 | var clientX = event.touches[0].clientX 45 | moveX = clientX - startX 46 | instance.setStyle({ 47 | "transition": "unset", 48 | "transform": "translate3d(" + moveX + "px, 0, " + cardZindex + "px) rotate(" + ROTATE_DEG / CONTEXT_WIDTH * moveX + "deg)", 49 | }) 50 | } 51 | 52 | function touchend(event, ownerinstance) { 53 | if (!lock) return 54 | if (!instance || !moveX || cardIndex !== currentCursor) { 55 | lock = false 56 | return 57 | } 58 | instance.setStyle({ 59 | "transform": Math.abs(moveX) < SLIDE_THRESHOLD ? 60 | // 如果小于滑出阈值,放弃setStyle,使用之前的transfrom 61 | "translate3d()" : 62 | "translate3d(" + (moveX > 0 ? CONTEXT_WIDTH : -CONTEXT_WIDTH) + "px, 0, " + cardZindex + "px) rotate(" + (moveX > 0 ? ROTATE_DEG : -ROTATE_DEG) + "deg)", 63 | "transition": "all ease " + (SLIDE_DURATION / 1000).toString() + "s", 64 | }) 65 | Math.abs(moveX) >= SLIDE_THRESHOLD && ownerinstance.callMethod('nextCard', { 66 | direction: moveX > 0 ? 'right' : 'left' 67 | }) 68 | lock = false 69 | startX = 0 70 | moveX = 0 71 | } 72 | 73 | function circleRange(index, current_cursor, showCards, total, removedCards, symbol) { 74 | if (showCards > total || current_cursor === undefined) return 75 | var range = [] 76 | var i = current_cursor 77 | while (range.length < showCards) { 78 | removedCards.indexOf(i) === -1 && range.push(i) 79 | i += 1 80 | if (i === total) i = 0 81 | } 82 | switch (symbol) { 83 | case 'in': 84 | return range.indexOf(index) >= 0 85 | case 'loc': 86 | return range.indexOf(index) 87 | } 88 | } 89 | 90 | function getRangeRemoves(removedCards, current_cursor, showCards, total) { 91 | var count = 0 92 | var i = current_cursor 93 | while (count < showCards && i < total) { 94 | removedCards.indexOf(i) === -1 && count++ 95 | i++ 96 | } 97 | return i 98 | } 99 | 100 | module.exports = { 101 | touchstart: touchstart, 102 | touchmove: touchmove, 103 | touchend: touchend, 104 | circleRange: circleRange, 105 | getRangeRemoves: getRangeRemoves 106 | } -------------------------------------------------------------------------------- /components/cardSwipe/cardSwipe.wxss: -------------------------------------------------------------------------------- 1 | .wrapper{ 2 | width: 100%; 3 | height: 100%; 4 | padding: 0 60rpx; 5 | box-sizing: border-box; 6 | overflow: hidden; 7 | } 8 | 9 | .cards{ 10 | position: relative; 11 | width: 100%; 12 | display: flex; 13 | justify-content: center; 14 | padding: 20rpx; 15 | box-sizing: border-box; 16 | transform-style: preserve-3d; 17 | } 18 | 19 | .card-next{ 20 | animation: fadeIn 0.5s linear; 21 | } 22 | 23 | @keyframes fadeIn{ 24 | from{ 25 | opacity: 0; 26 | } 27 | to{ 28 | opacity: 1; 29 | } 30 | } -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | cards: [], // 卡片数据,一个包含所有卡片对象的数组 8 | removed_cards: [],// 存放已经移除的卡片的索引数据,如果索引填充了其他卡片,需要将该索引移出 9 | transition: true,//是否开启过渡动画 10 | circling: false, // 是否列表循环 11 | rotate_deg: 0,// 整个滑动过程旋转角度 12 | slide_duration: 200,// 手指离开屏幕后滑出界面时长,单位(ms)毫秒 13 | show_cards: 3,// 显示几张卡片 14 | thershold: 60,// 松手后滑出界面阈值,单位px 15 | scale_ratio: 0.07,// 下层卡片收缩力度 16 | up_height: 40,// 下层卡片下移高度,单位rpx 17 | }, 18 | onLoad: function () { 19 | this.generateCards(5) 20 | }, 21 | generateCards(num) { 22 | const cards = [] 23 | for (let i = 0; i < num; i++) { 24 | cards.push({ 25 | title: `卡片${i + 1}`, 26 | src: `https://source.unsplash.com/collection/190727/500x600?id=${i}`, 27 | desc: `这是一段卡片${i + 1}的描述。` 28 | }) 29 | } 30 | this.setData({ 31 | cards: cards, 32 | current_cursor: cards.findIndex(item => item), 33 | removed_cards: [] 34 | }) 35 | }, 36 | onSwitch: function (e) { 37 | const { symbol } = e.currentTarget.dataset 38 | switch (symbol) { 39 | case 'loop': 40 | this.setData({ 41 | circling: e.detail.value 42 | }) 43 | break 44 | case 'transition': 45 | this.setData({ 46 | transition: e.detail.value 47 | }) 48 | break 49 | } 50 | }, 51 | onSlide: function (e) { 52 | const { symbol } = e.currentTarget.dataset 53 | switch (symbol) { 54 | case 'show_cards': 55 | case 'rotate_deg': 56 | case 'slide_duration': 57 | this.setData({ 58 | [symbol]: e.detail.value 59 | }) 60 | break 61 | } 62 | }, 63 | cardOperate(e) { 64 | const { symbol } = e.currentTarget.dataset 65 | const { cards } = this.data 66 | switch (symbol) { 67 | case 'add': 68 | this.setData({ 69 | [`cards[${cards.length}]`]: { 70 | title: `新增卡片${cards.length + 1}`, 71 | src: `https://source.unsplash.com/collection/190727/600x600?id=${cards.length + 1}`, 72 | desc: `这是一段新增卡片${cards.length + 1}的描述。` 73 | } 74 | }) 75 | break 76 | case 'reset': 77 | this.setData({ 78 | cards: null 79 | }, () => { 80 | this.generateCards(5) 81 | }) 82 | break 83 | case 'remove': 84 | const { removeIndex } = e.currentTarget.dataset 85 | const { removed_cards } = this.data 86 | if (removed_cards.includes(parseInt(removeIndex))) return 87 | removed_cards.push(parseInt(removeIndex)) 88 | this.setData({ 89 | [`cards[${removeIndex}]`]: null, 90 | removed_cards 91 | }) 92 | break 93 | } 94 | }, 95 | cardSwipe(e) { 96 | const { direction, swiped_card_index, current_cursor } = e.detail 97 | console.log(e.detail) 98 | wx.showToast({ 99 | title: `卡片${swiped_card_index + 1}向${direction === 'left' ? '左' : '右'}滑`, 100 | icon: 'none', 101 | duration: 1000 102 | }) 103 | this.setData({ 104 | current_cursor 105 | }) 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "card-swipe": "/components/cardSwipe/cardSwipe", 4 | "card": "/components/card/card" 5 | } 6 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 添加卡片 7 | 8 | 重置卡片 9 | 10 | 11 | 删除卡片 12 | 13 | 14 | 15 | {{index + 1}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 列表循环: 23 | 24 | 过渡动画: 25 | 26 | 27 | 28 | 展示卡片: 29 | 30 | 31 | 32 | 旋转角度: 33 | 34 | 35 | 36 | 滑动时长: 37 | 38 | 39 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .settings{ 2 | display: flex; 3 | width: 80%; 4 | align-items: center; 5 | z-index: 1000; 6 | } 7 | 8 | .setting{ 9 | flex: 1; 10 | } 11 | 12 | .btn-area{ 13 | display: flex; 14 | align-items: center; 15 | height: 60rpx; 16 | } 17 | 18 | .card-swipe{ 19 | width: 100%; 20 | height: 900rpx; 21 | } 22 | 23 | .rm-card{ 24 | width: 40rpx; 25 | height: 40rpx; 26 | flex-shrink: 0; 27 | background-color: #F08080; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | color: #ffffff; 32 | border-radius: 50%; 33 | margin-left: 10rpx; 34 | font-size: 18rpx; 35 | } 36 | 37 | .disable{ 38 | background-color: #f3f3f3; 39 | color: #000000; 40 | pointer-events: none; 41 | } 42 | 43 | .label{ 44 | white-space: nowrap; 45 | color: #666666; 46 | } 47 | 48 | .success{ 49 | color: #07c160; 50 | } 51 | 52 | .danger{ 53 | color: #F08080; 54 | } -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志", 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "postcss": true, 10 | "preloadBackgroundData": false, 11 | "minified": true, 12 | "newFeature": true, 13 | "autoAudits": false, 14 | "coverView": true, 15 | "showShadowRootInWxmlPanel": true, 16 | "scopeDataCheck": false 17 | }, 18 | "compileType": "miniprogram", 19 | "libVersion": "2.10.4", 20 | "appid": "wxe4cb5373b9837011", 21 | "projectname": "cardSwipe", 22 | "debugOptions": { 23 | "hidedInDevtools": [] 24 | }, 25 | "isGameTourist": false, 26 | "simulatorType": "wechat", 27 | "simulatorPluginLibVersion": {}, 28 | "condition": { 29 | "search": { 30 | "current": -1, 31 | "list": [] 32 | }, 33 | "conversation": { 34 | "current": -1, 35 | "list": [] 36 | }, 37 | "game": { 38 | "currentL": -1, 39 | "list": [] 40 | }, 41 | "miniprogram": { 42 | "current": -1, 43 | "list": [] 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | const formatTime = date => { 3 | const year = date.getFullYear() 4 | const month = date.getMonth() + 1 5 | const day = date.getDate() 6 | const hour = date.getHours() 7 | const minute = date.getMinutes() 8 | const second = date.getSeconds() 9 | 10 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 11 | } 12 | 13 | const formatNumber = n => { 14 | n = n.toString() 15 | return n[1] ? n : '0' + n 16 | } 17 | 18 | function rpx2px(rpx) { 19 | return rpx / 750 * app.globalData.screenWidth 20 | } 21 | 22 | function px2rpx(px) { 23 | return 750 / px * app.globalData.screenWidth 24 | } 25 | 26 | module.exports = { 27 | formatTime, 28 | rpx2px, 29 | px2rpx 30 | } 31 | --------------------------------------------------------------------------------