diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx
index 3fb69b6..cabfa43 100644
--- a/apps/fronted/src/components/editor/EditPanel.tsx
+++ b/apps/fronted/src/components/editor/EditPanel.tsx
@@ -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 ;
case "skills":
- default:
return null;
+ default:
+ if (activeSection.startsWith("custom")) {
+ return ;
+ } else {
+ return ;
+ }
}
};
diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx
index 5a47472..fce339a 100644
--- a/apps/fronted/src/components/editor/SidePanel.tsx
+++ b/apps/fronted/src/components/editor/SidePanel.tsx
@@ -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 (
)}
+ {/* 删除按钮 */}
+
+ {
+ 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"
+ )}
+ >
+
+
))}
+ {/* 添加自定义模块 */}
+
+
+ 添加自定义模块
+
+
{/* 主题色设置 */}
diff --git a/apps/fronted/src/components/editor/custom/CustomItem.tsx b/apps/fronted/src/components/editor/custom/CustomItem.tsx
new file mode 100644
index 0000000..1d85cd4
--- /dev/null
+++ b/apps/fronted/src/components/editor/custom/CustomItem.tsx
@@ -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 (
+
+
+
+ handleChange("title", value)}
+ placeholder="标题"
+ />
+ handleChange("subtitle", value)}
+ placeholder="副标题"
+ />
+
+
+
handleChange("dateRange", value)}
+ placeholder="例如: 2023.01 - 2024.01"
+ />
+
+ handleChange("description", value)}
+ type="editor"
+ placeholder="请输入详细描述..."
+ />
+
+
+ );
+};
+
+const DeleteConfirmDialog = ({
+ title,
+ onDelete
+}: {
+ title: string;
+ onDelete: () => void;
+}) => {
+ const theme = useResumeStore((state) => state.theme);
+
+ return (
+
+
+
+
+ e.stopPropagation()}
+ >
+
+ 确认删除
+
+ 您确定要删除 {title || "此项"} 吗?此操作无法撤销。
+
+
+
+ 取消
+
+ 删除
+
+
+
+
+ );
+};
+
+const CustomItem = ({
+ item,
+ sectionId
+}: {
+ item: CustomItemType;
+ sectionId: string;
+}) => {
+ const dragControls = useDragControls();
+ const [expandedId, setExpandedId] = useState(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 (
+
+ {
+ 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"
+ )}
+ >
+
+
+
+
+
setExpandedId(expandedId === item.id ? null : item.id)}
+ >
+
+
+ {item.title || "未命名模块"}
+
+ {item.subtitle && (
+
+ {item.subtitle}
+
+ )}
+
+
+
+
+ {
+ removeCustomItem(sectionId, item.id);
+ setExpandedId(null);
+ }}
+ />
+
+
+
+
+
+
+
+ {expandedId === item.id && (
+
+
+
+
{
+ updateCustomItem(sectionId, item.id, updatedItem);
+ }}
+ />
+
+
+ )}
+
+
+
+ );
+};
+
+export default CustomItem;
diff --git a/apps/fronted/src/components/editor/custom/CustomPanel.tsx b/apps/fronted/src/components/editor/custom/CustomPanel.tsx
new file mode 100644
index 0000000..1d11e64
--- /dev/null
+++ b/apps/fronted/src/components/editor/custom/CustomPanel.tsx
@@ -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 (
+
+
{
+ updateCustomData(sectionId, newOrder);
+ }}
+ className="space-y-3"
+ >
+ {items.map((item: CustomItemType) => (
+
+ ))}
+
+
+
+
+ );
+});
+
+CustomPanel.displayName = "CustomPanel";
+
+export default CustomPanel;
diff --git a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx
index c8cb126..3390b16 100644
--- a/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx
+++ b/apps/fronted/src/components/editor/experience/ExperiencePanel.tsx
@@ -16,7 +16,8 @@ const ExperiencePanel = () => {
company: "某科技有限公司",
position: "高级前端工程师",
date: "2020-至今",
- details: "负责公司核心产品..."
+ details: "负责公司核心产品...",
+ visible: true
};
updateExperience(newProject);
};
diff --git a/apps/fronted/src/components/preview/CustomSection.tsx b/apps/fronted/src/components/preview/CustomSection.tsx
new file mode 100644
index 0000000..c0bad4a
--- /dev/null
+++ b/apps/fronted/src/components/preview/CustomSection.tsx
@@ -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 (
+
+
+ {visibleItems.map((item) => (
+
+
+
+
+
+ {item.title}
+
+
+ {item.subtitle}
+
+
+
+ {item.dateRange && (
+
+ {item.dateRange}
+
+ )}
+
+ {item.description && (
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/apps/fronted/src/components/preview/EducationSection.tsx b/apps/fronted/src/components/preview/EducationSection.tsx
index 3f7f197..07d68e1 100644
--- a/apps/fronted/src/components/preview/EducationSection.tsx
+++ b/apps/fronted/src/components/preview/EducationSection.tsx
@@ -14,9 +14,7 @@ export function EducationSection({
globalSettings,
themeColor
}: EducationSectionProps) {
- // 只显示visible为true的教育经历
const visibleEducation = education.filter((edu) => edu.visible);
-
return (
{
switch (fontFamily) {
@@ -67,7 +68,8 @@ export function PreviewPanel() {
globalSettings,
projects,
draggingProjectId,
- colorTheme
+ colorTheme,
+ customData
} = useResumeStore();
const previewRef = React.useRef(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 (
+
+ );
+ }
+
switch (sectionId) {
case "education":
return (
diff --git a/apps/fronted/src/components/preview/SectionTitle.tsx b/apps/fronted/src/components/preview/SectionTitle.tsx
index 835f988..959ce9e 100644
--- a/apps/fronted/src/components/preview/SectionTitle.tsx
+++ b/apps/fronted/src/components/preview/SectionTitle.tsx
@@ -3,7 +3,7 @@ import { GlobalSettings } from "@/types/resume";
interface SectionTitleProps {
title: string;
themeColor: string;
- globalSettings: GlobalSettings;
+ globalSettings?: GlobalSettings;
}
export function SectionTitle({
diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts
index fa22646..95f1c0d 100644
--- a/apps/fronted/src/store/useResumeStore.ts
+++ b/apps/fronted/src/store/useResumeStore.ts
@@ -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;
- // 主题设置
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
+ ) => 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()(
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()(
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
+ )
+ }
+ }))
}),
{
diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts
index 5287b30..e1a823f 100644
--- a/apps/fronted/src/types/resume.ts
+++ b/apps/fronted/src/types/resume.ts
@@ -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",