feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -533,20 +533,46 @@ export async function fetchBoards(): Promise<{ boards: BoardWithCount[] }> {
|
||||
export interface PublicSettings {
|
||||
accent: string;
|
||||
trust_reviewed_publish: boolean;
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
allow_register: boolean;
|
||||
post_cooldown_hours: number;
|
||||
}
|
||||
|
||||
const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
|
||||
accent: "",
|
||||
trust_reviewed_publish: true,
|
||||
site_name: "姜十三论坛",
|
||||
site_description: "姜十三论坛 - 技术分享与讨论社区",
|
||||
allow_register: true,
|
||||
post_cooldown_hours: 24,
|
||||
};
|
||||
|
||||
function normalizePublicSettings(data: Partial<PublicSettings> | null | undefined): PublicSettings {
|
||||
return {
|
||||
accent: data?.accent ?? "",
|
||||
trust_reviewed_publish: data?.trust_reviewed_publish !== false,
|
||||
site_name: (data?.site_name || "").trim() || DEFAULT_PUBLIC_SETTINGS.site_name,
|
||||
site_description:
|
||||
data?.site_description !== undefined
|
||||
? String(data.site_description).trim()
|
||||
: DEFAULT_PUBLIC_SETTINGS.site_description,
|
||||
allow_register: data?.allow_register !== false,
|
||||
post_cooldown_hours:
|
||||
typeof data?.post_cooldown_hours === "number" && data.post_cooldown_hours >= 0
|
||||
? data.post_cooldown_hours
|
||||
: DEFAULT_PUBLIC_SETTINGS.post_cooldown_hours,
|
||||
};
|
||||
}
|
||||
|
||||
// SSR 读取公开站点设置(主题色注入用);后端不可用时降级为默认色,不阻塞渲染
|
||||
export async function fetchPublicSettings(): Promise<PublicSettings> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/settings`, ssrInit());
|
||||
if (!res.ok) return { accent: "", trust_reviewed_publish: true };
|
||||
const data = (await res.json()) as Partial<PublicSettings>;
|
||||
return {
|
||||
accent: data.accent ?? "",
|
||||
trust_reviewed_publish: data.trust_reviewed_publish !== false,
|
||||
};
|
||||
if (!res.ok) return { ...DEFAULT_PUBLIC_SETTINGS };
|
||||
return normalizePublicSettings((await res.json()) as Partial<PublicSettings>);
|
||||
} catch {
|
||||
return { accent: "", trust_reviewed_publish: true };
|
||||
return { ...DEFAULT_PUBLIC_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1182,10 +1208,13 @@ export interface LoginLogsResponse {
|
||||
}
|
||||
|
||||
export interface AdminUserSummary {
|
||||
online: number;
|
||||
in_cooldown: number;
|
||||
active_7d: number;
|
||||
failed_logins_24h: number;
|
||||
total: number;
|
||||
admins: number;
|
||||
banned: number;
|
||||
today_new: number;
|
||||
}
|
||||
|
||||
export interface AdminUsersResponse {
|
||||
@@ -1423,16 +1452,16 @@ export async function apiGetSettings(): Promise<PublicSettings> {
|
||||
// no-store:主题同步必须读到管理员刚保存的最新配色,不走浏览器缓存
|
||||
const res = await fetchWithRefresh("/api/settings", { method: "GET", cache: "no-store" });
|
||||
if (!res.ok) throw new Error("获取站点设置失败");
|
||||
const data = (await res.json()) as Partial<PublicSettings>;
|
||||
return {
|
||||
accent: data.accent ?? "",
|
||||
trust_reviewed_publish: data.trust_reviewed_publish !== false,
|
||||
};
|
||||
return normalizePublicSettings(await res.json());
|
||||
}
|
||||
|
||||
export type UpdateSiteSettingsBody = {
|
||||
accent?: string;
|
||||
trust_reviewed_publish?: boolean;
|
||||
site_name?: string;
|
||||
site_description?: string;
|
||||
allow_register?: boolean;
|
||||
post_cooldown_hours?: number;
|
||||
};
|
||||
|
||||
// 更新站点设置(字段可选);accent 传空串恢复默认主题色
|
||||
@@ -1444,10 +1473,7 @@ export async function apiUpdateSiteSettings(body: UpdateSiteSettingsBody): Promi
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "保存设置失败");
|
||||
return {
|
||||
accent: data.accent ?? "",
|
||||
trust_reviewed_publish: data.trust_reviewed_publish !== false,
|
||||
};
|
||||
return normalizePublicSettings(data as Partial<PublicSettings>);
|
||||
}
|
||||
|
||||
// ===== 板块(客户端,供角色授权弹窗勾选) =====
|
||||
@@ -1527,6 +1553,137 @@ export const apiAdminApproveComment = (id: number) =>
|
||||
export const apiAdminRejectComment = (id: number) =>
|
||||
moderationAction(`/api/admin/moderation/comments/${id}/reject`);
|
||||
|
||||
// ===== 内容库(帖子 / 评论 + 回收站) =====
|
||||
|
||||
export type AdminContentStatus = "live" | "pending" | "rejected" | "published" | "deleted";
|
||||
|
||||
export interface AdminContentCounts {
|
||||
posts_live: number;
|
||||
posts_pending: number;
|
||||
posts_rejected: number;
|
||||
posts_deleted: number;
|
||||
comments_live: number;
|
||||
comments_pending: number;
|
||||
comments_rejected: number;
|
||||
comments_deleted: number;
|
||||
}
|
||||
|
||||
export interface AdminContentPost {
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
deleted: boolean;
|
||||
deleted_at?: string;
|
||||
created_at: string;
|
||||
comment_count: number;
|
||||
board_id: number;
|
||||
board: Board;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface AdminContentComment {
|
||||
id: number;
|
||||
post_id: number;
|
||||
post_title: string;
|
||||
board_id: number;
|
||||
content: string;
|
||||
status: string;
|
||||
deleted: boolean;
|
||||
deleted_at?: string;
|
||||
created_at: string;
|
||||
board: Board;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export async function apiAdminContentCounts(): Promise<AdminContentCounts> {
|
||||
const res = await fetchWithRefresh("/api/admin/content/counts", { headers: clientHeaders() });
|
||||
if (!res.ok) throw new Error("获取内容统计失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiAdminContentPosts(
|
||||
page = 1,
|
||||
status: AdminContentStatus = "live",
|
||||
q = ""
|
||||
): Promise<{ posts: AdminContentPost[]; total: number; page: number }> {
|
||||
const params = new URLSearchParams({ page: String(page), status });
|
||||
if (q.trim()) params.set("q", q.trim());
|
||||
const res = await fetchWithRefresh(`/api/admin/content/posts?${params}`, {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取帖子列表失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function apiAdminContentComments(
|
||||
page = 1,
|
||||
status: AdminContentStatus = "live",
|
||||
q = ""
|
||||
): Promise<{ comments: AdminContentComment[]; total: number; page: number }> {
|
||||
const params = new URLSearchParams({ page: String(page), status });
|
||||
if (q.trim()) params.set("q", q.trim());
|
||||
const res = await fetchWithRefresh(`/api/admin/content/comments?${params}`, {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取评论列表失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
async function contentAction(url: string, method: string): Promise<void> {
|
||||
const res = await fetchWithRefresh(url, { method, headers: clientHeaders() });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
export const apiAdminSoftDeletePost = (id: number) =>
|
||||
contentAction(`/api/admin/content/posts/${id}`, "DELETE");
|
||||
export const apiAdminRestorePost = (id: number) =>
|
||||
contentAction(`/api/admin/content/posts/${id}/restore`, "PUT");
|
||||
export const apiAdminPurgePost = (id: number) =>
|
||||
contentAction(`/api/admin/content/posts/${id}/purge`, "DELETE");
|
||||
export const apiAdminSoftDeleteComment = (id: number) =>
|
||||
contentAction(`/api/admin/content/comments/${id}`, "DELETE");
|
||||
export const apiAdminRestoreComment = (id: number) =>
|
||||
contentAction(`/api/admin/content/comments/${id}/restore`, "PUT");
|
||||
export const apiAdminPurgeComment = (id: number) =>
|
||||
contentAction(`/api/admin/content/comments/${id}/purge`, "DELETE");
|
||||
|
||||
export async function fetchAdminContentCounts(
|
||||
cookieHeader?: string
|
||||
): Promise<AdminContentCounts> {
|
||||
const res = await fetch(`${API_BASE}/api/admin/content/counts`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
if (!res.ok) {
|
||||
return {
|
||||
posts_live: 0,
|
||||
posts_pending: 0,
|
||||
posts_rejected: 0,
|
||||
posts_deleted: 0,
|
||||
comments_live: 0,
|
||||
comments_pending: 0,
|
||||
comments_rejected: 0,
|
||||
comments_deleted: 0,
|
||||
};
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchAdminContentPosts(
|
||||
page = 1,
|
||||
status: AdminContentStatus = "live",
|
||||
cookieHeader?: string
|
||||
): Promise<{ posts: AdminContentPost[]; total: number; page: number }> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/admin/content/posts?page=${page}&status=${status}`,
|
||||
ssrInit(cookieHeaders(cookieHeader))
|
||||
);
|
||||
if (!res.ok) throw new Error("获取帖子列表失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ===== 群聊(二期) =====
|
||||
|
||||
// 群聊房间基础模型(与后端 model.ChatRoom 对齐)
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface RtPresenceData {
|
||||
export interface RtSettingsData {
|
||||
accent: string;
|
||||
trust_reviewed_publish?: boolean;
|
||||
site_name?: string;
|
||||
site_description?: string;
|
||||
allow_register?: boolean;
|
||||
post_cooldown_hours?: number;
|
||||
}
|
||||
|
||||
// 群聊新消息:data 为落库的 ChatMessage(含 sender)
|
||||
|
||||
Reference in New Issue
Block a user