诗和远方:旅行小账本丨云开发实战

196 阅读12分钟
原文链接: mp.weixin.qq.com

前言

最近沉迷小程序开发,发现了一款功能、界面、体验俱佳的小程序“旅行小账本”。该小程序由腾讯旅游操刀制作,简约大气,功能性强。借着最近云开发的热潮,着手做了个简约版——"旅行小账本"。效果比较满意,毕竟前后台一人单干。

IDE

  • 微信开发者工具

  • VSCode

小程序开发必然少不了微信开发者工具,再加上其对云开发的全面支持,再好不过的开发利器。但熟悉微信开发者工具的朋友们应该知道,它不支持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结构:

    <!--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>

二、云开发

在做完逆向工程的解构,页面基础结构基本搭建完成。但页面依旧是静态的,需要数据来填充。所以第二步就是数据库的设计。而小程序的云控制台恰好提供了数据的操作功能,为数据驱动提供基石。

1.云数据库设计

云数据库是一种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 消费类型图片

2.云储存管理

这是个非常实用的板块。类似于百度云盘,它提供了文件存储、上传与下载功能:

除此之外,它还会将你所上传的资源自动进行压缩操作,并生成一个地址供你引用 。该项目中的一些图片资源就是存在于此,然后在云数据库的字段中引用这些资源地址即可,十分方便,不必在本地存储,占用小程序内存:

3.云函数设计

云函数简单来说就是在云后端(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所设计的,在MVVM的世界里,数据是灵魂,一切都由数据来驱动。

1.【账本页】显示

账本页有两种显示的风格,左上角的按钮可以来回切换风格,下拉可刷新页面,显示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

    })

2.【账本页】增删改

账本页通过调用相应的云数据库API,可进行一系列的增删改操作。值得一提的是,修改时需要表单回显,删除时需要级联删除。因为一个账本中有许多收支情况,spenditems表就是进行收支记录,所以删除账本时需要级联删除对应的spenditems表中的收支信息。

3.【支出】与【收入】页面设计

因为收入与支出页面基本类似,所以使用自定义组件封装,可以复用:

    // 封装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>

4.【账本明细】页面

因为账本明细中需要显示每一天的消费信息,所以需要将数据表中的数据通过时间来分类,分成若干个数组,页面从而使用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

    }

以上是小程序中比较复杂的逻辑实现。

总结

篇幅有限,奉上项目github 如果你喜欢这篇文章或是这个项目,不妨进去点个Star支持下,有兴趣的朋友欢迎Fork,一起探讨知识或是旅行~~当然也希望您能留下一些宝贵的建议。感激不尽!

如果你有关于使用云开发TCB相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦!比心!


(关注了我可以说是相当优秀了)

点“阅读原文”直达项目github~

如果喜欢这篇文章或是这个项目,记得进去点个Star支持下哦~