mirror of
https://github.com/lingke-net/koring-space.git
synced 2026-09-12 05:45:17 +08:00
feat(发布说明): 新增结构化发布说明组件并更新相关页面
新建 `ReleaseNotes` 结构化发布说明组件,支持按提交类型分组展示并添加对应图标,同时兼容普通Markdown渲染 替换原有 `MarkdownLite` 组件至 `app/launcher/join-beta/page.tsx` 和 `app/download/page.tsx` 更新下载页面的发布说明标题为「此版本的变更」
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
Inbox,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MarkdownLite } from "@/components/markdown-lite";
|
||||
import { ReleaseNotes } from "@/components/release-notes";
|
||||
import {
|
||||
DOWNLOAD_SOURCES,
|
||||
resolveDownloadUrl,
|
||||
@@ -319,9 +319,9 @@ export default function DownloadPage() {
|
||||
<div className="border-t border-border/50 p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">版本更新内容</h3>
|
||||
<h3 className="text-lg font-semibold">此版本的变更</h3>
|
||||
</div>
|
||||
<MarkdownLite text={active.releaseNotes} />
|
||||
<ReleaseNotes text={active.releaseNotes} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MarkdownLite } from "@/components/markdown-lite";
|
||||
import { ReleaseNotes } from "@/components/release-notes";
|
||||
import {
|
||||
DOWNLOAD_SOURCES,
|
||||
resolveDownloadUrl,
|
||||
@@ -328,7 +328,7 @@ export default function JoinBetaPage() {
|
||||
</h2>
|
||||
</div>
|
||||
{data.releaseNotes ? (
|
||||
<MarkdownLite text={data.releaseNotes} />
|
||||
<ReleaseNotes text={data.releaseNotes} />
|
||||
) : data.aboutversion ? (
|
||||
<>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Sparkles,
|
||||
Bug,
|
||||
Zap,
|
||||
RefreshCw,
|
||||
FileText,
|
||||
Rocket,
|
||||
Wrench,
|
||||
ChevronRight,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MarkdownLite } from "./markdown-lite";
|
||||
|
||||
/**
|
||||
* Release Notes 结构化渲染器
|
||||
*
|
||||
* 适配 Koring Launcher 发布说明格式(scripts/release-notes.ps1 生成):
|
||||
* # Koring Launcher Releases X
|
||||
* ## 版本信息
|
||||
* 当前版本 {FullVersion}
|
||||
* 编译状态:BETA / RUN
|
||||
* 签名状态:已签名
|
||||
* 构建来源:commit xxxxxx
|
||||
* ## 更新了什么内容
|
||||
* <details><summary>·Commit 1cf906d</summary>提交说明…</details>
|
||||
*
|
||||
* 渲染策略:
|
||||
* - 版本信息行 -> 顶部小徽章(编译状态 / 签名状态 / 构建来源)
|
||||
* - 每条 <details> commit -> 按提交类型(feat/fix/perf/… 或 修复/新增/优化/…)
|
||||
* 归类到独立卡片,每种类型带独立图标;卡片内每条 commit 仍是可折叠的
|
||||
* <details>(原生 HTML,无 JS 依赖)
|
||||
* - 非 commit 结构内容 -> 回退 MarkdownLite 常规渲染
|
||||
*/
|
||||
|
||||
export type ChangeType =
|
||||
| "feat"
|
||||
| "fix"
|
||||
| "perf"
|
||||
| "refactor"
|
||||
| "docs"
|
||||
| "build"
|
||||
| "other";
|
||||
|
||||
interface TypeMeta {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
color: string; // 图标颜色 class
|
||||
}
|
||||
|
||||
const TYPE_META: Record<ChangeType, TypeMeta> = {
|
||||
feat: { label: "新增功能", icon: Sparkles, color: "text-emerald-500" },
|
||||
fix: { label: "问题修复", icon: Bug, color: "text-rose-500" },
|
||||
perf: { label: "体验优化", icon: Zap, color: "text-amber-500" },
|
||||
refactor: { label: "代码重构", icon: RefreshCw, color: "text-violet-500" },
|
||||
docs: { label: "文档", icon: FileText, color: "text-sky-500" },
|
||||
build: { label: "构建与发布", icon: Rocket, color: "text-slate-500" },
|
||||
other: { label: "其他", icon: Wrench, color: "text-muted-foreground" },
|
||||
};
|
||||
|
||||
const CARD_ORDER: ChangeType[] = [
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"docs",
|
||||
"build",
|
||||
"other",
|
||||
];
|
||||
|
||||
export interface CommitInfo {
|
||||
hash: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
type: ChangeType;
|
||||
}
|
||||
|
||||
export interface ReleaseInfoItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ParsedReleaseBody {
|
||||
mode: "structured" | "plain";
|
||||
info: ReleaseInfoItem[];
|
||||
commits: CommitInfo[];
|
||||
}
|
||||
|
||||
/** 根据提交说明推断变更类型(Conventional Commits / 中文前缀 / 英文动词) */
|
||||
export function classifyCommit(subject: string): ChangeType {
|
||||
const s = (subject || "").trim();
|
||||
if (!s) return "other";
|
||||
|
||||
// Conventional Commits: type(scope)?: message
|
||||
const m = s.match(
|
||||
/^(feat|feature|fix|perf|refactor|docs|style|test|chore|build|ci|revert)(?:\([^)]*\))?:\s*[\s\S]*$/i
|
||||
);
|
||||
if (m) {
|
||||
const t = m[1].toLowerCase();
|
||||
if (t === "feat" || t === "feature") return "feat";
|
||||
if (t === "fix") return "fix";
|
||||
if (t === "perf") return "perf";
|
||||
if (t === "refactor") return "refactor";
|
||||
if (t === "docs") return "docs";
|
||||
if (t === "build" || t === "ci") return "build";
|
||||
return "other"; // style/test/chore/revert
|
||||
}
|
||||
|
||||
// 中文关键词
|
||||
if (/修复/.test(s)) return "fix";
|
||||
if (/新增|添加|新功能|支持/.test(s)) return "feat";
|
||||
if (/优化|改进|提升|完善|增强/.test(s)) return "perf";
|
||||
if (/重构/.test(s)) return "refactor";
|
||||
|
||||
// 无前缀英文
|
||||
const en = s.match(/^([A-Za-z]+)\b/);
|
||||
if (en) {
|
||||
const w = en[1].toLowerCase();
|
||||
if (/^(fix|correct|resolve|repair)/.test(w)) return "fix";
|
||||
if (
|
||||
/^(add|new|create|implement|introduce|integrate|enable|import|support|build)/.test(w)
|
||||
)
|
||||
return "feat";
|
||||
if (/^(stabilize|adjust|improve|optimize|update|refactor|polish)/.test(w))
|
||||
return "perf";
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
|
||||
function stripHtml(s: string) {
|
||||
return s.replace(/<[^>]+>/g, "").trim();
|
||||
}
|
||||
|
||||
/** 解析发布说明正文 */
|
||||
export function parseReleaseBody(text: string): ParsedReleaseBody {
|
||||
if (!text || !text.trim()) return { mode: "plain", info: [], commits: [] };
|
||||
|
||||
const detailsBlocks = [...text.matchAll(/<details([^>]*)>([\s\S]*?)<\/details>/gi)];
|
||||
|
||||
if (detailsBlocks.length === 0) {
|
||||
return { mode: "plain", info: [], commits: [] };
|
||||
}
|
||||
|
||||
// 版本信息行(正文顶层,不含 details 内部)
|
||||
const info: ReleaseInfoItem[] = [];
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m = line.match(
|
||||
/^(当前版本|编译状态|签名状态|构建来源)\s*[::]?\s*(.+)$/
|
||||
);
|
||||
if (m) info.push({ label: m[1], value: m[2].trim() });
|
||||
}
|
||||
|
||||
const commits: CommitInfo[] = detailsBlocks.map((m) => {
|
||||
const inner = m[2];
|
||||
// summary 是标题行;正文 = inner 去掉 summary 后的部分
|
||||
const summaryMatch = inner.match(
|
||||
/<summary[^>]*>([\s\S]*?)<\/summary>/i
|
||||
);
|
||||
const summaryText = summaryMatch ? stripHtml(summaryMatch[1]) : "";
|
||||
const content = summaryMatch
|
||||
? inner.replace(summaryMatch[0], "")
|
||||
: inner;
|
||||
|
||||
const hashMatch =
|
||||
summaryText.match(/[0-9a-fA-F]{7,40}/) ||
|
||||
content.match(/[0-9a-fA-F]{7,40}/);
|
||||
const hash = hashMatch ? hashMatch[0] : "";
|
||||
|
||||
const lines = content
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const subject =
|
||||
lines[0] ||
|
||||
summaryText.replace(/^[·\s]*Commit\s*/i, "").trim() ||
|
||||
"未命名变更";
|
||||
const body = lines.slice(1).join("\n");
|
||||
|
||||
return {
|
||||
hash,
|
||||
subject,
|
||||
body,
|
||||
type: classifyCommit(subject),
|
||||
};
|
||||
});
|
||||
|
||||
return { mode: "structured", info, commits };
|
||||
}
|
||||
|
||||
function CommitRow({ commit }: { commit: CommitInfo }) {
|
||||
const meta = TYPE_META[commit.type];
|
||||
return (
|
||||
<li className="border-t border-border/40 first:border-t-0">
|
||||
<details className="group">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 px-4 py-2.5 text-sm transition-colors hover:bg-muted/40 [&::-webkit-details-marker]:hidden">
|
||||
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] leading-none text-muted-foreground">
|
||||
{commit.hash ? `#${commit.hash.slice(0, 7)}` : "commit"}
|
||||
</span>
|
||||
<meta.icon className={cn("size-4 shrink-0", meta.color)} />
|
||||
<span className="min-w-0 flex-1 leading-snug">{commit.subject}</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" />
|
||||
</summary>
|
||||
{commit.body && (
|
||||
<div className="border-l-2 border-border/60 px-4 pb-3 ml-[3.4rem]">
|
||||
<MarkdownLite text={commit.body} />
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeCard({ type, commits }: { type: ChangeType; commits: CommitInfo[] }) {
|
||||
const meta = TYPE_META[type];
|
||||
if (commits.length === 0) return null;
|
||||
return (
|
||||
<section className="overflow-hidden rounded-xl border border-border/50 bg-background/40">
|
||||
<header className="flex items-center gap-2 border-b border-border/50 bg-background/60 px-4 py-3">
|
||||
<span className="flex size-6 items-center justify-center rounded-md bg-muted">
|
||||
<meta.icon className={cn("size-4", meta.color)} />
|
||||
</span>
|
||||
<span className="font-semibold text-sm">{meta.label}</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{commits.length}
|
||||
</span>
|
||||
</header>
|
||||
<ul className="divide-y divide-border/40">
|
||||
{commits.map((c, i) => (
|
||||
<CommitRow key={c.hash || `${c.subject}-${i}`} commit={c} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReleaseNotes({ text, className }: { text: string; className?: string }) {
|
||||
const parsed = React.useMemo(() => parseReleaseBody(text), [text]);
|
||||
|
||||
if (parsed.mode === "plain") {
|
||||
return <MarkdownLite text={text} className={className} />;
|
||||
}
|
||||
|
||||
const grouped = new Map<ChangeType, CommitInfo[]>();
|
||||
for (const t of CARD_ORDER) grouped.set(t, []);
|
||||
for (const c of parsed.commits) {
|
||||
grouped.get(c.type)?.push(c);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-4", className)}>
|
||||
{/* 版本信息徽章 */}
|
||||
{parsed.info.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parsed.info.map((it) => (
|
||||
<span
|
||||
key={it.label}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background/40 px-3 py-1 text-xs"
|
||||
>
|
||||
<span className="text-muted-foreground">{it.label}</span>
|
||||
<span className="font-medium">{it.value}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提交类型卡片 */}
|
||||
{CARD_ORDER.map((t) => (
|
||||
<TypeCard key={t} type={t} commits={grouped.get(t) || []} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user