feat: add self-evaluation section

This commit is contained in:
JOYCEQL
2026-03-06 00:11:06 +08:00
parent 2fde976a04
commit c5225a2840
30 changed files with 276 additions and 11 deletions
+3
View File
@@ -9,6 +9,7 @@ import ProjectPanel from "./project/ProjectPanel";
import ExperiencePanel from "./experience/ExperiencePanel";
import CustomPanel from "./custom/CustomPanel";
import SkillPanel from "./skills/SkillPanel";
import SelfEvaluationPanel from "./self-evaluation/SelfEvaluationPanel";
import {
Tooltip,
TooltipContent,
@@ -34,6 +35,8 @@ export function EditPanel() {
return <ExperiencePanel />;
case "skills":
return <SkillPanel />;
case "selfEvaluation":
return <SelfEvaluationPanel />;
default:
if (activeSection?.startsWith("custom")) {
return <CustomPanel sectionId={activeSection} />;
+8 -2
View File
@@ -104,6 +104,12 @@ export function SidePanel() {
);
}, [currentTemplate]);
// 过滤掉 menuSections 中已存在的模块,避免重复添加和 key 冲突
const filteredModules = useMemo(() => {
const existingIds = new Set(menuSections.map((s: MenuSection) => s.id));
return availableModules.filter((m) => !existingIds.has(m.id));
}, [availableModules, menuSections]);
const fontOptions = [
{ value: "sans", label: t("typography.font.sans") },
{ value: "serif", label: t("typography.font.serif") },
@@ -180,7 +186,7 @@ export function SidePanel() {
<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) => (
{filteredModules.map((section) => (
<button
key={section.id}
onClick={() => {
@@ -201,7 +207,7 @@ export function SidePanel() {
))}
{/* Divider for Custom Section */}
{availableModules.length > 0 && (
{filteredModules.length > 0 && (
<div className="h-px bg-border my-1" />
)}
@@ -0,0 +1,30 @@
import { useResumeStore } from "@/store/useResumeStore";
import { cn } from "@/lib/utils";
import Field from "../Field";
const SelfEvaluationPanel = () => {
const { activeResume, updateSelfEvaluationContent } = useResumeStore();
const selfEvaluationContent = activeResume?.selfEvaluationContent ?? "";
const handleChange = (value: string) => {
updateSelfEvaluationContent(value);
};
return (
<div
className={cn(
"rounded-lg border p-4",
"bg-card",
"border-border"
)}
>
<Field
value={selfEvaluationContent}
onChange={handleChange}
type="editor"
placeholder="描述你的自我评价..."
/>
</div>
);
};
export default SelfEvaluationPanel;
+1 -1
View File
@@ -20,5 +20,5 @@ export const classicConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface ClassicTemplateProps {
@@ -29,6 +30,8 @@ const ClassicTemplate: React.FC<ClassicTemplateProps> = ({ data, template }) =>
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const sectionTitle = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
+1 -1
View File
@@ -20,5 +20,5 @@ export const creativeConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -7,6 +7,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface CreativeTemplateProps {
@@ -31,6 +32,8 @@ const CreativeTemplate: React.FC<CreativeTemplateProps> = ({ data, template }) =
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
+1 -1
View File
@@ -20,5 +20,5 @@ export const elegantConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface ElegantTemplateProps {
@@ -29,6 +30,8 @@ const ElegantTemplate: React.FC<ElegantTemplateProps> = ({ data, template }) =>
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
@@ -20,5 +20,5 @@ export const leftRightConfig: ResumeTemplate = {
basic: {
layout: "left",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface LeftRightTemplateProps {
@@ -29,6 +30,8 @@ const LeftRightTemplate: React.FC<LeftRightTemplateProps> = ({ data, template })
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
@@ -20,5 +20,5 @@ export const minimalistConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface MinimalistTemplateProps {
@@ -29,6 +30,8 @@ const MinimalistTemplate: React.FC<MinimalistTemplateProps> = ({ data, template
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
+1 -1
View File
@@ -20,5 +20,5 @@ export const modernConfig: ResumeTemplate = {
basic: {
layout: "center",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface ModernTemplateProps {
@@ -29,6 +30,8 @@ const ModernTemplate: React.FC<ModernTemplateProps> = ({ data, template }) => {
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
+1 -1
View File
@@ -20,5 +20,5 @@ export const timelineConfig: ResumeTemplate = {
basic: {
layout: "right",
},
availableSections: ["skills", "experience", "projects", "education"],
availableSections: ["skills", "experience", "projects", "education", "selfEvaluation"],
};
@@ -6,6 +6,7 @@ import ExperienceSection from "./sections/ExperienceSection";
import EducationSection from "./sections/EducationSection";
import ProjectSection from "./sections/ProjectSection";
import SkillSection from "./sections/SkillSection";
import SelfEvaluationSection from "./sections/SelfEvaluationSection";
import CustomSection from "./sections/CustomSection";
interface TimelineTemplateProps {
@@ -40,6 +41,8 @@ const TimelineTemplate: React.FC<TimelineTemplateProps> = ({ data, template }) =
return <SkillSection skill={data.skillContent} globalSettings={data.globalSettings} showTitle={false} />;
case "projects":
return <ProjectSection projects={data.projects} globalSettings={data.globalSettings} showTitle={false} />;
case "selfEvaluation":
return <SelfEvaluationSection content={data.selfEvaluationContent} globalSettings={data.globalSettings} showTitle={false} />;
default:
if (sectionId in data.customData) {
const title = data.menuSections.find((s) => s.id === sectionId)?.title || sectionId;
@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
import SectionTitle from "./SectionTitle";
import SectionWrapper from "../../shared/SectionWrapper";
import { GlobalSettings } from "@/types/resume";
import { normalizeRichTextContent } from "@/lib/richText";
interface SelfEvaluationSectionProps {
content?: string;
globalSettings?: GlobalSettings;
showTitle?: boolean;
}
const SelfEvaluationSection = ({ content, globalSettings, showTitle = true }: SelfEvaluationSectionProps) => {
return (
<SectionWrapper sectionId="selfEvaluation" style={{ marginTop: `${globalSettings?.sectionSpacing || 24}px` }}>
<SectionTitle type="selfEvaluation" globalSettings={globalSettings} showTitle={showTitle} />
<motion.div style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}>
<motion.div className="text-baseFont" layout="position"
style={{ fontSize: `${globalSettings?.baseFontSize || 14}px`, lineHeight: globalSettings?.lineHeight || 1.6 }}
dangerouslySetInnerHTML={{ __html: normalizeRichTextContent(content) }}
/>
</motion.div>
</SectionWrapper>
);
};
export default SelfEvaluationSection;
+4
View File
@@ -76,6 +76,7 @@ export const initialResumeState = {
<li></li>
</ul>
</div>`,
selfEvaluationContent: "",
experience: [
{
id: "1",
@@ -224,6 +225,7 @@ export const initialResumeStateEn = {
<li>Technical Leadership: Team management experience, led technology selection and architecture design for large projects</li>
</ul>
</div>`,
selfEvaluationContent: "",
experience: [
{
id: "1",
@@ -344,6 +346,7 @@ export const blankResumeState = {
},
education: [],
skillContent: "",
selfEvaluationContent: "",
experience: [],
projects: [],
menuSections: [initialResumeState.menuSections[0]],
@@ -366,6 +369,7 @@ export const blankResumeStateEn = {
},
education: [],
skillContent: "",
selfEvaluationContent: "",
experience: [],
projects: [],
menuSections: [initialResumeStateEn.menuSections[0]],
+1
View File
@@ -9,4 +9,5 @@ export const STANDARD_MODULES: Record<string, ResumeModule> = {
experience: { id: "experience", titleKey: "experience", icon: "💼" },
projects: { id: "projects", titleKey: "projects", icon: "🚀" },
education: { id: "education", titleKey: "education", icon: "🎓" },
selfEvaluation: { id: "selfEvaluation", titleKey: "selfEvaluation", icon: "💬" },
};
+2 -1
View File
@@ -316,7 +316,8 @@
"skills": "Skills",
"experience": "Experience",
"projects": "Projects",
"education": "Education"
"education": "Education",
"selfEvaluation": "Self Evaluation"
}
},
"theme": {
+2 -1
View File
@@ -276,7 +276,8 @@
"skills": "专业技能",
"experience": "工作经验",
"projects": "项目经历",
"education": "教育经历"
"education": "教育经历",
"selfEvaluation": "自我评价"
}
},
"theme": {
+8
View File
@@ -44,6 +44,7 @@ interface ResumeStore {
deleteProject: (id: string) => void;
setDraggingProjectId: (id: string | null) => void;
updateSkillContent: (skillContent: string) => void;
updateSelfEvaluationContent: (content: string) => void;
reorderSections: (newOrder: ResumeData["menuSections"]) => void;
toggleSectionVisibility: (sectionId: string) => void;
setActiveSection: (sectionId: string) => void;
@@ -417,6 +418,13 @@ export const useResumeStore = create(
}
},
updateSelfEvaluationContent: (selfEvaluationContent) => {
const { activeResumeId } = get();
if (activeResumeId) {
get().updateResume(activeResumeId, { selfEvaluationContent });
}
},
reorderSections: (newOrder) => {
const { activeResumeId, resumes } = get();
if (activeResumeId) {
+1
View File
@@ -183,6 +183,7 @@ export interface ResumeData {
projects: Project[];
customData: Record<string, CustomItem[]>;
skillContent: string;
selfEvaluationContent: string;
activeSection: string;
draggingProjectId: string | null;
menuSections: MenuSection[];