618 京東到家-小程式也狂歡

唐麟發表於2018-06-11

618 將至,學了這麼久小程式,也該自己動手實踐一下。最近實現了一個京東到家小程式,我將其中比較有意思,而且常用到的功能做一個展示,大家可以參考參考,完整專案在這裡 github.daojia.jd

本文的實現

  • 小球落入購物車的拋物線動畫-createAnimation
  • 跳動的小紅點-badge
  • 購物車的顯示和隱藏-actionsheet
  • 獲取詳細地理位置
  • 模糊查詢

參考文件:w3c小程式文件小程式實戰指南weui.wxml

小球落入購物車的拋物線動畫

圖
實現思路: 創造一個小球,給它一個運動軌跡,最後落入購物車區域。

如果是 DOM 程式設計,自然是獲取點選的位置,用 createElement 建立一個元素,定時器給它一個動態的座標,讓它運動起來,但這並不是 DOM ,而是小程式,使用的是資料繫結。

那這就更好了,只要資料改變,狀態就會相應的改變,那我們趕快給它實現座標的動態輸入吧。如果你真的就這樣開始做了,那你就會陷入和我之前一樣的困境,小球運動的初始位置不是固定的,你想要讓它在任何位置都能圓潤地滾到購物車,就需要用一個演算法,實現這條拋物線。

有聰明的小夥伴可能就想到了貝塞爾曲線,是的這樣確實可行,但對與我這樣的數學“天才”來說,正餘弦什麼的還是不了不了,打擾了,告辭。

我就想有什麼更簡單的方式,能夠動態的新增拋物線動畫呢,這時我上百度Google到了一個 api createAnimation ,就決定是你了。

這是 wx.createAnimation 的官方文件,簡單來說就是,它會建立一個動畫例項,這個例項可以描述你想要的動畫,並通過頁面的 animation 屬性繫結動畫。

那就開始了

  • 先在頁面檔案(也就是你的wxml)寫個 view 來裝載動畫:
<view animation="{{animationY}}" style="position:fixed;top:{{ballY}}px;" hidden="{{!showBall}}">
    <view class="ball" animation="{{animationX}}" style="position:fixed;left:{{ballX}}px;"></view>
</view>
複製程式碼

外層 view 實現 x軸運動,內層 view 實現 縱軸y運動。 ballX,ballY是它的(座標)位置,就是以螢幕左上為原點的座標軸。

  • js 檔案實現動畫的建立和填裝 小球在x軸勻速運動,在y軸以三次貝塞爾曲線規定的加速運動,實現拋物線落入購物車
runBall(e) {
    // 從全域性獲取螢幕高度和寬度
	let bottomX = this.data.screenWidth,
	bottomY = this.data.screenHeight
	// x, y表示手指點選橫縱座標, 即小球的起始座標
	let ballX = e.detail.x,
	ballY = e.detail.y
	this.setData({
		ballX: ballX-10,
		ballY,
		showBall: false,
		jump: false
    })
    //例項化動畫
	this.animationX = wx.createAnimation({
		duration: 1000, 
		timingFunction: 'linear',
	})
	this.animationY = wx.createAnimation({
		duration: 1000, 
		timingFunction: 'cubic-bezier(.93,-0.11,.85,.74)',
    })
    // 第一段動畫,向上運動一點
	this.setDelayTime(10).then(() => {
		this.animationX.translateX(-100).step()
		this.animationY.translateY(-10).step()
		this.setData({
			animationX: this.animationX.export(),
			animationY: this.animationY.export()
		})
		return this.setDelayTime(200);
	}).then(() => {
        // 第二段動畫,落入購物車
		this.setData({
			showBall: true
        })
        // 平移距離是根據我的元素位置,其他需要自行調整
		this.animationX.translateX(-ballX-ballY*0.5).step()
		this.animationY.translateY(bottomY).step()
		this.setData({
			animationX: this.animationX.export(),
			animationY: this.animationY.export()
		})
		return this.setDelayTime(1000);
	}).then(() => {
        // 重置動畫,回到原點
		this.animationX.translateX(0).step()
		this.animationY.translateY(0).step()
		this.setData({
			showBall: false,
			animationX: this.animationX.export(),
			animationY: this.animationY.export()
		})
	})
},
// 同步操作,傳入定時器時間,
setDelayTime(second) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve()
		}, second)
	})
},
複製程式碼
.ball {
    width: 40rpx;
    height: 40rpx;
    background-color: #4DCC57;
    border-radius: 50%;
    z-index: 999;
}
複製程式碼

