From 527a354acf0a2bde4eb8716ffac77e4a3f3157de Mon Sep 17 00:00:00 2001
From: JOYCEQL <1449239013@qq.com>
Date: Mon, 9 Dec 2024 23:57:43 +0800
Subject: [PATCH] feat: offline storage config
---
apps/fronted/next.config.mjs | 2 -
apps/fronted/src/app/dashboard/layout.tsx | 115 ++++++++++++
apps/fronted/src/app/dashboard/page.tsx | 6 +
.../src/app/dashboard/resumes/page.tsx | 130 +++++++++++++
.../src/app/dashboard/settings/page.tsx | 118 ++++++++++++
apps/fronted/src/app/workbench/[id]/page.tsx | 7 +-
apps/fronted/src/app/workbench/page.tsx | 164 ----------------
.../src/components/editor/EditorHeader.tsx | 2 +-
.../src/components/editor/SidePanel.tsx | 1 -
.../src/components/shared/EditButton.tsx | 2 +-
apps/fronted/src/components/shared/Logo.tsx | 68 +++++++
apps/fronted/src/components/ui/sidebar.tsx | 80 +++-----
apps/fronted/src/store/useResumeStore.ts | 175 ++++++++++++++----
apps/fronted/src/types/global.d.ts | 14 ++
apps/fronted/src/utils/fileSystem.ts | 118 ++++++++++++
15 files changed, 730 insertions(+), 272 deletions(-)
create mode 100644 apps/fronted/src/app/dashboard/layout.tsx
create mode 100644 apps/fronted/src/app/dashboard/page.tsx
create mode 100644 apps/fronted/src/app/dashboard/resumes/page.tsx
create mode 100644 apps/fronted/src/app/dashboard/settings/page.tsx
delete mode 100644 apps/fronted/src/app/workbench/page.tsx
create mode 100644 apps/fronted/src/components/shared/Logo.tsx
create mode 100644 apps/fronted/src/types/global.d.ts
create mode 100644 apps/fronted/src/utils/fileSystem.ts
diff --git a/apps/fronted/next.config.mjs b/apps/fronted/next.config.mjs
index 1b94064..25ae6da 100644
--- a/apps/fronted/next.config.mjs
+++ b/apps/fronted/next.config.mjs
@@ -1,6 +1,4 @@
/** @type {import('next').NextConfig} */
-import { fileURLToPath } from "url";
-import { dirname } from "path";
const nextConfig = {
typescript: {
diff --git a/apps/fronted/src/app/dashboard/layout.tsx b/apps/fronted/src/app/dashboard/layout.tsx
new file mode 100644
index 0000000..65aca4a
--- /dev/null
+++ b/apps/fronted/src/app/dashboard/layout.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { FileText, Settings, SettingsIcon } from "lucide-react";
+import { usePathname, useRouter } from "next/navigation";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
+import { useEffect, useState } from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import Logo from "@/components/shared/Logo";
+
+const sidebarItems = [
+ {
+ title: "简历",
+ url: "/dashboard/resumes",
+ icon: FileText,
+ },
+ {
+ title: "设置",
+ url: "/dashboard/settings",
+ icon: Settings,
+ },
+];
+
+const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
+ const router = useRouter();
+ const pathname = usePathname();
+ const [open, setOpen] = useState(true);
+ const [collapsible, setCollapsible] = useState<"offcanvas" | "icon" | "none">(
+ "icon"
+ );
+
+ useEffect(() => {
+ if (pathname.includes("/workbench")) {
+ setOpen(false);
+ return;
+ }
+ }, [pathname]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {sidebarItems.map((item) => (
+
+
+
+
+
+ {
+ router.push(item.url);
+ }}
+ >
+
+ {open && {item.title}}
+
+
+
+
+ {!open && (
+
+ {item.title}
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default DashboardLayout;
diff --git a/apps/fronted/src/app/dashboard/page.tsx b/apps/fronted/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..f3469d9
--- /dev/null
+++ b/apps/fronted/src/app/dashboard/page.tsx
@@ -0,0 +1,6 @@
+"use client";
+import { redirect } from "next/navigation";
+
+export default function Dashboard() {
+ redirect("/dashboard/resumes");
+}
diff --git a/apps/fronted/src/app/dashboard/resumes/page.tsx b/apps/fronted/src/app/dashboard/resumes/page.tsx
new file mode 100644
index 0000000..47e7e36
--- /dev/null
+++ b/apps/fronted/src/app/dashboard/resumes/page.tsx
@@ -0,0 +1,130 @@
+"use client";
+import React, { useEffect } from "react";
+import { Plus } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { getFileHandle, verifyPermission } from "@/utils/fileSystem";
+import { useResumeStore } from "@/store/useResumeStore";
+const ResumesList = () => {
+ return ;
+};
+
+const ResumeWorkbench = () => {
+ const {
+ resumes,
+ createResume,
+ deleteResume,
+ setActiveResume,
+ updateResume,
+ updateResumeFromFile,
+ } = useResumeStore();
+ const router = useRouter();
+
+ 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]);
+
+ const handleCreateResume = () => {
+ const newId = createResume(null);
+ setActiveResume(newId);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ 创建新简历
+ 从头开始创建一份新的简历
+
+
+
+ {Object.entries(resumes).map(([id, resume]) => (
+
+
+ {resume.title}
+
+
+
+ 创建于 {new Date(resume.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+
+ ))}
+
+
+ >
+ );
+};
+
+export default ResumesList;
diff --git a/apps/fronted/src/app/dashboard/settings/page.tsx b/apps/fronted/src/app/dashboard/settings/page.tsx
new file mode 100644
index 0000000..74f2b73
--- /dev/null
+++ b/apps/fronted/src/app/dashboard/settings/page.tsx
@@ -0,0 +1,118 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Folder } from "lucide-react";
+import { useState, useEffect } from "react";
+import { useConfigStore } from "@/store/useConfigStore";
+import {
+ getFileHandle,
+ getConfig,
+ storeFileHandle,
+ storeConfig,
+ verifyPermission,
+} from "@/utils/fileSystem";
+
+const SettingsPage = () => {
+ const [directoryHandle, setDirectoryHandle] =
+ useState(null);
+ const [folderPath, setFolderPath] = useState("");
+
+ useEffect(() => {
+ const loadSavedConfig = async () => {
+ try {
+ const handle = await getFileHandle("syncDirectory");
+ const path = await getConfig("syncDirectoryPath");
+ if (handle && path) {
+ const hasPermission = await verifyPermission(handle);
+ if (hasPermission) {
+ setDirectoryHandle(handle as FileSystemDirectoryHandle);
+ setFolderPath(path);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading saved config:", error);
+ }
+ };
+
+ loadSavedConfig();
+ }, []);
+
+ const handleSelectDirectory = async () => {
+ try {
+ if (!("showDirectoryPicker" in window)) {
+ alert(
+ "Your browser does not support directory selection. Please use a modern browser."
+ );
+ return;
+ }
+
+ const handle = await window.showDirectoryPicker({ mode: "readwrite" });
+ const hasPermission = await verifyPermission(handle);
+ if (hasPermission) {
+ setDirectoryHandle(handle);
+ // 获取文件夹路径
+ const path = handle.name;
+ setFolderPath(path);
+ // 存储文件句柄和路径
+ await storeFileHandle("syncDirectory", handle);
+ await storeConfig("syncDirectoryPath", path);
+ }
+ } catch (error) {
+ console.error("Error selecting directory:", error);
+ }
+ };
+
+ return (
+
+
+
系统设置
+
+
+
+
+
+
+ 文件同步配置
+
+
+ 请选择一个文件夹用于存储简历数据。所有的简历将会以 JSON
+ 格式保存在这个文件夹中。
+
+
+
+
+
+
+
+ {folderPath
+ ? `当前同步文件夹: ${folderPath}`
+ : "未配置同步文件夹"}
+
+
+
+
+ {!folderPath && (
+
+ 请先选择一个文件夹用于同步简历数据
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/apps/fronted/src/app/workbench/[id]/page.tsx b/apps/fronted/src/app/workbench/[id]/page.tsx
index 7f6c08b..138987d 100644
--- a/apps/fronted/src/app/workbench/[id]/page.tsx
+++ b/apps/fronted/src/app/workbench/[id]/page.tsx
@@ -10,7 +10,7 @@ import { PreviewPanel } from "@/components/preview/PreviewPanel";
import {
ResizableHandle,
ResizablePanel,
- ResizablePanelGroup
+ ResizablePanelGroup,
} from "@/components/ui/resizable";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
@@ -18,7 +18,7 @@ import {
Tooltip,
TooltipContent,
TooltipProvider,
- TooltipTrigger
+ TooltipTrigger,
} from "@/components/ui/tooltip";
const LAYOUT_CONFIG = {
@@ -27,10 +27,9 @@ const LAYOUT_CONFIG = {
EDIT_FOCUSED_WITH_SIDE: [0, 100, 0],
PREVIEW_FOCUSED_WITH_SIDE: [0, 0, 100],
EDIT_FOCUSED_NO_SIDE: [0, 100, 0],
- PREVIEW_FOCUSED_NO_SIDE: [0, 0, 100]
+ PREVIEW_FOCUSED_NO_SIDE: [0, 0, 100],
};
-// 创建自定义拖拽手柄组件
const DragHandle = ({ show = true }) => {
if (!show) return null;
diff --git a/apps/fronted/src/app/workbench/page.tsx b/apps/fronted/src/app/workbench/page.tsx
deleted file mode 100644
index 03c6be8..0000000
--- a/apps/fronted/src/app/workbench/page.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-"use client";
-import React, { useState } from "react";
-import { useResumeStore } from "@/store/useResumeStore";
-import { useRouter, usePathname } from "next/navigation";
-import { LayoutGrid, List, FileText, Plus, Settings } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- CardDescription,
- CardFooter,
-} from "@/components/ui/card";
-
-import {
- Sidebar,
- SidebarContent,
- SidebarFooter,
- SidebarGroup,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarProvider,
- SidebarTrigger,
-} from "@/components/ui/sidebar";
-
-const sidebarItems = [
- {
- title: "简历",
- url: "/workbench",
- icon: FileText,
- },
- {
- title: "设置",
- url: "#",
- icon: Settings,
- },
-];
-
-const ResumeWorkbench = () => {
- const { resumes, createResume, deleteResume } = useResumeStore();
- const router = useRouter();
- const pathname = usePathname();
- const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
- const { setActiveResume } = useResumeStore();
- return (
-
-
-
-
-
-
Magic Resume
-
-
-
-
- 导航
-
-
- {sidebarItems.map((item) => (
-
-
-
-
- {item.title}
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
- createResume(null)}
- >
-
- 创建新简历
- 从头开始创建
-
-
-
- {Object.entries(resumes).map(([id, resume]) => (
-
-
- {resume.title}
-
-
-
- 最后修改: {resume.updatedAt}
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
- );
-};
-
-export default ResumeWorkbench;
diff --git a/apps/fronted/src/components/editor/EditorHeader.tsx b/apps/fronted/src/components/editor/EditorHeader.tsx
index 0fc5be0..4bc570d 100644
--- a/apps/fronted/src/components/editor/EditorHeader.tsx
+++ b/apps/fronted/src/components/editor/EditorHeader.tsx
@@ -33,7 +33,7 @@ export function EditorHeader({ isMobile }: EditorHeaderProps) {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => {
- router.push("/");
+ router.push("/dashboard");
}}
>
Magic Resume
diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx
index 2469290..062cef1 100644
--- a/apps/fronted/src/components/editor/SidePanel.tsx
+++ b/apps/fronted/src/components/editor/SidePanel.tsx
@@ -570,7 +570,6 @@ export function SidePanel() {
checked={globalSettings.useIconMode}
onCheckedChange={(checked) =>
updateGlobalSettings({
- ...globalSettings,
useIconMode: checked,
})
}
diff --git a/apps/fronted/src/components/shared/EditButton.tsx b/apps/fronted/src/components/shared/EditButton.tsx
index c2e0ab8..2181af8 100644
--- a/apps/fronted/src/components/shared/EditButton.tsx
+++ b/apps/fronted/src/components/shared/EditButton.tsx
@@ -8,7 +8,7 @@ const EditButton = (
RefAttributes
) => {
return (
-
+
);
diff --git a/apps/fronted/src/components/shared/Logo.tsx b/apps/fronted/src/components/shared/Logo.tsx
new file mode 100644
index 0000000..f8899b4
--- /dev/null
+++ b/apps/fronted/src/components/shared/Logo.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+
+interface LogoProps {
+ size?: number;
+ className?: string;
+}
+
+const Logo: React.FC = ({ size = 100, className = "" }) => {
+ return (
+
+ );
+};
+
+export default Logo;
diff --git a/apps/fronted/src/components/ui/sidebar.tsx b/apps/fronted/src/components/ui/sidebar.tsx
index b2cd66d..23ab98e 100644
--- a/apps/fronted/src/components/ui/sidebar.tsx
+++ b/apps/fronted/src/components/ui/sidebar.tsx
@@ -23,7 +23,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
-const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_WIDTH_ICON = "4rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
@@ -505,88 +505,50 @@ const SidebarMenuItem = React.forwardRef<
));
SidebarMenuItem.displayName = "SidebarMenuItem";
-const sidebarMenuButtonVariants = cva(
- "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
- outline:
- "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
- },
- size: {
- default: "h-8 text-sm",
- sm: "h-7 text-xs",
- lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-);
-
const SidebarMenuButton = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps<"button"> & {
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
asChild?: boolean;
isActive?: boolean;
- tooltip?: string | React.ComponentProps;
- } & VariantProps
+ }
>(
(
{
asChild = false,
isActive = false,
- variant = "default",
- size = "default",
- tooltip,
className,
+ children,
...props
},
ref
) => {
- const Comp = asChild ? Slot : "button";
- const { isMobile, state } = useSidebar();
+ const Comp = asChild ? Slot : "a";
- const button = (
+ return (
svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:w-10 group-data-[collapsible=icon]:h-10",
+ isActive && "bg-sidebar-accent text-sidebar-accent-foreground",
+ className
+ )}
{...props}
- />
- );
-
- if (!tooltip) {
- return button;
- }
-
- if (typeof tooltip === "string") {
- tooltip = {
- children: tooltip,
- };
- }
-
- return (
-
- {button}
-
-
+ >
+ {children}
+
);
}
);
diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts
index 52a02fa..43066a3 100644
--- a/apps/fronted/src/store/useResumeStore.ts
+++ b/apps/fronted/src/store/useResumeStore.ts
@@ -1,6 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { DEFAULT_FIELD_ORDER } from "@/config";
+import { getFileHandle, getConfig, verifyPermission } from "@/utils/fileSystem";
import {
BasicInfo,
Education,
@@ -18,11 +19,12 @@ interface ResumeStore {
activeResumeId: string | null;
activeResume: ResumeData | null;
- createResume: (templateId: string | null) => void;
- deleteResume: (resumeId: string) => void;
+ createResume: (templateId: string | null) => string;
+ deleteResume: (resume: ResumeData) => void;
duplicateResume: (resumeId: string) => void;
updateResume: (resumeId: string, data: Partial) => void;
setActiveResume: (resumeId: string) => void;
+ updateResumeFromFile: (resume: ResumeData) => void;
updateBasicInfo: (data: Partial) => void;
updateEducation: (data: Education) => void;
@@ -201,20 +203,79 @@ const initialResumeState: Omit = {
globalSettings: initialGlobalSettings,
};
-export const useResumeStore = create()(
- persist(
+// 同步简历到文件系统
+const syncResumeToFile = async (resumeData: 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;
+ 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(
+ persist(
(set, get) => ({
resumes: {},
activeResumeId: null,
activeResume: null,
- updateResume: (resumeId, data) =>
+ createResume: (templateId = null) => {
+ const newId = crypto.randomUUID();
+ const newResume: ResumeData = {
+ ...initialResumeState,
+ id: newId,
+ templateId,
+ title: "新简历" + newId.slice(0, 6),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ set((state) => ({
+ resumes: {
+ ...state.resumes,
+ [newId]: newResume,
+ },
+ activeResumeId: newId,
+ activeResume: newResume,
+ }));
+
+ syncResumeToFile(newResume);
+
+ return newId;
+ },
+
+ updateResume: (resumeId, data) => {
set((state) => {
+ const resume = state.resumes[resumeId];
+ if (!resume) return state;
+
const updatedResume = {
- ...state.resumes[resumeId],
+ ...resume,
...data,
updatedAt: new Date().toISOString(),
};
+
+ syncResumeToFile(updatedResume);
+
return {
resumes: {
...state.resumes,
@@ -225,36 +286,46 @@ export const useResumeStore = create()(
? updatedResume
: state.activeResume,
};
- }),
-
- createResume: (templateId = null) => {
- const newId = crypto.randomUUID();
- const newResume: ResumeData = {
- ...initialResumeState,
- id: newId,
- templateId,
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- };
- get().updateResume(newId, newResume);
- get().setActiveResume(newId);
+ });
},
- deleteResume: (resumeId) => {
- const { resumes, activeResumeId } = get();
- const { [resumeId]: _, ...remainingResumes } = resumes;
- const newActiveResumeId =
- activeResumeId === resumeId ? null : activeResumeId;
+ // 从文件更新,直接更新resumes
+ updateResumeFromFile: (resume) => {
+ set((state) => ({
+ resumes: {
+ ...state.resumes,
+ [resume.id]: resume,
+ },
+ }));
+ },
- set({ resumes: remainingResumes });
+ deleteResume: (resume) => {
+ const resumeId = resume.id;
+ set((state) => {
+ const { [resumeId]: _, activeResume, ...rest } = state.resumes;
+ return {
+ resumes: rest,
+ activeResumeId: null,
+ activeResume: null,
+ };
+ });
- if (newActiveResumeId) {
- get().updateResume(newActiveResumeId, {
- ...resumes[newActiveResumeId],
- });
- } else {
- set({ activeResumeId: 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);
+ }
+ })();
},
duplicateResume: (resumeId) => {
@@ -277,13 +348,32 @@ export const useResumeStore = create()(
set({ activeResume: resume, activeResumeId: resumeId });
}
},
+
updateBasicInfo: (data) => {
- const { activeResumeId, updateResume } = get();
- if (activeResumeId) {
- updateResume(activeResumeId, {
- basic: { ...get().resumes[activeResumeId].basic, ...data },
- });
- }
+ set((state) => {
+ if (!state.activeResume || !state.activeResumeId) return state;
+
+ const updatedResume = {
+ ...state.activeResume,
+ basic: {
+ ...state.activeResume.basic,
+ ...data,
+ },
+ updatedAt: new Date().toISOString(),
+ };
+
+ const newState = {
+ ...state,
+ resumes: {
+ ...state.resumes,
+ [state.activeResumeId]: updatedResume,
+ },
+ activeResume: updatedResume,
+ };
+
+ syncResumeToFile(updatedResume);
+ return newState;
+ });
},
updateEducation: (education) => {
@@ -535,14 +625,19 @@ export const useResumeStore = create()(
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
},
+
updateGlobalSettings: (settings: Partial) => {
- const { activeResumeId, updateResume } = get();
+ const { activeResumeId, updateResume, activeResume } = get();
if (activeResumeId) {
updateResume(activeResumeId, {
- globalSettings: settings,
+ globalSettings: {
+ ...activeResume?.globalSettings,
+ ...settings,
+ },
});
}
},
+
setColorTheme: (colorTheme) => {
const { activeResumeId, updateResume } = get();
if (activeResumeId) {
diff --git a/apps/fronted/src/types/global.d.ts b/apps/fronted/src/types/global.d.ts
new file mode 100644
index 0000000..810a626
--- /dev/null
+++ b/apps/fronted/src/types/global.d.ts
@@ -0,0 +1,14 @@
+declare global {
+ interface Window {
+ showDirectoryPicker(
+ options?: FilePickerOptions
+ ): Promise;
+ }
+}
+
+interface FilePickerOptions {
+ multiple?: boolean;
+ mode?: "read" | "readwrite";
+}
+
+export {};
diff --git a/apps/fronted/src/utils/fileSystem.ts b/apps/fronted/src/utils/fileSystem.ts
new file mode 100644
index 0000000..02d6878
--- /dev/null
+++ b/apps/fronted/src/utils/fileSystem.ts
@@ -0,0 +1,118 @@
+// 存储文件句柄和配置信息
+const DB_NAME = "FileHandleDB";
+const HANDLE_STORE = "handles";
+const CONFIG_STORE = "config";
+const DB_VERSION = 2;
+
+let db: IDBDatabase | null = null;
+
+const initDB = (): Promise => {
+ return new Promise((resolve, reject) => {
+ if (db) {
+ resolve();
+ return;
+ }
+
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ db = request.result;
+ resolve();
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+ if (!db.objectStoreNames.contains(HANDLE_STORE)) {
+ db.createObjectStore(HANDLE_STORE);
+ }
+ if (!db.objectStoreNames.contains(CONFIG_STORE)) {
+ db.createObjectStore(CONFIG_STORE);
+ }
+ };
+ });
+};
+
+export const storeFileHandle = async (
+ key: string,
+ handle: FileSystemHandle
+): Promise => {
+ await initDB();
+ if (!db) throw new Error("Database not initialized");
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(HANDLE_STORE, "readwrite");
+ const store = transaction.objectStore(HANDLE_STORE);
+ const request = store.put(handle, key);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ });
+};
+
+export const getFileHandle = async (
+ key: string
+): Promise => {
+ await initDB();
+ if (!db) throw new Error("Database not initialized");
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(HANDLE_STORE, "readonly");
+ const store = transaction.objectStore(HANDLE_STORE);
+ const request = store.get(key);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result);
+ });
+};
+
+export const storeConfig = async (key: string, value: any): Promise => {
+ await initDB();
+ if (!db) throw new Error("Database not initialized");
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(CONFIG_STORE, "readwrite");
+ const store = transaction.objectStore(CONFIG_STORE);
+ const request = store.put(value, key);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ });
+};
+
+export const getConfig = async (key: string): Promise => {
+ await initDB();
+ if (!db) throw new Error("Database not initialized");
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(CONFIG_STORE, "readonly");
+ const store = transaction.objectStore(CONFIG_STORE);
+ const request = store.get(key);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result);
+ });
+};
+
+export const verifyPermission = async (
+ handle: FileSystemHandle,
+ mode: FileSystemPermissionMode = "readwrite"
+): Promise => {
+ if (!handle) {
+ return false;
+ }
+
+ const options = { mode };
+
+ // 检查当前权限
+ if ((await handle.queryPermission(options)) === "granted") {
+ return true;
+ }
+
+ // 请求权限
+ if ((await handle.requestPermission(options)) === "granted") {
+ return true;
+ }
+
+ return false;
+};