Files
lidf a777665775 feat: 初始化混沌学园微信公众号 HTML 排版 Skill
- SKILL.md: 完整设计规范(色彩体系、微信公众号兼容规则、图片排版方案)
- scripts/extract_wechat.py: 微信公众号文章抓取脚本
- scripts/build_hundun_html.py: 混沌风格 HTML 生成脚本(组件化)
- examples/hundun-lead-article.html: 领教营二期排版示例
- resources/color-palette.md: 混沌品牌色彩参考
2026-07-22 12:06:55 +08:00

251 lines
8.9 KiB
Python
Raw Permalink 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 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}")