公告: - 服务端把当前公告落库到 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。
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { createPinia, setActivePinia } from 'pinia'
|
||
import type { EventSettings } from '@/shared'
|
||
|
||
/**
|
||
* 榜单口径(standings.individual_to_team_weight)的保存契约。
|
||
*
|
||
* adminApi 的 http 实例在模块初始化时就绑定了 `globalThis.fetch`,所以这里先替换 fetch,
|
||
* 再用动态 import 拿到 store 与控制器,才能观察到真实请求的 URL / 方法与载荷。
|
||
*/
|
||
|
||
const SETTINGS: EventSettings = {
|
||
event: { name: '测试赛事', subtitle: null, host: null, logoPath: null, primaryColor: null },
|
||
display: { background: null, logo: null, title: '测试赛事', subtitle: null, primaryColor: null, rotateSeconds: 15 },
|
||
standings: {
|
||
individualToTeamWeight: 1,
|
||
rankingMode: 'points',
|
||
showIndividualBoard: true,
|
||
showIndividualMedals: true,
|
||
},
|
||
defaultProfileId: null,
|
||
requireApproval: false,
|
||
}
|
||
|
||
interface Captured {
|
||
url: string
|
||
method: string
|
||
body: Record<string, unknown> | null
|
||
}
|
||
|
||
function stubFetch(weight: number): Captured {
|
||
const captured: Captured = { url: '', method: '', body: null }
|
||
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
|
||
captured.url = String(input)
|
||
captured.method = init?.method ?? 'GET'
|
||
captured.body = init?.body === undefined ? null : (JSON.parse(String(init.body)) as Record<string, unknown>)
|
||
const data: EventSettings = { ...SETTINGS, standings: { ...SETTINGS.standings, individualToTeamWeight: weight } }
|
||
return new Response(JSON.stringify({ ok: true, data }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
}) as typeof fetch
|
||
return captured
|
||
}
|
||
|
||
test('保存榜单口径 PUT /api/settings,提交完整 standings 并回写 event store', async () => {
|
||
const captured = stubFetch(0.5)
|
||
const { useEventStore } = await import('../stores/event')
|
||
const { useBaseData } = await import('../components/baseData/useBaseData')
|
||
|
||
setActivePinia(createPinia())
|
||
const event = useEventStore()
|
||
event.settings = { ...SETTINGS }
|
||
const data = useBaseData()
|
||
|
||
const saved = await data.saveIndividualToTeamWeight(0.5)
|
||
|
||
assert.equal(captured.url, '/api/settings')
|
||
assert.equal(captured.method, 'PUT')
|
||
const standings = captured.body?.standings as Record<string, unknown>
|
||
assert.equal(standings.individualToTeamWeight, 0.5)
|
||
// 只提交一个字段会把其余口径留在服务端旧值上,所以整个 standings 子对象原样回传。
|
||
assert.equal(standings.rankingMode, 'points')
|
||
assert.equal(standings.showIndividualBoard, true)
|
||
assert.equal(standings.showIndividualMedals, true)
|
||
|
||
assert.equal(saved.standings.individualToTeamWeight, 0.5)
|
||
// 成绩录入页与积分预览直接读 event store 的设置,不回写就等于「保存成功但预览没变」。
|
||
assert.equal(event.settings?.standings.individualToTeamWeight, 0.5)
|
||
assert.equal(data.settings.value?.standings.individualToTeamWeight, 0.5)
|
||
})
|