feat: 新增贴纸系统,重构评论编辑器与内容处理
1. 新增多平台贴纸库:贴吧、微博、知乎、小红书、抖音、B站共500+贴纸,搭配颜文字贴纸 2. 实现贴纸选择器面板,支持分类浏览、键盘导航和懒加载 3. 重构评论内容渲染:替换旧的@提及高亮逻辑,新增HTML净化与双向兼容 4. 重构评论编辑器为Tiptap富文本版本,支持表情插入、格式编辑 5. 优化评论编辑体验,修复滚动位置保留问题,调整评论区UI样式 6. 移除旧版EmojiPicker工具类,新增贴纸数据管理模块
This commit is contained in:
116
frontend/src/components/emoji/StickerPicker.tsx
Normal file
116
frontend/src/components/emoji/StickerPicker.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState, useEffect, useId, useRef, useCallback } from 'react';
|
||||
import {
|
||||
STICKER_CATEGORIES,
|
||||
loadStickersByCategory,
|
||||
type Sticker,
|
||||
type StickerCategory,
|
||||
} from '../../data/stickers';
|
||||
|
||||
interface Props {
|
||||
onSelect: (sticker: Sticker) => void;
|
||||
}
|
||||
|
||||
/** 贴纸选择面板(分类 Tab + 懒加载 + 键盘导航 + 图片/纯文本混合渲染) */
|
||||
export default function StickerPicker({ onSelect }: Props) {
|
||||
const autoId = useId();
|
||||
const [active, setActive] = useState<StickerCategory>('热门');
|
||||
const [stickers, setStickers] = useState<Sticker[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setStickers([]);
|
||||
loadStickersByCategory(active)
|
||||
.then((list) => {
|
||||
if (cancelled) return;
|
||||
setStickers(list);
|
||||
setFocusIndex(0);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = gridRef.current?.querySelectorAll<HTMLElement>('[role="option"]')[focusIndex];
|
||||
el?.focus();
|
||||
}, [focusIndex, stickers]);
|
||||
|
||||
const onKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
const cols = 8;
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); setFocusIndex((i) => Math.min(stickers.length - 1, i + 1)); }
|
||||
else if (e.key === 'ArrowLeft') { e.preventDefault(); setFocusIndex((i) => Math.max(0, i - 1)); }
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); setFocusIndex((i) => Math.min(stickers.length - 1, i + cols)); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setFocusIndex((i) => Math.max(0, i - cols)); }
|
||||
else if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const s = stickers[focusIndex];
|
||||
if (s) onSelect(s);
|
||||
}
|
||||
}, [stickers, focusIndex, onSelect]);
|
||||
|
||||
return (
|
||||
<div className="sticker-picker" role="dialog" aria-label="贴纸选择">
|
||||
<div className="sticker-picker-tabs" role="tablist">
|
||||
{STICKER_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === cat}
|
||||
className={`sticker-picker-tab${active === cat ? ' active' : ''}`}
|
||||
onClick={() => setActive(cat)}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
ref={gridRef}
|
||||
className="sticker-picker-grid"
|
||||
role="listbox"
|
||||
aria-label={`${active}贴纸`}
|
||||
aria-activedescendant={`${autoId}-opt-${focusIndex}`}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="sticker-picker-loading">加载中…</div>
|
||||
) : stickers.length === 0 ? (
|
||||
<div className="sticker-picker-loading">暂无贴纸</div>
|
||||
) : (
|
||||
stickers.map((s, i) => (
|
||||
<button
|
||||
key={s.id}
|
||||
id={`${autoId}-opt-${i}`}
|
||||
type="button"
|
||||
role="option"
|
||||
tabIndex={focusIndex === i ? 0 : -1}
|
||||
aria-selected={focusIndex === i}
|
||||
aria-label={s.name}
|
||||
className="sticker-picker-item"
|
||||
onClick={() => onSelect(s)}
|
||||
onFocus={() => setFocusIndex(i)}
|
||||
>
|
||||
{s.type === 'text' && s.text ? (
|
||||
<span className="sticker-picker-text">{s.text}</span>
|
||||
) : (
|
||||
<img
|
||||
src={s.url}
|
||||
alt={s.name}
|
||||
width={32}
|
||||
height={32}
|
||||
style={{ width: 32, height: 32, objectFit: 'contain' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user