feat: 添加聊天详情页面和WebSocket测试工具,优化咨询列表展示
This commit is contained in:
parent
36df45cc52
commit
e706a44b04
|
|
@ -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