四、移动端入门到实战

郁子大约 51 分钟约 15189 字笔记渡一教育中枢课韩刚

(一)uni-app

官方文档open in new window

1.简介

  • 一个使用 Vue.js 开发所有前端应用的框架
  • 开发者编写一套代码,即可发布到 iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台

2.形成

  • 移动端技术太多,跨端框架或是未来发展趋势
  • 一套代码多端发布受开发者青睐
  • 完整的生态,受企业青睐

3.优势

4.和 Vue 的关系

  • 使用 VueJS 开发
  • 在发布到 H5 时,支持所有 vue 语法
  • 发布到 App 和小程序时,实现部分 Vue 语法

5.和小程序的关系

  • 组件标签靠近小程序规范
  • 接口能力(JS API)靠近微信小程序开发
  • 完整的小程序生命周期

6.和 Web 代码编写的区别

(二)uni-app 实战项目介绍

1.项目演示

2.测试账号

  • 账户名: duyiedu
  • 密码:duyiedu

3.项目结构分析

1)首页

  • 搜索引导
    • 搜索页面
  • 导航栏
    • 导航列表展示
    • 导航标签设置
  • 文章列表
    • 文章收藏
    • 图文信息展示
  • 文章详情
    • 作者关注
    • 富文本渲染
    • 文章评论
      • 评论回复
      • 指定评论回复
      • 评论发布
      • 评论组件展示

2)关注

  • 文章
    • 文章列表展示
  • 作者
    • 作者列表展示

3)我的

  • 登录
    • 个人信息展示
    • 我的文章
    • 意见反馈
      • 图片上传
      • 反馈信息上传
  • 未登录
    • 登录信息提示
    • 跳转登录页面

4)注册登录模块

  • 登录
    • 账号登录
      • 账号密码实现登录功能
    • 手机登录
      • 手机号验证码实现登录功能

5)项目整体流程

  • 页面构建
  • 数据处理
  • 逻辑实现
  • 适配发行
    • 多平台适配
    • 多平台打包
    • 多平台发布(安卓、IOS、小程序、h5)

(三)微信小程序简介

1.文档相关

2.开发者工具

1)必选项处理

2)appID 获取

  • 获取微信公众平台 appID

3.小程序代码构成

参考文档open in new window

  • .json 后缀的 JSON 配置文件
  • .wxml 后缀的 WXML 模板文件
  • .wxss 后缀的 WXSS 样式文件
  • .js 后缀的 JS 脚本逻辑文件

4.小程序基本结构

<view class="container">
  <view class="userinfo">
    <button wx:if="{{!hasUserInfo && canIUse}}">获取头像昵称</button>
    <block wx:else>
      <image src="{{userInfo.avatarUrl}}" background-size="cover"></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
  </view>
  <view class="usermotto">
    <text class="user-motto">{{motto}}</text>
  </view>
</view>

5.小程序基本操作

1)配置信息

{
  "pages": ["pages/index/index", "pages/logs/index"],
  "window": {
    "navigationBarTitleText": "Demo"
  },
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页"
      },
      {
        "pagePath": "pages/logs/index",
        "text": "日志"
      }
    ]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true
}
  • 页面配置
{
  "navigationBarBackgroundColor": "#ffffff",
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "微信接口功能演示",
  "backgroundColor": "#eeeeee",
  "backgroundTextStyle": "light"
}

2)生命周期

  • 全局生命周期函数
  /**
   * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
   */
  onLaunch: function () {

  },

  /**
   * 当小程序启动,或从后台进入前台显示,会触发 onShow
   */
  onShow: function (options) {

  },

  /**
   * 当小程序从前台进入后台,会触发 onHide
   */
  onHide: function () {

  },

  /**
   * 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
   */
  onError: function (msg) {

  }
  onLoad: function(options) {
    // 页面创建时执行
  },
  onShow: function() {
    // 页面出现在前台时执行
  },
  onReady: function() {
    // 页面首次渲染完毕时执行
  },
  onHide: function() {
    // 页面从前台变为后台时执行
  },
  onUnload: function() {
    // 页面销毁时执行
  },
  onPullDownRefresh: function() {
    // 触发下拉刷新时执行
  },
  onReachBottom: function() {
    // 页面触底时执行
  },
  onShareAppMessage: function () {
    // 页面被用户分享时执行
  },
  onPageScroll: function() {
    // 页面滚动时执行
  },
  onResize: function() {
    // 页面尺寸变化时执行
  }
Component({
  lifetimes: {
    created() {
      console.log("created,组件实例刚刚被创建好时, created 生命周期被触发");
    },
    attached() {
      console.log("组件实例进入页面节点树时执行");
    },
    detached() {
      console.log("在组件实例被从页面节点树移除时执行");
    },
  },
});

3)页面跳转

<navigator open-type="navigateTo" />
  • 页面重定向
    • 调用 API: wx.redirectTo
<navigator open-type="redirectTo" />
  • 页面返回
    • 调用 API: wx.navigateBack
    • 用户按左上角返回按钮
<navigator open-type="navigateBack" />
  • Tab 切换
    • 调用 API: wx.switchTab
    • 用户切换 Tab
<navigator open-type="switchTab" />
  • 重启动
    • 调用 API: wx.reLaunch
<navigator open-type="reLaunch" />

4)数据绑定

<view>{{message}}</view>
Page({
  data: {
    message: "hello world",
  },
});

5)条件渲染

<view wx:if="{{isShow}}">条件判断显示</view>
Page({
  data:{
    isShow:false
  }
})

6)列表渲染

<view wx:for="{{list}}" wx:for-index="idx" wx:for-item="itemName">{{idx}}: {{itemName.name}}</view>
Page({
  data: {
    list: [{ name: "a" }, { name: "b" }],
  },
});

(四)uni-app 环境搭建

1.uni-app 开发规范

<template>
  <view>页面内容</view>
</template>

<script>
  export default {
    data() {
      return {};
    },
    methods: {},
  };
</script>

<style></style>
uni.getStorageInfoSync();
  • 数据绑定事件处理同 Vue 规范
<template>
  <view @click="onClickFn">点击事件绑定</view>
</template>

<script>
  export default {
    methods: {
      onClickFn() {
        console.log("click事件");
      },
    },
  };
</script>

<style lang="scss" scoped></style>
  • 兼容多端运行,使用 flex 布局开发

2.uni-app 开发环境

1)开发工具

  • uni-app 官方推荐使用 HBuilderX 来开发 uni-app 类型的项目
  • 优点
    • 模板丰富
    • 完善的智能提示
    • 一键运行

2)下载 HBuilderX

详细安装文档open in new window

  • 访问 HBuilderX 官网首页open in new window
  • 点击首页的 DOWNLOAD 按钮
  • 选择下载 正式版 或者 Alpha 版(App 开发版)
  • 解压缩下载的 zip 包
  • 将解压之后的文件夹,存放到 纯英文 的目录中(且不能包含括号等特殊字符)
  • 双击 HBuilderX.exe 即可启动 HBuilderX

3.工程搭建

  • 文件 => 新建 => 项目

  • 填写项目基本信息

  • 项目创建成功

4.基本目录结构

目录名/文件名作用
【pages】内部存放所有页面
【static】存放所有静态资源,比如图片,字体图标
【unpackage】存放所有打包生成后的文件
app.vue应用配置,用来配置 App 全局样式以及监听应用生命周期
main.jsVue 初始化入口文件
manifest.json配置应用名称、appid、logo、版本等打包信息
pages.json配置页面路由、导航条、选项卡等页面类信息
uni.scss用途是为了方便整体控制应用的风格。比如按钮颜色、边框风格,uni.scss 文件里预置了一批 scss 变量预置

5.项目运行

1)浏览器运行

2)小程序运行

  • 填写自己的微信小程序的 AppID

  • 在 HBuilderX 中,配置“微信开发者工具”的安装路径

  • 在微信开发者工具中,通过设置-安全设置面板,开启“微信开发者工具”的服务端口

  • 在 HBuilderX 中,点击菜单栏中的运行-运行到小程序模拟器-微信开发者工具,将当前 uni-app 项目编译之后,自动运行到微信开发者工具中,从而方便查看项目效果与调试

3)app 真机运行

​ 确保手机与电脑是在同一个局域网下

  • 手机开启开发者模式
  • 选择数据管理
  • HBuilderX 选择真机运行
  • 等待基座安装
  • 安装完成手机运行项目

4)iOS 模拟器运行

  • 下载 XCode
  • 定义版本,运行模拟器

(五)uni-app 基础 API

参考文档open in new window

1.基础组件

组件演示open in new window

  • 基础组件在 uni-app 框架中已经内置
  • 无需将内置组件的文件导入项目,也无需注册内置组件
  • 随时可以直接使用,比如 <view> 组件

1)视图容器

组件作用
view视图容器,类似于 html 中的 div
scroll-viewopen in new window可滚动试图容器,要有宽度或高度才能滚动
swiper滑块视图容器,比如用于轮播 banner

2)基础内容

组件作用
icon图标,uni-icons
text文字,无法进行标签文本字符串解析
rich-text文字,使用 nodes 属性可以解析标签文本字符串
progress进度条

3)表单组件(Form)

组件作用
button按钮
checkbox多项选择器
editor富文本输入框
form表单
input输入框
label标签
picker弹出式聊表选择器
picker-view窗体内嵌入式聊表选择器
radio单项选择器
slider滑动选择器
switch开关选择器
textarea多行文本输入框

4)路由与页面跳转(Navigation)

组件作用
navigator页面链接,类似于 html 中的 a 标签

5)媒体组件

组件作用
audio音频
camera相机
image图片
video视频

6)组件公共属性集合

  • 特殊属性以 v- 开头,称为 vue 指令
  • 如 v-if、v-else、v-for、v-model

2.扩展组件

参考文档open in new window

3.自定义组件

  • Components 文件夹下定义组件
  • 页面引用组件,无需导入,直接使用即可
  • 其他操作(组件传值,事件绑定)同 Vue

4.基础 API

参考文档open in new window

1)网络请求

API作用
uni.request发起网络请求

参考文档open in new window

  • 解决 uni.request 网络请求 API 相对简单的问题
    • 可使用 @escook/request-miniprogram 处理网路请求
  • 在小程序中,无法使用 fetch 及 axios 发送网络请求
    • 测试接口地址:https://study.duyiedu.com/api/herolist

2)上传和下载

API作用
uni.unloadFileopen in new window上传文件
uni.downloadFile下载文件

3)图片处理

API作用
uni.chooseImageopen in new window从相册选择图片,或者拍照
uni.previewImage预览图片
uni.getImageInfo获取图片信息

4)数据缓存

参考链接open in new window

API作用
uni.getStorage异步获取本地数据缓存
uni.getStorageSync同步获取本地数据缓存
uni.setStorage异步设置本地数据缓存
uni.setStorageSync同步设置本地数据缓存
uni.removeStorage异步删除本地数据缓存
uni.removeStorageSync同步删除本地数据缓存

5)交互反馈

参考链接open in new window

API作用
uni.showToast显示提示框
uni.showLoading显示加载提示框
uni.hideToast隐藏提示框
uni.hideLoading隐藏加载提示框
uni.showModal显示模态框
uni.showActionSheet显示菜单列表

