feat: 增加日报模板

This commit is contained in:
Wxw-Gu
2026-08-12 16:10:22 +08:00
parent cf3f115124
commit d439b4b749
36 changed files with 5068 additions and 559 deletions
@@ -0,0 +1,244 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */
const fs = require('fs')
const path = require('path')
const { JSDOM } = require('jsdom')
const templates = [
{
id: 'mobile-feed',
className: 'template-mobile-feed',
label: 'Mobile 01',
name: '微信信息流',
width: 390
},
{
id: 'mobile-magazine',
className: 'template-mobile-magazine',
label: 'Mobile 02',
name: 'AI Magazine',
width: 390
},
{
id: 'mobile-dashboard',
className: 'template-mobile-dashboard',
label: 'Mobile 03',
name: 'AI Command Center',
width: 390
},
{
id: 'desktop-workspace',
className: 'template-desktop-workspace',
label: 'Desktop 01',
name: '三栏 AI 工作台',
width: 1440
},
{
id: 'desktop-editorial',
className: 'template-desktop-editorial',
label: 'Desktop 02',
name: 'Editorial 科技日报',
width: 1440
}
]
const sourcePath = path.resolve(process.argv[2] || '')
const outputDir = path.resolve(
process.argv[3] || path.join(process.cwd(), '.codex', 'report-template-preview')
)
const templatePath = path.join(process.cwd(), 'resources', 'daily_report_templates.html')
if (!sourcePath || !fs.existsSync(sourcePath)) {
console.error(
'用法: node scripts/generate-report-template-preview.cjs <现有日报.html> [输出目录]'
)
process.exit(1)
}
if (!fs.existsSync(templatePath)) {
console.error(`模板资源不存在: ${templatePath}`)
process.exit(1)
}
const document = new JSDOM(fs.readFileSync(sourcePath, 'utf8')).window.document
const templateHtml = fs.readFileSync(templatePath, 'utf8')
const one = (selector) => document.querySelector(selector)
const html = (selector) => one(selector)?.innerHTML || ''
const childrenAfterTitle = (selector) => {
const section = one(selector)
if (!section) return ''
return Array.from(section.children)
.filter((child) => !child.classList.contains('section-title'))
.map((child) => child.outerHTML)
.join('')
}
const sectionClass = (selector) => {
const section = one(selector)
return !section || section.classList.contains('empty-section') ? 'empty-section' : ''
}
const statValues = Array.from(document.querySelectorAll('.stat')).map((node) => {
const strong = node.querySelector('strong')?.textContent?.trim()
if (strong) return strong
return node.textContent?.trim().match(/[\d.]+\s*h?/i)?.[0] || ''
})
const title = one('.hero h1')?.textContent?.trim() || document.title
const subItems = Array.from(document.querySelectorAll('.sub > *'))
const dateTimeRange = subItems[0]?.textContent?.trim() || ''
const reportDate = dateTimeRange.match(/\d{4}-\d{2}-\d{2}/)?.[0] || ''
const recordNote = one('.record-note')?.textContent?.trim() || ''
const overview = one('.overview')?.textContent?.trim() || ''
const footer = one('.footer')?.textContent?.trim().replace(/\s+/g, ' ') || ''
const generatedAt = footer.match(/生成时间[:]\s*([^基]+?)(?:基于|$)/)?.[1]?.trim() || ''
const activityLine = Array.from(document.querySelectorAll('.analytics > .card')).find((node) =>
node.textContent?.includes('活跃时间线')
)
const values = {
REPORT_TITLE: title,
REPORT_DATE: reportDate,
DATE_RANGE: '今天',
TIME_SPAN: statValues[2] || dateTimeRange,
HERO_SUMMARY: overview,
HERO_TAKEAWAY: '',
HERO_PENDING: '',
HERO_STATUS_LINE: '',
HERO_AVATARS: html('.avatar-grid'),
HERO_AVATAR_CLASS: sectionClass('.avatar-grid'),
MESSAGE_COUNT: statValues[0] || '',
ACTIVE_USERS: statValues[1] || '',
TOPIC_COUNT: statValues[3] || '',
RECORD_NOTE: recordNote,
GENERATED_AT: generatedAt,
FOOTER_NOTE: footer,
TOPIC_CARDS: childrenAfterTitle('.topics'),
IMPORTANT_MESSAGES: childrenAfterTitle('.messages'),
QUOTE_BLOCKS: childrenAfterTitle('.quotes'),
QA_CARDS: childrenAfterTitle('.qa'),
HEAT_BARS: Array.from(document.querySelectorAll('.analytics > .heat-row'))
.map((node) => node.outerHTML)
.join(''),
RANK_ITEMS: Array.from(document.querySelectorAll('.analytics .rank'))
.map((node) => node.outerHTML)
.join(''),
ACTIVITY_TIMELINE: activityLine?.textContent?.trim() || '',
CLOUD_TAGS: html('.cloud-tags'),
RESOURCE_ITEMS: childrenAfterTitle('.resources'),
TODO_CARDS: '',
UNRESOLVED_CARDS: '',
STORYLINE_CARDS: '',
REVERSAL_CARDS: '',
CHAIN_CARDS: '',
VISION_TITLE: 'AI 识别的图片精选',
VISION_CARDS: childrenAfterTitle('.vision'),
VOICE_CARDS: '',
VOICE_RANK_CARDS: '',
BADGE_CARDS: '',
KEYWORDS_EMPTY_CLASS: sectionClass('.cloud'),
ANALYTICS_EMPTY_CLASS: sectionClass('.analytics'),
MESSAGES_EMPTY_CLASS: sectionClass('.messages'),
TOPICS_EMPTY_CLASS: sectionClass('.topics'),
QUOTES_EMPTY_CLASS: sectionClass('.quotes'),
RESOURCES_EMPTY_CLASS: sectionClass('.resources'),
QA_EMPTY_CLASS: sectionClass('.qa'),
ACTIONS_EMPTY_CLASS: 'empty-section',
STORYLINES_EMPTY_CLASS: 'empty-section',
REVERSALS_EMPTY_CLASS: 'empty-section',
CHAINS_EMPTY_CLASS: 'empty-section',
VISION_EMPTY_CLASS: sectionClass('.vision'),
VOICE_EMPTY_CLASS: 'empty-section',
VOICE_RANK_EMPTY_CLASS: 'empty-section',
BADGES_EMPTY_CLASS: 'empty-section',
HERO_TAKEAWAY_EMPTY_CLASS: 'empty-section',
HERO_PENDING_EMPTY_CLASS: 'empty-section',
HERO_STATUS_EMPTY_CLASS: 'empty-section'
}
const escapeHtml = (value) =>
String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
const render = (template) => {
let result = templateHtml
const replacements = {
...values,
TEMPLATE_CLASS: template.className,
TEMPLATE_LABEL: template.label,
TEMPLATE_NAME: template.name
}
const htmlKeys = new Set([
'HERO_AVATARS',
'TOPIC_CARDS',
'IMPORTANT_MESSAGES',
'QUOTE_BLOCKS',
'QA_CARDS',
'HEAT_BARS',
'RANK_ITEMS',
'CLOUD_TAGS',
'RESOURCE_ITEMS',
'TODO_CARDS',
'UNRESOLVED_CARDS',
'STORYLINE_CARDS',
'REVERSAL_CARDS',
'CHAIN_CARDS',
'VISION_CARDS',
'VOICE_CARDS',
'VOICE_RANK_CARDS',
'BADGE_CARDS'
])
for (const [key, value] of Object.entries(replacements)) {
const safeValue = htmlKeys.has(key) ? String(value || '') : escapeHtml(value)
result = result.replaceAll(`{{${key}}}`, safeValue)
}
return result.replace(/\{\{[A-Z0-9_]+\}\}/g, '')
}
fs.mkdirSync(outputDir, { recursive: true })
for (const template of templates) {
fs.writeFileSync(path.join(outputDir, `${template.id}.html`), render(template), 'utf8')
}
const reportName = title.replace(/日报$/, '')
const buttons = templates
.map(
(template, index) => `
<button class="${index === 0 ? 'active' : ''}" data-src="${template.id}.html" data-width="${template.width}">
<span>${template.label}</span><b>${template.name}</b>
</button>`
)
.join('')
const indexHtml = `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(reportName)} · 五套日报模板预览</title>
<style>
*{box-sizing:border-box} body{margin:0;background:#e9eeeb;color:#17211d;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif}
header{position:sticky;top:0;z-index:2;padding:18px 24px 14px;background:rgba(255,255,255,.95);border-bottom:1px solid #d7e0da;backdrop-filter:blur(14px)}
h1{margin:0;font-size:20px} p{margin:5px 0 0;color:#68736c;font-size:12px}.toolbar{display:flex;gap:8px;overflow-x:auto;margin-top:14px;padding-bottom:2px}
button{display:grid;flex:0 0 auto;gap:2px;min-width:142px;padding:9px 12px;border:1px solid #d8e1db;border-radius:9px;background:#fff;color:#24332b;text-align:left;cursor:pointer}
button span{color:#708078;font-size:9px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}button b{font-size:12px}button.active{border-color:#16835b;background:#eaf6ef;color:#0d6744}
.stage{display:flex;justify-content:center;min-height:calc(100vh - 132px);padding:24px;overflow:auto}.frame-shell{width:390px;max-width:100%;overflow:hidden;border:1px solid #cbd6cf;border-radius:14px;background:white;box-shadow:0 18px 48px rgba(30,55,43,.15);transition:width .2s ease}
iframe{display:block;width:100%;height:calc(100vh - 180px);min-height:680px;border:0;background:white}
@media(max-width:640px){header{padding:14px 12px 12px}.stage{padding:12px}.frame-shell{border-radius:10px}button{min-width:132px}}
</style>
</head>
<body>
<header><h1>${escapeHtml(reportName)} · 五套日报模板</h1><p>同一份 2026-08-11 真实日报数据,可直接切换比较布局、排版和信息密度。</p><div class="toolbar">${buttons}</div></header>
<main class="stage"><div class="frame-shell"><iframe title="日报模板预览" src="mobile-feed.html"></iframe></div></main>
<script>
const frame=document.querySelector('iframe');const shell=document.querySelector('.frame-shell');
document.querySelectorAll('button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('button').forEach(item=>item.classList.remove('active'));button.classList.add('active');frame.src=button.dataset.src;shell.style.width=button.dataset.width+'px'}));
</script>
</body>
</html>`
fs.writeFileSync(path.join(outputDir, 'index.html'), indexHtml, 'utf8')
console.log(path.join(outputDir, 'index.html'))
+1 -23
View File
@@ -182,7 +182,6 @@ fullRequest.report.sectionMeta = {
qa: { enabled: true, importance: 0.62, confidence: 0.8, totalCount: 2, displayedCount: 2 },
storylines: { enabled: true, importance: 0.68, confidence: 0.74, totalCount: 2, displayedCount: 2 },
reversals: { enabled: true, importance: 0.55, confidence: 0.72, totalCount: 1, displayedCount: 1 },
gallery: { enabled: true, importance: 0.64, confidence: 0.82, totalCount: 2, displayedCount: 2 },
voices: { enabled: true, importance: 0.6, confidence: 0.83, totalCount: 2, displayedCount: 2 },
badges: { enabled: true, importance: 0.45, confidence: 0.68, totalCount: 2, displayedCount: 2 },
chains: { enabled: true, importance: 0.58, confidence: 0.72, totalCount: 1, displayedCount: 1 }
@@ -218,24 +217,7 @@ fullRequest.report.reversals = [
{ topic: '接口异常', initialView: '最初以为后端服务不稳定。', finalView: '最终判断更像缓存与配置问题。', note: '多轮验证后,排查方向明显收敛。' }
]
fullRequest.report.media = {
gallery: [
{
sender: '阿宇',
time: '09:52',
imageUrl: sampleImage,
note: '图片发出后,群里立刻围绕异常现象、返回结构和复现环境展开讨论。',
stats: '12 条后续消息 · 6 人接话',
inferenceLabel: '基于图片后的聊天上下文推断'
},
{
sender: '佩佩',
time: '17:14',
imageUrl: sampleImage,
note: '第二张图带起一轮轻松但有效的快速确认。',
stats: '5 条后续消息 · 3 人接话',
inferenceLabel: '基于图片后的聊天上下文推断'
}
],
gallery: [],
voiceHighlights: [
{ title: '语音输出王', sender: '老周', note: '共发送 3 条语音,累计 97 秒。' },
{ title: '连续发言时刻', sender: '阿宇', note: '16:32 连发 2 条语音,共 54 秒。' }
@@ -281,7 +263,6 @@ async function renderRequest(request, targetBase) {
const qaCards = (report.qa || []).map((item) => `<div class="qa-card"><b>Q${escapeHtml(item.question)}</b><div>A${escapeHtml(item.answer)}${item.answerer ? `${escapeHtml(item.answerer)}` : ''}</div></div>`).join('')
const storylineCards = (report.storylines || []).map((item) => `<div class="card storyline-card"><div class="topic-title-row"><h3>${escapeHtml(item.title)}</h3></div><div class="storyline-steps">${item.stages.map((stage) => `<div class="storyline-step"><span>${escapeHtml(stage.time)}</span><b>${escapeHtml(stage.event)}</b></div>`).join('')}</div>${item.result ? `<p class="muted">${escapeHtml(item.result)}</p>` : ''}</div>`).join('')
const reversalCards = (report.reversals || []).map((item) => `<div class="qa-card"><b>${escapeHtml(item.topic)}</b><div>最初:${escapeHtml(item.initialView)}</div><div>后来:${escapeHtml(item.finalView)}</div>${item.note ? `<div>${escapeHtml(item.note)}</div>` : ''}</div>`).join('')
const galleryCards = (report.media.gallery || []).map((item) => `<div class="gallery-card"><img class="gallery-image" src="${item.imageUrl}" alt=""><div class="gallery-body"><div class="important-meta"><b>${escapeHtml(item.sender)}</b><span>${escapeHtml(item.time)}</span></div>${item.stats ? `<div class="gallery-stats">${escapeHtml(item.stats)}</div>` : ''}<div class="important-text">${escapeHtml(item.note)}</div>${item.inferenceLabel ? `<div class="topic-meta">${escapeHtml(item.inferenceLabel)}</div>` : ''}</div></div>`).join('')
const voiceCards = (report.media.voiceHighlights || []).map((item) => `<div class="qa-card"><b>${escapeHtml(item.title)} · ${escapeHtml(item.sender)}</b><div>${escapeHtml(item.note)}</div></div>`).join('')
const voiceRankCards = (report.analytics.voiceLeaderboard || []).map((item, index) => `<div class="rank"><img src="${avatars[item.sender] || avatarSvg(item.sender[0], '#e5e7eb')}" alt=""><b>${index + 1}. ${escapeHtml(item.sender)}</b><span>${item.count} 条 · ${item.durationSec} 秒</span></div>`).join('')
const badgeCards = (report.media.funBadges || []).map((item) => `<div class="badge-card"><span class="tag">${escapeHtml(item.title)}</span><b>${escapeHtml(item.owner)}</b><p>${escapeHtml(item.note)}</p></div>`).join('')
@@ -343,9 +324,6 @@ async function renderRequest(request, targetBase) {
REVERSALS_EMPTY_CLASS: report.sectionMeta.reversals?.enabled ? '' : 'empty-section',
REVERSAL_CARDS: reversalCards,
REVERSALS_MORE_NOTE: '',
GALLERY_EMPTY_CLASS: report.sectionMeta.gallery?.enabled ? '' : 'empty-section',
GALLERY_CARDS: galleryCards,
GALLERY_MORE_NOTE: '',
VOICE_EMPTY_CLASS: report.sectionMeta.voices?.enabled ? '' : 'empty-section',
VOICE_CARDS: voiceCards,
VOICE_MORE_NOTE: '',