- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
// 自定义侧栏工具的站点开放接口:
|
||
// - 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;
|
||
}
|