historial de acciones persistente en el jugador y visible desde playerstats

This commit is contained in:
2025-08-27 18:56:01 -06:00
parent d7c0b79549
commit fb07bdab51
3 changed files with 214 additions and 5 deletions

View File

@@ -6,11 +6,11 @@
:aria-label="hasShame ? 'Jugador con vergüenza' : 'Jugador'"
>
<div class="header">
<div class="name">{{ player.name || '—' }}</div>
<div class="name clickable" @click.stop="toggleHistory" :title="'Ver historial'">{{ player.name || '—' }}</div>
<div class="role" :class="player.role">{{ player.role || '—' }}</div>
<div v-if="hasShame" class="shame-badge" :title="`Vergüenza: ${player.shameTokens}`">😶 × {{ player.shameTokens }}</div>
</div>
<div class="tokens">
<div v-if="!showHistory" class="tokens">
<div class="token pill">
<span class="icon">🦃</span>
<span class="val"><AnimatedNumber :value="player.pavoTokens ?? 0" /></span>
@@ -26,16 +26,49 @@
</div>
</Transition>
</div>
<div class="score">
<div v-if="!showHistory" class="score">
<span class="label">Puntuación</span>
<span class="value"><AnimatedNumber :value="displayScore" /></span>
</div>
</div>
<!-- Fullscreen modal overlay for history -->
<div v-if="showHistory" class="history-overlay" @click.self="toggleHistory">
<div class="history-modal">
<div class="history-modal-header">
<div class="title">Historial del sistema {{ player.name }}</div>
<button class="close-history" @click.stop="toggleHistory">Cerrar</button>
</div>
<div v-if="loadingHistory" class="history-loading">Cargando</div>
<div v-else-if="!historyItems.length" class="history-empty">Sin historial</div>
<div v-else class="history-table">
<div class="history-scroll">
<div class="history-header history-grid">
<span class="th t">Hora</span>
<span class="th r">Rol</span>
<span class="th tok">Tokens</span>
<span class="th k">Evento</span>
<span class="th x">Mensaje</span>
<span class="th room">Sala</span>
</div>
<div v-for="m in historyItems" :key="m.timestamp + '-' + (m.kind||'')" class="history-row history-grid">
<span class="t">{{ fmtTime(m.timestamp) }}</span>
<span class="r">{{ (m.role || '') || '—' }}</span>
<span class="tok">🦃 {{ m.pavoTokens ?? 0 }} · 🌽 {{ m.eloteTokens ?? 0 }} <span v-if="(m.shameTokens ?? 0) > 0">· 😶 {{ m.shameTokens }}</span></span>
<span class="k">{{ m.kind }}</span>
<span class="x">{{ m.text }}</span>
<span class="room">{{ (m.roomId || '').slice(0,8) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
import AnimatedNumber from './AnimatedNumber.vue';
import { colyseusService } from '../../services/colyseus';
interface PlayerView {
sessionId?: string;
@@ -46,7 +79,7 @@ interface PlayerView {
shameTokens?: number;
}
const props = defineProps<{ player: PlayerView & { color?: string }; highlight?: boolean }>();
const props = defineProps<{ player: PlayerView & { color?: string; uuid?: string }; highlight?: boolean }>();
const highlight = computed(() => !!props.highlight);
const hasShame = computed(() => (props.player.shameTokens || 0) > 0);
@@ -54,6 +87,74 @@ const scoreAsP1 = computed(() => (props.player.pavoTokens || 0) * 1 + (props.pla
const scoreAsP2 = computed(() => (props.player.eloteTokens || 0) * 1 + (props.player.pavoTokens || 0) * 2);
const displayScore = computed(() => props.player.role === 'P2' ? scoreAsP2.value : scoreAsP1.value);
const primary = computed(() => props.player.color || '#667eea');
// History state
const showHistory = ref(false);
const loadingHistory = ref(false);
const historyItems = ref<any[]>([]);
const room = computed(() => colyseusService.gameRoom.value as any);
function toggleHistory() {
if (!showHistory.value) {
fetchHistory();
}
showHistory.value = !showHistory.value;
}
function fetchHistory() {
const r = room.value;
loadingHistory.value = true;
if (r) {
try { r.send('getSystemHistory', props.player.sessionId || ''); } catch { loadingHistory.value = false; }
} else {
// Fallback in lobby: fetch by UUID via admin API
const uuid = (props.player as any)?.uuid || '';
const base = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
if (!uuid) { loadingHistory.value = false; historyItems.value = []; return; }
fetch(`${base}/players/${uuid}/history`).then(r => r.json()).then((data) => {
historyItems.value = Array.isArray(data?.history) ? data.history : [];
loadingHistory.value = false;
}).catch(() => { loadingHistory.value = false; historyItems.value = []; });
}
}
function onSystemHistory(payload: any) {
if (!payload || payload.for !== (props.player.sessionId || '')) return;
try {
historyItems.value = Array.isArray(payload.history) ? payload.history : [];
} finally {
loadingHistory.value = false;
}
}
function fmtTime(ts: number): string {
try { return new Date(Number(ts)).toLocaleTimeString(); } catch { return ''; }
}
onMounted(() => {
const r = room.value;
if (r) r.onMessage('systemHistory', onSystemHistory);
});
onBeforeUnmount(() => {
// No explicit off(); guard inside handler
});
// Close on Escape and lock scroll while modal open
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && showHistory.value) {
e.stopPropagation();
showHistory.value = false;
}
}
onMounted(() => {
window.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown);
});
</script>
<style scoped>
@@ -68,6 +169,27 @@ const primary = computed(() => props.player.color || '#667eea');
.role { font-size:12px; padding:2px 8px; border-radius:10px; background:#f0f0f0; color:#555; }
.role.P1 { background: color-mix(in srgb, var(--primary) 15%, white); color: var(--primary); }
.role.P2 { background: color-mix(in srgb, var(--primary) 15%, white); color: var(--primary); }
.name.clickable { cursor: pointer; }
.name.clickable:hover { text-decoration: underline; text-underline-offset: 2px; }
.history-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index: 1500; }
.history-modal { width: min(900px, 94vw); background:#fff; border-radius:12px; border:1px solid #e5e9f0; box-shadow: 0 30px 80px rgba(0,0,0,0.45); }
.history-modal-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; border-bottom:1px solid #e5e9f0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:#fff; border-top-left-radius:12px; border-top-right-radius:12px; }
.history-modal-header .title { font-weight:800; font-size:14px; }
.close-history { background:#fff; color:#3949ab; border:1px solid #c7d2fe; border-radius:8px; padding:6px 10px; font-weight:700; cursor:pointer; }
.history-loading, .history-empty { font-size:12px; color:#666; padding:6px; text-align:center; }
.history-table {}
.history-scroll { max-height: 60vh; overflow:auto; border:1px solid #e5e9f0; border-radius:8px; background:#fff; margin: 10px; }
.history-grid { display:grid; grid-template-columns: 82px 36px 160px 110px 2fr 70px; gap:6px; align-items:center; }
.history-header { position: sticky; top: 0; z-index: 1; background:#667eea; border-bottom:1px solid #e5e9f0; padding:6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
.history-header .th { font-size:11px; font-weight:800; color:#ffffff; text-transform: uppercase; }
.history-row { padding:6px; border-bottom:1px solid #f1f5f9; }
.history-row:last-child { border-bottom: none; }
.history-row .t { font-family: monospace; font-size:11px; color:#666; }
.history-row .r { font-size:11px; font-weight:700; color:#555; text-transform: uppercase; }
.history-row .tok { font-size:12px; color:#334155; }
.history-row .k { font-size:12px; font-weight:700; color:#334155; }
.history-row .x { font-size:12px; color:#475569; }
.history-row .room { font-family: monospace; font-size:11px; color:#777; text-align:right; }
.shame-badge { margin-left:8px; margin-right:0; background:#fee2e2; color:#b91c1c; border:1px solid #fecaca; border-radius:999px; padding:2px 8px; font-weight:800; font-size:12px; display:flex; align-items:center; gap:6px; }
.tokens { display:flex; gap:10px; margin:8px 0; }
.pill { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#f7f7f7; border:1px solid #eee; }