# 使用云开发构建多媒体小程序

# 小程序·云开发

什么是小程序的云开发?一句话就是能够使开发者省去搭建服务器、申请域名的成本,从开发到运维提供整套解决方案的小程序开发方式。 官方定义是,小程序·云开发为开发者提供完整的云端支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代。 相对于传统小程序开发,云开发新提供了三大基础支持:

  • 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码
  • 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库
  • 存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理

由此开发者只需在小程序端调用 API 即可搭建简单的后端服务,无需考虑搭建服务器、申请合法域名带来的成本。云开发环境免费版对资源有一定限制,但在用户量不大的情况下可以提供稳定的服务,相对于真正的后端服务,云开发环境还集成了控制台,提供了运维的有效手段。

# 初始化小程序·云开发

# 注册小程序

登录微信公众平台,使用邮箱注册并激活小程序。进入小程序管理后台首页填写小程序信息,并添加开发者,详情见官方文档

# 创建小程序项目

  • 进入微信 web 开发者工具下载页,下载开发工具并安装。
  • 进入小程序管理后台-设置-开发设置,获取小程序的 AppID,新建小程序项目,选择 建立云开发快速启动模板,详情见小程序·云开发文档。

# 建立云开发环境

  • 在开发者工具工具栏左侧,点击 云开发 开通云开发功能。每个小程序免费提供两个环境,首先创建名为 test 的云环境。复制自动生成的环境 ID,编辑 miniprogram 文件夹下的 app.js,添加 env 参数。
wx.cloud.init({
  traceUser: true,
  env: "test-xxx"
});
  • 选择 cloudfunctions 文件夹,设置当前环境为 test
  • 编译小程序,如控制台无报错,则环境连接正常。

# 云开发环境

云开发提供了一整套云服务及简单、易用的 API 和管理界面,以尽可能降低后端开发成本,让开发者能够专注于核心业务逻辑的开发、尽可能轻松的完成后端的操作和管理。这套云环境包括数据库、存储和云函数。

# 云开发控制台

云开发控制台提供了云开发的管理界面和运维工具,开发者可以通过操作控制台来查看环境使用情况、操作数据库、存储、管理云函数等。 云开发控制台

# 数据库

云开发环境提供一个文档数据库,其 API 和功能类似于 MongoDB。进入控制台的数据库选项,新建一个集合。集合中每条数据都是 JSON 格式的。 数据库管理

  • 权限控制:小程序端操作数据库时,读写数据受权限控制限制。写入的记录会默认增加写入用户的 _openid,如果需要所有用户对数据有读权限,需要更改权限为 所有用户可读,仅创建者及管理员可写
  • 对数据库 API 进行封装
// 云db对象
const db = wx.cloud.database();

module.exports = {
  /**
   * 新增记录
   *
   * @param data 数据
   * @param collection 集合
   * @return {"_id": String, "errMsg": String}
   */
  add: function(data, collection) {
    return db.collection(collection).add({
      data: data
    });
  },

  /**
   * 查询记录
   *
   * @param collection 集合
   * @param where 查询条件
   * @param skip 查询起始位置
   * @param limit 查询数量
   * @return {"data": Array, "errMsg": String}
   */
  query: function(collection, where, skip, limit) {
    where = where || {};
    skip = skip || 0;
    limit = limit || 10;
    return db
      .collection(collection)
      .where(where)
      .orderBy("time", "desc")
      .skip(skip)
      .limit(limit)
      .get();
  },

  /**
   * 查询记录数量
   *
   * @param collection 集合
   * @param where 查询条件
   * @return {"total": Number, "errMsg": String}
   */
  count: function(collection, where) {
    where = where || {};
    return db
      .collection(collection)
      .where(where)
      .count();
  },

  /**
   * 新增/全部更新文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @param data 数据
   * @return {"_id": String, "errMsg": String}
   */
  addDoc: function(collection, doc, data) {
    collection = collection || lovc;
    return db
      .collection(collection)
      .doc(doc)
      .set({
        data: data
      });
  },

  /**
   * 查询文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"data": Object, "errMsg": String}
   */
  getDoc: function(collection, doc) {
    collection = collection || lovc;
    return db
      .collection(collection)
      .doc(doc)
      .get();
  },

  /**
   * 部分更新文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @param data 数据
   * @return {"stats": Object, "errMsg": String}
   */
  update: function(collection, doc, data) {
    collection = collection || lovc;
    return db
      .collection(collection)
      .doc(doc)
      .update({
        data: data
      });
  },

  /**
   * 删除文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"stats": Object, "errMsg": String}
   */
  remove: function(collection, doc) {
    collection = collection || lovc;
    return db
      .collection(collection)
      .doc(doc)
      .remove();
  }
};

