From 77a5e670ed0ecb371fa439afab5cab792d03b874 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Thu, 26 Feb 2026 01:02:25 +0800 Subject: [PATCH] feat: Implement dynamic template previews with i18n support and locale-aware routing --- .vscode/settings.json | 22 +- src/app/app/dashboard/client.tsx | 27 ++- src/app/app/dashboard/resumes/page.tsx | 285 +++++++++++++++-------- src/app/app/dashboard/templates/page.tsx | 153 ++++++++++-- src/components/shared/TemplateSheet.tsx | 152 +++++++++--- src/config/index.ts | 2 +- src/i18n/locales/en.json | 9 +- src/i18n/locales/zh.json | 9 +- src/routes/index.tsx | 7 +- 9 files changed, 488 insertions(+), 178 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a35afde..fdcedb5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,17 +2,25 @@ "css.validate": false, "less.validate": false, "scss.validate": false, - - "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], - "stylelint.validate": ["css", "less", "postcss", "scss", "sass"], + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "stylelint.validate": [ + "css", + "less", + "postcss", + "scss", + "sass" + ], "typescript.tsdk": "./node_modules/typescript/lib", - "search.exclude": { "**/node_modules": true, "dist": true, "build": true }, - "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", @@ -34,7 +42,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" @@ -43,4 +51,4 @@ "src/i18n", "src/i18n/locales" ] -} +} \ No newline at end of file diff --git a/src/app/app/dashboard/client.tsx b/src/app/app/dashboard/client.tsx index 3533823..3f1b766 100644 --- a/src/app/app/dashboard/client.tsx +++ b/src/app/app/dashboard/client.tsx @@ -22,7 +22,7 @@ import { TooltipTrigger } from "@/components/ui/tooltip"; import Logo from "@/components/shared/Logo"; -import { useTranslations } from "@/i18n/compat/client"; +import { useLocale, useTranslations } from "@/i18n/compat/client"; interface MenuItem { title: string; @@ -60,6 +60,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); const pathname = usePathname(); + const locale = useLocale(); const [open, setOpen] = useState(true); const [collapsible, setCollapsible] = useState<"offcanvas" | "icon" | "none">( "icon" @@ -94,11 +95,11 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { className="border-r border-border/40 bg-card/50 backdrop-blur-xl" > -
+
router.push(`/${locale}`)} + > router.push("/")} /> {open && ( @@ -121,11 +122,10 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { svg]:size-auto ${ - active - ? "bg-primary/10 text-primary font-bold hover:bg-primary/20 hover:text-primary" - : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" - }`} + className={`w-full transition-all duration-200 ease-in-out h-12 mb-1 [&>svg]:size-auto ${active + ? "bg-primary/10 text-primary font-bold hover:bg-primary/20 hover:text-primary" + : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" + }`} >
{ {item.items.map((subItem) => (
router.push(subItem.href)} > {subItem.title} diff --git a/src/app/app/dashboard/resumes/page.tsx b/src/app/app/dashboard/resumes/page.tsx index bc43dd5..381b465 100644 --- a/src/app/app/dashboard/resumes/page.tsx +++ b/src/app/app/dashboard/resumes/page.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect } from "react"; -import { useTranslations } from "@/i18n/compat/client"; +import { useTranslations, useLocale } from "@/i18n/compat/client"; import { useRouter } from "@/lib/navigation"; import { Plus, FileText, Settings, AlertCircle, Upload } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; @@ -18,14 +18,176 @@ import { cn } from "@/lib/utils"; import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem"; import { useResumeStore } from "@/store/useResumeStore"; import { initialResumeState } from "@/config/initialResumeData"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import ResumeTemplateComponent from "@/components/templates"; +import { DEFAULT_TEMPLATES } from "@/config"; import { generateUUID } from "@/utils/uuid"; + const ResumesList = () => { return ; }; +const ResumeCardItem = ({ id, resume, t, locale, setActiveResume, router, deleteResume, index }: any) => { + const containerRef = React.useRef(null); + const [scale, setScale] = React.useState(0.24); + + React.useEffect(() => { + if (!containerRef.current) return; + const observer = new ResizeObserver((entries) => { + const { width } = entries[0].contentRect; + if (width > 0) { + setScale(width / 793.700787); // Exact 210mm in pixels at 96dpi + } + }); + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); + + return ( + + + { setActiveResume(id); router.push(`/app/workbench/${id}`); }}> +
+
+
+ {(() => { + const template = DEFAULT_TEMPLATES.find(t => t.id === resume.templateId) || DEFAULT_TEMPLATES[0]; + return ; + })()} +
+
+
+ +
+
+
+ + {resume.title || t("dashboard.resumes.untitled")} + + + {t(`dashboard.templates.${DEFAULT_TEMPLATES.find(t => t.id === resume.templateId)?.id === "left-right" ? "leftRight" : (DEFAULT_TEMPLATES.find(t => t.id === resume.templateId)?.id || "classic")}.name`)} · {new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(resume.updatedAt || resume.createdAt))} + +
+
+
+ +
+ + + + + + + + + + e.stopPropagation()}> + + {t("dashboard.resumes.deleteConfirmTitle")} + + {t("dashboard.resumes.deleteConfirmDescription")} + + + + e.stopPropagation()}>{t("common.cancel")} + { + e.stopPropagation(); + deleteResume(resume); + toast.success(t("common.deleteSuccess")); + }} + > + {t("common.confirm")} + + + + + +
+
+
+
+ ); +}; + const ResumeWorkbench = () => { const t = useTranslations(); + const locale = useLocale(); const { resumes, setActiveResume, @@ -125,13 +287,14 @@ const ResumeWorkbench = () => { }; return ( - + + { animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.3, delay: 0.2 }} > -
+
{ > - + { > - + {t("dashboard.resumes.newResume")} - + {t("dashboard.resumes.newResumeDescription")} @@ -265,97 +428,23 @@ const ResumeWorkbench = () => { {Object.entries(resumes).map(([id, resume], index) => ( - - - - - - - - {resume.title || "未命名简历"} - - - {t("dashboard.resumes.created")} - - {new Date(resume.createdAt).toLocaleDateString()} - - - - -
- - - - - - -
-
-
-
+ id={id} + resume={resume} + t={t} + locale={locale} + setActiveResume={setActiveResume} + router={router} + deleteResume={deleteResume} + index={index} + /> ))}
+ ); }; export const runtime = "edge"; diff --git a/src/app/app/dashboard/templates/page.tsx b/src/app/app/dashboard/templates/page.tsx index e013853..a2b0901 100644 --- a/src/app/app/dashboard/templates/page.tsx +++ b/src/app/app/dashboard/templates/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { useTranslations } from "@/i18n/compat/client"; import { motion } from "framer-motion"; import { useRouter } from "@/lib/navigation"; @@ -9,6 +9,10 @@ import { useResumeStore } from "@/store/useResumeStore"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import ResumeTemplateComponent from "@/components/templates"; +import { initialResumeState, initialResumeStateEn } from "@/config/initialResumeData"; +import { useLocale } from "@/i18n/compat/client"; const container = { hidden: { opacity: 0 }, @@ -27,12 +31,49 @@ const item = { const TemplatesPage = () => { const t = useTranslations("dashboard.templates"); + const locale = useLocale(); const router = useRouter(); const createResume = useResumeStore((state) => state.createResume); const [previewTemplate, setPreviewTemplate] = useState<{ id: string; open: boolean; } | null>(null); + + const PRESET_COLORS = [ + { name: "default", value: "" }, + { name: "blue", value: "#3b82f6" }, + { name: "green", value: "#10b981" }, + { name: "purple", value: "#8b5cf6" }, + { name: "orange", value: "#f97316" }, + { name: "red", value: "#ef4444" }, + { name: "slate", value: "#475569" }, + { name: "black", value: "#000000" }, + ]; + const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[0].value); + const autoPlayRef = useRef(null); + + // Auto-cycle colors every 3 seconds + useState(() => { + // Only run on client + if (typeof window !== "undefined") { + let currentIndex = 0; + autoPlayRef.current = setInterval(() => { + currentIndex = (currentIndex + 1) % PRESET_COLORS.length; + setSelectedColor(PRESET_COLORS[currentIndex].value); + }, 3000); + } + }); + + const handleColorSelect = (value: string) => { + setSelectedColor(value); + // Stop autoplay when user manually selects a color + if (autoPlayRef.current) { + clearInterval(autoPlayRef.current); + autoPlayRef.current = null; + } + }; + + const baseData = locale === "en" ? initialResumeStateEn : initialResumeState; const handleCreateResume = (templateId: string) => { const template = DEFAULT_TEMPLATES.find((t) => t.id === templateId); @@ -46,7 +87,7 @@ const TemplatesPage = () => { updateResume(resumeId, { globalSettings: { ...resume.globalSettings, - themeColor: template.colorScheme.primary, + themeColor: selectedColor || template.colorScheme.primary, sectionSpacing: template.spacing.sectionGap, paragraphSpacing: template.spacing.itemGap, pagePadding: template.spacing.contentPadding, @@ -62,10 +103,36 @@ const TemplatesPage = () => { }; return ( -
-
-
+ +
+
+

{t("title")}

+ +
+ {PRESET_COLORS.map((color) => ( + + ))} +
{ height: "297px", }} > -