/** * 裁判端就地改队伍名。 * * # 为什么是输入框而不是设置面板里的两项 * * 现场改名字的时机是"比赛开始前,两队报上名来"。那时候裁判盯着的是两格画面,让他在 * 设置面板的某一节里找到对应项、改完再关掉,是一次没有必要的往返。名字就放在两格画面 * 各自下面,改完直接看到血条上的名字变了。 * * # 回车/失焦才提交 * * 逐字提交会在每次按键后写一次配置文件(还带一次 Argon2 无关的原子替换),而且中间态 * ("红"、"红队")会被后端截断/回落规则改写一次,光标跟着跳。失焦提交是唯一不会跟 * 打字打架的时机。 * * 超过上限的字数由后端统一截断(`TEAM_NAME_MAX_CHARS`),这里也限一下长度——输入框 * 里就能看到"打不进去了",比提交后被悄悄截掉诚实。 */ import { useEffect, useRef, useState } from "react"; import { DEFAULT_TEAM_NAMES } from "../lib/format"; /** 与 Rust 侧的 `config::TEAM_NAME_MAX_CHARS` 一致。 */ const MAX_CHARS = 12; interface Props { team: number; /** 当前生效的名字(来自快照,可能已经被后端规范化过)。 */ name: string; /** 提交。后端会规范化,成功之后快照会带新名字回来。 */ onSave: (name: string) => void; disabled?: boolean; } export function TeamNameEditor({ team, name, onSave, disabled }: Props) { const [draft, setDraft] = useState(name); const focused = useRef(false); /** Esc 已经改回去了,紧接着的那次失焦不该再提交。 */ const reverted = useRef(false); // 焦点不在输入框里时跟着快照走:改完之后后端返回的规范名要能显示出来,而正在打字时 // 不能被一份恰好到达的快照顶掉。 useEffect(() => { if (!focused.current) setDraft(name); }, [name]); const commit = () => { focused.current = false; if (reverted.current) { reverted.current = false; return; } if (draft === name) return; const trimmed = draft.trim(); setDraft(trimmed); onSave(trimmed); }; return ( ); }