# 个人中心

# 组件基本布局

目标:实现个人中心组件基本布局

  • 组件模板
<div class="user-profile">
  <div class="info">
    <van-image round src="https://img.yzcdn.cn/vant/cat.jpeg" />
    <h3 class="name">
      用户名
      <br />
      <van-tag size="mini">申请认证</van-tag>
    </h3>
  </div>
  <van-row>
    <van-col span="8">
      <p>0</p>
      <p>动态</p>
    </van-col>
    <van-col span="8">
      <p>0</p>
      <p>关注</p>
    </van-col>
    <van-col span="8">
      <p>0</p>
      <p>粉丝</p>
    </van-col>
  </van-row>
</div>
<van-row class="user-links">
  <van-col span="8">
    <van-icon name="newspaper-o" color="#7af"/>我的作品
  </van-col>
  <van-col span="8">
    <van-icon name="star-o" color="#f00"/>我的收藏
  </van-col>
  <van-col span="8">
    <van-icon name="tosend" color="#fa0"/>阅读历史
  </van-col>
</van-row>

<van-cell-group class="user-group">
  <van-cell icon="edit" title="编辑资料" to="/user/profile" is-link />
  <van-cell icon="chat-o" title="小智同学" to="/user/chat" is-link />
  <van-cell icon="setting-o" title="系统设置" is-link />
  <van-cell icon="warning-o" title="退出登录" is-link />
</van-cell-group>
  • 布局样式
.user {
  &-profile {
    width: 100%;
    height: 150px;
    display: block;
    background: #3296fa;
    color: #fff;
    .info {
      display: flex;
      padding: 20px;
      align-items: center;
      .van-image {
        width: 64px;
        height: 64px;
      }
      .name {
        font-size: 16px;
        font-weight: normal;
        margin-left: 10px;
      }
      .van-tag {
        background: #fff;
        color: #3296fa;
      }
    }
    p {
      margin: 0;
      text-align: center;
    }
  }
  &-group {
    margin-bottom: 15px;
  }
  &-links {
    padding: 15px 0;
    font-size: 12px;
    text-align: center;
    background-color: #fff;
    .van-icon {
      display: block;
      font-size: 24px;
      padding-bottom: 5px;
    }
  }
}

总结:工作量(布局样式要熟练)

# 用户信息动态渲染

目标:实现用户信息的动态渲染

  1. 调用接口获取用户数据
  2. 渲染模板
  • 封装用户信息接口调用方法
export const getUserInfo = () => {
  return request({
    method: 'get',
    url: 'app/v1_0/user'
  })
}
  • 获取用户个人信息
import { getUserInfo } from '@/api/api-login.js'
export default {
  name: 'User',
  data () {
    return {
      info: null
    }
  },
  methods: {
    // 调用接口获取用户信息
    async getUserInfo () {
      try {
        const ret = await getUserInfo()
        this.info = ret.data
      } catch {
        this.$toast.fail('获取用户信息失败')
      }
    }
  },
  created () {
    this.getUserInfo()
  }
}
  • 组件的动态渲染
<div class="user-profile">
  <div class="info">
    <van-image round :src="user.photo" />
    <h3 class="name">
      {{user.name}}
      <br />
      <van-tag size="mini">申请认证</van-tag>
    </h3>
  </div>
  <van-row>
    <van-col span="6">
      <p>{{user.art_count}}</p>
      <p>动态</p>
    </van-col>
    <van-col span="6">
      <p>{{user.follow_count}}</p>
      <p>关注</p>
    </van-col>
    <van-col span="6">
      <p>{{user.fans_count}}</p>
      <p>粉丝</p>
    </van-col>
    <van-col span="6">
      <p>{{user.like_count}}</p>
      <p>被赞</p>
    </van-col>
  </van-row>
</div>

总结:调用接口;获取数据;填充页面

# 退出功能

目标:实现退出功能

  1. 绑定事件
  2. 删除token(store和缓存)
  • 事件绑定
 <van-cell @click="logout()" icon="warning-o" title="退出登录" is-link />
  • 功能实现
