Recently addicted to small program development, found a function, interface, experience of the best small program “travel ledger.” The small program produced by Tencent tourism, simple atmosphere, strong functionality. Taking advantage of the recent boom in cloud development, I set out to create a simplified version of the “travel ledger.” The effect is more satisfactory, after all, the front and back of a single person.

Talk is cheap!

show~

IDE

  • Wechat developer tools
  • VSCode

Small program development is inevitably not wechat developer tools, coupled with its comprehensive support for cloud development, but the best development tool. However, those familiar with wechat developer tools should know that it does not support the Emmet abbreviation syntax, and WXML attribute values are expressed in single quotes by default (ocD means uncomfortable). VSCode is a good complement to the shortcomings of wechat developer tools, and supports diversified plug-in development, lightweight and easy to use. Therefore, it is recommended to use wechat developer tools +VSCode for development. Wechat developer tools is responsible for debugging and simulating the running of small programs, while VSCode is responsible for code editing. Each has its own role, which will make development more efficient and convenient.

The overall architecture

This project is based on the small program cloud development, using the template of cloud development quick start template. Because it is a full stack project, the front end uses the small program supported by the WXML + WXSS + JS development mode, named using BEM naming specification. The background is the use of cloud database + cloud storage for data management.

Overall project structure

| | - travelbook project name - cloudfunctions cloud function module | | - deleteItems - cloud of cascade deletion function - getTime access time - cloud function module | | - miniprogram project - components Custom components | - accountCover books cover component | - | | - pages page spendDetail spending detail components - accountBooks general ledger this page | - | - accountDetail accountCalendar zhang this calendar page Spending details page | - detail accountList spending selected books | - | - accountPage editAccount books editing | | - index front page - vant - weapp has praised vant framework component library | - series components...... Json Global JSON configuration app.wxss Global WXSSCopy the code

Reverse engineering

Before making the applet, it is necessary to reverse engineer the project and further deconstruct each page to understand the interaction details of the applet. Now I assume that I am a product designer of Tencent Tourism. After drawing the interface prototype, I write the corresponding interactive document. Of course, there may be some details in the deconstruction process that are not as careful as they should be…

Here is a prototype of the interface I drew

<! --switchList uses positioning layout --> <view bindtap="switchList" class="list"></view> <! --newAccount uses Flex layout --> <view class="newAccount" bindtap="createNewAccount">
    <view class="desc""> < p style =" max-width: 100%; clear: both; </view> <image src="{{}}"></image>
    <view class="title"</view> </view>Copy the code

<! Use flex + % layout --> <inputtype="text" class="accuntName" placeholder="Travel Book Name" bindinput="getInput" />
  
<van-panel title="Choose the Cover" class="panel">
    <van-row class="imageBox"> <! - use 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"> more cover < / view > < / van - col > < / van - row > < / van - a panel > < buttontype="primary" bindtap="save"> Save </button> <buttontype="warn" bindtap="delete"> delete < / button >Copy the code

<view class="accountDesc" bindtap="viewDetail"> <! - use wx:for<view class= <view class="accountName">
        <view>{{}}</view>
        <view class="accountTime">{{}}</view> </view> <! --> <image class="updateImg" catchtap="editAccount" src="{{}}"></image>
</view>
Copy the code

<! --switchList uses positioning layout --> <view bindtap="switchList" class="list"></view>

<view class="account__list-year">{{}}</view>
<view class="account__list-new account__list-public" bindtap="createNewAccount"> <! -- date dot --> <view class="account__list-point"></view>
    <view class="account__list-time">{{}}</view>
    <image src="{{}}"></image>
    <view class="account__list-title"> Create a new ledger </view> </view> <! - use wx:for<view class= <view class="account__list-item account__list-public" bindtap="viewDetail"> <! -- date dot --> <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>
Copy the code

<view class="account__spend">
    <image bindtap="getCalendar" class="account__spend-calendar" src="{{}}"></image>
    <view class="account__spend-text">
        <view class="account__spend-total"> Total cost (yuan)</view> <view class="account__spend-num">{{}}</view>
    </view>
    <image bindtap="accountAnalyze" class="account__spend-detail" src="{{}}"></image>
</view>

<view class="account__show-time"> Today </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> <! - use wx:for<view class= <view class="account__show-items-spend">
    <view>
        <image src="{{}}"></image>
    </view>
    <text>{{}}</text>
    <text class="account__show-items-money">{{}}</text>
</view>
Copy the code

