- TopBar 的「实例 N · 屏 N」只在该窗口确实处于调试态时渲染:非 windowed(kiosk)不显示, 窗口化下按 F11 全屏也不显示(同时监听 fullscreenchange,与 App.vue 口径一致) - 积分榜与团体奖牌榜的队名左侧渲染单位 logo:数据本来就下发了(dto/standings.go 的 LogoPath),只是页面没画。图标 1em 随行字号浮动、object-fit:contain,行高不会被撑开; 无 logo 不占位,加载失败 visibility:hidden 保留占位避免队名跳动,@load 会复原 - resolveAssetUrl(路径归一化)从 admin 目录收敛到 web/src/shared/asset.ts, 管理端与大屏共用一份;原 image-path 测试随之迁到 shared/tests 未做(需服务端 DTO 支持,本次未授权):分项成绩页与个人榜的单位列同样没有 logoPath 字段, 所以那两个页面仍无法显示单位 logo。 自检:vue-tsc + pnpm build 通过;前端 node --test 69/69。
133 lines
3.6 KiB
Vue
133 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onUnmounted, ref } from 'vue'
|
|
import { formatClock } from '@/shared'
|
|
import { useScoreboard } from '../useScoreboard'
|
|
import { isWindowed } from '../windowed'
|
|
import OfflineBadge from './OfflineBadge.vue'
|
|
|
|
const { settings, options } = useScoreboard()
|
|
|
|
const now = ref(new Date().toISOString())
|
|
// 右上角时钟精确到秒,所以每秒刷新一次;15 秒的旧间隔会让秒针跳格。
|
|
const timer = window.setInterval(() => {
|
|
now.value = new Date().toISOString()
|
|
}, 1_000)
|
|
onUnmounted(() => window.clearInterval(timer))
|
|
|
|
const display = computed(() => settings.value?.display ?? null)
|
|
const title = computed(() => display.value?.title.trim() || '赛事标题待配置')
|
|
const subtitle = computed(() => display.value?.subtitle?.trim() ?? '')
|
|
const host = computed(() => settings.value?.event.host?.trim() || '主办方待配置')
|
|
const logo = computed(() => display.value?.logo?.trim() || '')
|
|
const clock = computed(() => formatClock(now.value))
|
|
// 单屏窗口化时用来分辨「这块屏是哪个实例」;现场展示(kiosk,或窗口化后按 F11 全屏)都不该出现。
|
|
// 监听在 setup 阶段就挂上,onUnmounted 里摘掉,不引入额外 import。
|
|
const fullscreen = ref(document.fullscreenElement !== null)
|
|
function onFullscreenChange(): void {
|
|
fullscreen.value = document.fullscreenElement !== null
|
|
}
|
|
document.addEventListener('fullscreenchange', onFullscreenChange)
|
|
onUnmounted(() => document.removeEventListener('fullscreenchange', onFullscreenChange))
|
|
|
|
// 实例/屏号都缺失时返回空串,模板的 v-if 顺带挡掉空元素。
|
|
const identity = computed(() => {
|
|
if (!isWindowed() || fullscreen.value) return ''
|
|
const parts: string[] = []
|
|
if (options.value.instance !== null) parts.push(`实例 ${options.value.instance}`)
|
|
if (options.value.screen !== null) parts.push(`屏 ${options.value.screen}`)
|
|
return parts.join(' · ')
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<header class="top">
|
|
<div class="logo">
|
|
<img v-if="logo" :src="logo" alt="" />
|
|
<span v-else>会标</span>
|
|
</div>
|
|
|
|
<div class="titles">
|
|
<h1>{{ title }}</h1>
|
|
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
|
|
</div>
|
|
|
|
<div class="right">
|
|
<div class="clock">{{ clock }}</div>
|
|
<div class="host">{{ host }}</div>
|
|
<div v-if="identity" class="identity">{{ identity }}</div>
|
|
<OfflineBadge />
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 28px;
|
|
min-height: 132px;
|
|
flex: none;
|
|
}
|
|
.logo {
|
|
width: 108px;
|
|
height: 108px;
|
|
flex: none;
|
|
border-radius: 50%;
|
|
border: 3px solid var(--primary);
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 22px;
|
|
letter-spacing: 2px;
|
|
color: var(--text-dim);
|
|
overflow: hidden;
|
|
}
|
|
.logo img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
.titles {
|
|
min-width: 0;
|
|
}
|
|
h1 {
|
|
font-size: 62px;
|
|
line-height: 1.1;
|
|
letter-spacing: 8px;
|
|
white-space: nowrap;
|
|
background: linear-gradient(180deg, #ffffff 25%, var(--primary-light) 100%);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
}
|
|
.subtitle {
|
|
margin-top: 8px;
|
|
font-size: 26px;
|
|
letter-spacing: 6px;
|
|
color: var(--text-dim);
|
|
}
|
|
.right {
|
|
margin-left: auto;
|
|
text-align: right;
|
|
flex: none;
|
|
}
|
|
.clock {
|
|
font-size: 44px;
|
|
font-weight: 700;
|
|
letter-spacing: 3px;
|
|
color: var(--text);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.host {
|
|
margin-top: 6px;
|
|
font-size: 24px;
|
|
letter-spacing: 3px;
|
|
color: var(--text-dim);
|
|
}
|
|
.identity {
|
|
margin-top: 4px;
|
|
font-size: 18px;
|
|
letter-spacing: 1px;
|
|
color: rgba(157, 180, 212, 0.6);
|
|
}
|
|
</style>
|