Files
memind/scripts/preview-news002-template.mjs
T
2026-09-21 11:03:55 +08:00

323 lines
15 KiB
JavaScript
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.
#!/usr/bin/env node
/**
* 用固定样例数据渲染一张 news002 静态预览页,用于人工评审版式/色调/排版。
* 生产页由 Agent 按 NEWS002_TEMPLATE_SPEC 生成,这里只保证 CSS 骨架可视化。
*
* 用法:
* node scripts/preview-news002-template.mjs [输出路径]
* import { renderNews002PreviewHtml, writeNews002PreviewFile } from './preview-news002-template.mjs';
*/
import fs from 'node:fs';
import path from 'node:path';
import { NEWS002_STYLE_BLOCK } from '../news-morning-templates.mjs';
import { getLocalParts, normalizeTimezone } from '../schedule-time.mjs';
/** 预览用占位图:避免评审时依赖外网图片。生产页用的是新闻原文配图。 */
function placeholder(label, from, to) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="${from}"/><stop offset="1" stop-color="${to}"/>
</linearGradient></defs>
<rect width="640" height="360" fill="url(#g)"/>
<text x="50%" y="52%" fill="#fff" font-size="30" font-family="sans-serif"
text-anchor="middle" opacity="0.92">${label}</text>
</svg>`;
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
const BRIEF = [
'王毅同美国国务卿鲁比奥通电话,双方就中美关系交换意见',
'第23届中国—东盟博览会在南宁开幕,3400余家企业参展',
'马来西亚最高元首应习近平邀请,将对中国进行国事访问',
'沈阳等地举行九一八事变纪念活动,全城鸣响警世钟',
'Kimi 发布金融行业解决方案,开放 9 项金融技能',
'OpenAI 被曝接近攻克千禧年难题「霍奇猜想」',
'张一鸣净值破 1050 亿美元,首次成为亚洲首富',
'国际油价高位震荡,黄金避险需求维持强势',
];
const LEADS = [
{
key: 'wangyi-rubio-call',
tag: '中美关系',
title: '王毅同美国国务卿鲁比奥通电话,双方就中美关系交换意见',
body: '9月17日,中共中央政治局委员、外交部长王毅同美国国务卿鲁比奥通电话,着重就中美关系及共同关心的问题交换意见。这是双方近一个月来的第二次高层通话。',
source: '新华社', time: '08:12',
img: placeholder('中美高层通话', '#C8362F', '#7A1C18'),
},
{
key: 'asean-expo-nanning',
tag: '区域经贸',
title: '第23届中国—东盟博览会南宁开幕,东帝汶首次以共办方身份参展',
body: '本届东博会共有 70 多个国家和地区的 3400 多家企业参展,东帝汶首次以共办方身份亮相,聚焦 RCEP 框架下的贸易投资与产业对接。',
source: '中国新闻网', time: '09:40',
img: placeholder('东盟博览会', '#1B3A6B', '#0E2340'),
},
{
key: 'malaysia-king-visit',
tag: '外交',
title: '应习近平邀请,马来西亚最高元首将对中国进行国事访问',
body: '外交部宣布,马来西亚最高元首将于近期对中国进行国事访问。这是两国建交以来马方元首的又一次重要访华行程。',
source: '外交部', time: '10:05',
img: placeholder('元首外交', '#8A5A2B', '#4A2E13'),
},
];
const CARDS = {
domestic: [
{
key: 'shenyang-918-memorial', tag: '时政',
title: '勿忘九一八:沈阳等地举行纪念活动,全城鸣响警世钟',
body: '9月18日,沈阳九一八历史博物馆残历碑广场举行撞钟鸣警仪式,全市同步拉响防空警报。多地同期举办主题纪念活动。',
why: '这是全民国防教育的固定节点,各地交通与公共活动会同步做临时管制安排。',
source: '人民日报', time: '10:31', img: placeholder('九一八纪念', '#5C1F1C', '#2B0E0C'),
},
{
key: 'beijing-daily-frontpage', tag: '民生',
title: '北京日报刊发9月18日版面,聚焦当日重点时政与民生议题',
body: '版面重点覆盖城市更新、医保结算与秋季交通组织调整三项内容,其中医保异地结算范围进一步扩大。',
why: '异地就医结算范围扩大,直接影响在京工作的非京籍参保人报销流程。',
source: '北京日报', time: '07:20', img: '',
},
],
business: [
{
key: 'oil-gold-volatility', tag: '大宗',
title: '地缘因素扰动原油市场,国际油价高位震荡、黄金避险需求走强',
body: '布伦特原油盘中一度冲高后回落,金价受美债收益率与降息预期拉扯维持震荡。市场关注下周 OPEC+ 产量会议。',
why: '油价与国内成品油调价窗口挂钩,会传导到下一轮加油成本。',
source: '路透社', time: '06:55', img: placeholder('能源与贵金属', '#B07A1E', '#5C3D0A'),
},
{
key: 'zhang-yiming-richest', tag: '财富',
title: '张一鸣净值突破1050亿美元,首次成为亚洲首富',
body: '受字节跳动 AI 业务估值抬升带动,张一鸣个人净值首次登顶亚洲富豪榜,超越此前长期居首的印度实业家族。',
why: 'AI 估值外溢到一级市场,会影响今年国内科技赛道的融资定价。',
source: 'Bloomberg', time: '11:18', img: '',
},
],
tech: [
{
key: 'openai-hodge-conjecture', tag: 'AI',
title: 'OpenAI 被曝接近攻克千禧年难题「霍奇猜想」',
body: '多家媒体报道称,OpenAI 内部模型在霍奇猜想证明上取得阶段性进展,相关结果尚待同行评议。公司未正式回应。',
why: '若成立,这将是 AI 首次在千禧年难题上做出可验证贡献,值得等同行评议结论。',
source: 'Hacker News', time: '05:42', img: placeholder('AI for Science', '#1B3A6B', '#4A1E6B'),
},
{
key: 'kimi-finance-solution', tag: '大模型',
title: 'Kimi 发布金融行业解决方案,开放 9 项金融技能',
body: '月之暗面发布面向金融场景的解决方案,用户可在 kimi.com 体验研报解读、财务比对等 9 项技能,主打垂直场景落地。',
why: '大模型开始从通用对话转向按行业收费,这是商业化路径的关键信号。',
source: '36氪', time: '14:06', img: '',
},
],
};
const WEATHER = [
['北京', '☀️', '18/27°'], ['上海', '⛅', '23/30°'], ['广州', '🌦️', '26/33°'],
['深圳', '🌧️', '26/31°'], ['成都', '☁️', '20/26°'], ['杭州', '⛅', '22/30°'],
['西安', '☀️', '17/28°'], ['武汉', '⛅', '21/29°'],
];
const KNOWLEDGE = [
['📐', '什么是霍奇猜想?', '代数几何的千禧年难题之一,问的是复射影流形上哪些上同调类可以由代数闭链表示。'],
['🛢️', '布伦特与WTI的差价', '两者分别对应北海与美国内陆原油,价差主要反映运输成本与区域供需,常被用作地缘风险指标。'],
];
const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
function pad2(value) {
return String(value).padStart(2, '0');
}
export function buildNews002PreviewContext({
now = Date.now(),
timezone = normalizeTimezone(),
previewMode = false,
} = {}) {
const parts = getLocalParts(now, timezone);
const dateLabel = `${parts.year}年${parts.month}月${parts.day}日`;
const weekdayLabel = new Intl.DateTimeFormat('zh-CN', {
timeZone: timezone,
weekday: 'long',
}).format(new Date(now));
const mmdd = `${pad2(parts.month)}${pad2(parts.day)}`;
const slug = `daily-news-${mmdd}-news002-preview`;
const headlineCount = BRIEF.length + LEADS.length;
const footerTime = `${parts.year}-${pad2(parts.month)}-${pad2(parts.day)} 05:30 (UTC+8)`;
const historyDays = Array.from({ length: 7 }, (_, index) => {
const dayMs = now - index * 24 * 60 * 60 * 1000;
const dayParts = getLocalParts(dayMs, timezone);
return {
label: `${pad2(dayParts.month)}-${pad2(dayParts.day)}`,
weekday: WEEKDAY_SHORT[new Date(dayMs).getDay()],
today: index === 0,
};
});
return {
now,
timezone,
previewMode,
dateLabel,
weekdayLabel,
mmdd,
slug,
headlineCount,
footerTime,
historyDays,
pageTitle: previewMode
? `每日新闻早报 · news002预览 · ${dateLabel} · TKMind`
: `每日新闻早报 · ${dateLabel} · TKMind`,
draftTitle: `[news002预览] 每日新闻早报 · ${dateLabel}`,
metaDescription: previewMode
? 'news002 版式预览草稿:样例数据,仅供评审版式/色调/排版,非当日 Agent 全量生成。'
: 'news002 版式预览:事件唯一化、每条配图、报头版式、支持深色模式。',
};
}
export function renderNews002PreviewHtml(context = buildNews002PreviewContext()) {
const ctx = context.slug ? context : buildNews002PreviewContext(context);
const img = (src, alt, emoji) => (src
? `<div class="media"><img src="${src}" alt="${alt}" loading="lazy" referrerpolicy="no-referrer" onerror="this.parentNode.classList.add('ph');this.remove()"></div>`
: `<div class="media ph">${emoji}</div>`);
const source = (name, time) => `<div class="source"><a href="#">${name}</a><span class="dot"></span>${time}</div>`;
const section = (id, icon, label, inner, count = '') => `
<div class="section" id="${id}">
<div class="section-title"><span class="icon">${icon}</span><h2>${label}</h2>${count ? `<span class="count">${count}</span>` : ''}</div>
${inner}
</div>`;
const renderCards = (items, emoji) => items.map((c) => `
<div class="card" data-event-key="${c.key}">
${img(c.img, c.title, emoji)}
<div class="body">
<span class="tag">${c.tag}</span>
<h3>${c.title}</h3>
<p>${c.body}</p>
<div class="why"><b>为什么重要</b>:${c.why}</div>
${source(c.source, c.time)}
</div>
</div>`).join('');
const previewBanner = ctx.previewMode
? `<div class="lede" style="background:#FFF3E8;border-left:4px solid #C8362F;color:#7A1C18;margin-top:0">
这是 <b>news002 模板预览草稿</b>,内容为固定样例数据,用于评审新版式。满意后再决定是否切换正式模板并发布。
</div>`
: '';
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${ctx.pageTitle}</title>
<meta name="description" content="${ctx.metaDescription}">
${NEWS002_STYLE_BLOCK}
</head>
<body>
<div class="progress" id="progress"></div>
<div class="masthead">
<div class="kicker">TKMIND DAILY${ctx.previewMode ? ' · PREVIEW' : ''}</div>
<h1>每日新闻早报</h1>
<div class="meta"><b>${ctx.dateLabel}</b><span>${ctx.weekdayLabel}</span><span>${ctx.previewMode ? 'news002 预览' : `第 ${ctx.headlineCount} 条速读`}</span><span>样例 ${ctx.headlineCount} 条</span></div>
</div>
${previewBanner}
<div class="lede">中美外长再次通话,是今天最值得看的一件事 —— 它决定了后面一周市场对地缘风险的定价。</div>
<nav class="nav"><div class="nav-inner">
<a href="#brief">⚡ 速读</a><a href="#headlines">📰 头条</a><a href="#deepdive">🔍 深读</a>
<a href="#domestic">🇨🇳 国内</a><a href="#business">💰 财经</a><a href="#tech">🤖 科技</a>
<a href="#weather">🌤️ 天气</a><a href="#knowledge">📚 知识</a>
</div></nav>
<div class="container">
${section('brief', '⚡', '一分钟速读', `<ol class="brief">${BRIEF.map((t, i) => `<li><span class="n">${i + 1}</span><span>${t}</span></li>`).join('')}</ol>`, `${BRIEF.length} 条`)}
${section('headlines', '📰', '今日头条', LEADS.map((c) => `
<a class="lead-card" href="#" data-event-key="${c.key}">
${img(c.img, c.title, '📰')}
<div class="body">
<span class="tag">${c.tag}</span>
<h3>${c.title}</h3>
<p>${c.body}</p>
${source(c.source, c.time)}
</div>
</a>`).join(''))}
${section('deepdive', '🔍', '深读一条', `
<div class="deep" data-event-key="wangyi-rubio-call-analysis">
<span class="tag b">解读</span>
<h3>一通电话为什么值得看:中美沟通渠道正在被重新定义</h3>
<p>过去一年,中美高层互动从「危机后补救」转向「常态化管道」。这次通话距离上一次仅隔 27 天,节奏本身比内容更值得注意 —— 它意味着双方把沟通频率固定了下来。</p>
<div class="pull">节奏的稳定,往往比单次会谈的措辞更能说明关系走向。</div>
<p>对市场而言,稳定的沟通渠道降低了「意外升级」的尾部风险,这也是今天亚太股市在地缘消息面上没有出现明显波动的原因之一。后续观察点是月底的经贸工作组会议是否如期召开。</p>
${source('综合新华社、路透社', '12:00')}
</div>`)}
${section('domestic', '🇨🇳', '国内', renderCards(CARDS.domestic, '🇨🇳'))}
${section('business', '💰', '财经·市场', renderCards(CARDS.business, '💰'))}
${section('tech', '🤖', '科技·AI', renderCards(CARDS.tech, '🤖'))}
${section('weather', '🌤️', '今日天气', `
<div class="weather-today">
<div><div class="c">北京</div><div class="d">晴 · 东北风3级 · 空气优</div></div>
<div class="t">27°</div>
</div>
<div class="weather-grid">${WEATHER.map(([c, e, t]) => `<div class="weather-card"><div class="wc">${c}</div><div class="we">${e}</div><div class="wt">${t}</div></div>`).join('')}</div>`)}
${section('knowledge', '📚', '知识卡片', `<div class="k-grid">${KNOWLEDGE.map(([e, t, d]) => `<div class="k-item"><div class="k-icon">${e}</div><h4>${t}</h4><p>${d}</p></div>`).join('')}</div>`)}
<div class="subscribe">
<h3>每天早上 6 点,11 条不重复的新闻</h3>
<p>我们只写当天真正发生的事,去掉重复选题,每条都告诉你为什么重要。</p>
<a href="#">关注 TKMind 智趣</a>
</div>
${section('history', '📅', '往期回顾', `<div class="h-grid">
${ctx.historyDays.map((d) => `<a class="h-item${d.today ? ' today' : ''}" href="#"><div class="hd">${d.label}</div><div class="hw">${d.weekday}</div></a>`).join('')}
</div>`)}
</div>
<div class="footer">
来源:新华社 · 人民日报 · 路透社 · Bloomberg · 36氪 · Hacker News<br>
整理时间 ${ctx.footerTime} · TKMind 智趣${ctx.previewMode ? ' · news002 预览草稿' : ''}
</div>
<script>
(function () {
var bar = document.getElementById('progress');
function tick() {
var h = document.documentElement.scrollHeight - window.innerHeight;
bar.style.width = (h > 0 ? (window.scrollY / h) * 100 : 0) + '%';
}
window.addEventListener('scroll', tick, { passive: true });
tick();
})();
</script>
</body>
</html>`;
}
export function writeNews002PreviewFile(outputPath, options = {}) {
const context = buildNews002PreviewContext({ ...options, previewMode: options.previewMode ?? true });
const html = renderNews002PreviewHtml(context);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, html);
return { context, html, outputPath };
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename);
if (isMain) {
const OUT = process.argv[2] || path.join(process.cwd(), 'news002-preview.html');
const { context } = writeNews002PreviewFile(OUT, { previewMode: false });
console.log(`news002 preview written to ${OUT} (${context.slug})`);
}