<! Calendar using pole calendar plugin --> <! -- Do configuration in JSON -->"usingComponents": {
    "calendar": "plugin://calendar/calendar"} <! Days_style.push ({month:'current',
  day: new Date().getDate(),
  color: 'white',
  background: '#e0a58e'}) <! -- WXML references --> <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>
Copy the code

<! -- top column date and revenue structure --> <view class="account__title">
    <text class="account__title-time">{{}}</text>
    <text class="account__title-spend"</text> </view> <! -- Use Flex for detail structure --> <view class="account__detail">
    <image src="{{}}"></image>
    <view class="account__detail-name">{{}}</view>
    <view class="account__detail-money">{{}}</view>
</view>
Copy the code

<! Using the Van-tabs component of the Vant framework --> <! -- and encapsulates the custom component reuse TAB, which is described later --> <van-tabs active="{{ active }}" bind:change="onChange">
  <van-tab title="Spending.">
    <spendDetail detail="{{detail}}" accountKey="{{accountKey}}"></spendDetail>
  </van-tab>
  <van-tab title="Income">
    <spendDetail detail="{{income}}" accountKey="{{accountKey}}"></spendDetail>
  </van-tab>
</van-tabs>
Copy the code

Development of cloud

In the completion of reverse engineering deconstruction, the page infrastructure is basically built. But the page is still static and needs to be filled with data. So the second step is the database design. The cloud console of the small program just provides the operation function of data, providing the cornerstone for data drive.

Cloud Database Design

A cloud database is a NoSQL database. Each table is a collection. It is worth noting that the _id and _openID fields need to be included when designing the database. _ID is the primary key of the table, and _openID is the user ID. Each user has a different _openID to distinguish different users.

The following is the data table design in the project

Cover_photos account cover table stores the cover information required for creating books. - _id - _openid - cover_index cover index - cover_url cover url - isSelected whether the cover isSelectedCopy the code
Accounts Account table Is used to store accounts created by users. -_id - _openID - accountKey Unique identifier of the account - coverUrl Account cover -i Account index - inputValue Account name - Now Account creation time - Spend a lot of money on the booksCopy the code
Account_detail Expenditure type table Used to store consumption type - _id - _openID - detail type details - PIC_index Index of consumption type - PIC_URL Picture before the click - pic_URl_act Picture after the click -typeConsumption typeCopy the code
Account_income Income type table Is used to store income types - _id - _openID - PIC_index Index of income types - PIC_URL Picture before the click - PIC_URL_act Picture after the click -typeIncome typeCopy the code
Spend_items Consumption list - _id - _openID-accountKey Unique ID of the account - address consumption place - desc consumption description - fullDate Consumption time - Money consumption amount - pic_type Consumption type - pic_URL Consumption type pictureCopy the code

Cloud Storage Management

This is a very useful section. Similar to Baidu cloud Disk, it provides file storage, upload and download functions.

Cloud function design

Cloud functions simply refer to the code running in the cloud backend (Node.js), and the execution process of these codes cannot be seen locally. The fully closed only exposes the interface for local invocation, and the local only needs to wait for the result to be returned after the cloud code is executed. This is also the idea of interface – oriented programming.

Cloud function design in the project

// getTime to get the current time and format it as YYYY-MM-DD'wx-server-sdk'Main = async (event, context) => {var date = new date () var seperator1 => {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"Var CurrentDate = year + Seperator1 + month + Seperator1 + strDatereturn currentdate
}
Copy the code
// cloud function entry file // cloud function entry file const cloud = require('wx-server-sdk'Cloud.init () // Connect to cloud database const db = cloud.database() const _ = db.mand // cloud function exports.main = async (event, context) => { try {return await db.collection('spend_items')
      .where({
        accountKey: event.accountKey
      })
      .remove()
  } catch (e) {
    console.error(e)
  }
}
Copy the code

MVVM

We have the interface, we have the data. Everything is ready except the east wind! So the next step is MVVM design. Applets are essentially based on MVVM design, in the MVVM world, data is the soul, everything is driven by data.

The ledger page shows

The ledger page can be displayed in two styles, with a button in the upper left corner toggled back and forth, and a drop-down to refresh the page to show the ledger information stored in the Accounts data table. There is a small detail that needs to be displayed according to the time of creation. The later the creation, the earlier the display.

