mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: custom section
This commit is contained in:
@@ -8,6 +8,7 @@ import BasicPanel from "./basic/BasicPanel";
|
||||
import EducationPanel from "./education/EducationPanel";
|
||||
import ProjectPanel from "./project/ProjectPanel";
|
||||
import ExperiencePanel from "./experience/ExperiencePanel";
|
||||
import CustomPanel from "./custom/CustomPanel";
|
||||
|
||||
export function EditPanel() {
|
||||
const { theme, activeSection, menuSections } = useResumeStore();
|
||||
@@ -24,8 +25,13 @@ export function EditPanel() {
|
||||
case "experience":
|
||||
return <ExperiencePanel />;
|
||||
case "skills":
|
||||
default:
|
||||
return null;
|
||||
default:
|
||||
if (activeSection.startsWith("custom")) {
|
||||
return <CustomPanel sectionId={activeSection} />;
|
||||
} else {
|
||||
return <BasicPanel />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
Type,
|
||||
SpaceIcon,
|
||||
Palette,
|
||||
Plus
|
||||
Plus,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import {
|
||||
@@ -92,7 +93,9 @@ export function SidePanel() {
|
||||
globalSettings,
|
||||
updateGlobalSettings,
|
||||
colorTheme,
|
||||
setColorTheme
|
||||
setColorTheme,
|
||||
updateMenuSections,
|
||||
addCustomData
|
||||
} = useResumeStore();
|
||||
|
||||
const debouncedSetColor = useMemo(
|
||||
@@ -103,6 +106,27 @@ export function SidePanel() {
|
||||
[]
|
||||
);
|
||||
|
||||
const generateCustomSectionId = (menuSections: any[]) => {
|
||||
const customSections = menuSections.filter((s) =>
|
||||
s.id.startsWith("custom")
|
||||
);
|
||||
const nextNum = customSections.length + 1;
|
||||
return `custom-${nextNum}`;
|
||||
};
|
||||
|
||||
const handleCreateSection = () => {
|
||||
const sectionId = generateCustomSectionId(menuSections);
|
||||
const newSection = {
|
||||
id: sectionId,
|
||||
title: sectionId,
|
||||
icon: "➕",
|
||||
enabled: true,
|
||||
order: menuSections.length
|
||||
};
|
||||
|
||||
updateMenuSections([...menuSections, newSection]);
|
||||
addCustomData(sectionId);
|
||||
};
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
@@ -183,10 +207,41 @@ export function SidePanel() {
|
||||
<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[0].id);
|
||||
}}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md text-primary",
|
||||
theme === "dark"
|
||||
? "hover:bg-neutral-700 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 p-[16px]">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={handleCreateSection}
|
||||
className="flex justify-center w-full rounded-lg items-center gap-2 py-2 px-3 text-sm font-medium text-indigo-600 bg-indigo-50"
|
||||
>
|
||||
添加自定义模块
|
||||
</motion.button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{/* 主题色设置 */}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
motion,
|
||||
useDragControls,
|
||||
Reorder,
|
||||
AnimatePresence
|
||||
} from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { GripVertical, Eye, EyeOff, ChevronDown, Trash2 } from "lucide-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import Field from "../Field";
|
||||
|
||||
import { CustomItem as CustomItemType } from "@/types/resume";
|
||||
const CustomItemEditor = ({
|
||||
item,
|
||||
onSave
|
||||
}: {
|
||||
item: CustomItemType;
|
||||
onSave: (item: CustomItemType) => void;
|
||||
}) => {
|
||||
const handleChange = (field: keyof CustomItemType, value: string) => {
|
||||
onSave({ ...item, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="标题"
|
||||
value={item.title}
|
||||
onChange={(value) => handleChange("title", value)}
|
||||
placeholder="标题"
|
||||
/>
|
||||
<Field
|
||||
label="副标题"
|
||||
value={item.subtitle}
|
||||
onChange={(value) => handleChange("subtitle", value)}
|
||||
placeholder="副标题"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label="时间范围"
|
||||
value={item.dateRange}
|
||||
onChange={(value) => handleChange("dateRange", value)}
|
||||
placeholder="例如: 2023.01 - 2024.01"
|
||||
/>
|
||||
|
||||
<Field
|
||||
label="详细描述"
|
||||
value={item.description}
|
||||
onChange={(value) => handleChange("description", value)}
|
||||
type="editor"
|
||||
placeholder="请输入详细描述..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteConfirmDialog = ({
|
||||
title,
|
||||
onDelete
|
||||
}: {
|
||||
title: string;
|
||||
onDelete: () => void;
|
||||
}) => {
|
||||
const theme = useResumeStore((state) => state.theme);
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"text-sm",
|
||||
theme === "dark"
|
||||
? "hover:bg-red-900/50 text-red-400"
|
||||
: "hover:bg-red-50 text-red-600"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
className={cn(
|
||||
theme === "dark" ? "bg-neutral-900 border-neutral-800" : "bg-white"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
您确定要删除 {title || "此项"} 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onDelete}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomItem = ({
|
||||
item,
|
||||
sectionId
|
||||
}: {
|
||||
item: CustomItemType;
|
||||
sectionId: string;
|
||||
}) => {
|
||||
const dragControls = useDragControls();
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const { theme, updateCustomItem, removeCustomItem } = useResumeStore();
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const handleVisibilityToggle = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isUpdating) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
setTimeout(() => {
|
||||
updateCustomItem(sectionId, item.id, { visible: !item.visible });
|
||||
setIsUpdating(false);
|
||||
}, 10);
|
||||
},
|
||||
[item, updateCustomItem, isUpdating, sectionId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
id={item.id}
|
||||
value={item}
|
||||
dragListener={false}
|
||||
dragControls={dragControls}
|
||||
className={cn(
|
||||
"rounded-lg border overflow-hidden flex group",
|
||||
theme === "dark"
|
||||
? "bg-neutral-900/30 border-neutral-800"
|
||||
: "bg-white border-gray-100"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onPointerDown={(event) => {
|
||||
if (expandedId === item.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 === item.id
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50"
|
||||
)}
|
||||
>
|
||||
<GripVertical
|
||||
className={cn(
|
||||
"w-4 h-4",
|
||||
theme === "dark" ? "text-neutral-400" : "text-gray-400",
|
||||
expandedId === item.id && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
"px-4 py-4 flex items-center justify-between",
|
||||
expandedId === item.id &&
|
||||
(theme === "dark" ? "bg-neutral-800/50" : "bg-gray-50")
|
||||
)}
|
||||
onClick={() => setExpandedId(expandedId === item.id ? null : item.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3
|
||||
className={cn(
|
||||
"font-medium truncate text-gray-700",
|
||||
"dark:text-neutral-200"
|
||||
)}
|
||||
>
|
||||
{item.title || "未命名模块"}
|
||||
</h3>
|
||||
{item.subtitle && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm truncate",
|
||||
theme === "dark" ? "text-neutral-400" : "text-gray-500"
|
||||
)}
|
||||
>
|
||||
{item.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={isUpdating}
|
||||
onClick={handleVisibilityToggle}
|
||||
>
|
||||
{item.visible ? (
|
||||
<Eye className="w-4 h-4 text-indigo-600" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<DeleteConfirmDialog
|
||||
title={item.title}
|
||||
onDelete={() => {
|
||||
removeCustomItem(sectionId, item.id);
|
||||
setExpandedId(null);
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
rotate: expandedId === item.id ? 180 : 0
|
||||
}}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"w-5 h-5",
|
||||
theme === "dark" ? "text-neutral-400" : "text-gray-500"
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expandedId === item.id && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"px-4 pb-4 space-y-4",
|
||||
theme === "dark" ? "border-neutral-800" : "border-gray-100"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-px w-full",
|
||||
theme === "dark" ? "bg-neutral-800" : "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
<CustomItemEditor
|
||||
item={item}
|
||||
onSave={(updatedItem) => {
|
||||
updateCustomItem(sectionId, item.id, updatedItem);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomItem;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { memo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import CustomItem from "./CustomItem";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { CustomItem as CustomItemType } from "@/types/resume";
|
||||
|
||||
const CustomPanel = memo(({ sectionId }: { sectionId: string }) => {
|
||||
const { customData, addCustomItem, updateCustomData } = useResumeStore();
|
||||
const items = customData[sectionId] || [];
|
||||
const handleCreateItem = () => {
|
||||
addCustomItem(sectionId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-4 px-4 py-4 rounded-lg",
|
||||
"dark:bg-neutral-900/30 bg-white"
|
||||
)}
|
||||
>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={items}
|
||||
onReorder={(newOrder) => {
|
||||
updateCustomData(sectionId, newOrder);
|
||||
}}
|
||||
className="space-y-3"
|
||||
>
|
||||
{items.map((item: CustomItemType) => (
|
||||
<CustomItem key={item.id} item={item} sectionId={sectionId} />
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={handleCreateItem}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"dark:bg-indigo-600 hover:bg-indigo-700 text-white",
|
||||
"bg-indigo-600 hover:bg-indigo-700 text-white"
|
||||
)}
|
||||
>
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
添加
|
||||
</Button>
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CustomPanel.displayName = "CustomPanel";
|
||||
|
||||
export default CustomPanel;
|
||||
@@ -16,7 +16,8 @@ const ExperiencePanel = () => {
|
||||
company: "某科技有限公司",
|
||||
position: "高级前端工程师",
|
||||
date: "2020-至今",
|
||||
details: "负责公司核心产品..."
|
||||
details: "负责公司核心产品...",
|
||||
visible: true
|
||||
};
|
||||
updateExperience(newProject);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlobalSettings } from "@/types/resume";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
import { CustomItem } from "@/types/resume";
|
||||
|
||||
interface CustomSectionProps {
|
||||
sectionId: string;
|
||||
title: string;
|
||||
items: CustomItem[];
|
||||
globalSettings: GlobalSettings | undefined;
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
export function CustomSection({
|
||||
title,
|
||||
items,
|
||||
globalSettings,
|
||||
themeColor
|
||||
}: CustomSectionProps) {
|
||||
const visibleItems = items.filter((item) => {
|
||||
return item.visible && item.description;
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
style={{
|
||||
marginTop: `${globalSettings?.sectionSpacing || 24}px`
|
||||
}}
|
||||
>
|
||||
<SectionTitle
|
||||
title={title}
|
||||
themeColor={themeColor}
|
||||
globalSettings={globalSettings}
|
||||
/>
|
||||
{visibleItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<h4
|
||||
className="font-medium text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</h4>
|
||||
<motion.div
|
||||
layout
|
||||
className={"text-gray-600"}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
{item.dateRange && (
|
||||
<span
|
||||
className="text-gray-600 shrink-0 ml-4"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{item.dateRange}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div
|
||||
className="text-gray-600 mt-1"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -14,9 +14,7 @@ export function EducationSection({
|
||||
globalSettings,
|
||||
themeColor
|
||||
}: EducationSectionProps) {
|
||||
// 只显示visible为true的教育经历
|
||||
const visibleEducation = education.filter((edu) => edu.visible);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SectionTitle } from "./SectionTitle";
|
||||
import { ProjectItem } from "./ProjectItem";
|
||||
import { ExperienceSection } from "./ExperienceSection";
|
||||
import { EducationSection } from "./EducationSection";
|
||||
import { CustomSection } from "./CustomSection";
|
||||
|
||||
const getFontFamilyClass = (fontFamily: string) => {
|
||||
switch (fontFamily) {
|
||||
@@ -67,7 +68,8 @@ export function PreviewPanel() {
|
||||
globalSettings,
|
||||
projects,
|
||||
draggingProjectId,
|
||||
colorTheme
|
||||
colorTheme,
|
||||
customData
|
||||
} = useResumeStore();
|
||||
|
||||
const previewRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -196,6 +198,19 @@ export function PreviewPanel() {
|
||||
}, [contentHeight]);
|
||||
|
||||
const renderSection = (sectionId: string) => {
|
||||
if (sectionId.startsWith("custom")) {
|
||||
const sectionConfig = menuSections.find((s) => s.id === sectionId);
|
||||
return (
|
||||
<CustomSection
|
||||
sectionId={sectionId}
|
||||
title={sectionConfig?.title || "自定义模块"}
|
||||
items={customData[sectionId] || []}
|
||||
globalSettings={globalSettings}
|
||||
themeColor={currentThemeColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (sectionId) {
|
||||
case "education":
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GlobalSettings } from "@/types/resume";
|
||||
interface SectionTitleProps {
|
||||
title: string;
|
||||
themeColor: string;
|
||||
globalSettings: GlobalSettings;
|
||||
globalSettings?: GlobalSettings;
|
||||
}
|
||||
|
||||
export function SectionTitle({
|
||||
|
||||
@@ -6,9 +6,15 @@ import {
|
||||
Experience,
|
||||
GlobalSettings,
|
||||
DEFAULT_CONFIG,
|
||||
Project
|
||||
Project,
|
||||
CustomItem
|
||||
} from "../types/resume";
|
||||
|
||||
interface CustomSection {
|
||||
id: string;
|
||||
items: CustomItem[];
|
||||
}
|
||||
|
||||
interface ResumeStore {
|
||||
// 基础数据
|
||||
basic: BasicInfo;
|
||||
@@ -23,13 +29,13 @@ interface ResumeStore {
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
}[];
|
||||
customData: Record<string, CustomItem[]>;
|
||||
|
||||
// 主题设置
|
||||
theme: "light" | "dark";
|
||||
activeSection: string;
|
||||
|
||||
colorTheme: string; // 当前使用的主题色 ID
|
||||
|
||||
// 当前使用的主题色 ID
|
||||
colorTheme: string;
|
||||
setColorTheme: (colorTheme: string) => void;
|
||||
|
||||
// Actions
|
||||
@@ -43,6 +49,19 @@ interface ResumeStore {
|
||||
reorderSections: (newOrder: typeof initialState.menuSections) => void;
|
||||
toggleSectionVisibility: (sectionId: string) => void;
|
||||
setActiveSection: (sectionId: string) => void;
|
||||
updateMenuSections: (sections: typeof initialState.menuSections) => void;
|
||||
|
||||
addCustomData: (sectionId: string) => void;
|
||||
updateCustomData: (sectionId: string, items: CustomItem[]) => void;
|
||||
removeCustomData: (sectionId: string) => void;
|
||||
addCustomItem: (sectionId: string) => void;
|
||||
updateCustomItem: (
|
||||
sectionId: string,
|
||||
itemId: string,
|
||||
updates: Partial<CustomItem>
|
||||
) => void;
|
||||
removeCustomItem: (sectionId: string, itemId: string) => void;
|
||||
|
||||
toggleTheme: () => void;
|
||||
// 全局设置
|
||||
globalSettings: GlobalSettings;
|
||||
@@ -107,6 +126,7 @@ const initialState = {
|
||||
{ id: "skills", title: "技能特长", icon: "⚡", enabled: true, order: 3 },
|
||||
{ id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 4 }
|
||||
],
|
||||
customData: {},
|
||||
theme: "light" as const,
|
||||
|
||||
colorTheme: "#2563eb",
|
||||
@@ -201,6 +221,8 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
|
||||
setActiveSection: (sectionId) => set({ activeSection: sectionId }),
|
||||
|
||||
updateMenuSections: (sections) => set({ menuSections: sections }),
|
||||
|
||||
updateProjects: (project) =>
|
||||
set((state) => {
|
||||
const newProjects = state.projects.some((p) => p.id === project.id)
|
||||
@@ -253,7 +275,77 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
return {
|
||||
globalSettings: newSettings
|
||||
};
|
||||
})
|
||||
}),
|
||||
addCustomData: (sectionId) =>
|
||||
set((state) => ({
|
||||
customData: {
|
||||
...state.customData,
|
||||
[sectionId]: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
title: "",
|
||||
subtitle: "",
|
||||
dateRange: "",
|
||||
description: "",
|
||||
visible: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})),
|
||||
|
||||
updateCustomData: (sectionId, items) =>
|
||||
set((state) => ({
|
||||
customData: {
|
||||
...state.customData,
|
||||
[sectionId]: items
|
||||
}
|
||||
})),
|
||||
|
||||
removeCustomData: (sectionId) =>
|
||||
set((state) => {
|
||||
const { [sectionId]: _, ...rest } = state.customData;
|
||||
return { customData: rest };
|
||||
}),
|
||||
|
||||
addCustomItem: (sectionId) =>
|
||||
set((state) => ({
|
||||
customData: {
|
||||
...state.customData,
|
||||
[sectionId]: [
|
||||
...(state.customData[sectionId] || []),
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
title: "",
|
||||
subtitle: "",
|
||||
dateRange: "",
|
||||
description: "",
|
||||
visible: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})),
|
||||
|
||||
updateCustomItem: (sectionId, itemId, updates) => {
|
||||
console.log(sectionId, "sectionId");
|
||||
set((state) => ({
|
||||
customData: {
|
||||
...state.customData,
|
||||
[sectionId]: state.customData[sectionId].map((item) =>
|
||||
item.id === itemId ? { ...item, ...updates } : item
|
||||
)
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
removeCustomItem: (sectionId, itemId) =>
|
||||
set((state) => ({
|
||||
customData: {
|
||||
...state.customData,
|
||||
[sectionId]: state.customData[sectionId].filter(
|
||||
(item) => item.id !== itemId
|
||||
)
|
||||
}
|
||||
}))
|
||||
}),
|
||||
|
||||
{
|
||||
|
||||
@@ -140,6 +140,15 @@ export interface ResumeTheme {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface CustomItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
dateRange: string;
|
||||
description: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const THEME_COLORS = [
|
||||
"#2563eb",
|
||||
"#059669",
|
||||
|
||||
Reference in New Issue
Block a user