docs: 整理项目文档并统一界面表述
构建客户端 / 检查(类型 + 单元测试) (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,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。

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

验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
This commit is contained in:
2026-10-19 11:26:26 +08:00
parent ee22a82ecd
commit 6a0eb14b28
54 changed files with 2624 additions and 4752 deletions
+17 -14
View File
@@ -47,10 +47,14 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
const active = snapshot.recordings.filter((recording) => recording.active);
const cameras = snapshot.cameras.filter((camera) => camera.online).map((camera) => camera.id);
const guard = (action: Promise<unknown>) => {
const guard = (action: Promise<unknown>, onSuccess?: () => void) => {
setBusy(true);
void action
.then(() => api.refresh())
.then(() => {
onSuccess?.();
refresh();
return api.refresh();
})
.catch((err) => onError(err instanceof Error ? err.message : String(err)))
.finally(() => setBusy(false));
};
@@ -66,10 +70,11 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
type="button"
className="btn btn--primary"
disabled={busy || cameras.length === 0}
title={cameras.length === 0 ? "还没有在线的摄像头" : undefined}
title={cameras.length === 0 ? "暂无在线图传板" : undefined}
onClick={() => {
guard(api.startRecording(cameras));
onInfo("开始录制。停止之前请留在这台机器上——退出会先收尾再走。");
guard(api.startRecording(cameras), () =>
onInfo("已开始录制。退出程序时会先结束录制并保存文件。"),
);
}}
>
<span className="media__rec-glyph" aria-hidden="true" />
@@ -82,7 +87,6 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
disabled={busy || active.length === 0}
onClick={() => {
guard(api.stopRecording([]));
refresh();
}}
>
停止全部录制
@@ -97,11 +101,11 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
className="btn btn--ghost"
disabled={busy}
onClick={() => {
guard(api.screenshot(camera));
onInfo(
`已截图 0x${camera.toString(16).toUpperCase().padStart(8, "0")}(存在录制目录的 shots/ 下)。`,
guard(api.screenshot(camera), () =>
onInfo(
`已截图 0x${camera.toString(16).toUpperCase().padStart(8, "0")}(已保存到录制目录的 shots/)。`,
),
);
refresh();
}}
>
截图
@@ -141,7 +145,7 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
{recording.framesDropped > 0 && (
<span
className="strip__warn"
title="写盘跟不上时丢的帧。丢帧不报出来,回放里少半秒会被当成相机的问题。"
title="写入速度不足时丢弃的帧数,可能影响回放速度。"
>
丢 {recording.framesDropped} 帧
</span>
@@ -152,7 +156,6 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
className="btn btn--ghost"
onClick={() => {
guard(api.stopRecording([recording.camera]));
refresh();
}}
>
停止
@@ -232,8 +235,8 @@ export function MediaPanel({ snapshot, onError, onInfo }: Props) {
{playbackFailed ? (
<div className="playback__fallback">
<p className="playback__warn">
界面内回放失败——多半是这个目录不在资源协议的白名单里(改成默认的录制目录,
或者直接看下面这一条)。
无法在界面内回放,可能是录制目录不在允许访问的范围内。可改用默认录制目录,
或用系统播放器打开。
</p>
<button
type="button"