Add cart exports and sample request workflow

This commit is contained in:
Jacky Ser
2026-08-13 11:07:56 +08:00
parent 511dbcd0e8
commit 5101493901
4 changed files with 1662 additions and 104 deletions

View File

@@ -350,6 +350,216 @@ function Metric({ label, value }: { label: string; value: string }) {
);
}
type CartItem = {
product: CatalogProduct;
quantity: number;
source: string;
};
const CART_STORAGE_KEY = "gaudelo-product-cart";
type CartExportImage = {
base64: string;
extension: "jpeg";
};
const CART_EXPORT_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
const CART_IMAGE_SIZE_PX = 189; // 约 5 cm按 96 DPI 换算
function getCartExportFilename() {
const now = new Date();
const pad = (value: number) => String(value).padStart(2, "0");
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
return `高徳乐产品清单_${date}.xlsx`;
}
function getFullSpecEntries(product: CatalogProduct) {
return Object.entries(product.specs).filter(([, value]) => value);
}
function getKeySpecText(product: CatalogProduct) {
const operationAngle = product.specs["可操作角度"] ?? product.specs["操作角度范围"];
return [
...getFeaturedSpecs(product),
...(operationAngle ? [["可操作角度", operationAngle] as const] : []),
]
.map(([label, value]) => `${label}${value}`)
.join("\n") || "暂无主要参数";
}
function getFullSpecText(product: CatalogProduct) {
return getFullSpecEntries(product)
.map(([label, value]) => `${label}${value}`)
.join("\n") || "暂无详细参数";
}
function compressCartImage(src: string) {
return new Promise<CartExportImage | null>((resolve) => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = CART_IMAGE_SIZE_PX;
canvas.height = CART_IMAGE_SIZE_PX;
const context = canvas.getContext("2d");
if (!context) {
resolve(null);
return;
}
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);
const padding = 10;
const scale = Math.min(
(canvas.width - padding * 2) / image.naturalWidth,
(canvas.height - padding * 2) / image.naturalHeight,
);
const imageWidth = image.naturalWidth * scale;
const imageHeight = image.naturalHeight * scale;
context.drawImage(
image,
(canvas.width - imageWidth) / 2,
(canvas.height - imageHeight) / 2,
imageWidth,
imageHeight,
);
resolve({
base64: canvas.toDataURL("image/jpeg", 0.78),
extension: "jpeg",
});
};
image.onerror = () => resolve(null);
image.src = src;
});
}
async function buildCartWorkbook(cartItems: CartItem[]) {
const { default: ExcelJS } = await import("exceljs");
const workbook = new ExcelJS.Workbook();
workbook.creator = "高徳乐 AI 舵机选型助手";
workbook.created = new Date();
const summarySheet = workbook.addWorksheet("产品清单");
const detailsSheet = workbook.addWorksheet("详细参数");
summarySheet.views = [{ state: "frozen", ySplit: 1 }];
detailsSheet.views = [{ state: "frozen", ySplit: 1 }];
summarySheet.columns = [
{ header: "产品图片", key: "image", width: 27 },
{ header: "产品名称", key: "name", width: 34 },
{ header: "产品编码", key: "code", width: 24 },
{ header: "产品分类", key: "category", width: 18 },
{ header: "购买数量", key: "quantity", width: 12 },
{ header: "详细参数", key: "detailSpecs", width: 48 },
];
detailsSheet.columns = [
{ header: "产品名称", key: "name", width: 34 },
{ header: "产品编码", key: "code", width: 24 },
{ header: "产品分类", key: "category", width: 18 },
{ header: "购买数量", key: "quantity", width: 12 },
{ header: "参数名称", key: "label", width: 22 },
{ header: "参数值", key: "value", width: 48 },
];
const headerStyle = {
font: { bold: true, color: { argb: "FFFFFFFF" } },
fill: { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FF185EE8" } },
alignment: { vertical: "middle" as const, horizontal: "center" as const, wrapText: true },
};
summarySheet.getRow(1).eachCell((cell) => {
Object.assign(cell, headerStyle);
});
detailsSheet.getRow(1).eachCell((cell) => {
Object.assign(cell, headerStyle);
});
summarySheet.getRow(1).height = 26;
detailsSheet.getRow(1).height = 26;
const imageCache = new Map<string, Promise<CartExportImage | null>>();
const getImageAsset = (src: string) => {
const cached = imageCache.get(src);
if (cached) return cached;
const pending = compressCartImage(src);
imageCache.set(src, pending);
return pending;
};
for (const item of cartItems) {
const product = item.product;
const specEntries = getFullSpecEntries(product);
const summaryRow = summarySheet.addRow({
image: product.image ? "" : "暂无图片",
name: product.name,
code: product.code,
category: product.category,
quantity: item.quantity,
detailSpecs: getFullSpecText(product),
});
summaryRow.height = Math.max(142, Math.min(240, 24 + specEntries.length * 15));
summaryRow.eachCell((cell) => {
cell.alignment = { vertical: "middle", wrapText: true };
cell.border = {
top: { style: "thin", color: { argb: "FFDDE6F3" } },
bottom: { style: "thin", color: { argb: "FFDDE6F3" } },
left: { style: "thin", color: { argb: "FFDDE6F3" } },
right: { style: "thin", color: { argb: "FFDDE6F3" } },
};
});
if (product.image) {
const imageAsset = await getImageAsset(product.image);
if (imageAsset) {
const imageId = workbook.addImage(imageAsset);
summarySheet.addImage(imageId, {
tl: { col: 0.1, row: summaryRow.number - 1 + 0.08 },
ext: { width: CART_IMAGE_SIZE_PX, height: CART_IMAGE_SIZE_PX },
});
summaryRow.getCell(1).value = null;
} else {
summaryRow.getCell(1).value = "图片加载失败";
}
}
for (const [label, value] of specEntries.length > 0 ? specEntries : [["暂无参数", ""]]) {
const detailRow = detailsSheet.addRow({
name: product.name,
code: product.code,
category: product.category,
quantity: item.quantity,
label,
value,
});
detailRow.height = Math.max(24, Math.min(60, 18 + Math.ceil(String(value).length / 28) * 15));
detailRow.eachCell((cell) => {
cell.alignment = { vertical: "middle", wrapText: true };
cell.border = {
top: { style: "thin", color: { argb: "FFE5EBF3" } },
bottom: { style: "thin", color: { argb: "FFE5EBF3" } },
left: { style: "thin", color: { argb: "FFE5EBF3" } },
right: { style: "thin", color: { argb: "FFE5EBF3" } },
};
});
}
}
summarySheet.autoFilter = { from: "A1", to: `F${Math.max(1, summarySheet.rowCount)}` };
detailsSheet.autoFilter = { from: "A1", to: `F${Math.max(1, detailsSheet.rowCount)}` };
return workbook;
}
function downloadCartWorkbook(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function isMobileDevice() {
return /Android|iPhone|iPad|iPod|Mobi/i.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
}
/*
type SaveProductRequest = {
product: CatalogProduct;
badge?: string;
@@ -536,6 +746,7 @@ async function renderProductCardPng({ product, badge, subtitle, extraSpecs = []
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("PNG生成失败"))), "image/png");
});
}
*/
export default function Home() {
const [stage, setStage] = useState(0);
@@ -558,8 +769,22 @@ export default function Home() {
const [precisionGearFiltersOpen, setPrecisionGearFiltersOpen] = useState(false);
const [selectedCatalogProduct, setSelectedCatalogProduct] = useState<CatalogProduct | null>(null);
const [catalogVisualMode, setCatalogVisualMode] = useState<CatalogVisualMode>("main");
const [savingProductKey, setSavingProductKey] = useState("");
const [saveProductMessage, setSaveProductMessage] = useState("");
const [cartItems, setCartItems] = useState<CartItem[]>(() => {
if (typeof window === "undefined") return [];
try {
const storedCart = window.localStorage.getItem(CART_STORAGE_KEY);
const parsedCart = storedCart ? JSON.parse(storedCart) as CartItem[] : [];
return Array.isArray(parsedCart) ? parsedCart : [];
} catch {
return [];
}
});
const [cartOpen, setCartOpen] = useState(false);
const [cartMessage, setCartMessage] = useState("");
const [exportingCart, setExportingCart] = useState(false);
const [cartSampleFormOpen, setCartSampleFormOpen] = useState(false);
const [cartSampleSubmitted, setCartSampleSubmitted] = useState(false);
const [cartSampleLeadId, setCartSampleLeadId] = useState("");
const matches = useMemo(() => getMatches(requirements), [requirements]);
const topMatches = matches.slice(0, 3);
@@ -613,6 +838,7 @@ export default function Home() {
const selectedCatalogDimensionImage = selectedCatalogProduct
? getCatalogProductDimensionImage(selectedCatalogProduct)
: null;
const cartCount = cartItems.reduce((total, item) => total + item.quantity, 0);
useEffect(() => {
function updateCatalogToolbarState() {
@@ -642,65 +868,144 @@ export default function Home() {
};
}, []);
useEffect(() => {
try {
window.localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
} catch (error) {
console.warn("保存购物车失败", error);
}
}, [cartItems]);
function openCatalogProduct(product: CatalogProduct) {
setSelectedCatalogProduct(product);
setCatalogVisualMode("main");
}
async function saveProductCard(request: SaveProductRequest) {
if (savingProductKey) return;
const saveKey = `${request.product.id}-${request.badge ?? "product"}`;
setSavingProductKey(saveKey);
setSaveProductMessage("正在生成产品图片…");
function addToCart(product: CatalogProduct, source = product.category) {
setCartItems((current) => {
const existingItem = current.find((item) => item.product.id === product.id);
if (existingItem) {
return current.map((item) => item.product.id === product.id
? { ...item, quantity: item.quantity + 1, source }
: item);
}
return [...current, { product, quantity: 1, source }];
});
setCartMessage(`${product.name} 已加入购物车`);
setCartOpen(true);
window.setTimeout(() => setCartMessage(""), 2200);
}
function addTopMatchesToCart() {
if (topMatches.length === 0) return;
setCartItems((current) => {
const next = [...current];
topMatches.forEach((match) => {
const existingIndex = next.findIndex((item) => item.product.id === match.product.id);
if (existingIndex >= 0) {
const existingItem = next[existingIndex];
next[existingIndex] = {
...existingItem,
quantity: existingItem.quantity + 1,
source: "AI初选结果",
};
} else {
next.push({ product: match.product, quantity: 1, source: "AI初选结果" });
}
});
return next;
});
setCartMessage(`${topMatches.length} 款 AI 推荐型号已加入购物车`);
setCartOpen(true);
window.setTimeout(() => setCartMessage(""), 2400);
}
function updateCartQuantity(productId: string, quantity: number) {
if (quantity <= 0) {
setCartItems((current) => current.filter((item) => item.product.id !== productId));
return;
}
setCartItems((current) => current.map((item) => item.product.id === productId ? { ...item, quantity } : item));
}
function clearCart() {
setCartItems([]);
setCartSampleFormOpen(false);
setCartSampleSubmitted(false);
}
function openCartSampleForm() {
if (cartItems.length === 0) return;
setCartSampleSubmitted(false);
setCartSampleFormOpen(true);
}
function submitCartSampleRequest(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const id = `GDL-S-${new Date().toISOString().slice(5, 10).replace("-", "")}-${String(Date.now()).slice(-4)}`;
setCartSampleLeadId(id);
setCartSampleSubmitted(true);
}
async function saveCartList() {
if (cartItems.length === 0 || exportingCart) return;
setExportingCart(true);
setCartMessage("正在生成产品清单…");
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] }));
const workbook = await buildCartWorkbook(cartItems);
const buffer = await workbook.xlsx.writeBuffer();
const filename = getCartExportFilename();
const file = new File([buffer as BlobPart], filename, { type: CART_EXPORT_MIME });
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;
if (isMobileDevice() && typeof navigator.share === "function") {
const canShareFiles = typeof navigator.canShare !== "function" || navigator.canShare({ files: [file] });
if (canShareFiles) {
try {
await navigator.share({
title: "高徳乐产品清单",
text: "购物车产品清单",
files: [file],
});
setCartMessage("已打开系统分享面板");
return;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
}
}
} 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图片已下载");
}
downloadCartWorkbook(file, filename);
setCartMessage("产品清单已下载");
} catch (error) {
console.error("保存产品参数图片失败", error);
setSaveProductMessage("图片生成失败,请稍后重试");
console.error("生成购物车清单失败", error);
setCartMessage("产品清单生成失败,请稍后重试");
} finally {
setSavingProductKey("");
window.setTimeout(() => setSaveProductMessage(""), 2800);
setExportingCart(false);
window.setTimeout(() => setCartMessage(""), 2400);
}
}
function exportCartPdf() {
if (cartItems.length === 0) return;
const previousTitle = document.title;
document.body.classList.add("printing-cart");
document.title = getCartExportFilename().replace(/\.xlsx$/, ".pdf");
setCartOpen(false);
let fallbackCleanupTimer = 0;
const cleanupPrintMode = () => {
window.clearTimeout(fallbackCleanupTimer);
window.removeEventListener("afterprint", cleanupPrintMode);
document.body.classList.remove("printing-cart");
document.title = previousTitle;
};
window.addEventListener("afterprint", cleanupPrintMode);
fallbackCleanupTimer = window.setTimeout(cleanupPrintMode, 30000);
window.setTimeout(() => {
window.print();
}, 80);
}
function startSelection() {
setMobileMenuOpen(false);
setStage(1);
@@ -731,11 +1036,196 @@ export default function Home() {
return (
<main>
{saveProductMessage && (
<div className="product-save-toast" role="status" aria-live="polite">
{saveProductMessage}
{cartMessage && (
<div className="cart-toast" role="status" aria-live="polite">
{cartMessage}
</div>
)}
{cartOpen && (
<div
className="cart-drawer-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) setCartOpen(false);
}}
>
<aside className="cart-drawer" role="dialog" aria-modal="true" aria-labelledby="cart-title">
<div className="cart-drawer-header">
<div>
<p className="step-kicker"></p>
<h2 id="cart-title"> <span>{cartCount}</span></h2>
</div>
<button className="cart-close-button" type="button" onClick={() => setCartOpen(false)} aria-label="关闭购物车">×</button>
</div>
{cartItems.length > 0 ? (
cartSampleFormOpen ? (
<div className="cart-sample-scroll">
{!cartSampleSubmitted ? (
<form className="sample-form cart-sample-form" onSubmit={submitCartSampleRequest}>
<div>
<p className="step-kicker"></p>
<h3></h3>
<p> {cartItems.length} {cartCount} </p>
</div>
<div className="cart-sample-products">
{cartItems.map((item) => (
<div key={`sample-${item.product.id}`}>
<span>{item.product.name}</span>
<code>{item.product.code} · {item.quantity}</code>
</div>
))}
</div>
<div className="form-grid">
<label><span></span><input required name="company" placeholder="请输入公司名称" /></label>
<label><span></span><input required name="name" placeholder="姓名" /></label>
<label><span></span><input required name="phone" inputMode="tel" placeholder="用于样品确认" /></label>
<label><span></span><input name="email" type="email" placeholder="接收申请结果" /></label>
</div>
<label className="full-field"><span></span><textarea name="notes" placeholder="收货信息、使用场景、测试时间或其他要求" /></label>
<div className="cart-sample-form-actions">
<button className="outline-button" type="button" onClick={() => setCartSampleFormOpen(false)}></button>
<button className="panel-primary" type="submit"> <span></span></button>
</div>
<small className="demo-disclaimer"></small>
</form>
) : (
<div className="success-card cart-sample-success">
<div className="success-icon"></div>
<p className="step-kicker"></p>
<h3> {cartSampleLeadId}</h3>
<p> {cartCount} 线</p>
<button type="button" className="outline-button" onClick={() => setCartSampleFormOpen(false)}></button>
</div>
)}
</div>
) : (
<>
<div className="cart-list">
{cartItems.map((item) => (
<div className="cart-item" key={item.product.id}>
<div className="cart-item-image">
{item.product.image ? <img src={item.product.image} alt="" /> : <span>G</span>}
</div>
<div className="cart-item-copy">
<small>{item.source}</small>
<strong>{item.product.name}</strong>
<code>{item.product.code}</code>
<div className="cart-item-actions">
<div className="quantity-control" aria-label={`${item.product.name}数量`}>
<button type="button" onClick={() => updateCartQuantity(item.product.id, item.quantity - 1)} aria-label="减少数量"></button>
<span>{item.quantity}</span>
<button type="button" onClick={() => updateCartQuantity(item.product.id, item.quantity + 1)} aria-label="增加数量"></button>
</div>
<button className="cart-remove-button" type="button" onClick={() => updateCartQuantity(item.product.id, 0)}></button>
</div>
</div>
</div>
))}
</div>
<div className="cart-drawer-footer">
<p> {cartCount} </p>
<div>
<button className="outline-button" type="button" onClick={clearCart}></button>
<button
className="outline-button cart-export-button"
type="button"
onClick={saveCartList}
disabled={cartItems.length === 0 || exportingCart}
aria-label={exportingCart ? "正在生成产品清单" : "保存购物车产品列表"}
>
{exportingCart ? "生成中…" : "保存列表"}
</button>
<button
className="outline-button cart-pdf-button"
type="button"
onClick={exportCartPdf}
disabled={cartItems.length === 0}
aria-label="导出购物车 PDF"
>
PDF
</button>
<button className="panel-primary" type="button" onClick={() => { setCartOpen(false); startSelection(); }}> <span></span></button>
<button className="panel-primary cart-sample-button" type="button" onClick={openCartSampleForm}> <span></span></button>
</div>
</div>
</>
)
) : (
<div className="cart-empty">
<span>🛒</span>
<strong></strong>
<p> AI </p>
<div className="cart-empty-actions">
<button className="panel-primary" type="button" onClick={() => setCartOpen(false)}> <span></span></button>
<button className="outline-button cart-export-button" type="button" onClick={saveCartList} disabled aria-label="购物车为空,保存列表不可用"></button>
<button className="outline-button cart-pdf-button" type="button" onClick={exportCartPdf} disabled aria-label="购物车为空,导出 PDF 不可用"> PDF</button>
<button className="panel-primary cart-sample-button" type="button" onClick={openCartSampleForm} disabled aria-label="购物车为空,购买样品申请不可用"> <span></span></button>
</div>
</div>
)}
</aside>
</div>
)}
<section className="cart-print-sheet" aria-hidden="true">
<div className="cart-print-header">
<div>
<p>GAUDELOT · </p>
<h1></h1>
<span> {cartItems.length} / {cartCount} </span>
</div>
<strong>{getCartExportFilename().replace(/\.xlsx$/, ".pdf")}</strong>
</div>
<table className="cart-print-summary">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{cartItems.map((item) => (
<tr key={`print-summary-${item.product.id}`}>
<td className="cart-print-image-cell">
{item.product.image ? <img src={item.product.image} alt={item.product.name} /> : "暂无图片"}
</td>
<td>{item.product.name}</td>
<td>{item.product.code}</td>
<td>{item.product.category}</td>
<td>{item.quantity}</td>
<td className="cart-print-preline">{getKeySpecText(item.product)}</td>
<td>{item.product.specs["产品尺寸"] ?? item.product.specs["外形尺寸"] ?? "—"}</td>
</tr>
))}
</tbody>
</table>
<h2></h2>
<div className="cart-print-details">
{cartItems.map((item) => (
<section className="cart-print-product" key={`print-detail-${item.product.id}`}>
<div className="cart-print-product-heading">
<div>
<h3>{item.product.name}</h3>
<span>{item.product.code} · {item.product.category} · {item.quantity}</span>
</div>
</div>
<div className="cart-print-spec-grid">
{getFullSpecEntries(item.product).map(([label, value]) => (
<div key={`${item.product.id}-${label}`}>
<span>{label}</span>
<strong>{value}</strong>
</div>
))}
</div>
</section>
))}
</div>
<p className="cart-print-footer"> AI · </p>
</section>
<header className="site-header">
<a className="brand" href="#top" aria-label="高德乐 AI选型首页">
<span className="brand-wordmark">
@@ -750,6 +1240,11 @@ export default function Home() {
<a href="#how" onClick={() => setMobileMenuOpen(false)}></a>
</nav>
<div className="header-actions">
<button className="cart-header-button" type="button" onClick={() => setCartOpen(true)} aria-label={`打开购物车,当前${cartCount}件产品`}>
<span aria-hidden="true">🛒</span>
<small></small>
<b>{cartCount}</b>
</button>
<button className="header-cta" type="button" onClick={startSelection}>
<span></span>
</button>
@@ -1185,17 +1680,12 @@ export default function Home() {
<small></small>
</div>
<button
className={`product-save-button ${savingProductKey.startsWith(`${match.product.id}-`) ? "is-saving" : ""}`}
className="add-cart-button"
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}产品参数图片`}
onClick={() => addToCart(match.product, "AI初选结果")}
aria-label={`${match.product.name}加入购物车`}
>
{savingProductKey.startsWith(`${match.product.id}-`) ? "正在生成…" : "保存产品参数图片"} <span></span>
<span></span>
</button>
</div>
</article>
@@ -1216,7 +1706,7 @@ export default function Home() {
<button className="panel-primary" type="button" onClick={() => setShowSampleForm(true)}>
<span></span>
</button>
<button className="outline-button" type="button" onClick={() => window.print()}> / </button>
<button className="outline-button" type="button" onClick={addTopMatchesToCart}></button>
</div>
)}
@@ -1385,16 +1875,12 @@ export default function Home() {
<span></span>
</button>
<button
className={`product-save-button ${savingProductKey === `${product.id}-${product.category}` ? "is-saving" : ""}`}
className="add-cart-button"
type="button"
onClick={() => saveProductCard({
product,
badge: product.category,
subtitle: "完整产品参数",
})}
aria-label={`保存${product.name}产品参数图片`}
onClick={() => addToCart(product)}
aria-label={`${product.name}加入购物车`}
>
{savingProductKey === `${product.id}-${product.category}` ? "正在生成…" : "保存产品参数图片"} <span></span>
<span></span>
</button>
</div>
</article>
@@ -1493,17 +1979,12 @@ export default function Home() {
))}
</dl>
<button
className={`product-save-button ${savingProductKey === `${product.id}-${product.subCategory}` ? "is-saving" : ""}`}
className="add-cart-button"
type="button"
onClick={() => saveProductCard({
product,
badge: "精密齿轮配件",
subtitle: product.subCategory,
extraSpecs: [["产品子类", product.subCategory]],
})}
aria-label={`保存${product.name}产品参数图片`}
onClick={() => addToCart(product, "精密齿轮配件")}
aria-label={`${product.name}加入购物车`}
>
{savingProductKey === `${product.id}-${product.subCategory}` ? "正在生成…" : "保存产品参数图片"} <span></span>
<span></span>
</button>
</div>
</article>
@@ -1611,16 +2092,12 @@ export default function Home() {
AI继续确认需求 <span></span>
</button>
<button
className={`product-save-button modal-save-button ${savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "is-saving" : ""}`}
className="add-cart-button modal-cart-button"
type="button"
onClick={() => saveProductCard({
product: selectedCatalogProduct,
badge: selectedCatalogProduct.category,
subtitle: "完整产品参数",
})}
aria-label={`保存${selectedCatalogProduct.name}产品参数图片`}
onClick={() => addToCart(selectedCatalogProduct)}
aria-label={`${selectedCatalogProduct.name}加入购物车`}
>
{savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "正在生成…" : "保存产品参数图片"} <span></span>
<span></span>
</button>
<p></p>
</div>