mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: theme config
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { throttle } from "lodash";
|
||||
import { THEME_COLORS } from "@/types/resume";
|
||||
|
||||
const getFontFamilyClass = (fontFamily: string) => {
|
||||
switch (fontFamily) {
|
||||
@@ -26,7 +27,8 @@ export function PreviewPanel() {
|
||||
menuSections,
|
||||
globalSettings,
|
||||
projects,
|
||||
draggingProjectId
|
||||
draggingProjectId,
|
||||
colorTheme
|
||||
} = useResumeStore();
|
||||
const previewRef = React.useRef<HTMLDivElement>(null);
|
||||
const [scrollBehavior, setScrollBehavior] =
|
||||
@@ -36,6 +38,18 @@ export function PreviewPanel() {
|
||||
globalSettings?.fontFamily || "sans"
|
||||
);
|
||||
|
||||
// 获取当前主题色
|
||||
const currentThemeColor = useMemo(() => {
|
||||
return colorTheme || THEME_COLORS[0];
|
||||
}, [colorTheme]);
|
||||
|
||||
// 标题样式的公共配置
|
||||
const sectionTitleStyles = {
|
||||
fontSize: `${globalSettings?.headerSize || 18}px`,
|
||||
borderColor: currentThemeColor, // 使用主题色作为下边框颜色
|
||||
color: currentThemeColor // 使用主题色作为文字颜色
|
||||
};
|
||||
|
||||
// 处理自动滚动
|
||||
const handleScroll = React.useCallback(
|
||||
throttle((offset: number) => {
|
||||
@@ -101,9 +115,7 @@ export function PreviewPanel() {
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-semibold border-b border-gray-200 pb-2"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.headerSize || 18}px`
|
||||
}}
|
||||
style={sectionTitleStyles}
|
||||
>
|
||||
项目经历
|
||||
</h3>
|
||||
@@ -180,7 +192,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{project.role}
|
||||
@@ -190,7 +202,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{project.date}
|
||||
@@ -203,7 +215,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("whitespace-pre-wrap", "text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -220,7 +232,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("font-medium", "text-gray-800")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
技术栈:
|
||||
@@ -229,7 +241,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("whitespace-pre-wrap", "text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -245,7 +257,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("font-medium", "text-gray-800")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
主要职责:
|
||||
@@ -254,7 +266,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("whitespace-pre-wrap", "text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -270,7 +282,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("font-medium", "text-gray-800")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
项目成就:
|
||||
@@ -279,7 +291,7 @@ export function PreviewPanel() {
|
||||
layout
|
||||
className={cn("whitespace-pre-wrap", "text-gray-600")}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -293,8 +305,6 @@ export function PreviewPanel() {
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// ... 其他 section 渲染函数 ...
|
||||
|
||||
const renderSection = (sectionId: string) => {
|
||||
switch (sectionId) {
|
||||
case "basic":
|
||||
@@ -302,16 +312,14 @@ export function PreviewPanel() {
|
||||
<motion.div layout className="space-y-2">
|
||||
<h3
|
||||
className="text-lg font-semibold border-b border-gray-200 pb-2"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.headerSize || 18}px`
|
||||
}}
|
||||
style={sectionTitleStyles}
|
||||
>
|
||||
个人简介
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-600 whitespace-pre-wrap"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -331,9 +339,7 @@ export function PreviewPanel() {
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-semibold border-b border-gray-200 pb-2"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.headerSize || 18}px`
|
||||
}}
|
||||
style={sectionTitleStyles}
|
||||
>
|
||||
教育经历
|
||||
</h3>
|
||||
@@ -358,7 +364,7 @@ export function PreviewPanel() {
|
||||
<p
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{edu.degree}
|
||||
@@ -367,7 +373,7 @@ export function PreviewPanel() {
|
||||
<span
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{edu.date}
|
||||
@@ -376,7 +382,7 @@ export function PreviewPanel() {
|
||||
<p
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -398,9 +404,7 @@ export function PreviewPanel() {
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-semibold border-b border-gray-200 pb-2"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.headerSize || 18}px`
|
||||
}}
|
||||
style={sectionTitleStyles}
|
||||
>
|
||||
工作经验
|
||||
</h3>
|
||||
@@ -425,7 +429,7 @@ export function PreviewPanel() {
|
||||
<p
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{exp.position}
|
||||
@@ -434,7 +438,7 @@ export function PreviewPanel() {
|
||||
<span
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`
|
||||
}}
|
||||
>
|
||||
{exp.date}
|
||||
@@ -443,7 +447,7 @@ export function PreviewPanel() {
|
||||
<p
|
||||
className="text-gray-600"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6
|
||||
}}
|
||||
>
|
||||
@@ -455,7 +459,6 @@ export function PreviewPanel() {
|
||||
);
|
||||
case "projects":
|
||||
return renderProjects();
|
||||
// ... 其他 case
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -512,7 +515,7 @@ export function PreviewPanel() {
|
||||
layout="position"
|
||||
className="flex justify-center items-center space-x-4 flex-wrap"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.contentFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
color: "rgb(75, 85, 99)"
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
EyeOff,
|
||||
Layout,
|
||||
Type,
|
||||
SpaceIcon
|
||||
SpaceIcon,
|
||||
Palette,
|
||||
Plus
|
||||
} from "lucide-react";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { getThemeConfig } from "@/theme/themeConfig";
|
||||
@@ -22,6 +24,9 @@ import { Slider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { THEME_COLORS } from "@/types/resume";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: "无衬线体" },
|
||||
@@ -79,9 +84,19 @@ export function SidePanel() {
|
||||
toggleSectionVisibility,
|
||||
setActiveSection,
|
||||
globalSettings,
|
||||
updateGlobalSettings
|
||||
updateGlobalSettings,
|
||||
colorTheme,
|
||||
setColorTheme
|
||||
} = useResumeStore();
|
||||
|
||||
const debouncedSetColor = useMemo(
|
||||
() =>
|
||||
debounce((value) => {
|
||||
setColorTheme(value);
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
@@ -168,6 +183,62 @@ export function SidePanel() {
|
||||
</Reorder.Group>
|
||||
</SettingCard>
|
||||
|
||||
{/* 主题色设置 */}
|
||||
|
||||
<SettingCard icon={Palette} title="主题色">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{THEME_COLORS.map((presetTheme) => (
|
||||
<button
|
||||
key={presetTheme}
|
||||
className={cn(
|
||||
"relative group aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200",
|
||||
colorTheme === presetTheme
|
||||
? "border-black dark:border-white"
|
||||
: theme === "dark"
|
||||
? "border-neutral-800 hover:border-neutral-700"
|
||||
: "border-gray-100 hover:border-gray-200"
|
||||
)}
|
||||
onClick={() => setColorTheme(presetTheme)}
|
||||
>
|
||||
{/* 颜色展示 */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: presetTheme }}
|
||||
/>
|
||||
|
||||
{/* 选中指示器 */}
|
||||
{colorTheme === presetTheme && (
|
||||
<motion.div
|
||||
layoutId="theme-selected"
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/20 dark:bg-white/20"
|
||||
initial={false}
|
||||
transition={{
|
||||
type: "spring",
|
||||
bounce: 0.2,
|
||||
duration: 0.6
|
||||
}}
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full bg-white dark:bg-black" />
|
||||
</motion.div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
自定义
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
onChange={(e) => debouncedSetColor(e.target.value)}
|
||||
className="w-[40px] h-[40px] rounded-lg cursor-pointer overflow-hidden hover:scale-105 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{/* 排版设置 */}
|
||||
<SettingCard icon={Type} title="排版">
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Education,
|
||||
Experience,
|
||||
GlobalSettings,
|
||||
Project
|
||||
Project,
|
||||
ResumeTheme
|
||||
} from "../types/resume";
|
||||
|
||||
interface ResumeStore {
|
||||
@@ -29,6 +30,11 @@ interface ResumeStore {
|
||||
theme: "light" | "dark";
|
||||
activeSection: string;
|
||||
|
||||
colorTheme: string; // 当前使用的主题色 ID
|
||||
|
||||
// 新增 Actions
|
||||
setColorTheme: (colorTheme: string) => void;
|
||||
|
||||
// Actions
|
||||
updateBasicInfo: (data: Partial<BasicInfo>) => void;
|
||||
updateEducation: (id: string, data: Partial<Education>) => void;
|
||||
@@ -93,6 +99,10 @@ const initialState = {
|
||||
{ id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 4 }
|
||||
],
|
||||
theme: "light" as const,
|
||||
|
||||
// 主题色
|
||||
colorTheme: "#2563eb", // 默认使用经典蓝主题
|
||||
|
||||
activeSection: "basic",
|
||||
projects: [
|
||||
{
|
||||
@@ -128,6 +138,11 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setColorTheme: (colorTheme) => {
|
||||
console.log(colorTheme, "colorTheme");
|
||||
set({ colorTheme });
|
||||
},
|
||||
|
||||
setDraggingProjectId: (id) => set({ draggingProjectId: id }),
|
||||
|
||||
updateBasicInfo: (data) =>
|
||||
|
||||
@@ -68,3 +68,45 @@ export type GlobalSettings = {
|
||||
pagePadding: number;
|
||||
paragraphSpacing: number;
|
||||
};
|
||||
|
||||
export interface ThemeColors {
|
||||
primary: string; // 主色
|
||||
secondary: string; // 次要色
|
||||
text: {
|
||||
primary: string; // 主要文字颜色
|
||||
secondary: string; // 次要文字颜色
|
||||
accent: string; // 强调文字颜色
|
||||
};
|
||||
background: {
|
||||
primary: string; // 主要背景色
|
||||
secondary: string; // 次要背景色
|
||||
accent: string; // 强调背景色
|
||||
};
|
||||
border: string; // 边框颜色
|
||||
divider: string; // 分割线颜色
|
||||
}
|
||||
|
||||
export interface ResumeTheme {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const THEME_COLORS = [
|
||||
"#2563eb", // 经典蓝
|
||||
"#059669", // 翡翠绿
|
||||
"#7c3aed", // 优雅紫
|
||||
"#e11d48", // 玫瑰红
|
||||
"#d97706", // 琥珀金
|
||||
"#0891b2", // 青碧蓝
|
||||
"#4f46e5", // 靛青蓝
|
||||
"#0d9488", // 青蓝绿
|
||||
"#0284c7", // 天际蓝
|
||||
"#6d28d9", // 雅致紫
|
||||
"#c026d3", // 绯紫红
|
||||
"#db2777", // 粉红色
|
||||
"#ea580c", // 活力橙
|
||||
"#65a30d", // 青柠绿
|
||||
"#475569", // 岩石灰
|
||||
"#dc2626" // 中国红
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user