feat: 初始化混沌学园微信公众号 HTML 排版 Skill

- SKILL.md: 完整设计规范(色彩体系、微信公众号兼容规则、图片排版方案)
- scripts/extract_wechat.py: 微信公众号文章抓取脚本
- scripts/build_hundun_html.py: 混沌风格 HTML 生成脚本(组件化)
- examples/hundun-lead-article.html: 领教营二期排版示例
- resources/color-palette.md: 混沌品牌色彩参考
This commit is contained in:
2026-07-22 12:06:55 +08:00
commit a777665775
7 changed files with 1114 additions and 0 deletions
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
混沌学园风格微信公众号 HTML 生成脚本
用途:将提取的图片列表与文字内容,生成符合混沌品牌视觉体系的微信公众号 HTML 排版。
使用方法:
python3 build_hundun_html.py <extracted_content.txt> <输出HTML路径>
设计规范:
- 白底 (#FFFFFF) + 混沌黄 (#F5B700) + 石墨黑 (#181818)
- Hero 大图 + 双列瀑布流副图
- 100% 内联样式,微信公众号编辑器兼容
- 所有图片加 referrerpolicy="no-referrer"
详细规范请参阅 SKILL.md
"""
import sys
import re
import html as html_lib
from pathlib import Path
# ============================================================
# 混沌色彩体系常量
# ============================================================
COLOR_PRIMARY = "#F5B700" # 混沌黄
COLOR_SECONDARY = "#D99B00" # 深金(正文强调)
COLOR_DARK = "#181818" # 石墨黑
COLOR_DARK_SUB = "#242424" # 辅助深色(深色卡片子容器)
COLOR_BG = "#FFFFFF" # 原生白底
COLOR_CARD = "#F8F9FA" # 极浅灰(内容卡片)
COLOR_MODULE = "#FFFDF5" # 微黄底(课程模块卡片)
COLOR_TEXT = "#2B2B2B" # 主正文
COLOR_TEXT_ALT = "#333333" # 辅助正文
COLOR_TEXT_SUB = "#4E5969" # 二级正文
COLOR_BORDER = "#E5E6EB" # 边框灰
COLOR_DIVIDER = "#D9D9D9" # 虚线分割
FONT_STACK = (
"-apple-system, BlinkMacSystemFont, 'PingFang SC', "
"'Hiragino Sans GB', 'Microsoft YaHei', sans-serif"
)
def loadImages(filePath: str) -> dict:
"""
从 extracted_content.txt 加载图片索引映射。
:param filePath: 提取结果文件路径
:return: {索引: URL} 字典
"""
imgDict = {}
with open(filePath, 'r', encoding='utf-8') as f:
for line in f:
match = re.match(r'\[(\d+)\]\s+(https?://\S+)', line.strip())
if match:
idx = int(match.group(1))
url = html_lib.unescape(match.group(2))
imgDict[idx] = url
return imgDict
def renderHeroMasonry(imgDict: dict, imgIndices: list, maxSub: int = 6) -> str:
"""
渲染 Hero 大图 + 双列瀑布流副图。
:param imgDict: 全局图片索引字典
:param imgIndices: 本组图片索引列表
:param maxSub: 最大副图数量
:return: HTML 字符串
"""
urls = [imgDict[i] for i in imgIndices if i in imgDict]
if not urls:
return ""
heroUrl = urls[0]
subUrls = urls[1:1 + maxSub]
heroHtml = f'''
<section style="margin: 12px 0 10px 0;">
<img src="{heroUrl}" referrerpolicy="no-referrer"
style="width: 100%; height: auto; display: block; border-radius: 10px;
border: 1px solid {COLOR_BORDER};
box-shadow: 0 4px 12px rgba(0,0,0,0.08);" />
</section>
'''
if not subUrls:
return heroHtml
leftUrls = subUrls[0::2]
rightUrls = subUrls[1::2]
def buildCol(colUrls):
imgs = []
for u in colUrls:
imgs.append(
f'<img src="{u}" referrerpolicy="no-referrer"'
f' style="width: 100%; height: auto; display: block;'
f' border-radius: 6px; border: 1px solid {COLOR_BORDER};'
f' box-shadow: 0 2px 6px rgba(0,0,0,0.05);'
f' margin-bottom: 8px;" />'
)
return "\n".join(imgs)
leftHtml = buildCol(leftUrls)
rightHtml = buildCol(rightUrls)
masonryHtml = f'''
<section style="display: flex; justify-content: space-between;
align-items: flex-start; width: 100%;
box-sizing: border-box; margin-bottom: 10px;">
<section style="width: 48.5%; box-sizing: border-box;">
{leftHtml}
</section>
<section style="width: 48.5%; box-sizing: border-box;">
{rightHtml}
</section>
</section>
'''
return heroHtml + masonryHtml
def renderYearCard(year: str, title: str, desc: str, imgHtml: str,
highlight: bool = False) -> str:
"""
渲染时间线年份卡片。
:param year: 年份文字(如"2018年冬"
:param title: 小标题
:param desc: 描述段落
:param imgHtml: 内部图片 HTML(由 renderHeroMasonry 生成)
:param highlight: 是否用混沌黄边框高亮(用于最新年份)
:return: HTML 字符串
"""
borderStyle = f"2px solid {COLOR_PRIMARY}" if highlight else f"1px solid {COLOR_BORDER}"
badgeBg = COLOR_PRIMARY if highlight else COLOR_DARK
badgeColor = "#000000" if highlight else COLOR_PRIMARY
titleColor = COLOR_DARK
return f'''
<section style="margin-bottom: 24px; background-color: {COLOR_CARD};
padding: 16px; border-radius: 12px; border: {borderStyle};">
<section style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="background-color: {badgeBg}; color: {badgeColor};
font-size: 12px; font-weight: 800; padding: 2px 8px;
border-radius: 4px; margin-right: 8px;">{year}</span>
<h3 style="font-size: 16px; font-weight: 700; color: {titleColor}; margin: 0;">{title}</h3>
</section>
<p style="font-size: 14px; line-height: 1.7; color: {COLOR_TEXT_SUB}; margin: 0 0 10px 0;">
{desc}
</p>
{imgHtml}
</section>
'''
def renderModuleCard(name: str, dateRange: str, theme: str, desc: str) -> str:
"""
渲染课程模块卡片(微黄底)。
:param name: 模块名称(如"一模块 · 亮剑"
:param dateRange: 时间(如"2026.8.29 - 8.30"
:param theme: 主题文字
:param desc: 描述文字
:return: HTML 字符串
"""
return f'''
<section style="margin-bottom: 14px; background-color: {COLOR_MODULE};
padding: 16px; border-radius: 12px;
border: 1px solid {COLOR_PRIMARY};">
<section style="display: flex; justify-content: space-between;
align-items: center; margin-bottom: 8px;">
<h3 style="font-size: 16px; font-weight: 800; color: {COLOR_DARK}; margin: 0;">{name}</h3>
<span style="font-size: 12px; color: {COLOR_DARK}; background: {COLOR_PRIMARY};
font-weight: 700; padding: 2px 8px; border-radius: 4px;">{dateRange}</span>
</section>
<p style="font-size: 13px; color: {COLOR_SECONDARY}; margin: 0 0 6px 0; font-weight: 700;">{theme}</p>
<p style="font-size: 14px; color: {COLOR_TEXT_SUB}; margin: 0; line-height: 1.6;">{desc}</p>
</section>
'''
def renderRequirementCard(icon: str, title: str, items: list) -> str:
"""
渲染招募要求卡片。
:param icon: emoji 图标
:param title: 卡片标题(如"1. 产业研究能力"
:param items: 列表项文字列表
:return: HTML 字符串
"""
liHtml = "\n".join(f" <li>{item}</li>" for item in items)
return f'''
<section style="margin-bottom: 14px; background-color: {COLOR_CARD};
padding: 16px; border-radius: 12px;
border-left: 4px solid {COLOR_DARK};
border-top: 1px solid {COLOR_BORDER};
border-right: 1px solid {COLOR_BORDER};
border-bottom: 1px solid {COLOR_BORDER};">
<h3 style="font-size: 15px; font-weight: 700; color: {COLOR_DARK};
margin: 0 0 10px 0;">{icon} {title}</h3>
<ul style="margin: 0; padding-left: 18px; color: {COLOR_TEXT_SUB};
font-size: 14px; line-height: 1.7;">
{liHtml}
</ul>
</section>
'''
def wrapPage(innerHtml: str) -> str:
"""
将内部 HTML 包裹为完整的微信公众号页面容器。
:param innerHtml: 内部所有模块的 HTML
:return: 完整的 HTML 片段
"""
return f'''<section style="max-width: 667px; margin: 0 auto; box-sizing: border-box;
background-color: {COLOR_BG}; color: {COLOR_TEXT};
font-family: {FONT_STACK}; padding: 12px 4px;">
{innerHtml}
</section>'''
# ============================================================
# 示例用法(可按需修改 main 逻辑适配不同文章)
# ============================================================
if __name__ == "__main__":
if len(sys.argv) < 3:
print("用法: python3 build_hundun_html.py <extracted_content.txt> <输出HTML路径>")
print("示例: python3 build_hundun_html.py ./output/extracted_content.txt ./output/article.html")
sys.exit(1)
inputFile = sys.argv[1]
outputFile = sys.argv[2]
imgDict = loadImages(inputFile)
print(f"📦 已加载 {len(imgDict)} 张图片索引")
# 示例:生成一个简单的年份卡片
imgHtml = renderHeroMasonry(imgDict, [1, 2, 3, 4, 5])
yearCard = renderYearCard("2024年", "示例年份", "这是一段示例描述文字。", imgHtml)
fullHtml = wrapPage(yearCard)
Path(outputFile).write_text(fullHtml, encoding='utf-8')
print(f"✅ HTML 已生成: {outputFile}")