104 lines
3.4 KiB
JavaScript
104 lines
3.4 KiB
JavaScript
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 };
|
||
}
|