import { mapMutations } from 'vuex'
...mapMutations('login', ['deleteUserInfo']),
logout () {
  // 实现退出:确认退出,清除token,跳转到登录页面
  this.$dialog.confirm({
    title: '退出',
    message: '确认要退出吗?'
  }).then(() => {
    // 点击确定触发
    // 1、清除token
    this.deleteUserInfo()
    // 2、跳转到登录页面
    this.$router.push('/login')
    // this.$toast.success('确认退出')
  }).catch(() => {
    // 点击取消触发
    this.$toast.fail('取消退出')
  })
},

总结:基于vuex实现退出,触发mutation,置空state并且清空缓存中的token信息

# 编辑用户布局

目标:实现用户信息编辑功能

  1. 准备编辑用户的组件
  2. 配置组件的路由
  3. 获取用户的信息
  4. 填充用户信息到表单
  5. 提交表单
  • 基本布局
<div class="container">
  <van-nav-bar left-arrow @click-left="$router.back()" title="编辑资料" right-text="保存" ></van-nav-bar>
  <van-cell-group>
    <van-cell is-link title="头像" @click="showPhoto=true" center>
      <van-image
        slot="default"
        width="1.5rem"
        height="1.5rem"
        fit="cover"
        round
        :src="photo"
      />
    </van-cell>
    <van-cell is-link title="名称" @click="showName=true" :value="user.name" />
    <van-cell is-link title="性别" @click="showGender=true" :value="user.gender===0?'':''" />
    <van-cell is-link title="生日" @click="showBirthday=true" :value="user.birthday" />
  </van-cell-group>
  <van-popup v-model="showPhoto" position="bottom">
    <van-cell value="本地相册选择" is-link/>
    <van-cell value="拍照" is-link/>
  </van-popup>
  <van-popup v-model="showName" position="bottom">
    <van-field v-model="user.name" required placeholder="请输入用户名" />
  </van-popup>
  <van-popup v-model="showGender" position="bottom">
    <van-cell value="" @click="changeGender(0)" is-link/>
    <van-cell value="" @click="changeGender(1)" is-link/>
  </van-popup>
  <van-popup v-model="showBirthday" position="bottom">
    <van-datetime-picker
      title="选择生日"
      v-model="nowDate"
      type="date"
      :min-date="minDate"
      @cancel="dateShow=false"
      @confirm="confirmDate"
    />
  </van-popup>
</div>
  • 状态数据
data () {
  return {
    // 弹窗控制
    showPhoto: false,
    showName: false,
    showGender: false,
    showBirthday: false,
    nowDate: new Date(),
    minDate: new Date('1950-01-01'),
    // 默认用户信息
    photo: 'https://img.yzcdn.cn/vant/cat.jpeg',
    user: {
      name: '默认信息',
      gender: 0,
      birthday: '2019-10-10'
    }
  }
},
  • 数据处理
// 获取用户信息
export const getUserProfile = () => {
  return request({
    method: 'get',
    url: 'v1_0/user/profile'
  })
}
// -------------------------------------------
created () {
  this.getUserProfile()
},
methods: {
    async getUserProfile () {
      try {
        const ret = await getUserProfile()
        this.user = ret.data
      } catch {
        this.$toast.fail('获取用户信息失败')
      }
    },
    confirmDate (birthday) {
      // 生日选择:参数默认选中的是日期对象,但是我们需要字符串
      // 基于dayjs的包提供的api进行日期格式化
      const str = dayjs(birthday).format('YYYY-MM-DD')
      this.user.birthday = str
      this.showBirthday = false
    },
    changeGender (value) {
      // 性别选择
      this.user.gender = value
      this.showGender = false
    }
  }
}

# 编辑用户功能

目标:实现编辑用户数据更新

  1. 点击下拉选项触发弹窗
  2. 选中图片并进行预览
  • 预览头像
<input type="file" @change="preview" ref="myfile" style="display:none">
<van-cell value="本地相册选择" @click="$refs.myfile.click()" is-link />
  • 预览图片