# 存储

云环境提供了免费的 5G 云存储空间,文件上传后每个文件会生成一个 fileID 和一个 https 下载地址,在小程序中,srcposter 等属性中都可以直接使用。 存储管理

  • 对存储 API 进行封装
module.exports = {
  /**
   * 上传文件
   *
   * @param fileName 文件名
   * @param filePath 文件路径
   * @return {"errMsg": String, "fileID": String, "statusCode": Number}
   */
  upload: function(fileName, filePath) {
    return wx.cloud.uploadFile({
      cloudPath: fileName,
      filePath: filePath
    });
  },

  /**
   * 下载文件
   *
   * @param fileID 文件名
   * @return {"tempFilePath": String, "statusCode": Number}
   */
  download: function(fileID) {
    return wx.cloud.downloadFile({
      fileID: fileID
    });
  }
};

# 云函数

云函数即在云端(服务器端)运行的函数。通过使用云函数,开发者无需购买、搭建服务器,只需编写函数代码并部署到云端即可在小程序端调用,同时云函数之间也可互相调用。云函数本身是一个本地定义的 JS 方法,上传并部署到指定云环境中,运行在云端的 NodeJS 中。云函数调用时用户的 openid 会作为请求参数进行调用,从而能够与微信鉴权无缝结合。

# 创建云函数

  • 本地环境: 云函数依赖本地 NodeJS 环境,可以通过 npm 对服务进行扩展
  • REST 调用:传统小程序开发的后端服务需要合法域名,这就为小程序开发带来了额外成本,而云函数通过 http 调用服务是无需鉴别域名的,因此可以通过小程序端调用云函数,云端调用远程服务的方式拓展低成本的 REST服务
  • tcb-router:tcb-router是由腾讯官方提供的基于 koa 风格的云函数轻量级类路由库,主要用于优化服务端函数处理逻辑。云函数允许创建的数量是有限的,通过 tcb-router 可以对服务数量进行拓展
  • 选择小程序中的 cloudfunctions 文件夹,新建Node.js云函数,本地会自动生成云函数模板文件和 package.json,并安装 wx-server-sdk 依赖 创建云函数
  • 选择云函数所在文件夹,进入终端,通过 npm 安装所需依赖
  • 编辑云函数(REST 服务)
// 云函数入口文件
const cloud = require("wx-server-sdk");
const rp = require("request-promise");

cloud.init();

// 云函数入口函数
exports.main = async (event, context) => {
  if (event.method === "GET") {
    return get(event.url, event.data);
  } else if (event.method === "POST") {
    return post(event.url, event.data);
  }
};

/**
 * GET请求
 *
 * @param url REST服务地址
 * @param data 请求参数
 */
function get(url, data) {
  if (undefined !== data && "" !== data) {
    let queryString = "?";
    for (let key in data) {
      if (!queryString.endsWith("?")) {
        queryString = queryString.concat("&");
      }
      queryString = queryString
        .concat(key)
        .concat("=")
        .concat(data[key]);
    }
    url = url.concat(queryString);
  }
  return rp(url);
}

/**
 * POST请求
 *
 * @param url REST服务地址
 * @param data 请求参数
 */
function post(url, data) {
  return rp({
    uri: url,
    method: "POST",
    body: data,
    json: true
  });
}
  • 编辑云函数(tcb-router)
// 云函数入口文件
const cloud = require("wx-server-sdk");
const TcbRouter = require("tcb-router");

cloud.init();

