Add product card image saving

This commit is contained in:
Jacky Ser
2026-08-12 16:23:15 +08:00
parent 81098ea237
commit 62a3d6ed14
2 changed files with 326 additions and 10 deletions

View File

@@ -358,10 +358,10 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
.catalog-card-body { padding: 16px; } .catalog-card-body { padding: 16px; }
.catalog-card-body > p { margin: 0 0 5px; color: #8793a5; font-size: 9px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .catalog-card-body > p { margin: 0 0 5px; color: #8793a5; font-size: 9px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.catalog-card-body h3 { height: 42px; margin: 0 0 12px; overflow: hidden; font-size: 14px; line-height: 1.5; } .catalog-card-body h3 { height: 42px; margin: 0 0 12px; overflow: hidden; font-size: 14px; line-height: 1.5; }
.catalog-card-body dl { min-height: 83px; margin: 0; padding: 10px; border-radius: 9px; background: #f6f8fb; } .catalog-card-body dl { min-height: 0; margin: 0; padding: 10px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; border-radius: 9px; background: #f6f8fb; }
.catalog-card-body dl div { display: flex; justify-content: space-between; gap: 8px; padding: 3px 0; font-size: 9px; } .catalog-card-body dl div { min-width: 0; display: grid; align-content: start; gap: 4px; padding: 3px 0; font-size: 9px; }
.catalog-card-body dt { color: #8a95a7; } .catalog-card-body dt { overflow: hidden; color: #8a95a7; text-overflow: ellipsis; white-space: nowrap; }
.catalog-card-body dd { max-width: 58%; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; font-weight: 650; } .catalog-card-body dd { max-width: 100%; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; font-weight: 650; }
.catalog-card-body > button { .catalog-card-body > button {
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
@@ -375,6 +375,12 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 700;
} }
.catalog-card-body > .product-save-button { margin-top: 5px; padding: 8px 10px; border: 1px solid #d8e3f3; border-radius: 8px; color: #185ee8; background: #f7faff; }
.product-save-button { width: 100%; min-height: 33px; margin-top: 10px; padding: 8px 10px; display: inline-flex; align-items: center; justify-content: space-between; border: 1px solid #d8e3f3; border-radius: 8px; color: #185ee8; background: #f7faff; cursor: pointer; font-size: 10px; font-weight: 750; }
.product-save-button:hover { border-color: #a9c4f2; color: #104ec6; background: #eef5ff; }
.product-save-button span { font-size: 13px; line-height: 1; }
.product-save-button.is-saving { visibility: hidden; }
.product-save-toast { position: fixed; left: 50%; bottom: 24px; z-index: 120; transform: translateX(-50%); max-width: min(90vw, 360px); padding: 10px 15px; border: 1px solid #cbdaf2; border-radius: 999px; color: #174baf; background: rgba(248,251,255,.96); box-shadow: 0 12px 32px rgba(24,64,126,.18); font-size: 11px; font-weight: 750; text-align: center; backdrop-filter: blur(10px); }
.precision-gear-panel { .precision-gear-panel {
display: grid; display: grid;
grid-template-columns: minmax(260px, .8fr) 1.2fr; grid-template-columns: minmax(260px, .8fr) 1.2fr;
@@ -425,6 +431,7 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
.precision-gear-card-body dl div { display: flex; justify-content: space-between; gap: 5px; padding: 2px 0; font-size: 8px; } .precision-gear-card-body dl div { display: flex; justify-content: space-between; gap: 5px; padding: 2px 0; font-size: 8px; }
.precision-gear-card-body dt { color: #8a95a7; } .precision-gear-card-body dt { color: #8a95a7; }
.precision-gear-card-body dd { max-width: 62%; margin: 0; overflow: hidden; color: #52617a; text-overflow: ellipsis; white-space: nowrap; text-align: right; font-weight: 700; } .precision-gear-card-body dd { max-width: 62%; margin: 0; overflow: hidden; color: #52617a; text-overflow: ellipsis; white-space: nowrap; text-align: right; font-weight: 700; }
.precision-gear-card-body .product-save-button { margin-top: 10px; }
.precision-gear-empty { margin-top: 15px; } .precision-gear-empty { margin-top: 15px; }
.precision-gear-load-more { margin-top: 22px; } .precision-gear-load-more { margin-top: 22px; }
.catalog-empty { padding: 60px 20px; text-align: center; border: 1px dashed #cbd5e3; border-radius: 15px; background: white; } .catalog-empty { padding: 60px 20px; text-align: center; border: 1px dashed #cbd5e3; border-radius: 15px; background: white; }
@@ -492,12 +499,15 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
.catalog-modal-inline-info small { color: #96a0af; font-size: 8px; } .catalog-modal-inline-info small { color: #96a0af; font-size: 8px; }
.catalog-modal-image { min-height: min(56vh, 520px); display: flex; align-items: center; justify-content: center; border: 1px solid #e1e7f0; border-radius: 13px; overflow: hidden; background: #f5f7fb; } .catalog-modal-image { min-height: min(56vh, 520px); display: flex; align-items: center; justify-content: center; border: 1px solid #e1e7f0; border-radius: 13px; overflow: hidden; background: #f5f7fb; }
.catalog-modal-image img { display: block; max-width: 100%; max-height: min(56vh, 510px); width: 100%; height: auto; object-fit: contain; mix-blend-mode: multiply; } .catalog-modal-image img { display: block; max-width: 100%; max-height: min(56vh, 510px); width: 100%; height: auto; object-fit: contain; mix-blend-mode: multiply; }
.catalog-spec-table { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; margin-top: 18px; overflow: hidden; border: 1px solid #e0e5ed; border-radius: 11px; background: #e0e5ed; } .catalog-spec-table { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin-top: 18px; overflow: hidden; border: 1px solid #e0e5ed; border-radius: 11px; background: #e0e5ed; }
.catalog-spec-table div { min-height: 38px; padding: 7px 10px; display: grid; align-content: center; gap: 3px; background: white; } .catalog-spec-table div { min-width: 0; min-height: 58px; padding: 10px 12px; display: grid; align-content: center; gap: 4px; background: white; }
.catalog-spec-table span { color: #8b96a7; font-size: 8px; } .catalog-spec-table span,
.catalog-spec-table strong { font-size: 10px; } .catalog-spec-table strong { min-width: 0; overflow-wrap: anywhere; word-break: break-word; }
.catalog-spec-table span { color: #8b96a7; font-size: 8px; line-height: 1.35; }
.catalog-spec-table strong { font-size: 10px; line-height: 1.4; }
.catalog-modal-actions { display: flex; align-items: center; gap: 18px; margin-top: 20px; } .catalog-modal-actions { display: flex; align-items: center; gap: 18px; margin-top: 20px; }
.catalog-modal-actions .panel-primary { flex: 0 0 auto; } .catalog-modal-actions .panel-primary { flex: 0 0 auto; }
.catalog-modal-actions .modal-save-button { width: auto; min-width: 124px; margin-top: 0; flex: 0 0 auto; }
.catalog-modal-actions p { margin: 0; color: #8b95a5; font-size: 9px; line-height: 1.6; } .catalog-modal-actions p { margin: 0; color: #8b95a5; font-size: 9px; line-height: 1.6; }
.how-section { padding: 100px max(32px, calc((100vw - 1180px) / 2)); display: grid; grid-template-columns: .9fr 1.1fr; gap: 100px; color: white; background: linear-gradient(135deg, #0c1528, #14284e); } .how-section { padding: 100px max(32px, calc((100vw - 1180px) / 2)); display: grid; grid-template-columns: .9fr 1.1fr; gap: 100px; color: white; background: linear-gradient(135deg, #0c1528, #14284e); }
.eyebrow.light { color: #77a5ff; } .eyebrow.light { color: #77a5ff; }
@@ -524,6 +534,7 @@ footer > span { color: #9aa4b2; font-size: 10px; }
.catalog-grid { grid-template-columns: repeat(2, 1fr); } .catalog-grid { grid-template-columns: repeat(2, 1fr); }
.precision-gear-panel { grid-template-columns: 1fr; } .precision-gear-panel { grid-template-columns: 1fr; }
.precision-gear-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .precision-gear-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.catalog-spec-table { grid-template-columns: repeat(3, minmax(0, 1fr)); }
} }
@media (max-width: 720px) { @media (max-width: 720px) {
@@ -592,7 +603,10 @@ footer > span { color: #9aa4b2; font-size: 10px; }
.precision-gear-card-body { padding: 9px; } .precision-gear-card-body { padding: 9px; }
.catalog-card-body { padding: 15px; } .catalog-card-body { padding: 15px; }
.catalog-card-body h3 { height: auto; min-height: 42px; font-size: 14px; } .catalog-card-body h3 { height: auto; min-height: 42px; font-size: 14px; }
.catalog-card-body dl { min-height: 90px; } .catalog-card-body dl { min-height: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; padding: 9px 8px; }
.catalog-card-body dl div { min-width: 0; display: grid; align-content: start; justify-content: initial; gap: 3px; padding: 2px 0; font-size: 8px; }
.catalog-card-body dt { overflow: hidden; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
.catalog-card-body dd { max-width: 100%; overflow: hidden; font-size: 9px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.catalog-modal-backdrop { padding: 10px; } .catalog-modal-backdrop { padding: 10px; }
.catalog-modal { padding: 22px 16px; } .catalog-modal { padding: 22px 16px; }
.catalog-modal-visual-toolbar { align-items: flex-start; flex-direction: column; gap: 10px; } .catalog-modal-visual-toolbar { align-items: flex-start; flex-direction: column; gap: 10px; }
@@ -600,8 +614,12 @@ footer > span { color: #9aa4b2; font-size: 10px; }
.catalog-modal-inline-info h2 { font-size: 18px; } .catalog-modal-inline-info h2 { font-size: 18px; }
.catalog-modal-image { min-height: 290px; } .catalog-modal-image { min-height: 290px; }
.catalog-modal-image img { max-height: 290px; } .catalog-modal-image img { max-height: 290px; }
.catalog-spec-table { grid-template-columns: 1fr; } .catalog-spec-table { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.catalog-spec-table div { min-height: 52px; padding: 8px 9px; gap: 3px; }
.catalog-spec-table span { font-size: 8px; }
.catalog-spec-table strong { font-size: 9px; }
.catalog-modal-actions { align-items: stretch; flex-direction: column; } .catalog-modal-actions { align-items: stretch; flex-direction: column; }
.catalog-modal-actions .modal-save-button { width: 100%; }
.products-section { padding: 75px 16px; } .products-section { padding: 75px 16px; }
.how-section { grid-template-columns: 1fr; padding: 75px 20px; } .how-section { grid-template-columns: 1fr; padding: 75px 20px; }
footer { grid-template-columns: 1fr; justify-items: center; gap: 15px; } footer { grid-template-columns: 1fr; justify-items: center; gap: 15px; }

View File

@@ -350,6 +350,193 @@ function Metric({ label, value }: { label: string; value: string }) {
); );
} }
type SaveProductRequest = {
product: CatalogProduct;
badge?: string;
subtitle?: string;
extraSpecs?: Array<[string, string]>;
};
function getSaveProductSpecs(product: CatalogProduct, extraSpecs: Array<[string, string]> = []) {
return [
...extraSpecs.filter(([, value]) => value),
...Object.entries(product.specs).filter(([, value]) => value),
];
}
function wrapCanvasText(context: CanvasRenderingContext2D, value: string, maxWidth: number, maxLines = 2) {
const characters = Array.from(String(value));
const lines: string[] = [];
let current = "";
for (const character of characters) {
const next = current + character;
if (current && context.measureText(next).width > maxWidth) {
lines.push(current);
current = character;
if (lines.length === maxLines) break;
} else {
current = next;
}
}
if (lines.length < maxLines && current) lines.push(current);
if (lines.length === maxLines && characters.join("").length > lines.join("").length) {
const last = lines[maxLines - 1] ?? "";
lines[maxLines - 1] = `${last.slice(0, Math.max(1, last.length - 1))}`;
}
return lines;
}
function drawRoundedRect(
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
fill: string,
stroke?: string,
) {
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width - radius, y);
context.quadraticCurveTo(x + width, y, x + width, y + radius);
context.lineTo(x + width, y + height - radius);
context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - radius);
context.lineTo(x, y + radius);
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
context.fillStyle = fill;
context.fill();
if (stroke) {
context.strokeStyle = stroke;
context.lineWidth = 2;
context.stroke();
}
}
function loadProductCanvasImage(src: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`产品图片加载失败:${src}`));
image.src = src;
});
}
async function renderProductCardPng({ product, badge, subtitle, extraSpecs = [] }: SaveProductRequest) {
const specs = getSaveProductSpecs(product, extraSpecs);
const columnCount = 2;
const rowCount = Math.max(1, Math.ceil(specs.length / columnCount));
const cardWidth = 1200;
const padding = 52;
const headerHeight = 152;
const specCellHeight = 70;
const specGap = 10;
const mainHeight = Math.max(390, rowCount * specCellHeight + (rowCount - 1) * specGap + 28);
const cardHeight = padding + headerHeight + 24 + mainHeight + 24 + 42 + padding;
const canvas = document.createElement("canvas");
canvas.width = cardWidth;
canvas.height = cardHeight;
const context = canvas.getContext("2d");
if (!context) throw new Error("当前浏览器不支持图片生成");
context.fillStyle = "#eef3fa";
context.fillRect(0, 0, cardWidth, cardHeight);
drawRoundedRect(context, 18, 18, cardWidth - 36, cardHeight - 36, 26, "#ffffff", "#d9e3f2");
context.fillStyle = "#185ee8";
context.fillRect(padding, padding, 110, 7);
context.fillStyle = "#123267";
context.font = "800 25px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText("GAUDELOT", padding, padding + 34);
context.fillStyle = "#7b8ca8";
context.font = "600 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText("高徳乐产品资料", padding + 145, padding + 33);
if (badge) {
const badgeWidth = Math.max(100, context.measureText(badge).width + 34);
drawRoundedRect(context, cardWidth - padding - badgeWidth, padding + 8, badgeWidth, 32, 16, "#edf4ff");
context.fillStyle = "#185ee8";
context.font = "700 14px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText(badge, cardWidth - padding - badgeWidth + 17, padding + 29);
}
context.fillStyle = "#111d34";
context.font = "800 30px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
const titleLines = wrapCanvasText(context, product.name, 760, 2);
titleLines.forEach((line, index) => context.fillText(line, padding, padding + 82 + index * 36));
context.fillStyle = "#71809a";
context.font = "600 17px ui-monospace, SFMono-Regular, Menlo, monospace";
context.fillText(product.code, padding, padding + 145);
if (subtitle) {
context.fillStyle = "#8290a5";
context.font = "500 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText(subtitle, padding + 240, padding + 145);
}
const mainY = padding + headerHeight + 24;
const imageX = padding;
const imageWidth = 430;
const infoX = imageX + imageWidth + 24;
const infoWidth = cardWidth - padding - infoX;
drawRoundedRect(context, imageX, mainY, imageWidth, mainHeight, 18, "#f3f7fd", "#dce6f3");
drawRoundedRect(context, infoX, mainY, infoWidth, mainHeight, 18, "#f8faff", "#dce6f3");
try {
if (!product.image) throw new Error("没有产品主图");
const image = await loadProductCanvasImage(product.image);
const maxImageWidth = imageWidth - 34;
const maxImageHeight = mainHeight - 34;
const scale = Math.min(maxImageWidth / image.naturalWidth, maxImageHeight / image.naturalHeight);
const imageWidthOnCanvas = image.naturalWidth * scale;
const imageHeightOnCanvas = image.naturalHeight * scale;
context.drawImage(
image,
imageX + (imageWidth - imageWidthOnCanvas) / 2,
mainY + (mainHeight - imageHeightOnCanvas) / 2,
imageWidthOnCanvas,
imageHeightOnCanvas,
);
} catch {
context.fillStyle = "#9eb3d3";
context.font = "800 22px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText("GAUDELOT", imageX + 132, mainY + mainHeight / 2);
}
context.fillStyle = "#6f809b";
context.font = "700 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText("主要参数", infoX + 22, mainY + 31);
const cellsTop = mainY + 48;
const cellWidth = (infoWidth - 44 - specGap) / columnCount;
specs.forEach(([label, value], index) => {
const column = index % columnCount;
const row = Math.floor(index / columnCount);
const cellX = infoX + 22 + column * (cellWidth + specGap);
const cellY = cellsTop + row * (specCellHeight + specGap);
drawRoundedRect(context, cellX, cellY, cellWidth, specCellHeight, 10, "#ffffff", "#e4ebf4");
context.fillStyle = "#8b98ab";
context.font = "600 13px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
const labelLines = wrapCanvasText(context, label, cellWidth - 24, 1);
context.fillText(labelLines[0] ?? label, cellX + 12, cellY + 22);
context.fillStyle = "#15233d";
context.font = "800 16px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
const valueLines = wrapCanvasText(context, value, cellWidth - 24, 2);
valueLines.forEach((line, lineIndex) => context.fillText(line, cellX + 12, cellY + 47 + lineIndex * 18));
});
context.fillStyle = "#8b98ab";
context.font = "500 13px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif";
context.fillText("高徳乐官方产品目录 · 仅供选型参考,具体参数以正式文件为准", padding, cardHeight - padding - 11);
return new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("PNG生成失败"))), "image/png");
});
}
export default function Home() { export default function Home() {
const [stage, setStage] = useState(0); const [stage, setStage] = useState(0);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -371,6 +558,8 @@ export default function Home() {
const [precisionGearFiltersOpen, setPrecisionGearFiltersOpen] = useState(false); const [precisionGearFiltersOpen, setPrecisionGearFiltersOpen] = useState(false);
const [selectedCatalogProduct, setSelectedCatalogProduct] = useState<CatalogProduct | null>(null); const [selectedCatalogProduct, setSelectedCatalogProduct] = useState<CatalogProduct | null>(null);
const [catalogVisualMode, setCatalogVisualMode] = useState<CatalogVisualMode>("main"); const [catalogVisualMode, setCatalogVisualMode] = useState<CatalogVisualMode>("main");
const [savingProductKey, setSavingProductKey] = useState("");
const [saveProductMessage, setSaveProductMessage] = useState("");
const matches = useMemo(() => getMatches(requirements), [requirements]); const matches = useMemo(() => getMatches(requirements), [requirements]);
const topMatches = matches.slice(0, 3); const topMatches = matches.slice(0, 3);
@@ -458,6 +647,60 @@ export default function Home() {
setCatalogVisualMode("main"); setCatalogVisualMode("main");
} }
async function saveProductCard(request: SaveProductRequest) {
if (savingProductKey) return;
const saveKey = `${request.product.id}-${request.badge ?? "product"}`;
setSavingProductKey(saveKey);
setSaveProductMessage("正在生成产品图片…");
try {
const blob = await renderProductCardPng(request);
const safeName = `${request.product.code}-${request.product.name}`
.replace(/[\\/:*?"<>|]/g, "-")
.replace(/\s+/g, "-")
.slice(0, 90);
const fileName = `${safeName || request.product.id}.png`;
const file = new File([blob], fileName, { type: "image/png" });
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints > 0 && window.innerWidth <= 900);
const canShareFile = typeof navigator.share === "function" &&
(!navigator.canShare || navigator.canShare({ files: [file] }));
if (isMobile && canShareFile) {
try {
await navigator.share({
title: `${request.product.name} 产品资料`,
text: `高徳乐${request.product.category} · ${request.product.code}`,
files: [file],
});
setSaveProductMessage("已打开系统分享面板");
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
setSaveProductMessage("已取消保存");
} else {
throw error;
}
}
} else {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
setSaveProductMessage("PNG图片已下载");
}
} catch (error) {
console.error("保存产品图片失败", error);
setSaveProductMessage("图片生成失败,请稍后重试");
} finally {
setSavingProductKey("");
window.setTimeout(() => setSaveProductMessage(""), 2800);
}
}
function startSelection() { function startSelection() {
setMobileMenuOpen(false); setMobileMenuOpen(false);
setStage(1); setStage(1);
@@ -488,6 +731,11 @@ export default function Home() {
return ( return (
<main> <main>
{saveProductMessage && (
<div className="product-save-toast" role="status" aria-live="polite">
{saveProductMessage}
</div>
)}
<header className="site-header"> <header className="site-header">
<a className="brand" href="#top" aria-label="高德乐 AI选型首页"> <a className="brand" href="#top" aria-label="高德乐 AI选型首页">
<span className="brand-wordmark"> <span className="brand-wordmark">
@@ -936,6 +1184,19 @@ export default function Home() {
<strong>{match.product.category}</strong> <strong>{match.product.category}</strong>
<small></small> <small></small>
</div> </div>
<button
className={`product-save-button ${savingProductKey.startsWith(`${match.product.id}-`) ? "is-saving" : ""}`}
type="button"
onClick={() => saveProductCard({
product: match.product,
badge: index === 0 ? (match.coreCompatible ? "首选推荐" : "待复核候选") : `${match.confidence}% 匹配`,
subtitle: `${match.product.category} · ${match.product.size || "尺寸待复核"}`,
extraSpecs: [["AI匹配度", `${match.confidence}%`]],
})}
aria-label={`保存${match.product.name}产品图片`}
>
{savingProductKey.startsWith(`${match.product.id}-`) ? "正在生成…" : "保存产品"} <span></span>
</button>
</div> </div>
</article> </article>
))} ))}
@@ -1123,6 +1384,18 @@ export default function Home() {
<button type="button" onClick={() => openCatalogProduct(product)}> <button type="button" onClick={() => openCatalogProduct(product)}>
<span></span> <span></span>
</button> </button>
<button
className={`product-save-button ${savingProductKey === `${product.id}-${product.category}` ? "is-saving" : ""}`}
type="button"
onClick={() => saveProductCard({
product,
badge: product.category,
subtitle: "完整产品参数",
})}
aria-label={`保存${product.name}产品图片`}
>
{savingProductKey === `${product.id}-${product.category}` ? "正在生成…" : "保存产品"} <span></span>
</button>
</div> </div>
</article> </article>
))} ))}
@@ -1219,6 +1492,19 @@ export default function Home() {
<div key={label}><dt>{label}</dt><dd>{value}</dd></div> <div key={label}><dt>{label}</dt><dd>{value}</dd></div>
))} ))}
</dl> </dl>
<button
className={`product-save-button ${savingProductKey === `${product.id}-${product.subCategory}` ? "is-saving" : ""}`}
type="button"
onClick={() => saveProductCard({
product,
badge: "精密齿轮配件",
subtitle: product.subCategory,
extraSpecs: [["产品子类", product.subCategory]],
})}
aria-label={`保存${product.name}产品图片`}
>
{savingProductKey === `${product.id}-${product.subCategory}` ? "正在生成…" : "保存产品"} <span></span>
</button>
</div> </div>
</article> </article>
))} ))}
@@ -1324,6 +1610,18 @@ export default function Home() {
> >
AI继续确认需求 <span></span> AI继续确认需求 <span></span>
</button> </button>
<button
className={`product-save-button modal-save-button ${savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "is-saving" : ""}`}
type="button"
onClick={() => saveProductCard({
product: selectedCatalogProduct,
badge: selectedCatalogProduct.category,
subtitle: "完整产品参数",
})}
aria-label={`保存${selectedCatalogProduct.name}产品图片`}
>
{savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "正在生成…" : "保存产品"} <span></span>
</button>
<p></p> <p></p>
</div> </div>
</section> </section>