_gen_mobile_tour.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # -*- coding: utf-8 -*-
  2. """生成《移动端APP界面导览.html》单文件可点导览。
  3. 读取 doc/img/mobile-app/*.png(780x1688),按 PAGES/HOTSPOTS 配置把截图
  4. 串成可点击的模拟操作导览:底部 Tab / 九宫格 / 列表行 / 返回键均为热区,
  5. 点击跳转到对应页面截图。输出单文件 HTML(图片 base64 内嵌),双击即开。
  6. 用法:python doc/_gen_mobile_tour.py
  7. 输出:doc/移动端APP界面导览.html
  8. """
  9. import base64
  10. import io
  11. import json
  12. from pathlib import Path
  13. ROOT = Path(__file__).resolve().parent
  14. IMG_DIR = ROOT / "img" / "mobile-app"
  15. OUT = ROOT / "移动端APP界面导览.html"
  16. W, H = 780, 1688 # 截图像素尺寸(2 倍图,对应 390x844 视口)
  17. # 底部 TabBar(仅 4 个 Tab 页有):y 1595-1688,横向 4 等分
  18. TAB_Y = (1595, 1688)
  19. def tabbar():
  20. targets = ["p02", "p03", "p05", "p07"]
  21. labels = ["首页", "待办", "消息", "我的"]
  22. return [
  23. dict(x=int(i * W / 4), y=TAB_Y[0], w=int(W / 4), h=TAB_Y[1] - TAB_Y[0],
  24. to=t, label=f"Tab·{l}")
  25. for i, (t, l) in enumerate(zip(targets, labels))
  26. ]
  27. def back(to, label="返回"):
  28. return dict(x=10, y=10, w=75, h=80, to=to, label=label)
  29. # 看板页公共热区:返回键 + 顶部模块切换器(S1~S5 可视,同一页面壳布局一致)
  30. KANBAN_SWITCH = [("S1", "p16"), ("S2", "p17"), ("S3", "p18"), ("S4", "p19"), ("S5", "p20")]
  31. def kanban_chrome(self_id):
  32. spots = [back("p02")]
  33. for i, (code, pid) in enumerate(KANBAN_SWITCH):
  34. spots.append(dict(x=10 + i * 154, y=95, w=134, h=83, to=pid, label=f"切换到 {code} 看板"))
  35. return spots
  36. # 九宫格行列范围(02 首页):4 列 x 区间 + 4 行 y 区间
  37. GRID_COLS = [(25, 195), (205, 375), (385, 555), (565, 735)]
  38. GRID_ROWS = [(380, 560), (560, 730), (730, 900), (900, 1070)]
  39. def grid_cell(row, col, to, label):
  40. x0, x1 = GRID_COLS[col]
  41. y0, y1 = GRID_ROWS[row]
  42. return dict(x=x0, y=y0, w=x1 - x0, h=y1 - y0, to=to, label=label)
  43. PAGES = [
  44. dict(id="p01", file="01-登录页.png", title="登录页", group="登录",
  45. desc="账号密码登录(密码 SM2 加密传输),登录后自动识别所属租户。",
  46. hotspots=[dict(x=80, y=920, w=620, h=70, to="p02", label="登录")]),
  47. dict(id="p02", file="02-首页工作台.png", title="首页工作台", group="首页",
  48. desc="速览卡片 + 九宫格功能导航 + 未读公告;下拉可刷新。",
  49. hotspots=[
  50. dict(x=25, y=115, w=230, h=150, to="p03", label="速览·待办审批"),
  51. dict(x=275, y=115, w=230, h=150, to="p05", label="速览·未读消息"),
  52. dict(x=525, y=115, w=230, h=150, to="p08", label="速览·超时异常"),
  53. grid_cell(0, 0, "p03", "审批待办"), grid_cell(0, 1, "p08", "异常协同"),
  54. grid_cell(0, 2, "p16", "S1 看板"), grid_cell(0, 3, "p17", "S2 看板"),
  55. grid_cell(1, 0, "p18", "S3 看板"), grid_cell(1, 1, "p19", "S4 看板"),
  56. grid_cell(1, 2, "p20", "S5 看板"), grid_cell(1, 3, "p21", "S6 看板"),
  57. grid_cell(2, 0, "p22", "S7 看板"), grid_cell(2, 1, "p11", "S9 看板"),
  58. grid_cell(2, 2, "p12", "库存查询"), grid_cell(2, 3, "p13", "工单进度"),
  59. grid_cell(3, 0, "p14", "订单交付"), grid_cell(3, 1, "p15", "成品检验"),
  60. grid_cell(3, 2, "p05", "消息中心"),
  61. dict(x=25, y=1130, w=730, h=310, to="p05", label="未读公告"),
  62. ] + tabbar()),
  63. dict(id="p03", file="03-审批待办.png", title="待办列表", group="待办审批",
  64. desc="当前用户待处理审批单据,含单据类型、节点、发起人与到达时间。",
  65. hotspots=[dict(x=0, y=175, w=780, h=155, to="p04", label="第 1 条待办")] + tabbar()),
  66. dict(id="p04", file="04-审批详情.png", title="审批详情", group="待办审批",
  67. desc="单据内容 + 审批流转记录(含退回与意见),底部操作栏同意/拒绝/更多。",
  68. hotspots=[back("p03")]),
  69. dict(id="p05", file="05-消息中心.png", title="消息中心", group="消息",
  70. desc="系统通知与业务消息列表,未读高亮,可按全部/通知/公告筛选。",
  71. hotspots=[dict(x=0, y=175, w=780, h=135, to="p06", label="第 1 条消息")] + tabbar()),
  72. dict(id="p06", file="06-消息详情.png", title="消息详情", group="消息",
  73. desc="消息全文展示(图示为「改善任务即将到期」通知)。",
  74. hotspots=[back("p05")]),
  75. dict(id="p07", file="07-我的.png", title="我的", group="我的",
  76. desc="个人中心:用户与租户信息、修改密码、服务器设置、检查更新、隐私政策、关于。",
  77. hotspots=[dict(x=25, y=790, w=730, h=90, to="p01", label="退出登录")] + tabbar()),
  78. dict(id="p08", file="08-S8异常工作台.png", title="S8 异常工作台", group="S8 异常协同",
  79. desc="异常概览六宫格 + 闭环率/时长 + 近 14 天趋势 + 严重度/状态分布(H5 饼图待修复)。",
  80. hotspots=[back("p02"), dict(x=25, y=115, w=730, h=445, to="p09", label="异常概览→异常列表")]),
  81. dict(id="p09", file="09-S8异常列表.png", title="S8 异常列表", group="S8 异常协同",
  82. desc="全部异常单据,支持搜索与筛选,含超时/严重度/状态标识。",
  83. hotspots=[back("p08"), dict(x=0, y=175, w=780, h=170, to="p10", label="第 1 条异常")]),
  84. dict(id="p10", file="10-S8异常详情.png", title="S8 异常详情", group="S8 异常协同",
  85. desc="异常完整信息 + 协同操作(认领/驳回/补充说明)+ 处理时间线。",
  86. hotspots=[back("p09")]),
  87. dict(id="p16", file="16-S1看板.png", title="S1 产销协同看板", group="S1~S9 模块看板",
  88. desc="S1 产销协同:订单评审周期/满足率、主计划人效、成品库存周转等 L1~L3 指标。",
  89. hotspots=kanban_chrome("p16")),
  90. dict(id="p17", file="17-S2看板.png", title="S2 订单排程看板", group="S1~S9 模块看板",
  91. desc="S2 订单排程:订单排程相关 L1~L3 指标卡与下钻。",
  92. hotspots=kanban_chrome("p17")),
  93. dict(id="p18", file="18-S3看板.png", title="S3 供应协同看板", group="S1~S9 模块看板",
  94. desc="S3 供应协同:供应协同相关 L1~L3 指标卡与下钻。",
  95. hotspots=kanban_chrome("p18")),
  96. dict(id="p19", file="19-S4看板.png", title="S4 采购执行看板", group="S1~S9 模块看板",
  97. desc="S4 采购执行:采购执行相关 L1~L3 指标卡与下钻。",
  98. hotspots=kanban_chrome("p19")),
  99. dict(id="p20", file="20-S5看板.png", title="S5 物料仓储看板", group="S1~S9 模块看板",
  100. desc="S5 物料仓储:物料库存/周转相关 L1~L3 指标卡与下钻。",
  101. hotspots=kanban_chrome("p20")),
  102. dict(id="p21", file="21-S6看板.png", title="S6 生产执行看板", group="S1~S9 模块看板",
  103. desc="S6 生产执行:生产工单/报工相关 L1~L3 指标卡与下钻。",
  104. hotspots=kanban_chrome("p21")),
  105. dict(id="p22", file="22-S7看板.png", title="S7 成品仓储看板", group="S1~S9 模块看板",
  106. desc="S7 成品仓储:成品库存/出入库相关 L1~L3 指标卡与下钻。",
  107. hotspots=kanban_chrome("p22")),
  108. dict(id="p11", file="11-S9看板.png", title="S9 运营诊断看板", group="S1~S9 模块看板",
  109. desc="S9 运营诊断模块:L1 指标卡(质量退货率/订单交付周期/满足率/生产效率/库存周转)+ 趋势。",
  110. hotspots=kanban_chrome("p11")),
  111. dict(id="p12", file="12-库存查询.png", title="库存查询", group="查询中心",
  112. desc="按物料编码/批次号筛选,展示库位与数量(图示测试库 4481 条)。",
  113. hotspots=[back("p02")]),
  114. dict(id="p13", file="13-工单进度.png", title="工单进度", group="查询中心",
  115. desc="工单号/物料筛选,展示车间、交期、数量与执行进度条、当前工序。",
  116. hotspots=[back("p02")]),
  117. dict(id="p14", file="14-订单交付.png", title="订单交付", group="查询中心",
  118. desc="订单交付跟踪:客户、交期、数量与评审→排产→齐套→采购→备料→生产→发运进度链。",
  119. hotspots=[back("p02")]),
  120. dict(id="p15", file="15-成品检验.png", title="成品检验", group="查询中心",
  121. desc="检验任务/检验结果两个页签(当前测试租户暂无数据,为空态界面)。",
  122. hotspots=[back("p02")]),
  123. ]
  124. NAV_GROUPS = ["登录", "首页", "待办审批", "消息", "我的", "S8 异常协同", "S1~S9 模块看板", "查询中心"]
  125. def pct(v, total):
  126. return round(v / total * 100, 3)
  127. def img_b64(name):
  128. """PNG 截图转 WebP q85 内嵌(体积约为 PNG base64 的 1/4),失败回退 PNG。"""
  129. raw = (IMG_DIR / name).read_bytes()
  130. try:
  131. from PIL import Image
  132. im = Image.open(io.BytesIO(raw)).convert("RGB")
  133. buf = io.BytesIO()
  134. im.save(buf, "WEBP", quality=85, method=6)
  135. return "data:image/webp;base64," + base64.b64encode(buf.getvalue()).decode()
  136. except Exception:
  137. return "data:image/png;base64," + base64.b64encode(raw).decode()
  138. def build_page_html(p):
  139. areas = []
  140. for h in p["hotspots"]:
  141. areas.append(
  142. f'<a class="hot" href="#{h["to"]}" title="{h["label"]}" '
  143. f'style="left:{pct(h["x"], W)}%;top:{pct(h["y"], H)}%;'
  144. f'width:{pct(h["w"], W)}%;height:{pct(h["h"], H)}%"></a>'
  145. )
  146. return (
  147. f'<div class="page" id="{p["id"]}" data-title="{p["title"]}" data-desc="{p["desc"]}">'
  148. f'<img src="{img_b64(p["file"])}" alt="{p["title"]}">{"".join(areas)}</div>'
  149. )
  150. def build_nav():
  151. items = []
  152. for g in NAV_GROUPS:
  153. links = "".join(
  154. f'<a href="#{p["id"]}" data-page="{p["id"]}">{p["title"]}</a>'
  155. for p in PAGES if p["group"] == g
  156. )
  157. items.append(f'<div class="nav-group"><h3>{g}</h3>{links}</div>')
  158. return "".join(items)
  159. HTML = """<!DOCTYPE html>
  160. <html lang="zh-CN">
  161. <head>
  162. <meta charset="UTF-8">
  163. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  164. <title>Ai-DOP 移动端 APP 界面导览</title>
  165. <style>
  166. * { margin:0; padding:0; box-sizing:border-box; }
  167. body { font-family:"Microsoft YaHei","PingFang SC",sans-serif; background:#eef0f4; color:#303133; }
  168. header.top { background:#2F3A52; color:#fff; padding:14px 24px; }
  169. header.top h1 { font-size:18px; font-weight:600; }
  170. header.top p { font-size:12px; color:#c0c8d8; margin-top:4px; }
  171. .layout { display:flex; gap:24px; max-width:1200px; margin:20px auto; padding:0 16px; align-items:flex-start; }
  172. nav.side { width:220px; flex:none; background:#fff; border-radius:10px; padding:14px 16px; box-shadow:0 1px 4px rgba(0,0,0,.08); position:sticky; top:16px; }
  173. .nav-group h3 { font-size:13px; color:#909399; margin:12px 0 6px; border-left:3px solid #0F59A4; padding-left:8px; }
  174. .nav-group:first-child h3 { margin-top:0; }
  175. .nav-group a { display:block; font-size:13px; color:#303133; text-decoration:none; padding:5px 8px; border-radius:6px; }
  176. .nav-group a:hover { background:#f0f5fb; color:#0F59A4; }
  177. .nav-group a.active { background:#0F59A4; color:#fff; }
  178. main { flex:1; display:flex; flex-direction:column; align-items:center; }
  179. .phone { width:390px; background:#111; border-radius:36px; padding:12px; box-shadow:0 8px 30px rgba(0,0,0,.25); }
  180. .screen { position:relative; width:366px; aspect-ratio:390/844; background:#fff; border-radius:26px; overflow:hidden; }
  181. .page { position:absolute; inset:0; display:none; }
  182. .page.active { display:block; }
  183. .page img { width:100%; height:100%; object-fit:cover; display:block; }
  184. a.hot { position:absolute; display:block; border-radius:8px; transition:background .15s, box-shadow .15s; cursor:pointer; }
  185. a.hot:hover, a.hot:focus { background:rgba(15,89,164,.18); box-shadow:inset 0 0 0 2px #0F59A4; outline:none; }
  186. .caption { width:390px; margin-top:14px; background:#fff; border-radius:10px; padding:12px 16px; box-shadow:0 1px 4px rgba(0,0,0,.08); }
  187. .caption h2 { font-size:15px; color:#2F3A52; }
  188. .caption p { font-size:12.5px; color:#606266; margin-top:6px; line-height:1.6; }
  189. .tip { width:390px; margin-top:8px; font-size:12px; color:#909399; text-align:center; }
  190. @media (max-width:760px) {
  191. .layout { flex-direction:column; align-items:center; }
  192. nav.side { width:100%; position:static; }
  193. }
  194. </style>
  195. </head>
  196. <body>
  197. <header class="top">
  198. <h1>Ai-DOP 移动端 APP 界面导览</h1>
  199. <p>截图于 2026-09-30 · 本地 UAT 环境(UATAdminA @ UATTEST_CHL 测试数据)· 点击界面中的可点区域(Tab / 九宫格 / 列表行 / 返回键)模拟操作跳转,悬停可查看热区</p>
  200. </header>
  201. <div class="layout">
  202. <nav class="side">__NAV__</nav>
  203. <main>
  204. <div class="phone"><div class="screen" id="screen">__PAGES__</div></div>
  205. <div class="caption"><h2 id="capTitle"></h2><p id="capDesc"></p></div>
  206. <div class="tip">演示版导览:仅预设路径可跳转,表单不可真实输入 · 配合《移动端APP界面截图手册.md》阅读</div>
  207. </main>
  208. </div>
  209. <script>
  210. var pages = __PAGE_IDS__;
  211. function show(id) {
  212. if (pages.indexOf(id) < 0) id = pages[0];
  213. document.querySelectorAll('.page').forEach(function (el) {
  214. el.classList.toggle('active', el.id === id);
  215. });
  216. document.querySelectorAll('.nav-group a').forEach(function (a) {
  217. a.classList.toggle('active', a.getAttribute('data-page') === id);
  218. });
  219. var cur = document.getElementById(id);
  220. document.getElementById('capTitle').textContent = cur.getAttribute('data-title');
  221. document.getElementById('capDesc').textContent = cur.getAttribute('data-desc');
  222. if (location.hash !== '#' + id) history.replaceState(null, '', '#' + id);
  223. }
  224. window.addEventListener('hashchange', function () { show(location.hash.slice(1)); });
  225. show(location.hash.slice(1) || pages[0]);
  226. </script>
  227. </body>
  228. </html>
  229. """
  230. def main():
  231. pages_html = "".join(build_page_html(p) for p in PAGES)
  232. html = (HTML
  233. .replace("__NAV__", build_nav())
  234. .replace("__PAGES__", pages_html)
  235. .replace("__PAGE_IDS__", json.dumps([p["id"] for p in PAGES])))
  236. OUT.write_text(html, encoding="utf-8")
  237. size_kb = OUT.stat().st_size // 1024
  238. print(f"OK -> {OUT} ({size_kb} KB, {len(PAGES)} pages)")
  239. if __name__ == "__main__":
  240. main()