// 云函数入口函数
exports.main = async (event, context) => {
  const app = new TcbRouter({
    event
  });

  // app.use 表示该中间件会适用于所有的路由
  app.use(async (ctx, next) => {
    // 创建返回data对象
    ctx.data = {};
    // 执行下一中间件
    await next();
  });

  // 路由为数组表示,该中间件适用于多个路由
  // app.router(['x', 'y'], async (ctx, next) => {
  //   ctx.data.from = 'cloud';
  //   await next();
  // });

  app.router(
    "router",
    async (ctx, next) => {
      ctx.data.openId = event.userInfo.openId;
      await next();
    },
    async ctx => {
      ctx.data.other = event.other;
      // ctx.body 返回数据到小程序端
      ctx.body = {
        code: 0,
        data: ctx.data
      };
    }
  );

  return app.serve();
};
  • 选择云函数所在文件夹,上传并部署 云函数,在小程序端可以通过云函数调用访问服务
  • 对云函数 API 进行封装
module.exports = {
  /**
   * 通过云函数访问服务
   *
   * @param name 云函数名
   * @param params 参数
   * @return {"errMsg": String, "result": Object, "requestID": String}
   */
  call: function(name, params) {
    return wx.cloud.callFunction({
      name: name,
      data: params
    });
  },

  /**
   * 通过tcb-router访问服务
   *
   * @param url router路径
   * @param params 参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  tcbRouter: function(url, params) {
    params.$url = url;
    return this.call("router", params);
  },

  /**
   * 云函数-REST访问服务
   *
   * @param url REST服务地址
   * @param 请求方法
   * @param params 请求参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  rest: function(url, method, params) {
    return this.call("rest", {
      url: url,
      method: method,
      data: params
    });
  }
};

# 开发多媒体服务

# 引入 iView Webapp

引入iView Webapp作为小程序的前端框架。

  • 进入的 iView WebappGitHub,下载项目
# clone iView Weapp
git clone https://github.com/TalkingData/iview-weapp.git
cd iview-weapp
# 安装依赖
npm install
# 编译组件,便于打开到模拟器查看
npm run dev
  • dist 文件夹拷贝到小程序项目的 miniprogram 文件夹下
  • 通过开发工具打开 iView Webapp 项目下的 examples 目录,预览 iView Webapp 提供的组件。

# 封装媒体 API

与云开发原生支持 Promise 不同,原生小程序 API 只能通过回调获取返回值,对于项目中使用的相关小程序的媒体 API 进行封装,避免多层回调。

module.exports = {
  /**
   * 选择照片
   *
   * @return {"errMsg": String, "tempFilePaths": Array(String), "tempFiles": Array(Obejct)}
   */
  chooseImage: function() {
    return new Promise(function(resolve) {
      wx.chooseImage({
        count: 9,
        sizeType: ["original", "compressed"],
        sourceType: ["album"],
        success: res => {
          resolve(res);
        }
      });
    });
  },

  /**
   * 全屏预览照片
   *
   * @param current 当前显示图片的链接
   * @param urls 需要预览的图片链接列表(云文件ID @since 2.2.3)
   */
  previewImage: function(current, urls) {
    return new Promise(function(resolve, reject) {
      wx.previewImage({
        current: current,
        urls: urls,
        success: res => {
          resolve(res);
        },
        fail: err => {
          reject(err);
        }
      });
    });
  },

  /**
   * 选择视频
   *
   * @return {"errMsg": String, "tempFilePath": String, "thumbTempFilePath": String, "duration": Number, "width": Number, "height": Number, "size": Number}
   */
  chooseVideo: function() {
    return new Promise(function(resolve, reject) {
      wx.chooseVideo({
        sourceType: ["album"],
        compressed: true,
        maxDuration: 60,
        success: res => {
          resolve(res);
        },
        fail: err => {
          reject(err);
        }
      });
    });
  },

  /**
   * 保存video到本地
   *
   * @param filePath 文件路径
   * @return {"errMsg": String}
   */
  saveVideo: function(filePath) {
    return new Promise(function(resolve, reject) {
      wx.saveVideoToPhotosAlbum({
        filePath: filePath,
        success: res => {
          resolve(res);
        },
        fail: err => {
          reject(err);
        }
      });
    });
  },

  /**
   * 构造媒体存储标记
   *
   * @param index 文件类型 0:视频,1:声音,2:照片
   * @param fileID 云文件ID
   * @param author 上传者
   * @return Object
   */
  mark: function(index, fileID, author) {
    return {
      index: index,
      fileID: fileID,
      author: author
    };
  }
};

