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

1518 lines
43 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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
if (user && typeof user === 'object') {
return {
userId: user.employeesId || user.userId || user.id || '',
userName: user.realname || user.username || user.userName || user.userId || '',
checked: selectedUsers.value.includes(user.employeesId || user.userId || user.id || ''),
// 保持原有字段
...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>