From bb6f65e3d79f040f109fb7bc13bb47db5df72040 Mon Sep 17 00:00:00 2001 From: siyue Date: Sat, 26 Oct 2024 15:24:22 +0800 Subject: [PATCH] feat: editor update --- apps/fronted/package.json | 1 + .../workbench/compoents/Editor/RichText.tsx | 249 +++++++ apps/fronted/src/app/workbench/page.tsx | 675 +++++++++++++++++- apps/fronted/src/components/ui/textarea.tsx | 24 + pnpm-lock.yaml | 11 + 5 files changed, 936 insertions(+), 24 deletions(-) create mode 100644 apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx create mode 100644 apps/fronted/src/components/ui/textarea.tsx diff --git a/apps/fronted/package.json b/apps/fronted/package.json index 3f2bb41..4d1b06e 100644 --- a/apps/fronted/package.json +++ b/apps/fronted/package.json @@ -19,6 +19,7 @@ "@tiptap/extension-list-item": "^2.4.0", "@tiptap/extension-text-align": "^2.4.0", "@tiptap/extension-text-style": "^2.4.0", + "@tiptap/extension-underline": "^2.9.1", "@tiptap/pm": "^2.4.0", "@tiptap/react": "^2.4.0", "@tiptap/starter-kit": "^2.4.0", diff --git a/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx new file mode 100644 index 0000000..a5aabc7 --- /dev/null +++ b/apps/fronted/src/app/workbench/compoents/Editor/RichText.tsx @@ -0,0 +1,249 @@ +import React from "react"; +import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import TextAlign from "@tiptap/extension-text-align"; +import TextStyle from "@tiptap/extension-text-style"; +import Underline from "@tiptap/extension-underline"; +import Color from "@tiptap/extension-color"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + Bold, + Italic, + Underline as UnderlineIcon, + Strikethrough, + AlignLeft, + AlignCenter, + AlignRight, + AlignJustify, + List, + ListOrdered, + Quote, + Undo, + Redo +} from "lucide-react"; + +interface RichTextEditorProps { + content: string; + onChange: (content: string) => void; + placeholder?: string; +} + +const MenuButton = ({ onClick, isActive = false, children }) => ( + +); + +const RichTextEditor = ({ + content, + onChange, + placeholder +}: RichTextEditorProps) => { + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { + levels: [1, 2, 3] + } + }), + TextAlign.configure({ + types: ["heading", "paragraph"], + alignments: ["left", "center", "right", "justify"] + }), + TextStyle, + Underline, + Color + ], + content, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + editorProps: { + attributes: { + class: + "prose prose-sm sm:prose max-w-none focus:outline-none min-h-[150px] p-4", + placeholder: placeholder + } + } + }); + + if (!editor) { + return null; + } + + return ( +
+ {/* 工具栏 */} +
+
+ editor.chain().focus().toggleBold().run()} + isActive={editor.isActive("bold")} + > + + + editor.chain().focus().toggleItalic().run()} + isActive={editor.isActive("italic")} + > + + + editor.chain().focus().toggleUnderline().run()} + isActive={editor.isActive("underline")} + > + + + editor.chain().focus().toggleStrike().run()} + isActive={editor.isActive("strike")} + > + + +
+ + + +
+ +
+ + + +
+ editor.chain().focus().setTextAlign("left").run()} + isActive={editor.isActive({ textAlign: "left" })} + > + + + editor.chain().focus().setTextAlign("center").run()} + isActive={editor.isActive({ textAlign: "center" })} + > + + + editor.chain().focus().setTextAlign("right").run()} + isActive={editor.isActive({ textAlign: "right" })} + > + + + editor.chain().focus().setTextAlign("justify").run()} + isActive={editor.isActive({ textAlign: "justify" })} + > + + +
+ + + +
+ editor.chain().focus().toggleBulletList().run()} + isActive={editor.isActive("bulletList")} + > + + + editor.chain().focus().toggleOrderedList().run()} + isActive={editor.isActive("orderedList")} + > + + + editor.chain().focus().toggleBlockquote().run()} + isActive={editor.isActive("blockquote")} + > + + +
+ + + +
+ editor.chain().focus().undo().run()} + disabled={!editor.can().undo()} + > + + + editor.chain().focus().redo().run()} + disabled={!editor.can().redo()} + > + + +
+
+ + {/* 编辑区域 */} + + + {/* 气泡菜单 */} + {editor && ( + + editor.chain().focus().toggleBold().run()} + isActive={editor.isActive("bold")} + > + + + editor.chain().focus().toggleItalic().run()} + isActive={editor.isActive("italic")} + > + + + editor.chain().focus().toggleUnderline().run()} + isActive={editor.isActive("underline")} + > + + + + )} +
+ ); +}; + +export default RichTextEditor; diff --git a/apps/fronted/src/app/workbench/page.tsx b/apps/fronted/src/app/workbench/page.tsx index 6f8ed3e..9641e43 100644 --- a/apps/fronted/src/app/workbench/page.tsx +++ b/apps/fronted/src/app/workbench/page.tsx @@ -1,32 +1,659 @@ "use client"; -import Editor from "./compoents/Editor"; +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@/components/ui/resizable"; -import Preview from "./compoents/Preview"; -import styles from "./index.module.scss"; -const WorkBench = () => { + Type, + Layout, + Phone, + Mail, + Globe, + MapPin, + GraduationCap, + Briefcase, + Award, + Plus, + Trash, + Download, + User, + Cpu, + Calendar, + Trash2 +} from "lucide-react"; +import RichTextEditor from "./compoents/Editor/RichText"; + +const TabIcon = ({ icon: Icon, label }) => ( +
+ + {label} +
+); + +const FormField = ({ label, children }) => ( +
+ + {children} +
+); + +const Section = ({ title, children, className = "" }) => ( +
+

{title}

+ {children} +
+); + +// 简历数据结构 +interface ResumeData { + basics: { + name: string; + label: string; + email: string; + phone: string; + website: string; + location: string; + summary: string; + }; + education: Array<{ + institution: string; + area: string; + studyType: string; + startDate: string; + endDate: string; + description: string; + }>; + work: Array<{ + company: string; + position: string; + startDate: string; + endDate: string; + description: string; + }>; + skills: Array<{ + name: string; + level: string; + keywords: string[]; + }>; + projects: Array<{ + name: string; + description: string; + highlights: string[]; + url: string; + }>; +} + +// 简历预览组件 +const ResumePreview = ({ data }: { data: ResumeData }) => { + const handleDownloadPDF = () => { + window.print(); + }; + return ( -
- + {/* 固定的下载按钮 */} +
+
+ +
+
+ + {/* 简历预览内容 */} +
- - - - - - - - + {/* 个人信息头部 */} +
+