# 页面布局

# 底部标签栏

新建一个 Page,在 json 配置文件中引入TabBar组件:

"usingComponents": {
    "i-tab-bar": "../../dist/tab-bar/index",
    "i-tab-bar-item": "../../dist/tab-bar-item/index"
}

新建 4 个 tab 页和一个增加按钮区域,指定每个 tab-itemkey,绑定 TabBarbindchange 事件来监听点击标签页切换事件,指定current 属性可以切换各标签的 icon。这 4 个标签实际是写在同一个 Page 中的,当切换标签时,通过 wx:if 控制各个区域是否显示。

<i-tab-bar
  i-class="bar-high"
  fixed="true"
  current="{{ bar }}"
  bindchange="clickTabBar"
>
  <i-tab-bar-item
    key="video"
    icon="live"
    current-icon="live_fill"
    title="视频"
  ></i-tab-bar-item>
  <i-tab-bar-item
    key="audio"
    icon="play"
    current-icon="play_fill"
    title="声音"
  ></i-tab-bar-item>
  <i-tab-bar-item
    key="add"
    icon="add"
    current-icon="add"
    color="#ffff00"
  ></i-tab-bar-item>
  <i-tab-bar-item
    key="notice"
    icon="remind"
    current-icon="remind_fill"
    title="通知"
  ></i-tab-bar-item>
  <i-tab-bar-item
    key="mine"
    icon="mine"
    current-icon="mine_fill"
    title="我的"
  ></i-tab-bar-item>
</i-tab-bar>

底部标签栏 TabBar 组件的高度为 50px,内容区域可以设置 padding-bottom: 50px; 来避免底部标签页遮挡内容。

# 用户登录

button 组件的 open-type (微信开放能力)提供了用户主动登录的方式。

<button
  open-type="getUserInfo"
  bindgetuserinfo="onGetUserInfo"
  class="user-avatar"
  style="background-image: url({{avatarUrl}})"
></button>

处理用户登录事件,获取用户 openid 用于判断是否登录,查询时作为条件等:

  /**
   * 用户登录获取用户信息
   */
  onGetUserInfo: function(e) {
    let userInfo = e.detail.userInfo
    // 项目模板中默认提供的云函数,可用于获取用户openid
    cloud.call('login', {}).then(res => {
      let openid = res.result.openid
      userInfo.openid = openid
      // 将用户信息写入本地缓存
      wx.setStorage({
        key: 'userInfo',
        data: userInfo,
      })
      // 设置openid
      this.setData({
        openid: openid
      })
    }).catch(err => {
      console.log(err)
    })
    // 设置登录头像
    this.setData({
      avatarUrl: userInfo.avatarUrl
    })
  }

# 遮罩层组件

上传文件等场景中,如果不希望用户在当前操作完成前有其它动作,可以弹出遮罩层保护用户界面。

  • 新增小程序组件:在 miniprogram 目录下新建 components/mask 文件夹,选择 新增Component,默认生成类似 Page 的 4 个文件。
  • 编辑遮罩层组件 mask.wxml
<view class="mask" hidden="{{mask}}"></view>

mask.wxss

.mask {
  width: 100%;
  height: 100%;
  position: fixed;
  background-color: #999;
  z-index: 999;
  top: 0;
  left: 0;
  opacity: 0.1;
}

mask.js

  /**
   * 组件的属性列表
   */
  properties: {
    hidden: {
      type: Boolean,
      value: true
    }
  }
  • 引入组件 编辑 Page 的 json 配置文件:
  "usingComponents": {
    "mask": "../../components/mask/mask"
  }

编辑 Page 的 wxml 文件:

<!-- 遮罩对象 -->
<mask hidden="{{!mask}}"></mask>

