Files
Scoreboard/web/src/display/components/pages/StandingsPage.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

157 lines
3.8 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { formatRankChange, resolveAssetUrl } from '@/shared'
import { BOARD_AVAILABLE_PX, fitRowFont } from '../../fitFont'
import { useScoreboard } from '../../useScoreboard'
import AnimatedNumber from '../AnimatedNumber.vue'
import PageFrame from '../PageFrame.vue'
const { standings, rankDeltaFor } = useScoreboard()
const rows = computed(() => standings.value)
// 行高由 flex 均分,字号按行数算,单位再多也不会顶到轮播条下面。
const rowFont = computed(() => fitRowFont(rows.value.length, BOARD_AVAILABLE_PX))
// 单位 logo 加载失败时不显示破图:失败藏起来、重新加载出图后再放出来,
// 两种情况下都保留占位,行内文字与行高都不会跳动。
function setLogoVisible(event: Event, visible: boolean): void {
;(event.target as HTMLImageElement).style.visibility = visible ? '' : 'hidden'
}
</script>
<template>
<PageFrame title="总积分榜">
<div class="board" :style="{ '--row-font': rowFont }">
<div class="row head">
<span class="rank">名次</span>
<span class="name">单位</span>
<span class="delta" />
<span class="points">总分</span>
</div>
<div
v-for="row in rows"
:key="row.teamId"
class="row"
:class="{ flash: rankDeltaFor(row.teamId) !== null }"
>
<span class="rank" :class="`top-${Math.min(row.rank, 4)}`">{{ row.rank }}</span>
<span class="name">
<img
v-if="row.logoPath"
class="team-logo"
:src="resolveAssetUrl(row.logoPath)"
alt=""
@error="setLogoVisible($event, false)"
@load="setLogoVisible($event, true)"
/>
<span class="team-name">{{ row.teamName }}</span>
</span>
<span class="delta">
<span v-if="rankDeltaFor(row.teamId) !== null" :class="rankDeltaFor(row.teamId)! > 0 ? 'up' : 'down'">
{{ formatRankChange(rankDeltaFor(row.teamId)) }}
</span>
</span>
<span class="points"><AnimatedNumber :value="row.totalPoints" /></span>
</div>
<div v-if="rows.length === 0" class="empty">暂无积分数据</div>
</div>
</PageFrame>
</template>
<style scoped>
.board {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.row {
display: grid;
grid-template-columns: 150px 1fr 110px 260px;
align-items: center;
gap: 12px;
flex: 1 1 0;
min-height: 0;
padding: 0 18px;
font-size: var(--row-font, 48px);
line-height: 1.1;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
transition:
background-color 0.4s ease,
box-shadow 0.4s ease;
}
.row:last-child {
border-bottom: none;
}
.head {
flex: none;
font-size: 22px;
letter-spacing: 4px;
color: var(--text-dim);
border-bottom: 2px solid var(--primary-line);
}
.rank {
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.top-1 {
color: var(--gold);
}
.top-2 {
color: #d8e2ee;
}
.top-3 {
color: #e0995e;
}
.name {
display: flex;
align-items: center;
gap: 0.28em;
min-width: 0;
overflow: hidden;
}
/* logo 尺寸用 em 跟着行字号走:既不会撑开行高,也不会挤动名次/分数列。 */
.team-logo {
flex: none;
width: 1em;
height: 1em;
object-fit: contain;
}
.team-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.points {
text-align: right;
font-weight: 800;
color: var(--gold);
font-variant-numeric: tabular-nums;
}
.delta {
text-align: right;
font-size: 0.7em;
font-weight: 700;
}
.up {
color: #5cf08a;
}
.down {
color: #ff8a8a;
}
.flash {
background: var(--primary-flash);
box-shadow: inset 4px 0 0 var(--primary);
}
.empty {
flex: 1;
display: grid;
place-items: center;
font-size: 34px;
color: var(--text-dim);
}
</style>