feat(client): RMIC::2026 界面重做、可计算的版本机制与固件版本联动
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s

- feat(client): 界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形
- feat(client): 自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退
- feat(client): 新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页
- feat(client): 裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路
- feat(client): 设置抽屉常驻实时预览,与画面共用同一份准星绘制代码
- feat(client): 动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔
- feat(version): 版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写
- feat(firmware): Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容
- feat(client): 固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因
- feat(firmware): 固件包版本与客户端同源,都从 version.json 读
- fix(client): getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载
- fix(client): 启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页
- fix(client): 设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系
- fix(protocol): Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失
- fix(build): Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径
- fix(build): esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录
- build(client): 队标图标,含光学尺寸分级与手工拼的多尺寸 ICO
- build(ci): 守门 job 增加版本一致性检查,产物名改为 rmic-2026-*
- docs: 补充界面语言、版本机制与协议通告布局的说明
This commit is contained in:
2026-10-19 02:40:08 +08:00
parent cc6b82d7ec
commit 896b21fe44
332 changed files with 16729 additions and 2765 deletions
+213
View File
@@ -0,0 +1,213 @@
/**
* 固件维护页的 IPC 与 DTO。
*
* 字段名与 `desktop/src-tauri/src/maintenance.rs` 的 `#[serde(rename_all = "camelCase")]`
* 一一对应;改一边必须改另一边。
*/
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
export interface PortInfo {
portName: string;
label: string;
vid: number | null;
pid: number | null;
likelyEsp: boolean;
}
export interface PartitionEntry {
name: string;
kind: number;
subtype: number;
offset: number;
size: number;
flags: number;
}
export interface FreeRange {
start: number;
size: number;
}
export interface AppInfo {
offset: number;
version: string;
projectName: string;
buildTime: string;
idfVersion: string;
}
export interface DeviceSnapshot {
port: string;
chip: string;
revision: string | null;
flashSize: number;
flashSizeText: string;
mac: string | null;
features: string[];
secureBoot: boolean;
secureDownload: boolean;
flashEncryption: boolean;
securityFlags: number;
flashCryptCnt: number;
partitionTable: PartitionEntry[];
app: AppInfo | null;
freeRanges: FreeRange[];
tableSha256: string;
}
export interface BundleInfo {
path: string;
sourceKind: string;
sourcePath: string;
sourceSha256: string | null;
entryCount: number | null;
name: string;
version: string;
chip: string;
appFile: string;
appSha256: string;
appSize: number;
partitionTableFile: string;
partitionTableSha256: string;
bootloaderFile: string | null;
bootloaderSha256: string | null;
appPartitionSize: number;
flashSize: string;
flashSizeBytes: number;
flashMode: string;
flashFrequency: string;
notes: string | null;
}
export interface SlotCandidate {
offset: number;
size: number;
}
export interface FlashPlan {
port: string;
device: DeviceSnapshot;
bundle: BundleInfo;
candidates: SlotCandidate[];
targetOffset: number;
targetSize: number;
appSize: number;
appPartitionSize: number;
currentFactoryOffset: number;
currentFactorySize: number;
currentTable: PartitionEntry[];
patchedTable: PartitionEntry[];
tableShaBefore: string;
tableShaAfter: string;
oldAppPreserved: boolean;
warnings: string[];
canFlash: boolean;
}
export interface Mismatch {
offset: number;
expected: number;
actual: number;
}
export interface SlotAttempt {
offset: number;
size: number;
writeOk: boolean;
verifyOk: boolean;
mismatches: Mismatch[];
badSectors: number[];
error: string | null;
}
export interface FlashProgress {
phase: string;
message: string;
current: number;
total: number;
}
export interface FlashReport {
ok: boolean;
port: string;
chip: string;
mac: string | null;
bundleName: string;
bundleVersion: string;
targetOffset: number;
targetSize: number;
appSize: number;
appSha256: string;
tableShaBefore: string;
tableShaAfter: string;
attempts: SlotAttempt[];
appVerified: boolean;
tableVerified: boolean;
tableRestored: boolean;
needsRollback: boolean;
bootOk: boolean | null;
bootLog: string;
backupDir: string;
note: string | null;
error: string | null;
startedAt: string;
finishedAt: string;
}
async function call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
try {
return await invoke<T>(command, args);
} catch (raw) {
throw new Error(typeof raw === "string" ? raw : String(raw));
}
}
export const maintenanceApi = {
listPorts: () => call<PortInfo[]>("maintenance_list_ports"),
loadBundle: (path: string) => call<BundleInfo>("maintenance_load_bundle", { path }),
probe: (port: string) => call<DeviceSnapshot>("maintenance_probe", { port }),
plan: (port: string, bundlePath: string) =>
call<FlashPlan>("maintenance_plan", { port, bundlePath }),
flash: (port: string, bundlePath: string, backupApp = true) =>
call<FlashReport>("maintenance_flash", { port, bundlePath, backupApp }),
restoreTable: (port: string, backupDir: string) =>
call<string>("maintenance_restore_table", { port, backupDir }),
cancel: () => call<void>("maintenance_cancel"),
};
/** 原生 .zip 选择。返回 null 表示用户取消。 */
export async function selectBundleZip(): Promise<string | null> {
const selected = await open({
directory: false,
multiple: false,
title: "选择官方固件包(.zip)",
filters: [{ name: "固件包", extensions: ["zip"] }],
});
return typeof selected === "string" ? selected : null;
}
/** 兼容旧包:选择目录。返回 null 表示用户取消。 */
export async function selectBundleDirectory(): Promise<string | null> {
const selected = await open({
directory: true,
multiple: false,
title: "选择官方固件包目录(含 bundle.json)",
});
return typeof selected === "string" ? selected : null;
}
export function formatBytes(value: number): string {
if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MiB`;
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
return `${value} B`;
}
export function formatOffset(value: number): string {
return `0x${value.toString(16).toUpperCase().padStart(6, "0")}`;
}
export function shortSha(value: string): string {
return value.length > 16 ? `${value.slice(0, 12)}…${value.slice(-4)}` : value;
}