将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。 - fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示 - test(client): 按文件名核对媒体类型,避免依赖时间戳排序 验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
110 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|