fix: 111
This commit is contained in:
parent
d22dde58fc
commit
69fd60ac3b
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -16,6 +16,13 @@
|
||||||
- **简单提示框** - 轻量级确认对话框
|
- **简单提示框** - 轻量级确认对话框
|
||||||
- **美化表单对话框** - 添加作业的高级表单界面
|
- **美化表单对话框** - 添加作业的高级表单界面
|
||||||
|
|
||||||
|
#### 3. WebSocket实时聊天系统
|
||||||
|
- **STOMP协议支持** - 基于STOMP协议的WebSocket连接
|
||||||
|
- **实时消息传输** - 支持实时双向通讯
|
||||||
|
- **聊天室功能** - 支持多房间聊天
|
||||||
|
- **演示模式** - 本地模拟聊天,无需后端服务器
|
||||||
|
- **美观聊天界面** - 现代化的聊天UI设计
|
||||||
|
|
||||||
### 🎨 界面美化升级
|
### 🎨 界面美化升级
|
||||||
|
|
||||||
#### 1. 数据表格全面美化
|
#### 1. 数据表格全面美化
|
||||||
|
@ -97,11 +104,17 @@
|
||||||
### 📦 新增依赖和配置
|
### 📦 新增依赖和配置
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@stomp/stompjs": "^7.x.x",
|
||||||
|
"sockjs-client": "^1.x.x",
|
||||||
|
"@types/sockjs-client": "^1.x.x"
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"作业管理": "完整的学生作业管理系统",
|
"作业管理": "完整的学生作业管理系统",
|
||||||
"表格美化": "全面美化的数据表格组件",
|
"表格美化": "全面美化的数据表格组件",
|
||||||
"对话框美化": "多种风格的美观对话框",
|
"对话框美化": "多种风格的美观对话框",
|
||||||
"导航增强": "完善的路由导航系统"
|
"导航增强": "完善的路由导航系统",
|
||||||
|
"WebSocket聊天": "基于STOMP协议的实时聊天系统"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -126,6 +139,8 @@
|
||||||
- ✅ 响应式布局优化
|
- ✅ 响应式布局优化
|
||||||
- ✅ 交互动画增强
|
- ✅ 交互动画增强
|
||||||
- ✅ 主题色彩统一
|
- ✅ 主题色彩统一
|
||||||
|
- ✅ WebSocket实时聊天系统
|
||||||
|
- ✅ STOMP协议集成
|
||||||
|
|
||||||
### 🎯 下一步计划
|
### 🎯 下一步计划
|
||||||
- [ ] 添加设置页面功能
|
- [ ] 添加设置页面功能
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@stomp/stompjs": "^7.1.1",
|
||||||
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"element-plus": "^2.10.4",
|
"element-plus": "^2.10.4",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"sockjs-client": "^1.6.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
|
|
108
src/App.vue
108
src/App.vue
|
@ -16,6 +16,12 @@ const updateActiveIndex = () => {
|
||||||
case '/about':
|
case '/about':
|
||||||
activeIndex.value = '2'
|
activeIndex.value = '2'
|
||||||
break
|
break
|
||||||
|
case '/websocket':
|
||||||
|
activeIndex.value = '3'
|
||||||
|
break
|
||||||
|
case '/test':
|
||||||
|
activeIndex.value = '4'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
activeIndex.value = '1'
|
activeIndex.value = '1'
|
||||||
}
|
}
|
||||||
|
@ -40,8 +46,10 @@ const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
router.push('/about')
|
router.push('/about')
|
||||||
break
|
break
|
||||||
case '3':
|
case '3':
|
||||||
// 设置页面暂时跳转到首页
|
router.push('/websocket')
|
||||||
router.push('/')
|
break
|
||||||
|
case '4':
|
||||||
|
router.push('/test')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,103 +65,25 @@ const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
>
|
>
|
||||||
<el-menu-item index="1">
|
<el-menu-item index="1">
|
||||||
<el-icon><house /></el-icon>
|
<el-icon><House /></el-icon>
|
||||||
<span>首页</span>
|
<span>首页</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="2">
|
<el-menu-item index="2">
|
||||||
<el-icon><document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<span>作业</span>
|
<span>关于</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="3">
|
<el-menu-item index="3">
|
||||||
<el-icon><setting /></el-icon>
|
<el-icon><Link /></el-icon>
|
||||||
<span>设置</span>
|
<span>WebSocket 演示</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="4">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
<span>WebSocket 测试</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|
||||||
<!-- 主要内容区域 -->
|
<!-- 主要内容区域 -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<div class="welcome-section">
|
|
||||||
<el-card class="welcome-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>欢迎使用 Vue 3 + ElementPlus</span>
|
|
||||||
<el-button type="primary" class="button">
|
|
||||||
<el-icon><plus /></el-icon>
|
|
||||||
新建项目
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="demo-header">
|
|
||||||
<el-icon><star /></el-icon>
|
|
||||||
<span>组件丰富</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p>ElementPlus 提供了丰富的组件库,满足各种业务需求</p>
|
|
||||||
<el-button type="primary" size="small">了解更多</el-button>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="demo-header">
|
|
||||||
<el-icon><magic-stick /></el-icon>
|
|
||||||
<span>设计精美</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p>基于现代设计理念,提供美观的用户界面</p>
|
|
||||||
<el-button type="success" size="small">查看设计</el-button>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="demo-header">
|
|
||||||
<el-icon><cpu /></el-icon>
|
|
||||||
<span>高性能</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p>基于Vue 3构建,享受最新的响应式系统带来的性能提升</p>
|
|
||||||
<el-button type="warning" size="small">性能测试</el-button>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-components">
|
|
||||||
<h3>组件演示</h3>
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="component-demo">
|
|
||||||
<el-space wrap>
|
|
||||||
<el-button>默认按钮</el-button>
|
|
||||||
<el-button type="primary">主要按钮</el-button>
|
|
||||||
<el-button type="success">成功按钮</el-button>
|
|
||||||
<el-button type="info">信息按钮</el-button>
|
|
||||||
<el-button type="warning">警告按钮</el-button>
|
|
||||||
<el-button type="danger">危险按钮</el-button>
|
|
||||||
</el-space>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
title="恭喜!ElementPlus 已成功引入到您的 Vue 3 项目中"
|
|
||||||
type="success"
|
|
||||||
:closable="false"
|
|
||||||
show-icon>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 路由视图 -->
|
<!-- 路由视图 -->
|
||||||
<div class="router-view">
|
<div class="router-view">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
|
|
|
@ -0,0 +1,576 @@
|
||||||
|
<template>
|
||||||
|
<div class="websocket-chat">
|
||||||
|
<el-card class="chat-container">
|
||||||
|
<template #header>
|
||||||
|
<div class="chat-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-icon size="24" class="chat-icon"><chat-dot-round /></el-icon>
|
||||||
|
<div class="header-text">
|
||||||
|
<h3 class="chat-title">WebSocket 实时聊天</h3>
|
||||||
|
<p class="chat-subtitle">基于STOMP协议的实时通讯演示</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="connection-status">
|
||||||
|
<el-tag :type="connectionStatus === 'connected' ? 'success' : 'danger'" size="large">
|
||||||
|
<el-icon><circle-check v-if="connectionStatus === 'connected'" /><circle-close v-else /></el-icon>
|
||||||
|
{{ connectionStatusText }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 连接配置区域 -->
|
||||||
|
<div class="connection-config" v-if="connectionStatus === 'disconnected'">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="config-item">
|
||||||
|
<label class="config-label">
|
||||||
|
<el-icon><link /></el-icon>
|
||||||
|
服务器地址
|
||||||
|
</label>
|
||||||
|
<el-input
|
||||||
|
v-model="config.host"
|
||||||
|
placeholder="http://localhost:8080"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="config-item">
|
||||||
|
<label class="config-label">
|
||||||
|
<el-icon><connection /></el-icon>
|
||||||
|
WebSocket端点
|
||||||
|
</label>
|
||||||
|
<el-input
|
||||||
|
v-model="config.endpoint"
|
||||||
|
placeholder="/ws"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 15px;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="config-item">
|
||||||
|
<label class="config-label">
|
||||||
|
<el-icon><user /></el-icon>
|
||||||
|
用户名
|
||||||
|
</label>
|
||||||
|
<el-input
|
||||||
|
v-model="config.username"
|
||||||
|
placeholder="请输入您的用户名"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="config-item">
|
||||||
|
<label class="config-label">
|
||||||
|
<el-icon><chat-line-round /></el-icon>
|
||||||
|
聊天室
|
||||||
|
</label>
|
||||||
|
<el-input
|
||||||
|
v-model="config.room"
|
||||||
|
placeholder="general"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div class="connection-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="connectWebSocket"
|
||||||
|
:loading="connectionStatus === 'connecting'"
|
||||||
|
class="connect-btn"
|
||||||
|
>
|
||||||
|
<el-icon><connection /></el-icon>
|
||||||
|
{{ connectionStatus === 'connecting' ? '连接中...' : '连接到服务器' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
size="large"
|
||||||
|
@click="startMockMode"
|
||||||
|
class="mock-btn"
|
||||||
|
>
|
||||||
|
<el-icon><cpu /></el-icon>
|
||||||
|
演示模式(模拟连接)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 聊天区域 -->
|
||||||
|
<div class="chat-area" v-if="connectionStatus === 'connected'">
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-list" ref="messageListRef">
|
||||||
|
<div
|
||||||
|
v-for="(message, index) in messages"
|
||||||
|
:key="index"
|
||||||
|
class="message-item"
|
||||||
|
:class="{ 'own-message': message.sender === config.username }"
|
||||||
|
>
|
||||||
|
<div class="message-avatar">
|
||||||
|
<el-avatar :size="40">
|
||||||
|
{{ message.sender.charAt(0).toUpperCase() }}
|
||||||
|
</el-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-header">
|
||||||
|
<span class="message-sender">{{ message.sender }}</span>
|
||||||
|
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-text">{{ message.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length === 0" class="empty-messages">
|
||||||
|
<el-empty description="还没有消息,开始聊天吧!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="input-area">
|
||||||
|
<el-input
|
||||||
|
v-model="newMessage"
|
||||||
|
placeholder="输入消息..."
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
|
class="message-input"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="sendMessage"
|
||||||
|
:disabled="!newMessage.trim()"
|
||||||
|
>
|
||||||
|
<el-icon><promotion /></el-icon>
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作区域 -->
|
||||||
|
<div class="chat-actions">
|
||||||
|
<el-button @click="clearMessages" size="small">
|
||||||
|
<el-icon><delete /></el-icon>
|
||||||
|
清空消息
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="disconnectWebSocket" type="danger" size="small">
|
||||||
|
<el-icon><close /></el-icon>
|
||||||
|
断开连接
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, nextTick, onUnmounted, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import StompClient from '../util/websocket'
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected'
|
||||||
|
const connectionStatus = ref<ConnectionStatus>('disconnected')
|
||||||
|
|
||||||
|
// 配置信息
|
||||||
|
const config = reactive({
|
||||||
|
host: 'http://localhost:8080',
|
||||||
|
endpoint: '/ws',
|
||||||
|
username: 'User' + Math.floor(Math.random() * 1000),
|
||||||
|
room: 'general'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 消息相关
|
||||||
|
interface Message {
|
||||||
|
sender: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type?: 'system' | 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = ref<Message[]>([])
|
||||||
|
const newMessage = ref('')
|
||||||
|
const messageListRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
// WebSocket客户端
|
||||||
|
let stompClient: StompClient | null = null
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const connectionStatusText = computed(() => {
|
||||||
|
switch (connectionStatus.value) {
|
||||||
|
case 'connected': return '已连接'
|
||||||
|
case 'connecting': return '连接中'
|
||||||
|
case 'disconnected': return '未连接'
|
||||||
|
default: return '未知状态'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接WebSocket
|
||||||
|
const connectWebSocket = async () => {
|
||||||
|
if (!config.host || !config.endpoint || !config.username) {
|
||||||
|
ElMessage.error('请填写完整的连接信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionStatus.value = 'connecting'
|
||||||
|
|
||||||
|
try {
|
||||||
|
stompClient = new StompClient({
|
||||||
|
host: config.host,
|
||||||
|
endpoint: config.endpoint,
|
||||||
|
sendPath: `/app/chat.${config.room}`,
|
||||||
|
useSockJS: true,
|
||||||
|
|
||||||
|
onConnect: () => {
|
||||||
|
connectionStatus.value = 'connected'
|
||||||
|
ElMessage.success('WebSocket连接成功!')
|
||||||
|
|
||||||
|
// 发送加入房间消息
|
||||||
|
addSystemMessage(`${config.username} 加入了聊天室`)
|
||||||
|
|
||||||
|
// 订阅聊天室消息
|
||||||
|
stompClient?.subscribe(`/topic/chat.${config.room}`, (message) => {
|
||||||
|
addMessage(message.sender, message.content, message.timestamp)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
connectionStatus.value = 'disconnected'
|
||||||
|
ElMessage.error('WebSocket连接失败: ' + error.toString())
|
||||||
|
console.error('WebSocket错误:', error)
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage: (msg, path) => {
|
||||||
|
console.log('收到消息:', msg, '来自:', path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stompClient.connect()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
connectionStatus.value = 'disconnected'
|
||||||
|
ElMessage.error('连接失败: ' + error)
|
||||||
|
console.error('连接错误:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟模式(用于演示)
|
||||||
|
const startMockMode = () => {
|
||||||
|
connectionStatus.value = 'connected'
|
||||||
|
ElMessage.success('已启动演示模式')
|
||||||
|
|
||||||
|
addSystemMessage('已进入演示模式,消息将在本地模拟')
|
||||||
|
|
||||||
|
// 模拟一些初始消息
|
||||||
|
setTimeout(() => {
|
||||||
|
addMessage('Bot', '欢迎使用WebSocket聊天演示!', Date.now())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addMessage('System', '这是一个基于STOMP协议的实时聊天示例', Date.now())
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (!newMessage.value.trim()) return
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
sender: config.username,
|
||||||
|
content: newMessage.value.trim(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
room: config.room
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stompClient) {
|
||||||
|
// 真实WebSocket发送
|
||||||
|
stompClient.send(message)
|
||||||
|
} else {
|
||||||
|
// 演示模式,本地添加消息
|
||||||
|
addMessage(message.sender, message.content, message.timestamp)
|
||||||
|
|
||||||
|
// 模拟回复
|
||||||
|
setTimeout(() => {
|
||||||
|
const replies = [
|
||||||
|
'收到您的消息!',
|
||||||
|
'这是一个自动回复',
|
||||||
|
'很有趣的观点!',
|
||||||
|
'我同意您的看法',
|
||||||
|
'让我想想...'
|
||||||
|
]
|
||||||
|
const randomReply = replies[Math.floor(Math.random() * replies.length)]
|
||||||
|
addMessage('Echo Bot', randomReply, Date.now())
|
||||||
|
}, 1000 + Math.random() * 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
const addMessage = (sender: string, content: string, timestamp: number) => {
|
||||||
|
messages.value.push({
|
||||||
|
sender,
|
||||||
|
content,
|
||||||
|
timestamp,
|
||||||
|
type: 'user'
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加系统消息
|
||||||
|
const addSystemMessage = (content: string) => {
|
||||||
|
messages.value.push({
|
||||||
|
sender: 'System',
|
||||||
|
content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'system'
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (messageListRef.value) {
|
||||||
|
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空消息
|
||||||
|
const clearMessages = () => {
|
||||||
|
messages.value = []
|
||||||
|
ElMessage.success('消息已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
const disconnectWebSocket = () => {
|
||||||
|
if (stompClient) {
|
||||||
|
stompClient.disconnect()
|
||||||
|
stompClient = null
|
||||||
|
}
|
||||||
|
connectionStatus.value = 'disconnected'
|
||||||
|
messages.value = []
|
||||||
|
ElMessage.info('已断开连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (stompClient) {
|
||||||
|
stompClient.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.websocket-chat {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-icon {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-subtitle {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-config {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fc;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn {
|
||||||
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mock-btn {
|
||||||
|
background: linear-gradient(45deg, #67c23a, #85ce61);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #e8ecf4;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover {
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.own-message {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.own-message .message-content {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.own-message .message-header {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
color: #2c3e50;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-messages {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.message-input .el-input-group__append) {
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status .el-tag {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.message-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c4c7;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8abad;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,426 @@
|
||||||
|
<template>
|
||||||
|
<div class="websocket-demo">
|
||||||
|
<el-card class="demo-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>WebSocket 演示</span>
|
||||||
|
<el-tag :type="connectionStatus.type">{{ connectionStatus.text }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 连接配置 -->
|
||||||
|
<el-form :model="config" label-width="120px" class="config-form">
|
||||||
|
<el-form-item label="服务器地址:">
|
||||||
|
<el-input
|
||||||
|
v-model="config.host"
|
||||||
|
placeholder="http://localhost:8080"
|
||||||
|
:disabled="isConnected"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="WebSocket端点:">
|
||||||
|
<el-input
|
||||||
|
v-model="config.endpoint"
|
||||||
|
placeholder="/ws"
|
||||||
|
:disabled="isConnected"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用SockJS:">
|
||||||
|
<el-switch
|
||||||
|
v-model="config.useSockJS"
|
||||||
|
:disabled="isConnected"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
v-if="!isConnected"
|
||||||
|
type="primary"
|
||||||
|
@click="connect"
|
||||||
|
:loading="connecting"
|
||||||
|
>
|
||||||
|
连接
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="danger"
|
||||||
|
@click="disconnect"
|
||||||
|
>
|
||||||
|
断开连接
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 订阅管理 -->
|
||||||
|
<div class="subscription-section">
|
||||||
|
<h3>订阅管理</h3>
|
||||||
|
<el-form inline class="subscribe-form">
|
||||||
|
<el-form-item label="订阅路径:">
|
||||||
|
<el-input
|
||||||
|
v-model="newSubscriptionPath"
|
||||||
|
placeholder="/topic/messages"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="subscribe"
|
||||||
|
:disabled="!isConnected || !newSubscriptionPath"
|
||||||
|
>
|
||||||
|
订阅
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="subscriptions-list" v-if="subscriptions.length > 0">
|
||||||
|
<el-tag
|
||||||
|
v-for="path in subscriptions"
|
||||||
|
:key="path"
|
||||||
|
closable
|
||||||
|
@close="unsubscribe(path)"
|
||||||
|
class="subscription-tag"
|
||||||
|
>
|
||||||
|
{{ path }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 消息发送 -->
|
||||||
|
<div class="send-section">
|
||||||
|
<h3>发送消息</h3>
|
||||||
|
<el-form inline class="send-form">
|
||||||
|
<el-form-item label="发送路径:">
|
||||||
|
<el-input
|
||||||
|
v-model="sendPath"
|
||||||
|
placeholder="/app/send"
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="消息内容:">
|
||||||
|
<el-input
|
||||||
|
v-model="messageContent"
|
||||||
|
placeholder="输入消息内容"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="sendMessage"
|
||||||
|
:disabled="!isConnected || !sendPath || !messageContent"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 消息日志 -->
|
||||||
|
<div class="message-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>消息日志</h3>
|
||||||
|
<el-button size="small" @click="clearMessages">清空</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-list">
|
||||||
|
<div
|
||||||
|
v-for="(msg, index) in messages"
|
||||||
|
:key="index"
|
||||||
|
:class="['message-item', msg.type]"
|
||||||
|
>
|
||||||
|
<div class="message-time">{{ msg.time }}</div>
|
||||||
|
<div class="message-type">{{ msg.type }}</div>
|
||||||
|
<div class="message-path" v-if="msg.path">{{ msg.path }}</div>
|
||||||
|
<div class="message-content">{{ msg.content }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length === 0" class="no-messages">
|
||||||
|
暂无消息
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
|
import StompClient from '@/util/websocket'
|
||||||
|
|
||||||
|
// 连接配置
|
||||||
|
const config = ref({
|
||||||
|
host: 'http://localhost:8080',
|
||||||
|
endpoint: '/ws',
|
||||||
|
useSockJS: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const connecting = ref(false)
|
||||||
|
let stompClient: StompClient | null = null
|
||||||
|
|
||||||
|
// 订阅管理
|
||||||
|
const newSubscriptionPath = ref('/topic/messages')
|
||||||
|
const subscriptions = ref<string[]>([])
|
||||||
|
|
||||||
|
// 消息发送
|
||||||
|
const sendPath = ref('/app/send')
|
||||||
|
const messageContent = ref('')
|
||||||
|
|
||||||
|
// 消息日志
|
||||||
|
interface Message {
|
||||||
|
time: string
|
||||||
|
type: 'sent' | 'received' | 'info' | 'error'
|
||||||
|
path?: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = ref<Message[]>([])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const connectionStatus = computed(() => {
|
||||||
|
if (connecting.value) {
|
||||||
|
return { type: 'warning', text: '连接中...' }
|
||||||
|
}
|
||||||
|
return isConnected.value
|
||||||
|
? { type: 'success', text: '已连接' }
|
||||||
|
: { type: 'info', text: '未连接' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加消息到日志
|
||||||
|
const addMessage = (type: Message['type'], content: string, path?: string) => {
|
||||||
|
messages.value.unshift({
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
path
|
||||||
|
})
|
||||||
|
|
||||||
|
// 限制消息数量
|
||||||
|
if (messages.value.length > 100) {
|
||||||
|
messages.value = messages.value.slice(0, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接 WebSocket
|
||||||
|
const connect = async () => {
|
||||||
|
if (connecting.value || isConnected.value) return
|
||||||
|
|
||||||
|
connecting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
stompClient = new StompClient({
|
||||||
|
host: config.value.host,
|
||||||
|
endpoint: config.value.endpoint,
|
||||||
|
useSockJS: config.value.useSockJS,
|
||||||
|
onConnect: () => {
|
||||||
|
isConnected.value = true
|
||||||
|
connecting.value = false
|
||||||
|
addMessage('info', `成功连接到 ${config.value.host}${config.value.endpoint}`)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
isConnected.value = false
|
||||||
|
connecting.value = false
|
||||||
|
addMessage('error', `连接错误: ${JSON.stringify(error)}`)
|
||||||
|
},
|
||||||
|
onMessage: (msg, path) => {
|
||||||
|
addMessage('received', JSON.stringify(msg, null, 2), path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stompClient.connect()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
connecting.value = false
|
||||||
|
addMessage('error', `连接失败: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
const disconnect = () => {
|
||||||
|
if (stompClient) {
|
||||||
|
stompClient.disconnect()
|
||||||
|
stompClient = null
|
||||||
|
}
|
||||||
|
isConnected.value = false
|
||||||
|
subscriptions.value = []
|
||||||
|
addMessage('info', '已断开连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅消息
|
||||||
|
const subscribe = () => {
|
||||||
|
if (!stompClient || !newSubscriptionPath.value) return
|
||||||
|
|
||||||
|
const path = newSubscriptionPath.value.trim()
|
||||||
|
if (subscriptions.value.includes(path)) {
|
||||||
|
addMessage('error', `已经订阅了路径: ${path}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stompClient.subscribe(path, (msg) => {
|
||||||
|
// 消息已在 onMessage 回调中处理
|
||||||
|
})
|
||||||
|
|
||||||
|
subscriptions.value.push(path)
|
||||||
|
addMessage('info', `订阅路径: ${path}`)
|
||||||
|
newSubscriptionPath.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
const unsubscribe = (path: string) => {
|
||||||
|
if (!stompClient) return
|
||||||
|
|
||||||
|
stompClient.unsubscribe(path)
|
||||||
|
subscriptions.value = subscriptions.value.filter(p => p !== path)
|
||||||
|
addMessage('info', `取消订阅: ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (!stompClient || !sendPath.value || !messageContent.value) return
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
content: messageContent.value,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
sender: 'WebSocket Demo'
|
||||||
|
}
|
||||||
|
|
||||||
|
stompClient.send(message, sendPath.value)
|
||||||
|
addMessage('sent', JSON.stringify(message, null, 2), sendPath.value)
|
||||||
|
messageContent.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空消息
|
||||||
|
const clearMessages = () => {
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时断开连接
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.websocket-demo {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-section h3,
|
||||||
|
.send-section h3,
|
||||||
|
.message-section h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-form,
|
||||||
|
.send-form {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscriptions-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-tag {
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.sent {
|
||||||
|
border-left-color: #67c23a;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.received {
|
||||||
|
border-left-color: #409eff;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.info {
|
||||||
|
border-left-color: #909399;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.error {
|
||||||
|
border-left-color: #f56c6c;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-type {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-messages {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,223 @@
|
||||||
|
<template>
|
||||||
|
<div class="usage-guide">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<h2>WebSocket 使用说明</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-collapse v-model="activeNames">
|
||||||
|
<el-collapse-item title="1. 基本使用" name="1">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>引入和创建实例:</h4>
|
||||||
|
<pre><code>import StompClient from '@/util/websocket'
|
||||||
|
|
||||||
|
// 创建 WebSocket 客户端实例
|
||||||
|
const stompClient = new StompClient({
|
||||||
|
host: 'http://localhost:8080', // 服务器地址
|
||||||
|
endpoint: '/ws', // WebSocket 端点
|
||||||
|
useSockJS: true, // 是否使用 SockJS(默认 false)
|
||||||
|
onConnect: () => { // 连接成功回调
|
||||||
|
console.log('WebSocket 连接成功')
|
||||||
|
},
|
||||||
|
onError: (error) => { // 错误回调
|
||||||
|
console.error('WebSocket 错误:', error)
|
||||||
|
},
|
||||||
|
onMessage: (msg, path) => { // 全局消息处理
|
||||||
|
console.log('收到消息:', msg, '来自:', path)
|
||||||
|
}
|
||||||
|
})</code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="2. 连接和断开" name="2">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>建立连接:</h4>
|
||||||
|
<pre><code>// 连接到 WebSocket 服务器
|
||||||
|
stompClient.connect()</code></pre>
|
||||||
|
|
||||||
|
<h4>断开连接:</h4>
|
||||||
|
<pre><code>// 断开连接并清理所有订阅
|
||||||
|
stompClient.disconnect()</code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="3. 订阅消息" name="3">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>订阅特定路径的消息:</h4>
|
||||||
|
<pre><code>// 订阅消息
|
||||||
|
stompClient.subscribe('/topic/messages', (message) => {
|
||||||
|
console.log('收到消息:', message)
|
||||||
|
// 处理接收到的消息
|
||||||
|
})</code></pre>
|
||||||
|
|
||||||
|
<h4>取消订阅:</h4>
|
||||||
|
<pre><code>// 取消订阅指定路径
|
||||||
|
stompClient.unsubscribe('/topic/messages')</code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="4. 发送消息" name="4">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>发送消息到指定路径:</h4>
|
||||||
|
<pre><code>// 发送消息
|
||||||
|
const messageData = {
|
||||||
|
content: 'Hello WebSocket!',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
sender: 'user123'
|
||||||
|
}
|
||||||
|
|
||||||
|
stompClient.send(messageData, '/app/send')</code></pre>
|
||||||
|
|
||||||
|
<h4>使用默认发送路径:</h4>
|
||||||
|
<pre><code>// 创建客户端时设置默认发送路径
|
||||||
|
const stompClient = new StompClient({
|
||||||
|
host: 'http://localhost:8080',
|
||||||
|
endpoint: '/ws',
|
||||||
|
sendPath: '/app/send', // 默认发送路径
|
||||||
|
// ... 其他配置
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送消息(使用默认路径)
|
||||||
|
stompClient.send(messageData)</code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="5. 完整示例" name="5">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>在 Vue 组件中使用:</h4>
|
||||||
|
<pre><code><script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import StompClient from '@/util/websocket'
|
||||||
|
|
||||||
|
const messages = ref<any[]>([])
|
||||||
|
const isConnected = ref(false)
|
||||||
|
let stompClient: StompClient | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 创建 WebSocket 连接
|
||||||
|
stompClient = new StompClient({
|
||||||
|
host: 'http://localhost:8080',
|
||||||
|
endpoint: '/ws',
|
||||||
|
useSockJS: true,
|
||||||
|
onConnect: () => {
|
||||||
|
isConnected.value = true
|
||||||
|
console.log('连接成功')
|
||||||
|
|
||||||
|
// 连接成功后订阅消息
|
||||||
|
stompClient?.subscribe('/topic/messages', (message) => {
|
||||||
|
messages.value.push(message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
isConnected.value = false
|
||||||
|
console.error('连接错误:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stompClient.connect()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 组件卸载时断开连接
|
||||||
|
stompClient?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const sendMessage = (content: string) => {
|
||||||
|
if (stompClient && isConnected.value) {
|
||||||
|
stompClient.send({
|
||||||
|
content,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}, '/app/send')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script></code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="6. 服务器端配置示例" name="6">
|
||||||
|
<div class="code-example">
|
||||||
|
<h4>Spring Boot WebSocket 配置:</h4>
|
||||||
|
<pre><code>@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
// 启用简单的消息代理,消息的目的地前缀为 /topic 和 /queue
|
||||||
|
config.enableSimpleBroker("/topic", "/queue");
|
||||||
|
// 客户端发送消息的目的地前缀为 /app
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
// 注册 STOMP 端点
|
||||||
|
registry.addEndpoint("/ws")
|
||||||
|
.setAllowedOriginPatterns("*")
|
||||||
|
.withSockJS(); // 启用 SockJS 支持
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h4>消息处理控制器:</h4>
|
||||||
|
<pre><code>@Controller
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
@MessageMapping("/send")
|
||||||
|
@SendTo("/topic/messages")
|
||||||
|
public Message sendMessage(Message message) {
|
||||||
|
// 处理接收到的消息
|
||||||
|
message.setTimestamp(new Date());
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const activeNames = ref(['1'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.usage-guide {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example h4 {
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example pre {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example code {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__header) {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,6 +17,16 @@ const router = createRouter({
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/AboutView.vue'),
|
component: () => import('../views/AboutView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/websocket',
|
||||||
|
name: 'websocket',
|
||||||
|
component: () => import('../views/WebSocketDemoView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
name: 'test',
|
||||||
|
component: () => import('../views/TestView.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Client } from '@stomp/stompjs';
|
||||||
|
import type { IMessage, StompSubscription } from '@stomp/stompjs';
|
||||||
|
import SockJS from 'sockjs-client';
|
||||||
|
|
||||||
|
interface StompClientOptions {
|
||||||
|
endpoint: string; // WebSocket endpoint,例如:/ws
|
||||||
|
sendPath?: string; // 默认发送路径(可选)
|
||||||
|
host: string; // 服务器地址,例如:http://localhost:8080
|
||||||
|
useSockJS?: boolean; // 是否使用 SockJS(默认 false)
|
||||||
|
|
||||||
|
onConnect?: () => void; // 连接成功回调
|
||||||
|
onError?: (err: any) => void; // 错误回调
|
||||||
|
onMessage?: (msg: any, path: string) => void; // 全局消息处理回调(含路径)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StompClient {
|
||||||
|
private options: StompClientOptions;
|
||||||
|
private stompClient: Client;
|
||||||
|
private subscriptions: Map<string, StompSubscription> = new Map();
|
||||||
|
private messageHandlers: Map<string, (msg: any) => void> = new Map();
|
||||||
|
|
||||||
|
constructor(options: StompClientOptions) {
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
this.stompClient = new Client({
|
||||||
|
brokerURL: options.useSockJS
|
||||||
|
? undefined
|
||||||
|
: `ws://${options.host.replace(/^http(s)?:\/\//, '')}${options.endpoint}`,
|
||||||
|
webSocketFactory: options.useSockJS
|
||||||
|
? () => new SockJS(`${options.host}${options.endpoint}`)
|
||||||
|
: undefined,
|
||||||
|
reconnectDelay: 5000,
|
||||||
|
debug: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stompClient.onConnect = () => {
|
||||||
|
// 连接成功后,自动重新订阅所有路径
|
||||||
|
this.messageHandlers.forEach((handler, path) => {
|
||||||
|
const sub = this.stompClient.subscribe(path, (msg: IMessage) => {
|
||||||
|
const body = JSON.parse(msg.body);
|
||||||
|
handler(body);
|
||||||
|
this.options.onMessage?.(body, path);
|
||||||
|
});
|
||||||
|
this.subscriptions.set(path, sub);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.options.onConnect?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stompClient.onStompError = (frame) => {
|
||||||
|
console.error('STOMP 错误:', frame);
|
||||||
|
this.options.onError?.(frame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 建立连接 */
|
||||||
|
connect() {
|
||||||
|
this.stompClient.activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送消息,支持指定 destination,未指定则用默认 sendPath */
|
||||||
|
send(data: any, sendPath?: string) {
|
||||||
|
if (!this.stompClient.connected) {
|
||||||
|
console.warn('STOMP 未连接,无法发送消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = sendPath ?? this.options.sendPath;
|
||||||
|
if (!destination) {
|
||||||
|
console.warn('未提供发送路径');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stompClient.publish({
|
||||||
|
destination,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订阅指定路径 */
|
||||||
|
subscribe(path: string, handler: (msg: any) => void) {
|
||||||
|
this.messageHandlers.set(path, handler);
|
||||||
|
|
||||||
|
if (this.stompClient.connected) {
|
||||||
|
const sub = this.stompClient.subscribe(path, (msg: IMessage) => {
|
||||||
|
const body = JSON.parse(msg.body);
|
||||||
|
handler(body);
|
||||||
|
this.options.onMessage?.(body, path);
|
||||||
|
});
|
||||||
|
this.subscriptions.set(path, sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消订阅指定路径 */
|
||||||
|
unsubscribe(path: string) {
|
||||||
|
this.subscriptions.get(path)?.unsubscribe();
|
||||||
|
this.subscriptions.delete(path);
|
||||||
|
this.messageHandlers.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 断开连接并清理订阅 */
|
||||||
|
disconnect() {
|
||||||
|
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
||||||
|
this.subscriptions.clear();
|
||||||
|
this.messageHandlers.clear();
|
||||||
|
this.stompClient.deactivate();
|
||||||
|
console.log('STOMP 已断开连接');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import WebSocketChat from '../components/WebSocketChat.vue'
|
||||||
|
|
||||||
// 表格数据
|
// 表格数据
|
||||||
const tableData = ref([
|
const tableData = ref([
|
||||||
|
@ -268,6 +269,11 @@ const handleRowClick = (row: any) => {
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- WebSocket聊天演示 -->
|
||||||
|
<div class="websocket-section">
|
||||||
|
<WebSocketChat />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 添加用户对话框 -->
|
<!-- 添加用户对话框 -->
|
||||||
<el-dialog v-model="dialogVisible" title="添加新用户" width="500px">
|
<el-dialog v-model="dialogVisible" title="添加新用户" width="500px">
|
||||||
<el-form
|
<el-form
|
||||||
|
@ -935,4 +941,13 @@ const handleRowClick = (row: any) => {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* WebSocket聊天区域 */
|
||||||
|
.websocket-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #f0f2f5 0%, #e8ecf4 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 创建 WebSocket 客户端实例
|
||||||
|
import StompClient from "@/util/websocket.ts";
|
||||||
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
|
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const messages = ref<any[]>([])
|
||||||
|
let stompClient: StompClient | null = null
|
||||||
|
|
||||||
|
// 初始化 WebSocket 连接
|
||||||
|
const initWebSocket = () => {
|
||||||
|
// 如果已经连接,先断开
|
||||||
|
if (stompClient) {
|
||||||
|
stompClient.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
stompClient = new StompClient({
|
||||||
|
host: 'http://localhost:8080',
|
||||||
|
endpoint: '/ws',
|
||||||
|
useSockJS: true,
|
||||||
|
|
||||||
|
onConnect: () => {
|
||||||
|
console.log('WebSocket 连接成功')
|
||||||
|
isConnected.value = true
|
||||||
|
|
||||||
|
// 连接成功后订阅消息
|
||||||
|
stompClient?.subscribe('/topic/messages', (msg) => {
|
||||||
|
console.log('收到 messages 消息:', msg)
|
||||||
|
messages.value.unshift({
|
||||||
|
type: 'messages',
|
||||||
|
content: msg,
|
||||||
|
time: new Date().toLocaleTimeString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
stompClient?.subscribe('/topic/testMessages', (msg) => {
|
||||||
|
console.log('收到 testMessages 消息:', msg)
|
||||||
|
messages.value.unshift({
|
||||||
|
type: 'testMessages',
|
||||||
|
content: msg,
|
||||||
|
time: new Date().toLocaleTimeString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送测试消息
|
||||||
|
setTimeout(() => {
|
||||||
|
sendTestMessage()
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('WebSocket 错误:', error)
|
||||||
|
isConnected.value = false
|
||||||
|
},
|
||||||
|
|
||||||
|
// 注释掉全局 onMessage,避免重复处理
|
||||||
|
// onMessage: (msg, path) => {
|
||||||
|
// console.log('全局消息处理:', msg, '来自:', path)
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送测试消息
|
||||||
|
const sendTestMessage = () => {
|
||||||
|
if (stompClient && isConnected.value) {
|
||||||
|
stompClient.send({
|
||||||
|
content: '前端发送的测试消息 - ' + new Date().toLocaleTimeString()
|
||||||
|
}, '/app/send')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空消息
|
||||||
|
const clearMessages = () => {
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initWebSocket()
|
||||||
|
stompClient?.connect()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 组件卸载时断开连接
|
||||||
|
if (stompClient) {
|
||||||
|
stompClient.disconnect()
|
||||||
|
stompClient = null
|
||||||
|
}
|
||||||
|
isConnected.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="test-container">
|
||||||
|
<h2>WebSocket 测试页面</h2>
|
||||||
|
|
||||||
|
<!-- 连接状态 -->
|
||||||
|
<div class="status-section">
|
||||||
|
<el-tag :type="isConnected ? 'success' : 'danger'">
|
||||||
|
{{ isConnected ? '已连接' : '未连接' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="actions-section">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="sendTestMessage"
|
||||||
|
:disabled="!isConnected"
|
||||||
|
>
|
||||||
|
发送测试消息
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
@click="clearMessages"
|
||||||
|
>
|
||||||
|
清空消息
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="initWebSocket"
|
||||||
|
>
|
||||||
|
重新连接
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="messages-section">
|
||||||
|
<h3>消息记录 ({{ messages.length }})</h3>
|
||||||
|
<div class="message-list">
|
||||||
|
<div
|
||||||
|
v-for="(msg, index) in messages"
|
||||||
|
:key="index"
|
||||||
|
:class="['message-item', `type-${msg.type}`]"
|
||||||
|
>
|
||||||
|
<div class="message-header">
|
||||||
|
<span class="message-type">{{ msg.type }}</span>
|
||||||
|
<span class="message-time">{{ msg.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
{{ JSON.stringify(msg.content, null, 2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length === 0" class="no-messages">
|
||||||
|
暂无消息
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-section .el-button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-section h3 {
|
||||||
|
color: #409eff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #dcdfe6;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.type-messages {
|
||||||
|
border-left-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.type-testMessages {
|
||||||
|
border-left-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-type {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409eff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-messages {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="demo-view">
|
||||||
|
<div class="container">
|
||||||
|
<el-tabs v-model="activeTab" class="demo-tabs">
|
||||||
|
<el-tab-pane label="WebSocket 演示" name="demo">
|
||||||
|
<WebSocketDemo />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="使用说明" name="usage">
|
||||||
|
<WebSocketUsage />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import WebSocketDemo from '@/components/WebSocketDemo.vue'
|
||||||
|
import WebSocketUsage from '@/components/WebSocketUsage.vue'
|
||||||
|
|
||||||
|
const activeTab = ref('demo')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.demo-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-tabs {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,6 +4,7 @@
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
|
|
@ -17,4 +17,7 @@ export default defineConfig({
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>ws-begin</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- SpringBoot Web启动器 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringBoot WebSocket启动器 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringBoot 测试启动器 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class WebSocketApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(WebSocketApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.example.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcConfigurer corsConfigurer() {
|
||||||
|
return new WebMvcConfigurer() {
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOriginPatterns("*")
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.allowCredentials(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.example.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
// 启用简单的消息代理,消息的目的地前缀为 /topic 和 /queue
|
||||||
|
config.enableSimpleBroker("/topic", "/queue");
|
||||||
|
// 客户端发送消息的目的地前缀为 /app
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
// 注册 STOMP 端点,允许所有来源访问
|
||||||
|
registry.addEndpoint("/ws")
|
||||||
|
.setAllowedOriginPatterns("*")
|
||||||
|
.withSockJS(); // 启用 SockJS 支持
|
||||||
|
|
||||||
|
// 也注册一个不使用 SockJS 的端点(可选)
|
||||||
|
registry.addEndpoint("/ws")
|
||||||
|
.setAllowedOriginPatterns("*");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.example.controller;
|
||||||
|
|
||||||
|
import org.example.model.Message;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.messaging.handler.annotation.SendTo;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
@RestController
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
private final SimpMessagingTemplate simpMessagingTemplate;
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MessageController.class);
|
||||||
|
|
||||||
|
|
||||||
|
public MessageController(SimpMessagingTemplate simpMessagingTemplate) {
|
||||||
|
this.simpMessagingTemplate = simpMessagingTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/send")
|
||||||
|
@SendTo("/topic/messages")
|
||||||
|
public Message sendMessage(Message message) {
|
||||||
|
log.info("Received message: {}", message.getContent());
|
||||||
|
// 处理接收到的消息
|
||||||
|
message.setContent("Echo: " + message.getContent());
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sendMessage")
|
||||||
|
public String test() {
|
||||||
|
Message testMessage = new Message("Hello from server!");
|
||||||
|
simpMessagingTemplate.convertAndSend("/topic/messages", testMessage);
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/testMessages")
|
||||||
|
public String test2() {
|
||||||
|
Message testMessage = new Message("Hello from server2!");
|
||||||
|
simpMessagingTemplate.convertAndSend("/topic/testMessages", testMessage);
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.example.model;
|
||||||
|
|
||||||
|
public class Message {
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
public Message() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: websocket-stomp-app
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.example: INFO
|
||||||
|
org.springframework.web.socket: DEBUG
|
||||||
|
org.springframework.messaging: DEBUG
|
|
@ -0,0 +1,258 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket STOMP 消息应用</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.username-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.username-form input {
|
||||||
|
padding: 10px;
|
||||||
|
width: 200px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.username-form button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chat-area {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
height: 300px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
.message-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.message-content {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.message-form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.message-form input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px 0 0 5px;
|
||||||
|
}
|
||||||
|
.message-form button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.connecting {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.connected {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.disconnected {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="header">WebSocket STOMP 消息应用</h1>
|
||||||
|
|
||||||
|
<div class="username-form" id="usernameForm">
|
||||||
|
<input type="text" id="username" placeholder="输入您的用户名" maxlength="50">
|
||||||
|
<button onclick="connect()">连接</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" id="connectionStatus"></div>
|
||||||
|
|
||||||
|
<div class="chat-area" id="chatArea">
|
||||||
|
<div class="messages" id="messageArea"></div>
|
||||||
|
<div class="message-form">
|
||||||
|
<input type="text" id="messageInput" placeholder="输入消息..." maxlength="255">
|
||||||
|
<button onclick="sendMessage()">发送</button>
|
||||||
|
</div>
|
||||||
|
<button onclick="disconnect()" style="margin-top: 10px; background: #dc3545;">断开连接</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let stompClient = null;
|
||||||
|
let username = null;
|
||||||
|
|
||||||
|
function setConnectedStatus(connected) {
|
||||||
|
const status = document.getElementById('connectionStatus');
|
||||||
|
const usernameForm = document.getElementById('usernameForm');
|
||||||
|
const chatArea = document.getElementById('chatArea');
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
status.textContent = '已连接到服务器';
|
||||||
|
status.className = 'status connected';
|
||||||
|
usernameForm.style.display = 'none';
|
||||||
|
chatArea.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
status.textContent = '未连接';
|
||||||
|
status.className = 'status disconnected';
|
||||||
|
usernameForm.style.display = 'block';
|
||||||
|
chatArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
username = document.getElementById('username').value.trim();
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
const status = document.getElementById('connectionStatus');
|
||||||
|
status.textContent = '正在连接...';
|
||||||
|
status.className = 'status connecting';
|
||||||
|
|
||||||
|
const socket = new SockJS('/ws');
|
||||||
|
stompClient = Stomp.over(socket);
|
||||||
|
|
||||||
|
stompClient.connect({}, onConnected, onError);
|
||||||
|
} else {
|
||||||
|
alert('请输入用户名');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnected(frame) {
|
||||||
|
setConnectedStatus(true);
|
||||||
|
|
||||||
|
// 订阅消息频道
|
||||||
|
stompClient.subscribe('/topic/messages', onMessageReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
const status = document.getElementById('connectionStatus');
|
||||||
|
status.textContent = '连接失败: ' + error;
|
||||||
|
status.className = 'status disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
const messageContent = messageInput.value.trim();
|
||||||
|
|
||||||
|
if (messageContent && stompClient) {
|
||||||
|
const message = {
|
||||||
|
sender: username,
|
||||||
|
content: messageContent
|
||||||
|
};
|
||||||
|
|
||||||
|
stompClient.send("/app/send", {}, JSON.stringify(message));
|
||||||
|
messageInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessageReceived(payload) {
|
||||||
|
const message = JSON.parse(payload.body);
|
||||||
|
const messageArea = document.getElementById('messageArea');
|
||||||
|
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.classList.add('message');
|
||||||
|
|
||||||
|
const headerElement = document.createElement('div');
|
||||||
|
headerElement.classList.add('message-header');
|
||||||
|
headerElement.textContent = message.sender;
|
||||||
|
|
||||||
|
const contentElement = document.createElement('div');
|
||||||
|
contentElement.classList.add('message-content');
|
||||||
|
contentElement.textContent = message.content;
|
||||||
|
|
||||||
|
const timeElement = document.createElement('div');
|
||||||
|
timeElement.classList.add('message-time');
|
||||||
|
if (message.timestamp) {
|
||||||
|
const date = new Date(message.timestamp);
|
||||||
|
timeElement.textContent = date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
messageElement.appendChild(headerElement);
|
||||||
|
messageElement.appendChild(contentElement);
|
||||||
|
messageElement.appendChild(timeElement);
|
||||||
|
|
||||||
|
messageArea.appendChild(messageElement);
|
||||||
|
messageArea.scrollTop = messageArea.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (stompClient !== null) {
|
||||||
|
stompClient.disconnect();
|
||||||
|
}
|
||||||
|
setConnectedStatus(false);
|
||||||
|
document.getElementById('messageArea').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回车发送消息
|
||||||
|
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回车连接
|
||||||
|
document.getElementById('username').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
setConnectedStatus(false);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue