Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

FONTCONFIG_PATH=/var/task/fonts
FONTCONFIG_PATH=/var/task/fonts
NEXT_PUBLIC_RESUME_REPO_URL = 简历数据仓库接口URL
58 changes: 16 additions & 42 deletions src/app/app/dashboard/resumes/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = () => {
Expand All @@ -30,46 +30,20 @@ const ResumeWorkbench = () => {
resumes,
setActiveResume,
updateResume,
updateResumeFromFile,
getResumesFromRepository,
addResume,
deleteResume,
createResume
} = useResumeStore();
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 () => {
Expand Down
156 changes: 156 additions & 0 deletions src/store/resumeRepositoryManager.ts
Original file line number Diff line number Diff line change
@@ -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 };

Loading