feat: add ads/sponsor/captcha modules and polish home & chat UI

This commit is contained in:
2026-09-23 04:39:58 +08:00
parent 96314893bb
commit c1f6a6636b
27 changed files with 4278 additions and 121 deletions

View File

@@ -213,6 +213,30 @@ export interface OverviewResponse {
sidebar_pages?: SidebarPageItem[];
new_users: NewUser[];
checkin?: CheckinStatus | null;
ads?: PublicAdItem[];
ads_panel_title?: string;
ads_enabled?: boolean;
sponsors?: PublicSponsorItem[];
sponsors_panel_title?: string;
sponsors_enabled?: boolean;
}
export interface PublicAdItem {
id: number;
kind: "image" | "text" | string;
link_url: string;
image_url?: string;
title?: string;
text_color?: string;
bg_color?: string;
}
export interface PublicSponsorItem {
id: string;
name: string;
logo_url: string;
link_url?: string;
hover_text?: string;
}
/** 板块右栏:管理员公开资料 */
@@ -3031,3 +3055,164 @@ export async function apiOperations<T = Record<string, unknown>>(path: string, m
if (!res.ok) throw new Error(data.error || "操作失败,请重试");
return data as T;
}
/* ---------- 自助推广 / 赞助商 ---------- */
export interface AdDurationOption {
days: number;
label: string;
price_hint: string;
}
export interface AdPaymentOption {
id: string;
name: string;
kind: "alipay" | "wechat" | "other" | string;
qr_url: string;
hint: string;
enabled: boolean;
}
export interface AdConfig {
enabled: boolean;
panel_title: string;
durations: AdDurationOption[];
payments: AdPaymentOption[];
}
export interface SponsorItem {
id: string;
name: string;
logo_url: string;
link_url: string;
hover_text: string;
enabled: boolean;
sort_order: number;
}
export interface SponsorsConfig {
enabled: boolean;
panel_title: string;
items: SponsorItem[];
}
export interface AdRecord {
id: number;
kind: string;
status: string;
contact_email: string;
link_url: string;
image_url?: string;
title?: string;
text_color?: string;
bg_color?: string;
duration_days: number;
payment_id: string;
buyer_note?: string;
starts_at?: string | null;
ends_at?: string | null;
reject_reason?: string;
sort_order: number;
created_at: string;
reviewed_at?: string | null;
}
export async function apiFetchCaptcha(): Promise<{ id: string; image: string }> {
const res = await fetch("/api/captcha", { credentials: "include", cache: "no-store" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((data as { error?: string }).error || "获取验证码失败");
return data as { id: string; image: string };
}
export async function apiAdsPublicConfig(): Promise<AdConfig> {
const res = await fetch("/api/ads/config", { credentials: "include", cache: "no-store" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((data as { error?: string }).error || "加载广告配置失败");
return data as AdConfig;
}
export async function apiSubmitAd(body: Record<string, unknown>): Promise<{ ad: AdRecord; message: string }> {
const res = await fetch("/api/ads", {
method: "POST",
credentials: "include",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((data as { error?: string }).error || "提交失败");
return data as { ad: AdRecord; message: string };
}
export async function apiAdminSaveAdsConfig(cfg: AdConfig): Promise<AdConfig> {
return apiOperations<AdConfig>("/api/admin/ads/config", "PUT", cfg);
}
export async function apiAdminListAds(
status = "",
page = 1
): Promise<{ ads: AdRecord[]; total: number }> {
const q = new URLSearchParams({ page: String(page), size: "30" });
if (status) q.set("status", status);
return apiOperations(`/api/admin/ads?${q}`);
}
export async function fetchAdminAds(
cookie?: string,
status = ""
): Promise<{ ads: AdRecord[]; total: number }> {
const q = new URLSearchParams({ page: "1", size: "50" });
if (status) q.set("status", status);
const res = await fetch(`${API_BASE}/api/admin/ads?${q}`, {
...ssrInit(cookie ? { Cookie: cookie } : undefined),
credentials: "include",
});
if (!res.ok) throw new Error("加载广告列表失败");
return res.json();
}
export async function fetchAdminAdsConfig(cookie?: string): Promise<AdConfig> {
const res = await fetch(`${API_BASE}/api/admin/ads/config`, {
...ssrInit(cookie ? { Cookie: cookie } : undefined),
credentials: "include",
});
if (!res.ok) throw new Error("加载广告配置失败");
return res.json();
}
export async function apiAdminApproveAd(id: number) {
return apiOperations(`/api/admin/ads/${id}/approve`, "PUT");
}
export async function apiAdminRejectAd(id: number, reason = "") {
return apiOperations(`/api/admin/ads/${id}/reject`, "PUT", { reason });
}
export async function apiAdminDeleteAd(id: number) {
return apiOperations(`/api/admin/ads/${id}`, "DELETE");
}
export async function apiAdminUploadAdQR(file: File): Promise<{ url: string }> {
const fd = new FormData();
fd.append("file", file);
const res = await fetchWithRefresh("/api/admin/upload/ad-qr", {
method: "POST",
headers: clientHeaders(),
body: fd,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((data as { error?: string }).error || "上传失败");
return data as { url: string };
}
export async function apiAdminSaveSponsorsConfig(cfg: SponsorsConfig): Promise<SponsorsConfig> {
return apiOperations<SponsorsConfig>("/api/admin/sponsors/config", "PUT", cfg);
}
export async function fetchAdminSponsorsConfig(cookie?: string): Promise<SponsorsConfig> {
const res = await fetch(`${API_BASE}/api/admin/sponsors/config`, {
...ssrInit(cookie ? { Cookie: cookie } : undefined),
credentials: "include",
});
if (!res.ok) throw new Error("加载赞助商配置失败");
return res.json();
}