feat: Implement blank resume creation and enhance section management with standard modules and a new section addition UI

This commit is contained in:
JOYCEQL
2026-03-01 15:08:19 +08:00
parent e3e8cecd08
commit 5c6dc7fee2
16 changed files with 168 additions and 19 deletions
@@ -114,7 +114,7 @@ const TemplateThumbnail = ({
},
basic: {
...initialResumeState.basic,
layout: template.basic?.layout || "classic",
layout: (template.basic?.layout as any) || "left",
},
// Feed richer mock content in large preview.
experience: sampleExperience,
+2 -1
View File
@@ -253,7 +253,8 @@ const ResumeWorkbench = () => {
}, []);
const handleCreateFromModal = (templateId: string | null) => {
const newId = createResume(templateId);
const isBlank = !templateId;
const newId = createResume(templateId, isBlank);
if (templateId) {
const template = DEFAULT_TEMPLATES.find((t) => t.id === templateId);
+72 -10
View File
@@ -18,8 +18,16 @@ 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 { THEME_COLORS, MenuSection } from "@/types/resume";
import { ColorPicker } from "@/components/ui/color-picker";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Plus } from "lucide-react";
import { STANDARD_MODULES } from "@/config/modules";
import { DEFAULT_TEMPLATES } from "@/config";
const fontOptions = [
{ value: "sans", label: "无衬线体" },
@@ -84,6 +92,18 @@ export function SidePanel() {
const { themeColor = THEME_COLORS[0] } = globalSettings;
const t = useTranslations("workbench.sidePanel");
const currentTemplate = DEFAULT_TEMPLATES.find(
(t) => t.id === activeResume?.templateId
);
const availableModules = useMemo(() => {
return (
currentTemplate?.availableSections
?.map((id) => STANDARD_MODULES[id])
.filter(Boolean) || []
);
}, [currentTemplate]);
const fontOptions = [
{ value: "sans", label: t("typography.font.sans") },
{ value: "serif", label: t("typography.font.serif") },
@@ -145,15 +165,57 @@ export function SidePanel() {
reorderSections={reorderSections}
/>
<div className="space-y-2 py-4">
<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-primary bg-primary/5"
>
{t("layout.addCustomSection")}
</motion.button>
<div className="space-y-2 py-4">
<Popover>
<PopoverTrigger asChild>
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.9 }}
className="flex justify-center w-full rounded-lg items-center gap-2 py-2 px-3 text-sm font-medium text-primary bg-primary/5 border border-dashed border-primary/20 hover:bg-primary/10 transition-colors"
>
<Plus className="w-4 h-4" />
{t("layout.addCustomSection")}
</motion.button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="center">
<div className="flex flex-col gap-1">
{/* Standard Sections Library */}
{availableModules.map((section) => (
<button
key={section.id}
onClick={() => {
const newSection = {
id: section.id,
title: t(`layout.standardSections.${section.titleKey}`),
icon: section.icon,
enabled: true,
order: menuSections.length,
};
updateMenuSections([...menuSections, newSection]);
}}
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-left"
>
<span className="text-lg">{section.icon}</span>
<span>{t(`layout.standardSections.${section.titleKey}`)}</span>
</button>
))}
{/* Divider for Custom Section */}
{availableModules.length > 0 && (
<div className="h-px bg-border my-1" />
)}
{/* Add Custom Section */}
<button
onClick={handleCreateSection}
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-left text-muted-foreground italic"
>
<Plus className="w-4 h-4" />
{t("layout.addCustomSectionOption")}
</button>
</div>
</PopoverContent>
</Popover>
</div>
</SettingCard>
@@ -20,4 +20,5 @@ export const classicConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const creativeConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const elegantConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const leftRightConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const minimalistConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const modernConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
};
@@ -20,4 +20,5 @@ export const timelineConfig: ResumeTemplate = {
basic: {
layout: "right",
},
availableSections: ["skills", "experience", "projects", "education"],
};
+45 -1
View File
@@ -50,7 +50,7 @@ export const initialResumeState = {
id: "1",
school: "北京大学",
major: "计算机科学与技术",
degree: "本科",
degree: "",
startDate: "2013-09",
endDate: "2017-06",
visible: true,
@@ -326,3 +326,47 @@ export const initialResumeStateEn = {
activeSection: "basic",
globalSettings: initialGlobalSettings,
};
export const blankResumeState = {
...initialResumeState,
title: "新建简历",
basic: {
...initialResumeState.basic,
name: "",
title: "",
email: "",
phone: "",
location: "",
birthDate: "",
employementStatus: "",
photo: "",
customFields: [],
},
education: [],
skillContent: "",
experience: [],
projects: [],
menuSections: [initialResumeState.menuSections[0]],
};
export const blankResumeStateEn = {
...initialResumeStateEn,
title: "New Resume",
basic: {
...initialResumeStateEn.basic,
name: "",
title: "",
email: "",
phone: "",
location: "",
birthDate: "",
employementStatus: "",
photo: "",
customFields: [],
},
education: [],
skillContent: "",
experience: [],
projects: [],
menuSections: [initialResumeStateEn.menuSections[0]],
};
+12
View File
@@ -0,0 +1,12 @@
export interface ResumeModule {
id: string;
titleKey: string;
icon: string;
}
export const STANDARD_MODULES: Record<string, ResumeModule> = {
skills: { id: "skills", titleKey: "skills", icon: "⚡" },
experience: { id: "experience", titleKey: "experience", icon: "💼" },
projects: { id: "projects", titleKey: "projects", icon: "🚀" },
education: { id: "education", titleKey: "education", icon: "🎓" },
};
+8 -1
View File
@@ -291,7 +291,14 @@
"sidePanel": {
"layout": {
"title": "Layout",
"addCustomSection": "Add Custom Section"
"addCustomSection": "Add Module",
"addCustomSectionOption": "Add Custom Section",
"standardSections": {
"skills": "Skills",
"experience": "Experience",
"projects": "Projects",
"education": "Education"
}
},
"theme": {
"title": "Theme Color",
+8 -1
View File
@@ -251,7 +251,14 @@
"sidePanel": {
"layout": {
"title": "布局",
"addCustomSection": "添加自定义模块"
"addCustomSection": "添加模块",
"addCustomSectionOption": "添加自定义模块",
"standardSections": {
"skills": "专业技能",
"experience": "工作经验",
"projects": "项目经历",
"education": "教育经历"
}
},
"theme": {
"title": "主题色",
+12 -4
View File
@@ -15,6 +15,8 @@ import { DEFAULT_TEMPLATES } from "@/config";
import {
initialResumeState,
initialResumeStateEn,
blankResumeState,
blankResumeStateEn,
} from "@/config/initialResumeData";
import { generateUUID } from "@/utils/uuid";
interface ResumeStore {
@@ -22,7 +24,7 @@ interface ResumeStore {
activeResumeId: string | null;
activeResume: ResumeData | null;
createResume: (templateId: string | null) => string;
createResume: (templateId: string | null, isBlank?: boolean) => string;
deleteResume: (resume: ResumeData) => void;
duplicateResume: (resumeId: string) => string;
updateResume: (resumeId: string, data: Partial<ResumeData>) => void;
@@ -113,7 +115,7 @@ export const useResumeStore = create(
activeResumeId: null,
activeResume: null,
createResume: (templateId = null) => {
createResume: (templateId = null, isBlank = false) => {
const locale =
typeof document !== "undefined"
? document.cookie
@@ -122,8 +124,14 @@ export const useResumeStore = create(
?.split("=")[1] || "zh"
: "zh";
const initialResumeData =
locale === "en" ? initialResumeStateEn : initialResumeState;
let initialResumeData: any;
if (isBlank) {
initialResumeData =
locale === "en" ? blankResumeStateEn : blankResumeState;
} else {
initialResumeData =
locale === "en" ? initialResumeStateEn : initialResumeState;
}
const id = generateUUID();
const template = templateId
+1
View File
@@ -20,6 +20,7 @@ export interface ResumeTemplate {
basic: {
layout?: "left" | "center" | "right";
};
availableSections?: string[];
}
export interface TemplateConfig {