historial de acciones persistente en el jugador y visible desde playerstats
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user