360 lines
17 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>封面工作室 · 语言驱动设计系统</title>
<style>
:root {
--bg: #0a0e1a;
--panel: #11162a;
--border: #1e2640;
--border-h: #2d3655;
--text: #c5cee0;
--text-dim: #6b7280;
--accent: #3b82f6;
--accent-glow: #60a5fa;
--green: #10b981;
--gold: #f59e0b;
--purple: #8b5cf6;
--code-bg: #0d1117;
--radius: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, 'SF Pro Display', 'PingFang SC', sans-serif; min-height: 100vh; line-height: 1.6; }
header {
text-align: center; padding: 36px 20px 24px;
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 30px; font-weight: 700;
background: linear-gradient(90deg, var(--accent), var(--accent-glow));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
margin-bottom: 6px;
}
header p { font-size: 13px; color: var(--text-dim); letter-spacing: 1px; }
.main { display: grid; grid-template-columns: 1fr 1fr; gap: 0; height: calc(100vh - 120px); }
@media (max-width: 900px) { .main { grid-template-columns: 1fr; height: auto; } }
/* Left Panel */
.left { background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
.left-hdr {
padding: 14px 18px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
}
.left-hdr .t { font-size: 12px; font-weight: 600; letter-spacing: 1px; color: var(--text-dim); }
.left-hdr .s { font-size: 11px; color: var(--green); display: flex; align-items: center; gap: 6px; }
.left-hdr .s::before { content: ''; width: 5px; height: 5px; background: var(--green); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
.code-area { flex: 1; overflow-y: auto; padding: 18px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 12.5px; background: var(--code-bg); }
.code-block { margin-bottom: 6px; }
.c-num { color: #4a5568; display: inline-block; width: 32px; user-select: none; text-align: right; margin-right: 8px; }
.c-cmt { color: #636e7b; }
.c-kw { color: var(--purple); }
.c-str { color: var(--green); }
.c-fn { color: var(--accent); }
.c-nr { color: var(--gold); }
.c-hl { color: var(--accent-glow); }
.input-panel { padding: 16px 18px; border-top: 1px solid var(--border); background: var(--panel); }
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.input-panel select, .input-panel input { padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 13px; outline: none; width: 100%; }
.input-panel select:focus, .input-panel input:focus { border-color: var(--accent); }
.input-panel textarea { width: 100%; padding: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 13px; line-height: 1.7; resize: vertical; min-height: 100px; outline: none; font-family: inherit; }
.input-panel textarea:focus { border-color: var(--accent); }
.gen-btn { width: 100%; padding: 13px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 10px; transition: all 0.2s; letter-spacing: 1px; }
.gen-btn:hover { background: var(--accent-glow); }
.gen-btn:disabled { opacity: 0.5; cursor: not-allowed; background: var(--accent); }
/* Right Panel */
.right { background: var(--bg); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; position: relative; overflow-y: auto; }
.design-id-badge { position: absolute; top: 20px; left: 20px; background: var(--panel); padding: 10px 16px; border-radius: var(--radius); border: 1px solid var(--border); display: none; z-index: 2; }
.design-id-badge .l { font-size: 10px; color: var(--text-dim); letter-spacing: 1px; }
.design-id-badge .v { font-size: 13px; font-weight: 600; color: var(--accent); font-family: 'SF Mono', monospace; }
.empty-state { text-align: center; color: var(--text-dim); }
.empty-state .icon { font-size: 56px; margin-bottom: 16px; opacity: 0.25; }
.empty-state p { font-size: 14px; line-height: 2; }
.loading { display: none; text-align: center; padding: 40px; }
.loading.show { display: block; }
.loading .dots span { display: inline-block; width: 7px; height: 7px; background: var(--accent); border-radius: 50%; margin: 0 3px; animation: bounce 1.4s infinite; }
.loading .dots span:nth-child(1) { animation-delay: -0.32s; }
.loading .dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce { 0%,80%,100% { transform: scale(0); } 40% { transform: scale(1); } }
.preview-wrap { position: relative; border-radius: 12px; overflow: hidden; box-shadow: 0 16px 48px rgba(0,0,0,0.5); border: 1px solid var(--border); display: none; }
.preview-wrap.show { display: block; }
.preview-wrap img { max-width: 100%; max-height: 60vh; display: block; }
.watermark {
position: absolute; bottom: 14px; right: 14px;
background: rgba(0,0,0,0.75); padding: 6px 14px;
border-radius: 20px; font-size: 11px; font-family: 'SF Mono', monospace;
color: var(--accent-glow); border: 1px solid rgba(59,130,246,0.4);
letter-spacing: 1px;
}
.dl-bar { display: none; gap: 10px; margin-top: 20px; }
.dl-bar.show { display: flex; }
.dl-btn { padding: 11px 22px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); text-decoration: none; font-size: 13px; transition: all 0.2s; cursor: pointer; }
.dl-btn:hover { border-color: var(--accent); color: var(--accent); }
.dl-btn.primary { background: var(--accent); color: white; border-color: var(--accent); }
.exec-log { display: none; width: 100%; max-width: 600px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-top: 16px; }
.exec-log.show { display: block; }
.exec-log .t { font-size: 11px; color: var(--text-dim); letter-spacing: 1px; margin-bottom: 12px; }
.exec-row { display: flex; gap: 10px; padding: 7px 0; font-size: 12px; border-bottom: 1px solid rgba(30,38,64,0.5); }
.exec-row:last-child { border-bottom: none; }
.exec-row .n { color: var(--text-dim); font-family: 'SF Mono', monospace; min-width: 24px; }
.exec-row .act { color: var(--accent); min-width: 80px; font-weight: 500; }
.exec-row .det { color: var(--text-dim); flex: 1; }
.error-msg { display: none; padding: 12px; background: rgba(255,71,87,0.08); border: 1px solid rgba(255,71,87,0.2); border-radius: var(--radius); color: #ff6b7a; font-size: 12px; margin-top: 10px; }
.error-msg.show { display: block; }
</style>
</head>
<body>
<header>
<h1>封面工作室</h1>
<p>语言驱动设计系统 · 意图可追溯 · 设计属于你</p>
</header>
<div class="main">
<!-- Left: Design Logic + Input -->
<div class="left">
<div class="left-hdr">
<span class="t">DESIGN LOGIC</span>
<span class="s">READY</span>
</div>
<div class="code-area" id="code">
<span class="c-num">1</span><span class="c-cmt">// 等待用户输入设计意图...</span>
<span class="c-num">2</span><span class="c-kw">await</span> <span class="c-fn">userInput</span>();
<span class="c-num">3</span>
<span class="c-num">4</span><span class="c-cmt">// 系统将把自然语言翻译成可执行的设计逻辑</span>
<span class="c-num">5</span><span class="c-cmt">// 每步可追溯 · 编号唯一 · 归属性验证</span>
</div>
<div class="input-panel">
<div class="row2">
<select id="preset" onchange="updateCode()">
<option value="tech">科技蓝</option>
<option value="warm">奶油暖调</option>
<option value="minimal">极简黑白</option>
<option value="rose">玫瑰粉调</option>
<option value="green">文艺绿植</option>
</select>
<select id="layout">
<option value="default">标准排版</option>
<option value="hero">大标题封面</option>
<option value="quote">金句居中</option>
</select>
</div>
<div class="row2">
<select id="template">
<option value="xiaohongshu">📕 小红书 1080×1440</option>
</select>
<input type="text" id="tag" placeholder="标签 (可选)" maxlength="20">
</div>
<input type="text" id="title" placeholder="标题" style="width:100%;margin-bottom:10px;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:14px;outline:none;">
<textarea id="body" placeholder="正文...&#10;&#10;- 列表用 - 开头&#10;## 小标题 ##&#10;**加粗**文字"></textarea>
<button class="gen-btn" id="btn" onclick="gen()">执行设计逻辑</button>
<div class="error-msg" id="err"></div>
</div>
</div>
<!-- Right: Preview + Chain -->
<div class="right">
<div class="design-id-badge" id="badge">
<div class="l">DESIGN ID</div>
<div class="v" id="bid">---</div>
</div>
<div class="empty-state" id="empty">
<div class="icon">🎨</div>
<p>在左侧输入设计意图<br>系统翻译成可执行逻辑<br>生成带编号的封面</p>
</div>
<div class="loading" id="loading">
<div class="dots"><span></span><span></span><span></span></div>
<p style="margin-top:18px;color:var(--text-dim);font-size:13px;">编译设计意图...</p>
</div>
<div class="preview-wrap" id="previewBox">
<img id="previewImg" alt="封面">
<div class="watermark" id="wm">---</div>
</div>
<div class="dl-bar" id="dlBar">
<a class="dl-btn primary" id="dl" href="#" download>下载 PNG</a>
<button class="dl-btn" onclick="copyId()">复制设计编号</button>
</div>
<div class="exec-log" id="log">
<div class="t">EXECUTION LOG</div>
<div id="logBody"></div>
</div>
</div>
</div>
<script>
let currentId = '';
const presetNames = { tech: '科技蓝', warm: '奶油暖调', minimal: '极简黑白', rose: '玫瑰粉调', green: '文艺绿植' };
const layoutNames = { default: '标准排版', hero: '大标题封面', quote: '金句居中' };
function updateCode() {
const p = document.getElementById('preset').value;
const t = document.getElementById('title').value.trim() || '待输入';
const b = document.getElementById('body').value.trim();
const l = document.getElementById('layout').value;
const hasList = b.includes('-');
document.getElementById('code').innerHTML =
`<span class="c-num">1</span><span class="c-cmt">// =====================================</span>
<span class="c-num">2</span><span class="c-cmt">// DESIGN ID: ${currentId || '待生成'}</span>
<span class="c-num">3</span><span class="c-cmt">// =====================================</span>
<span class="c-num">4</span>
<span class="c-num">5</span><span class="c-kw">const</span> <span class="c-fn">userIntent</span> = {
<span class="c-num">6</span> title: <span class="c-str">"${t.substring(0,30)}${t.length>30?'...':''}"</span>,
<span class="c-num">7</span> style: <span class="c-str">"${presetNames[p]}"</span>,
<span class="c-num">8</span> layout: <span class="c-str">"${layoutNames[l]}"</span>,
<span class="c-num">9</span> owner: <span class="c-str">"用户"</span>,
<span class="c-num">10</span>};
<span class="c-num">11</span>
<span class="c-num">12</span><span class="c-cmt">// STEP 1: analyzeIntent() — 理解意图</span>
<span class="c-num">13</span><span class="c-kw">const</span> a = <span class="c-fn">analyzeIntent</span>(userIntent);
<span class="c-num">14</span><span class="c-cmt">// → 类型: ${hasList?'列表/教程':'段落文本'}</span>
<span class="c-num">15</span><span class="c-cmt">// → 情绪: ${p==='tech'?'冷静专业':p==='warm'?'温暖亲和':'简约高级'}</span>
<span class="c-num">16</span>
<span class="c-num">17</span><span class="c-cmt">// STEP 2: selectColorScheme() — 匹配配色</span>
<span class="c-num">18</span><span class="c-kw">const</span> cs = <span class="c-str">"${p}"</span>;
<span class="c-num">19</span><span class="c-cmt">// → ${presetNames[p]} · 配色方案匹配情绪</span>
<span class="c-num">20</span>
<span class="c-num">21</span><span class="c-cmt">// STEP 3: specifyLayout() — 确定布局</span>
<span class="c-num">22</span><span class="c-kw">const</span> ly = <span class="c-str">"${l}"</span>;
<span class="c-num">23</span><span class="c-cmt">// → ${layoutNames[l]} ${l==='hero'?'· 大标题视觉冲击':l==='quote'?'· 金句居中聚焦':'· 图文并茂'}</span>
<span class="c-num">24</span>
<span class="c-num">25</span><span class="c-cmt">// STEP 4: render() — 排版引擎执行</span>
<span class="c-num">26</span><span class="c-kw">await</span> <span class="c-fn">renderToImage</span>({
<span class="c-num">27</span> template: <span class="c-str">"xiaohongshu"</span>,
<span class="c-num">28</span> size: <span class="c-nr">1080</span> × <span class="c-nr">1440</span>,
<span class="c-num">29</span> engine: <span class="c-str">"Puppeteer + Chrome"</span>,
<span class="c-num">30</span> format: <span class="c-str">"PNG"</span>
<span class="c-num">31</span>});
<span class="c-num">32</span>
<span class="c-num">33</span><span class="c-cmt">// STEP 5: watermark() — 添加归属编号</span>
<span class="c-num">34</span><span class="c-fn">watermark</span>(<span class="c-hl">"${currentId || '即将生成...'}"</span>);
<span class="c-num">35</span><span class="c-cmt">// ✓ 完成 · 该设计归用户所有</span>`;
}
async function gen() {
const t = document.getElementById('title').value.trim();
const b = document.getElementById('body').value.trim();
const p = document.getElementById('preset').value;
const l = document.getElementById('layout').value;
const tag = document.getElementById('tag').value.trim();
if (!t && !b) { showErr('请输入标题或正文'); return; }
// Reset UI
document.getElementById('err').classList.remove('show');
document.getElementById('empty').style.display = 'none';
document.getElementById('previewBox').classList.remove('show');
document.getElementById('dlBar').classList.remove('show');
document.getElementById('log').classList.remove('show');
document.getElementById('badge').style.display = 'none';
document.getElementById('loading').classList.add('show');
document.getElementById('btn').disabled = true;
// Generate ID
currentId = 'DSG-' + Date.now().toString(36).toUpperCase();
document.getElementById('bid').textContent = currentId;
document.getElementById('wm').textContent = currentId;
updateCode();
try {
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
templateId: 'xiaohongshu',
presetId: p, title: t, body: b, tag,
layout: l,
}),
});
if (!r.ok) { const e = await r.json(); throw new Error(e.error || '生成失败'); }
const d = await r.json();
// Show result
document.getElementById('loading').classList.remove('show');
document.getElementById('previewImg').src = d.url;
document.getElementById('previewBox').classList.add('show');
document.getElementById('dlBar').classList.add('show');
document.getElementById('dl').href = d.url;
document.getElementById('badge').style.display = 'block';
// Show execution log from intent chain
if (d.intent_chain) {
document.getElementById('log').classList.add('show');
document.getElementById('logBody').innerHTML = d.intent_chain.map((s, i) =>
`<div class="exec-row">
<span class="n">${String(i+1).padStart(2,'0')}</span>
<span class="act">${s.step}</span>
<span class="det">${s.detail.substring(0,80)}</span>
</div>`
).join('');
}
// Update code with result
document.getElementById('code').innerHTML = document.getElementById('code').innerHTML
.replace(/即将生成.../g, currentId)
+ `
<span class="c-num">36</span>
<span class="c-num">37</span><span class="c-cmt">// ✅ 设计生成完成</span>
<span class="c-num">38</span><span class="c-cmt">// DESIGN ID: ${currentId}</span>
<span class="c-num">39</span><span class="c-cmt">// 证明: 左侧编号 = 图片上编号 → 属于你</span>`;
} catch (e) {
document.getElementById('loading').classList.remove('show');
document.getElementById('empty').style.display = 'block';
showErr(e.message);
document.getElementById('btn').disabled = false;
} finally {
document.getElementById('btn').disabled = false;
}
}
function showErr(m) { const e = document.getElementById('err'); e.textContent = m; e.classList.add('show'); setTimeout(() => e.classList.remove('show'), 5000); }
function copyId() {
navigator.clipboard.writeText(currentId).then(() => showErr('已复制: ' + currentId));
}
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') gen();
});
// Pre-fill demo
(function() {
document.getElementById('title').value = '零基础手搓AI封面生成器';
document.getElementById('body').value =
'语言驱动设计 · 意图透明 · 编号可追溯\n\n' +
'- 零成本 · 无需GPU\n' +
'- 极轻量 · 一行命令部署\n' +
'- 意图透明 · 每一步可见\n' +
'- 编号唯一 · 设计属于你';
updateCode();
})();
</script>
</body>
</html>