Files
jiang13-bbs/frontend/components/markdown/MdTimeline.tsx
freefire 0b60c4299b feat: 公文三栏单页布局,并修复时间线目录跳转与样式
公告/单页对齐帖子详情三栏壳;时间线按日锚点可跳转,条目改为日志行并用暖金日期区分 feat 色。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 21:15:15 +08:00

103 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from "react";
import { ExternalLink } from "lucide-react";
import {
groupTimelineByDate,
type TimelineItem,
} from "@/lib/timelineBlocks";
import { timelineDayId } from "@/lib/toc";
type MdTimelineProps = {
items: TimelineItem[];
/** 每项正文已渲染的 React 节点(与 items 等长);缺省则只显示纯文本 */
renderBody?: (item: TimelineItem, index: number) => ReactNode;
};
const PREFIX_RE =
/^(feat|fix|polish|chore|docs|refactor|style|perf|test)(?:\(([^)]*)\))?\s*:?\s*/i;
function splitTitlePrefix(title: string): { kind?: string; scope?: string; rest: string } {
const m = PREFIX_RE.exec(title);
if (!m) return { rest: title };
const rest = title.slice(m[0].length).trim() || title;
return {
kind: m[1].toLowerCase(),
scope: m[2]?.trim() || undefined,
rest,
};
}
/**
* 按日分组的垂直时间线(更新树)。
* 日分组 id 与 extractToc 的 tl-YYYY-MM-DD 对齐,供右侧目录跳转。
* 条目用普通段落而非 h3,避免被 .prose-content 标题样式放大。
*/
export default function MdTimeline({ items, renderBody }: MdTimelineProps) {
if (!items.length) {
return <p className="meta px-1 py-2">暂无时间线条目</p>;
}
const groups = groupTimelineByDate(items);
let flat = 0;
return (
<div className="j13-timeline" role="list" aria-label="时间线">
{groups.map((g) => (
<section
key={g.date}
id={timelineDayId(g.date)}
className="j13-timeline-day"
style={{ scrollMarginTop: 90 }}
aria-labelledby={`${timelineDayId(g.date)}-label`}
>
<div className="j13-timeline-day-label" id={`${timelineDayId(g.date)}-label`}>
<time dateTime={/^\d{4}-\d{2}-\d{2}$/.test(g.date) ? g.date : undefined}>
{g.date}
</time>
<span className="j13-timeline-day-count">{g.items.length}</span>
</div>
<ul className="j13-timeline-items">
{g.items.map((it) => {
const idx = flat++;
const { kind, scope, rest } = splitTitlePrefix(it.title);
const bodyNode = renderBody
? renderBody(it, idx)
: it.body.trim()
? <span className="whitespace-pre-wrap">{it.body}</span>
: null;
return (
<li key={`${g.date}-${idx}-${it.title}`} className="j13-timeline-item" role="listitem">
<span className="j13-timeline-dot" aria-hidden />
<div className="j13-timeline-card">
<div className="j13-timeline-head">
{kind ? (
<span className={`j13-timeline-chip is-${kind}`}>
{kind}
{scope ? <span className="j13-timeline-chip-scope">({scope})</span> : null}
</span>
) : null}
<p className="j13-timeline-summary">{rest}</p>
{it.sourceUrl ? (
<a
href={it.sourceUrl}
className="j13-timeline-src"
target="_blank"
rel="noopener noreferrer"
data-tip="查看提交"
aria-label="查看提交"
>
<ExternalLink size={12} />
</a>
) : null}
</div>
{bodyNode ? <div className="j13-timeline-body">{bodyNode}</div> : null}
</div>
</li>
);
})}
</ul>
</section>
))}
</div>
);
}