6)路由

API作用
uni.navigateTo保留当前页面,跳转到应用内某个页面
使用 uni.navigateBack 返回原页面
uni.redirectTo关闭当前页面,跳转到应用内的某个页面
uni.reLaunch关闭所有页面,打开应用内的某个页面
uni.switchTab跳转到 tab Bar 页面

5.页面布局相关

1)page

  • 页面容器 css 属性
page {
  height: 100%;
  background-color: red;
}

2)尺寸单位

  • px
  • rpx
  • upx
  • rem
  • vh
  • vw

3)外部样式文件引入

  • 同 vue 使用相同

6.uni-app 生命周期

参考文档open in new window

1)应用生命周期

  • 只能在 App.vue 里面监听,在其他页面监听无效
钩子作用
onLaunch初始化完成时触发,全局只触发一次
onShowuni-app 启动,或从后台进入前台显示时触发
onHideuni-app 应用从前台进入后台时触发

2)页面生命周期

钩子作用
onLoad监听页面加载
可获取上个页面传递的参数
onShow监听页面显示,每次出现在屏幕上都触发
onReady监听页面初次渲染完成
onHide监听页面隐藏
onUnload监听页面卸载
onReachBottom页面滚动到底部事件

3)组件生命周期

钩子作用
beforeCreate组件创建前
created组件创建后,组件挂载前
beforeMount组件挂载前
mounted组件挂载后
beforeDestroy组件销毁前
destroyed组件销毁后

7.uni-app 特色

1)条件编译

官方文档open in new window

  • 条件编译是用特殊的注释作为标记
    • 在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台
  • #ifdef#ifndef%PLATFORM% 开头,以 #endif 结尾
写法说明
#ifdefif defined,仅在某平台存在
#ifndefif not defined,除了某平台均存在
%PLATFORM%平台名称

  • %PLATFORM% 取值
生效条件
VUE3HBuilderX 3.2.0+(uni-app js 引擎版)open in new window
APPApp
APP-PLUSApp(uni-app js 引擎版)
APP-PLUS-NVUE 或 APP-NVUEApp nvue 页面
APP-ANDROIDApp Android 平台
仅限 uts 文件
APP-IOSApp iOS 平台
仅限 uts 文件
UNI-APP-XHBuilderX 3.9.0+(uni-app x 项目)
H5H5
MP-WEIXIN微信小程序
MP-ALIPAY支付宝小程序
MP-BAIDU百度小程序
MP-TOUTIAO抖音小程序
MP-LARK飞书小程序
MP-QQQQ 小程序
MP-KUAISHOU快手小程序
MP-JD京东小程序
MP-360360 小程序
MP微信小程序/支付宝小程序/百度小程序/抖音小程序/飞书小程序/QQ 小程序/360 小程序
QUICKAPP-WEBVIEW快应用通用(包含联盟、华为)
QUICKAPP-WEBVIEW-UNION快应用联盟
QUICKAPP-WEBVIEW-HUAWEI快应用华为
  • 条件编译支持的文件
    • .vue
    • .js
    • .css
    • pages.json
    • 各预编译语言文件
      • .scss
      • .less
      • .stylus
      • .ts
      • .pug
  • 条件编译是利用注释实现的,在不同语法里注释写法不一样
    • js 使用 // 注释
    • css 使用 /* 注释 */
    • vue/nvue 模板里使用 <!-- 注释 -->

2)插件安装

(六)uniCloud 基础配置

1.HBuilderX 中使用 uniCloud 云开发平台

uniCloud 参考文档open in new window


Web 控制台文档open in new window

1)传统业务开发流程

  • 前端 => 后端 => 运维 => 发布上线

2)使用 uniCloud 云开发平台

  • 前端 => 运维 => 发布上线

3)简介

  • uniCloud 是 DCloud 联合阿里云、腾讯云,为开发者提供的基于 serverless 模式和 js 编程的实现后端服务的云开发平台
  • 不需要服务器的购买配置即可快速创建一个完整的后端服务

4)优点

  • 用 JavaScript 开发前后台整体业务
  • 非 H5 项目免域名使用服务器
  • 敏捷性业务处理,不需要前后端分离开发

2.开发流程

1)云函数

2)云数据库

3)云存储及 CDN

参考文档open in new window

  • 可存储文件

3.创建云函数工程

1)指定 uniCloud 工程创建

2)保证 uni-app 应用标识 appID 填写

  • 保证用户为登录状态

3)创建云服务空间

  • 如果未实名认证,会跳转至实名认证页面认证
  • 等待实名认证审核之后可以开通服务空间
  • 若腾讯云实名认证提示身份证下已创建过多账户,则需要在腾讯云官网注销不用的账户

4)创建云函数

"use strict";
// 一个通过nodeJS运行的函数在服务器端使用
exports.main = async (event, context) => {
  // event为客户端上传的参数
  // context 包含了调用信息及运行状态,获取每次调用的上下文
  console.log("event : ", event);

  // 返回数据给客户端
  return {
    code: 0,
    msg: "云函数调用成功",
  };
};

5)云 WEB 控制台查看

6)云数据库操作

  • 在云数据库中操作数据,全部使用 双引号 定义值

7)云存储

  • 在云存储中上传文件
  • API 使用
uniCloud.uploadFile({});

8)跨域处理

参考文档open in new window

(七)uniCloud API 操作

1.云函数调用

参考文档open in new window

1)promise 方式

uniCloud
  .callFunction({
    name: "test", // 云函数名称
    data: {
      a: 1,
    }, // 请求参数
  })
  .then((res) => {});

2)callback 方式

uniCloud.callFunction({
  name: "test",
  data: {
    a: 1,
  },
  success() {}, // 成功
  fail() {}, // 失败
  complete() {}, // 完成(不管成功与失败)
});

2.云函数实现云数据库基本增删改查

1)获取数据库引用

const db = uniCloud.database();

2)获取数据表集合引用

const collection = db.collection("unicloud-test-714"); // unicloud-test-714 为数据表名称

3)新增记录

const res = collection.add({
  user: "alan",
});
"use strict";
const db = uniCloud.database(); // 获取数据库引用

exports.main = async (event, context) => {
  // 获取集合引用
  const collection = db.collection("unicloud-test-714");
  // 新增数据
  const res = await collection.add({
    user: "alan",
  });
  console.log(res);
  return {
    code: 0,
    msg: "云函数调用成功",
  };
};

4)删除记录

const res = await collection.doc("60ee51103b7d3500014124c1").remove();

5)数据更新

const res = await collection.doc("60ee52a1827eca0001e56bc4").update({
  name: "joob",
});

// 如果获取不到内容,需要重新插入记录
const res = await collection.doc("60ee52a1827eca0001e56bc4").set({
  name: "joob",
  type: "javascript",
});

update 与 set 的区别

  • 当没有找到指定记录时,使用 update 无法更新
  • 当没有找到指定记录时,使用 set 可以新增内容
    • 不存在创建添加

6)数据查找

// 查询全部
const res = await collection.get();

// 指定条件查询-查询id
const res = await collection.doc("id").get();

// 指定条件查询-查询其他条件
const res = await collection
  .where({
    name: "alan",
  })
  .get();

3.云存储操作

1)图片选择获取

参考文档open in new window

  • 使用 uni.chooseImage
uni.chooseImage({
  count: 1,
  success(res) {
    console.log(JSON.stringify(res.tempFilePaths));
  },
});

2)文件上传

参考文档open in new window

  • 使用 uniCloud.uploadFile
uni.chooseImage({
  count: 1,
  async success(res) {
    let result = await uniCloud.uploadFile({
      filePath: res.tempFilePaths[0],
      cloudPath: "a.jpg",
      success(res) {
        console.log(res);
      },
      fail(err) {
        console.log(err);
      },
    });
  },
});

3)图片删除

参考文档open in new window

  • 使用 uniCloud.deleteFile
  • 阿里云函数删除不能在客户端删除,应该在云函数中删除
let result = await uniCloud.deleteFile({
  fileList: ["https://vkceyugu.cdn.bspapp.com/VKCEYUGU-6ce25980-c28e-4e78-bdef-a96eb40ad98b/06a1cb3a-84b7-47a0-b554-8aff299cb255.jpg"],
});
console.log(result);

(八)项目搭建

1.初始化数据库

1)定义(选择)云服务空间

2)初始化数据库

参考文档open in new window

  • 使用 db_init.json 文件
  • 初始化 db_init.json 文件
    • source 文件夹 => db_init.json 文件
    • uniCloud 目录找到 database 目录,添加 db_init.json 文件

2.静态文件配置

1)导入 static 文件

  • 导入项目中需要的图片文件到 static/img/ 目录下

2)定义 css 预编译处理器

  • uni.scss 文件定义公共变量及混编方法
  • 每个页面下直接使用样式方法及变量
@mixin flex($level_style: space-between, $vertical_style: row, $isWrapper: nowrap) {
  display: flex;
  align-items: center;
  justify-content: $level_style;
  flex-wrap: $isWrapper;
  flex-direction: $vertical_style;
}

$base-color: #f25037;

* {
  box-sizing: border-box;

  &::before,
  &::after {
    box-sizing: border-box;
  }
}

3)创建 tabBar

参考文档open in new window

  • pages 目录中,创建首页(Home)、我的(Self)、关注(Follow) 3 个 tabBar 页面
  • 在 HBuilderX 中快速新建页面
    • pages 目录上鼠标右键,选择 新建页面
    • 在弹出的窗口中, 勾选 scss 模板、填写 页面的名称 之后,点击创建按钮
  • 配置 tabBar 效果,修改项目根目录中的 pages.json 配置文件,新增 tabBar 的配置节点
"tabBar": {
  "color": "#666",
  "selectedColor": "#f25037",
  "backgroundColor": "#fff",
  // 显示页面信息
  "list": [{
      // 页面路径
      "pagePath": "pages/Home/Home",
      // 默认图片
      "iconPath": "static/img/home.png",
      // 选中图片
      "selectedIconPath": "static/img/home-active.png",
      // 文字描述信息
      "text": "首页"
    },
    {
      "pagePath": "pages/Follow/Follow",
      "iconPath": "static/img/follow.png",
      "selectedIconPath": "static/img/follow-active.png",
      "text": "关注"
    },
    {
      "pagePath": "pages/Self/Self",
      "iconPath": "static/img/my.png",
      "selectedIconPath": "static/img/my-active.png",
      "text": "我的"
    }
  ]
}
  • 修改 globalStyle 样式
"globalStyle": {
  "navigationBarTextStyle": "white",
  "navigationBarTitleText": "渡一教育",
  "navigationBarBackgroundColor": "#f25037",
  "backgroundColor": "#F8F8F8"
}

4)创建 tabBar 需要的页面文件

  • Home.vue
  • Follow.vue
  • Self.vue

5)pages.json 设置前景色

"navigationBarTextStyle": "white"

(九)制作导航栏并适配多端

1.定义导航栏组件 NavBar

2.引入导航栏组件

  • 同名组件支持 easycom 规范
  • 不需要使用 import 导入
  • 特点:局部引入
easyCom components/组件名/组件名.vue

