feat: 添加 WebSocket 支持和连接管理功能
This commit is contained in:
@@ -2,17 +2,79 @@ export default {
|
||||
async fetch(request, env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1')
|
||||
// 本地开发环境延迟处理
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
|
||||
// API 路由处理
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
await env.KV.put('last-api-call', `${Date.now()} ${request.method} ${url.pathname}`);
|
||||
await env.KV.put('events:api:last-call', `${new Date().toISOString()} ${request.method} ${url.pathname}`);
|
||||
|
||||
// 获取所有可用的键名
|
||||
const availableKeys = [
|
||||
'events:api:last-call',
|
||||
'events:ws:connection',
|
||||
'events:ws:message',
|
||||
'events:ws:disconnection'
|
||||
];
|
||||
|
||||
return Response.json({
|
||||
timestamp: Date.now(),
|
||||
lastApiCall: await env.KV.get('last-api-call'),
|
||||
lastApiCall: await env.KV.get('events:api:last-call'),
|
||||
availableKeys: availableKeys,
|
||||
storedData: {
|
||||
apiLastCall: await env.KV.get('events:api:last-call'),
|
||||
wsConnection: await env.KV.get('events:ws:connection'),
|
||||
wsMessage: await env.KV.get('events:ws:message'),
|
||||
wsDisconnection: await env.KV.get('events:ws:disconnection'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// WebSocket 连接处理
|
||||
if (url.pathname === '/ws') {
|
||||
const upgradeHeader = request.headers.get('Upgrade');
|
||||
if (upgradeHeader !== 'websocket') {
|
||||
return new Response('Expected websocket', { status: 400 });
|
||||
}
|
||||
|
||||
const [client, server] = Object.values(new WebSocketPair());
|
||||
|
||||
// 处理服务器端WebSocket消息
|
||||
server.accept();
|
||||
env.KV.put('events:ws:connection', `${new Date().toISOString()} ${url.pathname}`);
|
||||
|
||||
// accept 后立即发送欢迎消息
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
|
||||
server.send(`欢迎连接到WebSocket服务器!连接时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`);
|
||||
|
||||
server.addEventListener('message', async (event) => {
|
||||
console.log('收到客户端消息:', event.data);
|
||||
await env.KV.put('events:ws:message', `${new Date().toISOString()} ${event.data}`);
|
||||
|
||||
// 回复消息给客户端
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
|
||||
server.send(`服务器收到: ${event.data} (时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })})`)
|
||||
});
|
||||
|
||||
server.addEventListener('close', () => {
|
||||
console.log('WebSocket连接关闭');
|
||||
env.KV.put('events:ws:disconnection', `${new Date().toISOString()} ${url.pathname}`);
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 101,
|
||||
webSocket: client,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(null, { status: 404 });
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
@@ -12,25 +12,25 @@ const appStore = useAppStore();
|
||||
<BaseLayoutHeader v-model:collapsed="siderCollapse" />
|
||||
</template>
|
||||
<template #tab>
|
||||
<div class="bg-green-100 dark:bg-green-900 text-green-900 dark:text-green-100 p-4">
|
||||
<div class="bg-green-100/80 dark:bg-green-900/80 text-green-900 dark:text-green-100 p-4">
|
||||
2#GlobalTab
|
||||
</div>
|
||||
</template>
|
||||
<template #sider>
|
||||
<div
|
||||
class="bg-purple-100 dark:bg-purple-900 text-purple-900 dark:text-purple-100 p-4 h-full overflow-hidden"
|
||||
class="bg-purple-100/80 dark:bg-purple-900/80 text-purple-900 dark:text-purple-100 p-4 h-full overflow-hidden"
|
||||
>
|
||||
3#GlobalSider
|
||||
</div>
|
||||
</template>
|
||||
<div class="bg-yellow-100 dark:bg-yellow-900 text-yellow-900 dark:text-yellow-100 p-4">
|
||||
<div class="bg-yellow-100/80 dark:bg-yellow-900/80 text-yellow-900 dark:text-yellow-100 p-4">
|
||||
4#GlobalMenu
|
||||
</div>
|
||||
<!-- <div>GlobalContent</div> -->
|
||||
<RouterView />
|
||||
<!-- <div>ThemeDrawer</div> -->
|
||||
<template #footer>
|
||||
<div class="bg-red-100 dark:bg-red-900 text-red-900 dark:text-red-100 h-full">
|
||||
<div class="bg-red-100/80 dark:bg-red-900/80 text-red-900 dark:text-red-100 h-full">
|
||||
5#GlobalFooter
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue';
|
||||
|
||||
// API 调用相关状态
|
||||
const apiResult = ref<string>('');
|
||||
const loading = ref(false);
|
||||
|
||||
// WebSocket 相关状态
|
||||
const ws = ref<WebSocket | null>(null);
|
||||
const wsConnected = ref(false);
|
||||
const wsMessages = ref<string[]>([]);
|
||||
const messageInput = ref('');
|
||||
const wsLoading = ref(false);
|
||||
const connectionAttempts = ref(0);
|
||||
const maxReconnectAttempts = 3;
|
||||
|
||||
// 消息容器引用,用于自动滚动
|
||||
const messagesContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
// API 调用功能
|
||||
const callApi = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -16,24 +30,587 @@ const callApi = async () => {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// WebSocket 连接管理
|
||||
const connectWebSocket = async () => {
|
||||
if (ws.value?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
wsLoading.value = true;
|
||||
connectionAttempts.value++;
|
||||
|
||||
try {
|
||||
// 根据当前环境确定WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
ws.value = new WebSocket(wsUrl);
|
||||
|
||||
ws.value.onopen = () => {
|
||||
wsConnected.value = true;
|
||||
wsLoading.value = false;
|
||||
connectionAttempts.value = 0;
|
||||
wsMessages.value.push(`✅ WebSocket连接已建立 (${new Date().toLocaleTimeString()})`);
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
wsMessages.value.push(`📨 收到: ${event.data}`);
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
ws.value.onclose = (event) => {
|
||||
wsConnected.value = false;
|
||||
wsLoading.value = false;
|
||||
const reason = event.reason || '连接意外断开';
|
||||
wsMessages.value.push(
|
||||
`❌ WebSocket连接已关闭: ${reason} (${new Date().toLocaleTimeString()})`,
|
||||
);
|
||||
scrollToBottom();
|
||||
|
||||
// 自动重连逻辑
|
||||
if (connectionAttempts.value < maxReconnectAttempts && !event.wasClean) {
|
||||
setTimeout(() => {
|
||||
wsMessages.value.push(
|
||||
`🔄 尝试重新连接 (${connectionAttempts.value}/${maxReconnectAttempts})...`,
|
||||
);
|
||||
scrollToBottom();
|
||||
connectWebSocket();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
wsLoading.value = false;
|
||||
wsMessages.value.push(`⚠️ WebSocket连接错误: ${error} (${new Date().toLocaleTimeString()})`);
|
||||
scrollToBottom();
|
||||
};
|
||||
} catch {
|
||||
wsLoading.value = false;
|
||||
wsMessages.value.push(`❌ 连接失败 (${new Date().toLocaleTimeString()})`);
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
// 断开WebSocket连接
|
||||
const disconnectWebSocket = () => {
|
||||
if (ws.value) {
|
||||
ws.value.close(1000, '用户主动断开连接');
|
||||
ws.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送WebSocket消息
|
||||
const sendMessage = () => {
|
||||
if (ws.value?.readyState === WebSocket.OPEN && messageInput.value.trim()) {
|
||||
const message = messageInput.value.trim();
|
||||
ws.value.send(message);
|
||||
wsMessages.value.push(`🚀 发送: ${message} (${new Date().toLocaleTimeString()})`);
|
||||
messageInput.value = '';
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
// 发送模拟数据
|
||||
const sendMockData = () => {
|
||||
if (ws.value?.readyState === WebSocket.OPEN) {
|
||||
const mockMessages = [
|
||||
'你好,这是一条测试消息',
|
||||
'WebSocket 连接正常',
|
||||
'实时通信功能演示',
|
||||
'模拟数据发送成功',
|
||||
'Hello World!',
|
||||
'这是一条中文消息',
|
||||
'实时数据传输测试',
|
||||
'WebSocket 功能验证',
|
||||
];
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * mockMessages.length);
|
||||
const randomMessage = mockMessages[randomIndex]!;
|
||||
ws.value.send(randomMessage);
|
||||
wsMessages.value.push(`🚀 发送: ${randomMessage} (${new Date().toLocaleTimeString()})`);
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
// 消息记录管理
|
||||
const clearMessages = async () => {
|
||||
wsMessages.value = [];
|
||||
await scrollToBottom();
|
||||
};
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const exportMessages = () => {
|
||||
const dataStr = JSON.stringify(wsMessages.value, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `websocket-messages-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 键盘快捷键支持
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
// Ctrl/Cmd + K 清空消息
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
clearMessages();
|
||||
}
|
||||
// Ctrl/Cmd + E 导出消息
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
|
||||
event.preventDefault();
|
||||
exportMessages();
|
||||
}
|
||||
};
|
||||
|
||||
// 计算属性优化性能
|
||||
const canSendMessage = computed(() => wsConnected.value && messageInput.value.trim());
|
||||
const connectionStatusText = computed(() => {
|
||||
if (wsLoading.value) return '连接中...';
|
||||
if (wsConnected.value) return '已连接';
|
||||
return '未连接';
|
||||
});
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws.value) {
|
||||
ws.value.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-3">
|
||||
<div class="bg-white rounded-lg shadow-md p-4 max-w-xs w-full">
|
||||
<h1 class="text-xl font-bold text-gray-800 mb-3 text-center">API 示例</h1>
|
||||
<div
|
||||
class="transition-all duration-500 p-2 sm:p-3 bg-gray-50 dark:bg-gray-900 via-blue-50 dark:via-slate-800 to-slate-100 dark:to-gray-900 bg-gradient-to-br"
|
||||
>
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
|
||||
<!-- API 调用示例 -->
|
||||
<div
|
||||
class="backdrop-blur-sm rounded-2xl shadow-lg border p-4 sm:p-5 hover:shadow-2xl hover:scale-[1.02] transition-all duration-500 cursor-pointer group bg-white dark:bg-gray-800 border-white dark:border-gray-700 hover:bg-white dark:hover:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-center mb-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center mr-2"
|
||||
>
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-gray-100">API 调用示例</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="callApi"
|
||||
:disabled="loading"
|
||||
class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold py-1.5 px-3 rounded-md hover:from-blue-600 hover:to-purple-700 transition-all duration-200 disabled:opacity-50 shadow-sm text-sm"
|
||||
:aria-label="loading ? '正在调用API' : '调用API接口'"
|
||||
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||
>
|
||||
{{ loading ? '调用中...' : '调用 API' }}
|
||||
<span v-if="loading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
调用中...
|
||||
</span>
|
||||
<span v-else>调用 API</span>
|
||||
</button>
|
||||
|
||||
<div v-if="apiResult" class="mt-3 bg-gray-50 rounded-md p-2">
|
||||
<h3 class="text-gray-700 font-semibold mb-1.5 text-xs">响应结果:</h3>
|
||||
<pre class="text-gray-600 text-xs overflow-x-auto">{{ apiResult }}</pre>
|
||||
<div
|
||||
v-if="apiResult"
|
||||
class="mt-4 rounded-lg p-4 border bg-gray-50 dark:bg-gray-800 dark:from-gray-800 dark:to-gray-700 border-gray-200 dark:border-gray-600 bg-gradient-to-r from-gray-50 dark:from-gray-800 to-gray-100 dark:to-gray-700"
|
||||
>
|
||||
<h3
|
||||
class="font-semibold mb-2 flex items-center text-sm text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
响应结果:
|
||||
</h3>
|
||||
<pre
|
||||
class="text-sm overflow-x-auto p-3 rounded border text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-600"
|
||||
>{{ apiResult }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket 示例 -->
|
||||
<div
|
||||
class="backdrop-blur-sm rounded-2xl shadow-lg border p-4 sm:p-5 hover:shadow-2xl hover:scale-[1.02] transition-all duration-500 cursor-pointer group bg-white dark:bg-gray-800 border-white dark:border-gray-700 hover:bg-white dark:hover:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<div
|
||||
class="w-8 h-8 bg-gradient-to-r from-green-500 to-teal-600 rounded-lg flex items-center justify-center mr-2"
|
||||
>
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18.5V6M9 9l3-3 3 3m-3 9l3 3-3 3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-gray-100">WebSocket 示例</h2>
|
||||
</div>
|
||||
|
||||
<!-- 连接状态和控制按钮 -->
|
||||
<div class="mb-4">
|
||||
<!-- 连接状态显示 -->
|
||||
<div
|
||||
class="flex items-center justify-between mb-3 p-2.5 rounded-lg bg-gray-50 dark:bg-gray-700"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="WebSocket连接状态"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-4 h-4 rounded-full transition-all duration-500 shadow-lg"
|
||||
:class="
|
||||
wsConnected
|
||||
? 'bg-gradient-to-br from-green-400 to-green-600 animate-pulse'
|
||||
: 'bg-gradient-to-br from-red-400 to-red-600'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="wsLoading"
|
||||
class="absolute inset-0 w-4 h-4 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 animate-ping"
|
||||
/>
|
||||
<div
|
||||
v-if="wsConnected"
|
||||
class="absolute inset-0 w-4 h-4 rounded-full bg-green-400 animate-ping opacity-20"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-800 dark:text-gray-100">
|
||||
{{ connectionStatusText }}
|
||||
</div>
|
||||
<div
|
||||
v-if="connectionAttempts > 0"
|
||||
class="text-xs text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
重连次数: {{ connectionAttempts }}/{{ maxReconnectAttempts }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-300">
|
||||
{{ wsConnected ? '🟢 实时通信' : wsLoading ? '🟡 连接中' : '🔴 未连接' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
@click="connectWebSocket"
|
||||
:disabled="wsConnected || wsLoading"
|
||||
:aria-label="
|
||||
wsLoading
|
||||
? '正在连接WebSocket'
|
||||
: wsConnected
|
||||
? 'WebSocket已连接'
|
||||
: '连接WebSocket'
|
||||
"
|
||||
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||
>
|
||||
<svg
|
||||
v-if="wsLoading"
|
||||
class="animate-spin -ml-1 mr-2 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ wsLoading ? '连接中...' : '连接' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="disconnectWebSocket"
|
||||
:disabled="!wsConnected || wsLoading"
|
||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
|
||||
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
断开
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送消息 -->
|
||||
<div class="mb-4">
|
||||
<div class="flex gap-2">
|
||||
<label class="sr-only" for="messageInput">要发送的消息</label>
|
||||
<input
|
||||
id="messageInput"
|
||||
v-model="messageInput"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="输入要发送的消息..."
|
||||
:disabled="!wsConnected"
|
||||
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
|
||||
class="flex-1 border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
|
||||
/>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!canSendMessage"
|
||||
:aria-label="
|
||||
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
|
||||
"
|
||||
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
<button
|
||||
@click="sendMockData"
|
||||
:disabled="!wsConnected"
|
||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
|
||||
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
模拟
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息记录 -->
|
||||
<div
|
||||
class="rounded-lg p-3 border bg-gray-50 dark:bg-gray-800 dark:from-gray-800 dark:to-gray-700 border-gray-200 dark:border-gray-600 bg-gradient-to-r from-gray-50 dark:from-gray-800 to-gray-100 dark:to-gray-700"
|
||||
role="log"
|
||||
aria-label="WebSocket消息记录"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="text-gray-700 dark:text-gray-200 font-semibold flex items-center text-sm">
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 text-blue-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
消息记录:
|
||||
</h3>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
@click="exportMessages"
|
||||
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
|
||||
title="导出消息 (Ctrl+E)"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l4-4m-4 4l-4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
@click="clearMessages"
|
||||
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
|
||||
title="清空消息 (Ctrl+K)"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="messagesContainer"
|
||||
class="max-h-48 overflow-y-auto rounded-lg border p-2 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div
|
||||
v-if="wsMessages.length === 0"
|
||||
class="text-gray-500 dark:text-gray-400 text-sm text-center py-6"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 mx-auto mb-1 text-gray-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
暂无消息
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(message, index) in wsMessages"
|
||||
:key="index"
|
||||
class="text-sm p-2 rounded-lg transition-all duration-300 hover:shadow-lg hover:scale-[1.02] animate-fade-in"
|
||||
:class="
|
||||
message.includes('发送:')
|
||||
? 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30 text-blue-800 dark:text-blue-200 border-l-4 border-blue-400 dark:border-blue-500'
|
||||
: message.includes('收到:')
|
||||
? 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30 text-green-800 dark:text-green-200 border-l-4 border-green-400 dark:border-green-500'
|
||||
: message.includes('连接已建立')
|
||||
? 'bg-gradient-to-r from-emerald-50 to-emerald-100 dark:from-emerald-900/30 dark:to-emerald-800/30 text-emerald-800 dark:text-emerald-200 border-l-4 border-emerald-400 dark:border-emerald-500'
|
||||
: message.includes('连接已关闭') || message.includes('连接失败')
|
||||
? 'bg-gradient-to-r from-red-50 to-red-100 dark:from-red-900/30 dark:to-red-800/30 text-red-800 dark:text-red-200 border-l-4 border-red-400 dark:border-red-500'
|
||||
: message.includes('尝试重新连接')
|
||||
? 'bg-gradient-to-r from-yellow-50 to-yellow-100 dark:from-yellow-900/30 dark:to-yellow-800/30 text-yellow-800 dark:text-yellow-200 border-l-4 border-yellow-400 dark:border-yellow-500'
|
||||
: 'bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 border-l-4 border-gray-400 dark:border-gray-500'
|
||||
"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷键提示 -->
|
||||
<div
|
||||
class="mt-4 sm:mt-6 backdrop-blur-sm rounded-2xl shadow-lg border p-3 sm:p-4 bg-white/70 dark:bg-gray-800/70 border-white dark:border-gray-700"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2 flex items-center">
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
快捷键提示:
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
|
||||
<div
|
||||
class="flex items-center justify-between p-1.5 rounded-lg bg-gray-50/70 dark:bg-gray-700"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-300">发送消息:</span>
|
||||
<kbd
|
||||
class="px-1.5 py-0.5 rounded text-xs bg-white dark:bg-gray-600 border-gray-200 dark:border-gray-500 text-gray-700 dark:text-gray-200"
|
||||
>Enter</kbd
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between p-1.5 rounded-lg bg-gray-50/70 dark:bg-gray-700"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-300">清空消息:</span>
|
||||
<kbd
|
||||
class="px-1.5 py-0.5 rounded text-xs bg-white dark:bg-gray-600 border-gray-200 dark:border-gray-500 text-gray-700 dark:text-gray-200"
|
||||
>Ctrl+K</kbd
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between p-1.5 rounded-lg bg-gray-50/70 dark:bg-gray-700"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-300">导出消息:</span>
|
||||
<kbd
|
||||
class="px-1.5 py-0.5 rounded text-xs bg-white dark:bg-gray-600 border-gray-200 dark:border-gray-500 text-gray-700 dark:text-gray-200"
|
||||
>Ctrl+E</kbd
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user