+ {data.basics.name} +

+ {data.basics.label && ( +
+ {data.basics.label} +
+ )} +
+ {data.basics.email && ( +
+ + {data.basics.email} +
+ )} + {data.basics.phone && ( +
+ + {data.basics.phone} +
+ )} + {data.basics.location && ( +
+ + {data.basics.location} +
+ )} + {data.basics.website && ( + + )} +
+
+ + {/* 主要内容区域 */} +
+ {/* 个人简介 */} + {data.basics.summary && ( +
+

+ + 个人简介 +

+
+ {data.basics.summary} +
+
+ )} + + {/* 工作经历 */} + {data.work.length > 0 && ( +
+

+ + 工作经历 +

+
+ {data.work.map((work, index) => ( +
+
+

+ {work.company} +

+ + {work.startDate} - {work.endDate} + +
+
+ {work.position} +
+
+
+ ))} +
+
+ )} + + {/* 教育经历 */} + {data.education.length > 0 && ( +
+

+ + 教育经历 +

+
+ {data.education.map((edu, index) => ( +
+
+

+ {edu.institution} +

+ + {edu.startDate} - {edu.endDate} + +
+
+ {edu.studyType} · {edu.area} +
+
+
+ ))} +
+
+ )} + + {/* 技能专长 */} + {data.skills.length > 0 && ( +
+

+ + 技能专长 +

+
+ {data.skills.map((skill, index) => ( +
+
+ {skill.name} +
+
+ {skill.keywords.join(" · ")} +
+
+ ))} +
+
+ )} + + {/* 项目经历 */} + {data.projects.length > 0 && ( +
+

+ + 项目经历 +

+
+ {data.projects.map((project, index) => ( +
+
+

+ {project.name} +

+ {project.url && ( + + 项目链接 + + )} +
+
+ {project.highlights.length > 0 && ( +
    + {project.highlights.map((highlight, i) => ( +
  • + {highlight} +
  • + ))} +
+ )} +
+ ))} +
+
+ )} +
+
); }; -export default WorkBench; +// 主编辑器组件 +const ResumeEditor = () => { + const [resumeData, setResumeData] = useState({ + basics: { + name: "", + label: "", + email: "", + phone: "", + website: "", + location: "", + summary: "" + }, + education: [], + work: [], + skills: [], + projects: [] + }); + + // 更新基本信息 + const updateBasics = (field: keyof ResumeData["basics"], value: string) => { + setResumeData((prev) => ({ + ...prev, + basics: { + ...prev.basics, + [field]: value + } + })); + }; + + // 添加教育经历 + const addEducation = () => { + setResumeData((prev) => ({ + ...prev, + education: [ + ...prev.education, + { + institution: "", + area: "", + studyType: "", + startDate: "", + endDate: "", + description: "" + } + ] + })); + }; + + return ( +
+
+ +
+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + updateBasics("name", e.target.value)} + placeholder="你的名字" + className="bg-white" + /> + + + updateBasics("label", e.target.value)} + placeholder="期望职位" + className="bg-white" + /> + +
+ +
+ +
+ + + updateBasics("email", e.target.value) + } + placeholder="邮箱地址" + className="pl-10 bg-white" + /> +
+
+ +
+ + + updateBasics("phone", e.target.value) + } + placeholder="联系电话" + className="pl-10 bg-white" + /> +
+
+
+ +
+ +
+ + + updateBasics("website", e.target.value) + } + placeholder="个人网站或博客" + className="pl-10 bg-white" + /> +
+
+ +
+ + + updateBasics("location", e.target.value) + } + placeholder="所在城市" + className="pl-10 bg-white" + /> +
+
+
+ + + updateBasics("summary", value)} + placeholder="简单介绍自己" + /> + +
+
+
+ + + +
+

