Files
amz-pic-flow/public/index.html

846 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Touchdog AI 智能电商出图系统</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--primary-color: #E60012;
--secondary-color: #c5000f;
--accent-color: #FF6B6B;
--bg-light: #fafafa;
--text-dark: #333;
}
body {
background: linear-gradient(135deg, #fff5f5 0%, #fafafa 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-height: 100vh;
}
.main-container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.brand-header {
background: linear-gradient(135deg, var(--primary-color) 0%, #ff4444 100%);
color: white;
padding: 2rem;
border-radius: 16px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(230, 0, 18, 0.2);
}
.brand-logo { font-size: 2rem; font-weight: bold; }
.card {
border: none;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 1.5rem;
transition: all 0.3s;
overflow: hidden;
}
.card:hover {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.card-header-custom {
background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%);
border-bottom: 2px solid var(--primary-color);
padding: 1rem 1.5rem;
}
.step-badge {
background: var(--primary-color);
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
margin-right: 0.75rem;
}
.btn-primary {
background: var(--primary-color);
border-color: var(--primary-color);
border-radius: 8px;
padding: 0.6rem 1.5rem;
font-weight: 500;
}
.btn-primary:hover {
background: var(--secondary-color);
border-color: var(--secondary-color);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background: var(--primary-color);
border-color: var(--primary-color);
}
.json-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
background: #1e1e1e;
color: #d4d4d4;
border-radius: 8px;
border: none;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.image-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
transition: all 0.3s;
}
.image-card:hover {
transform: scale(1.02);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.image-card img {
width: 100%;
height: 200px;
object-fit: cover;
cursor: pointer;
}
.image-card .card-body {
padding: 1rem;
}
.image-type-badge {
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
}
.badge-main { background: #4CAF50; }
.badge-aplus { background: #2196F3; }
.progress-ring {
width: 120px;
height: 120px;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
}
.tab-content { padding-top: 1.5rem; }
.nav-tabs .nav-link {
color: var(--text-dark);
border: none;
padding: 0.75rem 1.5rem;
font-weight: 500;
}
.nav-tabs .nav-link.active {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
background: transparent;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}
.status-success { background: #4CAF50; }
.status-failed { background: #f44336; }
.status-pending { background: #ff9800; }
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 1050; }
.ref-image-preview {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
border: 2px solid #ddd;
}
.ref-image-preview:hover {
border-color: var(--primary-color);
}
</style>
</head>
<body>
<div class="main-container">
<!-- Brand Header -->
<div class="brand-header">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="brand-logo">
<i class="bi bi-stars"></i> Touchdog AI 智能电商出图系统
</div>
<p class="mb-0 mt-2 opacity-75">输入SKU数据 → Brain智能规划 → 自动生成12张专业电商图</p>
</div>
<div class="text-end">
<div class="badge bg-light text-dark px-3 py-2">
<i class="bi bi-cpu"></i> Gemini 3 Pro Image
</div>
</div>
</div>
</div>
<!-- Main Tabs -->
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#skuTab">
<i class="bi bi-box-seam"></i> SKU智能出图
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#legacyTab">
<i class="bi bi-image"></i> 传统模式
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#historyTab">
<i class="bi bi-clock-history"></i> 历史记录
</button>
</li>
</ul>
<div class="tab-content">
<!-- SKU智能出图 Tab -->
<div class="tab-pane fade show active" id="skuTab">
<div class="row">
<!-- 左侧SKU输入 -->
<div class="col-lg-5">
<div class="card">
<div class="card-header-custom">
<span class="step-badge">1</span>
<strong>输入SKU数据</strong>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0 fw-bold">SKU JSON数据</label>
<button class="btn btn-sm btn-outline-secondary" onclick="loadSampleSKU()">
<i class="bi bi-file-earmark-code"></i> 加载示例
</button>
</div>
<textarea class="form-control json-editor" id="skuInput" rows="18"
placeholder='{"sku_id": "...", "product_name": "...", ...}'></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-bold">参考图片URLs</label>
<div id="refImagesPreview" class="d-flex gap-2 flex-wrap mb-2"></div>
<small class="text-muted">在SKU JSON中设置 ref_images 字段</small>
</div>
<button class="btn btn-primary w-100" id="generateSkuBtn" onclick="generateSKU()">
<i class="bi bi-magic"></i> 开始智能生图
</button>
</div>
</div>
<!-- Brain分析结果 -->
<div class="card" id="analysisCard" style="display:none;">
<div class="card-header-custom">
<span class="step-badge">2</span>
<strong>Brain分析结果</strong>
</div>
<div class="card-body">
<div id="analysisContent"></div>
</div>
</div>
</div>
<!-- 右侧:生成结果 -->
<div class="col-lg-7">
<div class="card">
<div class="card-header-custom d-flex justify-content-between align-items-center">
<div>
<span class="step-badge">3</span>
<strong>生成结果</strong>
<span class="badge bg-secondary ms-2" id="resultCount">0/12</span>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="downloadAllImages()" id="downloadAllBtn" disabled>
<i class="bi bi-download"></i> 批量下载
</button>
</div>
<div class="card-body">
<div id="generationProgress" style="display:none;" class="mb-4">
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-danger" id="progressBar" style="width: 0%"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<small class="text-muted" id="progressText">准备中...</small>
<small class="text-muted" id="progressStats"></small>
</div>
</div>
<div id="imageResults" class="image-grid">
<div class="text-center text-muted py-5">
<i class="bi bi-images" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3">输入SKU数据后点击"开始智能生图"</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 传统模式 Tab -->
<div class="tab-pane fade" id="legacyTab">
<div class="card p-4">
<div class="card-header-custom mb-3">
<span class="step-badge">1</span>
<strong>素材与需求录入</strong>
</div>
<div class="row g-4">
<div class="col-md-4">
<label class="form-label fw-bold">参考图片上传 (R2存储)</label>
<div class="border rounded p-3 bg-light text-center" style="border-style: dashed !important; cursor: pointer;" onclick="document.getElementById('imageUpload').click()">
<i class="bi bi-cloud-upload fs-1 text-muted"></i>
<p class="text-muted mb-0">点击或拖拽上传图片</p>
<input type="file" id="imageUpload" multiple accept="image/*" class="d-none" onchange="handleFileSelect(this)">
</div>
<div id="uploadPreview" class="d-flex flex-wrap gap-2 mt-2"></div>
<button class="btn btn-outline-primary w-100 mt-3" id="uploadBtn" onclick="uploadImages()">
<i class="bi bi-upload"></i> 开始上传
</button>
</div>
<div class="col-md-8">
<div class="mb-3">
<label class="form-label fw-bold">产品介绍</label>
<textarea class="form-control" id="productIntro" rows="3" placeholder="例如:这是一款超轻伊丽莎白圈,防水易清洁..."></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-bold">图片设计要求</label>
<textarea class="form-control" id="designReqs" rows="3" placeholder="例如需要4张主图纯白背景展示细节..."></textarea>
</div>
</div>
</div>
<hr class="my-4">
<div class="card-header-custom mb-3">
<span class="step-badge">2</span>
<strong>生成提示词</strong>
</div>
<button class="btn btn-primary mb-3" id="genPromptsBtn" onclick="generatePrompts()">
<i class="bi bi-chat-right-quote"></i> 调用大模型生成提示词
</button>
<textarea class="form-control json-editor" id="promptsJson" rows="6" readonly></textarea>
<hr class="my-4">
<div class="card-header-custom mb-3">
<span class="step-badge">3</span>
<strong>生成图片</strong>
</div>
<div class="d-flex gap-2 mb-3">
<button class="btn btn-success flex-grow-1" id="genImagesBtn" onclick="generateImages()">
<i class="bi bi-palette"></i> 开始批量生图
</button>
<button class="btn btn-secondary" onclick="downloadAllLegacy()">
<i class="bi bi-download"></i> 批量下载
</button>
</div>
<div id="legacyImageResults" class="image-grid"></div>
</div>
</div>
<!-- 历史记录 Tab -->
<div class="tab-pane fade" id="historyTab">
<div class="card">
<div class="card-header-custom d-flex justify-content-between">
<strong><i class="bi bi-clock-history"></i> 生成历史</strong>
<button class="btn btn-sm btn-outline-danger" onclick="clearHistory()">
<i class="bi bi-trash"></i> 清空历史
</button>
</div>
<div class="card-body">
<div id="historyList" class="list-group list-group-flush"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay" style="display:none;">
<div class="spinner-border text-danger" style="width: 4rem; height: 4rem;" role="status"></div>
<h4 class="mt-4" id="loadingTitle">正在生成中...</h4>
<p class="text-muted" id="loadingSubtitle">Brain正在分析产品并规划图片策略</p>
<div class="progress mt-3" style="width: 300px; height: 6px;">
<div class="progress-bar bg-danger" id="loadingProgress" style="width: 0%"></div>
</div>
<small class="text-muted mt-2" id="loadingStatus">初始化...</small>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/axios.min.js"></script>
<script>
// ============================================
// 状态管理
// ============================================
let state = {
currentSKU: null,
currentPlan: null,
generatedImages: [],
uploadedUrls: [],
history: []
};
// ============================================
// 工具函数
// ============================================
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const bgClass = type === 'error' ? 'text-bg-danger' : type === 'warning' ? 'text-bg-warning' : 'text-bg-success';
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center ${bgClass} border-0`;
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
container.appendChild(toastEl);
new bootstrap.Toast(toastEl).show();
setTimeout(() => toastEl.remove(), 4000);
}
function setLoading(show, title = '', subtitle = '', status = '') {
const overlay = document.getElementById('loadingOverlay');
overlay.style.display = show ? 'flex' : 'none';
if (title) document.getElementById('loadingTitle').textContent = title;
if (subtitle) document.getElementById('loadingSubtitle').textContent = subtitle;
if (status) document.getElementById('loadingStatus').textContent = status;
}
function updateLoadingProgress(percent, status) {
document.getElementById('loadingProgress').style.width = percent + '%';
if (status) document.getElementById('loadingStatus').textContent = status;
}
// ============================================
// SKU智能出图功能
// ============================================
async function loadSampleSKU() {
try {
const res = await axios.get('/api/sample-sku');
document.getElementById('skuInput').value = JSON.stringify(res.data, null, 2);
updateRefImagesPreview(res.data.ref_images || []);
showToast('示例SKU已加载');
} catch (e) {
showToast('加载示例失败', 'error');
}
}
function updateRefImagesPreview(urls) {
const container = document.getElementById('refImagesPreview');
container.innerHTML = urls.map(url =>
`<img src="${url}" class="ref-image-preview" onclick="window.open('${url}')" alt="参考图">`
).join('');
}
async function generateSKU() {
const skuInput = document.getElementById('skuInput').value.trim();
if (!skuInput) {
showToast('请输入SKU数据', 'error');
return;
}
let sku;
try {
sku = JSON.parse(skuInput);
} catch (e) {
showToast('SKU数据JSON格式错误', 'error');
return;
}
if (!sku.sku_id) {
showToast('SKU数据缺少sku_id字段', 'error');
return;
}
state.currentSKU = sku;
state.generatedImages = [];
// 显示加载状态
setLoading(true, '正在生成中...', 'Brain正在分析产品并规划图片策略', '初始化...');
document.getElementById('generateSkuBtn').disabled = true;
document.getElementById('generationProgress').style.display = 'block';
document.getElementById('imageResults').innerHTML = '';
document.getElementById('analysisCard').style.display = 'none';
try {
// 调用生成API
updateLoadingProgress(5, 'Stage 1: Brain正在规划...');
const response = await axios.post('/api/generate-sku', { sku }, {
timeout: 600000 // 10分钟超时
});
if (response.data.success) {
state.currentPlan = response.data.plan;
state.generatedImages = response.data.results;
// 显示分析结果
showAnalysis(response.data.plan.analysis);
// 显示生成的图片
renderGeneratedImages(response.data.results);
// 更新统计
const successCount = response.data.summary.success;
document.getElementById('resultCount').textContent = `${successCount}/12`;
document.getElementById('downloadAllBtn').disabled = successCount === 0;
// 保存到历史
saveToHistory(sku, response.data.results);
showToast(`生成完成!成功 ${successCount}/12 张图片`);
} else {
throw new Error(response.data.error || '生成失败');
}
} catch (error) {
console.error('Generation error:', error);
showToast('生成失败: ' + (error.response?.data?.error || error.message), 'error');
} finally {
setLoading(false);
document.getElementById('generateSkuBtn').disabled = false;
document.getElementById('generationProgress').style.display = 'none';
}
}
function showAnalysis(analysis) {
const card = document.getElementById('analysisCard');
const content = document.getElementById('analysisContent');
content.innerHTML = `
<div class="row g-3">
<div class="col-6">
<small class="text-muted">产品品类</small>
<div class="fw-bold">${analysis.product_category || '-'}</div>
</div>
<div class="col-6">
<small class="text-muted">背景策略</small>
<div class="fw-bold">${analysis.background_strategy === 'light' ? '浅色背景' : '深色背景'}</div>
</div>
<div class="col-6">
<small class="text-muted">Logo颜色</small>
<div class="fw-bold" style="color: ${analysis.logo_color === 'red' ? '#E60012' : '#333'}">
${analysis.logo_color === 'red' ? '红色' : '白色'}
</div>
</div>
<div class="col-6">
<small class="text-muted">核心卖点</small>
<div class="fw-bold">${(analysis.core_selling_points || []).join(', ')}</div>
</div>
<div class="col-12">
<small class="text-muted">视觉风格</small>
<div>${analysis.visual_style || '-'}</div>
</div>
</div>
`;
card.style.display = 'block';
}
function renderGeneratedImages(results) {
const container = document.getElementById('imageResults');
container.innerHTML = results.map(img => {
const isMain = img.id.startsWith('Main_');
const badgeClass = isMain ? 'badge-main' : 'badge-aplus';
const statusClass = img.status === 'success' ? 'status-success' : 'status-failed';
return `
<div class="image-card">
${img.url ?
`<img src="${img.url}" onclick="window.open('${img.url}')" alt="${img.id}">` :
`<div class="d-flex align-items-center justify-content-center bg-light" style="height:200px">
<i class="bi bi-exclamation-triangle text-danger fs-1"></i>
</div>`
}
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge image-type-badge ${badgeClass}">${img.id}</span>
<span><span class="status-dot ${statusClass}"></span>${img.status === 'success' ? '成功' : '失败'}</span>
</div>
<div class="small text-muted">${img.type || ''}</div>
${img.purpose ? `<div class="small mt-1">${img.purpose}</div>` : ''}
</div>
</div>
`;
}).join('');
}
function downloadAllImages() {
const successImages = state.generatedImages.filter(img => img.status === 'success' && img.url);
if (successImages.length === 0) {
showToast('没有可下载的图片', 'warning');
return;
}
successImages.forEach((img, idx) => {
const link = document.createElement('a');
link.href = img.url;
link.download = `${state.currentSKU?.sku_id || 'image'}-${img.id}.png`;
link.target = '_blank';
document.body.appendChild(link);
setTimeout(() => {
link.click();
document.body.removeChild(link);
}, idx * 500);
});
showToast(`开始下载 ${successImages.length} 张图片`);
}
// ============================================
// 历史记录
// ============================================
function saveToHistory(sku, results) {
const entry = {
id: Date.now(),
date: new Date().toLocaleString(),
sku_id: sku.sku_id,
product_name: sku.product_name,
results: results
};
let history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
history.unshift(entry);
if (history.length > 50) history = history.slice(0, 50);
localStorage.setItem('skuHistory', JSON.stringify(history));
renderHistory();
}
function renderHistory() {
const history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
const container = document.getElementById('historyList');
if (history.length === 0) {
container.innerHTML = '<div class="text-center text-muted py-4">暂无历史记录</div>';
return;
}
container.innerHTML = history.map(entry => {
const successCount = entry.results?.filter(r => r.status === 'success').length || 0;
return `
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>${entry.sku_id}</strong>
<small class="text-muted ms-2">${entry.product_name || ''}</small>
<div class="small text-muted">${entry.date}</div>
</div>
<div>
<span class="badge bg-success">${successCount}/12</span>
<button class="btn btn-sm btn-outline-primary ms-2" onclick="loadHistoryEntry(${entry.id})">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
`;
}).join('');
}
function loadHistoryEntry(id) {
const history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
const entry = history.find(h => h.id === id);
if (entry) {
state.generatedImages = entry.results;
renderGeneratedImages(entry.results);
const successCount = entry.results.filter(r => r.status === 'success').length;
document.getElementById('resultCount').textContent = `${successCount}/12`;
document.getElementById('downloadAllBtn').disabled = successCount === 0;
// 切换到SKU标签页
document.querySelector('[data-bs-target="#skuTab"]').click();
showToast('已加载历史记录');
}
}
function clearHistory() {
if (confirm('确定要清空所有历史记录吗?')) {
localStorage.removeItem('skuHistory');
renderHistory();
showToast('历史记录已清空');
}
}
// ============================================
// 传统模式功能(保留原有功能)
// ============================================
function handleFileSelect(input) {
const container = document.getElementById('uploadPreview');
container.innerHTML = '';
if (input.files) {
Array.from(input.files).forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'ref-image-preview';
container.appendChild(img);
};
reader.readAsDataURL(file);
});
}
}
async function uploadImages() {
const fileInput = document.getElementById('imageUpload');
const files = fileInput.files;
if (!files.length) {
showToast('请先选择文件', 'error');
return;
}
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
document.getElementById('uploadBtn').disabled = true;
const res = await axios.post('/api/upload-r2', formData);
state.uploadedUrls = res.data.urls;
showToast(`成功上传 ${res.data.urls.length} 张图片`);
} catch (e) {
showToast('上传失败', 'error');
} finally {
document.getElementById('uploadBtn').disabled = false;
}
}
async function generatePrompts() {
const intro = document.getElementById('productIntro').value;
const reqs = document.getElementById('designReqs').value;
if (!intro || !reqs) {
showToast('请填写产品介绍和设计要求', 'error');
return;
}
try {
document.getElementById('genPromptsBtn').disabled = true;
const templateRes = await axios.get('/api/prompt-template');
let prompt = templateRes.data.content
.replace('[产品介绍]', intro)
.replace('[图片设计要求]', reqs);
const res = await axios.post('/api/generate-prompts', { prompt });
let content = res.data.data?.choices?.[0]?.message?.content || '';
// 解析JSON
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const prompts = JSON.parse(jsonMatch[1] || jsonMatch[0]);
document.getElementById('promptsJson').value = JSON.stringify(prompts, null, 2);
state.legacyPrompts = prompts;
showToast('提示词生成成功');
}
} catch (e) {
showToast('生成失败', 'error');
} finally {
document.getElementById('genPromptsBtn').disabled = false;
}
}
async function generateImages() {
if (!state.legacyPrompts?.length) {
showToast('请先生成提示词', 'error');
return;
}
const container = document.getElementById('legacyImageResults');
container.innerHTML = '';
state.legacyImages = [];
document.getElementById('genImagesBtn').disabled = true;
for (const item of state.legacyPrompts) {
const promptText = item.ai_prompt || item.prompt;
if (!promptText) continue;
const card = document.createElement('div');
card.className = 'image-card';
card.innerHTML = `
<div class="d-flex align-items-center justify-content-center bg-light" style="height:200px">
<div class="spinner-border text-secondary"></div>
</div>
<div class="card-body">
<div class="small text-muted">${item.type || item.id}</div>
</div>
`;
container.appendChild(card);
try {
const res = await axios.post('/api/generate-image', {
prompt: promptText,
refImages: state.uploadedUrls
});
const imgUrl = res.data.data?.[0]?.url;
if (imgUrl) {
card.querySelector('.bg-light').innerHTML = `<img src="${imgUrl}" onclick="window.open('${imgUrl}')" style="width:100%;height:200px;object-fit:cover;cursor:pointer">`;
state.legacyImages.push({ prompt: promptText, url: imgUrl });
}
} catch (e) {
card.querySelector('.bg-light').innerHTML = `<i class="bi bi-exclamation-triangle text-danger fs-1"></i>`;
}
}
document.getElementById('genImagesBtn').disabled = false;
showToast('图片生成完成');
}
function downloadAllLegacy() {
if (!state.legacyImages?.length) {
showToast('没有可下载的图片', 'warning');
return;
}
state.legacyImages.forEach((img, idx) => {
const link = document.createElement('a');
link.href = img.url;
link.download = `generated-${idx}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
// ============================================
// 初始化
// ============================================
window.onload = () => {
renderHistory();
// 监听SKU输入变化更新参考图预览
document.getElementById('skuInput').addEventListener('input', (e) => {
try {
const sku = JSON.parse(e.target.value);
if (sku.ref_images) {
updateRefImagesPreview(sku.ref_images);
}
} catch {}
});
};
</script>
</body>
</html>