feat: baseInfoPanel visible control

This commit is contained in:
JOYCEQL
2024-11-18 22:25:23 +08:00
parent 473b65ec4e
commit a1f6e7bb13
9 changed files with 515 additions and 245 deletions
+1 -1
View File
@@ -284,7 +284,7 @@ export default function Home() {
className={cn(
"w-full h-screen overflow-hidden",
theme === "dark"
? "bg-neutral-900 text-neutral-200"
? "dark bg-neutral-900 text-neutral-200"
: "bg-white text-gray-900"
)}
>
+31 -9
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Settings2, Image } from "lucide-react";
import { Settings2, Image, EyeOff, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import PhotoConfigDrawer from "./PhotoConfigDrawer";
@@ -40,14 +40,36 @@ const PhotoSelector: React.FC<Props> = ({ className, theme }) => {
<Image className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 px-2"
onClick={() => setShowConfig(true)}
>
<Settings2 className="w-3 h-3" />
</Button>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
className="h-6 px-2"
onClick={() => setShowConfig(true)}
>
<Settings2 className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 px-2"
onClick={() => {
updateBasicInfo({
...basic,
photoConfig: {
...basic.photoConfig,
visible: !(basic.photoConfig?.visible ?? true)
}
});
}}
>
{basic.photoConfig?.visible !== false ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
</Button>
</div>
</div>
{basic.photo && (
+3 -2
View File
@@ -15,16 +15,17 @@ type FieldProps = {
type?: "text" | "textarea" | "date" | "editor";
placeholder?: string;
required?: boolean;
className?: string;
};
// Field 组件
const Field: React.FC<FieldProps> = ({
label,
value,
onChange,
type = "text",
placeholder,
required
required,
className
}) => {
const theme = useResumeStore((state) => state.theme);
@@ -10,27 +10,6 @@ export function PdfExport() {
const { basic, theme } = useResumeStore();
const [isExporting, setIsExporting] = useState(false);
const calculatePageCount = (
contentHeight: number,
pageHeightPx: number
): number => {
const rawPages = contentHeight / pageHeightPx;
// 保留两位小数
const roundedPages = Math.round(rawPages * 100) / 100;
// 根据小数部分决定是否需要额外的页
const decimalPart = roundedPages % 1;
if (decimalPart === 0) {
return roundedPages;
} else if (decimalPart <= 0.05) {
// 如果小数部分小于等于0.05,向下取整
return Math.floor(roundedPages);
} else {
// 如果小数部分大于0.05,向上取整
return Math.ceil(roundedPages);
}
};
const handleExport = () => {
setIsExporting(true);
const element = document.querySelector("#resume-preview");
@@ -41,10 +20,10 @@ export function PdfExport() {
// 内容高度的问题
const contentHeight = element.scrollHeight - 30.22;
const A4_HEIGHT_MM = 297;
const MARGINS_MM = 8; // 上下边距各4mm
const MARGINS_MM = 8;
const CONTENT_HEIGHT_MM = A4_HEIGHT_MM - MARGINS_MM;
const DPI = 96; // 标准屏幕DPI
const MM_TO_PX = DPI / 25.4; // 1英寸=25.4毫米
const DPI = 96;
const MM_TO_PX = DPI / 25.4;
const pageHeightPx = Math.ceil(CONTENT_HEIGHT_MM * MM_TO_PX);
const numberOfPages = Math.ceil(contentHeight / pageHeightPx);
@@ -67,13 +46,13 @@ export function PdfExport() {
filename: `${basic.name}_简历.pdf`,
image: { type: "jpeg", quality: 0.98 },
pagebreak: {
mode: ["css", "legacy"] // 避免在元素中间断页
mode: ["css", "legacy"]
},
html2canvas: {
scale: window.devicePixelRatio * 3, // 增加清晰度 useCORS: true,
scale: window.devicePixelRatio * 3,
letterRendering: true,
scrollY: -window.scrollY, // 添加这个
height: pageHeightPx * numberOfPages // 设置为精确的高度
scrollY: -window.scrollY,
height: pageHeightPx * numberOfPages
},
jsPDF: {
unit: "mm",
@@ -104,12 +83,7 @@ export function PdfExport() {
return (
<div>
<motion.button
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center space-x-2
${
theme === "dark"
? "bg-indigo-600 hover:bg-indigo-700 text-white"
: "bg-black hover:bg-neutral-800 text-white"
}
className={`bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium flex items-center space-x-2
disabled:opacity-50 disabled:cursor-not-allowed`}
whileHover={!isExporting ? { scale: 1.02 } : {}}
whileTap={!isExporting ? { scale: 0.98 } : {}}
@@ -27,6 +27,7 @@ import { cn } from "@/lib/utils";
import { THEME_COLORS } from "@/types/resume";
import debounce from "lodash/debounce";
import { useMemo } from "react";
import { Switch } from "../ui/switch";
const fontOptions = [
{ value: "sans", label: "无衬线体" },
@@ -603,6 +604,28 @@ export function SidePanel() {
</div>
</div>
</SettingCard>
<SettingCard icon={SpaceIcon} title="模式">
<div>
<Label
className={cn(
theme === "dark" ? "text-neutral-300" : "text-gray-600"
)}
>
</Label>
<div className="flex items-center gap-4">
<Switch
checked={globalSettings.useIconMode}
onCheckedChange={(checked) =>
updateGlobalSettings({
...globalSettings,
useIconMode: checked
})
}
/>
</div>
</div>
</SettingCard>
</div>
</motion.div>
);
@@ -1,47 +1,50 @@
import React, { useState } from "react";
import { PlusCircle, GripVertical, Trash2, LucideIcon } from "lucide-react";
import React, { useState, useEffect } from "react";
import { PlusCircle, GripVertical, Trash2, Eye, EyeOff } from "lucide-react";
import { Button } from "@/components/ui/button";
import Field from "../Field";
import { useResumeStore } from "@/store/useResumeStore";
import { cn } from "@/lib/utils";
import { Reorder } from "framer-motion";
import { Reorder, AnimatePresence, motion } from "framer-motion";
import IconSelector from "../IconSelector";
import PhotoUpload from "@/components/PhotoSelector";
import Field from "../Field";
import { useResumeStore } from "@/store/useResumeStore";
import { BasicFieldType, CustomFieldType } from "@/types/resume";
type CustomFieldType = {
id: string;
label: string;
value: string;
icon: string;
};
type Theme = "dark" | "light";
interface IconSelectorProps {
value: string;
onChange: (value: string) => void;
theme: Theme;
}
const DEFAULT_FIELD_ORDER: BasicFieldType[] = [
{ id: "1", key: "name", label: "姓名", type: "text", visible: true },
{
id: "2",
key: "employementStatus",
label: "求职状态",
type: "text",
visible: true
},
{ id: "3", key: "title", label: "职位", type: "text", visible: true },
{ id: "4", key: "birthDate", label: "出生日期", type: "date", visible: true },
{ id: "5", key: "email", label: "电子邮箱", type: "text", visible: true },
{ id: "6", key: "phone", label: "电话", type: "text", visible: true },
{ id: "7", key: "location", label: "所在地", type: "text", visible: true },
{
id: "8",
key: "summary",
label: "个人简介",
type: "textarea",
visible: true
}
];
interface CustomFieldProps {
field: CustomFieldType;
onUpdate: (field: CustomFieldType) => void;
onDelete: (id: string) => void;
theme: Theme;
theme: "dark" | "light";
}
interface BasicInfo {
name?: string;
title?: string;
birthDate?: string;
email?: string;
phone?: string;
location?: string;
summary?: string;
customFields?: CustomFieldType[];
icons?: Record<string, string>;
employementStatus?: string;
}
const itemAnimations = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
transition: { type: "spring", stiffness: 500, damping: 50, mass: 1 }
};
const CustomField: React.FC<CustomFieldProps> = ({
field,
@@ -55,198 +58,330 @@ const CustomField: React.FC<CustomFieldProps> = ({
id={field.id}
className="group touch-none list-none"
>
<div
<motion.div
{...itemAnimations}
className={cn(
"grid grid-cols-[auto_auto_1fr_1fr_auto] gap-2 items-center p-2 rounded-lg border border-transparent",
theme === "dark"
? "hover:border-neutral-700"
: "hover:border-neutral-200"
"grid grid-cols-[auto_auto_1fr_1fr_auto_auto] gap-3 items-center p-3",
"bg-white dark:bg-neutral-800 rounded-xl shadow-sm",
"border border-neutral-100 dark:border-neutral-700",
"transition-all duration-200",
"hover:shadow-md hover:border-neutral-200 dark:hover:border-neutral-600",
!field.visible && "opacity-60"
)}
>
<GripVertical className="w-4 h-4 cursor-grab active:cursor-grabbing text-neutral-400" />
<IconSelector
value={field.icon}
onChange={(value) => onUpdate({ ...field, icon: value })}
theme={theme}
/>
<div className="flex items-center justify-center">
<GripVertical
className={cn(
"w-4 h-4 cursor-grab active:cursor-grabbing",
"text-neutral-300 dark:text-neutral-500",
"transition-colors duration-200",
"group-hover:text-neutral-400 dark:group-hover:text-neutral-400"
)}
/>
</div>
<div className="flex items-center justify-center">
<IconSelector
value={field.icon}
onChange={(value) => onUpdate({ ...field, icon: value })}
theme={theme}
/>
</div>
<Field
label=""
value={field.label}
onChange={(value) => onUpdate({ ...field, label: value })}
placeholder="字段名称"
className={cn(
"bg-neutral-50 dark:bg-neutral-900",
"border-neutral-200 dark:border-neutral-700",
"focus:border-blue-500 dark:focus:border-blue-400",
"placeholder-neutral-400 dark:placeholder-neutral-500"
)}
/>
<Field
label=""
value={field.value}
onChange={(value) => onUpdate({ ...field, value })}
placeholder="字段内容"
/>
<Trash2
onClick={() => onDelete(field.id)}
size={26}
className={cn(
"p-1.5 rounded-md cursor-pointer",
theme === "dark"
? "text-red-400 hover:text-red-300"
: "text-red-500 hover:text-red-600"
"bg-neutral-50 dark:bg-neutral-900",
"border-neutral-200 dark:border-neutral-700",
"focus:border-blue-500 dark:focus:border-blue-400",
"placeholder-neutral-400 dark:placeholder-neutral-500"
)}
/>
</div>
{field.visible ? (
<Eye
className="w-4 h-4 cursor-pointer"
onClick={() => onUpdate({ ...field, visible: !field.visible })}
/>
) : (
<EyeOff
className="w-4 h-4 cursor-pointer"
onClick={() => onUpdate({ ...field, visible: !field.visible })}
/>
)}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onDelete(field.id)}
className={cn(
"p-2 rounded-lg transition-colors",
"hover:bg-red-100 dark:hover:bg-red-900/40",
"text-red-500 bg-red-50 dark:bg-red-900/20"
)}
>
<Trash2 className="w-4 h-4" />
</motion.button>
</motion.div>
</Reorder.Item>
);
};
const BasicPanel: React.FC = () => {
const { basic, updateBasicInfo, theme } = useResumeStore();
const [fields, setFields] = useState<CustomFieldType[]>(
basic?.customFields || []
const [customFields, setCustomFields] = useState<CustomFieldType[]>(
basic?.customFields?.map((field) => ({
...field,
visible: field.visible ?? true
})) || []
);
const [basicFields, setBasicFields] = useState<BasicFieldType[]>(() => {
if (!basic.fieldOrder) {
return DEFAULT_FIELD_ORDER;
}
return basic.fieldOrder.map((field) => ({
...field,
visible: field.visible ?? true
}));
});
useEffect(() => {
if (!basic.fieldOrder) {
updateBasicInfo({
...basic,
fieldOrder: DEFAULT_FIELD_ORDER
});
}
}, []);
const handleBasicReorder = (newOrder: BasicFieldType[]) => {
setBasicFields(newOrder);
updateBasicInfo({
...basic,
fieldOrder: newOrder
});
};
const toggleFieldVisibility = (fieldId: string, isVisible: boolean) => {
const newFields = basicFields.map((field) =>
field.id === fieldId ? { ...field, visible: isVisible } : field
);
setBasicFields(newFields);
updateBasicInfo({
...basic,
fieldOrder: newFields
});
};
const addCustomField = () => {
const fieldToAdd: CustomFieldType = {
id: crypto.randomUUID(),
label: "",
value: "",
icon: "User"
icon: "User",
visible: true
};
const updatedFields = [...fields, fieldToAdd];
setFields(updatedFields);
const updatedFields = [...customFields, fieldToAdd];
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
customFields: updatedFields
});
};
const updateField = (updatedField: CustomFieldType) => {
const updatedFields = fields.map((field) =>
const updateCustomField = (updatedField: CustomFieldType) => {
const updatedFields = customFields.map((field) =>
field.id === updatedField.id ? updatedField : field
);
setFields(updatedFields);
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
customFields: updatedFields
});
};
const deleteField = (id: string) => {
const updatedFields = fields.filter((field) => field.id !== id);
setFields(updatedFields);
const deleteCustomField = (id: string) => {
const updatedFields = customFields.filter((field) => field.id !== id);
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
customFields: updatedFields
});
};
const handleReorder = (newOrder: CustomFieldType[]) => {
setFields(newOrder);
const handleCustomFieldsReorder = (newOrder: CustomFieldType[]) => {
setCustomFields(newOrder);
updateBasicInfo({
...basic,
customFields: newOrder
});
};
const renderField = (
key: keyof BasicInfo,
label: string,
options: {
type?: "date" | "textarea" | "text" | "editor" | undefined;
isCustom?: boolean;
field?: CustomFieldType | null;
} = {}
) => {
const { type = "text", isCustom = false, field = null } = options;
const selectedIcon = basic?.icons?.[key] || "User";
const renderBasicField = (field: BasicFieldType) => {
const selectedIcon = basic?.icons?.[field.key] || "User";
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<IconSelector
value={selectedIcon}
onChange={(value) => {
updateBasicInfo({
...basic,
icons: {
...(basic?.icons || {}),
[key]: value
}
});
}}
theme={theme}
/>
<span className="text-sm font-medium">{label}</span>
</div>
<Field
label=""
value={isCustom ? field?.value || "" : (basic?.[key] as string) || ""}
onChange={(value) =>
isCustom && field
? updateField({ ...field, value })
: updateBasicInfo({ ...basic, [key]: value })
}
placeholder={`请输入${label}`}
type={type}
/>
</div>
<Reorder.Item
value={field}
id={field.id}
key={field.id}
className="group touch-none list-none"
>
<motion.div
{...itemAnimations}
className={cn(
"grid grid-cols-[auto_auto_1fr_auto] gap-3 items-center p-3",
"bg-white dark:bg-neutral-800 rounded-xl shadow-sm",
"border border-neutral-100 dark:border-neutral-700",
"transition-all duration-200",
"hover:shadow-md hover:border-neutral-200 dark:hover:border-neutral-600",
!field.visible && "opacity-60"
)}
>
<div className="flex items-center justify-center">
<GripVertical
className={cn(
"w-4 h-4 cursor-grab active:cursor-grabbing",
"text-neutral-300 dark:text-neutral-500",
"transition-colors duration-200",
"group-hover:text-neutral-400 dark:group-hover:text-neutral-400"
)}
/>
</div>
<div className="flex items-center justify-center">
<IconSelector
value={selectedIcon}
onChange={(value) => {
updateBasicInfo({
...basic,
icons: {
...(basic?.icons || {}),
[field.key]: value
}
});
}}
theme={theme}
/>
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-200">
{field.label}
</span>
</div>
<Field
label=""
value={(basic[field.key] as string) || ""}
onChange={(value) =>
updateBasicInfo({
...basic,
[field.key]: value
})
}
placeholder={`请输入${field.label}`}
type={field.type}
/>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 px-2"
onClick={() => toggleFieldVisibility(field.id, !field.visible)}
>
{field.visible ? (
<Eye className="w-4 h-4 cursor-pointer" />
) : (
<EyeOff className="w-4 h-4 cursor-pointer" />
)}
</Button>
</motion.div>
</Reorder.Item>
);
};
return (
<div className="space-y-5 p-4">
<PhotoUpload theme={theme} />
<div className="flex justify-between items-start gap-4">
<div className="flex-1">{renderField("name", "姓名")}</div>
<div className="space-y-6 p-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white dark:bg-neutral-800 rounded-xl p-3 shadow-sm border border-neutral-100 dark:border-neutral-700"
>
<PhotoUpload theme={theme} />
</motion.div>
<div>{renderField("employementStatus", "在职状态")}</div>
</div>
{renderField("title", "职位")}
{renderField("birthDate", "出生日期", { type: "date" })}
<div className="grid grid-cols-2 gap-5">
{renderField("email", "电子邮箱")}
{renderField("phone", "电话")}
</div>
{renderField("location", "所在地")}
{renderField("summary", "个人简介", { type: "textarea" })}
<div className="space-y-6">
<div className="space-y-3">
<h3 className="font-medium text-neutral-900 dark:text-neutral-200 px-1">
</h3>
<AnimatePresence mode="popLayout">
<Reorder.Group
axis="y"
as="div"
values={basicFields}
onReorder={handleBasicReorder}
className="space-y-3"
>
{basicFields.map((field) => renderBasicField(field))}
</Reorder.Group>
</AnimatePresence>
</div>
{/* 自定义字段 */}
<div className="space-y-4">
<Reorder.Group
axis="y"
values={fields}
onReorder={handleReorder}
className="space-y-2"
>
{Array.isArray(fields) &&
fields.map((field) => (
<CustomField
key={field.id}
field={field}
onUpdate={updateField}
onDelete={deleteField}
theme={theme}
/>
))}
</Reorder.Group>
<div className="space-y-3">
<h3 className="font-medium text-neutral-900 dark:text-neutral-200 px-1">
</h3>
<AnimatePresence mode="popLayout">
<Reorder.Group
axis="y"
as="div"
values={customFields}
onReorder={handleCustomFieldsReorder}
className="space-y-3"
>
{Array.isArray(customFields) &&
customFields.map((field) => (
<CustomField
key={field.id}
field={field}
onUpdate={updateCustomField}
onDelete={deleteCustomField}
theme={theme}
/>
))}
</Reorder.Group>
</AnimatePresence>
<Button
onClick={addCustomField}
className={cn(
"w-full",
theme === "dark"
? "bg-indigo-600 hover:bg-indigo-700 text-white"
: "bg-black hover:bg-neutral-800 text-white"
)}
>
<PlusCircle
className={cn(
"w-4 h-4 mr-2",
theme === "dark" ? "text-neutral-400" : "text-neutral-500"
)}
/>
</Button>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
<Button
onClick={addCustomField}
className={cn(
"w-full mt-4 transition-colors",
"text-white shadow-sm",
"bg-indigo-600",
"hover:bg-indigo-700 hover:bg-indigo-700"
)}
>
<PlusCircle className="w-4 h-4 mr-2" />
</Button>
</motion.div>
</div>
</div>
</div>
);
+132 -38
View File
@@ -2,20 +2,105 @@ import { cn } from "@/lib/utils";
import {
BasicInfo,
getBorderRadiusValue,
GlobalSettings
GlobalSettings,
BasicFieldType
} from "@/types/resume";
import { motion } from "framer-motion";
import React from "react";
import * as Icons from "lucide-react";
interface BaaseInfoProps {
interface BaseInfoProps {
basic: BasicInfo;
globalSettings: GlobalSettings | undefined;
}
export function BaseInfo({ basic, globalSettings }: BaaseInfoProps) {
export function BaseInfo({ basic, globalSettings }: BaseInfoProps) {
const useIconMode = globalSettings?.useIconMode ?? false;
const getIcon = (iconName: string) => {
const IconComponent = Icons[
iconName as keyof typeof Icons
] as React.ElementType;
return IconComponent ? <IconComponent className="w-4 h-4" /> : null;
};
const getOrderedFields = () => {
if (!basic.fieldOrder) {
return [
{
key: "email",
value: basic.email,
icon: basic.icons?.email || "Mail",
label: "电子邮箱",
visible: true
}
].filter((item) => Boolean(item.value && item.visible));
}
return basic.fieldOrder
.filter(
(field) =>
field.visible !== false &&
field.key !== "name" &&
field.key !== "title"
)
.map((field) => ({
key: field.key,
value:
field.key === "birthDate" && basic[field.key]
? new Date(basic[field.key] as string).toLocaleDateString()
: (basic[field.key] as string),
icon: basic.icons?.[field.key] || "User",
label: field.label,
visible: field.visible
}))
.filter((item) => Boolean(item.value));
};
const orderedFields = getOrderedFields();
const allFields = [
...orderedFields,
...(basic.customFields
?.filter((field) => field.visible !== false)
.map((field) => ({
key: field.id,
value: field.value,
icon: field.icon,
label: field.label,
visible: true
})) || [])
];
const getNameField = () => {
const nameField = basic.fieldOrder?.find(
(field) => field.key === "name"
) || {
key: "name",
label: "姓名",
visible: true
};
return nameField.visible !== false ? nameField : null;
};
const getTitleField = () => {
const titleField = basic.fieldOrder?.find(
(field) => field.key === "title"
) || {
key: "title",
label: "职位",
visible: true
};
return titleField.visible !== false ? titleField : null;
};
const nameField = getNameField();
const titleField = getTitleField();
return (
<div className="text-center space-y-4">
{basic.photo && (
{/* 头像部分 */}
{basic.photo && basic.photoConfig?.visible && (
<motion.div layout="position" className="flex justify-center">
<div
style={{
@@ -39,47 +124,56 @@ export function BaseInfo({ basic, globalSettings }: BaaseInfoProps) {
</motion.div>
)}
<motion.h1
layout="position"
className={cn("font-bold", "text-gray-900")}
style={{
fontSize: `${(globalSettings?.headerSize || 24) * 1.5}px`
}}
>
{basic.name}
</motion.h1>
<motion.h2
layout="position"
className="text-gray-600"
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{basic.title}
</motion.h2>
{/* 姓名 - 仅在可见时显示 */}
{nameField && basic[nameField.key] && (
<motion.h1
layout="position"
className="font-bold text-gray-900"
style={{
fontSize: `${(globalSettings?.headerSize || 24) * 1.5}px`
}}
>
{basic[nameField.key] as string}
</motion.h1>
)}
{/* 职位 - 仅在可见时显示 */}
{titleField && basic[titleField.key] && (
<motion.h2
layout="position"
className="text-gray-600"
style={{
fontSize: `${globalSettings?.subheaderSize || 16}px`
}}
>
{basic[titleField.key] as string}
</motion.h2>
)}
{/* 其他字段 */}
<motion.div
layout="position"
className="flex justify-center items-center space-x-4 flex-wrap"
className="flex justify-center items-center flex-wrap gap-3"
style={{
fontSize: `${globalSettings?.baseFontSize || 14}px`,
color: "rgb(75, 85, 99)"
}}
>
{[
basic.email,
basic.phone,
basic.location,
basic.employementStatus,
basic.birthDate ? new Date(basic.birthDate).toLocaleDateString() : "",
...(basic.customFields?.map((field) => field.value) || [])
]
.filter(Boolean)
.map((item, index, array) => (
<React.Fragment key={index}>
<span>{item}</span>
{index < array.length - 1 && <span></span>}
</React.Fragment>
))}
{allFields.map((item, index) => (
<div key={item.key} className="flex items-center">
{useIconMode ? (
<span className="flex items-center gap-1">
{getIcon(item.icon)}
<span>{item.value}</span>
</span>
) : (
<span className="flex items-center gap-1">
<span className="text-gray-500">{item.label}:</span>
<span>{item.value}</span>
</span>
)}
</div>
))}
</motion.div>
</div>
);
+2 -1
View File
@@ -141,7 +141,8 @@ const initialState = {
lineHeight: 1,
sectionSpacing: 20,
headerSize: 18,
subheaderSize: 16
subheaderSize: 16,
useIconMode: false
}
};
+21 -1
View File
@@ -4,6 +4,7 @@ export interface PhotoConfig {
aspectRatio: "1:1" | "4:3" | "3:4" | "16:9" | "custom";
borderRadius: "none" | "medium" | "full" | "custom";
customBorderRadius: number;
visible?: boolean;
}
export const DEFAULT_CONFIG: PhotoConfig = {
@@ -11,7 +12,8 @@ export const DEFAULT_CONFIG: PhotoConfig = {
height: 96,
aspectRatio: "1:1",
borderRadius: "none",
customBorderRadius: 0
customBorderRadius: 0,
visible: true
};
// 助手函数
@@ -43,6 +45,21 @@ export const getBorderRadiusValue = (config?: PhotoConfig) => {
}
};
export interface BasicFieldType {
id: string;
key: keyof BasicInfo;
label: string;
type?: "date" | "textarea" | "text" | "editor";
visible: boolean;
}
export interface CustomFieldType {
id: string;
label: string;
value: string;
icon: string;
visible?: boolean;
}
export interface BasicInfo {
birthDate: string;
name: string;
@@ -55,11 +72,13 @@ export interface BasicInfo {
employementStatus: string;
photo: string;
photoConfig: PhotoConfig;
fieldOrder?: BasicFieldType[]; // 新增字段排序
customFields: Array<{
id: string;
label: string;
value: string;
icon: string;
visible?: boolean;
}>;
}
@@ -108,6 +127,7 @@ export type GlobalSettings = {
sectionSpacing?: number | undefined;
headerSize?: number | undefined;
subheaderSize?: number | undefined;
useIconMode?: boolean | undefined;
};
export interface ResumeTheme {