源码顶级干货 | 登录和客服聊天全流程
先看效果
已关注
关注
重播 分享 赞
先实现登录
后端部分(koa)- 数据库 – 小程序端
// 第一步const mongoose = require('mongoose')mongoose.pluralize(null) // 去掉集合后面的sconst { Schema, model } = mongooseconst versionKey = { versionKey: false }const moment = require('moment')moment.locale('zh-cn')const { nanoid } = require('nanoid')// 第二步// 小程序用户账户表const UserSchema = new Schema({// 用户唯一ID 用于加密uid: {type: String,unique: true,default: () => nanoid(12)},// 小程序openidopenidUnique: {type: String,unique: true,default: ''},// 方式1 手机号登录mobile: { // 手机号type: String,trim: true,default: ''},password: { // 密码type: String,select: false, // 不返回前端default: ''},// 方式2 用户昵称和头像登录nickname: { // 昵称type: String,default: '用户',trim: true,//去掉空格},avatar: { // 头像type: String,default: 'https://diancan-1252107261.cos.accelerate.myqcloud.com/lvyou/avatarurl.png'},// 用户信息gender: { // 性别type: Number,default: 0// 0 未知// 1 男// 2 女},birthday: {//生日type: String,default: ''},age: {//年龄type: String,default: ''},status: { // 用户状态type: Number,default: 1// 0 禁用// 1 正常},last_login_time: { // 最后登录时间type: Date,default: null},created_at: { // 创建时间type: Date,default: Date.now}}, versionKey)// 第三步module.exports = {modelUser: model('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: {//Logotype: String,default: 'https://q3.itc.cn/q_70/images01/20240903/f9b3b59fe62c425fbcf432dd0c01aa90.jpeg'},tradeName: {//店铺名称type: String,default: '心潮文化'},phone: {//账号type: String,required: true,trim: true,//去掉空格},password: {//密码,加密type: String,required: true,select: false,//私密},adminUid: {//唯一标识uidtype: String,unique: true,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.bodyImageLogin(nickname, avatar, openId)// 获取唯一openidconst openidUnique = await getOpenid(openId);// 判断用户之前是否已有账号const res = await modelUser.find({ openidUnique }).lean()const last_login_time = new Date()if (res.length > 0) {//已有账号const token = { user_Token: gentoken(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_Token: gentoken(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.bizId, message: '发送成功' })} else {ctx.send(res.body.message, 422)}})// 小程序端:手机号,验证码登录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_Token: gentoken(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_Token: gentoken(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.bodyAdminRegister(phone, password)const res = await adminUser.find({ phone })if (res.length > 0) {ctx.send('ERROR', 202, null, '账号已经注册')} 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.bodyAdminRegister(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_Token: gentoken(res[0].adminUid) }ctx.send('SUCCESS', 200, { ...res[0], ...token })} else {ctx.send('ERROR', 422, null, '账号或者密码错误')}})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 = trueconst 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-image: url('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-height: 100vh;}.login-region{position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);background-color: #ffffff;padding: 30px 100px;border-radius: 10px;}.login-input{margin: 20px 0;width: 350px;}.login-title{font-size: 30px;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 = truetry {// 上传头像const uploadAvatar = await uni.uploadFile({url:uploadUrl,filePath:userInfo.avatar,name:'file'})const fileurl = JSON.parse(uploadAvatar.data).dataconsole.log(fileurl, 'fileurl');// 用户登录 获取codeuni.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;left: 50%;top: 40%;transform: translate(-50%, -50%);}.login-view{width: 80vw;display: flex;flex-direction: column;align-items: center;#avatar-button {width: 150rpx;height: 150rpx;border-radius: 50%;position: relative;}.avatar{width: 150rpx;height: 150rpx;border-radius: 50%;position: absolute;left: 0;top: 0;}.form-submit{width: 100%;.weui-input{padding: 20rpx;margin: 20rpx;border-bottom: 1rpx solid #f2f2f2;}.submit-button{background:linear-gradient(to right,#A2C5FE, #C0E7FD);padding: 15rpx 0;margin: 55rpx 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 {string} token - 管理员token* @returns {Object|null} 验证通过返回管理员信息,失败返回null*/const verifyAdminToken = async (token) => {if (!token) return nulltry {// 解密tokenconst 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.Server} server - http服务器实例* @returns {Server} socket.io实例*/function initSocketIO(server) {// 创建socket.io实例并配置跨域const io = new Server(server, {cors: {origin: '*', // 允许所有来源methods: ["GET", "POST"],allowedHeaders: ["Content-Type", "Authorization"],credentials: true},allowEIO3: true, // 兼容低版本客户端transports: ['websocket', 'polling']})// 监听socket连接io.on('connection', async (socket) => {const socketQuery = socket.handshake.queryconsole.log('socket连接请求:', {userid: socketQuery.userid || socketQuery.adminUid,clientType: socketQuery.clientType,isAdmin: socketQuery.clientType === 'ADMIN'})// ========== 管理员身份校验 ==========if (socketQuery.clientType === 'ADMIN') {// 校验tokenconst adminInfo = await verifyAdminToken(socketQuery.authorization)// Token无效,直接断开连接if (!adminInfo) {console.log('管理员Token无效,拒绝连接:', socketQuery.adminUid)socket.disconnect()return}// Token有效,记录管理员信息到socket实例socket.adminInfo = adminInfosocket.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,time: new 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-inputv-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([])// 选中发送的用户idconst 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, // 选中的用户IDmessagetype: '001', // 管理员消息类型message: userMessage.value.trim(),avatar: adminInfo.logo, // 管理员头像nickname: adminInfo.tradeName // 管理员昵称}// 发送消息到后端socket.value.emit('adminMessage', msgObj)// 核心:更新分组的messageDataconst 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 = indexuserId.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},reconnectionAttempts: 4,reconnectionDelay: 2000})// 连接成功socket.value.on('connect', () => {console.log('管理员端socket连接成功')ElMessage.success(`客服(${adminInfo.tradeName})连接成功`)})// 接收用户消息socket.value.on('adminchat', (res) => {console.log('收到用户消息:', res)// 核心1:更新分组的messageDataif (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;height: 700px;}.chat-left{width: 150px;height: 700px;overflow-y: auto;background-color: #f5f5f5;}.user-message{display: flex;padding: 8px;cursor: pointer;transition: background-color 0.2s;}.user-message:hover{background-color: #e9e9e9;}.user-message img{width: 40px;height: 40px;border-radius: 4px;object-fit: cover;display: block;margin-right: 5px;}.user-message p{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;margin: 0;}.user-message p:nth-child(2){font-size: 14px;color: #666;padding-top: 2px;}.actives{background: burlywood;}.chat-right{background-color: #eaeaea;flex: 1;}.Chat-content{height: 550px;overflow-y: auto;padding: 0 10px;}.send-message{align-self: flex-end;margin: 0 4px 4px 0;}.message{display: flex;align-items: center;margin: 15px 0;}.message img{width: 30px;height: 30px;border-radius: 5px;object-fit: cover;}.message p{border-radius: 5px;padding: 3px 7px;margin: 0;}.msg-right{align-self: flex-end;}.msg-left{padding-right: 95px;}.msg-right{padding-left: 95px;}.msg-left p{background-color: #ffffff;margin-left: 5px;}.msg-right p{background-color: #304bcc;color: #ffffff;margin-right: 5px;}</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"><inputclass="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 = falsetextareaValue.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协议而不是httpconst socketUrl = baseUrl.replace('http://', 'ws://')// 先关闭已有连接(防止重复连接)if (socketObj.value) {socketObj.value.disconnect()socketObj.value = null}socketObj.value = io(socketUrl, {transports: ['websocket'], // 强制使用websocketquery: {authorization: user.user_Token,clientType: 'USER',userid: user._id},reconnectionAttempts: 4,reconnectionDelay: 2000,timeout: 5000, // 增加连接超时时间})// 连接成功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({ scrollTop: 99999 })})// 断开连接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.value) returnconst 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({ scrollTop: 99999 })}// 页面显示时初始化连接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;padding: 20rpx;}.message {display: flex;align-items: center;margin: 20rpx 0;}.message image {width: 65rpx;height: 65rpx;border-radius: 10rpx;}.message text {border-radius: 10rpx;padding: 10rpx;}.msg-right {align-self: flex-end;}.msg-left {padding-right: 150rpx;}.msg-right {padding-left: 150rpx;}.msg-left text {background-color: #ffffff;margin-left: 10rpx;}.msg-right text {background-color: #304bcc;color: #ffffff;margin-right: 10rpx;}.Input_field {background-color: #e4e2e2;position: fixed;left: 0;right: 0;bottom: v-bind('keyboardHeight + "px"');display: flex;align-items: v-bind('textareaValue.alignItems');padding-bottom: v-bind('paddingBottom');padding-top: 20rpx;padding-left: 30rpx;padding-right: 30rpx;transition:bottom 0.35s ease,padding-bottom 0.35s ease;}.Input_field input {width: 100%;background-color: #fff;border-radius: 10rpx;padding: 20rpx;}.Input_field image {height: 70rpx;width: 70rpx;margin-left: 20rpx;}</style>
夜雨聆风