Поэзия и расстояние: актуальная разработка облачного журнала путешествий

внешний интерфейс WeChat
Поэзия и расстояние: актуальная разработка облачного журнала путешествий

Недавно я увлекся разработкой небольших программ, и нашел небольшую программу "Путеводитель" с отличными функциями, интерфейсом и опытом. Этот апплет предоставленТенсент ПутешествияСделано ножом, просто и атмосферно, и функционально. недавнимоблачная разработкаВсплеск России, начал делать простую версию - "путешествие небольшой бухгалтерской книги". Эффект вполне удовлетворительный, ведь фронт и бэк офис работают поодиночке.

Говорить дешево!
show~

IDE

Инструменты разработчика WeChat незаменимы для разработки небольших программ, а в сочетании с всесторонней поддержкой облачной разработки это идеальный инструмент разработки. Но друзья, знакомые с инструментами разработчика WeChat, должны знать, что он не поддерживаетЭммет Аббревиатура Синтаксис, а значение атрибута wxml по умолчанию обозначается одинарными кавычками (обсессивно-компульсивное расстройство означает дискомфорт).
А VSCode компенсирует недостатки инструментов разработчика WeChat и поддерживает диверсификацию.Разработка плагина, легкий и простой в использовании.
Поэтому для разработки рекомендуется использовать инструменты разработчика WeChat + VSCode. Инструмент разработчика WeChat отвечает за отладку и моделирование работы апплета, а 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

Обратный инжиниринг

Перед выполнением этого апплета необходимо провести реинжиниринг проекта для дальнейшей деконструкции каждой страницы, чтобы получить более глубокое понимание деталей взаимодействия этого апплета. Итак, теперь я предполагаю, что являюсь продуктовым дизайнером Tencent Travel, и, нарисовав прототип интерфейса, написал соответствующий интерактивный документ. Конечно, некоторые детали могут быть не так тщательно обработаны в процессе деконструкции...

Ниже приведен прототип интерфейса, который я нарисовал.

Затем разберите детали каждой страницы и завершите простую структуру 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>

облачная разработка

После завершения деконструкции обратного проектирования базовая структура страницы в основном завершена. Но страница по-прежнему статична и требует данных для заполнения. Итак, второй шаг — это проектирование базы данных. Облачная консоль апплета просто обеспечивает функцию работы с данными и обеспечивает краеугольный камень для управления данными.

Дизайн облачной базы данных

Облачная база данных — это база данных 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      消费类型图片

Управление облачным хранилищем

Это очень полезная доска. похожий наОблачный диск Baidu, который обеспечивает хранение файлов, функции загрузки и скачивания.

Кроме того, он автоматически сжимает загружаемые вами ресурсы и генерирует адрес для справки. Некоторые ресурсы изображений в этом проекте существуют здесь, и потом вы можете ссылаться на адреса этих ресурсов в полях облачной базы данных, что очень удобно, и не нужно хранить локально и занимает память апплета.

Дизайн облачных функций

облачная функцияПроще говоря, это код, работающий в облачном бэкенде (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 данные — это душа, и все управляется данными.

отображение страницы бухгалтерской книги

Страница бухгалтерской книги имеет два стиля отображения. Кнопка в верхнем левом углу может переключать стили вперед и назад. Потяните вниз, чтобы обновить страницу, чтобы отобразить информацию бухгалтерской книги, хранящуюся в таблице данных счетов. В отображении есть небольшая деталь, которую нужно отображать по времени создания, чем позже она создана, тем больше она отображается.

// 页面数据设计, 在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 облачной базы данных. Стоит отметить, что для модификации требуется эхо формы, а для удаления требуется каскадное удаление. Поскольку в бухгалтерской книге много квитанций и платежей, для записи квитанций и платежей используется таблица расходов_элементов, поэтому при удалении книги необходимо каскадно удалить информацию о квитанциях и платежах в соответствующей таблице расходов_элементов.

какая-то важная логика

  • Крышка радиологики
    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>

Значки типа дохода и расхода выбираются с использованием двух представлений для сохранения и, выбирая разные типы, переходят к другим значкам.

// 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
  }

Выше приведена более сложная логическая реализация в апплете.

немного понимания

журнал фиксацииGitHub.com/fighting ха О…

Когда я раньше работал над проектом, я просто написал предложение на github в виде журнала коммитов. На этот раз я сделал более формальный журнал отправки, первоначальная цель которого — контролировать себя, чтобы не быть ленивым, настаивать на выполнении части проекта каждый день и подводить итоги недостатков. Учись и учись расти быстрее!

Количество мест ограничено, вот проектgithubЕсли вам нравится эта статья или этот проект, вы можете зайти и нажать на звездочку, чтобы поддержать его.Заинтересованные друзья приветствуют вилку для обсуждения знаний или совместного путешествия~~ Конечно, я надеюсь, что вы можете оставить несколько ценных предложений. Благодарный!

Жизнь – это не только суета, но и поэзия, и расстояние. Наконец, я хотел бы поблагодарить всех в Tencent Travel за разработку такого простого, красивого и щедрого апплета, который действительно является делом совести!