mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: side setting only drag handle can do
This commit is contained in:
@@ -1,18 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { motion, Reorder } from "framer-motion";
|
||||
import {
|
||||
GripVertical,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Layout,
|
||||
Type,
|
||||
SpaceIcon,
|
||||
Palette,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Layout, Type, SpaceIcon, Palette } from "lucide-react";
|
||||
import debounce from "lodash/debounce";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,12 +12,12 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import LayoutSetting from "./layout/LayoutSetting";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { THEME_COLORS } from "@/types/resume";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useMemo } from "react";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: "无衬线体" },
|
||||
@@ -133,128 +123,15 @@ export function SidePanel() {
|
||||
>
|
||||
<div className="p-4 space-y-4">
|
||||
<SettingCard icon={Layout} title="布局">
|
||||
{menuSections
|
||||
.filter((item) => item.id === "basic")
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"rounded-lg group border mb-2 hover:border-primary",
|
||||
"dark:hover:bg-neutral-800 dark:bg-neutral-900/50 dark:border-neutral-800",
|
||||
"hover:bg-gray-50 bg-white border-gray-100",
|
||||
activeSection === item.id &&
|
||||
"border-primary dark:border-primary text-primary"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center p-3 pl-[32px] space-x-3 ">
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg ml-[12px]",
|
||||
"dark:text-neutral-300",
|
||||
"text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn("text-sm flex-1 cursor-pointer")}
|
||||
onClick={() => setActiveSection(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<LayoutSetting
|
||||
menuSections={menuSections}
|
||||
activeSection={activeSection}
|
||||
setActiveSection={setActiveSection}
|
||||
toggleSectionVisibility={toggleSectionVisibility}
|
||||
updateMenuSections={updateMenuSections}
|
||||
reorderSections={reorderSections}
|
||||
/>
|
||||
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={menuSections}
|
||||
onReorder={reorderSections}
|
||||
className="space-y-2 list-none"
|
||||
>
|
||||
{menuSections
|
||||
.filter((item) => item.id !== "basic")
|
||||
.map((item) => (
|
||||
<Reorder.Item
|
||||
key={item.id}
|
||||
value={item}
|
||||
className={cn(
|
||||
"rounded-lg cursor-move group border hover:border-primary",
|
||||
"dark:hover:bg-neutral-800 dark:bg-neutral-900/50 dark:border-neutral-800 dark:hover:border-primary",
|
||||
"hover:bg-gray-50 bg-white border-gray-100",
|
||||
activeSection === item.id &&
|
||||
"border-primary dark:border-primary text-primary"
|
||||
)}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileDrag={{ scale: 1.02 }}
|
||||
>
|
||||
<div className="flex items-center p-3 space-x-3">
|
||||
<GripVertical
|
||||
className={cn(
|
||||
"w-5 h-5 transition-opacity",
|
||||
"dark:text-neutral-400",
|
||||
"text-gray-400"
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg mr-2",
|
||||
"dark:text-neutral-300",
|
||||
"text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn("text-sm flex-1 cursor-pointer")}
|
||||
onClick={() => setActiveSection(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => toggleSectionVisibility(item.id)}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md",
|
||||
"dark:hover:bg-neutral-700 dark:text-neutral-300",
|
||||
"hover:bg-gray-100 text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.enabled ? (
|
||||
<Eye className="w-4 h-4 text-primary" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => {
|
||||
updateMenuSections(
|
||||
menuSections.filter(
|
||||
(section) => section.id !== item.id
|
||||
)
|
||||
);
|
||||
setActiveSection(
|
||||
menuSections[
|
||||
menuSections.findIndex((s) => s.id === item.id) - 1
|
||||
].id
|
||||
);
|
||||
}}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md text-primary",
|
||||
"dark:hover:bg-neutral-700 dark:text-neutral-300",
|
||||
"hover:bg-gray-100 text-gray-600"
|
||||
)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
<div className="space-y-2 py-4">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { motion, Reorder, useDragControls } from "framer-motion";
|
||||
import { Eye, EyeOff, GripVertical, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MenuSection } from "@/types/resume";
|
||||
|
||||
interface LayoutItemProps {
|
||||
item: MenuSection;
|
||||
isBasic?: boolean;
|
||||
activeSection: string;
|
||||
setActiveSection: (id: string) => void;
|
||||
toggleSectionVisibility: (id: string) => void;
|
||||
updateMenuSections: (sections: MenuSection[]) => void;
|
||||
menuSections: MenuSection[];
|
||||
}
|
||||
|
||||
const LayoutItem = ({
|
||||
item,
|
||||
isBasic = false,
|
||||
activeSection,
|
||||
setActiveSection,
|
||||
toggleSectionVisibility,
|
||||
updateMenuSections,
|
||||
menuSections,
|
||||
}: LayoutItemProps) => {
|
||||
const dragControls = useDragControls();
|
||||
|
||||
if (isBasic) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg group border mb-2 hover:border-primary",
|
||||
"dark:hover:bg-neutral-800 dark:bg-neutral-900/50 dark:border-neutral-800",
|
||||
"hover:bg-gray-50 bg-white border-gray-100",
|
||||
activeSection === item.id &&
|
||||
"border-primary dark:border-primary text-primary"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center p-3 pl-[32px] space-x-3 ">
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg ml-[12px]",
|
||||
"dark:text-neutral-300",
|
||||
"text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn("text-sm flex-1 cursor-pointer")}
|
||||
onClick={() => setActiveSection(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
id={item.id}
|
||||
value={item}
|
||||
dragListener={false}
|
||||
dragControls={dragControls}
|
||||
className={cn(
|
||||
"rounded-lg group border hover:border-primary flex overflow-hidden ",
|
||||
"dark:hover:bg-neutral-800 dark:bg-neutral-900/50 dark:border-neutral-800 dark:hover:border-primary",
|
||||
"hover:bg-gray-50 bg-white border-gray-100",
|
||||
activeSection === item.id &&
|
||||
"border-primary dark:border-primary text-primary"
|
||||
)}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileDrag={{ scale: 1.02 }}
|
||||
>
|
||||
<div
|
||||
onPointerDown={(event) => {
|
||||
dragControls.start(event);
|
||||
}}
|
||||
className={cn(
|
||||
"w-8 flex items-center justify-center touch-none shrink-0",
|
||||
"border-gray-100 dark:border-neutral-800",
|
||||
"cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50"
|
||||
)}
|
||||
>
|
||||
<GripVertical
|
||||
className={cn(
|
||||
"w-4 h-4",
|
||||
"text-gray-400 dark:text-neutral-400",
|
||||
"transform transition-transform group-hover:scale-110"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex select-none items-center p-3 space-x-3 flex-1 ">
|
||||
<div className="flex flex-1 items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg mr-2",
|
||||
"dark:text-neutral-300",
|
||||
"text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn("text-sm flex-1 cursor-pointer")}
|
||||
onClick={() => setActiveSection(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => toggleSectionVisibility(item.id)}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md mr-2",
|
||||
"dark:hover:bg-neutral-700 dark:text-neutral-300",
|
||||
"hover:bg-gray-100 text-gray-600"
|
||||
)}
|
||||
>
|
||||
{item.enabled ? (
|
||||
<Eye className="w-4 h-4 text-primary" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => {
|
||||
updateMenuSections(
|
||||
menuSections.filter((section) => section.id !== item.id)
|
||||
);
|
||||
setActiveSection(
|
||||
menuSections[
|
||||
menuSections.findIndex((s) => s.id === item.id) - 1
|
||||
].id
|
||||
);
|
||||
}}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md text-primary",
|
||||
"dark:hover:bg-neutral-700 dark:text-neutral-300",
|
||||
"hover:bg-gray-100 text-gray-600"
|
||||
)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutItem;
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { MenuSection } from "@/types/resume";
|
||||
import LayoutItem from "./LayoutItem";
|
||||
|
||||
interface LayoutPanelProps {
|
||||
menuSections: MenuSection[];
|
||||
activeSection: string;
|
||||
setActiveSection: (id: string) => void;
|
||||
toggleSectionVisibility: (id: string) => void;
|
||||
updateMenuSections: (sections: MenuSection[]) => void;
|
||||
reorderSections: (sections: MenuSection[]) => void;
|
||||
}
|
||||
|
||||
const LayoutSetting = ({
|
||||
menuSections,
|
||||
activeSection,
|
||||
setActiveSection,
|
||||
toggleSectionVisibility,
|
||||
updateMenuSections,
|
||||
reorderSections,
|
||||
}: LayoutPanelProps) => {
|
||||
const basicSection = menuSections.find((item) => item.id === "basic");
|
||||
const draggableSections = menuSections.filter((item) => item.id !== "basic");
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg bg-white dark:bg-neutral-900/30">
|
||||
{basicSection && (
|
||||
<LayoutItem
|
||||
item={basicSection}
|
||||
isBasic={true}
|
||||
activeSection={activeSection}
|
||||
setActiveSection={setActiveSection}
|
||||
toggleSectionVisibility={toggleSectionVisibility}
|
||||
updateMenuSections={updateMenuSections}
|
||||
menuSections={menuSections}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={draggableSections}
|
||||
onReorder={(newOrder) => {
|
||||
const updatedSections = [
|
||||
...menuSections.filter((item) => item.id === "basic"),
|
||||
...newOrder,
|
||||
];
|
||||
reorderSections(updatedSections);
|
||||
}}
|
||||
className="space-y-2"
|
||||
>
|
||||
{draggableSections.map((item) => (
|
||||
<LayoutItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
activeSection={activeSection}
|
||||
setActiveSection={setActiveSection}
|
||||
toggleSectionVisibility={toggleSectionVisibility}
|
||||
updateMenuSections={updateMenuSections}
|
||||
menuSections={menuSections}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutSetting;
|
||||
Reference in New Issue
Block a user