Files
drawlogo/poster.html
T

200 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="logo/favicon.svg">
<title>画个标 · 智能海报设计</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 14px; color: #1f2937; background: #f3f4f6; min-height: 100vh; }
a { color: #4f46e5; text-decoration: none; }
a:hover { text-decoration: underline; }
.topbar { display: flex; align-items: center; justify-content: space-between; padding: 0 20px; height: 54px; background: linear-gradient(90deg, #1e1b4b, #312e81); color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.15); position: sticky; top: 0; z-index: 20; }
.topbar .brand { display: flex; align-items: center; gap: 10px; }
.topbar .brand .icon { font-size: 20px; }
.topbar .brand .name { font-size: 17px; font-weight: 700; letter-spacing: 1px; }
.topbar .brand .tag { font-size: 11px; color: #c7d2fe; background: rgba(255,255,255,.12); padding: 2px 8px; border-radius: 20px; }
.topbar .nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.topbar .nav a { color: #e0e7ff; font-size: 13px; padding: 6px 12px; border: 1px solid rgba(255,255,255,.35); border-radius: 6px; background: rgba(255,255,255,.1); transition: background .15s; }
.topbar .nav a:hover { background: rgba(255,255,255,.22); text-decoration: none; }
.topbar .nav a.active { background: #fff; color: #312e81; font-weight: 700; }
.workspace { display: grid; grid-template-columns: 320px 1fr; gap: 16px; max-width: 1200px; margin: 22px auto 60px; padding: 0 16px; }
@media (max-width: 820px) { .workspace { grid-template-columns: 1fr; } }
.config { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); height: fit-content; }
.config .label { font-size: 13px; color: #6b7280; margin: 12px 0 6px; }
.config .label:first-child { margin-top: 0; }
.spec-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.spec-tab { font-size: 12px; padding: 5px 10px; border: 1px solid #e5e7eb; border-radius: 20px; cursor: pointer; background: #fff; color: #4b5563; }
.spec-tab.active { background: #4f46e5; color: #fff; border-color: #4f46e5; }
.config input, .config textarea { width: 100%; padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 13px; font-family: inherit; }
.config textarea { resize: vertical; min-height: 60px; }
.btn-primary { display: block; width: 100%; margin-top: 16px; padding: 10px; background: #4f46e5; color: #fff; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; }
.btn-primary:disabled { opacity: .6; cursor: not-allowed; }
.plans { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; align-content: start; }
.plan-card { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.plan-card .svg-wrap { display: flex; justify-content: center; background: #f8fafc; padding: 10px; }
.plan-card .svg-wrap svg { max-height: 260px; width: auto; height: auto; }
.plan-card .ops { display: flex; gap: 6px; padding: 10px; border-top: 1px solid #f1f5f9; }
.plan-card .ops button { flex: 1; padding: 6px; border: 1px solid #e5e7eb; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; }
.plan-card .concept { padding: 0 10px 10px; font-size: 12px; color: #6b7280; line-height: 1.5; }
.empty { grid-column: 1/-1; text-align: center; color: #6b7280; padding: 60px 20px; background: #fff; border-radius: 12px; border: 1px dashed #c7d2fe; }
.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: #111827; color: #fff; padding: 10px 18px; border-radius: 8px; font-size: 13px; z-index: 50; }
.hidden { display: none !important; }
</style>
</head>
<body>
<!-- ===== 顶部工具栏 ===== -->
<header class="topbar">
<div class="brand">
<img class="icon" src="logo/logo-mark.svg" alt="画个标" width="20" height="20">
<span class="name">画个标</span>
<span class="tag">✨ 海报设计</span>
</div>
<nav class="nav">
<a href="index.html" title="LOGO 设计工作台">🎨 LOGO</a>
<a href="avatar.html" title="头像设计工作台">🖼 头像</a>
<a class="active" href="poster.html" title="海报设计工作台">📰 海报</a>
<a href="emoji.html" title="动态表情设计工作台">😀 表情</a>
<a href="skills.html" title="共享技能中心">🧩 技能中心</a>
<a href="poster-skills.html" title="海报技能中心">🧩 海报技能</a>
<a href="sources.html" title="图标素材中心">📚 素材中心</a>
<a href="poster-sources.html" title="海报素材中心">📚 海报素材</a>
</nav>
</header>
<div class="workspace">
<!-- 左:配置 -->
<aside class="config">
<div class="label">📐 画布规格</div>
<div class="spec-tabs" id="spec-tabs"></div>
<div class="label">主标题</div>
<input type="text" id="p-title" maxlength="40" placeholder="如:夏季狂欢节">
<div class="label">副标题(可选)</div>
<input type="text" id="p-sub" maxlength="60" placeholder="如:全场 5 折起">
<div class="label">主题描述(AI 设计依据)</div>
<textarea id="p-desc" maxlength="200" placeholder="如:面向年轻人的夏季促销活动,热闹活泼,暖色系…"></textarea>
<button class="btn-primary" id="btn-gen">🤖 AI 生成海报</button>
</aside>
<!-- 右:方案库 -->
<div class="plans" id="plans">
<div class="empty">🎯 填写主题信息,点击「AI 生成海报」</div>
</div>
</div>
<div id="toast" class="toast hidden"></div>
<script src="js/data/poster-sources.js"></script>
<script src="js/data/poster-builtin.js?v=20260823e"></script>
<script src="js/core/posterlib.js?v=20260823e"></script>
<script src="js/core/ai.js?v=20260927a"></script>
<script src="js/core/poster-store.js"></script>
<script src="js/core/poster-generator.js"></script>
<script src="js/core/poster-renderer.js"></script>
<script src="js/core/poster-exporter.js"></script>
<script src="js/core/poster-ai.js"></script>
<script>
(function () {
'use strict';
function toast(msg) {
const el = document.getElementById('toast');
el.textContent = msg; el.classList.remove('hidden');
setTimeout(() => el.classList.add('hidden'), 2200);
}
function renderSpecs() {
const wrap = document.getElementById('spec-tabs');
wrap.innerHTML = HGB_PosterStore.SPECS.map(s =>
`<button class="spec-tab ${s.id === HGB_PosterStore.get().specId ? 'active' : ''}" data-spec="${s.id}">${s.name}</button>`).join('');
wrap.querySelectorAll('.spec-tab').forEach(b => b.addEventListener('click', () => {
HGB_PosterStore.set({ specId: b.dataset.spec });
renderSpecs();
// 已有方案重新按新画布渲染
HGB_PosterStore.set({ plans: HGB_PosterStore.get().plans.map(p => Object.assign({}, p, {
elements: HGB_PosterGenerator.generate(HGB_PosterStore.spec(), HGB_PosterGenerator.defaultDecision(
(HGB_PosterStore.get().title || ''), (HGB_PosterStore.get().sub || '')))
})) });
renderPlans();
}));
}
function inlineSvg(plan) {
return HGB_PosterExporter.buildSvg(HGB_PosterStore.SPECS.find(s => s.id === plan.specId) || HGB_PosterStore.spec(), plan.elements);
}
function renderPlans() {
const wrap = document.getElementById('plans');
const plans = HGB_PosterStore.get().plans;
if (!plans.length) {
wrap.innerHTML = '<div class="empty">🎯 填写主题信息,点击「AI 生成海报」</div>';
return;
}
wrap.innerHTML = plans.map((p, i) => `
<div class="plan-card">
<div class="svg-wrap">${inlineSvg(p)}</div>
<div class="concept">${p.name}${p.concept ? ' · ' + p.concept.replace(/[<>&"]/g, '') : ''}</div>
<div class="ops">
<button data-act="svg" data-i="${i}">导出 SVG</button>
<button data-act="png" data-i="${i}">导出 PNG</button>
</div>
</div>`).join('');
wrap.querySelectorAll('button[data-act]').forEach(b => b.addEventListener('click', async () => {
const p = plans[parseInt(b.dataset.i, 10)];
const spec = HGB_PosterStore.SPECS.find(s => s.id === p.specId) || HGB_PosterStore.spec();
const base = (p.name || '海报');
if (b.dataset.act === 'svg') { HGB_PosterExporter.exportSvg(spec, p.elements, base); toast('✅ 已导出 SVG'); }
else { await HGB_PosterExporter.exportPng(spec, p.elements, base, 1); toast('✅ 已导出 PNG'); }
}));
}
function generateLocal(count) {
const title = (document.getElementById('p-title').value || '我的海报').trim();
const sub = document.getElementById('p-sub').value.trim();
const palettes = HGB_PosterGenerator.PALETTES;
const layouts = HGB_PosterGenerator.LAYOUTS;
const spec = HGB_PosterStore.spec();
const plans = [];
for (let i = 0; i < count; i++) {
const d = HGB_PosterGenerator.defaultDecision(title, sub);
d.paletteId = palettes[i % palettes.length].id;
d.layoutId = layouts[i % layouts.length].id;
plans.push({ id: 'p_' + i, name: layouts[i % layouts.length].name + ' · ' + palettes[i % palettes.length].name, concept: '内置引擎兜底', elements: HGB_PosterGenerator.generate(spec, d), specId: spec.id });
}
return plans;
}
async function generate() {
const btn = document.getElementById('btn-gen');
btn.disabled = true; btn.textContent = '生成中…';
const opt = {
industry: '通用',
title: document.getElementById('p-title').value.trim(),
sub: document.getElementById('p-sub').value.trim(),
desc: document.getElementById('p-desc').value.trim(),
count: 4
};
try {
const r = await HGB_PosterAI.generatePostersAI(opt);
HGB_PosterStore.set({ plans: r.plans, title: opt.title, sub: opt.sub });
} catch (e) {
// AI 失败 → 降级内置引擎(保证页面不空白)
HGB_PosterStore.set({ plans: generateLocal(4), title: opt.title, sub: opt.sub });
toast('⚠️ AI 失败,已降级为内置引擎');
}
renderPlans();
btn.disabled = false; btn.textContent = '🤖 AI 生成海报';
}
document.getElementById('btn-gen').addEventListener('click', generate);
renderSpecs();
renderPlans();
})();
</script>
</body>
</html>