feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
91
frontend/lib/widgetApi.ts
Normal file
91
frontend/lib/widgetApi.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// 自定义侧栏工具的站点开放接口:
|
||||
// - 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;
|
||||
}
|
||||
Reference in New Issue
Block a user