Files
websocket-qris/examples/clientsocket/components/WebSocketClient.vue
2025-09-24 18:42:16 +07:00

368 lines
7.3 KiB
Vue

<template>
<div class="container">
<h1>WebSocket Client</h1>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-item">
<div
class="status-indicator"
:class="connectionStatusClass"
></div>
<span>{{ connectionStatusText }}</span>
</div>
<div class="status-item">
<span>Client ID:</span>
<strong>{{ connectionState?.clientId || 'Not connected' }}</strong>
</div>
<div class="status-item">
<span>Room:</span>
<strong>{{ connectionState?.currentRoom || 'None' }}</strong>
</div>
<div class="status-item">
<span>Messages:</span>
<strong>{{ messages?.length || 0 }}/{{ config?.maxMessages || 1000 }}</strong>
</div>
<div class="status-item">
<span>Uptime:</span>
<strong>{{ connectionState?.uptime || '00:00:00' }}</strong>
</div>
<div class="status-item">
<span>Health:</span>
<div
class="health-indicator"
:class="connectionHealthClass"
>
{{ connectionHealthText }}
</div>
</div>
</div>
<!-- Message Limit Warning -->
<div v-if="shouldShowMessageWarning" class="message-limit-warning">
<v-icon>mdi-alert</v-icon>
Message limit approaching ({{ messages?.length || 0 }}/{{ config?.maxMessages || 1000 }})
</div>
<!-- Tabs -->
<div class="tabs">
<button
class="tab"
:class="{ active: activeTab === 'connection' }"
@click="activeTab = 'connection'"
>
Connection
</button>
<button
class="tab"
:class="{ active: activeTab === 'messaging' }"
@click="activeTab = 'messaging'"
>
Messaging
</button>
<button
class="tab"
:class="{ active: activeTab === 'database' }"
@click="activeTab = 'database'"
>
Database
</button>
<button
class="tab"
:class="{ active: activeTab === 'monitoring' }"
@click="activeTab = 'monitoring'"
>
Monitoring
</button>
<button
class="tab"
:class="{ active: activeTab === 'admin' }"
@click="activeTab = 'admin'"
>
Admin
</button>
</div>
<!-- Tab Content -->
<div class="tab-content" :class="{ active: activeTab === 'connection' }">
<ConnectionTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'messaging' }">
<MessagingTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'database' }">
<DatabaseTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'monitoring' }">
<MonitoringTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'admin' }">
<AdminTab />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useWebSocket } from '~/composables/useWebSocket'
import ConnectionTab from './tabs/ConnectionTab.vue'
import MessagingTab from './tabs/MessagingTab.vue'
import DatabaseTab from './tabs/DatabaseTab.vue'
import MonitoringTab from './tabs/MonitoringTab.vue'
import AdminTab from './tabs/AdminTab.vue'
const activeTab = ref('connection')
const {
isConnected,
connectionStatus,
connectionState,
config,
messages,
stats,
onlineUsers,
activityLog,
connect,
disconnect,
cleanup,
isMessageLimitReached,
shouldShowMessageWarning,
connectionHealthColor,
connectionHealthText
} = useWebSocket()
const connectionStatusClass = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'status-connected'
case 'connecting': return 'status-connecting'
case 'disconnected': return 'status-disconnected'
default: return 'status-disconnected'
}
})
const connectionStatusText = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'Connected'
case 'connecting': return 'Connecting...'
case 'disconnected': return 'Disconnected'
case 'error': return 'Error'
default: return 'Unknown'
}
})
const connectionHealthClass = computed(() => {
switch (connectionState?.connectionHealth) {
case 'excellent': return 'health-excellent'
case 'good': return 'health-good'
case 'warning': return 'health-warning'
case 'poor': return 'health-poor'
default: return 'health-poor'
}
})
onMounted(() => {
// Auto-connect on mount
connect()
})
onUnmounted(() => {
cleanup()
})
</script>
<style scoped>
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 24px;
overflow: hidden;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 32px;
font-weight: 600;
}
.status-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
margin-bottom: 24px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-connected {
background: #4CAF50;
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
}
.status-connecting {
background: #FFC107;
animation: pulse 1.5s infinite;
}
.status-disconnected {
background: #F44336;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.health-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: bold;
}
.health-excellent {
background: #d4edda;
color: #155724;
}
.health-good {
background: #d1ecf1;
color: #0c5460;
}
.health-warning {
background: #fff3cd;
color: #856404;
}
.health-poor {
background: #f8d7da;
color: #721c24;
}
.tabs {
display: flex;
border-bottom: 2px solid #e9ecef;
margin-bottom: 24px;
overflow-x: auto;
background: white;
border-radius: 8px 8px 0 0;
}
.tab {
padding: 16px 24px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: all 0.3s ease;
font-weight: 500;
color: #666;
}
.tab:hover {
background: #f8f9fa;
color: #333;
}
.tab.active {
border-bottom-color: #1976d2;
color: #1976d2;
font-weight: 600;
background: #f8f9fa;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-limit-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 16px;
margin: 16px 0;
border-radius: 6px;
font-size: 14px;
color: #856404;
display: flex;
align-items: center;
gap: 8px;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 16px;
border-radius: 8px;
}
.status-bar {
grid-template-columns: 1fr;
padding: 16px;
}
.tabs {
flex-wrap: wrap;
}
.tab {
padding: 12px 16px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.container {
padding: 12px;
}
.status-bar {
padding: 12px;
}
.tab {
padding: 8px 12px;
font-size: 12px;
}
}
</style>