教育经历

+ +
+ +
+ {resumeData.education.map((edu, index) => ( + +
+ + { + const newEducation = [...resumeData.education]; + newEducation[index].institution = e.target.value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + placeholder="学校名称" + /> + + +
+ +
+ + { + const newEducation = [ + ...resumeData.education + ]; + newEducation[index].startDate = + e.target.value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + className="pl-10" + /> +
+
+ +
+ + { + const newEducation = [ + ...resumeData.education + ]; + newEducation[index].endDate = e.target.value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + className="pl-10" + /> +
+
+
+ +
+ + { + const newEducation = [...resumeData.education]; + newEducation[index].studyType = e.target.value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + placeholder="学历" + /> + + + { + const newEducation = [...resumeData.education]; + newEducation[index].area = e.target.value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + placeholder="专业" + /> + +
+ + + { + const newEducation = [...resumeData.education]; + newEducation[index].description = value; + setResumeData({ + ...resumeData, + education: newEducation + }); + }} + placeholder="在校经历描述" + /> + + +
+ +
+
+
+ ))} +
+
+
+ + {/* 其他 TabsContent 内容类似 */} +
+
+
+
+ +
+
+ ); +}; + +export default ResumeEditor; diff --git a/apps/fronted/src/components/ui/textarea.tsx b/apps/fronted/src/components/ui/textarea.tsx new file mode 100644 index 0000000..9f9a6dc --- /dev/null +++ b/apps/fronted/src/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +