diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx index cb320cd..3fb69b6 100644 --- a/apps/fronted/src/components/editor/EditPanel.tsx +++ b/apps/fronted/src/components/editor/EditPanel.tsx @@ -7,17 +7,7 @@ import { cn } from "@/lib/utils"; import BasicPanel from "./basic/BasicPanel"; import EducationPanel from "./education/EducationPanel"; import ProjectPanel from "./project/ProjectPanel"; -interface Project { - id: string; - name: string; - role: string; - date: string; - description: string; - technologies: string; - responsibilities: string; - achievements: string; - visible: boolean; -} +import ExperiencePanel from "./experience/ExperiencePanel"; export function EditPanel() { const { theme, activeSection, menuSections } = useResumeStore(); @@ -31,6 +21,9 @@ export function EditPanel() { return ; case "education": return ; + case "experience": + return ; + case "skills": default: return null; } diff --git a/apps/fronted/src/components/editor/PdfExport.tsx b/apps/fronted/src/components/editor/PdfExport.tsx index 462dffd..1559dc5 100644 --- a/apps/fronted/src/components/editor/PdfExport.tsx +++ b/apps/fronted/src/components/editor/PdfExport.tsx @@ -55,11 +55,14 @@ export function PdfExport() { scale: window.devicePixelRatio * 3, letterRendering: true, scrollY: -window.scrollY, + useCORS: true, + allowTaint: true, height: pageHeightPx * numberOfPages }, jsPDF: { unit: "mm", format: "a4", + useCORS: true, orientation: "portrait", putTotalPages: false } diff --git a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx new file mode 100644 index 0000000..e9a5117 --- /dev/null +++ b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx @@ -0,0 +1,367 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useResumeStore } from "@/store/useResumeStore"; +import { + AnimatePresence, + motion, + Reorder, + useDragControls +} from "framer-motion"; +import { ChevronDown, Eye, EyeOff, GripVertical, Trash2 } from "lucide-react"; +import { useCallback, useState } from "react"; +import Field from "../Field"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from "@/components/ui/alert-dialog"; +import { Experience } from "@/types/resume"; + +interface ProjectEditorProps { + experience: Experience; + onSave: (experience: Experience) => void; + onDelete: () => void; + onCancel: () => void; +} + +interface DeleteConfirmDialogProps { + projectName: string; + onDelete: () => void; +} + +const DeleteConfirmDialog: React.FC = ({ + projectName, + onDelete +}) => { + const theme = useResumeStore((state) => state.theme); + + return ( + + + e.stopPropagation()} + > + + + + e.stopPropagation()} + > + + + 确认删除经历 + + + 您确定要删除经历 {projectName} 吗?此操作无法撤销。 + + + + + 取消 + + void }) => { + e.preventDefault(); + onDelete(); + }} + className={cn( + theme === "dark" + ? "bg-red-600 hover:bg-red-700 text-white" + : "bg-red-600 hover:bg-red-700 text-white" + )} + > + 删除 + + + + + ); +}; + +const ProjectEditor: React.FC = ({ + experience, + onSave +}) => { + const theme = useResumeStore((state) => state.theme); + const [data, setData] = useState(experience); + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + if (!data.company || !data.position || !data.date) return; + onSave(data); + }; + + return ( + + + + + setData((prev) => ({ ...prev, company: value })) + } + placeholder="项目名称" + required + /> + + setData((prev) => ({ ...prev, position: value })) + } + placeholder="如:前端工程师" + required + /> + + setData((prev) => ({ ...prev, date: value }))} + placeholder="如:2023.01 - 2023.06" + required + /> + setData((prev) => ({ ...prev, details: value }))} + type="editor" + placeholder="简要描述项目的背景和目标..." + /> + + + + 保存修改 + + + + ); +}; + +const ExperienceItem = ({ experience }: { experience: Experience }) => { + const dragControls = useDragControls(); + const [expandedId, setExpandedId] = useState(null); + const { theme, updateExperience, deleteExperience } = useResumeStore(); + const [isUpdating, setIsUpdating] = useState(false); + + const handleVisibilityToggle = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + + if (isUpdating) return; + + setIsUpdating(true); + setTimeout(() => { + updateExperience({ + ...experience, + visible: !experience.visible + }); + setIsUpdating(false); + }, 10); + }, + [experience, updateExperience, isUpdating] + ); + + return ( + { + useResumeStore.getState().setDraggingProjectId(null); + }} + className={cn( + "rounded-lg border overflow-hidden flex group", + theme === "dark" + ? "bg-neutral-900/30 border-neutral-800" + : "bg-white border-gray-100" + )} + > + {/* 拖拽手柄区域 */} + { + if (expandedId === experience.id) return; + dragControls.start(event); + useResumeStore.getState().setDraggingProjectId(experience.id); + }} + onPointerUp={() => { + useResumeStore.getState().setDraggingProjectId(null); + }} + onPointerCancel={() => { + useResumeStore.getState().setDraggingProjectId(null); + }} + className={cn( + "w-12 flex items-center justify-center border-r shrink-0 touch-none", + theme === "dark" ? "border-neutral-800" : "border-gray-100", + expandedId === experience.id + ? "cursor-not-allowed" + : "cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50" + )} + > + + + + {/* 内容区域 */} + + { + if (expandedId === experience.id) { + setExpandedId(null); + } else { + setExpandedId(experience.id); + } + }} + > + + + {experience.company || "家里蹲公司"} + + + + + {experience.visible ? ( + + ) : ( + + )} + + { + deleteExperience(experience.id); + setExpandedId(null); + }} + /> + + + + + + + {expandedId === experience.id && ( + + e.stopPropagation()} + > + + { + updateExperience(updatedData); + setExpandedId(null); + }} + onDelete={() => { + deleteExperience(experience.id); + setExpandedId(null); + }} + onCancel={() => setExpandedId(null)} + /> + + + )} + + + + ); +}; + +export default ExperienceItem; diff --git a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx new file mode 100644 index 0000000..a87eae3 --- /dev/null +++ b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx @@ -0,0 +1,59 @@ +"use client"; +import { cn } from "@/lib/utils"; +import { useResumeStore } from "@/store/useResumeStore"; +import { Reorder } from "framer-motion"; +import { PlusCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import ExperienceItem from "./ExperienceItem"; +import { Experience } from "@/types/resume"; + +const ExperiencePanel = () => { + const { theme, experience = [], updateExperience } = useResumeStore(); + console.log(1111, "experienceexperience"); + const handleCreateProject = () => { + const newProject: Experience = { + id: crypto.randomUUID(), + company: "某科技有限公司", + position: "高级前端工程师", + date: "2020-至今", + details: "负责公司核心产品..." + }; + updateExperience(newProject); + }; + + return ( + + { + useResumeStore.setState({ experience: newOrder }); + }} + className="space-y-3" + > + {experience.map((item) => ( + + ))} + + + + 添加经历 + + + + ); +}; +export default ExperiencePanel; diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index 7de5ded..fa22646 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -37,8 +37,8 @@ interface ResumeStore { updateEducation: (data: Education) => void; deleteEducation: (id: string) => void; - updateExperience: (id: string, data: Partial) => void; - + updateExperience: (data: Experience) => void; + deleteExperience: (id: string) => void; // 菜单操作 reorderSections: (newOrder: typeof initialState.menuSections) => void; toggleSectionVisibility: (sectionId: string) => void; @@ -165,16 +165,21 @@ export const useResumeStore = create()( setDraggingProjectId: (id) => set({ draggingProjectId: id }), updateBasicInfo: (data) => { - console.log(data, "data"); set((state) => ({ basic: { ...state.basic, ...data } })); }, - updateExperience: (id, data) => - set((state) => ({ - experience: state.experience.map((exp) => - exp.id === id ? { ...exp, ...data } : exp + updateExperience: (experience) => + set((state) => { + const newExperience = state.experience.some( + (p) => p.id === experience.id ) - })), + ? state.experience.map((p) => + p.id === experience.id ? { ...experience } : p + ) + : [...state.experience, { ...experience }]; + + return { experience: newExperience }; + }), reorderSections: (newOrder) => { const updatedSections = newOrder.map((section, index) => ({ @@ -229,6 +234,12 @@ export const useResumeStore = create()( set((state) => ({ education: state.education.filter((p) => p.id !== id) })), + + deleteExperience: (id) => + set((state) => ({ + experience: state.experience.filter((p) => p.id !== id) + })), + toggleTheme: () => set((state) => ({ theme: state.theme === "light" ? "dark" : "light" diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts index 96d8701..5287b30 100644 --- a/apps/fronted/src/types/resume.ts +++ b/apps/fronted/src/types/resume.ts @@ -99,6 +99,7 @@ export interface Experience { position: string; date: string; details: string; + visible?: boolean; } export interface Skill {