在 js 文件中通过指定 mask 属性来打开/关闭遮罩层。

# 消息提示

在 json 配置文件中引入Toast组件。 引入 $Toast 对象:

const { $Toast } = require("../../dist/base/index");
  /**
   * 弹出toast提示
   *
   * @param content loading显示内容
   * @param type toast类型 default、success、warning、error、loading
   * @param modal 遮罩层是否打开
   * @param duration 持续时间,单位s,0为不自动关闭,需调用 $Toast.hide() 方法手动关闭
   * @param mask toast是否可关闭
   */
  popToast: function(content, type, modal, duration, mask) {
    duration = duration || 0
    mask = mask || false
    modal = modal || false
    $Toast({
      content: content,
      type: type,
      duration: duration,
      mask: mask
    })
    // 打开遮罩层
    this.setData({
      hidden: modal
    })
  },

  /**
   * 关闭toast提示
   */
  hideToast: function() {
    $Toast.hide()
    // 关闭遮罩层
    this.setData({
      hidden: true
    })
  }

# 处理多媒体上传

为了将上传文件与上传用户相关联、便于查找上传的文件,将数据库和存储结合使用。即将文件上传到存储后,获取返回的 fileID,与上传者信息、文件类型、文件描述等一起写入数据库。 在 json 配置文件中引入ActionSheet组件。

  <i-action-sheet visible="{{ showAdd }}" actions="{{ addActions }}" show-cancel bind:cancel="cancelAdd" bind:click="handleChooseMedia" />

处理 TabBar 的点击事件,当选择的是新增按钮时,弹出 ActionSheet 选项。

  /**
   * TabBar点击事件
   */
  clickTabBar({
    detail
  }) {
    let key = detail.key
    if ('add' === key) {
      // 点击添加按钮弹出上传选项
      this.setData({
        showAdd: true
      })
    } else {
      this.setData({
        bar: key
      })
    }
  }

弹出新增选项 ActionSheet 点击事件绑定了 handleChooseMedia 函数:

  handleChooseMedia({
    detail
  }) {
    // ActionSheet隐藏
    this.cancelAdd()
    // 登录提示
    if (this.data.openid === '') {
      this.popToast('请先登录~', 'warning', true, 3, true)
      return
    }
    // actions选项的索引,从0开始
    const index = detail.index;
    let that = this;
    if (0 === index) {
      // 视频
      media.chooseVideo().then(res => {
        that.popToast('上传中...', 'loading')
        // 文件
        let tempFilePath = res.tempFilePath
        let tempFilename = that.splitFileName(tempFilePath)
        // 缩略图文件(目前微信开发工具有这个字段,真机无)
        let thumbTempFilePath = res.thumbTempFilePath
        // 上传视频文件
        cloud.upload(tempFilename, tempFilePath).then(res => {
          let mark = media.mark(index, res.fileID, that.data.nickName)
          if (thumbTempFilePath) {
            // 缩略图文件
            let thumbTempFilename = that.splitFileName(thumbTempFilePath)
            // 上传缩略图文件
            cloud.upload(thumbTempFilename, thumbTempFilePath).then(res => {
              let thumb = res.fileID
              mark.thumb = thumb
              // 写入数据库
              cloud.add(mark).then(res => {
                let id = res._id
                // 跳转到编辑视频详情页面
                wx.navigateTo({
                  url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                    .concat('&id=').concat(id)
                })
                that.hideToast()
              }).catch(err => {
                console.log(err)
              })
            })
          } else {
            // 未生成缩略图,使用默认图片
            let thumb = 'cloud://test-d518bb.7465-test-d518bb/system/default-poster.jpg'
            mark.thumb = thumb
            // 写入数据库
            cloud.add(mark).then(res => {
              that.hideToast()
              let id = res._id
              wx.navigateTo({
                url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                  .concat('&id=').concat(id)
              })
            }).catch(err => {
              console.log(err)
            })
          }
        }).catch(err => {
          console.log(err)
        })
      }).catch(err => {
        console.log(err)
      })
    } else if (1 === index) {
      // 照片
      media.chooseImage().then(res => {
        res.tempFilePaths.forEach(function(filePath, i) {
          that.popToast('上传中...', 'loading')
          let filename = that.splitFileName(filePath)
          cloud.upload(filename, filePath).then(res => {
            let mark = media.mark(index, res.fileID, that.data.nickName)
            // 写入数据库
            cloud.add(mark).then(res => {
              that.loadAlbum()
              that.hideToast()
            }).catch(err => {
              console.log(err)
            })
          }).catch(err => {
            console.log(err)
          })
        })
      }).catch(err => {
        console.log(err)
      })
    }
  }