這段js做的事情:

  1. 獲取螢幕高度和寬度
  2. 獲取點選位置,我這裡就是加入購物車的那個加號,也可以是點選商品位置,不同需求就不同。
  3. 例項化動畫 animationX、animationY
  4. 為這個例項新增動畫,我這裡有幾段動畫,為了實現小球先向上運動一點距離,再向下,做拋物線運動。還有一個目的就是隱藏這一段,你可以看到 showBall 有不同狀態,因為運動並沒有從我的初始位置開始,這裡有bug
  5. 最後回到原點並隱藏小球

關於 wx.createAnimation api 的使用,例項.運動方式.step()step分隔動畫,export() 匯出動畫,timingFunction 規定運動速度效果,詳細可見文件。

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案–回撥函式和事件--更合理和更強大。這裡只需要知道,可以讓這幾段動畫分步執行的就行。詳細見廖雪峰Promise

另外,有一個小彩蛋就是,顯示數量的小紅點,在每次小球落入購物車的時候跳一下,以提醒使用者。是這樣實現的:算了,見下一段

跳動的小紅點

這個小紅點是用 weuibadge 效果,關於badge詳情見官方文件:weui.wxml

<view class="weui-badge {{jump?'badgeJump':''}}" style="position: absolute; display: {{shopCar.length==0?'none':''}}">{{shopCar.length}}</view>
複製程式碼

動態新增類名,jump 決定是否使用 badgeJump 類。

this.runBall(e)
this.setDelayTime(1000).then(() => {
	this.hasCarList()
	this.getTotalPrice()
	this.setData({
		jump: true
	})
})
複製程式碼
.badgeJump {
    animation: jump 500ms;
}
@keyframes jump {
    0% {
        transform: translateY(0);
    }
    50% {
        transform: translateY(-50rpx);
    }
    100% {
        transform: translateY(0);
    }
}
複製程式碼

同樣是在呼叫 runBall 方法的事件中,先呼叫runBall方法,在小球下落時間結束之後再將值傳入,這樣頁面的效果跟著小球的下落在改變,最後小紅點調皮地跳動一下,提示使用者有新商品加入購物車啦。

購物車的顯示和隱藏

想要實現的是點選一下購物車圖示,從下而上彈出一個選單,顯示已新增進購物車的物品。可以用 weui actionsheet 實現,沒錯又是 weui。但是我想要的是一個滾動區域而不是固定的,所以還是自己寫一個類似原生actionsheet 的吧,還可以練手。

  1. 購物車和遮罩的結構

首先要確定的是,整個頁面就是一屏大小,頁面不能撐出滾動條,但內部可以有滾動區域。遮罩就是整個螢幕的大小,購物車區域可以自行設定一個高度,給它一個固定定位,初始位置是在整個螢幕的下面,並不顯示在可視區,當使用者點選購物車圖示時,從下方彈出來,單擊遮罩區域,縮回去回去並隱藏。來看程式碼是怎麼實現的:

<!-- 遮罩區域 類名控制隱藏與否 -->
<view class="{{clicked?'weui-mask':'weui-mask-on'}}" catchtap="hideMask"></view>
<!-- 購物車區域 -->
<view class="weui-actionSheet {{clicked?'weui-actionsheet_toggle':''}}">
    <view class="cart-header">
        <icon class="total-select" type="{{selectAllStatus?'success':'circle'}}" color="#4DCC57" bindtap="selectAll" />
        <text>{{selectAllStatus?'全選':'全不選'}}</text>
        <text class="cart-total-num"> (已選{{shopCar.length}}件)</text>
        <text class="clear_cart" catchtap="clearCart">清空購物車</text>
    </view>
    <!-- 加入購物車的商品列表 -->
    <scroll-view scroll-y style="width: 100%; height: 700rpx;">
    </scroll-view>
