apply_theme.py 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. # -*- coding: utf-8 -*-
  2. """给导入的各仿真页注入统一外壳: 顶栏 + 配色 token + 返回链接。
  3. 只做**加法**(追加一段 style + 在 body 开头插一个顶栏), 不改页面原有 DOM 与 JS,
  4. 所以不会破坏各页自己的 canvas 绘制逻辑。canvas 内部配色由各页 JS 决定, 本层不动 —— 这是已知边界。"""
  5. import pathlib, re, sys
  6. SIM = pathlib.Path("/Users/yuanying/guanlan-rudong-v2/outputs/rudong/windscada/_demo/sim")
  7. MARK = "guanlan-shell-v1"
  8. PAGES = {
  9. "涡激共振模拟器.html": ("涡激共振模拟器", "机理通识 · 十二款机型预设"),
  10. "涡激实测核验.html": ("涡激实测核验", "实测 · 九台秒级"),
  11. "偏航滑移事件回放.html": ("偏航滑移事件回放", "实测 · 单台"),
  12. "故障分布三维视图.html": ("故障分布三维视图", "实测 · 机组 × 系统 × 月份"),
  13. "变流器故障仿真.html": ("变流器故障仿真", "注入 · 波形为合成"),
  14. "变桨故障仿真.html": ("变桨故障仿真", "注入 · 波形为合成"),
  15. "整机联调台_1.5MW.html": ("整机联调台 · 1.5 MW", "正演 · 控制核心层参数"),
  16. "整机联调台_4MW.html": ("整机联调台 · 4 MW", "正演 · 控制核心层参数"),
  17. }
  18. SHELL_CSS = """
  19. <style id="%s">
  20. :root{--gl-bg:#edf7f9;--gl-ink:#133047;--gl-ink2:#28536b;--gl-ink3:#61788b;
  21. --gl-line:#cbe2eb;--gl-spec:#0e8898;--gl-meas:#2077a8}
  22. .gl-top{position:sticky;top:0;z-index:9999;display:flex;align-items:center;gap:14px;
  23. padding:11px 26px;background:linear-gradient(110deg,#10384d,#0e6470);color:#fff;
  24. font:14px/1.5 "PingFang SC","Hiragino Sans GB","Microsoft YaHei",ui-sans-serif,system-ui,sans-serif;
  25. box-shadow:0 2px 12px rgba(12,80,102,.16)}
  26. .gl-top .gl-logo{width:30px;height:30px;border-radius:8px;background:rgba(255,255,255,.16);
  27. display:grid;place-items:center;font-weight:700;font-size:15px;flex:0 0 auto}
  28. .gl-top b{font-size:14.5px;letter-spacing:.02em}
  29. .gl-top small{display:block;font:11px "SF Mono",Menlo,Consolas,monospace;opacity:.75;letter-spacing:.1em}
  30. .gl-top .gl-back{margin-left:auto;color:#e8fbff;text-decoration:none;border:1px solid #a7dce3;
  31. border-radius:999px;padding:6px 14px;font-size:12.5px;white-space:nowrap}
  32. .gl-top .gl-back:hover{background:rgba(255,255,255,.12)}
  33. body{background:var(--gl-bg)!important}
  34. </style>
  35. """ % MARK
  36. def shell(title, sub):
  37. return (f'<div class="gl-top"><div class="gl-logo">观</div>'
  38. f'<div><b>{title}</b><small>{sub}</small></div>'
  39. f'<a class="gl-back" href="./仿真集成台.html">← 返回仿真集成台</a></div>')
  40. def apply(path, title, sub):
  41. s = path.read_text(encoding="utf-8")
  42. if MARK in s:
  43. # 幂等剥离。注意: 不能用 <div class="gl-top">.*?</div></div> —— 非贪婪会一路扫进页面正文,
  44. # 把内容一起删掉(曾把一页删掉 4 KB)。改用唯一的结束锚: 顶栏最后是 </a></div>。
  45. s = re.sub(r'<style id="%s">.*?</style>\s*' % MARK, "", s, flags=re.S)
  46. s = re.sub(r'<div class="gl-top">.*?返回仿真集成台</a></div>\s*', "", s, flags=re.S, count=1)
  47. # 主路径: 有 </head> / <body>
  48. m = re.search(r"</head\s*>", s, re.I)
  49. if m:
  50. s = s[:m.start()] + SHELL_CSS + s[m.start():]
  51. else:
  52. # 回退: 片段式 HTML (无 head/body 标签, 浏览器容忍)。锚到 <title> 或最后一个 </style> 之后
  53. a = re.search(r"</title\s*>", s, re.I) or re.search(r"</style\s*>(?![\s\S]*</style\s*>)", s, re.I)
  54. if not a: return None
  55. s = s[:a.end()] + "\n" + SHELL_CSS + s[a.end():]
  56. b = re.search(r"<body[^>]*>", s, re.I)
  57. if b:
  58. s = s[:b.end()] + "\n" + shell(title, sub) + s[b.end():]
  59. else:
  60. # 回退: 插在最后一个 </style> 之后 (即样式区结束、可见内容开始处)
  61. c = None
  62. for c in re.finditer(r"</style\s*>", s, re.I): pass
  63. if not c: return None
  64. s = s[:c.end()] + "\n" + shell(title, sub) + s[c.end():]
  65. path.write_text(s, encoding="utf-8"); return len(s)
  66. ok = 0
  67. for f,(t,sub) in PAGES.items():
  68. p = SIM/f
  69. if not p.exists(): print(f" 缺: {f}"); continue
  70. n = apply(p, t, sub)
  71. if n: ok += 1; print(f" ✓ {f} ({n//1024} KB)")
  72. else: print(f" ✗ {f} 结构不符, 未注入")
  73. print(f"\n注入 {ok}/{len(PAGES)} 页")