Data: {isList: {isList: {isList: {isList: {isList:false// Convert the page style identitytrueVertical stylefalseFor horizontal style accounts: [], // Store query ledger data now: NULL, // store date time year: null // Store year} // convert display styleswitchList() {// Set the page style styleletisList = ! this.data.isList this.setData({ isList }) wx.setStorage({ key:"isList"Var isList = wx.getStoragesync (data: isList})}'isList'Db.collection ();'accounts') .get({ success: res => { this.setData({ accounts: Res.data.reverse (), // reverse the array, IsList}) wx.hideloading ()}}) // Call the cloud function interface to get the current date wx.cloud.callFunction({// cloud function interface name is the name of the created cloud function, here is'getTime'
    name: 'getTime',
    success: (res) => {
    let year = res.result.split(The '-')[0]
    this.setData({
      now: res.result,
      year
    })
    },
    fail: console.error
})
Copy the code

Account page additions and deletions

The account page can be added, deleted or modified by calling the corresponding cloud database API. It is worth mentioning that form echo is required for modification and cascading deletion is required for deletion. Because there are many items in an account, the spend_items table is used to record the items. Therefore, deleting the account requires cascading deletion of the items in the corresponding SPend_items table.

Some important logic

  • Cover radio logic
    Data: {images: [], // Cover array selectImg: null, // Select other cover isSelected: {}, // selected image inputValue:' ', // account name now: null, // current time account: {}'0': isSelected} to implement selectThis(e) {let index = e.currentTarget.dataset.index
        let coverUrl = e.currentTarget.dataset.coverurl
        let is = this.data.isSelected[index]
        letObj = {coverUrl} // obj[index] property dynamically changes obj[index] =! is obj.i = index this.setData({ isSelected: obj }) }Copy the code
  • Form echo logic
    // During page loading, the accountKey is used to obtain the outputlet{ 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 ()}}) // Modifysave() {
        let { id } = this.data.account
        let{I, coverUrl, value} = this.data.isSelected // Use the same value if the value is not changedlet inputValue = this.data.inputValue || value
        
        db.collection('accounts')
          .doc(id)
          .update({
            data: {
                inputValue,
                coverUrl,
                i
            }
        })
    }
    Copy the code
  • Cascading deletion logic
    db.collection('accounts')
        .doc(this.data.account.id)
        .remove()
        .then(() => {
          wx.hideLoading()
          wx.showToast({
            title: 'Deleted successfully'
          })
          setTimeout(() => {
            wx.reLaunch({
              url: '.. /accountBooks/accountBooks'})}, 400)}) wx.cloud.callFunction({name: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey: {accountKey:'deleteItems',
        data: {
          accountKey
        }
      })
    Copy the code

Account book receipts and expenditures

Because the income and expense pages are basically similar, they are wrapped with custom components that can be reused.

// Encapsulate spendDetail component // register component properties: {detail: {type: Object
    },
    accountKey: {
      type: Number
    },
    isSpend: {
      type: Boolean}} // Reference component <van-tab title="Spending.">
    <spendDetail detail="{{detail}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
  </van-tab>
  <van-tab title="Income">
    <spendDetail detail="{{income}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
</van-tab>
Copy the code

Income and expenditure type icon choose to use two views to store, by selecting different types, jump to different ICONS

// js
data: {
    address: ' ',
    money: 0,
    desc: ' ', selectPicIndex: 0, selectIndex: 0}let { index } = e.currentTarget.dataset
  let{ selectPicIndex } = this.data selectPicIndex = index this.setData({ selectPicIndex }) }, // Select details in the consumption category selectSpendDetail(e) {let { index } = e.currentTarget.dataset
  let{selectIndex} = this.data selectIndex = index this.setData({selectIndex})} // WXML // Consumption type <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 > subtype < < / view > / / consumption 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>
Copy the code

Account page details

Because the expenditure details need to display the consumption information of each day, the data in the data table needs to be classified by time and divided into several arrays. The page then uses WX :for to traverse these arrays. Before display, first need to determine whether there is income and expenditure information.

/ / the time classification algorithm is {} = > [[{1} time], [time {2}], [time {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)}})}}) / / use the map method structure [{}, {}, {},... 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
  }
Copy the code

The above is a small program in the more complex logic implementation.

A little feeling

Submit logs github.com/FightingHao…

When I was working on a project, I just scribbled a sentence on github as a commit log. I made a formal submission log this time. The original intention of doing this was to supervise myself not to be lazy, to finish part of the project every day and to summarize the shortcomings. Learning and learning can grow faster!

If you like this article or this project, you might as well click on the Star support, interested friends welcome Fork, discuss knowledge or travel ~~ of course, also hope you can leave some valuable suggestions. Thank you so much!

Life is not only the casual, there are poems and distant. Finally, we would like to thank you Tencent tourism greatly designed such a simple and beautiful small program products, it is conscience!