乐于分享
好东西不私藏

源码顶级干货 | 登录和客服聊天全流程

本文最后更新于2026-03-11,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜

源码顶级干货 | 登录和客服聊天全流程

先看效果

已关注

关注

重播 分享

先实现登录

    后端部分(koa)- 数据库 – 小程序端

// 第一步const mongoose = require('mongoose')mongoose.pluralize(null// 去掉集合后面的sconst { Schema, model } = mongooseconst versionKey = { versionKeyfalse }const moment = require('moment')moment.locale('zh-cn')const { nanoid } = require('nanoid')// 第二步// 小程序用户账户表const UserSchema = new Schema({    // 用户唯一ID 用于加密    uid: {         typeString,        uniquetrue,        default() => nanoid(12)    },    // 小程序openid    openidUnique: {         typeString,        uniquetrue,        default''    },    // 方式1 手机号登录    mobile: { // 手机号        typeString,        trimtrue,        default''    },    password: { // 密码        typeString,        selectfalse// 不返回前端        default''    },    // 方式2 用户昵称和头像登录    nickname: { // 昵称        typeString,        default'用户',        trimtrue,//去掉空格    },    avatar: { // 头像        typeString,        default'https://diancan-1252107261.cos.accelerate.myqcloud.com/lvyou/avatarurl.png'    },    // 用户信息    gender: { // 性别        typeNumber,        default0        // 0 未知        // 1 男        // 2 女    },    birthday: {//生日        typeString,        default''    },    age: {//年龄        typeString,        default''    },        status: { // 用户状态        typeNumber,        default1        // 0 禁用        // 1 正常    },    last_login_time: { // 最后登录时间        typeDate,        defaultnull    },    created_at: { // 创建时间        typeDate,        defaultDate.now    }}, versionKey)// 第三步module.exports = {    modelUsermodel('user'UserSchema)}

   后端部分(koa)- 数据库 – pc端

// 第一步const mongoose require('mongoose')mongoose.pluralize(null// 去掉集合后面的sconst { Schema, model } = mongooseconst versionKey = { versionKey: false }const moment require('moment')moment.locale('zh-cn')// 第二步// 小程序用户账户表const adminUserSchema new Schema({  clientType: {//io角色定义      type: String,      default'ADMIN'  },      logo: {//Logo      type: String,      default'https://q3.itc.cn/q_70/images01/20240903/f9b3b59fe62c425fbcf432dd0c01aa90.jpeg'  },  tradeName: {//店铺名称      type: String,      default'心潮文化'  },  phone: {//账号      type: String,      requiredtrue,      trimtrue,//去掉空格  },  password: {//密码,加密      type: String,      requiredtrue,      selectfalse,//私密  },  adminUid: {//唯一标识uid      type: String,      uniquetrue,      default: () => new Date().getTime()  },}, versionKey)// 第三步module.exports = {  adminUser: model('adminUser', adminUserSchema)}

   后端部分(koa)- 数据库 – 接口部分 – 小程序部分

