Files
simba-rmic-2026/desktop/src/components/UnlockDialog.tsx
T
sloray 6a0eb14b28
构建客户端 / 检查(类型 + 单元测试) (push) Waiting to run
构建客户端 / Linux x64(deb + AppImage) (push) Blocked by required conditions
构建客户端 / Windows x64(NSIS 安装包) (push) Blocked by required conditions
构建客户端 / macOS 通用(.app + .dmg) (push) Blocked by required conditions
docs: 整理项目文档并统一界面表述
将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。

- fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示
- test(client): 按文件名核对媒体类型,避免依赖时间戳排序

验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
2026-10-19 11:26:26 +08:00

110 lines
3.3 KiB
TypeScript

/**
* 解锁对话框。
*
* 它只是**入口**,不是闸门:真正拦住改绑的是后端的 `require_unlocked`。所以这里即使
* 被人绕过、甚至被改成永远返回成功,配置也改不动。
*
* # 它是"校验"而不是"弹窗"
*
* 这一层出现时,背后的一切都停了:整屏压暗、面板上扫过一条校验线、左下角挂着 `LOCKED`。
* 现场有人要改绑定,多半是在出问题的时候;那时候界面必须明确地说"现在这一步是在验证身份",
* 而不是给出一个和别处一样的灰色小框。
*/
import { useEffect, useRef, useState } from "react";
import { api } from "../lib/ipc";
interface Props {
/** 为什么需要密码:显示在标题下。 */
reason: string;
onUnlocked: () => void;
onCancel: () => void;
}
export function UnlockDialog({ reason, onUnlocked, onCancel }: Props) {
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const input = useRef<HTMLInputElement | null>(null);
useEffect(() => {
input.current?.focus();
}, []);
const submit = async () => {
if (busy) return;
setBusy(true);
setError(null);
try {
const ok = await api.unlock(password);
if (ok) {
onUnlocked();
} else {
setError("密码错误");
setPassword("");
input.current?.focus();
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
}
};
return (
<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>
<header className="lock__head">
<h2 className="lock__title">需要密码</h2>
<p className="lock__reason">{reason}</p>
</header>
<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>
{error && <p className="lock__error">{error}</p>}
<p className="lock__note">
密码用于防止现场误改设置;能修改本机配置或程序的人仍可绕过。
忘记密码时,可删除配置文件中的 password 一行。
</p>
<div className="lock__actions">
<button type="button" className="btn btn--ghost" onClick={onCancel}>
取消
</button>
<button
type="button"
className="btn btn--primary"
onClick={() => void submit()}
disabled={busy || password.length === 0}
>
{busy ? "校验中…" : "解锁"}
<span className="btn__key">⏎</span>
</button>
</div>
</div>
</div>
);
}