小程序-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: ' 玉水寨在白沙溪镇,是纳西族中部地区的东巴圣地,是丽江古城的溯源。\n\nTips:门票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),从此,我们爱上了这片土地。马达加斯加是一个海岛,一年分成旱季和雨季,没有特别的低温或者高温季节,几乎全年都适合旅游,只是观赏的重点略有不同而已。 \n导游小温向我们介绍,在这里,每年的7月到9月,可以近距离观看座头鲸,于是,我们从那时开始期待这个夏季的到来。',
        poi: {
          longitude: 117.2,
          latitude: 37.5,
          name: "塔那那利佛",
        },
        description: "",
        id: 1,
        commentNum: 0,
        likeNum: 0,
      },
      {
        type: "TEXT",
        content: '第一天 8月10日 天气晴\n\n长时间的飞行,多少会有一些枯燥,然而只要你愿意,依然可以看到心中的那片风景。 \n嗨!别郁闷了,和我一起到三万英尺的高空来看云。 \n喜欢飞机起飞的刹那间,加速再加速直到脱离开地球的引力冲向自由的天空。喜欢像鸟一样俯视地面的视角,高高在上,笑看人间。 \n天,蓝,云,白。机窗外的云时而像珍珠点点,时而像棉絮团团。\n夕阳将至,云和机翼被镀上一层华丽的金。 \n金红色的阳光与蓝色的天空最终合成出一片淡淡的紫,绚丽而梦幻。',
        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;width:90%;">
        <input type="text" bindinput="titleInput" style="background-color:white;border-radius:2px;" value="{{diaryTitle}}" placeholder="请输入日记标题"></input>
      </view>
      <view class="modal-button" style="width: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('\n');
    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="width:50%" bindtap="inputCancel">
      <image class="icon" mode="aspectFit" src="../../images/tabbar/cancel.png"></image>
    </view>
    <view class="item" style="width:50%" bindtap="inputDone">
      <image class="icon" mode="aspectFit" src="../../images/tabbar/ok.png"></image>
    </view>
  </view>
</template>

<view style="width: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/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://www.cnblogs.com/storebook/p/9509614.html

微信小程序,作为腾讯旗下的轻量级应用平台,凭借其独特的优势和特点,已经深入渗透到人们的生活中。以下是微信小程序的一些关键优势和特点,以及我们为您准备的资源介绍: 优势与特点: 即用即走,无需安装:用户只需在微信内搜索或扫码即可使用,无需下载安装,节省手机存储空间,也降低了用户的使用门槛。 跨平台兼容性:微信小程序可在多种操作系统和设备上运行,无需考虑不同平台的适配问题,为开发者提供了统一的开发环境。 丰富的API接口:微信提供了丰富的API接口,使得开发者能够轻松实现各种功能,如微信支付、用户授权、消息推送等。 强大的社交属性:微信小程序与微信生态紧密结合,可以充分利用微信的社交属性,实现用户裂变和增长。 低成本开发:相较于传统App,微信小程序的开发成本更低,周期更短,降低了企业的开发门槛和成本。 资源介绍: “微信小程序-项目源码-原生开发框架-含效果截图示例”这份资源,不仅包含了完整的微信小程序项目源码,而且基于原生开发框架,确保了代码的健壮性和可扩展性。源码中涵盖了微信小程序的基础架构、页面布局、功能实现等各个方面,通过详细的注释和说明,让您能够快速上手并掌握微信小程序的开发技巧。 同时,我们还提供了丰富的效果截图示例,让您能够直观地了解项目的最终效果,更好地评估项目的实用性和商业价值。无论您是前端开发者、小程序爱好者,还是希望拓展业务的企业,这份资源都将为您带来极大的帮助和启示。快来查看吧,开启您的小程序开发之旅!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值