3.搭建结构

uni-icons 图标插件open in new window

4.处理小程序显示错位问题

参考文档open in new window

// 获取手机系统信息
const systemInfo = uni.getSystemInfoSync();
// 设置状态栏高度
this.statusBarHeight = systemInfo.statusBarHeight ? systemInfo.statusBarHeight * 2 : 20;

5.获取胶囊信息

参考文档open in new window

  • 需要实现条件编译
// #ifdef MP-WEIXIN
// 获取胶囊信息
const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
this.marginRight = menuButtonInfo.width * 2 + 50;
this.statusBarHeight = menuButtonInfo.top * 2;
// #endif

// (胶囊底部高度 - 状态栏的高度) + (胶囊顶部高度 - 状态栏内的高度) = 导航栏的高度
this.navBarHeight = menuButtonInfo.bottom - info.statusBarHeight + (menuButtonInfo.top - info.statusBarHeight);

(十)制作选项卡

1.创建组件

  • 定义组件 TabBar.vue
  • 在 Home.vue 中引入

2.使用 scroll-view 组件

参考文档open in new window

  • 使用 scroll-view 横向滚动的时候,内部需添加一个容器包裹滚动内容
<scroll-view class="tab-scroll" scroll-x="true">
  <view class="tab-scroll-box">
    <view
      v-for="(item, index) in labelList"
      :class="{ active: activeIndex === index }"
      :key="index"
      @click="handleClickTabBarItem(index)"
      class="tab-scroll-item"
    >
      {{ item.name }}
    </view>
  </view>
</scroll-view>

3.点击设置按 icon 跳转到设置标签页面

<view class="tab-icons">
  <uni-icons @click="toLabelEditPage" type="gear" size="26" color="#666"></uni-icons>
</view>
  • 创建 LabelEdit 页面后跳转
toLabelEditPage() {
  uni.navigateTo({
    url: '/pages/LabelEdit/LabelEdit'
  });
}

4.数据获取

  • 在 Home.vue 的 onLoad 生命周期内定义 getLabelList 方法
onLoad() {
  this.getLabelList();
}
  • 定义云函数,获取 label 表中的数据
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  // event为客户端上传的参数
  const collection = db.collection("label");
  const res = await collection.get();

  // 返回数据给客户端
  return {
    code: 0,
    data: res.data,
  };
};
  • Home.vue 获取数据,并将数据传递到 TabBar 组件
<TabBar :labelList="labelList"></TabBar>
  • 使用 uniCloud.callFunction 方法获取数据
getLabelList() {
  uniCloud.callFunction({
    name: 'getLabelList',
    success: (res) => {
      this.labelList = res.result.data;
    }
  });
}
  • TabBar 组件内部使用 props 属性获取数据
props: {
  labelList: {
    type: Array,
    default: () => []
  }
}

(十一)封装请求方法

  • 减少代码的冗余
  • 优化代码的可读性及可维护性

1.定义公共的 http 请求方法

  • 创建 http.js 文件,导出一个封装好的 promise 对象
  • 内部调用 uniCloud
export default ({ name, data = {} }) => {
  return new Promise((resolve, reject) => {
    uni.showLoading({});
    uniCloud.callFunction({
      name, // 要调用的云函数名称
      data,
      success({ result }) {
        if (result.code === 0) {
          resolve(result.data);
        } else {
          uni.showToast({
            icon: "error",
            title: result.msg,
          });
        }
      },
      fail(err) {
        // 网络错误或后端错误
        reject(err);
      },
      complete() {
        // 不论请求成功还是失败都执行
        uni.hideLoading();
      },
    });
  });
};

2.创建接口文件调用公共方法

  • ajax/api/interface/home.js
import ajax from "../../http.js";

export const _getLabelList = (data) =>
  ajax({
    name: "getLabelList",
    data,
  });

3.将 http 方法挂载到 Vue 原型上

  • 供每个页面使用
  • 使用 Webpack 的 require.context 方法对所有的请求函数收集
  • 通过执行 require.context() 获取一个特定的上下文
    • 主要用来实现自动化导入模块
    • 在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个 API
    • 会遍历文件夹中的指定文件,然后自动导入,不需要每次显式调用 import 导入模块
  • ajax/api/index.js
/**
 * 批量导出文件
 * 参数1:API目录的相对路径
 * 参数2:是否查询子目录
 * 参数3:需要查询的文件后缀名
 */

const requireApi = require.context(".", true, /.js$/);
let modules = {};
requireApi.keys().forEach((key, index) => {
  // 当前文件,跳过
  if (key === "./index.js") return;

  Object.assign(modules, requireApi(key));
});
export default modules;
  • main.js 挂载 $http 方法
import modules from "./ajax/api/index.js";
Vue.prototype.$http = modules;

4.页面/组件内部调用方法

async getLabelList() {
  const res = await this.$http.getLabelList();
  this.labelList = res;
}

(十二)制作文章列表

1.容器组件

1)定义 ArticleList 组件

  • 使用 swiper 组件open in new window 实现滚动效果
  • SwiperItem 数量动态变化,当前的 SwiperItem 数量应该与选项卡的数量相同
  • 获取选项卡的数量,根据选项卡数量渲染 SwiperItem
  • Home.vue 传递 labelList 和 activeIndex
<ArticleList :labelList="labelList" :activeIndex="activeIndex" @changeActiveIndex="handleChangeActiveIndex"></ArticleList>
  • ArticleList 内根据 labelList 数量渲染 SwiperItem
<swiper class="swiper-container">
  <swiper-item v-for="(item, index) in labelList" :key="index">
    <view class="swiper-item uni-bg-red">
      <ListItem :item="item"></ListItem>
    </view>
  </swiper-item>
</swiper>

2)实现选项卡与 Swiper 组件联动效果

  • 绑定选项卡点击事件
    • 发送事件,调整 activeIndex 值,将 activeIndex 值调整为父组件传递的值
  • Swiper 指定 current 属性为 currentIndex
    • 观察 activeIndex 的变化,同步变化 currentIndex
  • 监听 Swiper 的 change 事件,触发父组件改变 activeIndex 属性
<swiper :current="currentIndex" @change="changeActiveIndex" class="swiper-container">
  <swiper-item v-for="(item, index) in labelList" :key="index">
    <view class="swiper-item uni-bg-red">
      <ListItem :item="item"></ListItem>
    </view>
  </swiper-item>
</swiper>
data() {
  return {
    currentIndex: 0
  };
},
watch: {
  activeIndex(newVal) {
    this.currentIndex = newVal;
  }
},
methods: {
  changeActiveIndex(e) {
    const idx = e.detail.current;
    this.$emit('changeActiveIndex', idx);
  }
}

3)选项卡自动调整位置

  • scroll-view 组件open in new window 添加属性 scroll-with-animationscroll-left 属性
  • 动态设置 scroll-into-view 属性
  • 为每一项添加 ID 属性切换跳转
    • 值应为某子元素 id(id 不能以数字开头)
<scroll-view :scroll-with-animation="true" :scroll-into-view="`item${activeIndex}`" class="tab-scroll" scroll-x="true">
  <view class="tab-scroll-box">
    <view
      v-for="(item, index) in labelList"
      :class="{ active: activeIndex === index }"
      :key="index"
      :id="`item${index}`"
      @click="handleClickTabBarItem(index)"
      class="tab-scroll-item"
    >
      {{ item.name }}
    </view>
  </view>
</scroll-view>

2.文章卡片

1)创建文章组件 ListItem

  • 使用 scroll-view 实现竖向滚动容器
<view class="list-scroll-container">
  <scroll-view scroll-y="true" class="list-scroll">
    <view>
      <ListCard v-for="item in 50" :key="item"></ListCard>
    </view>
  </scroll-view>
</view>
  • 注意在样式定义时设定多级高度
.list-scroll-container {
  height: 100%;

  .list-scroll {
    overflow: hidden;
    height: 100%;
  }
}

2)定义文章卡片组件 ListCard

  • 创建基本样式及结构
  • 定义多状态卡片模块
  • 通过父组件传递 mode 属性
  • 根据 mode 展示相应类型的卡片

3)定义 uni-app 模板

  • 需要使用自定义模板的场景
    • 调整页面 head 中的 meta 配置
    • 补充 SEO 相关的一些配置(仅首页)
    • 加入百度统计等三方 js

参考文档open in new window

  • Referrer 是 HTTP 请求头中的一部分,用于指示浏览器从哪个页面跳转到了当前页面
  • Referrer-Policy 是一个新的 HTTP 头,用于控制浏览器如何发送 Referrer 信息
  • never 表示浏览器永远不会发送 Referrer 信息,即使用户从其他页面跳转到当前页面
    • 可以提高用户的隐私保护,防止敏感信息泄露
    • 解决请求某些第三方网站资源 403 错误
<!-- 设置页面的Referrer-Policy -->
<meta name="referrer" content="never" />
  • 根目录下创建 index.html 文件
  • manifest 文件的 Web 配置中 index 模板路径使用 index.html 文件
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <meta name="referrer" content="never" />
    <title><%= htmlWebpackPlugin.options.title %></title>
    <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
  </head>
  <body>
    <noscript>
      <strong>Please enable JavaScript to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>









 








3.数据渲染

1)创建云函数

  • 定义云函数 getArticleList
  • 删除不需要返回的文章内容 content
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const res = await db
    .collection("article")
    .aggregate()
    .project({
      content: 0,
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "数据请求成功",
    data: res.data,
  };
};





 
 
 
 
 








  • 配置 interface
export const getArticleList = (data) =>
  ajax({
    name: "getArticleList",
    data,
  });

2)前端获取数据

  • ArticleList 组件
created() {
  this.getArticleList();
},
methods: {
  async getArticleList() {
    const res = await this.$http.getArticleList();
    this.articleList = res;
  }
}

3)数据渲染

  • ListItem 组件中循环渲染
<view class="list-scroll-container">
  <scroll-view scroll-y="true" class="list-scroll">
    <view>
      <ListCard v-for="article in articleList" :article="article" :key="article._id"></ListCard>
    </view>
  </scroll-view>
</view>

4)根据选项卡分类渲染数据

  • Home.vue 添加“全部”分类
async getLabelList() {
  const res = await this.$http.getLabelList();
  this.labelList = [
    {
      name: '全部'
    },
    ...res
  ];
}
  • 请求 ArticleList 时传递数据
  • 为了保证导航数据的正确获取,调整 created 函数的 getArticleList 方法在 watch 中调用
// created() {
//   this.getArticleList();
// },
watch: {
  activeIndex(newVal) {
    this.currentIndex = newVal;
  },
  labelList() {
    this.getArticleList();
  }
}
  • 云函数过滤数据
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { classify } = event;

  let match = {};
  if (classify !== "全部") {
    match = {
      classify,
    };
  }

  const res = await db
    .collection("article")
    .aggregate() // 使用聚合的形式获取数据
    .match(match) // 根据匹配条件返回数据
    .project({
      content: 0, // 本次查询不需要返回文章内容 content 字段
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "数据请求成功",
    data: res.data,
  };
};
  • 前端缓存数据
    • 将数组形式的文章列表转换存储为对象形式
    • 每次 change 切换卡片内容时判断是否有缓存数据
    • 使用 $set 方法重新渲染页面
