/* global React */
const { useState: useT, useEffect: useTE, useRef: useTR } = React;
/* =====================================================
Global Toast host + showToast()
usage: window.showToast({ type: 'success', msg: '奏折已呈' })
===================================================== */
const ToastIcons = {
success: (
),
error: (
),
info: (
),
warning: (
),
};
function ToastHost() {
const [items, setItems] = useT([]);
useTE(() => {
window.showToast = ({ type = 'success', msg = '', duration = 3000 } = {}) => {
const id = Date.now() + Math.random();
setItems(x => [...x, { id, type, msg }]);
setTimeout(() => setItems(x => x.map(i => i.id === id ? { ...i, leaving: true } : i)), duration);
setTimeout(() => setItems(x => x.filter(i => i.id !== id)), duration + 220);
};
}, []);
return (
{items.map(t => (
{ToastIcons[t.type]}
{t.msg}
))}
);
}
/* =====================================================
Confirm Modal (controlled)
===================================================== */
function ConfirmModal({ open, title = '朕问你', message, confirmText = '确认', cancelText = '朕再想想', danger = false, requireTyping, onConfirm, onCancel }) {
const [typed, setTyped] = useT('');
useTE(() => { if (open) setTyped(''); }, [open]);
if (!open) return null;
const canConfirm = !requireTyping || typed.trim() === requireTyping;
return (
e.stopPropagation()}>
{danger && (
)}
{danger ? 'DANGER · 慎之' : 'CONFIRM · 请示'}
{title}
{message}
{requireTyping && (
setTyped(e.target.value)}/>
)}
);
}
/* =====================================================
Info Modal — "朕曰"
===================================================== */
function InfoModal({ open, title = '朕曰', children, onClose }) {
if (!open) return null;
return (
e.stopPropagation()}>
{title}
{children}
);
}
/* =====================================================
Loading helpers
===================================================== */
function SealSpinner({ size = 48, text }) {
return (
);
}
function FullscreenLoading({ text = '稍候,待朕过目……' }) {
return (
);
}
function PageTopBar() {
return ;
}
function LetterSkeleton() {
return (
);
}
/* =====================================================
Empty State
===================================================== */
function EmptyState({ icon, title, subtitle, cta, onCta }) {
return (
{icon}
{title}
{subtitle}
{cta &&
}
);
}
/* =====================================================
Form Field (with states)
===================================================== */
function TextField({ label, required, state = 'default', hint, error, ...rest }) {
const cls = `bx-field ${state === 'success' ? 'bx-field--success' : ''} ${state === 'error' ? 'bx-field--error' : ''}`;
return (
);
}
/* =====================================================
违规词提示条 · 写信 / 评论共用
blocked: 必须修改才能提交的违规词 [{word,category}]
warnings: 仅提醒、不阻止提交的敏感词 [{word,category}]
===================================================== */
function SensitiveNotice({ blocked = [], warnings = [] }) {
if (blocked.length === 0 && warnings.length === 0) return null;
return (
{blocked.length > 0 && (
!
检测到 {blocked.length} 个违规词,须修改后方可呈递
{blocked.map((b, i) => (
{b.word}
))}
这些词可能触发抖音限流/封禁,请删去或换种说法。如要留联系方式,可在被陛下选中后私下沟通。
)}
{warnings.length > 0 && (
·
另有 {warnings.length} 个敏感词,建议斟酌(不影响呈递)
{warnings.map((w, i) => (
{w.word}
))}
)}
);
}
Object.assign(window, { ToastHost, ConfirmModal, InfoModal, SealSpinner, FullscreenLoading, PageTopBar, LetterSkeleton, EmptyState, TextField, SensitiveNotice });