fix(display): 实时赛况不再挤压省略 —— 单位列宽由内容算,字号把单位列一并纳入预算

主人报:大屏「实时赛况」页在两栏显示时,单位名较长、成绩较长(如 1:12.66)会被挤压显示省略号。

实测根因:**单位列宽写死**(`.boards-2 .row` 的 `60px minmax(0,1fr) 190px …`)而单位名墨迹实测
206~343px → 必然进省略态;单栏 524 vs 694、split 166 vs 437 同理。弹性宽度全给了运动员列。
更关键的是 `fitFont.ts` 的 `fitLiveRowFontPx` **只把成绩列纳入字号计算**,单位列从未进过这笔账 ——
所以字号自适应对它一直没生效。

改法:
- `fitFont.ts` 新增 `LiveRowGeometry`(各版式行宽 1710/836/815、名次列、间距、单位列下限与预算)、
  `liveTeamColumnPx`(按单位名墨迹定列宽,含 logo 1.28em)、`liveNameColumnPx`;
  `fitLiveRowFontPx` 增加 teams/teamsWithLogo 参数,按「名次+名字(≥4 汉字)+单位+成绩装得进一行」逐像素收字号;
  `resultTextEm` 更名 `textEm`(成绩与单位名共用同一套字符宽度表)。
- `LivePage.vue` 把单位名喂进字号计算并导出 `--team-width`,网格改用该变量。
- **取舍**:以收字号换完整显示(split 最狠 56→41px);单位列封顶 380/430/900px,超过才由单位名省略
  —— **成绩与名次在任何情况下都完整**(观众最关心的先保住)。

肉眼验收(1920×1080,ReadMediaFile 逐张看过):两栏 + 10 字单位名 + 12:34.56 → 全完整,43px(改前 44px);
单栏同样全完整,88px;**对照组短单位名网格改前改后完全一致、0 省略**(常态数据零变化)。
测试:fitFont +5、live-page +1(单位列宽由内容算、网格不得再有写死单位列、必须把单位名喂进 fitFont),
并修正 2 条因网格写法变化而失真的既有断言;改前跑这些用例会红。前端 767/767、vue-tsc 通过。
未验证:真机 4K/投影观感与真实服务端数据(本次数据为接口桩)。
This commit is contained in:
AskaEth committed 2026-09-13 23:52:52 +08:00
1 parent 2888200045
commit b74768b7d5
4 files changed
+331 -34

No files matched your search

