aesthetic-patterns.template.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. // ==============================================================================
  2. // 精美 HTML 报告 · 视觉模式库(20+ copy-paste 小组件)
  3. // ==============================================================================
  4. // 使用方法:
  5. // 1. 不要整文件 require,而是查阅 → 复制对应 pattern 到你的 <品类>-components.js
  6. // 2. 每个 pattern 都有:① 适用场景 ② 输入参数 ③ HTML 范式 ④ 变体
  7. // 3. 所有 pattern 都假设 BRAND.MAIN 是品类主色
  8. // ==============================================================================
  9. // 公共依赖(你需要在实际 components.js 里定义)
  10. const BRAND_STUB = {
  11. MAIN: '#4A90E2',
  12. CORAL: '#FF6B6B',
  13. GOLD: '#D4AF37',
  14. JIANG_GREEN: '#1A7C5F',
  15. };
  16. function esc(s) {
  17. return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  18. }
  19. function fmtLikes(n) {
  20. if (!n) return '0';
  21. if (n >= 10000) return (n / 10000).toFixed(1) + 'w';
  22. if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
  23. return String(n);
  24. }
  25. // ============================================================
  26. // P01 · Eyebrow · 小标签(所有 section head 开头必有)
  27. // ============================================================
  28. // 适用:section head / insight / decision list 前的小标签
  29. // 输入:text, color, letterSpacing
  30. function renderEyebrow({ text, color = BRAND_STUB.MAIN, letterSpacing = '0.2em' }) {
  31. return `<div class="text-mono text-xs"
  32. style="color:${color}; letter-spacing:${letterSpacing}; font-weight:700; margin-bottom:10px;">
  33. ${esc(text)}
  34. </div>`;
  35. }
  36. // ============================================================
  37. // P02 · Section Head · 三段式标题(eyebrow + title + subtitle)
  38. // ============================================================
  39. // 适用:每子章节开头
  40. // 输入:eyebrow, title(带 HTML, 允许 <strong>), subtitle, color
  41. function renderSectionHead({ eyebrow, title, subtitle, color = BRAND_STUB.MAIN }) {
  42. return `<div style="margin-bottom:20px;">
  43. ${eyebrow ? renderEyebrow({ text: eyebrow, color }) : ''}
  44. <h2 style="font-family:var(--font-head); font-weight:800; font-size:1.9rem; line-height:1.3;
  45. color:var(--text-1); margin:0 0 12px 0;">${title || ''}</h2>
  46. ${subtitle ? `<div style="color:var(--text-2); font-size:0.95rem; line-height:1.75; max-width:900px;">${subtitle}</div>` : ''}
  47. </div>`;
  48. }
  49. // ============================================================
  50. // P03 · VOC Card · 证据卡(核心组件 · 全报告贯穿)
  51. // ============================================================
  52. // 适用:所有子章节 ≥ 3 张
  53. // 输入:item { platform, nickname, ip, content, likes }, accent, maxChars
  54. function renderVocCard(item, opts = {}) {
  55. if (!item) return '';
  56. const accent = opts.accent || BRAND_STUB.MAIN;
  57. const maxChars = opts.maxChars || 200;
  58. const content = String(item.content || '').slice(0, maxChars);
  59. const more = (item.content || '').length > maxChars ? '…' : '';
  60. return `<div style="padding:14px 16px; background:var(--bg-1); border:1px solid var(--border);
  61. border-left:3px solid ${accent}; border-radius:6px;">
  62. <div style="display:flex; justify-content:space-between; gap:10px; margin-bottom:8px; font-size:0.78rem;">
  63. <div style="flex:1; min-width:0;">
  64. <span class="text-mono" style="color:${accent}; font-weight:700; letter-spacing:0.1em;">
  65. ${(item.platform || 'XHS').toUpperCase()}
  66. </span>
  67. <span style="color:var(--text-3);"> · ${esc(item.nickname || '匿名')}</span>
  68. ${item.ip ? `<span style="color:var(--text-3);"> · ${esc(item.ip)}</span>` : ''}
  69. </div>
  70. <div style="color:${accent}; font-family:var(--font-mono); font-weight:700; white-space:nowrap;">
  71. ♥${fmtLikes(item.likes)}
  72. </div>
  73. </div>
  74. <div style="color:var(--text-1); font-size:0.88rem; line-height:1.7;">
  75. 「${esc(content)}${more}」
  76. </div>
  77. </div>`;
  78. }
  79. // ============================================================
  80. // P04 · VOC Card Compact · 紧凑证据卡(3 列布局用)
  81. // ============================================================
  82. function renderVocCardCompact(item, opts = {}) {
  83. if (!item) return '';
  84. const accent = opts.accent || BRAND_STUB.MAIN;
  85. const maxChars = opts.maxChars || 120;
  86. const content = String(item.content || '').slice(0, maxChars);
  87. const more = (item.content || '').length > maxChars ? '…' : '';
  88. return `<div style="padding:10px 12px; background:var(--bg-1); border-left:3px solid ${accent}; border-radius:4px;">
  89. <div style="font-size:0.72rem; color:var(--text-3); margin-bottom:6px;">
  90. <span class="text-mono" style="color:${accent}; font-weight:700;">${(item.platform || 'XHS').toUpperCase()}</span>
  91. · ${esc(item.nickname || '匿名')} · ♥${fmtLikes(item.likes)}
  92. </div>
  93. <div style="color:var(--text-1); font-size:0.82rem; line-height:1.6;">「${esc(content)}${more}」</div>
  94. </div>`;
  95. }
  96. // ============================================================
  97. // P05 · StatStrip · KPI 数字条(4-6 个并列数字)
  98. // ============================================================
  99. // 适用:Cover / 章节 hero / 章末总结
  100. // 输入:stats = [{ num, label, delta, color }]
  101. function renderStatStrip(stats, opts = {}) {
  102. const accent = opts.accent || BRAND_STUB.MAIN;
  103. const cols = stats.length;
  104. return `<div class="grid" style="grid-template-columns:repeat(${cols},1fr); gap:12px; margin:20px 0;">
  105. ${stats.map((s) => `
  106. <div style="padding:18px; background:${s.color || accent}0C; border:1px solid ${s.color || accent}33; border-radius:8px; text-align:center;">
  107. <div style="color:${s.color || accent}; font-family:var(--font-head); font-weight:900; font-size:2rem; line-height:1;">${esc(s.num)}</div>
  108. <div style="color:var(--text-2); font-size:0.82rem; margin-top:6px;">${esc(s.label)}</div>
  109. ${s.delta ? `<div style="color:${s.color || accent}; font-size:0.72rem; font-family:var(--font-mono); margin-top:4px;">${esc(s.delta)}</div>` : ''}
  110. </div>`).join('')}
  111. </div>`;
  112. }
  113. // ============================================================
  114. // P06 · Single Big Stat · 巨型单数字(镇场)
  115. // ============================================================
  116. // 适用:Cover 核心数字 / 章节 hero 唯一金句数字
  117. function renderBigStat({ num, label, delta, color = BRAND_STUB.MAIN }) {
  118. return `<div style="padding:32px 40px; background:${color}0C; border:1px solid ${color}33; border-radius:12px; text-align:center; margin:24px 0;">
  119. <div style="color:${color}; font-family:var(--font-head); font-weight:900; font-size:4rem; line-height:1; letter-spacing:-0.02em;">${esc(num)}</div>
  120. <div style="color:var(--text-1); font-size:1rem; margin-top:12px; font-weight:600;">${esc(label)}</div>
  121. ${delta ? `<div style="color:${color}; font-size:0.82rem; font-family:var(--font-mono); margin-top:6px;">${esc(delta)}</div>` : ''}
  122. </div>`;
  123. }
  124. // ============================================================
  125. // P07 · Insight Hero · 金句观点卡(渐变背景)
  126. // ============================================================
  127. // 适用:章节核心洞察 · 每章 1-2 处
  128. function renderInsightHero({ eyebrow, title, subtitle, color = BRAND_STUB.MAIN }) {
  129. return `<div style="padding:24px 28px; background:linear-gradient(135deg, ${color}14, ${color}04);
  130. border:2px solid ${color}44; border-radius:12px; margin:20px 0;">
  131. ${eyebrow ? `<div class="text-mono text-xs" style="color:${color}; letter-spacing:0.2em; margin-bottom:12px; font-weight:700;">${esc(eyebrow)}</div>` : ''}
  132. <div style="color:var(--text-1); font-size:1.1rem; line-height:1.8;">${title || ''}</div>
  133. ${subtitle ? `<div style="color:var(--text-2); font-size:0.9rem; line-height:1.7; margin-top:10px;">${subtitle}</div>` : ''}
  134. </div>`;
  135. }
  136. // ============================================================
  137. // P08 · Decision List · 落地决策列表(每子章收尾)
  138. // ============================================================
  139. // 适用:每子章节最后 · 2 列 grid
  140. // 输入:items = ['<strong>P0 · 动词</strong>:具体动作', ...]
  141. function renderDecisionList(items, opts = {}) {
  142. const accent = opts.accent || BRAND_STUB.MAIN;
  143. const title = opts.title || '落地决策';
  144. const columns = opts.columns || 2;
  145. return `<div style="margin-top:24px; padding:20px 24px; background:${accent}08; border:1px solid ${accent}33; border-radius:8px;">
  146. <div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.18em; margin-bottom:14px; font-weight:700;">🎯 ${esc(title)}</div>
  147. <div class="grid" style="grid-template-columns:repeat(${columns},1fr); gap:10px;">
  148. ${items.map((x) => `
  149. <div style="padding:10px 14px; background:var(--bg-1); border-left:3px solid ${accent}; border-radius:4px; color:var(--text-1); font-size:0.86rem; line-height:1.65;">
  150. ${x}
  151. </div>`).join('')}
  152. </div>
  153. </div>`;
  154. }
  155. // ============================================================
  156. // P09 · Compare Table · 对比表格
  157. // ============================================================
  158. // 适用:竞品 / 本品 vs 竞品矩阵
  159. // 输入:headers = [], rows = [[cell, cell, ...], ...]
  160. // cell = string | { html, color, bold }
  161. function renderCompareTable({ title, headers = [], rows = [], accent = BRAND_STUB.MAIN }) {
  162. return `<div style="margin:20px 0;">
  163. ${title ? `<div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.18em; margin-bottom:12px; font-weight:700;">${esc(title)}</div>` : ''}
  164. <div style="overflow-x:auto;">
  165. <table style="width:100%; border-collapse:collapse; font-size:0.86rem;">
  166. <thead>
  167. <tr style="border-bottom:2px solid ${accent}44;">
  168. ${headers.map((h) => `<th style="text-align:left; padding:10px 12px; color:var(--text-3); font-weight:700; font-size:0.78rem; letter-spacing:0.05em;">${esc(h)}</th>`).join('')}
  169. </tr>
  170. </thead>
  171. <tbody>
  172. ${rows.map((row) => `
  173. <tr style="border-bottom:1px solid var(--border);">
  174. ${row.map((cell) => {
  175. const c = typeof cell === 'object' ? cell : { html: cell };
  176. return `<td style="padding:10px 12px; color:${c.color || 'var(--text-1)'}; ${c.bold ? 'font-weight:700;' : ''}">${c.html || c}</td>`;
  177. }).join('')}
  178. </tr>`).join('')}
  179. </tbody>
  180. </table>
  181. </div>
  182. </div>`;
  183. }
  184. // ============================================================
  185. // P10 · Matrix 2x2 · 无人地带四象限
  186. // ============================================================
  187. // 适用:Ch6 竞品 × 无人地带定位
  188. // 关键纪律:第 4 象限(本品独占)必须品类色高亮
  189. function renderMatrix2x2({ xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants = {}, accent = BRAND_STUB.MAIN }) {
  190. const renderQ = (q) => {
  191. if (!q) return '<div></div>';
  192. return `<div style="padding:16px; background:${(q.color || accent)}0C; border:1px solid ${(q.color || accent)}33; border-radius:6px;">
  193. <div class="text-mono text-xs" style="color:${q.color || accent}; font-weight:700; margin-bottom:8px;">${q.title || ''}</div>
  194. <ul style="margin:0; padding-left:18px; color:var(--text-1); font-size:0.82rem; line-height:1.7;">
  195. ${(q.items || []).map((i) => `<li>${i}</li>`).join('')}
  196. </ul>
  197. ${q.note ? `<div style="color:var(--text-3); font-size:0.76rem; margin-top:8px; font-style:italic;">${esc(q.note)}</div>` : ''}
  198. </div>`;
  199. };
  200. return `<div style="margin:24px 0;">
  201. <div style="display:grid; grid-template-columns:auto 1fr 1fr; gap:12px; align-items:stretch;">
  202. <div></div>
  203. <div style="text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700;">${esc(xLow || '')}</div>
  204. <div style="text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700;">${esc(xHigh || '')}</div>
  205. <div style="writing-mode:vertical-rl; text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700; padding:8px 0;">${esc(yHigh || '')}</div>
  206. ${renderQ(quadrants.q2)}
  207. ${renderQ(quadrants.q1)}
  208. <div style="writing-mode:vertical-rl; text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700; padding:8px 0;">${esc(yLow || '')}</div>
  209. ${renderQ(quadrants.q3)}
  210. ${renderQ(quadrants.q4)}
  211. </div>
  212. <div style="text-align:center; color:${accent}; font-family:var(--font-mono); font-size:0.82rem; letter-spacing:0.12em; margin-top:8px;">
  213. ← ${esc(xLabel || '')} → ${yLabel ? '· Y: ' + esc(yLabel) : ''}
  214. </div>
  215. </div>`;
  216. }
  217. // ============================================================
  218. // P11 · Theory Box · 理论映射盒(KANO / JTBD / 3C)
  219. // ============================================================
  220. function renderTheoryBox({ theory, mapping, accent = BRAND_STUB.MAIN }) {
  221. return `<div style="padding:16px 18px; background:${accent}08; border-left:3px solid ${accent}; border-radius:8px;">
  222. ${theory ? `<div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.15em; font-weight:700; margin-bottom:10px;">${esc(theory)}</div>` : ''}
  223. ${mapping || ''}
  224. </div>`;
  225. }
  226. // ============================================================
  227. // P12 · Finding Card · 发现洞察卡(icon + 标题 + 正文)
  228. // ============================================================
  229. // 适用:3-4 列并排的小发现
  230. function renderFindingCard({ icon, title, body, accent = BRAND_STUB.MAIN }) {
  231. return `<div style="padding:16px 18px; background:var(--bg-1); border:1px solid ${accent}33; border-left:4px solid ${accent}; border-radius:8px;">
  232. <div style="display:flex; align-items:flex-start; gap:12px;">
  233. ${icon ? `<div style="font-size:1.6rem; line-height:1;">${icon}</div>` : ''}
  234. <div style="flex:1;">
  235. ${title ? `<div style="color:var(--text-1); font-weight:700; font-size:0.95rem; margin-bottom:6px;">${title}</div>` : ''}
  236. <div style="color:var(--text-2); font-size:0.84rem; line-height:1.7;">${body || ''}</div>
  237. </div>
  238. </div>
  239. </div>`;
  240. }
  241. // ============================================================
  242. // P13 · Progress Bar · 横向比例条(单维度对比)
  243. // ============================================================
  244. // 适用:竞品市占 / 假设覆盖率 / 渠道占比
  245. function renderProgressBar({ label, value, max = 100, accent = BRAND_STUB.MAIN, suffix = '%' }) {
  246. const pct = Math.min(100, (value / max) * 100);
  247. return `<div style="margin:8px 0;">
  248. <div style="display:flex; justify-content:space-between; font-size:0.78rem; margin-bottom:4px;">
  249. <span style="color:var(--text-2);">${esc(label)}</span>
  250. <span class="text-mono" style="color:${accent}; font-weight:700;">${value}${suffix}</span>
  251. </div>
  252. <div style="height:6px; background:var(--bg-2); border-radius:3px; overflow:hidden;">
  253. <div style="height:100%; width:${pct}%; background:linear-gradient(90deg, ${accent}, ${accent}cc);"></div>
  254. </div>
  255. </div>`;
  256. }
  257. // ============================================================
  258. // P14 · Inline Bar Chart · 小柱图(纯 div · 无依赖)
  259. // ============================================================
  260. // 适用:多项对比 5-10 个
  261. function renderInlineBars({ items, max, accent = BRAND_STUB.MAIN, height = 80 }) {
  262. const maxVal = max || Math.max(...items.map((i) => i.value));
  263. return `<div style="display:flex; align-items:flex-end; gap:6px; height:${height}px; margin:16px 0;">
  264. ${items.map((i) => {
  265. const pct = Math.max(4, (i.value / maxVal) * 100);
  266. return `<div style="flex:1; text-align:center;">
  267. <div style="height:${pct}%; background:${i.color || accent}; border-radius:2px 2px 0 0; margin-top:auto; position:relative;">
  268. <div class="text-mono" style="position:absolute; top:-18px; left:50%; transform:translateX(-50%); font-size:0.72rem; color:${i.color || accent}; white-space:nowrap;">${i.value}</div>
  269. </div>
  270. <div style="font-size:0.72rem; color:var(--text-3); margin-top:6px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${esc(i.label)}</div>
  271. </div>`;
  272. }).join('')}
  273. </div>`;
  274. }
  275. // ============================================================
  276. // P15 · Tag Cloud · 标签云(高频词)
  277. // ============================================================
  278. function renderTagCloud({ tags, accent = BRAND_STUB.MAIN }) {
  279. return `<div style="display:flex; flex-wrap:wrap; gap:8px; margin:16px 0;">
  280. ${tags.map((t) => {
  281. const size = t.count ? Math.min(1.1, 0.78 + Math.log10(t.count) * 0.1) : 0.82;
  282. const alpha = t.count ? Math.min(44, 14 + t.count) : 28;
  283. return `<span class="text-mono" style="padding:4px 10px; background:${accent}${alpha.toString(16).padStart(2, '0')}; border:1px solid ${accent}44; border-radius:12px; font-size:${size}rem; color:${accent};">#${esc(t.label)}${t.count ? ` · ${t.count}` : ''}</span>`;
  284. }).join('')}
  285. </div>`;
  286. }
  287. // ============================================================
  288. // P16 · Flow Diagram · 决策链流程图(纯 div + Unicode 箭头)
  289. // ============================================================
  290. function renderFlow({ steps, accent = BRAND_STUB.MAIN }) {
  291. return `<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:20px 0;">
  292. ${steps.map((s, i) => {
  293. const box = `<div style="padding:8px 14px; background:${accent}14; border:1px solid ${accent}; border-radius:6px; color:var(--text-1); font-size:0.86rem; white-space:nowrap;">${esc(s)}</div>`;
  294. const arrow = i < steps.length - 1 ? `<div style="color:${accent}; font-size:1.2rem; font-weight:700;">→</div>` : '';
  295. return box + arrow;
  296. }).join('')}
  297. </div>`;
  298. }
  299. // ============================================================
  300. // P17 · Platform Badge · 平台徽章(带图标)
  301. // ============================================================
  302. function renderPlatformBadge(platform, count, color) {
  303. const icons = { xhs: '📕', douyin: '🎵', weibo: '📱', jd: '🛒', tmall: '🛍', amazon: '📦' };
  304. const labels = { xhs: 'XHS', douyin: 'DOUYIN', weibo: 'WEIBO', jd: 'JD', tmall: 'TMALL', amazon: 'AMZN' };
  305. return `<span class="text-mono"
  306. style="padding:4px 10px; background:var(--bg-2); border:1px solid ${color || 'var(--border)'};
  307. border-radius:4px; font-size:0.78rem; color:${color || 'var(--text-2)'};
  308. letter-spacing:0.08em; font-weight:700;">
  309. ${icons[platform] || '📊'} ${labels[platform] || platform.toUpperCase()}${count !== undefined ? ` · ${count}` : ''}
  310. </span>`;
  311. }
  312. // ============================================================
  313. // P18 · Hypothesis Tag · 假设标签(H1-H8)
  314. // ============================================================
  315. function renderHypothesisTag(h, desc, color) {
  316. return `<div style="display:inline-flex; align-items:center; gap:8px; padding:6px 12px; background:${color || BRAND_STUB.MAIN}14; border:1px solid ${color || BRAND_STUB.MAIN}44; border-radius:4px; margin:4px 4px 4px 0;">
  317. <span class="text-mono" style="color:${color || BRAND_STUB.MAIN}; font-weight:800; font-size:0.78rem;">${h}</span>
  318. <span style="color:var(--text-2); font-size:0.82rem;">${esc(desc || '')}</span>
  319. </div>`;
  320. }
  321. // ============================================================
  322. // P19 · Page Indicator · 右下角页码(固定定位)
  323. // ============================================================
  324. // 使用:作为独立 HTML 节点放在 body 末尾,配合 JS 更新
  325. function renderPageIndicator() {
  326. return `<div id="page-indicator"
  327. style="position:fixed; right:20px; bottom:20px; padding:6px 12px;
  328. background:var(--bg-2); border:1px solid var(--border); border-radius:4px;
  329. font-family:var(--font-mono); font-size:0.78rem; color:var(--text-3); z-index:50;">
  330. 1 / 1
  331. </div>`;
  332. }
  333. // 配套 JS · 嵌到 NAV_JS 末尾
  334. const PAGE_INDICATOR_JS = `
  335. (function(){
  336. const indicator = document.getElementById('page-indicator');
  337. const sections = document.querySelectorAll('.report-section');
  338. if (!indicator || !sections.length) return;
  339. indicator.textContent = '1 / ' + sections.length;
  340. const observer = new IntersectionObserver((entries) => {
  341. for (const e of entries) {
  342. if (e.isIntersecting && e.intersectionRatio > 0.5) {
  343. const idx = [...sections].indexOf(e.target) + 1;
  344. indicator.textContent = idx + ' / ' + sections.length;
  345. break;
  346. }
  347. }
  348. }, { threshold: [0.5] });
  349. sections.forEach((s) => observer.observe(s));
  350. })();
  351. `;
  352. // ============================================================
  353. // P20 · Nav JS · 键盘导航(标配)
  354. // ============================================================
  355. // 嵌到 render.js 的 NAV_JS 常量里
  356. const NAV_JS = `
  357. (function() {
  358. const sections = document.querySelectorAll('.report-section');
  359. let idx = 0;
  360. function go(i) {
  361. idx = Math.max(0, Math.min(sections.length - 1, i));
  362. sections[idx].scrollIntoView({ behavior: 'smooth' });
  363. if (sections[idx].id) history.replaceState(null, '', '#' + sections[idx].id);
  364. }
  365. document.addEventListener('keydown', (e) => {
  366. if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); go(idx + 1); }
  367. if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); go(idx - 1); }
  368. if (e.key === 'Home') go(0);
  369. if (e.key === 'End') go(sections.length - 1);
  370. });
  371. if (location.hash) {
  372. const i = [...sections].findIndex((s) => '#' + s.id === location.hash);
  373. if (i >= 0) idx = i;
  374. }
  375. })();
  376. `;
  377. // ============================================================
  378. // P21 · Gradient Cover Background(Cover 专用)
  379. // ============================================================
  380. const COVER_STYLE = `
  381. .cover {
  382. background: linear-gradient(180deg, #0A0A0A 0%, #111 100%);
  383. position: relative;
  384. overflow: hidden;
  385. }
  386. .cover::before {
  387. content: '';
  388. position: absolute;
  389. top: -50%;
  390. right: -10%;
  391. width: 60%;
  392. height: 200%;
  393. background: radial-gradient(circle, BRAND_MAIN_ALPHA 0%, transparent 70%);
  394. opacity: 0.3;
  395. pointer-events: none;
  396. }
  397. `;
  398. // ============================================================
  399. // P22 · Print Friendly · 打印友好
  400. // ============================================================
  401. const PRINT_CSS = `
  402. @media print {
  403. .report-section {
  404. min-height: auto;
  405. page-break-after: always;
  406. padding: 20px;
  407. border: none;
  408. }
  409. body { background: white; color: black; }
  410. .report-section, .section-inner { background: white !important; }
  411. [style*="color:var(--text-1)"] { color: black !important; }
  412. [style*="color:var(--text-2)"] { color: #333 !important; }
  413. [style*="color:var(--text-3)"] { color: #666 !important; }
  414. #page-indicator { display: none; }
  415. }
  416. `;
  417. // ============================================================
  418. // P23 · Mobile Degradation · 移动端降级
  419. // ============================================================
  420. const MOBILE_CSS = `
  421. @media (max-width: 768px) {
  422. .report-section { padding: 24px 16px; }
  423. .section-inner { max-width: 100%; }
  424. .grid { grid-template-columns: 1fr !important; gap: 10px !important; }
  425. .cover h1 { font-size: 2rem !important; }
  426. .divider h1 { font-size: 2.8rem !important; }
  427. h2 { font-size: 1.4rem !important; }
  428. [style*="font-size:4rem"] { font-size: 2.4rem !important; }
  429. [style*="font-size:5rem"] { font-size: 2.8rem !important; }
  430. [style*="font-size:3.6rem"] { font-size: 2rem !important; }
  431. }
  432. `;
  433. // ============================================================
  434. // 导出(只是作为参考,实际使用时 copy-paste 到你的 components.js)
  435. // ============================================================
  436. module.exports = {
  437. // 基础工具
  438. esc,
  439. fmtLikes,
  440. // P01-P23 视觉模式
  441. renderEyebrow, // P01
  442. renderSectionHead, // P02
  443. renderVocCard, // P03
  444. renderVocCardCompact, // P04
  445. renderStatStrip, // P05
  446. renderBigStat, // P06
  447. renderInsightHero, // P07
  448. renderDecisionList, // P08
  449. renderCompareTable, // P09
  450. renderMatrix2x2, // P10
  451. renderTheoryBox, // P11
  452. renderFindingCard, // P12
  453. renderProgressBar, // P13
  454. renderInlineBars, // P14
  455. renderTagCloud, // P15
  456. renderFlow, // P16
  457. renderPlatformBadge, // P17
  458. renderHypothesisTag, // P18
  459. renderPageIndicator, // P19
  460. NAV_JS, // P20
  461. PAGE_INDICATOR_JS,
  462. COVER_STYLE, // P21
  463. PRINT_CSS, // P22
  464. MOBILE_CSS, // P23
  465. };