const Router = require('@koa/router')const router = new Router()const {    ImageLogin,    Vercode} = require('@/config/valiData')const { modelUser } = require('@/models/collection')const { gentoken } = require('@/token/jwt'// 生成tokenconst { getOpenid } = require('@/token/getOpenid'// 小程序获取用户唯一openIdconst { verCode, queryCode } = require('@/alicode/index')// 小程序用户昵称和头像登录router.post('/imageLogin'async (ctx) => {  const { nickname, avatar, openId} = ctx.request.body  ImageLogin(nickname, avatar, openId)  // 获取唯一openid  const openidUnique = await getOpenid(openId);  // 判断用户之前是否已有账号  const res = await modelUser.find({ openidUnique }).lean()   const last_login_time = new Date()  if (res.length > 0) {//已有账号      const token = { user_Tokengentoken(res[0].openidUnique) }      // 更新最后登录时间      await modelUser.updateOne(        { openidUnique },        { $set: { last_login_time } }      )      ctx.send('SUCCESS'200, { ...res[0], ...token })  } else {//没有账号      await modelUser.create({ nickname, avatar, openidUnique, last_login_time })      const userData = await modelUser.find({ openidUnique }).lean()      const token = { user_Tokengentoken(userData[0].openidUnique) }      ctx.send('SUCCESS'200, { ...userData[0], ...token })  }  })// 小程序手机号登录// 小程序端:发送验证码router.get('/vercode'async ctx => {    const { phoneNumbers } = ctx.query    // 校验    Vercode(phoneNumbers)    const res = await verCode(phoneNumbers)    if (res.body.code == 'OK' && res.body.message == 'OK') {        ctx.send('SUCCESS'200, { bizId: res.body.bizIdmessage'发送成功' })    } else {        ctx.send(res.body.message422)    }})// 小程序端:手机号,验证码登录router.post('/mobile-registration'async ctx => {    const { mobile, code, bizId } = ctx.request.body    //校验    Mobileregistration(mobile, code, bizId)    // 验证验证码是否正确    await queryCode(mobile, bizId, code)    // 判断用户之前是否已有账号    const res = await modelUser.find({ mobile }).lean()    if (res.length > 0) {//已有账号        const token = { user_Tokengentoken(res[0].mobile) }        ctx.send('SUCCESS'200, { ...res[0], ...token })    } else {//没有账号        const nickname = '用户_' + mobile.slice(-4)        await modelUser.create({ mobile, nickname })        const userData = await modelUser.find({ mobile }).lean()        const token = { user_Tokengentoken(userData[0].mobile) }        ctx.send('SUCCESS'200, { ...userData[0], ...token })    }})module.exports = router.routes()

   后端部分(koa)- 数据库 – 接口部分 – 网页部分

const Router = require('@koa/router')const router = new Router()const crypto = require('crypto')const {    AdminRegister} = require('@/config/valiData')const { adminUser } = require('@/models/admin-collection')const { gentoken } = require('@/token/jwt'// 生成token// 后台注册用户router.post('/adminRegister'async (ctx) => {  const { phone, password } = ctx.request.body  AdminRegister(phone, password)  const res = await adminUser.find({ phone })  if (res.length > 0) {    ctx.send('ERROR'202null'账号已经注册')  } else {    //创建哈希对象    const hash = crypto.createHash('sha256').update(password)    // 生成哈希值    const passwordHash = hash.digest('hex')    await adminUser.create({ phone, password: passwordHash })    ctx.send('SUCCESS'200, {msg'注册成功'})  }})// 后台登录用户router.post('/adminLogin'async (ctx) => {  const { phone, password } = ctx.request.body  AdminRegister(phone, password)  //创建哈希对象  const hash = crypto.createHash('sha256').update(password)  // 生成哈希值  const passwordHash = hash.digest('hex')  const res = await adminUser.find({ phone, password: passwordHash }).lean()  if (res.length > 0) {      const token = { admin_Tokengentoken(res[0].adminUid) }      ctx.send('SUCCESS'200, { ...res[0], ...token })  } else {      ctx.send('ERROR'422null'账号或者密码错误')  }  })module.exports = router.routes()

   前端部分 – 网页部分

<template>    <divid="login-view">        <divclass="login-region">            <divclass="login-input login-title">心潮文化</div>            <divclass="login-input">                <el-inputplaceholder="商家手机号"v-model="mobile"clearable/>            </div>            <divclass="login-input">                <el-inputplaceholder="商家密码"v-model="password"clearableshow-password/>            </div>            <el-buttontype="success":loading="loadIng"class="login-input" @click="goLogin">登录</el-button>        </div>    </div></template><scriptsetup>    import { ref } from 'vue';    import request from '@/api/request.js'    import { useRouter } from 'vue-router'    const $router = useRouter()  // 这是路由跳转的    const mobile = ref('')    const password = ref('')    const loadIng = ref(false)    async function goLogin(){        try {            loadIng.value = true            const res = await request.post('/adminLogin',{phone:mobile.value,password:password.value})            localStorage.setItem('menuid'JSON.stringify('1'))            localStorage.setItem('adminInfor'JSON.stringify(res.data))            loadIng.value = false            $router.push('/index')        } catch (error) {            loadIng.value = false        }    }</script><style>#login-view{    background-imageurl('https://diancan-1252107261.cos.accelerate.myqcloud.com/lvyou/icon/login-pc-bei.jpg');    background-position: center center;    background-repeat: no-repeat;    background-size: cover;    min-height100vh;}.login-region{    position: absolute;    top50%;    left50%;    transformtranslate(-50%, -50%);    background-color#ffffff;    padding30px 100px;    border-radius10px;}.login-input{    margin20px 0;    width350px;}.login-title{    font-size30px;    font-weight: bold;    text-align: center;    background-color:inherit !important;    color#67c23a;}.login-region div{    background-color#f6f7f9;}</style>

    前端部分 – 小程序部分

<template>  <viewclass="ImageLogin">    <viewclass="login-view">      <buttonopen-type="chooseAvatar"id="avatar-button" @chooseavatar="chooseavatar">        <imageclass="avatar":src="userInfo.avatar === '' ? '/static/touxiang.png' : userInfo.avatar"></image>      </button>      <formclass="form-submit" @submit="fromSubmit">        <inputtype="nickname"class="weui-input"name="input"placeholder="请输入昵称"/>        <buttonform-type="submit"class="submit-button":loading="loading">登录</button>      </form>    </view>  </view></template><scriptsetup>import { reactive, ref } from 'vue';import { uploadUrl } from '@/api/request.js'  import { userData } from '@/store/index.js'// 存储头像昵称const userInfo = reactive({  avatar:'',  nickname:''})// 获取头像const chooseavatar = (event)=>{  userInfo.avatar = event.detail.avatarUrl}const loading = ref(false)const fromSubmit = async(event)=>{  userInfo.nickname = event.detail.value.input  // 校验  if(userInfo.avatar === '' || userInfo.nickname.trim() === ''){    uni.showToast({      icon:'none',      title:'请填写头像和昵称'    })    return false  }  loading.value = true  try {    // 上传头像    const uploadAvatar = await uni.uploadFile({      url:uploadUrl,      filePath:userInfo.avatar,      name:'file'    })    const fileurl = JSON.parse(uploadAvatar.data).data    console.log(fileurl, 'fileurl');    // 用户登录 获取code    uni.login({      success:async(res)=>{        await userData().isNotLoggedIn(          userInfo.nickname,          fileurl,          res.code        )        uni.switchTab({ url'/pages/my/index' })        setTimeout(() => {          uni.showToast({            icon:'success',            title:'登录成功'          })                      }, 1000);      }    })  } catch(err) {  } finally {    loading.value = false  }}</script><stylescopedlang="less">     .ImageLogin {      position: absolute;      left50%;      top40%;      transformtranslate(-50%, -50%);    }    .login-view{      width80vw;      display: flex;      flex-direction: column;      align-items: center;      #avatar-button {        width150rpx;        height150rpx;        border-radius50%;        position: relative;      }      .avatar{        width150rpx;        height150rpx;        border-radius50%;        position: absolute;        left0;        top0;      }      .form-submit{        width100%;        .weui-input{          padding20rpx;          margin20rpx;          border-bottom1rpx solid #f2f2f2;        }        .submit-button{          background:linear-gradient(to right,#A2C5FE#C0E7FD);          padding15rpx 0;          margin55rpx 20rpx 0 20rpx;        }      }    }</style>

再实现客服聊天

    后端部分(koa) – socket.io

// routes/chat/index.jsconst { Server } = require('socket.io')const { adminUser } = require('../../models/admin-collection')const jwt = require('jsonwebtoken')const { secretkey } = require('../../token/tokentime').security/** * 校验管理员Token * @param {stringtoken - 管理员token * @returns {Object|null} 验证通过返回管理员信息,失败返回null */const verifyAdminToken = async (token) => {  if (!token) return null  try {    // 解密token    const decoded = jwt.verify(token, secretkey)     // 根据token中的uid查询管理员信息    const adminInfo = await adminUser.findOne({       adminUid: decoded.uid || decoded.uuid || decoded.userid    })    return adminInfo || null  } catch (err) {    console.error('管理员Token校验失败:', err.message)    return null  }}/** * 初始化Socket.IO服务 * @param {http.Serverserver - http服务器实例 * @returns {Server} socket.io实例 */function initSocketIO(server) {  // 创建socket.io实例并配置跨域  const io = new Server(server, {    cors: {      origin'*'// 允许所有来源      methods: ["GET""POST"],      allowedHeaders: ["Content-Type""Authorization"],      credentialstrue    },    allowEIO3true// 兼容低版本客户端    transports: ['websocket''polling']  })  // 监听socket连接  io.on('connection'async (socket) => {    const socketQuery = socket.handshake.query    console.log('socket连接请求:', {      userid: socketQuery.userid || socketQuery.adminUid,      clientType: socketQuery.clientType,      isAdmin: socketQuery.clientType === 'ADMIN'    })    // ========== 管理员身份校验 ==========    if (socketQuery.clientType === 'ADMIN') {      // 校验token      const adminInfo = await verifyAdminToken(socketQuery.authorization)      // Token无效,直接断开连接      if (!adminInfo) {        console.log('管理员Token无效,拒绝连接:', socketQuery.adminUid)        socket.disconnect()        return      }      // Token有效,记录管理员信息到socket实例      socket.adminInfo = adminInfo      socket.join('adminRoom')      console.log(`管理员(${adminInfo.tradeName})连接成功,ID:${adminInfo.adminUid}`)    } else {      // 普通用户连接      socket.join(socketQuery.userid)      // 查询客服信息(发送欢迎消息)      const adminInfo = await adminUser.findOne()      const welcomeMessage = {        avatar: adminInfo?.logo || '默认头像地址',        message'我是客服,请问有什么可以帮助你的',        messagetype'001'// 001表示管理员消息        nickname: adminInfo?.tradeName || '客服',        userid: socketQuery.userid,        timenew Date().getTime()      }      // 发送欢迎消息给用户      io.to(socketQuery.userid).emit('wxchat', welcomeMessage)      console.log(`已向用户 ${socketQuery.userid} 发送欢迎消息`)    }    // 监听用户发送消息    socket.on('userMessage'(message) => {      console.log('收到用户消息:', message)      // 转发给所有管理员      io.to('adminRoom').emit('adminchat', message)    })    // 监听管理员回复消息    socket.on('adminMessage'(message) => {      // 验证管理员身份(防止非法请求)      if (!socket.adminInfo) {        console.warn('非管理员尝试发送消息:', message.userid)        return      }      console.log(`管理员(${socket.adminInfo.tradeName})回复用户(${message.userid}):`, message.message)      // 转发给指定用户      io.to(message.userid).emit('wxchat', {        ...message,        adminName: socket.adminInfo.tradeName,        adminAvatar: socket.adminInfo.logo      })    })    // 监听socket错误    socket.on('error'(err) => {      console.error('Socket错误:', err)    })    // 监听断开连接    socket.on('disconnect'(reason) => {      const identity = socket.adminInfo         ? `管理员(${socket.adminInfo.tradeName})`         : `用户(${socket.handshake.query.userid})`      console.log(`${identity}断开连接,原因:`, reason)    })  })  return io}// 导出初始化函数module.exports = {  initSocketIO}

   网页部分 – vite

<template>    <divid="Content-page">        <Paging:pagData="[{name:'客服',router:''}]" />        <divclass="content-main">            <divclass="Chat-area">                <divclass="global-display global-f-flex">                    <divclass="chat-left">                        <divclass="user-message"v-for="(item,index) in userListMessages":key="index"                        :class="{'actives':userListIndex === index}" @click="selectUser(item.useridIng,index)">                            <img:src="item.avatarIng">                            <div>                                <p>{{item.nicknameIng}}</p>                                <p>{{item.messageIng}}</p>                            </div>                        </div>                    </div>                    <divclass="chat-right global-display global-f-direction global-j-content global-f-flex">                        <divclass="Chat-content"ref="scrollContainer">                            <divclass="global-display global-f-direction"v-for="(item,index) in selectMessage":key="index">                                <!-- 用户 -->                                <divclass="message msg-left"v-if="item.messagetype == '002'">                                    <img:src="item.avatar"/>                                    <p>{{ item.message }}</p>                                </div>                                <!-- 管理员 -->                                <divclass="message msg-right"v-else>                                    <p>{{ item.message }}</p>                                    <img:src="item.avatar"/>                                </div>                            </div>                        </div>                        <div>                            <el-input                                v-model="userMessage"                                :rows="4"                                resize="none"                                type="textarea"                                placeholder="请输入聊天内容"                                @keyup.enter="sendMessage"                                :disabled="selectMessage.length > 0 ? false : true"                            />                        </div>                        <el-buttonclass="send-message"type="primary":disabled="selectMessage.length > 0 ? false : true"size="default" @click="sendMessage">发送</el-button>                    </div>                </div>            </div>        </div>    </div></template><scriptsetup>    // 顶部导航组件    import Paging from '@/page/component/Paging-component.vue'    import { ref, onMounted, onUnmounted } from 'vue'    import { io } from 'socket.io-client' // 保留你的io引入方式    import { ElMessage } from 'element-plus'    // 后端socket地址    const socketUrl = 'ws://192.168.0.103:8933'    // socket实例    const socket = ref(null)    // 输入的消息    const userMessage = ref('')    // 核心:按用户分组存储消息([{userid:xxx, data:[消息列表]}])    const messageData = ref([])    // 左边用户列表(和别人的字段名完全一致)    const userListMessages = ref([])    // 当前选中的用户索引    const userListIndex = ref(-1)    // 当前选中用户的聊天记录    const selectMessage = ref([])    // 选中发送的用户id    const userId = ref('')    const scrollContainer = ref(null);    // ========== 管理员本地信息 ==========    const adminInfo = JSON.parse(localStorage.getItem('adminInfor')) || {}    // 发送消息    function sendMessage() {        if (userMessage.value.trim() === '' || userId.value === '') {            ElMessage.warning('请选择聊天用户并输入消息内容')            return        }        // 构造管理员消息对象        const msgObj = {            userid: userId.value// 选中的用户ID            messagetype'001',   // 管理员消息类型            message: userMessage.value.trim(),            avatar: adminInfo.logo// 管理员头像            nickname: adminInfo.tradeName // 管理员昵称        }        // 发送消息到后端        socket.value.emit('adminMessage', msgObj)        // 核心:更新分组的messageData        const userIndex = messageData.value.findIndex(item => item.userid === userId.value)        if (userIndex !== -1) {            messageData.value[userIndex].data.push(msgObj)        }        // 清空输入框        userMessage.value = ''        // 滚动到底部        setTimeout(() => {            if (scrollContainer.value) {                scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight            }        }, 300)    }    // 选择用户    function selectUser(userid, index) {        userListIndex.value = index        userId.value = userid        // 从分组的messageData中查找该用户的消息        messageData.value.forEach(item => {            if (item.userid === userid) {                selectMessage.value = item.data            }        })    }    // ========== 初始化Socket连接 ==========    function initSocket() {        // 创建socket连接(管理员身份)        socket.value = io(socketUrl, {            transports: ['websocket'],            query: {                authorization: adminInfo.admin_Token,                clientType: adminInfo.clientType,                adminUid: adminInfo.adminUid,                userId: adminInfo._id            },            reconnectionAttempts4,            reconnectionDelay2000        })        // 连接成功        socket.value.on('connect'() => {            console.log('管理员端socket连接成功')            ElMessage.success(`客服(${adminInfo.tradeName})连接成功`)        })        // 接收用户消息        socket.value.on('adminchat'(res) => {            console.log('收到用户消息:', res)            // 核心1:更新分组的messageData            if (messageData.value.length <= 0) {                messageData.value = [{                    userid: res.userid,                    data: [res]                }]            } else {                const userIndex = messageData.value.findIndex(item => item.userid === res.userid)                if (userIndex === -1) {                    messageData.value.push({                        userid: res.userid,                        data: [res]                    })                } else {                    messageData.value[userIndex].data.push(res)                }            }            // 核心2:更新左侧用户列表            const obj = {                avatarIng: res.avatar,                messageIng: res.message,                messagetypeIng: res.messagetype,                nicknameIng: res.nickname,                useridIng: res.userid            }            if (userListMessages.value.length > 0) {                if (userListMessages.value.some(item => item.useridIng === res.userid)) {                    const userListIndexs = userListMessages.value.findIndex(item => item.useridIng === res.userid)                    userListMessages.value[userListIndexs].messageIng = res.message                } else {                    userListMessages.value.push(obj)                }            } else {                userListMessages.value.push(obj)            }        })        // 连接错误        socket.value.on('connect_error'(err) => {            console.error('管理员端socket连接失败:', err)            ElMessage.error('连接聊天服务器失败,请重试!')        })        // 断开连接        socket.value.on('disconnect'(reason) => {            console.log('管理员端socket断开:', reason)            ElMessage.warning('聊天连接已断开,正在尝试重连...')            if (reason !== 'io client disconnect') {                setTimeout(initSocket, 3000)            }        })    }    onMounted(() => {        initSocket()    })    onUnmounted(() => {        if (socket.value) {            socket.value.disconnect()        }    })</script><stylescoped>    .Chat-area{        display: flex;        height700px;    }    .chat-left{        width150px;        height700px;        overflow-y: auto;        background-color#f5f5f5;    }    .user-message{        display: flex;        padding8px;        cursor: pointer;        transition: background-color 0.2s;    }    .user-message:hover{        background-color#e9e9e9;    }    .user-message img{        width40px;        height40px;        border-radius4px;        object-fit: cover;        display: block;        margin-right5px;    }    .user-message p{        overflow:hidden;         text-overflow:ellipsis;        display:-webkit-box;         -webkit-box-orient:vertical;        -webkit-line-clamp:1;        margin0;    }    .user-message p:nth-child(2){        font-size14px;        color#666;        padding-top2px;    }    .actives{        background: burlywood;    }    .chat-right{        background-color#eaeaea;        flex1;    }    .Chat-content{        height550px;        overflow-y: auto;        padding0 10px;    }    .send-message{        align-self: flex-end;        margin0 4px 4px 0;    }    .message{        display: flex;        align-items: center;        margin15px 0;    }    .message img{        width30px;        height30px;        border-radius5px;        object-fit: cover;    }    .message p{        border-radius5px;        padding3px 7px;        margin0;    }    .msg-right{        align-self: flex-end;    }    .msg-left{        padding-right95px;    }    .msg-right{        padding-left95px;    }    .msg-left p{        background-color#ffffff;        margin-left5px;    }    .msg-right p{        background-color#304bcc;        color#ffffff;        margin-right5px;    }</style>

   小程序部分(uniapp)

<template>  <viewclass="messag-content">    <blockv-for="(item,index) in messageData":key="index">      <!-- 管理员 -->      <viewclass="message msg-left"v-if="item.messagetype == '001'">        <image:src="item.avatar"mode="aspectFill"/>        <text>{{ item.message }}</text>      </view>      <!-- 自己发的 -->      <viewclass="message msg-right"v-else>        <text>{{ item.message }}</text>        <image:src="item.avatar"mode="aspectFill"/>      </view>    </block>  </view>  <viewstyle="height: 300rpx;"></view>  <!-- 输入框 -->  <viewclass="Input_field">    <input    class="input-content"      type="text"      placeholder="输入消息"      maxlength="-1"      cursor-spacing="40"      confirm-type="send"      @confirm="sendMessage"      @linechange="lineChange"      v-model="userMessage"      :adjust-position="false"    />    <imagesrc="/static/fasong.png"mode="aspectFit" @click="sendMessage"/>  </view></template><scriptsetup>import { computed, onMounted, ref, getCurrentInstance, reactive } from 'vue'import { baseUrl } from '../../api/request'const io = require('../../api/weapp.socket.io.js')import { Base64 } from 'js-base64'import { onHide, onShow } from '@dcloudio/uni-app'  const instance = getCurrentInstance();  // 存储textarea的属性  const textareaValue = reactive({    autoHeight:true,    alignItems:'center',    height:'0px'  })  // 输入框换行时触发  const lineChange = (event)=>{    // console.log(event);    const {height,lineCount} = event.detail    // 如果>=2行    textareaValue.alignItems = lineCount >= 2 ? 'flex-end' : 'center'    // 如果>=6行,不再自动增高    if(lineCount >= 6){      textareaValue.autoHeight = false      textareaValue.height = height    }else{      textareaValue.autoHeight = true    }  }  // textarea的父级高度  const textareaHeight = ref('')  const keyboardHeight = ref(0)  // 计算 padding-bottom(单位 rpx)  const paddingBottom = computed(() => {    return keyboardHeight.value > 0 ? '20rpx' : '58rpx'  })    // 获取textarea的父级高度  onMounted(()=>{    setTimeout(()=>{      const query = uni.createSelectorQuery().in(instance);      query.select('.input-content').boundingClientRect((res)=>{        textareaHeight.value = res.height + 'px'      }).exec()    },300)    uni.onKeyboardHeightChange(res => {      keyboardHeight.value = res.height    })      })  const socketObj = ref(null// weapp.socket.io对象const messageData = ref([]) // 存储聊天消息const userMessage = ref(''// 输入的消息// 获取本地缓存用户信息function userInfo() {  let user = uni.getStorageSync('userInfo')  if (!user) {    uni.showToast({ title'请先登录'icon'none' })    return null  }  const base64Toekn = Base64.encode(user.user_Token + ':')  return {    user_Token'Basic ' + base64Toekn,    _id: user._id,    avatar: user.avatar,    nickname: user.nickname  }}// 初始化socket连接function initSocket() {  const user = userInfo()  if (!user) return  // 关键修改:使用ws协议而不是http  const socketUrl = baseUrl.replace('http://''ws://')  // 先关闭已有连接(防止重复连接)  if (socketObj.value) {    socketObj.value.disconnect()    socketObj.value = null  }  socketObj.value = io(socketUrl, {    transports: ['websocket'], // 强制使用websocket    query: {      authorization: user.user_Token,      clientType'USER',      userid: user._id    },    reconnectionAttempts4,    reconnectionDelay2000,    timeout5000// 增加连接超时时间  })  // 连接成功  socketObj.value.on('connect'() => {    console.log('Socket连接成功')    uni.showToast({ title'连接成功'icon'success' })  })  // 连接失败(关键:捕获连接失败)  socketObj.value.on('connect_error'(err) => {    console.error('Socket连接失败:', err)    uni.showToast({ title'连接失败,请重试'icon'none' })  })  // 重新连接失败  socketObj.value.on('reconnect_failed'() => {    console.log('重新连接失败')    uni.showToast({ title'重新连接失败'icon'none' })  })  // 正在重新连接  socketObj.value.on('reconnect_attempt'() => {    console.log('正在重新连接')  })  // 监听错误  socketObj.value.on('error'(err) => {    console.error('Socket错误:', err)    if (err === '401') {      uni.navigateTo({ url'/pages/login/index' })    }  })  // 接收管理员消息  socketObj.value.on('wxchat'(data) => {    console.log('收到管理员消息:', data)    messageData.value.push(data)    // 滚动到最新消息    uni.pageScrollTo({ scrollTop99999 })  })  // 断开连接  socketObj.value.on('disconnect'(reason) => {    console.log('Socket断开连接:', reason)    // 如果不是手动断开,自动重连    if (reason !== 'io client disconnect') {      setTimeout(() => initSocket(), 3000)    }  })}// 发送消息function sendMessage() {  const msg = userMessage.value.trim()  if (!msg || !socketObj.valuereturn  const user = userInfo()  const msgObj = {    avatar: user.avatar,    message: msg,    messagetype"002",    nickname: user.nickname,    userid: user._id  }  // 发送消息  socketObj.value.emit('userMessage', msgObj)  // 添加到本地消息列表  messageData.value.push(msgObj)  // 清空输入框  userMessage.value = ''  // 滚动到底部  uni.pageScrollTo({ scrollTop99999 })}// 页面显示时初始化连接onShow(() => {  if (!socketObj.value) {  initSocket()  }})// 页面卸载时关闭连接(防止内存泄漏)onHide(() => {  if (socketObj.value) {    socketObj.value.disconnect()    socketObj.value = null  }})</script><stylescoped>page {  background-color#eaeaea;}.messag-content {  display: flex;  flex-direction: column;  padding20rpx;}.message {  display: flex;  align-items: center;  margin20rpx 0;}.message image {  width65rpx;  height65rpx;  border-radius10rpx;}.message text {  border-radius10rpx;  padding10rpx;}.msg-right {  align-self: flex-end;}.msg-left {  padding-right150rpx;}.msg-right {  padding-left150rpx;}.msg-left text {  background-color#ffffff;  margin-left10rpx;}.msg-right text {  background-color#304bcc;  color#ffffff;  margin-right10rpx;}.Input_field {    background-color#e4e2e2;    position: fixed;    left0;    right0;    bottomv-bind('keyboardHeight + "px"');    display: flex;    align-itemsv-bind('textareaValue.alignItems');    padding-bottomv-bind('paddingBottom');    padding-top20rpx;    padding-left30rpx;    padding-right30rpx;    transition:      bottom 0.35s ease,      padding-bottom 0.35s ease;}.Input_field input {  width100%;  background-color#fff;  border-radius10rpx;  padding20rpx;}.Input_field image {  height70rpx;  width70rpx;  margin-left20rpx;}</style>
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 源码顶级干货 | 登录和客服聊天全流程

猜你喜欢

  • 暂无文章

评论 抢沙发

9 + 8 =