data() {
  return {
    currentIndex: 0,
    // articleList: []
    articleData: {}
  };
},
watch: {
  activeIndex(newVal) {
    this.currentIndex = newVal;
  },
  labelList() {
    this.getArticleList(this.currentIndex);
  }
},
methods: {
  changeActiveIndex(e) {
    const idx = e.detail.current;
    this.$emit('changeActiveIndex', idx);
    if (!this.articleData[idx] || this.articleData[idx].length === 0) {
      this.getArticleList(idx);
    }
  },
  async getArticleList(idx) {
    // const res = await this.$http.getArticleList();
    // this.articleList = res;
    const res = await this.$http.getArticleList({
      classify: this.labelList[idx].name
    });
    this.$set(this.articleData, idx, res);
  }
}

4.上拉加载更多

1)ListItem 组件使用 uni-load-more 插件

下载地址open in new window

<uni-load-more v-if="articleList.length === 0 || articleList.length > 5" :status="loadData.loading"></uni-load-more>
props: {
  articleList: {
    type: Array,
    default: () => []
  },
  loadData: {
    type: Object,
    default: () => ({
      loading: 'loading'
    })
  }
}

2)修改参数传递

  • 云函数添加 page 及 pageSize 属性
  • 云函数内限制返回值
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { classify, page = 1, pageSize = 10 } = event;

  let match = {};
  if (classify !== "全部") {
    match = {
      classify,
    };
  }

  const res = await db
    .collection("article")
    .aggregate() // 使用聚合的形式获取数据
    .match(match) // 根据匹配条件返回数据
    .project({
      content: 0, // 本次查询不需要返回文章内容 content 字段
    })
    .skip(pageSize * (page - 1)) // 首页从0开始
    .limit(pageSize) // 每页最多返回多少条数据
    .end();

  // 统计总数据
  const { total } = await db.collection("article").where(match).count();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "数据请求成功",
    data: {
      data: res.data,
      total,
    },
  };
};
  • 子组件监听 scroll-view 的 scrolltolower 事件
  • 触底时,通知父组件根据当前的 page 值请求新的数据
handleLoadMoreArticle() {
  this.page++;
  this.getArticleList(this.currentIndex);
}
  • 前端调整数据处理,将直接赋值变为追加数据
async getArticleList(idx) {
  const { data: res, total } = await this.$http.getArticleList({
    classify: this.labelList[idx].name,
    page: this.page,
    pageSize: this.pageSize
  });

  // 填充数据时改变为追加数据
  let oldList = this.articleData[idx] || [];
  oldList.push(...res);
  this.$set(this.articleData, idx, oldList);
}

3)分类页数处理

  • 创建每一个分类的存储对象
  • 存储当前请求的 page 和数据加载状态
async getArticleList(idx) {
  // 初始化判断当前分类是否含有数据,并记录当前的分类页数
  if (!this.loadData[idx]) {
    this.loadData[idx] = {
      page: 1,
      loading: 'loading',
      total: 0
    };
  }

  const { data: res, total } = await this.$http.getArticleList({
    classify: this.labelList[idx].name,
    page: this.loadData[idx].page,
    pageSize: this.pageSize
  });

  // 填充数据时改变为追加数据
  let oldList = this.articleData[idx] || [];
  oldList.push(...res);
  this.loadData[idx].total = total;
  this.$set(this.articleData, idx, oldList);
}
  • 处理数据全部加载完成状态
handleLoadMoreArticle() {
  // 没有数据时不再进行数据的请求
  if (this.loadData[this.currentIndex].total === this.articleData[this.currentIndex].length) {
    this.loadData[this.currentIndex] = {
      ...this.loadData[this.currentIndex],
      ...{
        loading: 'noMore',
        page: this.loadData[this.currentIndex].page
      }
    };
    this.$forceUpdate();
    return;
  }
  this.loadData[this.currentIndex].page++;
  this.getArticleList(this.currentIndex);
}

(十三)用户登录

1.搭建登录页面

1)下载 uni-forms 插件

下载地址open in new window

2)封装 SendCode 组件

<template>
  <view class="code-container">
    <view @click="getForm" class="vCode-btn">{{ runTime ? `${time}秒后重新获取` : '获取验证码' }}</view>
  </view>
</template>
  • 创建定时器
  • 组件销毁时,清除定时器副作用
// 离开页面时清除定时器
beforeDestroy () {
  clearInterval(this.timeId);
  this.timeId = null;
  this.runTime = false;
  this.time = 60;
}

2.表单验证

1)创建 userRulesMixin 文件

  • common 文件夹下创建 userRulesMixin
export default {
  // 使用install的形式安装mixin
  install(Vue) {
    Vue.mixin({
      data() {
        return {
          userRules: {
            loginName: {
              rules: [
                {
                  required: true,
                  errorMessage: "账户名不能为空",
                },
                {
                  validateFunction: this.validateLoginName,
                }, // 自定义验证规则
              ],
            },
            password: {
              rules: [
                {
                  required: true,
                  errorMessage: "密码不能为空",
                },
              ],
            },
            phone: {
              rules: [
                {
                  required: true,
                  errorMessage: "电话号码不能为空",
                },
                {
                  validateFunction: this.validatePhone,
                }, // 自定义验证规则
              ],
            },
            vCode: {
              rules: [
                {
                  required: true,
                  errorMessage: "验证码不能为空",
                },
              ],
            },
          },
          mobileReg: /^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/,
        };
      },
      methods: {
        validateLoginName(rule, val, data, callback) {
          switch (true) {
            case val.length < 6:
              callback("用户名长度不正确");
              break;
            default:
              return true;
          }
        },
        /* 验证手机号码 */
        validatePhone(rule, val, data, callback) {
          switch (true) {
            case !this.mobileReg.test(val):
              callback("手机号码格式不正确");
              break;
            default:
              return true;
          }
        },
      },
    });
  },
};

2)main.js 中使用 mixin 文件

import userRulesMixin from "./common/userRulesMixin.js";
Vue.use(userRulesMixin);

3)组件或页面内直接使用

<uni-forms :modelValue="formData" :rules="userRules" class="form" ref="form"></uni-forms>
onLoad() {
  console.log(this.test()); // 调用 mixin 中的 methods
}

验证手机号码的正则表达式

/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/

3.账户名密码登录

1)前端数据整理

  • 定义发送函数,将用户信息和本次请求的用户登录类型传递给后端
async handleSubmitLogin() {
  const res = await this.$refs.form.validate();
  this.sendUserInfo({
    ...res,
    type: this.type
  });
}
  • 创建用户相关请求接口 user.js
import ajax from "../../http.js";

export const sendUserInfo = (data) =>
  ajax({
    name: "sendUserInfo",
    data,
  });
  • 定义云函数 sendUserInfo
"use strict";
const db = new uniCloud.database();
exports.main = async (event, context) => {
  const { loginName, password, phone, type } = event;

  const { affectedDocs, data } = await db
    .collection("user")
    .aggregate()
    .match(
      type === "account"
        ? {
            loginName,
            password,
          }
        : {
            phone,
          },
    )
    .end();

  // 返回数据给客户端
  return affectedDocs
    ? {
        code: 0,
        msg: "获取用户信息成功",
        data: data[0],
      }
    : {
        code: 1,
        msg: type === "account" ? "获取用户信息失败,请检查用户名或密码" : "验证码或手机号码错误",
      };
};
  • 前端接收返回信息,处理数据
    • 跳转到上一个历史记录
    • 使用 uni-app 内置的 Vuex 存储用户信息

2)使用 Vuex 存储用户信息

  • 实例化 Store 对象
  • index.js
import Vue from "vue";
import Vuex from "vuex";
import state from "./state.js";
import mutations from "./mutations.js";

Vue.use(Vuex);

export default new Vuex.Store({
  state,
  mutations,
});
  • state.js
export default {
  userInfo: uni.getStorageSync("userInfo") || null,
};
  • mutations.js
export default {
  updateUserInfo(state, userInfo) {
    uni.setStorageSync("userInfo", userInfo);
    state.userInfo = userInfo;
  },
};
  • main.js 中注册 store
// 使用Vuex
import store from "./store/index.js";

App.mpType = "app";
const app = new Vue({
  ...App,
  store,
});
  • Login.vue 获取用户信息后保存
import { mapMutations } from "vuex";
export default {
  methods: {
    ...mapMutations(["updateUserInfo"]),
    async handleSendUserInfo(userInfo) {
      const res = await this.$http.sendUserInfo(userInfo);
      if (res) {
        this.updateUserInfo(res);
        uni.showToast({
          icon: "success",
          title: "登录成功",
        });
        setTimeout(() => {
          // #ifdef H5
          uni.switchTab({
            url: "/pages/Home/Home",
          });
          // #endif
          // #ifndef H5
          uni.navigateBack();
          // #endif
        }, 2000);
      }
    },
  },
};

4.手机验证码登录

1)处理小程序兼容问题

  • 小程序不兼容 :rules="userRules" 的写法
<SendCode @getFormRefs="handleGetFormRefs" @setCurrentCode="returnCode = $event" class="code-component"></SendCode>
onReady() {
  this.$refs.form.setRules(this.userRules);
}
  • 小程序无法通过 props 获取父组件的 refs 对象
  • 通过父子组件 emit 回调函数传参解决
// Login.vue
handleGetFormRefs(cb) {
  cb && cb(this.$refs.form);
}

// SendCode.vue
getFormRefs() {
  if (this.runTime) return; // 防抖
  this.$emit('getFormRefs', this.handleSendCode);
}

2)定义云函数 sendVerificationCode

"use strict";
exports.main = async (event, context) => {
  const { phone } = event;
  // 创建一个随机数,返回给前端
  const currentNumber = Math.random().toString().substr(2, 6);

  try {
    const res = await uniCloud.sendSms({
      appid: "__UNI__ACE04D2", // manifest.json中进行获取
      smsKey: "25cfa81fcade420555022b1ee4021a8a",
      smsSecret: "d79e45ec2456c4d6ffc7956dba4e1f43",
      phone,
      templateId: "12333", // 获取的短信模版
      data: {
        code: currentNumber,
        expMinute: "1",
      },
    });
    // 调用成功,请注意这时不代表发送成功
    return {
      code: 0,
      data: {
        mobileCode: currentNumber,
        msg: "请在手机注意查收验证码",
      },
    };
  } catch (err) {
    return {
      code: 1,
      msg: "短信验证码发送失败",
    };
  }
};

3)发送验证码

  • 通过 form 对象单独验证手机号码格式
  • 启动定时器,调整文本显示内容
  • 调用云函数,发送验证码
  • 前端接受返回值,保存验证码,加入验证码验证规则
  • 提交用户数据,指定参数 phone 及 type 类型
