feat: 公告持久化(底栏常驻)+ 团体可单独报名 + 榜单系数开关

公告:
- 服务端把当前公告落库到 settings(display.announce_text / announce_seconds),
  作为 display.announce 随 snapshot 与 /api/settings 下发(非 admin 快照也保留)
- announce 指令除转发外写库;新增 announce_clear 清空并广播;两者写审计
- realtime 协议登记 announce_clear(补上,使清除能即时推到大屏)
- 大屏把「全屏推送」与「底栏常驻」拆开:推送 seconds 秒后只退出全屏态,公告留在底栏,
  直到被替换或清除;新增公告状态机(可测)与底栏取值优先级
  推送中 > 服务端持久值 > 「暂无公告」;管理端加「清除公告」与当前公告状态

团体报名:
- EntryPicker 团体/对抗模式允许不勾队员直接报名该单位(此前必须勾人,导致
  「只想给单位报个团体项目」做不到、单位下没有运动员时完全报不了)
- 核实录入页团体行:显示/填名次/保存/发布/算分闭环无缺陷(用真实服务端跑过),
  抽出共享的 entryRowLabel 供两处复用

榜单口径:
- 基础数据 → 积分模板页新增「榜单口径(全局)」卡片:individualToTeamWeight
  数字输入(默认 1、0–100),说明「个人项目得分按该系数折算计入单位总分」,
  保存走 PUT /api/settings;因该值在服务端是全局键、模板表里没有此字段,
  故做成独立卡片并标注全局,不塞进模板表单

自检:服务端 11 包全绿;前端 vue-tsc + build、node --test 99/99。
This commit is contained in:
AskaEth committed 2026-09-10 11:06:58 +08:00
1 parent cded54e440
commit b5f40642ff
24 files changed
+1073 -67

No files matched your search

+2 -2
View File
@@ -9,7 +9,7 @@ interface TickerItem {
label: string
}
const { standings, latestResults, announcement, detail, settings } = useScoreboard()
const { standings, latestResults, announceText, detail, settings } = useScoreboard()
// 与团体奖牌榜页同一口径:按服务端名次(总分优先)取前 3,不在前端按金→银→铜重排。
const topMedals = computed(() => standings.value.slice(0, 3))
@@ -79,7 +79,7 @@ onUnmounted(() => window.clearInterval(timer))
</template>
<template v-else>
<div class="announce">{{ announcement?.text ?? '暂无公告' }}</div>
<div class="announce">{{ announceText }}</div>
</template>
</div>
</div>
@@ -3,9 +3,9 @@ import { computed } from 'vue'
import { useScoreboard } from '../../useScoreboard'
import PageFrame from '../PageFrame.vue'
const { announcement } = useScoreboard()
const { announceText } = useScoreboard()
const text = computed(() => announcement.value?.text ?? '暂无公告')
const text = computed(() => announceText.value)
// 重复三遍 + padding-left 100%,滚动到末尾时下一条刚好接上,不出现空白。
const marquee = computed(() => `${text.value}  ·  ${text.value}  ·  ${text.value}`)
</script>