</view>
複製程式碼
<!-- 底部購物車圖示區域 -->
<view class="cart" style="margin-top: 0;">
    <view class="shop_ft" style="z-index: -999;">
        <view class="weui-cell shop_car">
            <view class="weui-cell__hd {{shopCar.length==0?'car-icon_empty':'car-icon_fill'}}" catchtap="{{shopCar.length==0?'':'showCart'}}">
                <view class="weui-badge {{jump?'badgeJump':''}}" style="position: absolute; display: {{shopCar.length==0?'none':''}}">{{shopCar.length}}</view>
            </view>
            <view class="weui-cell__bd car_status" style="{{shopCar.length==0?'':'color: red;'}}">
                <text>{{shopCar.length==0?'購物車是空的':car_status}}</text>
            </view>
        </view>
        <view class="weui-cell__ft pay" style="{{shopCar.length==0?'':'background: #4DCC57'}}" catchtap="submitOrder">
            <text>去結算</text>
        </view>
    </view>
</view>
複製程式碼
  1. 樣式
/* 遮罩層 */
.weui-mask {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, .6);
}
.weui-mask-on {
    display: block;
}
/* 購物車 actionsheet */
.weui-actionSheet {
    background-color: #fff;    
    position: fixed;
    bottom: 0;
    width: 100%; 
    max-height: 800rpx; 
    margin-bottom: 100rpx;
    z-index: 99; 
    /* 事先隱藏才下方 */
    transform: translateY(100%);
    transition: transform .3s;
    font-size: 32rpx;
    backface-visibility: hidden;
    /*定義當元素不面向螢幕時是否可見*/
}
/* 回到原位顯示出來 */
.weui-actionsheet_toggle {
    transform: translate(0, 0);
    transition: transform .3s;
}
複製程式碼
  1. js 控制顯示與隱藏
hideMask() {
	this.setData({
		clicked: false
	})
},
//  點選購物車的觸發事件
showCart() {
	const shopCar = wx.getStorageSync('myCart')
	shopCar.forEach(item => {
		if (item.id == this.data.shop_id) {
			myCart = item.list
		}
	})
	this.setData({
        // 購物車和遮罩的顯隱
	    clicked: !this.data.clicked,
		shopCar: myCart
	})
	this.getTotalPrice()
},
複製程式碼

這個功能的難點應該是樣式,是整個頁面高度的控制和購物車區域的定位,這裡還有一個scroll-view高度自適應的方法,可以試一試,
flex 佈局 、scroll-view的容器設定 flex: auto; overflow: auto;

獲取詳細地理位置

618 京東到家-小程式也狂歡
開始做這個功能的時候,第一個想法是,這種東西肯定是有api支援的,翻文件去。 果然有:wx.getLocation(OBJECT),nice,那不就OK了嗎?你們哪,還是 naive,too young,too simple。仔細看,該方法只是返回的位置座標等資訊,並未返回地理位置名稱。

騰訊位置服務 提供的介面 SDK 可以解決問題。怎麼弄官方文件已經寫的很清楚了,接下來就看看我是怎麼應用的

首先,按照文件申請祕鑰,並下載 SDK,下載完成後,將檔案解壓到 utils 資料夾,這裡有兩個檔案,壓縮和不壓縮的,建議壓縮的。

然後,在需要獲取位置的檔案中引入

const QQMapWX = require('../../utils/qqmap-wx-jssdk')
const qqmapsdk
複製程式碼

接下來就可以使用SDK獲取詳細位置了,如下

