Upload latest code and optimized prompts (v6)
This commit is contained in:
2018
public/css/bootstrap-icons.css
vendored
Normal file
2018
public/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
public/css/bootstrap.min.css
vendored
Normal file
6
public/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/bootstrap-icons.woff
Normal file
BIN
public/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
public/fonts/bootstrap-icons.woff2
Normal file
BIN
public/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
845
public/index.html
Normal file
845
public/index.html
Normal 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>
|
||||
3
public/js/axios.min.js
vendored
Normal file
3
public/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user