feat: 新增自动更新版本引导流程及相关配置调整

- 新增完整的upvp更新引导页面套件,涵盖版本展示、版本检查、测试协议确认与完成页流程
- 新增appVersion配置字段用于记录当前应用版本,调整配置文件存储路径与旧配置迁移逻辑
- 更新路由配置与全局状态管理以支持更新引导流程
- 更新自动更新计划文档与示例配置文件
This commit is contained in:
2026-08-31 03:04:23 +08:00
parent fa44d41d44
commit b3da357d57
16 changed files with 391 additions and 28 deletions
+1 -2
View File
@@ -1,3 +1,2 @@
appVersion: 1.2.1
oobe: false oobe: false
theme:
darkMode: light
+4
View File
@@ -379,6 +379,10 @@ idle → checking → available → downloading → downloaded → quitAndInstal
- **发布说明切换**:默认显示当前版本;检测到可用更新后自动切到最新版本(`getReleaseNotes(v{version})`), - **发布说明切换**:默认显示当前版本;检测到可用更新后自动切到最新版本(`getReleaseNotes(v{version})`),
退出重进回到当前版本 退出重进回到当前版本
- **进度持久化**:每次状态/进度变化写入 `Koring.yml``update` - **进度持久化**:每次状态/进度变化写入 `Koring.yml``update`
- **版本引导(upvp**`Koring.yml` 增加 `appVersion` 字段(启动时不自动刷新)。
程序版本 ≠ `appVersion` → 进入 `upvp` 引导:更新已完成 → 检查版本
(程序版本更大 →「已更新至 X」;更小 → 版本倒退警告)→ 测试版更新需同意
Beta 协议(复用 OOBE 协议页)→ 结束页写入 `appVersion = 程序版本` 后进主页
state/version/percent/transferred/total/source/error);应用启动时清理上次的进行中状态 state/version/percent/transferred/total/source/error);应用启动时清理上次的进行中状态
- 配套改动:发布流水线 `gh release create` 上传 `release-notes.md` 附件 - 配套改动:发布流水线 `gh release create` 上传 `release-notes.md` 附件
(旧版本发布的 release 无此附件,页面会显示回退/空态) (旧版本发布的 release 无此附件,页面会显示回退/空态)
File diff suppressed because one or more lines are too long
+18 -4
View File
@@ -9,12 +9,13 @@ const CURRENT_VERSION = 1;
/** /**
* 配置文件路径: * 配置文件路径:
* - 打包后 → 系统用户数据目录(userData),避免安装到 Program Files 等只读目录时写入失败 * - 打包后 → 安装目录(可执行文件旁)。默认 per-user 安装(%LOCALAPPDATA%\Programs)可写;
* 若当初选择 per-machine 安装(Program Files)会无写权限,属已知限制
* - 开发模式 → 项目根目录(与旧行为一致,方便调试) * - 开发模式 → 项目根目录(与旧行为一致,方便调试)
*/ */
export function configPath(): string { export function configPath(): string {
if (app.isPackaged) { if (app.isPackaged) {
return path.join(app.getPath('userData'), CONFIG_FILE); return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
} }
return path.join(__dirname, '..', CONFIG_FILE); return path.join(__dirname, '..', CONFIG_FILE);
} }
@@ -130,7 +131,10 @@ export interface UpdateConfig {
} }
export interface AppConfig { export interface AppConfig {
/** 配置结构版本(迁移用) */
version: number; version: number;
/** 当前应用版本号(app.getVersion(),每次启动刷新;配置文件自述用) */
appVersion: string;
oobe: boolean; oobe: boolean;
app: AppInfoConfig; app: AppInfoConfig;
theme: ThemeConfig; theme: ThemeConfig;
@@ -148,6 +152,7 @@ export interface AppConfig {
const DEFAULTS: AppConfig = { const DEFAULTS: AppConfig = {
version: CURRENT_VERSION, version: CURRENT_VERSION,
appVersion: '',
oobe: true, oobe: true,
app: { language: 'zh-CN' }, app: { language: 'zh-CN' },
theme: { darkMode: 'auto', parallax: true }, theme: { darkMode: 'auto', parallax: true },
@@ -220,12 +225,19 @@ export function loadConfig(): AppConfig {
} }
} }
export function saveConfig(config: AppConfig): void { /**
* 保存配置(稀疏:仅写与默认值不同的部分)。
* - force=true:全量写入(首次启动使用,保证配置文件在安装目录可见)
* - 稀疏结果为空时:保留现有文件不删(重置由 config:reset / factoryReset 显式删除)
*/
export function saveConfig(config: AppConfig, force = false): void {
const filePath = configPath(); const filePath = configPath();
const sparse = diffValue(config, DEFAULTS) as Record<string, unknown> | undefined; const sparse = diffValue(config, DEFAULTS) as Record<string, unknown> | undefined;
if (!sparse || Object.keys(sparse).length === 0) { if (!sparse || Object.keys(sparse).length === 0) {
try { fs.unlinkSync(filePath); } catch {} if (force) {
fs.writeFileSync(filePath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
}
return; return;
} }
@@ -256,6 +268,8 @@ function mergeDeep<T>(base: T, patch: unknown): T {
export function getConfig(): AppConfig { export function getConfig(): AppConfig {
if (!current) { if (!current) {
current = loadConfig(); current = loadConfig();
// 注意:不在此处刷新 appVersion —— 升级后保持旧版本号,
// 由渲染端 upvp(版本更新引导)流程完成后写入新版本号
} }
return current; return current;
} }
+14 -19
View File
@@ -17,7 +17,6 @@ import { registerJavaHandlers } from './handlers/java';
import { registerUpdateHandlers } from './handlers/update'; import { registerUpdateHandlers } from './handlers/update';
import { updateService } from './updater'; import { updateService } from './updater';
import { saveConfig, configExists, getConfig, flushConfig, configPath } from './config'; import { saveConfig, configExists, getConfig, flushConfig, configPath } from './config';
import { authPath } from './auth';
const { app } = electron; const { app } = electron;
@@ -32,25 +31,20 @@ const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.B
splashWindow: null, splashWindow: null,
}; };
// 迁移旧版「可执行文件旁」存储 → userData(仅打包模式)。 // 迁移旧版 userData 存储 → 安装目录(仅打包模式;配置已改回存安装目录)。
// 复制而非移动,避免破坏用户已有文件;userData 已有目标文件则跳过。 // 复制而非移动,避免破坏用户已有文件;安装目录已有目标文件则跳过。
function migrateLegacyFiles(): void { function migrateLegacyFiles(): void {
if (!app.isPackaged) return; if (!app.isPackaged) return;
const exeDir = path.dirname(app.getPath('exe')); const exeDir = path.dirname(app.getPath('exe'));
const pairs: { name: string; dest: string }[] = [ const dest = path.join(exeDir, 'Koring.yml');
{ name: 'Koring.yml', dest: configPath() }, if (fs.existsSync(dest)) return;
{ name: 'koring-auth.json', dest: authPath() }, const src = path.join(app.getPath('userData'), 'Koring.yml');
]; if (!fs.existsSync(src)) return;
for (const { name, dest } of pairs) { try {
if (fs.existsSync(dest)) continue; fs.copyFileSync(src, dest);
const src = path.join(exeDir, name); console.log(`[migrate] copied Koring.yml ${src}${dest}`);
if (!fs.existsSync(src)) continue; } catch (e) {
try { console.error(`[migrate] failed to copy Koring.yml:`, e);
fs.copyFileSync(src, dest);
console.log(`[migrate] copied ${name}${dest}`);
} catch (e) {
console.error(`[migrate] failed to copy ${name}:`, e);
}
} }
} }
@@ -73,7 +67,8 @@ function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof
// 3. Load (or create) configgetConfig 会缓存到主进程内存,成为唯一权威) // 3. Load (or create) configgetConfig 会缓存到主进程内存,成为唯一权威)
const config = getConfig(); const config = getConfig();
if (isFirstLaunch) { if (isFirstLaunch) {
saveConfig(config); // 首次启动:全量写入,确保配置文件在安装目录可见(稀疏保存下全默认不落盘)
saveConfig(config, true);
} }
return { isFirstLaunch, config }; return { isFirstLaunch, config };
@@ -175,7 +170,7 @@ function registerAllHandlers() {
app.whenReady().then(() => { app.whenReady().then(() => {
registerAllHandlers(); registerAllHandlers();
// Migrate legacy exe-dir config/auth to userData before anything reads them // Migrate legacy userData config to install dir before anything reads them
migrateLegacyFiles(); migrateLegacyFiles();
// Run startup checks before creating windows // Run startup checks before creating windows
+27 -1
View File
@@ -35,6 +35,12 @@ import { OobeBetaTest } from "./pages/oobe/step-beta-test";
import { OobeFinish } from "./pages/oobe/step-finish"; import { OobeFinish } from "./pages/oobe/step-finish";
import { OobeLegal } from "./pages/oobe/step-legal"; import { OobeLegal } from "./pages/oobe/step-legal";
import { OobeAboutInfo } from "./pages/oobe/about-info"; import { OobeAboutInfo } from "./pages/oobe/about-info";
import { UpvpComplete } from "./pages/upvp/step-complete";
import { UpvpVersion } from "./pages/upvp/step-version";
import { UpvpCheck } from "./pages/upvp/step-check";
import { UpvpBetaTest } from "./pages/upvp/step-beta-test";
import { UpvpFinish } from "./pages/upvp/step-finish";
import { VERSION } from "./lib/version";
const pageMap = { const pageMap = {
home: Home, home: Home,
@@ -56,6 +62,12 @@ const pageMap = {
"oobe/finish": OobeFinish, "oobe/finish": OobeFinish,
"oobe/about-info": OobeAboutInfo, "oobe/about-info": OobeAboutInfo,
"oobe/legal": OobeLegal, "oobe/legal": OobeLegal,
upvp: UpvpComplete,
"upvp/complete": UpvpComplete,
"upvp/version": UpvpVersion,
"upvp/check": UpvpCheck,
"upvp/beta-test": UpvpBetaTest,
"upvp/finish": UpvpFinish,
debug: Debug, debug: Debug,
"debug-splash": SplashDebug, "debug-splash": SplashDebug,
"debug-display": DisplayDebug, "debug-display": DisplayDebug,
@@ -83,9 +95,23 @@ function App() {
syncBackgroundFromConfig(); syncBackgroundFromConfig();
useAuthStore.getState().initFromRegistry(); useAuthStore.getState().initFromRegistry();
useKoringAuthStore.getState().initFromDisk(); useKoringAuthStore.getState().initFromDisk();
// Navigate to OOBE on first launch or if oobe not completed
// 首次启动 / 未完成 OOBE → OOBE
if (isFirstLaunch || cfg.oobe) { if (isFirstLaunch || cfg.oobe) {
useRouteStore.getState().navigate("oobe"); useRouteStore.getState().navigate("oobe");
return;
}
// 配置里没有版本记录 → 补写当前版本(新装/旧配置迁移),直接进主页
if (!cfg.appVersion) {
useConfigStore.getState().setAppVersion(VERSION);
return;
}
// 程序版本 ≠ 配置版本 → 进入更新引导(upvp):
// 升级后 appVersion 仍是旧版本号(主进程不自动刷新),由 upvp 流程完成时写入新版本
if (cfg.appVersion !== VERSION) {
useRouteStore.getState().navigate("upvp/complete");
} }
}); });
+2
View File
@@ -107,6 +107,8 @@ export interface InstanceMeta {
export interface AppConfig { export interface AppConfig {
version: number; version: number;
/** 当前应用版本号(写入配置时由主进程刷新) */
appVersion: string;
oobe: boolean; oobe: boolean;
app: AppInfoConfig; app: AppInfoConfig;
theme: ThemeConfig; theme: ThemeConfig;
+14
View File
@@ -0,0 +1,14 @@
import { type ReactNode } from "react";
interface UpvpLayoutProps {
children: ReactNode;
}
/** 与 OOBE 相同的全屏居中布局框架 */
export function UpvpLayout({ children }: UpvpLayoutProps) {
return (
<div className="h-full flex flex-col items-center justify-center relative">
{children}
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
interface NextButtonProps {
onClick: () => void;
disabled?: boolean;
}
/** 与 OOBE 相同的下一步圆形按钮 */
export function NextButton({ onClick, disabled = false }: NextButtonProps) {
return (
<div className="absolute bottom-12">
<button
onClick={onClick}
disabled={disabled}
className="w-12 h-12 rounded-full bg-foreground/[0.06] hover:bg-foreground/[0.12] flex items-center justify-center text-foreground/60 hover:text-foreground transition-all duration-200 text-xl disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-foreground/[0.06] disabled:hover:text-foreground/60"
>
</button>
</div>
);
}
+78
View File
@@ -0,0 +1,78 @@
import { useState, useEffect } from "react";
import { useRouteStore } from "@/stores/routeStore";
import { UpvpLayout } from "./layout";
import { NextButton } from "./next-button";
import { Loader2 } from "lucide-react";
/** 第三步:Beta 测试协议(仅测试版更新需要同意;框架与 OOBE 一致) */
export function UpvpBetaTest() {
const navigate = useRouteStore((s) => s.navigate);
const [loading, setLoading] = useState(true);
const [text, setText] = useState("");
const [checked, setChecked] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
fetch(`${import.meta.env.BASE_URL}protocol-beta.txt`)
.then((r) => r.text())
.then(setText)
.catch(() => setText("无法加载协议内容"))
.finally(() => setLoading(false));
}, 1500);
return () => clearTimeout(timer);
}, []);
if (loading) {
return (
<UpvpLayout>
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-6 h-6 animate-spin text-foreground/40" />
<span className="text-sm text-muted-foreground">...</span>
</div>
</UpvpLayout>
);
}
return (
<UpvpLayout>
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
{/* 标题 */}
<div className="text-center space-y-1">
<h2 className="text-lg font-bold text-foreground">Koring APP Beta </h2>
<p className="text-xs text-muted-foreground"></p>
</div>
{/* 协议内容 */}
<div className="w-full h-[300px] rounded-xl bg-foreground/[0.03] border border-border/50 p-4 overflow-y-auto">
<pre className="text-xs text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">
{text}
</pre>
</div>
{/* 勾选框 */}
<label className="flex items-start gap-2.5 cursor-pointer select-none group">
<div className="relative mt-0.5">
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
className="sr-only peer"
/>
<div className="w-4 h-4 rounded border border-border/60 bg-foreground/[0.03] peer-checked:bg-primary peer-checked:border-primary transition-colors flex items-center justify-center">
{checked && (
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="2 6 5 9 10 3" />
</svg>
)}
</div>
</div>
<span className="text-xs text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
</span>
</label>
</div>
<NextButton onClick={() => navigate("upvp/finish")} disabled={!checked} />
</UpvpLayout>
);
}
+72
View File
@@ -0,0 +1,72 @@
import { useEffect, useState } from "react";
import { useRouteStore } from "@/stores/routeStore";
import { useConfigStore } from "@/stores/configStore";
import { VERSION } from "@/lib/version";
import { BUILD_MODE } from "@/lib/mode";
import { compareVersions } from "@/api/update";
import { UpvpLayout } from "./layout";
import { NextButton } from "./next-button";
import { Loader2, ArrowUpCircle, AlertTriangle } from "lucide-react";
/** 第三步:检查版本(程序版本 vs 配置文件 appVersion */
export function UpvpCheck() {
const navigate = useRouteStore((s) => s.navigate);
const appVersion = useConfigStore((s) => s.config.appVersion);
const [state, setState] = useState<"loading" | "updated" | "rollback">("loading");
useEffect(() => {
let cancelled = false;
compareVersions(VERSION, appVersion)
.then((r) => {
if (cancelled) return;
// a>b → 已更新;a<b → 版本倒退;相等/无效 → 视为已更新(正常流程)
setState(r.result === "a<b" ? "rollback" : "updated");
})
.catch(() => {
if (!cancelled) setState("updated");
});
return () => {
cancelled = true;
};
}, [appVersion]);
const isTestBuild = BUILD_MODE === "dev" || BUILD_MODE === "beta";
const nextRoute = isTestBuild ? "upvp/beta-test" : "upvp/finish";
return (
<UpvpLayout>
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
{state === "loading" ? (
<div className="flex flex-col items-center gap-3 py-4">
<Loader2 className="w-6 h-6 animate-spin text-foreground/40" />
<span className="text-sm text-muted-foreground">...</span>
</div>
) : state === "updated" ? (
<div className="flex flex-col items-center gap-3 text-center">
<ArrowUpCircle className="w-14 h-14 text-emerald-500" />
<h2 className="text-lg font-bold text-foreground">{VERSION}</h2>
<div className="text-xs text-muted-foreground space-y-1 font-mono">
<p>{VERSION}</p>
<p>{appVersion || "(无记录)"}</p>
</div>
</div>
) : (
<div className="flex flex-col items-center gap-3 text-center">
<AlertTriangle className="w-14 h-14 text-amber-500" />
<h2 className="text-lg font-bold text-foreground">退</h2>
<p className="text-xs text-muted-foreground leading-relaxed max-w-sm">
退
</p>
<div className="text-xs text-muted-foreground space-y-1 font-mono">
<p>{VERSION}</p>
<p>{appVersion}</p>
</div>
</div>
)}
</div>
{state !== "loading" && <NextButton onClick={() => navigate(nextRoute)} />}
</UpvpLayout>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { useRouteStore } from "@/stores/routeStore";
import { UpvpLayout } from "./layout";
import { NextButton } from "./next-button";
import { CheckCircle2 } from "lucide-react";
/** 第一步:更新已完成 */
export function UpvpComplete() {
const navigate = useRouteStore((s) => s.navigate);
return (
<UpvpLayout>
<div className="flex flex-col items-center gap-4">
<CheckCircle2 className="w-16 h-16 text-emerald-500" />
<h2 className="text-2xl font-bold text-foreground"></h2>
<p className="text-sm text-muted-foreground">Koring Launcher </p>
</div>
<NextButton onClick={() => navigate("upvp/version")} />
</UpvpLayout>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
import { useRouteStore } from "@/stores/routeStore";
import { useConfigStore } from "@/stores/configStore";
import { VERSION } from "@/lib/version";
import { compareVersions } from "@/api/update";
import { UpvpLayout } from "./layout";
import { AppleHelloEnglishEffect } from "@/components/ui/apple-hello-effect";
/**
* 第四步(结束):与 OOBE 结束页一致。
* 写入 appVersion 规则:
* - 程序版本 > appVersion(正常升级)→ 更新 appVersion 为当前版本号
* - 程序版本 < appVersion(版本倒退)→ 不修改,保持原样
*/
export function UpvpFinish() {
const navigate = useRouteStore((s) => s.navigate);
const appVersion = useConfigStore((s) => s.config.appVersion);
const setAppVersion = useConfigStore((s) => s.setAppVersion);
const [ready, setReady] = useState(false);
const [shouldWrite, setShouldWrite] = useState(false);
useEffect(() => {
let cancelled = false;
compareVersions(VERSION, appVersion)
.then((r) => {
if (cancelled) return;
// 仅版本倒退(a<b)保持原样;相等/无效也写入(无害,值为当前版本)
setShouldWrite(r.result !== "a<b");
})
.catch(() => {
if (!cancelled) setShouldWrite(true);
})
.finally(() => {
if (!cancelled) setReady(true);
});
return () => {
cancelled = true;
};
}, [appVersion]);
const handleFinish = () => {
if (shouldWrite) {
// 正常升级:记录当前版本到配置,下次启动程序版本与配置版本一致 → 正常进入主页
setAppVersion(VERSION);
}
// 版本倒退:不修改 appVersion,下次启动仍会进入 upvp 提醒
navigate("home");
};
return (
<UpvpLayout>
<div className="flex flex-col items-center gap-6">
<AppleHelloEnglishEffect className="text-foreground" />
</div>
<div className="absolute bottom-12">
<button
onClick={handleFinish}
disabled={!ready}
className="h-12 px-6 rounded-full bg-foreground/[0.06] hover:bg-foreground/[0.12] flex items-center justify-center text-foreground/60 hover:text-foreground transition-all duration-200 text-sm font-medium disabled:opacity-40 disabled:cursor-not-allowed"
>
</button>
</div>
</UpvpLayout>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { useRouteStore } from "@/stores/routeStore";
import { BUILD_MODE } from "@/lib/mode";
import { VersionCard } from "@/components/VersionCard";
import { UpvpLayout } from "./layout";
import { NextButton } from "./next-button";
/** 第二步:版本展示(与 OOBE 版本页一致,纯展示版本卡片;检查在下一步) */
export function UpvpVersion() {
const navigate = useRouteStore((s) => s.navigate);
const isTestBuild = BUILD_MODE === "dev" || BUILD_MODE === "beta";
return (
<UpvpLayout>
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
{/* 版本卡片(OOBE 模式:仅展示,无按钮) */}
<VersionCard oobe className="w-full" />
{/* 测试版警告(与 OOBE 版本页一致) */}
{isTestBuild && (
<p className="text-xs text-muted-foreground text-center max-w-sm leading-relaxed">
使
</p>
)}
</div>
<NextButton onClick={() => navigate("upvp/check")} />
</UpvpLayout>
);
}
+9
View File
@@ -44,10 +44,13 @@ interface ConfigState {
setUi: (patch: Partial<UiConfig>) => void; setUi: (patch: Partial<UiConfig>) => void;
setInstances: (instances: InstanceMeta[]) => void; setInstances: (instances: InstanceMeta[]) => void;
setOobe: (value: boolean) => void; setOobe: (value: boolean) => void;
/** 记录当前应用版本号到配置(升级引导完成后由 upvp 流程写入) */
setAppVersion: (version: string) => void;
} }
const DEFAULT_CONFIG: AppConfig = { const DEFAULT_CONFIG: AppConfig = {
version: 1, version: 1,
appVersion: "",
oobe: true, oobe: true,
app: { language: "zh-CN" }, app: { language: "zh-CN" },
theme: { darkMode: "auto", parallax: true }, theme: { darkMode: "auto", parallax: true },
@@ -167,4 +170,10 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
set({ config: { ...config, oobe: value } }); set({ config: { ...config, oobe: value } });
submit("oobe", value); submit("oobe", value);
}, },
setAppVersion: (version) => {
const { config } = get();
set({ config: { ...config, appVersion: version } });
submit("appVersion", version);
},
})); }));
+13 -1
View File
@@ -20,6 +20,12 @@ export type RouteKey =
| "oobe/finish" | "oobe/finish"
| "oobe/about-info" | "oobe/about-info"
| "oobe/legal" | "oobe/legal"
| "upvp"
| "upvp/complete"
| "upvp/version"
| "upvp/check"
| "upvp/beta-test"
| "upvp/finish"
| "debug" | "debug"
| "debug-splash" | "debug-splash"
| "debug-display" | "debug-display"
@@ -64,6 +70,12 @@ export const allRoutes: RouteItem[] = [
{ key: "oobe/finish", label: "完成", path: "/oobe/finish", hidden: true }, { key: "oobe/finish", label: "完成", path: "/oobe/finish", hidden: true },
{ key: "oobe/about-info", label: "关于信息", path: "/oobe/about-info", hidden: true, backable: true }, { key: "oobe/about-info", label: "关于信息", path: "/oobe/about-info", hidden: true, backable: true },
{ key: "oobe/legal", label: "法律信息", path: "/oobe/legal", hidden: true }, { key: "oobe/legal", label: "法律信息", path: "/oobe/legal", hidden: true },
{ key: "upvp", label: "更新引导", path: "/upvp", hidden: true },
{ key: "upvp/complete", label: "更新已完成", path: "/upvp/complete", hidden: true },
{ key: "upvp/version", label: "当前版本", path: "/upvp/version", hidden: true },
{ key: "upvp/check", label: "检查版本", path: "/upvp/check", hidden: true },
{ key: "upvp/beta-test", label: "测试协议", path: "/upvp/beta-test", hidden: true },
{ key: "upvp/finish", label: "完成", path: "/upvp/finish", hidden: true },
{ key: "debug", label: "调试", path: "/debug", hidden: true }, { key: "debug", label: "调试", path: "/debug", hidden: true },
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true }, { key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
{ key: "debug-display", label: "显示效果调试", path: "/debug/display", hidden: true }, { key: "debug-display", label: "显示效果调试", path: "/debug/display", hidden: true },
@@ -75,7 +87,7 @@ export const allRoutes: RouteItem[] = [
const topLevelKeys = new Set(routes.map((r) => r.key)); const topLevelKeys = new Set(routes.map((r) => r.key));
function getRouteTitleBarMode(key: RouteKey): TitleBarMode { function getRouteTitleBarMode(key: RouteKey): TitleBarMode {
if (key === "oobe" || key.startsWith("oobe/")) return "oobe"; if (key === "oobe" || key.startsWith("oobe/") || key === "upvp" || key.startsWith("upvp/")) return "oobe";
return topLevelKeys.has(key) ? "default" : "sub"; return topLevelKeys.has(key) ? "default" : "sub";
} }