From 473b65ec4e706fa8dfb1913aeee5624c83e0c213 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Mon, 18 Nov 2024 19:38:56 +0800 Subject: [PATCH] feat: editor add avatar --- apps/fronted/package.json | 2 + .../src/components/PhotoConfigDrawer.tsx | 459 + apps/fronted/src/components/PhotoSelector.tsx | 76 + .../components/editor/basic/BasicPanel.tsx | 3 + .../src/components/preview/BaseInfo.tsx | 31 +- .../src/components/preview/PreviewPanel.tsx | 103 +- apps/fronted/src/components/ui/drawer.tsx | 118 + apps/fronted/src/store/useResumeStore.ts | 10 +- apps/fronted/src/types/resume.ts | 49 +- pnpm-lock.yaml | 10796 +++++++++------- 10 files changed, 6822 insertions(+), 4825 deletions(-) create mode 100644 apps/fronted/src/components/PhotoConfigDrawer.tsx create mode 100644 apps/fronted/src/components/PhotoSelector.tsx create mode 100644 apps/fronted/src/components/ui/drawer.tsx diff --git a/apps/fronted/package.json b/apps/fronted/package.json index 92976de..9853a66 100644 --- a/apps/fronted/package.json +++ b/apps/fronted/package.json @@ -46,9 +46,11 @@ "react-resizable-panels": "^2.0.20", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", "zustand": "^4.5.4" }, "devDependencies": { + "@types/lodash": "^4.17.13", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/apps/fronted/src/components/PhotoConfigDrawer.tsx b/apps/fronted/src/components/PhotoConfigDrawer.tsx new file mode 100644 index 0000000..d53d071 --- /dev/null +++ b/apps/fronted/src/components/PhotoConfigDrawer.tsx @@ -0,0 +1,459 @@ +import React, { useState, useRef, useEffect, useMemo } from "react"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerFooter, + DrawerClose +} from "@/components/ui/drawer"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import { Upload, X, Link as LinkIcon } from "lucide-react"; +import { + PhotoConfig, + DEFAULT_CONFIG, + getRatioMultiplier, + getBorderRadiusValue +} from "@/types/resume"; +import { motion } from "framer-motion"; +import { Textarea } from "@/components/ui/textarea"; + +interface Props { + isOpen: boolean; + onClose: () => void; + photo?: string; + config?: PhotoConfig; + onPhotoChange: (photo: string | undefined, config?: PhotoConfig) => void; + onConfigChange: (config: PhotoConfig) => void; + theme: "dark" | "light"; +} + +const PhotoConfigDrawer: React.FC = ({ + isOpen, + onClose, + photo, + config: initialConfig, + onPhotoChange, + onConfigChange, + theme +}) => { + const inputRef = useRef(null); + const [previewUrl, setPreviewUrl] = useState(photo); + const [isDragging, setIsDragging] = useState(false); + const [imageUrl, setImageUrl] = useState(photo || ""); + const drawerContentRef = useRef(null); + const [config, setConfig] = useState( + initialConfig || DEFAULT_CONFIG + ); + const isMobile = useMemo(() => { + return window.innerWidth <= 768; + }, [window.innerWidth]); + useEffect(() => { + if (isOpen) { + setConfig(initialConfig || DEFAULT_CONFIG); + setPreviewUrl(photo); + setImageUrl(photo || ""); + } + + const handleClick = (e: MouseEvent) => { + if (!drawerContentRef.current?.contains(e.target as Node)) { + onClose(); + } + }; + document.addEventListener("mousedown", handleClick); + + return () => { + document.removeEventListener("mousedown", handleClick); + }; + }, [isOpen, initialConfig, photo]); + + const handleFile = (file: File) => { + if (file.size > 5 * 1024 * 1024) { + alert("图片大小不能超过5MB"); + return; + } + + if (!file.type.startsWith("image/")) { + alert("请上传图片文件"); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + const result = reader.result as string; + setPreviewUrl(result); + }; + reader.readAsDataURL(file); + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + handleFile(file); + } + }; + + const handleUrlChange = (e: string) => { + const url = e; + setImageUrl(url); + setPreviewUrl(url); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) { + handleFile(file); + } + }; + + const handleRemovePhoto = () => { + setPreviewUrl(undefined); + setImageUrl(""); + if (inputRef.current) { + inputRef.current.value = ""; + } + }; + + const handleConfigChange = (updates: Partial) => { + const newConfig = { ...config, ...updates }; + + if (config.aspectRatio !== "custom") { + if ("width" in updates) { + const ratio = getRatioMultiplier(config.aspectRatio); + newConfig.height = + Math.round(updates.width! * ratio) > 200 + ? 200 + : Math.round(updates.width! * ratio); + } + if ("height" in updates) { + const ratio = 1 / getRatioMultiplier(config.aspectRatio); + newConfig.width = + Math.round(updates.height! * ratio) > 200 + ? 200 + : Math.round(updates.height! * ratio); + } + } + + setConfig(newConfig); + onConfigChange(newConfig); + }; + + const handleInputChange = ( + e: React.ChangeEvent, + key: "width" | "height" | "customBorderRadius" + ) => { + const value = Number(e.target.value) > 200 ? 200 : e.target.value; + + if (value === "") { + setConfig((prev) => ({ ...prev, [key]: "" })); + return; + } + + const numValue = Number(value); + if (!isNaN(numValue)) { + setConfig((prev) => ({ ...prev, [key]: numValue })); + } + }; + + const handleInputBlur = ( + e: React.FocusEvent, + key: "width" | "height" | "customBorderRadius" + ) => { + const value = e.target.value; + const numValue = value === "" ? 0 : Number(value); + + if (key === "customBorderRadius") { + const maxRadius = Math.min(config.width, config.height) / 2; + const validValue = Math.max(0, Math.min(numValue, maxRadius)); + handleConfigChange({ customBorderRadius: validValue }); + } else { + const validValue = Math.max(24, Math.min(numValue, 200)); + handleConfigChange({ [key]: validValue }); + } + }; + + const handleSave = () => { + onPhotoChange(previewUrl, config); + onClose(); + }; + return ( + !open && onClose()} + > + +
+ + 头像配置 + +
+ {previewUrl ? ( +
+ Profile +
+ +
+
+ ) : ( + + )} + +
+ +
+
+

在线链接

+