feat: 封面工作室UI全面升级 · 暗色科技风 · 意图链步骤卡片展示 · 署名 · 优化排版

This commit is contained in:
bingshuo 2026-05-27 19:05:36 +08:00
parent 801baac9c8
commit 4cc1751dc9

View File

@ -4,21 +4,24 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>封面工作室 · 语言驱动排版设计</title> <title>封面工作室 · 语言驱动排版设计</title>
<meta name="description" content="输入文字,自动排版,生成封面。开源免费无需GPU。"> <meta name="description" content="输入文字,自动排版,生成封面。每一步意图透明,设计真正属于你。">
<style> <style>
:root { :root {
--bg: #0a0e27; --bg: #080d1a;
--surface: #111636; --surface: #0f1832;
--border: #1e2a4a; --surface2: #141f40;
--border: #1a2a52;
--border-h: #2a3f72;
--text: #c8d6e5; --text: #c8d6e5;
--text-muted: #6b7d99; --text-dim: #6b7d99;
--accent: #4f8cff; --accent: #4f8cff;
--accent-glow: #00d4ff; --accent-dim: #3a6fd9;
--danger: #ff4757; --glow: #00d4ff;
--success: #2ed573; --green: #2ed573;
--radius: 12px; --gold: #ffb347;
--radius: 14px;
--radius-sm: 10px;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@ -27,583 +30,564 @@ body {
font-family: -apple-system, 'PingFang SC', 'Noto Sans SC', sans-serif; font-family: -apple-system, 'PingFang SC', 'Noto Sans SC', sans-serif;
min-height: 100vh; min-height: 100vh;
line-height: 1.6; 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 */
.header { .header {
text-align: center; text-align: center;
padding: 48px 24px 32px; 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 { .header h1 {
font-size: 36px; font-size: 40px;
font-weight: 800; font-weight: 800;
background: linear-gradient(135deg, var(--accent), var(--accent-glow)); background: linear-gradient(135deg, var(--accent) 30%, var(--glow) 70%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
letter-spacing: 4px;
margin-bottom: 10px;
}
.header .sub {
font-size: 15px;
color: var(--text-dim);
letter-spacing: 2px; letter-spacing: 2px;
margin-bottom: 8px;
} }
.header .desc {
font-size: 16px;
color: var(--text-muted);
}
.header .badges { .header .badges {
margin-top: 16px; margin-top: 18px;
display: flex; display: flex;
gap: 10px; gap: 10px;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
.badge { .badge {
display: inline-block; padding: 5px 16px;
padding: 4px 14px;
font-size: 12px; font-size: 12px;
border-radius: 20px; border-radius: 20px;
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text-muted); color: var(--text-dim);
letter-spacing: 1px; 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 Layout ── */ /* Main grid */
.main { .main {
max-width: 1200px; max-width: 1300px;
margin: 0 auto; margin: 0 auto;
padding: 0 24px 80px; padding: 0 28px 100px;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 480px 1fr;
gap: 32px; gap: 36px;
align-items: start;
}
@media (max-width: 1000px) {
.main { grid-template-columns: 1fr; max-width: 600px; }
} }
@media (max-width: 900px) { /* Panel */
.main {
grid-template-columns: 1fr;
}
}
/* ── Panel ── */
.panel { .panel {
background: var(--surface); background: var(--surface);
border-radius: var(--radius);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 28px; 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-title { .panel-head {
font-size: 14px; font-size: 13px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--text-muted); color: var(--text-dim);
margin-bottom: 20px; margin-bottom: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
} }
.panel-head::before {
.panel-title::before {
content: ''; content: '';
display: block; display: block;
width: 4px; width: 3px;
height: 16px; height: 14px;
background: var(--accent); background: var(--accent);
border-radius: 2px; border-radius: 2px;
} }
/* ── Form ── */ /* Form */
.form-group { .field { margin-bottom: 18px; }
margin-bottom: 18px; .field label {
}
.form-group label {
display: block; display: block;
font-size: 13px; font-size: 12px;
color: var(--text-muted); color: var(--text-dim);
margin-bottom: 6px; margin-bottom: 7px;
letter-spacing: 1px; letter-spacing: 1.5px;
font-weight: 500;
} }
.field input, .field textarea, .field select {
.form-group input,
.form-group textarea,
.form-group select {
width: 100%; width: 100%;
padding: 12px 16px; padding: 14px 18px;
background: var(--bg); background: var(--surface2);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: var(--radius-sm);
color: var(--text); color: var(--text);
font-size: 15px; font-size: 15px;
font-family: inherit; font-family: inherit;
transition: border-color 0.2s; transition: all 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none; outline: none;
}
.field input:focus, .field textarea:focus, .field select:focus {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(79, 140, 255, 0.1); box-shadow: 0 0 0 3px rgba(79,140,255,0.08);
} }
.field textarea {
.form-group textarea { min-height: 200px;
min-height: 180px;
resize: vertical; resize: vertical;
line-height: 1.7; 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; }
.form-row { .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
/* ── Preset Selector ── */ /* Presets */
.preset-grid { .preset-row {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 8px; gap: 7px;
margin-bottom: 18px; margin-bottom: 18px;
} }
.preset-dot {
.preset-btn { padding: 10px 4px;
padding: 10px 6px; border: 2px solid var(--border);
border: 2px solid transparent; border-radius: var(--radius-sm);
border-radius: 8px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-dim);
background: var(--bg); background: var(--surface2);
transition: all 0.2s; transition: all 0.2s;
font-family: inherit; font-family: inherit;
} }
.preset-dot:hover { border-color: var(--border-h); color: var(--text); }
.preset-btn:hover { .preset-dot.active {
border-color: var(--border);
color: var(--text);
}
.preset-btn.active {
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
background: rgba(79, 140, 255, 0.08); background: rgba(79,140,255,0.08);
} }
/* ── Generate Button ── */ /* Generate btn */
.generate-btn { .gen-btn {
width: 100%; width: 100%;
padding: 14px 24px; padding: 16px 28px;
background: linear-gradient(135deg, var(--accent), #3d6fd9); background: linear-gradient(135deg, var(--accent), var(--accent-dim));
color: #fff; color: #fff;
border: none; border: none;
border-radius: 10px; border-radius: var(--radius-sm);
font-size: 16px; font-size: 17px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
letter-spacing: 2px; letter-spacing: 2px;
transition: all 0.2s; transition: all 0.25s;
font-family: inherit; font-family: inherit;
margin-top: 6px;
position: relative;
overflow: hidden;
} }
.gen-btn::after {
.generate-btn:hover { content: '';
transform: translateY(-1px); position: absolute;
box-shadow: 0 8px 30px rgba(79, 140, 255, 0.3); 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%; }
.generate-btn:disabled { .gen-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(79,140,255,0.25); }
opacity: 0.5; .gen-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
cursor: not-allowed; .gen-btn .spinner {
transform: none; display: none; width: 18px; height: 18px;
box-shadow: none; 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; }
.generate-btn .spinner { .gen-btn.loading .txt { display: none; }
display: none;
width: 16px;
height: 16px;
border: 2px solid #fff;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
.generate-btn.loading .spinner { display: inline-block; }
.generate-btn.loading .btn-text { display: none; }
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
/* ── Preview ── */ .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; }
.preview-area { .error.show { display: block; }
/* Preview area */
.preview-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
min-height: 360px;
justify-content: center; justify-content: center;
min-height: 400px;
} }
.preview-empty {
.preview-placeholder {
text-align: center; text-align: center;
color: var(--text-muted); padding: 80px 40px;
padding: 60px 20px;
}
.preview-placeholder .icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.preview-placeholder .text {
font-size: 15px;
line-height: 1.8;
} }
.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 { .preview-img {
max-width: 100%; max-width: 100%;
max-height: 500px; max-height: 520px;
border-radius: 8px; border-radius: 10px;
border: 1px solid var(--border); border: 1px solid var(--border);
display: none; display: none;
box-shadow: 0 8px 40px rgba(0,0,0,0.4);
} }
.preview-img.show { display: block; } .preview-img.show { display: block; }
/* ── Download Button ── */ .loading-dots { display: none; gap: 8px; justify-content: center; padding: 80px 40px; }
.download-bar { .loading-dots.show { display: flex; }
display: none; .loading-dots span { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: dot 1.2s ease-in-out infinite; }
margin-top: 16px; .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
width: 100%; .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
text-align: center; @keyframes dot { 0%,80%,100% { transform: scale(0.5); opacity: 0.2; } 40% { transform: scale(1); opacity: 1; } }
}
.download-bar.show { display: block; } /* Download bar */
.dl-bar { display: none; margin-top: 20px; width: 100%; text-align: center; }
.download-btn { .dl-bar.show { display: block; }
.dl-btn {
display: inline-block; display: inline-block;
padding: 10px 28px; padding: 12px 32px;
background: var(--surface); background: var(--surface2);
color: var(--accent); color: var(--accent);
border: 1px solid var(--accent); border: 1px solid var(--accent);
border-radius: 8px; border-radius: var(--radius-sm);
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer;
letter-spacing: 1px; letter-spacing: 1px;
cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.dl-btn:hover { background: rgba(79,140,255,0.1); }
.download-btn:hover { /* Intent Chain */
background: rgba(79, 140, 255, 0.1); .intent-chain {
} margin-top: 28px;
/* ── Loading ── */
.loading-dots {
display: flex;
gap: 8px;
justify-content: center;
padding: 40px;
}
.loading-dots span {
width: 10px;
height: 10px;
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.6); opacity: 0.3; } 40% { transform: scale(1); opacity: 1; } }
/* ── Error ── */
.error-msg {
display: none; display: none;
padding: 12px 16px;
background: rgba(255, 71, 87, 0.1);
border: 1px solid var(--danger);
border-radius: 8px;
color: var(--danger);
font-size: 13px;
margin-top: 12px;
} }
.error-msg.show { display: block; } .intent-chain.show { display: block; }
/* ── Footer ── */ .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 { .footer {
text-align: center; text-align: center;
padding: 32px 24px; padding: 40px 24px;
font-size: 13px; font-size: 13px;
color: var(--text-muted); color: var(--text-dim);
opacity: 0.5; opacity: 0.4;
letter-spacing: 1px; letter-spacing: 2px;
}
.footer a {
color: var(--accent);
text-decoration: none;
}
.footer .divider {
margin: 0 8px;
opacity: 0.3;
} }
.footer a { color: var(--accent); text-decoration: none; }
.footer .sep { margin: 0 10px; opacity: 0.3; }
</style> </style>
</head> </head>
<body> <body>
<header class="header"> <header class="header">
<h1>封面工作室</h1> <h1>封面工作室</h1>
<p class="desc">语言驱动排版设计 · 输入文字,自动生成封面</p> <p class="sub">语言驱动排版设计 · 输入文字,自动生成封面</p>
<div class="badges"> <div class="badges">
<span class="badge">开源免费</span> <span class="badge highlight">开源免费</span>
<span class="badge">无需GPU</span> <span class="badge">无需 GPU</span>
<span class="badge">一键部署</span> <span class="badge">意图透明</span>
<span class="badge">MIT协议</span> <span class="badge">设计属于你</span>
</div> </div>
</header> </header>
<div class="main"> <div class="main">
<!-- 左侧:输入面板 --> <!-- 左侧:输入面板 -->
<div class="panel" id="inputPanel"> <div class="panel" id="formPanel">
<div class="panel-title">📝 内容输入</div> <div class="panel-head">内容输入</div>
<div class="form-group"> <div class="field">
<label for="templateSelect">模板</label> <label for="tpl">模板</label>
<select id="templateSelect"></select> <select id="tpl"></select>
</div> </div>
<div class="form-group"> <div class="field">
<label>预设风格</label> <label>风格预设</label>
<div class="preset-grid" id="presetGrid"></div> <div class="preset-row" id="presets"></div>
</div> </div>
<div class="form-row"> <div class="row2">
<div class="form-group"> <div class="field">
<label for="layoutSelect">布局</label> <label for="layout">布局</label>
<select id="layoutSelect"> <select id="layout">
<option value="default">标准排版</option> <option value="default">标准排版</option>
<option value="hero">大标题封面</option> <option value="hero">大标题封面</option>
<option value="quote">金句居中</option> <option value="quote">金句居中</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="field">
<label for="tagInput">标签(可选)</label> <label for="tag">标签</label>
<input type="text" id="tagInput" placeholder="例如:教程 · 干货"> <input type="text" id="tag" placeholder="教程 · 干货" maxlength="20">
</div> </div>
</div> </div>
<div class="form-group"> <div class="field">
<label for="titleInput">标题</label> <label for="title">标题</label>
<input type="text" id="titleInput" placeholder="标题将作为封面大标题"> <input type="text" id="title" placeholder="标题将作为封面大标题" maxlength="60">
</div> </div>
<div class="form-group"> <div class="field">
<label for="bodyInput">正文(支持 Markdown 列表和加粗)</label> <label for="body">正文(支持 Markdown</label>
<textarea id="bodyInput" placeholder="输入正文内容...&#10;&#10;支持格式:&#10;- 列表项用 - 或 • 开头&#10;## 小标题用 ## 开头&#10;**加粗文字** 用双星号包裹&#10;&#10;无需GPU · 语言驱动排版 · 开源免费&#10;一行命令部署到你的服务器&#10;支持多种风格和模板"></textarea> <textarea id="body" placeholder="输入正文…&#10;&#10;- 列表用 - 开头&#10;## 小标题用 ## 开头&#10;**加粗** 用双星号&#10;&#10;语言驱动排版 · 零成本 · 无需GPU"></textarea>
</div> </div>
<button class="generate-btn" id="genBtn" onclick="doGenerate()"> <button class="gen-btn" id="genBtn" onclick="go()">
<span class="spinner"></span> <span class="spinner"></span>
<span class="btn-text">生成封面</span> <span class="txt">生成封面</span>
</button> </button>
<div class="error" id="err"></div>
<div class="error-msg" id="errorMsg"></div>
</div> </div>
<!-- 右侧:预览面板 --> <!-- 右侧:预览 + 意图链 -->
<div class="panel" id="previewPanel"> <div>
<div class="panel-title">👁️ 预览</div> <div class="panel" id="previewPanel">
<div class="preview-area" id="previewArea"> <div class="panel-head">预览</div>
<div class="preview-placeholder" id="placeholder"> <div class="preview-wrap">
<div class="icon">🎨</div> <div class="preview-empty" id="empty">
<div class="text"> <div class="icon">🎨</div>
在左侧输入内容<br> <div class="hint">输入文字 · 选择风格<br>点击生成</div>
选择模板和风格<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="loading-dots" id="loadingDots" style="display:none"> </div>
<span></span><span></span><span></span>
</div> <div class="panel intent-chain" id="chain">
<img class="preview-img" id="previewImg" alt="封面预览"> <div class="title">意图链 · 设计你的</div>
<div class="download-bar" id="downloadBar"> <div id="chainBody"></div>
<a class="download-btn" id="downloadLink" href="#" download>💾 下载封面 (PNG)</a>
</div>
</div> </div>
</div> </div>
</div> </div>
<footer class="footer"> <footer class="footer">
<span>铸渊封面工作室 v2.0</span> 铸渊封面工作室 v2.1
<span class="divider">·</span> <span class="sep">·</span>
<span>开源协议 <a href="#" target="_blank">MIT</a></span> <a href="https://guanghulab.com/code/bingshuo/guanghulab" target="_blank">开源仓库</a>
<span class="divider">·</span> <span class="sep">·</span>
<span>代码仓库 <a href="https://guanghulab.com/code/bingshuo/guanghulab" target="_blank">guanghulab.com</a></span> MIT
</footer> </footer>
<script> <script>
// ── State ── let tpls = [], curTpl = null, curPreset = 'tech'
let templates = []
let currentTemplate = null
let currentPreset = 'tech'
let currentLayout = 'default'
// ── Init ──
async function init() { async function init() {
try { try {
const res = await fetch('/api/templates') const r = await fetch('/api/templates')
templates = await res.json() tpls = await r.json()
renderTemplateSelect() const sel = document.getElementById('tpl')
selectTemplate(templates[0]?.id) 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('titleInput').value = '零基础手搓AI封面生成器' document.getElementById('body').value =
document.getElementById('bodyInput').value = '语言驱动排版 · 意图透明 · 设计属于你\n\n' +
'不需要GPU · 不花一分钱 · 语言驱动自动排版\n\n' + '- 零成本 纯 HTML/CSS 排版渲染\n' +
'- **零成本** 纯 HTML/CSS 排版渲染,无需 AI 绘画模型\n' + '- 极轻量 2GB 内存一行命令部署\n' +
'- **极轻量** 2GB 内存就能跑,一行命令部署\n' + '- 语言驱动 输入文字自动分析推荐\n' +
'- **语言驱动** 输入文字,自动分析内容类型和情绪,推荐配色和布局\n' + '- 意图透明 每一步推理可见\n' +
'- **开源免费** MIT 协议,代码完全公开\n' + '- 设计属于你 署名 @冰朔'
'- **可扩展** 模板注册表机制,新增模板零改动核心代码'
} catch (e) { } catch (e) {
console.error('加载模板失败:', e) console.error(e)
showError('无法加载模板列表,请检查服务器连接') document.getElementById('err').textContent = '连接服务器失败'
document.getElementById('err').classList.add('show')
} }
} }
function renderTemplateSelect() { function pick(id) {
const sel = document.getElementById('templateSelect') curTpl = tpls.find(t => t.id === id)
sel.innerHTML = templates.map(t => curPreset = curTpl?.presets?.[0]?.id || 'tech'
`<option value="${t.id}">${t.icon} ${t.name} — ${t.description}</option>` document.getElementById('presets').innerHTML = (curTpl?.presets || []).map(p =>
).join('') `<button class="preset-dot ${p.id === curPreset ? 'active' : ''}" onclick="setPreset('${p.id}')">${p.name}</button>`
sel.onchange = () => selectTemplate(sel.value)
}
function selectTemplate(id) {
currentTemplate = templates.find(t => t.id === id)
if (!currentTemplate) return
currentPreset = currentTemplate.presets?.[0]?.id || 'tech'
renderPresets()
document.getElementById('templateSelect').value = id
}
function renderPresets() {
const grid = document.getElementById('presetGrid')
const presets = currentTemplate?.presets || []
grid.innerHTML = presets.map(p =>
`<button class="preset-btn ${p.id === currentPreset ? 'active' : ''}"
onclick="selectPreset('${p.id}')">${p.name}</button>`
).join('') ).join('')
} }
function selectPreset(id) { function setPreset(id) { curPreset = id; pick(curTpl.id) }
currentPreset = id
renderPresets()
}
// ── Generate ── async function go() {
async function doGenerate() { const title = document.getElementById('title').value.trim()
const title = document.getElementById('titleInput').value.trim() const body = document.getElementById('body').value.trim()
const body = document.getElementById('bodyInput').value.trim() const tag = document.getElementById('tag').value.trim()
const tag = document.getElementById('tagInput').value.trim() const layout = document.getElementById('layout').value
const layout = document.getElementById('layoutSelect').value
if (!title && !body) { if (!title && !body) {
showError('请至少输入标题或正文内容') const e = document.getElementById('err'); e.textContent = '请输入标题或正文'; e.classList.add('show')
return return
} }
setLoading(true) document.getElementById('err').classList.remove('show')
hideError() 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 { try {
const res = await fetch('/api/generate', { const r = await fetch('/api/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ templateId: curTpl?.id, presetId: curPreset, title, body, tag, layout }),
templateId: currentTemplate?.id,
presetId: currentPreset,
title,
body,
tag,
layout,
}),
}) })
if (!r.ok) { const e = await r.json(); throw new Error(e.error || '生成失败') }
const d = await r.json()
if (!res.ok) { document.getElementById('loading').classList.remove('show')
const err = await res.json() document.getElementById('empty').style.display = 'none'
throw new Error(err.error || '生成失败') 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('')
} }
const data = await res.json()
showPreview(data.url || data.urls[0])
} catch (e) { } catch (e) {
showError(e.message || '生成失败,请稍后重试') document.getElementById('loading').classList.remove('show')
setLoading(false) 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
} }
} }
function setLoading(loading) { document.addEventListener('keydown', e => {
const btn = document.getElementById('genBtn') if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') go()
const dots = document.getElementById('loadingDots')
const placeholder = document.getElementById('placeholder')
const img = document.getElementById('previewImg')
const bar = document.getElementById('downloadBar')
if (loading) {
btn.classList.add('loading')
btn.disabled = true
placeholder.style.display = 'none'
img.classList.remove('show')
bar.classList.remove('show')
dots.style.display = 'flex'
} else {
btn.classList.remove('loading')
btn.disabled = false
dots.style.display = 'none'
}
}
function showPreview(url) {
setLoading(false)
const placeholder = document.getElementById('placeholder')
const img = document.getElementById('previewImg')
const bar = document.getElementById('downloadBar')
const link = document.getElementById('downloadLink')
placeholder.style.display = 'none'
img.src = url
img.classList.add('show')
bar.classList.add('show')
link.href = url
}
function showError(msg) {
const el = document.getElementById('errorMsg')
el.textContent = msg
el.classList.add('show')
}
function hideError() {
document.getElementById('errorMsg').classList.remove('show')
}
// ── Keyboard shortcut ──
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
doGenerate()
}
}) })
// ── Start ──
init() init()
</script> </script>
</body> </body>