From b26456d22bf62b4c53b60e70354ce50d295ed63d Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Tue, 19 Nov 2024 20:50:29 +0800 Subject: [PATCH] feat: education config --- .../workbench/compoents/Editor/RichText.tsx | 2 +- apps/fronted/src/app/workbench/page.tsx | 3 + .../src/components/editor/EditPanel.tsx | 95 +--- .../src/components/editor/EducationPanel.tsx | 0 apps/fronted/src/components/editor/Field.tsx | 2 +- .../editor/education/EducationItem.tsx | 428 ++++++++++++++++++ .../editor/education/EducationPanel.tsx | 65 +++ .../components/editor/project/ProjectItem.tsx | 7 +- .../editor/project/ProjectPanel.tsx | 62 +++ .../components/preview/EducationSection.tsx | 63 ++- .../src/components/preview/PreviewPanel.tsx | 2 +- .../src/components/preview/SectionTitle.tsx | 2 +- apps/fronted/src/store/useResumeStore.ts | 40 +- apps/fronted/src/types/resume.ts | 12 +- 14 files changed, 651 insertions(+), 132 deletions(-) create mode 100644 apps/fronted/src/components/editor/EducationPanel.tsx create mode 100644 apps/fronted/src/components/editor/education/EducationItem.tsx create mode 100644 apps/fronted/src/components/editor/education/EducationPanel.tsx create mode 100644 apps/fronted/src/components/editor/project/ProjectPanel.tsx diff --git a/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx index 573dda8..cb5d443 100644 --- a/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx +++ b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx @@ -36,7 +36,7 @@ import { useResumeStore } from "@/store/useResumeStore"; import { cn } from "@/lib/utils"; interface RichTextEditorProps { - content: string; + content?: string; onChange: (content: string) => void; placeholder?: string; } diff --git a/apps/fronted/src/app/workbench/page.tsx b/apps/fronted/src/app/workbench/page.tsx index 248a30e..15aa85d 100644 --- a/apps/fronted/src/app/workbench/page.tsx +++ b/apps/fronted/src/app/workbench/page.tsx @@ -89,6 +89,9 @@ export default function Home() { setPanelSizes(newSizes); setLayoutKey((prev) => prev + 1); }; + useEffect(() => { + document.documentElement.classList.remove("dark"); + }, []); useEffect(() => { let newSizes; diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx index db49ac0..cb320cd 100644 --- a/apps/fronted/src/components/editor/EditPanel.tsx +++ b/apps/fronted/src/components/editor/EditPanel.tsx @@ -1,27 +1,12 @@ "use client"; import React from "react"; -import { - motion, - AnimatePresence, - Reorder, - useDragControls -} from "framer-motion"; -import { - CalendarIcon, - PlusCircle, - ChevronDown, - Trash2, - X, - GripVertical -} from "lucide-react"; +import { motion } from "framer-motion"; import { useResumeStore } from "@/store/useResumeStore"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import Field from "./Field"; -import ProjectItem from "./project/ProjectItem"; import BasicPanel from "./basic/BasicPanel"; - +import EducationPanel from "./education/EducationPanel"; +import ProjectPanel from "./project/ProjectPanel"; interface Project { id: string; name: string; @@ -34,35 +19,8 @@ interface Project { visible: boolean; } -// 主面板组件 export function EditPanel() { - const { - theme, - activeSection, - menuSections, - basic, - projects = [], - updateBasicInfo, - updateProjects, - deleteProject - } = useResumeStore(); - - const [editingId, setEditingId] = React.useState(null); - const handleCreateProject = () => { - const newProject: Project = { - id: crypto.randomUUID(), - name: "Project Name", - role: "Project Role", - date: "", - description: "", - technologies: "", - responsibilities: "", - achievements: "", - visible: true - }; - setEditingId(newProject.id); - updateProjects(newProject); - }; + const { theme, activeSection, menuSections } = useResumeStore(); const renderFields = () => { switch (activeSection) { @@ -70,41 +28,9 @@ export function EditPanel() { return ; case "projects": - return ( -
- { - useResumeStore.setState({ projects: newOrder }); - }} - className="space-y-3" - > - {projects.map((project) => ( - - ))} - - {/* 添加项目按钮 */} - - -
- ); + return ; + case "education": + return ; default: return null; } @@ -146,10 +72,9 @@ export function EditPanel() { {renderFields()} diff --git a/apps/fronted/src/components/editor/EducationPanel.tsx b/apps/fronted/src/components/editor/EducationPanel.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/fronted/src/components/editor/Field.tsx b/apps/fronted/src/components/editor/Field.tsx index 4bd6825..0c6094f 100644 --- a/apps/fronted/src/components/editor/Field.tsx +++ b/apps/fronted/src/components/editor/Field.tsx @@ -10,7 +10,7 @@ import RichTextEditor from "@/app/workbench/compoents/Editor/RichText"; type FieldProps = { label: string; - value: string; + value?: string; onChange: (value: string) => void; type?: "text" | "textarea" | "date" | "editor"; placeholder?: string; diff --git a/apps/fronted/src/components/editor/education/EducationItem.tsx b/apps/fronted/src/components/editor/education/EducationItem.tsx new file mode 100644 index 0000000..6744b5f --- /dev/null +++ b/apps/fronted/src/components/editor/education/EducationItem.tsx @@ -0,0 +1,428 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useResumeStore } from "@/store/useResumeStore"; +import { Education } from "@/types/resume"; +import { + useDragControls, + Reorder, + motion, + AnimatePresence +} from "framer-motion"; +import { GripVertical, Eye, EyeOff, ChevronDown, Trash2 } from "lucide-react"; +import { useState, useCallback } from "react"; +import Field from "../Field"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from "@/components/ui/alert-dialog"; + +interface EducationEditorProps { + education: Education; + onSave: (educaton: Education) => void; + onDelete: () => void; + onCancel: () => void; +} + +interface DeleteConfirmDialogProps { + schoolName: string; + onDelete: () => void; +} + +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); + }; + + return ( +
+
+
+ + setData((prev) => ({ ...prev, school: value })) + } + placeholder="学校名称" + required + /> + + setData((prev) => ({ ...prev, location: value })) + } + placeholder="城市" + /> +
+ +
+ setData((prev) => ({ ...prev, major: value }))} + placeholder="专业名称" + required + /> + + setData((prev) => ({ ...prev, degree: value })) + } + placeholder="学历" + required + /> +
+ + {/* 时间和GPA */} +
+ + setData((prev) => ({ ...prev, startDate: value })) + } + type="date" + placeholder="YYYY-MM" + required + /> + + setData((prev) => ({ ...prev, endDate: value })) + } + type="date" + placeholder="YYYY-MM" + required + /> +
+ + setData((prev) => ({ ...prev, gpa: value }))} + placeholder="选填" + /> + + {/* 在校经历描述 */} + + setData((prev) => ({ ...prev, description: value })) + } + type="editor" + placeholder="描述你的在校表现、获奖经历等..." + /> +
+ +
+ +
+
+ ); +}; + +const DeleteConfirmDialog: React.FC = ({ + schoolName, + onDelete +}) => { + const theme = useResumeStore((state) => state.theme); + + return ( + + + + + e.stopPropagation()} + > + + + 确认删除项目 + + + 您确定要删除项目 {schoolName} 吗?此操作无法撤销。 + + + + + 取消 + + 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 EducationItem = ({ education }: { education: Education }) => { + const dragControls = useDragControls(); + const [expandedId, setExpandedId] = useState(null); + const { theme, updateEducation, deleteEducation } = useResumeStore(); + + const [isUpdating, setIsUpdating] = useState(false); + + const handleVisibilityToggle = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + + if (isUpdating) return; + + setIsUpdating(true); + setTimeout(() => { + updateEducation({ + ...education, + visible: !education.visible + }); + setIsUpdating(false); + }, 10); + }, + [education, updateEducation, isUpdating] + ); + + return ( + + {/* 拖拽手柄区域 */} +
{ + if (expandedId === education.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 === education.id + ? "cursor-not-allowed" + : "cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50" + )} + > + +
+ + {/* 内容区域 */} +
+
{ + if (expandedId === education.id) { + setExpandedId(null); + } else { + setExpandedId(education.id); + } + }} + > +
+
+
+