// 预览图片
previewImg (e) {
  // 预览图片信息
  const img = e.target.files
  if (img.length === 0) {
    return this.$toast.fail('请选择文件')
  }
  // 根据图片文件预览
  const fr = new FileReader()
  // 读取图片文件
  fr.readAsDataURL(img[0])
  fr.onload = () => {
    // 文件加载并转换成地址成功后触发
    this.user.photo = fr.result
    // 隐藏窗口
    this.showPhoto = false
  }
},
  • 提交表单
// 上传头像
export const saveUserPhoto = (formData) => {
  return request({
    method: 'patch',
    url: 'v1_0/user/photo',
    data: formData
  })
}

// 保存用户信息
export const saveUserInfo = (data) => {
  return request({
    method: 'patch',
    url: 'v1_0/user/profile',
    data: data
  })
}

// 保存用户信息
async saveInfo () {
  // 编辑用户提交表单
  // 1、上传文件
  try {
    const fd = new FormData()
    fd.append('photo', this.$refs.myfile.files[0])
    await saveUserPhoto(fd)
  } catch {
    return this.$toast.fail('上传头像失败')
  }
  // 2、更新用户表单数据
  try {
    await saveUserInfo({
      name: this.user.name,
      gender: this.user.gender,
      birthday: this.user.birthday
    })
  } catch {
    return this.$toast.fail('更新用户信息失败')
  }
},
//总结:

//1. 如何触发弹窗选中文件?利用了file类型的input触发

//2. 如何直接操作DOM元素?this.$refs.myfile
//3. 如何基于原生js的api实现图片预览 FileReader
//4. 提交表单需要分两步:头像单独上传;表单项单独提交

# 小智同学-基本布局

  1. //目标:实现聊天功能基本布局
    
    //1. 实现页面聊天布局
    //2. 熟悉websocket技术
    //3. 基于websocke实现基本的聊天功能
    
  • 基本布局
<div class="container">
  <van-nav-bar fixed left-arrow @click-left="$router.back()" title="小智同学"></van-nav-bar>
  <div class="chat-list">
    <div class="chat-item left">
      <van-image fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
      <div class="chat-pao">ewqewq</div>
    </div>
    <div class="chat-item right">
      <div class="chat-pao">ewqewq</div>
      <van-image  fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
    </div>
  </div>
  <div class="reply-container van-hairline--top">
    <van-field v-model="value" placeholder="说点什么...">
      <span @click="send()" slot="button" style="font-size:12px;color:#999">提交</span>
    </van-field>
  </div>
</div>
  • 布局样式
.container {
  height: 100%;
  width: 100%;
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  background:#fafafa;
  padding: 46px 0 50px 0;
  .chat-list {
    height: 100%;
    overflow-y: scroll;
    .chat-item{
      padding: 10px;
      .van-image{
        vertical-align: top;
        width: 40px;
        height: 40px;
      }
      .chat-pao{
        vertical-align: top;
        display: inline-block;
        min-width: 40px;
        max-width: 70%;
        min-height: 40px;
        line-height: 38px;
        border: 0.5px solid #c2d9ea;
        border-radius: 4px;
        position: relative;
        padding: 0 10px;
        background-color: #e0effb;
        word-break: break-all;
        font-size: 14px;
        color: #333;
        &::before{
          content: "";
          width: 10px;
          height: 10px;
          position: absolute;
          top: 12px;
          border-top:0.5px solid #c2d9ea;
          border-right:0.5px solid #c2d9ea;
          background: #e0effb;
        }
      }
    }
  }
}
.chat-item.right{
  text-align: right;
  .chat-pao{
    margin-left: 0;
    margin-right: 15px;
    &::before{
      right: -6px;
      transform: rotate(45deg);
    }
  }
}
.chat-item.left{
  text-align: left;
  .chat-pao{
    margin-left: 15px;
    margin-right: 0;
    &::before{
      left: -5px;
      transform: rotate(-135deg);
    }
  }
}
.reply-container {
  position: fixed;
  left: 0;
  bottom: 0;
  height: 44px;
  width: 100%;
  background: #f5f5f5;
  z-index: 9999;
}
  • 状态数据
