mejoras de UI
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
<template>
|
||||
<div class="panel glass">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Eventos y comparación</h2>
|
||||
<div v-if="filtersCollapsed && (activeFilters?.hasFilters || selectedPlayerUuid)" class="active-filters-summary">
|
||||
<span class="filter-tag" v-if="activeFilters?.dataSource !== 'aggregated'">
|
||||
{{ activeFilters.dataSource === 'active-rooms' ? '🔴 Tiempo Real' : '📁 Agregado' }}
|
||||
</span>
|
||||
<span class="filter-tag" v-if="activeFilters?.round !== 'all'">
|
||||
Round {{ activeFilters.round }}
|
||||
</span>
|
||||
<span class="filter-tag" v-if="activeFilters?.game !== 'all'">
|
||||
{{ activeFilters.game }}
|
||||
</span>
|
||||
<span class="filter-tag player-tag" v-if="selectedPlayerUuid && activeFilters?.selectedPlayer">
|
||||
👤 {{ activeFilters.selectedPlayer }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="placeholder">Cargando datos…</div>
|
||||
|
||||
<!-- Regular bars view -->
|
||||
@@ -101,6 +117,14 @@ interface Props {
|
||||
playerBarGradient: string;
|
||||
viewMode: 'count' | 'percent' | 'ratio';
|
||||
loading?: boolean;
|
||||
filtersCollapsed?: boolean;
|
||||
activeFilters?: {
|
||||
dataSource: string;
|
||||
round: string;
|
||||
game: string;
|
||||
hasFilters: boolean;
|
||||
selectedPlayer?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -249,11 +273,42 @@ function friendlyEventName(eventType: string): string {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0 0 10px;
|
||||
margin: 0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.active-filters-summary {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(102,126,234,0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tag.player-tag {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
box-shadow: 0 2px 6px rgba(16,185,129,0.25);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #64748b;
|
||||
padding: 12px;
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface DetailedEvent {
|
||||
kind: string;
|
||||
round?: number;
|
||||
gameVariant?: string;
|
||||
playerUuid?: string;
|
||||
playerName?: string;
|
||||
}
|
||||
|
||||
export type DataSource = 'aggregated' | 'active-rooms';
|
||||
|
||||
@@ -3,13 +3,24 @@
|
||||
<div class="header glass light">
|
||||
<h1>📈 Leaderboard</h1>
|
||||
<div class="actions">
|
||||
<button class="btn-collapse" @click="filtersCollapsed = !filtersCollapsed" :title="filtersCollapsed ? 'Mostrar filtros' : 'Ocultar filtros'">
|
||||
<span class="collapse-icon" :class="{ rotated: filtersCollapsed }">▼</span>
|
||||
<span class="collapse-text">{{ filtersCollapsed ? 'Mostrar filtros' : 'Ocultar filtros' }}</span>
|
||||
</button>
|
||||
<button class="btn" @click="refreshAll" :disabled="loading">{{ loading ? 'Actualizando…' : 'Actualizar' }}</button>
|
||||
<button class="btn" @click="downloadCSV" :disabled="loading" title="Descargar datos actuales como CSV">
|
||||
📊 CSV
|
||||
</button>
|
||||
<button class="btn toggle" @click="cycleViewMode" :disabled="loading">
|
||||
{{ viewModeLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section" :class="{ collapsed: filtersCollapsed }">
|
||||
|
||||
<Transition name="filters-slide">
|
||||
<div v-if="!filtersCollapsed" class="filters-content">
|
||||
<DataSourceSelector v-model="eventFilters.dataSource.value" />
|
||||
|
||||
<EventFilters
|
||||
@@ -21,8 +32,12 @@
|
||||
@update:game-filter="eventFilters.gameFilter.value = $event"
|
||||
@reset-filters="eventFilters.resetFilters"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="controls glass light">
|
||||
<Transition name="filters-slide">
|
||||
<div v-if="!filtersCollapsed" class="controls glass light">
|
||||
<div class="legend">
|
||||
<span class="key global"></span> Global
|
||||
<span class="sep">·</span>
|
||||
@@ -54,6 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<EventChart
|
||||
:event-types="EVENTS"
|
||||
@@ -64,6 +80,14 @@
|
||||
:player-bar-gradient="playerBarGradient"
|
||||
:view-mode="viewMode"
|
||||
:loading="loading"
|
||||
:filters-collapsed="filtersCollapsed"
|
||||
:active-filters="{
|
||||
dataSource: eventFilters.dataSource.value,
|
||||
round: eventFilters.roundFilter.value.toString(),
|
||||
game: eventFilters.gameFilter.value,
|
||||
hasFilters: eventFilters.hasActiveFilters.value,
|
||||
selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -80,6 +104,7 @@ interface RoomState { players?: any[]; systemMessages?: { kind: string }[] }
|
||||
|
||||
const loading = ref(false);
|
||||
const eventFilters = useEventFilters();
|
||||
const filtersCollapsed = ref(false);
|
||||
|
||||
// View mode cycling
|
||||
type ViewMode = 'count' | 'percent' | 'ratio';
|
||||
@@ -433,6 +458,78 @@ watch(() => playersFiltered.value.length, () => {
|
||||
// Removed totals table and sorting; keep actions stream for per-player counts only
|
||||
const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]);
|
||||
|
||||
function downloadCSV() {
|
||||
const currentEvents = eventFilters.currentSourceEvents.value;
|
||||
|
||||
// Create CSV headers
|
||||
const headers = ['Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'DataSource'];
|
||||
|
||||
// Create CSV rows - one row per individual event occurrence
|
||||
const rows: string[][] = [];
|
||||
|
||||
// Add detailed individual event rows if we have detailed data
|
||||
currentEvents.forEach(event => {
|
||||
if (EVENTS.includes(event.kind)) {
|
||||
rows.push([
|
||||
event.kind,
|
||||
'1',
|
||||
event.round?.toString() || '',
|
||||
event.gameVariant || '',
|
||||
event.playerUuid || '',
|
||||
event.playerName || '',
|
||||
eventFilters.dataSource.value
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Also add per-player aggregated data from playersActionsByUuid
|
||||
Object.entries(playersActionsByUuid.value).forEach(([uuid, counts]) => {
|
||||
const player = players.value.find(p => p.uuid === uuid);
|
||||
EVENTS.forEach(eventType => {
|
||||
const count = counts[eventType] || 0;
|
||||
if (count > 0) {
|
||||
// Add one row per occurrence for proper import compatibility
|
||||
for (let i = 0; i < count; i++) {
|
||||
rows.push([
|
||||
eventType,
|
||||
'1',
|
||||
'', // Round info not available in aggregated player data
|
||||
'', // GameVariant info not available in aggregated player data
|
||||
uuid,
|
||||
player?.name || '',
|
||||
'player-aggregated'
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert to CSV string
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
// Create and download file
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
|
||||
// Generate filename with current filters and timestamp
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
|
||||
const dataSourceLabel = eventFilters.dataSource.value === 'aggregated' ? 'agregados' : 'activos';
|
||||
const filterLabel = eventFilters.hasActiveFilters.value ? `_${eventFilters.filterSummary.value.replace(/\s+/g, '_')}` : '';
|
||||
const filename = `eventos_${dataSourceLabel}${filterLabel}_${timestamp}.csv`;
|
||||
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -568,6 +665,70 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
|
||||
}
|
||||
|
||||
|
||||
/* Filters section styles */
|
||||
.filters-section {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #3949ab;
|
||||
border: 1px solid #c7d2fe;
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-collapse:hover {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.collapse-icon.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.filters-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Transition for filters */
|
||||
.filters-slide-enter-active,
|
||||
.filters-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filters-slide-enter-from,
|
||||
.filters-slide-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.filters-slide-enter-to,
|
||||
.filters-slide-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 200px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-controls {
|
||||
flex-direction: column;
|
||||
@@ -585,6 +746,18 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.btn-collapse .collapse-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-collapse {
|
||||
padding: 8px;
|
||||
min-width: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.player-chips {
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user