mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: workbench i18n
This commit is contained in:
@@ -144,6 +144,7 @@ const Field = ({
|
||||
);
|
||||
return;
|
||||
}
|
||||
setShowPolishDialog(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Layout, Type, SpaceIcon, Palette } from "lucide-react";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -81,6 +82,19 @@ export function SidePanel() {
|
||||
} = activeResume || {};
|
||||
|
||||
const { themeColor } = globalSettings;
|
||||
const t = useTranslations('workbench.sidePanel');
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: t('typography.font.sans') },
|
||||
{ value: "serif", label: t('typography.font.serif') },
|
||||
{ value: "mono", label: t('typography.font.mono') },
|
||||
];
|
||||
|
||||
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(
|
||||
() =>
|
||||
@@ -122,7 +136,7 @@ export function SidePanel() {
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
>
|
||||
<div className="p-4 space-y-4">
|
||||
<SettingCard icon={Layout} title="布局">
|
||||
<SettingCard icon={Layout} title={t('layout.title')}>
|
||||
<LayoutSetting
|
||||
menuSections={menuSections}
|
||||
activeSection={activeSection}
|
||||
@@ -139,13 +153,13 @@ export function SidePanel() {
|
||||
onClick={handleCreateSection}
|
||||
className="flex justify-center w-full rounded-lg items-center gap-2 py-2 px-3 text-sm font-medium text-primary bg-indigo-50"
|
||||
>
|
||||
添加自定义模块
|
||||
{t('layout.addCustomSection')}
|
||||
</motion.button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{/* 主题色设置 */}
|
||||
<SettingCard icon={Palette} title="主题色">
|
||||
<SettingCard icon={Palette} title={t('theme.title')}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{THEME_COLORS.map((presetTheme) => (
|
||||
@@ -186,7 +200,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
自定义
|
||||
{t('theme.custom')}
|
||||
</div>
|
||||
<motion.input
|
||||
type="color"
|
||||
@@ -198,11 +212,11 @@ export function SidePanel() {
|
||||
</SettingCard>
|
||||
|
||||
{/* 排版设置 */}
|
||||
<SettingCard icon={Type} title="排版">
|
||||
<SettingCard icon={Type} title={t('typography.title')}>
|
||||
<div className="space-y-6">
|
||||
{/* <div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
字体
|
||||
{t('typography.font.title')}
|
||||
</Label>
|
||||
<Select
|
||||
value={globalSettings?.fontFamily}
|
||||
@@ -220,16 +234,15 @@ export function SidePanel() {
|
||||
</motion.div>
|
||||
<SelectContent
|
||||
className={cn(
|
||||
theme === "dark"
|
||||
? "bg-neutral-900 border-neutral-800 text-white"
|
||||
: "bg-white border-gray-200"
|
||||
"dark:bg-neutral-900 dark:border-neutral-800 text-white",
|
||||
"bg-white border-gray-200"
|
||||
)}
|
||||
>
|
||||
{fontOptions.map((font) => (
|
||||
<SelectItem
|
||||
key={font.value}
|
||||
value={font.value}
|
||||
className="cursor-pointer transition-colors focus:bg-gray-100 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800"
|
||||
className="cursor-pointer transition-colors hover:bg-gray-100 focus:bg-gray-100 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800"
|
||||
>
|
||||
{font.label}
|
||||
</SelectItem>
|
||||
@@ -241,7 +254,7 @@ export function SidePanel() {
|
||||
{/* 行高选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
行高
|
||||
{t('typography.lineHeight.title')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
@@ -261,7 +274,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
基础字号
|
||||
{t('typography.baseFontSize.title')}
|
||||
</Label>
|
||||
<Select
|
||||
value={globalSettings?.baseFontSize?.toString()}
|
||||
@@ -279,8 +292,8 @@ export function SidePanel() {
|
||||
</motion.div>
|
||||
<SelectContent
|
||||
className={cn(
|
||||
"dark:bg-neutral-900 dark:border-neutral-800 text-white",
|
||||
"bg-white text-gray-700 border-gray-200"
|
||||
"dark:bg-neutral-900 dark:border-neutral-800 dark:text-white",
|
||||
"bg-white border-gray-200"
|
||||
)}
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 18, 20, 24].map((size) => (
|
||||
@@ -298,7 +311,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
模块标题字号
|
||||
{t('typography.headerSize.title')}
|
||||
</Label>
|
||||
<Select
|
||||
value={globalSettings?.headerSize?.toString()}
|
||||
@@ -335,7 +348,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
模块二级标题字号
|
||||
{t('typography.subheaderSize.title')}
|
||||
</Label>
|
||||
<Select
|
||||
value={globalSettings?.subheaderSize?.toString()}
|
||||
@@ -373,11 +386,11 @@ export function SidePanel() {
|
||||
</SettingCard>
|
||||
|
||||
{/* 间距设置 */}
|
||||
<SettingCard icon={SpaceIcon} title="间距">
|
||||
<SettingCard icon={SpaceIcon} title={t('spacing.title')}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
页边距
|
||||
{t('spacing.pagePadding.title')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
@@ -397,7 +410,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
模块间距
|
||||
{t('spacing.sectionSpacing.title')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
@@ -417,7 +430,7 @@ export function SidePanel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
段落间距
|
||||
{t('spacing.paragraphSpacing.title')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
@@ -437,10 +450,10 @@ export function SidePanel() {
|
||||
</SettingCard>
|
||||
|
||||
{/* 模式设置 */}
|
||||
<SettingCard icon={SpaceIcon} title="模式">
|
||||
<SettingCard icon={SpaceIcon} title={t('mode.title')}>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-600 dark:text-neutral-300">
|
||||
简洁模式
|
||||
{t('mode.useIconMode.title')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { PlusCircle, GripVertical, Trash2, Eye, EyeOff } from "lucide-react";
|
||||
import { Reorder, AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -30,6 +31,8 @@ const CustomField: React.FC<CustomFieldProps> = ({
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
const t = useTranslations("workbench.basicPanel");
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
value={field}
|
||||
@@ -71,7 +74,7 @@ const CustomField: React.FC<CustomFieldProps> = ({
|
||||
label: value,
|
||||
})
|
||||
}
|
||||
placeholder="标签"
|
||||
placeholder={t("customFields.placeholders.label")}
|
||||
className={cn(
|
||||
"bg-neutral-50 dark:bg-neutral-900",
|
||||
"border-neutral-200 dark:border-neutral-700",
|
||||
@@ -87,7 +90,7 @@ const CustomField: React.FC<CustomFieldProps> = ({
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
placeholder="值"
|
||||
placeholder={t("customFields.placeholders.value")}
|
||||
className={cn(
|
||||
"bg-neutral-50 dark:bg-neutral-900",
|
||||
"border-neutral-200 dark:border-neutral-700",
|
||||
@@ -142,6 +145,7 @@ const BasicPanel: React.FC = () => {
|
||||
visible: field.visible ?? true,
|
||||
}));
|
||||
});
|
||||
const t = useTranslations("workbench.basicPanel");
|
||||
|
||||
const handleBasicReorder = (newOrder: BasicFieldType[]) => {
|
||||
setBasicFields(newOrder);
|
||||
@@ -205,6 +209,7 @@ const BasicPanel: React.FC = () => {
|
||||
customFields: newOrder,
|
||||
});
|
||||
};
|
||||
|
||||
const renderBasicField = (field: BasicFieldType) => {
|
||||
const selectedIcon = basic?.icons?.[field.key] || "User";
|
||||
|
||||
@@ -258,7 +263,7 @@ const BasicPanel: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<div className=" w-[80px] ml-[4px] text-sm font-medium text-neutral-700 dark:text-neutral-200">
|
||||
{field.label}
|
||||
{t(`basicFields.${field.key}`)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Field
|
||||
@@ -310,7 +315,7 @@ const BasicPanel: React.FC = () => {
|
||||
<motion.div className="space-y-6">
|
||||
<motion.div className="space-y-3">
|
||||
<motion.h3 className="font-medium text-neutral-900 dark:text-neutral-200 px-1">
|
||||
基本字段
|
||||
{t("basicField")}
|
||||
</motion.h3>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<Reorder.Group
|
||||
@@ -327,7 +332,7 @@ const BasicPanel: React.FC = () => {
|
||||
|
||||
<motion.div className="space-y-3">
|
||||
<motion.h3 className="font-medium text-neutral-900 dark:text-neutral-200 px-1">
|
||||
自定义字段
|
||||
{t("customField")}
|
||||
</motion.h3>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<Reorder.Group
|
||||
@@ -355,7 +360,7 @@ const BasicPanel: React.FC = () => {
|
||||
>
|
||||
<Button onClick={addCustomField} className="w-full mt-4">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
添加自定义字段
|
||||
{t("customFields.addButton")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
@@ -367,7 +372,7 @@ const BasicPanel: React.FC = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<motion.h3 className="font-medium text-neutral-900 dark:text-neutral-200 px-1">
|
||||
Github 贡献图
|
||||
{t("githubContributions")}
|
||||
</motion.h3>
|
||||
|
||||
<Switch
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GripVertical, Eye, EyeOff, ChevronDown, Trash2 } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import Field from "../Field";
|
||||
import ThemeModal from "@/components/shared/ThemeModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface EducationEditorProps {
|
||||
education: Education;
|
||||
@@ -25,6 +26,7 @@ const EducationEditor: React.FC<EducationEditorProps> = ({
|
||||
education,
|
||||
onSave,
|
||||
}) => {
|
||||
const t = useTranslations("workbench.educationItem");
|
||||
const handleChange = (field: keyof Education, value: string) => {
|
||||
onSave({
|
||||
...education,
|
||||
@@ -37,40 +39,40 @@ const EducationEditor: React.FC<EducationEditorProps> = ({
|
||||
<div className="grid gap-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="学校名称"
|
||||
label={t("labels.school")}
|
||||
value={education.school}
|
||||
onChange={(value) => handleChange("school", value)}
|
||||
placeholder="学校名称"
|
||||
placeholder={t("placeholders.school")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="所在地"
|
||||
label={t("labels.location")}
|
||||
value={education.location || ""}
|
||||
onChange={(value) => handleChange("location", value)}
|
||||
placeholder="城市"
|
||||
placeholder={t("placeholders.location")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="专业"
|
||||
label={t("labels.major")}
|
||||
value={education.major}
|
||||
onChange={(value) => handleChange("major", value)}
|
||||
placeholder="专业名称"
|
||||
placeholder={t("placeholders.major")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="学历"
|
||||
label={t("labels.degree")}
|
||||
value={education.degree}
|
||||
onChange={(value) => handleChange("degree", value)}
|
||||
placeholder="学历"
|
||||
placeholder={t("placeholders.degree")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="开始时间"
|
||||
label={t("labels.startDate")}
|
||||
value={education.startDate}
|
||||
onChange={(value) => handleChange("startDate", value)}
|
||||
type="date"
|
||||
@@ -78,7 +80,7 @@ const EducationEditor: React.FC<EducationEditorProps> = ({
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="结束时间"
|
||||
label={t("labels.endDate")}
|
||||
value={education.endDate}
|
||||
onChange={(value) => handleChange("endDate", value)}
|
||||
type="date"
|
||||
@@ -88,18 +90,18 @@ const EducationEditor: React.FC<EducationEditorProps> = ({
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label="GPA"
|
||||
label={t("labels.gpa")}
|
||||
value={education.gpa || ""}
|
||||
onChange={(value) => handleChange("gpa", value)}
|
||||
placeholder="选填"
|
||||
placeholder={t("placeholders.gpa")}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label="在校经历"
|
||||
label={t("labels.description")}
|
||||
value={education.description}
|
||||
onChange={(value) => handleChange("description", value)}
|
||||
type="editor"
|
||||
placeholder="描述你的在校表现、获奖经历等..."
|
||||
placeholder={t("placeholders.description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,20 +3,22 @@ import { cn } from "@/lib/utils";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import EducationItem from "./EducationItem";
|
||||
import { Education } from "@/types/resume";
|
||||
|
||||
const EducationPanel = () => {
|
||||
const t = useTranslations('workbench.educationPanel');
|
||||
const { activeResume, updateEducation, updateEducationBatch } =
|
||||
useResumeStore();
|
||||
const { education = [] } = activeResume || {};
|
||||
const handleCreateProject = () => {
|
||||
const newEducation: Education = {
|
||||
id: crypto.randomUUID(),
|
||||
school: "家里蹲大学",
|
||||
major: "",
|
||||
degree: "",
|
||||
school: t('defaultProject.school'),
|
||||
major: t('defaultProject.major'),
|
||||
degree: t('defaultProject.degree'),
|
||||
startDate: "2015-09-01",
|
||||
endDate: "2019-06-30",
|
||||
description: "",
|
||||
@@ -50,10 +52,11 @@ const EducationPanel = () => {
|
||||
|
||||
<Button onClick={handleCreateProject} className="w-full">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
添加
|
||||
{t('addButton')}
|
||||
</Button>
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationPanel;
|
||||
|
||||
@@ -13,6 +13,7 @@ import Field from "../Field";
|
||||
import { Experience } from "@/types/resume";
|
||||
import ThemeModal from "@/components/shared/ThemeModal";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ProjectEditorProps {
|
||||
experience: Experience;
|
||||
@@ -25,6 +26,8 @@ const ProjectEditor: React.FC<ProjectEditorProps> = ({
|
||||
experience,
|
||||
onSave,
|
||||
}) => {
|
||||
const t = useTranslations("workbench.experiencePanel");
|
||||
|
||||
const handleChange = (field: keyof Experience, value: string) => {
|
||||
onSave({
|
||||
...experience,
|
||||
@@ -37,33 +40,33 @@ const ProjectEditor: React.FC<ProjectEditorProps> = ({
|
||||
<div className="grid gap-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="公司名称"
|
||||
label={t("placeholders.company")}
|
||||
value={experience.company}
|
||||
onChange={(value) => handleChange("company", value)}
|
||||
placeholder="项目名称"
|
||||
placeholder={t("placeholders.company")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="岗位"
|
||||
label={t("placeholders.position")}
|
||||
value={experience.position}
|
||||
onChange={(value) => handleChange("position", value)}
|
||||
placeholder="如:前端工程师"
|
||||
placeholder={t("placeholders.position")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Field
|
||||
label="开始时间-结束时间"
|
||||
label={t("placeholders.date")}
|
||||
value={experience.date}
|
||||
onChange={(value) => handleChange("date", value)}
|
||||
placeholder="如:2023.01 - 2023.06"
|
||||
placeholder={t("placeholders.date")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="主要职责"
|
||||
label={t("placeholders.details")}
|
||||
value={experience.details}
|
||||
onChange={(value) => handleChange("details", value)}
|
||||
type="editor"
|
||||
placeholder="简要描述项目的背景和目标..."
|
||||
placeholder={t("placeholders.details")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,23 @@ import { cn } from "@/lib/utils";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ExperienceItem from "./ExperienceItem";
|
||||
import { Experience } from "@/types/resume";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
|
||||
const ExperiencePanel = () => {
|
||||
const t = useTranslations("workbench.experiencePanel");
|
||||
const { activeResume, updateExperience, updateExperienceBatch } =
|
||||
useResumeStore();
|
||||
const { experience = [] } = activeResume || {};
|
||||
const handleCreateProject = () => {
|
||||
const newProject: Experience = {
|
||||
id: crypto.randomUUID(),
|
||||
company: "某科技有限公司",
|
||||
position: "高级前端工程师",
|
||||
date: "2020-至今",
|
||||
details: "负责公司核心产品...",
|
||||
company: t("defaultProject.company"),
|
||||
position: t("defaultProject.position"),
|
||||
date: t("defaultProject.date"),
|
||||
details: t("defaultProject.details"),
|
||||
visible: true,
|
||||
};
|
||||
updateExperience(newProject);
|
||||
@@ -44,10 +46,11 @@ const ExperiencePanel = () => {
|
||||
|
||||
<Button onClick={handleCreateProject} className="w-full">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
添加
|
||||
{t("addButton")}
|
||||
</Button>
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExperiencePanel;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ChevronDown, Eye, EyeOff, GripVertical, Trash2 } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import Field from "../Field";
|
||||
import ThemeModal from "@/components/shared/ThemeModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -30,6 +31,7 @@ interface ProjectEditorProps {
|
||||
}
|
||||
|
||||
const ProjectEditor: React.FC<ProjectEditorProps> = ({ project, onSave }) => {
|
||||
const t = useTranslations("workbench.projectItem");
|
||||
const handleChange = (field: keyof Project, value: string) => {
|
||||
onSave({
|
||||
...project,
|
||||
@@ -42,33 +44,33 @@ const ProjectEditor: React.FC<ProjectEditorProps> = ({ project, onSave }) => {
|
||||
<div className="grid gap-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="项目名称"
|
||||
label={t("labels.name")}
|
||||
value={project.name}
|
||||
onChange={(value) => handleChange("name", value)}
|
||||
placeholder="项目名称"
|
||||
placeholder={t("placeholders.name")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="担任角色"
|
||||
label={t("labels.role")}
|
||||
value={project.role}
|
||||
onChange={(value) => handleChange("role", value)}
|
||||
placeholder="如:前端负责人"
|
||||
placeholder={t("placeholders.role")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Field
|
||||
label="项目时间"
|
||||
label={t("labels.date")}
|
||||
value={project.date}
|
||||
onChange={(value) => handleChange("date", value)}
|
||||
placeholder="如:2023.01 - 2023.06"
|
||||
placeholder={t("placeholders.date")}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="项目描述"
|
||||
label={t("labels.description")}
|
||||
value={project.description}
|
||||
onChange={(value) => handleChange("description", value)}
|
||||
type="editor"
|
||||
placeholder="简要描述项目的背景和目标..."
|
||||
placeholder={t("placeholders.description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,21 +2,23 @@ import { cn } from "@/lib/utils";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ProjectItem from "./ProjectItem";
|
||||
import { Project } from "@/types/resume";
|
||||
|
||||
const ProjectPanel = () => {
|
||||
const t = useTranslations("workbench.projectPanel");
|
||||
const { activeResume, updateProjects, updateProjectsBatch } =
|
||||
useResumeStore();
|
||||
const { projects = [] } = activeResume || {};
|
||||
const handleCreateProject = () => {
|
||||
const newProject: Project = {
|
||||
id: crypto.randomUUID(),
|
||||
name: "Project Name",
|
||||
role: "Project Role",
|
||||
date: "",
|
||||
description: "",
|
||||
name: t("defaultProject.name"),
|
||||
role: t("defaultProject.role"),
|
||||
date: t("defaultProject.date"),
|
||||
description: t("defaultProject.description"),
|
||||
visible: true,
|
||||
};
|
||||
updateProjects(newProject);
|
||||
@@ -43,10 +45,11 @@ const ProjectPanel = () => {
|
||||
|
||||
<Button onClick={handleCreateProject} className="w-full">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
添加
|
||||
{t("addButton")}
|
||||
</Button>
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectPanel;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Download, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const getOptimizedStyles = () => {
|
||||
const styleCache = new Map();
|
||||
@@ -70,6 +71,7 @@ const PdfExport = () => {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const { activeResume } = useResumeStore();
|
||||
const { globalSettings = {}, title } = activeResume || {};
|
||||
const t = useTranslations("pdfExport");
|
||||
|
||||
const handleExport = async () => {
|
||||
const exportStartTime = performance.now();
|
||||
@@ -118,10 +120,10 @@ const PdfExport = () => {
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
console.log(`Total export took ${performance.now() - exportStartTime}ms`);
|
||||
toast.success("PDF 导出成功!");
|
||||
toast.success(t("toast.success"));
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
toast.error("PDF 导出失败,请重试");
|
||||
toast.error(t("toast.error"));
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -140,7 +142,7 @@ const PdfExport = () => {
|
||||
) : (
|
||||
<Download className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isExporting ? "导出中..." : "导出 PDF"}</span>
|
||||
<span>{isExporting ? t("button.exporting") : t("button.default")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,5 +3,5 @@ import { setUserLocale } from "@/i18n/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
export default async function updateLocale(locale: string) {
|
||||
setUserLocale(locale);
|
||||
revalidatePath("/app");
|
||||
// revalidatePath("/app");
|
||||
}
|
||||
|
||||
@@ -74,5 +74,228 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pdfExport": {
|
||||
"button": {
|
||||
"exporting": "Exporting...",
|
||||
"default": "Export PDF"
|
||||
},
|
||||
"toast": {
|
||||
"success": "PDF exported successfully!",
|
||||
"error": "PDF export failed, please try again"
|
||||
}
|
||||
},
|
||||
"workbench": {
|
||||
"sidePanel": {
|
||||
"layout": {
|
||||
"title": "Layout",
|
||||
"addCustomSection": "Add Custom Section"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Color",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"typography": {
|
||||
"title": "Typography",
|
||||
"font": {
|
||||
"title": "Font",
|
||||
"sans": "Sans-serif",
|
||||
"serif": "Serif",
|
||||
"mono": "Monospace"
|
||||
},
|
||||
"lineHeight": {
|
||||
"title": "Line Height",
|
||||
"normal": "Default",
|
||||
"relaxed": "Relaxed",
|
||||
"loose": "Loose"
|
||||
},
|
||||
"baseFontSize": {
|
||||
"title": "Base Font Size"
|
||||
},
|
||||
"headerSize": {
|
||||
"title": "Section Header Size"
|
||||
},
|
||||
"subheaderSize": {
|
||||
"title": "Section Subheader Size"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"title": "Spacing",
|
||||
"pagePadding": {
|
||||
"title": "Page Padding"
|
||||
},
|
||||
"sectionSpacing": {
|
||||
"title": "Section Spacing"
|
||||
},
|
||||
"paragraphSpacing": {
|
||||
"title": "Paragraph Spacing"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"title": "Mode",
|
||||
"useIconMode": {
|
||||
"title": "Compact Mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"basicPanel": {
|
||||
"basicField": "Basic",
|
||||
"customField": "Custom",
|
||||
"customFields": {
|
||||
"placeholders": {
|
||||
"label": "Label",
|
||||
"value": "Value"
|
||||
},
|
||||
"addButton": "Add Custom Field"
|
||||
},
|
||||
"basicFields": {
|
||||
"name": "Name",
|
||||
"title": "Title",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"website": "Website",
|
||||
"location": "Location",
|
||||
"birthDate": "Birth Date",
|
||||
"employementStatus": "Employ"
|
||||
},
|
||||
"fieldVisibility": {
|
||||
"show": "Show",
|
||||
"hide": "Hide"
|
||||
},
|
||||
"githubContributions": "GitHub Contributions"
|
||||
},
|
||||
"experiencePanel": {
|
||||
"title": "Work Experience",
|
||||
"addButton": "Add Work Experience",
|
||||
"defaultProject": {
|
||||
"company": "Tech Company Ltd.",
|
||||
"position": "Senior Frontend Engineer",
|
||||
"date": "2020-Present",
|
||||
"details": "Responsible for core product development..."
|
||||
},
|
||||
"placeholders": {
|
||||
"company": "company name",
|
||||
"position": "job title",
|
||||
"date": "employment period",
|
||||
"details": "job responsibilities and achievements"
|
||||
}
|
||||
},
|
||||
"experienceItem": {
|
||||
"labels": {
|
||||
"company": "Company Name",
|
||||
"position": "Position",
|
||||
"date": "Employment Period",
|
||||
"details": "Job Responsibilities"
|
||||
},
|
||||
"placeholders": {
|
||||
"company": "Enter company name",
|
||||
"position": "e.g., Frontend Engineer",
|
||||
"date": "e.g., 2020-Present",
|
||||
"details": "Describe your responsibilities and achievements in this role"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "Show",
|
||||
"hide": "Hide"
|
||||
}
|
||||
},
|
||||
"projectPanel": {
|
||||
"title": "Project Experience",
|
||||
"addButton": "Add Project",
|
||||
"defaultProject": {
|
||||
"name": "Personal Project",
|
||||
"description": "Project Description",
|
||||
"role": "Responsibilities",
|
||||
"date": "2023.01 - 2023.06"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "Project Name",
|
||||
"description": "Briefly describe project background and goals",
|
||||
"role": "Your role and responsibilities in the project",
|
||||
"date": "Project time range",
|
||||
"link": "Project link (optional)"
|
||||
}
|
||||
},
|
||||
"projectItem": {
|
||||
"labels": {
|
||||
"name": "Project Name",
|
||||
"role": "Project Role",
|
||||
"date": "Project Period",
|
||||
"description": "Project Description",
|
||||
"technologies": "Tech Stack",
|
||||
"link": "Project Link"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "Enter project name",
|
||||
"role": "Your role in the project",
|
||||
"date": "Project time range",
|
||||
"description": "Briefly describe project background and goals",
|
||||
"technologies": "Technologies and tools used",
|
||||
"link": "Project link (optional)"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "Show",
|
||||
"hide": "Hide"
|
||||
}
|
||||
},
|
||||
"educationPanel": {
|
||||
"title": "Education",
|
||||
"addButton": "Add Education",
|
||||
"defaultProject": {
|
||||
"school": "School Name",
|
||||
"degree": "Degree",
|
||||
"major": "Major",
|
||||
"date": "2020.09 - 2024.06"
|
||||
},
|
||||
"placeholders": {
|
||||
"school": "Enter school name",
|
||||
"degree": "Select degree",
|
||||
"major": "Enter major",
|
||||
"date": "Enter study period"
|
||||
}
|
||||
},
|
||||
"educationItem": {
|
||||
"labels": {
|
||||
"school": "School Name",
|
||||
"degree": "Degree",
|
||||
"major": "Major",
|
||||
"date": "Study Period",
|
||||
"description": "Description",
|
||||
"gpa": "GPA",
|
||||
"location": "Location",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date"
|
||||
},
|
||||
"placeholders": {
|
||||
"school": "Enter school name",
|
||||
"degree": "Select degree",
|
||||
"major": "Enter major",
|
||||
"date": "Enter study time range",
|
||||
"description": "Enter school description",
|
||||
"gpa": "Enter GPA",
|
||||
"location": "Enter location"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "Show",
|
||||
"hide": "Hide"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,5 +74,228 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pdfExport": {
|
||||
"button": {
|
||||
"exporting": "导出中...",
|
||||
"default": "导出 PDF"
|
||||
},
|
||||
"toast": {
|
||||
"success": "PDF 导出成功!",
|
||||
"error": "PDF 导出失败,请重试"
|
||||
}
|
||||
},
|
||||
"workbench": {
|
||||
"sidePanel": {
|
||||
"layout": {
|
||||
"title": "布局",
|
||||
"addCustomSection": "添加自定义模块"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题色",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"typography": {
|
||||
"title": "排版",
|
||||
"font": {
|
||||
"title": "字体",
|
||||
"sans": "无衬线体",
|
||||
"serif": "衬线体",
|
||||
"mono": "等宽体"
|
||||
},
|
||||
"lineHeight": {
|
||||
"title": "行高",
|
||||
"normal": "默认",
|
||||
"relaxed": "适中",
|
||||
"loose": "宽松"
|
||||
},
|
||||
"baseFontSize": {
|
||||
"title": "基础字号"
|
||||
},
|
||||
"headerSize": {
|
||||
"title": "模块标题字号"
|
||||
},
|
||||
"subheaderSize": {
|
||||
"title": "模块二级标题字号"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"title": "间距",
|
||||
"pagePadding": {
|
||||
"title": "页边距"
|
||||
},
|
||||
"sectionSpacing": {
|
||||
"title": "模块间距"
|
||||
},
|
||||
"paragraphSpacing": {
|
||||
"title": "段落间距"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"title": "模式",
|
||||
"useIconMode": {
|
||||
"title": "简洁模式"
|
||||
}
|
||||
}
|
||||
},
|
||||
"basicPanel": {
|
||||
"basicField": "基本信息",
|
||||
"customField": "自定义字段",
|
||||
"customFields": {
|
||||
"placeholders": {
|
||||
"label": "标签",
|
||||
"value": "值"
|
||||
},
|
||||
"addButton": "添加自定义字段"
|
||||
},
|
||||
"basicFields": {
|
||||
"name": "姓名",
|
||||
"title": "职位",
|
||||
"email": "邮箱",
|
||||
"phone": "电话",
|
||||
"website": "个人网站",
|
||||
"location": "地址",
|
||||
"birthDate": "出生日期",
|
||||
"employementStatus": "求职状态"
|
||||
},
|
||||
"fieldVisibility": {
|
||||
"show": "显示",
|
||||
"hide": "隐藏"
|
||||
},
|
||||
"githubContributions": "GitHub 贡献"
|
||||
},
|
||||
"experiencePanel": {
|
||||
"title": "工作经历",
|
||||
"addButton": "添加工作经历",
|
||||
"defaultProject": {
|
||||
"company": "某科技有限公司",
|
||||
"position": "高级前端工程师",
|
||||
"date": "2020-至今",
|
||||
"details": "负责公司核心产品..."
|
||||
},
|
||||
"placeholders": {
|
||||
"company": "请输入公司名称",
|
||||
"position": "请输入职位",
|
||||
"date": "请输入工作时间",
|
||||
"details": "请输入工作职责和成就"
|
||||
}
|
||||
},
|
||||
"experienceItem": {
|
||||
"labels": {
|
||||
"company": "公司名称",
|
||||
"position": "岗位",
|
||||
"date": "工作时间",
|
||||
"details": "工作职责"
|
||||
},
|
||||
"placeholders": {
|
||||
"company": "请输入公司名称",
|
||||
"position": "如:前端工程师",
|
||||
"date": "如:2020-至今",
|
||||
"details": "描述你在这份工作中的职责和成就"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "编辑",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "显示",
|
||||
"hide": "隐藏"
|
||||
}
|
||||
},
|
||||
"projectPanel": {
|
||||
"title": "项目经历",
|
||||
"addButton": "添加项目",
|
||||
"defaultProject": {
|
||||
"name": "个人项目",
|
||||
"description": "项目描述",
|
||||
"role": "负责内容",
|
||||
"technologies": "技术栈",
|
||||
"date": "2023.01 - 2023.06"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "项目名称",
|
||||
"description": "简要描述项目背景和目标",
|
||||
"role": "你在项目中的角色和职责",
|
||||
"technologies": "使用的技术和工具",
|
||||
"date": "项目时间范围",
|
||||
"link": "项目链接(可选)"
|
||||
}
|
||||
},
|
||||
"projectItem": {
|
||||
"labels": {
|
||||
"name": "项目名称",
|
||||
"role": "项目角色",
|
||||
"date": "项目时间",
|
||||
"description": "项目描述",
|
||||
"link": "项目链接"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "请输入项目名称",
|
||||
"role": "你在项目中的角色",
|
||||
"date": "项目时间范围",
|
||||
"description": "简要描述项目背景和目标",
|
||||
"link": "项目链接(可选)"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "编辑",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "显示",
|
||||
"hide": "隐藏"
|
||||
}
|
||||
},
|
||||
"educationPanel": {
|
||||
"title": "教育背景",
|
||||
"addButton": "添加教育经历",
|
||||
"defaultProject": {
|
||||
"school": "学校名称",
|
||||
"degree": "学历",
|
||||
"major": "专业",
|
||||
"date": "2020.09 - 2024.06"
|
||||
},
|
||||
"placeholders": {
|
||||
"school": "请输入学校名称",
|
||||
"degree": "请选择学历",
|
||||
"major": "请输入专业名称",
|
||||
"date": "请输入就读时间范围"
|
||||
}
|
||||
},
|
||||
"educationItem": {
|
||||
"labels": {
|
||||
"school": "学校名称",
|
||||
"degree": "学历",
|
||||
"major": "专业",
|
||||
"date": "就读时间",
|
||||
"description": "学校简介",
|
||||
"gpa": "GPA",
|
||||
"location": "所在地",
|
||||
"startDate": "开始时间",
|
||||
"endDate": "结束时间"
|
||||
},
|
||||
"placeholders": {
|
||||
"school": "请输入学校名称",
|
||||
"degree": "请选择学历",
|
||||
"major": "请输入专业名称",
|
||||
"date": "请输入就读时间范围",
|
||||
"description": "请输入学校简介",
|
||||
"gpa": "请输入GPA",
|
||||
"location": "请输入所在地"
|
||||
},
|
||||
"buttons": {
|
||||
"edit": "编辑",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"visibility": {
|
||||
"show": "显示",
|
||||
"hide": "隐藏"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user