methods: {
  async handleSendCode(form) {
    const { phone } = await form.validateField(['phone']);
    // 开启定时器
    this.runTime = true;
    this.setTimer();
    // 发送数据请求到云函数
    const { msg, mobileCode } = await this.$http.sendVerificationCode({ phone });
    uni.showToast({
      icon: 'none',
      title: msg
    });
    this.$emit('setCurrentCode', mobileCode);
  },
  setTimer() {
    this.timer = setInterval(() => {
      if (this.time === 1) {
        clearInterval(this.timer);
        this.timer = null;
        this.runTime = false;
        this.time = 60;
      }
      this.time--;
    }, 1000);
  }
},
beforeDestroy() {
  if (this.timer) clearInterval(this.timer);
  this.timer = null;
  this.runTime = false;
  this.time = 60;
}

(十四)实现收藏按钮组件

1.获取用户信息及文章信息

  • 由于多个页面中都会用到 userInfo 对象,可将 userInfo 对象放在一个全局的 mixin 中
  • 保证每个页面或组件内部都可以使用

1)定义 commonMixin 文件

import { mapState, mapMutations } from "vuex";

export default {
  install(Vue) {
    Vue.mixin({
      computed: {
        ...mapState(["userInfo"]),
      },
      methods: {
        ...mapMutations(["updateUserInfo"]),
        // 检测用户是否登录
        async handleCheckedLogin() {
          return new Promise((resolve) => {
            if (this.userInfo) {
              resolve();
            } else {
              uni.navigateTo({
                url: "/pages/UserInfo/Login/Login",
              });
            }
          });
        },
      },
    });
  },
};
// SaveLikes.vue
async handleChangeSaveStatus() {
  await this.handleCheckedLogin();
}

2)main.js 文件中使用 commonMixin 文件

// main.js
import commonMixin from "./common/commonMixin.js";
Vue.use(commonMixin);

2.定义云函数

"use strict";
// 定义数据库引用
const db = uniCloud.database();
// 定义修改指令
const dbCmd = db.command;

exports.main = async (event, context) => {
  const { userId, articleId } = event;
  // 获取用户Ids集合
  const userInfo = await db.collection("user").doc(userId).get();

  const articleIds = userInfo.data[0].article_likes_ids;
  let returnMsg = null;
  let articleArr = null;

  if (articleIds.includes(articleId)) {
    // 如果包含则删除收藏
    articleArr = dbCmd.pull(articleId);
    returnMsg = "取消收藏成功";
  } else {
    articleArr = dbCmd.addToSet(articleId);
    returnMsg = "添加收藏成功";
  }

  await db.collection("user").doc(userId).update({
    article_likes_ids: articleArr,
  });
  const updateUserInfo = await db.collection("user").doc(userId).get();

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: returnMsg,
      newUserInfo: updateUserInfo.data[0],
    },
  };
};

3.调用云函数

1)将文章信息传递到 SaveLikes.vue 内部使用

<!-- ListCard.vue -->
<SaveLikes :item="article"></SaveLikes>

2)暴露云函数调用

export const updateSaveLikes = (data) =>
  ajax({
    name: "updateSaveLikes",
    data,
  });

3)点击 icon 发送数据到云服务器

  • 修改存储的用户信息 article_likes_ids 数组
  • 弹出提示信息
async handleChangeSaveStatus() {
  await this.handleCheckedLogin();
  const { msg, newUserInfo } = await this.$http.updateSaveLikes({
    articleId: this.item._id,
    userId: this.userInfo._id
  });
  uni.showToast({
    icon: 'success',
    title: msg
  });
  this.updateUserInfo({
    ...this.userInfo,
    ...newUserInfo
  });
}
  • 重新对保存状态 icon 图标赋值
<view @click.stop="handleChangeSaveStatus" class="save-icons">
  <uni-icons :type="isLike ? 'heart-filled' : 'heart'" color="#ff6600" size="20"></uni-icons>
</view>
  • 使用计算属性保存用户收藏状态
computed: {
  isLike () {
    return this.userInfo && this.userInfo.article_likes_ids.includes(this.item._id);
  }
}

(十五)搜索页面

1.调整 NavBar 为动态组件

  • 添加是否为搜索页面的判断

1)isSearch

  • 当页面为搜索页面时,添加返回 icon 图标,并绑定返回事件
<view v-if="isSearch" :style="{ top: statusBarHeight + 'rpx' }" @click="returnArticleList" class="return-icon">
  <uni-icons type="back" size="22" color="white"></uni-icons>
</view>

2)为点击事件添加条件处理

  • 当页面为搜索页面时,阻止跳转事件
toSearchPage() {
  if (this.isSearch) return;
  uni.navigateTo({
    url: '/pages/Search/Search'
  });
}

3)添加回退事件

  • 根据平台使用指定事件
returnArticleList() {
  // #ifdef H5
  uni.switchTab({
    url: '/pages/Home/Home'
  });
  // #endif
  // #ifndef H5
  uni.navigateBack();
  // #endif
}

2.搭建搜索页面结构

  • 根据当前状态,条件渲染
<view class="search-container">
  <!-- 搜索导航组件 -->
  <NavBar :isSearch="isSearch"></NavBar>
  <!-- 搜索包裹 -->
  <view class="search-wrapper">
    <!-- 没有进行搜索的操作 -->
    <view v-if="false" class="search-history-container">
      <!-- 头部 -->
      <view class="search-header">
        <text class="history-text">搜索历史</text>
        <text class="history-clean">清空</text>
      </view>
      <!-- 内容部分 -->
      <view class="search-history-content">
        <view v-for="item in 10" :key="item" class="history-content-item">直播</view>
      </view>

      <view class="no-data">当前没有搜索历史</view>
    </view>

    <!-- 开始进行搜索的操作 -->
    <view v-else class="search-list-container">
      <ListItem v-if="searchList.length"></ListItem>
      <view v-else class="no-data">没有搜索到相关数据</view>
    </view>
  </view>
</view>

3.业务逻辑处理

1)获取文本输入内容

input 组件参考文档open in new window

  • 添加右下角点击按钮事件
  • 调整右下角显示文字
<NavBar :isSearch="isSearch" :parentVal="parentVal" @sendSearchData="handleSendSearchKeyword" @updateVal="parentVal = $event"></NavBar>
  • 父子组件实现数据双向绑定
// NavBar.vue
computed: {
  searchVal: {
    get() {
      return this.parentVal;
    },
    set(val) {
      this.$emit('updateVal', val);
      if (!val) {
        this.$emit('sendSearchData');
      }
    }
  }
}

2)定义云函数并返回数据

"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { searchVal } = event;

  const res = await db
    .collection("article")
    .aggregate() // 使用聚合的形式进行数据的获取
    .match({
      title: new RegExp(searchVal),
    }) // 使用正则表达式进行模糊匹配,只要包含就返回
    .project({
      content: 0, // 本次查询不需要返回文章详情给前端
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "搜索数据请求成功",
    data: {
      articleList: res.data,
    },
  };
};








 
 
 














3)搜索关键词

  • 处理空数据内容,清空操作后返回一个空的 searchList
async handleSendSearchKeyword() {
  this.isShowHistory = false;
  if (!this.parentVal) {
    this.isShowHistory = true;
    this.searchList = [];
    return;
  }
  const { articleList, total } = await this.$http.getSearchHistory({
    searchVal: this.parentVal
  });
  this.searchList = articleList;
}

4)切换历史记录显示

  • 定义显示状态标识
// Search.vue
data() {
  return {
    isShowHistory: true,
    searchList: [],
  }
}
  • 点击卡片生成历史记录
// ListCard.vue
toArticleDetail() {
  this.$emit('saveHistory');
}
  • 通过自定义事件形式传递数据到 Search.vue
<!-- ListItem.vue -->
<view>
  <ListCard v-for="article in articleList" :article="article" :key="article._id" @saveHistory="$emit('saveHistory')"></ListCard>
</view>
<uni-load-more v-if="isShowLoading && (articleList.length === 0 || articleList.length > 5)" :status="loadData.loading"></uni-load-more>
saveHistory() {
  this.updateSearchHistory(this.parentVal);
}

5)Vuex 管理搜索历史记录

// state.js
export default {
  // 初始值绑定
  searchHistory: uni.getStorageSync("searchHistory") || [],
};

// mutations.js
updateSearchHistory(state, keyword) {
  let history = state.searchHistory;
  if (history.length > 0 && history.findIndex(item => item === keyword) > -1)
    return;
  history.unshift(keyword)
  uni.setStorageSync("searchHistory", history);
  state.searchHistory = history;
}
  • 点击历史记录搜索
openHistory(val) {
  this.parentVal = val;
  this.handleSendSearchKeyword();
}
  • 点击清空按钮清空历史记录
    • 通过 ListCard.vue 发送自定义事件
    • Search.vue 接收到自定义事件后,通过触发 mutation 里面的指定事件,清空 state 的指定属性
// mutations.js
cleanSearchHistory(state) {
  uni.removeStorageSync("searchHistory");
  state.searchHistory = [];
  uni.showToast({
    icon: "success",
    title: "清空完成",
  });
}

(十六)标签页面

1.跳转标签页面鉴权

  • 判断用户是否登录,未登录直接跳转登录页面
// TabBar.vue
async toLabelEditPage() {
  await this.handleCheckedLogin();
  uni.navigateTo({
    url: '/pages/LabelEdit/LabelEdit'
  });
}

2.控制标签按钮状态

  • 添加状态属性监听
  • 发送切换按钮文本及是否修改标签体
<view @click="handleEditLabel" class="label-edit">{{ this.isEdit ? '完成' : '编辑' }}</view>
data() {
  return {
    isEdit: false
  };
},
methods: {
  handleEditLabel() {
    this.isEdit && this.handleUpdateLabel();
    this.isEdit = !this.isEdit;
  },
  handleUpdateLabel() {}
}

3.保存 labelList 到全局仓库

  • store 定义 labelList 属性
// state.js
export default {
  labelList: uni.getStorageSync("labelList") || [],
};

// mutations.js
updateLabelList(state, labelList) {
  uni.setStorageSync('labelList', labelList);
  state.labelList = labelList;
},
  • 在 ArticleList.vue 替换 labelList 的引用
created() {
  this.labelList.length && this.getArticleList(this.activeIndex);
}
  • 修改父组件 Home.vue
import { mapState, mapMutations } from "vuex";
export default {
  name: "Home",
  onLoad() {
    this.getLabelList();
  },
  data() {
    return {
      activeIndex: 0,
    };
  },
  computed: {
    ...mapState(["labelList"]),
  },
  methods: {
    ...mapMutations(["updateLabelList"]),
    async getLabelList() {
      if (this.labelList.length) return;
      const res = await this.$http.getLabelList();
      this.updateLabelList([
        {
          name: "全部",
        },
        ...res,
      ]);
    },
  },
};

4.选项卡业务逻辑处理

1)创建我的标签及推荐标签数组

  • 使用计算属性创建数组
computed: {
  ...mapState(['labelList']),
  selfLabelList() {
    return this.labelList.filter((item) => this.labelIds.includes(item._id));
  },
  recommendLabelList() {
    return this.labelList.filter((item) => !this.labelIds.includes(item._id) && item._id);
  }
}
  • 初始化定义本地数据对象
    • 为了不影响用户数据,页面内定义属性拷贝用户数组信息数据
    • 在 watch 中调用,为了防止用户在修改之后,改变用户时无法获取准确信息
    • 使用 immediate 初始化
