1518 lines
43 KiB
Vue
1518 lines
43 KiB
Vue
<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>
|