# 视频列表页

在小程序中,video 是原生组件,层级很高,作为列表元素时会遮挡底部标签栏,因此视频列表以缩略图列表的形式呈现,列表区域设置 padding-bottom: 50px; 来保证能够完全呈现。 点击缩略图,可以跳转到新页面观看视频和具体信息。

  • 视频列表区域
<view wx:for="{{videoFiles}}" wx:key="{{item.id}}" class='video-image-warpper'>
  <image class='video-image' mode='aspectFill' src='{{item.thumb}}' bindtap='playVideo' data-index="{{item.id}}"></image>
  <view class="video-like-icon">
    <i-icon type="like_fill" size="28" color="#ff5050" />
    <text>99k+</text>
  </view>
  <i-icon class="play-icon" type="play" size="28" color="#fff" />
</view>
</view>
  • 加载视频列表
  /**
   * 加载视频
   */
  loadVideo: function() {
    let that = this
    cloud.query({
      // 查询该用户上传的文件
      // _openid: that.data.openid,
      index: 0
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID,
          thumb: e.thumb,
          author: e.author,
          desc: e.desc
        })
      })
      that.setData({
        videoFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 视频列表页效果图 视频列表页

# 视频详情页

  • 点击缩略图,跳转到视频详情页 跳转页面传递参数,包括视频和缩略图的 fileID、视频信息等。
playVideo: function(e) {
    let video
    files.forEach(function(element, i) {
      if (e.currentTarget.dataset['index'] === element.id) {
        video = element
        return
      }
    })
    if (undefined !== video) {
      wx.navigateTo({
        url: '../video/video?fileID='.concat(video.fileID)
          .concat('&thumb=').concat(video.thumb)
          .concat('&author=').concat(video.author)
          .concat('&desc=').concat(video.desc == undefined ? '' : video.desc)
      })
    }
  }
<video
  id="video"
  src="{{fileID}}"
  poster="{{thumb}}"
  object-fit="cover"
  direction="0"
  bindended="onVideoEnd"
></video>
  • 保存视频
  /**
   * 保存视频
   */
  saveVideo: function() {
    let that = this
    cloud.download(this.data.fileID).then(res => {
      // 临时文件路径
      let tempFilePath = res.tempFilePath
      media.saveVideo(tempFilePath).then(res => {}).catch(err => {
        console.log(err)
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 视频详情页效果图

视频详情页

# 相册

  • 编辑相册展示区域 图片参数详见官方文档。 图片裁剪、缩放模式选择 mode='aspectFill',从而保持纵横比缩放图片,只保证图片的短边能完全显示出来。
<image
  class="album-image"
  mode="aspectFill"
  wx:for="{{albumFiles}}"
  wx:key="{{item.id}}"
  src="{{item.fileID}}"
  bindtap="imageToPreview"
  data-current="{{item.fileID}}"
/>
  • 加载相册
  /**
   * 初始化album
   *
   * TODO 分页加载
   */
  loadAlbum: function() {
    let that = this
    cloud.query({
      index: 1
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID
        })
      })
      that.setData({
        albumFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 相册效果图

相册

  • 预览图片 图片绑定点击事件,生成预览图片:
  /**
   * 相册图片点击全屏预览
   */
  imageToPreview: function(e) {
    // 被点击图片云文件ID
    let current = e.currentTarget.dataset['current']
    // 所有图片云文件ID
    let fileIDs = []
    this.data.albumFiles.forEach(function(e, i) {
      fileIDs.push(e.fileID)
    })
    media.previewImage(current, fileIDs)
  }
  • 预览效果图

照片预览