feat(client): RMIC::2026 界面重做、可计算的版本机制与固件版本联动
- 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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user