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:
@@ -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}")
|
||||
Reference in New Issue
Block a user