export default {
  data () {
    return {
      value: '',
      loading: false
    }
  },
  methods: {
    send () {

    }
  }
}

# websocket介绍

目标:熟悉websocket通信规则

  • http通信流程
    • 建立连接(三次握手)
    • 发送接收数据
    • 断开连接(四次挥手)
  • websocket通信流程(双向的:客户端可以向服务器发送消息,服务器也可以主动向客户端发送消息)
    • 首次发送请求时,需要建立连接
    • 后续收发消息就不再需要建立连接
    • 长时间没有数据交互会自动断开连接(也可以手动断开连接)

image-20210131195932810

结论:websocket做双向通信更加高效(比如聊天功能,推送服务)

# websocket基本使用

//目标:熟悉websocket基本用法

//浏览器为 HTTP 通信提供了 XMLHttpRequest 对象,同样的,也为 WebSocket 通信提供了一个操作接口:WebSocket。(http https) === (ws wss)
  • 基本通信流程
    • 拨号(建立连接)
    • 通话(双向通信)
    • 结束通话(关闭连接)
// 创建连接(和服务器建立连接,回复你)
var ws = new WebSocket("wss://echo.websocket.org");

// 连接成功
ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  // 发送消息
  ws.send("Hello WebSockets!");
};

// 接受信息
ws.onmessage = function(evt) {
  // 服务回复消息
  console.log( "Received Message: " + evt.data);
  // 关闭连接
  ws.close();
};

// 连接已经关闭
ws.onclose = function(evt) {
  console.log("Connection closed.");
}; 

# websocket第三方包用法

目标:熟悉socket.io基本信息和用法

官方网站:https://socket.io/get-started/chat/

既支持服务器,也支持客户端

  • 服务端代码(基于socket.io自己做)
/*
  websocket服务端
  作用:1、接收客户端发送的请求;2、向客户端返回数据
*/
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http);

// 监听跟路径
app.get('/', function(req, res){
  // 返回的结果是一个网页
  res.sendFile(__dirname + '/index.html');
});

// 监听客户端的链接
io.on('connection', function(socket){
  // 监听客户端端开链接事件
  socket.on('disconnect', function(){
    console.log('user disconnected');
  });
  
  // 监听客户端发送消息的事件
  socket.on('chat message', function(msg){
    // 客户端发送的消息msg
    // emit方法的作用是向客户端返回消息
    // emit参数一表示客户端监听的接收消息的事件
    // emit参数二表示服务器返回给客户端的消息
    io.emit('chat message', msg);
  });
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});
  • 客户端代码
<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <meta charset="utf-8" >                  
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <body>
    <!-- 消息列表 -->
    <ul id="messages"></ul>

    <!-- 发送消息的表单 -->
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>

    <!-- SocketIO 提供了一个客户端实现:socket.io.js -->
    <script src="https://cdn.bootcdn.net/ajax/libs/socket.io/3.0.0-rc4/socket.io.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script>
    <script>
      // 建立连接,得到 socket 通信对象
      var socket = io('ws://localhost:3000/')

      socket.on('connect', () => {
        console.log('建立连接成功了')
      })

      $('form').submit(function(e){
        e.preventDefault();
        // 向服务器发送消息
        // chat message事件名称与谁有关?服务器的事件监听有关
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
      });

      // 客户端监听服务器返回消息的事件
      socket.on('chat message', function(msg){
        // msg表示服务器返回的消息,通过jQuery把消息添加到页面
        $('#messages').append($('<li>').text(msg));
      });
    </script>
  </body>
</html>

总结:我们关注的是客户端代码。

  • //- 发消息:`socket.emit('chat message', '内容');`
    //- 收消息:`socket.on('chat message', function(msg){}`
    
    //---
    
    //- 关于socket通信案例运行步骤
    
    //1. 安装依赖包 `npm i`
    //2. 运行后端socket  `node server.js`
    //3. 打开前端页面 http://localhost:3000/
    //4. 输入内容可以进行socket通信
    

# 小智同学聊天功能

