feat(client): RMIC::2026 界面重做、可计算的版本机制与固件版本联动
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s

- feat(client): 界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形
- feat(client): 自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退
- feat(client): 新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页
- feat(client): 裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路
- feat(client): 设置抽屉常驻实时预览,与画面共用同一份准星绘制代码
- feat(client): 动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔
- feat(version): 版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写
- feat(firmware): Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容
- feat(client): 固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因
- feat(firmware): 固件包版本与客户端同源,都从 version.json 读
- fix(client): getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载
- fix(client): 启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页
- fix(client): 设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系
- fix(protocol): Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失
- fix(build): Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径
- fix(build): esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录
- build(client): 队标图标,含光学尺寸分级与手工拼的多尺寸 ICO
- build(ci): 守门 job 增加版本一致性检查,产物名改为 rmic-2026-*
- docs: 补充界面语言、版本机制与协议通告布局的说明
This commit is contained in:
2026-10-19 02:40:08 +08:00
parent cc6b82d7ec
commit 896b21fe44
332 changed files with 16729 additions and 2765 deletions
+43 -23
View File
@@ -3,6 +3,12 @@
*
* 它只是**入口**,不是闸门:真正拦住改绑的是后端的 `require_unlocked`。所以这里即使
* 被人绕过、甚至被改成永远返回成功,配置也改不动。
*
* # 它是"校验"而不是"弹窗"
*
* 这一层出现时,背后的一切都停了:整屏压暗、面板上扫过一条校验线、左下角挂着 `LOCKED`。
* 现场有人要改绑定,多半是在出问题的时候;那时候界面必须明确地说"现在这一步是在验证身份",
* 而不是给出一个和别处一样的灰色小框。
*/
import { useEffect, useRef, useState } from "react";
@@ -49,38 +55,52 @@ export function UnlockDialog({ reason, onUnlocked, onCancel }: Props) {
};
return (
<div className="overlay" role="dialog" aria-modal="true">
<div className="dialog">
<h2 className="dialog__title">需要密码</h2>
<p className="dialog__hint">{reason}</p>
<div className="overlay" role="dialog" aria-modal="true" aria-label="需要密码">
<div className={`lock plate${error ? " is-wrong" : ""}`}>
<span className="lock__scan" aria-hidden="true" />
<span className="lock__tag label">LOCKED</span>
<input
ref={input}
className="input"
type="password"
value={password}
placeholder="密码"
autoComplete="off"
onChange={(event) => setPassword(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") void submit();
if (event.key === "Escape") onCancel();
}}
/>
<header className="lock__head">
<h2 className="lock__title">需要密码</h2>
<p className="lock__reason">{reason}</p>
</header>
{error && <p className="dialog__error">{error}</p>}
<label className="lock__field">
<span className="lock__field-label label">ACCESS KEY</span>
<input
ref={input}
className="input input--lg"
type="password"
value={password}
placeholder="输入现场密码"
autoComplete="off"
onChange={(event) => setPassword(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") void submit();
if (event.key === "Escape") onCancel();
}}
/>
</label>
<p className="dialog__note">
{error && <p className="lock__error">{error}</p>}
<p className="lock__note">
密码只挡现场随手改绑,不是安全边界:配置文件与程序都在本机,愿意改的人绕得过去。
忘了密码就删掉配置里的 password 一行。
</p>
<div className="dialog__actions">
<button className="button button--ghost" onClick={onCancel}>
<div className="lock__actions">
<button type="button" className="btn btn--ghost" onClick={onCancel}>
取消
</button>
<button className="button button--primary" onClick={() => void submit()} disabled={busy}>
{busy ? "校验中…" : "解锁"}
<button
type="button"
className="btn btn--primary"
onClick={() => void submit()}
disabled={busy || password.length === 0}
>
{busy ? "校验中…" : "校验并解锁"}
<span className="btn__key">⏎</span>
</button>
</div>
</div>