import { useCallback, useEffect, useRef } from 'react'; /** HTML 转义:& < > " ' 全转义 */ export function escapeHtml(str) { return String(str == null ? '' : str).replace(/[&<>"']/g, (ch) => { switch (ch) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return ch; } }); } /** 分页接口返回归一化:兼容裸数组与 { posts|list|items, total, page, pageSize } 两种形态 */ export function normalizePagedList(data) { if (Array.isArray(data)) return { items: data, total: data.length, page: 1, pageSize: data.length }; if (data && typeof data === 'object') { const items = data.posts || data.list || data.items || []; return { items, total: typeof data.total === 'number' ? data.total : items.length, page: data.page || 1, pageSize: typeof data.pageSize === 'number' ? data.pageSize : (items.length || 20), }; } return { items: [], total: 0, page: 1, pageSize: 20 }; } /** 格式化时间为 YYYY-MM-DD HH:mm */ export function formatDate(input) { const d = input instanceof Date ? input : new Date(input); if (isNaN(d.getTime())) return ''; const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } /** 简单节流:fn 在 wait 毫秒内最多执行一次 */ export function throttle(fn, wait = 200) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= wait) { last = now; return fn.apply(this, args); } }; } /** 底部 toast(依赖 index.html 中的 #snackbar,类名沿用 v1) */ export function showSnackbar(msg) { const el = document.getElementById('snackbar'); if (!el) return; el.textContent = msg; el.classList.remove('hide'); el.classList.add('show'); clearTimeout(el._timer); el._timer = setTimeout(() => { el.classList.add('hide'); setTimeout(() => el.classList.remove('show', 'hide'), 300); }, 2500); } /** 弹窗焦点管理(B3):把焦点移到容器内首个可聚焦元素(input/select/textarea/button) */ export function focusDialog(container) { if (!container) return; const el = container.querySelector('input, select, textarea, button, [tabindex]:not([tabindex="-1"])'); if (el && typeof el.focus === 'function') el.focus(); } /** * 弹窗键盘/焦点管理(B3): * - 打开时记录触发元素,并把焦点移到弹窗内首个可聚焦元素 * - Esc 关闭弹窗 * - 关闭后焦点还给触发元素 * 返回 { dialogRef, onKeyDown }:dialogRef 绑到弹窗容器(.dialog-overlay),onKeyDown 绑其键盘事件。 */ export function useDialog(open, onClose) { const dialogRef = useRef(null); const triggerRef = useRef(null); useEffect(() => { if (open) { triggerRef.current = document.activeElement; focusDialog(dialogRef.current); } else if (triggerRef.current && typeof triggerRef.current.focus === 'function' && document.body.contains(triggerRef.current)) { triggerRef.current.focus(); triggerRef.current = null; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const onKeyDown = useCallback((e) => { if (e.key === 'Escape') { e.stopPropagation(); if (onClose) onClose(); } }, [onClose]); return { dialogRef, onKeyDown }; }