<view class="location">
	<navigator class="par" hover-class="none" url="../location/location?page=1">
		<image src="../../assets/images/location.png"/>
        <!-- 顯示地址 -->
		<text>{{address}}</text>
	</navigator>
</view>
複製程式碼

index.js

onLoad(options) {
    /*判斷是第一次載入還是從position頁面返回
    如果從position頁面返回,會傳遞使用者選擇的地點*/
    if (app.globalData.address) {
        //設定變數 address 的值
        this.setData({
            address: app.globalData.address
        });
    } else {
      // 例項化API核心類
        qqmapsdk = new QQMapWX({
            //此key需要使用者自己申請
            key: 'JF2BZ-DDH65-DCUIH-QU3TN-FT4UF-EEFEH'
        });
        var that = this
        // 呼叫介面
        qqmapsdk.reverseGeocoder({
            success: function (res) {
                that.setData({
                    address: res.result.address     // 詳細地址
                })  
            },
            fail: function (res) {
            // console.log(res)
            },
            complete: function (res) {
            // console.log(res)
            }
        });
    }
},
複製程式碼

我的需求是,在進入程式時,自動獲取位置資訊,並顯示在頁面,所以直接放置在 onload 裡,success 的回撥函式返回的 res.result.address 就是詳細資訊了.

而我這裡另外做了一個判斷,是因為我還提供了一個使用者自主選擇地址的功能,地址顯示優先為使用者自主選擇的。有相同需求的小夥伴可以繼續看下去。

<view class="weui-cells weui_cell__nav-chooseLocation" bindtap="chooseLocation" style="display: {{page==1?'':'none'}};">
    <view class="weui-cell">
        <view class="weui-cell__hd">
            <image src="http://static-o2o.360buyimg.com/daojia/new/images/icon/location-eye@2x.png" style="margin-right: 5px;vertical-align: top;width:20px; height: 20px;"></image>
        </view>
        <view class="weui-cell__bd">點選定位當前地點</view>
        <view class="weui-cell__ft weui-cell__ft_in-access"></view>
    </view>
</view>   
複製程式碼

location.js

//選擇地點
chooseLocation: function () {
    wx.chooseLocation({
        success: res => {
            //選擇地點之後返回到首頁
            wx.switchTab({
                url: '/pages/index/index'
            })
            this.setData({
                address: res.address
            })
            // 設定全域性變數
            app.globalData.address = res.address
        },
        fail: err => {
            console.log(err)
        }
    })
},
複製程式碼

繫結的 catchtab 屬性觸發 chooseLocation 事件,wx.chooseLocation api 將直接返回當前位置,如果沒有,檢查是否開啟定位。將位置存入全域性變數,在返回首頁之後可見,地址已改為使用者選擇的。

這裡的頁面跳轉選擇 switchTab 而不是常用的 navigateToredirectTo,是因為小程式為了效能消耗考慮,不允許開啟超過 5 個的頁面,返回主頁 tabbar 使用 switchTab 是較好的選擇,而通過設定全域性變數 address 來傳遞地址資訊,也正是在這種情況下,比較適用的傳遞資料的方法之一。

搜尋-模糊查詢

618 京東到家-小程式也狂歡

看起來這個查詢有很多,比如關鍵字啊,熱搜啊,歷史記錄查啊,但實際上用的都是同一個查詢器,先來看看吧

  1. 搜尋頁面佈局 使用的是 weuisearchbar ,簡單實用
<view class="weui-search-bar">
    <view class="weui-search-bar__form">
        <view class="weui-search-bar__box">
            <icon class="weui-icon-search_in-box" type="search" size="14"></icon>
            <input type="text" class="weui-search-bar__input" placeholder="{{placeholder}}" value="{{inputVal}}" bindinput="inputTyping" />
            <view class="weui-icon-clear" wx:if="{{hasItems}}" bindtap="clearInput">
                <icon type="clear" size="14"></icon>
            </view>
        </view>
    </view>
    <view class="weui-search-bar__btn">
        <text class="weui-search-bar__text" catchtap="doSearch">搜尋</text>
    </view>
