feat: 站点单页、时间线导入与行级 BBCode 短代码

首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 19:52:06 +08:00
parent 2aacf99a49
commit a83276060f
52 changed files with 5073 additions and 648 deletions

View File

@@ -1,7 +1,10 @@
import type { ReactNode } from "react";
import type { Components } from "react-markdown";
import { renderMarkdownToReact } from "@/components/markdown/renderMarkdown";
import { splitHideSegments, type HideKind } from "@/lib/hideBlocks";
import { splitContentSegments } from "@/lib/directiveBlocks";
import type { HideKind } from "@/lib/hideBlocks";
import MdHideBlock from "@/components/markdown/MdHideBlock";
import MdTimeline from "@/components/markdown/MdTimeline";
type MarkdownBodyProps = {
content: string;
@@ -9,20 +12,23 @@ type MarkdownBodyProps = {
components?: Components;
/** 详情页解锁门禁需要 */
postId?: number;
/** 登录跳转(公告等无 postId 时) */
loginHref?: string;
loggedIn?: boolean;
};
/**
* 站点统一 Markdown 渲染(服务端):GFM + Shiki;支持 :::hide 内嵌块。
* 站点统一 Markdown 渲染(服务端):GFM + Shiki;支持 [hide] / [timeline]。
*/
export default async function MarkdownBody({
content,
className = "prose-content",
components,
postId,
loginHref,
loggedIn = false,
}: MarkdownBodyProps) {
const segs = splitHideSegments(content || "");
const segs = splitContentSegments(content || "");
const nodes = [];
for (let i = 0; i < segs.length; i++) {
const seg = segs[i];
@@ -33,6 +39,25 @@ export default async function MarkdownBody({
);
continue;
}
if (seg.type === "timeline") {
const bodies: ReactNode[] = [];
for (let j = 0; j < seg.items.length; j++) {
const it = seg.items[j];
if (!it.body.trim()) {
bodies.push(null);
continue;
}
bodies.push(await renderMarkdownToReact(it.body, components));
}
nodes.push(
<MdTimeline
key={`tl-${i}`}
items={seg.items}
renderBody={(_it, idx) => bodies[idx]}
/>
);
continue;
}
const kind = seg.kind as HideKind;
if (seg.locked) {
nodes.push(
@@ -42,6 +67,7 @@ export default async function MarkdownBody({
points={seg.points}
locked
postId={postId}
loginHref={loginHref}
loggedIn={loggedIn}
/>
);