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
+9 -9
View File
@@ -76,7 +76,7 @@ pub fn config_path(state: Shared<'_>) -> String {
pub fn unlock(password: String, state: Shared<'_>) -> Result<bool, String> {
state
.unlock(&password)
.map_err(|err| format!("存下来的密码哈希读不出来:{err}"))
.map_err(|err| format!("无法读取已保存的密码哈希:{err}"))
}
/// 手动重新上锁(并顺手把发现列表关掉)。
@@ -305,7 +305,7 @@ pub fn screenshot(camera: u32, state: Shared<'_>) -> Result<String, String> {
let frame = state
.latest_frames()
.latest(camera)
.ok_or_else(|| format!("还没有收到 0x{camera:08X} 的画面,暂时截不了"))?;
.ok_or_else(|| format!("尚未收到 0x{camera:08X} 的画面,暂时无法截图"))?;
let config = state.config();
let dir = config.recordings_dir().join("shots");
@@ -329,7 +329,7 @@ pub fn start_recording(
state: Shared<'_>,
) -> Result<Vec<cam_core::snapshot::RecordingDto>, String> {
if cameras.is_empty() {
return Err("没有指定要录哪一路".into());
return Err("请先选择要录制的画面".into());
}
let config = state.config();
@@ -347,8 +347,8 @@ pub fn start_recording(
Some(measured) if measured > 1.0 => measured,
_ => {
log::warn!(
"0x{camera:08X} 还没有测到帧率,录制先按 {FALLBACK_INPUT_FPS} fps 走;\
回放速度可能不准,可以改用 [recording] fps 明确指定"
"尚未测得 0x{camera:08X} 的帧率,录制暂用 {FALLBACK_INPUT_FPS} fps;\
回放速度可能不准确,可在 [recording] 中设置 fps"
);
FALLBACK_INPUT_FPS
}
@@ -365,7 +365,7 @@ pub fn start_recording(
config.recording.crf,
&config.recording.preset,
)
.map_err(|err| format!("起不了录制:{err:#}"))?;
.map_err(|err| format!("无法开始录制:{err:#}"))?;
started.push(dto);
}
@@ -405,7 +405,7 @@ pub fn reveal(path: String, state: Shared<'_>) -> Result<(), String> {
}
if !target.exists() {
return Err(format!("{} 不存在了", target.display()));
return Err(format!("文件不存在:{}", target.display()));
}
tauri_plugin_opener::reveal_item_in_dir(&target)
@@ -427,7 +427,7 @@ pub fn open_file(path: String, state: Shared<'_>) -> Result<(), String> {
}
if !target.exists() {
return Err(format!("{} 不存在了", target.display()));
return Err(format!("文件不存在:{}", target.display()));
}
tauri_plugin_opener::open_path(&target, None::<&str>)
@@ -470,7 +470,7 @@ pub fn clear_config(password: Option<String>, state: Shared<'_>) -> Result<Strin
if path.exists() {
let backup = PathBuf::from(format!("{}.bak", path.display()));
std::fs::rename(&path, &backup)
.map_err(|err| format!("挪走 {} 失败:{err}", path.display()))?;
.map_err(|err| format!("移动文件 {} 失败:{err}", path.display()))?;
log::warn!("配置已重置,旧文件留在 {}", backup.display());
}
+1 -1
View File
@@ -252,7 +252,7 @@ fn run() -> Result<(), Box<dyn Error>> {
log::debug!("stream: {}", state.stream_info().url);
if args.demo {
state.note("演示模式已打开:画面与血量由本机合成,不是场上的板子。");
state.note("演示模式已开启:画面与血量由本机合成。");
demo::spawn(Arc::clone(&state));
}
+9 -6
View File
@@ -99,7 +99,7 @@ impl Recorder {
.stderr(Stdio::piped())
.spawn()
.with_context(|| {
format!("could not start ffmpeg ({ffmpeg}); 用 [recording] ffmpeg 指定它的路径")
format!("无法启动 ffmpeg({ffmpeg}),请在 [recording] 中设置 ffmpeg 路径")
})?;
let stdin = child
@@ -268,11 +268,11 @@ pub fn save_screenshot(jpeg: &[u8], dir: &Path, camera: u32) -> Result<PathBuf>
);
let image = image::load_from_memory_with_format(jpeg, image::ImageFormat::Jpeg)
.map_err(|err| anyhow!("这一帧不是能解码的 JPEG:{err}"))?;
.map_err(|err| anyhow!("无法解码当前 JPEG 帧:{err}"))?;
image
.save_with_format(&path, image::ImageFormat::Png)
.map_err(|err| anyhow!("写不了 {}:{err}", path.display()))?;
.map_err(|err| anyhow!("无法写入文件 {}:{err}", path.display()))?;
Ok(path)
}
@@ -290,9 +290,9 @@ fn pump(
// 读出来拼进错误里:只说"Broken pipe"对现场毫无帮助。
let reason = child_stderr(child);
return Err(anyhow!(
"writing to ffmpeg failed: {err}{}",
"向 ffmpeg 写入画面失败:{err}{}",
reason
.map(|r| format!(";ffmpeg 说:{r}"))
.map(|r| format!(";ffmpeg 输出:{r}"))
.unwrap_or_default()
));
}
@@ -776,7 +776,10 @@ mod tests {
names.contains(&"b.PNG"),
"后缀大小写不该决定它是不是一张截图"
);
assert_eq!(files[0].kind, "video");
let video = files.iter().find(|file| file.name == "a.mp4").unwrap();
let image = files.iter().find(|file| file.name == "b.PNG").unwrap();
assert_eq!(video.kind, "video");
assert_eq!(image.kind, "image");
let _ = std::fs::remove_dir_all(&dir);
}
+3 -3
View File
@@ -375,13 +375,13 @@ impl AppState {
match self.unlock(presented) {
Ok(true) => return Ok(()),
Ok(false) => {
return Err("密码不对。改摄像头/装甲板绑定、切换队伍都需要密码。".into())
return Err("密码错误。修改图传板、装甲板绑定或队伍号需要密码。".into())
}
Err(err) => return Err(format!("存下来的密码哈希读不出来:{err}")),
Err(err) => return Err(format!("无法读取已保存的密码哈希:{err}")),
}
}
Err("这个改动需要密码。先解锁(密码只是防止现场随手改绑,见 README 的说明)。".into())
Err("修改设置需要密码,请先解锁。".into())
}
/// 打开/关闭设置面板带来的发现列表。