595 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, maximum-scale=1.0, user-scalable=no">
<title>封面工作室 · 语言驱动排版设计</title>
<meta name="description" content="输入文字,自动排版,生成封面。每一步意图透明,设计真正属于你。">
<style>
:root {
--bg: #080d1a;
--surface: #0f1832;
--surface2: #141f40;
--border: #1a2a52;
--border-h: #2a3f72;
--text: #c8d6e5;
--text-dim: #6b7d99;
--accent: #4f8cff;
--accent-dim: #3a6fd9;
--glow: #00d4ff;
--green: #2ed573;
--gold: #ffb347;
--radius: 14px;
--radius-sm: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, 'PingFang SC', 'Noto Sans SC', sans-serif;
min-height: 100vh;
line-height: 1.6;
background-image:
radial-gradient(ellipse at 30% 20%, rgba(79,140,255,0.06) 0%, transparent 50%),
radial-gradient(ellipse at 70% 80%, rgba(0,212,255,0.04) 0%, transparent 50%);
}
/* Header */
.header {
text-align: center;
padding: 56px 24px 36px;
position: relative;
}
.header::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
.header h1 {
font-size: 40px;
font-weight: 800;
background: linear-gradient(135deg, var(--accent) 30%, var(--glow) 70%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 4px;
margin-bottom: 10px;
}
.header .sub {
font-size: 15px;
color: var(--text-dim);
letter-spacing: 2px;
}
.header .badges {
margin-top: 18px;
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.badge {
padding: 5px 16px;
font-size: 12px;
border-radius: 20px;
border: 1px solid var(--border);
color: var(--text-dim);
letter-spacing: 1px;
background: rgba(79,140,255,0.04);
}
.badge.highlight {
border-color: var(--green);
color: var(--green);
background: rgba(46,213,115,0.06);
}
/* Main grid */
.main {
max-width: 1300px;
margin: 0 auto;
padding: 0 28px 100px;
display: grid;
grid-template-columns: 480px 1fr;
gap: 36px;
align-items: start;
}
@media (max-width: 1000px) {
.main { grid-template-columns: 1fr; max-width: 600px; }
}
/* Panel */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px;
position: relative;
overflow: hidden;
}
.panel::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(79,140,255,0.15), transparent);
}
.panel-head {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 3px;
color: var(--text-dim);
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.panel-head::before {
content: '';
display: block;
width: 3px;
height: 14px;
background: var(--accent);
border-radius: 2px;
}
/* Form */
.field { margin-bottom: 18px; }
.field label {
display: block;
font-size: 12px;
color: var(--text-dim);
margin-bottom: 7px;
letter-spacing: 1.5px;
font-weight: 500;
}
.field input, .field textarea, .field select {
width: 100%;
padding: 14px 18px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 15px;
font-family: inherit;
transition: all 0.2s;
outline: none;
}
.field input:focus, .field textarea:focus, .field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(79,140,255,0.08);
}
.field textarea {
min-height: 200px;
resize: vertical;
line-height: 1.8;
}
.field textarea::placeholder, .field input::placeholder { color: rgba(107,125,153,0.4); }
.field select { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7d99' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* Presets */
.preset-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 7px;
margin-bottom: 18px;
}
.preset-dot {
padding: 10px 4px;
border: 2px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
text-align: center;
font-size: 12px;
color: var(--text-dim);
background: var(--surface2);
transition: all 0.2s;
font-family: inherit;
}
.preset-dot:hover { border-color: var(--border-h); color: var(--text); }
.preset-dot.active {
border-color: var(--accent);
color: var(--accent);
background: rgba(79,140,255,0.08);
}
/* Generate btn */
.gen-btn {
width: 100%;
padding: 16px 28px;
background: linear-gradient(135deg, var(--accent), var(--accent-dim));
color: #fff;
border: none;
border-radius: var(--radius-sm);
font-size: 17px;
font-weight: 700;
cursor: pointer;
letter-spacing: 2px;
transition: all 0.25s;
font-family: inherit;
margin-top: 6px;
position: relative;
overflow: hidden;
}
.gen-btn::after {
content: '';
position: absolute;
top: 0; left: -100%;
width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.gen-btn:hover::after { left: 100%; }
.gen-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(79,140,255,0.25); }
.gen-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
.gen-btn .spinner {
display: none; width: 18px; height: 18px;
border: 2px solid #fff; border-top-color: transparent;
border-radius: 50%; animation: spin 0.6s linear infinite;
margin-right: 10px; vertical-align: middle;
}
.gen-btn.loading .spinner { display: inline-block; }
.gen-btn.loading .txt { display: none; }
@keyframes spin { to { transform: rotate(360deg); } }
.error { display: none; padding: 12px 16px; background: rgba(255,71,87,0.08); border: 1px solid rgba(255,71,87,0.3); border-radius: var(--radius-sm); color: #ff6b7a; font-size: 13px; margin-top: 12px; }
.error.show { display: block; }
/* Preview area */
.preview-wrap {
display: flex;
flex-direction: column;
align-items: center;
min-height: 360px;
justify-content: center;
}
.preview-empty {
text-align: center;
padding: 80px 40px;
}
.preview-empty .icon { font-size: 56px; margin-bottom: 18px; opacity: 0.25; }
.preview-empty .hint { font-size: 15px; color: var(--text-dim); line-height: 2; }
.preview-img {
max-width: 100%;
max-height: 520px;
border-radius: 10px;
border: 1px solid var(--border);
display: none;
box-shadow: 0 8px 40px rgba(0,0,0,0.4);
}
.preview-img.show { display: block; }
.loading-dots { display: none; gap: 8px; justify-content: center; padding: 80px 40px; }
.loading-dots.show { display: flex; }
.loading-dots span { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: dot 1.2s ease-in-out infinite; }
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes dot { 0%,80%,100% { transform: scale(0.5); opacity: 0.2; } 40% { transform: scale(1); opacity: 1; } }
/* Download bar */
.dl-bar { display: none; margin-top: 20px; width: 100%; text-align: center; }
.dl-bar.show { display: block; }
.dl-btn {
display: inline-block;
padding: 12px 32px;
background: var(--surface2);
color: var(--accent);
border: 1px solid var(--accent);
border-radius: var(--radius-sm);
text-decoration: none;
font-size: 14px;
font-weight: 600;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.2s;
}
.dl-btn:hover { background: rgba(79,140,255,0.1); }
/* Intent Chain */
.intent-chain {
margin-top: 28px;
display: none;
}
.intent-chain.show { display: block; }
.intent-chain .title {
font-size: 13px;
font-weight: 700;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.intent-chain .title::before {
content: '';
display: block;
width: 3px;
height: 14px;
background: var(--green);
border-radius: 2px;
}
.step-card {
display: flex;
gap: 14px;
padding: 14px 0;
position: relative;
}
.step-card:not(:last-child)::after {
content: '';
position: absolute;
left: 17px;
top: 46px;
width: 1px;
height: calc(100% - 32px);
background: linear-gradient(to bottom, var(--border), transparent);
}
.step-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--surface2);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
border: 1px solid var(--border);
}
.step-icon.user { border-color: var(--green); background: rgba(46,213,115,0.06); }
.step-icon.system { border-color: var(--accent); background: rgba(79,140,255,0.04); }
.step-body { flex: 1; min-width: 0; }
.step-body .step-who { font-size: 11px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px; }
.step-body .step-who.user { color: var(--green); }
.step-body .step-who.system { color: var(--accent); }
.step-body .step-label { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.step-body .step-detail { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
.step-body .step-why { font-size: 12px; color: rgba(107,125,153,0.5); margin-top: 4px; font-style: italic; }
/* Signature */
.signature {
text-align: center;
padding: 24px 0 0;
font-size: 14px;
color: var(--green);
letter-spacing: 2px;
font-weight: 600;
display: none;
}
.signature.show { display: block; }
/* Footer */
.footer {
text-align: center;
padding: 40px 24px;
font-size: 13px;
color: var(--text-dim);
opacity: 0.4;
letter-spacing: 2px;
}
.footer a { color: var(--accent); text-decoration: none; }
.footer .sep { margin: 0 10px; opacity: 0.3; }
</style>
</head>
<body>
<header class="header">
<h1>封面工作室</h1>
<p class="sub">语言驱动排版设计 · 输入文字,自动生成封面</p>
<div class="badges">
<span class="badge highlight">开源免费</span>
<span class="badge">无需 GPU</span>
<span class="badge">意图透明</span>
<span class="badge">设计属于你</span>
</div>
</header>
<div class="main">
<!-- 左侧:输入面板 -->
<div class="panel" id="formPanel">
<div class="panel-head">内容输入</div>
<div class="field">
<label for="tpl">模板</label>
<select id="tpl"></select>
</div>
<div class="field">
<label>风格预设</label>
<div class="preset-row" id="presets"></div>
</div>
<div class="row2">
<div class="field">
<label for="layout">布局</label>
<select id="layout">
<option value="default">标准排版</option>
<option value="hero">大标题封面</option>
<option value="quote">金句居中</option>
</select>
</div>
<div class="field">
<label for="tag">标签</label>
<input type="text" id="tag" placeholder="教程 · 干货" maxlength="20">
</div>
</div>
<div class="field">
<label for="title">标题</label>
<input type="text" id="title" placeholder="标题将作为封面大标题" maxlength="60">
</div>
<div class="field">
<label for="body">正文(支持 Markdown</label>
<textarea id="body" placeholder="输入正文…&#10;&#10;- 列表用 - 开头&#10;## 小标题用 ## 开头&#10;**加粗** 用双星号&#10;&#10;语言驱动排版 · 零成本 · 无需GPU"></textarea>
</div>
<button class="gen-btn" id="genBtn" onclick="go()">
<span class="spinner"></span>
<span class="txt">生成封面</span>
</button>
<div class="error" id="err"></div>
</div>
<!-- 右侧:预览 + 意图链 -->
<div>
<div class="panel" id="previewPanel">
<div class="panel-head">预览</div>
<div class="preview-wrap">
<div class="preview-empty" id="empty">
<div class="icon">🎨</div>
<div class="hint">输入文字 · 选择风格<br>点击生成</div>
</div>
<div class="loading-dots" id="loading"><span></span><span></span><span></span></div>
<img class="preview-img" id="preview" alt="封面预览">
<div class="dl-bar" id="dlBar">
<a class="dl-btn" id="dlLink" href="#" download>下载 PNG</a>
</div>
<div class="signature" id="sig">✨ 设计属于你 · 系统只是你的手</div>
</div>
</div>
<div class="panel intent-chain" id="chain">
<div class="title">意图链 · 设计你的</div>
<div id="chainBody"></div>
</div>
</div>
</div>
<footer class="footer">
铸渊封面工作室 v2.1
<span class="sep">·</span>
<a href="https://guanghulab.com/code/bingshuo/guanghulab" target="_blank">开源仓库</a>
<span class="sep">·</span>
MIT
</footer>
<script>
let tpls = [], curTpl = null, curPreset = 'tech'
async function init() {
try {
const r = await fetch('/api/templates')
tpls = await r.json()
const sel = document.getElementById('tpl')
sel.innerHTML = tpls.map(t => `<option value="${t.id}">${t.icon} ${t.name}</option>`).join('')
sel.onchange = () => pick(sel.value)
pick(tpls[0]?.id)
document.getElementById('title').value = '零基础手搓AI封面生成器'
document.getElementById('body').value =
'语言驱动排版 · 意图透明 · 设计属于你\n\n' +
'- 零成本 纯 HTML/CSS 排版渲染\n' +
'- 极轻量 2GB 内存一行命令部署\n' +
'- 语言驱动 输入文字自动分析推荐\n' +
'- 意图透明 每一步推理可见\n' +
'- 设计属于你 署名 @冰朔'
} catch (e) {
console.error(e)
document.getElementById('err').textContent = '连接服务器失败'
document.getElementById('err').classList.add('show')
}
}
function pick(id) {
curTpl = tpls.find(t => t.id === id)
curPreset = curTpl?.presets?.[0]?.id || 'tech'
document.getElementById('presets').innerHTML = (curTpl?.presets || []).map(p =>
`<button class="preset-dot ${p.id === curPreset ? 'active' : ''}" onclick="setPreset('${p.id}')">${p.name}</button>`
).join('')
}
function setPreset(id) { curPreset = id; pick(curTpl.id) }
async function go() {
const title = document.getElementById('title').value.trim()
const body = document.getElementById('body').value.trim()
const tag = document.getElementById('tag').value.trim()
const layout = document.getElementById('layout').value
if (!title && !body) {
const e = document.getElementById('err'); e.textContent = '请输入标题或正文'; e.classList.add('show')
return
}
document.getElementById('err').classList.remove('show')
document.getElementById('empty').style.display = 'none'
document.getElementById('preview').classList.remove('show')
document.getElementById('dlBar').classList.remove('show')
document.getElementById('sig').classList.remove('show')
document.getElementById('chain').classList.remove('show')
document.getElementById('loading').classList.add('show')
document.getElementById('genBtn').classList.add('loading')
document.getElementById('genBtn').disabled = true
try {
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: curTpl?.id, presetId: curPreset, title, body, tag, layout }),
})
if (!r.ok) { const e = await r.json(); throw new Error(e.error || '生成失败') }
const d = await r.json()
document.getElementById('loading').classList.remove('show')
document.getElementById('empty').style.display = 'none'
const img = document.getElementById('preview')
img.src = d.url
img.classList.add('show')
document.getElementById('dlBar').classList.add('show')
document.getElementById('dlLink').href = d.url
document.getElementById('sig').classList.add('show')
// 意图链
if (d.intent_chain) {
document.getElementById('chain').classList.add('show')
document.getElementById('chainBody').innerHTML = d.intent_chain.map(s =>
`<div class="step-card">
<div class="step-icon ${s.who === '用户' ? 'user' : 'system'}">${s.icon}</div>
<div class="step-body">
<div class="step-who ${s.who === '用户' ? 'user' : 'system'}">${s.who}</div>
<div class="step-label">${s.step}</div>
<div class="step-detail">${s.detail}</div>
${s.why ? `<div class="step-why">→ ${s.why}</div>` : ''}
</div>
</div>`
).join('')
}
} catch (e) {
document.getElementById('loading').classList.remove('show')
document.getElementById('empty').style.display = 'block'
const err = document.getElementById('err')
err.textContent = e.message
err.classList.add('show')
} finally {
document.getElementById('genBtn').classList.remove('loading')
document.getElementById('genBtn').disabled = false
}
}
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') go()
})
init()
</script>
</body>
</html>