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,