watch: {
  userInfo: {
    immediate: true,
    handler(newVal, oldVal) {
      this.labelIds = [...this.userInfo.label_ids];
    }
  }
}
  • 渲染页面数据
<!-- 用户标签组 -->
<view class="label-content">
  <view v-for="(item, index) in selfLabelList" :key="item._id" class="label-content-item">
    {{ item.name }}
    <uni-icons v-if="isEdit" @click="deleteLabelItem(item)" class="icon-close" type="clear" size="20" color="red"></uni-icons>
  </view>
  <view v-if="!selfLabelList.length" class="no-data">当前没有数据</view>
</view>

<!-- 推荐标签组 -->
<view class="label-content">
  <view v-for="(item, index) in recommendLabelList" :key="item._id" @click="changeSelfList(item)" class="label-content-item">{{ item.name }}</view>
  <view v-if="!recommendLabelList.length" class="no-data">当前没有数据</view>
</view>
  • 为标签切换添加点击事件
    • 切换标签应该在 isEdit 情况下切换
    • 追加用户标签,直接修改 labelIds
    • 删除标签,只过滤匹配内容
    • 在未点击保存按钮时,不需要真正地存储数据
changeSelfList(item) {
  if (!this.isEdit) return;
  this.labelIds.push(item._id);
},
deleteLabelItem(item) {
  this.labelIds = this.labelIds.filter((val) => val !== item._id);
}

2)发送数据

  • 创建云函数
    • 修改数据库中用户 label_ids 字段
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { userId, label_ids } = event;

  const res = await db.collection("user").doc(userId).update({
    label_ids,
  });

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: "修改成功",
    },
  };
};
  • 前端收集数据
    • 当前用户 ID
    • 现阶段收藏的数组 selfIds
  • 前端接收返回值
    • 重新调整 userInfo
async handleUpdateLabel() {
  const label_ids = this.selfLabelList.map((item) => item._id);
  const res = await this.$http.updateLabelIds({
    userId: this.userInfo._id,
    label_ids
  });
  uni.showToast({
    icon: 'success',
    title: res.msg
  });
  this.updateUserInfo({
    ...this.userInfo,
    label_ids
  });
}

3)处理其他页面副作用

  • 调整 Home.vue 中 labelList 获取方式
    • 判断是否包含用户信息
    • 不包含直接返回
    • 否则,显示用户收藏内容
computed: {
  // ...mapState(['labelList'])
  labelList() {
    if (this.userInfo) {
      this.activeIndex = 0;
      return [...this.$store.state.labelList.slice(0, 1), ...this.$store.state.labelList.filter((item) => this.userInfo.label_ids.includes(item._id))];
    } else {
      return this.$store.state.labelList;
    }
  }
}
  • ArticleList.vue 监听到 labelList 的值改变后,清空列表对象
watch: {
  labelList() {
    this.articleData = {};
    this.loadData = {};
    this.getArticleList(this.currentIndex);
  }
}

(十七)文章详情页面

  • 搭建基本结构
  • 初始化样式

1.URL 参数处理

  • 在 ListCard 组件跳转过程中,携带参数到详情页面实现数据渲染
toArticleDetail() {
  this.$emit('saveHistory');
  const {
    _id,
    title,
    author,
    create_time,
    thumbs_up_count,
    browse_count
  } = this.article;
  const params = {
    _id,
    title,
    author,
    create_time,
    thumbs_up_count,
    browse_count
  };
  uni.navigateTo({
    url: `/pages/ArticleDetail/ArticleDetail?params=${JSON.stringify(params)}`
  });
}
  • ArticleDetail.vue 中接收参数
onLoad(...options) {
  this.articleData = JSON.parse(options[0].params);
}

2.发送数据请求

  • 前端传递当前文章 ID
onLoad(...options) {
  this.articleData = JSON.parse(options[0].params);
  this.getArticleDetail();
},
data() {
  return {
    articleData: null
  };
},
methods: {
  async getArticleDetail() {
    const res = await this.$http.getArticleDetail({
      article_id: this.articleData._id
    });
    this.articleData = res;
  }
}
  • 定义云函数
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { article_id } = event;

  const articleList = await db
    .collection("article")
    .aggregate()
    .match({
      _id: article_id,
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "文章获取成功",
    data: articleList.data[0],
  };
};

3.将文章详情内容解析为 Markdown 格式

1)解析内容数据

import uParse from "@/components/gaoyia-parse/parse.vue";

export default {
  name: "ArticleDetail",
  components: {
    uParse,
  },
};
  • App.vue 全局引入 uParse 样式
<style>
  /*每个页面公共css */
  @import url("/components/gaoyia-parse/parse.css");
</style>

2)解析 markdown 文本

  • 使用第三方插件 marked
  • npm install markedyarn add marked
import marked from "marked";

export default {
  computed: {
    content() {
      // // 小程序没有加载到this.articleData前调用子属性会报错
      // return this.articleData.content ? marked(this.articleData.content) : null;
      try {
        return marked(this.articleData.content);
      } catch (e) {
        return null;
      }
    },
  },
};

4.制作评论组件

<CommentModel :showModel="showModel"></CommentModel>
  • 完成内容区域结构样式创建
  • 当用户为未登录状态时,跳转登录页面
<view @click="handleOpenCommentModel" class="input-container">
  <text>谈谈你的看法</text>
  <uni-icons type="compose" size="16" color="#f07373"></uni-icons>
</view>
async handleOpenCommentModel() {
  await this.handleCheckedLogin();
  this.showModel = true;
}
  • 前端收集数据,获取用户 id,文章 id,评论内容
async handleSendComment(content) {
  const { msg } = await this.$http.updateArticleComment({
    articleId: this.articleData._id,
    userId: this.userInfo._id,
    content
  });
  uni.showToast({
    icon: 'success',
    title: msg
  });
  this.showModel = false;
}
  • 创建云函数
    • 为 article 追加评论内容
    • 为 comment_id 指定随机 id 值
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { userId, articleId, content } = event;

  // 获取用户信息
  let user = await db.collection("user").doc(userId).get();
  user = user.data[0];

  const article = await db.collection("article").doc(articleId).get();

  let commentObj = {
    comment_id: generatedId(5),
    comment_content: content,
    create_time: Date.now(),
    author: {
      author_id: user._id,
      author_name: user.author_name,
      avatar: user.avatar,
      professional: user.professional,
    },
    replyArr: [],
    is_reply: false,
  };

  commentObj = dbCmd.unshift(commentObj);
  await db.collection("article").doc(articleId).update({
    comments: commentObj,
  });

  // 生成随机数id
  function generatedId(num) {
    return Number(Math.random().toString().substr(3, num) + Date.now()).toString(36);
  }

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: "添加数据成功",
    },
  };
};

5.制作评论展示组件

async getCommentList() {
  const res = await this.$http.getCommentList({
    articleId: this.articleData._id
  });
  this.commentList = res;
}
  • 定义云函数 getCommentList
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { articleId, pageSize = 10, page = 1 } = event;

  const list = await db
    .collection("article")
    .aggregate()
    .match({
      _id: articleId,
    })
    .unwind("$comments") // 从指定的节点获取内容
    .project({
      _id: 0,
      comments: 1,
    })
    .replaceRoot({
      newRoot: "$comments",
    })
    .skip(pageSize * (page - 1))
    .limit(pageSize)
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "数据请求成功",
    data: list.data,
  };
};
  • 获取文章详情时过滤 comments 字段
const articleList = await db
  .collection("article")
  .aggregate()
  .match({
    _id: article_id,
  })
  .project({
    comments: 0,
  })
  .end();






 
 
 

  • 创建评论展示组件 CommentList.vue

6.回复指定评论

1)绑定指定评论回复按钮点击事件

  • 添加 comment_id 传递给云函数
  • 保证云函数知道当前是回复哪条评论
<view class="comment-info">
  <view @click="handleReplyComment({ comment, isReply })" class="comment-button">回复</view>
</view>

2)调整云函数

  • 从当前的 comments 集合当中,通过传递的 comment_id 获取指定的记录值
  • 为当前的记录值的 replyArr 属性插入新的评论内容
  • 修改指定索引值的数据的 replyArr 的记录值
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { userId, articleId, content, comment_id = "", reply_id = "", is_reply = false } = event;

  // 获取用户信息
  let user = await db.collection("user").doc(userId).get();
  user = user.data[0];

  const article = await db.collection("article").doc(articleId).get();
  // 获取指定文章下的所有的评论内容
  const comments = article.data[0].comments;

  let commentObj = {
    comment_id: generatedId(5),
    comment_content: content,
    create_time: Date.now(),
    author: {
      author_id: user._id,
      author_name: user.author_name,
      avatar: user.avatar,
      professional: user.professional,
    },
    replyArr: [],
    is_reply,
  };

  // 当前的评论内容是空的时候的处理
  if (comment_id === "") {
    commentObj.replyArr = [];
    commentObj = dbCmd.unshift(commentObj);
  } else {
    let commentAuthor = null;
    // 获取对当前这一条评论的子回复的索引值获取
    let commentIndex = comments.findIndex((item) => item.comment_id === comment_id);
    if (is_reply) {
      commentAuthor = comments[commentIndex].replyArr.find((item) => item.comment_id === reply_id).author.author_name;
    } else {
      commentAuthor = comments.find((item) => item.comment_id === comment_id).author.author_name;
    }

    commentObj.to = commentAuthor;
    commentObj = {
      [commentIndex]: {
        replyArr: dbCmd.unshift(commentObj),
      },
    };
  }

  /*
  	const obj = {arr:[{name:'1'},{name:2}]}
  	xxx.update({
  		arr:{
  			0:{
  				name:3
  			}
  		}
  	})
  */

  await db.collection("article").doc(articleId).update({
    comments: commentObj,
  });

  // 生成随机数id
  function generatedId(num) {
    return Number(Math.random().toString().substr(3, num) + Date.now()).toString(36);
  }

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: "添加数据成功",
    },
  };
};








 
 
 







 
 











 
 


 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 






















3)使用组件自调用的形式

  • 在自调用的过程当中需要保证组件指定了 name 属性值
  • 使用自调用的过程中父组件 ArticleDetail 会接收到两次$emit 事件
    • 一次是当前组件 CommentList 作为父组件时获取到的递归子组件触发的事件
    • 一次是当前组件 CommentList 向上级 ArticleDetail 组件传递时触发的事件
<!-- CommentList.vue -->
<view class="comment-info">
  <view @click="handleReplyComment({ comment, isReply })" class="comment-button">回复</view>
</view>
<view v-for="item in comment.replyArr" :key="item._comment_id" class="comment-reply-list">
  <CommentList :isReply="true" :comment="item" @replyComment="handleReplyComment"></CommentList>
</view>
handleReplyComment(data) {
  // 区分主回复还是子回复
  if (data.isReply) {
    data.comment.reply_id = data.comment.comment_id;
    data.comment.comment_id = this.comment.comment_id;
  }
  this.$emit('replyComment', data);
}

4)父组件处理更新评论内容

  • 当指定回复发送事件时,需要指定 reply_id,并且调整 comment_id 值
  • 渲染完成后,将 replayData 清空
