603 lines
12 KiB
Vue
603 lines
12 KiB
Vue
<template>
|
|
<div class="messaging-tab">
|
|
<!-- Connection Status -->
|
|
<div class="connection-status" :class="connectionState.connectionStatus">
|
|
<div class="status-indicator">
|
|
<div class="status-dot" :class="connectionState.connectionStatus"></div>
|
|
<span class="status-text">
|
|
{{
|
|
connectionState.connectionStatus === "connected"
|
|
? "Connected"
|
|
: connectionState.connectionStatus === "connecting"
|
|
? "Connecting..."
|
|
: connectionState.connectionStatus === "error"
|
|
? "Connection Error"
|
|
: "Disconnected"
|
|
}}
|
|
</span>
|
|
</div>
|
|
<div class="connection-details" v-if="connectionState.clientId">
|
|
<span>Client ID: {{ connectionState.clientId }}</span>
|
|
<span v-if="connectionState.currentRoom"
|
|
>Room: {{ connectionState.currentRoom }}</span
|
|
>
|
|
<span v-if="connectionState.connectionLatency > 0"
|
|
>Latency: {{ connectionState.connectionLatency }}ms</span
|
|
>
|
|
<span v-if="connectionState.connectionHealth"
|
|
>Health: {{ connectionState.connectionHealth }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="input-group">
|
|
<label for="broadcastMessage">Broadcast Message</label>
|
|
<textarea
|
|
id="broadcastMessage"
|
|
v-model="messageText"
|
|
placeholder="Enter message to broadcast to all clients"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="targetClientId"
|
|
>Target Client ID (for direct messages)</label
|
|
>
|
|
<input
|
|
id="targetClientId"
|
|
v-model="targetClientId"
|
|
type="text"
|
|
placeholder="Client ID for direct message"
|
|
/>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="roomMessage">Room Message</label>
|
|
<textarea
|
|
id="roomMessage"
|
|
v-model="roomMessage"
|
|
placeholder="Enter message to send to specific room"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="targetRoom">Target Room</label>
|
|
<input
|
|
id="targetRoom"
|
|
v-model="targetRoom"
|
|
type="text"
|
|
placeholder="Room name"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button
|
|
class="btn btn-primary"
|
|
:disabled="!isConnected || !messageText.trim()"
|
|
@click="handleSendBroadcast"
|
|
>
|
|
Send Broadcast
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-info"
|
|
:disabled="
|
|
!isConnected || !targetClientId.trim() || !messageText.trim()
|
|
"
|
|
@click="handleSendDirectMessage"
|
|
>
|
|
Send Direct Message
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-success"
|
|
:disabled="!isConnected || !targetRoom.trim() || !roomMessage.trim()"
|
|
@click="handleSendRoomMessage"
|
|
>
|
|
Send Room Message
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-warning"
|
|
:disabled="!isConnected"
|
|
@click="sendHeartbeat"
|
|
>
|
|
Send Heartbeat
|
|
</button>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button
|
|
class="btn btn-secondary"
|
|
:disabled="!isConnected"
|
|
@click="getOnlineUsers"
|
|
>
|
|
Get Online Users
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-secondary"
|
|
:disabled="!isConnected"
|
|
@click="getServerInfo"
|
|
>
|
|
Get Server Info
|
|
</button>
|
|
|
|
<button class="btn btn-secondary" @click="clearMessages">
|
|
Clear Messages
|
|
</button>
|
|
|
|
<button class="btn btn-secondary" @click="clearActivityLog">
|
|
Clear Activity Log
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Online Users -->
|
|
<div class="online-users-section" v-if="onlineUsers.length > 0">
|
|
<h3>Online Users ({{ onlineUsers.length }})</h3>
|
|
<div class="online-users">
|
|
<div
|
|
v-for="user in onlineUsers"
|
|
:key="user.client_id"
|
|
class="user-item"
|
|
:class="{
|
|
'current-user': user.client_id === connectionState.clientId,
|
|
}"
|
|
>
|
|
<div class="user-info">
|
|
<div class="user-id">{{ user.client_id }}</div>
|
|
<div class="user-details">
|
|
<span>Room: {{ user.room }}</span>
|
|
<span>IP: {{ user.ip_address }}</span>
|
|
<span>Connected: {{ formatTime(user.connected_at) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="user-actions">
|
|
<button
|
|
class="btn btn-sm btn-info"
|
|
@click="setTargetClient(user.client_id)"
|
|
>
|
|
Direct Message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Log -->
|
|
<div class="activity-log-section">
|
|
<h3>Activity Log</h3>
|
|
<div class="activity-log">
|
|
<div
|
|
v-for="(activity, index) in activityLog.slice(0, 20)"
|
|
:key="index"
|
|
class="activity-item"
|
|
>
|
|
<div class="activity-time">{{ formatTime(activity.timestamp) }}</div>
|
|
<div class="activity-event">{{ activity.event }}</div>
|
|
<div class="activity-details">{{ activity.details }}</div>
|
|
</div>
|
|
<div v-if="activityLog.length === 0" class="no-activity">
|
|
No activity logged yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from "vue";
|
|
import { useWebSocket } from "../../composables/useWebSocket";
|
|
|
|
const {
|
|
isConnected,
|
|
connectionState,
|
|
onlineUsers,
|
|
activityLog,
|
|
broadcastMessage,
|
|
sendDirectMessage,
|
|
sendRoomMessage,
|
|
sendHeartbeat,
|
|
getOnlineUsers,
|
|
getServerInfo,
|
|
clearMessages,
|
|
clearActivityLog,
|
|
connect,
|
|
} = useWebSocket();
|
|
|
|
const messageText = ref("");
|
|
const targetClientId = ref("");
|
|
const roomMessage = ref("");
|
|
const targetRoom = ref("");
|
|
|
|
const handleSendBroadcast = () => {
|
|
if (messageText.value.trim()) {
|
|
broadcastMessage(messageText.value.trim());
|
|
messageText.value = "";
|
|
}
|
|
};
|
|
|
|
const handleSendDirectMessage = () => {
|
|
if (targetClientId.value.trim() && messageText.value.trim()) {
|
|
sendDirectMessage(targetClientId.value.trim(), messageText.value.trim());
|
|
messageText.value = "";
|
|
}
|
|
};
|
|
|
|
const handleSendRoomMessage = () => {
|
|
if (targetRoom.value.trim() && roomMessage.value.trim()) {
|
|
sendRoomMessage(targetRoom.value.trim(), roomMessage.value.trim());
|
|
roomMessage.value = "";
|
|
}
|
|
};
|
|
|
|
const setTargetClient = (clientId: string) => {
|
|
targetClientId.value = clientId;
|
|
};
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
return new Date(timestamp).toLocaleString();
|
|
};
|
|
|
|
// Initialize connection on component mount
|
|
onMounted(() => {
|
|
console.log("MessagingTab mounted, initializing WebSocket connection...");
|
|
if (!isConnected.value) {
|
|
console.log("Attempting to connect to WebSocket...");
|
|
connect();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.messaging-tab {
|
|
padding: 20px 0;
|
|
}
|
|
|
|
/* Connection Status Styles */
|
|
.connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
margin-bottom: 24px;
|
|
border-radius: 8px;
|
|
border: 2px solid;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.connection-status.connected {
|
|
background: #e8f5e8;
|
|
border-color: #4caf50;
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.connection-status.connecting {
|
|
background: #fff3e0;
|
|
border-color: #ff9800;
|
|
color: #ef6c00;
|
|
}
|
|
|
|
.connection-status.error {
|
|
background: #ffebee;
|
|
border-color: #f44336;
|
|
color: #c62828;
|
|
}
|
|
|
|
.connection-status.disconnected {
|
|
background: #f5f5f5;
|
|
border-color: #9e9e9e;
|
|
color: #616161;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: #4caf50;
|
|
}
|
|
|
|
.status-dot.connecting {
|
|
background: #ff9800;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.status-dot.error {
|
|
background: #f44336;
|
|
animation: pulse 0.5s infinite;
|
|
}
|
|
|
|
.status-dot.disconnected {
|
|
background: #9e9e9e;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.connection-details {
|
|
display: flex;
|
|
gap: 20px;
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.connection-details span {
|
|
padding: 4px 8px;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
padding: 24px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.input-group label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.input-group input,
|
|
.input-group textarea {
|
|
padding: 12px 16px;
|
|
border: 2px solid #e9ecef;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.input-group input:focus,
|
|
.input-group textarea:focus {
|
|
outline: none;
|
|
border-color: #1976d2;
|
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #1976d2;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #1565c0;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
|
|
}
|
|
|
|
.btn-info {
|
|
background: #17a2b8;
|
|
color: white;
|
|
}
|
|
|
|
.btn-info:hover:not(:disabled) {
|
|
background: #138496;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
|
|
}
|
|
|
|
.btn-success {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover:not(:disabled) {
|
|
background: #218838;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
|
}
|
|
|
|
.btn-warning {
|
|
background: #ffc107;
|
|
color: #212529;
|
|
}
|
|
|
|
.btn-warning:hover:not(:disabled) {
|
|
background: #e0a800;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #5a6268;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 8px 16px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.online-users-section {
|
|
margin: 32px 0;
|
|
}
|
|
|
|
.online-users-section h3 {
|
|
margin-bottom: 16px;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.online-users {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.user-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
margin: 8px 0;
|
|
background: white;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.user-item.current-user {
|
|
border-left: 4px solid #1976d2;
|
|
background: #f0f8ff;
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.user-id {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.user-details {
|
|
font-size: 12px;
|
|
color: #666;
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.user-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.activity-log-section {
|
|
margin-top: 32px;
|
|
}
|
|
|
|
.activity-log-section h3 {
|
|
margin-bottom: 16px;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.activity-log {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.activity-item {
|
|
display: grid;
|
|
grid-template-columns: 150px 120px 1fr;
|
|
gap: 16px;
|
|
padding: 12px 16px;
|
|
margin: 8px 0;
|
|
background: white;
|
|
border-radius: 6px;
|
|
border: 1px solid #e9ecef;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.activity-time {
|
|
color: #666;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.activity-event {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.activity-details {
|
|
color: #666;
|
|
}
|
|
|
|
.no-activity {
|
|
text-align: center;
|
|
color: #666;
|
|
font-style: italic;
|
|
padding: 40px;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.controls {
|
|
grid-template-columns: 1fr;
|
|
padding: 16px;
|
|
}
|
|
|
|
.activity-item {
|
|
grid-template-columns: 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.user-item {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
|
|
.user-actions {
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style>
|