feat: 站点单页、时间线导入与行级 BBCode 短代码
首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -86,7 +86,13 @@ export type AnnouncementColor =
|
||||
| "orange"
|
||||
| "red"
|
||||
| "purple"
|
||||
| "gray";
|
||||
| "gray"
|
||||
| "teal"
|
||||
| "cyan"
|
||||
| "pink"
|
||||
| "amber"
|
||||
| "indigo"
|
||||
| "rose";
|
||||
|
||||
// 右栏站点公告摘要(标签/日期/标题)
|
||||
export interface AnnouncementSummary {
|
||||
@@ -95,6 +101,7 @@ export interface AnnouncementSummary {
|
||||
tag: string;
|
||||
tag_color: AnnouncementColor;
|
||||
created_at: string;
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
// 公告详情/后台管理完整对象
|
||||
@@ -105,10 +112,42 @@ export interface Announcement {
|
||||
tag: string;
|
||||
tag_color: AnnouncementColor;
|
||||
published: boolean;
|
||||
pinned: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SidebarPageItem {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
}
|
||||
|
||||
export interface SitePage {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
content: string;
|
||||
excerpt: string;
|
||||
published: boolean;
|
||||
show_in_sidebar: boolean;
|
||||
sort_order: number;
|
||||
allow_comments: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SitePageInput {
|
||||
slug: string;
|
||||
title: string;
|
||||
content: string;
|
||||
excerpt?: string;
|
||||
published: boolean;
|
||||
show_in_sidebar: boolean;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
export interface NewUser {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -130,6 +169,8 @@ export interface OverviewResponse {
|
||||
active_users: ActiveUser[];
|
||||
boards: BoardWithCount[];
|
||||
announcements: AnnouncementSummary[];
|
||||
announcements_total?: number;
|
||||
sidebar_pages?: SidebarPageItem[];
|
||||
new_users: NewUser[];
|
||||
checkin?: CheckinStatus | null;
|
||||
}
|
||||
@@ -1509,11 +1550,15 @@ export async function apiDoCheckin(): Promise<CheckinStatus> {
|
||||
|
||||
// ===== 站点公告 =====
|
||||
|
||||
// SSR 公告详情(仅已发布;草稿/不存在由调用方按 404 处理)
|
||||
// SSR 公告详情(仅已发布;转发 cookie 以便按登录态脱敏 hide)
|
||||
export async function fetchAnnouncementDetail(
|
||||
id: string
|
||||
id: string,
|
||||
cookieHeader?: string
|
||||
): Promise<{ announcement: Announcement } | null> {
|
||||
const res = await fetch(`${API_BASE}/api/announcements/${id}`, ssrInit());
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/announcements/${id}`,
|
||||
ssrInit(cookieHeaders(cookieHeader))
|
||||
);
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error("获取公告失败");
|
||||
return res.json();
|
||||
@@ -1550,6 +1595,7 @@ export interface AnnouncementInput {
|
||||
tag: string;
|
||||
tag_color: AnnouncementColor;
|
||||
published: boolean;
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
async function saveAnnouncement(
|
||||
@@ -1586,6 +1632,126 @@ export async function apiAdminDeleteAnnouncement(id: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiAdminToggleAnnouncementPin(
|
||||
id: number
|
||||
): Promise<{ announcement: Announcement }> {
|
||||
const res = await fetchWithRefresh(`/api/admin/announcements/${id}/pin`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "置顶失败");
|
||||
return data as { announcement: Announcement };
|
||||
}
|
||||
|
||||
/** SSR / 公开:分页公告列表 */
|
||||
export async function fetchAnnouncementsPage(
|
||||
page = 1,
|
||||
size = 20
|
||||
): Promise<{ announcements: Announcement[]; total: number; page: number }> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/announcements?page=${page}&size=${size}`,
|
||||
ssrInit()
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取公告失败");
|
||||
return data as { announcements: Announcement[]; total: number; page: number };
|
||||
}
|
||||
|
||||
export type TimelineGitItem = {
|
||||
date: string;
|
||||
title: string;
|
||||
body: string;
|
||||
source_url?: string;
|
||||
sha?: string;
|
||||
};
|
||||
|
||||
export async function apiTimelineFromGit(input: {
|
||||
urls: string[];
|
||||
follow_pages?: boolean;
|
||||
}): Promise<{ items: TimelineGitItem[]; warning?: string; error?: string }> {
|
||||
const res = await fetchWithRefresh("/api/timeline/from-git", {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "导入失败");
|
||||
return data as { items: TimelineGitItem[]; warning?: string; error?: string };
|
||||
}
|
||||
|
||||
export async function fetchSitePageBySlug(
|
||||
slug: string
|
||||
): Promise<{ page: SitePage } | null> {
|
||||
const res = await fetch(`${API_BASE}/api/pages/${encodeURIComponent(slug)}`, ssrInit());
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error("获取页面失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiAdminListSitePages(): Promise<{ pages: SitePage[] }> {
|
||||
const res = await fetchWithRefresh("/api/admin/pages", { headers: clientHeaders() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取单页失败");
|
||||
return data as { pages: SitePage[] };
|
||||
}
|
||||
|
||||
export async function fetchAdminSitePages(
|
||||
cookieHeader?: string
|
||||
): Promise<{ pages: SitePage[] }> {
|
||||
const res = await fetch(`${API_BASE}/api/admin/pages`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取单页失败");
|
||||
return data as { pages: SitePage[] };
|
||||
}
|
||||
|
||||
async function saveSitePage(
|
||||
method: "POST" | "PUT",
|
||||
url: string,
|
||||
input: SitePageInput
|
||||
): Promise<{ page: SitePage }> {
|
||||
const res = await fetchWithRefresh(url, {
|
||||
method,
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "保存页面失败");
|
||||
return data as { page: SitePage };
|
||||
}
|
||||
|
||||
export function apiAdminCreateSitePage(input: SitePageInput) {
|
||||
return saveSitePage("POST", "/api/admin/pages", input);
|
||||
}
|
||||
|
||||
export function apiAdminUpdateSitePage(id: number, input: SitePageInput) {
|
||||
return saveSitePage("PUT", `/api/admin/pages/${id}`, input);
|
||||
}
|
||||
|
||||
export async function apiAdminDeleteSitePage(id: number): Promise<void> {
|
||||
const res = await fetchWithRefresh(`/api/admin/pages/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "删除页面失败");
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiGetAdminSettings(): Promise<
|
||||
PublicSettings & { timeline_git_import?: string }
|
||||
> {
|
||||
const res = await fetchWithRefresh("/api/admin/settings", { headers: clientHeaders() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取设置失败");
|
||||
return {
|
||||
...normalizePublicSettings(data as Partial<PublicSettings>),
|
||||
timeline_git_import:
|
||||
typeof data.timeline_git_import === "string" ? data.timeline_git_import : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 用户管理(管理员) =====
|
||||
|
||||
// 后台用户列表项:email/最近活跃/登录 IP 仅管理员接口返回
|
||||
@@ -1901,6 +2067,7 @@ export type UpdateSiteSettingsBody = {
|
||||
attachment_max_mb?: number;
|
||||
attachment_max_count?: number;
|
||||
image_max_mb?: number;
|
||||
timeline_git_import?: string;
|
||||
};
|
||||
|
||||
// 更新站点设置(字段可选);accent 传空串恢复默认主题色
|
||||
|
||||
Reference in New Issue
Block a user