Files
rainblogweb/frontend/src/lib/utils.js
T

104 lines
3.4 KiB
JavaScript
Raw 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.
import { useCallback, useEffect, useRef } from 'react';
/** HTML 转义:& < > " ' 全转义 */
export function escapeHtml(str) {
return String(str == null ? '' : str).replace(/[&<>"']/g, (ch) => {
switch (ch) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case "'": return '&#39;';
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 };
}