From f1fc952c67e3e14d160ed18035b4e7e5f4b0aded Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Mon, 15 Jun 2026 23:56:28 +0800 Subject: [PATCH] feat: update navigation to support parameter objects and add error-safe persistence to resume store --- .../app/dashboard/resumes/ResumeCardItem.tsx | 4 +- .../app/dashboard/resumes/ResumeWorkbench.tsx | 6 +-- src/app/app/dashboard/templates/page.tsx | 2 +- src/components/preview/PreviewDock.tsx | 2 +- src/lib/navigation.ts | 16 +++++-- src/store/useResumeStore.ts | 44 +++++++++++++++++-- 6 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/app/app/dashboard/resumes/ResumeCardItem.tsx b/src/app/app/dashboard/resumes/ResumeCardItem.tsx index e91d3ef..2f6143b 100644 --- a/src/app/app/dashboard/resumes/ResumeCardItem.tsx +++ b/src/app/app/dashboard/resumes/ResumeCardItem.tsx @@ -93,7 +93,7 @@ export const ResumeCardItem = ({ onClick={(e) => { e.stopPropagation(); setActiveResume(id); - router.push(`/app/workbench/${id}`); + router.push({ to: "/app/workbench/$id", params: { id } }); }} >
@@ -139,7 +139,7 @@ export const ResumeCardItem = ({ onClick={(e) => { e.stopPropagation(); setActiveResume(id); - router.push(`/app/workbench/${id}`); + router.push({ to: "/app/workbench/$id", params: { id } }); }} className="flex-1 flex items-center justify-center gap-1.5 hover:bg-white dark:hover:bg-gray-800/80 transition-all duration-200 text-gray-700 dark:text-gray-200 hover:text-primary font-medium text-sm group" > diff --git a/src/app/app/dashboard/resumes/ResumeWorkbench.tsx b/src/app/app/dashboard/resumes/ResumeWorkbench.tsx index 6ea09ad..b5b204a 100644 --- a/src/app/app/dashboard/resumes/ResumeWorkbench.tsx +++ b/src/app/app/dashboard/resumes/ResumeWorkbench.tsx @@ -100,7 +100,7 @@ export const ResumeWorkbench = () => { setIsCreateModalOpen(false); setActiveResume(newId); - router.push(`/app/workbench/${newId}`); + router.push({ to: "/app/workbench/$id", params: { id: newId } }); }; const duplicateResume = async (resume: any) => { @@ -138,7 +138,7 @@ export const ResumeWorkbench = () => { setActiveResume(resumeId); setIsImportDialogOpen(false); toast.success(t("dashboard.resumes.importSuccess")); - router.push(`/app/workbench/${resumeId}`); + router.push({ to: "/app/workbench/$id", params: { id: resumeId } }); }; const extractImagesFromPdf = async (file: File) => { @@ -234,7 +234,7 @@ export const ResumeWorkbench = () => { setActiveResume(resumeId); setIsImportDialogOpen(false); toast.success(t("dashboard.resumes.importDialog.pdfSuccess")); - router.push(`/app/workbench/${resumeId}`); + router.push({ to: "/app/workbench/$id", params: { id: resumeId } }); }; const handleJsonFileChange = async ( diff --git a/src/app/app/dashboard/templates/page.tsx b/src/app/app/dashboard/templates/page.tsx index f375fd3..a719f8c 100644 --- a/src/app/app/dashboard/templates/page.tsx +++ b/src/app/app/dashboard/templates/page.tsx @@ -264,7 +264,7 @@ const TemplatesPage = () => { }); } - router.push(`/app/workbench/${resumeId}`); + router.push({ to: "/app/workbench/$id", params: { id: resumeId } }); }; return ( diff --git a/src/components/preview/PreviewDock.tsx b/src/components/preview/PreviewDock.tsx index 1b1b962..3ec90ea 100644 --- a/src/components/preview/PreviewDock.tsx +++ b/src/components/preview/PreviewDock.tsx @@ -147,7 +147,7 @@ const PreviewDock = ({ const targetPath = `/app/workbench/${newId}`; setActiveResume(newId); toast.success(t("copyResume.success")); - router.push(targetPath); + router.push({ to: "/app/workbench/$id", params: { id: newId } }); requestAnimationFrame(() => { if (window.location.pathname !== targetPath) { diff --git a/src/lib/navigation.ts b/src/lib/navigation.ts index 3e6fbb2..01bf0ce 100644 --- a/src/lib/navigation.ts +++ b/src/lib/navigation.ts @@ -1,13 +1,23 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; -type NavigateTarget = string; +type NavigateTarget = + | string + | { + to: string; + params?: Record; + search?: Record; + hash?: string; + }; export function useRouter() { const navigate = useNavigate(); + const toNavigateOptions = (target: NavigateTarget) => + typeof target === "string" ? { to: target } : target; return { - push: (to: NavigateTarget) => navigate({ to }), - replace: (to: NavigateTarget) => navigate({ to, replace: true }), + push: (target: NavigateTarget) => navigate(toNavigateOptions(target) as any), + replace: (target: NavigateTarget) => + navigate({ ...toNavigateOptions(target), replace: true } as any), back: () => window.history.back(), forward: () => window.history.forward(), refresh: () => window.location.reload() diff --git a/src/store/useResumeStore.ts b/src/store/useResumeStore.ts index 999ffd0..68129ab 100644 --- a/src/store/useResumeStore.ts +++ b/src/store/useResumeStore.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; -import { persist } from "zustand/middleware"; +import { createJSONStorage, persist } from "zustand/middleware"; +import type { StateStorage } from "zustand/middleware"; import { getFileHandle, verifyPermission } from "@/utils/fileSystem"; import { BasicInfo, @@ -75,6 +76,32 @@ interface ResumeStore { type PersistedResumeStore = Pick; +const warnedPersistFailures = new Set(); + +const warnPersistFailure = (name: string, error: unknown) => { + if (warnedPersistFailures.has(name)) { + return; + } + + warnedPersistFailures.add(name); + console.warn( + `[resume-store] Failed to persist "${name}" to localStorage. Changes remain available in memory for this session.`, + error + ); +}; + +const createSafeLocalStorage = (): StateStorage => ({ + getItem: (name) => localStorage.getItem(name), + setItem: (name, value) => { + try { + localStorage.setItem(name, value); + } catch (error) { + warnPersistFailure(name, error); + } + }, + removeItem: (name) => localStorage.removeItem(name), +}); + const parseTimestamp = (value?: string): number | null => { if (!value) { return null; @@ -201,7 +228,7 @@ const debouncedSyncToFile = ( }; export const useResumeStore = create( - persist( + persist( (set, get) => ({ resumes: {}, activeResumeId: null, @@ -380,8 +407,14 @@ export const useResumeStore = create( }, setActiveResume: (resumeId) => { - const resume = get().resumes[resumeId]; - set({ activeResume: resume ?? null, activeResumeId: resumeId }); + const { resumes, activeResume, activeResumeId } = get(); + const nextResume = resumes[resumeId] ?? null; + + if (activeResumeId === resumeId && activeResume === nextResume) { + return; + } + + set({ activeResume: nextResume, activeResumeId: resumeId }); }, updateBasicInfo: (data) => { @@ -795,6 +828,9 @@ export const useResumeStore = create( }), { name: "resume-storage", + storage: createJSONStorage(() => + createSafeLocalStorage() + ), partialize: (state): PersistedResumeStore => ({ resumes: state.resumes, activeResumeId: state.activeResumeId,