• 小程序-demo:小熊の日记


    ylbtech-小程序-demo:小熊の日记

    1、CHANGELOG.md

    # 2016-10-12
    
    * 更新开发者工具至`v0.10.101100`
    * 修改`new`页的数据绑定方式 & 修改多行文本框输入时的bug
    
    # 2016-10-13
    
    * 完善日志编辑页

    2、README.md

    # 微信小程序之小熊の日记 #
    
    ## 关于 ##
    
    * 我是一名后端程序员,做这个仅仅是因为觉得微信小程序好玩;
    * 没有明确的产品意图,东抄抄西抄抄只是为了验证和学习微信小程序;
    * 大体是想做一个个人/家庭日常记录的app;
    * 持续开发中,有兴趣请持续关注
    
    ## 预览 ##
    
    * 概览
    
    <p align="center">
        <img src="./files/preview.gif" alt="截频演示" width="100%">
    </p>
    
    ## 功能特点 ##
    
    * 功能完备,实用导向
    * Server端API支持
    * 涵盖众多组件、API使用,适用于学习微信小程序
    * 多行文本模拟实现
    * tab切换
    * 模态框
    * 本地数据组织及存储
    * 图片预览功能
    
    ## 使用步骤
    
    1. 克隆代码:
    
    ```bash
    $ cd path/to/your/workspace
    $ git clone https://github.com/harveyqing/BearDiary.git
    ```
    
    2. 打开`微信Web开放者工具`(注意:须`v0.10.101100`及以上版本)
    
    3. 添加项目
    
        * AppID:选`无AppID`
        * 项目名称:任意填写
        * 项目目录:path/to/your/workspace
        * 点击 `添加项目`
    
    ## 开发计划 ##
    
    - [ ] 开发server端API接口
    - [ ] 完成日记撰写页
    - [ ] 添加评论、喜欢、收藏功能
    - [ ] 规范`coding style`
    
    ## 小程序开发相关资源 ##
    
    ### 开发者工具下载 ###
    
    > 最新版本 0.10.101100
    
    - [windows 64](https://servicewechat.com/wxa-dev-logic/download_redirect?type=x64&from=mpwiki&t=1476434677599)
    - [windows 32](https://servicewechat.com/wxa-dev-logic/download_redirect?type=ia32&from=mpwiki&t=1476434677599)
    - [mac](https://servicewechat.com/wxa-dev-logic/download_redirect?type=darwin&from=mpwiki&t=1476434677599)
    
    ### 开发者文档 ###
    
    - [微信官方文档](https://mp.weixin.qq.com/debug/wxadoc/dev/)
    
    ### 最好的资源集 ###
    
    - [justjavac/awesome-wechat-weapp](https://github.com/justjavac/awesome-wechat-weapp)
    
    ## Anyway, 欢迎PR ##
    
    ## LICENSE ##
    
    [MIT](./LICENSE)

    3、

    1.返回顶部
    0、
         
     
    1、app.js
    // app.js
    
    const config = require('config');
    const diaries = require('demo/diaries');
    
    App({
    
      onLaunch: function () {
      },
    
      // 获取用户信息
      getUserInfo: function(cb) {
        var that = this;
    
        if (this.globalData.userInfo) {
          typeof cb == 'function' && cb(this.globalData.userInfo)
        } else {
          // 先登录
          wx.login({
            success: function() {
              wx.getUserInfo({
                success: (res) => {
                  that.globalData.userInfo = res.userInfo;
                  typeof cb == 'function' && cb(that.globalData.userInfo)
                }
              })
            }
          })
        }
      },
    
      // 获取本地全部日记列表
      getDiaryList(cb) {
        var that = this;
    
        if (this.globalData.diaryList) {
          typeof cb == 'function' && cb(this.globalData.diaryList);
        } else {
          let list = [];
    
          this.getLocalDiaries(storage => {
            // 本地缓存数据
            for (var k in storage) {
              list.push(storage[k]);
            }
          });
    
          // 本地假数据
          list.push(...diaries.diaries);
          that.globalData.diaryList = list;
          typeof cb == 'function' && cb(that.globalData.diaryList)
        }
      },
    
      // 获取本地日记缓存
      getLocalDiaries(cb) {
        var that = this;
    
        if (this.globalData.localDiaries) {
          typeof cb == 'function' && cb(this.globalData.localDiaries);
        } else {
          wx.getStorage({
            key: config.storage.diaryListKey,
            success: (res) => {
              that.globalData.localDiaries = res.data;
              typeof cb == 'function' && cb(that.globalData.localDiaries);
            },
            fail: (error) => {
              that.globalData.localDiaries = {};
              typeof cb == 'function' && cb(that.globalData.localDiaries);
            }
          });
        }
      },
    
      // 获取当前设备信息
      getDeviceInfo: function(callback) {
        var that = this;
    
        if (this.globalData.deviceInfo) {
          typeof callback == "function" && callback(this.globalData.deviceInfo)
        } else {
          wx.getSystemInfo({
            success: function(res) {
              that.globalData.deviceInfo = res;
              typeof callback == "function" && callback(that.globalData.deviceInfo)
            }
          })
        }
      },
    
      globalData: {
        // 设备信息,主要用于获取屏幕尺寸而做适配
        deviceInfo: null,
    
        // 本地日记缓存列表 + 假数据
        // TODO 真实数据同步至服务端,本地只做部分缓存
        diaryList: null,
    
        // 本地日记缓存
        localDiaries: null,
    
        // 用户信息
        userInfo: null,
      }
    
    })
    2、app.json
    {
      "pages":[
        "pages/list/list",
        "pages/mine/mine",
        "pages/new/new",
        "pages/entry/entry"
      ],
      "window":{
        "backgroundTextStyle": "light",
        "navigationBarBackgroundColor": "#39b5de",
        "navigationBarTitleText": "小熊の日记",
        "navigationBarTextStyle": "white",
        "backgroundColor": "#eceff4"
      },
      "tabBar": {
        "color": "#858585",
        "selectedColor": "#39b5de",
        "backgroundColor": "#ffffff",
        "borderStyle": "black",
        "list":[
          {
            "pagePath": "pages/list/list",
            "iconPath": "images/icons/mark.png",
            "selectedIconPath": "images/icons/markHL.png",
            "text": "印记"
          },
          {
            "pagePath": "pages/mine/mine",
            "iconPath": "images/icons/mine.png",
            "selectedIconPath": "images/icons/mineHL.png",
            "text": "我的"
          }
        ]
      },
      "debug": true
    }
    3、app.wxss
    /**
     app.wxss
     全局样式
    **/
    
      page {
      width: 100%;
      height: 100%;
      padding: 0;
      background-color: #eceff4;
      font-size: 30rpx;
      font-family: -apple-system-font, 'Helvetica Neue', Helvetica, 'Microsoft YaHei', sans-serif;
    
    }
    3、config.js
    // 全局配置
    
    module.exports = {
        /** 腾讯地图 **/
        map: {
            baseUrl: 'https://apis.map.qq.com/ws',
            key: '2TCBZ-IM7K5-XHCIZ-QXLRT-CIT4J-DEFSM',
        },
    
        /** 输入框控件设置 **/
        input: {
            charWidth: 14,  // 单个字符的宽度,in rpx
        },
    
        /** 本地存储 **/
        // TODO 数据通过API全部存储于服务端
        storage: {
            diaryListKey: 'bearDiaryList',
        }
    };
    4、project.config.json
    {
        "description": "项目配置文件。",
        "packOptions": {
            "ignore": []
        },
        "setting": {
            "urlCheck": true,
            "es6": true,
            "postcss": true,
            "minified": true,
            "newFeature": true
        },
        "compileType": "miniprogram",
        "libVersion": "2.2.3",
        "appid": "wx7d22ab7088f2db6a",
        "projectname": "BearDiary",
        "isGameTourist": false,
        "condition": {
            "search": {
                "current": -1,
                "list": []
            },
            "conversation": {
                "current": -1,
                "list": []
            },
            "game": {
                "currentL": -1,
                "list": []
            },
            "miniprogram": {
                "current": -1,
                "list": []
            }
        }
    }
    5、images
    6、
    2. pages返回顶部
    1、demo
    -diaries.js
    var diaries = [
      {
        meta: {  // 内容元数据
          cover: "http://m.chanyouji.cn/index-cover/64695-2679221.jpg?imageView2/1/w/620/h/330/format/jpg",
          avatar: "https://pic4.zhimg.com/e515adf3b_xl.jpg",
          title: "此刻静好,愿幸福长存",
          meta: "2016.10.17",
          create_time: "2016.10.18 11:57:27",
          nickName: "肥肥的小狗熊",
        },
        list: [
          {
            type: "TEXT",
            content: '9月11日,15年的911事件使这天蒙上了特殊的意义。2016年的这一天,怀着激动的心情,开启了高原寻秘之旅,向着那圣洁之地出发。全程自驾近2000公里,雨崩徒步80公里,完成觐见之旅。',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "北京市",
            },
            description: "",
            id: 1,
            commentNum: 0,
            likeNum: 0,
          },
          {
              type: "IMAGE",
              content: 'http://p.chanyouji.cn/1473699595/1740E45C-D5AF-497E-A351-06E5BA22B1A3.jpg',
              poi: {
                  longitude: 117.2,
                  latitude: 37.5,
                  name: "深圳市",
              },
              description: "深圳宝安国际机场",
              id: 2,
              commentNum: 1,
              likeNum: 5,
          },
          {
            type: "IMAGE",
            content: 'http://p.chanyouji.cn/1473699603/7C3B253F-0A31-4754-B042-E04115F2C931.jpg',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "丽江三义机场",
            },
            description: "丽江三义机场",
            id: 2,
            commentNum: 1,
            likeNum: 5,
          },
          {
            type: "TEXT",
            content: ' 玉水寨在白沙溪镇,是纳西族中部地区的东巴圣地,是丽江古城的溯源。
    
    Tips:门票50元/人,游玩时间2小时。',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "玉水寨",
            },
            description: "",
            id: 1,
            commentNum: 0,
            likeNum: 0,
          },
          {
            type: "IMAGE",
            content: 'http://p.chanyouji.cn/1473685830/2A48B40F-1F11-498D-ABD2-B0EDCD09F776.jpg',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "玉水寨",
            },
            description: "阳光下的玉水寨",
            id: 2,
            commentNum: 1,
            likeNum: 5,
          },
          {
            type: "VIDEO",
            content: 'http://flv.bn.netease.com/videolib3/1605/22/auDfZ8781/HD/auDfZ8781-mobile.mp4',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "深圳宝安国际机场",
            },
            description: "",
            id: 2,
            commentNum: 10,
            likeNum: 200,
          },
        ]
      },
      {
        meta: {  // 内容元数据
          cover: "http://m.chanyouji.cn/articles/625/ca9e50f74e273041e3a399bf5528f7b5.jpg?imageView2/1/w/620/h/330/format/jpg",
          avatar: "https://pic4.zhimg.com/e515adf3b_xl.jpg",
          title: "梦想实现的地方-马达加斯加第二季",
          meta: "2013.8.10",
          create_time: "2016.10.18 11:57:27",
          nickName: "小闹钟",
        },
        list: [
          {
            type: "TEXT",
            content: '2012年十一,我和朋友一行五人第一次登上这个被非洲大陆抛弃的岛屿,看到了可爱的狐猴,憨态可掬的变色龙,明信片一样的猴面包树,天真的孩子淳朴的人民,结识了我们生命中一个重要的朋友导游小温(可以加地接小温QQ或微信咨询:109300820),从此,我们爱上了这片土地。马达加斯加是一个海岛,一年分成旱季和雨季,没有特别的低温或者高温季节,几乎全年都适合旅游,只是观赏的重点略有不同而已。 
    导游小温向我们介绍,在这里,每年的7月到9月,可以近距离观看座头鲸,于是,我们从那时开始期待这个夏季的到来。',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "塔那那利佛",
            },
            description: "",
            id: 1,
            commentNum: 0,
            likeNum: 0,
          },
          {
            type: "TEXT",
            content: '第一天 8月10日 天气晴
    
    长时间的飞行,多少会有一些枯燥,然而只要你愿意,依然可以看到心中的那片风景。 
    嗨!别郁闷了,和我一起到三万英尺的高空来看云。 
    喜欢飞机起飞的刹那间,加速再加速直到脱离开地球的引力冲向自由的天空。喜欢像鸟一样俯视地面的视角,高高在上,笑看人间。 
    天,蓝,云,白。机窗外的云时而像珍珠点点,时而像棉絮团团。
    夕阳将至,云和机翼被镀上一层华丽的金。 
    金红色的阳光与蓝色的天空最终合成出一片淡淡的紫,绚丽而梦幻。',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "塔那那利佛",
            },
            description: "",
            id: 1,
            commentNum: 0,
            likeNum: 0,
          },
          {
            type: "IMAGE",
            content: 'http://p.chanyouji.cn/64695/1377177446705p182j2oa9031j1p0b5vpuvj1voj2.jpg-o',
            poi: {
              longitude: 117.2,
              latitude: 37.5,
              name: "塔那那利佛",
            },
            description: "",
            id: 2,
            commentNum: 1,
            likeNum: 5,
          },
        ]
      }
    ]
    
    module.exports = {
      diaries: diaries,
    }
    2、services
    -geo.js
    // 基于腾讯地图API的地理位置功能封装
    
    const config = require('../config.js');
    const request = require('request.js');
    
    const statusCodeMap = {  // 请求失败原因映射
        110: '请求来源未被授权',
        301: '请求参数信息有误',
        311: 'key格式错误',
        306: '请求有护持信息请检查字符串',
    }
    
    module.exports = {
    
        // 地图API请求方法
        mapRequest(method, params, callback) {
            var url = [config.map.baseUrl, method, 'v1/'].join('/');
            let param = Object.assign({'key': config.map.key}, params);
            let queryString = Object.keys(param).map(q => [q, param[q]].join('=')).join('&');
            url += '?' + queryString;
    
            request({'method': 'GET', 'url': url}).then(resp => {
                if (resp.status != 0) {
                    console.log('请求错误:' + (statusCodeMap[resp.status] || resp.message));
                    request
                }
    
                return callback(resp);
            }).catch(err => {console.log(err);});
        },
    
        // 格式化地理位置
        formatLocation(loc) {
            return [loc.latitude, loc.longitude].map(f => f.toString()).join(',');
        },
    }
    -request.js
    // 对微信网络请求的异步封装
    
    module.exports = (options) => {
        return new Promise((resolve, reject) => {
            options = Object.assign(options, {
                success(result) {
                    if (result.statusCode === 200) {
                        resolve(result.data);
                    } else {
                        reject(result);
                    }
                },
    
                fail: reject,
            });
    
            wx.request(options);
        });
    };
    3、utills
    -input.js
    // 输入框相关处理函数
    
    module.exports = {
    
      // 计算字符串长度(英文占一个字符,中文汉字占2个字符)
      strlen(str) {
        var len = 0;
        for (var i = 0; i < str.length; i++) {
          var c = str.charCodeAt(i);
          if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) {
            len++;
          } else {
            len += 2;
          }
        }
        return len;
      },
    }
    -utill.js
    // 工具函数
    
    function formatTime(date) {
      var year = date.getFullYear()
      var month = date.getMonth() + 1
      var day = date.getDate()
    
      var hour = date.getHours()
      var minute = date.getMinutes()
      var second = date.getSeconds();
    
    
      return [year, month, day].map(formatNumber).join('.') + ' ' + [hour, minute, second].map(formatNumber).join(':')
    }
    
    function formatNumber(n) {
      n = n.toString()
      return n[1] ? n : '0' + n
    }
    
    // 将一维数组转为二维数组
    function listToMatrix(list, elementPerSubArray) {
      let matrix = [], i, k;
    
      for (i = 0, k = -1; i < list.length; i += 1) {
        if (i % elementPerSubArray === 0) {
          k += 1;
          matrix[k] = [];
        }
    
        matrix[k].push(list[i]);
      }
    
      return matrix;
    }
    
    module.exports = {
      formatTime: formatTime,
      listToMatrix: listToMatrix,
    }
    4、
    3.返回顶部
    1、entry
    a) .js
    // entry.js
    
    const toolbar = [
      '../../images/nav/download.png', '../../images/nav/fav.png',
      '../../images/nav/share.png', '../../images/nav/comment.png',
    ];
    const app = getApp();
    
    Page({
      data: {
        // 当前日志详情
        diary: undefined,
    
        // 右上角工具栏
        toolbar: toolbar,
    
        // 图片预览模式
        previewMode: false,
    
        // 当前预览索引
        previewIndex: 0,
    
        // 多媒体内容列表
        mediaList: [],
      },
    
      // 加载日记
      getDiary(params) {
        console.log("Loading diary data...", params);
    
        var id = params["id"], diary;
        app.getDiaryList(list => {
          if (typeof id === 'undefined') {
            diary = list[0];
          } else {
            diary = list[id];
          }
        });
    
        this.setData({
          diary: diary,
        });
      },
    
      // 过滤出预览图片列表
      getMediaList() {
        if (typeof this.data.diary !== 'undefined' &&
          this.data.diary.list.length) {
          this.setData({
            mediaList: this.data.diary.list.filter(
              content => content.type === 'IMAGE'),
          })
        }
      },
    
      // 进入预览模式
      enterPreviewMode(event) {
        let url = event.target.dataset.src;
        let urls = this.data.mediaList.map(media => media.content);
        let previewIndex = urls.indexOf(url);
    
        this.setData({previewMode: true, previewIndex});
      },
    
      // 退出预览模式
      leavePreviewMode() {
        this.setData({previewMode: false, previewIndex: 0});
      },
    
      onLoad: function(params) {
        this.getDiary(params);
        this.getMediaList();
      },
    
      onHide: function() {
      },
    })
    b) .json
    c) .wxml
    <!-- dairy.wxml -->
    
    <!-- 单条内容 -->
    <template name="content-item">
      <block wx:if="{{content.type == 'TEXT'}}">
        <view style="margin-top:30rpx">
          <text wx:if="{{content.type == 'TEXT'}}" class="text">{{content.content}}</text>
        </view>
      </block>
      <block wx:if="{{content.type == 'IMAGE'}}">
        <image class="media" mode="aspectFill" src="{{content.content}}" bindtap="enterPreviewMode" data-src="{{content.content}}"></image>
        <view style="margin-top: 10rpx">{{content.description}}</view>
      </block>
      <block wx:if="{{content.type == 'VIDEO'}}">
        <video class="media" src="{{content.content}}"></video>
        <view style="margin-top: 10rpx">{{content.description}}</view>
      </block>
      <template is="content-footer" data="{{content}}"></template>
    </template>
    
    <!-- 日记正文footer -->
    <template name="content-footer">
      <view class="footer">
        <view class="left">
          <image mode="aspectFit" src="../../images/icons/poi.png"></image>
          <text style="margin-left:10rpx;">{{content.poi.name}}</text>
        </view>
        <view class="right">
          <image mode="aspectFit" src="../../images/icons/comment.png"></image>
          <view>{{content.commentNum}}</view>
        </view>
        <view class="right">
          <image mode="aspectFit" src="../../images/icons/like.png"></image>
          <view>{{content.likeNum}}</view>
        </view>
      </view>
    </template>
    
    <view class="container">
      <view class="header" style="background-image:url({{diary.meta.cover}})">
        <!--顶部固定工具栏-->
        <view class="toolbar">
          <image class="item" mode="aspectFit" wx:for="{{toolbar}}" src="{{item}}"></image>
        </view>
    
        <!--日记meta信息区-->
        <view class="title">
          <image class="avatar" mode="aspectFit" src="{{diary.meta.avatar}}"> </image>
          <view class="desc">
              <view class="item">{{diary.meta.title}}</view>
              <view class="item">{{diary.meta.meta}}</view>
          </view>
        </view>
      </view>
    
      <!--日记正文-->
      <view wx:for="{{diary.list}}" wx:for-item="content" class="content">
        <template is="content-item" data="{{content}}"></template>
      </view>
    
      <view id="footer">
        <view class="container">
          <view class="item" style="font-size:50rpx;">
            <view style="display:inline-block">THE</view>
            <view style="display:inline-block;margin-left:10rpx;color:#2EA1CA;">END</view>
          </view>
          <view class="item" style="font-size:24rpx;color:gray">DESIGNED BY 小闹钟</view>
        </view>
      </view>
    </view>
    
    <!-- 预览模式 -->
    <swiper class="swiper-container" duration="400" current="{{previewIndex}}" bindtap="leavePreviewMode" style="display:{{previewMode ? 'block' : 'none'}};">
      <block wx:for="{{mediaList}}" wx:for-item="media">
        <swiper-item>
          <image src="{{media.content}}" mode="aspectFit"></image>
        </swiper-item>
      </block>
    </swiper>
    d) .wxss
    /** item.wxss **/
    
    .container {
      font-size: 26rpx;
    }
    
    .header {
      height: 400rpx;
    }
    
    .toolbar {
      height: 60rpx;
      position: fixed;
      top: 0;
      right: 0;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
    }
    
    .toolbar .item {
      width: 40rpx;
      height: 40rpx;
      margin: 10rpx 20rpx;
    }
    
    .title {
      height: 120rpx;
      position: absolute;
      top: 280rpx;
    }
    
    .title .avatar {
      margin: 20rpx;
      width: 80rpx;
      height: 80rpx;
      border-radius: 40rpx;
      float: left;
    }
    
    .title .desc {
      height: 100rpx;
      width: 630rpx;
      margin: 10rpx 0;
      float: right;
      color: white;
      display: flex;
      flex-direction: column;
    }
    
    .desc .item {
      height: 50%;
      padding: 12rpx 0;
    }
    
    .content {
      padding: 10rpx;
      border-bottom: 1px solid #E5E7ED;
    }
    
    .content .text {
      line-height: 42rpx;
    }
    
    .content .media {
      width: 730rpx;
    }
    
    .content .footer {
      height: 60rpx;
      margin-top: 10rpx;
      font-size:22rpx;
      color: #70899D;
    }
    
    .content .footer image {
      width: 20rpx;
      height: 20rpx;
    }
    
    .content .footer .left {
      display: inline-block;
      height: 40rpx;
      margin: 10rpx 0;
    }
    
    .content .footer .right {
      display: flex;
      justify-content: space-between;
      align-items: center;
      float: right;
      height: 40rpx;
      margin-left: 20rpx;
      background-color: #E5E7ED;
      font-size: 20rpx;
    }
    
    .content .footer .right image {
      margin: 10rpx;
    }
    
    .content .footer .right text {
      font-size: 20rpx;
      padding:10rpx 10rpx 10rpx 0;
    }
    
    #footer {
      width: 100%;
      height: 300rpx;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    #footer .container {
      height: 100rpx;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      align-items: center;
    }
    
    #footer .container .item {
      height: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .swiper-container {
      position: fixed;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: #000;
    }
    
    .swiper-container image {
      width: 100%;
      height: 100%;
    }
    e)
    2、list
    a) .js
    // index.js
    // 日记聚合页
    
    const config = require("../../config");
    
    var app = getApp();
    
    Page({
    
      data: {
        // 日记列表
        // TODO 从server端拉取
        diaries: null,
    
        // 是否显示loading
        showLoading: false,
    
        // loading提示语
        loadingMessage: '',
      },
    
      /**
       * 生命周期函数--监听页面加载
       */
      onLoad() {
        this.getDiaries();
      },
    
      /**
       * 获取日记列表
       * 目前为本地缓存数据 + 本地假数据
       * TODO 从服务端拉取
       */
      getDiaries() {
        var that = this;
        app.getDiaryList(list => {
          that.setData({diaries: list});
        })
      },
    
      // 查看详情
      showDetail(event) {
        wx.navigateTo({
          url: '../entry/entry?id=' + event.currentTarget.id,
        });
      }
    })
    b) .json
    c) .wxml
    <!--list.wxml-->
    
    <scroll-view scroll-y="true">
      <view wx:for="{{diaries}}" wx:for-index="idx" class="item-container" bindtap="showDetail" id="{{idx}}">
        <image mode="aspectFit" src="{{item.meta.cover}}" class="cover"></image>
        <view class="desc">
          <view class="left">
            <view style="font-size:32rpx;margin:10rpx 0;">{{item.meta.title}}</view>
            <view style="font-size:24rpx;color:darkgray">{{item.meta.meta}}</view>
          </view>
          <view class="right">
            <image mode="aspectFit" src="{{item.meta.avatar}}"></image>
            <text style="font-size:24rpx;margin-top:10rpx;color:darkgray">{{item.meta.nickName}}</text>
          </view>
        </view>
      </view>
    </scroll-view>
    d) .wxss
    /** list.wxss **/
    
    .item-container {
      margin: 10rpx 20rpx;
      position: relative;
    }
    
    .cover {
      width: 100%;
      height: 400rpx;
      display: block;
    }
    
    .desc {
      margin: 10rpx 0;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 20rpx;
      border-bottom: 1px solid #E5E7ED;
    }
    
    .desc .left {
    }
    
    .desc .right {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .right image{
      display: block;
      width: 60rpx;
      height: 60rpx;
      border-radius: 30rpx;
    }
    e)
    3、mine
    a) .js
    // mine.js
    
    // 自定义标签
    var iconPath = "../../images/icons/"
    var tabs = [
        {
            "icon": iconPath + "mark.png",
            "iconActive": iconPath + "markHL.png",
            "title": "日记",
            "extraStyle": "",
        },
        {
            "icon": iconPath + "collect.png",
            "iconActive": iconPath + "collectHL.png",
            "title": "收藏",
            "extraStyle": "",
        },
        {
            "icon": iconPath + "like.png",
            "iconActive": iconPath + "likeHL.png",
            "title": "喜欢",
            "extraStyle": "",
        },
        {
            "icon": iconPath + "more.png",
            "iconActive": iconPath + "moreHL.png",
            "title": "更多",
            "extraStyle": "border:none;",
        },
    ]
    var userInfo = {
        avatar: "https://pic4.zhimg.com/e515adf3b_xl.jpg",
        nickname: "小闹钟",
        sex: "♂",  // 0, male; 1, female
        meta: '1篇日记',
    }
    
    
    Page({
    
        // data
        data: {
            // 展示的tab标签
            tabs: tabs,
    
            // 当前选中的标签
            currentTab: "tab1",
    
            // 高亮的标签索引
            highLightIndex: "0",
    
            // 模态对话框样式 
            modalShowStyle: "",
    
            // 待新建的日记标题
            diaryTitle: "",
    
            // TODO 用户信息
            userInfo: userInfo,
        },
    
        // 隐藏模态框
        hideModal() {
            this.setData({modalShowStyle: ""});
        },
    
        // 清除日记标题
        clearTitle() {
            this.setData({diaryTitle: ""});
        },
    
        onShow: function() {
            this.hideModal();
            this.clearTitle();
        },
    
        // 点击tab项事件
        touchTab: function(event){
            var tabIndex = parseInt(event.currentTarget.id);
            var template = "tab" + (tabIndex + 1).toString();
    
            this.setData({
                currentTab: template,
                highLightIndex: tabIndex.toString()
            }
            );
        },
    
        // 点击新建日记按钮
        touchAdd: function (event) {
            this.setData({
                modalShowStyle: "opacity:1;pointer-events:auto;"
            })
        },
    
        // 新建日记
        touchAddNew: function(event) {
            this.hideModal();
    
            wx.navigateTo({
                url: "../new/new?title=" + this.data.diaryTitle,
            });
        },
    
        // 取消标题输入
        touchCancel: function(event) {
            this.hideModal();
            this.clearTitle();
        }, 
    
        // 标题输入事件
        titleInput: function(event) {
            this.setData({
                diaryTitle: event.detail.value,
            })
        }
    })
    b) .json
    c) .wxml
    <!--mine.wxml-->
    
    <template name="tab1">
        <view>
        </view>
    </template>
    
    <template name="tab2">
        <view>
        </view>
    </template>
    
    <template name="tab3">
        <view>
        </view>
    </template>
    
    <template name="tab4">
        <view>
        </view>
    </template>
    
    <view>
      <!--一个全屏模态对话框-->
      <view class="modal" style="{{modalShowStyle}}">
        <view class="dialog">
          <view class="modal-item" style="display:flex;justify-content:center;align-items:center;">
          请输入日记标题
          </view>
          <view class="modal-item" style="margin:0 auto;90%;">
            <input type="text" bindinput="titleInput" style="background-color:white;border-radius:2px;" value="{{diaryTitle}}" placeholder="请输入日记标题"></input>
          </view>
          <view class="modal-button" style="100%">
            <view style="color:green;border-right:1px solid #E5E7ED;" bindtap="touchAddNew">确定</view>
            <view bindtap="touchCancel">取消</view>
          </view>
        </view>
      </view>
    
      <view class="header">
        <view class="profile">
          <image class="avatar" mode="aspectFit" src="{{userInfo.avatar}}"></image>
          <view class="description">
            <view class="item">
              <view style="margin-right:5px">{{userInfo.nickname}}</view>
              <view>{{userInfo.sex}}</view>
            </view>
            <view class="item">{{userInfo.meta}}</view>
          </view>
          <image class="add" mode="aspectFill" src="../../images/icons/add.png" bindtap="touchAdd"></image>
        </view>
    
        <view class="tablist">
          <view wx:for="{{tabs}}" wx:for-index="idx" class="tab" bindtap="touchTab" style="{{item.extraStyle}}" id="{{idx}}">
            <view class="content" style="color:{{highLightIndex == idx ? '#54BFE2' : ''}};">
              <image class="image" mode="aspectFit" src="{{highLightIndex == idx ? item.iconActive : item.icon}}"></image>
              <view style="margin-top:2px;">{{item.title}}</view>
            </view>
          </view>
        </view>
      </view>
    
        <template is="{{currentTab}}"></template>
    </view>
    d) .wxss
    /**mine.wxss**/
    
    .header {
      height: 130px;
      background: white;
    }
    
    .header .profile {
      height: 50%;
    }
    
    .profile .avatar {
      width: 50px;
      height: 50px;
      float: left;
      margin: 7.5px 10px;
      border-radius: 25px;
    }
    
    .profile .description {
      display: inline-block;
      margin: 7.5px auto;
      height: 50px;
    }
    
    .description .item {
        height: 50%;
        display: flex;
        align-items: center;
    }
    
    .profile .add {
        float: right;
        margin: 15px 10px;
        height: 35px;
        width: 35px;
    }
    
    .header .tablist {
        height: 50%;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    
    .tablist .tab {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 50px;
        width: 25%;
        margin: 7.5px 0px;
        border-right: 1px solid #eceff4;
    }
    
    .tab .content{
        width: 25px;
        height: 50px;
        font-size: 12px;
        color: #B3B3B3;
    }
    
    .tab .image {
        width: 25px;
        height: 25px;
        margin-top: 10px;
    }
    
    .modal {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background: rgba(0, 0, 0, .5);
        z-index: 99999;
        opacity: 0;
        transition: opacity 400ms ease-in;
        pointer-events: none;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    
    .modal .dialog {
        width: 84%;
        height: 28%;
        background-color: #eceff4;
        border-radius: 4px;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
    }
    
    .dialog .modal-item {
        height: 33.3%;
        width: 100%;
    }
    
    .modal-button {
        height: 100rpx;
        margin-bottom: 0;
        display: flex;
        flex-direction: row;
        justify-content: space-between;
    }
    
    .modal-button view {
        width: 50%;
        border-top: 1px solid #E5E7ED;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    e)
    4、new
    a) .js
    // new.js
    // TODO 并不是所有非中文字符宽度都为中文字符宽度一半,需特殊处理
    // TODO 由于文本框聚焦存在bug,故编辑模式待实现
    
    const input = require('../../utils/input');
    const config = require('../../config');
    const geo = require('../../services/geo');
    const util = require('../../utils/util');
    
    const RESOLUTION = 750;  // 微信规定屏幕宽度为750rpx
    const MARGIN = 10;  // 写字面板左右margin
    const ROW_CHARS = Math.floor((RESOLUTION - 2 * MARGIN) / config.input.charWidth);
    const MAX_CHAR = 1000;  // 最多输1000字符
    
    // 内容布局
    const layoutColumnSize = 3;
    
    // 日记内容类型
    const TEXT = 'TEXT';
    const IMAGE = 'IMAGE';
    const VIDEO = 'VIDEO';
    
    const mediaActionSheetItems = ['拍照', '选择照片', '选择视频'];
    const mediaActionSheetBinds = ['chooseImage', 'chooseImage', 'chooseVideo'];
    
    var app = getApp();
    
    Page({
    
      data: {
        // 日记对象
        diary: {
          meta: {},
          list: [],
        },
    
        // 日记内容布局列表(2x2矩阵)
        layoutList: [],
    
        // 是否显示loading
        showLoading: false,
    
        // loading提示语
        loadingMessage: '',
    
        // 页面所处模式
        showMode: 'common',
    
        // 输入框状态对象
        inputStatus: {
          row: 0,
          column: 0,
          lines: [''],
          mode: 'INPUT',
          auto: false,  // 是否有自动换行
        },
    
        // 当前位置信息
        poi: null,
    
        // 点击`图片`tab的action-sheet
        mediaActionSheetHidden: true,
    
        // 多媒体文件插入action-sheet
        mediaActionSheetItems: mediaActionSheetItems,
    
        // 多媒体文件插入项点击事件
        mediaActionSheetBinds: mediaActionSheetBinds,
    
        // 是否显示底部tab栏
        showTab: true,
      },
    
      // 显示底部tab
      showTab() {
        this.setData({showTab: true});
      },
    
      // 隐藏底部tab
      hideTab() {
        this.setData({showTab: false});
      },
    
      // 显示loading提示
      showLoading(loadingMessage) {
        this.setData({showLoading: true, loadingMessage});
      },
    
      // 隐藏loading提示
      hideLoading() {
        this.setData({showLoading: false, loadingMessage: ''});
      },
    
      // 数据初始化
      init() {
        this.getPoi();
        this.setMeta();
      },
    
      // 设置日记数据
      setDiary(diary) {
        let layout = util.listToMatrix(diary.list, layoutColumnSize);
        this.setData({diary: diary, layoutList: layout});
        this.saveDiary(diary);
      },
    
      // 保存日记
      // TODO sync to server
      saveDiary(diary) {
        const key = config.storage.diaryListKey;
    
        app.getLocalDiaries(diaries => {
          diaries[diary.meta.title] = diary;
          wx.setStorage({key: key, data: diaries});
        })
      },
    
      // 页面初始化
      onLoad: function(options) {
        if (options) {
          let title = options.title;
          if (title) {this.setData({
            'diary.meta.title': title,
            'diary.meta.create_time': util.formatTime(new Date()),
            'diary.meta.cover': ''
          });}
        }
    
        this.init();
      },
    
      // 页面渲染完成
      onReady: function(){
        wx.setNavigationBarTitle({title: '编辑日记'});
      },
    
      onShow:function(){
        // 页面显示
      },
    
      onHide:function(){
        // 页面隐藏
      },
    
      onUnload:function(){
        // 页面关闭
        console.log('页面跳转中...');
      },
    
      // 清除正在输入文本
      clearInput() {
        this.setData({inputStatus: {
          row: 0,
          common: 0,
          lines: [''],
          mode: 'INPUT',
          auto: false,
        }});
      },
    
      // 结束文本输入
      inputDone() {
        let text = this.data.inputStatus.lines.join('
    ');
        let diary = this.data.diary;
    
        if (text) {
          diary.list.push(this.makeContent(TEXT, text, ''));
          this.setDiary(diary);
        }
    
        this.inputCancel();
      },
    
      // 进入文本编辑模式
      inputTouch(event) {
        this.setData({showMode: 'inputText'});
      },
    
      // 取消文本编辑
      inputCancel() {
        this.setData({showMode: 'common'});
        this.clearInput();
      },
    
      // 文本输入
      textInput(event) {
        console.log(event);
        let context = event.detail;
    
        // 输入模式
        if (this.data.inputStatus.mode === 'INPUT') {
          if (context.value.length != context.cursor) {
            console.log('用户输入中...');
          } else {
            let text = context.value;
            let len = input.strlen(text);
            let lines = this.data.inputStatus.lines;
            let row = this.data.inputStatus.row;
            let [extra, extra_index] = [[['']], 0];
            let hasNewLine = false;
            console.log('当前文本长度: ' + len);
    
            // 当前输入长度超过规定长度
            if (len >= ROW_CHARS) {
              // TODO 此处方案不完善
              // 一次输入最好不超过两行
              hasNewLine = true;
              while (input.strlen(text) > ROW_CHARS) {
                let last = text[text.length - 1];
    
                if (input.strlen(extra[extra_index] + last) > ROW_CHARS) {
                  extra_index += 1;
                  extra[extra_index] = [''];
                }
    
                extra[extra_index].unshift(last);
                text = text.slice(0, -1);
              }
          }
    
          lines[lines.length - 1] = text;
          if (hasNewLine) {
            extra.reverse().forEach((element, index, array) => {
              lines.push(element.join(''));
              row += 1;
            });
          }
    
          let inputStatus = {
            lines: lines,
            row: row,
            mode: 'INPUT',
            auto: true,  // // 自动换行的则处于输入模式
          };
    
          this.setData({inputStatus});
          }
        }
      },
    
      // 文本框获取到焦点
      focusInput(event) {
        let isInitialInput = this.data.inputStatus.row == 0 &&
                             this.data.inputStatus.lines[0].length == 0;
        let isAutoInput = this.data.inputStatus.mode == 'INPUT' &&
                          this.data.inputStatus.auto == true;
        let mode = 'EDIT';
    
        if (isInitialInput || isAutoInput) {
          mode = 'INPUT';
        }
    
        this.setData({'inputStatus.mode': mode});
      },
    
      // 点击多媒体插入按钮
      mediaTouch() {
        this.setData({
          showTab: false,
          mediaActionSheetHidden: false,
        });
      },
    
      mediaActionSheetChange(event) {
        this.setData({
          showTab: true,
          mediaActionSheetHidden: true,
        })
      },
    
      // 将内容写入至日记对象
      writeContent(res, type) {
        let diary = this.data.diary;
    
        if (type === IMAGE) {
          res.tempFilePaths.forEach((element, index, array) => {
            // TODO 内容上传至服务器
            diary.list.push(this.makeContent(type, element, ''))
          });
        }
    
        if (type === VIDEO) {
          // TODO 内容上传至服务器
          diary.list.push(this.makeContent(type, res.tempFilePath, ''))
        }
    
        // 设置日记封面
        if (type === IMAGE && !this.data.diary.meta.cover) {
          this.setData({'diary.meta.cover': res.tempFilePaths[0]});
        }
    
        this.setDiary(diary);
        this.hideLoading();
        this.showTab();
      },
    
      // 从相册选择照片或拍摄照片
      chooseImage() {
        let that = this;
    
        wx.chooseImage({
          count: 9,  // 最多选9张
          sizeType: ['origin', 'compressed'],
          sourceType: ['album', 'camera'],
    
          success: (res) => {
            this.setData({mediaActionSheetHidden: true});
            this.showLoading('图片处理中...');
            that.writeContent(res, IMAGE);
          }
        })
      },
    
      // 从相册选择视频文件
      chooseVideo() {
        let that = this;
    
        wx.chooseVideo({
          sourceType: ['album'],  // 仅从相册选择
          success: (res) => {
            this.setData({mediaActionSheetHidden: true});
            this.showLoading('视频处理中...');
            that.writeContent(res, VIDEO);
          }
        })
      },
    
      // 获得当前位置信息
      getPoi() {
        var that = this;
        wx.getLocation({
          type: 'gcj02',
          success: function(res) {
            geo.mapRequest(
              'geocoder',
              {'location': geo.formatLocation(res)},
              loc => {
                let poi = {
                  'latitude': res.latitude,
                  'longitude': res.longitude,
                  'name': loc.result.address,
                };
                that.setData({poi: poi});
              })
          }
        })
      },
    
      // 构造日记内容对象
      makeContent(type, content, description) {
        return {
          type: type,
          content: content,
          description: description,
          poi: this.data.poi,
        };
      },
    
      // 构造日记meta信息
      setMeta() {
        var that = this;
        app.getUserInfo(info => {
          that.setData({
            'diary.meta.avatar': info.avatarUrl,
            'diary.meta.nickName': info.nickName,
          })
        })
      },
    
    })
    b) .json
    c) .wxml
    <!--new.wxml-->
    
    <template name="common">
      <scroll-view class="container" scroll-y="true">
        <view class="common-container">
          <view class="item-group" wx:for="{{layoutList}}" wx:for-item="group">
            <block wx:for="{{group}}" wx:for-item="item">
              <block wx:if="{{item.type == 'TEXT'}}">
                <view class="album-item content-text">
                  <view>{{item.content}}</view>
                </view>
              </block>
              <block wx:elif="{{item.type == 'IMAGE'}}">
                <image src="{{item.content}}" class="album-item" mode="aspectFill"></image>
              </block>
              <block wx:elif="{{item.type == 'VIDEO'}}">
                <video class="album-item" src="{{item.content}}"></video>
              </block>
            </block>
          </view>
        </view>
      </scroll-view>
    
      <view class="tabbar" style="display:{{showTab ? 'flex' : 'none'}};">
        <view class="item" bindtap="inputTouch">
          <image class="icon" mode="aspectFit" src="../../images/tabbar/text.png"></image>
        </view>
        <view class="item" bindtap="mediaTouch">
          <image class="icon" mode="aspectFit" src="../../images/tabbar/image.png"></image>
        </view>
        <view class="item">
          <image class="icon" mode="aspectFit" src="../../images/tabbar/more.png"></image>
        </view>
      </view>
    
      <action-sheet hidden="{{mediaActionSheetHidden}}" bindchange="mediaActionSheetChange">
        <block wx:for-items="{{mediaActionSheetItems}}" wx:for-index="id">
          <action-sheet-item class="action-item" bindtap="{{mediaActionSheetBinds[id]}}">
            {{item}}
          </action-sheet-item>
        </block>
        <action-sheet-cancel class='action-cacel'>取消</action-sheet-cancel>
      </action-sheet>
    </template>
    
    <template name="inputText">
      <view class="input-container">
        <view style="height:47rpx" wx:for="{{inputStatus.lines}}" wx:for-index="idx">
          <input type="text" data-index="{{idx}}" placeholder="" bindinput="textInput" bindchange="textInputChange" value="{{item}}" auto-focus="{{idx == inputStatus.row ? true : false}}" bindfocus="focusInput"/>
        </view>
      </view>
      <view class="tabbar">
        <view class="item" style="50%" bindtap="inputCancel">
          <image class="icon" mode="aspectFit" src="../../images/tabbar/cancel.png"></image>
        </view>
        <view class="item" style="50%" bindtap="inputDone">
          <image class="icon" mode="aspectFit" src="../../images/tabbar/ok.png"></image>
        </view>
      </view>
    </template>
    
    <view style="100%;height:100%">
      <block wx:if="{{showMode == 'common'}}">
        <template is="{{showMode}}" data="{{showTab: showTab, mediaActionSheetHidden: mediaActionSheetHidden, mediaActionSheetItems: mediaActionSheetItems, mediaActionSheetBinds: mediaActionSheetBinds, layoutList: layoutList}}"></template>
      </block>
      <block wx:if="{{showMode == 'inputText'}}">
        <template is="{{showMode}}" data="{{inputStatus}}"></template>
      </block>
      <loading hidden="{{!showLoading}}" bindchange="hideLoading">
        {{loadingMessage}}
      </loading>
    </view>
    d) .wxss
    /** new.wxss **/
    
    .container {
      height: 91%;
    }
    
    .common-container {
      margin: 0.1rem;
    }
    
    .item-group {
      display: flex;
      align-items: center;
    }
    
    .album-item {
      flex-direction: column;
      margin: 0.1rem;
      background: white;
      width: 6.4rem;
      height: 6.4rem;
    }
    
    .content-text{
      justify-content: center;
      align-items: center;
      display: flex;
    }
    
    .content-text view {
      overflow: hidden;
      text-overflow: ellipsis;
      font-size: 10px;
      line-height: 15px;
    }
    
    .tabbar {
      position: fixed;
      width: 100%;
      height: 8%;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: white;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
    }
    
    .tabbar .item {
      width: 33.33%;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .item .icon {
      width: 50rpx;
      height: 50rpx;
    }
    
    .input-container {
      height: 80%;
      background-color: #eceff4;
      background-image: linear-gradient(#E1E6EA .1em, transparent .1em);
      background-size: 100% 48rpx;
      padding: 0;
      box-sizing: border-box;
      margin: 0 10rpx;
    }
    
    .input-container input{
      height: 47rpx;
      max-height: 47rpx;
      font-size: 28rpx;
      margin: 0px;
    }
    
    .action-item, .action-cacel {
      font-size: 30rpx;
      color: #39b5de;
    }
    e)
    5、
    a) .js
    b) .json
    c) .wxml
    d) .wxss
    e)
    6、
    4.返回顶部
     
    5.返回顶部
    0、
    1、
     
    6.返回顶部
     
    warn 作者:ylbtech
    出处:http://ylbtech.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    路由器、交换机学习之IP地址、使用网络掩码划分子网
    PCB线宽与电流计算器--在线计算
    数组的访问形式
    STM32开发环境--使用MDK建立一个工程
    电源模块PCB设计
    STM32--TIM定时器时钟分割(疑难)
    STM32——输入捕获实验原理及配置步骤
    STM32——PWM基本知识及配置过程
    STM32——通用定时器基本定时功能
    STM32——NVIV:嵌套中断向量控制器
  • 原文地址:https://www.cnblogs.com/storebook/p/9509614.html
Copyright © 2020-2023  润新知