From bb97e50e58a454eb077b95c6bd75ffebfc2c51ed Mon Sep 17 00:00:00 2001 From: "WIN-UGQIHHLSKBB\\EDY" <1790703832@qq.com> Date: Fri, 26 Jun 2026 17:18:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20mac=E8=BE=93=E5=85=A5=E6=B3=95=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E5=85=BC=E5=AE=B9=20=E7=99=BB=E5=BD=95=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E8=87=AA=E5=8A=A8=E5=8B=BE=E9=80=89=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dialog/LoginRegisterModal/index.tsx | 2 +- .../ReferenceUploadPromptSection.tsx | 20 ++++++-- .../components/music/VoiceLyricsTextbox.tsx | 46 ++++++++++++++++++- .../components/speech/VoiceSpeechPanel.tsx | 21 +++++++-- src/utils/tiptapImeUtils.ts | 30 ++++++++++++ 5 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 src/utils/tiptapImeUtils.ts diff --git a/src/dialog/LoginRegisterModal/index.tsx b/src/dialog/LoginRegisterModal/index.tsx index ac755ac..9ae4528 100644 --- a/src/dialog/LoginRegisterModal/index.tsx +++ b/src/dialog/LoginRegisterModal/index.tsx @@ -26,7 +26,7 @@ export function LoginRegisterModal({ onClose, onSelect }: LoginRegisterModalProp const [password, setPassword] = useState(""); const [code, setCode] = useState(""); const [inviteCode, setSceneCode] = useState(""); - const [agreementChecked, setAgreementChecked] = useState(false); + const [agreementChecked, setAgreementChecked] = useState(true); const [loading, setLoading] = useState(false); const wxPollRef = useRef(null); diff --git a/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx b/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx index 304da8a..b9ac26b 100644 --- a/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx +++ b/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx @@ -5,6 +5,7 @@ * 编辑器交互由 Tiptap / ProseMirror 接管,@主体标签是 inline atom node。 * 剪切/粘贴主体走自定义 MIME `application/x-popiart-subjects+json`。 */ +import { deferUntilTiptapCompositionEnd, isTiptapEditorComposing } from "@/utils/tiptapImeUtils"; import { mergeAttributes, Node as TiptapNode, type Editor } from "@tiptap/core"; import Placeholder from "@tiptap/extension-placeholder"; import { NodeSelection, TextSelection } from "@tiptap/pm/state"; @@ -1137,11 +1138,20 @@ export function ReferenceUploadPromptSection(props: CommonUploadPromptSectionPro useEffect(() => { if (!editor) return; - const currentAst = tiptapDocToAst(editor.getJSON()); - if (isPromptAstEqual(currentAst, content)) return; - isApplyingEditorContentRef.current = true; - editor.commands.setContent(astToTiptapDoc(content), { emitUpdate: false }); - isApplyingEditorContentRef.current = false; + + const syncEditorFromContent = () => { + const currentAst = tiptapDocToAst(editor.getJSON()); + if (isPromptAstEqual(currentAst, content)) return; + isApplyingEditorContentRef.current = true; + editor.commands.setContent(astToTiptapDoc(content), { emitUpdate: false }); + isApplyingEditorContentRef.current = false; + }; + + if (isTiptapEditorComposing(editor)) { + return deferUntilTiptapCompositionEnd(editor, syncEditorFromContent); + } + + syncEditorFromContent(); }, [content, editor]); useEffect(() => { diff --git a/src/pages/Voice/components/music/VoiceLyricsTextbox.tsx b/src/pages/Voice/components/music/VoiceLyricsTextbox.tsx index 4624193..1e01e9c 100644 --- a/src/pages/Voice/components/music/VoiceLyricsTextbox.tsx +++ b/src/pages/Voice/components/music/VoiceLyricsTextbox.tsx @@ -1,4 +1,12 @@ -import { useCallback, useLayoutEffect, useRef, useState, type ClipboardEvent, type KeyboardEvent } from "react"; +import { + useCallback, + useLayoutEffect, + useRef, + useState, + type ClipboardEvent, + type CompositionEvent, + type KeyboardEvent, +} from "react"; import { useTranslation } from "react-i18next"; import type { LyricsSectionTagItem } from "./lyricsSectionTags"; @@ -23,6 +31,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: const { t } = useTranslation(); const editorRef = useRef(null); const isInternalUpdateRef = useRef(false); + const isComposingRef = useRef(false); const pendingSlashOpenIndexRef = useRef(null); const [popover, setPopover] = useState(null); const [activeIndex, setActiveIndex] = useState(0); @@ -62,6 +71,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: const editor = editorRef.current; if (!editor) return; + if (isComposingRef.current) return; + if (isInternalUpdateRef.current) { isInternalUpdateRef.current = false; return; @@ -79,7 +90,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: setLyricsTextboxCaretOffset(editor, Math.min(offset, value.length)); }, [value]); - const handleInput = useCallback(() => { + const syncInputState = useCallback(() => { const editor = editorRef.current; if (!editor) return; @@ -105,6 +116,32 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: }); }, [commitLyrics, openPopover]); + const handleInput = useCallback(() => { + if (isComposingRef.current) { + const editor = editorRef.current; + if (editor) { + editor.dataset.empty = getLyricsTextboxPlainText(editor).length === 0 ? "true" : "false"; + } + return; + } + + syncInputState(); + }, [syncInputState]); + + const handleCompositionStart = useCallback(() => { + isComposingRef.current = true; + }, []); + + const handleCompositionEnd = useCallback( + (_event: CompositionEvent) => { + isComposingRef.current = false; + requestAnimationFrame(() => { + syncInputState(); + }); + }, + [syncInputState], + ); + const handleSelectTag = useCallback( (item: LyricsSectionTagItem) => { if (!popover) return; @@ -134,6 +171,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: if ( event.key === "/" && !event.nativeEvent.isComposing && + !isComposingRef.current && !event.ctrlKey && !event.metaKey && !event.altKey && @@ -146,6 +184,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: if (!popover) return; + if (event.nativeEvent.isComposing || isComposingRef.current) return; + if (event.key === "Escape") { event.preventDefault(); closePopover(); @@ -198,6 +238,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: contentEditable suppressContentEditableWarning onInput={handleInput} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} onKeyDown={handleKeyDown} onPaste={handlePaste} /> diff --git a/src/pages/Voice/components/speech/VoiceSpeechPanel.tsx b/src/pages/Voice/components/speech/VoiceSpeechPanel.tsx index c014955..2611355 100644 --- a/src/pages/Voice/components/speech/VoiceSpeechPanel.tsx +++ b/src/pages/Voice/components/speech/VoiceSpeechPanel.tsx @@ -1,3 +1,4 @@ +import { deferUntilTiptapCompositionEnd, isTiptapEditorComposing } from "@/utils/tiptapImeUtils"; import { Extension, mergeAttributes, Node as TiptapNode, type Editor } from "@tiptap/core"; import Placeholder from "@tiptap/extension-placeholder"; import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; @@ -1129,6 +1130,7 @@ const VoiceSpeechPromptEditor = forwardRef maxLengthRef.current) { + if (isTiptapEditorComposing(currentEditor)) return; isApplyingEditorContentRef.current = true; currentEditor.commands.setContent(speechTextToTiptapDoc(lastValidValueRef.current), { emitUpdate: false }); isApplyingEditorContentRef.current = false; @@ -1159,11 +1161,20 @@ const VoiceSpeechPromptEditor = forwardRef { + const current = tiptapDocToSpeechText(editor.getJSON()); + if (current === value) return; + isApplyingEditorContentRef.current = true; + editor.commands.setContent(speechTextToTiptapDoc(value), { emitUpdate: false }); + isApplyingEditorContentRef.current = false; + }; + + if (isTiptapEditorComposing(editor)) { + return deferUntilTiptapCompositionEnd(editor, syncFromParent); + } + + syncFromParent(); }, [editor, value]); useImperativeHandle( diff --git a/src/utils/tiptapImeUtils.ts b/src/utils/tiptapImeUtils.ts new file mode 100644 index 0000000..959c0c1 --- /dev/null +++ b/src/utils/tiptapImeUtils.ts @@ -0,0 +1,30 @@ +import type { Editor } from "@tiptap/core"; + +export function isTiptapEditorComposing(editor: Editor | null | undefined): boolean { + return Boolean(editor?.view?.composing); +} + +/** IME 组合结束后执行回调;未在组合时立即执行。返回 useEffect 清理函数。 */ +export function deferUntilTiptapCompositionEnd(editor: Editor, callback: () => void): () => void { + if (!editor.view.composing) { + callback(); + return () => {}; + } + + const dom = editor.view.dom; + let cancelled = false; + + const onCompositionEnd = () => { + dom.removeEventListener("compositionend", onCompositionEnd); + if (!cancelled) { + callback(); + } + }; + + dom.addEventListener("compositionend", onCompositionEnd); + + return () => { + cancelled = true; + dom.removeEventListener("compositionend", onCompositionEnd); + }; +}