final
This commit is contained in:
@@ -47,12 +47,15 @@
|
||||
:final-group2="finalGroup2"
|
||||
:final-rows="finalRows"
|
||||
:final-tie="finalTie"
|
||||
:score-for="scoreFor"
|
||||
:final-total-score="finalTotalScore"
|
||||
:preliminary-proof-stage="preliminaryProofStage"
|
||||
:final-proof-stage="finalProofStage"
|
||||
:podium-ordered="podiumOrdered"
|
||||
:podium-image="podiumImage"
|
||||
:podium-name="podiumName"
|
||||
:podium-has-result="podiumHasResult"
|
||||
:podium-score-display="podiumScoreDisplay"
|
||||
:podium-score-main="podiumScoreMain"
|
||||
:podium-tie-subtitle="podiumTieSubtitle"
|
||||
:can-view-proofs="canViewProofs"
|
||||
:has-score-proof="hasScoreProof"
|
||||
:score-proof-for="scoreProofFor"
|
||||
@@ -62,6 +65,32 @@
|
||||
@open-proof="onOpenProofFromPayload"
|
||||
/>
|
||||
|
||||
<LiveModePanel
|
||||
v-else-if="mode === 'live'"
|
||||
:t="t"
|
||||
:live-settings="liveSettings"
|
||||
:live-group-code="liveMonitorGroupCode"
|
||||
:live-group-members="liveMonitorMembers"
|
||||
:prelim-tie-rows="prelimTieRows"
|
||||
:preliminary-rows="preliminaryRows"
|
||||
:finalists="finalists"
|
||||
:live-final-group="liveMonitorFinalGroup"
|
||||
:live-final-rows="liveMonitorFinalRows"
|
||||
:final-tie-rows="finalTieRows"
|
||||
:podium-ordered="podiumOrdered"
|
||||
:player-image="playerImage"
|
||||
:display-name="displayName"
|
||||
:secondary-name="secondaryName"
|
||||
:score-for="scoreFor"
|
||||
:prelim-total="preliminaryTotalScore"
|
||||
:final-total="finalTotalScore"
|
||||
:podium-image="podiumImage"
|
||||
:podium-name="podiumName"
|
||||
:podium-has-result="podiumHasResult"
|
||||
:podium-score-main="podiumScoreMain"
|
||||
:podium-tie-subtitle="podiumTieSubtitle"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<AdminLoginPanel
|
||||
v-if="!adminToken"
|
||||
@@ -80,6 +109,7 @@
|
||||
:admin-tabs="adminTabs"
|
||||
:admin-tab="adminTab"
|
||||
:view-proof-in-view="canViewProofs"
|
||||
:live-settings="liveSettings"
|
||||
:group-setup-input="groupSetupInput"
|
||||
:admin-group-cards="adminGroupCards"
|
||||
:new-player="newPlayer"
|
||||
@@ -90,12 +120,13 @@
|
||||
:player-image="playerImage"
|
||||
:group-option-label="groupOptionLabel"
|
||||
:normalized-group-code="normalizedGroupCode"
|
||||
:preliminary-rows="preliminaryRows"
|
||||
:prelim-tie-rows="prelimTieRows"
|
||||
:final-group1="finalGroup1"
|
||||
:final-group2="finalGroup2"
|
||||
:final-tie-rows="finalTieRows"
|
||||
:preliminary-score-rows="adminPreliminaryRows"
|
||||
:prelim-tie-score-rows="adminPrelimTieRows"
|
||||
:final-score-rows="adminFinalRows"
|
||||
:final-tie-score-rows="adminFinalTieRows"
|
||||
:final-rows="finalRows"
|
||||
:preliminary-total-score="preliminaryTotalScore"
|
||||
:final-total-score="finalTotalScore"
|
||||
:prelim-tie="prelimTie"
|
||||
:final-tie="finalTie"
|
||||
:score-filters="scoreFilters"
|
||||
@@ -114,6 +145,7 @@
|
||||
@refresh="fetchState()"
|
||||
@logout="adminLogout"
|
||||
@toggle-view-proof="updateViewProofSetting"
|
||||
@update-live-setting="updateLiveModeSetting"
|
||||
@change-admin-tab="adminTab = $event"
|
||||
@update:group-setup-input="groupSetupInput = $event"
|
||||
@save-group-setup="saveGroupSetup"
|
||||
@@ -168,6 +200,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import ViewModePanel from './components/ViewModePanel.vue'
|
||||
import LiveModePanel from './components/LiveModePanel.vue'
|
||||
import AdminLoginPanel from './components/AdminLoginPanel.vue'
|
||||
import AdminPanel from './components/AdminPanel.vue'
|
||||
import ProofModal from './components/ProofModal.vue'
|
||||
@@ -181,9 +214,11 @@ import { convertNameAuto } from './utils/nameTransliteration'
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || '/api'
|
||||
const FALLBACK_SYNC_MS = 15000
|
||||
const STREAM_RETRY_MS = 1500
|
||||
const PRELIM_ROUND_STAGES = ['prelim_r1', 'prelim_r2', 'prelim_r3']
|
||||
const FINAL_ROUND_STAGES = ['final_r1', 'final_r2']
|
||||
|
||||
const loading = ref(true)
|
||||
const mode = ref('view')
|
||||
const mode = ref('live')
|
||||
const viewTab = ref('groups')
|
||||
const adminTab = ref('players')
|
||||
const language = ref(localStorage.getItem('shooting_lang') === 'en' ? 'en' : 'ar')
|
||||
@@ -204,8 +239,8 @@ const playerFilter = ref('')
|
||||
const playerSort = ref('id')
|
||||
const scoreFilters = reactive({
|
||||
preliminary: '',
|
||||
final_common: '',
|
||||
prelim_tiebreak: '',
|
||||
final: '',
|
||||
final_tiebreak: '',
|
||||
})
|
||||
|
||||
@@ -214,6 +249,10 @@ const liveMode = ref('rotate')
|
||||
const selectedLiveGroup = ref('')
|
||||
const liveTick = ref(0)
|
||||
let liveTimer = null
|
||||
|
||||
const liveMonitorGroupIndex = ref(0)
|
||||
const liveMonitorFinalIndex = ref(0)
|
||||
let liveMonitorTimer = null
|
||||
let syncTimer = null
|
||||
let stream = null
|
||||
let streamRetryTimer = null
|
||||
@@ -256,6 +295,27 @@ const podiumRows = computed(() => state.value.derived?.podium || [])
|
||||
const prelimTie = computed(() => state.value.derived?.preliminaryRanking?.tieBreak || { required: false, resolved: true, slots: 0, playerIds: [] })
|
||||
const finalTie = computed(() => state.value.derived?.finalRanking?.tieBreak || { required: false, resolved: true, slots: 0, playerIds: [] })
|
||||
const canViewProofs = computed(() => Boolean(state.value.settings?.viewProofInView))
|
||||
const liveSettings = computed(() => {
|
||||
const raw = state.value.settings?.liveMode || {}
|
||||
const groupDisplayMode = raw.groupDisplayMode === 'fixed' ? 'fixed' : 'rotate'
|
||||
const finalDisplayMode = raw.finalDisplayMode === 'fixed' ? 'fixed' : 'rotate'
|
||||
const activeView = normalizeLiveActiveView(raw.activeView || 'group_live')
|
||||
return {
|
||||
activeView,
|
||||
showGroupLive: raw.showGroupLive !== false,
|
||||
groupDisplayMode,
|
||||
groupFixedCode: normalizedGroupCode(raw.groupFixedCode || ''),
|
||||
showPrelimTie: raw.showPrelimTie !== false,
|
||||
showPrelimOverall: raw.showPrelimOverall !== false,
|
||||
showFinalGroups: raw.showFinalGroups !== false,
|
||||
finalDisplayMode,
|
||||
finalFixedGroup: Number(raw.finalFixedGroup) === 2 ? 2 : 1,
|
||||
showFinalTie: raw.showFinalTie !== false,
|
||||
showPodium: raw.showPodium !== false,
|
||||
rotationIntervalSec: Number(raw.rotationIntervalSec) > 0 ? Number(raw.rotationIntervalSec) : 5,
|
||||
rotationPlayerCount: Number(raw.rotationPlayerCount) > 0 ? Number(raw.rotationPlayerCount) : 12,
|
||||
}
|
||||
})
|
||||
|
||||
const viewTabs = computed(() => [
|
||||
{ id: 'groups', label: t('tabs.groups') },
|
||||
@@ -267,6 +327,7 @@ const viewTabs = computed(() => [
|
||||
|
||||
const adminTabs = computed(() => [
|
||||
{ id: 'players', label: t('tabs.players') },
|
||||
{ id: 'liveMode', label: t('tabs.liveModeAdmin') },
|
||||
{ id: 'preliminary', label: t('tabs.preliminary') },
|
||||
{ id: 'prelimTie', label: t('tabs.prelimTie') },
|
||||
{ id: 'final', label: t('tabs.final') },
|
||||
@@ -346,6 +407,28 @@ const liveMembers = computed(() => {
|
||||
if (!code) return []
|
||||
return playersSorted.value.filter((player) => normalizedGroupCode(player.groupCode) === code)
|
||||
})
|
||||
|
||||
const liveMonitorGroupRotationCodes = computed(() => activeGroupCodes.value)
|
||||
const liveMonitorGroupCode = computed(() => {
|
||||
if (liveSettings.value.groupDisplayMode === 'fixed') {
|
||||
const fixed = normalizedGroupCode(liveSettings.value.groupFixedCode)
|
||||
if (fixed) return fixed
|
||||
return liveMonitorGroupRotationCodes.value[0] || ''
|
||||
}
|
||||
return liveMonitorGroupRotationCodes.value[liveMonitorGroupIndex.value] || ''
|
||||
})
|
||||
const liveMonitorMembers = computed(() => {
|
||||
const code = normalizedGroupCode(liveMonitorGroupCode.value)
|
||||
if (!code) return []
|
||||
return playersSorted.value.filter((player) => normalizedGroupCode(player.groupCode) === code)
|
||||
})
|
||||
const liveMonitorFinalGroup = computed(() => {
|
||||
if (liveSettings.value.finalDisplayMode === 'fixed') {
|
||||
return liveSettings.value.finalFixedGroup === 2 ? 2 : 1
|
||||
}
|
||||
return liveMonitorFinalIndex.value % 2 === 0 ? 1 : 2
|
||||
})
|
||||
const liveMonitorFinalRows = computed(() => (liveMonitorFinalGroup.value === 2 ? finalGroup2.value : finalGroup1.value))
|
||||
const podiumOrdered = computed(() => [podiumRows.value[1] || null, podiumRows.value[0] || null, podiumRows.value[2] || null])
|
||||
|
||||
const prelimTieRows = computed(() => {
|
||||
@@ -358,6 +441,123 @@ const finalTieRows = computed(() => {
|
||||
return finalRows.value.filter((row) => ids.has(row.playerId))
|
||||
})
|
||||
|
||||
const playerByID = computed(() => {
|
||||
const map = new Map()
|
||||
for (const player of playersSorted.value) {
|
||||
map.set(player.id, player)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const preliminaryRowByID = computed(() => {
|
||||
const map = new Map()
|
||||
for (const row of preliminaryRows.value) {
|
||||
map.set(row.playerId, row)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const finalRowByID = computed(() => {
|
||||
const map = new Map()
|
||||
for (const row of finalRows.value) {
|
||||
map.set(row.playerId, row)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const adminPreliminaryRows = computed(() => {
|
||||
const rows = playersSorted.value.map((player) => {
|
||||
const derived = preliminaryRowByID.value.get(player.id)
|
||||
return {
|
||||
playerId: player.id,
|
||||
nameAr: player.nameAr,
|
||||
nameEn: player.nameEn,
|
||||
groupCode: player.groupCode,
|
||||
imageData: player.imageData,
|
||||
score: derived?.score ?? preliminaryTotalScore(player.id),
|
||||
tieBreak: derived?.tieBreak ?? 0,
|
||||
rank: derived?.rank ?? 0,
|
||||
}
|
||||
})
|
||||
rows.sort(compareRowsByGroupThenId)
|
||||
return rows
|
||||
})
|
||||
|
||||
const adminPrelimTieRows = computed(() => {
|
||||
const rows = (prelimTie.value.playerIds || [])
|
||||
.map((playerId) => {
|
||||
const player = playerByID.value.get(playerId)
|
||||
if (!player) return null
|
||||
const derived = preliminaryRowByID.value.get(playerId)
|
||||
return {
|
||||
playerId,
|
||||
nameAr: player.nameAr,
|
||||
nameEn: player.nameEn,
|
||||
groupCode: player.groupCode,
|
||||
imageData: player.imageData,
|
||||
score: derived?.score ?? preliminaryTotalScore(playerId),
|
||||
tieBreak: derived?.tieBreak ?? 0,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
rows.sort(compareRowsByScoreGroupThenId)
|
||||
return rows
|
||||
})
|
||||
|
||||
const adminFinalRows = computed(() => {
|
||||
const rows = [...finalGroup1.value, ...finalGroup2.value].map((row) => {
|
||||
const player = playerByID.value.get(row.playerId)
|
||||
const ranked = finalRowByID.value.get(row.playerId)
|
||||
return {
|
||||
playerId: row.playerId,
|
||||
nameAr: player?.nameAr || row.nameAr,
|
||||
nameEn: player?.nameEn || row.nameEn,
|
||||
groupCode: player?.groupCode || row.groupCode,
|
||||
imageData: player?.imageData || row.imageData,
|
||||
finalGroup: row.finalGroup,
|
||||
seed: row.seed,
|
||||
score: ranked?.score ?? row.score ?? 0,
|
||||
tieBreak: ranked?.tieBreak ?? row.tieBreak ?? 0,
|
||||
}
|
||||
})
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const groupA = Number(a.finalGroup || 0)
|
||||
const groupB = Number(b.finalGroup || 0)
|
||||
if (groupA !== groupB) return groupA - groupB
|
||||
const seedA = Number(a.seed || 0)
|
||||
const seedB = Number(b.seed || 0)
|
||||
if (seedA !== seedB) return seedA - seedB
|
||||
return a.playerId - b.playerId
|
||||
})
|
||||
|
||||
return rows
|
||||
})
|
||||
|
||||
const adminFinalTieRows = computed(() => {
|
||||
const rows = (finalTie.value.playerIds || [])
|
||||
.map((playerId) => {
|
||||
const player = playerByID.value.get(playerId)
|
||||
if (!player) return null
|
||||
const derived = finalRowByID.value.get(playerId)
|
||||
return {
|
||||
playerId,
|
||||
nameAr: player.nameAr,
|
||||
nameEn: player.nameEn,
|
||||
groupCode: player.groupCode,
|
||||
imageData: player.imageData,
|
||||
finalGroup: derived?.finalGroup ?? 0,
|
||||
seed: derived?.seed ?? 0,
|
||||
score: derived?.score ?? finalTotalScore(playerId),
|
||||
tieBreak: derived?.tieBreak ?? 0,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
rows.sort(compareRowsByScoreGroupThenId)
|
||||
return rows
|
||||
})
|
||||
|
||||
watch(language, (value) => {
|
||||
localStorage.setItem('shooting_lang', value)
|
||||
document.documentElement.lang = value
|
||||
@@ -375,6 +575,18 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() =>
|
||||
`${mode.value}|${liveSettings.value.activeView}|${liveSettings.value.groupDisplayMode}|${liveSettings.value.finalDisplayMode}|${liveSettings.value.rotationIntervalSec}|${liveMonitorGroupRotationCodes.value.join('|')}|${finalGroup1.value.length}|${finalGroup2.value.length}`,
|
||||
() => {
|
||||
if (mode.value === 'live') {
|
||||
startLiveMonitorRotation()
|
||||
return
|
||||
}
|
||||
stopLiveMonitorRotation()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => liveSelectableGroups.value.join('|'),
|
||||
() => {
|
||||
@@ -403,6 +615,30 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => liveMonitorGroupRotationCodes.value.join('|'),
|
||||
() => {
|
||||
if (liveMonitorGroupRotationCodes.value.length === 0) {
|
||||
liveMonitorGroupIndex.value = 0
|
||||
return
|
||||
}
|
||||
if (liveMonitorGroupIndex.value >= liveMonitorGroupRotationCodes.value.length) {
|
||||
liveMonitorGroupIndex.value = 0
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => `${finalGroup1.value.length}|${finalGroup2.value.length}`,
|
||||
() => {
|
||||
if (liveMonitorFinalIndex.value < 0) {
|
||||
liveMonitorFinalIndex.value = 0
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
document.documentElement.lang = language.value
|
||||
document.documentElement.dir = language.value === 'ar' ? 'rtl' : 'ltr'
|
||||
@@ -412,6 +648,7 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopLiveRotation()
|
||||
stopLiveMonitorRotation()
|
||||
stopRealtimeSync()
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
})
|
||||
@@ -425,6 +662,12 @@ function setLanguage(next) {
|
||||
if (next === 'ar' || next === 'en') language.value = next
|
||||
}
|
||||
|
||||
function normalizeLiveActiveView(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
const allowed = new Set(['group_live', 'prelim_tie', 'prelim_overall', 'final_groups', 'final_tie', 'podium'])
|
||||
return allowed.has(normalized) ? normalized : 'group_live'
|
||||
}
|
||||
|
||||
function formatServerTime(iso) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(language.value === 'ar' ? 'ar-AE' : 'en-US', {
|
||||
@@ -448,6 +691,39 @@ function groupOptionLabel(code) {
|
||||
return `${normalized} (${count})`
|
||||
}
|
||||
|
||||
function groupOrderValue(code) {
|
||||
const normalized = normalizedGroupCode(code)
|
||||
if (!normalized) return Number.MAX_SAFE_INTEGER - 1
|
||||
const idx = assignableGroups.value.indexOf(normalized)
|
||||
if (idx >= 0) return idx
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
|
||||
function compareRowsByGroupThenId(a, b) {
|
||||
const orderA = groupOrderValue(a.groupCode)
|
||||
const orderB = groupOrderValue(b.groupCode)
|
||||
if (orderA !== orderB) return orderA - orderB
|
||||
|
||||
const groupA = normalizedGroupCode(a.groupCode)
|
||||
const groupB = normalizedGroupCode(b.groupCode)
|
||||
if (groupA !== groupB) {
|
||||
if (!groupA) return 1
|
||||
if (!groupB) return -1
|
||||
return groupA.localeCompare(groupB)
|
||||
}
|
||||
|
||||
const idA = Number(a.playerId || a.id || 0)
|
||||
const idB = Number(b.playerId || b.id || 0)
|
||||
return idA - idB
|
||||
}
|
||||
|
||||
function compareRowsByScoreGroupThenId(a, b) {
|
||||
const scoreA = Number(a.score || 0)
|
||||
const scoreB = Number(b.score || 0)
|
||||
if (scoreA !== scoreB) return scoreB - scoreA
|
||||
return compareRowsByGroupThenId(a, b)
|
||||
}
|
||||
|
||||
function saveGroupSetup() {
|
||||
const parsed = parseGroupList(groupSetupInput.value)
|
||||
primaryGroups.value = parsed.length > 0 ? parsed : DEFAULT_PRIMARY_GROUPS
|
||||
@@ -628,12 +904,12 @@ async function adminLogout() {
|
||||
await fetchState(false)
|
||||
}
|
||||
|
||||
async function updateViewProofSetting(enabled) {
|
||||
async function updateAdminSettings(payload) {
|
||||
try {
|
||||
state.value = await api('/admin/settings', {
|
||||
method: 'PUT',
|
||||
admin: true,
|
||||
body: { viewProofInView: Boolean(enabled) },
|
||||
body: payload,
|
||||
})
|
||||
showToast(t('messages.saved'))
|
||||
} catch (error) {
|
||||
@@ -641,6 +917,15 @@ async function updateViewProofSetting(enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateViewProofSetting(enabled) {
|
||||
await updateAdminSettings({ viewProofInView: Boolean(enabled) })
|
||||
}
|
||||
|
||||
async function updateLiveModeSetting(patch) {
|
||||
if (!patch || typeof patch !== 'object') return
|
||||
await updateAdminSettings({ liveMode: patch })
|
||||
}
|
||||
|
||||
async function createPlayer() {
|
||||
if (!newPlayer.nameAr || !newPlayer.nameEn) {
|
||||
showToast(t('messages.mustProvideNames'), 'error')
|
||||
@@ -971,6 +1256,33 @@ function scoreFor(stage, playerId) {
|
||||
return state.value.scores?.[stage]?.[String(playerId)] ?? 0
|
||||
}
|
||||
|
||||
function totalScoreByStages(stages, playerId) {
|
||||
return stages.reduce((sum, stage) => sum + Number(scoreFor(stage, playerId) || 0), 0)
|
||||
}
|
||||
|
||||
function preliminaryTotalScore(playerId) {
|
||||
return totalScoreByStages(PRELIM_ROUND_STAGES, playerId)
|
||||
}
|
||||
|
||||
function finalTotalScore(playerId) {
|
||||
return totalScoreByStages(FINAL_ROUND_STAGES, playerId)
|
||||
}
|
||||
|
||||
function proofStageByFamily(stages, playerId) {
|
||||
for (const stage of stages) {
|
||||
if (hasScoreProof(stage, playerId)) return stage
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function preliminaryProofStage(playerId) {
|
||||
return proofStageByFamily([...PRELIM_ROUND_STAGES].reverse(), playerId)
|
||||
}
|
||||
|
||||
function finalProofStage(playerId) {
|
||||
return proofStageByFamily([...FINAL_ROUND_STAGES].reverse(), playerId)
|
||||
}
|
||||
|
||||
function scoreDraftKey(stage, playerId) {
|
||||
return `${stage}:${playerId}`
|
||||
}
|
||||
@@ -1056,6 +1368,7 @@ function closeResetStageModal() {
|
||||
async function confirmResetStage(resetProofs) {
|
||||
const stage = resetModal.stage
|
||||
if (!stage) return
|
||||
const targets = resetStageTargets(stage)
|
||||
|
||||
try {
|
||||
state.value = await api(`/admin/scores/${stage}/reset`, {
|
||||
@@ -1064,11 +1377,11 @@ async function confirmResetStage(resetProofs) {
|
||||
body: { resetProofs: Boolean(resetProofs) },
|
||||
})
|
||||
for (const key of Object.keys(scoreDrafts)) {
|
||||
if (key.startsWith(`${stage}:`)) delete scoreDrafts[key]
|
||||
if (targets.some((target) => key.startsWith(`${target}:`))) delete scoreDrafts[key]
|
||||
}
|
||||
if (resetProofs) {
|
||||
for (const key of Object.keys(scoreAdviceCache)) {
|
||||
if (key.startsWith(`${stage}:`)) delete scoreAdviceCache[key]
|
||||
if (targets.some((target) => key.startsWith(`${target}:`))) delete scoreAdviceCache[key]
|
||||
}
|
||||
}
|
||||
closeResetStageModal()
|
||||
@@ -1078,6 +1391,12 @@ async function confirmResetStage(resetProofs) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetStageTargets(stage) {
|
||||
if (stage === 'preliminary') return PRELIM_ROUND_STAGES
|
||||
if (stage === 'final') return FINAL_ROUND_STAGES
|
||||
return [stage]
|
||||
}
|
||||
|
||||
function isFinalist(playerId) {
|
||||
return finalists.value.some((row) => row.playerId === playerId)
|
||||
}
|
||||
@@ -1104,6 +1423,36 @@ function stopLiveRotation() {
|
||||
}
|
||||
}
|
||||
|
||||
function startLiveMonitorRotation() {
|
||||
stopLiveMonitorRotation()
|
||||
|
||||
const canRotateGroups =
|
||||
liveSettings.value.activeView === 'group_live' && liveSettings.value.groupDisplayMode === 'rotate' && liveMonitorGroupRotationCodes.value.length > 1
|
||||
const canRotateFinal =
|
||||
liveSettings.value.activeView === 'final_groups' && liveSettings.value.finalDisplayMode === 'rotate' && finalGroup1.value.length > 0 && finalGroup2.value.length > 0
|
||||
if (!canRotateGroups && !canRotateFinal) return
|
||||
|
||||
liveMonitorGroupIndex.value = 0
|
||||
liveMonitorFinalIndex.value = 0
|
||||
|
||||
const intervalMs = Math.max(3, Number(liveSettings.value.rotationIntervalSec) || 5) * 1000
|
||||
liveMonitorTimer = setInterval(() => {
|
||||
if (canRotateGroups) {
|
||||
liveMonitorGroupIndex.value = (liveMonitorGroupIndex.value + 1) % liveMonitorGroupRotationCodes.value.length
|
||||
}
|
||||
if (canRotateFinal) {
|
||||
liveMonitorFinalIndex.value = (liveMonitorFinalIndex.value + 1) % 2
|
||||
}
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
function stopLiveMonitorRotation() {
|
||||
if (liveMonitorTimer) {
|
||||
clearInterval(liveMonitorTimer)
|
||||
liveMonitorTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function buildStreamURL() {
|
||||
const base = API_BASE.endsWith('/') ? API_BASE.slice(0, -1) : API_BASE
|
||||
return `${base}/events`
|
||||
@@ -1181,15 +1530,14 @@ function podiumHasResult(entry) {
|
||||
return Boolean(entry && entry.score > 0)
|
||||
}
|
||||
|
||||
function podiumScoreDisplay(entry) {
|
||||
function podiumScoreMain(entry) {
|
||||
if (!podiumHasResult(entry)) return '-'
|
||||
return podiumScoreText(entry)
|
||||
return `${t('table.score')}: ${entry.score}`
|
||||
}
|
||||
|
||||
function podiumScoreText(entry) {
|
||||
if (!entry || entry.score === 0) return '—'
|
||||
const scoreText = `${t('table.score')}: ${entry.score}`
|
||||
if (!finalTie.value.required) return scoreText
|
||||
return `${scoreText} · ${t('table.tieScore')}: ${entry.tieBreak}`
|
||||
function podiumTieSubtitle(entry) {
|
||||
if (!podiumHasResult(entry)) return ''
|
||||
if (!finalTie.value.required) return ''
|
||||
return `${t('table.tieScore')}: ${entry.tieBreak}`
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user