nursing_unit_vue/src/views/test/index copy.vue

1518 lines
43 KiB
Vue
Raw Normal View History

2025-12-29 08:50:07 +08:00
<template>
<div class="websocket-debugger">
<!-- 头部控制区域 -->
<a-card title="WebSocket调试工具" :bordered="false">
<template #extra>
<a-space>
<a-switch v-model:checked="autoReconnect" checked-children="自动重连" un-checked-children="手动重连" />
<a-button @click="getOnlineUsers" :loading="loadingUsers" :disabled="!isConnected">
<template #icon>
<ReloadOutlined />
</template>
刷新用户
</a-button>
</a-space>
</template>
<!-- 连接配置区域 -->
<div class="config-section">
<h3>连接配置</h3>
<a-form layout="horizontal" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="WebSocket地址">
<a-input v-model:value="wsConfig.url" placeholder="ws://localhost:8091/nursing-unit_101/sdWebsocket/" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="用户ID" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input v-model:value="wsConfig.userId" placeholder="输入用户ID" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="心跳间隔(秒)" :label-col="{ span: 10 }" :wrapper-col="{ span: 14 }">
<a-input-number v-model:value="wsConfig.heartbeat" :min="5" :max="300" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 控制按钮 -->
<div class="control-buttons">
<a-button type="primary" @click="connect" :disabled="isConnected || isConnecting" :loading="isConnecting">
{{ isConnecting ? '连接中...' : '连接' }}
</a-button>
<a-button @click="disconnect" :disabled="!isConnected && !isConnecting" style="margin-left: 10px;">
断开
</a-button>
<a-button @click="testPing" :disabled="!isConnected" style="margin-left: 10px;">
测试Ping
</a-button>
<a-button @click="clearAll" style="margin-left: 10px;">
清空
</a-button>
<a-button @click="toggleLogPanel" style="margin-left: 10px;">
{{ showLogPanel ? '隐藏日志' : '显示日志' }}
</a-button>
<!-- 用户管理按钮 -->
<a-button @click="showUserManager" :disabled="!isConnected" style="margin-left: 10px;" type="primary" ghost>
<template #icon>
<UserOutlined />
</template>
用户管理
</a-button>
</div>
<!-- 连接状态和用户信息 -->
<div class="connection-status">
<a-tag :color="statusColor">{{ statusText }}</a-tag>
<span style="margin-left: 20px;">
<a-tag color="blue">用户: {{ wsConfig.userId }}</a-tag>
<a-tag color="green">在线用户: {{ onlineUsers.length }}</a-tag>
<a-tag color="orange">选中用户: {{ selectedUsers.length }}</a-tag>
<a-tag color="purple">心跳: {{ wsConfig.heartbeat }}s</a-tag>
</span>
</div>
</div>
</a-card>
<!-- 主内容区域三列布局 -->
<div class="content-area">
<a-row :gutter="16">
<!-- 左侧用户列表 -->
<a-col :span="6">
<a-card title="在线用户" :bordered="false" style="height: 100%;">
<template #extra>
<a-space>
<a-tooltip title="全选/取消全选">
<a-checkbox v-model:checked="selectAllUsers" :indeterminate="indeterminate"
@change="onSelectAllChange" />
</a-tooltip>
<a-button type="link" size="small" @click="getOnlineUsers" :loading="loadingUsers">
刷新
</a-button>
</a-space>
</template>
<div class="user-list">
<div v-if="onlineUsers.length === 0" class="empty-user">
<a-empty description="暂无在线用户" />
</div>
<a-list v-else :data-source="onlineUsers" :loading="loadingUsers" size="small">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<div class="user-item">
<a-checkbox v-model:checked="item.checked"
@change="(e) => onUserSelectChange(item.userId, e.target.checked)" />
<a-tag :color="item.userId === wsConfig.userId ? 'red' : 'blue'" class="user-tag">
{{ item.userId === wsConfig.userId ? '当前' : '在线' }}
</a-tag>
<div class="user-info">
<div class="user-name">{{ item.userName || item.userId }}</div>
<div class="user-id" style="font-size: 10px; color: #999;">ID: {{ item.userId }}</div>
</div>
</div>
</template>
<template #description>
<div class="user-actions">
<a-space size="small">
<a-button type="link" size="small" @click="sendToUser(item.userId)" title="发送消息">
发送
</a-button>
<a-button type="link" size="small" danger @click="removeUser(item.userId)"
:disabled="item.userId === wsConfig.userId" title="移除用户">
移除
</a-button>
</a-space>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<!-- 选中用户操作栏 -->
<div v-if="selectedUsers.length > 0" class="selected-users-bar">
<div class="selected-count">
已选择 {{ selectedUsers.length }} 个用户
</div>
<div class="selected-actions">
<a-space>
<a-button type="primary" size="small" @click="sendToSelectedUsers">
发送消息
</a-button>
<a-button type="primary" size="small" @click="broadcastToSelectedUsers">
批量广播
</a-button>
<a-button size="small" @click="clearSelectedUsers">
取消选择
</a-button>
</a-space>
</div>
</div>
</div>
<!-- 用户统计 -->
<div class="user-stats">
<a-statistic title="在线用户" :value="onlineUsers.length" />
<a-statistic title="会话数" :value="sessionCount" />
</div>
</a-card>
</a-col>
<!-- 中间消息发送区 -->
<a-col :span="6">
<a-card title="发送消息" :bordered="false" style="height: 100%;">
<div class="send-area">
<!-- 接收用户选择 -->
<div class="receiver-section">
<a-form-item label="接收用户">
<div class="selected-users-display">
<a-tag v-for="userId in selectedUsers" :key="userId" closable @close="removeSelectedUser(userId)">
{{ getUserNameById(userId) }}
</a-tag>
<a-tag v-if="selectedUsers.length === 0" color="default">
全部用户
</a-tag>
</div>
</a-form-item>
<a-form-item label="发送方式">
<a-radio-group v-model:value="sendMode">
<a-radio value="single">单用户</a-radio>
<a-radio value="multiple">多用户</a-radio>
<a-radio value="broadcast">广播</a-radio>
</a-radio-group>
</a-form-item>
</div>
<!-- 消息输入 -->
<a-form layout="vertical">
<a-form-item label="消息类型">
<a-select v-model:value="messageType" style="width: 200px;">
<a-select-option value="text">文本</a-select-option>
<a-select-option value="json">JSON</a-select-option>
<a-select-option value="ping">心跳(Ping)</a-select-option>
<a-select-option value="command">命令</a-select-option>
</a-select>
</a-form-item>
<!-- 消息内容 -->
<a-form-item label="消息内容">
<a-textarea v-model:value="sendMessage" placeholder="输入要发送的消息" :rows="6" :disabled="!isConnected" />
</a-form-item>
<!-- 预定义消息 -->
<a-form-item label="快捷消息">
<div class="quick-messages">
<a-tag v-for="(msg, index) in quickMessages" :key="index" @click="selectQuickMessage(msg)"
style="cursor: pointer; margin-bottom: 5px;">
{{ msg.label }}
</a-tag>
</div>
</a-form-item>
<!-- 发送按钮 -->
<a-form-item>
<a-space>
<a-button type="primary" @click="sendMessageToUsers"
:disabled="!isConnected || !sendMessage.trim()">
{{ getSendButtonText() }}
</a-button>
<a-button @click="clearSend">清空输入</a-button>
<a-switch v-model:checked="autoSendPing" checked-children="自动ping" un-checked-children="手动ping" />
</a-space>
</a-form-item>
</a-form>
</div>
</a-card>
</a-col>
<!-- 右侧消息接收区 -->
<a-col :span="12">
<a-card title="消息记录" :bordered="false" style="height: 100%;" :extra="`消息数: ${messages.length}`">
<div class="receive-area">
<!-- 消息控制栏 -->
<div class="message-controls">
<a-space>
<a-checkbox v-model:checked="autoScroll">自动滚动</a-checkbox>
<a-checkbox v-model:checked="showTimestamp">显示时间</a-checkbox>
<a-checkbox v-model:checked="showSender">显示发送者</a-checkbox>
<a-checkbox v-model:checked="showReceiver">显示接收者</a-checkbox>
<a-select v-model:value="messageFilter" style="width: 120px;" size="small">
<a-select-option value="all">全部消息</a-select-option>
<a-select-option value="send">发送消息</a-select-option>
<a-select-option value="receive">接收消息</a-select-option>
<a-select-option value="system">系统消息</a-select-option>
<a-select-option value="broadcast">广播消息</a-select-option>
<a-select-option value="private">私聊消息</a-select-option>
</a-select>
<a-button size="small" @click="exportMessages">导出</a-button>
</a-space>
</div>
<!-- 消息列表 -->
<div class="message-list" ref="messageListRef">
<div v-for="(msg, index) in filteredMessages" :key="index"
:class="['message-item', `message-${msg.type}`]">
<div class="message-header">
<div class="message-info">
<span v-if="showTimestamp" class="message-time">{{ formatTime(msg.time) }}</span>
<a-tag :color="getMessageColor(msg.type)" size="small">
{{ getMessageTypeText(msg.type) }}
</a-tag>
<span v-if="showSender && msg.sender" class="message-sender">来自: {{ msg.sender }}</span>
<span v-if="showReceiver && msg.receiver" class="message-receiver">发送给: {{
Array.isArray(msg.receiver)
? msg.receiver.join(', ') : msg.receiver }}</span>
</div>
</div>
<div class="message-content">
<pre>{{ formatMessage(msg.content) }}</pre>
</div>
</div>
<div v-if="filteredMessages.length === 0" class="empty-message">
暂无消息记录
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 用户管理模态框 -->
<a-modal v-model:open="showUserModal" title="用户管理" width="600px" :footer="null">
<div class="user-manager" style="padding: 14px;">
<a-table :data-source="onlineUsers" :columns="userColumns" :loading="loadingUsers" rowKey="userId">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.userId === wsConfig.userId ? 'red' : 'green'">
{{ record.userId === wsConfig.userId ? '当前用户' : '在线' }}
</a-tag>
</template>
<template v-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="sendToUser(record.userId)">
发消息
</a-button>
<a-button type="link" size="small" @click="toggleUserSelection(record.userId)">
{{ record.checked ? '取消选择' : '选择' }}
</a-button>
</a-space>
</template>
</template>
</a-table>
<div class="user-manager-actions">
<a-space>
<a-button @click="getOnlineUsers">刷新列表</a-button>
<a-button type="primary" @click="sendToSelectedUsers" :disabled="selectedUsers.length === 0">
发送给选中用户 ({{ selectedUsers.length }})
</a-button>
<a-button @click="showUserModal = false">关闭</a-button>
</a-space>
</div>
</div>
</a-modal>
<!-- 发送消息模态框 -->
<a-modal v-model:open="showSendModal" :title="sendModalTitle" @ok="sendMessageToSelected"
@cancel="showSendModal = false">
<a-form layout="vertical">
<a-form-item label="接收用户">
<a-select v-model:value="modalSelectedUsers" mode="multiple" placeholder="请选择接收用户" style="width: 100%"
:options="userOptions" :max-tag-count="3" />
</a-form-item>
<a-form-item label="消息内容">
<a-textarea v-model:value="modalMessage" placeholder="输入要发送的消息" :rows="4" />
</a-form-item>
<a-form-item label="消息类型">
<a-radio-group v-model:value="modalMessageType">
<a-radio value="text">文本</a-radio>
<a-radio value="json">JSON</a-radio>
<a-radio value="command">命令</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
<!-- 日志面板 -->
<a-collapse v-if="showLogPanel" style="margin-top: 229px;">
<a-collapse-panel key="1" header="详细日志">
<div class="log-panel">
<div class="log-header">
<a-space>
<span>日志总数: {{ logs.length }}</span>
<a-button size="small" @click="clearLogs">清空日志</a-button>
<a-switch v-model:checked="logTimestamp" checked-children="时间戳" un-checked-children="无时间" />
<a-button size="small" @click="exportLogs">导出日志</a-button>
</a-space>
</div>
<div class="log-content">
<div v-for="(log, index) in logs" :key="index" :class="['log-item', `log-${log.level}`]">
<span v-if="logTimestamp" class="log-time">[{{ log.time }}]</span>
<span class="log-level">[{{ log.level.toUpperCase() }}]</span>
<span class="log-message">{{ log.message }}</span>
</div>
<div v-if="logs.length === 0" class="empty-log">
暂无日志
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
<!-- 底部统计 -->
<a-card style="margin-top: 20px;">
<a-row>
<a-col :span="3">
<a-statistic title="连接次数" :value="stats.connectCount" />
</a-col>
<a-col :span="3">
<a-statistic title="发送消息" :value="stats.sendCount" />
</a-col>
<a-col :span="3">
<a-statistic title="接收消息" :value="stats.receiveCount" />
</a-col>
<a-col :span="3">
<a-statistic title="私聊消息" :value="stats.privateCount" />
</a-col>
<a-col :span="3">
<a-statistic title="广播消息" :value="stats.broadcastCount" />
</a-col>
<a-col :span="3">
<a-statistic title="在线用户" :value="onlineUsers.length" />
</a-col>
<a-col :span="3">
<a-statistic title="连接时长" :value="formatDuration(stats.connectDuration)" />
</a-col>
<a-col :span="3">
<a-button @click="showStatsDetail" type="link">详细统计</a-button>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { UserOutlined, ReloadOutlined } from '@ant-design/icons-vue';
// WebSocket连接状态
const ws = ref(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const statusText = ref('未连接');
const statusColor = ref('default');
// 配置
const wsConfig = reactive({
url: 'ws://localhost:8091/nursing-unit_101/sdWebsocket/',
userId: '1990230015776468901',
heartbeat: 30
});
// 用户管理
const onlineUsers = ref([]);
const selectedUsers = ref([]);
const selectAllUsers = ref(false);
const indeterminate = ref(false);
const loadingUsers = ref(false);
const sessionCount = ref(0);
const showUserModal = ref(false);
const showSendModal = ref(false);
const sendModalTitle = ref('发送消息');
// 发送相关
const sendMessage = ref('');
const messageType = ref('text');
const sendMode = ref('single');
const modalSelectedUsers = ref([]);
const modalMessage = ref('');
const modalMessageType = ref('text');
// 消息记录
const messages = ref([]);
const messageFilter = ref('all');
const autoScroll = ref(true);
const showTimestamp = ref(true);
const showSender = ref(true);
const showReceiver = ref(true);
const showLogPanel = ref(true);
const messageListRef = ref(null);
// 日志
const logs = ref([]);
const logTimestamp = ref(true);
// 快捷消息
const quickMessages = ref([
{ label: 'Ping', value: 'ping' },
{ label: '检查连接', value: 'check' },
{ label: 'Hello', value: 'Hello WebSocket!' },
{ label: 'JSON消息', value: '{"cmd": "test", "data": "测试数据"}' },
{ label: '获取用户', value: '{"cmd": "getUsers"}' },
{ label: '广播消息', value: '{"cmd": "broadcast", "data": "广播消息"}' }
]);
// 统计
const stats = reactive({
connectCount: 0,
sendCount: 0,
receiveCount: 0,
privateCount: 0,
broadcastCount: 0,
connectStart: null,
connectDuration: 0
});
// 自动重连
const autoReconnect = ref(true);
const autoSendPing = ref(false);
let heartbeatTimer = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// 用户表格列
const userColumns = [
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
},
{
title: '用户姓名',
dataIndex: 'userName',
key: 'userName',
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
},
{
title: '状态',
key: 'status',
width: 100,
},
{
title: '操作',
key: 'actions',
width: 150,
},
];
// 计算属性
const filteredMessages = computed(() => {
if (messageFilter.value === 'all') {
return messages.value;
}
return messages.value.filter(msg => msg.type === messageFilter.value);
});
const userOptions = computed(() => {
return onlineUsers.value.map(user => ({
label: (user.userName || user.userId) + (user.userId === wsConfig.userId ? ' (当前)' : ''),
value: user.userId,
disabled: user.userId === wsConfig.userId
}));
});
// 添加日志
const addLog = (level, logMessage) => {
const logEntry = {
time: new Date().toLocaleTimeString(),
level,
message: logMessage
};
logs.value.unshift(logEntry);
};
// 添加消息记录
const addMessage = (type, content, sender, receiver) => {
const msg = {
time: new Date(),
type,
content: typeof content === 'object' ? JSON.stringify(content, null, 2) : content,
sender,
receiver
};
messages.value.unshift(msg);
// 更新统计
if (type === 'send') stats.sendCount++;
else if (type === 'receive') stats.receiveCount++;
else if (type === 'private') stats.privateCount++;
else if (type === 'broadcast') stats.broadcastCount++;
// 自动滚动
if (autoScroll.value) {
nextTick(() => {
if (messageListRef.value) {
messageListRef.value.scrollTop = messageListRef.value.scrollHeight;
}
});
}
};
// 连接WebSocket
const connect = async () => {
if (isConnecting.value || isConnected.value) return;
isConnecting.value = true;
statusText.value = '连接中...';
statusColor.value = 'processing';
addLog('info', `正在连接WebSocket...`);
try {
if (ws.value) ws.value.close();
const fullUrl = `${wsConfig.url}${wsConfig.userId}`.replace('http', 'ws');
ws.value = new WebSocket(fullUrl);
ws.value.onopen = (event) => {
isConnected.value = true;
isConnecting.value = false;
statusText.value = '已连接';
statusColor.value = 'success';
reconnectAttempts = 0;
stats.connectCount++;
stats.connectStart = Date.now();
addLog('success', '✅ WebSocket连接成功');
addMessage('system', '连接成功', '系统');
message.success('WebSocket连接成功');
startHeartbeat();
startConnectTimer();
getOnlineUsers();
};
ws.value.onmessage = (event) => {
const data = event.data;
addLog('receive', `收到消息: ${data}`);
try {
const jsonData = JSON.parse(data);
// 处理不同类型的消息
if (jsonData.type === 'userList') {
// 收到用户列表
updateOnlineUsers(jsonData.users || []);
} else if (jsonData.type === 'private') {
// 私聊消息
addMessage('private', data, jsonData.from, wsConfig.userId);
} else if (jsonData.type === 'broadcast') {
// 广播消息
addMessage('broadcast', data, jsonData.from, '所有人');
} else {
// 普通消息
addMessage('receive', data, '服务器');
}
} catch (e) {
// 非JSON消息
if (data === 'ping') {
addLog('heartbeat', '收到心跳响应');
} else {
addMessage('receive', data, '服务器');
}
}
};
ws.value.onclose = (event) => {
isConnected.value = false;
isConnecting.value = false;
statusText.value = '已断开';
statusColor.value = 'error';
addLog('warn', `连接关闭 (代码: ${event.code})`);
addMessage('system', '连接已断开', '系统');
stopHeartbeat();
stopConnectTimer();
if (autoReconnect.value && event.code !== 1000 && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
reconnectTimer = setTimeout(() => connect(), 3000);
}
};
ws.value.onerror = (error) => {
isConnecting.value = false;
statusText.value = '连接错误';
statusColor.value = 'error';
addLog('error', '连接错误');
message.error('连接失败');
};
} catch (error) {
isConnecting.value = false;
statusText.value = '连接异常';
statusColor.value = 'error';
addLog('error', `连接异常: ${error.message}`);
}
};
// 断开连接
const disconnect = () => {
if (ws.value) {
ws.value.close(1000, '用户主动断开');
}
isConnected.value = false;
statusText.value = '已断开';
statusColor.value = 'default';
onlineUsers.value = [];
selectedUsers.value = [];
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
stopHeartbeat();
stopConnectTimer();
};
// 获取在线用户
const getOnlineUsers = () => {
if (!isConnected.value) {
message.warning('请先连接WebSocket');
return;
}
loadingUsers.value = true;
addLog('info', '获取在线用户...');
// ✅ 只保留发送获取用户列表的命令
if (isConnected.value && ws.value) {
const cmd = { cmd: 'getUsers', sender: wsConfig.userId };
ws.value.send(JSON.stringify(cmd));
addMessage('send', JSON.stringify(cmd), wsConfig.userId, '服务器');
}
loadingUsers.value = false; // 确保加载状态结束
};
// 更新在线用户列表
const updateOnlineUsers = (users) => {
console.log("🌊 ~ updateOnlineUsers ~ users:", users)
const userList = users.map(user => {
// 如果是对象格式有employeesId和realname
2025-12-29 08:50:07 +08:00
if (user && typeof user === 'object') {
return {
userId: user.employeesId || user.userId || user.id || '',
2025-12-29 08:50:07 +08:00
userName: user.realname || user.username || user.userName || user.userId || '',
checked: selectedUsers.value.includes(user.employeesId || user.userId || user.id || ''),
2025-12-29 08:50:07 +08:00
// 保持原有字段
...user
};
}
// 如果是字符串用户ID
else if (typeof user === 'string') {
return {
userId: user,
userName: user,
checked: selectedUsers.value.includes(user)
};
}
// 其他格式
else {
return {
userId: '',
userName: '',
checked: false
};
}
});
onlineUsers.value = userList;
sessionCount.value = onlineUsers.value.length;
updateSelectAllState();
addLog('success', `更新在线用户列表,共 ${users.length} 个用户`);
};
// 全选/取消全选
const onSelectAllChange = (checked) => {
onlineUsers.value = onlineUsers.value.map(user => ({
...user,
checked
}));
selectedUsers.value = checked ? onlineUsers.value.map(u => u.userId) : [];
indeterminate.value = false;
};
// 用户选择变化
const onUserSelectChange = (userId, checked) => {
const user = onlineUsers.value.find(u => u.userId === userId);
if (user) user.checked = checked;
selectedUsers.value = onlineUsers.value.filter(u => u.checked).map(u => u.userId);
updateSelectAllState();
};
// 更新全选状态
const updateSelectAllState = () => {
const checkedCount = onlineUsers.value.filter(u => u.checked).length;
selectAllUsers.value = checkedCount === onlineUsers.value.length;
indeterminate.value = checkedCount > 0 && checkedCount < onlineUsers.value.length;
};
// 发送消息给单个用户
const sendToUser = (userId) => {
if (!isConnected.value) {
message.warning('请先连接WebSocket');
return;
}
modalSelectedUsers.value = [userId];
modalMessage.value = '';
modalMessageType.value = 'text';
sendModalTitle.value = `发送消息给 ${userId}`;
showSendModal.value = true;
};
// 发送消息给选中用户
const sendToSelectedUsers = () => {
if (selectedUsers.value.length === 0) {
message.warning('请先选择要发送的用户');
return;
}
modalSelectedUsers.value = [...selectedUsers.value];
modalMessage.value = '';
modalMessageType.value = 'text';
sendModalTitle.value = `发送消息给 ${selectedUsers.value.length} 个用户`;
showSendModal.value = true;
};
// 广播给选中用户
const broadcastToSelectedUsers = () => {
if (selectedUsers.value.length === 0) {
message.warning('请先选择要广播的用户');
return;
}
if (!sendMessage.value.trim()) {
message.warning('请输入要广播的消息');
return;
}
selectedUsers.value.forEach(userId => {
sendMessageToUser(userId, sendMessage.value);
});
addMessage('broadcast', sendMessage.value, wsConfig.userId, selectedUsers.value);
addLog('info', `广播消息给 ${selectedUsers.value.length} 个用户`);
message.success(`消息已发送给 ${selectedUsers.value.length} 个用户`);
};
// 发送消息给单个用户
const sendMessageToUser = (userId, msg) => {
if (!isConnected.value || !ws.value) {
message.warning('WebSocket未连接');
return false;
}
try {
const messageObj = {
type: 'private',
to: userId,
from: wsConfig.userId,
message: msg,
timestamp: Date.now()
};
ws.value.send(JSON.stringify(messageObj));
addMessage('private', JSON.stringify(messageObj, null, 2), wsConfig.userId, userId);
addLog('send', `发送私聊消息给 ${userId}`);
return true;
} catch (error) {
addLog('error', `发送给 ${userId} 失败: ${error.message}`);
return false;
}
};
// 发送消息(模态框确认)
const sendMessageToSelected = () => {
if (modalSelectedUsers.value.length === 0) {
message.warning('请选择接收用户');
return;
}
if (!modalMessage.value.trim()) {
message.warning('请输入消息内容');
return;
}
modalSelectedUsers.value.forEach(userId => {
sendMessageToUser(userId, modalMessage.value);
});
showSendModal.value = false;
modalMessage.value = '';
modalSelectedUsers.value = [];
message.success(`消息已发送给 ${modalSelectedUsers.value.length} 个用户`);
};
// 发送消息给用户
const sendMessageToUsers = () => {
if (!isConnected.value) {
message.warning('请先连接WebSocket');
return;
}
if (!sendMessage.value.trim()) {
message.warning('请输入消息内容');
return;
}
switch (sendMode.value) {
case 'single':
if (selectedUsers.value.length === 0) {
message.warning('请选择一个用户');
return;
}
sendMessageToUser(selectedUsers.value[0], sendMessage.value);
break;
case 'multiple':
if (selectedUsers.value.length === 0) {
message.warning('请选择要发送的用户');
return;
}
broadcastToSelectedUsers();
break;
case 'broadcast':
// 发送广播消息
try {
const broadcastMsg = {
type: 'broadcast',
from: wsConfig.userId,
message: sendMessage.value,
timestamp: Date.now()
};
if (ws.value) {
ws.value.send(JSON.stringify(broadcastMsg));
addMessage('broadcast', JSON.stringify(broadcastMsg, null, 2), wsConfig.userId, '所有人');
addLog('info', '发送广播消息');
message.success('广播消息已发送');
}
} catch (error) {
addLog('error', `广播消息发送失败: ${error.message}`);
}
break;
}
sendMessage.value = '';
};
// 获取发送按钮文本
const getSendButtonText = () => {
switch (sendMode.value) {
case 'single':
if (selectedUsers.value.length === 0) {
return '发送消息';
} else {
const userName = getUserNameById(selectedUsers.value[0]);
return `发送给 ${userName}`;
}
case 'multiple':
if (selectedUsers.value.length === 0) {
return '发送给选中用户';
} else {
return `发送给 ${selectedUsers.value.length} 个用户`;
}
case 'broadcast':
return '广播给所有人';
default:
return '发送消息';
}
};
// 移除选中用户
const removeSelectedUser = (userId) => {
const index = selectedUsers.value.indexOf(userId);
if (index > -1) {
selectedUsers.value.splice(index, 1);
const user = onlineUsers.value.find(u => u.userId === userId);
if (user) user.checked = false;
updateSelectAllState();
}
};
// 清除选中用户
const clearSelectedUsers = () => {
selectedUsers.value = [];
onlineUsers.value = onlineUsers.value.map(user => ({ ...user, checked: false }));
selectAllUsers.value = false;
indeterminate.value = false;
};
// 移除用户
const removeUser = (userId) => {
if (userId === wsConfig.userId) {
message.warning('不能移除当前用户');
return;
}
Modal.confirm({
title: '确认移除',
content: `确定要移除用户 ${userId} 吗?`,
onOk: () => {
onlineUsers.value = onlineUsers.value.filter(user => user.userId !== userId);
removeSelectedUser(userId);
addLog('info', `移除用户: ${userId}`);
message.success('用户已移除');
}
});
};
// 用户管理界面
const showUserManager = () => {
getOnlineUsers();
showUserModal.value = true;
};
// 切换用户选择
const toggleUserSelection = (userId) => {
const user = onlineUsers.value.find(u => u.userId === userId);
if (user) {
user.checked = !user.checked;
onUserSelectChange(userId, user.checked);
}
};
// 开始心跳
const startHeartbeat = () => {
stopHeartbeat();
if (autoSendPing.value) {
heartbeatTimer = setInterval(() => {
if (isConnected.value && ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.send('ping');
addMessage('system', '发送心跳ping', '系统');
}
}, wsConfig.heartbeat * 1000);
}
};
// 停止心跳
const stopHeartbeat = () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
};
// 开始连接时长计时
const startConnectTimer = () => {
stats.connectDuration = 0;
const timer = setInterval(() => {
if (isConnected.value) {
stats.connectDuration = Math.floor((Date.now() - stats.connectStart) / 1000);
} else {
clearInterval(timer);
}
}, 1000);
};
// 停止连接时长计时
const stopConnectTimer = () => {
if (stats.connectStart) {
stats.connectDuration = Math.floor((Date.now() - stats.connectStart) / 1000);
stats.connectStart = null;
}
};
// 格式化时长
const formatDuration = (seconds) => {
if (!seconds) return '0秒';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}${minutes}${secs}`;
} else if (minutes > 0) {
return `${minutes}${secs}`;
} else {
return `${secs}`;
}
};
// 格式化时间
const formatTime = (date) => {
if (!showTimestamp.value) return '';
return date.toLocaleTimeString();
};
// 格式化消息
const formatMessage = (content) => {
if (!content) return '';
try {
const obj = JSON.parse(content);
return JSON.stringify(obj, null, 2);
} catch (e) {
return content;
}
};
// 获取消息类型文本
const getMessageTypeText = (type) => {
const typeMap = {
send: '发送',
receive: '接收',
system: '系统',
private: '私聊',
broadcast: '广播'
};
return typeMap[type] || type;
};
// 获取消息颜色
const getMessageColor = (type) => {
const colors = {
send: 'blue',
receive: 'green',
system: 'orange',
private: 'purple',
broadcast: 'cyan'
};
return colors[type] || 'default';
};
// 选择快捷消息
const selectQuickMessage = (msg) => {
sendMessage.value = msg.value;
if (msg.value === 'ping' || msg.value === 'check') {
messageType.value = 'ping';
} else if (msg.value.startsWith('{') && msg.value.endsWith('}')) {
messageType.value = 'json';
}
};
// 清空输入
const clearSend = () => {
sendMessage.value = '';
};
// 清空消息
const clearMessages = () => {
messages.value = [];
stats.sendCount = 0;
stats.receiveCount = 0;
stats.privateCount = 0;
stats.broadcastCount = 0;
};
// 清空日志
const clearLogs = () => {
logs.value = [];
};
// 清空所有
const clearAll = () => {
clearMessages();
clearLogs();
message.success('已清空所有记录');
};
// 切换日志面板
const toggleLogPanel = () => {
showLogPanel.value = !showLogPanel.value;
};
// 导出消息
const exportMessages = () => {
const data = {
exportTime: new Date().toISOString(),
config: { ...wsConfig },
stats: { ...stats },
messages: messages.value
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `websocket-messages-${new Date().getTime()}.json`;
link.click();
URL.revokeObjectURL(url);
addLog('info', '消息记录已导出');
message.success('消息记录已导出');
};
// 导出日志
const exportLogs = () => {
const data = {
exportTime: new Date().toISOString(),
logs: logs.value
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `websocket-logs-${new Date().getTime()}.json`;
link.click();
URL.revokeObjectURL(url);
addLog('info', '日志已导出');
message.success('日志已导出');
};
// 显示统计详情
const showStatsDetail = () => {
Modal.info({
title: '详细统计信息',
width: 600,
content: `
<div style="font-family: monospace;">
<p><strong>连接统计:</strong></p>
<p>连接次数: ${stats.connectCount}</p>
<p>发送消息: ${stats.sendCount}</p>
<p>接收消息: ${stats.receiveCount}</p>
<p>私聊消息: ${stats.privateCount}</p>
<p>广播消息: ${stats.broadcastCount}</p>
<p>连接时长: ${formatDuration(stats.connectDuration)}</p>
<p><strong>用户统计:</strong></p>
<p>在线用户: ${onlineUsers.value.length}</p>
<p>选中用户: ${selectedUsers.value.length}</p>
<p><strong>消息统计:</strong></p>
<p>消息总数: ${messages.value.length}</p>
<p>日志条数: ${logs.value.length}</p>
</div>
`,
});
};
// 测试Ping
const testPing = () => {
if (!isConnected.value || !ws.value) {
message.warning('WebSocket未连接');
return;
}
ws.value.send('ping');
addMessage('send', 'ping', wsConfig.userId, '服务器');
addLog('info', '发送ping测试');
message.success('ping已发送');
};
// 监听自动ping
watch(autoSendPing, (newVal) => {
if (newVal && isConnected.value) {
startHeartbeat();
} else {
stopHeartbeat();
}
});
// 根据用户ID获取用户姓名
const getUserNameById = (userId) => {
const user = onlineUsers.value.find(u => u.userId === userId);
return user ? (user.userName || user.userId) : userId;
};
// 组件卸载时清理
onBeforeUnmount(() => {
disconnect();
clearAll();
});
// 模拟在线用户列表(实际应该从服务器获取)
onMounted(() => {
});
</script>
<style scoped>
.websocket-debugger {
padding: 20px;
}
.config-section {
margin-bottom: 20px;
}
.config-section h3 {
margin-bottom: 16px;
color: #333;
}
.control-buttons {
margin: 20px 0;
}
.connection-status {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.content-area {
margin-top: 20px;
height: 500px;
}
/* 用户列表样式 */
.user-list {
height: 300px;
overflow-y: auto;
margin-bottom: 10px;
}
.user-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.user-info {
flex: 1;
min-width: 0;
/* 防止内容溢出 */
}
.user-name {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-tag {
min-width: 40px;
text-align: center;
}
.user-id {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-actions {
margin-top: 4px;
}
.empty-user {
text-align: center;
padding: 40px 0;
color: #999;
}
.selected-users-bar {
padding: 10px;
background: #f0f9ff;
border: 1px solid #91d5ff;
border-radius: 4px;
margin-top: 10px;
}
.selected-count {
font-weight: 500;
color: #1890ff;
margin-bottom: 8px;
}
.selected-users-display {
min-height: 32px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
background: #fafafa;
}
/* 消息区域 */
.send-area,
.receive-area {
height: 100%;
}
.receiver-section {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.message-controls {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.quick-messages {
margin-bottom: 10px;
}
.message-list {
height: 300px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 10px;
background: #fafafa;
}
.message-item {
margin-bottom: 10px;
padding: 8px;
border-radius: 4px;
background: white;
border-left: 4px solid #d9d9d9;
}
.message-send {
border-left-color: #1890ff;
background: #e6f7ff;
}
.message-receive {
border-left-color: #52c41a;
background: #f6ffed;
}
.message-system {
border-left-color: #faad14;
background: #fff7e6;
}
.message-private {
border-left-color: #722ed1;
background: #f9f0ff;
}
.message-broadcast {
border-left-color: #13c2c2;
background: #e6fffb;
}
.message-header {
margin-bottom: 5px;
}
.message-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #666;
flex-wrap: wrap;
}
.message-time {
font-family: monospace;
}
.message-sender,
.message-receiver {
font-size: 11px;
color: #8c8c8c;
}
.message-content pre {
margin: 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
}
.empty-message,
.empty-log {
text-align: center;
color: #999;
padding: 20px;
}
/* 用户统计 */
.user-stats {
display: flex;
justify-content: space-around;
padding: 10px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
/* 日志面板 */
.log-panel {
max-height: 300px;
overflow-y: auto;
}
.log-header {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.log-content {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
}
.log-item {
padding: 4px 8px;
border-bottom: 1px solid #f5f5f5;
}
.log-info {
color: #1890ff;
}
.log-success {
color: #52c41a;
}
.log-warn {
color: #faad14;
}
.log-error {
color: #ff4d4f;
}
.log-heartbeat {
color: #eb2f96;
}
.log-time {
margin-right: 8px;
color: #999;
}
.log-level {
margin-right: 8px;
font-weight: bold;
}
/* 用户管理 */
.user-manager-actions {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
text-align: right;
}
</style>