diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index 9249ba17..19f7c5c3 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -15,6 +15,7 @@ import { Save, ReplaceAll, MoreHorizontal, + Pencil, Sparkles, Search, X, @@ -58,6 +59,7 @@ import { } from "@/lib/app-helpers"; const SUPPORTED_PASTE_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"]); +const MOBILE_EDITOR_QUERY = "(max-width: 639px)"; type NoteSearchMatch = { from: number; @@ -260,7 +262,6 @@ export const EditorPane = ({ const [, setEditorStateVersion] = useState(0); const [imageUploadState, setImageUploadState] = useState<"idle" | "compressing" | "uploading" | "error">("idle"); const [historyOpen, setHistoryOpen] = useState(false); - const [editorActionsOpen, setEditorActionsOpen] = useState(false); const [mobileNotebookSheetOpen, setMobileNotebookSheetOpen] = useState(false); const [notebookUpdatePending, setNotebookUpdatePending] = useState(false); const [noteSearchOpen, setNoteSearchOpen] = useState(false); @@ -268,8 +269,13 @@ export const EditorPane = ({ const [noteSearchReplaceOpen, setNoteSearchReplaceOpen] = useState(false); const [noteSearchReplacement, setNoteSearchReplacement] = useState(""); const [noteSearchIndex, setNoteSearchIndex] = useState(0); + const [isMobileViewport, setIsMobileViewport] = useState(() => + typeof window === "undefined" ? false : window.matchMedia(MOBILE_EDITOR_QUERY).matches + ); + const [isMobileEditing, setIsMobileEditing] = useState(false); const notebookOptions = useMemo(() => getNotebookMoveOptions(notebooks), [notebooks]); const readOnly = isTrashView || Boolean(memo?.isDeleted); + const effectiveReadOnly = readOnly || (isMobileViewport && !isMobileEditing); const memoRef = useRef(memo); const editorRef = useRef(null); @@ -280,11 +286,25 @@ export const EditorPane = ({ const editingMemoIdRef = useRef(memo?.id ?? null); const imageCompressionEnabledRef = useRef(imageCompressionEnabled); + useEffect(() => { + const mediaQuery = window.matchMedia(MOBILE_EDITOR_QUERY); + const updateMobileViewport = () => setIsMobileViewport(mediaQuery.matches); + + updateMobileViewport(); + mediaQuery.addEventListener("change", updateMobileViewport); + + return () => mediaQuery.removeEventListener("change", updateMobileViewport); + }, []); + + useEffect(() => { + setIsMobileEditing(false); + }, [memo?.id]); + const insertImageFiles = useCallback((files: File[]) => { const currentMemo = memoRef.current; const currentEditor = editorRef.current; - if (!currentMemo || currentMemo.isDeleted || !currentEditor || files.length === 0) { + if (!currentMemo || currentMemo.isDeleted || !currentEditor || !currentEditor.isEditable || files.length === 0) { return; } @@ -339,7 +359,7 @@ export const EditorPane = ({ }), ], content: memo?.contentJson ?? { type: "doc", content: [{ type: "paragraph" }] }, - editable: Boolean(memo && !memo.isDeleted && !isTrashView), + editable: Boolean(memo && !effectiveReadOnly), editorProps: { attributes: { class: "prose prose-slate max-w-none focus:outline-none min-h-[300px] px-4 py-3 sm:px-7", @@ -464,7 +484,7 @@ export const EditorPane = ({ }, [noteSearchMatches, noteSearchOpen, selectNoteSearchMatch]); const replaceAllNoteSearchMatches = useCallback(() => { - if (!editor || readOnly || noteSearchMatches.length === 0) { + if (!editor || effectiveReadOnly || noteSearchMatches.length === 0) { return; } @@ -483,7 +503,7 @@ export const EditorPane = ({ setNoteSearchIndex(0); window.requestAnimationFrame(() => noteSearchInputRef.current?.focus()); - }, [editor, noteSearchMatches, noteSearchReplacement, readOnly]); + }, [editor, effectiveReadOnly, noteSearchMatches, noteSearchReplacement]); useEffect(() => { if (!editor) { @@ -563,7 +583,6 @@ export const EditorPane = ({ const sameMemo = editingMemoIdRef.current === memo.id; memoRef.current = memo; - currentEditor?.setEditable(!memo.isDeleted && !isTrashView); if (sameMemo && hasUnsavedChangesRef.current && !memo.isDeleted) { return; @@ -611,6 +630,10 @@ export const EditorPane = ({ }; }, [isTrashView, memo, editor]); + useEffect(() => { + editor?.setEditable(Boolean(memo && !effectiveReadOnly)); + }, [editor, effectiveReadOnly, memo]); + useEffect(() => { if (!editor || !memo) { return; @@ -810,7 +833,7 @@ export const EditorPane = ({ : "0/0"; const updateMemoNotebook = (notebookId: string, sourceMemo: MemoDetail = memoRef.current ?? memo) => { - if (readOnly || notebookId === sourceMemo.notebookId || notebookUpdatePending) { + if (effectiveReadOnly || notebookId === sourceMemo.notebookId || notebookUpdatePending) { setMobileNotebookSheetOpen(false); return; } @@ -847,7 +870,7 @@ export const EditorPane = ({ }); }; - const handleMobileDone = () => { + const handleMobileBack = () => { if (readOnly || !editor || !hasUnsavedChanges) { onBackToList(); return; @@ -864,8 +887,25 @@ export const EditorPane = ({ }); }; + const handleMobileDone = () => { + if (readOnly || !editor || !hasUnsavedChanges) { + setIsMobileEditing(false); + return; + } + + saveMutation.mutate(undefined, { + onSuccess: () => setIsMobileEditing(false), + onError: (error) => { + const sourceError = error instanceof MemoSaveRequestError ? error.originalError : error; + if (error instanceof MemoSaveRequestError && shouldQueueMemoSaveError(sourceError)) { + setIsMobileEditing(false); + } + }, + }); + }; + return ( -
+
@@ -876,7 +916,7 @@ export const EditorPane = ({ title={hasUnsavedChanges && !readOnly ? "保存并返回列表" : "返回列表"} aria-label={hasUnsavedChanges && !readOnly ? "保存并返回列表" : "返回列表"} disabled={mobileDoneDisabled} - onClick={handleMobileDone} + onClick={handleMobileBack} > @@ -938,14 +978,16 @@ export const EditorPane = ({ {mobileStatusLabel} - + {isMobileEditing && !readOnly && ( + + )} @@ -969,6 +1011,7 @@ export const EditorPane = ({
)} - + {(!isMobileViewport || isMobileEditing) && }
+ {isMobileViewport && !isMobileEditing && !readOnly && ( + + )} + {historyOpen && (