feat: 论坛 emoji 表情支持

- 新增 dyEmoji 表情库与解析测试、EmojiPicker 选择器与 EmojiImg 组件
- Markdown 编辑器/渲染支持表情图片,新增 public/emoji 资源与生成脚本
- 聊天室与首页适配
This commit is contained in:
2026-09-28 01:28:41 +08:00
parent 2f96a36275
commit 34aee91a13
3522 changed files with 1560 additions and 251 deletions

View File

@@ -32,12 +32,14 @@ import {
ChevronDown,
History,
Ellipsis,
Smile,
} from "lucide-react";
import { apiSearchPosts, apiUploadPostImage, type PostListItem } from "@/lib/api";
import { toast } from "@/lib/toast";
import MarkdownBodyClient from "@/components/MarkdownBodyClient";
import ImageInsertModal from "@/components/ImageInsertModal";
import TimelineEditor from "@/components/TimelineEditor";
import EmojiPicker from "@/components/EmojiPicker";
import { resolveTimelineBlockForEdit } from "@/lib/directiveBlocks";
import { SHIKI_LANG_IDS } from "@/components/markdown/shikiHighlighter";
import { postPath } from "@/lib/urlStyle";
@@ -88,7 +90,7 @@ type WrapOpts = {
linePrefix?: string;
};
type PopoverKind = "link" | "code" | "hide" | "heading" | "more" | null;
type PopoverKind = "link" | "code" | "hide" | "heading" | "more" | "emoji" | null;
const CODE_LANGS = ["plaintext", "aardio", ...SHIKI_LANG_IDS.filter((l) => l !== "plaintext")] as const;
@@ -412,6 +414,36 @@ export default function MarkdownEditor({
[value, onChange]
);
/** 行内插入(无换行 padding):表情等短代码用 */
const insertInline = useCallback(
(text: string) => {
const el = taRef.current;
if (!el) {
onChange(value + text);
return;
}
const start = savedSel.current?.start ?? el.selectionStart;
const end = savedSel.current?.end ?? el.selectionEnd;
const next = value.slice(0, start) + text + value.slice(end);
onChange(next);
requestAnimationFrame(() => {
el.focus();
const caret = start + text.length;
el.setSelectionRange(caret, caret);
});
savedSel.current = null;
},
[value, onChange]
);
const pickEmoji = useCallback(
(key: string) => {
insertInline(`[:${key}:]`);
setPopover(null);
},
[insertInline]
);
const uploadImageFile = async (file: File | null) => {
if (!file || disabled) return;
retryFile.current = file;
@@ -976,6 +1008,19 @@ export default function MarkdownEditor({
</div>
)}
</div>
<div className="j13-md-popover-anchor relative">
<ToolBtn
label="表情"
icon={<Smile size={15} />}
active={popover === "emoji"}
onClick={() => openPopover("emoji")}
/>
{popover === "emoji" && (
<div className="j13-md-popover j13-md-emoji-popover" role="dialog" aria-label="插入表情">
<EmojiPicker onPick={pickEmoji} />
</div>
)}
</div>
<div className="j13-md-popover-anchor relative">
<ToolBtn
label={uploading ? "上传中…" : "图片"}