mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
168 lines
7.0 KiB
TypeScript
168 lines
7.0 KiB
TypeScript
// AboutVersion:展示当前版本的更新内容。
|
||||
|
|
// 数据源:getReleaseNotes()(GitHub release-notes.md,主进程自动切加速源,回退最新版)。
|
|||
|
|
// 不直接渲染 Markdown —— 解析后按提交类型(新增/修复/优化/重构/文档/其他)分组,
|
|||
|
|
// 每组配图标,用卡片展示。
|
|||
|
|
import { useCallback, useEffect, useState } from "react";
|
|||
|
|
import { Sparkles, Bug, Gauge, RefreshCw, FileText, Wrench, ExternalLink, GitCommitHorizontal } from "lucide-react";
|
|||
|
|
import { getReleaseNotes, type ReleaseNotesResult } from "@/api/update";
|
|||
|
|
import { VERSION } from "@/lib/version";
|
|||
|
|
import { parseReleaseNotes, type ChangeType, type VersionChange } from "./parse";
|
|||
|
|
|
|||
|
|
const GITHUB_RELEASES = "https://github.com/dream-pep/koring-launcher/releases";
|
|||
|
|
|
|||
|
|
const CATEGORY_ORDER: ChangeType[] = ["feat", "fix", "perf", "refactor", "docs", "other"];
|
|||
|
|
|
|||
|
|
const CATEGORY_META: Record<ChangeType, { label: string; icon: React.ComponentType<{ className?: string }>; iconCls: string; dotCls: string }> = {
|
|||
|
|
feat: { label: "新增功能", icon: Sparkles, iconCls: "text-sky-600 dark:text-sky-400 bg-sky-500/10", dotCls: "bg-sky-500" },
|
|||
|
|
fix: { label: "修复", icon: Bug, iconCls: "text-red-600 dark:text-red-400 bg-red-500/10", dotCls: "bg-red-500" },
|
|||
|
|
perf: { label: "性能优化", icon: Gauge, iconCls: "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10", dotCls: "bg-emerald-500" },
|
|||
|
|
refactor: { label: "重构", icon: RefreshCw, iconCls: "text-violet-600 dark:text-violet-400 bg-violet-500/10", dotCls: "bg-violet-500" },
|
|||
|
|
docs: { label: "文档", icon: FileText, iconCls: "text-amber-600 dark:text-amber-400 bg-amber-500/10", dotCls: "bg-amber-500" },
|
|||
|
|
other: { label: "其他", icon: Wrench, iconCls: "text-foreground/60 bg-foreground/[0.06]", dotCls: "bg-foreground/40" },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 一条变更(含 commit 标)+ 短分隔条 */
|
|||
|
|
function ChangeItem({ change }: { change: VersionChange }) {
|
|||
|
|
return (
|
|||
|
|
<div className="flex items-start gap-2.5 px-4 py-2.5 first:pt-3 last:pb-3">
|
|||
|
|
{change.commit && (
|
|||
|
|
<span className="inline-flex items-center gap-1 shrink-0 mt-[3px] font-mono text-[10px] px-1.5 py-0.5 rounded bg-foreground/[0.05] dark:bg-white/[0.05] text-muted-foreground/80">
|
|||
|
|
<GitCommitHorizontal className="w-3 h-3" />
|
|||
|
|
{change.commit}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
<div className="flex-1 min-w-0">
|
|||
|
|
<p className="text-[13px] font-medium text-foreground leading-snug">{change.title}</p>
|
|||
|
|
{change.description && (
|
|||
|
|
<p className="text-[12px] text-muted-foreground/80 leading-relaxed mt-0.5">{change.description}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 一个分类卡片:图标头部 + 该类型的变更列表 */
|
|||
|
|
function CategoryCard({ type, items }: { type: ChangeType; items: VersionChange[] }) {
|
|||
|
|
const meta = CATEGORY_META[type];
|
|||
|
|
const Icon = meta.icon;
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-xl overflow-hidden border border-black/[0.06] dark:border-white/[0.07] bg-white/85 dark:bg-black/45 backdrop-blur-[12px]">
|
|||
|
|
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-black/[0.05] dark:border-white/[0.06]">
|
|||
|
|
<span className={`flex items-center justify-center w-6 h-6 rounded-md ${meta.iconCls}`}>
|
|||
|
|
<Icon className="w-3.5 h-3.5" />
|
|||
|
|
</span>
|
|||
|
|
<span className="text-[13px] font-semibold text-foreground">{meta.label}</span>
|
|||
|
|
<span className="text-[11px] text-muted-foreground/60 ml-auto tabular-nums">{items.length} 项</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="divide-y divide-black/[0.04] dark:divide-white/[0.05]">
|
|||
|
|
{items.map((c, i) => (
|
|||
|
|
<ChangeItem key={`${c.commit ?? ""}-${i}`} change={c} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function EmptyCard({ text }: { text: string }) {
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-xl border border-black/[0.06] dark:border-white/[0.07] bg-white/85 dark:bg-black/45 backdrop-blur-[12px] px-5 py-10 text-center">
|
|||
|
|
<p className="text-[13px] text-muted-foreground">{text}</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function AboutVersion({ className }: { className?: string }) {
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
const [result, setResult] = useState<ReleaseNotesResult | null>(null);
|
|||
|
|
|
|||
|
|
const load = useCallback(async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
setError(null);
|
|||
|
|
try {
|
|||
|
|
const r = await getReleaseNotes();
|
|||
|
|
setResult(r);
|
|||
|
|
} catch (e) {
|
|||
|
|
setError(e instanceof Error ? e.message : String(e));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
load();
|
|||
|
|
}, [load]);
|
|||
|
|
|
|||
|
|
const parsed = result ? parseReleaseNotes(result.notes, result.version, result.tag) : null;
|
|||
|
|
|
|||
|
|
const grouped = parsed
|
|||
|
|
? CATEGORY_ORDER.map((t) => ({ type: t, items: parsed.changes.filter((c) => c.type === t) })).filter(
|
|||
|
|
(g) => g.items.length > 0,
|
|||
|
|
)
|
|||
|
|
: [];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className={className}>
|
|||
|
|
{/* 头部:版本 + 来源 */}
|
|||
|
|
<div className="flex items-center justify-between mb-2 px-1">
|
|||
|
|
<span className="text-[12px] text-muted-foreground/80">
|
|||
|
|
{parsed ? (
|
|||
|
|
<>
|
|||
|
|
版本 v{parsed.version}
|
|||
|
|
{result?.isLatest && parsed.version !== VERSION && (
|
|||
|
|
<span className="ml-2 opacity-70">(当前版本暂无说明,展示最新版本内容)</span>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>版本 v{VERSION}</>
|
|||
|
|
)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{[0, 1, 2].map((i) => (
|
|||
|
|
<div key={i} className="h-[76px] rounded-xl bg-white/50 dark:bg-white/[0.04] animate-pulse" />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : error ? (
|
|||
|
|
<EmptyCard text="获取此版本的更新内容失败" />
|
|||
|
|
) : !result || !parsed ? (
|
|||
|
|
<EmptyCard text="未能获取到发布说明(GitHub 与加速源均不可用,或该版本尚未发布)" />
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2.5">
|
|||
|
|
{parsed.noCommits && parsed.changes.length === 0 ? (
|
|||
|
|
<EmptyCard text="此版本暂无变更记录" />
|
|||
|
|
) : grouped.length === 0 ? (
|
|||
|
|
<EmptyCard text="此版本暂无变更记录" />
|
|||
|
|
) : (
|
|||
|
|
grouped.map((g) => <CategoryCard key={g.type} type={g.type} items={g.items} />)
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 底部操作 */}
|
|||
|
|
<div className="flex items-center gap-2 mt-3 px-1">
|
|||
|
|
{error && (
|
|||
|
|
<>
|
|||
|
|
<p className="text-[11px] text-destructive/80 flex-1 truncate">{error}</p>
|
|||
|
|
<button
|
|||
|
|
onClick={load}
|
|||
|
|
className="text-[12px] font-medium text-primary hover:underline shrink-0"
|
|||
|
|
>
|
|||
|
|
重试
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
<button
|
|||
|
|
onClick={() => window.electronAPI?.openExternal(GITHUB_RELEASES)}
|
|||
|
|
className="inline-flex items-center gap-0.5 text-[12px] text-muted-foreground hover:text-foreground transition-colors ml-auto shrink-0"
|
|||
|
|
>
|
|||
|
|
查看 GitHub Releases
|
|||
|
|
<ExternalLink className="w-3 h-3" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|