generated from VLADIMIR/template_frontend
Показать таймер на внешнем экране
This commit is contained in:
@@ -29,10 +29,14 @@ const mediaError = ref('')
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const binding = ref(false)
|
const binding = ref(false)
|
||||||
const bindSuccess = ref(false)
|
const bindSuccess = ref(false)
|
||||||
|
const timerNow = ref(Date.now())
|
||||||
|
const timerRemainingAtSync = ref(0)
|
||||||
|
const timerSyncedAt = ref(Date.now())
|
||||||
const introAudioElement = ref<HTMLAudioElement>()
|
const introAudioElement = ref<HTMLAudioElement>()
|
||||||
const conclusionVideoElement = ref<HTMLVideoElement>()
|
const conclusionVideoElement = ref<HTMLVideoElement>()
|
||||||
let appliedCommandVersion = 0
|
let appliedCommandVersion = 0
|
||||||
let pollTimer: number | null = null
|
let pollTimer: number | null = null
|
||||||
|
let timerTick: number | null = null
|
||||||
|
|
||||||
const bindingFlow = computed(() => Boolean(route.query.screenId && route.query.accessToken))
|
const bindingFlow = computed(() => Boolean(route.query.screenId && route.query.accessToken))
|
||||||
const qrValue = computed(() => {
|
const qrValue = computed(() => {
|
||||||
@@ -49,6 +53,15 @@ const gameOptions = computed(() =>
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
const screenMode = computed(() => state.value?.mode || 'SCREEN_MODE_COVER')
|
const screenMode = computed(() => state.value?.mode || 'SCREEN_MODE_COVER')
|
||||||
|
const timerRemainingSeconds = computed(() => {
|
||||||
|
if (state.value?.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(state.value?.introAudio))
|
const introAudio = computed(() => applicationFileURL(state.value?.introAudio))
|
||||||
const conclusionVideo = computed(() => applicationFileURL(state.value?.conclusionVideo))
|
const conclusionVideo = computed(() => applicationFileURL(state.value?.conclusionVideo))
|
||||||
const screenBackground = computed(() => {
|
const screenBackground = computed(() => {
|
||||||
@@ -60,6 +73,11 @@ function saveSession() {
|
|||||||
sessionStorage.setItem(sessionKey, JSON.stringify({ screenId: screenId.value, accessToken: accessToken.value }))
|
sessionStorage.setItem(sessionKey, JSON.stringify({ screenId: screenId.value, accessToken: accessToken.value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncTimer(value: ScreenState | undefined) {
|
||||||
|
timerRemainingAtSync.value = Math.max(0, Math.floor(value?.timerRemainingSeconds || 0))
|
||||||
|
timerSyncedAt.value = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
function readSession() {
|
function readSession() {
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(sessionStorage.getItem(sessionKey) || '{}') as {
|
const saved = JSON.parse(sessionStorage.getItem(sessionKey) || '{}') as {
|
||||||
@@ -89,6 +107,7 @@ async function loadState() {
|
|||||||
const res = await client.GetScreenState({ screenId: screenId.value, accessToken: accessToken.value })
|
const res = await client.GetScreenState({ screenId: screenId.value, accessToken: accessToken.value })
|
||||||
if (res.error) throw new Error(res.error)
|
if (res.error) throw new Error(res.error)
|
||||||
state.value = res.state
|
state.value = res.state
|
||||||
|
syncTimer(state.value)
|
||||||
if (state.value?.gameId && state.value.mode === 'SCREEN_MODE_RESULTS') await loadResults()
|
if (state.value?.gameId && state.value.mode === 'SCREEN_MODE_RESULTS') await loadResults()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error(err instanceof Error ? err.message : 'Не удалось получить состояние экрана')
|
message.error(err instanceof Error ? err.message : 'Не удалось получить состояние экрана')
|
||||||
@@ -191,6 +210,9 @@ onMounted(async () => {
|
|||||||
if (!screenId.value || !accessToken.value) await createSession()
|
if (!screenId.value || !accessToken.value) await createSession()
|
||||||
await loadState()
|
await loadState()
|
||||||
pollTimer = window.setInterval(loadState, 1000)
|
pollTimer = window.setInterval(loadState, 1000)
|
||||||
|
timerTick = window.setInterval(() => {
|
||||||
|
timerNow.value = Date.now()
|
||||||
|
}, 250)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error(err instanceof Error ? err.message : 'Не удалось подготовить экран')
|
message.error(err instanceof Error ? err.message : 'Не удалось подготовить экран')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -200,6 +222,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (pollTimer != null) window.clearInterval(pollTimer)
|
if (pollTimer != null) window.clearInterval(pollTimer)
|
||||||
|
if (timerTick != null) window.clearInterval(timerTick)
|
||||||
if (resultsScrollTimer != null) window.clearInterval(resultsScrollTimer)
|
if (resultsScrollTimer != null) window.clearInterval(resultsScrollTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -244,6 +267,10 @@ onUnmounted(() => {
|
|||||||
<div v-else-if="screenMode === 'SCREEN_MODE_CONCLUSION'" class="screen-message">Видео закрывашки не задано</div>
|
<div v-else-if="screenMode === 'SCREEN_MODE_CONCLUSION'" class="screen-message">Видео закрывашки не задано</div>
|
||||||
<div v-else-if="screenMode === 'SCREEN_MODE_INTRO_DONE'" class="screen-message">Успехов</div>
|
<div v-else-if="screenMode === 'SCREEN_MODE_INTRO_DONE'" class="screen-message">Успехов</div>
|
||||||
<div v-else-if="screenMode === 'SCREEN_MODE_CONCLUSION_DONE'" class="screen-message">Спасибо за игру!</div>
|
<div v-else-if="screenMode === 'SCREEN_MODE_CONCLUSION_DONE'" class="screen-message">Спасибо за игру!</div>
|
||||||
|
<div v-else-if="screenMode === 'SCREEN_MODE_TIMER'" class="timer-screen">
|
||||||
|
<h1 class="timer-label">{{ state.timerLabel || 'Перерыв' }}</h1>
|
||||||
|
<div class="timer-value">{{ timerDisplay }}</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="screenMode === 'SCREEN_MODE_RESULTS'" ref="resultsContainer" class="results-screen">
|
<div v-else-if="screenMode === 'SCREEN_MODE_RESULTS'" ref="resultsContainer" class="results-screen">
|
||||||
<h1>Результаты</h1>
|
<h1>Результаты</h1>
|
||||||
<table>
|
<table>
|
||||||
@@ -382,6 +409,27 @@ onUnmounted(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timer-screen {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-label {
|
||||||
|
font-size: clamp(32px, 6vw, 88px);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-value {
|
||||||
|
font-size: clamp(96px, 24vw, 360px);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.screen-media {
|
.screen-media {
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
width: 900px;
|
width: 900px;
|
||||||
|
|||||||
Reference in New Issue
Block a user