feat: project drag style

This commit is contained in:
siyue
2024-10-27 22:57:15 +08:00
parent 4133a02505
commit 54991dc2e3
3 changed files with 279 additions and 341 deletions
@@ -2,8 +2,8 @@
import { AnimatePresence, motion } from "framer-motion";
import { useResumeStore } from "@/store/useResumeStore";
import { getThemeConfig } from "@/theme/themeConfig";
import { cn } from "@/lib/utils";
import React, { useEffect } from "react";
const getFontFamilyClass = (fontFamily: string) => {
switch (fontFamily) {
@@ -24,336 +24,248 @@ export function PreviewPanel() {
experience,
menuSections,
globalSettings,
projects
projects,
draggingProjectId
} = useResumeStore();
console.log(projects, 111);
const fontFamilyClass = getFontFamilyClass(
globalSettings?.fontFamily || "sans"
);
useEffect(() => {
console.log(draggingProjectId, "draggingProjectId");
}, [draggingProjectId]);
const renderProjects = () => (
<motion.div
layout
className="space-y-4"
style={{
marginTop: `${globalSettings?.sectionSpacing || 24}px`
}}
>
<h3
className="text-lg font-semibold border-b border-gray-200 pb-2"
style={{
fontSize: `${globalSettings?.headerSize || 18}px`
}}
>
</h3>
<AnimatePresence mode="popLayout" initial={false}>
{projects.map((project) => (
<motion.div
key={project.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
scale: draggingProjectId === project.id ? 1.01 : 1
}}
exit={{ opacity: 0, y: -20 }}
transition={{
type: "spring",
stiffness: 500,
damping: 50,
mass: 1
}}
className={cn(
"space-y-2 relative rounded-lg p-4",
draggingProjectId === project.id && "z-10"
)}
style={{
marginTop: `${globalSettings?.paragraphSpacing || 1.5}em`
}}
>
{/* 拖拽时的高光效果 */}
{draggingProjectId === project.id && (
<motion.div
layoutId="project-highlight"
className="absolute inset-0 rounded-lg pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div
className={cn(
"absolute inset-0 rounded-lg",
theme === "dark"
? "bg-gradient-to-r from-indigo-500/10 via-purple-500/10 to-indigo-500/10 border-2 border-indigo-500/20"
: "bg-gradient-to-r from-indigo-500/5 via-purple-500/5 to-indigo-500/5 border-2 border-indigo-500/10"
)}
>
<div
className={cn(
"absolute inset-0 animate-pulse",
theme === "dark" ? "bg-white/5" : "bg-black/5",
"rounded-lg"
)}
/>
</div>
</motion.div>
)}
<motion.div
layout
className="flex justify-between items-start relative z-10"
>
<motion.div layout>
<motion.h4
layout
className={cn(
"font-medium",
theme === "dark" ? "text-neutral-200" : "text-gray-800"
)}
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{project.name || "未命名项目"}
</motion.h4>
<motion.p
layout
className={cn(
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{project.role}
</motion.p>
</motion.div>
<motion.span
layout
className={cn(
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{project.date}
</motion.span>
</motion.div>
{project.description && (
<motion.p
layout
className={cn(
"whitespace-pre-wrap",
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.description}
</motion.p>
)}
{project.technologies && (
<motion.div layout>
<motion.p
layout
className={cn(
"font-medium",
theme === "dark" ? "text-neutral-300" : "text-gray-800"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className={cn(
"whitespace-pre-wrap",
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.technologies}
</motion.p>
</motion.div>
)}
{project.responsibilities && (
<motion.div layout>
<motion.p
layout
className={cn(
"font-medium",
theme === "dark" ? "text-neutral-300" : "text-gray-800"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className={cn(
"whitespace-pre-wrap",
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.responsibilities}
</motion.p>
</motion.div>
)}
{project.achievements && (
<motion.div layout>
<motion.p
layout
className={cn(
"font-medium",
theme === "dark" ? "text-neutral-300" : "text-gray-800"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className={cn(
"whitespace-pre-wrap",
theme === "dark" ? "text-neutral-400" : "text-gray-600"
)}
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.achievements}
</motion.p>
</motion.div>
)}
</motion.div>
))}
</AnimatePresence>
</motion.div>
);
// 其他 section 的渲染函数保持不变...
const renderSection = (sectionId: string) => {
switch (sectionId) {
case "basic":
return (
<motion.div layout className="space-y-2">
<h3
className="text-lg font-semibold border-b border-gray-200 pb-2"
style={{
fontSize: `${globalSettings?.headerSize || 18}px`
}}
>
</h3>
<p
className="text-gray-600 whitespace-pre-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{basic.summary}
</p>
</motion.div>
);
case "education":
return (
<motion.div
layout
className="space-y-4"
style={{
marginTop: `${globalSettings?.sectionSpacing || 24}px`
}}
>
<h3
className="text-lg font-semibold border-b border-gray-200 pb-2"
style={{
fontSize: `${globalSettings?.headerSize || 18}px`
}}
>
</h3>
{education.map((edu) => (
<div
key={edu.id}
className="space-y-2"
style={{
marginTop: `${globalSettings?.paragraphSpacing || 1.5}em`
}}
>
<div className="flex justify-between items-start">
<div>
<h4
className="font-medium text-gray-800"
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{edu.school}
</h4>
<p
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{edu.degree}
</p>
</div>
<span
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{edu.date}
</span>
</div>
<p
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{edu.details}
</p>
</div>
))}
</motion.div>
);
case "experience":
return (
<motion.div
layout
className="space-y-4"
style={{
marginTop: `${globalSettings?.sectionSpacing || 24}px`
}}
>
<h3
className="text-lg font-semibold border-b border-gray-200 pb-2"
style={{
fontSize: `${globalSettings?.headerSize || 18}px`
}}
>
</h3>
{experience.map((exp) => (
<div
key={exp.id}
className="space-y-2"
style={{
marginTop: `${globalSettings?.paragraphSpacing || 1.5}em`
}}
>
<div className="flex justify-between items-start">
<div>
<h4
className="font-medium text-gray-800"
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{exp.company}
</h4>
<p
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{exp.position}
</p>
</div>
<span
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{exp.date}
</span>
</div>
<p
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{exp.details}
</p>
</div>
))}
</motion.div>
);
case "projects":
return (
<motion.div
layout
className="space-y-4"
style={{
marginTop: `${globalSettings?.sectionSpacing || 24}px`
}}
>
<h3
className="text-lg font-semibold border-b border-gray-200 pb-2"
style={{
fontSize: `${globalSettings?.headerSize || 18}px`
}}
>
</h3>
<AnimatePresence mode="popLayout" initial={false}>
{projects.map((project) => (
<motion.div
key={project.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
type: "spring",
stiffness: 500,
damping: 50,
mass: 1
}}
className="space-y-2"
style={{
marginTop: `${globalSettings?.paragraphSpacing || 1.5}em`
}}
>
<motion.div
layout
className="flex justify-between items-start"
>
<motion.div layout>
<motion.h4
layout
className="font-medium text-gray-800"
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{project.name}
</motion.h4>
<motion.p
layout
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{project.role}
</motion.p>
</motion.div>
<motion.span
layout
className="text-gray-600"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
{project.date}
</motion.span>
</motion.div>
{/* 项目描述 */}
{project.description && (
<motion.p
layout
className="text-gray-600 whitespace-pre-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.description}
</motion.p>
)}
{/* 技术栈 */}
{project.technologies && (
<motion.div layout>
<motion.p
layout
className="text-gray-800 font-medium"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className="text-gray-600 whitespace-pre-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.technologies}
</motion.p>
</motion.div>
)}
{/* 主要职责 */}
{project.responsibilities && (
<motion.div layout>
<motion.p
layout
className="text-gray-800 font-medium"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className="text-gray-600 whitespace-pre-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.responsibilities}
</motion.p>
</motion.div>
)}
{/* 项目成就 */}
{project.achievements && (
<motion.div layout>
<motion.p
layout
className="text-gray-800 font-medium"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
}}
>
</motion.p>
<motion.p
layout
className="text-gray-600 whitespace-pre-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`,
lineHeight: globalSettings?.lineHeight || 1.6
}}
>
{project.achievements}
</motion.p>
</motion.div>
)}
</motion.div>
))}
</AnimatePresence>
</motion.div>
);
return renderProjects();
// 其他 case 保持不变...
default:
return null;
}
@@ -365,11 +277,10 @@ export function PreviewPanel() {
"flex-1 overflow-y-auto",
theme === "dark" ? "bg-neutral-900" : "bg-gray-100"
)}
initial={{ x: 100, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="py-8 px-4 min-h-screen flex justify-center">
{/* A4 纸张容器 */}
<motion.div
layout
className={cn(
@@ -377,10 +288,10 @@ export function PreviewPanel() {
"bg-white",
"shadow-lg",
"relative mx-auto",
fontFamilyClass
fontFamilyClass,
theme === "dark" && "bg-neutral-900 text-neutral-100"
)}
>
{/* 内容区域 */}
<div
className="relative"
style={{
@@ -393,7 +304,10 @@ export function PreviewPanel() {
<div className="text-center space-y-4">
<motion.h1
layout="position"
className="font-bold text-gray-900"
className={cn(
"font-bold",
theme === "dark" ? "text-neutral-100" : "text-gray-900"
)}
style={{
fontSize: `${(globalSettings?.headerSize || 24) * 1.5}px`
}}
@@ -402,7 +316,9 @@ export function PreviewPanel() {
</motion.h1>
<motion.h2
layout="position"
className="text-gray-600"
className={
theme === "dark" ? "text-neutral-400" : "text-gray-600"
}
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
@@ -411,22 +327,30 @@ export function PreviewPanel() {
</motion.h2>
<motion.div
layout="position"
className="flex justify-center items-center space-x-4 text-gray-600 flex-wrap"
className="flex justify-center items-center space-x-4 flex-wrap"
style={{
fontSize: `${globalSettings?.contentFontSize || 14}px`
fontSize: `${globalSettings?.contentFontSize || 14}px`,
color:
theme === "dark"
? "rgb(163, 163, 163)"
: "rgb(75, 85, 99)"
}}
>
<span>{basic.email}</span>
<span></span>
<span>{basic.phone}</span>
<span></span>
<span>{basic.location}</span>
<span></span>
<span>
{basic.birthDate
{[
basic.email,
basic.phone,
basic.location,
basic.birthDate
? new Date(basic.birthDate).toLocaleDateString()
: ""}
</span>
: ""
]
.filter(Boolean)
.map((item, index, array) => (
<React.Fragment key={index}>
<span>{item}</span>
{index < array.length - 1 && <span></span>}
</React.Fragment>
))}
</motion.div>
</div>
@@ -136,6 +136,9 @@ const ProjectItem = ({ project }: { project: Project }) => {
value={project}
dragListener={false}
dragControls={dragControls}
onDragEnd={() => {
useResumeStore.getState().setDraggingProjectId(null);
}}
className={cn(
"rounded-lg border overflow-hidden flex group",
theme === "dark"
@@ -148,6 +151,13 @@ const ProjectItem = ({ project }: { project: Project }) => {
onPointerDown={(event) => {
if (expandedId === project.id) return;
dragControls.start(event);
useResumeStore.getState().setDraggingProjectId(project.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",
+4
View File
@@ -46,9 +46,12 @@ interface ResumeStore {
projects: Project[];
updateProjects: (project: Project) => void;
deleteProject: (id: string) => void;
draggingProjectId: string | null;
setDraggingProjectId: (id: string | null) => void;
}
const initialState = {
draggingProjectId: null,
basic: {
name: "张三",
title: "高级前端工程师",
@@ -123,6 +126,7 @@ export const useResumeStore = create<ResumeStore>()(
persist(
(set) => ({
...initialState,
setDraggingProjectId: (id) => set({ draggingProjectId: id }),
updateBasicInfo: (data) =>
set((state) => ({ basic: { ...state.basic, ...data } })),