</view>
複製程式碼
  1. 使用 bindinput="inputTyping" ,它可以監測你的每一次輸入,並通過 e.detail.value 向你反饋出來,我們可以以此獲得關鍵字。
inputTyping: function (e) {
	this.setData({
		search: {
			inputVal: e.detail.value        // 由於我的搜尋是使用模板建立的,資料傳遞需要巢狀一下
	    },
	    input: e.detail.value,
    })
    this.showKeyList(e.detail.value)
},
複製程式碼
  1. weui-searchbar 自帶清空搜尋框,挺方便,接拿來用。
clearInput: function () {
	this.setData({
		search: {
			inputVal: ""
		},
	})
},
複製程式碼
  1. 接下來就是對關鍵字的處理
    將關鍵字字串用 split 方法,分割為字串陣列,在關鍵字列表中查詢每一個關鍵字元,使用 indexOf 方法檢索,如果存在這樣一個字元,就返回這個字元的下標,如果沒有將返回 -1,最後將檢索結果放入陣列中,這就是在搜尋框下部顯示的關鍵字列表了。
showKeyList(keyWords) {
	const key = keyWords ? keyWords.split('') : []
	const keyListAll = app.globalData.keyList
	const keyList = []
	keyListAll.forEach(item => {
		key.forEach(k => {
			let i = item.indexOf(k)
			if (i > -1) {
				keyList.push(item)
			}
		})
    })
	this.setData({
		search: {
			keyList,    // 查詢結果將在頁面顯示
		}
    })
},
複製程式碼
  1. 既然有了關鍵字,那麼就可以查詢資料 在這裡,我也是使用如上方法,分割字串,依次查詢,並且將這個方法封裝成一個函式,只需傳入要檢索的關鍵詞和目標陣列,就可以在任何地方呼叫它進行查詢。
// 關鍵字查詢
search(keyWords, arr) {
	const cur = []
	const desc = new Set()      // 使用 set 可以去重
	const key = keyWords ? keyWords.split('') : []
	arr.forEach(ele => {
		key.forEach(k => {
			let i = ele.title.indexOf(k)
			if (i > -1) {
				cur.push(ele.id)
				desc.add(ele)
			}
		})
	})
	this.setData({
		cur,
		search: {
		    desc: Array.from(desc)
		}
	})
},
// 資料查詢
searchItem(keyWords) {
    const goods = app.globalData.details,
        shops = app.globalData.shopInfo,
        temp = [],
        list = []

	goods.forEach(item => {
		item.desc.forEach(ele => {
			temp.push(ele)
		})			
    })
    // 呼叫搜尋
	this.search(keyWords, temp)
	shops.forEach(item => {
		this.data.cur.forEach(i => {
			if (item.goodslist.indexOf(i) > -1) {
				list.push(item)
			}
		})
	})
	var resList = Array.from(new Set([...list]))
	this.setData({
		search: {
			resList,
		    desc: this.data.search.desc,
		},
	})
	if (this.data.search.resList.length > 0) {
		this.setData({
			hasItems: true,
			search: {
				resList,
				desc: this.data.search.desc,
				hasItems: true,
			},
		})
	} else {
		this.setData({
			hasItems: false,
			search: {
				hasItems: false,
			},
		})
	}
},
複製程式碼
  1. 熱搜查詢和歷史記錄再次查詢 馬上就用到了剛才封裝的方法了, 這是熱搜和歷史記錄共用的查詢方法,只需一步呼叫
add_search(e) {
	const index = e.currentTarget.dataset.index
	this.setData({
		search: {
			inputVal: index,
		},
		input: index,
	})
	this.searchItem(index)
},
複製程式碼

其實查詢功能還可以精簡很多,只是我的偽資料陣列太多,需要聯合查詢,比較麻煩。感覺資料處理還是SQL語句方便多哈,多表查詢,模糊查詢。。。

以上

希望對大家有幫助

相關文章