Files
jiang13-bbs/frontend/components/sidebar/CustomWidgetBlock.tsx
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

55 lines
1.9 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.
"use client";
import { useEffect, useLayoutEffect, useRef } from "react";
import type { SidebarWidgetPublic, User } from "@/lib/api";
import { mountWidget } from "@/lib/widgetRuntime";
import { ensureWidgetApi } from "@/lib/widgetApi";
// 站长自定义工具块:所有工具统一在 Shadow DOM 内渲染(attachShadow 是客户端能力,
// 因此服务端只输出空容器,HTML/CSS/JS 全部在客户端 effect 中注入,无水合不一致)。
// - CSS 注入 shadow 内,不污染全站,也不被全站样式污染;
// - JS 以函数形式执行,接收 (root, shadow, toolId, registerCleanup),
// DOM 查找从 root 出发,同页多个同款工具互不干扰;
// - unmount 时统一执行 cleanup 并清空 shadow。
// 站长可信内容,不做消毒/沙箱。
export default function CustomWidgetBlock({
widget,
viewer,
}: {
widget: SidebarWidgetPublic;
viewer?: User | null;
}) {
const hostRef = useRef<HTMLElement>(null);
// 服务端用 useEffect 避免告警,客户端用 layout effect 首帧前完成注入,避免闪烁
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
// 两个 effect 同类型、按声明顺序执行:先建立站点接口,再挂载工具,
// 保证用户脚本首帧同步读取 window.J13 时接口已就绪。
useIsomorphicLayoutEffect(() => {
ensureWidgetApi(viewer);
}, [viewer]);
useIsomorphicLayoutEffect(() => {
const host = hostRef.current;
if (!host) return;
const handle = mountWidget(host, {
id: widget.id,
html: widget.html,
css: widget.css,
js: widget.js,
});
return () => handle.unmount();
}, [widget.id, widget.html, widget.css, widget.js]);
return (
<section
ref={hostRef}
className="j13-widget-body"
data-widget-id={widget.id}
data-widget-name={widget.name}
/>
);
}