feat: 新增贴纸系统,重构评论编辑器与内容处理
1. 新增多平台贴纸库:贴吧、微博、知乎、小红书、抖音、B站共500+贴纸,搭配颜文字贴纸 2. 实现贴纸选择器面板,支持分类浏览、键盘导航和懒加载 3. 重构评论内容渲染:替换旧的@提及高亮逻辑,新增HTML净化与双向兼容 4. 重构评论编辑器为Tiptap富文本版本,支持表情插入、格式编辑 5. 优化评论编辑体验,修复滚动位置保留问题,调整评论区UI样式 6. 移除旧版EmojiPicker工具类,新增贴纸数据管理模块
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { POST_CONTENT_PURIFY_CONFIG } from './postContent';
|
||||
|
||||
/** 转义 HTML 并保留换行 */
|
||||
function escapeWithBreaks(text: string): string {
|
||||
return text
|
||||
@@ -18,6 +21,54 @@ export function highlightMentions(text: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** 判断内容是否为 HTML(包含常见 HTML 标签) */
|
||||
function isHtmlContent(text: string): boolean {
|
||||
return /<(?:p|div|span|br|h[1-6]|ul|ol|li|pre|code|blockquote|a|img|table|strong|em|u|s)\b/i.test(text);
|
||||
}
|
||||
|
||||
/** 在 HTML 文本节点中高亮 @ 提及(DOM 遍历,避免破坏标签) */
|
||||
function processMentionsInHtml(html: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
const walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT);
|
||||
const textNodes: Text[] = [];
|
||||
let node: Node | null;
|
||||
while ((node = walker.nextNode())) {
|
||||
textNodes.push(node as Text);
|
||||
}
|
||||
for (const textNode of textNodes) {
|
||||
const text = textNode.textContent ?? '';
|
||||
if (!/@[\w\u4e00-\u9fa5_-]/.test(text)) continue;
|
||||
const frag = document.createDocumentFragment();
|
||||
const parts = text.split(/(@[\w\u4e00-\u9fa5_-]+)/);
|
||||
for (const part of parts) {
|
||||
const m = part.match(/^@([\w\u4e00-\u9fa5_-]+)$/);
|
||||
if (m) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'mention';
|
||||
span.setAttribute('data-name', m[1]);
|
||||
span.setAttribute('role', 'link');
|
||||
span.setAttribute('tabindex', '0');
|
||||
span.textContent = part;
|
||||
frag.appendChild(span);
|
||||
} else if (part) {
|
||||
frag.appendChild(document.createTextNode(part));
|
||||
}
|
||||
}
|
||||
textNode.parentNode?.replaceChild(frag, textNode);
|
||||
}
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** 渲染评论内容:HTML 净化 + @提及高亮,兼容旧版纯文本 */
|
||||
export function renderCommentContent(content: string): string {
|
||||
if (isHtmlContent(content)) {
|
||||
const sanitized = DOMPurify.sanitize(content, POST_CONTENT_PURIFY_CONFIG) as string;
|
||||
return processMentionsInHtml(sanitized);
|
||||
}
|
||||
return highlightMentions(content);
|
||||
}
|
||||
|
||||
/** 相对时间:刚刚 / N分钟前 / N小时前 / N天前;更早用具体日期 */
|
||||
export function formatTime(iso: string) {
|
||||
const d = new Date(iso);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/** 评论表情列表(OwO 面板) */
|
||||
export const EMOJI_LIST = [
|
||||
'😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',
|
||||
'😋', '😎', '😍', '😘', '🥰', '😗', '😙', '😚', '🙂', '🤗',
|
||||
'🤩', '🤔', '🤨', '😐', '😑', '😶', '🙄', '😏', '😣', '😥',
|
||||
'😮', '🤐', '😯', '😪', '😫', '🥱', '😴', '😌', '😛', '😜',
|
||||
'😝', '🤤', '😒', '😓', '😔', '😕', '🙃', '🤑', '😲', '🙁',
|
||||
'😖', '😞', '😟', '😤', '😢', '😭', '😦', '😧', '😨', '😩',
|
||||
'🤯', '😬', '😰', '😱', '🥵', '🥶', '😳', '🤪', '😵', '🥴',
|
||||
'😠', '😡', '🤬', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '😇',
|
||||
'🥳', '🥺', '🤠', '🤡', '🤥', '🤫', '🤭', '🧐', '🤓', '😈',
|
||||
'👻', '💀', '☠️', '👽', '👾', '🤖', '🎃', '😺', '😸', '😹',
|
||||
'😻', '😼', '😽', '🙀', '😿', '😾', '👋', '🤚', '🖐', '✋',
|
||||
'🖖', '👌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉',
|
||||
'👆', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏',
|
||||
'🙌', '👐', '🤲', '🤝', '🙏', '💪', '🦾', '🦿', '🦵', '🦶',
|
||||
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔',
|
||||
'❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️',
|
||||
'✨', '⭐', '🌟', '💫', '🔥', '💥', '💯', '✅', '❌', '❓',
|
||||
'❗', '💢', '💤', '💦', '🎉', '🎊', '🎁', '🏆', '🥇', '🥈',
|
||||
];
|
||||
@@ -107,14 +107,18 @@ function extractGatedInnerHtml(el: Element, bodyClass: string, badgeClass: strin
|
||||
.join('');
|
||||
}
|
||||
|
||||
/** 判断 HTML 正文是否为空(忽略空段落等) */
|
||||
/** 判断 HTML 正文是否为空(忽略空段落等,含图片/视频视为非空) */
|
||||
export function isHtmlEmpty(html: string): boolean {
|
||||
if (!html.trim()) return true;
|
||||
const doc = new DOMParser().parseFromString(
|
||||
DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG) as string,
|
||||
'text/html',
|
||||
);
|
||||
return (doc.body.textContent ?? '').trim().length === 0;
|
||||
// 有文本内容
|
||||
if ((doc.body.textContent ?? '').trim().length > 0) return false;
|
||||
// 有图片、视频等媒体节点
|
||||
if (doc.body.querySelector('img, video, iframe')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 根据登录状态渲染帖子正文 HTML */
|
||||
|
||||
Reference in New Issue
Block a user