- SKILL.md: 完整设计规范(色彩体系、微信公众号兼容规则、图片排版方案) - scripts/extract_wechat.py: 微信公众号文章抓取脚本 - scripts/build_hundun_html.py: 混沌风格 HTML 生成脚本(组件化) - examples/hundun-lead-article.html: 领教营二期排版示例 - resources/color-palette.md: 混沌品牌色彩参考
251 lines
8.9 KiB
Python
251 lines
8.9 KiB
Python
#!/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}")
|