feat: next-intl i18n config

This commit is contained in:
JOYCEQL
2025-01-02 23:17:01 +08:00
committed by qingchen
parent d30524abab
commit e617c48dfe
26 changed files with 494 additions and 258 deletions
+4 -13
View File
@@ -5,26 +5,17 @@ const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const config = {
typescript: {
ignoreBuildErrors: true
},
async redirects() {
return [
{
source: "/",
destination: "/en",
permanent: true
}
];
ignoreBuildErrors: true,
},
async rewrites() {
return [
{
source: "/generate-pdf",
destination:
"http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf"
}
"http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf",
},
];
}
},
};
export default withNextIntl(config);
@@ -0,0 +1,56 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import {
getMessages,
getTranslations,
setRequestLocale,
} from "next-intl/server";
import { ReactNode } from "react";
import Document from "@/components/Document";
import { locales } from "@/i18n/config";
import { Providers } from "@/app/providers";
type Props = {
children: ReactNode;
params: { locale: string };
};
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params: { locale },
}: Props): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: "common" });
return {
title: t("title"),
};
}
export default async function LocaleLayout({
children,
params: { locale },
}: Props) {
// Enable static rendering
setRequestLocale(locale);
// Ensure that the incoming locale is valid
if (!locales.includes(locale as any)) {
notFound();
}
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<Document locale={locale}>
<NextIntlClientProvider messages={messages}>
<Providers>{children}</Providers>
</NextIntlClientProvider>
</Document>
);
}
@@ -5,14 +5,18 @@ import { Button } from "@/components/ui/button";
import Header from "@/components/home/Header";
import FeaturesAnimation from "@/components/home/FeaturesAnimation";
import HeroAnimation from "@/components/home/HeroAnimation";
import { setRequestLocale } from "next-intl/server";
export default function LandingPage() {
type Props = {
params: { locale: string };
};
export default function LandingPage({ params: { locale } }: Props) {
const t = useTranslations("home");
setRequestLocale(locale);
return (
<div className="min-h-screen bg-background">
<Header />
<section className="relative pt-32 pb-16">
<div className="absolute inset-0 -z-10 h-full w-full bg-white dark:bg-black bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:14px_24px]" />
<div className="max-w-5xl mx-auto px-4">
-27
View File
@@ -1,27 +0,0 @@
import { notFound } from "next/navigation";
import { NextIntlClientProvider, useMessages } from "next-intl";
import { locales } from "@/i18n/config";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = useMessages();
if (!locales.includes(locale as any)) {
notFound();
}
return (
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
);
}
@@ -22,21 +22,23 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import Logo from "@/components/shared/Logo";
const sidebarItems = [
{
title: "简历",
url: "/dashboard/resumes",
icon: FileText,
},
{
title: "设置",
url: "/dashboard/settings",
icon: Settings,
},
];
import { useTranslations } from "next-intl";
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
const t = useTranslations("dashboard");
const sidebarItems = [
{
title: t("sidebar.resumes"),
url: "/app/dashboard/resumes",
icon: FileText,
},
{
title: t("sidebar.settings"),
url: "/app/dashboard/settings",
icon: Settings,
},
];
const router = useRouter();
const pathname = usePathname();
const [open, setOpen] = useState(true);
@@ -0,0 +1,30 @@
import { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages, getTranslations } from "next-intl/server";
import { ReactNode } from "react";
import Document from "@/components/Document";
import Client from "./client";
type Props = {
children: ReactNode;
};
export async function generateMetadata({
params: { locale },
}: Props): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: "common" });
return {
title: t("title") + " - " + t("dashboard"),
};
}
export default async function LocaleLayout({ children }: Props) {
const locale = await getLocale();
const messages = await getMessages();
return (
<Document locale={locale}>
<NextIntlClientProvider messages={messages}>
<Client>{children}</Client>
</NextIntlClientProvider>
</Document>
);
}
@@ -2,5 +2,5 @@
import { redirect } from "next/navigation";
export default function Dashboard() {
redirect("/dashboard/resumes");
redirect("/app/dashboard/resumes");
}
@@ -16,6 +16,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem";
import { useResumeStore } from "@/store/useResumeStore";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
const ResumesList = () => {
return <ResumeWorkbench />;
@@ -32,6 +33,7 @@ const ResumeWorkbench = () => {
} = useResumeStore();
const router = useRouter();
const [hasConfiguredFolder, setHasConfiguredFolder] = React.useState(false);
const t = useTranslations();
useEffect(() => {
const syncResumesFromFiles = async () => {
@@ -104,7 +106,9 @@ const ResumeWorkbench = () => {
{hasConfiguredFolder ? (
<Alert className="mb-6" variant="default">
<AlertDescription className="flex items-center justify-between">
<span className="text-green-600"></span>
<span className="text-green-600">
{t("dashboard.resumes.synced")}
</span>
<Button
size="sm"
className="ml-4 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white"
@@ -113,7 +117,7 @@ const ResumeWorkbench = () => {
}}
>
<Settings className="w-4 h-4 mr-2" />
{t("dashboard.resumes.view")}
</Button>
</AlertDescription>
</Alert>
@@ -148,7 +152,7 @@ const ResumeWorkbench = () => {
transition={{ duration: 0.3 }}
>
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-indigo-500 to-purple-500 text-transparent bg-clip-text">
{t("dashboard.resumes.myResume")}
</h1>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
@@ -156,7 +160,7 @@ const ResumeWorkbench = () => {
className="bg-gradient-to-r from-indigo-500 to-purple-500 text-white hover:from-indigo-600 hover:to-purple-600"
>
<Plus className="mr-2 h-4 w-4" />
{t("dashboard.resumes.create")}
</Button>
</motion.div>
</motion.div>
@@ -188,8 +192,12 @@ const ResumeWorkbench = () => {
>
<Plus className="h-8 w-8 text-indigo-500" />
</motion.div>
<CardTitle className="text-xl"></CardTitle>
<CardDescription className="mt-2"></CardDescription>
<CardTitle className="text-xl">
{t("dashboard.resumes.newResume")}
</CardTitle>
<CardDescription className="mt-2">
{t("dashboard.resumes.newResumeDescription")}
</CardDescription>
</CardContent>
</Card>
</motion.div>
@@ -242,10 +250,10 @@ const ResumeWorkbench = () => {
onClick={(e) => {
e.stopPropagation();
setActiveResume(id);
router.push(`/workbench/${id}`);
router.push(`/app/workbench/${id}`);
}}
>
{t("common.edit")}
</Button>
</motion.div>
<motion.div
@@ -261,7 +269,7 @@ const ResumeWorkbench = () => {
deleteResume(resume);
}}
>
{t("common.delete")}
</Button>
</motion.div>
</div>
+34
View File
@@ -0,0 +1,34 @@
import { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages, getTranslations } from "next-intl/server";
import { ReactNode } from "react";
import Document from "@/components/Document";
type Props = {
children: ReactNode;
params: { locale: string };
};
export async function generateMetadata({
params: { locale },
}: Props): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: "common" });
return {
title: t("title"),
};
}
export default async function LocaleLayout({ children }: Props) {
const locale = await getLocale();
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<Document locale={locale}>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</Document>
);
}
@@ -0,0 +1,29 @@
import { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages, getTranslations } from "next-intl/server";
import { ReactNode } from "react";
import Document from "@/components/Document";
type Props = {
children: ReactNode;
};
export async function generateMetadata({
params: { locale },
}: Props): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: "common" });
return {
title: t("title") + " - " + t("dashboard"),
};
}
export default async function LocaleLayout({ children }: Props) {
const locale = await getLocale();
const messages = await getMessages();
return (
<Document locale={locale}>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</Document>
);
}
+5 -22
View File
@@ -1,27 +1,10 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Providers } from "./providers";
import { Toaster } from "@/components/ui/sonner";
import { ReactNode } from "react";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "魔法简历",
description: "极度自由的在线简历编辑器"
type Props = {
children: ReactNode;
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
<Toaster richColors position="top-center" />
</body>
</html>
);
export default function RootLayout({ children }: Props) {
return children;
}
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import { ReactNode } from "react";
const inter = Inter({
subsets: ["latin"],
});
type Props = {
children: ReactNode;
locale: string;
};
export default function Document({ children, locale }: Props) {
return (
<html className={inter.className} lang={locale} suppressHydrationWarning>
<body>{children}</body>
</html>
);
}
@@ -16,6 +16,7 @@ import {
} from "@/components/ui/hover-card";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
interface EditorHeaderProps {
isMobile?: boolean;
@@ -28,7 +29,7 @@ export function EditorHeader({ isMobile }: EditorHeaderProps) {
const themeConfig = getThemeConfig();
const { errors, selectError } = useGrammarCheck();
const router = useRouter();
const t = useTranslations();
const visibleSections = menuSections
?.filter((section) => section.enabled)
.sort((a, b) => a.order - b.order);
@@ -46,10 +47,10 @@ export function EditorHeader({ isMobile }: EditorHeaderProps) {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => {
router.push("/dashboard");
router.push("/app/dashboard");
}}
>
<span className="text-lg font-semibold">Magic Resume</span>
<span className="text-lg font-semibold">{t("common.title")}</span>
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
</motion.div>
</div>
+3 -3
View File
@@ -57,10 +57,10 @@ export default function Header() {
initial={{ y: 0, opacity: 1 }}
animate={{
y: isVisible ? 0 : -100,
opacity: isVisible ? 1 : 0
opacity: isVisible ? 1 : 0,
}}
transition={{
duration: 0.2
duration: 0.2,
}}
>
<div className="mt-4 rounded-full bg-background/70 backdrop-blur-[8px] border border-border/50">
@@ -79,7 +79,7 @@ export default function Header() {
</div>
</ThemeToggle>
<GitHubStars />
<Link href="/dashboard">
<Link href="/app/dashboard">
<Button
variant="default"
className="bg-primary hover:opacity-90 text-white h-8 text-sm rounded-full px-4"
@@ -6,16 +6,17 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { locales, localeNames } from "@/i18n/config";
import updateLocale from "./UpdateLocale";
export default function LanguageSwitch() {
const locale = useLocale();
const router = useRouter();
const handleLanguageChange = (newLocale: string) => {
const handleLanguageChange = async (newLocale: string) => {
const currentPath = window.location.pathname;
if (currentPath === `/${locale}`) {
router.push(`/${newLocale}`);
@@ -23,6 +24,8 @@ export default function LanguageSwitch() {
const newPath = currentPath.replace(`/${locale}/`, `/${newLocale}/`);
router.push(newPath);
}
await updateLocale(newLocale);
router.refresh();
};
return (
@@ -0,0 +1,7 @@
"use server";
import { setUserLocale } from "@/i18n/db";
import { revalidatePath } from "next/cache";
export default async function updateLocale(locale: string) {
setUserLocale(locale);
revalidatePath("/app");
}
@@ -0,0 +1,163 @@
import { DEFAULT_FIELD_ORDER } from ".";
import { GlobalSettings, DEFAULT_CONFIG, ResumeData } from "../types/resume";
const initialGlobalSettings: GlobalSettings = {
baseFontSize: 16,
pagePadding: 32,
paragraphSpacing: 12,
lineHeight: 1.5,
sectionSpacing: 10,
headerSize: 18,
subheaderSize: 16,
useIconMode: true,
themeColor: "#2563eb",
};
export const initialResumeState = {
title: "新建简历",
basic: {
name: "张三",
title: "高级前端工程师",
employementStatus: "全职",
email: "zhangsan@example.com",
phone: "13800138000",
location: "北京市朝阳区",
birthDate: "1995-01",
fieldOrder: DEFAULT_FIELD_ORDER,
icons: {
email: "Mail",
phone: "Phone",
birthDate: "CalendarRange",
employementStatus: "Briefcase",
location: "MapPin",
},
photoConfig: DEFAULT_CONFIG,
customFields: [
{
id: "personal",
label: "个人网站",
value: "https://zhangsan.dev",
icon: "Globe",
},
{
id: "github",
label: "GitHub",
value: "https://github.com/zhangsan",
icon: "Github",
},
],
photo: "/avatar.png",
githubKey: "",
githubUseName: "",
githubContributionsVisible: false,
},
education: [
{
id: "1",
school: "北京大学",
major: "计算机科学与技术",
degree: "本科",
startDate: "2013-09",
endDate: "2017-06",
visible: true,
gpa: "3.8/4.0",
location: "北京",
description: `<ul class="custom-list">
<li>主修课程:数据结构、算法设计、操作系统、计算机网络、Web开发技术</li>
<li>专业排名前 5%,连续三年获得一等奖学金</li>
<li>担任计算机协会技术部部长,组织多次技术分享会</li>
<li>参与开源项目贡献,获得 GitHub Campus Expert 认证</li>
</ul>`,
},
],
skillContent: `<div class="skill-content">
<ul class="custom-list">
<li>前端框架:精通 React、Vue.js,熟悉 Next.js、Nuxt.js 等 SSR 框架</li>
<li>开发语言:TypeScript、JavaScript(ES6+)、HTML5、CSS3</li>
<li>UI/样式:精通 TailwindCSS、Sass/Less、CSS Module、Styled-components</li>
<li>状态管理:Redux、Vuex、Zustand、Jotai、React Query</li>
<li>工程化工具:Webpack、Vite、Rollup、Babel、ESLint</li>
<li>测试工具:Jest、React Testing Library、Cypress</li>
<li>性能优化:熟悉浏览器渲染原理、性能指标监控、代码分割、懒加载等优化技术</li>
<li>版本控制:Git、SVN</li>
<li>技术管理:具备团队管理经验,主导过多个大型项目的技术选型和架构设计</li>
<li>技术分享:定期组织团队技术分享,主导建设团队技术博客</li>
<li>敏捷开发:熟悉 Scrum 开发流程,具有良好的项目把控能力</li>
<li>英语能力:CET-6 分数 560,具备良好的英文文档阅读和技术交流能力</li>
</ul>
</div>`,
experience: [
{
id: "1",
company: "字节跳动",
position: "高级前端工程师",
date: "2021.07 - 至今",
visible: true,
details: `<ul class="custom-list">
<li>负责抖音创作者平台的开发与维护,主导多个核心功能的技术方案设计</li>
<li>优化项目工程化配置,将构建时间从 8 分钟优化至 2 分钟,提升团队开发效率</li>
<li>设计并实现组件库,提升代码复用率达 70%,显著减少开发时间</li>
<li>主导性能优化项目,使平台首屏加载时间减少 50%,接入 APM 监控系统</li>
<li>指导初级工程师,组织技术分享会,提升团队整体技术水平</li>
</ul>`,
},
],
draggingProjectId: null,
projects: [
{
id: "p1",
name: "抖音创作者中台",
role: "前端负责人",
date: "2022.06 - 2023.12",
description:
"基于 React 的创作者数据分析和内容管理平台,服务百万级创作者群体,包含数据分析、内容管理、收益管理等多个子系统。",
visible: true,
},
{
id: "p2",
name: "微信小程序开发者工具",
role: "核心开发者",
date: "2020.03 - 2021.06",
description:
"为开发者提供小程序开发、调试和发布的一站式解决方案。基于 Electron 构建的跨平台桌面应用。",
visible: true,
},
{
id: "p3",
name: "前端监控平台",
role: "技术负责人",
date: "2021.09 - 2022.03",
description:
"一个完整的前端监控解决方案,包含错误监控、性能监控、用户行为分析等功能。",
visible: true,
},
],
menuSections: [
{ id: "basic", title: "基本信息", icon: "👤", enabled: true, order: 0 },
{ id: "skills", title: "专业技能", icon: "⚡", enabled: true, order: 1 },
{
id: "experience",
title: "工作经验",
icon: "💼",
enabled: true,
order: 2,
},
{
id: "projects",
title: "项目经历",
icon: "🚀",
enabled: true,
order: 3,
},
{
id: "education",
title: "教育经历",
icon: "🎓",
enabled: true,
order: 4,
},
],
customData: {},
activeSection: "basic",
globalSettings: initialGlobalSettings,
};
+17
View File
@@ -0,0 +1,17 @@
import { cookies } from "next/headers";
import { defaultLocale } from "./config";
// This cookie name is used by `next-intl` on the public pages too. By
// reading/writing to this locale, we can ensure that the user's locale
// is consistent across public and private pages. In case you save the
// locale of registered users in a database, you can of course also use
// that instead when the user is logged in.
const COOKIE_NAME = "NEXT_LOCALE";
export async function getUserLocale() {
return cookies().get(COOKIE_NAME)?.value || defaultLocale;
}
export async function setUserLocale(locale: string) {
cookies().set(COOKIE_NAME, locale);
}
+23
View File
@@ -1,4 +1,10 @@
{
"common": {
"title": "Magic Resume",
"dashboard": "Dashboard",
"edit": "Edit",
"delete": "Delete"
},
"home": {
"header": {
"title": "Magic Resume",
@@ -29,5 +35,22 @@
"footer": {
"copyright": "© 2024 Magic Resume. All rights reserved."
}
},
"dashboard": {
"sidebar": {
"resumes": "Resumes",
"settings": "Settings"
},
"resumes": {
"synced": "Synced Files",
"view": "View",
"myResume": "My Resumes",
"create": "Create Resume",
"newResume": "New Resume",
"newResumeDescription": "Create a new resume to get started."
},
"settings": {
"title": "Settings"
}
}
}
+23
View File
@@ -1,4 +1,10 @@
{
"common": {
"title": "魔法简历",
"dashboard": "仪表板",
"edit": "编辑",
"delete": "删除"
},
"home": {
"header": {
"title": "魔法简历",
@@ -29,5 +35,22 @@
"footer": {
"copyright": "© 2024 Magic Resume. All rights reserved."
}
},
"dashboard": {
"sidebar": {
"resumes": "简历",
"settings": "设置"
},
"resumes": {
"synced": "已备份文件夹",
"view": "去查看",
"myResume": "我的简历",
"create": "创建简历",
"newResume": "新建简历",
"newResumeDescription": "创建一个新简历以开始。"
},
"settings": {
"title": "设置"
}
}
}
+17 -2
View File
@@ -1,8 +1,23 @@
import { getRequestConfig } from "next-intl/server";
import { defaultLocale, locales } from "./config";
import { getUserLocale } from "./db";
export default getRequestConfig(async ({ requestLocale }) => {
const locale = await requestLocale;
// Read from potential `[locale]` segment
let locale = await requestLocale;
if (!locale) {
// The user is logged in
locale = await getUserLocale();
}
// Ensure that the incoming locale is valid
if (!locales.includes(locale as any)) {
locale = defaultLocale;
}
return {
messages: (await import(`./locales/${locale}.json`)).default
locale,
messages: (await import(`./locales/${locale}.json`)).default,
};
});
+10
View File
@@ -0,0 +1,10 @@
import { createNavigation } from "next-intl/navigation";
import { defineRouting } from "next-intl/routing";
import { defaultLocale, locales } from "./config";
export const routing = defineRouting({
locales,
defaultLocale,
});
export const { Link, usePathname } = createNavigation(routing);
+4 -7
View File
@@ -1,12 +1,9 @@
import createMiddleware from "next-intl/middleware";
import { locales, defaultLocale } from "./i18n/config";
import { routing } from "./i18n/routing.public";
export default createMiddleware({
locales,
defaultLocale,
localePrefix: "always"
});
export default createMiddleware(routing);
export const config = {
matcher: ["/", "/(zh|en)"]
// Match only public pathnames
matcher: ["/", "/(zh|en)/:path*"],
};
+1 -153
View File
@@ -14,6 +14,7 @@ import {
MenuSection,
} from "../types/resume";
import { DEFAULT_TEMPLATES } from "@/config";
import { initialResumeState } from "@/config/initialResumeData";
interface ResumeStore {
resumes: Record<string, ResumeData>;
activeResumeId: string | null;
@@ -58,159 +59,6 @@ interface ResumeStore {
setTemplate: (templateId: string) => void;
}
const initialGlobalSettings: GlobalSettings = {
baseFontSize: 16,
pagePadding: 32,
paragraphSpacing: 12,
lineHeight: 1.5,
sectionSpacing: 10,
headerSize: 18,
subheaderSize: 16,
useIconMode: true,
themeColor: "#2563eb",
};
const initialResumeState: Omit<
ResumeData,
"id" | "createdAt" | "updatedAt" | "templateId"
> = {
title: "新建简历",
basic: {
name: "张三",
title: "高级前端工程师",
employementStatus: "全职",
email: "zhangsan@example.com",
phone: "13800138000",
location: "北京市朝阳区",
birthDate: "1995-01",
fieldOrder: DEFAULT_FIELD_ORDER,
icons: {
email: "Mail",
phone: "Phone",
birthDate: "CalendarRange",
employementStatus: "Briefcase",
location: "MapPin",
},
photoConfig: DEFAULT_CONFIG,
customFields: [
{
id: "personal",
label: "个人网站",
value: "https://zhangsan.dev",
icon: "Globe",
},
{
id: "github",
label: "GitHub",
value: "https://github.com/zhangsan",
icon: "Github",
},
],
photo: "/avatar.png",
githubKey: "",
githubUseName: "",
githubContributionsVisible: false,
},
education: [
{
id: "1",
school: "北京大学",
major: "计算机科学与技术",
degree: "本科",
startDate: "2013-09",
endDate: "2017-06",
visible: true,
gpa: "3.8/4.0",
location: "北京",
description: `<ul class="custom-list">
<li>主修课程:数据结构、算法设计、操作系统、计算机网络、Web开发技术</li>
<li>专业排名前 5%,连续三年获得一等奖学金</li>
<li>担任计算机协会技术部部长,组织多次技术分享会</li>
<li>参与开源项目贡献,获得 GitHub Campus Expert 认证</li>
</ul>`,
},
],
skillContent: `<div class="skill-content">
<ul class="custom-list">
<li>前端框架:精通 React、Vue.js,熟悉 Next.js、Nuxt.js 等 SSR 框架</li>
<li>开发语言:TypeScript、JavaScript(ES6+)、HTML5、CSS3</li>
<li>UI/样式:精通 TailwindCSS、Sass/Less、CSS Module、Styled-components</li>
<li>状态管理:Redux、Vuex、Zustand、Jotai、React Query</li>
<li>工程化工具:Webpack、Vite、Rollup、Babel、ESLint</li>
<li>测试工具:Jest、React Testing Library、Cypress</li>
<li>性能优化:熟悉浏览器渲染原理、性能指标监控、代码分割、懒加载等优化技术</li>
<li>版本控制:Git、SVN</li>
<li>技术管理:具备团队管理经验,主导过多个大型项目的技术选型和架构设计</li>
<li>技术分享:定期组织团队技术分享,主导建设团队技术博客</li>
<li>敏捷开发:熟悉 Scrum 开发流程,具有良好的项目把控能力</li>
<li>英语能力:CET-6 分数 560,具备良好的英文文档阅读和技术交流能力</li>
</ul>
</div>`,
experience: [
{
id: "1",
company: "字节跳动",
position: "高级前端工程师",
date: "2021.07 - 至今",
visible: true,
details: `<ul class="custom-list">
<li>负责抖音创作者平台的开发与维护,主导多个核心功能的技术方案设计</li>
<li>优化项目工程化配置,将构建时间从 8 分钟优化至 2 分钟,提升团队开发效率</li>
<li>设计并实现组件库,提升代码复用率达 70%,显著减少开发时间</li>
<li>主导性能优化项目,使平台首屏加载时间减少 50%,接入 APM 监控系统</li>
<li>指导初级工程师,组织技术分享会,提升团队整体技术水平</li>
</ul>`,
},
],
draggingProjectId: null,
projects: [
{
id: "p1",
name: "抖音创作者中台",
role: "前端负责人",
date: "2022.06 - 2023.12",
description:
"基于 React 的创作者数据分析和内容管理平台,服务百万级创作者群体,包含数据分析、内容管理、收益管理等多个子系统。",
visible: true,
},
{
id: "p2",
name: "微信小程序开发者工具",
role: "核心开发者",
date: "2020.03 - 2021.06",
description:
"为开发者提供小程序开发、调试和发布的一站式解决方案。基于 Electron 构建的跨平台桌面应用。",
visible: true,
},
{
id: "p3",
name: "前端监控平台",
role: "技术负责人",
date: "2021.09 - 2022.03",
description:
"一个完整的前端监控解决方案,包含错误监控、性能监控、用户行为分析等功能。",
visible: true,
},
],
menuSections: [
{ id: "basic", title: "基本信息", icon: "👤", enabled: true, order: 0 },
{ id: "skills", title: "专业技能", icon: "⚡", enabled: true, order: 1 },
{
id: "experience",
title: "工作经验",
icon: "💼",
enabled: true,
order: 2,
},
{ id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 3 },
{ id: "education", title: "教育经历", icon: "🎓", enabled: true, order: 4 },
],
customData: {},
activeSection: "basic",
globalSettings: initialGlobalSettings,
};
// 同步简历到文件系统
const syncResumeToFile = async (
resumeData: ResumeData,