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) => ( ))} {/* Divider for Custom Section */} {filteredModules.length > 0 && (
)} {/* Add Custom Section */}
{/* 主题色设置 */} 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) => ( ))}
{/* 排版设置 */}

{t("typography.font.note")}

{/* 行高选择 */}
updateGlobalSettings?.({ lineHeight: value }) } /> {globalSettings?.lineHeight}
{/* 间距设置 */}
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" />
px
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" />
px
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" />
px
{/* 模式设置 */}
updateGlobalSettings({ useIconMode: checked, }) } />
updateGlobalSettings({ centerSubtitle: checked, }) } />
updateGlobalSettings({ flexibleHeaderLayout: checked, }) } />
); }