feat: add ads/sponsor/captcha modules and polish home & chat UI
This commit is contained in:
8
frontend/lib/ads.ts
Normal file
8
frontend/lib/ads.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/** 首页侧栏图片广告展示尺寸(与 .j13-ad-promo 宽一致:248 = 两列文字 + 中间 6px 间距) */
|
||||
export const AD_IMAGE_WIDTH = 248;
|
||||
export const AD_IMAGE_HEIGHT = 60;
|
||||
/** 单列文字槽宽度:(248 - 6) / 2 */
|
||||
export const AD_TEXT_COL_WIDTH = 121;
|
||||
/** 购买页建议文案 */
|
||||
export const AD_IMAGE_SIZE_LABEL = `${AD_IMAGE_WIDTH}×${AD_IMAGE_HEIGHT}`;
|
||||
export const AD_IMAGE_SIZE_LABEL_2X = `${AD_IMAGE_WIDTH * 2}×${AD_IMAGE_HEIGHT * 2}`;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user