界面部分。后端在前一个提交里,这一提交把它接上,并删掉 client/。 用户端: - 底部胶囊血条(宽度/数字/阈值/脉动可配),未接入时显示占位说明而不是 0; - 瞄准器画在 canvas 上并与画面共用同一次变换计算——位置跟着缩放走、大小不跟, 像真瞄具;偏移校准存的是画面比例而不是像素,换分辨率不会失效; - 命中反馈:整屏闪红 + 抖动 + 准星旁的伤害数字。大号数字刻意不放画面正中: 第一版放了,正好压住瞄准点。掉血事件由相邻两次读数之差派生(主板只报"现在多少血"); - 摄像头/装甲板下拉绑定、队伍号、镜像;改动要密码,退出/关闭也要。 裁判端: - 两路等宽(flex: 1 1 0 + min-width: 0)各自独立缩放; - 装甲板发现列表:id、地址、自报队伍、血量、四条齐不齐、静默多久、分到哪个槽位; - 截图 PNG + ffmpeg 录 mp4,界面内回放(资源协议白名单外的目录给"用系统播放器打开"的退路)。 密码:Argon2id PHC,首启强制设置,会话内解锁一次。执行点在 Rust 侧命令里,界面上的 解锁对话框只是入口。故意不做 --camera-id 这类命令行覆盖:那是一条不用密码就能改绑的路。 其它: - 配置换成 serde + toml,字段名统一 camelCase(磁盘=IPC=TS),仍然拒绝未知键名, 原子写;旧客户端配置首次启动导入并备份成 .bak(只在默认路径下做); - 演示模式(CAM_DEMO=1)合成协议数据报喂进真实接收路径,没有板子也能把整条链路跑起来; - 仅 dev 的浏览器预览(?preview=referee),两重门保证不进正式产物(已验证); - CSP 收紧成白名单,验证方式是"前端能不能连上后端":clients 从 0 变成 1 同时证明 JS 加载、IPC、WebSocket、React effect 四件事。 删掉 client/:它的功能全部被 desktop/ 覆盖,且其中有几处真 bug 在新实现里改掉了 (槽位分配会让两格显示同一块板子、帧率窗口基线、密码哈希截断被当成密码错、录制文件名 同秒互相覆盖)。注意 client/ 里那几处**未提交**的改动随之丢弃,历史里的版本还在。 测试:protocol 15、cam-core 69、src-tauri 30;前端 tsc + vite build 通过。
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
/**
|
|
* 后端命令的封装。
|
|
*
|
|
* 每个函数对应 `desktop/src-tauri/src/commands.rs` 里的一个 `#[tauri::command]`。
|
|
* Tauri 会把 JS 侧的 camelCase 参数名转成 Rust 侧的 snake_case,所以这里的参数名写成
|
|
* 前端习惯的样子即可。
|
|
*
|
|
* # 错误
|
|
*
|
|
* 后端的 `Err(String)` 会以 rejected promise 的形式回来,字符串就是给现场的人看的那句
|
|
* 话。`call()` 把 `unknown` 归一成 `Error`,调用方只管 `catch (err) { err.message }`。
|
|
*/
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
import type {
|
|
ConfigView,
|
|
HudView,
|
|
MediaFileDto,
|
|
RecordingDto,
|
|
Role,
|
|
Snapshot,
|
|
StreamInfo,
|
|
} from "./types";
|
|
|
|
/**
|
|
* 把后端的字符串错误变成真正的 `Error`。
|
|
*
|
|
* 在没有 Tauri 的 `vite dev` 会话里(`?preview=` 那种),这条走预览实现——它用
|
|
* `await import()` 引入,所以正式构建里那段代码根本不会被打进产物(见 `dev-preview.ts`)。
|
|
*/
|
|
async function call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
|
if (import.meta.env.DEV && !("__TAURI_INTERNALS__" in window)) {
|
|
const preview = await import("./dev-preview");
|
|
return preview.previewInvoke<T>(command, args);
|
|
}
|
|
|
|
try {
|
|
return await invoke<T>(command, args);
|
|
} catch (raw) {
|
|
throw new Error(typeof raw === "string" ? raw : String(raw));
|
|
}
|
|
}
|
|
|
|
export const api = {
|
|
// ---- 只读 ----
|
|
streamInfo: () => call<StreamInfo>("stream_info"),
|
|
snapshot: () => call<Snapshot>("get_snapshot"),
|
|
config: () => call<ConfigView>("get_config"),
|
|
mediaFiles: () => call<MediaFileDto[]>("list_media_files"),
|
|
configPath: () => call<string>("config_path"),
|
|
|
|
// ---- 密码 ----
|
|
unlock: (password: string) => call<boolean>("unlock", { password }),
|
|
lock: () => call<void>("lock"),
|
|
setPassword: (fresh: string, current?: string) =>
|
|
call<ConfigView>("set_password", { current: current ?? null, fresh }),
|
|
setDiscoveryOpen: (open: boolean) => call<void>("set_discovery_open", { open }),
|
|
|
|
// ---- 绑定与身份 ----
|
|
bindCamera: (cameraId: number | null, password?: string) =>
|
|
call<ConfigView>("bind_camera", { cameraId, password: password ?? null }),
|
|
bindArmor: (armorId: number | null, password?: string) =>
|
|
call<ConfigView>("bind_armor", { armorId, password: password ?? null }),
|
|
setTeam: (team: number, password?: string) =>
|
|
call<ConfigView>("set_team", { team, password: password ?? null }),
|
|
setRole: (role: Role, password?: string) =>
|
|
call<ConfigView>("set_role", { role, password: password ?? null }),
|
|
setMirror: (mirror: boolean) => call<ConfigView>("set_mirror", { mirror }),
|
|
saveHud: (hud: HudView) => call<ConfigView>("save_hud", { hud }),
|
|
setRefereeSlots: (slots: {
|
|
cameraA: number | null;
|
|
cameraB: number | null;
|
|
armorA: number | null;
|
|
armorB: number | null;
|
|
manual: boolean;
|
|
}) => call<ConfigView>("set_referee_slots", slots),
|
|
setRecordingOptions: (options: {
|
|
dir: string | null;
|
|
ffmpeg: string | null;
|
|
crf: number | null;
|
|
preset: string | null;
|
|
}) => call<ConfigView>("set_recording_options", options),
|
|
|
|
// ---- 截图与录制 ----
|
|
screenshot: (camera: number) => call<string>("screenshot", { camera }),
|
|
startRecording: (cameras: number[]) => call<RecordingDto[]>("start_recording", { cameras }),
|
|
stopRecording: (cameras: number[]) => call<RecordingDto[]>("stop_recording", { cameras }),
|
|
reveal: (path: string) => call<void>("reveal", { path }),
|
|
openFile: (path: string) => call<void>("open_file", { path }),
|
|
|
|
// ---- 退出与重置 ----
|
|
clearConfig: (password?: string) =>
|
|
call<string>("clear_config", { password: password ?? null }),
|
|
quit: (password?: string) => call<void>("quit_app", { password: password ?? null }),
|
|
|
|
refresh: () => call<void>("refresh"),
|
|
};
|