import { toPng } from "html-to-image"; import type { ChangeEvent } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import type { CertificateFieldKey, CertificateFormData, CertificateTemplateDefinition, } from "@/lib/certificate"; import { certificateFieldLabels, certificateFieldOrder, certificateTemplates, customTemplateUploadEnabled, getQrSource, } from "@/lib/certificate"; import { fetchStats, incrementGenerationCount, type StatsResponse } from "@/lib/stats"; import { TemplateScaleFrame } from "@/templates/template-primitives"; const defaultFormData: CertificateFormData = { childName: "", familyAddress: "", parentName: "", birthDate: "", birthAddress: "", }; const emptyStats: StatsResponse = { total: 0, byTemplate: {}, updatedAt: null, }; const previewWidth = 760; const statsUnavailableMessage = "统计服务未连接,当前数量不会持久化。请同时运行 npm run dev 和 npm run dev:server,或直接运行 npm run dev:all。"; const statsIncrementFailedMessage = "图片已下载,但统计服务没有记录这次生成。请同时运行 npm run dev 和 npm run dev:server,或直接运行 npm run dev:all。"; function TemplateThumbnail({ template, formData, qrSource, currentCount, }: { template: CertificateTemplateDefinition; formData: CertificateFormData; qrSource: string; currentCount: number; }) { const scale = 220 / template.width; const Component = template.Component; return (
); } export function CertificateGenerator() { const [selectedTemplateId, setSelectedTemplateId] = useState( certificateTemplates[0]?.id ?? "", ); const [formData, setFormData] = useState(defaultFormData); const [stats, setStats] = useState(emptyStats); const [isExporting, setIsExporting] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [statsMessage, setStatsMessage] = useState(""); const exportRef = useRef(null); const selectedTemplate = useMemo( () => certificateTemplates.find((template) => template.id === selectedTemplateId) ?? certificateTemplates[0], [selectedTemplateId], ); const qrSource = useMemo(() => getQrSource(), []); const currentCount = stats.total + 1; const SelectedTemplateComponent = selectedTemplate.Component; useEffect(() => { let active = true; fetchStats() .then((nextStats) => { if (!active) return; setStats(nextStats); setStatsMessage(""); }) .catch(() => { if (!active) return; setStatsMessage(statsUnavailableMessage); }); return () => { active = false; }; }, []); const handleChange = (key: CertificateFieldKey) => (event: ChangeEvent) => { setFormData((current) => ({ ...current, [key]: event.target.value, })); }; const handleExport = async () => { if (!exportRef.current) { setErrorMessage("导出节点尚未准备好,请稍后重试。"); return; } setIsExporting(true); setErrorMessage(""); try { const dataUrl = await toPng(exportRef.current, { cacheBust: true, pixelRatio: 1, canvasWidth: selectedTemplate.width, canvasHeight: selectedTemplate.height, backgroundColor: "#ffffff", }); const link = document.createElement("a"); link.href = dataUrl; link.download = `${selectedTemplate.name}.png`; link.click(); try { const nextStats = await incrementGenerationCount(selectedTemplate.id); setStats(nextStats); setStatsMessage(""); } catch { setStatsMessage(statsIncrementFailedMessage); } } catch (error) { setErrorMessage(error instanceof Error ? error.message : "导出失败,请稍后重试。"); } finally { setIsExporting(false); } }; return (

模板切换

{certificateTemplates.map((template) => { const active = template.id === selectedTemplate.id; return ( ); })}