From 9efed2d18db8290bb301c94ddee0dc355c109958 Mon Sep 17 00:00:00 2001 From: weicanie <2042365244@qq.com> Date: Mon, 15 Sep 2025 21:13:43 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E4=BF=9D=E6=8C=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90=E5=8D=95=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/useResumeStore.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/store/useResumeStore.ts b/src/store/useResumeStore.ts index 1d94360c..5625777f 100644 --- a/src/store/useResumeStore.ts +++ b/src/store/useResumeStore.ts @@ -1,22 +1,21 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; +import { DEFAULT_TEMPLATES } from "@/config"; +import { + initialResumeState, + initialResumeStateEn, +} from "@/config/initialResumeData"; import { getFileHandle, verifyPermission } from "@/utils/fileSystem"; +import { generateUUID } from "@/utils/uuid"; +import { create } from "zustand"; import { BasicInfo, + CustomItem, Education, Experience, GlobalSettings, + MenuSection, Project, - CustomItem, ResumeData, - MenuSection, } from "../types/resume"; -import { DEFAULT_TEMPLATES } from "@/config"; -import { - initialResumeState, - initialResumeStateEn, -} from "@/config/initialResumeData"; -import { generateUUID } from "@/utils/uuid"; interface ResumeStore { resumes: Record; activeResumeId: string | null; @@ -106,8 +105,8 @@ const syncResumeToFile = async ( } }; -export const useResumeStore = create( - persist( +export const useResumeStore = create( + ( (set, get) => ({ resumes: {}, activeResumeId: null, @@ -615,9 +614,6 @@ export const useResumeStore = create( syncResumeToFile(resume); return resume.id; }, - }), - { - name: "resume-storage", - } + }) ) ); From 7bc32d59ccbaf867bd09d7199ce32ff490b097c4 Mon Sep 17 00:00:00 2001 From: weicanie <2042365244@qq.com> Date: Mon, 15 Sep 2025 21:33:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E5=A4=96=E9=83=A8API=E6=8F=90=E4=BE=9B=E7=AE=80?= =?UTF-8?q?=E5=8E=86=E6=95=B0=E6=8D=AE=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 3 +- src/store/resumeRepositoryManager.ts | 156 +++++++++++++++++++++++++++ src/store/useResumeStore.ts | 84 +++------------ src/types/repository.ts | 25 +++++ 4 files changed, 198 insertions(+), 70 deletions(-) create mode 100644 src/store/resumeRepositoryManager.ts create mode 100644 src/types/repository.ts diff --git a/.env b/.env index ecbb77b6..b51bcd4c 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ -FONTCONFIG_PATH=/var/task/fonts \ No newline at end of file +FONTCONFIG_PATH=/var/task/fonts +NEXT_PUBLIC_RESUME_REPO_URL = 简历数据仓库接口URL \ No newline at end of file diff --git a/src/store/resumeRepositoryManager.ts b/src/store/resumeRepositoryManager.ts new file mode 100644 index 00000000..c58e4073 --- /dev/null +++ b/src/store/resumeRepositoryManager.ts @@ -0,0 +1,156 @@ +import { ResumeRepositoryManager } from '../types/repository'; +import { ResumeData } from '../types/resume'; +import { getFileHandle, verifyPermission } from '../utils/fileSystem'; + +// 同步简历到文件系统 +const syncResumeToFile = async (resumeData: ResumeData, prevResume?: ResumeData) => { + try { + const handle = await getFileHandle('syncDirectory'); + if (!handle) { + console.warn('No directory handle found'); + return; + } + + const hasPermission = await verifyPermission(handle); + if (!hasPermission) { + console.warn('No permission to write to directory'); + return; + } + + const dirHandle = handle as FileSystemDirectoryHandle; + + // 避免简历标题发生变化时保留不再使用的文件 + if (prevResume && prevResume.id === resumeData.id && prevResume.title !== resumeData.title) { + try { + await dirHandle.removeEntry(`${prevResume.title}.json`); + } catch (error) { + console.warn('Error deleting old file:', error); + } + } + const fileName = `${resumeData.title}.json`; + const fileHandle = await dirHandle.getFileHandle(fileName, { + create: true + }); + const writable = await fileHandle.createWritable(); + // 如果简历文件已存在,则直接替换其内容 + await writable.write(JSON.stringify(resumeData, null, 2)); + await writable.close(); + } catch (error) { + console.error('Error syncing resume to file:', error); + } +}; +// 从文件系统中删除简历 +// 不在具体业务逻辑中暴露文件句柄 +const deleteResumeFromFile = async (resume: ResumeData) => { + try { + const handle = await getFileHandle('syncDirectory'); + if (!handle) return; + + const hasPermission = await verifyPermission(handle); + if (!hasPermission) return; + + const dirHandle = handle as FileSystemDirectoryHandle; + try { + await dirHandle.removeEntry(`${resume.title}.json`); + } catch (error) {} + } catch (error) { + console.error('Error deleting resume file:', error); + } +}; +// 从文件系统中获取简历数据,并同步到store +const getResumesFromFiles = async (updateResumeFromFile: (resume: ResumeData) => void) => { + try { + const handle = await getFileHandle('syncDirectory'); + if (!handle) return; + + const hasPermission = await verifyPermission(handle); + if (!hasPermission) return; + + const dirHandle = handle as FileSystemDirectoryHandle; + + // @ts-ignore + for await (const entry of dirHandle.values()) { + if (entry.kind === 'file' && entry.name.endsWith('.json')) { + try { + const file = await entry.getFile(); + const content = await file.text(); + const resumeData = JSON.parse(content); + // 将简历数据同步到store + updateResumeFromFile(resumeData); + } catch (error) { + console.error('Error reading resume file:', error); + } + } + } + } catch (error) { + console.error('Error syncing resumes from files:', error); + } +}; + +const syncResuemToAPI = async (resumeData: ResumeData, prevResume?: ResumeData) => { + try { + await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, { + method: 'POST', + body: JSON.stringify({ actionType: 'sync', payload: { resumeData, prevResume } }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }); + } catch (error) { + console.error('Error syncing resume to database:', error); + } +}; + +const deleteResumeFromAPI = async (resumeData: ResumeData) => { + try { + await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, { + method: 'POST', + body: JSON.stringify({ actionType: 'delete', payload: { resumeData } }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }); + } catch (error) { + console.error('Error deleting resume from database:', error); + } +}; + +const getResumesFromAPI = async (updateResumeFromFile: (resume: ResumeData) => void) => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, { + method: 'POST', + body: JSON.stringify({ actionType: 'get' }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }); + const data = await response.json(); + // data.data: prisma-ai后端统一返回`SDF` + for (const resume of data.data) { + updateResumeFromFile(resume); + } + } catch (error) { + console.error('Error getting resumes from database:', error); + } +}; + +// 使用浏览器端的 File System Access API +const clientfileManager: ResumeRepositoryManager = { + syncResumeToRepository: syncResumeToFile, + deleteResumeFromRepository: deleteResumeFromFile, + getResumeFromRepository: getResumesFromFiles +}; + +// 使用外部API(服务器文件系统、数据库、OSS...) +const outerAPIManager: ResumeRepositoryManager = { + syncResumeToRepository: syncResuemToAPI, + deleteResumeFromRepository: deleteResumeFromAPI, + getResumeFromRepository: getResumesFromAPI +}; +const resumeRepoManager = clientfileManager; + +export { resumeRepoManager }; + diff --git a/src/store/useResumeStore.ts b/src/store/useResumeStore.ts index 5625777f..e5a7ce03 100644 --- a/src/store/useResumeStore.ts +++ b/src/store/useResumeStore.ts @@ -3,7 +3,6 @@ import { initialResumeState, initialResumeStateEn, } from "@/config/initialResumeData"; -import { getFileHandle, verifyPermission } from "@/utils/fileSystem"; import { generateUUID } from "@/utils/uuid"; import { create } from "zustand"; import { @@ -16,6 +15,7 @@ import { Project, ResumeData, } from "../types/resume"; +import { resumeRepoManager } from "./resumeRepositoryManager"; interface ResumeStore { resumes: Record; activeResumeId: string | null; @@ -23,6 +23,7 @@ interface ResumeStore { createResume: (templateId: string | null) => string; deleteResume: (resume: ResumeData) => void; + getResumesFromRepository: () => void; duplicateResume: (resumeId: string) => string; updateResume: (resumeId: string, data: Partial) => void; setActiveResume: (resumeId: string) => void; @@ -61,50 +62,6 @@ interface ResumeStore { addResume: (resume: ResumeData) => string; } -// 同步简历到文件系统 -const syncResumeToFile = async ( - resumeData: ResumeData, - prevResume?: ResumeData -) => { - try { - const handle = await getFileHandle("syncDirectory"); - if (!handle) { - console.warn("No directory handle found"); - return; - } - - const hasPermission = await verifyPermission(handle); - if (!hasPermission) { - console.warn("No permission to write to directory"); - return; - } - - const dirHandle = handle as FileSystemDirectoryHandle; - - if ( - prevResume && - prevResume.id === resumeData.id && - prevResume.title !== resumeData.title - ) { - try { - await dirHandle.removeEntry(`${prevResume.title}.json`); - } catch (error) { - console.warn("Error deleting old file:", error); - } - } - - const fileName = `${resumeData.title}.json`; - const fileHandle = await dirHandle.getFileHandle(fileName, { - create: true, - }); - const writable = await fileHandle.createWritable(); - await writable.write(JSON.stringify(resumeData, null, 2)); - await writable.close(); - } catch (error) { - console.error("Error syncing resume to file:", error); - } -}; - export const useResumeStore = create( ( (set, get) => ({ @@ -150,7 +107,7 @@ export const useResumeStore = create( activeResume: newResume, })); - syncResumeToFile(newResume); + resumeRepoManager.syncResumeToRepository(newResume); return id; }, @@ -165,7 +122,7 @@ export const useResumeStore = create( ...data, }; - syncResumeToFile(updatedResume, resume); + resumeRepoManager.syncResumeToRepository(updatedResume, resume); return { resumes: { @@ -180,6 +137,10 @@ export const useResumeStore = create( }); }, + getResumesFromRepository: () => { + resumeRepoManager.getResumeFromRepository(get().updateResumeFromFile); + }, + // 从文件更新,直接更新resumes updateResumeFromFile: (resume) => { set((state) => ({ @@ -197,33 +158,18 @@ export const useResumeStore = create( } }, - deleteResume: (resume) => { + deleteResume: resume => { const resumeId = resume.id; - set((state) => { + set(state => { const { [resumeId]: _, activeResume, ...rest } = state.resumes; return { resumes: rest, activeResumeId: null, - activeResume: null, + activeResume: null }; }); - - (async () => { - try { - const handle = await getFileHandle("syncDirectory"); - if (!handle) return; - - const hasPermission = await verifyPermission(handle); - if (!hasPermission) return; - - const dirHandle = handle as FileSystemDirectoryHandle; - try { - await dirHandle.removeEntry(`${resume.title}.json`); - } catch (error) {} - } catch (error) { - console.error("Error deleting resume file:", error); - } - })(); + + resumeRepoManager.deleteResumeFromRepository(resume); }, duplicateResume: (resumeId) => { @@ -288,7 +234,7 @@ export const useResumeStore = create( activeResume: updatedResume, }; - syncResumeToFile(updatedResume, state.activeResume); + resumeRepoManager.syncResumeToRepository(updatedResume, state.activeResume); return newState; }); @@ -611,7 +557,7 @@ export const useResumeStore = create( activeResumeId: resume.id, })); - syncResumeToFile(resume); + resumeRepoManager.syncResumeToRepository(resume); return resume.id; }, }) diff --git a/src/types/repository.ts b/src/types/repository.ts new file mode 100644 index 00000000..6bc72422 --- /dev/null +++ b/src/types/repository.ts @@ -0,0 +1,25 @@ +import { + ResumeData +} from './resume'; + +/** + * 简历数据仓库管理器 + */ +interface ResumeRepositoryManager { + /** + * 进行简历数据的创建(C)和更新(U) + */ + syncResumeToRepository: (resumeData: ResumeData, prevResume?: ResumeData) => Promise; + /** + * 进行简历数据的删除(D) + */ + deleteResumeFromRepository: (resume: ResumeData) => Promise; + /** + * 进行简历数据的获取(R) + * @description 从文件系统或数据库中获取简历数据,并同步到store中 + */ + getResumeFromRepository: (updateResumeFromFile: (resume: ResumeData) => void) => Promise; +} + +export { type ResumeRepositoryManager }; + From bef9960dc9b7f7e7ff9f04effc9ed030971f618a Mon Sep 17 00:00:00 2001 From: weicanie <2042365244@qq.com> Date: Mon, 15 Sep 2025 21:51:13 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E7=BB=84=E4=BB=B6=E4=BB=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=BB=93=E5=BA=93=E4=B8=AD=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=AE=80=E5=8E=86=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app/dashboard/resumes/page.tsx | 58 +++++++------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/src/app/app/dashboard/resumes/page.tsx b/src/app/app/dashboard/resumes/page.tsx index 2bf65112..a916d74f 100644 --- a/src/app/app/dashboard/resumes/page.tsx +++ b/src/app/app/dashboard/resumes/page.tsx @@ -1,10 +1,5 @@ "use client"; -import React, { useEffect } from "react"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { Plus, FileText, Settings, AlertCircle, Upload } from "lucide-react"; -import { motion, AnimatePresence } from "framer-motion"; -import { toast } from "sonner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,11 +8,16 @@ import { CardFooter, CardTitle } from "@/components/ui/card"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { initialResumeState } from "@/config/initialResumeData"; import { cn } from "@/lib/utils"; -import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem"; import { useResumeStore } from "@/store/useResumeStore"; -import { initialResumeState } from "@/config/initialResumeData"; +import { getConfig, getFileHandle } from "@/utils/fileSystem"; +import { AnimatePresence, motion } from "framer-motion"; +import { AlertCircle, FileText, Plus, Settings, Upload } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import React, { useEffect } from "react"; +import { toast } from "sonner"; import { generateUUID } from "@/utils/uuid"; const ResumesList = () => { @@ -30,7 +30,7 @@ const ResumeWorkbench = () => { resumes, setActiveResume, updateResume, - updateResumeFromFile, + getResumesFromRepository, addResume, deleteResume, createResume @@ -38,38 +38,12 @@ const ResumeWorkbench = () => { const router = useRouter(); const [hasConfiguredFolder, setHasConfiguredFolder] = React.useState(false); - useEffect(() => { - const syncResumesFromFiles = async () => { - try { - const handle = await getFileHandle("syncDirectory"); - if (!handle) return; - - const hasPermission = await verifyPermission(handle); - if (!hasPermission) return; - - const dirHandle = handle as FileSystemDirectoryHandle; - - for await (const entry of dirHandle.values()) { - if (entry.kind === "file" && entry.name.endsWith(".json")) { - try { - const file = await entry.getFile(); - const content = await file.text(); - const resumeData = JSON.parse(content); - updateResumeFromFile(resumeData); - } catch (error) { - console.error("Error reading resume file:", error); - } - } - } - } catch (error) { - console.error("Error syncing resumes from files:", error); - } - }; - - if (Object.keys(resumes).length === 0) { - syncResumesFromFiles(); - } - }, [resumes, updateResume]); + // 将数据仓库中的简历数据同步到store + useEffect(() => { + if (Object.keys(resumes).length === 0) { + getResumesFromRepository(); + } + }, [resumes, updateResume, getResumesFromRepository]); useEffect(() => { const loadSavedConfig = async () => {