import { useMemo } from "react"; import { motion } from "framer-motion"; import { Layout, Type, SpaceIcon, Palette, Zap } from "lucide-react"; import debounce from "lodash/debounce"; import { useTranslations } from "@/i18n/compat/client"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; 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, 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"; import { getFontOptions, normalizeFontFamily } from "@/utils/fonts"; const lineHeightOptions = [ { value: "normal", label: "默认" }, { value: "relaxed", label: "适中" }, { value: "loose", label: "宽松" }, ]; function SettingCard({ icon: Icon, title, action, children, }: { icon: any; title: string; action?: React.ReactNode; children: React.ReactNode; }) { return ( {title} {action && {action}} {children} ); } export function SidePanel({ onSectionCreated, }: { onSectionCreated?: () => void; } = {}) { const { activeResume, setActiveSection, toggleSectionVisibility, updateGlobalSettings, updateMenuSections, setThemeColor, reorderSections, addCustomData, removeCustomData, } = useResumeStore(); const { menuSections = [], globalSettings = {}, activeSection, customData = {}, } = activeResume || {}; 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]); // 过滤掉 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 = getFontOptions((key) => t(`typography.font.${key}`)); const selectedFontFamily = normalizeFontFamily(globalSettings?.fontFamily); const lineHeightOptions = [ { value: "normal", label: t("typography.lineHeight.normal") }, { value: "relaxed", label: t("typography.lineHeight.relaxed") }, { value: "loose", label: t("typography.lineHeight.loose") }, ]; const debouncedSetColor = useMemo( () => debounce((value: string) => { setThemeColor(value); }, 100), [] ); const generateCustomSectionId = ( menuSections: MenuSection[], customData: Record ) => { const usedIds = new Set([ ...menuSections.map((section) => section.id), ...Object.keys(customData), ]); let nextNum = 1; while (usedIds.has(`custom-${nextNum}`)) { nextNum += 1; } return `custom-${nextNum}`; }; const handleCreateSection = () => { const sectionId = generateCustomSectionId(menuSections, customData); const newSection = { id: sectionId, title: sectionId, icon: "➕", enabled: true, order: menuSections.length, }; updateMenuSections([...menuSections, newSection]); addCustomData(sectionId); setActiveSection(sectionId); onSectionCreated?.(); }; return ( {t("layout.addCustomSection")} {/* Standard Sections Library */} {filteredModules.map((section) => ( { const newSection = { id: section.id, title: t(`layout.standardSections.${section.titleKey}`), icon: section.icon, enabled: true, order: menuSections.length, }; updateMenuSections([...menuSections, newSection]); setActiveSection(section.id); onSectionCreated?.(); }} className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-left" > {section.icon} {t(`layout.standardSections.${section.titleKey}`)} ))} {/* Divider for Custom Section */} {filteredModules.length > 0 && ( )} {/* Add Custom Section */} {t("layout.addCustomSectionOption")} {/* 主题色设置 */} debouncedSetColor(value)} className={cn( "h-7 w-auto px-3 py-0 rounded-full border shadow-none transition-all flex items-center gap-1.5 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background", !THEME_COLORS.includes(themeColor) ? "border-primary/40 text-primary bg-primary/5 hover:bg-primary/10 hover:border-primary/60" : "border-border text-muted-foreground bg-transparent hover:bg-accent/50 hover:text-foreground" )} style={{ backgroundColor: "transparent" }} title={t("theme.custom")} > {t("theme.custom")} {!THEME_COLORS.includes(themeColor) && ( )} } > {THEME_COLORS.map((presetTheme) => ( setThemeColor(presetTheme)} title={presetTheme} > ))} {/* 排版设置 */} {t('typography.font.title')} updateGlobalSettings?.({ fontFamily: value }) } > {fontOptions.map((font) => ( {font.label} ))} {t("typography.font.note")} {/* 行高选择 */} {t("typography.lineHeight.title")} updateGlobalSettings?.({ lineHeight: value }) } /> {globalSettings?.lineHeight} {t("typography.baseFontSize.title")} updateGlobalSettings?.({ baseFontSize: parseInt(value) }) } > {[12, 13, 14, 15, 16, 18, 20, 24].map((size) => ( {size}px ))} {t("typography.headerSize.title")} updateGlobalSettings?.({ headerSize: parseInt(value) }) } > {[12, 13, 14, 15, 16, 18, 20, 24].map((size) => ( {size}px ))} {t("typography.subheaderSize.title")} updateGlobalSettings?.({ subheaderSize: parseInt(value) }) } > {[12, 13, 14, 15, 16, 18, 20, 24].map((size) => ( {size}px ))} {/* 间距设置 */} {t("spacing.pagePadding.title")} updateGlobalSettings?.({ pagePadding: value }) } className="flex-1" /> ) => { const value = Number(e.target.value); if (!isNaN(value) && value >= 0 && value <= 100) { updateGlobalSettings?.({ pagePadding: value }); } }} className="h-full w-12 border-0 text-center focus-visible:ring-0 focus-visible:ring-offset-0 no-spinner" /> { const currentValue = globalSettings?.pagePadding || 0; if (currentValue < 100) { updateGlobalSettings?.({ pagePadding: currentValue + 1, }); } }} > 增加 { const currentValue = globalSettings?.pagePadding || 0; if (currentValue > 0) { updateGlobalSettings?.({ pagePadding: currentValue - 1, }); } }} > 减少 px {t("spacing.sectionSpacing.title")} updateGlobalSettings?.({ sectionSpacing: value }) } className="flex-1" /> ) => { const value = Number(e.target.value); if (!isNaN(value) && value >= 1 && value <= 100) { updateGlobalSettings?.({ sectionSpacing: value }); } }} className="h-full w-12 border-0 text-center focus-visible:ring-0 focus-visible:ring-offset-0 no-spinner" /> { const currentValue = globalSettings?.sectionSpacing || 0; if (currentValue < 100) { updateGlobalSettings?.({ sectionSpacing: currentValue + 1, }); } }} > 增加 { const currentValue = globalSettings?.sectionSpacing || 0; if (currentValue > 1) { updateGlobalSettings?.({ sectionSpacing: currentValue - 1, }); } }} > 减少 px {t("spacing.paragraphSpacing.title")} updateGlobalSettings?.({ paragraphSpacing: value }) } className="flex-1" /> ) => { const value = Number(e.target.value); if (!isNaN(value) && value >= 1) { updateGlobalSettings?.({ paragraphSpacing: value }); } }} className="h-full w-12 border-0 text-center focus-visible:ring-0 focus-visible:ring-offset-0 no-spinner" /> { const currentValue = globalSettings?.paragraphSpacing || 0; if (currentValue < 100) { updateGlobalSettings?.({ paragraphSpacing: currentValue + 1, }); } }} > 增加 { const currentValue = globalSettings?.paragraphSpacing || 0; if (currentValue > 1) { updateGlobalSettings?.({ paragraphSpacing: currentValue - 1, }); } }} > 减少 px {/* 模式设置 */} {t("mode.useIconMode.title")} updateGlobalSettings({ useIconMode: checked, }) } /> {t("mode.centerSubtitle.title")} updateGlobalSettings({ centerSubtitle: checked, }) } /> {t("mode.flexibleHeaderLayout.title")} updateGlobalSettings({ flexibleHeaderLayout: checked, }) } /> ); }
{t("typography.font.note")}