2026-06-15 15:04:43 -07:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
import fs from 'node:fs/promises';
|
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildFeedThumbnailSvg,
|
|
|
|
|
|
extractCoverSignals,
|
|
|
|
|
|
generateHtmlThumbnail,
|
|
|
|
|
|
resolveCoverDataUri,
|
|
|
|
|
|
} from '../mindspace-thumbnails.mjs';
|
|
|
|
|
|
|
|
|
|
|
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
2026-07-05 23:14:41 +08:00
|
|
|
|
const sampleDir = path.join(root, 'public', 'thumbnail-demo-samples');
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const outputDir = path.join(root, 'public', 'thumbnail-demo');
|
2026-07-05 23:14:41 +08:00
|
|
|
|
|
|
|
|
|
|
/** A/B pairs: bad = 缺 mindspace-cover,good = 方案 A 完整元数据 + hero 图 */
|
|
|
|
|
|
const pairs = [
|
2026-06-15 15:04:43 -07:00
|
|
|
|
{
|
2026-07-05 23:14:41 +08:00
|
|
|
|
id: 'vietnam-guide',
|
|
|
|
|
|
label: '越南旅游指南',
|
|
|
|
|
|
bad: path.join(sampleDir, 'vietnam-guide-bad.html'),
|
|
|
|
|
|
good: path.join(sampleDir, 'vietnam-guide-good.html'),
|
2026-06-15 15:04:43 -07:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-07-05 23:14:41 +08:00
|
|
|
|
function galleryCard({ id, title, subtitle, variant }) {
|
2026-06-15 15:04:43 -07:00
|
|
|
|
return `
|
|
|
|
|
|
<article class="feed-card">
|
2026-07-05 23:14:41 +08:00
|
|
|
|
<div class="feed-cover"><img src="./${id}-${variant}-feed.svg" alt="${title} 预览图" /></div>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
<div class="feed-body">
|
|
|
|
|
|
<h3>${title}</h3>
|
|
|
|
|
|
<p>${subtitle}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 23:14:41 +08:00
|
|
|
|
async function renderVariant({ htmlPath, variant, id }) {
|
|
|
|
|
|
const html = await fs.readFile(htmlPath, 'utf8');
|
|
|
|
|
|
const signals = extractCoverSignals(html);
|
|
|
|
|
|
const coverDataUri = signals.image
|
|
|
|
|
|
? await resolveCoverDataUri({
|
|
|
|
|
|
storageRoot: outputDir,
|
|
|
|
|
|
contentBaseDir: path.dirname(htmlPath),
|
|
|
|
|
|
imageUrl: signals.image,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
const feedSvg = buildFeedThumbnailSvg(signals, { coverDataUri });
|
|
|
|
|
|
const feedPath = path.join(outputDir, `${id}-${variant}-feed.svg`);
|
|
|
|
|
|
await fs.writeFile(feedPath, feedSvg, 'utf8');
|
|
|
|
|
|
await generateHtmlThumbnail(outputDir, `${id}-${variant}-generated.svg`, html, {
|
|
|
|
|
|
contentBaseDir: path.dirname(htmlPath),
|
|
|
|
|
|
title: signals.title,
|
|
|
|
|
|
subtitle: signals.subtitle,
|
|
|
|
|
|
});
|
|
|
|
|
|
return { signals, hasPhoto: Boolean(coverDataUri) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
async function main() {
|
|
|
|
|
|
await fs.mkdir(outputDir, { recursive: true });
|
2026-07-05 23:14:41 +08:00
|
|
|
|
const sections = [];
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-05 23:14:41 +08:00
|
|
|
|
for (const pair of pairs) {
|
|
|
|
|
|
const bad = await renderVariant({ htmlPath: pair.bad, variant: 'bad', id: pair.id });
|
|
|
|
|
|
const good = await renderVariant({ htmlPath: pair.good, variant: 'good', id: pair.id });
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-05 23:14:41 +08:00
|
|
|
|
sections.push(`
|
|
|
|
|
|
<section class="sample">
|
|
|
|
|
|
<h2>${pair.label}</h2>
|
|
|
|
|
|
<p class="hint">左侧:未写 mindspace-cover(退化为纯色渐变) · 右侧:方案 A(完整元数据 + hero 主图)</p>
|
|
|
|
|
|
<div class="pair">
|
|
|
|
|
|
<div class="thumb">
|
|
|
|
|
|
<img src="./${pair.id}-bad-feed.svg" alt="缺 cover" />
|
|
|
|
|
|
<span class="bad">❌ 缺 mindspace-cover · ${bad.hasPhoto ? '有图' : '无图'} · tag=${bad.signals.tag}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="thumb">
|
|
|
|
|
|
<img src="./${pair.id}-good-feed.svg" alt="方案 A" />
|
|
|
|
|
|
<span class="good">✅ 方案 A · ${good.hasPhoto ? '照片封面' : '场景/渐变'} · tag=${good.signals.tag}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feed-grid">
|
|
|
|
|
|
${galleryCard({ id: pair.id, title: pair.label, subtitle: '缺 cover · 信息流效果', variant: 'bad' })}
|
|
|
|
|
|
${galleryCard({ id: pair.id, title: pair.label, subtitle: '方案 A · 信息流效果', variant: 'good' })}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>`);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const html = `<!doctype html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
2026-07-05 23:14:41 +08:00
|
|
|
|
<title>MindSpace 缩略图 · 方案 A 对比</title>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
<style>
|
|
|
|
|
|
:root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f4f0e7; color: #18211d; }
|
2026-07-05 23:14:41 +08:00
|
|
|
|
body { margin: 0; padding: 24px; max-width: 960px; }
|
2026-06-15 15:04:43 -07:00
|
|
|
|
h1 { margin: 0 0 8px; font-size: 28px; }
|
|
|
|
|
|
.lead { margin: 0 0 24px; color: #5f6963; line-height: 1.6; }
|
|
|
|
|
|
.compare { display: grid; gap: 28px; }
|
|
|
|
|
|
.sample { padding: 20px; border-radius: 20px; background: #fff; box-shadow: 0 10px 30px rgba(24,33,29,0.08); }
|
2026-07-05 23:14:41 +08:00
|
|
|
|
.sample h2 { margin: 0 0 8px; font-size: 20px; }
|
|
|
|
|
|
.hint { margin: 0 0 16px; font-size: 13px; color: #6d7771; }
|
|
|
|
|
|
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
2026-06-15 15:04:43 -07:00
|
|
|
|
.thumb { display: grid; gap: 8px; }
|
2026-07-05 23:14:41 +08:00
|
|
|
|
.thumb img { width: 100%; max-width: 270px; border-radius: 12px; box-shadow: 0 8px 24px rgba(24,33,29,0.12); }
|
|
|
|
|
|
.thumb span { font-size: 12px; line-height: 1.5; }
|
|
|
|
|
|
.thumb span.bad { color: #b45309; }
|
|
|
|
|
|
.thumb span.good { color: #2f6f57; }
|
|
|
|
|
|
.feed-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 520px; margin-top: 18px; }
|
|
|
|
|
|
.feed-card { border-radius: 12px; overflow: hidden; background: #fff; box-shadow: 0 2px 10px rgba(24,33,29,0.07); }
|
2026-06-15 15:04:43 -07:00
|
|
|
|
.feed-cover { aspect-ratio: 3 / 4; background: #ece7df; }
|
|
|
|
|
|
.feed-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
|
|
.feed-body { padding: 10px 12px 12px; }
|
|
|
|
|
|
.feed-body h3 { margin: 0; font-size: 14px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
|
|
|
|
.feed-body p { margin: 6px 0 0; font-size: 12px; color: #939c97; }
|
2026-07-05 23:14:41 +08:00
|
|
|
|
code { background: #f0ebe3; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
2026-07-05 23:14:41 +08:00
|
|
|
|
<h1>方案 A:mindspace-cover 对比 Demo</h1>
|
|
|
|
|
|
<p class="lead">
|
|
|
|
|
|
页面生成时在 <code><head></code> 写入 <code>mindspace-cover</code> 元数据并引用 hero 主图,
|
|
|
|
|
|
缩略图会从「纯色渐变」升级为「照片封面 + 标题」。
|
|
|
|
|
|
交付前可跑 <code>npm run check:mindspace-cover</code> 自检。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="compare">${sections.join('')}</div>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</body>
|
|
|
|
|
|
</html>`;
|
|
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(path.join(outputDir, 'index.html'), html, 'utf8');
|
2026-07-05 23:14:41 +08:00
|
|
|
|
console.log(`已生成对比页: ${path.join(outputDir, 'index.html')}`);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
console.log('本地查看: http://localhost:5173/thumbnail-demo/');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await main();
|