From a9478eaf20180abde2b7b3503efcebd8201773a6 Mon Sep 17 00:00:00 2001 From: tianma Date: Sat, 27 Jun 2026 02:43:32 +0800 Subject: [PATCH] feat: improve mobile notebook navigation --- apps/web/src/app/App.tsx | 448 ++++++++++++++++++++++++++++----------- 1 file changed, 324 insertions(+), 124 deletions(-) diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx index 7e01cf84..a325fdcc 100644 --- a/apps/web/src/app/App.tsx +++ b/apps/web/src/app/App.tsx @@ -31,6 +31,7 @@ import { Folder, HardDrive, History, + Home, ImageIcon, Inbox, Italic, @@ -43,7 +44,6 @@ import { Merge, Minus, MoreHorizontal, - PanelLeft, Pencil, Plus, Quote, @@ -59,6 +59,7 @@ import { Tags, Trash2, Undo2, + UserRound, X, } from "lucide-react"; import { @@ -182,6 +183,8 @@ const WorkspaceApp = ({ const [assetsOpen, setAssetsOpen] = useState(false); const [tagsOpen, setTagsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); + const [mobileNotebookPickerOpen, setMobileNotebookPickerOpen] = useState(false); + const [mobileSearchFocusToken, setMobileSearchFocusToken] = useState(0); const [search, setSearch] = useState(""); const [syncSummary, setSyncSummary] = useState(emptySyncQueueSummary); const [isOnline, setIsOnline] = useState(() => (typeof navigator === "undefined" ? true : navigator.onLine)); @@ -496,119 +499,161 @@ const WorkspaceApp = ({ }); }; + const handleSelectNotebook = (notebookId: string) => { + setMemoView("notebook"); + setSelectedNotebookId(notebookId); + setSelectedMemoIds(new Set()); + setMobileNotebookPickerOpen(false); + setActivePane("memos"); + }; + + const handleMobileHome = () => { + if (memoView === "trash") { + setMemoView("notebook"); + } + + setSelectedMemoIds(new Set()); + setActivePane("memos"); + }; + + const handleMobileSearch = () => { + setActivePane("memos"); + setMobileSearchFocusToken((value) => value + 1); + }; + return (
-
- +
+ -
- setActivePane("notebooks")} - onSearch={setSearch} - onCreateMemo={handleCreateMemo} - onOpenMemo={(memoId) => { - setSelectedMemoId(memoId); - setActivePane("editor"); - }} - onToggleMemo={(memoId) => { - setSelectedMemoIds((current) => toggleMemoSelection(current, memoId)); - }} - onMerge={handleMerge} - onMoveSelectedMemos={handleMoveSelectedMemos} - /> -
+
+ setMobileNotebookPickerOpen(true)} + onSearch={setSearch} + onCreateMemo={handleCreateMemo} + onOpenMemo={(memoId) => { + setSelectedMemoId(memoId); + setActivePane("editor"); + }} + onToggleMemo={(memoId) => { + setSelectedMemoIds((current) => toggleMemoSelection(current, memoId)); + }} + onMerge={handleMerge} + onMoveSelectedMemos={handleMoveSelectedMemos} + /> +
-
- setActivePane("memos")} - onSaved={async (memo) => { - queryClient.setQueryData(["memo", memo.id], { memo }); - await queryClient.invalidateQueries({ queryKey: ["memos"] }); - }} - onDeleted={async (memoId) => { - await deleteMemoMutation.mutateAsync({ memoId }); - setSelectedMemoId(null); - setActivePane("memos"); - }} - onPermanentDeleted={async (memoId) => { - await deleteMemoMutation.mutateAsync({ memoId, permanent: true }); - setSelectedMemoId(null); - setActivePane("memos"); - }} - onRestored={async (memoId) => { - await restoreMemoMutation.mutateAsync(memoId); - }} - /> -
-
+
+ setActivePane("memos")} + onSaved={async (memo) => { + queryClient.setQueryData(["memo", memo.id], { memo }); + await queryClient.invalidateQueries({ queryKey: ["memos"] }); + }} + onDeleted={async (memoId) => { + await deleteMemoMutation.mutateAsync({ memoId }); + setSelectedMemoId(null); + setActivePane("memos"); + }} + onPermanentDeleted={async (memoId) => { + await deleteMemoMutation.mutateAsync({ memoId, permanent: true }); + setSelectedMemoId(null); + setActivePane("memos"); + }} + onRestored={async (memoId) => { + await restoreMemoMutation.mutateAsync(memoId); + }} + /> +
+
{assetsOpen ? setAssetsOpen(false)} /> : null} {tagsOpen ? setTagsOpen(false)} /> : null} {settingsOpen ? setSettingsOpen(false)} /> : null} + {activePane !== "editor" ? ( + setAssetsOpen(true)} + onOpenSettings={() => setSettingsOpen(true)} + onSearch={handleMobileSearch} + /> + ) : null} + {mobileNotebookPickerOpen ? ( + setMobileNotebookPickerOpen(false)} + onSelect={handleSelectNotebook} + /> + ) : null}
); }; @@ -970,6 +1015,153 @@ const getSyncStatusLabel = (summary: SyncQueueSummary, isOnline: boolean, isSync return "已同步"; }; +const MobileBottomNav = ({ + canCreateMemo, + isCreating, + onCreateMemo, + onHome, + onOpenAssets, + onOpenSettings, + onSearch, +}: { + canCreateMemo: boolean; + isCreating: boolean; + onCreateMemo: () => void; + onHome: () => void; + onOpenAssets: () => void; + onOpenSettings: () => void; + onSearch: () => void; +}) => ( + +); + +const MobileBottomNavButton = ({ + active = false, + icon, + label, + onClick, +}: { + active?: boolean; + icon: ReactNode; + label: string; + onClick: () => void; +}) => ( + +); + +const MobileNotebookPicker = ({ + notebooks, + selectedNotebookId, + onClose, + onSelect, +}: { + notebooks: Notebook[]; + selectedNotebookId: string | null; + onClose: () => void; + onSelect: (notebookId: string) => void; +}) => { + const tree = useMemo(() => buildNotebookTree(notebooks), [notebooks]); + + return ( +
+
event.stopPropagation()} + > +
+
切换笔记本
+ +
+
+ {tree.map((node) => ( + + ))} +
+
+
+ ); +}; + +const MobileNotebookPickerItem = ({ + node, + depth, + selectedNotebookId, + onSelect, +}: { + node: NotebookNode; + depth: number; + selectedNotebookId: string | null; + onSelect: (notebookId: string) => void; +}) => { + const selected = node.id === selectedNotebookId; + + return ( +
+ + {node.children.length > 0 ? ( +
+ {node.children.map((child) => ( + + ))} +
+ ) : null} +
+ ); +}; + const NotebookTreeItem = ({ node, depth, @@ -1121,12 +1313,13 @@ const MemoListPane = ({ selectedMemoId, selectedMemoIds, search, + searchFocusToken, isLoading, isCreating, isMerging, isMoving, multiSelectKeyDown, - onBackToNotebooks, + onOpenNotebookPicker, onSearch, onCreateMemo, onOpenMemo, @@ -1141,12 +1334,13 @@ const MemoListPane = ({ selectedMemoId: string | null; selectedMemoIds: Set; search: string; + searchFocusToken: number; isLoading: boolean; isCreating: boolean; isMerging: boolean; isMoving: boolean; multiSelectKeyDown: boolean; - onBackToNotebooks: () => void; + onOpenNotebookPicker: () => void; onSearch: (value: string) => void; onCreateMemo: () => void; onOpenMemo: (memoId: string) => void; @@ -1156,6 +1350,7 @@ const MemoListPane = ({ }) => { const [moveTargetNotebookId, setMoveTargetNotebookId] = useState(notebook?.id ?? notebooks[0]?.id ?? ""); const memoGroups = useMemo(() => groupMemosByMonth(memos), [memos]); + const searchInputRef = useRef(null); useEffect(() => { if (notebook?.id) { @@ -1163,16 +1358,32 @@ const MemoListPane = ({ } }, [notebook?.id]); + useEffect(() => { + if (searchFocusToken === 0) { + return; + } + + searchInputRef.current?.focus(); + }, [searchFocusToken]); + return (
- +
-
+
{view === "trash" ? "回收站" : notebook?.name ?? "全部笔记"}
@@ -1194,6 +1405,7 @@ const MemoListPane = ({
-
+
{selectedMemoIds.size > 0 ? (
@@ -1277,18 +1489,6 @@ const MemoListPane = ({
)}
- {selectedMemoIds.size === 0 ? ( - - ) : null}
); };