mejora filtro por salas
This commit is contained in:
@@ -97,7 +97,7 @@ export function useEventFilters() {
|
||||
const parts = [];
|
||||
if (roundFilter.value.length) parts.push(`Round ${roundFilter.value.join(',')}`);
|
||||
if (gameFilter.value.length) parts.push(`Game ${gameFilter.value.join(',')}`);
|
||||
if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.map(r => r.slice(0,8)).join(',')}`);
|
||||
if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.length}`);
|
||||
return parts.length > 0 ? parts.join(' + ') : 'Sin filtros';
|
||||
});
|
||||
|
||||
|
||||
@@ -72,31 +72,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room Filter Section -->
|
||||
<div class="room-chips">
|
||||
<div class="search-controls">
|
||||
<input class="search" v-model="roomSearch" placeholder="Buscar sala…" />
|
||||
<div class="pagination compact" v-if="roomPageCount > 1">
|
||||
<button class="pg-btn compact" @click="prevRoomPage" :disabled="roomPage <= 1">‹</button>
|
||||
<span class="pg-ind">{{ roomPage }}/{{ roomPageCount }}</span>
|
||||
<button class="pg-btn compact" @click="nextRoomPage" :disabled="roomPage >= roomPageCount">›</button>
|
||||
<!-- Room Filter Section: slice controls -->
|
||||
<div class="room-slice">
|
||||
<div class="slice-header">
|
||||
<div class="slice-info">
|
||||
<span class="total">Salas totales: {{ availableRooms.length }}</span>
|
||||
<span v-if="availableRooms.length" class="slice-summary">
|
||||
Analizando {{ roomSliceIds.length }} ({{ sliceStartLabel }}–{{ sliceEndLabel }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="r in roomsPage"
|
||||
:key="r.roomId"
|
||||
class="chip room-chip"
|
||||
:class="{ active: selectedRoomIds.includes(r.roomId) }"
|
||||
@click="toggleRoom(r.roomId)"
|
||||
:title="`Sala: ${r.roomId} (${r.playerCount || 0} jugadores)`"
|
||||
>
|
||||
<span class="avatar">🏠</span>
|
||||
<span class="label">{{ r.name }}</span>
|
||||
<span class="count" v-if="r.playerCount">{{ r.playerCount }}</span>
|
||||
</button>
|
||||
<button v-if="selectedRoomIds.length" class="chip clear" @click="clearRooms">Quitar selección</button>
|
||||
</div>
|
||||
<div class="dual-slider">
|
||||
<div class="track"></div>
|
||||
<div class="highlight" :style="{ left: startPct + '%', width: (endPct - startPct) + '%' }"></div>
|
||||
<input class="range start" type="range" min="0" :max="maxIndex" v-model.number="roomSliceStart" @input="onSliceStart" />
|
||||
<input class="range end" type="range" min="0" :max="maxIndex" v-model.number="roomSliceEnd" @input="onSliceEnd" />
|
||||
</div>
|
||||
<div class="quick-select">
|
||||
<button class="qs-btn" @click="selectRecent(1)" title="Más reciente">•</button>
|
||||
<button class="qs-btn" @click="selectRecent(5)" title="5 recientes">5</button>
|
||||
<button class="qs-btn" @click="selectRecent(10)" title="10 recientes">10</button>
|
||||
<button class="qs-btn" @click="selectRecent(25)" title="25 recientes">25</button>
|
||||
<button class="qs-btn" @click="addRecent(10)" title="Agregar 10 más">+10</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -481,6 +480,46 @@ const roomsPage = computed(() => {
|
||||
|
||||
const selectedUuids = ref<string[]>([]);
|
||||
const selectedRoomIds = ref<string[]>([]);
|
||||
const roomSliceStart = ref(0);
|
||||
const roomSliceEnd = ref(0);
|
||||
const maxIndex = computed(() => Math.max(0, availableRooms.value.length - 1));
|
||||
const sliceInitialized = ref(false);
|
||||
const roomSliceIds = computed(() => {
|
||||
const a = Math.max(0, Math.min(roomSliceStart.value | 0, maxIndex.value));
|
||||
const b = Math.max(0, Math.min(roomSliceEnd.value | 0, maxIndex.value));
|
||||
const start = Math.min(a, b);
|
||||
const end = Math.max(a, b);
|
||||
return availableRooms.value.slice(start, end + 1).map(r => r.roomId);
|
||||
});
|
||||
const sliceStartLabel = computed(() => roomSliceIds.value.length ? Math.min(roomSliceStart.value, roomSliceEnd.value) + 1 : 0);
|
||||
const sliceEndLabel = computed(() => roomSliceIds.value.length ? Math.max(roomSliceStart.value, roomSliceEnd.value) + 1 : 0);
|
||||
const startPct = computed(() => maxIndex.value > 0 ? (Math.min(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0);
|
||||
const endPct = computed(() => maxIndex.value > 0 ? (Math.max(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0);
|
||||
function onSliceStart() {
|
||||
if (roomSliceStart.value > roomSliceEnd.value) roomSliceStart.value = roomSliceEnd.value;
|
||||
}
|
||||
function onSliceEnd() {
|
||||
if (roomSliceEnd.value < roomSliceStart.value) roomSliceEnd.value = roomSliceStart.value;
|
||||
}
|
||||
|
||||
function selectRecent(count: number) {
|
||||
const m = maxIndex.value;
|
||||
if (m < 0) return;
|
||||
const start = Math.max(0, m - (count - 1));
|
||||
roomSliceStart.value = start;
|
||||
roomSliceEnd.value = m;
|
||||
}
|
||||
|
||||
function addRecent(count: number) {
|
||||
const m = maxIndex.value;
|
||||
if (m < 0) return;
|
||||
// Current selection length anchored at end
|
||||
const currentLen = roomSliceIds.value.length;
|
||||
const newLen = Math.min(m + 1, Math.max(0, currentLen) + Math.max(1, count));
|
||||
const start = Math.max(0, m - (newLen - 1));
|
||||
roomSliceStart.value = start;
|
||||
roomSliceEnd.value = m;
|
||||
}
|
||||
const playerLoading = ref(false);
|
||||
const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
|
||||
const playersActionsByUuid = ref<Record<string, Record<string, number>>>({});
|
||||
@@ -521,11 +560,33 @@ function toggleRoom(roomId: string) {
|
||||
}
|
||||
|
||||
function clearRooms() {
|
||||
suppressSliceSync = true;
|
||||
selectedRoomIds.value = [];
|
||||
eventFilters.roomFilter.value = [];
|
||||
eventFilters.applyFilters(EVENTS);
|
||||
// allow user to re-enable by moving sliders
|
||||
requestAnimationFrame(() => { suppressSliceSync = false; });
|
||||
}
|
||||
|
||||
// Keep event filters in sync with slice selection
|
||||
let suppressSliceSync = false;
|
||||
// Initialize full-range selection as soon as rooms are available (first time)
|
||||
watch(maxIndex, (m) => {
|
||||
if (m >= 0 && !sliceInitialized.value) {
|
||||
roomSliceStart.value = 0;
|
||||
roomSliceEnd.value = m;
|
||||
selectedRoomIds.value = [...roomSliceIds.value];
|
||||
eventFilters.roomFilter.value = [...selectedRoomIds.value];
|
||||
sliceInitialized.value = true;
|
||||
}
|
||||
});
|
||||
watch([roomSliceStart, roomSliceEnd, () => availableRooms.value.length], () => {
|
||||
if (suppressSliceSync) return;
|
||||
selectedRoomIds.value = [...roomSliceIds.value];
|
||||
eventFilters.roomFilter.value = [...selectedRoomIds.value];
|
||||
eventFilters.applyFilters(EVENTS);
|
||||
});
|
||||
|
||||
function goHome() {
|
||||
router.push('/');
|
||||
}
|
||||
@@ -687,20 +748,31 @@ function setupStreams() {
|
||||
playersActionsByUuid.value = byUuid;
|
||||
eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts);
|
||||
|
||||
// Extract unique room IDs from aggregated events - this is our primary source for rooms
|
||||
// Extract unique room IDs from aggregated events and track "newness" by last seen index
|
||||
const roomIds = new Set<string>();
|
||||
allDetailedEvents.forEach(event => {
|
||||
if (event.roomId && event.roomId.trim()) {
|
||||
roomIds.add(event.roomId);
|
||||
}
|
||||
const lastSeenIndex: Record<string, number> = {};
|
||||
allDetailedEvents.forEach((event, idx) => {
|
||||
const rid = (event.roomId || '').trim();
|
||||
if (!rid) return;
|
||||
roomIds.add(rid);
|
||||
lastSeenIndex[rid] = idx; // increasing idx means newer
|
||||
});
|
||||
|
||||
// Build available rooms list from aggregated events
|
||||
availableRooms.value = Array.from(roomIds).map(roomId => ({
|
||||
roomId,
|
||||
name: `Sala ${roomId.slice(0, 8)}`,
|
||||
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length
|
||||
}));
|
||||
// Build available rooms list sorted by lastSeenIndex ASC (older first, newest get the highest index)
|
||||
availableRooms.value = Array.from(roomIds)
|
||||
.map(roomId => ({
|
||||
roomId,
|
||||
name: `Sala ${roomId.slice(0, 8)}`,
|
||||
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length,
|
||||
_lastSeen: lastSeenIndex[roomId] ?? -1
|
||||
}))
|
||||
.sort((a, b) => (a._lastSeen - b._lastSeen))
|
||||
.map(({ _lastSeen, ...rest }) => rest);
|
||||
// Initialize slice window: full range by default, preserve user selection if present
|
||||
if (availableRooms.value.length > 0 && selectedRoomIds.value.length === 0) {
|
||||
roomSliceStart.value = 0;
|
||||
roomSliceEnd.value = availableRooms.value.length - 1;
|
||||
}
|
||||
|
||||
// Compute metrics from score history
|
||||
computeMetricsFromScores();
|
||||
@@ -938,6 +1010,35 @@ function downloadJSON() {
|
||||
border-top: 1px solid rgba(203, 213, 225, 0.5);
|
||||
}
|
||||
|
||||
/* Room slice controls */
|
||||
.room-slice { margin-top: 10px; padding-top: 12px; border-top: 1px solid rgba(203,213,225,0.5); display: flex; flex-direction: column; gap: 10px; }
|
||||
.slice-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.slice-info { display: flex; align-items: center; gap: 10px; color: #334155; font-weight: 600; }
|
||||
.slice-summary { color: #475569; font-weight: 700; }
|
||||
|
||||
/* Dual range slider */
|
||||
.dual-slider { position: relative; height: 36px; padding: 16px 8px; }
|
||||
.dual-slider .track { position: absolute; left: 8px; right: 8px; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%); box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
|
||||
.dual-slider .highlight { position: absolute; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(90deg, #06b6d4, #8b5cf6); box-shadow: 0 2px 8px rgba(139,92,246,0.25); }
|
||||
.dual-slider .range { -webkit-appearance: none; appearance: none; position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: transparent; pointer-events: none; }
|
||||
.dual-slider .range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
|
||||
.dual-slider .range::-moz-range-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
|
||||
.dual-slider .range::-ms-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
|
||||
.dual-slider .range.start { z-index: 2; }
|
||||
.dual-slider .range.end { z-index: 3; }
|
||||
|
||||
/* Quick select buttons: super compact and subtle */
|
||||
.quick-select { display: flex; justify-content: flex-end; gap: 6px; margin-top: 4px; }
|
||||
.qs-btn { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148,163,184,0.35); background: rgba(255,255,255,0.6); color: #475569; font-size: 11px; font-weight: 800; cursor: pointer; opacity: 0.85; transition: all 0.2s ease; }
|
||||
.qs-btn:hover { opacity: 1; box-shadow: 0 2px 6px rgba(0,0,0,0.08); transform: translateY(-1px); }
|
||||
.qs-btn:active { transform: translateY(0); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dual-slider { height: 32px; padding: 14px 6px; }
|
||||
.dual-slider .track, .dual-slider .highlight { height: 6px; }
|
||||
.dual-slider .range::-webkit-slider-thumb, .dual-slider .range::-moz-range-thumb { width: 18px; height: 18px; }
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user