新增管理员手动发送站内信(三语富文本),支持发送记录查看与删除;修复全站「从媒体库选择」因分类过滤导致列表为空的问题;玩家端支持渲染管理员自定义消息;并优化后台列表页双层卡片布局。
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
/** 去除 HTML 标签,用于列表摘要与跑马灯纯文本 */
|
|
export function stripHtml(html: string): string {
|
|
if (!html) return '';
|
|
if (!/[<>]/.test(html)) return html.trim();
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
return (doc.body.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
/** 判断富文本是否实质为空 */
|
|
export function isHtmlEmpty(html: string): boolean {
|
|
return !stripHtml(html);
|
|
}
|
|
|
|
const ALLOWED_TAGS = new Set([
|
|
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li',
|
|
'img', 'a', 'h2', 'h3', 'blockquote', 'div', 'span',
|
|
]);
|
|
|
|
function sanitizeNode(node: Node): Node | null {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return node.cloneNode(false);
|
|
}
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
|
|
const el = node as HTMLElement;
|
|
const tag = el.tagName.toLowerCase();
|
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
const frag = document.createDocumentFragment();
|
|
for (const child of Array.from(el.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) frag.appendChild(safe);
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
const out = document.createElement(tag);
|
|
if (tag === 'img') {
|
|
const src = el.getAttribute('src')?.trim();
|
|
if (!src || /^javascript:/i.test(src)) return null;
|
|
out.setAttribute('src', src);
|
|
const alt = el.getAttribute('alt');
|
|
if (alt) out.setAttribute('alt', alt);
|
|
return out;
|
|
}
|
|
if (tag === 'a') {
|
|
const href = el.getAttribute('href')?.trim();
|
|
if (!href || /^javascript:/i.test(href)) return null;
|
|
out.setAttribute('href', href);
|
|
out.setAttribute('target', '_blank');
|
|
out.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
for (const child of Array.from(el.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) out.appendChild(safe);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 富文本 HTML 白名单净化(公告/站内信预览) */
|
|
export function sanitizeAnnouncementHtml(html: string): string {
|
|
if (!html?.trim()) return '';
|
|
if (!/[<>]/.test(html)) return html;
|
|
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
const container = document.createElement('div');
|
|
for (const child of Array.from(doc.body.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) container.appendChild(safe);
|
|
}
|
|
return container.innerHTML;
|
|
}
|