Files
Scoreboard/web/src/display/components/TopBar.vue
T
AskaEth 6f47849e1e fix(display): 隐藏调试标识 + 积分榜显示单位 logo
- 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。
2026-09-10 10:20:03 +08:00

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>