feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 04:00:14 +08:00
parent cda5fad2e5
commit 9a55c2f6f2
41 changed files with 5655 additions and 1810 deletions

View File

@@ -609,6 +609,8 @@ export interface PublicSettings {
anim_smooth_scroll: boolean;
/** 装饰入场子开关;缺省 true */
anim_chrome: boolean;
/** 帖内 Markdown 外链是否新标签打开;缺省 true */
post_link_new_tab: boolean;
}
const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
@@ -625,6 +627,7 @@ const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
anim_code_fold: true,
anim_smooth_scroll: true,
anim_chrome: true,
post_link_new_tab: true,
};
function normalizePublicSettings(data: Partial<PublicSettings> | null | undefined): PublicSettings {
@@ -653,6 +656,7 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
anim_code_fold: data?.anim_code_fold !== false,
anim_smooth_scroll: data?.anim_smooth_scroll !== false,
anim_chrome: data?.anim_chrome !== false,
post_link_new_tab: data?.post_link_new_tab !== false,
};
}
@@ -869,6 +873,133 @@ export interface EconomyStats {
escrowed_points: number;
posts_by_type: Record<string, number>;
recent_ledger: PointLedgerItem[];
checkin_series?: { date: string; count: number }[];
ledger_by_reason?: { key: string; label: string; count: number }[];
delta_series?: { date: string; granted: number; spent: number }[];
net_delta_7d?: number;
}
export interface AnalyticsDayCount {
date: string;
count: number;
}
export interface AnalyticsVisitDay {
date: string;
pv: number;
uv: number;
}
export interface AnalyticsNamedCount {
key: string;
label: string;
count: number;
}
export interface AnalyticsTopViewPost {
id: number;
title: string;
view_count: number;
board_id: number;
}
export interface AnalyticsOverview {
range_days: number;
today_pv: number;
today_uv: number;
visits: AnalyticsVisitDay[];
posts_series: AnalyticsDayCount[];
comments_series: AnalyticsDayCount[];
registers_series: AnalyticsDayCount[];
boards: AnalyticsNamedCount[];
post_types: AnalyticsNamedCount[];
top_viewed: AnalyticsTopViewPost[];
}
export async function fetchAdminAnalytics(
cookieHeader?: string,
range: "7d" | "30d" = "7d"
): Promise<AnalyticsOverview> {
const res = await fetch(
`${API_BASE}/api/admin/analytics/overview?range=${range}`,
ssrInit(cookieHeaders(cookieHeader))
);
if (!res.ok) throw new Error("failed");
return res.json();
}
export async function apiAdminAnalytics(range: "7d" | "30d" = "7d"): Promise<AnalyticsOverview> {
const res = await fetchWithRefresh(`/api/admin/analytics/overview?range=${range}`, {
method: "GET",
headers: clientHeaders(),
});
if (!res.ok) throw new Error("failed");
return res.json();
}
export async function apiTelemetryPageView(): Promise<void> {
try {
await fetchWithRefresh("/api/telemetry/pageview", {
method: "POST",
headers: clientHeaders(),
});
} catch {
/* 埋点失败不影响浏览 */
}
}
export type AdminChatRoomType = "group" | "direct";
export interface AdminChatParticipant {
id: number;
name: string;
avatar: string;
}
export interface AdminChatMessage {
id: number;
room_id: number;
room_name: string;
room_type?: AdminChatRoomType;
is_default: boolean;
sender_id: number;
sender_name: string;
sender_avatar: string;
participants?: AdminChatParticipant[];
content: string;
recalled_at?: string | null;
created_at: string;
}
export async function apiAdminChatMessages(
page = 1,
q = "",
roomType: AdminChatRoomType = "group"
): Promise<{
messages: AdminChatMessage[];
total: number;
page: number;
}> {
const params = new URLSearchParams({
page: String(page),
room_type: roomType,
});
if (q.trim()) params.set("q", q.trim());
const res = await fetchWithRefresh(`/api/admin/chat/messages?${params}`, {
method: "GET",
headers: clientHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "获取聊天消息失败");
return data;
}
export async function apiAdminRecallChatMessage(roomId: number, mid: number) {
const res = await fetchWithRefresh(`/api/admin/chat/rooms/${roomId}/messages/${mid}`, {
method: "DELETE",
headers: clientHeaders(),
});
return res.json();
}
export async function apiAdminEconomyStats(): Promise<EconomyStats & { error?: string }> {
@@ -1325,6 +1456,7 @@ export interface AdminUser {
role: string; // user | board_admin | admin | super_admin | owner
board_ids: number[]; // 板块管理员的授权板块
banned: boolean;
points?: number;
post_count: number;
comment_count: number;
created_at: string;
@@ -1619,6 +1751,7 @@ export type UpdateSiteSettingsBody = {
anim_code_fold?: boolean;
anim_smooth_scroll?: boolean;
anim_chrome?: boolean;
post_link_new_tab?: boolean;
};
// 更新站点设置(字段可选);accent 传空串恢复默认主题色

View File

@@ -52,6 +52,7 @@ export interface RtSettingsData {
anim_code_fold?: boolean;
anim_smooth_scroll?: boolean;
anim_chrome?: boolean;
post_link_new_tab?: boolean;
}
// 群聊新消息:data 为落库的 ChatMessage(含 sender)