feat: 论坛 emoji 表情支持
- 新增 dyEmoji 表情库与解析测试、EmojiPicker 选择器与 EmojiImg 组件 - Markdown 编辑器/渲染支持表情图片,新增 public/emoji 资源与生成脚本 - 聊天室与首页适配
This commit is contained in:
@@ -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 ? "上传中…" : "图片"}
|
||||
|
||||
Reference in New Issue
Block a user