From dee3680f9e64c73fd8be854bd18f26bc53f78b89 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Thu, 21 Nov 2024 23:31:05 +0800 Subject: [PATCH 01/10] feat: experience --- .../src/components/editor/EditPanel.tsx | 15 +- .../src/components/editor/PdfExport.tsx | 3 + .../editor/experience/ExperienceItem.tsx | 367 ++++++++++++++++++ .../editor/experience/ExperiencePanel.tsx | 59 +++ apps/fronted/src/store/useResumeStore.ts | 27 +- apps/fronted/src/types/resume.ts | 1 + 6 files changed, 453 insertions(+), 19 deletions(-) create mode 100644 apps/fronted/src/components/editor/experience/ExperienceItem.tsx create mode 100644 apps/fronted/src/components/editor/experience/ExperiencePanel.tsx 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 { From 1f9bf22f1c8a835ca15dcd8fac20049e1d7a9f02 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Thu, 21 Nov 2024 23:53:45 +0800 Subject: [PATCH 02/10] style: eye style --- apps/fronted/src/components/editor/SidePanel.tsx | 2 +- apps/fronted/src/components/editor/basic/BasicPanel.tsx | 4 ++-- .../src/components/editor/education/EducationItem.tsx | 6 +++--- .../src/components/editor/education/EducationPanel.tsx | 9 ++------- .../src/components/editor/experience/ExperienceItem.tsx | 2 +- .../src/components/editor/experience/ExperiencePanel.tsx | 9 ++------- .../src/components/editor/project/ProjectItem.tsx | 4 +--- .../src/components/editor/project/ProjectPanel.tsx | 9 ++------- 8 files changed, 14 insertions(+), 31 deletions(-) diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx index 4ee3b17..5a47472 100644 --- a/apps/fronted/src/components/editor/SidePanel.tsx +++ b/apps/fronted/src/components/editor/SidePanel.tsx @@ -178,7 +178,7 @@ export function SidePanel() { )} > {item.enabled ? ( - + ) : ( )} diff --git a/apps/fronted/src/components/editor/basic/BasicPanel.tsx b/apps/fronted/src/components/editor/basic/BasicPanel.tsx index 21aba7f..0c00b31 100644 --- a/apps/fronted/src/components/editor/basic/BasicPanel.tsx +++ b/apps/fronted/src/components/editor/basic/BasicPanel.tsx @@ -107,7 +107,7 @@ const CustomField: React.FC = ({ {field.visible ? ( onUpdate({ ...field, visible: !field.visible })} /> ) : ( @@ -291,7 +291,7 @@ const BasicPanel: React.FC = () => { onClick={() => toggleFieldVisibility(field.id, !field.visible)} > {field.visible ? ( - + ) : ( )} diff --git a/apps/fronted/src/components/editor/education/EducationItem.tsx b/apps/fronted/src/components/editor/education/EducationItem.tsx index 6744b5f..939f157 100644 --- a/apps/fronted/src/components/editor/education/EducationItem.tsx +++ b/apps/fronted/src/components/editor/education/EducationItem.tsx @@ -193,14 +193,14 @@ const DeleteConfirmDialog: React.FC = ({ theme === "dark" ? "text-neutral-200" : "text-gray-900" )} > - 确认删除项目 + 确认删除经历 - 您确定要删除项目 {schoolName} 吗?此操作无法撤销。 + 您确定要删除经历 {schoolName} 吗?此操作无法撤销。 @@ -355,7 +355,7 @@ const EducationItem = ({ education }: { education: Education }) => { onClick={handleVisibilityToggle} > {education.visible ? ( - + ) : ( )} diff --git a/apps/fronted/src/components/editor/education/EducationPanel.tsx b/apps/fronted/src/components/editor/education/EducationPanel.tsx index a8db8c0..028928d 100644 --- a/apps/fronted/src/components/editor/education/EducationPanel.tsx +++ b/apps/fronted/src/components/editor/education/EducationPanel.tsx @@ -48,15 +48,10 @@ const ProjectPanel = () => { - 添加项目 + 添加 diff --git a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx index e9a5117..94cce28 100644 --- a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx +++ b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx @@ -294,7 +294,7 @@ const ExperienceItem = ({ experience }: { experience: Experience }) => { onClick={handleVisibilityToggle} > {experience.visible ? ( - + ) : ( )} diff --git a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx index a87eae3..c8cb126 100644 --- a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx +++ b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx @@ -42,15 +42,10 @@ const ExperiencePanel = () => { - 添加经历 + 添加 diff --git a/apps/fronted/src/components/editor/project/ProjectItem.tsx b/apps/fronted/src/components/editor/project/ProjectItem.tsx index d7971c2..0b20992 100644 --- a/apps/fronted/src/components/editor/project/ProjectItem.tsx +++ b/apps/fronted/src/components/editor/project/ProjectItem.tsx @@ -225,7 +225,6 @@ const ProjectItem = ({ project }: { project: Project }) => { : "bg-white border-gray-100" )} > - {/* 拖拽手柄区域 */} { if (expandedId === project.id) return; @@ -256,7 +255,6 @@ const ProjectItem = ({ project }: { project: Project }) => { /> - {/* 内容区域 */} { onClick={handleVisibilityToggle} > {project.visible ? ( - + ) : ( )} diff --git a/apps/fronted/src/components/editor/project/ProjectPanel.tsx b/apps/fronted/src/components/editor/project/ProjectPanel.tsx index 8a89b60..0a1a7db 100644 --- a/apps/fronted/src/components/editor/project/ProjectPanel.tsx +++ b/apps/fronted/src/components/editor/project/ProjectPanel.tsx @@ -45,15 +45,10 @@ const ProjectPanel = () => { - 添加项目 + 添加 From 008c64639d34355e5666c5f82369e148d3240fa3 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 14:04:23 +0800 Subject: [PATCH 03/10] feat: custom section --- .../src/components/editor/EditPanel.tsx | 8 +- .../src/components/editor/SidePanel.tsx | 59 +++- .../components/editor/custom/CustomItem.tsx | 290 ++++++++++++++++++ .../components/editor/custom/CustomPanel.tsx | 54 ++++ .../editor/experience/ExperiencePanel.tsx | 3 +- .../src/components/preview/CustomSection.tsx | 91 ++++++ .../components/preview/EducationSection.tsx | 2 - .../src/components/preview/PreviewPanel.tsx | 17 +- .../src/components/preview/SectionTitle.tsx | 2 +- apps/fronted/src/store/useResumeStore.ts | 102 +++++- apps/fronted/src/types/resume.ts | 9 + 11 files changed, 624 insertions(+), 13 deletions(-) create mode 100644 apps/fronted/src/components/editor/custom/CustomItem.tsx create mode 100644 apps/fronted/src/components/editor/custom/CustomPanel.tsx create mode 100644 apps/fronted/src/components/preview/CustomSection.tsx diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx index 3fb69b6..cabfa43 100644 --- a/apps/fronted/src/components/editor/EditPanel.tsx +++ b/apps/fronted/src/components/editor/EditPanel.tsx @@ -8,6 +8,7 @@ import BasicPanel from "./basic/BasicPanel"; import EducationPanel from "./education/EducationPanel"; import ProjectPanel from "./project/ProjectPanel"; import ExperiencePanel from "./experience/ExperiencePanel"; +import CustomPanel from "./custom/CustomPanel"; export function EditPanel() { const { theme, activeSection, menuSections } = useResumeStore(); @@ -24,8 +25,13 @@ export function EditPanel() { case "experience": return ; case "skills": - default: return null; + default: + if (activeSection.startsWith("custom")) { + return ; + } else { + return ; + } } }; diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx index 5a47472..fce339a 100644 --- a/apps/fronted/src/components/editor/SidePanel.tsx +++ b/apps/fronted/src/components/editor/SidePanel.tsx @@ -9,7 +9,8 @@ import { Type, SpaceIcon, Palette, - Plus + Plus, + Trash2 } from "lucide-react"; import { useResumeStore } from "@/store/useResumeStore"; import { @@ -92,7 +93,9 @@ export function SidePanel() { globalSettings, updateGlobalSettings, colorTheme, - setColorTheme + setColorTheme, + updateMenuSections, + addCustomData } = useResumeStore(); const debouncedSetColor = useMemo( @@ -103,6 +106,27 @@ export function SidePanel() { [] ); + const generateCustomSectionId = (menuSections: any[]) => { + const customSections = menuSections.filter((s) => + s.id.startsWith("custom") + ); + const nextNum = customSections.length + 1; + return `custom-${nextNum}`; + }; + + const handleCreateSection = () => { + const sectionId = generateCustomSectionId(menuSections); + const newSection = { + id: sectionId, + title: sectionId, + icon: "➕", + enabled: true, + order: menuSections.length + }; + + updateMenuSections([...menuSections, newSection]); + addCustomData(sectionId); + }; return ( )} + {/* 删除按钮 */} + + { + updateMenuSections( + menuSections.filter((section) => section.id !== item.id) + ); + setActiveSection(menuSections[0].id); + }} + className={cn( + "p-1.5 rounded-md text-primary", + theme === "dark" + ? "hover:bg-neutral-700 text-neutral-300" + : "hover:bg-gray-100 text-gray-600" + )} + > + + ))} + {/* 添加自定义模块 */} + + + 添加自定义模块 + + {/* 主题色设置 */} diff --git a/apps/fronted/src/components/editor/custom/CustomItem.tsx b/apps/fronted/src/components/editor/custom/CustomItem.tsx new file mode 100644 index 0000000..1d85cd4 --- /dev/null +++ b/apps/fronted/src/components/editor/custom/CustomItem.tsx @@ -0,0 +1,290 @@ +import { useCallback, useState } from "react"; +import { + motion, + useDragControls, + Reorder, + AnimatePresence +} from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useResumeStore } from "@/store/useResumeStore"; +import { GripVertical, Eye, EyeOff, ChevronDown, Trash2 } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from "@/components/ui/alert-dialog"; +import Field from "../Field"; + +import { CustomItem as CustomItemType } from "@/types/resume"; +const CustomItemEditor = ({ + item, + onSave +}: { + item: CustomItemType; + onSave: (item: CustomItemType) => void; +}) => { + const handleChange = (field: keyof CustomItemType, value: string) => { + onSave({ ...item, [field]: value }); + }; + + return ( + + + + handleChange("title", value)} + placeholder="标题" + /> + handleChange("subtitle", value)} + placeholder="副标题" + /> + + + handleChange("dateRange", value)} + placeholder="例如: 2023.01 - 2024.01" + /> + + handleChange("description", value)} + type="editor" + placeholder="请输入详细描述..." + /> + + + ); +}; + +const DeleteConfirmDialog = ({ + title, + onDelete +}: { + title: string; + onDelete: () => void; +}) => { + const theme = useResumeStore((state) => state.theme); + + return ( + + + e.stopPropagation()} + > + + + + e.stopPropagation()} + > + + 确认删除 + + 您确定要删除 {title || "此项"} 吗?此操作无法撤销。 + + + + 取消 + + 删除 + + + + + ); +}; + +const CustomItem = ({ + item, + sectionId +}: { + item: CustomItemType; + sectionId: string; +}) => { + const dragControls = useDragControls(); + const [expandedId, setExpandedId] = useState(null); + const { theme, updateCustomItem, removeCustomItem } = useResumeStore(); + + const [isUpdating, setIsUpdating] = useState(false); + + const handleVisibilityToggle = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (isUpdating) return; + + setIsUpdating(true); + setTimeout(() => { + updateCustomItem(sectionId, item.id, { visible: !item.visible }); + setIsUpdating(false); + }, 10); + }, + [item, updateCustomItem, isUpdating, sectionId] + ); + + return ( + + { + if (expandedId === item.id) return; + dragControls.start(event); + }} + className={cn( + "w-12 flex items-center justify-center border-r shrink-0 touch-none", + theme === "dark" ? "border-neutral-800" : "border-gray-100", + expandedId === item.id + ? "cursor-not-allowed" + : "cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50" + )} + > + + + + + setExpandedId(expandedId === item.id ? null : item.id)} + > + + + {item.title || "未命名模块"} + + {item.subtitle && ( + + {item.subtitle} + + )} + + + + + {item.visible ? ( + + ) : ( + + )} + + { + removeCustomItem(sectionId, item.id); + setExpandedId(null); + }} + /> + + + + + + + + {expandedId === item.id && ( + + + + { + updateCustomItem(sectionId, item.id, updatedItem); + }} + /> + + + )} + + + + ); +}; + +export default CustomItem; diff --git a/apps/fronted/src/components/editor/custom/CustomPanel.tsx b/apps/fronted/src/components/editor/custom/CustomPanel.tsx new file mode 100644 index 0000000..1d11e64 --- /dev/null +++ b/apps/fronted/src/components/editor/custom/CustomPanel.tsx @@ -0,0 +1,54 @@ +import { memo } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Reorder } from "framer-motion"; +import { PlusCircle } from "lucide-react"; +import CustomItem from "./CustomItem"; +import { useResumeStore } from "@/store/useResumeStore"; +import { CustomItem as CustomItemType } from "@/types/resume"; + +const CustomPanel = memo(({ sectionId }: { sectionId: string }) => { + const { customData, addCustomItem, updateCustomData } = useResumeStore(); + const items = customData[sectionId] || []; + const handleCreateItem = () => { + addCustomItem(sectionId); + }; + + return ( + + { + updateCustomData(sectionId, newOrder); + }} + className="space-y-3" + > + {items.map((item: CustomItemType) => ( + + ))} + + + + 添加 + + + + ); +}); + +CustomPanel.displayName = "CustomPanel"; + +export default CustomPanel; diff --git a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx index c8cb126..3390b16 100644 --- a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx +++ b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx @@ -16,7 +16,8 @@ const ExperiencePanel = () => { company: "某科技有限公司", position: "高级前端工程师", date: "2020-至今", - details: "负责公司核心产品..." + details: "负责公司核心产品...", + visible: true }; updateExperience(newProject); }; diff --git a/apps/fronted/src/components/preview/CustomSection.tsx b/apps/fronted/src/components/preview/CustomSection.tsx new file mode 100644 index 0000000..c0bad4a --- /dev/null +++ b/apps/fronted/src/components/preview/CustomSection.tsx @@ -0,0 +1,91 @@ +"use client"; +import { motion } from "framer-motion"; +import { GlobalSettings } from "@/types/resume"; +import { SectionTitle } from "./SectionTitle"; +import { CustomItem } from "@/types/resume"; + +interface CustomSectionProps { + sectionId: string; + title: string; + items: CustomItem[]; + globalSettings: GlobalSettings | undefined; + themeColor: string; +} + +export function CustomSection({ + title, + items, + globalSettings, + themeColor +}: CustomSectionProps) { + const visibleItems = items.filter((item) => { + return item.visible && item.description; + }); + + return ( + + + {visibleItems.map((item) => ( + + + + + + {item.title} + + + {item.subtitle} + + + + {item.dateRange && ( + + {item.dateRange} + + )} + + {item.description && ( + + )} + + ))} + + ); +} diff --git a/apps/fronted/src/components/preview/EducationSection.tsx b/apps/fronted/src/components/preview/EducationSection.tsx index 3f7f197..07d68e1 100644 --- a/apps/fronted/src/components/preview/EducationSection.tsx +++ b/apps/fronted/src/components/preview/EducationSection.tsx @@ -14,9 +14,7 @@ export function EducationSection({ globalSettings, themeColor }: EducationSectionProps) { - // 只显示visible为true的教育经历 const visibleEducation = education.filter((edu) => edu.visible); - return ( { switch (fontFamily) { @@ -67,7 +68,8 @@ export function PreviewPanel() { globalSettings, projects, draggingProjectId, - colorTheme + colorTheme, + customData } = useResumeStore(); const previewRef = React.useRef(null); @@ -196,6 +198,19 @@ export function PreviewPanel() { }, [contentHeight]); const renderSection = (sectionId: string) => { + if (sectionId.startsWith("custom")) { + const sectionConfig = menuSections.find((s) => s.id === sectionId); + return ( + + ); + } + switch (sectionId) { case "education": return ( diff --git a/apps/fronted/src/components/preview/SectionTitle.tsx b/apps/fronted/src/components/preview/SectionTitle.tsx index 835f988..959ce9e 100644 --- a/apps/fronted/src/components/preview/SectionTitle.tsx +++ b/apps/fronted/src/components/preview/SectionTitle.tsx @@ -3,7 +3,7 @@ import { GlobalSettings } from "@/types/resume"; interface SectionTitleProps { title: string; themeColor: string; - globalSettings: GlobalSettings; + globalSettings?: GlobalSettings; } export function SectionTitle({ diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index fa22646..95f1c0d 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -6,9 +6,15 @@ import { Experience, GlobalSettings, DEFAULT_CONFIG, - Project + Project, + CustomItem } from "../types/resume"; +interface CustomSection { + id: string; + items: CustomItem[]; +} + interface ResumeStore { // 基础数据 basic: BasicInfo; @@ -23,13 +29,13 @@ interface ResumeStore { enabled: boolean; order: number; }[]; + customData: Record; - // 主题设置 theme: "light" | "dark"; activeSection: string; - colorTheme: string; // 当前使用的主题色 ID - + // 当前使用的主题色 ID + colorTheme: string; setColorTheme: (colorTheme: string) => void; // Actions @@ -43,6 +49,19 @@ interface ResumeStore { reorderSections: (newOrder: typeof initialState.menuSections) => void; toggleSectionVisibility: (sectionId: string) => void; setActiveSection: (sectionId: string) => void; + updateMenuSections: (sections: typeof initialState.menuSections) => void; + + addCustomData: (sectionId: string) => void; + updateCustomData: (sectionId: string, items: CustomItem[]) => void; + removeCustomData: (sectionId: string) => void; + addCustomItem: (sectionId: string) => void; + updateCustomItem: ( + sectionId: string, + itemId: string, + updates: Partial + ) => void; + removeCustomItem: (sectionId: string, itemId: string) => void; + toggleTheme: () => void; // 全局设置 globalSettings: GlobalSettings; @@ -107,6 +126,7 @@ const initialState = { { id: "skills", title: "技能特长", icon: "⚡", enabled: true, order: 3 }, { id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 4 } ], + customData: {}, theme: "light" as const, colorTheme: "#2563eb", @@ -201,6 +221,8 @@ export const useResumeStore = create()( setActiveSection: (sectionId) => set({ activeSection: sectionId }), + updateMenuSections: (sections) => set({ menuSections: sections }), + updateProjects: (project) => set((state) => { const newProjects = state.projects.some((p) => p.id === project.id) @@ -253,7 +275,77 @@ export const useResumeStore = create()( return { globalSettings: newSettings }; - }) + }), + addCustomData: (sectionId) => + set((state) => ({ + customData: { + ...state.customData, + [sectionId]: [ + { + id: crypto.randomUUID(), + title: "", + subtitle: "", + dateRange: "", + description: "", + visible: true + } + ] + } + })), + + updateCustomData: (sectionId, items) => + set((state) => ({ + customData: { + ...state.customData, + [sectionId]: items + } + })), + + removeCustomData: (sectionId) => + set((state) => { + const { [sectionId]: _, ...rest } = state.customData; + return { customData: rest }; + }), + + addCustomItem: (sectionId) => + set((state) => ({ + customData: { + ...state.customData, + [sectionId]: [ + ...(state.customData[sectionId] || []), + { + id: crypto.randomUUID(), + title: "", + subtitle: "", + dateRange: "", + description: "", + visible: true + } + ] + } + })), + + updateCustomItem: (sectionId, itemId, updates) => { + console.log(sectionId, "sectionId"); + set((state) => ({ + customData: { + ...state.customData, + [sectionId]: state.customData[sectionId].map((item) => + item.id === itemId ? { ...item, ...updates } : item + ) + } + })); + }, + + removeCustomItem: (sectionId, itemId) => + set((state) => ({ + customData: { + ...state.customData, + [sectionId]: state.customData[sectionId].filter( + (item) => item.id !== itemId + ) + } + })) }), { diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts index 5287b30..e1a823f 100644 --- a/apps/fronted/src/types/resume.ts +++ b/apps/fronted/src/types/resume.ts @@ -140,6 +140,15 @@ export interface ResumeTheme { color: string; } +export interface CustomItem { + id: string; + title: string; + subtitle: string; + dateRange: string; + description: string; + visible: boolean; +} + export const THEME_COLORS = [ "#2563eb", "#059669", From 0de260895041710353c7a869e51e3a9083ee5110 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 14:12:50 +0800 Subject: [PATCH 04/10] fix: experience preview visible --- .../editor/experience/ExperiencePanel.tsx | 1 - .../components/preview/ExperienceSection.tsx | 81 ++++++++++--------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx index 3390b16..aa90f7f 100644 --- a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx +++ b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx @@ -9,7 +9,6 @@ import { Experience } from "@/types/resume"; const ExperiencePanel = () => { const { theme, experience = [], updateExperience } = useResumeStore(); - console.log(1111, "experienceexperience"); const handleCreateProject = () => { const newProject: Experience = { id: crypto.randomUUID(), diff --git a/apps/fronted/src/components/preview/ExperienceSection.tsx b/apps/fronted/src/components/preview/ExperienceSection.tsx index cdc78e4..e9a03d1 100644 --- a/apps/fronted/src/components/preview/ExperienceSection.tsx +++ b/apps/fronted/src/components/preview/ExperienceSection.tsx @@ -25,52 +25,55 @@ export function ExperienceSection({ themeColor={themeColor} globalSettings={globalSettings} /> - {experience.map((exp) => ( - - - - - {exp.company} - + {experience.map( + (exp) => + exp.visible && ( + + + + + {exp.company} + + + {exp.position} + + + + {exp.date} + + - {exp.position} + {exp.details} - - {exp.date} - - - - {exp.details} - - - ))} + ) + )} ); } From 1042eb45d29e1599fbe616a0ebc3fbe87ba626e3 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 14:32:09 +0800 Subject: [PATCH 05/10] style: editorPanel border style --- apps/fronted/src/components/editor/custom/CustomItem.tsx | 7 ++++--- .../src/components/editor/education/EducationItem.tsx | 7 ++++--- .../src/components/editor/experience/ExperienceItem.tsx | 7 ++++--- apps/fronted/src/components/editor/project/ProjectItem.tsx | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/fronted/src/components/editor/custom/CustomItem.tsx b/apps/fronted/src/components/editor/custom/CustomItem.tsx index 1d85cd4..efc00ee 100644 --- a/apps/fronted/src/components/editor/custom/CustomItem.tsx +++ b/apps/fronted/src/components/editor/custom/CustomItem.tsx @@ -158,9 +158,10 @@ const CustomItem = ({ dragControls={dragControls} className={cn( "rounded-lg border overflow-hidden flex group", - theme === "dark" - ? "bg-neutral-900/30 border-neutral-800" - : "bg-white border-gray-100" + "bg-white hover:border-indigo-600", + "dark:bg-neutral-900/30", + "border-gray-100 dark:border-neutral-800", + "dark:hover:border-indigo-600" )} > { dragControls={dragControls} className={cn( "rounded-lg border overflow-hidden flex group", - theme === "dark" - ? "bg-neutral-900/30 border-neutral-800" - : "bg-white border-gray-100" + "bg-white hover:border-indigo-600", + "dark:bg-neutral-900/30", + "border-gray-100 dark:border-neutral-800", + "dark:hover:border-indigo-600" )} > {/* 拖拽手柄区域 */} diff --git a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx index 94cce28..0027fa4 100644 --- a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx +++ b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx @@ -213,9 +213,10 @@ const ExperienceItem = ({ experience }: { experience: Experience }) => { }} className={cn( "rounded-lg border overflow-hidden flex group", - theme === "dark" - ? "bg-neutral-900/30 border-neutral-800" - : "bg-white border-gray-100" + "bg-white hover:border-indigo-600", + "dark:bg-neutral-900/30", + "border-gray-100 dark:border-neutral-800", + "dark:hover:border-indigo-600" )} > {/* 拖拽手柄区域 */} diff --git a/apps/fronted/src/components/editor/project/ProjectItem.tsx b/apps/fronted/src/components/editor/project/ProjectItem.tsx index 0b20992..7908e85 100644 --- a/apps/fronted/src/components/editor/project/ProjectItem.tsx +++ b/apps/fronted/src/components/editor/project/ProjectItem.tsx @@ -220,9 +220,10 @@ const ProjectItem = ({ project }: { project: Project }) => { }} className={cn( "rounded-lg border overflow-hidden flex group", - theme === "dark" - ? "bg-neutral-900/30 border-neutral-800" - : "bg-white border-gray-100" + "bg-white hover:border-indigo-600", + "dark:bg-neutral-900/30", + "border-gray-100 dark:border-neutral-800", + "dark:hover:border-indigo-600" )} > Date: Fri, 22 Nov 2024 14:48:58 +0800 Subject: [PATCH 06/10] perf: customPanel pref --- apps/fronted/src/components/editor/custom/CustomItem.tsx | 8 ++++++-- apps/fronted/src/components/preview/CustomSection.tsx | 2 +- apps/fronted/src/store/useResumeStore.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/fronted/src/components/editor/custom/CustomItem.tsx b/apps/fronted/src/components/editor/custom/CustomItem.tsx index efc00ee..18f451a 100644 --- a/apps/fronted/src/components/editor/custom/CustomItem.tsx +++ b/apps/fronted/src/components/editor/custom/CustomItem.tsx @@ -103,7 +103,11 @@ const DeleteConfirmDialog = ({ )} onClick={(e) => e.stopPropagation()} > - + 确认删除 您确定要删除 {title || "此项"} 吗?此操作无法撤销。 @@ -189,7 +193,7 @@ const CustomItem = ({ { - return item.visible && item.description; + return item.visible && (item.title || item.description); }); return ( diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index 95f1c0d..b08bea1 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -315,7 +315,7 @@ export const useResumeStore = create()( ...(state.customData[sectionId] || []), { id: crypto.randomUUID(), - title: "", + title: "未命名模块", subtitle: "", dateRange: "", description: "", From 09f9cf762cb4f19de0fe041087035c6ff0c96175 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 14:50:10 +0800 Subject: [PATCH 07/10] perf: customPanel pref --- apps/fronted/src/store/useResumeStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index b08bea1..4a894c1 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -283,7 +283,7 @@ export const useResumeStore = create()( [sectionId]: [ { id: crypto.randomUUID(), - title: "", + title: "未命名模块", subtitle: "", dateRange: "", description: "", From 7f3b62845199f549cc1e55374b487084f2286dab Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 15:02:47 +0800 Subject: [PATCH 08/10] perf: sidePanel Section Delete --- apps/fronted/src/components/editor/SidePanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx index fce339a..dfd4996 100644 --- a/apps/fronted/src/components/editor/SidePanel.tsx +++ b/apps/fronted/src/components/editor/SidePanel.tsx @@ -216,7 +216,11 @@ export function SidePanel() { updateMenuSections( menuSections.filter((section) => section.id !== item.id) ); - setActiveSection(menuSections[0].id); + setActiveSection( + menuSections[ + menuSections.findIndex((s) => s.id === item.id) - 1 + ].id + ); }} className={cn( "p-1.5 rounded-md text-primary", From 48b27462402dc2ffd391354b4c15d378a8c139aa Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 15:23:28 +0800 Subject: [PATCH 09/10] perf: delete editor save button --- .../editor/education/EducationItem.tsx | 92 +++++-------------- .../editor/experience/ExperienceItem.tsx | 56 ++++------- .../components/editor/project/ProjectItem.tsx | 69 +++++++------- .../components/preview/ExperienceSection.tsx | 7 +- 4 files changed, 80 insertions(+), 144 deletions(-) diff --git a/apps/fronted/src/components/editor/education/EducationItem.tsx b/apps/fronted/src/components/editor/education/EducationItem.tsx index 98fcf4d..b237f01 100644 --- a/apps/fronted/src/components/editor/education/EducationItem.tsx +++ b/apps/fronted/src/components/editor/education/EducationItem.tsx @@ -26,7 +26,7 @@ import { interface EducationEditorProps { education: Education; - onSave: (educaton: Education) => void; + onSave: (education: Education) => void; onDelete: () => void; onCancel: () => void; } @@ -40,41 +40,28 @@ const EducationEditor: React.FC = ({ education, onSave }) => { - const theme = useResumeStore((state) => state.theme); - const [data, setData] = useState(education); - - const handleSave = (e: React.FormEvent) => { - e.preventDefault(); - if ( - !data.school || - !data.major || - !data.degree || - !data.startDate || - !data.endDate - ) - return; - onSave(data); + const handleChange = (field: keyof Education, value: string) => { + onSave({ + ...education, + [field]: value + }); }; return ( - + - setData((prev) => ({ ...prev, school: value })) - } + value={education.school} + onChange={(value) => handleChange("school", value)} placeholder="学校名称" required /> - setData((prev) => ({ ...prev, location: value })) - } + value={education.location || ""} + onChange={(value) => handleChange("location", value)} placeholder="城市" /> @@ -82,40 +69,33 @@ const EducationEditor: React.FC = ({ setData((prev) => ({ ...prev, major: value }))} + value={education.major} + onChange={(value) => handleChange("major", value)} placeholder="专业名称" required /> - setData((prev) => ({ ...prev, degree: value })) - } + value={education.degree} + onChange={(value) => handleChange("degree", value)} placeholder="学历" required /> - {/* 时间和GPA */} - setData((prev) => ({ ...prev, startDate: value })) - } + value={education.startDate} + onChange={(value) => handleChange("startDate", value)} type="date" placeholder="YYYY-MM" required /> - setData((prev) => ({ ...prev, endDate: value })) - } + value={education.endDate} + onChange={(value) => handleChange("endDate", value)} type="date" placeholder="YYYY-MM" required @@ -124,37 +104,20 @@ const EducationEditor: React.FC = ({ setData((prev) => ({ ...prev, gpa: value }))} + value={education.gpa || ""} + onChange={(value) => handleChange("gpa", value)} placeholder="选填" /> - {/* 在校经历描述 */} - setData((prev) => ({ ...prev, description: value })) - } + value={education.description} + onChange={(value) => handleChange("description", value)} type="editor" placeholder="描述你的在校表现、获奖经历等..." /> - - - - 保存修改 - - - + ); }; @@ -271,7 +234,6 @@ const EducationItem = ({ education }: { education: Education }) => { "dark:hover:border-indigo-600" )} > - {/* 拖拽手柄区域 */} { if (expandedId === education.id) return; @@ -295,7 +257,6 @@ const EducationItem = ({ education }: { education: Education }) => { /> - {/* 内容区域 */} { /> { - updateEducation(updatedEducation); - setExpandedId(null); - }} + onSave={updateEducation} onDelete={() => { deleteEducation(education.id); setExpandedId(null); diff --git a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx index 0027fa4..45bf67a 100644 --- a/apps/fronted/src/components/editor/experience/ExperienceItem.tsx +++ b/apps/fronted/src/components/editor/experience/ExperienceItem.tsx @@ -114,67 +114,48 @@ 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); + const handleChange = (field: keyof Experience, value: string) => { + onSave({ + ...experience, + [field]: value + }); }; return ( - + - setData((prev) => ({ ...prev, company: value })) - } + value={experience.company} + onChange={(value) => handleChange("company", value)} placeholder="项目名称" required /> - setData((prev) => ({ ...prev, position: value })) - } + value={experience.position} + onChange={(value) => handleChange("position", value)} placeholder="如:前端工程师" required /> setData((prev) => ({ ...prev, date: value }))} + value={experience.date} + onChange={(value) => handleChange("date", value)} placeholder="如:2023.01 - 2023.06" required /> setData((prev) => ({ ...prev, details: value }))} + value={experience.details} + onChange={(value) => handleChange("details", value)} type="editor" placeholder="简要描述项目的背景和目标..." /> - - - 保存修改 - - - + ); }; @@ -219,7 +200,6 @@ const ExperienceItem = ({ experience }: { experience: Experience }) => { "dark:hover:border-indigo-600" )} > - {/* 拖拽手柄区域 */} { if (expandedId === experience.id) return; @@ -250,7 +230,6 @@ const ExperienceItem = ({ experience }: { experience: Experience }) => { /> - {/* 内容区域 */} { /> { - updateExperience(updatedData); - setExpandedId(null); - }} + onSave={updateExperience} onDelete={() => { deleteExperience(experience.id); setExpandedId(null); diff --git a/apps/fronted/src/components/editor/project/ProjectItem.tsx b/apps/fronted/src/components/editor/project/ProjectItem.tsx index 7908e85..378d905 100644 --- a/apps/fronted/src/components/editor/project/ProjectItem.tsx +++ b/apps/fronted/src/components/editor/project/ProjectItem.tsx @@ -122,65 +122,69 @@ const DeleteConfirmDialog: React.FC = ({ }; const ProjectEditor: React.FC = ({ project, onSave }) => { - const theme = useResumeStore((state) => state.theme); - const [data, setData] = useState(project); - - const handleSave = (e: React.FormEvent) => { - e.preventDefault(); - if (!data.name || !data.role || !data.date) return; - onSave(data); + const handleChange = (field: keyof Project, value: string) => { + onSave({ + ...project, + [field]: value + }); }; return ( - + setData((prev) => ({ ...prev, name: value }))} + value={project.name} + onChange={(value) => handleChange("name", value)} placeholder="项目名称" required /> setData((prev) => ({ ...prev, role: value }))} + value={project.role} + onChange={(value) => handleChange("role", value)} placeholder="如:前端负责人" required /> setData((prev) => ({ ...prev, date: value }))} + value={project.date} + onChange={(value) => handleChange("date", value)} placeholder="如:2023.01 - 2023.06" required /> - setData((prev) => ({ ...prev, description: value })) - } + value={project.description} + onChange={(value) => handleChange("description", value)} type="editor" placeholder="简要描述项目的背景和目标..." /> + handleChange("technologies", value)} + type="editor" + placeholder="使用的技术栈和工具..." + /> + handleChange("responsibilities", value)} + type="editor" + placeholder="在项目中的主要职责..." + /> + handleChange("achievements", value)} + type="editor" + placeholder="项目取得的成果..." + /> - - - 保存修改 - - - + ); }; @@ -353,7 +357,6 @@ const ProjectItem = ({ project }: { project: Project }) => { project={project} onSave={(updatedProject) => { updateProjects(updatedProject); - setExpandedId(null); }} onDelete={() => { deleteProject(project.id); diff --git a/apps/fronted/src/components/preview/ExperienceSection.tsx b/apps/fronted/src/components/preview/ExperienceSection.tsx index e9a03d1..f933120 100644 --- a/apps/fronted/src/components/preview/ExperienceSection.tsx +++ b/apps/fronted/src/components/preview/ExperienceSection.tsx @@ -62,15 +62,14 @@ export function ExperienceSection({ {exp.date} - - {exp.details} - + dangerouslySetInnerHTML={{ __html: exp.details }} + /> ) )} From 95424bf7482d63e96e3bf89b09f9ba4acf36813f Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 22 Nov 2024 17:39:52 +0800 Subject: [PATCH 10/10] feat: skills && default data --- .../workbench/compoents/Editor/RichText.tsx | 4 +- .../src/components/{editor => }/PdfExport.tsx | 0 .../src/components/editor/EditPanel.tsx | 3 +- .../src/components/editor/EditorHeader.tsx | 2 +- apps/fronted/src/components/editor/Field.tsx | 2 +- .../src/components/editor/IconSelector.tsx | 2 +- .../components/editor/skills/SkillPanel.tsx | 32 ++++ .../src/components/preview/PreviewPanel.tsx | 12 +- .../src/components/preview/SkillPanel.tsx | 48 ++++++ apps/fronted/src/store/useResumeStore.ts | 149 +++++++++++++----- apps/fronted/src/types/resume.ts | 4 +- 11 files changed, 207 insertions(+), 51 deletions(-) rename apps/fronted/src/components/{editor => }/PdfExport.tsx (100%) create mode 100644 apps/fronted/src/components/editor/skills/SkillPanel.tsx create mode 100644 apps/fronted/src/components/preview/SkillPanel.tsx diff --git a/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx index cb5d443..2500f0d 100644 --- a/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx +++ b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx @@ -584,7 +584,7 @@ const RichTextEditor = ({ {/* Bubble Menu */} - {editor && ( + {/* {editor && ( - )} + )} */} ); }; diff --git a/apps/fronted/src/components/editor/PdfExport.tsx b/apps/fronted/src/components/PdfExport.tsx similarity index 100% rename from apps/fronted/src/components/editor/PdfExport.tsx rename to apps/fronted/src/components/PdfExport.tsx diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx index cabfa43..2fa0ade 100644 --- a/apps/fronted/src/components/editor/EditPanel.tsx +++ b/apps/fronted/src/components/editor/EditPanel.tsx @@ -9,6 +9,7 @@ import EducationPanel from "./education/EducationPanel"; import ProjectPanel from "./project/ProjectPanel"; import ExperiencePanel from "./experience/ExperiencePanel"; import CustomPanel from "./custom/CustomPanel"; +import SkillPanel from "./skills/SkillPanel"; export function EditPanel() { const { theme, activeSection, menuSections } = useResumeStore(); @@ -25,7 +26,7 @@ export function EditPanel() { case "experience": return ; case "skills": - return null; + return ; default: if (activeSection.startsWith("custom")) { return ; diff --git a/apps/fronted/src/components/editor/EditorHeader.tsx b/apps/fronted/src/components/editor/EditorHeader.tsx index 1b26b50..fbb956d 100644 --- a/apps/fronted/src/components/editor/EditorHeader.tsx +++ b/apps/fronted/src/components/editor/EditorHeader.tsx @@ -4,7 +4,7 @@ import { motion } from "framer-motion"; import { Sun, Moon, Eye } from "lucide-react"; import { useResumeStore } from "@/store/useResumeStore"; import { getThemeConfig } from "@/theme/themeConfig"; -import { PdfExport } from "./PdfExport"; +import { PdfExport } from "../PdfExport"; interface EditorHeaderProps { isMobile?: boolean; diff --git a/apps/fronted/src/components/editor/Field.tsx b/apps/fronted/src/components/editor/Field.tsx index 0c6094f..7e97ccb 100644 --- a/apps/fronted/src/components/editor/Field.tsx +++ b/apps/fronted/src/components/editor/Field.tsx @@ -9,7 +9,7 @@ import { useResumeStore } from "@/store/useResumeStore"; import RichTextEditor from "@/app/workbench/compoents/Editor/RichText"; type FieldProps = { - label: string; + label?: string; value?: string; onChange: (value: string) => void; type?: "text" | "textarea" | "date" | "editor"; diff --git a/apps/fronted/src/components/editor/IconSelector.tsx b/apps/fronted/src/components/editor/IconSelector.tsx index e5bad82..d41eeb0 100644 --- a/apps/fronted/src/components/editor/IconSelector.tsx +++ b/apps/fronted/src/components/editor/IconSelector.tsx @@ -82,7 +82,7 @@ interface IconOption { } interface IconSelectorProps { - value: string; + value?: string; onChange: (value: string) => void; theme?: "light" | "dark"; } diff --git a/apps/fronted/src/components/editor/skills/SkillPanel.tsx b/apps/fronted/src/components/editor/skills/SkillPanel.tsx new file mode 100644 index 0000000..c814100 --- /dev/null +++ b/apps/fronted/src/components/editor/skills/SkillPanel.tsx @@ -0,0 +1,32 @@ +"use client"; +import { useResumeStore } from "@/store/useResumeStore"; +import Field from "../Field"; +import { cn } from "@/lib/utils"; + +const SkillPanel = () => { + const { updateSkillContent, skillContent } = useResumeStore(); + + const handleChange = (value: string) => { + updateSkillContent(value); + }; + + return ( + + + + ); +}; + +export default SkillPanel; diff --git a/apps/fronted/src/components/preview/PreviewPanel.tsx b/apps/fronted/src/components/preview/PreviewPanel.tsx index fe3b754..ca889cd 100644 --- a/apps/fronted/src/components/preview/PreviewPanel.tsx +++ b/apps/fronted/src/components/preview/PreviewPanel.tsx @@ -12,6 +12,7 @@ import { ProjectItem } from "./ProjectItem"; import { ExperienceSection } from "./ExperienceSection"; import { EducationSection } from "./EducationSection"; import { CustomSection } from "./CustomSection"; +import { SkillSection } from "./SkillPanel"; const getFontFamilyClass = (fontFamily: string) => { switch (fontFamily) { @@ -69,7 +70,8 @@ export function PreviewPanel() { projects, draggingProjectId, colorTheme, - customData + customData, + skillContent } = useResumeStore(); const previewRef = React.useRef(null); @@ -230,6 +232,14 @@ export function PreviewPanel() { ); case "projects": return renderProjects(); + case "skills": + return ( + + ); default: return null; } diff --git a/apps/fronted/src/components/preview/SkillPanel.tsx b/apps/fronted/src/components/preview/SkillPanel.tsx new file mode 100644 index 0000000..ae1ad5d --- /dev/null +++ b/apps/fronted/src/components/preview/SkillPanel.tsx @@ -0,0 +1,48 @@ +import { motion } from "framer-motion"; +import { GlobalSettings } from "@/types/resume"; +import { SectionTitle } from "./SectionTitle"; + +interface SkillSectionProps { + skill: string; + globalSettings: GlobalSettings | undefined; + themeColor: string; +} + +export function SkillSection({ + skill, + globalSettings, + themeColor +}: SkillSectionProps) { + if (!skill) { + return null; + } + + return ( + + + + + + + ); +} diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index 4a894c1..01a6659 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -72,6 +72,10 @@ interface ResumeStore { deleteProject: (id: string) => void; draggingProjectId: string | null; setDraggingProjectId: (id: string | null) => void; + + // 技能特长 + skillContent: string; + updateSkillContent: (skillContent: string) => void; } const initialState = { @@ -79,14 +83,17 @@ const initialState = { basic: { name: "张三", title: "高级前端工程师", - email: "example@email.com", + email: "zhangsan@example.com", phone: "13800138000", - location: "北京市", - birthDate: "", + location: "北京市朝阳区", + birthDate: "1995-01", icons: {}, photoConfig: DEFAULT_CONFIG, - customFields: [], - employementStatus: "", + customFields: [ + { id: "personal", label: "个人网站", value: "https://zhangsan.dev" }, + { id: "github", label: "GitHub", value: "https://github.com/zhangsan" } + ], + employementStatus: "全职", photo: "avatar.svg" }, education: [ @@ -95,22 +102,106 @@ const initialState = { school: "北京大学", major: "计算机科学与技术", degree: "本科", - startDate: "2019-09", - endDate: "2023-06", + startDate: "2013-09", + endDate: "2017-06", visible: true, gpa: "3.8/4.0", location: "北京", - description: - "主修课程:数据结构、算法设计、操作系统、计算机网络、数据库系统\n在校期间保持专业前10%,获得优秀学生奖学金,参与多个开源项目" + description: ` + 主修课程:数据结构、算法设计、操作系统、计算机网络、Web开发技术 + 专业排名前 5%,连续三年获得一等奖学金 + 担任计算机协会技术部部长,组织多次技术分享会 + 参与开源项目贡献,获得 GitHub Campus Expert 认证 + ` } ], + skillContent: ` + + 前端框架:精通 React、Vue.js,熟悉 Next.js、Nuxt.js 等 SSR 框架 + 开发语言:TypeScript、JavaScript(ES6+)、HTML5、CSS3 + UI/样式:精通 TailwindCSS、Sass/Less、CSS Module、Styled-components + 状态管理:Redux、Vuex、Zustand、Jotai、React Query + 工程化工具:Webpack、Vite、Rollup、Babel、ESLint + 测试工具:Jest、React Testing Library、Cypress + 性能优化:熟悉浏览器渲染原理、性能指标监控、代码分割、懒加载等优化技术 + 版本控制:Git、SVN + + + 技术管理:具备团队管理经验,主导过多个大型项目的技术选型和架构设计 + 技术分享:定期组织团队技术分享,主导建设团队技术博客 + 敏捷开发:熟悉 Scrum 开发流程,具有良好的项目把控能力 + 英语能力:CET-6 分数 560,具备良好的英文文档阅读和技术交流能力 + +`, experience: [ { id: "1", - company: "某科技有限公司", + company: "字节跳动", position: "高级前端工程师", - date: "2020-至今", - details: "负责公司核心产品..." + date: "2021.07 - 至今", + details: ` + 负责抖音创作者平台的开发与维护,主导多个核心功能的技术方案设计 + 优化项目工程化配置,将构建时间从 8 分钟优化至 2 分钟,提升团队开发效率 + 设计并实现组件库,提升代码复用率达 70%,显著减少开发时间 + 主导性能优化项目,使平台首屏加载时间减少 50%,接入 APM 监控系统 + 指导初级工程师,组织技术分享会,提升团队整体技术水平 + ` + } + ], + projects: [ + { + id: "p1", + name: "抖音创作者中台", + role: "前端负责人", + date: "2022.06 - 2023.12", + description: + "基于 React 的创作者数据分析和内容管理平台,服务百万级创作者群体,包含数据分析、内容管理、收益管理等多个子系统。", + technologies: + "React 18、TypeScript、TailwindCSS、Zustand、React Query、shadcn/ui、Recharts", + responsibilities: `- 主导项目技术选型和架构设计 +- 建立项目开发规范和 CI/CD 流程 +- 负责核心功能模块开发和性能优化 +- 指导团队成员,把控代码质量`, + achievements: `- 系统性能显著提升,首屏加载时间减少 50% +- 代码重用率提高到 80%,显著减少开发时间 +- 平台日活跃用户提升 200% +- 获得公司年度最佳项目奖`, + visible: true + }, + { + id: "p2", + name: "微信小程序开发者工具", + role: "核心开发者", + date: "2020.03 - 2021.06", + description: + "为开发者提供小程序开发、调试和发布的一站式解决方案。基于 Electron 构建的跨平台桌面应用。", + technologies: + "Electron、React、TypeScript、Monaco Editor、Node.js、WebSocket", + responsibilities: `- 负责编辑器核心功能开发 +- 实现实时预览和调试功能 +- 优化开发者使用体验 +- 处理性能问题和 Bug 修复`, + achievements: `- 编辑器性能提升 40% +- 月活用户增长 150% +- 获得最佳工具奖`, + visible: true + }, + { + id: "p3", + name: "前端监控平台", + role: "技术负责人", + date: "2021.09 - 2022.03", + description: + "一个完整的前端监控解决方案,包含错误监控、性能监控、用户行为分析等功能。", + technologies: "Vue 3、TypeScript、Vite、IndexedDB、WebWorker、Canvas", + responsibilities: `- 设计监控 SDK 和数据采集方案 +- 开发可视化数据面板 +- 优化数据处理性能 +- 制定监控告警策略`, + achievements: `- 准确捕获 99.9% 的前端异常 +- 帮助业务快速定位并解决问题 +- 平均故障排查时间减少 60%`, + visible: true } ], menuSections: [ @@ -133,40 +224,12 @@ const initialState = { activeSection: "basic", - projects: [ - { - id: "p1", - name: "企业中台系统", - role: "前端负责人", - date: "2023.06 - 2023.12", - description: - "基于 React 的企业级中台项目,包含工作流、报表、系统管理等多个子系统", - technologies: "React, TypeScript, TailwindCSS, shadcn/ui", - responsibilities: - "负责整体技术方案设计和团队管理,把控项目进度和代码质量", - achievements: "系统整体性能提升 50%,代码重用率提高到 80%", - visible: true - }, - { - id: "p2", - name: "xxx", - role: "前端负责人", - date: "2023.06 - 2023.12", - description: - "基于 React 的企业级中台项目,包含工作流、报表、系统管理等多个子系统", - technologies: "React, TypeScript, TailwindCSS, shadcn/ui", - responsibilities: - "负责整体技术方案设计和团队管理,把控项目进度和代码质量", - achievements: "系统整体性能提升 50%,代码重用率提高到 80%", - visible: true - } - ], globalSettings: { fontFamily: "sans", baseFontSize: 14, pagePadding: 20, paragraphSpacing: 20, - lineHeight: 1, + lineHeight: 1.2, sectionSpacing: 20, headerSize: 18, subheaderSize: 16, @@ -345,7 +408,9 @@ export const useResumeStore = create()( (item) => item.id !== itemId ) } - })) + })), + + updateSkillContent: (content) => set({ skillContent: content }) }), { diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts index e1a823f..ebd8e54 100644 --- a/apps/fronted/src/types/resume.ts +++ b/apps/fronted/src/types/resume.ts @@ -56,7 +56,7 @@ export interface CustomFieldType { id: string; label: string; value: string; - icon: string; + icon?: string; visible?: boolean; } export interface BasicInfo { @@ -75,7 +75,7 @@ export interface BasicInfo { id: string; label: string; value: string; - icon: string; + icon?: string; visible?: boolean; }>; }
+ {item.subtitle} +
+ {exp.position} +
- {exp.position} + {exp.details}
- {exp.details} -