Files
matrix-show/frontend/src/views/LiveView.vue
T
2026-06-04 17:59:53 +08:00

131 lines
5.6 KiB
Vue

<template>
<div class="live-view">
<div class="live-header">
<div class="show-title">
<h1>Hermes Live Show</h1>
<span class="live-badge" v-if="gameStore.isRunning">● LIVE</span>
</div>
<div class="show-phase">{{ phaseLabel }}</div>
</div>
<div class="live-body">
<div class="live-main">
<ChatLog :messages="messageStore.messages" />
</div>
<div class="live-sidebar">
<div class="sidebar-section">
<div class="section-title">登场角色</div>
<div class="live-characters">
<div
v-for="char in characterStore.characters"
:key="char.id"
class="live-char-chip"
:class="{ dead: char.status === 'dead' }"
>
<div class="chip-avatar">{{ char.name[0] }}</div>
<span>{{ char.name }}</span>
</div>
</div>
</div>
<div class="sidebar-section">
<ProgressBar :current-phase="gameStore.currentPhase" />
</div>
<div class="sidebar-section" v-if="clueStore.clues.length > 0">
<div class="section-title">线索</div>
<div class="live-clues">
<div
v-for="clue in clueStore.clues.filter(c => c.is_unlocked)"
:key="clue.id"
class="live-clue-item"
>
<span class="clue-icon">🔍</span>
<span>{{ clue.name }}</span>
</div>
<div v-if="clueStore.clues.filter(c => !c.is_unlocked).length > 0" class="clue-remaining">
还有 {{ clueStore.clues.filter(c => !c.is_unlocked).length }} 条线索待解锁
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed } from 'vue'
import { useMessageStore } from '../stores/messageStore'
import { useCharacterStore } from '../stores/characterStore'
import { useClueStore } from '../stores/clueStore'
import { useGameStore } from '../stores/gameStore'
import { connectSocket, disconnectSocket } from '../services/socket'
import ChatLog from '../components/ChatLog.vue'
import ProgressBar from '../components/ProgressBar.vue'
import type { Message } from '../types/message'
import type { GamePhase } from '../types/game'
const messageStore = useMessageStore()
const characterStore = useCharacterStore()
const clueStore = useClueStore()
const gameStore = useGameStore()
const PHASE_LABELS: Record<GamePhase, string> = {
intro: '🎬 开场介绍',
round1_speak: '💬 第一轮发言',
round1_search: '🔍 第一轮搜证',
round2_speak: '💬 第二轮发言',
round2_search: '🔍 第二轮搜证',
final_discuss: '🗣 最终讨论',
voting: '🗳 投票环节',
reveal: '🔎 揭晓真凶',
}
const phaseLabel = computed(() => PHASE_LABELS[gameStore.currentPhase] || '准备中')
onMounted(async () => {
const socket = connectSocket()
socket.on('new_message', (msg: Message) => { messageStore.addMessage(msg) })
socket.on('state_change', (data: { phase: GamePhase }) => { gameStore.state.current_phase = data.phase })
socket.on('clue_unlocked', () => {
if (gameStore.activeScriptId) {
clueStore.fetchClues(gameStore.activeScriptId)
}
})
await gameStore.fetchState()
if (gameStore.activeScriptId) {
await Promise.all([
characterStore.fetchCharacters(),
messageStore.fetchMessages(),
clueStore.fetchClues(gameStore.activeScriptId),
])
}
})
onUnmounted(() => { disconnectSocket() })
</script>
<style scoped>
.live-view { display: flex; flex-direction: column; height: 100%; background: var(--bg-primary); }
.live-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1.5rem; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); }
.show-title { display: flex; align-items: center; gap: 0.75rem; }
.show-title h1 { font-size: 1.2rem; font-weight: 700; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.live-badge { color: var(--accent-danger); font-weight: 700; font-size: 0.8rem; animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.show-phase { font-size: 0.9rem; color: var(--text-secondary); font-weight: 500; }
.live-body { flex: 1; display: flex; overflow: hidden; }
.live-main { flex: 1; overflow: hidden; }
.live-sidebar { width: 220px; background: var(--bg-secondary); border-left: 1px solid var(--border-color); overflow-y: auto; }
.sidebar-section { padding: 0.75rem; border-bottom: 1px solid var(--border-color); }
.section-title { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.5px; }
.live-characters { display: flex; flex-direction: column; gap: 0.4rem; }
.live-char-chip { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.5rem; border-radius: 6px; background: var(--bg-card); font-size: 0.8rem; }
.live-char-chip.dead { opacity: 0.4; }
.chip-avatar { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); display: flex; align-items: center; justify-content: center; font-size: 0.65rem; font-weight: 700; color: white; }
.live-clues { display: flex; flex-direction: column; gap: 0.3rem; }
.live-clue-item { font-size: 0.75rem; display: flex; align-items: center; gap: 0.3rem; }
.clue-remaining { font-size: 0.7rem; color: var(--text-muted); font-style: italic; }
</style>