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 (
);
})}
{errorMessage ? {errorMessage}
: null}
{statsMessage ? {statsMessage}
: null}
);
}