handleReplyComment(data) {
  this.replyData = {
    comment_id: data.comment.comment_id,
    is_reply: data.isReply
  };
  // 当前为回复内容的时候添加回复的ID
  data.comment.reply_id && (this.replyData.reply_id = data.comment.reply_id);
  this.handleOpenCommentModel();
}

7.关注作者

  • 判断当前的登录用户是否包含文章作者的用户 ID
<button @click="handleFollowAuthor" type="default" class="detail-header-button">{{ isFollowing ? '取消关注' : '关注' }}</button>
computed: {
  isFollowing() {
    // 解决小程序报错
    try {
      return this.userInfo && this.userInfo.author_likes_ids.includes(this.articleData.author.id);
    } catch (e) {
      return false;
    }
  }
}
  • 绑定事件,传递文章作者 ID 及当前登录用户 ID
async handleFollowAuthor() {
  await this.handleCheckedLogin();
  const { msg } = await this.$http.updateFollowAuthor({
    authorId: this.articleData.author.id,
    userId: this.userInfo._id
  });
  uni.showToast({
    icon: 'success',
    title: msg
  });
  // 处理用户存储信息
  let followIds = [...this.userInfo.author_likes_ids];
  if (followIds.includes(this.articleData.author.id)) {
    followIds = followIds.filter((item) => item !== this.articleData.author.id);
  } else {
    followIds.push(this.articleData.author.id);
  }
  this.updateUserInfo({
    ...this.userInfo,
    author_likes_ids: followIds
  });
}
  • 定义云函数
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { userId, authorId } = event;

  const user = await db.collection("user").doc(userId).get();
  const authorLikesIds = user.data[0].author_likes_ids;
  let returnMsg = "";

  let author_ids = null;
  if (authorLikesIds.includes(authorId)) {
    returnMsg = "取消关注成功";
    author_ids = dbCmd.pull(authorId);
  } else {
    returnMsg = "关注作者成功";
    author_ids = dbCmd.addToSet(authorId);
  }

  // 将处理完的内容重新插入
  await db.collection("user").doc(userId).update({
    author_likes_ids: author_ids,
  });

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: returnMsg,
    },
  };
};
  • 使用公共组件 SaveLikes 收藏文章
<!-- ArticleDetail.vue -->
<SaveLikes :item="articleData" size="22" class="detail-bottom-icon-box"></SaveLikes>
<!-- SaveLikes.vue -->
<uni-icons :type="isLike ? 'heart-filled' : 'heart'" :size="size" color="#ff6600"></uni-icons>

<script>
  export default {
    props: {
      item: Object,
      size: {
        type: String,
        default: "20",
      },
    },
  };
</script>
  • ArticleList.vue 解决每次请求都要加载文章列表的 Bug
labelList(newVal, oldVal) {
  // 判断当前labelList与新的labelList是否相同,相同则不重新数获取据
  if (JSON.stringify(newVal) === JSON.stringify(oldVal)) return;

  this.articleData = {};
  this.loadData = {};
  this.getArticleList(this.currentIndex);
}

8.实现点赞+浏览次数

  • 请求文章详情的云函数 getArticleDetail ,浏览次数+1
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { article_id } = event;

  // 每次请求+1
  await db.collection("article").update({
    browse_count: dbCmd.inc(1),
  });

  const articleList = await db
    .collection("article")
    .aggregate()
    .match({
      _id: article_id,
    })
    .project({
      comments: 0,
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "文章获取成功",
    data: articleList.data[0],
  };
};





 
 
 
 




















  • ArticleDetail.vue 使用计算属性渲染点赞 icon
isLike() {
  try {
    return this.userInfo && this.userInfo.thumbs_up_article_ids.includes(this.articleData._id);
  } catch (e) {
    return false;
  }
}
  • 定义云函数
"use strict";
const db = uniCloud.database();
// 使用操作符
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { articleId, userId } = event;

  const userList = await db.collection("user").doc(userId).get();
  const thumbs_up_article_ids = userList.data[0].thumbs_up_article_ids;
  let tempArr = null;
  let returnMsg = "";
  let thumbsNumber = null;

  // 判断当前用户是否有点赞操作
  if (thumbs_up_article_ids.includes(articleId)) {
    tempArr = dbCmd.pull(articleId);
    thumbsNumber = -1;
    returnMsg = "您取消了点赞";
  } else {
    tempArr = dbCmd.addToSet(articleId);
    thumbsNumber = 1;
    returnMsg = "点赞成功";
  }

  // 处理用户字段
  await await db.collection("user").doc(userId).update({
    thumbs_up_article_ids: tempArr,
  });

  // 处理文章数量字段
  await await db
    .collection("article")
    .doc(articleId)
    .update({
      thumbs_up_count: dbCmd.inc(thumbsNumber),
    });

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: returnMsg,
    },
  };
};
  • 绑定点赞文章事件
async handleLikeArticle() {
  await this.handleCheckedLogin();

  const { msg } = await this.$http.updateLikeArticle({
    articleId: this.articleData._id,
    userId: this.userInfo._id
  });
  msg &&
    uni.showToast({
      icon: 'success',
      title: msg
    });

  // 修改用户信息
  let thumbsArr = [...this.userInfo.thumbs_up_article_ids];
  if (thumbsArr.includes(this.articleData._id)) {
    this.articleData.thumbs_up_count -= 1;
    thumbsArr = thumbsArr.filter((item) => item != this.articleData._id);
  } else {
    this.articleData.thumbs_up_count += 1;
    thumbsArr.push(this.articleData._id);
  }
  this.updateUserInfo({
    ...this.userInfo,
    thumbs_up_article_ids: thumbsArr
  });
}

(十八)评论页面

1.创建 CommentDetail.vue

  • 创建页面
  • pages.json 中注册页面
  • 跳转页面时携带指定的文章 ID
toCommentDetail() {
  uni.navigateTo({
    url: `/pages/CommentDetail/CommentDetail?articleId=${this.articleData._id}`
  });
}

2.编写结构样式

1)使用评论组件

<view v-for="item in commentList" :key="item.comment_id" class="comment-content-container">
  <CommentList :comment="item" @replyComment="handleReplyComment"></CommentList>
</view>

2)使用 uni-load-more 组件

  • 调整组件内部初始化属性
<uni-load-more
  v-if="commentList.length === 0 || commentList.length > 5"
  :status="loading"
  :contentText="{ contentdown: '上拉显示更多', contentrefresh: '正在加载...', contentnomore: '没有更多评论了' }"
></uni-load-more>

3)追加评论列表

async getCommentList() {
  const res = await this.$http.getCommentList({
    articleId: this.articleId,
    page: this.page,
    pageSize: this.pageSize
  });
  if (res.length === 0) {
    this.loading = 'noMore';
    return;
  }
  let oldList = JSON.parse(JSON.stringify(this.commentList));
  oldList.push(...res);
  this.commentList = oldList;
}

3.添加评论内容输入组件

<CommentModel :showModel="showModel" @sendComment="handleSendComment" @hideCommentModel="showModel = $event"></CommentModel>

4.发布评论

1)回复事件函数处理

handleReplyComment(data) {
  this.replyData = {
    comment_id: data.comment.comment_id,
    is_reply: data.isReply
  };
  // 当前为回复内容的时候添加回复的ID
  data.comment.reply_id && (this.replyData.reply_id = data.comment.reply_id);
  this.handleOpenCommentModel();
}

2)处理副作用

async handleSendComment(content) {
  const { msg } = await this.$http.updateArticleComment({
    articleId: this.articleId,
    userId: this.userInfo._id,
    content,
    ...this.replyData // 扩展当前是否为回复指定评论内容
  });
  uni.showToast({
    icon: 'success',
    title: msg
  });
  this.showModel = false;
  this.getCommentList();
  this.replyData = {};
  this.page = 1;
  this.commentList = [];
  this.loading = 'loading';
}











 
 
 
 
 
 

(十九)关注页面

1.添加路由守卫

uni-simple-routeropen in new window


快速上手open in new window

{
  "dependencies": {
    "marked": "^2.1.3",
    "uni-read-pages": "^1.0.5",
    "uni-simple-router": "^2.0.7"
  }
}

1)pages.json 增加配置项 meta

{
  "path": "pages/Follow/Follow",
  "style": {
    "navigationBarTitleText": "关注",
    "enablePullDownRefresh": false
  },
  "meta": {
    "auth": true
  }, {
    "path": "pages/UserInfo/Login/Login",
    "style": {
      "navigationBarTitleText": "用户登录",
      "enablePullDownRefresh": false
    },
    "name": "Login"
  }
}

2)vue.config.js 扩展读取 meta 属性

const TransformPages = require("uni-read-pages");
const { webpack } = new TransformPages();
module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DefinePlugin({
        ROUTES: webpack.DefinePlugin.runtimeValue(() => {
          const tfPages = new TransformPages({
            includes: ["path", "name", "meta", "aliasPath"], // 扩展meta属性
          });
          return JSON.stringify(tfPages.routes);
        }, true),
      }),
    ],
  },
};








 







3)添加守卫规则

// 全局路由前置守卫
router.beforeEach((to, from, next) => {
  // 判断当前页面是否需要登录并且现在是没有登录的状态
  if (to.meta.auth && !store.state.userInfo) {
    next({
      name: "Login",
      NAVTYPE: "push", // 跳转到普通页面,新开保留历史记录
    });
  } else {
    next();
  }
});

4)ArticleDetail.vue 修改路由参数接收方式

onLoad(...options) {
  // this.articleData = JSON.parse(options[0].params);
  this.articleData = this.$Router.currentRoute.query.params;
  this.getArticleDetail();
  this.getCommentList();
}


 



5)Follow.vue 处理小程序无法兼容问题

onLoad() {
  // #ifdef MP-WEIXIN
  if (!this.$store.state.userInfo) {
    uni.redirectTo({
      url: '/pages/UserInfo/Login/Login'
    });
    return;
  }
  // #endif
}

2.收藏文章列表

1)定义云函数

"use strict";
const db = uniCloud.database();
const $ = db.command.aggregate; // 获取一个聚合的操作符

exports.main = async (event, context) => {
  const { userId } = event;

  let userInfo = await db.collection("user").doc(userId).get();
  let article_likes_ids = userInfo.data[0].article_likes_ids; // 获取用户的收藏文章的数组

  const list = await db
    .collection("article")
    .aggregate()
    .addFields({
      // 判断这个文章的数组是否包含文章的_id ,$_id 指的是文章列表里面的_id,如果包含,返回true,否则,返回false,在这个里面是过滤查询的每一条记录值
      is_like: $.in(["$_id", article_likes_ids]),
    })
    .project({
      content: 0,
    })
    .match({
      is_like: true,
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "请求成功",
    data: list.data,
  };
};

2)获取当前登录用户的收藏文章列表

onLoad() {
  // #ifdef MP-WEIXIN
  if (!this.$store.state.userInfo) {
    uni.redirectTo({
      url: '/pages/UserInfo/Login/Login'
    });
    return;
  }
  // #endif
  this.getFollowArticle();
},
data() {
  return {
    currentIndex: 0,
    articleList: [],
    isShowLoading: false,
    noData: false
  };
},
methods: {
  async getFollowArticle() {
    const res = await this.$http.getFollowArticle({
      userId: this.userInfo._id
    });
    this.articleList = res;
    !res.length && (this.noData = true);
  }
}

