mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: 支持通过外部API提供简历数据源
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
|
||||
FONTCONFIG_PATH=/var/task/fonts
|
||||
FONTCONFIG_PATH=/var/task/fonts
|
||||
NEXT_PUBLIC_RESUME_REPO_URL = 简历数据仓库接口URL
|
||||
@@ -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 };
|
||||
|
||||
+15
-69
@@ -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<string, ResumeData>;
|
||||
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<ResumeData>) => 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<ResumeStore>(
|
||||
(
|
||||
(set, get) => ({
|
||||
@@ -150,7 +107,7 @@ export const useResumeStore = create<ResumeStore>(
|
||||
activeResume: newResume,
|
||||
}));
|
||||
|
||||
syncResumeToFile(newResume);
|
||||
resumeRepoManager.syncResumeToRepository(newResume);
|
||||
|
||||
return id;
|
||||
},
|
||||
@@ -165,7 +122,7 @@ export const useResumeStore = create<ResumeStore>(
|
||||
...data,
|
||||
};
|
||||
|
||||
syncResumeToFile(updatedResume, resume);
|
||||
resumeRepoManager.syncResumeToRepository(updatedResume, resume);
|
||||
|
||||
return {
|
||||
resumes: {
|
||||
@@ -180,6 +137,10 @@ export const useResumeStore = create<ResumeStore>(
|
||||
});
|
||||
},
|
||||
|
||||
getResumesFromRepository: () => {
|
||||
resumeRepoManager.getResumeFromRepository(get().updateResumeFromFile);
|
||||
},
|
||||
|
||||
// 从文件更新,直接更新resumes
|
||||
updateResumeFromFile: (resume) => {
|
||||
set((state) => ({
|
||||
@@ -197,33 +158,18 @@ export const useResumeStore = create<ResumeStore>(
|
||||
}
|
||||
},
|
||||
|
||||
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<ResumeStore>(
|
||||
activeResume: updatedResume,
|
||||
};
|
||||
|
||||
syncResumeToFile(updatedResume, state.activeResume);
|
||||
resumeRepoManager.syncResumeToRepository(updatedResume, state.activeResume);
|
||||
|
||||
return newState;
|
||||
});
|
||||
@@ -611,7 +557,7 @@ export const useResumeStore = create<ResumeStore>(
|
||||
activeResumeId: resume.id,
|
||||
}));
|
||||
|
||||
syncResumeToFile(resume);
|
||||
resumeRepoManager.syncResumeToRepository(resume);
|
||||
return resume.id;
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ResumeData
|
||||
} from './resume';
|
||||
|
||||
/**
|
||||
* 简历数据仓库管理器
|
||||
*/
|
||||
interface ResumeRepositoryManager {
|
||||
/**
|
||||
* 进行简历数据的创建(C)和更新(U)
|
||||
*/
|
||||
syncResumeToRepository: (resumeData: ResumeData, prevResume?: ResumeData) => Promise<void>;
|
||||
/**
|
||||
* 进行简历数据的删除(D)
|
||||
*/
|
||||
deleteResumeFromRepository: (resume: ResumeData) => Promise<void>;
|
||||
/**
|
||||
* 进行简历数据的获取(R)
|
||||
* @description 从文件系统或数据库中获取简历数据,并同步到store中
|
||||
*/
|
||||
getResumeFromRepository: (updateResumeFromFile: (resume: ResumeData) => void) => Promise<void>;
|
||||
}
|
||||
|
||||
export { type ResumeRepositoryManager };
|
||||
|
||||
Reference in New Issue
Block a user