generated from VLADIMIR/template_frontend
192 lines
5.5 KiB
Vue
192 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
|
import type {
|
|
ResultsQuestion,
|
|
ScreenState,
|
|
TeamResult,
|
|
} from '@/api/generated/crabs/evening_detective_server'
|
|
import { applicationFileURL } from '@/utils/application'
|
|
|
|
const props = defineProps<{
|
|
state: ScreenState
|
|
questions: ResultsQuestion[]
|
|
teams: TeamResult[]
|
|
}>()
|
|
|
|
const timerNow = ref(Date.now())
|
|
const timerRemainingAtSync = ref(0)
|
|
const timerSyncedAt = ref(Date.now())
|
|
const timerStateAtSync = ref<string>()
|
|
let timerTick: number | null = null
|
|
|
|
const screenBackground = computed(() => {
|
|
const image = props.state.backgroundImage || props.state.coverImage
|
|
return image
|
|
? { backgroundImage: `linear-gradient(rgb(0 0 0 / 55%), rgb(0 0 0 / 55%)), url("${image}")` }
|
|
: {}
|
|
})
|
|
const timerRemainingSeconds = computed(() => {
|
|
if (props.state.timerState !== 'SCREEN_TIMER_STATE_RUNNING') return timerRemainingAtSync.value
|
|
return Math.max(0, timerRemainingAtSync.value - (timerNow.value - timerSyncedAt.value) / 1000)
|
|
})
|
|
const timerDisplay = computed(() => {
|
|
const minutes = Math.floor(timerRemainingSeconds.value / 60)
|
|
const seconds = Math.floor(timerRemainingSeconds.value % 60)
|
|
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
|
})
|
|
const introAudio = computed(() => applicationFileURL(props.state.introAudio))
|
|
const conclusionVideo = computed(() => applicationFileURL(props.state.conclusionVideo))
|
|
|
|
watch(
|
|
() => [props.state.timerRemainingSeconds, props.state.timerState, props.state.stateVersion],
|
|
() => {
|
|
const remaining = Math.max(0, Number(props.state.timerRemainingSeconds || 0))
|
|
if (props.state.timerState === 'SCREEN_TIMER_STATE_RUNNING' && timerStateAtSync.value === props.state.timerState) {
|
|
const localRemaining = Math.max(0, timerRemainingAtSync.value - (Date.now() - timerSyncedAt.value) / 1000)
|
|
if (Math.abs(remaining - localRemaining) <= 0.5) return
|
|
}
|
|
timerRemainingAtSync.value = remaining
|
|
timerSyncedAt.value = Date.now()
|
|
timerStateAtSync.value = props.state.timerState
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
onMounted(() => {
|
|
timerTick = window.setInterval(() => {
|
|
timerNow.value = Date.now()
|
|
}, 250)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (timerTick != null) window.clearInterval(timerTick)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="screen-preview" :style="screenBackground">
|
|
<div v-if="state.mode === 'SCREEN_MODE_TIMER'" class="timer-preview">
|
|
<h2>{{ state.timerLabel || 'Перерыв' }}</h2>
|
|
<strong>{{ timerDisplay }}</strong>
|
|
</div>
|
|
<audio v-else-if="state.mode === 'SCREEN_MODE_INTRO' && introAudio" :src="introAudio" controls />
|
|
<video
|
|
v-else-if="state.mode === 'SCREEN_MODE_CONCLUSION' && conclusionVideo"
|
|
:src="conclusionVideo"
|
|
autoplay
|
|
class="preview-video"
|
|
loop
|
|
muted
|
|
playsinline
|
|
/>
|
|
<div v-else-if="state.mode === 'SCREEN_MODE_CONCLUSION'" class="preview-message">
|
|
Видео закрывашки не задано
|
|
</div>
|
|
<div v-else-if="state.mode === 'SCREEN_MODE_INTRO_DONE'" class="preview-message">Успехов</div>
|
|
<div v-else-if="state.mode === 'SCREEN_MODE_CONCLUSION_DONE'" class="preview-message">
|
|
Спасибо за игру!
|
|
</div>
|
|
<div v-else-if="state.mode === 'SCREEN_MODE_TEXT'" class="preview-message">{{ state.text }}</div>
|
|
<div v-else-if="state.mode === 'SCREEN_MODE_RESULTS'" class="preview-results">
|
|
<h2>Результаты</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Место</th>
|
|
<th>Команда</th>
|
|
<th v-for="question in questions" :key="question.code">{{ question.code }}</th>
|
|
<th>Сумма</th>
|
|
<th>Ходы</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="result in teams" :key="result.team?.ID">
|
|
<td>{{ result.place }}</td>
|
|
<td>{{ result.team?.name }}</td>
|
|
<td v-for="question in questions" :key="question.code">
|
|
{{ result.answers?.find((answer) => answer.questionCode === question.code)?.score ?? 0 }}
|
|
</td>
|
|
<td>{{ result.totalScore }}</td>
|
|
<td>{{ result.actionsCount }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<img v-else-if="state.coverImage" :src="state.coverImage" alt="Обложка дела" class="preview-cover" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.screen-preview {
|
|
align-items: center;
|
|
aspect-ratio: 16 / 9;
|
|
background-color: #111;
|
|
background-position: center;
|
|
background-size: cover;
|
|
color: #eee;
|
|
display: flex;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
position: relative;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.timer-preview {
|
|
max-width: 95%;
|
|
}
|
|
|
|
.timer-preview h2,
|
|
.preview-message,
|
|
.preview-results h2 {
|
|
font-size: clamp(16px, 3vw, 32px);
|
|
margin: 0 0 10px;
|
|
}
|
|
|
|
.timer-preview strong {
|
|
display: block;
|
|
font-size: clamp(48px, 10vw, 120px);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 0.9;
|
|
}
|
|
|
|
.screen-preview audio,
|
|
.preview-video {
|
|
max-width: 90%;
|
|
pointer-events: none;
|
|
width: 90%;
|
|
}
|
|
|
|
.preview-video {
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.preview-cover {
|
|
height: 100%;
|
|
object-fit: contain;
|
|
width: 100%;
|
|
}
|
|
|
|
.preview-results {
|
|
max-height: 90%;
|
|
max-width: 96%;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
}
|
|
|
|
.preview-results table {
|
|
border-collapse: collapse;
|
|
font-size: 10px;
|
|
width: 100%;
|
|
}
|
|
|
|
.preview-results th,
|
|
.preview-results td {
|
|
border-bottom: 1px solid rgb(255 255 255 / 18%);
|
|
padding: 3px;
|
|
text-align: left;
|
|
}
|
|
</style>
|