mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: 新增自动更新版本引导流程及相关配置调整
- 新增完整的upvp更新引导页面套件,涵盖版本展示、版本检查、测试协议确认与完成页流程 - 新增appVersion配置字段用于记录当前应用版本,调整配置文件存储路径与旧配置迁移逻辑 - 更新路由配置与全局状态管理以支持更新引导流程 - 更新自动更新计划文档与示例配置文件
This commit is contained in:
+1
-2
@@ -1,3 +1,2 @@
|
||||
appVersion: 1.2.1
|
||||
oobe: false
|
||||
theme:
|
||||
darkMode: light
|
||||
|
||||
@@ -379,6 +379,10 @@ idle → checking → available → downloading → downloaded → quitAndInstal
|
||||
- **发布说明切换**:默认显示当前版本;检测到可用更新后自动切到最新版本(`getReleaseNotes(v{version})`),
|
||||
退出重进回到当前版本
|
||||
- **进度持久化**:每次状态/进度变化写入 `Koring.yml` 的 `update` 段
|
||||
- **版本引导(upvp)**:`Koring.yml` 增加 `appVersion` 字段(启动时不自动刷新)。
|
||||
程序版本 ≠ `appVersion` → 进入 `upvp` 引导:更新已完成 → 检查版本
|
||||
(程序版本更大 →「已更新至 X」;更小 → 版本倒退警告)→ 测试版更新需同意
|
||||
Beta 协议(复用 OOBE 协议页)→ 结束页写入 `appVersion = 程序版本` 后进主页
|
||||
(state/version/percent/transferred/total/source/error);应用启动时清理上次的进行中状态
|
||||
- 配套改动:发布流水线 `gh release create` 上传 `release-notes.md` 附件
|
||||
(旧版本发布的 release 无此附件,页面会显示回退/空态)
|
||||
|
||||
File diff suppressed because one or more lines are too long
+18
-4
@@ -9,12 +9,13 @@ const CURRENT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* 配置文件路径:
|
||||
* - 打包后 → 系统用户数据目录(userData),避免安装到 Program Files 等只读目录时写入失败
|
||||
* - 打包后 → 安装目录(可执行文件旁)。默认 per-user 安装(%LOCALAPPDATA%\Programs)可写;
|
||||
* 若当初选择 per-machine 安装(Program Files)会无写权限,属已知限制
|
||||
* - 开发模式 → 项目根目录(与旧行为一致,方便调试)
|
||||
*/
|
||||
export function configPath(): string {
|
||||
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);
|
||||
}
|
||||
@@ -130,7 +131,10 @@ export interface UpdateConfig {
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
/** 配置结构版本(迁移用) */
|
||||
version: number;
|
||||
/** 当前应用版本号(app.getVersion(),每次启动刷新;配置文件自述用) */
|
||||
appVersion: string;
|
||||
oobe: boolean;
|
||||
app: AppInfoConfig;
|
||||
theme: ThemeConfig;
|
||||
@@ -148,6 +152,7 @@ export interface AppConfig {
|
||||
|
||||
const DEFAULTS: AppConfig = {
|
||||
version: CURRENT_VERSION,
|
||||
appVersion: '',
|
||||
oobe: true,
|
||||
app: { language: 'zh-CN' },
|
||||
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 sparse = diffValue(config, DEFAULTS) as Record<string, unknown> | undefined;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -256,6 +268,8 @@ function mergeDeep<T>(base: T, patch: unknown): T {
|
||||
export function getConfig(): AppConfig {
|
||||
if (!current) {
|
||||
current = loadConfig();
|
||||
// 注意:不在此处刷新 appVersion —— 升级后保持旧版本号,
|
||||
// 由渲染端 upvp(版本更新引导)流程完成后写入新版本号
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
+14
-19
@@ -17,7 +17,6 @@ import { registerJavaHandlers } from './handlers/java';
|
||||
import { registerUpdateHandlers } from './handlers/update';
|
||||
import { updateService } from './updater';
|
||||
import { saveConfig, configExists, getConfig, flushConfig, configPath } from './config';
|
||||
import { authPath } from './auth';
|
||||
|
||||
const { app } = electron;
|
||||
|
||||
@@ -32,25 +31,20 @@ const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.B
|
||||
splashWindow: null,
|
||||
};
|
||||
|
||||
// 迁移旧版「可执行文件旁」存储 → userData(仅打包模式)。
|
||||
// 复制而非移动,避免破坏用户已有文件;userData 已有目标文件则跳过。
|
||||
// 迁移旧版 userData 存储 → 安装目录(仅打包模式;配置已改回存安装目录)。
|
||||
// 复制而非移动,避免破坏用户已有文件;安装目录已有目标文件则跳过。
|
||||
function migrateLegacyFiles(): void {
|
||||
if (!app.isPackaged) return;
|
||||
const exeDir = path.dirname(app.getPath('exe'));
|
||||
const pairs: { name: string; dest: string }[] = [
|
||||
{ name: 'Koring.yml', dest: configPath() },
|
||||
{ name: 'koring-auth.json', dest: authPath() },
|
||||
];
|
||||
for (const { name, dest } of pairs) {
|
||||
if (fs.existsSync(dest)) continue;
|
||||
const src = path.join(exeDir, name);
|
||||
if (!fs.existsSync(src)) continue;
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(`[migrate] copied ${name} → ${dest}`);
|
||||
} catch (e) {
|
||||
console.error(`[migrate] failed to copy ${name}:`, e);
|
||||
}
|
||||
const dest = path.join(exeDir, 'Koring.yml');
|
||||
if (fs.existsSync(dest)) return;
|
||||
const src = path.join(app.getPath('userData'), 'Koring.yml');
|
||||
if (!fs.existsSync(src)) return;
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(`[migrate] copied Koring.yml ${src} → ${dest}`);
|
||||
} catch (e) {
|
||||
console.error(`[migrate] failed to copy Koring.yml:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +67,8 @@ function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof
|
||||
// 3. Load (or create) config(getConfig 会缓存到主进程内存,成为唯一权威)
|
||||
const config = getConfig();
|
||||
if (isFirstLaunch) {
|
||||
saveConfig(config);
|
||||
// 首次启动:全量写入,确保配置文件在安装目录可见(稀疏保存下全默认不落盘)
|
||||
saveConfig(config, true);
|
||||
}
|
||||
|
||||
return { isFirstLaunch, config };
|
||||
@@ -175,7 +170,7 @@ function registerAllHandlers() {
|
||||
app.whenReady().then(() => {
|
||||
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();
|
||||
|
||||
// Run startup checks before creating windows
|
||||
|
||||
+27
-1
@@ -35,6 +35,12 @@ import { OobeBetaTest } from "./pages/oobe/step-beta-test";
|
||||
import { OobeFinish } from "./pages/oobe/step-finish";
|
||||
import { OobeLegal } from "./pages/oobe/step-legal";
|
||||
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 = {
|
||||
home: Home,
|
||||
@@ -56,6 +62,12 @@ const pageMap = {
|
||||
"oobe/finish": OobeFinish,
|
||||
"oobe/about-info": OobeAboutInfo,
|
||||
"oobe/legal": OobeLegal,
|
||||
upvp: UpvpComplete,
|
||||
"upvp/complete": UpvpComplete,
|
||||
"upvp/version": UpvpVersion,
|
||||
"upvp/check": UpvpCheck,
|
||||
"upvp/beta-test": UpvpBetaTest,
|
||||
"upvp/finish": UpvpFinish,
|
||||
debug: Debug,
|
||||
"debug-splash": SplashDebug,
|
||||
"debug-display": DisplayDebug,
|
||||
@@ -83,9 +95,23 @@ function App() {
|
||||
syncBackgroundFromConfig();
|
||||
useAuthStore.getState().initFromRegistry();
|
||||
useKoringAuthStore.getState().initFromDisk();
|
||||
// Navigate to OOBE on first launch or if oobe not completed
|
||||
|
||||
// 首次启动 / 未完成 OOBE → OOBE
|
||||
if (isFirstLaunch || cfg.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");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ export interface InstanceMeta {
|
||||
|
||||
export interface AppConfig {
|
||||
version: number;
|
||||
/** 当前应用版本号(写入配置时由主进程刷新) */
|
||||
appVersion: string;
|
||||
oobe: boolean;
|
||||
app: AppInfoConfig;
|
||||
theme: ThemeConfig;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -44,10 +44,13 @@ interface ConfigState {
|
||||
setUi: (patch: Partial<UiConfig>) => void;
|
||||
setInstances: (instances: InstanceMeta[]) => void;
|
||||
setOobe: (value: boolean) => void;
|
||||
/** 记录当前应用版本号到配置(升级引导完成后由 upvp 流程写入) */
|
||||
setAppVersion: (version: string) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: AppConfig = {
|
||||
version: 1,
|
||||
appVersion: "",
|
||||
oobe: true,
|
||||
app: { language: "zh-CN" },
|
||||
theme: { darkMode: "auto", parallax: true },
|
||||
@@ -167,4 +170,10 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||
set({ config: { ...config, oobe: value } });
|
||||
submit("oobe", value);
|
||||
},
|
||||
|
||||
setAppVersion: (version) => {
|
||||
const { config } = get();
|
||||
set({ config: { ...config, appVersion: version } });
|
||||
submit("appVersion", version);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -20,6 +20,12 @@ export type RouteKey =
|
||||
| "oobe/finish"
|
||||
| "oobe/about-info"
|
||||
| "oobe/legal"
|
||||
| "upvp"
|
||||
| "upvp/complete"
|
||||
| "upvp/version"
|
||||
| "upvp/check"
|
||||
| "upvp/beta-test"
|
||||
| "upvp/finish"
|
||||
| "debug"
|
||||
| "debug-splash"
|
||||
| "debug-display"
|
||||
@@ -64,6 +70,12 @@ export const allRoutes: RouteItem[] = [
|
||||
{ key: "oobe/finish", label: "完成", path: "/oobe/finish", hidden: true },
|
||||
{ key: "oobe/about-info", label: "关于信息", path: "/oobe/about-info", hidden: true, backable: 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-splash", label: "启动动画调试", path: "/debug/splash", 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));
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user