Upload latest code and optimized prompts (v6)

This commit is contained in:
tony
2025-12-14 19:52:54 +08:00
commit f5cb1042ae
42 changed files with 15302 additions and 0 deletions

845
public/index.html Normal file
View File

@@ -0,0 +1,845 @@
<!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>