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:
+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