最近沉迷小程序开发,发现了一款功能、界面、体验俱佳的小程序“旅行小账本”。该小程序由腾讯旅游操刀制作,简约大气,功能性强。借着最近云开发的热潮,着手做了个简约版——"旅行小账本"。效果比较满意,毕竟前后台一人单干。
Talk is cheap!
show~
小程序开发必然少不了微信开发者工具,再加上其对云开发的全面支持,再好不过的开发利器。但熟悉微信开发者工具的朋友们应该知道,它不支持Emmet缩写语法,并且wxml的属性值默认用单引号表示(强迫症表示很难受)。
而VSCode很好的补足了微信开发者工具的不足之处,并且支持多元化插件开发,轻量好用。
所以这里推荐采用微信开发者工具+VSCode配合开发。微信开发者工具负责调试、模拟小程序运行情况,VSCode负责代码编辑工作。二者各司其职,会使开发更加的高效、便捷。
该项目基于小程序云开发,使用的模板是云开发快速启动模板
由于是个全栈项目,前端使用小程序所支持的wxml + wxss + js开发模式,命名采用BEM命名规范。后台则是借助云数据库+云储存进行数据管理。
项目总体结构
|-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 复制代码
在做该小程序之前,有必要进行项目的逆向工程,进一步解构每一个页面,从而深入了解这款小程序的交互细节。那么现在我假设自己为腾讯旅游的产品设计师,在绘制完界面原型后,撰写了相应的交互文档。当然解构过程中可能有些细节处理并没有那么仔细到位...
以下是我绘制的界面原型
接下来对每个页面的细节进行解构,并完成简单的wxml结构
复制代码 旅行中的每一笔开支都有独特的意义! 创建一个新账本
复制代码 更多封面
复制代码 {{}} {{}}
{{}} {{}} 创建一个新账本 复制代码 {{}} {{}}
总花费(元) {{}} 今天 收入(元) +{{}} 支出(元) -{{}} 复制代码 {{}} {{}}
"usingComponents": { "calendar": "plugin://calendar/calendar" } days_style.push({ month: 'current', day: new Date().getDate(), color: 'white', background: '#e0a58e' })复制代码
{{}} 支出{{}}元 收入{{}}元 复制代码 {{}} {{}}
复制代码
在做完逆向工程的解构,页面基础结构基本搭建完成。但页面依旧是静态的,需要数据来填充。所以第二步就是数据库的设计。而小程序的云控制台恰好提供了数据的操作功能,为数据驱动提供基石。
云数据库是一种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 } } // 引用组件 复制代码
收入与支出类型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 // 消费类型// 消费子类型 {{item.type}} 复制代码 {{item.detail_type}} {{item.detail_type}}
// 通过时间分类算法 {} => [ [{时间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 } 复制代码
以上是小程序中比较复杂的逻辑实现。
提交日志github.com/FightingHao…
之前做项目时,只是在github提交时草草写一句话当做提交日志。这次做了一个比较正式提交日志,做这个的初衷其实是为了监督自己不要偷懒,坚持每天完成项目一部分,并总结不足之处。学而时习之才能成长的更快!
篇幅有限,奉上项目github如果你喜欢这篇文章或是这个项目,不妨进去点个Star支持下,有兴趣的朋友欢迎Fork,一起探讨知识或是旅行~~当然也希望您能留下一些宝贵的建议。感激不尽!
生活不止眼前的苟且,还有诗和远方。最后要感谢腾讯旅游的各位大大设计出一个这么简洁美观大方的小程序产品,实属良心之作!