feat(settings): 站点界面动画总开关与子开关

管理后台可关全站动画,并细调代码折叠、平滑滚动、装饰入场;统一 lib/motion,尊重系统减少动态效果。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 06:59:26 +08:00
parent c6df1b3b7e
commit 2b27703e6c
14 changed files with 511 additions and 46 deletions

View File

@@ -552,6 +552,14 @@ export interface PublicSettings {
code_block_auto_fold: boolean;
/** 折叠阈值行数(超过则折叠;0=无预览);缺省 5 */
code_block_fold_lines: number;
/** 界面动画总开关;缺省 true */
ui_animations: boolean;
/** 代码折叠动画子开关;缺省 true */
anim_code_fold: boolean;
/** 平滑滚动子开关;缺省 true */
anim_smooth_scroll: boolean;
/** 装饰入场子开关;缺省 true */
anim_chrome: boolean;
}
const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
@@ -563,6 +571,10 @@ const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
post_cooldown_hours: 24,
code_block_auto_fold: true,
code_block_fold_lines: 5,
ui_animations: true,
anim_code_fold: true,
anim_smooth_scroll: true,
anim_chrome: true,
};
function normalizePublicSettings(data: Partial<PublicSettings> | null | undefined): PublicSettings {
@@ -586,6 +598,10 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
data.code_block_fold_lines <= 100
? Math.round(data.code_block_fold_lines)
: DEFAULT_PUBLIC_SETTINGS.code_block_fold_lines,
ui_animations: data?.ui_animations !== false,
anim_code_fold: data?.anim_code_fold !== false,
anim_smooth_scroll: data?.anim_smooth_scroll !== false,
anim_chrome: data?.anim_chrome !== false,
};
}
@@ -931,10 +947,30 @@ export async function apiDrawLottery(postId: string) {
return res.json();
}
export async function apiDeletePost(postId: string) {
/** 管理删帖类型(与后端 model.PostDeleteType* 一致) */
export const POST_DELETE_TYPES = [
{ value: "spam", label: "垃圾广告" },
{ value: "violation", label: "违规内容" },
{ value: "off_topic", label: "灌水跑题" },
{ value: "duplicate", label: "重复内容" },
{ value: "other", label: "其他" },
] as const;
export function postDeleteTypeLabel(t?: string): string {
return POST_DELETE_TYPES.find((x) => x.value === t)?.label || t || "";
}
export async function apiDeletePost(
postId: string,
opts?: { delete_type?: string; delete_reason?: string }
) {
const res = await fetchWithRefresh(`/api/posts/${postId}`, {
method: "DELETE",
headers: clientHeaders(),
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({
delete_type: opts?.delete_type || "",
delete_reason: opts?.delete_reason || "",
}),
});
return res.json();
}
@@ -1073,6 +1109,15 @@ export async function apiDeleteComment(postId: string, commentId: number) {
return res.json();
}
/** 版主硬删评论及子树(不占位) */
export async function apiPurgeComment(postId: string, commentId: number) {
const res = await fetchWithRefresh(`/api/posts/${postId}/comments/${commentId}/purge`, {
method: "DELETE",
headers: clientHeaders(),
});
return res.json();
}
// ===== 通知 =====
export async function apiFetchNotifications(page = 1): Promise<NotificationsResponse> {
@@ -1488,6 +1533,10 @@ export type UpdateSiteSettingsBody = {
post_cooldown_hours?: number;
code_block_auto_fold?: boolean;
code_block_fold_lines?: number;
ui_animations?: boolean;
anim_code_fold?: boolean;
anim_smooth_scroll?: boolean;
anim_chrome?: boolean;
};
// 更新站点设置(字段可选);accent 传空串恢复默认主题色
@@ -1657,16 +1706,26 @@ export async function apiAdminContentComments(
return data;
}
async function contentAction(url: string, method: string): Promise<void> {
const res = await fetchWithRefresh(url, { method, headers: clientHeaders() });
async function contentAction(
url: string,
method: string,
body?: Record<string, unknown>
): Promise<void> {
const res = await fetchWithRefresh(url, {
method,
headers: clientHeaders(body ? { "Content-Type": "application/json" } : undefined),
body: body ? JSON.stringify(body) : undefined,
});
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 apiAdminSoftDeletePost = (
id: number,
opts: { delete_type: string; delete_reason: string }
) => contentAction(`/api/admin/content/posts/${id}`, "DELETE", opts);
export const apiAdminRestorePost = (id: number) =>
contentAction(`/api/admin/content/posts/${id}/restore`, "PUT");
export const apiAdminPurgePost = (id: number) =>