原创:豪豪javascript
最近沉迷小程序开发,发现了一款功能、界面、体验俱佳的小程序“旅行小帐本”。着手作了个简约版——"旅行小帐本"。效果比较满意,毕竟先后台一人单干。html
小程序开发必然少不了微信开发者工具,再加上其对云开发的全面支持,再好不过的开发利器。但熟悉微信开发者工具的朋友们应该知道,它不支持Emmet缩写语法,而且wxml的属性值默认用单引号表示(强迫症表示很难受)。 而VSCode很好的补足了微信开发者工具的不足之处,而且支持多元化插件开发,轻量好用。 因此这里推荐采用微信开发者工具+VSCode配合开发。微信开发者工具负责调试、模拟小程序运行状况,VSCode负责代码编辑工做。两者各司其职,会使开发更加的高效、便捷。前端
该项目基于小程序云开发,使用的模板是云开发快速启动模板。 因为是个全栈项目,前端使用小程序所支持的wxml + wxss + js开发模式,命名采用BEM命名规范。后台则是借助云数据库+云储存进行数据管理。java
项目整体结构算法
|-travelbook 项目名
|-cloudfunctions 云函数模块
|-deleteItems 级联删除--云函数
|-getTime 获取时间--云函数
|-miniprogram 项目模块
|-components 自定义组件
|-accountCover 帐本封面组件
|-spendDetail 支出细节组件
|-pages 页面
|-accountBooks 总帐本页
|-accountCalendar 帐本日历页
|-accountDetail 支出细节页
|-accountList 支出明细页
|-accountPage 选定帐本页
|-editAccount 帐本编辑页
|-index 首页
|-vant-weapp 有赞vant框架组件库
|-··· 系列组件...
app.js 全局js
app.json 全局json配置
app.wxss 全局wxss
复制代码
在作该小程序以前,有必要进行项目的逆向工程,进一步解构每个页面,从而深刻了解这款小程序的交互细节。那么如今我假设本身为腾讯旅游的产品设计师,在绘制完界面原型后,撰写了相应的交互文档。固然解构过程当中可能有些细节处理并无那么仔细到位...数据库
如下是我绘制的界面原型 编程
<!--switchList使用定位布局-->
<view bindtap="switchList" class="list"></view>
<!--newAccount使用flex布局-->
<view class="newAccount" bindtap="createNewAccount">
<view class="desc">旅行中的每一笔开支都有独特的意义!</view>
<image src="{{}}"></image>
<view class="title">建立一个新帐本</view>
</view>
复制代码
<!--总体用flex + 百分比布局-->
<input type="text" class="accuntName" placeholder="旅行帐本名称" bindinput="getInput" />
<van-panel title="选择封面" class="panel">
<van-row class="imageBox">
<!--使用wx:for遍历数据库帐本图片信息-->
<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遍历数据库帐本信息-->
<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">建立一个新帐本</view>
</view>
<!--使用wx:for遍历数据库帐本信息-->
<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遍历数据库帐本信息-->
<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>
<!--收支细节结构 使用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>
复制代码
在作完逆向工程的解构,页面基础结构基本搭建完成。但页面依旧是静态的,须要数据来填充。因此第二步就是数据库的设计。而小程序的云控制台刚好提供了数据的操做功能,为数据驱动提供基石。json
云数据库是一种NoSQL数据库。每一张表是一个集合。值得注意的是在设计数据库时,_id
和_openid
这两个字段须要带上。_id
是表的主键,而_openid
是用户标识,每一个用户都有不一样的_openid
,可区分不一样用户。小程序
如下是项目中的数据表设计后端
cover_photos 帐本封面表 用于存储建立帐本时须要的封面信息
- _id
- _openid
- cover_index 封面索引
- cover_url 封面url
- isSelected 封面是否选中
复制代码
accounts 帐本表 用于存储用户建立的帐本
- _id
- _openid
- accountKey 帐本惟一标识
- coverUrl 帐本封面
- i 帐本索引
- inputValue 帐本名字
- now 帐本建立时间
- spend 帐本总花费
复制代码
account_detail 支出类型表 用于存储消费类型
- _id
- _openid
- detail 类型细节
- 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 消费类型图片
复制代码
这是个很是实用的板块。相似于百度云盘,它提供了文件存储、上传与下载功能。
云函数简单来讲就是在云后端(Node.js)运行的代码,本地看不到这些代码的执行过程,全封闭式只暴露接口供本地调用执行,本地只需等待云端代码执行完毕后返回结果。这也是面向接口编程的思想体现。
项目中的云函数设计
// getTime 获取当前时间并格式化为 yyyy-mm-dd
// 云函数入口文件
const cloud = require('wx-server-sdk')
// 初始化云函数
cloud.init()
// 云函数入口函数
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 批量删除,云数据库的批量删除只容许在云函数中执行
// 云函数入口文件
const cloud = require('wx-server-sdk')
// 初始化云函数
cloud.init()
// 链接云数据库
const db = cloud.database()
const _ = db.command
// 云函数入口函数
exports.main = async (event, context) => {
try {
return await db.collection('spend_items')
.where({
accountKey: event.accountKey
})
.remove()
} catch (e) {
console.error(e)
}
}
复制代码
界面有了,数据有了。万事俱备,只欠东风!因此下一步就是MVVM的设计。小程序本质就是基于MVVM所设计的,在MVVM的世界里,数据是灵魂,一切都由数据来驱动。
帐本页有两种显示的风格,左上角的按钮能够来回切换风格,下拉可刷新页面,显示accounts数据表中存储的帐本信息。显示时有个小细节,须要根据建立的时间前后来显示,越晚建立的越先显示。
// 页面数据设计, 在wxml中使用{{}}符号引用数据,数据就动态显示到了页面上
data: {
isList: false, // 转换页面风格的标识 true为竖向风格 false为横向风格
accounts: [], // 存储查询的帐本数据
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(), // 反转数组,优先显示建立早的帐本
isList
})
wx.hideLoading()
}
})
// 调用云函数接口 获取当前日期
wx.cloud.callFunction({
// 云函数接口名就是建立的云函数名字,这里是'getTime'
name: 'getTime',
success: (res) => {
let year = res.result.split('-')[0]
this.setData({
now: res.result,
year
})
},
fail: console.error
})
复制代码
帐本页经过调用相应的云数据库API,可进行一系列的增删改操做。值得一提的是,修改时须要表单回显,删除时须要级联删除。由于一个帐本中有许多收支状况,spend_items表就是进行收支记录,因此删除帐本时须要级联删除对应的spend_items表中的收支信息。
一些重要的逻辑
data: {
images: [], // 封面数组
selectImg: null, // 选择其它封面
isSelected: {}, // 选中的图片
inputValue: '', // 帐本名字
now: null, // 当前时间
account: {} // 传入帐本信息
}
// 单选逻辑 经过构造{'0': isSelected}来实现
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] 属性动态改变
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 } }) } ```
db.collection('accounts')
.doc(this.data.account.id)
.remove()
.then(() => {
wx.hideLoading()
wx.showToast({
title: '删除成功'
})
setTimeout(() => {
wx.reLaunch({
url: '../accountBooks/accountBooks'
})
}, 400)
})
// 调用deleteItems云函数, 传入对应accountKey主键, 经过云函数批量删除
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
})
},
// 选择消费类别中的细节
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>
复制代码
由于收支明细中须要显示每一天的消费信息,因此须要将数据表中的数据经过时间来分类,分红若干个数组,页面从而使用wx:for来遍历这些数组。在显示以前,首先须要判断有无收支信息。
// 经过时间分类算法 {} => [ [{时间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 方法构造 [{}, {}, {}, ...] 类型数组
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
}
})
// 判断自身是否存在数组中
isExist(item, arr) {
for (let i = 0; i < arr.length; i++) {
if (item == arr[i][0].fullDate)
return true
}
return false
}
复制代码
以上是小程序中比较复杂的逻辑实现。
运用云开发,开发一份专属本身的旅行小帐本吧~