diff --git a/components/TabBar-optimized.vue b/components/TabBar-optimized.vue new file mode 100644 index 0000000..c6482f1 --- /dev/null +++ b/components/TabBar-optimized.vue @@ -0,0 +1,167 @@ + + + + + + + + + diff --git a/config/tabbar.config.js b/config/tabbar.config.js new file mode 100644 index 0000000..fd21f9a --- /dev/null +++ b/config/tabbar.config.js @@ -0,0 +1,47 @@ +/** + * TabBar 配置文件 + * 集中管理底部导航栏配置 + */ + +export const TAB_BAR_CONFIG = [ + { + text: "在线咨询", + icon: "/static/tabbar/tabbar-icon1.png", + activeIcon: "/static/tabbar/tabbar-icon1-active.png", + pagePath: "/pages/consultation/index", + // 可选配置 + badge: "", // 角标文字 + dot: false, // 是否显示小红点 + }, + { + text: "人工转接", + icon: "/static/tabbar/tabbar-icon4.png", + activeIcon: "/static/tabbar/tabbar-icon4-active.png", + pagePath: "/pages/transfer/index", + badge: "", + dot: false, + }, + { + text: "我的", + icon: "/static/tabbar/tabbar-icon3.png", + activeIcon: "/static/tabbar/tabbar-icon3-active.png", + pagePath: "/pages/my/index", + badge: "", + dot: false, + }, +]; + +// TabBar 样式配置 +export const TAB_BAR_STYLE = { + color: "#999999", // 未激活颜色 + selectedColor: "#4a6cf7", // 激活颜色 + backgroundColor: "#ffffff", // 背景色 + borderTop: false, // 是否显示顶部边框 + height: 148, // 高度(rpx) +}; + +export default { + tabs: TAB_BAR_CONFIG, + style: TAB_BAR_STYLE, +}; + diff --git a/config/websocket.config.js b/config/websocket.config.js new file mode 100644 index 0000000..7e7da31 --- /dev/null +++ b/config/websocket.config.js @@ -0,0 +1,100 @@ +/** + * WebSocket 配置文件 + * + * 集中管理 WebSocket 相关配置 + */ + +// 根据环境选择 WebSocket 地址 +const getWebSocketUrl = () => { + // #ifdef H5 + // H5 开发环境 + if (process.env.NODE_ENV === 'development') { + return 'ws://localhost:8082/ws/chat' + } + // 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 { + // WebSocket 服务器地址(动态获取) + url: getWebSocketUrl(), + + // 重连配置 + reconnect: { + enabled: true, // 是否启用自动重连 + maxAttempts: 10, // 最大重连次数 + interval: 3000, // 重连间隔(毫秒) + incrementInterval: true, // 是否递增重连间隔 + maxInterval: 30000 // 最大重连间隔 + }, + + // 心跳配置 + heartbeat: { + enabled: true, // 是否启用心跳 + interval: 30000, // 心跳间隔(毫秒) + timeout: 10000, // 心跳超时时间 + pingMessage: { // 心跳消息格式 + type: 'ping', + timestamp: () => Date.now() + } + }, + + // 消息队列配置 + messageQueue: { + enabled: true, // 是否启用消息队列 + maxSize: 100, // 队列最大长度 + clearOnConnect: false // 连接成功后是否清空队列 + }, + + // 日志配置 + log: { + enabled: true, // 是否启用日志 + level: 'info' // 日志级别:debug, info, warn, error + }, + + // 消息类型定义 + messageTypes: { + // 文字消息 + TEXT: 'text', + // 图片消息 + IMAGE: 'image', + // 语音消息 + VOICE: 'voice', + // 系统消息 + SYSTEM: 'system', + // 心跳 + PING: 'ping', + PONG: 'pong', + // 已读回执 + READ: 'read', + // 输入中 + TYPING: 'typing', + // 人工转接 + TRANSFER: 'transfer' + }, + + // 事件名称定义 + events: { + OPEN: 'open', + CLOSE: 'close', + ERROR: 'error', + MESSAGE: 'message', + RECONNECT: 'reconnect', + RECONNECT_FAILED: 'reconnect_failed' + } +} + diff --git a/docs/WebSocket使用示例.md b/docs/WebSocket使用示例.md new file mode 100644 index 0000000..84533ad --- /dev/null +++ b/docs/WebSocket使用示例.md @@ -0,0 +1,607 @@ +# WebSocket 使用示例 + +## 📦 已创建的文件 + +✅ `/utils/websocket-manager.js` - WebSocket 管理器(已完成) + +--- + +## 🚀 快速开始 + +### 1. 在 App.vue 中初始化连接 + +```vue + +``` + +--- + +### 2. 在聊天页面中使用 + +#### 方式 A:修改现有的 `dialogBox.vue` + +```vue + + + + + + + + {{ msg.content }} + + + + {{ msg.content }} + + + + + + + + 发送 + + + + + +``` + +--- + +### 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. **添加更多功能** + - 图片消息 + - 语音消息 + - 消息已读 + - 输入中状态 + +随时告诉我需要什么!🎯 + diff --git a/docs/WebSocket实施计划.md b/docs/WebSocket实施计划.md new file mode 100644 index 0000000..6164f33 --- /dev/null +++ b/docs/WebSocket实施计划.md @@ -0,0 +1,616 @@ +# 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(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(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 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(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 _connections = + new ConcurrentDictionary(); + + // 添加连接 + 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 GetOnlineUsers() + { + return _connections.Keys; + } + + // 获取在线用户数 + public int GetOnlineCount() + { + return _connections.Count; + } +} +``` + +#### 3. 注册中间件 + +**文件**:`Startup.cs` + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + // 注册连接管理器为单例 + services.AddSingleton(); + + // 其他服务... + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 启用 WebSocket + app.UseWebSockets(new WebSocketOptions + { + KeepAliveInterval = TimeSpan.FromSeconds(30) + }); + + // 注册 WebSocket 中间件 + app.Map("/ws/chat", builder => + { + builder.UseMiddleware(); + }); + + // 其他中间件... + 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 GetHistory(string userId, int page = 1, int pageSize = 50) + { + // TODO: 从数据库查询历史消息 + var messages = new List + { + 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 SendOffline([FromBody] SendMessageRequest request) + { + // TODO: 保存离线消息到数据库 + return Ok(new { success = true }); + } + + // 标记已读 + [HttpPost("MarkRead")] + public async Task 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. ✅ 调试和测试支持 + +随时告诉我!🚀 + diff --git a/pages.json b/pages.json index 936ee35..8ca91ea 100644 --- a/pages.json +++ b/pages.json @@ -27,13 +27,7 @@ "navigationStyle": "custom" } }, - { - "path": "pages/my/index", - "style": { - "navigationBarTitleText": "我的", - "navigationStyle": "custom" - } - }, + { "path": "pages/my/personalInfo", "style": { @@ -58,13 +52,28 @@ } }, { - "path": "pages/home/conversations/index", + "path": "pages/consultation/index", "style": { - "navigationBarTitleText": "会话列表", + "navigationBarTitleText": "在线咨询", "enablePullDownRefresh": false, "navigationStyle": "custom" } }, + { + "path": "pages/transfer/index", + "style": { + "navigationBarTitleText": "人工转接", + "enablePullDownRefresh": false, + "navigationStyle": "custom" + } + }, + { + "path": "pages/my/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, { "path": "pages/login/login/index", "style": { @@ -104,16 +113,16 @@ "backgroundColor": "#ffffff", "list": [ { - "pagePath": "pages/home/conversations/index", + "pagePath": "pages/consultation/index", "iconPath": "static/tabbar/icon_home.png", "selectedIconPath": "static/tabbar/icon_home_active.png", - "text": "会话列表" + "text": "在线咨询" }, { - "pagePath": "pages/notes/index", + "pagePath": "pages/transfer/index", "iconPath": "static/tabbar/icon_message.png", "selectedIconPath": "static/tabbar/icon_message_active.png", - "text": "留言板" + "text": "人工转接" }, { "pagePath": "pages/my/index", diff --git a/pages/consultation/index.vue b/pages/consultation/index.vue new file mode 100644 index 0000000..c36adce --- /dev/null +++ b/pages/consultation/index.vue @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + {{ item.title }} + {{ item.desc }} + + + + + + + + 暂无咨询服务 + + + + + + + + + + + + + + + + diff --git a/pages/my/index.vue b/pages/my/index.vue index b1779ff..956ac9a 100644 --- a/pages/my/index.vue +++ b/pages/my/index.vue @@ -1,89 +1,99 @@ - - + + + + - - - - - - - {{ teacherInfo.name }} - - - {{ teacherInfo.collegeName }} + + + + + - - - {{ teacherInfo.professionalName }} + + {{ teacherInfo.name }} + + + {{ teacherInfo.collegeName }} + + + + {{ teacherInfo.professionalName }} + - - - - 36 - 总答题 - - - 10 - 已完成 - - - 26 - 未回复 - - - - - - - - - - - + + + 36 + 总答题 - 个人信息 - - - - - - - + + 10 + 已完成 - 修改密码 - - - - - - - + + 26 + 未回复 - 退出登录 - - - - - - - + + + + + + + + + + 个人信息 + + + + + + + + + 修改密码 + + + + + + + + + 退出登录 + + + + + + + + + + + + + diff --git a/static/tabbar/tabbar-icon4-active.png b/static/tabbar/tabbar-icon4-active.png new file mode 100644 index 0000000..612c2a8 Binary files /dev/null and b/static/tabbar/tabbar-icon4-active.png differ diff --git a/static/tabbar/tabbar-icon4.png b/static/tabbar/tabbar-icon4.png new file mode 100644 index 0000000..8cebd49 Binary files /dev/null and b/static/tabbar/tabbar-icon4.png differ diff --git a/utils/websocket-manager.js b/utils/websocket-manager.js new file mode 100644 index 0000000..7c3a484 --- /dev/null +++ b/utils/websocket-manager.js @@ -0,0 +1,353 @@ +/** + * WebSocket 管理器 + * + * 功能: + * 1. 自动重连 + * 2. 心跳检测 + * 3. 消息队列 + * 4. 事件监听 + * + * 使用示例: + * import wsManager from '@/utils/websocket-manager.js' + * + * // 连接 + * wsManager.connect('wss://example.com/chat') + * + * // 监听消息 + * wsManager.on('message', (data) => { + * console.log('收到消息:', data) + * }) + * + * // 发送消息 + * wsManager.send({ type: 'text', content: 'Hello' }) + */ + +class WebSocketManager { + constructor() { + // WebSocket 实例 + this.socketTask = null + + // 连接状态 + this.isConnected = false + this.isConnecting = false + + // 配置 + this.url = '' + this.reconnectAttempts = 0 + this.maxReconnectAttempts = 5 + this.reconnectInterval = 5000 + this.heartbeatInterval = 30000 + + // 定时器 + this.reconnectTimer = null + this.heartbeatTimer = null + + // 消息队列(连接未建立时暂存) + this.messageQueue = [] + + // 事件监听器 + this.listeners = { + message: [], // 收到消息 + open: [], // 连接成功 + close: [], // 连接关闭 + error: [], // 连接错误 + reconnect: [] // 重连中 + } + } + + /** + * 连接 WebSocket + * @param {String} url WebSocket 服务器地址 + * @param {Object} options 配置选项 + */ + connect(url, options = {}) { + // 如果已经在连接中,直接返回 + if (this.isConnecting || this.isConnected) { + console.log('[WebSocket] 已在连接中或已连接') + return + } + + this.url = url + this.isConnecting = true + + // 合并配置 + Object.assign(this, options) + + console.log(`[WebSocket] 开始连接: ${url}`) + + // 创建连接 + this.socketTask = uni.connectSocket({ + url: this.url, + success: () => { + console.log('[WebSocket] 连接请求已发送') + }, + fail: (err) => { + console.error('[WebSocket] 连接失败:', err) + this.isConnecting = false + this.handleError(err) + this.handleReconnect() + } + }) + + // 监听连接打开 + this.socketTask.onOpen(() => { + console.log('[WebSocket] 连接成功') + this.isConnected = true + this.isConnecting = false + this.reconnectAttempts = 0 + + // 触发 open 事件 + this.emit('open') + + // 启动心跳 + this.startHeartbeat() + + // 发送队列中的消息 + this.flushMessageQueue() + }) + + // 监听消息 + this.socketTask.onMessage((res) => { + console.log('[WebSocket] 收到消息:', res.data) + + try { + const data = JSON.parse(res.data) + + // 处理 pong(心跳响应) + if (data.type === 'pong') { + console.log('[WebSocket] 收到心跳响应') + return + } + + // 触发 message 事件 + this.emit('message', data) + } catch (e) { + console.error('[WebSocket] 消息解析失败:', e) + // 如果不是 JSON,直接传递原始数据 + this.emit('message', res.data) + } + }) + + // 监听连接关闭 + this.socketTask.onClose((res) => { + console.log('[WebSocket] 连接已关闭:', res) + this.isConnected = false + this.isConnecting = false + + // 停止心跳 + this.stopHeartbeat() + + // 触发 close 事件 + this.emit('close', res) + + // 尝试重连 + this.handleReconnect() + }) + + // 监听错误 + this.socketTask.onError((err) => { + console.error('[WebSocket] 连接错误:', err) + this.isConnected = false + this.isConnecting = false + + // 触发 error 事件 + this.handleError(err) + }) + } + + /** + * 发送消息 + * @param {Object|String} data 要发送的数据 + */ + send(data) { + // 如果未连接,加入队列 + if (!this.isConnected) { + console.warn('[WebSocket] 未连接,消息加入队列') + this.messageQueue.push(data) + return + } + + const message = typeof data === 'string' ? data : JSON.stringify(data) + + this.socketTask.send({ + data: message, + success: () => { + console.log('[WebSocket] 消息发送成功:', data) + }, + fail: (err) => { + console.error('[WebSocket] 消息发送失败:', err) + } + }) + } + + /** + * 关闭连接 + * @param {Boolean} manual 是否手动关闭(手动关闭不重连) + */ + close(manual = true) { + console.log(`[WebSocket] 关闭连接 (手动: ${manual})`) + + // 如果是手动关闭,清除重连定时器 + if (manual) { + this.clearReconnectTimer() + this.reconnectAttempts = this.maxReconnectAttempts // 阻止重连 + } + + this.stopHeartbeat() + + if (this.socketTask) { + this.socketTask.close({ + code: 1000, + reason: manual ? '手动关闭' : '自动关闭' + }) + this.socketTask = null + } + + this.isConnected = false + this.isConnecting = false + } + + /** + * 启动心跳 + */ + startHeartbeat() { + console.log('[WebSocket] 启动心跳') + this.stopHeartbeat() + + this.heartbeatTimer = setInterval(() => { + if (this.isConnected) { + console.log('[WebSocket] 发送心跳') + this.send({ type: 'ping', timestamp: Date.now() }) + } + }, this.heartbeatInterval) + } + + /** + * 停止心跳 + */ + stopHeartbeat() { + if (this.heartbeatTimer) { + console.log('[WebSocket] 停止心跳') + clearInterval(this.heartbeatTimer) + this.heartbeatTimer = null + } + } + + /** + * 处理重连 + */ + handleReconnect() { + // 如果达到最大重连次数,停止重连 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('[WebSocket] 达到最大重连次数,停止重连') + return + } + + // 如果已经在重连中,直接返回 + if (this.reconnectTimer) { + return + } + + this.reconnectAttempts++ + console.log(`[WebSocket] 准备重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`) + + // 触发 reconnect 事件 + this.emit('reconnect', this.reconnectAttempts) + + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null + this.connect(this.url) + }, this.reconnectInterval) + } + + /** + * 清除重连定时器 + */ + clearReconnectTimer() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer) + this.reconnectTimer = null + } + } + + /** + * 发送队列中的消息 + */ + flushMessageQueue() { + if (this.messageQueue.length === 0) { + return + } + + console.log(`[WebSocket] 发送队列中的 ${this.messageQueue.length} 条消息`) + + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift() + this.send(message) + } + } + + /** + * 处理错误 + */ + handleError(err) { + this.emit('error', err) + } + + /** + * 监听事件 + * @param {String} event 事件名称 + * @param {Function} callback 回调函数 + */ + on(event, callback) { + if (this.listeners[event]) { + this.listeners[event].push(callback) + } + } + + /** + * 移除事件监听 + * @param {String} event 事件名称 + * @param {Function} callback 回调函数 + */ + off(event, callback) { + if (this.listeners[event]) { + const index = this.listeners[event].indexOf(callback) + if (index > -1) { + this.listeners[event].splice(index, 1) + } + } + } + + /** + * 触发事件 + * @param {String} event 事件名称 + * @param {*} data 数据 + */ + emit(event, data) { + if (this.listeners[event]) { + this.listeners[event].forEach(callback => { + try { + callback(data) + } catch (e) { + console.error(`[WebSocket] 事件 ${event} 回调执行失败:`, e) + } + }) + } + } + + /** + * 获取连接状态 + */ + getState() { + return { + isConnected: this.isConnected, + isConnecting: this.isConnecting, + reconnectAttempts: this.reconnectAttempts, + queueLength: this.messageQueue.length + } + } +} + +// 导出单例 +export default new WebSocketManager() +