| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- # -*- coding: utf-8 -*-
- """生成《移动端APP界面导览.html》单文件可点导览。
- 读取 doc/img/mobile-app/*.png(780x1688),按 PAGES/HOTSPOTS 配置把截图
- 串成可点击的模拟操作导览:底部 Tab / 九宫格 / 列表行 / 返回键均为热区,
- 点击跳转到对应页面截图。输出单文件 HTML(图片 base64 内嵌),双击即开。
- 用法:python doc/_gen_mobile_tour.py
- 输出:doc/移动端APP界面导览.html
- """
- import base64
- import io
- import json
- from pathlib import Path
- ROOT = Path(__file__).resolve().parent
- IMG_DIR = ROOT / "img" / "mobile-app"
- OUT = ROOT / "移动端APP界面导览.html"
- W, H = 780, 1688 # 截图像素尺寸(2 倍图,对应 390x844 视口)
- # 底部 TabBar(仅 4 个 Tab 页有):y 1595-1688,横向 4 等分
- TAB_Y = (1595, 1688)
- def tabbar():
- targets = ["p02", "p03", "p05", "p07"]
- labels = ["首页", "待办", "消息", "我的"]
- return [
- dict(x=int(i * W / 4), y=TAB_Y[0], w=int(W / 4), h=TAB_Y[1] - TAB_Y[0],
- to=t, label=f"Tab·{l}")
- for i, (t, l) in enumerate(zip(targets, labels))
- ]
- def back(to, label="返回"):
- return dict(x=10, y=10, w=75, h=80, to=to, label=label)
- # 看板页公共热区:返回键 + 顶部模块切换器(S1~S5 可视,同一页面壳布局一致)
- KANBAN_SWITCH = [("S1", "p16"), ("S2", "p17"), ("S3", "p18"), ("S4", "p19"), ("S5", "p20")]
- def kanban_chrome(self_id):
- spots = [back("p02")]
- for i, (code, pid) in enumerate(KANBAN_SWITCH):
- spots.append(dict(x=10 + i * 154, y=95, w=134, h=83, to=pid, label=f"切换到 {code} 看板"))
- return spots
- # 九宫格行列范围(02 首页):4 列 x 区间 + 4 行 y 区间
- GRID_COLS = [(25, 195), (205, 375), (385, 555), (565, 735)]
- GRID_ROWS = [(380, 560), (560, 730), (730, 900), (900, 1070)]
- def grid_cell(row, col, to, label):
- x0, x1 = GRID_COLS[col]
- y0, y1 = GRID_ROWS[row]
- return dict(x=x0, y=y0, w=x1 - x0, h=y1 - y0, to=to, label=label)
- PAGES = [
- dict(id="p01", file="01-登录页.png", title="登录页", group="登录",
- desc="账号密码登录(密码 SM2 加密传输),登录后自动识别所属租户。",
- hotspots=[dict(x=80, y=920, w=620, h=70, to="p02", label="登录")]),
- dict(id="p02", file="02-首页工作台.png", title="首页工作台", group="首页",
- desc="速览卡片 + 九宫格功能导航 + 未读公告;下拉可刷新。",
- hotspots=[
- dict(x=25, y=115, w=230, h=150, to="p03", label="速览·待办审批"),
- dict(x=275, y=115, w=230, h=150, to="p05", label="速览·未读消息"),
- dict(x=525, y=115, w=230, h=150, to="p08", label="速览·超时异常"),
- grid_cell(0, 0, "p03", "审批待办"), grid_cell(0, 1, "p08", "异常协同"),
- grid_cell(0, 2, "p16", "S1 看板"), grid_cell(0, 3, "p17", "S2 看板"),
- grid_cell(1, 0, "p18", "S3 看板"), grid_cell(1, 1, "p19", "S4 看板"),
- grid_cell(1, 2, "p20", "S5 看板"), grid_cell(1, 3, "p21", "S6 看板"),
- grid_cell(2, 0, "p22", "S7 看板"), grid_cell(2, 1, "p11", "S9 看板"),
- grid_cell(2, 2, "p12", "库存查询"), grid_cell(2, 3, "p13", "工单进度"),
- grid_cell(3, 0, "p14", "订单交付"), grid_cell(3, 1, "p15", "成品检验"),
- grid_cell(3, 2, "p05", "消息中心"),
- dict(x=25, y=1130, w=730, h=310, to="p05", label="未读公告"),
- ] + tabbar()),
- dict(id="p03", file="03-审批待办.png", title="待办列表", group="待办审批",
- desc="当前用户待处理审批单据,含单据类型、节点、发起人与到达时间。",
- hotspots=[dict(x=0, y=175, w=780, h=155, to="p04", label="第 1 条待办")] + tabbar()),
- dict(id="p04", file="04-审批详情.png", title="审批详情", group="待办审批",
- desc="单据内容 + 审批流转记录(含退回与意见),底部操作栏同意/拒绝/更多。",
- hotspots=[back("p03")]),
- dict(id="p05", file="05-消息中心.png", title="消息中心", group="消息",
- desc="系统通知与业务消息列表,未读高亮,可按全部/通知/公告筛选。",
- hotspots=[dict(x=0, y=175, w=780, h=135, to="p06", label="第 1 条消息")] + tabbar()),
- dict(id="p06", file="06-消息详情.png", title="消息详情", group="消息",
- desc="消息全文展示(图示为「改善任务即将到期」通知)。",
- hotspots=[back("p05")]),
- dict(id="p07", file="07-我的.png", title="我的", group="我的",
- desc="个人中心:用户与租户信息、修改密码、服务器设置、检查更新、隐私政策、关于。",
- hotspots=[dict(x=25, y=790, w=730, h=90, to="p01", label="退出登录")] + tabbar()),
- dict(id="p08", file="08-S8异常工作台.png", title="S8 异常工作台", group="S8 异常协同",
- desc="异常概览六宫格 + 闭环率/时长 + 近 14 天趋势 + 严重度/状态分布(H5 饼图待修复)。",
- hotspots=[back("p02"), dict(x=25, y=115, w=730, h=445, to="p09", label="异常概览→异常列表")]),
- dict(id="p09", file="09-S8异常列表.png", title="S8 异常列表", group="S8 异常协同",
- desc="全部异常单据,支持搜索与筛选,含超时/严重度/状态标识。",
- hotspots=[back("p08"), dict(x=0, y=175, w=780, h=170, to="p10", label="第 1 条异常")]),
- dict(id="p10", file="10-S8异常详情.png", title="S8 异常详情", group="S8 异常协同",
- desc="异常完整信息 + 协同操作(认领/驳回/补充说明)+ 处理时间线。",
- hotspots=[back("p09")]),
- dict(id="p16", file="16-S1看板.png", title="S1 产销协同看板", group="S1~S9 模块看板",
- desc="S1 产销协同:订单评审周期/满足率、主计划人效、成品库存周转等 L1~L3 指标。",
- hotspots=kanban_chrome("p16")),
- dict(id="p17", file="17-S2看板.png", title="S2 订单排程看板", group="S1~S9 模块看板",
- desc="S2 订单排程:订单排程相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p17")),
- dict(id="p18", file="18-S3看板.png", title="S3 供应协同看板", group="S1~S9 模块看板",
- desc="S3 供应协同:供应协同相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p18")),
- dict(id="p19", file="19-S4看板.png", title="S4 采购执行看板", group="S1~S9 模块看板",
- desc="S4 采购执行:采购执行相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p19")),
- dict(id="p20", file="20-S5看板.png", title="S5 物料仓储看板", group="S1~S9 模块看板",
- desc="S5 物料仓储:物料库存/周转相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p20")),
- dict(id="p21", file="21-S6看板.png", title="S6 生产执行看板", group="S1~S9 模块看板",
- desc="S6 生产执行:生产工单/报工相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p21")),
- dict(id="p22", file="22-S7看板.png", title="S7 成品仓储看板", group="S1~S9 模块看板",
- desc="S7 成品仓储:成品库存/出入库相关 L1~L3 指标卡与下钻。",
- hotspots=kanban_chrome("p22")),
- dict(id="p11", file="11-S9看板.png", title="S9 运营诊断看板", group="S1~S9 模块看板",
- desc="S9 运营诊断模块:L1 指标卡(质量退货率/订单交付周期/满足率/生产效率/库存周转)+ 趋势。",
- hotspots=kanban_chrome("p11")),
- dict(id="p12", file="12-库存查询.png", title="库存查询", group="查询中心",
- desc="按物料编码/批次号筛选,展示库位与数量(图示测试库 4481 条)。",
- hotspots=[back("p02")]),
- dict(id="p13", file="13-工单进度.png", title="工单进度", group="查询中心",
- desc="工单号/物料筛选,展示车间、交期、数量与执行进度条、当前工序。",
- hotspots=[back("p02")]),
- dict(id="p14", file="14-订单交付.png", title="订单交付", group="查询中心",
- desc="订单交付跟踪:客户、交期、数量与评审→排产→齐套→采购→备料→生产→发运进度链。",
- hotspots=[back("p02")]),
- dict(id="p15", file="15-成品检验.png", title="成品检验", group="查询中心",
- desc="检验任务/检验结果两个页签(当前测试租户暂无数据,为空态界面)。",
- hotspots=[back("p02")]),
- ]
- NAV_GROUPS = ["登录", "首页", "待办审批", "消息", "我的", "S8 异常协同", "S1~S9 模块看板", "查询中心"]
- def pct(v, total):
- return round(v / total * 100, 3)
- def img_b64(name):
- """PNG 截图转 WebP q85 内嵌(体积约为 PNG base64 的 1/4),失败回退 PNG。"""
- raw = (IMG_DIR / name).read_bytes()
- try:
- from PIL import Image
- im = Image.open(io.BytesIO(raw)).convert("RGB")
- buf = io.BytesIO()
- im.save(buf, "WEBP", quality=85, method=6)
- return "data:image/webp;base64," + base64.b64encode(buf.getvalue()).decode()
- except Exception:
- return "data:image/png;base64," + base64.b64encode(raw).decode()
- def build_page_html(p):
- areas = []
- for h in p["hotspots"]:
- areas.append(
- f'<a class="hot" href="#{h["to"]}" title="{h["label"]}" '
- f'style="left:{pct(h["x"], W)}%;top:{pct(h["y"], H)}%;'
- f'width:{pct(h["w"], W)}%;height:{pct(h["h"], H)}%"></a>'
- )
- return (
- f'<div class="page" id="{p["id"]}" data-title="{p["title"]}" data-desc="{p["desc"]}">'
- f'<img src="{img_b64(p["file"])}" alt="{p["title"]}">{"".join(areas)}</div>'
- )
- def build_nav():
- items = []
- for g in NAV_GROUPS:
- links = "".join(
- f'<a href="#{p["id"]}" data-page="{p["id"]}">{p["title"]}</a>'
- for p in PAGES if p["group"] == g
- )
- items.append(f'<div class="nav-group"><h3>{g}</h3>{links}</div>')
- return "".join(items)
- HTML = """<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Ai-DOP 移动端 APP 界面导览</title>
- <style>
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:"Microsoft YaHei","PingFang SC",sans-serif; background:#eef0f4; color:#303133; }
- header.top { background:#2F3A52; color:#fff; padding:14px 24px; }
- header.top h1 { font-size:18px; font-weight:600; }
- header.top p { font-size:12px; color:#c0c8d8; margin-top:4px; }
- .layout { display:flex; gap:24px; max-width:1200px; margin:20px auto; padding:0 16px; align-items:flex-start; }
- 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; }
- .nav-group h3 { font-size:13px; color:#909399; margin:12px 0 6px; border-left:3px solid #0F59A4; padding-left:8px; }
- .nav-group:first-child h3 { margin-top:0; }
- .nav-group a { display:block; font-size:13px; color:#303133; text-decoration:none; padding:5px 8px; border-radius:6px; }
- .nav-group a:hover { background:#f0f5fb; color:#0F59A4; }
- .nav-group a.active { background:#0F59A4; color:#fff; }
- main { flex:1; display:flex; flex-direction:column; align-items:center; }
- .phone { width:390px; background:#111; border-radius:36px; padding:12px; box-shadow:0 8px 30px rgba(0,0,0,.25); }
- .screen { position:relative; width:366px; aspect-ratio:390/844; background:#fff; border-radius:26px; overflow:hidden; }
- .page { position:absolute; inset:0; display:none; }
- .page.active { display:block; }
- .page img { width:100%; height:100%; object-fit:cover; display:block; }
- a.hot { position:absolute; display:block; border-radius:8px; transition:background .15s, box-shadow .15s; cursor:pointer; }
- a.hot:hover, a.hot:focus { background:rgba(15,89,164,.18); box-shadow:inset 0 0 0 2px #0F59A4; outline:none; }
- .caption { width:390px; margin-top:14px; background:#fff; border-radius:10px; padding:12px 16px; box-shadow:0 1px 4px rgba(0,0,0,.08); }
- .caption h2 { font-size:15px; color:#2F3A52; }
- .caption p { font-size:12.5px; color:#606266; margin-top:6px; line-height:1.6; }
- .tip { width:390px; margin-top:8px; font-size:12px; color:#909399; text-align:center; }
- @media (max-width:760px) {
- .layout { flex-direction:column; align-items:center; }
- nav.side { width:100%; position:static; }
- }
- </style>
- </head>
- <body>
- <header class="top">
- <h1>Ai-DOP 移动端 APP 界面导览</h1>
- <p>截图于 2026-09-30 · 本地 UAT 环境(UATAdminA @ UATTEST_CHL 测试数据)· 点击界面中的可点区域(Tab / 九宫格 / 列表行 / 返回键)模拟操作跳转,悬停可查看热区</p>
- </header>
- <div class="layout">
- <nav class="side">__NAV__</nav>
- <main>
- <div class="phone"><div class="screen" id="screen">__PAGES__</div></div>
- <div class="caption"><h2 id="capTitle"></h2><p id="capDesc"></p></div>
- <div class="tip">演示版导览:仅预设路径可跳转,表单不可真实输入 · 配合《移动端APP界面截图手册.md》阅读</div>
- </main>
- </div>
- <script>
- var pages = __PAGE_IDS__;
- function show(id) {
- if (pages.indexOf(id) < 0) id = pages[0];
- document.querySelectorAll('.page').forEach(function (el) {
- el.classList.toggle('active', el.id === id);
- });
- document.querySelectorAll('.nav-group a').forEach(function (a) {
- a.classList.toggle('active', a.getAttribute('data-page') === id);
- });
- var cur = document.getElementById(id);
- document.getElementById('capTitle').textContent = cur.getAttribute('data-title');
- document.getElementById('capDesc').textContent = cur.getAttribute('data-desc');
- if (location.hash !== '#' + id) history.replaceState(null, '', '#' + id);
- }
- window.addEventListener('hashchange', function () { show(location.hash.slice(1)); });
- show(location.hash.slice(1) || pages[0]);
- </script>
- </body>
- </html>
- """
- def main():
- pages_html = "".join(build_page_html(p) for p in PAGES)
- html = (HTML
- .replace("__NAV__", build_nav())
- .replace("__PAGES__", pages_html)
- .replace("__PAGE_IDS__", json.dumps([p["id"] for p in PAGES])))
- OUT.write_text(html, encoding="utf-8")
- size_kb = OUT.stat().st_size // 1024
- print(f"OK -> {OUT} ({size_kb} KB, {len(PAGES)} pages)")
- if __name__ == "__main__":
- main()
|