generated from VLADIMIR/template_frontend
fix
This commit is contained in:
@@ -26,15 +26,18 @@ import type {
|
||||
GameReview,
|
||||
MapItem,
|
||||
Question,
|
||||
ResultsQuestion,
|
||||
ScreenCommand,
|
||||
ScreenMode,
|
||||
ScreenState,
|
||||
Team,
|
||||
TeamAnswers,
|
||||
TeamResult,
|
||||
Ticket,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import { getURL } from '@/api/generated/crabs/evening_detective_server/client'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||
@@ -93,6 +96,8 @@ const teamsNeedingApplications = computed(
|
||||
),
|
||||
)
|
||||
const screens = ref<ScreenState[]>([])
|
||||
const previewQuestions = ref<ResultsQuestion[]>([])
|
||||
const previewTeams = ref<TeamResult[]>([])
|
||||
const mapItems = ref<MapItem[]>([])
|
||||
const mapSaving = ref(false)
|
||||
const screenVolume = ref(100)
|
||||
@@ -106,6 +111,7 @@ const currentScreenMode = computed<ScreenMode | undefined>(() => {
|
||||
return modes[0] as ScreenMode
|
||||
})
|
||||
const timerScreen = computed(() => screens.value[0])
|
||||
const previewScreen = computed(() => screens.value.find((screen) => screen.status === 'connected') || screens.value[0])
|
||||
const timerStateLabel = computed(() => {
|
||||
switch (timerScreen.value?.timerState) {
|
||||
case 'SCREEN_TIMER_STATE_RUNNING':
|
||||
@@ -175,6 +181,16 @@ async function getGameScreens() {
|
||||
timerLabel.value = screens.value[0].timerLabel || 'Перерыв'
|
||||
timerFormInitialized.value = true
|
||||
}
|
||||
if (previewScreen.value?.mode === 'SCREEN_MODE_RESULTS') {
|
||||
const result = await client.GetGameResults({ gameId: Number(gameId) })
|
||||
if (result.error == '') {
|
||||
previewQuestions.value = result.questions || []
|
||||
previewTeams.value = result.teams || []
|
||||
}
|
||||
} else {
|
||||
previewQuestions.value = []
|
||||
previewTeams.value = []
|
||||
}
|
||||
}
|
||||
getGameScreens()
|
||||
|
||||
@@ -911,6 +927,7 @@ useSimplePolling(() => {
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div class="map-preview-layout">
|
||||
<div class="map-block">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<p class="screens-title">Карта помещения</p>
|
||||
@@ -992,6 +1009,17 @@ useSimplePolling(() => {
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<aside class="screen-preview-panel">
|
||||
<p class="screens-title">Превью экрана</p>
|
||||
<ScreenPreview
|
||||
v-if="previewScreen?.status === 'connected'"
|
||||
:state="previewScreen"
|
||||
:questions="previewQuestions"
|
||||
:teams="previewTeams"
|
||||
/>
|
||||
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="team-block team-block-hover" @click="showAddTeamModal = true">
|
||||
<p class="place-image-plus">+</p>
|
||||
@@ -1590,13 +1618,30 @@ useSimplePolling(() => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.map-block {
|
||||
.map-preview-layout {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
}
|
||||
|
||||
.map-block,
|
||||
.screen-preview-panel {
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
margin: 12px 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.screen-preview-placeholder {
|
||||
align-items: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #111;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.map-field {
|
||||
background-color: #151515;
|
||||
border: 1px dashed rgb(255 255 255 / 20%);
|
||||
@@ -1700,6 +1745,12 @@ useSimplePolling(() => {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (width < 1024px) {
|
||||
.map-preview-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.issued-ticket {
|
||||
align-items: center;
|
||||
border-top: 1px solid #777;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<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())
|
||||
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 - Math.floor((timerNow.value - timerSyncedAt.value) / 1000))
|
||||
})
|
||||
const timerDisplay = computed(() => {
|
||||
const minutes = Math.floor(timerRemainingSeconds.value / 60)
|
||||
const seconds = 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],
|
||||
() => {
|
||||
timerRemainingAtSync.value = Math.max(0, Math.floor(props.state.timerRemainingSeconds || 0))
|
||||
timerSyncedAt.value = Date.now()
|
||||
},
|
||||
{ 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_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>
|
||||
Reference in New Issue
Block a user