3.关注作者列表

1)监听全局事件

  • 处理关注页面数据不刷新问题
  • 使用 uni.$emit 事件注册全局事件
// SaveLikes.vue
async handleChangeSaveStatus() {
  // ......
  // 触发 Follow.vue 重新获取列表数据
  uni.$emit('updateArticle');
}
// Follow.vue
onLoad() {
  // #ifdef MP-WEIXIN
  if (!this.$store.state.userInfo) {
    uni.redirectTo({
      url: '/pages/UserInfo/Login/Login'
    });
    return;
  }
  // #endif
  this.getFollowArticle();

  // 没有这个历史记录栈的时候不会触发这个事件
  uni.$on('updateArticle', (e) => {
    this.getFollowArticle('noLoading');
  });
}












 
 
 
 

2)搭建关注作者区域结构

  • 关联 swiper 滑动组件
  • 添加结构样式
<swiper :current="currentIndex" @change="currentIndex = $event.detail.current" class="follow-list-swiper">
  <swiper-item :class="{ 'no-data': articleDataNone }">
    {{ articleDataNone && '暂无收藏的文章' }}
    <ListItem v-if="articleList.length" :isShowLoading="isShowLoading" :articleList="articleList"></ListItem>
  </swiper-item>
  <swiper-item>
    <AuthorList v-if="authorList.length" :authorList="authorList"></AuthorList>
    <view v-if="authorDataNone" class="no-data">暂无关注的作者</view>
  </swiper-item>
</swiper>

3)定义云函数

"use strict";
const db = uniCloud.database();
const $ = db.command.aggregate; // 获取一个聚合的操作符

exports.main = async (event, context) => {
  const { userId } = event;

  let userInfo = await db.collection("user").doc(userId).get();
  let author_likes_ids = userInfo.data[0].author_likes_ids; // 获取用户的收藏文章的数组

  const list = await db
    .collection("user")
    .aggregate()
    .addFields({
      // 判断这个文章的数组是否包含文章的_id ,$_id 指的是文章列表里面的_id,如果包含,返回true,否则,返回false,在这个里面是过滤查询的每一条记录值
      is_like: $.in(["$id", author_likes_ids]),
    })
    .project({
      password: 0,
      loginName: 0,
    })
    .match({
      is_like: true,
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "请求成功",
    data: list.data,
  };
};

4)客户端渲染

<AuthorList v-if="authorList.length" :authorList="authorList"></AuthorList>
async getFollowAuthor(isLoading) {
  const res = await this.$http.getFollowAuthor({
    userId: this.userInfo._id,
    isLoading
  });
  if (res.length) {
    this.authorDataNone = '';
  } else {
    this.authorDataNone = true;
  }
  this.authorList = res;
}
  • 请求防抖处理
// http.js
export default ({ name, data = {} }) => {
  const loading = data.isLoading;
  data.isLoading && delete data.isLoading;

  return new Promise((resolve, reject) => {
    !loading &&
      uni.showLoading({
        title: "加载中...",
      });
    uniCloud.callFunction({
      name, // 要调用的云函数名称
      data,
      success({ result }) {
        if (result.code === 0) {
          resolve(result.data);
        } else {
          uni.showToast({
            icon: "error",
            title: result.msg,
          });
        }
      },
      fail(err) {
        // 网络错误或后端错误
        reject(err);
      },
      complete() {
        // 不论请求成功还是失败都执行
        !loading && uni.hideLoading();
      },
    });
  });
};


 
 


 
 
 
 



















 




5)同步数据

  • 定义全局事件,实现修改关注作者时触发 updateAuthor 事件重新调用
// ArticleDetail.vue
async handleFollowAuthor() {
  // ......
  // 更新关注作者列表
  uni.$emit('updateAuthor');
}
// Follow.vue
uni.$on("updateAuthor", (e) => {
  this.getFollowAuthor("noLoading");
});

6)修改卡片组件跳转方式

// ListCard.vue
toArticleDetail() {
  // ......

  // 手动转换为绝对路径
  // uni.navigateTo({
  //   url: `/pages/ArticleDetail/ArticleDetail?params=${JSON.stringify(params)}`
  // });

  // 使用Router对象方法进行跳转
  this.$Router.push({
    path: '/pages/ArticleDetail/ArticleDetail',
    query: params
  });
}
// ArticleDetail.vue
onLoad(...options) {
  // this.articleData = JSON.parse(options[0].params);
  // this.articleData = this.$Router.currentRoute.query.params;
  this.articleData = this.$Router.currentRoute.query;
  this.getArticleDetail();
  this.getCommentList();
}




 



(二十)个人中心

1.页面结构搭建

1)未登录状态

  • 展示未登录提示结构
  • 在未登录状态下可实现页面跳转

2)登录状态

  • 展示用户登录信息内容
  • 添加退出按钮,定义退出函数

2.我的文章

1)渲染用户信息

2)Self.vue 处理 app 升级版本逻辑

  • 初始化 onLoad 事件
  • 判断是否有新版本下载,获取当前的版本
onLoad() {
  // 判断是否有新版本进行下载及获取当前的版本
  // #ifdef APP-PLUS
  uni.getSystemInfo({
    success: (res) => {
      if (res.platform == 'android') {
        plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
          this.currentVersion = wgtinfo;
          this.checkVersion();
        });
      }
    }
  });
  // #endif
}
  • 判断当前版本是否小于最新版本
async checkVersion() {
  const { version, downLoadLinkUrl } = await this.$http.getCurrentVersion();
  if (version > this.currentVersion) {
    this.haveNewVersion = true;
    this.downLoadLinkUrl = downLoadLinkUrl;
  }
}
// common.js
import ajax from "../../http.js";

export const getCurrentVersion = (data) =>
  ajax({
    name: "getCurrentVersion",
    data,
  });
  • 下载最新版本内容并安装
getNewVersion() {
  uni.showLoading({
    title: '下载中,请稍后'
  });
  let dTask = plus.downloader.createDownload(this.downLoadLinkUrl, {}, function (d, status) {
    // 下载完成
    uni.hideLoading({});
    if (status == 200) {
      plus.runtime.install(plus.io.convertLocalFileSystemURL(d.filename), {}, {}, function (error) {
        uni.showToast({
          title: '安装失败',
          duration: 1500,
          icon: 'none'
        });
      });
    } else {
      uni.showToast({
        title: '更新失败',
        duration: 1500,
        icon: 'none'
      });
    }
  });
  dTask.start();
}

3)处理我的文章

  • 创建页面,引入组件
<view>
  <ListCard v-for="article in articleList" :article="article" :key="article._id"></ListCard>
  <view v-if="noArticle" class="no-data">您还没有发布过任何文章</view>
</view>
  • 获取数据,定义云函数
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;

exports.main = async (event, context) => {
  const { userId } = event;

  let userInfo = await db.collection("user").doc(userId).get();
  let article_ids = userInfo.data[0].article_ids;

  const list = await db
    .collection("article")
    .aggregate()
    .project({
      content: 0,
      comments: 0,
    })
    .match({
      id: dbCmd.in(article_ids),
    })
    .end();

  // 返回数据给客户端
  return {
    code: 0,
    msg: "请求成功",
    data: list.data,
  };
};

3.意见反馈

1)搭建页面结构

  • 创建结构
  • 处理样式

2)上传图片

ChooseImage 参考文档open in new window

async handleUploadImage() {
  const count = 5 - this.imageList.length;
  uni.chooseImage({
    count,
    success: (res) => {
      const tempFilePaths = res.tempFilePaths;
      tempFilePaths.forEach((url, index) => {
        if (index < count) {
          this.imageList.push({
            url,
            // #ifdef H5
            name: res.tempFiles[index].name,
            // #endif
            // #ifndef H5
            name: Date.now().toString() + url.substr(url.lastIndexOf('.')) // 时间戳 + 获取扩展名
            // #endif
          });
        }
      });
    }
  });
}

3)发送数据到云函数

图片直传参考文档open in new window

  • 收集图片上传成功之后的访问地址 fileID
getFileId() {
  return this.imageList.map((item) => {
    return new Promise(async (resolve) => {
      const result = await uniCloud.uploadFile({
        filePath: item.url,
        cloudPath: item.name
      });
      resolve(result.fileID);
    });
  });
}
  • 定义云函数
"use strict";
const db = uniCloud.database();
exports.main = async (event, context) => {
  const { userId, feedbackImageList, content } = event;

  await db.collection("feedback").add({
    user_id: userId,
    feedbackImageList,
    content,
  });

  return {
    code: 0,
    data: {
      msg: "提交反馈成功",
    },
  };
};

4)修改用户头像

  • 获取修改头像内容,上传云函数
  • 修改图片,删除原图片
"use strict";
const db = uniCloud.database();
const dbCmd = db.command;
exports.main = async (event, context) => {
  const { userId, filePath } = event;

  // 更新用户
  const user = await db.collection("user").doc(userId).get();
  const oldUrl = user.data[0].avatar;
  try {
    await uniCloud.deleteFile({
      fileList: [oldUrl],
    });
  } catch (e) {
    console.log(e);
  }

  // 更新作者头像
  const id = user.data[0].id;
  let res = await db
    .collection("article")
    .where({
      "author.id": id,
    })
    .update({
      "author.avatar": filePath,
    });

  await db.collection("user").doc(userId).update({
    avatar: filePath,
  });

  // 返回数据给客户端
  return {
    code: 0,
    data: {
      msg: "修改头像成功",
    },
  };
};
// Self.vue
async getAvatarFileId(filePath, cloudPath) {
  const { fileID } = await uniCloud.uploadFile({
    filePath,
    cloudPath
  });
  return fileID;
},
async handleUploadAvatar(filePath) {
  const { msg } = await this.$http.updateUserAvatar({
    userId: this.userInfo._id,
    filePath
  });
  uni.showToast({
    title: msg,
    icon: 'none'
  });
  this.updateUserInfo({
    ...this.userInfo,
    avatar: filePath
  });
},
handleChangeAvatar() {
  uni.chooseImage({
    count: 1,
    success: async (res) => {
      const filePath = await this._uploadFile(
        res.tempFilePaths[0],
        res.tempFiles[0].name || Date.now().toString() + res.tempFilePaths[0].substr(res.tempFilePaths[0].lastIndexOf('.'))
      );
      await this.handleUploadAvatar(filePath);
    }
  });
}

(二十一)发布

1.Wap 端发行打包

打包参考文档open in new window

  • uniCloud 网页托管配置
  • 使用 uniCloud 前端网页托管,需要配置安全域名

参考文档open in new window

2.微信小程序发布

打包参考文档open in new window

  • 打包配置文件
    • 获取小程序 ID
    • 配置安全域名

  • 发布小程序
  • 提交预发布版本
  • 提交审核

参考文档open in new window

3.App 安卓系统应用打包发布

1)配置

  • 基础配置

  • 使用图标
    • 使用 1024 * 1024 图标
  • 其他配置暂时忽略

2)证书下载

证书下载地址open in new window

上次编辑于: