595 lines
17 KiB
HTML
595 lines
17 KiB
HTML
<!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="输入正文… - 列表用 - 开头 ## 小标题用 ## 开头 **加粗** 用双星号 语言驱动排版 · 零成本 · 无需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>
|