Files
jiang13-bbs/frontend/lib/widgetApi.ts
freefire 7b327bc8cc feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
2026-09-24 18:53:45 +08:00

92 lines
2.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 自定义侧栏工具的站点开放接口:
// - CSS:工具内直接使用站点主题变量(var(--accent) 等),随明暗主题与
// 后台自定义主题色自动变化(CSS 变量沿 DOM 继承,无需任何 JS);
// - JS:window.J13 提供当前用户、主题读取/监听、站点概览懒加载。
// 本模块只在浏览器端(CustomWidgetBlock 的 effect 内)执行。
import type { OverviewResponse, User } from "@/lib/api";
export interface WidgetViewer {
id: number;
username: string;
nickname: string;
role: string;
avatar: string | null;
}
export interface WidgetApi {
version: 1;
/** 当前登录用户(未登录为 null);每次工具挂载时刷新 */
user: WidgetViewer | null;
theme: {
get: () => "light" | "dark";
/** 监听明暗切换,返回取消监听函数 */
onChange: (cb: (theme: "light" | "dark") => void) => () => void;
};
/** 站点公开概览统计(结果模块级缓存,多个工具共享一次请求) */
getOverview: () => Promise<OverviewResponse | null>;
}
declare global {
interface Window {
J13?: WidgetApi;
}
}
function toViewer(user: User | null | undefined): WidgetViewer | null {
if (!user) return null;
return {
id: user.id,
username: user.username,
nickname: user.nickname,
role: user.role,
avatar: user.avatar || null,
};
}
// 概览请求全站只发一次(无论多少个工具、多少次挂载)
let overviewPromise: Promise<OverviewResponse | null> | null = null;
function loadOverview(): Promise<OverviewResponse | null> {
if (!overviewPromise) {
overviewPromise = fetch("/api/overview", { credentials: "include" })
.then((res) => (res.ok ? res.json() : null))
.catch(() => null);
}
return overviewPromise;
}
/**
* 幂等初始化 window.J13 并刷新当前用户信息。
* 必须在注入站长 JS 之前调用;多个工具共享同一个实例。
*/
export function ensureWidgetApi(viewer: User | null | undefined): WidgetApi {
const root = document.documentElement;
const readTheme = (): "light" | "dark" =>
root.classList.contains("dark") ? "dark" : "light";
if (!window.J13) {
const listeners = new Set<(theme: "light" | "dark") => void>();
// 主题切换 = html 元素 .dark 类的增删,观察其变化广播给工具
const observer = new MutationObserver(() => {
const theme = readTheme();
listeners.forEach((cb) => cb(theme));
});
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
window.J13 = {
version: 1,
user: null,
theme: {
get: readTheme,
onChange(cb) {
listeners.add(cb);
return () => listeners.delete(cb);
},
},
getOverview: loadOverview,
};
}
window.J13.user = toViewer(viewer);
return window.J13;
}