Merge branch 'main' of http://sl.vrgon.com:3000/JiXinHui/YingXingAI
This commit is contained in:
		
						commit
						524baaa0bd
					
				|  | @ -1,2 +1,3 @@ | |||
| # 忽略 unpackage 文件夹 | ||||
| /unpackage/ | ||||
| /docs | ||||
|  | @ -14,19 +14,6 @@ const getWebSocketUrl = () => { | |||
|   // H5 生产环境
 | ||||
|   return 'wss://120.55.234.65:8082/ws/chat' | ||||
|   // #endif
 | ||||
|    | ||||
|   // #ifdef MP-WEIXIN
 | ||||
|   // 小程序必须使用 wss(加密连接)
 | ||||
|   return 'wss://120.55.234.65:8082/ws/chat' | ||||
|   // #endif
 | ||||
|    | ||||
|   // #ifdef APP-PLUS
 | ||||
|   // App 可以使用 ws 或 wss
 | ||||
|   return 'wss://120.55.234.65:8082/ws/chat' | ||||
|   // #endif
 | ||||
|    | ||||
|   // 默认地址
 | ||||
|   return 'ws://120.55.234.65:8082/ws/chat' | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|  |  | |||
|  | @ -1,607 +0,0 @@ | |||
| # WebSocket 使用示例 | ||||
| 
 | ||||
| ## 📦 已创建的文件 | ||||
| 
 | ||||
| ✅ `/utils/websocket-manager.js` - WebSocket 管理器(已完成) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🚀 快速开始 | ||||
| 
 | ||||
| ### 1. 在 App.vue 中初始化连接 | ||||
| 
 | ||||
| ```vue | ||||
| <script> | ||||
| import wsManager from '@/utils/websocket-manager.js' | ||||
| import { mapState } from 'vuex' | ||||
| 
 | ||||
| export default { | ||||
|   computed: { | ||||
|     ...mapState(['vuex_token', 'vuex_user']) | ||||
|   }, | ||||
|    | ||||
|   onLaunch() { | ||||
|     console.log('App Launch') | ||||
|      | ||||
|     // 如果已登录,连接 WebSocket | ||||
|     if (this.vuex_token) { | ||||
|       this.connectWebSocket() | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     connectWebSocket() { | ||||
|       // 替换为你的 WebSocket 服务器地址 | ||||
|       const wsUrl = `wss://your-server.com/chat?token=${this.vuex_token}` | ||||
|        | ||||
|       wsManager.connect(wsUrl, { | ||||
|         reconnectInterval: 3000,       // 重连间隔 3 秒 | ||||
|         maxReconnectAttempts: 10,      // 最多重连 10 次 | ||||
|         heartbeatInterval: 30000       // 心跳间隔 30 秒 | ||||
|       }) | ||||
|        | ||||
|       // 监听连接成功 | ||||
|       wsManager.on('open', () => { | ||||
|         console.log('[App] WebSocket 连接成功') | ||||
|         uni.showToast({ | ||||
|           title: '已连接', | ||||
|           icon: 'success' | ||||
|         }) | ||||
|       }) | ||||
|        | ||||
|       // 监听连接关闭 | ||||
|       wsManager.on('close', (res) => { | ||||
|         console.log('[App] WebSocket 已关闭', res) | ||||
|       }) | ||||
|        | ||||
|       // 监听连接错误 | ||||
|       wsManager.on('error', (err) => { | ||||
|         console.error('[App] WebSocket 错误', err) | ||||
|         uni.showToast({ | ||||
|           title: '连接失败', | ||||
|           icon: 'none' | ||||
|         }) | ||||
|       }) | ||||
|        | ||||
|       // 监听重连 | ||||
|       wsManager.on('reconnect', (attempts) => { | ||||
|         console.log(`[App] 正在重连 (${attempts})`) | ||||
|       }) | ||||
|        | ||||
|       // 监听消息(全局消息处理) | ||||
|       wsManager.on('message', (data) => { | ||||
|         console.log('[App] 收到消息:', data) | ||||
|          | ||||
|         // 根据消息类型处理 | ||||
|         switch (data.type) { | ||||
|           case 'message': | ||||
|             // 新消息 | ||||
|             this.handleNewMessage(data) | ||||
|             break | ||||
|           case 'notification': | ||||
|             // 系统通知 | ||||
|             this.handleNotification(data) | ||||
|             break | ||||
|           default: | ||||
|             console.log('[App] 未知消息类型:', data.type) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     handleNewMessage(data) { | ||||
|       // 更新 Vuex 中的消息列表 | ||||
|       this.$store.commit('addMessage', data) | ||||
|        | ||||
|       // 显示新消息提示 | ||||
|       uni.showTabBarRedDot({ | ||||
|         index: 1 // 消息页的 tabBar 索引 | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     handleNotification(data) { | ||||
|       uni.showToast({ | ||||
|         title: data.content, | ||||
|         icon: 'none' | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   onHide() { | ||||
|     // App 进入后台,保持连接 | ||||
|     console.log('App Hide') | ||||
|   }, | ||||
|    | ||||
|   onShow() { | ||||
|     // App 从后台回到前台 | ||||
|     console.log('App Show') | ||||
|      | ||||
|     // 如果连接已断开,重新连接 | ||||
|     const state = wsManager.getState() | ||||
|     if (!state.isConnected && !state.isConnecting) { | ||||
|       this.connectWebSocket() | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 2. 在聊天页面中使用 | ||||
| 
 | ||||
| #### 方式 A:修改现有的 `dialogBox.vue` | ||||
| 
 | ||||
| ```vue | ||||
| <template> | ||||
|   <view> | ||||
|     <!-- 消息列表 --> | ||||
|     <scroll-view  | ||||
|       :scroll-top="scrollTop"  | ||||
|       :scroll-with-animation="true"  | ||||
|       class="scroll"  | ||||
|       scroll-y="true" | ||||
|     > | ||||
|       <view v-for="(msg, index) in messageList" :key="index" class="message-item"> | ||||
|         <!-- 我方消息 --> | ||||
|         <view v-if="msg.fromUserId == vuex_user.id" class="my-message"> | ||||
|           <text>{{ msg.content }}</text> | ||||
|         </view> | ||||
|         <!-- 对方消息 --> | ||||
|         <view v-else class="other-message"> | ||||
|           <text>{{ msg.content }}</text> | ||||
|         </view> | ||||
|       </view> | ||||
|     </scroll-view> | ||||
|      | ||||
|     <!-- 输入框 --> | ||||
|     <view class="input-box"> | ||||
|       <input  | ||||
|         v-model="inputValue"  | ||||
|         placeholder="请输入消息"  | ||||
|         @confirm="sendMessage" | ||||
|       /> | ||||
|       <button @click="sendMessage">发送</button> | ||||
|     </view> | ||||
|   </view> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import wsManager from '@/utils/websocket-manager.js' | ||||
| import { mapState } from 'vuex' | ||||
| 
 | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       messageList: [], | ||||
|       inputValue: '', | ||||
|       scrollTop: 0, | ||||
|       targetUserId: '', // 对方用户 ID | ||||
|       messageHandler: null | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   computed: { | ||||
|     ...mapState(['vuex_user']) | ||||
|   }, | ||||
|    | ||||
|   onLoad(options) { | ||||
|     // 获取对方用户 ID | ||||
|     this.targetUserId = options.userId || '' | ||||
|      | ||||
|     // 加载历史消息 | ||||
|     this.loadHistoryMessages() | ||||
|      | ||||
|     // 监听新消息 | ||||
|     this.messageHandler = (data) => { | ||||
|       // 只处理与当前聊天对象相关的消息 | ||||
|       if (data.fromUserId === this.targetUserId || data.toUserId === this.targetUserId) { | ||||
|         this.messageList.push({ | ||||
|           fromUserId: data.fromUserId, | ||||
|           toUserId: data.toUserId, | ||||
|           content: data.content, | ||||
|           timestamp: data.timestamp | ||||
|         }) | ||||
|          | ||||
|         // 滚动到底部 | ||||
|         this.$nextTick(() => { | ||||
|           this.scrollToBottom() | ||||
|         }) | ||||
|          | ||||
|         // 发送已读回执 | ||||
|         this.sendReadReceipt(data.messageId) | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     wsManager.on('message', this.messageHandler) | ||||
|   }, | ||||
|    | ||||
|   onUnload() { | ||||
|     // 移除消息监听 | ||||
|     if (this.messageHandler) { | ||||
|       wsManager.off('message', this.messageHandler) | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     // 加载历史消息 | ||||
|     async loadHistoryMessages() { | ||||
|       try { | ||||
|         const res = await this.$u.api.getMessages({ | ||||
|           userId: this.targetUserId, | ||||
|           page: 1, | ||||
|           pageSize: 50 | ||||
|         }) | ||||
|          | ||||
|         this.messageList = res.data || [] | ||||
|          | ||||
|         this.$nextTick(() => { | ||||
|           this.scrollToBottom() | ||||
|         }) | ||||
|       } catch (e) { | ||||
|         console.error('加载历史消息失败:', e) | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|     // 发送消息 | ||||
|     sendMessage() { | ||||
|       if (!this.inputValue.trim()) { | ||||
|         uni.showToast({ | ||||
|           title: '请输入消息', | ||||
|           icon: 'none' | ||||
|         }) | ||||
|         return | ||||
|       } | ||||
|        | ||||
|       // 通过 WebSocket 发送 | ||||
|       wsManager.send({ | ||||
|         type: 'message', | ||||
|         fromUserId: this.vuex_user.id, | ||||
|         toUserId: this.targetUserId, | ||||
|         content: this.inputValue, | ||||
|         timestamp: Date.now() | ||||
|       }) | ||||
|        | ||||
|       // 立即显示到界面(乐观更新) | ||||
|       this.messageList.push({ | ||||
|         fromUserId: this.vuex_user.id, | ||||
|         toUserId: this.targetUserId, | ||||
|         content: this.inputValue, | ||||
|         timestamp: Date.now(), | ||||
|         sending: true // 标记为发送中 | ||||
|       }) | ||||
|        | ||||
|       // 清空输入框 | ||||
|       this.inputValue = '' | ||||
|        | ||||
|       // 滚动到底部 | ||||
|       this.$nextTick(() => { | ||||
|         this.scrollToBottom() | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     // 发送已读回执 | ||||
|     sendReadReceipt(messageId) { | ||||
|       wsManager.send({ | ||||
|         type: 'read', | ||||
|         messageId: messageId, | ||||
|         userId: this.vuex_user.id | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     // 滚动到底部 | ||||
|     scrollToBottom() { | ||||
|       this.scrollTop = 99999 | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 3. 在 Vuex 中管理 WebSocket 状态(可选) | ||||
| 
 | ||||
| ```javascript | ||||
| // store/index.js | ||||
| export default new Vuex.Store({ | ||||
|   state: { | ||||
|     // ... 其他状态 | ||||
|     wsConnected: false, | ||||
|     unreadCount: 0, | ||||
|     messages: [] | ||||
|   }, | ||||
|    | ||||
|   mutations: { | ||||
|     // WebSocket 连接状态 | ||||
|     SET_WS_CONNECTED(state, connected) { | ||||
|       state.wsConnected = connected | ||||
|     }, | ||||
|      | ||||
|     // 新消息 | ||||
|     ADD_MESSAGE(state, message) { | ||||
|       state.messages.push(message) | ||||
|       state.unreadCount++ | ||||
|     }, | ||||
|      | ||||
|     // 清空未读 | ||||
|     CLEAR_UNREAD(state) { | ||||
|       state.unreadCount = 0 | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   actions: { | ||||
|     // 在 App.vue 中调用 | ||||
|     wsConnected({ commit }) { | ||||
|       commit('SET_WS_CONNECTED', true) | ||||
|     }, | ||||
|      | ||||
|     wsDisconnected({ commit }) { | ||||
|       commit('SET_WS_CONNECTED', false) | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 📋 消息协议设计 | ||||
| 
 | ||||
| ### 客户端 → 服务器 | ||||
| 
 | ||||
| #### 1. 发送文字消息 | ||||
| ```json | ||||
| { | ||||
|   "type": "message", | ||||
|   "fromUserId": "user123", | ||||
|   "toUserId": "user456", | ||||
|   "content": "你好", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 2. 发送心跳 | ||||
| ```json | ||||
| { | ||||
|   "type": "ping", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 3. 发送已读回执 | ||||
| ```json | ||||
| { | ||||
|   "type": "read", | ||||
|   "messageId": "msg123", | ||||
|   "userId": "user123" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 服务器 → 客户端 | ||||
| 
 | ||||
| #### 1. 推送消息 | ||||
| ```json | ||||
| { | ||||
|   "type": "message", | ||||
|   "messageId": "msg123", | ||||
|   "fromUserId": "user456", | ||||
|   "toUserId": "user123", | ||||
|   "content": "你好", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 2. 心跳响应 | ||||
| ```json | ||||
| { | ||||
|   "type": "pong", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 3. 系统通知 | ||||
| ```json | ||||
| { | ||||
|   "type": "notification", | ||||
|   "content": "系统维护通知", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 4. 错误消息 | ||||
| ```json | ||||
| { | ||||
|   "type": "error", | ||||
|   "code": 401, | ||||
|   "message": "未授权", | ||||
|   "timestamp": 1635678901234 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🔧 后端实现参考 | ||||
| 
 | ||||
| ### Node.js + ws | ||||
| 
 | ||||
| ```javascript | ||||
| const WebSocket = require('ws') | ||||
| const wss = new WebSocket.Server({ port: 8080 }) | ||||
| 
 | ||||
| // 存储所有连接(用户 ID -> WebSocket) | ||||
| const clients = new Map() | ||||
| 
 | ||||
| wss.on('connection', (ws, req) => { | ||||
|   // 从 URL 获取 token | ||||
|   const token = new URL(req.url, 'http://localhost').searchParams.get('token') | ||||
|    | ||||
|   // 验证 token,获取用户 ID | ||||
|   const userId = verifyToken(token) | ||||
|    | ||||
|   if (!userId) { | ||||
|     ws.close(4001, '未授权') | ||||
|     return | ||||
|   } | ||||
|    | ||||
|   // 保存连接 | ||||
|   clients.set(userId, ws) | ||||
|   console.log(`用户 ${userId} 已连接`) | ||||
|    | ||||
|   // 处理消息 | ||||
|   ws.on('message', (message) => { | ||||
|     const data = JSON.parse(message) | ||||
|      | ||||
|     switch (data.type) { | ||||
|       case 'message': | ||||
|         // 转发消息 | ||||
|         handleMessage(data) | ||||
|         break | ||||
|       case 'ping': | ||||
|         // 心跳响应 | ||||
|         ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })) | ||||
|         break | ||||
|       case 'read': | ||||
|         // 已读回执 | ||||
|         handleReadReceipt(data) | ||||
|         break | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|   // 处理断开 | ||||
|   ws.on('close', () => { | ||||
|     clients.delete(userId) | ||||
|     console.log(`用户 ${userId} 已断开`) | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| // 转发消息 | ||||
| function handleMessage(data) { | ||||
|   const { toUserId, fromUserId, content } = data | ||||
|    | ||||
|   // 保存到数据库 | ||||
|   saveMessage({ fromUserId, toUserId, content }) | ||||
|    | ||||
|   // 推送给接收者 | ||||
|   const targetWs = clients.get(toUserId) | ||||
|   if (targetWs && targetWs.readyState === WebSocket.OPEN) { | ||||
|     targetWs.send(JSON.stringify({ | ||||
|       type: 'message', | ||||
|       messageId: generateId(), | ||||
|       fromUserId, | ||||
|       toUserId, | ||||
|       content, | ||||
|       timestamp: Date.now() | ||||
|     })) | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 📊 完整流程图 | ||||
| 
 | ||||
| ``` | ||||
| ┌─────────────┐ | ||||
| │  App 启动    │ | ||||
| └──────┬──────┘ | ||||
|        │ | ||||
|        ▼ | ||||
| ┌─────────────┐ | ||||
| │ 检查登录状态 │ | ||||
| └──────┬──────┘ | ||||
|        │ | ||||
|        ▼ (已登录) | ||||
| ┌─────────────┐ | ||||
| │ 连接 WS     │ ← wsManager.connect() | ||||
| └──────┬──────┘ | ||||
|        │ | ||||
|        ▼ | ||||
| ┌─────────────┐ | ||||
| │ 监听消息     │ ← wsManager.on('message') | ||||
| └──────┬──────┘ | ||||
|        │ | ||||
|        ├─→ (收到消息) → 更新 UI | ||||
|        │ | ||||
|        └─→ (发送消息) → wsManager.send() | ||||
|                            │ | ||||
|                            ▼ | ||||
|                        ┌─────────────┐ | ||||
|                        │  服务器处理  │ | ||||
|                        └──────┬──────┘ | ||||
|                               │ | ||||
|                               ▼ | ||||
|                        ┌─────────────┐ | ||||
|                        │  推送给对方  │ | ||||
|                        └─────────────┘ | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## ✅ 测试清单 | ||||
| 
 | ||||
| ### 功能测试 | ||||
| - [ ] 连接成功 | ||||
| - [ ] 发送消息 | ||||
| - [ ] 接收消息 | ||||
| - [ ] 心跳正常 | ||||
| - [ ] 手动断开 | ||||
| - [ ] 自动重连 | ||||
| - [ ] 消息队列 | ||||
| 
 | ||||
| ### 场景测试 | ||||
| - [ ] App 切换到后台 | ||||
| - [ ] App 从后台恢复 | ||||
| - [ ] 网络断开 | ||||
| - [ ] 网络恢复 | ||||
| - [ ] 服务器重启 | ||||
| - [ ] 并发多人聊天 | ||||
| 
 | ||||
| ### 性能测试 | ||||
| - [ ] 消息延迟 < 200ms | ||||
| - [ ] 重连时间 < 5s | ||||
| - [ ] 内存占用正常 | ||||
| - [ ] CPU 占用正常 | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🎉 总结 | ||||
| 
 | ||||
| 现在你已经有: | ||||
| 
 | ||||
| 1. ✅ **WebSocket 管理器** (`utils/websocket-manager.js`) | ||||
|    - 自动重连 | ||||
|    - 心跳检测 | ||||
|    - 消息队列 | ||||
|    - 事件监听 | ||||
| 
 | ||||
| 2. ✅ **使用示例** | ||||
|    - App.vue 初始化 | ||||
|    - 聊天页面使用 | ||||
|    - Vuex 状态管理 | ||||
| 
 | ||||
| 3. ✅ **消息协议** | ||||
|    - 客户端 → 服务器 | ||||
|    - 服务器 → 客户端 | ||||
| 
 | ||||
| 4. ✅ **后端参考** | ||||
|    - Node.js + ws 实现 | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🚀 下一步 | ||||
| 
 | ||||
| 1. **确认后端技术栈** | ||||
|    - 告诉我你用什么后端,我帮你写完整的服务端代码 | ||||
| 
 | ||||
| 2. **集成到现有聊天页面** | ||||
|    - 我帮你修改 `dialogBox.vue` | ||||
| 
 | ||||
| 3. **添加更多功能** | ||||
|    - 图片消息 | ||||
|    - 语音消息 | ||||
|    - 消息已读 | ||||
|    - 输入中状态 | ||||
| 
 | ||||
| 随时告诉我需要什么!🎯 | ||||
| 
 | ||||
|  | @ -1,616 +0,0 @@ | |||
| # WebSocket 实施计划 | ||||
| 
 | ||||
| ## 🎯 项目信息 | ||||
| 
 | ||||
| - **项目名称**:英星AI - 浙江科技大学招生咨询系统 | ||||
| - **技术栈**:uni-app + Vue 2 | ||||
| - **后端**:ASP.NET Core(推测) | ||||
| - **方案选择**:WebSocket 原生方案 ✅ | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 📋 实施步骤 | ||||
| 
 | ||||
| ### 阶段一:前端准备(已完成 ✅) | ||||
| 
 | ||||
| #### 1. WebSocket 管理器 | ||||
| - ✅ 文件:`utils/websocket-manager.js` | ||||
| - ✅ 功能:自动重连、心跳检测、消息队列、事件监听 | ||||
| 
 | ||||
| #### 2. WebSocket 配置 | ||||
| - ✅ 文件:`config/websocket.config.js` | ||||
| - ✅ 功能:集中管理 WebSocket 配置 | ||||
| 
 | ||||
| #### 3. 使用文档 | ||||
| - ✅ 文件:`docs/WebSocket使用示例.md` | ||||
| - ✅ 文件:`docs/实时通讯方案对比.md` | ||||
| - ✅ 文件:`docs/实时通讯快速选型.md` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 阶段二:后端开发(待实施 ⏳) | ||||
| 
 | ||||
| #### 选项 A:ASP.NET Core WebSocket 服务(推荐) | ||||
| 
 | ||||
| **时间**:3-5 天 | ||||
| 
 | ||||
| **工作量:** | ||||
| 1. 创建 WebSocket 中间件(1 天) | ||||
| 2. 实现消息路由(1 天) | ||||
| 3. 集成用户认证(1 天) | ||||
| 4. 消息持久化(1 天) | ||||
| 5. 测试调试(1 天) | ||||
| 
 | ||||
| **文件清单:** | ||||
| - `WebSocketMiddleware.cs` - WebSocket 中间件 | ||||
| - `WebSocketConnectionManager.cs` - 连接管理器 | ||||
| - `WebSocketMessageHandler.cs` - 消息处理器 | ||||
| - `ChatController.cs` - HTTP API(获取历史消息) | ||||
| - `MessageRepository.cs` - 消息存储 | ||||
| 
 | ||||
| **后端代码**:见下文 `.NET Core WebSocket 服务端代码` | ||||
| 
 | ||||
| #### 选项 B:使用现有 SignalR(备选) | ||||
| 
 | ||||
| 你的项目已经有 SignalR: | ||||
| ```javascript | ||||
| // main.js | ||||
| var connection = new HubConnectionBuilder() | ||||
|   .withUrl("http://sl.vrgon.com:8003/ChatHub") | ||||
|   .build(); | ||||
| ``` | ||||
| 
 | ||||
| **建议:** | ||||
| - 如果 SignalR 服务正常,可以继续使用 | ||||
| - 如果需要切换到原生 WebSocket,按选项 A 实施 | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 阶段三:前端集成(2-3 天) | ||||
| 
 | ||||
| #### 1. 在 App.vue 初始化 WebSocket | ||||
| 
 | ||||
| **文件**:`App.vue` | ||||
| 
 | ||||
| ```javascript | ||||
| import wsManager from '@/utils/websocket-manager.js' | ||||
| import wsConfig from '@/config/websocket.config.js' | ||||
| 
 | ||||
| export default { | ||||
|   onLaunch() { | ||||
|     // 登录后连接 WebSocket | ||||
|     if (this.vuex_token) { | ||||
|       this.connectWebSocket() | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     connectWebSocket() { | ||||
|       // 构建 WebSocket URL(带 token) | ||||
|       const wsUrl = `${wsConfig.url}?token=${this.vuex_token}` | ||||
|        | ||||
|       // 连接 | ||||
|       wsManager.connect(wsUrl, { | ||||
|         reconnectInterval: wsConfig.reconnect.interval, | ||||
|         maxReconnectAttempts: wsConfig.reconnect.maxAttempts, | ||||
|         heartbeatInterval: wsConfig.heartbeat.interval | ||||
|       }) | ||||
|        | ||||
|       // 监听事件 | ||||
|       this.setupWebSocketListeners() | ||||
|     }, | ||||
|      | ||||
|     setupWebSocketListeners() { | ||||
|       // 连接成功 | ||||
|       wsManager.on('open', () => { | ||||
|         console.log('[App] WebSocket 已连接') | ||||
|       }) | ||||
|        | ||||
|       // 收到消息 | ||||
|       wsManager.on('message', (data) => { | ||||
|         this.handleWebSocketMessage(data) | ||||
|       }) | ||||
|        | ||||
|       // 连接关闭 | ||||
|       wsManager.on('close', () => { | ||||
|         console.log('[App] WebSocket 已断开') | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     handleWebSocketMessage(data) { | ||||
|       // 根据消息类型处理 | ||||
|       switch (data.type) { | ||||
|         case 'message': | ||||
|           // 新消息提醒 | ||||
|           this.$store.commit('addMessage', data) | ||||
|           break | ||||
|         case 'system': | ||||
|           // 系统通知 | ||||
|           uni.showToast({ title: data.content, icon: 'none' }) | ||||
|           break | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 2. 修改聊天页面 | ||||
| 
 | ||||
| **文件**:`pages/message/dialogBox/dialogBox.vue` | ||||
| 
 | ||||
| 需要: | ||||
| 1. 导入 WebSocket 管理器 | ||||
| 2. 监听实时消息 | ||||
| 3. 发送消息通过 WebSocket | ||||
| 4. 加载历史消息通过 HTTP API | ||||
| 
 | ||||
| **代码示例**:见 `docs/WebSocket使用示例.md` | ||||
| 
 | ||||
| #### 3. 添加聊天相关 API | ||||
| 
 | ||||
| **文件**:`common/http.api.js` | ||||
| 
 | ||||
| ```javascript | ||||
| // 获取聊天历史记录 | ||||
| let getChatHistory = (params = {}) => vm.$u.get('api/Chat/GetHistory', params); | ||||
| 
 | ||||
| // 发送离线消息(WebSocket 断开时使用) | ||||
| let sendOfflineMessage = (params = {}) => vm.$u.post('api/Chat/SendOffline', params); | ||||
| 
 | ||||
| // 标记消息已读 | ||||
| let markMessageRead = (params = {}) => vm.$u.post('api/Chat/MarkRead', params); | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ### 阶段四:测试验证(2-3 天) | ||||
| 
 | ||||
| #### 1. 功能测试 | ||||
| 
 | ||||
| - [ ] 连接建立 | ||||
| - [ ] 发送文字消息 | ||||
| - [ ] 接收文字消息 | ||||
| - [ ] 心跳保持 | ||||
| - [ ] 自动重连 | ||||
| - [ ] 离线消息推送 | ||||
| - [ ] 消息已读回执 | ||||
| 
 | ||||
| #### 2. 场景测试 | ||||
| 
 | ||||
| - [ ] 单人聊天 | ||||
| - [ ] 多人并发 | ||||
| - [ ] 网络断开 | ||||
| - [ ] 网络恢复 | ||||
| - [ ] App 切换后台 | ||||
| - [ ] App 恢复前台 | ||||
| - [ ] 登录/登出 | ||||
| 
 | ||||
| #### 3. 性能测试 | ||||
| 
 | ||||
| - [ ] 消息延迟 < 200ms | ||||
| - [ ] 重连时间 < 5s | ||||
| - [ ] 内存占用正常 | ||||
| - [ ] CPU 占用正常 | ||||
| - [ ] 100+ 并发用户 | ||||
| 
 | ||||
| #### 4. 兼容性测试 | ||||
| 
 | ||||
| - [ ] H5(Chrome、Safari) | ||||
| - [ ] 微信小程序 | ||||
| - [ ] Android App | ||||
| - [ ] iOS App | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🔧 后端实现 | ||||
| 
 | ||||
| ### .NET Core WebSocket 服务端代码 | ||||
| 
 | ||||
| #### 1. WebSocket 中间件 | ||||
| 
 | ||||
| **文件**:`WebSocketMiddleware.cs` | ||||
| 
 | ||||
| ```csharp | ||||
| using System; | ||||
| using System.Net.WebSockets; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Newtonsoft.Json; | ||||
| 
 | ||||
| public class WebSocketMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly WebSocketConnectionManager _connectionManager; | ||||
| 
 | ||||
|     public WebSocketMiddleware(RequestDelegate next, WebSocketConnectionManager connectionManager) | ||||
|     { | ||||
|         _next = next; | ||||
|         _connectionManager = connectionManager; | ||||
|     } | ||||
| 
 | ||||
|     public async Task InvokeAsync(HttpContext context) | ||||
|     { | ||||
|         // 检查是否是 WebSocket 请求 | ||||
|         if (!context.WebSockets.IsWebSocketRequest) | ||||
|         { | ||||
|             await _next(context); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // 验证 token | ||||
|         var token = context.Request.Query["token"].ToString(); | ||||
|         var userId = ValidateToken(token); | ||||
|          | ||||
|         if (string.IsNullOrEmpty(userId)) | ||||
|         { | ||||
|             context.Response.StatusCode = 401; | ||||
|             await context.Response.WriteAsync("Unauthorized"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // 接受 WebSocket 连接 | ||||
|         var webSocket = await context.WebSockets.AcceptWebSocketAsync(); | ||||
|          | ||||
|         // 保存连接 | ||||
|         _connectionManager.AddConnection(userId, webSocket); | ||||
|          | ||||
|         Console.WriteLine($"[WebSocket] 用户 {userId} 已连接"); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             await HandleWebSocketAsync(userId, webSocket); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             // 移除连接 | ||||
|             _connectionManager.RemoveConnection(userId); | ||||
|             Console.WriteLine($"[WebSocket] 用户 {userId} 已断开"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async Task HandleWebSocketAsync(string userId, WebSocket webSocket) | ||||
|     { | ||||
|         var buffer = new byte[1024 * 4]; | ||||
| 
 | ||||
|         while (webSocket.State == WebSocketState.Open) | ||||
|         { | ||||
|             var result = await webSocket.ReceiveAsync( | ||||
|                 new ArraySegment<byte>(buffer),  | ||||
|                 CancellationToken.None | ||||
|             ); | ||||
| 
 | ||||
|             if (result.MessageType == WebSocketMessageType.Close) | ||||
|             { | ||||
|                 await webSocket.CloseAsync( | ||||
|                     WebSocketCloseStatus.NormalClosure, | ||||
|                     "关闭连接", | ||||
|                     CancellationToken.None | ||||
|                 ); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             // 解析消息 | ||||
|             var message = Encoding.UTF8.GetString(buffer, 0, result.Count); | ||||
|             await HandleMessageAsync(userId, message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async Task HandleMessageAsync(string userId, string message) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var data = JsonConvert.DeserializeObject<WebSocketMessage>(message); | ||||
| 
 | ||||
|             switch (data.Type) | ||||
|             { | ||||
|                 case "message": | ||||
|                     // 处理聊天消息 | ||||
|                     await HandleChatMessageAsync(userId, data); | ||||
|                     break; | ||||
| 
 | ||||
|                 case "ping": | ||||
|                     // 处理心跳 | ||||
|                     await SendToUserAsync(userId, new | ||||
|                     { | ||||
|                         type = "pong", | ||||
|                         timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() | ||||
|                     }); | ||||
|                     break; | ||||
| 
 | ||||
|                 case "read": | ||||
|                     // 处理已读回执 | ||||
|                     await HandleReadReceiptAsync(userId, data); | ||||
|                     break; | ||||
| 
 | ||||
|                 default: | ||||
|                     Console.WriteLine($"未知消息类型: {data.Type}"); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Console.WriteLine($"消息处理失败: {ex.Message}"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async Task HandleChatMessageAsync(string fromUserId, WebSocketMessage data) | ||||
|     { | ||||
|         var toUserId = data.ToUserId; | ||||
|         var content = data.Content; | ||||
| 
 | ||||
|         // 保存消息到数据库 | ||||
|         var messageId = await SaveMessageAsync(fromUserId, toUserId, content); | ||||
| 
 | ||||
|         // 构建消息对象 | ||||
|         var messageObj = new | ||||
|         { | ||||
|             type = "message", | ||||
|             messageId = messageId, | ||||
|             fromUserId = fromUserId, | ||||
|             toUserId = toUserId, | ||||
|             content = content, | ||||
|             timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() | ||||
|         }; | ||||
| 
 | ||||
|         // 发送给接收者 | ||||
|         await SendToUserAsync(toUserId, messageObj); | ||||
| 
 | ||||
|         // 发送给发送者(确认消息已发送) | ||||
|         await SendToUserAsync(fromUserId, messageObj); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<string> SaveMessageAsync(string fromUserId, string toUserId, string content) | ||||
|     { | ||||
|         // TODO: 保存消息到数据库 | ||||
|         // 返回消息 ID | ||||
|         return Guid.NewGuid().ToString(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task HandleReadReceiptAsync(string userId, WebSocketMessage data) | ||||
|     { | ||||
|         // TODO: 更新消息已读状态 | ||||
|         Console.WriteLine($"用户 {userId} 已读消息 {data.MessageId}"); | ||||
|     } | ||||
| 
 | ||||
|     private async Task SendToUserAsync(string userId, object message) | ||||
|     { | ||||
|         var webSocket = _connectionManager.GetConnection(userId); | ||||
|         if (webSocket != null && webSocket.State == WebSocketState.Open) | ||||
|         { | ||||
|             var json = JsonConvert.SerializeObject(message); | ||||
|             var bytes = Encoding.UTF8.GetBytes(json); | ||||
|             await webSocket.SendAsync( | ||||
|                 new ArraySegment<byte>(bytes), | ||||
|                 WebSocketMessageType.Text, | ||||
|                 true, | ||||
|                 CancellationToken.None | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private string ValidateToken(string token) | ||||
|     { | ||||
|         // TODO: 验证 JWT token,返回用户 ID | ||||
|         // 这里简化处理,直接返回一个用户 ID | ||||
|         return "user123"; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 消息实体 | ||||
| public class WebSocketMessage | ||||
| { | ||||
|     public string Type { get; set; } | ||||
|     public string FromUserId { get; set; } | ||||
|     public string ToUserId { get; set; } | ||||
|     public string Content { get; set; } | ||||
|     public string MessageId { get; set; } | ||||
|     public long Timestamp { get; set; } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 2. 连接管理器 | ||||
| 
 | ||||
| **文件**:`WebSocketConnectionManager.cs` | ||||
| 
 | ||||
| ```csharp | ||||
| using System.Collections.Concurrent; | ||||
| using System.Net.WebSockets; | ||||
| 
 | ||||
| public class WebSocketConnectionManager | ||||
| { | ||||
|     // 存储所有连接(用户 ID -> WebSocket) | ||||
|     private readonly ConcurrentDictionary<string, WebSocket> _connections =  | ||||
|         new ConcurrentDictionary<string, WebSocket>(); | ||||
| 
 | ||||
|     // 添加连接 | ||||
|     public void AddConnection(string userId, WebSocket webSocket) | ||||
|     { | ||||
|         _connections.TryAdd(userId, webSocket); | ||||
|     } | ||||
| 
 | ||||
|     // 移除连接 | ||||
|     public void RemoveConnection(string userId) | ||||
|     { | ||||
|         _connections.TryRemove(userId, out _); | ||||
|     } | ||||
| 
 | ||||
|     // 获取连接 | ||||
|     public WebSocket GetConnection(string userId) | ||||
|     { | ||||
|         _connections.TryGetValue(userId, out var webSocket); | ||||
|         return webSocket; | ||||
|     } | ||||
| 
 | ||||
|     // 获取所有在线用户 | ||||
|     public IEnumerable<string> GetOnlineUsers() | ||||
|     { | ||||
|         return _connections.Keys; | ||||
|     } | ||||
| 
 | ||||
|     // 获取在线用户数 | ||||
|     public int GetOnlineCount() | ||||
|     { | ||||
|         return _connections.Count; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 3. 注册中间件 | ||||
| 
 | ||||
| **文件**:`Startup.cs` | ||||
| 
 | ||||
| ```csharp | ||||
| public class Startup | ||||
| { | ||||
|     public void ConfigureServices(IServiceCollection services) | ||||
|     { | ||||
|         // 注册连接管理器为单例 | ||||
|         services.AddSingleton<WebSocketConnectionManager>(); | ||||
|          | ||||
|         // 其他服务... | ||||
|     } | ||||
| 
 | ||||
|     public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | ||||
|     { | ||||
|         // 启用 WebSocket | ||||
|         app.UseWebSockets(new WebSocketOptions | ||||
|         { | ||||
|             KeepAliveInterval = TimeSpan.FromSeconds(30) | ||||
|         }); | ||||
| 
 | ||||
|         // 注册 WebSocket 中间件 | ||||
|         app.Map("/ws/chat", builder => | ||||
|         { | ||||
|             builder.UseMiddleware<WebSocketMiddleware>(); | ||||
|         }); | ||||
| 
 | ||||
|         // 其他中间件... | ||||
|         app.UseRouting(); | ||||
|         app.UseAuthentication(); | ||||
|         app.UseAuthorization(); | ||||
|         app.UseEndpoints(endpoints => | ||||
|         { | ||||
|             endpoints.MapControllers(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### 4. HTTP API(获取历史消息) | ||||
| 
 | ||||
| **文件**:`ChatController.cs` | ||||
| 
 | ||||
| ```csharp | ||||
| [ApiController] | ||||
| [Route("api/[controller]")] | ||||
| public class ChatController : ControllerBase | ||||
| { | ||||
|     // 获取聊天历史 | ||||
|     [HttpGet("GetHistory")] | ||||
|     public async Task<IActionResult> GetHistory(string userId, int page = 1, int pageSize = 50) | ||||
|     { | ||||
|         // TODO: 从数据库查询历史消息 | ||||
|         var messages = new List<object> | ||||
|         { | ||||
|             new | ||||
|             { | ||||
|                 messageId = "msg001", | ||||
|                 fromUserId = "user123", | ||||
|                 toUserId = userId, | ||||
|                 content = "你好", | ||||
|                 timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         return Ok(new | ||||
|         { | ||||
|             success = true, | ||||
|             data = messages, | ||||
|             total = messages.Count | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // 发送离线消息 | ||||
|     [HttpPost("SendOffline")] | ||||
|     public async Task<IActionResult> SendOffline([FromBody] SendMessageRequest request) | ||||
|     { | ||||
|         // TODO: 保存离线消息到数据库 | ||||
|         return Ok(new { success = true }); | ||||
|     } | ||||
| 
 | ||||
|     // 标记已读 | ||||
|     [HttpPost("MarkRead")] | ||||
|     public async Task<IActionResult> MarkRead([FromBody] MarkReadRequest request) | ||||
|     { | ||||
|         // TODO: 更新消息已读状态 | ||||
|         return Ok(new { success = true }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class SendMessageRequest | ||||
| { | ||||
|     public string ToUserId { get; set; } | ||||
|     public string Content { get; set; } | ||||
| } | ||||
| 
 | ||||
| public class MarkReadRequest | ||||
| { | ||||
|     public string MessageId { get; set; } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 📊 时间安排 | ||||
| 
 | ||||
| | 阶段 | 工作内容 | 预计时间 | 负责人 | | ||||
| |------|----------|----------|--------| | ||||
| | **阶段一** | 前端准备 | ✅ 已完成 | 前端 | | ||||
| | **阶段二** | 后端开发 | 3-5 天 | 后端 | | ||||
| | **阶段三** | 前端集成 | 2-3 天 | 前端 | | ||||
| | **阶段四** | 测试验证 | 2-3 天 | 全员 | | ||||
| | **总计** | | **7-11 天** | | | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 🎯 下一步行动 | ||||
| 
 | ||||
| ### 立即可做(前端) | ||||
| 
 | ||||
| 1. **在 App.vue 中初始化 WebSocket** | ||||
|    - 导入 `websocket-manager.js` | ||||
|    - 在 `onLaunch` 中调用 `wsManager.connect()` | ||||
|    - 设置事件监听 | ||||
| 
 | ||||
| 2. **测试 WebSocket 管理器** | ||||
|    - 使用在线 WebSocket 测试服务(如 `wss://echo.websocket.org`) | ||||
|    - 验证连接、发送、接收功能 | ||||
| 
 | ||||
| ### 需要后端配合 | ||||
| 
 | ||||
| 1. **确认后端技术栈** | ||||
|    - 是否是 ASP.NET Core? | ||||
|    - 后端开发人员是谁? | ||||
| 
 | ||||
| 2. **部署 WebSocket 服务** | ||||
|    - 使用上面提供的 .NET Core 代码 | ||||
|    - 或者告诉我你的后端技术,我提供对应代码 | ||||
| 
 | ||||
| 3. **配置服务器** | ||||
|    - 确保服务器支持 WebSocket | ||||
|    - 配置 Nginx 反向代理(如果需要) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## 📞 联系与支持 | ||||
| 
 | ||||
| 如果你需要: | ||||
| 1. ✅ 其他后端语言的 WebSocket 服务端代码(Node.js/Java/Go/Python) | ||||
| 2. ✅ 帮助修改 `dialogBox.vue` 集成 WebSocket | ||||
| 3. ✅ Nginx 配置 WebSocket 反向代理 | ||||
| 4. ✅ 调试和测试支持 | ||||
| 
 | ||||
| 随时告诉我!🚀 | ||||
| 
 | ||||
							
								
								
									
										33
									
								
								pages.json
								
								
								
								
							
							
						
						
									
										33
									
								
								pages.json
								
								
								
								
							|  | @ -11,15 +11,30 @@ | |||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "pages": [ | ||||
|     { | ||||
|       "path": "pages/home/index/index", | ||||
|       "style": { | ||||
|         "navigationBarTitleText": "首页", | ||||
|         "enablePullDownRefresh": false, | ||||
|         "navigationStyle": "custom" | ||||
|       } | ||||
|     }, | ||||
| 	"pages": [ | ||||
| 		{ | ||||
| 			"path": "pages/home/index/index", | ||||
| 			"style": { | ||||
| 				"navigationBarTitleText": "首页", | ||||
| 				"enablePullDownRefresh": false, | ||||
| 				"navigationStyle": "custom" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			"path": "pages/test/websocket-test", | ||||
| 			"style": { | ||||
| 				"navigationBarTitleText": "WebSocket 测试", | ||||
| 				"enablePullDownRefresh": false | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			"path": "pages/chat/chat-detail", | ||||
| 			"style": { | ||||
| 				"navigationBarTitleText": "聊天", | ||||
| 				"navigationStyle": "custom", | ||||
| 				"enablePullDownRefresh": false | ||||
| 			} | ||||
| 		}, | ||||
|     { | ||||
|       "path": "pages/home/messageBoard/index", | ||||
|       "style": { | ||||
|  |  | |||
|  | @ -0,0 +1,379 @@ | |||
| <template> | ||||
|   <view class="chat-detail-page"> | ||||
|     <!-- 顶部导航 --> | ||||
|     <view class="chat-header"> | ||||
|       <view class="header-left" @click="goBack"> | ||||
|         <u-icon name="arrow-left" color="#333" size="20"></u-icon> | ||||
|       </view> | ||||
|       <view class="header-title">{{ userName }}</view> | ||||
|       <view class="header-right" @click="showUserInfo"> | ||||
|         <u-icon name="more-dot-fill" color="#333" size="20"></u-icon> | ||||
|       </view> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 消息列表 --> | ||||
|     <scroll-view | ||||
|       class="message-list" | ||||
|       scroll-y | ||||
|       :scroll-into-view="scrollIntoView" | ||||
|       scroll-with-animation | ||||
|     > | ||||
|       <view | ||||
|         v-for="(msg, index) in messageList" | ||||
|         :key="msg.id" | ||||
|         :id="'msg-' + msg.id" | ||||
|         class="message-item" | ||||
|       > | ||||
|         <!-- 我发送的消息 --> | ||||
|         <view v-if="msg.isSelf" class="message-self"> | ||||
|           <view class="message-content-wrapper"> | ||||
|             <view class="message-content">{{ msg.content }}</view> | ||||
|           </view> | ||||
|           <image class="message-avatar" :src="myAvatar"></image> | ||||
|         </view> | ||||
| 
 | ||||
|         <!-- 对方发送的消息 --> | ||||
|         <view v-else class="message-other"> | ||||
|           <image class="message-avatar" :src="otherAvatar"></image> | ||||
|           <view class="message-content-wrapper"> | ||||
|             <view class="message-content">{{ msg.content }}</view> | ||||
|           </view> | ||||
|         </view> | ||||
|       </view> | ||||
| 
 | ||||
|       <!-- 空状态 --> | ||||
|       <view v-if="messageList.length === 0" class="empty-message"> | ||||
|         <text>暂无消息</text> | ||||
|       </view> | ||||
|     </scroll-view> | ||||
| 
 | ||||
|     <!-- 输入栏 --> | ||||
|     <view class="input-bar"> | ||||
|       <view class="input-wrapper"> | ||||
|         <input | ||||
|           class="input-field" | ||||
|           v-model="inputValue" | ||||
|           placeholder="请输入消息..." | ||||
|           confirm-type="send" | ||||
|           @confirm="sendMessage" | ||||
|         /> | ||||
|       </view> | ||||
|       <view class="send-button" @click="sendMessage"> | ||||
|         <text>发送</text> | ||||
|       </view> | ||||
|     </view> | ||||
|   </view> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'ChatDetail', | ||||
|   data() { | ||||
|     return { | ||||
|       // 用户信息 | ||||
|       userId: '', | ||||
|       userName: '', | ||||
|        | ||||
|       // 头像 | ||||
|       myAvatar: '/static/avatar/default-avatar.png', | ||||
|       otherAvatar: '/static/avatar/default-avatar.png', | ||||
|        | ||||
|       // 消息列表 | ||||
|       messageList: [ | ||||
|         { | ||||
|           id: 1, | ||||
|           content: '你好,请问学校的招生政策是怎样的?', | ||||
|           isSelf: false, | ||||
|           timestamp: '2024-01-15 09:45' | ||||
|         }, | ||||
|         { | ||||
|           id: 2, | ||||
|           content: '你好!我们学校今年的招生政策主要包括...', | ||||
|           isSelf: true, | ||||
|           timestamp: '2024-01-15 09:46' | ||||
|         }, | ||||
|         { | ||||
|           id: 3, | ||||
|           content: '好的,谢谢老师!那请问学费是多少呢?', | ||||
|           isSelf: false, | ||||
|           timestamp: '2024-01-15 09:47' | ||||
|         }, | ||||
|       ], | ||||
|        | ||||
|       // 输入框 | ||||
|       inputValue: '', | ||||
|        | ||||
|       // 滚动位置 | ||||
|       scrollIntoView: '', | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   onLoad(options) { | ||||
|     // 获取传入的用户信息 | ||||
|     this.userId = options.userId || '' | ||||
|     this.userName = options.name || '用户' | ||||
|      | ||||
|     console.log('[聊天详情] 用户ID:', this.userId) | ||||
|     console.log('[聊天详情] 用户名:', this.userName) | ||||
|      | ||||
|     // 加载历史消息 | ||||
|     this.loadHistoryMessages() | ||||
|      | ||||
|     // 滚动到底部 | ||||
|     this.$nextTick(() => { | ||||
|       this.scrollToBottom() | ||||
|     }) | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     // 返回 | ||||
|     goBack() { | ||||
|       uni.navigateBack() | ||||
|     }, | ||||
|      | ||||
|     // 显示用户信息 | ||||
|     showUserInfo() { | ||||
|       uni.showToast({ | ||||
|         title: '用户信息', | ||||
|         icon: 'none' | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     // 发送消息 | ||||
|     sendMessage() { | ||||
|       if (!this.inputValue.trim()) { | ||||
|         uni.showToast({ | ||||
|           title: '请输入消息内容', | ||||
|           icon: 'none' | ||||
|         }) | ||||
|         return | ||||
|       } | ||||
|        | ||||
|       // 构建消息对象 | ||||
|       const message = { | ||||
|         id: Date.now(), | ||||
|         content: this.inputValue, | ||||
|         isSelf: true, | ||||
|         timestamp: this.formatTime(new Date()) | ||||
|       } | ||||
|        | ||||
|       // 添加到消息列表 | ||||
|       this.messageList.push(message) | ||||
|        | ||||
|       // 清空输入框 | ||||
|       this.inputValue = '' | ||||
|        | ||||
|       // 滚动到底部 | ||||
|       this.$nextTick(() => { | ||||
|         this.scrollToBottom() | ||||
|       }) | ||||
|        | ||||
|       // TODO: 通过 WebSocket 发送消息 | ||||
|       // wsManager.send({ | ||||
|       //   type: 'message', | ||||
|       //   toUserId: this.userId, | ||||
|       //   content: message.content | ||||
|       // }) | ||||
|        | ||||
|       console.log('[聊天详情] 发送消息:', message) | ||||
|        | ||||
|       // 模拟对方回复 | ||||
|       setTimeout(() => { | ||||
|         this.receiveMessage('收到,我马上处理') | ||||
|       }, 1000) | ||||
|     }, | ||||
|      | ||||
|     // 接收消息 | ||||
|     receiveMessage(content) { | ||||
|       const message = { | ||||
|         id: Date.now(), | ||||
|         content: content, | ||||
|         isSelf: false, | ||||
|         timestamp: this.formatTime(new Date()) | ||||
|       } | ||||
|        | ||||
|       this.messageList.push(message) | ||||
|        | ||||
|       this.$nextTick(() => { | ||||
|         this.scrollToBottom() | ||||
|       }) | ||||
|     }, | ||||
|      | ||||
|     // 加载历史消息 | ||||
|     loadHistoryMessages() { | ||||
|       // TODO: 从服务器加载历史消息 | ||||
|       // this.$u.api.getChatHistory({ | ||||
|       //   userId: this.userId | ||||
|       // }).then(res => { | ||||
|       //   this.messageList = res.data | ||||
|       // }) | ||||
|        | ||||
|       console.log('[聊天详情] 加载历史消息') | ||||
|     }, | ||||
|      | ||||
|     // 滚动到底部 | ||||
|     scrollToBottom() { | ||||
|       if (this.messageList.length > 0) { | ||||
|         const lastMsg = this.messageList[this.messageList.length - 1] | ||||
|         this.scrollIntoView = 'msg-' + lastMsg.id | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|     // 格式化时间 | ||||
|     formatTime(date) { | ||||
|       const hours = date.getHours().toString().padStart(2, '0') | ||||
|       const minutes = date.getMinutes().toString().padStart(2, '0') | ||||
|       return `${hours}:${minutes}` | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .chat-detail-page { | ||||
|   height: 100vh; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   background-color: #f5f6fa; | ||||
| } | ||||
| 
 | ||||
| /* ===== 顶部导航 ===== */ | ||||
| .chat-header { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: space-between; | ||||
|   height: 44px; | ||||
|   padding: 0 15px; | ||||
|   background: #fff; | ||||
|   border-bottom: 1rpx solid #e5e5e5; | ||||
| } | ||||
| 
 | ||||
| .header-left, | ||||
| .header-right { | ||||
|   width: 44px; | ||||
|   height: 44px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
| 
 | ||||
| .header-title { | ||||
|   flex: 1; | ||||
|   text-align: center; | ||||
|   font-size: 17px; | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
| } | ||||
| 
 | ||||
| /* ===== 消息列表 ===== */ | ||||
| .message-list { | ||||
|   flex: 1; | ||||
|   padding: 15px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
| 
 | ||||
| .message-item { | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| /* 我发送的消息 */ | ||||
| .message-self { | ||||
|   display: flex; | ||||
|   justify-content: flex-end; | ||||
|   align-items: flex-start; | ||||
| } | ||||
| 
 | ||||
| .message-self .message-content-wrapper { | ||||
|   max-width: 70%; | ||||
|   margin-right: 10px; | ||||
| } | ||||
| 
 | ||||
| .message-self .message-content { | ||||
|   background: #4a6cf7; | ||||
|   color: #fff; | ||||
|   padding: 12px 15px; | ||||
|   border-radius: 8px; | ||||
|   font-size: 15px; | ||||
|   line-height: 1.5; | ||||
|   word-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| /* 对方发送的消息 */ | ||||
| .message-other { | ||||
|   display: flex; | ||||
|   justify-content: flex-start; | ||||
|   align-items: flex-start; | ||||
| } | ||||
| 
 | ||||
| .message-other .message-content-wrapper { | ||||
|   max-width: 70%; | ||||
|   margin-left: 10px; | ||||
| } | ||||
| 
 | ||||
| .message-other .message-content { | ||||
|   background: #fff; | ||||
|   color: #333; | ||||
|   padding: 12px 15px; | ||||
|   border-radius: 8px; | ||||
|   font-size: 15px; | ||||
|   line-height: 1.5; | ||||
|   word-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| /* 头像 */ | ||||
| .message-avatar { | ||||
|   width: 40px; | ||||
|   height: 40px; | ||||
|   border-radius: 8px; | ||||
|   background-color: #e8e8e8; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
| 
 | ||||
| /* 空状态 */ | ||||
| .empty-message { | ||||
|   text-align: center; | ||||
|   padding: 100px 20px; | ||||
|   color: #999; | ||||
|   font-size: 14px; | ||||
| } | ||||
| 
 | ||||
| /* ===== 输入栏 ===== */ | ||||
| .input-bar { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 10px 15px; | ||||
|   background: #fff; | ||||
|   border-top: 1rpx solid #e5e5e5; | ||||
|   padding-bottom: calc(10px + env(safe-area-inset-bottom)); | ||||
| } | ||||
| 
 | ||||
| .input-wrapper { | ||||
|   flex: 1; | ||||
|   margin-right: 10px; | ||||
| } | ||||
| 
 | ||||
| .input-field { | ||||
|   height: 36px; | ||||
|   padding: 0 15px; | ||||
|   background: #f5f5f5; | ||||
|   border-radius: 18px; | ||||
|   font-size: 15px; | ||||
| } | ||||
| 
 | ||||
| .send-button { | ||||
|   width: 60px; | ||||
|   height: 36px; | ||||
|   background: #4a6cf7; | ||||
|   border-radius: 18px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   color: #fff; | ||||
|   font-size: 15px; | ||||
| } | ||||
| 
 | ||||
| .send-button:active { | ||||
|   opacity: 0.8; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
|  | @ -9,28 +9,38 @@ | |||
|       scroll-y | ||||
|       enable-back-to-top | ||||
|     > | ||||
|       <!-- 内容内层 - 添加内边距和安全区域 --> | ||||
|       <view class="main-content"> | ||||
|         <!-- 咨询入口列表 --> | ||||
|         <!-- 聊天会话列表 --> | ||||
|         <view  | ||||
|           class="consultation-item"  | ||||
|           v-for="(item, index) in consultationList"  | ||||
|           :key="index"  | ||||
|           @click="handleConsultation(item)" | ||||
|           class="chat-item"  | ||||
|           v-for="(item, index) in chatList"  | ||||
|           :key="item.id"  | ||||
|           @click="openChat(item)" | ||||
|         > | ||||
|           <view class="item-left"> | ||||
|             <image class="item-icon" :src="item.icon"></image> | ||||
|             <view class="item-info"> | ||||
|               <text class="item-title">{{ item.title }}</text> | ||||
|               <text class="item-desc">{{ item.desc }}</text> | ||||
|           <view class="chat-avatar-wrapper"> | ||||
|             <image class="chat-avatar" :src="item.avatar"></image> | ||||
|             <!-- 未读数角标 --> | ||||
|             <view class="unread-badge" v-if="item.unreadCount > 0"> | ||||
|               {{ item.unreadCount > 99 ? '99+' : item.unreadCount }} | ||||
|             </view> | ||||
|           </view> | ||||
|            | ||||
|           <view class="chat-content"> | ||||
|             <view class="chat-header"> | ||||
|               <text class="chat-name">{{ item.name }}</text> | ||||
|               <text class="chat-time">{{ item.lastMessageTime }}</text> | ||||
|             </view> | ||||
|             <view class="chat-preview"> | ||||
|               <text class="preview-text">{{ item.lastMessage }}</text> | ||||
|             </view> | ||||
|           </view> | ||||
|           <u-icon name="arrow-right" color="#999" size="20"></u-icon> | ||||
|         </view> | ||||
| 
 | ||||
|         <!-- 无数据提示 --> | ||||
|         <view class="empty-tip" v-if="consultationList.length === 0"> | ||||
|           暂无咨询服务 | ||||
|         <view class="empty-container" v-if="chatList.length === 0"> | ||||
|           <image class="empty-image" src="/static/common/icon/empty-chat.png"></image> | ||||
|           <text class="empty-text">暂无咨询消息</text> | ||||
|           <text class="empty-hint">学生咨询后会显示在这里</text> | ||||
|         </view> | ||||
|       </view> | ||||
|     </scroll-view> | ||||
|  | @ -38,14 +48,6 @@ | |||
|     <view class="page-tabbar"> | ||||
|       <TabBar :currentPath="'/pages/consultation/index'" @change="handleTabChange" /> | ||||
|     </view> | ||||
| 
 | ||||
|     <u-modal | ||||
|       v-model="showModal" | ||||
|       :show-cancel-button="false" | ||||
|       title="提示" | ||||
|       :content="modalContent" | ||||
|       @confirm="showModal = false" | ||||
|     ></u-modal> | ||||
|   </view> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -61,41 +63,103 @@ export default { | |||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showModal: false, | ||||
|       modalContent: '', | ||||
|       consultationList: [ | ||||
|       // 聊天会话列表(模拟数据) | ||||
|       chatList: [ | ||||
|         { | ||||
|           id: 1, | ||||
|           title: "智能问答", | ||||
|           desc: "AI智能机器人为您解答", | ||||
|           icon: "/static/common/icon/robot.png", | ||||
|           type: "ai" | ||||
|           userId: 'user_001', | ||||
|           name: '山东考生1', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '你好,在吗', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 2, | ||||
|         }, | ||||
|         { | ||||
|           id: 2, | ||||
|           title: "招生咨询", | ||||
|           desc: "招生相关问题咨询", | ||||
|           icon: "/static/common/icon/admissions.png", | ||||
|           type: "admissions" | ||||
|           userId: 'user_002', | ||||
|           name: '河北考生2', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '我是保研的是否了解到了后续计划,谁信谁的呢们爱国士...', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 0, | ||||
|         }, | ||||
|         { | ||||
|           id: 3, | ||||
|           title: "教务咨询", | ||||
|           desc: "教务相关问题咨询", | ||||
|           icon: "/static/common/icon/academic.png", | ||||
|           type: "academic" | ||||
|           userId: 'user_003', | ||||
|           name: '山东考生34523', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '请问,学校宿舍几人间?', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 1, | ||||
|         }, | ||||
|         { | ||||
|           id: 4, | ||||
|           userId: 'user_004', | ||||
|           name: '招办王老师', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '你好,在吗', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 0, | ||||
|         }, | ||||
|         { | ||||
|           id: 5, | ||||
|           userId: 'user_005', | ||||
|           name: '山东考生34523', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '请问,学校宿舍几人间?', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 3, | ||||
|         }, | ||||
|         { | ||||
|           id: 6, | ||||
|           userId: 'user_006', | ||||
|           name: '招办王老师', | ||||
|           avatar: '/static/avatar/default-avatar.png', | ||||
|           lastMessage: '你好,在吗', | ||||
|           lastMessageTime: '09:50', | ||||
|           unreadCount: 0, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|    | ||||
|   onLoad() { | ||||
|     // 加载聊天列表 | ||||
|     this.loadChatList(); | ||||
|   }, | ||||
|    | ||||
|   onShow() { | ||||
|     // 页面显示时刷新列表 | ||||
|     this.refreshChatList(); | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     handleTabChange(path, index) { | ||||
|       console.log("切换到标签页:", path, index); | ||||
|     }, | ||||
|     handleConsultation(item) { | ||||
|       // 这里可以跳转到具体的咨询页面 | ||||
|       this.modalContent = `即将进入${item.title}`; | ||||
|       this.showModal = true; | ||||
|      | ||||
|     // 打开聊天页面 | ||||
|     openChat(item) { | ||||
|       console.log('打开聊天:', item); | ||||
|       uni.navigateTo({ | ||||
|         url: `/pages/chat/chat-detail?userId=${item.userId}&name=${item.name}` | ||||
|       }); | ||||
|     }, | ||||
|      | ||||
|     // 加载聊天列表 | ||||
|     loadChatList() { | ||||
|       // TODO: 接入真实API | ||||
|       // this.$u.api.getChatList().then(res => { | ||||
|       //   this.chatList = res.data; | ||||
|       // }); | ||||
|        | ||||
|       console.log('[在线咨询] 加载聊天列表'); | ||||
|     }, | ||||
|      | ||||
|     // 刷新聊天列表 | ||||
|     refreshChatList() { | ||||
|       console.log('[在线咨询] 刷新聊天列表'); | ||||
|       // TODO: 刷新数据 | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | @ -167,58 +231,116 @@ export default { | |||
|   z-index: 100; | ||||
| } | ||||
| 
 | ||||
| .consultation-item { | ||||
|   background-color: #fff; | ||||
|   padding: 20px; | ||||
|   margin-bottom: 10px; | ||||
|   border-radius: 12px; | ||||
|   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); | ||||
| /* ===== 聊天列表项 ===== */ | ||||
| .chat-item { | ||||
|   display: flex; | ||||
|   padding: 15px; | ||||
|   background: #fff; | ||||
|   border-bottom: 1rpx solid #f0f0f0; | ||||
|   transition: background-color 0.2s; | ||||
| } | ||||
| 
 | ||||
| .chat-item:active { | ||||
|   background-color: #f5f5f5; | ||||
| } | ||||
| 
 | ||||
| .chat-avatar-wrapper { | ||||
|   position: relative; | ||||
|   margin-right: 12px; | ||||
| } | ||||
| 
 | ||||
| .chat-avatar { | ||||
|   width: 50px; | ||||
|   height: 50px; | ||||
|   border-radius: 8px; | ||||
|   background-color: #e8e8e8; | ||||
| } | ||||
| 
 | ||||
| .unread-badge { | ||||
|   position: absolute; | ||||
|   top: -5px; | ||||
|   right: -5px; | ||||
|   min-width: 18px; | ||||
|   height: 18px; | ||||
|   padding: 0 5px; | ||||
|   background: #ff4d4f; | ||||
|   border-radius: 9px; | ||||
|   color: #fff; | ||||
|   font-size: 11px; | ||||
|   line-height: 18px; | ||||
|   text-align: center; | ||||
|   border: 2px solid #fff; | ||||
| } | ||||
| 
 | ||||
| .chat-content { | ||||
|   flex: 1; | ||||
|   min-width: 0; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: space-between; | ||||
| } | ||||
| 
 | ||||
| .chat-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   cursor: pointer; | ||||
|   transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .consultation-item:active { | ||||
|   background-color: #f8f8f8; | ||||
| } | ||||
| 
 | ||||
| .item-left { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   flex: 1; | ||||
| } | ||||
| 
 | ||||
| .item-icon { | ||||
|   width: 48px; | ||||
|   height: 48px; | ||||
|   margin-right: 15px; | ||||
|   border-radius: 8px; | ||||
| } | ||||
| 
 | ||||
| .item-info { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| .item-title { | ||||
|   font-size: 16px; | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
| 
 | ||||
| .item-desc { | ||||
| .chat-name { | ||||
|   font-size: 16px; | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
|   max-width: 200px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .chat-time { | ||||
|   font-size: 12px; | ||||
|   color: #999; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
| 
 | ||||
| .chat-preview { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| .preview-text { | ||||
|   font-size: 13px; | ||||
|   color: #999; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   flex: 1; | ||||
| } | ||||
| 
 | ||||
| /* ===== 空状态 ===== */ | ||||
| .empty-container { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   padding: 100px 20px; | ||||
| } | ||||
| 
 | ||||
| .empty-image { | ||||
|   width: 120px; | ||||
|   height: 120px; | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| .empty-text { | ||||
|   font-size: 16px; | ||||
|   color: #666; | ||||
|   margin-bottom: 8px; | ||||
| } | ||||
| 
 | ||||
| .empty-hint { | ||||
|   font-size: 13px; | ||||
|   color: #999; | ||||
| } | ||||
| 
 | ||||
| .empty-tip { | ||||
|   text-align: center; | ||||
|   padding: 50px 20px; | ||||
|   color: #999; | ||||
|   font-size: 14px; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,533 @@ | |||
| <template> | ||||
|   <view class="test-page"> | ||||
|     <view class="test-header"> | ||||
|       <text class="title">WebSocket 测试工具</text> | ||||
|       <text class="subtitle">实时测试 WebSocket 连接和消息</text> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 连接状态 --> | ||||
|     <view class="status-card"> | ||||
|       <view class="status-item"> | ||||
|         <text class="label">连接状态:</text> | ||||
|         <text :class="['value', connectionStatus.class]">{{ connectionStatus.text }}</text> | ||||
|       </view> | ||||
|       <view class="status-item"> | ||||
|         <text class="label">队列消息:</text> | ||||
|         <text class="value">{{ queueLength }} 条</text> | ||||
|       </view> | ||||
|       <view class="status-item"> | ||||
|         <text class="label">重连次数:</text> | ||||
|         <text class="value">{{ reconnectAttempts }} 次</text> | ||||
|       </view> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 连接控制 --> | ||||
|     <view class="control-card"> | ||||
|       <view class="control-title">连接控制</view> | ||||
|       <input | ||||
|         class="url-input" | ||||
|         v-model="wsUrl" | ||||
|         placeholder="输入 WebSocket 地址" | ||||
|       /> | ||||
|       <view class="button-group"> | ||||
|         <button class="btn btn-primary" @click="handleConnect" :disabled="isConnected"> | ||||
|           连接 | ||||
|         </button> | ||||
|         <button class="btn btn-danger" @click="handleDisconnect" :disabled="!isConnected"> | ||||
|           断开 | ||||
|         </button> | ||||
|       </view> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 消息发送 --> | ||||
|     <view class="message-card"> | ||||
|       <view class="control-title">发送消息</view> | ||||
|       <textarea | ||||
|         class="message-input" | ||||
|         v-model="messageToSend" | ||||
|         placeholder="输入消息内容(JSON 格式)" | ||||
|         :rows="5" | ||||
|       ></textarea> | ||||
|       <view class="button-group"> | ||||
|         <button class="btn btn-success" @click="handleSendMessage" :disabled="!isConnected"> | ||||
|           发送消息 | ||||
|         </button> | ||||
|         <button class="btn btn-secondary" @click="handleSendPing" :disabled="!isConnected"> | ||||
|           发送心跳 | ||||
|         </button> | ||||
|       </view> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 快速测试 --> | ||||
|     <view class="quick-test-card"> | ||||
|       <view class="control-title">快速测试</view> | ||||
|       <view class="button-group"> | ||||
|         <button class="btn btn-info" @click="testEcho"> | ||||
|           Echo 服务测试 | ||||
|         </button> | ||||
|         <button class="btn btn-info" @click="testLocalServer"> | ||||
|           本地服务器测试 | ||||
|         </button> | ||||
|       </view> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- 日志显示 --> | ||||
|     <view class="log-card"> | ||||
|       <view class="log-header"> | ||||
|         <text class="control-title">日志 ({{ logs.length }})</text> | ||||
|         <button class="btn-clear" @click="clearLogs">清空</button> | ||||
|       </view> | ||||
|       <scroll-view class="log-content" scroll-y> | ||||
|         <view | ||||
|           v-for="(log, index) in logs" | ||||
|           :key="index" | ||||
|           :class="['log-item', `log-${log.type}`]" | ||||
|         > | ||||
|           <text class="log-time">{{ log.time }}</text> | ||||
|           <text class="log-type">{{ log.typeText }}</text> | ||||
|           <text class="log-message">{{ log.message }}</text> | ||||
|         </view> | ||||
|         <view v-if="logs.length === 0" class="empty-log"> | ||||
|           暂无日志 | ||||
|         </view> | ||||
|       </scroll-view> | ||||
|     </view> | ||||
|   </view> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import wsManager from '@/utils/websocket-manager.js' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'WebSocketTest', | ||||
|   data() { | ||||
|     return { | ||||
|       // WebSocket URL | ||||
|       wsUrl: 'wss://echo.websocket.org', | ||||
|        | ||||
|       // 连接状态 | ||||
|       isConnected: false, | ||||
|       reconnectAttempts: 0, | ||||
|       queueLength: 0, | ||||
|        | ||||
|       // 消息 | ||||
|       messageToSend: JSON.stringify({ | ||||
|         type: 'test', | ||||
|         content: 'Hello WebSocket!', | ||||
|         timestamp: Date.now() | ||||
|       }, null, 2), | ||||
|        | ||||
|       // 日志 | ||||
|       logs: [], | ||||
|        | ||||
|       // 计时器 | ||||
|       startTime: 0 | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   computed: { | ||||
|     connectionStatus() { | ||||
|       if (this.isConnected) { | ||||
|         return { text: '已连接', class: 'status-connected' } | ||||
|       } else if (this.reconnectAttempts > 0) { | ||||
|         return { text: '重连中...', class: 'status-reconnecting' } | ||||
|       } else { | ||||
|         return { text: '未连接', class: 'status-disconnected' } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   onLoad() { | ||||
|     this.setupEventListeners() | ||||
|     this.addLog('info', '测试工具已加载') | ||||
|   }, | ||||
|    | ||||
|   onUnload() { | ||||
|     // 清理事件监听 | ||||
|     wsManager.off('open', this.handleOpen) | ||||
|     wsManager.off('close', this.handleClose) | ||||
|     wsManager.off('error', this.handleError) | ||||
|     wsManager.off('message', this.handleMessage) | ||||
|     wsManager.off('reconnect', this.handleReconnect) | ||||
|   }, | ||||
|    | ||||
|   methods: { | ||||
|     // 设置事件监听 | ||||
|     setupEventListeners() { | ||||
|       this.handleOpen = () => { | ||||
|         this.isConnected = true | ||||
|         this.reconnectAttempts = 0 | ||||
|         const duration = Date.now() - this.startTime | ||||
|         this.addLog('success', `连接成功 (耗时 ${duration}ms)`) | ||||
|       } | ||||
|        | ||||
|       this.handleClose = (res) => { | ||||
|         this.isConnected = false | ||||
|         this.addLog('warning', `连接关闭 (code: ${res.code})`) | ||||
|       } | ||||
|        | ||||
|       this.handleError = (err) => { | ||||
|         this.addLog('error', `连接错误: ${JSON.stringify(err)}`) | ||||
|       } | ||||
|        | ||||
|       this.handleMessage = (data) => { | ||||
|         this.addLog('message', `收到消息: ${JSON.stringify(data, null, 2)}`) | ||||
|       } | ||||
|        | ||||
|       this.handleReconnect = (attempts) => { | ||||
|         this.reconnectAttempts = attempts | ||||
|         this.addLog('info', `正在重连 (第 ${attempts} 次)`) | ||||
|       } | ||||
|        | ||||
|       wsManager.on('open', this.handleOpen) | ||||
|       wsManager.on('close', this.handleClose) | ||||
|       wsManager.on('error', this.handleError) | ||||
|       wsManager.on('message', this.handleMessage) | ||||
|       wsManager.on('reconnect', this.handleReconnect) | ||||
|     }, | ||||
|      | ||||
|     // 连接 | ||||
|     handleConnect() { | ||||
|       if (!this.wsUrl) { | ||||
|         uni.showToast({ title: '请输入 WebSocket 地址', icon: 'none' }) | ||||
|         return | ||||
|       } | ||||
|        | ||||
|       this.addLog('info', `开始连接: ${this.wsUrl}`) | ||||
|       this.startTime = Date.now() | ||||
|        | ||||
|       wsManager.connect(this.wsUrl, { | ||||
|         reconnectInterval: 3000, | ||||
|         maxReconnectAttempts: 5, | ||||
|         heartbeatInterval: 30000 | ||||
|       }) | ||||
|        | ||||
|       this.updateState() | ||||
|     }, | ||||
|      | ||||
|     // 断开连接 | ||||
|     handleDisconnect() { | ||||
|       this.addLog('info', '手动断开连接') | ||||
|       wsManager.close(true) | ||||
|       this.isConnected = false | ||||
|       this.reconnectAttempts = 0 | ||||
|     }, | ||||
|      | ||||
|     // 发送消息 | ||||
|     handleSendMessage() { | ||||
|       try { | ||||
|         const message = JSON.parse(this.messageToSend) | ||||
|         this.addLog('send', `发送消息: ${JSON.stringify(message)}`) | ||||
|         wsManager.send(message) | ||||
|       } catch (e) { | ||||
|         this.addLog('error', `消息格式错误: ${e.message}`) | ||||
|         uni.showToast({ title: 'JSON 格式错误', icon: 'none' }) | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|     // 发送心跳 | ||||
|     handleSendPing() { | ||||
|       const ping = { | ||||
|         type: 'ping', | ||||
|         timestamp: Date.now() | ||||
|       } | ||||
|       this.addLog('send', `发送心跳: ${JSON.stringify(ping)}`) | ||||
|       wsManager.send(ping) | ||||
|     }, | ||||
|      | ||||
|     // Echo 服务测试 | ||||
|     testEcho() { | ||||
|       this.wsUrl = 'wss://echo.websocket.org' | ||||
|       this.messageToSend = JSON.stringify({ | ||||
|         type: 'test', | ||||
|         content: 'Echo 测试消息', | ||||
|         timestamp: Date.now() | ||||
|       }, null, 2) | ||||
|        | ||||
|       this.addLog('info', '开始 Echo 服务测试') | ||||
|       uni.showToast({ title: '点击"连接"开始测试', icon: 'none' }) | ||||
|     }, | ||||
|      | ||||
|     // 本地服务器测试 | ||||
|     testLocalServer() { | ||||
|       this.wsUrl = 'ws://localhost:8082/ws/chat?token=test-token' | ||||
|       this.messageToSend = JSON.stringify({ | ||||
|         type: 'message', | ||||
|         fromUserId: 'test_user', | ||||
|         toUserId: 'admin', | ||||
|         content: '本地服务器测试', | ||||
|         timestamp: Date.now() | ||||
|       }, null, 2) | ||||
|        | ||||
|       this.addLog('info', '开始本地服务器测试') | ||||
|       uni.showToast({ title: '点击"连接"开始测试', icon: 'none' }) | ||||
|     }, | ||||
|      | ||||
|     // 更新状态 | ||||
|     updateState() { | ||||
|       const state = wsManager.getState() | ||||
|       this.isConnected = state.isConnected | ||||
|       this.reconnectAttempts = state.reconnectAttempts | ||||
|       this.queueLength = state.queueLength | ||||
|     }, | ||||
|      | ||||
|     // 添加日志 | ||||
|     addLog(type, message) { | ||||
|       const typeMap = { | ||||
|         info: 'ℹ️ 信息', | ||||
|         success: '✅ 成功', | ||||
|         warning: '⚠️ 警告', | ||||
|         error: '❌ 错误', | ||||
|         send: '📤 发送', | ||||
|         message: '📩 接收' | ||||
|       } | ||||
|        | ||||
|       const now = new Date() | ||||
|       const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` | ||||
|        | ||||
|       this.logs.unshift({ | ||||
|         type, | ||||
|         typeText: typeMap[type] || type, | ||||
|         message, | ||||
|         time | ||||
|       }) | ||||
|        | ||||
|       // 最多保留 100 条日志 | ||||
|       if (this.logs.length > 100) { | ||||
|         this.logs.pop() | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|     // 清空日志 | ||||
|     clearLogs() { | ||||
|       this.logs = [] | ||||
|       this.addLog('info', '日志已清空') | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .test-page { | ||||
|   padding: 20rpx; | ||||
|   background-color: #f5f6fa; | ||||
|   min-height: 100vh; | ||||
| } | ||||
| 
 | ||||
| .test-header { | ||||
|   text-align: center; | ||||
|   padding: 40rpx 0; | ||||
| } | ||||
| 
 | ||||
| .title { | ||||
|   font-size: 36rpx; | ||||
|   font-weight: bold; | ||||
|   color: #333; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .subtitle { | ||||
|   font-size: 24rpx; | ||||
|   color: #999; | ||||
|   margin-top: 10rpx; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .status-card, | ||||
| .control-card, | ||||
| .message-card, | ||||
| .quick-test-card, | ||||
| .log-card { | ||||
|   background: #fff; | ||||
|   border-radius: 16rpx; | ||||
|   padding: 30rpx; | ||||
|   margin-bottom: 20rpx; | ||||
|   box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); | ||||
| } | ||||
| 
 | ||||
| .status-item { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 20rpx 0; | ||||
|   border-bottom: 1rpx solid #f0f0f0; | ||||
| } | ||||
| 
 | ||||
| .status-item:last-child { | ||||
|   border-bottom: none; | ||||
| } | ||||
| 
 | ||||
| .label { | ||||
|   font-size: 28rpx; | ||||
|   color: #666; | ||||
| } | ||||
| 
 | ||||
| .value { | ||||
|   font-size: 28rpx; | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
| } | ||||
| 
 | ||||
| .status-connected { | ||||
|   color: #52c41a; | ||||
| } | ||||
| 
 | ||||
| .status-reconnecting { | ||||
|   color: #faad14; | ||||
| } | ||||
| 
 | ||||
| .status-disconnected { | ||||
|   color: #999; | ||||
| } | ||||
| 
 | ||||
| .control-title { | ||||
|   font-size: 30rpx; | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
|   margin-bottom: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .url-input, | ||||
| .message-input { | ||||
|   width: 100%; | ||||
|   padding: 20rpx; | ||||
|   border: 1rpx solid #d9d9d9; | ||||
|   border-radius: 8rpx; | ||||
|   font-size: 26rpx; | ||||
|   margin-bottom: 20rpx; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| .message-input { | ||||
|   min-height: 200rpx; | ||||
|   font-family: 'Courier New', monospace; | ||||
| } | ||||
| 
 | ||||
| .button-group { | ||||
|   display: flex; | ||||
|   gap: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .btn { | ||||
|   flex: 1; | ||||
|   height: 80rpx; | ||||
|   line-height: 80rpx; | ||||
|   text-align: center; | ||||
|   border-radius: 8rpx; | ||||
|   font-size: 28rpx; | ||||
|   border: none; | ||||
| } | ||||
| 
 | ||||
| .btn-primary { | ||||
|   background: #1890ff; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| .btn-danger { | ||||
|   background: #ff4d4f; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| .btn-success { | ||||
|   background: #52c41a; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| .btn-secondary { | ||||
|   background: #d9d9d9; | ||||
|   color: #666; | ||||
| } | ||||
| 
 | ||||
| .btn-info { | ||||
|   background: #722ed1; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| .btn:disabled { | ||||
|   opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .log-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   margin-bottom: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .btn-clear { | ||||
|   padding: 10rpx 20rpx; | ||||
|   background: #f0f0f0; | ||||
|   border: none; | ||||
|   border-radius: 8rpx; | ||||
|   font-size: 24rpx; | ||||
|   color: #666; | ||||
| } | ||||
| 
 | ||||
| .log-content { | ||||
|   height: 600rpx; | ||||
|   border: 1rpx solid #f0f0f0; | ||||
|   border-radius: 8rpx; | ||||
|   padding: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .log-item { | ||||
|   padding: 20rpx; | ||||
|   margin-bottom: 10rpx; | ||||
|   border-radius: 8rpx; | ||||
|   font-size: 24rpx; | ||||
|   line-height: 1.6; | ||||
|   word-break: break-all; | ||||
| } | ||||
| 
 | ||||
| .log-info { | ||||
|   background: #e6f7ff; | ||||
|   border-left: 4rpx solid #1890ff; | ||||
| } | ||||
| 
 | ||||
| .log-success { | ||||
|   background: #f6ffed; | ||||
|   border-left: 4rpx solid #52c41a; | ||||
| } | ||||
| 
 | ||||
| .log-warning { | ||||
|   background: #fffbe6; | ||||
|   border-left: 4rpx solid #faad14; | ||||
| } | ||||
| 
 | ||||
| .log-error { | ||||
|   background: #fff2f0; | ||||
|   border-left: 4rpx solid #ff4d4f; | ||||
| } | ||||
| 
 | ||||
| .log-send { | ||||
|   background: #f9f0ff; | ||||
|   border-left: 4rpx solid #722ed1; | ||||
| } | ||||
| 
 | ||||
| .log-message { | ||||
|   background: #fff0f6; | ||||
|   border-left: 4rpx solid #eb2f96; | ||||
| } | ||||
| 
 | ||||
| .log-time { | ||||
|   color: #999; | ||||
|   margin-right: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .log-type { | ||||
|   font-weight: 500; | ||||
|   margin-right: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .log-message { | ||||
|   color: #333; | ||||
| } | ||||
| 
 | ||||
| .empty-log { | ||||
|   text-align: center; | ||||
|   padding: 100rpx 0; | ||||
|   color: #999; | ||||
|   font-size: 28rpx; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
		Loading…
	
		Reference in New Issue