This commit is contained in:
Guwan 2025-08-01 00:03:53 +08:00
parent d22dde58fc
commit 69fd60ac3b
22 changed files with 2185 additions and 91 deletions

View File

@ -16,6 +16,13 @@
- **简单提示框** - 轻量级确认对话框
- **美化表单对话框** - 添加作业的高级表单界面
#### 3. WebSocket实时聊天系统
- **STOMP协议支持** - 基于STOMP协议的WebSocket连接
- **实时消息传输** - 支持实时双向通讯
- **聊天室功能** - 支持多房间聊天
- **演示模式** - 本地模拟聊天,无需后端服务器
- **美观聊天界面** - 现代化的聊天UI设计
### 🎨 界面美化升级
#### 1. 数据表格全面美化
@ -97,11 +104,17 @@
### 📦 新增依赖和配置
```json
{
"dependencies": {
"@stomp/stompjs": "^7.x.x",
"sockjs-client": "^1.x.x",
"@types/sockjs-client": "^1.x.x"
},
"features": {
"作业管理": "完整的学生作业管理系统",
"表格美化": "全面美化的数据表格组件",
"对话框美化": "多种风格的美观对话框",
"导航增强": "完善的路由导航系统"
"导航增强": "完善的路由导航系统",
"WebSocket聊天": "基于STOMP协议的实时聊天系统"
}
}
```
@ -126,6 +139,8 @@
- ✅ 响应式布局优化
- ✅ 交互动画增强
- ✅ 主题色彩统一
- ✅ WebSocket实时聊天系统
- ✅ STOMP协议集成
### 🎯 下一步计划
- [ ] 添加设置页面功能

View File

@ -15,8 +15,11 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@stomp/stompjs": "^7.1.1",
"@types/sockjs-client": "^1.5.4",
"element-plus": "^2.10.4",
"pinia": "^3.0.3",
"sockjs-client": "^1.6.1",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},

View File

@ -16,6 +16,12 @@ const updateActiveIndex = () => {
case '/about':
activeIndex.value = '2'
break
case '/websocket':
activeIndex.value = '3'
break
case '/test':
activeIndex.value = '4'
break
default:
activeIndex.value = '1'
}
@ -40,8 +46,10 @@ const handleSelect = (key: string, keyPath: string[]) => {
router.push('/about')
break
case '3':
//
router.push('/')
router.push('/websocket')
break
case '4':
router.push('/test')
break
}
}
@ -57,103 +65,25 @@ const handleSelect = (key: string, keyPath: string[]) => {
@select="handleSelect"
>
<el-menu-item index="1">
<el-icon><house /></el-icon>
<el-icon><House /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="2">
<el-icon><document /></el-icon>
<span>作业</span>
<el-icon><Document /></el-icon>
<span>关于</span>
</el-menu-item>
<el-menu-item index="3">
<el-icon><setting /></el-icon>
<span>设置</span>
<el-icon><Link /></el-icon>
<span>WebSocket 演示</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><ChatDotRound /></el-icon>
<span>WebSocket 测试</span>
</el-menu-item>
</el-menu>
<!-- 主要内容区域 -->
<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">
<RouterView />

View File

@ -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>

View File

@ -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>

View File

@ -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>&lt;script setup lang="ts"&gt;
import { ref, onMounted, onUnmounted } from 'vue'
import StompClient from '@/util/websocket'
const messages = ref&lt;any[]&gt;([])
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')
}
}
&lt;/script&gt;</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>

View File

@ -17,6 +17,16 @@ const router = createRouter({
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
{
path: '/websocket',
name: 'websocket',
component: () => import('../views/WebSocketDemoView.vue'),
},
{
path: '/test',
name: 'test',
component: () => import('../views/TestView.vue'),
},
],
})

109
src/util/websocket.ts Normal file
View File

@ -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 已断开连接');
}
}

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import WebSocketChat from '../components/WebSocketChat.vue'
//
const tableData = ref([
@ -268,6 +269,11 @@ const handleRowClick = (row: any) => {
</el-col>
</el-row>
<!-- WebSocket聊天演示 -->
<div class="websocket-section">
<WebSocketChat />
</div>
<!-- 添加用户对话框 -->
<el-dialog v-model="dialogVisible" title="添加新用户" width="500px">
<el-form
@ -935,4 +941,13 @@ const handleRowClick = (row: any) => {
transform: translateY(-1px);
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>

235
src/views/TestView.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -4,6 +4,7 @@
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"verbatimModuleSyntax": false,
"paths": {
"@/*": ["./src/*"]

View File

@ -17,4 +17,7 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
define: {
global: 'globalThis',
},
})

38
ws-begin/.gitignore vendored Normal file
View File

@ -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

56
ws-begin/pom.xml Normal file
View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
}
};
}
}

View File

@ -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("*");
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>