mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: add internationalization support for date selection and year input placeholders
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { CalendarIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronUpIcon, ChevronDownIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -159,7 +157,11 @@ const Field = ({
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{currentDate ? formatDate(currentDate) : <span>选择日期</span>}
|
||||
{currentDate ? (
|
||||
formatDate(currentDate)
|
||||
) : (
|
||||
<span>{t("field.selectDate")}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
@@ -167,7 +169,7 @@ const Field = ({
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="输入年份"
|
||||
placeholder={t("field.enterYear")}
|
||||
className="w-full pr-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={yearInput}
|
||||
onChange={handleYearInput}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -86,119 +87,366 @@ interface IconSelectorProps {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const iconOptions: IconOption[] = [
|
||||
const getIconOptions = (t: any): IconOption[] => [
|
||||
// 个人信息类
|
||||
{ label: "用户", value: "User", icon: User, category: "个人信息" },
|
||||
{ label: "邮箱", value: "Mail", icon: Mail, category: "个人信息" },
|
||||
{ label: "电话", value: "Phone", icon: Phone, category: "个人信息" },
|
||||
{ label: "地址", value: "MapPin", icon: MapPin, category: "个人信息" },
|
||||
{ label: "网站", value: "Globe", icon: Globe, category: "个人信息" },
|
||||
{
|
||||
label: "手机",
|
||||
label: t("icons.user"),
|
||||
value: "User",
|
||||
icon: User,
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
{
|
||||
label: t("icons.email"),
|
||||
value: "Mail",
|
||||
icon: Mail,
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
{
|
||||
label: t("icons.phone"),
|
||||
value: "Phone",
|
||||
icon: Phone,
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
{
|
||||
label: t("icons.address"),
|
||||
value: "MapPin",
|
||||
icon: MapPin,
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
{
|
||||
label: t("icons.website"),
|
||||
value: "Globe",
|
||||
icon: Globe,
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
{
|
||||
label: t("icons.mobile"),
|
||||
value: "Smartphone",
|
||||
icon: Smartphone,
|
||||
category: "个人信息",
|
||||
category: t("categories.personal"),
|
||||
},
|
||||
|
||||
// 教育背景类
|
||||
{
|
||||
label: "学历",
|
||||
label: t("icons.education"),
|
||||
value: "GraduationCap",
|
||||
icon: GraduationCap,
|
||||
category: "教育背景",
|
||||
category: t("categories.education"),
|
||||
},
|
||||
{
|
||||
label: t("icons.school"),
|
||||
value: "School",
|
||||
icon: School,
|
||||
category: t("categories.education"),
|
||||
},
|
||||
{
|
||||
label: t("icons.major"),
|
||||
value: "Book",
|
||||
icon: Book,
|
||||
category: t("categories.education"),
|
||||
},
|
||||
{
|
||||
label: t("icons.library"),
|
||||
value: "Library",
|
||||
icon: Library,
|
||||
category: t("categories.education"),
|
||||
},
|
||||
{
|
||||
label: t("icons.scholarship"),
|
||||
value: "Award",
|
||||
icon: Award,
|
||||
category: t("categories.education"),
|
||||
},
|
||||
{ label: "学校", value: "School", icon: School, category: "教育背景" },
|
||||
{ label: "专业", value: "Book", icon: Book, category: "教育背景" },
|
||||
{ label: "图书馆", value: "Library", icon: Library, category: "教育背景" },
|
||||
{ label: "奖学金", value: "Award", icon: Award, category: "教育背景" },
|
||||
|
||||
// 工作经验类
|
||||
{ label: "工作", value: "Briefcase", icon: Briefcase, category: "工作经验" },
|
||||
{ label: "公司", value: "Building2", icon: Building2, category: "工作经验" },
|
||||
{ label: "办公室", value: "Building", icon: Building, category: "工作经验" },
|
||||
{
|
||||
label: "日期范围",
|
||||
label: t("icons.work"),
|
||||
value: "Briefcase",
|
||||
icon: Briefcase,
|
||||
category: t("categories.experience"),
|
||||
},
|
||||
{
|
||||
label: t("icons.company"),
|
||||
value: "Building2",
|
||||
icon: Building2,
|
||||
category: t("categories.experience"),
|
||||
},
|
||||
{
|
||||
label: t("icons.office"),
|
||||
value: "Building",
|
||||
icon: Building,
|
||||
category: t("categories.experience"),
|
||||
},
|
||||
{
|
||||
label: t("icons.dateRange"),
|
||||
value: "CalendarRange",
|
||||
icon: CalendarRange,
|
||||
category: "工作经验",
|
||||
category: t("categories.experience"),
|
||||
},
|
||||
{
|
||||
label: t("icons.workTime"),
|
||||
value: "Clock",
|
||||
icon: Clock,
|
||||
category: t("categories.experience"),
|
||||
},
|
||||
{ label: "工作时间", value: "Clock", icon: Clock, category: "工作经验" },
|
||||
|
||||
// 技能类
|
||||
{ label: "编程", value: "Code", icon: Code, category: "技能" },
|
||||
{ label: "系统", value: "Cpu", icon: Cpu, category: "技能" },
|
||||
{ label: "数据库", value: "Database", icon: Database, category: "技能" },
|
||||
{ label: "终端", value: "Terminal", icon: Terminal, category: "技能" },
|
||||
{ label: "技术栈", value: "Layers", icon: Layers, category: "技能" },
|
||||
|
||||
// 语言类
|
||||
{ label: "语言", value: "Languages", icon: Languages, category: "语言" },
|
||||
{
|
||||
label: "口语",
|
||||
value: "MessageSquare",
|
||||
icon: MessageSquare,
|
||||
category: "语言",
|
||||
label: t("icons.programming"),
|
||||
value: "Code",
|
||||
icon: Code,
|
||||
category: t("categories.skills"),
|
||||
},
|
||||
{
|
||||
label: "交流",
|
||||
label: t("icons.system"),
|
||||
value: "Cpu",
|
||||
icon: Cpu,
|
||||
category: t("categories.skills"),
|
||||
},
|
||||
{
|
||||
label: t("icons.database"),
|
||||
value: "Database",
|
||||
icon: Database,
|
||||
category: t("categories.skills"),
|
||||
},
|
||||
{
|
||||
label: t("icons.terminal"),
|
||||
value: "Terminal",
|
||||
icon: Terminal,
|
||||
category: t("categories.skills"),
|
||||
},
|
||||
{
|
||||
label: t("icons.techStack"),
|
||||
value: "Layers",
|
||||
icon: Layers,
|
||||
category: t("categories.skills"),
|
||||
},
|
||||
|
||||
// 语言类
|
||||
{
|
||||
label: t("icons.language"),
|
||||
value: "Languages",
|
||||
icon: Languages,
|
||||
category: t("categories.languages"),
|
||||
},
|
||||
{
|
||||
label: t("icons.speaking"),
|
||||
value: "MessageSquare",
|
||||
icon: MessageSquare,
|
||||
category: t("categories.languages"),
|
||||
},
|
||||
{
|
||||
label: t("icons.communication"),
|
||||
value: "MessagesSquare",
|
||||
icon: MessagesSquare,
|
||||
category: "语言",
|
||||
category: t("categories.languages"),
|
||||
},
|
||||
|
||||
// 项目经验类
|
||||
{ label: "项目", value: "FolderGit2", icon: FolderGit2, category: "项目" },
|
||||
{ label: "分支", value: "GitBranch", icon: GitBranch, category: "项目" },
|
||||
{ label: "发布", value: "Rocket", icon: Rocket, category: "项目" },
|
||||
{ label: "目标", value: "Target", icon: Target, category: "项目" },
|
||||
{
|
||||
label: t("icons.project"),
|
||||
value: "FolderGit2",
|
||||
icon: FolderGit2,
|
||||
category: t("categories.projects"),
|
||||
},
|
||||
{
|
||||
label: t("icons.branch"),
|
||||
value: "GitBranch",
|
||||
icon: GitBranch,
|
||||
category: t("categories.projects"),
|
||||
},
|
||||
{
|
||||
label: t("icons.release"),
|
||||
value: "Rocket",
|
||||
icon: Rocket,
|
||||
category: t("categories.projects"),
|
||||
},
|
||||
{
|
||||
label: t("icons.target"),
|
||||
value: "Target",
|
||||
icon: Target,
|
||||
category: t("categories.projects"),
|
||||
},
|
||||
|
||||
// 成就与证书类
|
||||
{ label: "奖杯", value: "Trophy", icon: Trophy, category: "成就证书" },
|
||||
{ label: "奖牌", value: "Medal", icon: Medal, category: "成就证书" },
|
||||
{ label: "星级", value: "Star", icon: Star, category: "成就证书" },
|
||||
{
|
||||
label: t("icons.trophy"),
|
||||
value: "Trophy",
|
||||
icon: Trophy,
|
||||
category: t("categories.achievements"),
|
||||
},
|
||||
{
|
||||
label: t("icons.medal"),
|
||||
value: "Medal",
|
||||
icon: Medal,
|
||||
category: t("categories.achievements"),
|
||||
},
|
||||
{
|
||||
label: t("icons.star"),
|
||||
value: "Star",
|
||||
icon: Star,
|
||||
category: t("categories.achievements"),
|
||||
},
|
||||
|
||||
// 兴趣爱好类
|
||||
{ label: "兴趣", value: "Heart", icon: Heart, category: "兴趣爱好" },
|
||||
{ label: "音乐", value: "Music", icon: Music, category: "兴趣爱好" },
|
||||
{ label: "艺术", value: "Palette", icon: Palette, category: "兴趣爱好" },
|
||||
{ label: "摄影", value: "Camera", icon: Camera, category: "兴趣爱好" },
|
||||
{
|
||||
label: t("icons.interest"),
|
||||
value: "Heart",
|
||||
icon: Heart,
|
||||
category: t("categories.hobbies"),
|
||||
},
|
||||
{
|
||||
label: t("icons.music"),
|
||||
value: "Music",
|
||||
icon: Music,
|
||||
category: t("categories.hobbies"),
|
||||
},
|
||||
{
|
||||
label: t("icons.art"),
|
||||
value: "Palette",
|
||||
icon: Palette,
|
||||
category: t("categories.hobbies"),
|
||||
},
|
||||
{
|
||||
label: t("icons.photography"),
|
||||
value: "Camera",
|
||||
icon: Camera,
|
||||
category: t("categories.hobbies"),
|
||||
},
|
||||
|
||||
// 社交媒体类
|
||||
{ label: "Github", value: "Github", icon: Github, category: "社交媒体" },
|
||||
{ label: "领英", value: "Linkedin", icon: Linkedin, category: "社交媒体" },
|
||||
{ label: "推特", value: "Twitter", icon: Twitter, category: "社交媒体" },
|
||||
{ label: "脸书", value: "Facebook", icon: Facebook, category: "社交媒体" },
|
||||
{ label: "照片", value: "Instagram", icon: Instagram, category: "社交媒体" },
|
||||
{
|
||||
label: "Github",
|
||||
value: "Github",
|
||||
icon: Github,
|
||||
category: t("categories.social"),
|
||||
},
|
||||
{
|
||||
label: t("icons.linkedin"),
|
||||
value: "Linkedin",
|
||||
icon: Linkedin,
|
||||
category: t("categories.social"),
|
||||
},
|
||||
{
|
||||
label: t("icons.twitter"),
|
||||
value: "Twitter",
|
||||
icon: Twitter,
|
||||
category: t("categories.social"),
|
||||
},
|
||||
{
|
||||
label: t("icons.facebook"),
|
||||
value: "Facebook",
|
||||
icon: Facebook,
|
||||
category: t("categories.social"),
|
||||
},
|
||||
{
|
||||
label: t("icons.instagram"),
|
||||
value: "Instagram",
|
||||
icon: Instagram,
|
||||
category: t("categories.social"),
|
||||
},
|
||||
|
||||
// 其他类
|
||||
{ label: "简介", value: "FileText", icon: FileText, category: "其他" },
|
||||
{ label: "审核", value: "FileCheck", icon: FileCheck, category: "其他" },
|
||||
{ label: "筛选", value: "Filter", icon: Filter, category: "其他" },
|
||||
{ label: "链接", value: "Link", icon: Link, category: "其他" },
|
||||
{ label: "薪资", value: "Wallet", icon: Wallet, category: "其他" },
|
||||
{ label: "创意", value: "Lightbulb", icon: Lightbulb, category: "其他" },
|
||||
{ label: "发送", value: "Send", icon: Send, category: "其他" },
|
||||
{ label: "分享", value: "Share2", icon: Share2, category: "其他" },
|
||||
{ label: "设置", value: "Settings", icon: Settings, category: "其他" },
|
||||
{ label: "搜索", value: "SearchIcon", icon: SearchIcon, category: "其他" },
|
||||
{ label: "标记", value: "Flag", icon: Flag, category: "其他" },
|
||||
{ label: "收藏", value: "Bookmark", icon: Bookmark, category: "其他" },
|
||||
{ label: "点赞", value: "ThumbsUp", icon: ThumbsUp, category: "其他" },
|
||||
{ label: "技能", value: "Zap", icon: Zap, category: "其他" },
|
||||
{
|
||||
label: t("icons.profile"),
|
||||
value: "FileText",
|
||||
icon: FileText,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.review"),
|
||||
value: "FileCheck",
|
||||
icon: FileCheck,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.filter"),
|
||||
value: "Filter",
|
||||
icon: Filter,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.link"),
|
||||
value: "Link",
|
||||
icon: Link,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.salary"),
|
||||
value: "Wallet",
|
||||
icon: Wallet,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.idea"),
|
||||
value: "Lightbulb",
|
||||
icon: Lightbulb,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.send"),
|
||||
value: "Send",
|
||||
icon: Send,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.share"),
|
||||
value: "Share2",
|
||||
icon: Share2,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.settings"),
|
||||
value: "Settings",
|
||||
icon: Settings,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.search"),
|
||||
value: "SearchIcon",
|
||||
icon: SearchIcon,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.flag"),
|
||||
value: "Flag",
|
||||
icon: Flag,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.bookmark"),
|
||||
value: "Bookmark",
|
||||
icon: Bookmark,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.thumbsUp"),
|
||||
value: "ThumbsUp",
|
||||
icon: ThumbsUp,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
{
|
||||
label: t("icons.skill"),
|
||||
value: "Zap",
|
||||
icon: Zap,
|
||||
category: t("categories.others"),
|
||||
},
|
||||
];
|
||||
|
||||
const IconSelector: React.FC<IconSelectorProps> = ({ value, onChange }) => {
|
||||
const t = useTranslations("iconSelector");
|
||||
const iconOptions = React.useMemo(() => getIconOptions(t), [t]);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState("");
|
||||
const [isHovered, setIsHovered] = React.useState("");
|
||||
const [selectedCategory, setSelectedCategory] = React.useState("全部");
|
||||
const [selectedCategory, setSelectedCategory] = React.useState(t("all"));
|
||||
|
||||
const selectedIcon =
|
||||
iconOptions.find((i) => i.value === value) || iconOptions[0];
|
||||
const Icon = selectedIcon.icon;
|
||||
|
||||
const categories = [
|
||||
"全部",
|
||||
t("all"),
|
||||
...Array.from(new Set(iconOptions.map((icon) => icon.category))),
|
||||
];
|
||||
|
||||
@@ -207,7 +455,7 @@ const IconSelector: React.FC<IconSelectorProps> = ({ value, onChange }) => {
|
||||
icon.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
icon.value.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory =
|
||||
selectedCategory === "全部" || icon.category === selectedCategory;
|
||||
selectedCategory === t("all") || icon.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
@@ -266,7 +514,7 @@ const IconSelector: React.FC<IconSelectorProps> = ({ value, onChange }) => {
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索图标..."
|
||||
placeholder={t("searchPlaceholder")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className={cn(
|
||||
@@ -354,9 +602,9 @@ const IconSelector: React.FC<IconSelectorProps> = ({ value, onChange }) => {
|
||||
)}
|
||||
>
|
||||
<SearchIcon className="w-12 h-12 mb-2 opacity-20" />
|
||||
<p>未找到匹配的图标</p>
|
||||
<p>{t("noMatchingIcons")}</p>
|
||||
<p className="text-xs opacity-70">
|
||||
{searchTerm ? "请尝试其他搜索关键词" : "请选择其他分类"}
|
||||
{searchTerm ? t("tryOtherKeywords") : t("selectOtherCategory")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
@@ -16,19 +17,15 @@ import {
|
||||
Bold,
|
||||
Italic,
|
||||
Underline as UnderlineIcon,
|
||||
Strikethrough,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
AlignJustify,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Undo,
|
||||
Redo,
|
||||
PaintBucket,
|
||||
Type,
|
||||
ChevronDown,
|
||||
Highlighter,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
@@ -47,25 +44,30 @@ interface RichTextEditorProps {
|
||||
onPolish?: () => void;
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ label: "黑色", value: "#000000" },
|
||||
{ label: "深灰", value: "#333333" },
|
||||
{ label: "灰色", value: "#666666" },
|
||||
{ label: "红色", value: "#FF0000" },
|
||||
{ label: "橙色", value: "#FF4D00" },
|
||||
{ label: "橙黄", value: "#FF9900" },
|
||||
{ label: "黄色", value: "#FFCC00" },
|
||||
{ label: "黄绿", value: "#33CC00" },
|
||||
{ label: "绿色", value: "#00CC00" },
|
||||
{ label: "青色", value: "#00CCCC" },
|
||||
{ label: "浅蓝", value: "#0066FF" },
|
||||
{ label: "蓝色", value: "#0000FF" },
|
||||
{ label: "紫色", value: "#6600FF" },
|
||||
{ label: "紫红", value: "#CC00FF" },
|
||||
{ label: "粉色", value: "#FF00FF" },
|
||||
interface ColorOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const getColors = (t: any): ColorOption[] => [
|
||||
{ label: t("colors.black"), value: "#000000" },
|
||||
{ label: t("colors.darkGray"), value: "#333333" },
|
||||
{ label: t("colors.gray"), value: "#666666" },
|
||||
{ label: t("colors.red"), value: "#FF0000" },
|
||||
{ label: t("colors.orange"), value: "#FF4D00" },
|
||||
{ label: t("colors.orangeYellow"), value: "#FF9900" },
|
||||
{ label: t("colors.yellow"), value: "#FFCC00" },
|
||||
{ label: t("colors.yellowGreen"), value: "#33CC00" },
|
||||
{ label: t("colors.green"), value: "#00CC00" },
|
||||
{ label: t("colors.cyan"), value: "#00CCCC" },
|
||||
{ label: t("colors.lightBlue"), value: "#0066FF" },
|
||||
{ label: t("colors.blue"), value: "#0000FF" },
|
||||
{ label: t("colors.purple"), value: "#6600FF" },
|
||||
{ label: t("colors.magenta"), value: "#CC00FF" },
|
||||
{ label: t("colors.pink"), value: "#FF00FF" },
|
||||
];
|
||||
|
||||
const BG_COLORS = COLORS;
|
||||
const getBgColors = getColors;
|
||||
|
||||
interface MenuButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -140,6 +142,8 @@ const MenuButton = ({
|
||||
|
||||
const TextColorButton = ({ editor }) => {
|
||||
const [activeColor, setActiveColor] = React.useState<string | null>(null);
|
||||
const t = useTranslations("richEditor");
|
||||
const colors = getColors(t);
|
||||
|
||||
React.useEffect(() => {
|
||||
const color = editor?.getAttributes("textStyle").color;
|
||||
@@ -163,14 +167,14 @@ const TextColorButton = ({ editor }) => {
|
||||
: "none",
|
||||
}}
|
||||
/>
|
||||
<span className="sr-only">文字颜色</span>
|
||||
<span className="sr-only">{t("textColor")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-3 rounded-lg">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<PaintBucket className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">文字颜色</span>
|
||||
<span className="text-sm font-medium">{t("textColor")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
<button
|
||||
@@ -184,7 +188,7 @@ const TextColorButton = ({ editor }) => {
|
||||
/
|
||||
</span>
|
||||
</button>
|
||||
{COLORS.map((color) => (
|
||||
{colors.map((color: ColorOption) => (
|
||||
<button
|
||||
key={color.value}
|
||||
className={`h-8 w-8 rounded-md border hover:scale-110 transition-transform relative
|
||||
@@ -214,8 +218,10 @@ const TextColorButton = ({ editor }) => {
|
||||
|
||||
const BackgroundColorButton = ({ editor }) => {
|
||||
const [activeBgColor, setActiveBgColor] = React.useState<string | null>(null);
|
||||
const t = useTranslations("richEditor");
|
||||
const bgColors = getBgColors(t);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// highlight 属性可能直接返回颜色值
|
||||
const highlight =
|
||||
editor?.getAttributes("highlight").color ||
|
||||
@@ -248,14 +254,14 @@ const BackgroundColorButton = ({ editor }) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="sr-only">背景颜色</span>
|
||||
<span className="sr-only">{t("backgroundColor")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-3 rounded-lg">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Highlighter className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">背景颜色</span>
|
||||
<span className="text-sm font-medium">{t("backgroundColor")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
<button
|
||||
@@ -269,7 +275,7 @@ const BackgroundColorButton = ({ editor }) => {
|
||||
/
|
||||
</span>
|
||||
</button>
|
||||
{BG_COLORS.map((color) => (
|
||||
{bgColors.map((color: ColorOption) => (
|
||||
<button
|
||||
key={color.value}
|
||||
className={`h-8 w-8 rounded-md border hover:scale-110 transition-transform relative
|
||||
@@ -300,81 +306,12 @@ const BackgroundColorButton = ({ editor }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const HeadingSelect = ({ editor }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-9 px-2.5 rounded-md flex items-center gap-1.5 min-w-[100px]",
|
||||
"hover:bg-primary/5 dark:hover:bg-neutral-800"
|
||||
)}
|
||||
>
|
||||
<Type className="h-5 w-5" />
|
||||
<span className="text-sm">
|
||||
{editor.isActive("heading", { level: 1 })
|
||||
? "标题 1"
|
||||
: editor.isActive("heading", { level: 2 })
|
||||
? "标题 2"
|
||||
: editor.isActive("heading", { level: 3 })
|
||||
? "标题 3"
|
||||
: "正文"}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cn("w-40 p-1 rounded-lg", "bg-white dark:bg-neutral-900")}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{[
|
||||
{ label: "正文", value: "p" },
|
||||
{ label: "标题 1", value: "1" },
|
||||
{ label: "标题 2", value: "2" },
|
||||
{ label: "标题 3", value: "3" },
|
||||
].map((item) => (
|
||||
<Button
|
||||
key={item.value}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full justify-start px-2 py-1.5 text-sm rounded-md",
|
||||
editor.isActive(
|
||||
item.value === "p" ? "paragraph" : "heading",
|
||||
item.value === "p"
|
||||
? undefined
|
||||
: { level: parseInt(item.value) }
|
||||
)
|
||||
? "bg-primary/10 text-primary dark:bg-neutral-800 dark:text-neutral-200"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (item.value === "p") {
|
||||
editor.chain().focus().setParagraph().run();
|
||||
} else {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: parseInt(item.value) })
|
||||
.run();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const RichTextEditor = ({
|
||||
content = "",
|
||||
onChange,
|
||||
onPolish,
|
||||
}: RichTextEditorProps) => {
|
||||
const t = useTranslations("richEditor");
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
@@ -446,42 +383,35 @@ const RichTextEditor = ({
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Toolbar */}
|
||||
<div
|
||||
className={cn(
|
||||
"border-b px-2 py-1.5 flex flex-wrap items-center gap-3",
|
||||
"bg-background dark:bg-neutral-900/50 dark:border-neutral-800"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<HeadingSelect editor={editor} />
|
||||
</div>
|
||||
|
||||
<div className={cn("h-5 w-px", "bg-border/60 dark:bg-neutral-800")} />
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
isActive={editor.isActive("bold")}
|
||||
tooltip="加粗"
|
||||
tooltip={t("bold")}
|
||||
>
|
||||
<Bold className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive("italic")}
|
||||
tooltip="斜体"
|
||||
tooltip={t("italic")}
|
||||
>
|
||||
<Italic className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive("underline")}
|
||||
tooltip="下划线"
|
||||
tooltip={t("underline")}
|
||||
>
|
||||
<UnderlineIcon className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<TextColorButton editor={editor} tooltip="文字颜色" />
|
||||
<TextColorButton editor={editor} />
|
||||
<BackgroundColorButton editor={editor} />
|
||||
</div>
|
||||
|
||||
@@ -491,28 +421,28 @@ const RichTextEditor = ({
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().setTextAlign("left").run()}
|
||||
isActive={editor.isActive({ textAlign: "left" })}
|
||||
tooltip="左对齐"
|
||||
tooltip={t("alignLeft")}
|
||||
>
|
||||
<AlignLeft className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().setTextAlign("center").run()}
|
||||
isActive={editor.isActive({ textAlign: "center" })}
|
||||
tooltip="居中对齐"
|
||||
tooltip={t("alignCenter")}
|
||||
>
|
||||
<AlignCenter className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().setTextAlign("right").run()}
|
||||
isActive={editor.isActive({ textAlign: "right" })}
|
||||
tooltip="右对齐"
|
||||
tooltip={t("alignRight")}
|
||||
>
|
||||
<AlignRight className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().setTextAlign("justify").run()}
|
||||
isActive={editor.isActive({ textAlign: "justify" })}
|
||||
tooltip="两端对齐"
|
||||
tooltip={t("alignJustify")}
|
||||
>
|
||||
<AlignJustify className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
@@ -524,14 +454,14 @@ const RichTextEditor = ({
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
isActive={editor.isActive("bulletList")}
|
||||
tooltip="无序列表"
|
||||
tooltip={t("bulletList")}
|
||||
>
|
||||
<List className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
isActive={editor.isActive("orderedList")}
|
||||
tooltip="有序列表"
|
||||
tooltip={t("orderedList")}
|
||||
>
|
||||
<ListOrdered className="h-5 w-5" />
|
||||
</MenuButton>
|
||||
@@ -543,14 +473,14 @@ const RichTextEditor = ({
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
tooltip="撤销"
|
||||
tooltip={t("undo")}
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</MenuButton>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
tooltip="重做"
|
||||
tooltip={t("redo")}
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</MenuButton>
|
||||
@@ -562,7 +492,7 @@ const RichTextEditor = ({
|
||||
className="bg-gradient-to-r from-purple-400 to-pink-500 hover:from-pink-500 hover:to-purple-400 text-white border-none shadow-md transition-all duration-300"
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-2" />
|
||||
AI 润色
|
||||
{t("aiPolish")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -434,6 +434,121 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"field": {
|
||||
"selectDate": "Select Date",
|
||||
"enterYear": "Enter Year"
|
||||
},
|
||||
"richEditor": {
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"underline": "Underline",
|
||||
"textColor": "Text Color",
|
||||
"backgroundColor": "Background Color",
|
||||
"alignLeft": "Align Left",
|
||||
"alignCenter": "Align Center",
|
||||
"alignRight": "Align Right",
|
||||
"alignJustify": "Justify",
|
||||
"bulletList": "Bullet List",
|
||||
"orderedList": "Ordered List",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"aiPolish": "AI Polish",
|
||||
"paragraph": "Paragraph",
|
||||
"heading1": "Heading 1",
|
||||
"heading2": "Heading 2",
|
||||
"heading3": "Heading 3",
|
||||
"colors": {
|
||||
"black": "Black",
|
||||
"darkGray": "Dark Gray",
|
||||
"gray": "Gray",
|
||||
"red": "Red",
|
||||
"orange": "Orange",
|
||||
"orangeYellow": "Orange Yellow",
|
||||
"yellow": "Yellow",
|
||||
"yellowGreen": "Yellow Green",
|
||||
"green": "Green",
|
||||
"cyan": "Cyan",
|
||||
"lightBlue": "Light Blue",
|
||||
"blue": "Blue",
|
||||
"purple": "Purple",
|
||||
"magenta": "Magenta",
|
||||
"pink": "Pink"
|
||||
}
|
||||
},
|
||||
"iconSelector": {
|
||||
"all": "All",
|
||||
"searchPlaceholder": "Search icons...",
|
||||
"noMatchingIcons": "No matching icons found",
|
||||
"tryOtherKeywords": "Please try other search keywords",
|
||||
"selectOtherCategory": "Please select another category",
|
||||
"categories": {
|
||||
"personal": "Personal Info",
|
||||
"education": "Education",
|
||||
"experience": "Work Experience",
|
||||
"skills": "Skills",
|
||||
"languages": "Languages",
|
||||
"projects": "Projects",
|
||||
"achievements": "Achievements",
|
||||
"hobbies": "Hobbies",
|
||||
"social": "Social Media",
|
||||
"others": "Others"
|
||||
},
|
||||
"icons": {
|
||||
"user": "User",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"address": "Address",
|
||||
"website": "Website",
|
||||
"mobile": "Mobile",
|
||||
"education": "Education",
|
||||
"school": "School",
|
||||
"major": "Major",
|
||||
"library": "Library",
|
||||
"scholarship": "Scholarship",
|
||||
"work": "Work",
|
||||
"company": "Company",
|
||||
"office": "Office",
|
||||
"dateRange": "Date Range",
|
||||
"workTime": "Work Time",
|
||||
"programming": "Programming",
|
||||
"system": "System",
|
||||
"database": "Database",
|
||||
"terminal": "Terminal",
|
||||
"techStack": "Tech Stack",
|
||||
"language": "Language",
|
||||
"speaking": "Speaking",
|
||||
"communication": "Communication",
|
||||
"project": "Project",
|
||||
"branch": "Branch",
|
||||
"release": "Release",
|
||||
"target": "Target",
|
||||
"trophy": "Trophy",
|
||||
"medal": "Medal",
|
||||
"star": "Star",
|
||||
"interest": "Interest",
|
||||
"music": "Music",
|
||||
"art": "Art",
|
||||
"photography": "Photography",
|
||||
"linkedin": "LinkedIn",
|
||||
"twitter": "Twitter",
|
||||
"facebook": "Facebook",
|
||||
"instagram": "Instagram",
|
||||
"profile": "Profile",
|
||||
"review": "Review",
|
||||
"filter": "Filter",
|
||||
"link": "Link",
|
||||
"salary": "Salary",
|
||||
"idea": "Idea",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"settings": "Settings",
|
||||
"search": "Search",
|
||||
"flag": "Flag",
|
||||
"bookmark": "Bookmark",
|
||||
"thumbsUp": "Thumbs Up",
|
||||
"skill": "Skill"
|
||||
}
|
||||
},
|
||||
"aiPolishDialog": {
|
||||
"title": "AI Polish",
|
||||
"description": {
|
||||
|
||||
@@ -411,6 +411,121 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"field": {
|
||||
"selectDate": "选择日期",
|
||||
"enterYear": "输入年份"
|
||||
},
|
||||
"richEditor": {
|
||||
"bold": "加粗",
|
||||
"italic": "斜体",
|
||||
"underline": "下划线",
|
||||
"textColor": "文字颜色",
|
||||
"backgroundColor": "背景颜色",
|
||||
"alignLeft": "左对齐",
|
||||
"alignCenter": "居中对齐",
|
||||
"alignRight": "右对齐",
|
||||
"alignJustify": "两端对齐",
|
||||
"bulletList": "无序列表",
|
||||
"orderedList": "有序列表",
|
||||
"undo": "撤销",
|
||||
"redo": "重做",
|
||||
"aiPolish": "AI 润色",
|
||||
"paragraph": "正文",
|
||||
"heading1": "标题 1",
|
||||
"heading2": "标题 2",
|
||||
"heading3": "标题 3",
|
||||
"colors": {
|
||||
"black": "黑色",
|
||||
"darkGray": "深灰",
|
||||
"gray": "灰色",
|
||||
"red": "红色",
|
||||
"orange": "橙色",
|
||||
"orangeYellow": "橙黄",
|
||||
"yellow": "黄色",
|
||||
"yellowGreen": "黄绿",
|
||||
"green": "绿色",
|
||||
"cyan": "青色",
|
||||
"lightBlue": "浅蓝",
|
||||
"blue": "蓝色",
|
||||
"purple": "紫色",
|
||||
"magenta": "紫红",
|
||||
"pink": "粉色"
|
||||
}
|
||||
},
|
||||
"iconSelector": {
|
||||
"all": "全部",
|
||||
"searchPlaceholder": "搜索图标...",
|
||||
"noMatchingIcons": "未找到匹配的图标",
|
||||
"tryOtherKeywords": "请尝试其他搜索关键词",
|
||||
"selectOtherCategory": "请选择其他分类",
|
||||
"categories": {
|
||||
"personal": "个人信息",
|
||||
"education": "教育背景",
|
||||
"experience": "工作经验",
|
||||
"skills": "技能",
|
||||
"languages": "语言",
|
||||
"projects": "项目",
|
||||
"achievements": "成就证书",
|
||||
"hobbies": "兴趣爱好",
|
||||
"social": "社交媒体",
|
||||
"others": "其他"
|
||||
},
|
||||
"icons": {
|
||||
"user": "用户",
|
||||
"email": "邮箱",
|
||||
"phone": "电话",
|
||||
"address": "地址",
|
||||
"website": "网站",
|
||||
"mobile": "手机",
|
||||
"education": "学历",
|
||||
"school": "学校",
|
||||
"major": "专业",
|
||||
"library": "图书馆",
|
||||
"scholarship": "奖学金",
|
||||
"work": "工作",
|
||||
"company": "公司",
|
||||
"office": "办公室",
|
||||
"dateRange": "日期范围",
|
||||
"workTime": "工作时间",
|
||||
"programming": "编程",
|
||||
"system": "系统",
|
||||
"database": "数据库",
|
||||
"terminal": "终端",
|
||||
"techStack": "技术栈",
|
||||
"language": "语言",
|
||||
"speaking": "口语",
|
||||
"communication": "交流",
|
||||
"project": "项目",
|
||||
"branch": "分支",
|
||||
"release": "发布",
|
||||
"target": "目标",
|
||||
"trophy": "奖杯",
|
||||
"medal": "奖牌",
|
||||
"star": "星级",
|
||||
"interest": "兴趣",
|
||||
"music": "音乐",
|
||||
"art": "艺术",
|
||||
"photography": "摄影",
|
||||
"linkedin": "领英",
|
||||
"twitter": "推特",
|
||||
"facebook": "脸书",
|
||||
"instagram": "照片",
|
||||
"profile": "简介",
|
||||
"review": "审核",
|
||||
"filter": "筛选",
|
||||
"link": "链接",
|
||||
"salary": "薪资",
|
||||
"idea": "创意",
|
||||
"send": "发送",
|
||||
"share": "分享",
|
||||
"settings": "设置",
|
||||
"search": "搜索",
|
||||
"flag": "标记",
|
||||
"bookmark": "收藏",
|
||||
"thumbsUp": "点赞",
|
||||
"skill": "技能"
|
||||
}
|
||||
},
|
||||
"photoConfig": {
|
||||
"title": "头像设置",
|
||||
"description": "自定义您的简历头像",
|
||||
|
||||
Reference in New Issue
Block a user