131 lines
5.6 KiB
Vue
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>
|