feat: 站点单页、时间线导入与行级 BBCode 短代码
首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,8 @@ type MdHideBlockProps = {
|
||||
points?: number;
|
||||
locked?: boolean;
|
||||
postId?: number;
|
||||
/** 登录跳转路径(公告等无 postId 时使用) */
|
||||
loginHref?: string;
|
||||
loggedIn?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
@@ -37,6 +39,7 @@ export default function MdHideBlock({
|
||||
points = 0,
|
||||
locked = false,
|
||||
postId,
|
||||
loginHref,
|
||||
loggedIn = false,
|
||||
children,
|
||||
}: MdHideBlockProps) {
|
||||
@@ -46,6 +49,9 @@ export default function MdHideBlock({
|
||||
const [pwd, setPwd] = useState("");
|
||||
const [pwdError, setPwdError] = useState("");
|
||||
const label = hideKindLabel(kind);
|
||||
const loginTo =
|
||||
loginHref ||
|
||||
(postId ? `/login?redirect=${encodeURIComponent(`/post/${postId}`)}` : "/login");
|
||||
|
||||
if (locked) {
|
||||
const unlockPoints = async () => {
|
||||
@@ -115,11 +121,8 @@ export default function MdHideBlock({
|
||||
: "登录后即可阅读此段"}
|
||||
</p>
|
||||
<div className="j13-hide-gate-actions">
|
||||
{kind === "login" && !loggedIn && postId && (
|
||||
<Link
|
||||
href={`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{kind === "login" && !loggedIn && (
|
||||
<Link href={loginTo} className="btn btn-primary">
|
||||
去登录
|
||||
</Link>
|
||||
)}
|
||||
@@ -133,23 +136,24 @@ export default function MdHideBlock({
|
||||
{busy ? "解锁中…" : `支付 ${points || 0} 积分解锁`}
|
||||
</button>
|
||||
)}
|
||||
{kind === "points" && !loggedIn && postId && (
|
||||
<Link
|
||||
href={`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{kind === "points" && !loggedIn && (
|
||||
<Link href={loginTo} className="btn btn-primary">
|
||||
登录后解锁
|
||||
</Link>
|
||||
)}
|
||||
{kind === "reply" && postId && !loggedIn && (
|
||||
{kind === "reply" && !loggedIn && (
|
||||
<Link
|
||||
href={`/login?redirect=${encodeURIComponent(`/post/${postId}#comment-input`)}`}
|
||||
href={
|
||||
postId
|
||||
? `/login?redirect=${encodeURIComponent(`/post/${postId}#comment-input`)}`
|
||||
: loginTo
|
||||
}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
登录后去回复
|
||||
</Link>
|
||||
)}
|
||||
{kind === "reply" && loggedIn && (
|
||||
{kind === "reply" && loggedIn && postId && (
|
||||
<a
|
||||
href="#comment-input"
|
||||
className="btn btn-primary"
|
||||
|
||||
67
frontend/components/markdown/MdTimeline.tsx
Normal file
67
frontend/components/markdown/MdTimeline.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import {
|
||||
groupTimelineByDate,
|
||||
type TimelineItem,
|
||||
} from "@/lib/timelineBlocks";
|
||||
|
||||
type MdTimelineProps = {
|
||||
items: TimelineItem[];
|
||||
/** 每项正文已渲染的 React 节点(与 items 等长);缺省则只显示纯文本 */
|
||||
renderBody?: (item: TimelineItem, index: number) => ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* 按日分组的垂直时间线(更新树)。颜色走 CSS 令牌。
|
||||
*/
|
||||
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) => (
|
||||
<div key={g.date} className="j13-timeline-day">
|
||||
<div className="j13-timeline-day-label">
|
||||
<time dateTime={g.date}>{g.date}</time>
|
||||
</div>
|
||||
<ul className="j13-timeline-items">
|
||||
{g.items.map((it) => {
|
||||
const idx = flat++;
|
||||
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">
|
||||
<h3 className="j13-timeline-title">{it.title}</h3>
|
||||
{it.sourceUrl ? (
|
||||
<a
|
||||
href={it.sourceUrl}
|
||||
className="j13-timeline-src"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-tip="查看提交"
|
||||
aria-label="查看提交"
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{renderBody ? (
|
||||
<div className="j13-timeline-body">{renderBody(it, idx)}</div>
|
||||
) : it.body.trim() ? (
|
||||
<div className="j13-timeline-body whitespace-pre-wrap">{it.body}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user