- 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。
157 lines
3.8 KiB
Vue
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>
|