Add command table functionality to chat store and ChatView component
- Introduced `commandTable` state in chat store to manage command output. - Implemented WebSocket listener for `commandTable` messages. - Enhanced ChatView.vue to display command table with dynamic content and styling. - Added `clearCommandTable` method to reset command table state. - Updated server broadcast logic to send structured command table data for various statistics.
This commit is contained in:
@@ -21,6 +21,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const historyResults = ref([]);
|
const historyResults = ref([]);
|
||||||
const unreadChatsCount = ref(0);
|
const unreadChatsCount = ref(0);
|
||||||
const errorMessage = ref(null);
|
const errorMessage = ref(null);
|
||||||
|
const commandTable = ref(null);
|
||||||
const remainingSecondsToTimeout = ref(1800);
|
const remainingSecondsToTimeout = ref(1800);
|
||||||
const awaitingLoginUsername = ref(false);
|
const awaitingLoginUsername = ref(false);
|
||||||
const awaitingLoginPassword = ref(false);
|
const awaitingLoginPassword = ref(false);
|
||||||
@@ -191,6 +192,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
handleWebSocketMessage({ type: 'commandResult', ...data });
|
handleWebSocketMessage({ type: 'commandResult', ...data });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socketInstance.on('commandTable', (data) => {
|
||||||
|
handleWebSocketMessage({ type: 'commandTable', ...data });
|
||||||
|
});
|
||||||
|
|
||||||
socketInstance.on('unreadChats', (data) => {
|
socketInstance.on('unreadChats', (data) => {
|
||||||
handleWebSocketMessage({ type: 'unreadChats', ...data });
|
handleWebSocketMessage({ type: 'unreadChats', ...data });
|
||||||
});
|
});
|
||||||
@@ -315,6 +320,15 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'commandTable': {
|
||||||
|
const title = data.title || 'Ausgabe';
|
||||||
|
const columns = Array.isArray(data.columns) ? data.columns : [];
|
||||||
|
const rows = Array.isArray(data.rows) ? data.rows : [];
|
||||||
|
commandTable.value = { title, columns, rows };
|
||||||
|
// Tabelle ist persistent; temporäre Fehlermeldung löschen
|
||||||
|
errorMessage.value = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'unreadChats':
|
case 'unreadChats':
|
||||||
unreadChatsCount.value = data.count || 0;
|
unreadChatsCount.value = data.count || 0;
|
||||||
break;
|
break;
|
||||||
@@ -543,6 +557,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
socket.value.emit('requestOpenConversations');
|
socket.value.emit('requestOpenConversations');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearCommandTable() {
|
||||||
|
commandTable.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
function setView(view) {
|
function setView(view) {
|
||||||
currentView.value = view;
|
currentView.value = view;
|
||||||
|
|
||||||
@@ -574,6 +592,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
inboxResults.value = [];
|
inboxResults.value = [];
|
||||||
historyResults.value = [];
|
historyResults.value = [];
|
||||||
|
commandTable.value = null;
|
||||||
searchData.value = {
|
searchData.value = {
|
||||||
nameIncludes: '',
|
nameIncludes: '',
|
||||||
minAge: null,
|
minAge: null,
|
||||||
@@ -689,6 +708,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
unreadChatsCount,
|
unreadChatsCount,
|
||||||
remainingSecondsToTimeout,
|
remainingSecondsToTimeout,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
commandTable,
|
||||||
searchData,
|
searchData,
|
||||||
awaitingLoginUsername,
|
awaitingLoginUsername,
|
||||||
awaitingLoginPassword,
|
awaitingLoginPassword,
|
||||||
@@ -703,6 +723,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
userSearch,
|
userSearch,
|
||||||
requestHistory,
|
requestHistory,
|
||||||
requestOpenConversations,
|
requestOpenConversations,
|
||||||
|
clearCommandTable,
|
||||||
setView,
|
setView,
|
||||||
logout,
|
logout,
|
||||||
restoreSession
|
restoreSession
|
||||||
|
|||||||
@@ -22,6 +22,30 @@
|
|||||||
<div v-if="chatStore.errorMessage" class="error-message">
|
<div v-if="chatStore.errorMessage" class="error-message">
|
||||||
{{ chatStore.errorMessage }}
|
{{ chatStore.errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="chatStore.commandTable" class="command-table-container">
|
||||||
|
<div class="command-table-header">
|
||||||
|
<strong>{{ chatStore.commandTable.title }}</strong>
|
||||||
|
<button class="command-table-close" @click="chatStore.clearCommandTable()">Schließen</button>
|
||||||
|
</div>
|
||||||
|
<div class="command-table-scroll">
|
||||||
|
<table class="command-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(column, idx) in chatStore.commandTable.columns" :key="`head-${idx}`">
|
||||||
|
{{ column }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, rowIdx) in chatStore.commandTable.rows" :key="`row-${rowIdx}`">
|
||||||
|
<td v-for="(cell, cellIdx) in row" :key="`cell-${rowIdx}-${cellIdx}`">
|
||||||
|
{{ cell }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="chatStore.currentConversation && currentUserInfo" :class="['chat-header', 'chat-header-gender-' + currentUserInfo.gender]">
|
<div v-else-if="chatStore.currentConversation && currentUserInfo" :class="['chat-header', 'chat-header-gender-' + currentUserInfo.gender]">
|
||||||
<h2>{{ chatStore.currentConversation }} ({{ currentUserInfo.gender }})</h2>
|
<h2>{{ chatStore.currentConversation }} ({{ currentUserInfo.gender }})</h2>
|
||||||
<div class="chat-header-info">
|
<div class="chat-header-info">
|
||||||
@@ -159,5 +183,54 @@ onMounted(async () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.command-table-container {
|
||||||
|
margin: 0.8em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.6em 0.8em;
|
||||||
|
background: #f4f6f8;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table-close {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table-scroll {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table th,
|
||||||
|
.command-table td {
|
||||||
|
padding: 0.45em 0.6em;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-table th {
|
||||||
|
background: #fafafa;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -325,6 +325,14 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
socket.emit('commandResult', { lines: payload, kind });
|
socket.emit('commandResult', { lines: payload, kind });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendCommandTable(socket, title, columns, rows) {
|
||||||
|
socket.emit('commandTable', {
|
||||||
|
title: String(title || 'Ausgabe'),
|
||||||
|
columns: Array.isArray(columns) ? columns.map((c) => String(c)) : [],
|
||||||
|
rows: Array.isArray(rows) ? rows : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hasRight(client, right) {
|
function hasRight(client, right) {
|
||||||
return !!client.chatAuth && client.chatAuth.rights instanceof Set && client.chatAuth.rights.has(right);
|
return !!client.chatAuth && client.chatAuth.rights instanceof Set && client.chatAuth.rights.has(right);
|
||||||
}
|
}
|
||||||
@@ -412,7 +420,7 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
if (sub === 'today') {
|
if (sub === 'today') {
|
||||||
const day = new Date().toISOString().slice(0, 10);
|
const day = new Date().toISOString().slice(0, 10);
|
||||||
const dayRecords = records.filter((r) => r.day === day);
|
const dayRecords = records.filter((r) => r.day === day);
|
||||||
sendCommandResult(socket, `Logins heute (${day}): ${dayRecords.length}`);
|
sendCommandTable(socket, 'Statistik: Heute', ['Tag', 'Logins'], [[day, dayRecords.length]]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +431,7 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dayRecords = records.filter((r) => r.day === day);
|
const dayRecords = records.filter((r) => r.day === day);
|
||||||
sendCommandResult(socket, `Logins am ${day}: ${dayRecords.length}`);
|
sendCommandTable(socket, 'Statistik: Datum', ['Tag', 'Logins'], [[day, dayRecords.length]]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,8 +445,8 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
const filtered = records.filter((r) => r.day >= from && r.day <= to);
|
const filtered = records.filter((r) => r.day >= from && r.day <= to);
|
||||||
const perDay = aggregateTop(filtered, (r) => r.day, 1000)
|
const perDay = aggregateTop(filtered, (r) => r.day, 1000)
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.map(([day, count]) => `${day}: ${count}`);
|
.map(([day, count]) => [day, count]);
|
||||||
sendCommandResult(socket, [`Logins ${from} bis ${to}: ${filtered.length}`, ...perDay]);
|
sendCommandTable(socket, `Statistik: Zeitraum ${from} bis ${to}`, ['Tag', 'Logins'], perDay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,25 +456,32 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
const maxAge = Math.max(...ages);
|
const maxAge = Math.max(...ages);
|
||||||
const youngest = records.find((r) => r.age === minAge);
|
const youngest = records.find((r) => r.age === minAge);
|
||||||
const oldest = records.find((r) => r.age === maxAge);
|
const oldest = records.find((r) => r.age === maxAge);
|
||||||
sendCommandResult(socket, [
|
sendCommandTable(socket, 'Statistik: Alter', ['Kategorie', 'Name', 'Alter'], [
|
||||||
`Jüngster Nutzer: ${youngest.userName} (${youngest.age})`,
|
['Jüngster Nutzer', youngest.userName, youngest.age],
|
||||||
`Ältester Nutzer: ${oldest.userName} (${oldest.age})`
|
['Ältester Nutzer', oldest.userName, oldest.age]
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'names') {
|
if (sub === 'names') {
|
||||||
const topNames = aggregateTop(records, (r) => r.userName, 20);
|
const topNames = aggregateTop(records, (r) => r.userName, 20);
|
||||||
sendCommandResult(socket, [
|
sendCommandTable(
|
||||||
`Namen gesamt (verschieden): ${new Set(records.map((r) => r.userName)).size}`,
|
socket,
|
||||||
...topNames.map(([name, count]) => `${name}: ${count}`)
|
`Statistik: Namen (gesamt verschieden: ${new Set(records.map((r) => r.userName)).size})`,
|
||||||
]);
|
['Name', 'Anzahl'],
|
||||||
|
topNames.map(([name, count]) => [name, count])
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'countries') {
|
if (sub === 'countries') {
|
||||||
const topCountries = aggregateTop(records, (r) => r.country, 20);
|
const topCountries = aggregateTop(records, (r) => r.country, 20);
|
||||||
sendCommandResult(socket, topCountries.map(([country, count]) => `${country}: ${count}`));
|
sendCommandTable(
|
||||||
|
socket,
|
||||||
|
'Statistik: Länder',
|
||||||
|
['Land', 'Anzahl'],
|
||||||
|
topCountries.map(([country, count]) => [country, count])
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +493,15 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.');
|
sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendCommandResult(socket, buildAllStats(readLoginRecords()));
|
const lines = buildAllStats(readLoginRecords());
|
||||||
|
const rows = lines.map((line) => {
|
||||||
|
const separatorIndex = line.indexOf(':');
|
||||||
|
if (separatorIndex === -1) {
|
||||||
|
return [line, ''];
|
||||||
|
}
|
||||||
|
return [line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim()];
|
||||||
|
});
|
||||||
|
sendCommandTable(socket, 'Statistik: Übersicht', ['Metrik', 'Wert'], rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeKickCommand(socket, client, parts) {
|
function executeKickCommand(socket, client, parts) {
|
||||||
|
|||||||
Reference in New Issue
Block a user