feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
This commit is contained in:
@@ -129,6 +129,10 @@ function shouldShowTip(host: Element, tip: string): boolean {
|
||||
/**
|
||||
* 全站悬停即显提示:监听 [data-tip],替代浏览器原生 title 的延迟气泡。
|
||||
* 挂在 root layout 即可,业务侧写 data-tip="文案" 并保留 aria-label。
|
||||
*
|
||||
* 移动端策略:仅在 (hover: hover) and (pointer: fine) 设备上响应真实鼠标
|
||||
* 指针与键盘焦点。触屏 tap 既派发合成鼠标事件又会抢占焦点,若照常弹出,
|
||||
* 会出现「一点击就弹、只能点别处关闭」的体验,因此触屏一律不触发。
|
||||
*/
|
||||
export default function TipHost() {
|
||||
const [tip, setTip] = useState<TipState>(null);
|
||||
@@ -193,6 +197,15 @@ export default function TipHost() {
|
||||
}, delay);
|
||||
};
|
||||
|
||||
// 仅在具备真实悬停能力的设备(鼠标/触控板)上启用;纯触屏直接不参与
|
||||
const hoverMq = window.matchMedia("(hover: hover) and (pointer: fine)");
|
||||
let canHover = hoverMq.matches;
|
||||
// 只在键盘导航期间允许聚焦气泡(:focus-visible 语义),点按产生的焦点不弹
|
||||
let keyboardActive = false;
|
||||
// 触摸后的短暂窗口,兜底忽略浏览器派发的合成鼠标事件
|
||||
let lastTouchAt = 0;
|
||||
const TOUCH_GUARD_MS = 500;
|
||||
|
||||
const onOver = (e: Event) => {
|
||||
const t = e.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
@@ -204,8 +217,19 @@ export default function TipHost() {
|
||||
tryShow(host);
|
||||
};
|
||||
|
||||
const onPointerOver = (e: PointerEvent) => {
|
||||
if (!canHover || e.pointerType !== "mouse") return;
|
||||
if (Date.now() - lastTouchAt < TOUCH_GUARD_MS) return;
|
||||
onOver(e);
|
||||
};
|
||||
|
||||
const onFocusIn = (e: FocusEvent) => {
|
||||
if (!canHover || !keyboardActive) return;
|
||||
onOver(e);
|
||||
};
|
||||
|
||||
const onOut = (e: Event) => {
|
||||
if (!(e instanceof MouseEvent) && !(e instanceof FocusEvent)) return;
|
||||
if (!(e instanceof PointerEvent) && !(e instanceof FocusEvent)) return;
|
||||
const related = "relatedTarget" in e ? e.relatedTarget : null;
|
||||
if (
|
||||
related instanceof Node &&
|
||||
@@ -217,25 +241,52 @@ export default function TipHost() {
|
||||
hide();
|
||||
};
|
||||
|
||||
const onKeyDown = () => {
|
||||
keyboardActive = true;
|
||||
};
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (e.pointerType !== "keyboard") keyboardActive = false;
|
||||
};
|
||||
|
||||
const onTouchStart = () => {
|
||||
lastTouchAt = Date.now();
|
||||
keyboardActive = false;
|
||||
hide();
|
||||
};
|
||||
|
||||
const onHoverMqChange = (e: MediaQueryListEvent) => {
|
||||
canHover = e.matches;
|
||||
if (!canHover) hide();
|
||||
};
|
||||
|
||||
const onScrollOrResize = () => {
|
||||
if (!activeEl.current) return;
|
||||
tryShow(activeEl.current);
|
||||
};
|
||||
|
||||
// mouseover 冒泡,便于 closest;立即显示,无延时
|
||||
document.addEventListener("mouseover", onOver);
|
||||
document.addEventListener("mouseout", onOut);
|
||||
document.addEventListener("focusin", onOver);
|
||||
// pointerover 冒泡,便于 closest;仅响应真实鼠标指针,触屏/手写笔不弹
|
||||
document.addEventListener("pointerover", onPointerOver);
|
||||
document.addEventListener("pointerout", onOut);
|
||||
document.addEventListener("focusin", onFocusIn);
|
||||
document.addEventListener("focusout", onOut);
|
||||
document.addEventListener("keydown", onKeyDown, true);
|
||||
document.addEventListener("pointerdown", onPointerDown, true);
|
||||
document.addEventListener("touchstart", onTouchStart, true);
|
||||
hoverMq.addEventListener("change", onHoverMqChange);
|
||||
window.addEventListener("scroll", onScrollOrResize, true);
|
||||
window.addEventListener("resize", onScrollOrResize);
|
||||
|
||||
return () => {
|
||||
clearPending();
|
||||
document.removeEventListener("mouseover", onOver);
|
||||
document.removeEventListener("mouseout", onOut);
|
||||
document.removeEventListener("focusin", onOver);
|
||||
document.removeEventListener("pointerover", onPointerOver);
|
||||
document.removeEventListener("pointerout", onOut);
|
||||
document.removeEventListener("focusin", onFocusIn);
|
||||
document.removeEventListener("focusout", onOut);
|
||||
document.removeEventListener("keydown", onKeyDown, true);
|
||||
document.removeEventListener("pointerdown", onPointerDown, true);
|
||||
document.removeEventListener("touchstart", onTouchStart, true);
|
||||
hoverMq.removeEventListener("change", onHoverMqChange);
|
||||
window.removeEventListener("scroll", onScrollOrResize, true);
|
||||
window.removeEventListener("resize", onScrollOrResize);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user