+ {education.school || "未填写学校"} +

+ {(education.major || education.degree) && ( +

+ {[education.major, education.degree] + .filter(Boolean) + .join(" · ")} +

+ )} +
+
+
+
+ + { + deleteEducation(education.id); + setExpandedId(null); + }} + /> + + + +
+
+ + {expandedId === education.id && ( + +
e.stopPropagation()} + > +
+ { + updateEducation(updatedEducation); + setExpandedId(null); + }} + onDelete={() => { + deleteEducation(education.id); + setExpandedId(null); + }} + onCancel={() => setExpandedId(null)} + /> +
+ + )} + +
+ + ); +}; + +export default EducationItem; diff --git a/apps/fronted/src/components/editor/education/EducationPanel.tsx b/apps/fronted/src/components/editor/education/EducationPanel.tsx new file mode 100644 index 0000000..a8db8c0 --- /dev/null +++ b/apps/fronted/src/components/editor/education/EducationPanel.tsx @@ -0,0 +1,65 @@ +"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 EducationItem from "./EducationItem"; +import { Education, Project } from "@/types/resume"; + +const ProjectPanel = () => { + const { theme, education = [], updateEducation } = useResumeStore(); + + const handleCreateProject = () => { + const newEducation: Education = { + id: crypto.randomUUID(), + school: "家里蹲大学", + major: "", + degree: "", + startDate: "2015-09-01", + endDate: "2019-06-30", + description: "", + visible: true + }; + updateEducation(newEducation); + }; + + return ( +
+ { + useResumeStore.setState({ education: newOrder }); + }} + className="space-y-3" + > + {education.map((education) => ( + + ))} + + + +
+ ); +}; +export default ProjectPanel; diff --git a/apps/fronted/src/components/editor/project/ProjectItem.tsx b/apps/fronted/src/components/editor/project/ProjectItem.tsx index 7121a3f..d7971c2 100644 --- a/apps/fronted/src/components/editor/project/ProjectItem.tsx +++ b/apps/fronted/src/components/editor/project/ProjectItem.tsx @@ -121,12 +121,7 @@ const DeleteConfirmDialog: React.FC = ({ ); }; -const ProjectEditor: React.FC = ({ - project, - onSave, - onDelete, - onCancel -}) => { +const ProjectEditor: React.FC = ({ project, onSave }) => { const theme = useResumeStore((state) => state.theme); const [data, setData] = useState(project); diff --git a/apps/fronted/src/components/editor/project/ProjectPanel.tsx b/apps/fronted/src/components/editor/project/ProjectPanel.tsx new file mode 100644 index 0000000..8a89b60 --- /dev/null +++ b/apps/fronted/src/components/editor/project/ProjectPanel.tsx @@ -0,0 +1,62 @@ +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 ProjectItem from "./ProjectItem"; +import { Project } from "@/types/resume"; + +const ProjectPanel = () => { + const { theme, projects = [], updateProjects } = useResumeStore(); + + const handleCreateProject = () => { + const newProject: Project = { + id: crypto.randomUUID(), + name: "Project Name", + role: "Project Role", + date: "", + description: "", + technologies: "", + responsibilities: "", + achievements: "", + visible: true + }; + updateProjects(newProject); + }; + + return ( +
+ { + useResumeStore.setState({ projects: newOrder }); + }} + className="space-y-3" + > + {projects.map((project) => ( + + ))} + + + +
+ ); +}; +export default ProjectPanel; diff --git a/apps/fronted/src/components/preview/EducationSection.tsx b/apps/fronted/src/components/preview/EducationSection.tsx index 24bb95f..1c76d66 100644 --- a/apps/fronted/src/components/preview/EducationSection.tsx +++ b/apps/fronted/src/components/preview/EducationSection.tsx @@ -1,3 +1,4 @@ +"use client"; import { motion } from "framer-motion"; import { Education, GlobalSettings } from "@/types/resume"; import { SectionTitle } from "./SectionTitle"; @@ -13,6 +14,9 @@ export function EducationSection({ globalSettings, themeColor }: EducationSectionProps) { + // 只显示visible为true的教育经历 + const visibleEducation = education.filter((edu) => edu.visible); + return ( - {education.map((edu) => ( + {visibleEducation.map((edu) => (
-
-

- {edu.school} -

+
+
+

+ {edu.school} +

+ {edu.location && ( + + · {edu.location} + + )} +

- {edu.degree} + {[edu.major, edu.degree].filter(Boolean).join(" · ")} + {edu.gpa && ` · GPA ${edu.gpa}`}

- {edu.date} + {`${new Date(edu.startDate).toLocaleDateString()} - ${new Date(edu.endDate).toLocaleDateString()}`}
-

- {edu.details} -

+ {edu.description && ( +
+ )}
))} diff --git a/apps/fronted/src/components/preview/PreviewPanel.tsx b/apps/fronted/src/components/preview/PreviewPanel.tsx index bfd877c..c39a1a8 100644 --- a/apps/fronted/src/components/preview/PreviewPanel.tsx +++ b/apps/fronted/src/components/preview/PreviewPanel.tsx @@ -161,7 +161,7 @@ export function PreviewPanel() { layout className="space-y-4" style={{ - marginTop: `${globalSettings?.sectionSpacing || 24}px` + marginTop: `${globalSettings?.paragraphSpacing || 20}px` }} > {title} diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index 9b2631c..0a47b4e 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -34,7 +34,9 @@ interface ResumeStore { // Actions updateBasicInfo: (data: Partial) => void; - updateEducation: (id: string, data: Partial) => void; + updateEducation: (data: Education) => void; + deleteEducation: (id: string) => void; + updateExperience: (id: string, data: Partial) => void; // 菜单操作 @@ -73,9 +75,15 @@ const initialState = { { id: "1", school: "北京大学", - degree: "计算机科学与技术", - date: "2016-2020", - details: "主修课程:..." + major: "计算机科学与技术", + degree: "本科", + startDate: "2019-09", + endDate: "2023-06", + visible: true, + gpa: "3.8/4.0", + location: "北京", + description: + "主修课程:数据结构、算法设计、操作系统、计算机网络、数据库系统\n在校期间保持专业前10%,获得优秀学生奖学金,参与多个开源项目" } ], experience: [ @@ -159,13 +167,6 @@ export const useResumeStore = create()( updateBasicInfo: (data) => set((state) => ({ basic: { ...state.basic, ...data } })), - updateEducation: (id, data) => - set((state) => ({ - education: state.education.map((edu) => - edu.id === id ? { ...edu, ...data } : edu - ) - })), - updateExperience: (id, data) => set((state) => ({ experience: state.experience.map((exp) => @@ -204,11 +205,28 @@ export const useResumeStore = create()( return { projects: newProjects }; }), + updateEducation: (education) => + set((state) => { + const newEducations = state.education.some( + (p) => p.id === education.id + ) + ? state.education.map((p) => + p.id === education.id ? { ...education } : p + ) + : [...state.education, { ...education }]; + + return { education: newEducations }; + }), + deleteProject: (id) => set((state) => ({ projects: state.projects.filter((p) => p.id !== id) })), + deleteEducation: (id) => + set((state) => ({ + education: state.education.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 140da74..3725667 100644 --- a/apps/fronted/src/types/resume.ts +++ b/apps/fronted/src/types/resume.ts @@ -16,7 +16,6 @@ export const DEFAULT_CONFIG: PhotoConfig = { visible: true }; -// 助手函数 export const getRatioMultiplier = (ratio: PhotoConfig["aspectRatio"]) => { switch (ratio) { case "4:3": @@ -72,7 +71,7 @@ export interface BasicInfo { employementStatus: string; photo: string; photoConfig: PhotoConfig; - fieldOrder?: BasicFieldType[]; // 新增字段排序 + fieldOrder?: BasicFieldType[]; customFields: Array<{ id: string; label: string; @@ -85,9 +84,14 @@ export interface BasicInfo { export interface Education { id: string; school: string; + major: string; degree: string; - date: string; - details: string; + startDate: string; + endDate: string; + gpa?: string; + location?: string; + description?: string; + visible?: boolean; } export interface Experience {