+41 -11
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { fitLiveRowFontPx, liveResultColumnPx } from '../../fitFont'
import { fitLiveRowFontPx, liveResultColumnPx, liveTeamColumnPx } from '../../fitFont'
import {
LIVE_REFRESH_INTERVAL_MS,
fallbackLiveSources,
@@ -49,7 +49,7 @@ const emptyText = computed(() =>
liveFeed.value === null && !liveStale.value ? '正在获取当前项目…' : '暂无进行中的项目',
)
// 字号取行数最多的那一栏,两栏行高才一致;成绩列宽再反过来给字号封顶(见 fitFont.ts)。
// 字号取行数最多的那一栏,两栏行高才一致;成绩列与单位列的宽度再反过来给字号封顶(见 fitFont.ts)。
const rowCount = computed(() =>
boards.value.reduce((max, board) => Math.max(max, board.rows.length), 0),
)
@@ -62,20 +62,37 @@ const layout = computed(() => liveBoardLayout(boards.value.length, rowCount.valu
// 成绩列宽与字号互相牵制,所以由页面把一屏的成绩文本收齐了一次算完:行数 → 基准字号,
// 文本宽度 → 字号上限,字号 + 文本宽度 → 列宽。两栏各算各的会让两栏列宽不一致。
// 单位名列同理:它和成绩抢的是同一行的宽度,长单位名(8~12 个汉字)必须在同一笔账里算,
// 否则列宽写死、名字一长就被挤成三个点(见 fitFont.ts 的 fitLiveRowFontPx)。
const resultTexts = computed(() =>
boards.value.flatMap((board) => board.rows.map((row) => displayResult(row))),
)
const teamTexts = computed(() =>
boards.value.flatMap((board) => board.rows.map((row) => row.shortName ?? row.teamName)),
)
// 有单位 logo 时单位名那一格还要多占一个 logo 宽(TeamLogo 是 1em + 0.28em 间距)。
const teamsWithLogo = computed(() =>
boards.value.some((board) => board.rows.some((row) => row.logoPath !== null && row.logoPath !== '')),
)
const rowFontPx = computed(() =>
fitLiveRowFontPx(
liveBlockRowCount(rowCount.value, layout.value),
layout.value,
resultTexts.value,
teamTexts.value,
teamsWithLogo.value,
),
)
const boardStyle = computed(() => ({
'--row-font': `${rowFontPx.value}px`,
'--rs-width': `${liveResultColumnPx(resultTexts.value, layout.value, rowFontPx.value)}px`,
'--team-width': `${liveTeamColumnPx(
teamTexts.value,
layout.value,
rowFontPx.value,
teamsWithLogo.value,
)}px`,
}))
// 定时兜底:项目切换靠 scheduled_at 自己过期,快照不会为时间流逝推送,所以必须有这条轮询。
@@ -219,9 +236,12 @@ h3 {
}
.row {
display: grid;
/* 成绩列宽由内容算(--rs-width,见 fitFont.ts):写死列宽时 56px 字号的 `1:09.09`
墨迹有 201px,超出的部分会被 .rows 的 overflow:hidden 裁掉。名字列可省略号截断,先让位。 */
grid-template-columns: 90px minmax(0, 1fr) 300px var(--rs-width, 220px);
/* 成绩列宽与单位列宽都由内容算(--rs-width / --team-width,见 fitFont.ts):写死列宽时
56px 字号的 `1:09.09` 墨迹有 201px、10 个汉字的单位名有 343px,超出的部分不是被
.rows 的 overflow:hidden 裁掉,就是缩成三个点。名字列可省略号截断,先让位。 */
grid-template-columns:
90px minmax(0, 1fr)
var(--team-width, 300px) var(--rs-width, 220px);
align-items: center;
gap: 12px;
flex: 1 1 0;
@@ -232,23 +252,33 @@ h3 {
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
animation: row-in 0.45s ease-out both;
}
/* 单栏满宽:名次列留足两位数(96px 字号下 `10` 的墨迹约 134px),运动员与单位各占一半弹性宽度。
/* 单栏满宽:名次列留足两位数(96px 字号下 `10` 的墨迹约 134px),运动员与单位一起分剩余宽度。
单位列写成 minmax(单位宽, 1fr):单位名短的时候和运动员列一样宽(观感与改前一致),长了就先吃
自己那份、再吃运动员列的余量,不必挤别人。字号会跟着收,保证它不会收空(见 fitFont.ts)。
行高再封顶 3.2 倍字号——行数少时(尤其只有 1 行)不把行拉满整屏高:96px 的字铺在 460px 高的行里
四周全是空的,封顶后 1 行约占可用高度的七成,余下的高度留在面板底部。
这两条只加在单栏版式上:两个项目时每栏的行数可能悬殊,行高拉伸与否维持原样。 */
.boards-1 .row {
grid-template-columns: 170px minmax(0, 1fr) minmax(0, 1fr) var(--rs-width, 220px);
grid-template-columns:
170px minmax(0, 1fr)
minmax(var(--team-width, 300px), 1fr) var(--rs-width, 220px);
gap: 18px;
max-height: calc(var(--row-font, 34px) * 3.2);
}
/* 拆成两块后每块只剩半屏宽:名次列窄一档但必须装得下两位数(56px 字号下 `10` 约 78px),
运动员列分到的宽度比单位多一半,4 个汉字的名字才不会被省略号截掉(实测 4 字名 @56px 约 224px)。 */
运动员列分到的宽度比单位多一半(1.5fr : 1fr,与 fitFont.ts 的 nameFr/teamFr 一致),
单位列同样保底内容宽,长单位名不再被截成三个点。 */
.boards-1 .rows.split .row {
grid-template-columns: 88px minmax(0, 1.5fr) minmax(0, 1fr) var(--rs-width, 150px);
grid-template-columns:
88px minmax(0, 1.5fr)
minmax(var(--team-width, 150px), 1fr) var(--rs-width, 150px);
}
.boards-2 .row {
/* 两栏时名次 / 单位也窄一档,成绩列同样跟着内容走。 */
grid-template-columns: 60px minmax(0, 1fr) 190px var(--rs-width, 150px);
/* 两栏时名次也窄一档;成绩列与单位列都跟着内容走,剩下的宽度全给运动员列——
半屏里两列(运动员 / 单位)再互相比份额,行数一多就两边都不够。 */
grid-template-columns:
60px minmax(0, 1fr)
var(--team-width, 190px) var(--rs-width, 150px);
gap: 8px;
}
.row:last-child {
+159 -9
View File
@@ -58,6 +58,35 @@ interface LiveLayoutSpec {
resultBudgetPx: number
}
/**
* 一行网格的几何(px),与 LivePage.vue 的 `grid-template-columns` 一一对应。
*
* 1920×1080 舞台实测(页框 48 + 页内边距 28 + 面板内边距 20 + 行内边距 6,两个面板再扣 26 的栏间距):
* 单栏满宽的一行内容盒 1710px、拆两块(半屏)836px、两栏 815px。这几个数字跟着网格一起改。
*/
export interface LiveRowGeometry {
/** 一行内容盒(不含行内边距)的可用宽度。 */
rowWidthPx: number
/** 名次列宽。 */
rankPx: number
/** 单个列间距(一行有三处)。 */
gapPx: number
/** 单位列宽下限:原来的写死列宽,单位名短时观感不变。 */
teamFloorPx: number
/** 单位列宽预算:顶到这里就不再变宽(再长的单位名只能省略号收场),保证不会把整行撑破。 */
teamBudgetPx: number
/**
* 单位列是否与运动员列一起分剩余宽度(CSS 里的 `minmax(单位宽, 1fr)`)。
*
* 单栏与拆两块时两列都参与分宽度,单位名只要不超过它那份就不必动别人;两栏时宽度太紧,
* 单位列按内容定宽、剩下的全给运动员列(`minmax(0, 1fr)`),免得两个项目互相挤。
*/
teamSharesWidth: boolean
/** 分剩余宽度时的份数比(运动员 : 单位),与网格里的 fr 一致。 */
nameFr: number
teamFr: number
}
/**
* 各版式的四个数字。
*
@@ -71,6 +100,44 @@ const LIVE_LAYOUT_SPECS: Record<LiveLayout, LiveLayoutSpec> = {
duo: { ratio: 0.48, capPx: MAX_ROW_FONT_PX, resultFloorPx: 150, resultBudgetPx: 240 },
}
const LIVE_ROW_GEOMETRY: Record<LiveLayout, LiveRowGeometry> = {
single: {
rowWidthPx: 1710,
rankPx: 170,
gapPx: 18,
teamFloorPx: 300,
teamBudgetPx: 900,
teamSharesWidth: true,
nameFr: 1,
teamFr: 1,
},
split: {
rowWidthPx: 836,
rankPx: 88,
gapPx: 18,
teamFloorPx: 150,
teamBudgetPx: 380,
teamSharesWidth: true,
nameFr: 1.5,
teamFr: 1,
},
duo: {
rowWidthPx: 815,
rankPx: 60,
gapPx: 8,
teamFloorPx: 190,
teamBudgetPx: 430,
teamSharesWidth: false,
nameFr: 1,
teamFr: 1,
},
}
/** 一行网格的几何,测试与页面共用(见 LiveRowGeometry 的说明)。 */
export function liveRowGeometry(layout: LiveLayout): LiveRowGeometry {
return LIVE_ROW_GEOMETRY[layout]
}
/**
* 成绩列宽度下限(px),与 LivePage.vue 网格里原来的写死列宽一致。
*
@@ -96,14 +163,15 @@ export function liveResultColumnBudgetPx(layout: LiveLayout): number {
const RESULT_COLUMN_PAD_PX = 4
/**
* 成绩文本的估算墨迹宽(em = 墨迹宽 / 字号)。
* 文本的估算墨迹宽(em = 墨迹宽 / 字号)。
*
* 纯函数里量不到真实字形,所以按字符类别取四种内置字体里最宽的那个再上浮一点:
* 数字 0.70(MiSans 的 `0`/`8` 实测 0.6786,最宽)、小数点与冒号 0.34(实测 0.3393)、
* 拉丁字母 0.85(实测最宽的 `D` 是 0.8036)、空白 0.30、其余(汉字 / 破折号等)1.00
* (汉字实测 1.00)。取值只许偏大:偏大多留几像素,偏小就会重新裁掉末位数字。
* (汉字实测 1.00)。取值只许偏大:偏大多留几像素,偏小就会重新裁掉末位数字——
* 单位名同理(10 个汉字实测 10.0em,正好落在这一档)。
*/
export function resultTextEm(text: string): number {
export function textEm(text: string): number {
let em = 0
for (const ch of text) {
if (ch >= '0' && ch <= '9') em += 0.7
@@ -118,29 +186,111 @@ export function resultTextEm(text: string): number {
/** 一屏所有成绩里最宽的那个(em);页面一次算全页,各行列宽才不会左右错开。 */
function widestResultEm(results: readonly string[]): number {
let widest = 0
for (const text of results) widest = Math.max(widest, resultTextEm(text))
for (const text of results) widest = Math.max(widest, textEm(text))
return widest
}
/** 单位列的字号比例:与 LivePage.vue 里 `.team { font-size: 0.78em }` 一致。 */
const TEAM_FONT_EM = 0.78
/** 单位名前的 logo(1em)加间距(0.28em),按单位列自己的字号算。 */
const TEAM_LOGO_EM = 1.28
/** 运动员列至少要留的宽度(em):4 个汉字的名字(复姓、团体成员)不该被单位名挤没。 */
const NAME_MIN_EM = 4
/** 一屏所有单位名里最宽的那个(em),已经折成单位列自己的字号。 */
function widestTeamEm(teams: readonly string[], withLogo: boolean): number {
let widest = 0
for (const text of teams) widest = Math.max(widest, textEm(text))
if (widest <= 0) return 0
return (widest + (withLogo ? TEAM_LOGO_EM : 0)) * TEAM_FONT_EM
}
/**
* 实时赛况的行字号(px):行数定基准,成绩列宽只做上限。
* 实时赛况的单位列宽(px):当前字号下最宽单位名的估算墨迹 + 余量,下限是原来的写死列宽,
* 上限是预算(再长也只能省略号,见 LiveRowGeometry.teamBudgetPx)。
*
* 真机缺陷:两栏时成绩列写死 150px,而 `1:09.09` 在 56px 字号下墨迹 201px,末位数字被外层
* 真机缺陷:单位列原来两栏写死 190px、单栏按 1fr 分(单栏满宽时约 540px),而 10 个汉字的
* 单位名在 34px 字号下就有 343px 墨迹,「第三保障支援分队」这类名字一律被省略号截断——
* 观众看到的三个点,是列宽写死造成的,不是名字真的长到排不下。
*/
export function liveTeamColumnPx(
teams: readonly string[],
layout: LiveLayout,
fontPx: number,
withLogo = false,
): number {
const row = LIVE_ROW_GEOMETRY[layout]
const need = Math.ceil(widestTeamEm(teams, withLogo) * fontPx) + RESULT_COLUMN_PAD_PX
return Math.min(row.teamBudgetPx, Math.max(row.teamFloorPx, need))
}
/**
* 运动员列实际分到的宽度(px):其余三列定宽,剩下的按份数分(见 LiveRowGeometry)。
*
* 两栏(duo)时单位列不参与分宽度,剩下的全归运动员列;单栏与拆两块时单位列也要一份,
* 但它至少是算出来的内容宽——单位名一长就先吃自己那份,再吃运动员列的余量。
*/
export function liveNameColumnPx(
results: readonly string[],
teams: readonly string[],
layout: LiveLayout,
fontPx: number,
withLogo = false,
): number {
const row = LIVE_ROW_GEOMETRY[layout]
const free =
row.rowWidthPx - row.rankPx - row.gapPx * 3 - liveResultColumnPx(results, layout, fontPx)
const team = liveTeamColumnPx(teams, layout, fontPx, withLogo)
if (!row.teamSharesWidth) return free - team
const share = (free * row.teamFr) / (row.nameFr + row.teamFr)
return free - Math.max(team, share)
}
/** 当前字号下这一行装得下吗:三列定宽 + 名字列至少 4 个汉字。 */
function liveRowFitsPx(
results: readonly string[],
teams: readonly string[],
layout: LiveLayout,
fontPx: number,
withLogo: boolean,
): boolean {
const name = liveNameColumnPx(results, teams, layout, fontPx, withLogo)
return name >= NAME_MIN_EM * fontPx
}
/**
* 实时赛况的行字号(px):行数定基准,成绩列宽与整行宽度再做上限。
*
* 真机缺陷一:两栏时成绩列写死 150px,而 `1:09.09` 在 56px 字号下墨迹 201px,末位数字被外层
* overflow:hidden 裁掉;1–6 行时字号都大于 41px(150px 列的容限),于是全中招。这里把
* 「装不下就收字号」当兜底:典型成绩(4.2em)在预算内仍能取满该版式的上限,靠加宽成绩列解决;
* 只有 `64:21.50` 这类特别长的成绩才真的收字号。
*
* 真机缺陷二:单位列原来写死(两栏 190px / 拆两块 150px),长单位名一律省略号。单位名与成绩
* 抢的是同一行的宽度,所以一条行内网格的账要一起算:名次 + 名字(≥4 个汉字)+ 单位 + 成绩
* 装得进一行,装不下就往下收字号,直到名字列守不住 4 个汉字为止(那时由单位名省略号收场,
* 成绩与名次仍然完整)。单位名长的代价是整屏字号略小,比把单位名截成三个点强。
*/
export function fitLiveRowFontPx(
rowCount: number,
layout: LiveLayout,
results: readonly string[],
teams: readonly string[] = [],
teamsWithLogo = false,
): number {
const spec = LIVE_LAYOUT_SPECS[layout]
const base = fitRowFontPx(rowCount, BOARD_AVAILABLE_PX, spec.ratio, spec.capPx)
const widest = widestResultEm(results)
if (widest <= 0) return base
const byWidth = Math.floor((spec.resultBudgetPx - RESULT_COLUMN_PAD_PX) / widest)
return Math.max(MIN_ROW_FONT_PX, Math.min(base, byWidth))
let font = base
if (widest > 0) {
font = Math.min(font, Math.floor((spec.resultBudgetPx - RESULT_COLUMN_PAD_PX) / widest))
}
while (font > MIN_ROW_FONT_PX && !liveRowFitsPx(results, teams, layout, font, teamsWithLogo)) {
font--
}
return Math.max(MIN_ROW_FONT_PX, font)
}
/**
+101 -11
View File
@@ -5,10 +5,13 @@ import {
MAX_ROW_FONT_PX,
fitLiveRowFontPx,
fitRowFont,
liveNameColumnPx,
liveResultColumnBudgetPx,
liveResultColumnFloorPx,
liveResultColumnPx,
resultTextEm,
liveRowGeometry,
liveTeamColumnPx,
textEm,
} from '../fitFont'
import { LIVE_SPLIT_MIN_ROWS } from '../live'
@@ -27,17 +30,19 @@ test('fitRowFont 行数多时守住 16px 下限', () => {
// 真机实测(Noto Sans SC、56px):`1:09.09` 墨迹 201px、`12.345 米` 252px、`64:21.50` 234px。
// 估算一旦小于实测,末位数字就会重新被 .rows 的 overflow:hidden 裁掉(屏上是 `1:09.0`)。
test('成绩估宽按实测最宽的字体上浮,宁可偏大', () => {
assert.ok(resultTextEm('1:09.09') * 56 >= 201)
assert.ok(resultTextEm('12.345 米') * 56 >= 252)
assert.ok(resultTextEm('64:21.50') * 56 >= 234)
assert.ok(textEm('1:09.09') * 56 >= 201)
assert.ok(textEm('12.345 米') * 56 >= 252)
assert.ok(textEm('64:21.50') * 56 >= 234)
// MiSans 的数字比 Noto 宽(`0` 实测 0.6786em),按最宽的那个估。
assert.ok(resultTextEm('0:00.00') >= 4)
assert.equal(resultTextEm(''), 0)
assert.ok(textEm('0:00.00') >= 4)
assert.equal(textEm(''), 0)
// 单位名同样走这套字符宽度表:汉字是方块字,实测正好 1.00em(10 个汉字 = 10em)。
assert.ok(textEm('机关直属通信保障分队') >= 10)
})
test('两栏 1–10 行:字号封顶 56px、墨迹一律装得进算出来的列宽', () => {
const results = ['1:06.33', '1:09.09', '1:10.55', '59.99']
const widest = resultTextEm('1:06.33')
const widest = textEm('1:06.33')
const expectedFont = [56, 56, 56, 56, 53, 44, 38, 33, 29, 26]
for (let rows = 1; rows <= 10; rows++) {
const font = fitLiveRowFontPx(rows, 'duo', results)
@@ -51,7 +56,7 @@ test('两栏 1–10 行:字号封顶 56px、墨迹一律装得进算出来的
test('两栏 1–7 行才需要加宽成绩列,8 行以上仍用原来的 150px', () => {
const results = ['1:06.33']
const widest = resultTextEm('1:06.33')
const widest = textEm('1:06.33')
for (let rows = 1; rows <= 7; rows++) {
const font = fitLiveRowFontPx(rows, 'duo', results)
// 写死 150px 时这些行数真的会裁:这正是本次要修的真机缺陷。
@@ -71,7 +76,7 @@ test('长成绩:先加宽列,装不下才收字号,且不越预算', () =>
assert.ok(font < MAX_ROW_FONT_PX, '特别长的成绩要让出字号')
assert.ok(font >= 40, '让出的幅度不该把大屏字变小')
const column = liveResultColumnPx(results, 'duo', font)
assert.ok(resultTextEm('64:21.50') * font <= column)
assert.ok(textEm('64:21.50') * font <= column)
assert.ok(column <= liveResultColumnBudgetPx('duo'))
// 典型成绩(7 字符)不该因为这条上限被收字号。
@@ -91,7 +96,7 @@ test('单栏满宽:字号用满高度(96px 上限),短成绩仍守着原
// 单栏也会裁长成绩(220px 装不下 96px 的 `64:21.50`),同样由内容定宽修掉。
const font = fitLiveRowFontPx(1, 'single', ['64:21.50'])
assert.equal(font, 89)
assert.ok(liveResultColumnPx(['64:21.50'], 'single', font) >= resultTextEm('64:21.50') * font)
assert.ok(liveResultColumnPx(['64:21.50'], 'single', font) >= textEm('64:21.50') * font)
})
test('拆两块:每块 ≤5 行时字号回到 56px 上限,且行数越多越只增不减', () => {
@@ -102,7 +107,7 @@ test('拆两块:每块 ≤5 行时字号回到 56px 上限,且行数越多
assert.equal(font, MAX_ROW_FONT_PX, `${rows} 行`)
assert.ok(font > fitLiveRowFontPx(rows, 'single', results), `${rows} 行拆开必须更大`)
const column = liveResultColumnPx(results, 'split', font)
assert.ok(resultTextEm('1:06.33') * font <= column)
assert.ok(textEm('1:06.33') * font <= column)
assert.ok(column <= liveResultColumnBudgetPx('split'))
}
})
@@ -135,3 +140,88 @@ test('行数越多字号只减不增', () => {
}
}
})
// 现场量级:单位名 8~12 个汉字(「第三保障支援分队」这类),成绩 `1:12.66` / `12:34.56`。
// 真机缺陷:单位列原来两栏写死 190px、拆两块 150px、单栏按 1fr 分(满宽约 540px),
// 于是 10 个汉字(在 34px 字号下墨迹 343px)一律被省略号截成三个点。
const LONG_TEAMS = ['第三保障支援分队', '机关直属通信保障分队', '第一分公司第二项目部']
const LONG_RESULTS = ['1:12.66', '12:34.56', '1:09.09']
const LAYOUTS = ['single', 'split', 'duo'] as const
test('长单位名:单位列宽按内容算,10 个汉字的名字不再被省略号截掉', () => {
for (const layout of LAYOUTS) {
const font = fitLiveRowFontPx(6, layout, LONG_RESULTS, LONG_TEAMS)
const column = liveTeamColumnPx(LONG_TEAMS, layout, font)
// 单位名在单位列自己的字号(.team { font-size: 0.78em })下的墨迹必须装得进列宽:
// 这一条就是「单元格不进入省略态」的判据(省略号只在 scrollWidth > clientWidth 时出现)。
const ink = textEm('机关直属通信保障分队') * 0.78 * font
assert.ok(ink <= column, `${layout}:单位名墨迹 ${ink}px > 列宽 ${column}px,仍会省略`)
// 单位名前的 logo(1em + 0.28em 间距)也要算进去。
const withLogo = liveTeamColumnPx(LONG_TEAMS, layout, font, true)
assert.ok(textEm('机关直属通信保障分队') * 0.78 * font + 1.28 * 0.78 * font <= withLogo)
}
})
test('长单位名 + 长成绩:整行仍装得下,运动员列至少留 4 个汉字', () => {
for (const layout of LAYOUTS) {
const geo = liveRowGeometry(layout)
const font = fitLiveRowFontPx(6, layout, LONG_RESULTS, LONG_TEAMS)
const name = liveNameColumnPx(LONG_RESULTS, LONG_TEAMS, layout, font)
const used =
geo.rankPx +
geo.gapPx * 3 +
name +
liveTeamColumnPx(LONG_TEAMS, layout, font) +
liveResultColumnPx(LONG_RESULTS, layout, font)
assert.ok(used <= geo.rowWidthPx, `${layout}:一行要 ${used}px,只有 ${geo.rowWidthPx}px`)
assert.ok(name >= 4 * font, `${layout}:${font}px 字号下运动员列只剩 ${name}px`)
}
})
test('长单位名的代价是收字号而不是省略号,且不至于把小屏字收垮', () => {
for (const layout of LAYOUTS) {
const withTeams = fitLiveRowFontPx(6, layout, LONG_RESULTS, LONG_TEAMS)
const withoutTeams = fitLiveRowFontPx(6, layout, LONG_RESULTS)
assert.ok(withTeams <= withoutTeams, `${layout}:长单位名只该收字号`)
// 现场量级(8~12 汉字)下最狠的一档是拆两块版式:56px → 39px 左右,仍然一眼可读。
assert.ok(withTeams >= 36, `${layout}:收字号收到 ${withTeams}px,观感塌了`)
}
// 成绩列本来就按内容定宽,长单位名不该让它变窄。
const font = fitLiveRowFontPx(6, 'duo', LONG_RESULTS, LONG_TEAMS)
assert.ok(textEm('12:34.56') * font <= liveResultColumnPx(LONG_RESULTS, 'duo', font))
})
test('短单位名不受这条改动影响:字号与不带单位名时一模一样', () => {
// 4~5 个汉字的单位名(「机关本部」「第一分公司」)是常态,不该为它收字号。
const short = ['第一分公司']
const results = ['1:06.33']
for (const layout of LAYOUTS) {
for (let rows = 1; rows <= 10; rows++) {
assert.equal(
fitLiveRowFontPx(rows, layout, results, short),
fitLiveRowFontPx(rows, layout, results),
`${layout} ${rows} 行`,
)
}
}
})
test('单位列宽的下限与预算:短名字观感不变,极端长度不再撑破一行', () => {
for (const layout of LAYOUTS) {
const geo = liveRowGeometry(layout)
assert.equal(liveTeamColumnPx([], layout, 56), geo.teamFloorPx)
assert.equal(liveTeamColumnPx(['一分队'], layout, 56), geo.teamFloorPx)
// 单位名前的 logo 要单独占一格,算宽时不能漏。
const named = ['第三保障支援分队']
assert.ok(
liveTeamColumnPx(named, layout, 56, true) > liveTeamColumnPx(named, layout, 56),
`${layout}:logo 没算进单位列`,
)
// 极端长度顶到预算为止:省略号收场,但成绩与名次不受影响(列宽不会无限长)。
const extreme = ['第三保障支援分队第二支队通信保障中队勤务保障']
assert.equal(liveTeamColumnPx(extreme, layout, 56), geo.teamBudgetPx)
assert.ok(liveTeamColumnPx(['第三保障支援分队'], layout, 96) <= geo.teamBudgetPx)
assert.ok(geo.rankPx + geo.gapPx * 3 + geo.teamBudgetPx + liveResultColumnBudgetPx(layout) <= geo.rowWidthPx)
}
})
+30 -3
View File
@@ -314,6 +314,32 @@ test('成绩列宽由内容算出来,网格里不能写死列宽', () => {
assert.match(source, /\.rs \{[^}]*white-space: nowrap/s)
})
test('单位列宽同样由内容算出来,长单位名不再被写死列宽截成三个点', () => {
const source = readFileSync(
new URL('../components/pages/LivePage.vue', import.meta.url),
'utf8',
)
// 真机缺陷:单位列写死 190px(两栏)/ 150px(拆两块),10 个汉字的单位名墨迹 343px,
// 现场看到的是「第三保障…」。列宽与字号一起交给 fitFont.ts 算(--team-width)。
assert.doesNotMatch(source, /minmax\(0, 1fr\) 190px var\(--rs-width/)
assert.doesNotMatch(source, /minmax\(0, 1fr\) 300px var\(--rs-width/)
assert.doesNotMatch(source, /minmax\(0, 1\.5fr\) minmax\(0, 1fr\) var\(--rs-width/)
assert.match(source, /var\(--team-width, 190px\)/)
assert.match(source, /var\(--team-width, 300px\)/)
assert.match(source, /var\(--team-width, 150px\)/)
// 单栏与拆两块:单位列写成 minmax(内容宽, 1fr),短名字时与运动员列一样宽,观感不变。
assert.match(source, /minmax\(var\(--team-width, 300px\), 1fr\)/)
assert.match(source, /minmax\(var\(--team-width, 150px\), 1fr\)/)
// 页面必须把单位名与 logo 一起喂给字号计算,否则又变成「算完字号再被单位名挤破」。
assert.match(source, /liveTeamColumnPx/)
assert.match(source, /row\.shortName \?\? row\.teamName/)
assert.match(source, /'--team-width': `\$\{liveTeamColumnPx/)
assert.match(
source,
/fitLiveRowFontPx\(\s*liveBlockRowCount\([\s\S]*?teamTexts\.value,\s*teamsWithLogo\.value,/,
)
})
test('单栏铺满页框:不再用 max-width 收窄居中', () => {
const source = readFileSync(
new URL('../components/pages/LivePage.vue', import.meta.url),
@@ -347,11 +373,12 @@ test('拆两块时的名次列必须装得下两位数', () => {
'utf8',
)
// 56px 字号下 `10` 的墨迹约 78px:名次列窄于 80px 会被挤进运动员列。
const matched = /\.boards-1 \.rows\.split \.row \{[^}]*grid-template-columns: (\d+)px/s.exec(
// 列定义现在换行写(一行放不下三列的长名字),正则要允许冒号后面换行。
const matched = /\.boards-1 \.rows\.split \.row \{[^}]*grid-template-columns:\s*(\d+)px/s.exec(
source,
)
assert.ok(matched !== null, '拆块版式要单独给一套网格')
assert.ok(Number(matched[1]) >= 80, `名次列 ${matched[1]}px 装不下两位数`)
// 两个项目的版式(duo)沿用改前的网格,别跟着一起动。
assert.match(source, /\.boards-2 \.row \{[^}]*60px minmax\(0, 1fr\) 190px/s)
// 两个项目的版式(duo)名次列沿用改前的 60px,纹丝不动。
assert.match(source, /\.boards-2 \.row \{[^}]*grid-template-columns:\s*60px minmax\(0, 1fr\)/s)
})