最近沉迷小程序開發(fā),發(fā)現(xiàn)了一款功能、界面、體驗俱佳的小程序“旅行小賬本”。該小程序由騰訊旅游操刀制作,簡約大氣,功能性強。借著最近云開發(fā)的熱潮,著手做了個簡約版——"旅行小賬本"。效果比較滿意,畢竟前后臺一人單干。
Talk is cheap!
show~
IDE
小程序開發(fā)必然少不了微信開發(fā)者工具,再加上其對云開發(fā)的全面支持,再好不過的開發(fā)利器。但熟悉微信開發(fā)者工具的朋友們應該知道,它不支持Emmet縮寫語法,并且wxml的屬性值默認用單引號表示(強迫癥表示很難受)。
而VSCode很好的補足了微信開發(fā)者工具的不足之處,并且支持多元化插件開發(fā),輕量好用。
所以這里推薦采用微信開發(fā)者工具+VSCode配合開發(fā)。微信開發(fā)者工具負責調試、模擬小程序運行情況,VSCode負責代碼編輯工作。二者各司其職,會使開發(fā)更加的高效、便捷。
總體架構
該項目基于小程序云開發(fā),使用的模板是云開發(fā)快速啟動模板
由于是個全棧項目,前端使用小程序所支持的wxml + wxss + js開發(fā)模式,命名采用BEM命名規(guī)范。后臺則是借助云數(shù)據(jù)庫+云儲存進行數(shù)據(jù)管理。
項目總體結構
|-travelbook 項目名
|-cloudfunctions 云函數(shù)模塊
|-deleteItems 級聯(lián)刪除--云函數(shù)
|-getTime 獲取時間--云函數(shù)
|-miniprogram 項目模塊
|-components 自定義組件
|-accountCover 賬本封面組件
|-spendDetail 支出細節(jié)組件
|-pages 頁面
|-accountBooks 總賬本頁
|-accountCalendar 賬本日歷頁
|-accountDetail 支出細節(jié)頁
|-accountList 支出明細頁
|-accountPage 選定賬本頁
|-editAccount 賬本編輯頁
|-index 首頁
|-vant-weapp 有贊vant框架組件庫
|-··· 系列組件...
app.js 全局js
app.json 全局json配置
app.wxss 全局wxss
復制代碼
逆向工程
在做該小程序之前,有必要進行項目的逆向工程,進一步解構每一個頁面,從而深入了解這款小程序的交互細節(jié)。那么現(xiàn)在我假設自己為騰訊旅游的產(chǎn)品設計師,在繪制完界面原型后,撰寫了相應的交互文檔。當然解構過程中可能有些細節(jié)處理并沒有那么仔細到位...
以下是我繪制的界面原型
接下來對每個頁面的細節(jié)進行解構,并完成簡單的wxml結構
<!--switchList使用定位布局-->
<view bindtap="switchList" class="list"></view>
<!--newAccount使用flex布局-->
<view class="newAccount" bindtap="createNewAccount">
<view class="desc">旅行中的每一筆開支都有獨特的意義!</view>
<image src="{{}}"></image>
<view class="title">創(chuàng)建一個新賬本</view>
</view>
復制代碼
|
<!--整體用flex + 百分比布局-->
<input type="text" class="accuntName" placeholder="旅行賬本名稱" bindinput="getInput" />
<van-panel title="選擇封面" class="panel">
<van-row class="imageBox">
<!--使用wx:for遍歷數(shù)據(jù)庫賬本圖片信息-->
<van-col span="8" class="imgCol" bindtap="selectThis">
<image class="select" src="{{}}"></image>
</van-col>
<van-col span="8">
<view class="addBox" bindtap="useMore">更多封面</view>
</van-col>
</van-row>
</van-panel>
<button type="primary" bindtap="save">保存</button>
<button type="warn" bindtap="delete">刪除</button>
復制代碼
|
<view class="accountDesc" bindtap="viewDetail">
<!--使用wx:for遍歷數(shù)據(jù)庫賬本信息-->
<view class="accountName">
<view>{{}}</view>
<view class="accountTime">{{}}</view>
</view>
<!--絕對定位-->
<image class="updateImg" catchtap="editAccount" src="{{}}"></image>
</view>
復制代碼
|
<!--switchList使用定位布局-->
<view bindtap="switchList" class="list"></view>
<view class="account__list-year">{{}}</view>
<view class="account__list-new account__list-public" bindtap="createNewAccount">
<!--日期小圓點-->
<view class="account__list-point"></view>
<view class="account__list-time">{{}}</view>
<image src="{{}}"></image>
<view class="account__list-title">創(chuàng)建一個新賬本</view>
</view>
<!--使用wx:for遍歷數(shù)據(jù)庫賬本信息-->
<view class="account__list-item account__list-public" bindtap="viewDetail">
<!--日期小圓點-->
<view class="account__list-point"></view>
<image src="{{}}" mode="aspectFill"></image>
<view class="account__list-name">{{}}</view>
<view class="account__list-time">{{}}</view>
<image class="account__list-update" catchtap="editAccount" src="{{}}"></image>
</view>
|
<view class="account__spend">
<image bindtap="getCalendar" class="account__spend-calendar" src="{{}}"></image>
<view class="account__spend-text">
<view class="account__spend-total">總花費(元)</view>
<view class="account__spend-num">{{}}</view>
</view>
<image bindtap="accountAnalyze" class="account__spend-detail" src="{{}}"></image>
</view>
<view class="account__show-time">今天</view>
<view class="account__show-detail">
<view class="account__show-income account__show-public">
<view class="account__show-title">收入(元)</view>
<text class="account__show-in">+{{}}</text>
</view>
<view class="account__show-spend account__show-public">
<view class="account__show-title">支出(元)</view>
<text class="account__show-out">-{{}}</text>
</view>
</view>
<!--使用wx:for遍歷數(shù)據(jù)庫賬本信息-->
<view class="account__show-items-spend">
<view>
<image src="{{}}"></image>
</view>
<text>{{}}</text>
<text class="account__show-items-money">{{}}</text>
</view>
|
<!--日歷使用極點日歷的插件-->
<!--json中做配置-->
"usingComponents": {
"calendar": "plugin://calendar/calendar"
}
<!--js改變樣式-->
days_style.push({
month: 'current',
day: new Date().getDate(),
color: 'white',
background: '#e0a58e'
})
<!--wxml中引用-->
<calendar weeks-type="cn" cell-size="50" next="{{true}}" prev="{{true}}"
show-more-days="{{true}}" calendar-style="demo6-calendar"
header-style="calendar-header"board-style="calendar-board" active-type="rounded"
lunar="true" header-style="header"calendar-style="calendar"days-color="{{days_style}}">
</calendar>
|
<!--頂欄日期及收支結構-->
<view class="account__title">
<text class="account__title-time">{{}}</text>
<text class="account__title-spend">支出{{}}元 收入{{}}元</text>
</view>
<!--收支細節(jié)結構 使用flex彈性布局-->
<view class="account__detail">
<image src="{{}}"></image>
<view class="account__detail-name">{{}}</view>
<view class="account__detail-money">{{}}</view>
</view>
|
<!--使用vant框架的van-tabs組件-->
<!--并封裝自定義組件復用收支頁,自定義組件后面會詳細說明-->
<van-tabs active="{{ active }}" bind:change="onChange">
<van-tab title="支出">
<spendDetail detail="{{detail}}" accountKey="{{accountKey}}"></spendDetail>
</van-tab>
<van-tab title="收入">
<spendDetail detail="{{income}}" accountKey="{{accountKey}}"></spendDetail>
</van-tab>
</van-tabs>
復制代碼
|
云開發(fā)
在做完逆向工程的解構,頁面基礎結構基本搭建完成。但頁面依舊是靜態(tài)的,需要數(shù)據(jù)來填充。所以第二步就是數(shù)據(jù)庫的設計。而小程序的云控制臺恰好提供了數(shù)據(jù)的操作功能,為數(shù)據(jù)驅動提供基石。
云數(shù)據(jù)庫設計
云數(shù)據(jù)庫是一種NoSQL數(shù)據(jù)庫。每一張表是一個集合。值得注意的是在設計數(shù)據(jù)庫時, _id和 _openid 這兩個字段需要帶上。 _id 是表的主鍵,而 _openid 是用戶標識,每個用戶都有不同的 _openid ,可區(qū)分不同用戶。
以下是項目中的數(shù)據(jù)表設計
cover_photos 賬本封面表 用于存儲創(chuàng)建賬本時需要的封面信息
- _id
- _openid
- cover_index 封面索引
- cover_url 封面url
- isSelected 封面是否選中
復制代碼
|
accounts 賬本表 用于存儲用戶創(chuàng)建的賬本
- _id
- _openid
- accountKey 賬本唯一標識
- coverUrl 賬本封面
- i 賬本索引
- inputValue 賬本名字
- now 賬本創(chuàng)建時間
- spend 賬本總花費
復制代碼
|
account_detail 支出類型表 用于存儲消費類型
- _id
- _openid
- detail 類型細節(jié)
- pic_index 消費類型索引
- pic_url 未點擊時的圖片
- pic_url_act 點擊后的圖片
- type 消費類型
復制代碼
|
account_income 收入類型表 用于存儲收入類型
- _id
- _openid
- pic_index 收入類型索引
- pic_url 未點擊時的圖片
- pic_url_act 點擊后的圖片
- type 收入類型
復制代碼
|
spend_items 消費明細表
- _id
- _openid
- accountKey 賬本唯一標識
- address 消費地點
- desc 消費描述
- fullDate 消費時間
- money 消費金額
- pic_type 消費類型
- pic_url 消費類型圖片
復制代碼
|
云儲存管理
這是個非常實用的板塊。類似于百度云盤,它提供了文件存儲、上傳與下載功能。
除此之外,它還會將你所上傳的資源自動進行壓縮操作,并生成一個地址供你引用。該項目中的一些圖片資源就是存在于此,然后在云數(shù)據(jù)庫的字段中引用這些資源地址即可,十分方便,不必在本地存儲,占用小程序內存。
云函數(shù)設計
云函數(shù)簡單來說就是在云后端(Node.js)運行的代碼,本地看不到這些代碼的執(zhí)行過程,全封閉式只暴露接口供本地調用執(zhí)行,本地只需等待云端代碼執(zhí)行完畢后返回結果。這也是面向接口編程的思想體現(xiàn)。
項目中的云函數(shù)設計
// getTime 獲取當前時間并格式化為 yyyy-mm-dd
// 云函數(shù)入口文件
const cloud = require('wx-server-sdk')
// 初始化云函數(shù)
cloud.init()
// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {
var date = new Date()
var seperator1 = "-"
var year = date.getFullYear()
var month = date.getMonth() + 1
var strDate = date.getDate()
if (month >= 1 && month <= 9) {
month = "0" + month
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate
}
// 格式化當前時間
var currentdate = year + seperator1 + month + seperator1 + strDate
return currentdate
}
復制代碼
|
// deleteItems 批量刪除,云數(shù)據(jù)庫的批量刪除只允許在云函數(shù)中執(zhí)行
// 云函數(shù)入口文件
const cloud = require('wx-server-sdk')
// 初始化云函數(shù)
cloud.init()
// 連接云數(shù)據(jù)庫
const db = cloud.database()
const _ = db.command
// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {
try {
return await db.collection('spend_items')
.where({
accountKey: event.accountKey
})
.remove()
} catch (e) {
console.error(e)
}
}
|
MVVM
界面有了,數(shù)據(jù)有了。萬事俱備,只欠東風!所以下一步就是MVVM的設計。小程序本質就是基于MVVM所設計的,在MVVM的世界里,數(shù)據(jù)是靈魂,一切都由數(shù)據(jù)來驅動。
賬本頁顯示
賬本頁有兩種顯示的風格,左上角的按鈕可以來回切換風格,下拉可刷新頁面,顯示accounts數(shù)據(jù)表中存儲的賬本信息。顯示時有個小細節(jié),需要根據(jù)創(chuàng)建的時間先后來顯示,越晚創(chuàng)建的越先顯示。
// 頁面數(shù)據(jù)設計, 在wxml中使用{{}}符號引用數(shù)據(jù),數(shù)據(jù)就動態(tài)顯示到了頁面上
data: {
isList: false, // 轉換頁面風格的標識 true為豎向風格 false為橫向風格
accounts: [], // 存儲查詢的賬本數(shù)據(jù)
now: null, // 存儲當日時間
year: null // 存儲年份
}
// 轉換顯示風格
switchList() {
// 設置頁面風格樣式
let isList = !this.data.isList
this.setData({
isList
})
wx.setStorage({
key: "isList",
data: isList
})
}
// 獲取頁面風格轉換標識
var isList = wx.getStorageSync('isList')
// 查詢賬本
db.collection('accounts')
.get({
success: res => {
this.setData({
accounts: res.data.reverse(), // 反轉數(shù)組,優(yōu)先顯示創(chuàng)建早的賬本
isList
})
wx.hideLoading()
}
})
// 調用云函數(shù)接口 獲取當前日期
wx.cloud.callFunction({
// 云函數(shù)接口名就是創(chuàng)建的云函數(shù)名字,這里是'getTime'
name: 'getTime',
success: (res) => {
let year = res.result.split('-')[0]
this.setData({
now: res.result,
year
})
},
fail: console.error
})
|
賬本頁增刪改
賬本頁通過調用相應的云數(shù)據(jù)庫API,可進行一系列的增刪改操作。值得一提的是,修改時需要表單回顯,刪除時需要級聯(lián)刪除。因為一個賬本中有許多收支情況,spend_items表就是進行收支記錄,所以刪除賬本時需要級聯(lián)刪除對應的spend_items表中的收支信息。
一些重要的邏輯
-
封面單選邏輯
data: {
images: [], // 封面數(shù)組
selectImg: null, // 選擇其它封面
isSelected: {}, // 選中的圖片
inputValue: '', // 賬本名字
now: null, // 當前時間
account: {} // 傳入賬本信息
}
// 單選邏輯 通過構造{'0': isSelected}來實現(xiàn)
selectThis(e) {
let index = e.currentTarget.dataset.index
let coverUrl = e.currentTarget.dataset.coverurl
let is = this.data.isSelected[index]
let obj = {
coverUrl
}
// obj[index] 屬性動態(tài)改變
obj[index] = !is
obj.i = index
this.setData({
isSelected: obj
})
}
復制代碼
表單回顯邏輯
// 頁面加載時先通過對應的accountKey, 得到回顯信息
let { i, id, value, url, accountKey } = options
photos.get({
success: res => {
this.setData({
images: res.data,
account: {
id,
value,
url,
i,
accountKey
},
isSelected: obj
})
wx.hideLoading()
}
})
// 修改
save() {
let { id } = this.data.account
let { i, coverUrl, value } = this.data.isSelected
// 若沒修改 則為之前的value
let inputValue = this.data.inputValue || value
db.collection('accounts')
.doc(id)
.update({
data: {
inputValue,
coverUrl,
i
}
})
}
復制代碼
級聯(lián)刪除邏輯
db.collection('accounts')
.doc(this.data.account.id)
.remove()
.then(() => {
wx.hideLoading()
wx.showToast({
title: '刪除成功'
})
setTimeout(() => {
wx.reLaunch({
url: '../accountBooks/accountBooks'
})
}, 400)
})
// 調用deleteItems云函數(shù), 傳入對應accountKey主鍵, 通過云函數(shù)批量刪除
wx.cloud.callFunction({
name: 'deleteItems',
data: {
accountKey
}
})
復制代碼
|
賬本頁收支
因為收入與支出頁面基本類似,所以使用自定義組件封裝,可以復用。
// 封裝spendDetail組件
// 注冊組件
properties: {
detail: {
type: Object
},
accountKey: {
type: Number
},
isSpend: {
type: Boolean
}
}
// 引用組件
<van-tab title="支出">
<spendDetail detail="{{detail}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
</van-tab>
<van-tab title="收入">
<spendDetail detail="{{income}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
</van-tab>
|
收入與支出類型icon選擇使用兩個view來存放,通過選擇不同類型,跳轉不同的icon
// js
data: {
address: '',
money: 0,
desc: '',
selectPicIndex: 0,
selectIndex: 0
}
// 選擇消費類別
selectSpend(e) {
let { index } = e.currentTarget.dataset
let { selectPicIndex } = this.data
selectPicIndex = index
this.setData({
selectPicIndex
})
},
// 選擇消費類別中的細節(jié)
selectSpendDetail(e) {
let { index } = e.currentTarget.dataset
let { selectIndex } = this.data
selectIndex = index
this.setData({
selectIndex
})
}
// wxml
// 消費類型
<view class="expense">
<block wx:for="{{detail}}" wx:key="index">
<view class="expense__type" bindtap="selectSpend" data-index="{{index}}">
<block wx:if="{{selectPicIndex == item.pic_index}}">
<view class="expense__type-icon" style="background-color: #e64343">
<image src="{{item.pic_url_act}}"></image>
</view>
</block>
<block wx:else>
<view class="expense__type-icon">
<image src="{{item.pic_url}}"></image>
</view>
</block>
<view class="expense__type-name">{{item.type}}</view>
</view>
</block>
</view>
// 消費子類型
<view class="detail">
<block wx:for="{{detail[selectPicIndex].detail}}" wx:key="index">
<view class="detail__type" bindtap="selectSpendDetail" data-index="{{index}}">
<image class="detail__type-icon" src="{{item.detail_url}}"></image>
<block wx:if="{{selectIndex == item.detail_index}}">
<view class="detail__type-name" style="color: #f86319; border-bottom: 1rpx solid #f86319;">
{{item.detail_type}}
</view>
</block>
<block wx:else>
<view class="detail__type-name" style="border-bottom: 1rpx solid #e4e2e2;">
{{item.detail_type}}
</view>
</block>
</view>
</block>
</view>
|
賬本頁明細
因為收支明細中需要顯示每一天的消費信息,所以需要將數(shù)據(jù)表中的數(shù)據(jù)通過時間來分類,分成若干個數(shù)組,頁面從而使用wx:for來遍歷這些數(shù)組。在顯示之前,首先需要判斷有無收支信息。
// 通過時間分類算法 {} => [ [{時間1}], [{時間2}], [{時間3}] ]
arr.forEach(item => {
if (!_this.isExist(item.fullDate, dateArr)) {
dateArr.push([item])
} else {
dateArr.forEach(res => {
if (res[0].fullDate == item.fullDate) {
res.push(item)
}
})
}
})
// 使用map 方法構造 [{}, {}, {}, ...] 類型數(shù)組
dateArr = dateArr.map((item) => {
let spend = 0
let income = 0
item.forEach(res => {
if (res.money > 0) {
spend += res.money
} else {
income += (-res.money)
}
})
return {
item,
spend,
income
}
})
// 判斷自身是否存在數(shù)組中
isExist(item, arr) {
for (let i = 0; i < arr.length; i++) {
if (item == arr[i][0].fullDate)
return true
}
return false
}
|
以上是小程序中比較復雜的邏輯實現(xiàn)。
一點感悟
之前做項目時,只是在github提交時草草寫一句話當做提交日志。這次做了一個比較正式提交日志,做這個的初衷其實是為了監(jiān)督自己不要偷懶,堅持每天完成項目一部分,并總結不足之處。學而時習之才能成長的更快!
篇幅有限,奉上項目 如果你喜歡這篇文章或是這個項目,不妨進去點個Star支持下,有興趣的朋友歡迎Fork,一起探討知識或是旅行~~當然也希望您能留下一些寶貴的建議。感激不盡!
生活不止眼前的茍且,還有詩和遠方。最后要感謝騰訊旅游的各位大大設計出一個這么簡潔美觀大方的小程序產(chǎn)品,實屬良心之作!
|