目标:实现小智同学聊天功能

  1. 创建socket链接
  2. 发送消息给服务器
  3. 接收消息并进行展示
  • 小智头像
import avatar from '@/assets/xz.png'
// data中数据
avatar: avatar
<van-image fit="cover" round :src="avatar" />
  • 自己的头像
<van-image  fit="cover" round :src="photo||avatar" />
  • 开始聊天
  1. 聊天记录
list: []
  1. 建立连接(初始化socket对象;监控链接成功的事件,成功链接后进行提示)
// 服务器端使用socket.io包,客户端使用socket.io-client包
import { io } from 'socket.io-client'
methods: {
  initSocket () {
    // 初始化Socket
    this.socket = io('ws://localhost:3000/')
    // 链接成功之后,后端接口会有一个默认的返回信息
    this.socket.on('connect', () => {
      // 链接建立成功
      this.list.push({
        type: 1,
        name: '小智',
        msg: '欢迎聊天'
      })
    })
    // 监听服务器返回的消息
    this.socket.on('chat message', (data) => {
      // 把返回的消息添加到列表中
      this.list.push({
        type: 1,
        name: '小智',
        msg: data
      })
    })
  }
},
created () {
  this.initSocket()
}
  1. 渲染记录
<div class="chat-item" :class='[{left: item.type===1}, {right: item.type===2}]' v-for='(item, index) in list' :key='index'>
  <!-- 左侧内容 -->
  <template v-if='item.type===1'>
    <van-image fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
    <div class="chat-pao">{{item.name + ':' + item.msg}}</div>
  </template>
  <!-- 右侧内容 -->
  <template v-else>
    <div class="chat-pao">{{item.name + ':' + item.msg}}</div>
    <van-image  fit="cover" round :src="photo||avatar" />
  </template>
</div>
  1. 发送消息
send () {
  // 点击按钮发送消息给服务器
  this.socket.emit('chat message', this.value)
  // 把发送的消息添加到列表中
  this.list.push({
    type: 2,
    name: '我',
    msg: this.value
  })
  // 清空表单
  this.value = ''
},
  1. 接收消息
this.socket.on('connect', () => {
    // 建了链接后默认  小智给你打招呼
    this.list.push({ name: 'xz', msg: '你好' })
})
// 监听服务器返回的消息
this.socket.on('chat message', (data) => {
    // 把返回的消息添加到列表中
    this.list.push({
        type: 1,
        name: '小智',
        msg: data
    })
})
  1. 关闭链接
destroyed () {
  this.socket.close()
}

# 关于组件的缓存

//默认情况下,切换组件页面后,原来的组件自动销毁,再次显示组件时,组件需要重新创建(相关生命周期函数会再次触发)

//那么有时,为了满足特殊场景,需要对组件进行缓存(通过 <keep-alive>标签进行组件缓存)

//缓存组件后,会有如下好处

//1. keep-alive包裹组件后,那么组件会进行缓存,再次显示组件时,效率更高
//2. keep-alive缓存组件可以使组件的状态保留,从而满足维持组件当前状态的需求
  • 关于缓存相关的生命周期函数
    • activated 显示组件时触发
    • deactivated 离开组件时触发
<template>
  <div>
    <div>Cachea</div>
    <div>{{count}}</div>
    <button @click='handleClick'>点击</button>
  </div>
</template>
<script>
export default {
  name: 'Cachea',
  data () {
    return {
      count: 0
    }
  },
  methods: {
    handleClick () {
      this.count += 1
    }
  },
  created () {
    console.log('a')
  },
  activated () {
    // 显示组件时触发
    console.log('a', 'activated')
    this.count = 0
  },
  deactivated () {
    // 离开组件时触发
    console.log('a', 'deactivated')
  }
}
</script>
  • 根据特定路由组件设置缓存
{ path: 'home', component: Home, meta: { isCache: true } },
<div class="wrapper">
  <keep-alive>
    <router-view v-if='$route.meta.isCache'/>
  </keep-alive>
  <router-view v-if='!$route.meta.isCache'/>
</div>