349 lines
40 KiB
Python
349 lines
40 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""Build the durable Aetherbound UI design/status gallery."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timezone
|
||
import json
|
||
from pathlib import Path
|
||
import re
|
||
import shutil
|
||
from typing import Any
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
OUTPUT_DIR = ROOT / "docs/design-authority/gallery"
|
||
OUTPUT = OUTPUT_DIR / "index.html"
|
||
ASSET_DIR = OUTPUT_DIR / "assets"
|
||
DESIGN_INDEX = ROOT / "docs/design-authority/design-index.json"
|
||
PAGE_PLAN = ROOT / "docs/03_PAGES_AND_UX.md"
|
||
|
||
STAGES = [
|
||
"SPEC_READY",
|
||
"AI_DRAFT",
|
||
"DRAFT_REVIEW",
|
||
"APPROVED_FOR_RECONSTRUCTION",
|
||
"DECONSTRUCTION_IN_PROGRESS",
|
||
"DECONSTRUCTION_REVIEW",
|
||
"ASSET_GENERATION_IN_PROGRESS",
|
||
"ASSET_REVIEW",
|
||
"RECONSTRUCTION_READY",
|
||
"IMPLEMENTATION_IN_PROGRESS",
|
||
"IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW",
|
||
"VERIFIED_AWAITING_OWNER_ACCEPTANCE",
|
||
"COMPLETE",
|
||
]
|
||
|
||
STAGE_DEFINITIONS = [
|
||
("SPEC_READY", "规格", "页面合同、状态矩阵、响应式与文案约束"),
|
||
("AI_DRAFT", "AI 设计稿", "提示词、生成记录、原始稿与来源"),
|
||
("DRAFT_REVIEW", "AI 稿评审", "Owner 确认构图、层级、主体和文字槽位"),
|
||
("APPROVED_FOR_RECONSTRUCTION", "批准解构", "冻结视觉方向,允许进入解构,不等于允许编码"),
|
||
("DECONSTRUCTION_IN_PROGRESS", "解构中", "图层、焦点、引擎文字、响应式锚点与交互映射"),
|
||
("DECONSTRUCTION_REVIEW", "解构评审", "Owner 检查解构是否忠实且可实现"),
|
||
("ASSET_GENERATION_IN_PROGRESS", "素材生成中", "背景清理、角色、道具、图标和组件素材"),
|
||
("ASSET_REVIEW", "素材评审", "检查来源、裁切、比例、透明度、无烘焙文字"),
|
||
("RECONSTRUCTION_READY", "重建包就绪", "runtime spec、素材清单、命中区域和状态绑定"),
|
||
("IMPLEMENTATION_IN_PROGRESS", "编码中", "Godot 页面、状态、输入与本地化实现"),
|
||
("IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW", "运行候选", "行为完成,等待真实截图审查"),
|
||
("VERIFIED_AWAITING_OWNER_ACCEPTANCE", "证据待验收", "触控、响应式、截图和诊断证据齐全"),
|
||
("COMPLETE", "完成", "Owner runtime acceptance 已记录"),
|
||
]
|
||
|
||
STATUS_LABELS = {
|
||
"COMPLETE": ("已完成", "complete"),
|
||
"SPEC_READY": ("规格已就绪", "active"),
|
||
"AI_DRAFT": ("AI 稿生成中", "active"),
|
||
"DRAFT_REVIEW": ("AI 稿待评审", "active"),
|
||
"APPROVED_FOR_RECONSTRUCTION": ("已批准解构", "active"),
|
||
"DECONSTRUCTION_IN_PROGRESS": ("解构中", "active"),
|
||
"DECONSTRUCTION_REVIEW": ("解构待评审", "active"),
|
||
"ASSET_GENERATION_IN_PROGRESS": ("素材生成中", "active"),
|
||
"ASSET_REVIEW": ("素材待评审", "active"),
|
||
"RECONSTRUCTION_READY": ("重建包就绪", "active"),
|
||
"IMPLEMENTATION_IN_PROGRESS": ("编码中", "active"),
|
||
"IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW": ("运行候选待验收", "active"),
|
||
"VERIFIED_AWAITING_OWNER_ACCEPTANCE": ("证据完成待验收", "active"),
|
||
"NOT_STARTED": ("未开始", "not-started"),
|
||
}
|
||
|
||
RUNTIME_PREVIEWS = {
|
||
"SYS-001": "runtime/assets/entry/boot/background.png",
|
||
"SYS-002": "runtime/assets/entry/first_launch/background.png",
|
||
"SYS-003": "runtime/assets/backgrounds/title_master_v002.png",
|
||
"SYS-005": "runtime/assets/backgrounds/p9_3_guild_hall.png",
|
||
"HUB-001": "runtime/assets/backgrounds/guild_master_b_composite.png",
|
||
}
|
||
|
||
DECONSTRUCTION_BOARDS = {
|
||
"HUB-001": "docs/design-authority/pages/HUB-001-guild-desk/reconstruction/v001/deconstruction-board.png",
|
||
"PTY-001": "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/deconstruction-board.png",
|
||
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/reconstruction/v001/deconstruction-board.png",
|
||
"RSK-002": "docs/design-authority/pages/RSK-002-risk-commitment/reconstruction/v001/deconstruction-board.png",
|
||
"BAT-001": "docs/design-authority/pages/BAT-001-battle-opening/reconstruction/v001/deconstruction-board.png",
|
||
"BAT-002": "docs/design-authority/pages/BAT-002-live-battle/reconstruction/v001/deconstruction-board.png",
|
||
}
|
||
|
||
ASSET_CONTACT_SHEETS = {
|
||
"HUB-001": "docs/design-authority/pages/HUB-001-guild-desk/reconstruction/v001/asset-contact-sheet.png",
|
||
"PTY-001": "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/asset-contact-sheet.png",
|
||
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/reconstruction/v001/asset-contact-sheet.png",
|
||
"RSK-002": "docs/design-authority/pages/RSK-002-risk-commitment/reconstruction/v001/asset-contact-sheet.png",
|
||
"BAT-001": "docs/design-authority/pages/BAT-001-battle-opening/reconstruction/v001/asset-contact-sheet.png",
|
||
"BAT-002": "docs/design-authority/pages/BAT-002-live-battle/reconstruction/v001/asset-contact-sheet.png",
|
||
}
|
||
|
||
DRAFTS = {
|
||
"SYS-003": "docs/design-authority/pages/SYS-003-title/drafts/v002/title-v002-raw.png",
|
||
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/drafts/v001/readiness-v001-raw.png",
|
||
}
|
||
|
||
GROUPS = {
|
||
"SYS-001": "入口模块",
|
||
"SYS-002": "入口模块",
|
||
"SYS-003": "入口模块",
|
||
"SYS-005": "入口模块",
|
||
"SYS-008": "入口模块",
|
||
}
|
||
|
||
|
||
def relative_url(path: Path) -> str | None:
|
||
if not path.is_file():
|
||
return None
|
||
return Path(__import__("os").path.relpath(path, OUTPUT_DIR)).as_posix()
|
||
|
||
|
||
def copy_gallery_asset(source: Path, key: str) -> str | None:
|
||
if not source.is_file():
|
||
return None
|
||
ASSET_DIR.mkdir(parents=True, exist_ok=True)
|
||
target = ASSET_DIR / f"{key}{source.suffix.lower()}"
|
||
shutil.copy2(source, target)
|
||
return (Path("assets") / target.name).as_posix()
|
||
|
||
|
||
def page_rows() -> list[tuple[str, str]]:
|
||
pattern = re.compile(r"^\|\s*`([A-Z]{3}-\d{3})`\s*\|\s*([^|]+?)\s*\|", re.MULTILINE)
|
||
rows: list[tuple[str, str]] = []
|
||
seen: set[str] = set()
|
||
for page_id, name in pattern.findall(PAGE_PLAN.read_text(encoding="utf-8")):
|
||
if page_id not in seen:
|
||
rows.append((page_id, name.strip()))
|
||
seen.add(page_id)
|
||
return rows
|
||
|
||
|
||
def image_field(entry: dict[str, Any], field: str, page_id: str) -> str | None:
|
||
value = entry.get(field)
|
||
if not value:
|
||
return None
|
||
candidate = ROOT / "docs/design-authority" / str(value)
|
||
return copy_gallery_asset(candidate, f"{page_id.lower()}-{field}")
|
||
|
||
|
||
def build_page(page_id: str, name: str, entry: dict[str, Any]) -> dict[str, Any]:
|
||
status = str(entry.get("status", "NOT_STARTED"))
|
||
if not entry:
|
||
status = "NOT_STARTED"
|
||
approved = image_field(entry, "approved", page_id) or image_field(entry, "master", page_id)
|
||
draft = image_field(entry, "draft", page_id) or image_field(entry, "next_draft", page_id)
|
||
if page_id in DRAFTS:
|
||
draft = copy_gallery_asset(ROOT / DRAFTS[page_id], f"{page_id.lower()}-draft")
|
||
runtime = copy_gallery_asset(ROOT / RUNTIME_PREVIEWS[page_id], f"{page_id.lower()}-runtime") if page_id in RUNTIME_PREVIEWS else None
|
||
if entry.get("runtime_preview"):
|
||
runtime = image_field(entry, "runtime_preview", page_id)
|
||
runtime_captures = []
|
||
for index, capture in enumerate(entry.get("runtime_captures", [])):
|
||
copied = copy_gallery_asset(ROOT / "docs/design-authority" / str(capture), f"{page_id.lower()}-runtime-capture-{index + 1}")
|
||
if copied:
|
||
runtime_captures.append({"src": copied, "path": str(capture)})
|
||
deconstruction = copy_gallery_asset(ROOT / DECONSTRUCTION_BOARDS[page_id], f"{page_id.lower()}-deconstruction") if page_id in DECONSTRUCTION_BOARDS else None
|
||
deconstruction_detail = image_field(entry, "deconstruction_detail", page_id)
|
||
assets = copy_gallery_asset(ROOT / ASSET_CONTACT_SHEETS[page_id], f"{page_id.lower()}-assets") if page_id in ASSET_CONTACT_SHEETS else None
|
||
label, tone = STATUS_LABELS.get(status, (status, "not-started"))
|
||
stage = STAGES.index(status) + 1 if status in STAGES else 0
|
||
if status == "COMPLETE":
|
||
missing = "无,已完成设计、实现、验证和 Owner 验收。"
|
||
elif status == "SPEC_READY":
|
||
missing = "生成并评审个人 AI 设计稿,完成重建素材和 runtime 设计包。"
|
||
elif status == "DRAFT_REVIEW":
|
||
missing = "等待 Owner 评审 AI 设计稿;通过后才能进入批准解构。"
|
||
elif status == "APPROVED_FOR_RECONSTRUCTION":
|
||
missing = "视觉方向已批准;下一步先完成图层/文字/交互解构与 Owner 解构评审。"
|
||
elif status == "DECONSTRUCTION_IN_PROGRESS":
|
||
missing = "完成图像取证、图层归属、引擎文字槽位、响应式锚点和命中区域。"
|
||
elif status == "DECONSTRUCTION_REVIEW":
|
||
missing = "等待 Owner 检查解构映射;通过后才能生成和处理素材。"
|
||
elif status == "ASSET_GENERATION_IN_PROGRESS":
|
||
missing = "按已审查的解构清单生成背景、角色、道具、图标和组件素材。"
|
||
elif status == "ASSET_REVIEW":
|
||
missing = "等待 Owner 检查素材来源、比例、裁切、透明度和无烘焙文字;通过后才能进入重建包。"
|
||
elif status == "RECONSTRUCTION_READY":
|
||
missing = "重建包已就绪,下一步才允许进入 Godot 编码。"
|
||
elif status == "IMPLEMENTATION_IN_PROGRESS":
|
||
missing = "先通过批准稿与 Runtime 的视觉保真检查,再继续页面状态、输入、响应式和本地化验收。"
|
||
elif status == "DECONSTRUCTION_REVIEW":
|
||
missing = "等待 Owner 检查图层归属、引擎文字槽位、响应式锚点、命中区域和素材 provenance;通过后才能进入素材/重建就绪。"
|
||
elif status == "NOT_STARTED":
|
||
missing = "尚未进入当前页面生产流程。"
|
||
else:
|
||
missing = "继续当前状态的页面生产门,不能跳过 Owner 评审。"
|
||
stage_details = []
|
||
current_index = stage - 1
|
||
for index, (stage_id, stage_label, deliverable) in enumerate(STAGE_DEFINITIONS):
|
||
if status == "COMPLETE" or index < current_index:
|
||
phase = "done"
|
||
elif index == current_index and status != "NOT_STARTED":
|
||
phase = "current"
|
||
else:
|
||
phase = "pending"
|
||
stage_details.append({
|
||
"id": stage_id,
|
||
"label": stage_label,
|
||
"deliverable": deliverable,
|
||
"phase": phase,
|
||
})
|
||
group = GROUPS.get(page_id)
|
||
if group is None:
|
||
prefix = page_id.split("-", 1)[0]
|
||
group = {
|
||
"SET": "设置与数据",
|
||
"HUB": "公会与经营",
|
||
"SHP": "市场与招募",
|
||
"PTY": "队伍准备",
|
||
"RSK": "风险选择",
|
||
"BAT": "战斗循环",
|
||
"OUT": "结算与恢复",
|
||
"PRO": "成长与进度",
|
||
"PST": "结局与后期",
|
||
}.get(prefix, "未分类页面")
|
||
references = []
|
||
for key in ("contract", "state_matrix", "review", "approved", "approved_manifest", "visual_reference", "scale_reference", "rejected_draft", "rejected_draft_manifest", "rejected_draft_review", "deconstruction", "deconstruction_detail", "deconstruction_plan", "deconstruction_review", "deconstruction_text_slots", "deconstruction_hit_areas", "top_rail_decomposition", "asset_plan", "asset_manifest", "asset_contact_sheet", "asset_review", "rail_slice_manifest", "rail_fit_preview", "reconstruction_spec", "runtime_asset_manifest", "runtime_hit_areas", "runtime", "preflight", "verification", "visual_fidelity", "owner_acceptance", "provenance"):
|
||
if entry.get(key):
|
||
path = str(entry[key])
|
||
prefix = "../" if path.startswith(("pages/", "modules/")) else "../../../"
|
||
references.append({"label": key, "path": path, "url": prefix + path})
|
||
return {
|
||
"id": page_id,
|
||
"name": name,
|
||
"group": group,
|
||
"status": status,
|
||
"statusLabel": label,
|
||
"tone": tone,
|
||
"stage": stage,
|
||
"stageTotal": len(STAGES),
|
||
"stageDetails": stage_details,
|
||
"approved": approved,
|
||
"draft": draft,
|
||
"deconstruction": deconstruction,
|
||
"deconstructionDetail": deconstruction_detail,
|
||
"assets": assets,
|
||
"runtime": runtime,
|
||
"runtimeCaptures": runtime_captures,
|
||
"missing": missing,
|
||
"references": references,
|
||
"search": f"{page_id} {name} {group} {label} {missing}".lower(),
|
||
}
|
||
|
||
|
||
def gallery_data() -> dict[str, Any]:
|
||
index = json.loads(DESIGN_INDEX.read_text(encoding="utf-8"))
|
||
entries = index.get("pages", {})
|
||
pages = [build_page(page_id, name, entries.get(page_id, {})) for page_id, name in page_rows()]
|
||
counts = {
|
||
"total": len(pages),
|
||
"complete": sum(page["status"] == "COMPLETE" for page in pages),
|
||
"active": sum(page["status"] != "NOT_STARTED" and page["status"] != "COMPLETE" for page in pages),
|
||
"notStarted": sum(page["status"] == "NOT_STARTED" for page in pages),
|
||
"withDesign": sum(bool(page["approved"] or page["draft"]) for page in pages),
|
||
"withRuntime": sum(bool(page["runtime"]) for page in pages),
|
||
}
|
||
return {
|
||
"generatedAt": datetime.now(timezone.utc).astimezone().isoformat(timespec="minutes"),
|
||
"stages": STAGES,
|
||
"pages": pages,
|
||
"counts": counts,
|
||
}
|
||
|
||
|
||
HTML = r'''<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#182127">
|
||
<title>Aetherbound Guild | UI Design Ledger</title>
|
||
<style>
|
||
:root{--ink:#182127;--ink2:#2d3c43;--muted:#66767b;--paper:#f6f7f2;--white:#fff;--line:#d8dfdc;--gold:#d6a72e;--gold-soft:#fff4ce;--jade:#27845e;--blue:#3969a8;--teal:#13818a;--coral:#c94e52;--slate:#87959a;--shadow:0 16px 34px rgba(24,33,39,.12)}
|
||
*{box-sizing:border-box}html{background:var(--paper);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}body{margin:0;letter-spacing:0}button,input{font:inherit;letter-spacing:0}button{cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid var(--blue);outline-offset:2px}img{display:block;max-width:100%}
|
||
.app{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.side{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;color:#eef4f0;background:var(--ink);border-right:1px solid #3a4c53}.brand{padding:22px 20px;border-bottom:1px solid rgba(255,255,255,.13)}.brand-mark{display:inline-grid;width:34px;height:34px;place-items:center;margin-bottom:12px;color:var(--ink);font-weight:900;background:var(--gold);border:2px solid #f0d980;border-radius:3px}.brand h1{margin:0;font-size:15px}.brand p{margin:6px 0 0;color:#adbec1;font-size:11px;line-height:1.45}.side-nav{display:grid;gap:3px;padding:16px 10px}.side-nav button{min-height:44px;display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:8px;padding:8px 10px;color:#b9c6c9;text-align:left;background:transparent;border:1px solid transparent;border-radius:3px}.side-nav button:hover{color:#fff;background:#26363d}.side-nav button.is-active{color:#fff;background:#304047;border-color:#52676e;box-shadow:inset 4px 0 var(--gold)}.side-nav b{font-size:12px}.side-nav span{color:var(--gold);font-size:10px;font-weight:900}.side-nav em{min-width:24px;padding:3px 5px;color:#c8d3d5;font-size:10px;font-style:normal;text-align:center;border:1px solid #52646a;border-radius:2px}.side-foot{margin-top:auto;padding:16px 20px;color:#aebdc1;font-size:10px;line-height:1.45;border-top:1px solid rgba(255,255,255,.13)}.side-foot strong{display:block;color:#fff;font-size:11px}.side-foot code{color:var(--gold);font-size:10px}
|
||
.main{min-width:0}.top{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:64px;padding:12px 28px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line)}.crumb{min-width:0;display:flex;gap:9px;align-items:center;font-size:12px}.crumb span{color:var(--muted)}.crumb strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.crumb b{color:#a4b0b2}.top-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto}.top-actions button{min-height:34px;padding:5px 10px;color:var(--ink2);background:#fff;border:1px solid var(--line);border-radius:3px}.top-actions button:hover{border-color:var(--ink2)}.top-actions .menu{display:none}
|
||
main{max-width:1700px;margin:0 auto;padding:30px}.eyebrow{display:block;color:var(--teal);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.hero{display:flex;justify-content:space-between;gap:28px;align-items:end;margin-bottom:22px}.hero h2{margin:5px 0 8px;font-size:30px;line-height:1.15}.hero p{max-width:780px;margin:0;color:var(--muted);font-size:13px;line-height:1.55;overflow-wrap:anywhere}.generated{color:var(--muted);font-size:11px;white-space:nowrap}.metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:1px;margin-bottom:18px;background:var(--line);border:1px solid var(--line)}.metric{min-height:88px;padding:15px;background:#fff}.metric strong{display:block;font-size:26px;line-height:1}.metric span{display:block;margin-top:8px;color:var(--muted);font-size:11px}.metric.complete strong{color:var(--jade)}.metric.active strong{color:var(--gold)}.metric.not strong{color:var(--slate)}.progress-band{display:grid;grid-template-columns:minmax(170px,250px) 1fr auto;align-items:center;gap:16px;margin-bottom:26px;padding:12px 15px;background:#fff;border:1px solid var(--line)}.progress-label{font-size:11px;font-weight:800}.progress-track{height:10px;overflow:hidden;background:#e6ece9;border-radius:2px}.progress-fill{height:100%;background:var(--jade)}.progress-note{color:var(--muted);font-size:11px;white-space:nowrap}
|
||
.toolbar{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:14px}.filters{display:flex;flex-wrap:wrap;gap:5px}.filters button{min-height:34px;padding:6px 10px;color:var(--ink2);background:#fff;border:1px solid var(--line);border-radius:3px}.filters button.is-active{color:#fff;background:var(--ink);border-color:var(--ink)}.search{width:min(300px,100%);min-height:36px;padding:7px 10px;background:#fff;border:1px solid var(--line);border-radius:3px}.group{margin-top:27px}.group-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px;border-bottom:2px solid var(--ink)}.group-heading h3{margin:0 0 7px;font-size:16px}.group-heading span{color:var(--muted);font-size:11px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.card{min-width:0;display:grid;grid-template-rows:auto auto 1fr auto;padding:13px;background:#fff;border:1px solid var(--line);box-shadow:0 3px 0 rgba(24,33,39,.04)}.card:hover{border-color:#8fa0a4;box-shadow:var(--shadow);transform:translateY(-1px)}.card-head{display:flex;align-items:start;justify-content:space-between;gap:10px}.card-id{color:var(--teal);font-size:10px;font-weight:900;letter-spacing:.05em}.card h4{margin:4px 0 0;font-size:16px;line-height:1.2}.pill{flex:0 0 auto;padding:5px 7px;font-size:10px;font-weight:800;border:1px solid}.pill.complete{color:var(--jade);background:#e8f5ee;border-color:#a6d5ba}.pill.active{color:#8a6312;background:var(--gold-soft);border-color:#e2c55c}.pill.not-started{color:#66767b;background:#eef1f0;border-color:#cbd4d2}.stage{display:flex;gap:3px;margin:13px 0 11px}.stage i{height:5px;flex:1;background:#e3e8e6}.stage i.done{background:var(--jade)}.stage i.current{background:var(--gold)}.art-row{display:grid;grid-template-columns:1fr 1fr;gap:6px;min-height:100px;margin-bottom:12px}.art{position:relative;min-width:0;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#eef2ef;border:1px solid var(--line)}.art img{width:100%;height:100%;object-fit:cover}.art.design img{object-fit:cover}.art.runtime img{object-fit:cover}.art small{position:absolute;left:5px;top:5px;padding:3px 5px;color:var(--ink2);font-size:9px;background:rgba(255,255,255,.9)}.art.empty{min-height:100px;color:#9aa7a9;font-size:11px;text-align:center}.card-meta{display:grid;gap:5px;color:var(--muted);font-size:11px;line-height:1.4}.card-meta strong{color:var(--ink2);font-size:10px}.card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:13px;padding-top:10px;border-top:1px solid var(--line)}.card-foot button{min-height:32px;padding:5px 9px;color:#fff;background:var(--ink);border:1px solid var(--ink);border-radius:3px}.card-foot button:hover{background:var(--ink2)}.card-foot span{color:var(--muted);font-size:10px}
|
||
.spotlight{display:grid;grid-template-columns:minmax(260px,1fr) minmax(260px,1.2fr);gap:18px;align-items:center;margin:0 0 26px;padding:16px 18px;background:#fff;border:1px solid var(--gold);box-shadow:0 5px 0 rgba(214,167,46,.14)}.spotlight-copy{min-width:0}.spotlight-kicker{color:#8a6312;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.spotlight h3{margin:5px 0 6px;font-size:20px}.spotlight p{margin:0;color:var(--muted);font-size:12px;line-height:1.5}.spotlight-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:11px}.spotlight-meta span{padding:4px 6px;color:var(--ink2);font-size:10px;background:var(--gold-soft);border:1px solid #ead58d}.spotlight button{margin-top:13px;min-height:34px;padding:6px 10px;color:#fff;background:var(--ink);border:1px solid var(--ink);border-radius:3px}.spotlight button:hover{background:var(--ink2)}.spotlight-image{min-width:0;overflow:hidden;background:#eef2ef;border:1px solid var(--line)}.spotlight-image img{width:100%;max-height:230px;object-fit:cover}
|
||
.table{display:none;background:#fff;border:1px solid var(--line)}.table-row{display:grid;grid-template-columns:100px 1.3fr 170px 1fr 110px;gap:10px;align-items:center;min-height:48px;padding:8px 12px;border-bottom:1px solid var(--line);font-size:11px}.table-row:last-child{border-bottom:0}.table-row.header{color:var(--muted);font-size:10px;font-weight:900;background:#f2f5f3}.table-row .id{color:var(--teal);font-weight:900}.table-row .missing{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.table-row button{justify-self:end;min-height:30px;padding:5px 8px;color:#fff;background:var(--ink);border:0;border-radius:3px}
|
||
.empty{padding:34px;color:var(--muted);text-align:center;background:#fff;border:1px dashed #b8c5c3}.drawer{position:fixed;z-index:30;inset:0;display:none;background:rgba(24,33,39,.38)}.drawer.is-open{display:block}.drawer-panel{position:absolute;right:0;top:0;width:min(560px,100%);height:100%;overflow:auto;padding:25px;background:var(--paper);box-shadow:-18px 0 40px rgba(0,0,0,.2)}.drawer-close{float:right;width:34px;height:34px;color:var(--ink);background:#fff;border:1px solid var(--line);border-radius:3px}.drawer-panel h2{margin:34px 0 4px;font-size:25px}.drawer-panel .lead{margin:0 0 18px;color:var(--muted);font-size:12px;line-height:1.5}.detail-status{display:inline-block;margin-bottom:16px}.detail-images{display:grid;gap:10px}.detail-images figure{margin:0;background:#fff;border:1px solid var(--line)}.detail-images img{width:100%;max-height:280px;object-fit:cover}.detail-images figcaption{padding:8px 10px;color:var(--muted);font-size:10px;border-top:1px solid var(--line)}.detail-block{margin-top:18px;padding-top:15px;border-top:1px solid var(--line)}.detail-block h3{margin:0 0 8px;font-size:13px}.detail-block p{margin:0;color:var(--muted);font-size:12px;line-height:1.55}.refs{display:grid;gap:5px;margin:0;padding:0;list-style:none}.refs a{color:var(--blue);font-size:11px;word-break:break-word}.workflow{display:grid;gap:6px;margin:0;padding:0;list-style:none}.workflow li{display:grid;grid-template-columns:18px 1fr;gap:8px;align-items:start;padding:7px 8px;background:#fff;border:1px solid var(--line);font-size:11px}.workflow li::before{content:' ';width:8px;height:8px;margin-top:3px;background:var(--slate)}.workflow li.done::before{background:var(--jade)}.workflow li.current{border-color:var(--gold);background:var(--gold-soft)}.workflow li.current::before{background:var(--gold)}.workflow b{display:block;color:var(--ink2);font-size:11px}.workflow span{display:block;margin-top:2px;color:var(--muted);font-size:10px;line-height:1.4}.legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px;color:var(--muted);font-size:10px}.legend span{display:inline-flex;align-items:center;gap:5px}.legend i{width:9px;height:9px;display:inline-block}.legend .g{background:var(--jade)}.legend .y{background:var(--gold)}.legend .s{background:var(--slate)}
|
||
@media(max-width:1120px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.metric:nth-child(n+4){border-top:1px solid var(--line)}}
|
||
.capture-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.capture-grid figure{margin:0;background:#fff;border:1px solid var(--line)}.capture-grid img{width:100%;height:108px;object-fit:cover}.capture-grid figcaption{padding:6px 7px;color:var(--muted);font-size:9px;line-height:1.35;border-top:1px solid var(--line);overflow-wrap:anywhere}.nav-toggle{display:none}
|
||
@media(max-width:800px){.app{display:block}.side{position:fixed;inset:0 auto 0 0;width:min(290px,calc(100vw - 48px));height:100vh;z-index:50;transform:translateX(-105%);transition:transform .18s ease;box-shadow:18px 0 36px rgba(0,0,0,.25)}.side.is-open{transform:translateX(0)}.side-nav{grid-template-columns:1fr}.top{padding:10px 16px;z-index:40}.nav-toggle{display:block!important}.top-actions #toggle-table{display:none}.top-actions #reset-filter{display:none}main{padding:20px 15px;overflow:hidden}.hero{display:block}.hero h2{font-size:26px;overflow-wrap:anywhere}.generated{display:block;margin-top:12px}.toolbar{align-items:stretch;flex-direction:column}.search{width:100%}.grid{grid-template-columns:1fr}.progress-band{grid-template-columns:1fr;gap:8px}.progress-note{white-space:normal}.spotlight{grid-template-columns:1fr;gap:12px}.spotlight-image{order:-1}.table-row{grid-template-columns:75px 1fr 100px}.table-row .missing{display:none}.table-row.header{display:none}}
|
||
@media(max-width:500px){.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.metric{min-height:76px}.metric strong{font-size:22px}.art-row{min-height:120px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="side">
|
||
<div class="brand"><span class="brand-mark">AG</span><h1>Aetherbound Guild</h1><p>UI Design Ledger<br>设计稿 · 解构 · 素材 · Runtime</p></div>
|
||
<nav class="side-nav" aria-label="Gallery navigation">
|
||
<button class="is-active" data-filter="all"><span>01</span><b>全部页面</b><em id="nav-all">50</em></button>
|
||
<button data-filter="complete"><span>02</span><b>已完成</b><em id="nav-complete">0</em></button>
|
||
<button data-filter="active"><span>03</span><b>进行中</b><em id="nav-active">0</em></button>
|
||
<button data-filter="not-started"><span>04</span><b>未开始</b><em id="nav-not">0</em></button>
|
||
<button data-filter="has-art"><span>05</span><b>已有视觉稿</b><em id="nav-art">0</em></button>
|
||
</nav>
|
||
<div class="side-foot"><strong>当前边界</strong><span id="side-boundary">HUB-001 Guild desk deconstruction review</span><br><code>one page at a time</code></div>
|
||
</aside>
|
||
<div class="main">
|
||
<header class="top"><button class="nav-toggle" id="toggle-side" aria-expanded="false">导航</button><div class="crumb"><span>Aetherbound Guild</span><b>/</b><strong>UI Design Ledger</strong></div><div class="top-actions"><button id="toggle-table">列表视图</button><button id="reset-filter">重置筛选</button></div></header>
|
||
<main>
|
||
<section class="hero"><div><span class="eyebrow">长期设计总览</span><h2>页面从设计稿到可玩页面的进度</h2><p>这里汇总每一个玩家页面的 AI 稿、解构板、素材清单、运行时预览、当前生产门和剩余缺口。解构评审与素材评审独立于编码,避免错误理解直接进入 Godot。</p></div><span class="generated" id="generated">生成时间</span></section>
|
||
<section class="metrics" aria-label="Progress summary"><div class="metric complete"><strong id="metric-complete">0</strong><span>已完成并验收</span></div><div class="metric active"><strong id="metric-active">0</strong><span>正在生产</span></div><div class="metric not"><strong id="metric-not">0</strong><span>尚未开始</span></div><div class="metric"><strong id="metric-art">0</strong><span>已有设计或 AI 稿</span></div><div class="metric"><strong id="metric-runtime">0</strong><span>已有 runtime 预览</span></div></section>
|
||
<section class="progress-band"><span class="progress-label">总体完成度 <b id="progress-value">0%</b></span><div class="progress-track"><div class="progress-fill" id="progress-fill" style="width:0%"></div></div><span class="progress-note" id="progress-note">还差 50 个页面</span></section>
|
||
<section class="spotlight" id="spotlight" aria-live="polite"></section>
|
||
<div class="toolbar"><div class="filters" role="toolbar" aria-label="Filter pages"><button class="is-active" data-filter="all">全部</button><button data-filter="complete">已完成</button><button data-filter="active">进行中</button><button data-filter="not-started">未开始</button><button data-filter="has-art">有视觉稿</button></div><input class="search" id="search" type="search" placeholder="搜索页面 ID、名称或缺口…" aria-label="搜索页面"></div>
|
||
<div id="groups"></div><div class="table" id="table"></div>
|
||
<div class="legend"><span><i class="g"></i>已完成</span><span><i class="y"></i>进行中</span><span><i class="s"></i>未开始</span></div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
<div class="drawer" id="drawer" aria-hidden="true"><section class="drawer-panel" role="dialog" aria-modal="true" aria-labelledby="detail-title"><button class="drawer-close" id="drawer-close" aria-label="关闭">×</button><div id="detail"></div></section></div>
|
||
<script id="gallery-data" type="application/json">__DATA__</script>
|
||
<script>
|
||
const DATA=JSON.parse(document.getElementById('gallery-data').textContent);let filter='all',query='',listMode=false;
|
||
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
|
||
const statusClass=p=>p.tone;const visible=()=>DATA.pages.filter(p=>{const ok=filter==='all'||filter==='complete'&&p.status==='COMPLETE'||filter==='active'&&p.status!=='COMPLETE'&&p.status!=='NOT_STARTED'||filter==='not-started'&&p.status==='NOT_STARTED'||filter==='has-art'&&(p.approved||p.draft);return ok&&(!query||p.search.includes(query.toLowerCase()))});
|
||
function imageBox(src,label,kind){return src?`<figure class="art ${kind}"><img src="${src}" alt="${label}" loading="lazy"><small>${label}</small></figure>`:`<div class="art empty"><span>${label}<br>尚未提供</span></div>`}
|
||
function card(p){const bars=Array.from({length:p.stageTotal},(_,i)=>`<i class="${i<p.stage?'done':i===p.stage?'current':''}"></i>`).join('');const current=p.stageDetails.find(x=>x.phase==='current');const showRuntime=Boolean(p.runtime)&&(p.status!=='NOT_STARTED');const second=showRuntime?p.runtime:(p.assets||p.deconstruction||p.approved);const secondLabel=showRuntime?(p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 运行候选'):p.assets?'素材清单':p.deconstruction?'解构板':'批准设计稿';const secondKind=showRuntime?'runtime':(p.assets||p.deconstruction?'design':'runtime');return `<article class="card"><div class="card-head"><div><div class="card-id">${p.id}</div><h4>${p.name}</h4></div><span class="pill ${statusClass(p)}">${p.statusLabel}</span></div><div class="stage" title="生产门 ${p.stage}/${p.stageTotal}">${bars}</div><div><div class="art-row">${imageBox(p.draft,'AI / 设计稿','design')}${imageBox(second,secondLabel,secondKind)}</div><div class="card-meta"><div><strong>当前阶段:</strong>${current?current.label:(p.status==='COMPLETE'?'全部阶段已完成':'尚未开始')}</div><div><strong>还差:</strong>${p.missing}</div><div><strong>生产门:</strong>${p.stage}/${p.stageTotal}</div></div></div><div class="card-foot"><span>${p.group}</span><button data-open="${p.id}">查看详情</button></div></article>`}
|
||
function renderSpotlight(){const candidates=DATA.pages.filter(p=>p.status!=='NOT_STARTED'&&p.status!=='COMPLETE'&&p.runtime);if(!candidates.length){$('#spotlight').style.display='none';return}const p=candidates[0];$('#spotlight').style.display='grid';$('#spotlight').innerHTML=`<div class="spotlight-copy"><span class="spotlight-kicker">当前运行候选 · ${p.statusLabel}</span><h3>${p.id} · ${p.name}</h3><p>这个页面已经完成 AI 稿、解构、素材、重建和 Godot 编码。下面直接展示真实 Runtime 截图;点击详情可以查看完整制作链路和所有证据。</p><div class="spotlight-meta"><span>生产门 ${p.stage}/${p.stageTotal}</span><span>${p.group}</span><span>Owner runtime review 待确认</span></div><button data-open="${p.id}">查看 ${p.id} 完整页面</button></div><figure class="spotlight-image"><img src="${p.runtime}" alt="${p.id} runtime preview"><figcaption class="sr-only">${p.id} runtime preview</figcaption></figure>`;$('#spotlight').querySelector('button').onclick=()=>openDetail(p.id)}
|
||
function render(){const pages=visible();renderSpotlight();const groups=pages.reduce((m,p)=>{(m[p.group]??=[]).push(p);return m},{});$('#groups').innerHTML=pages.length?Object.entries(groups).map(([g,items])=>`<section class="group"><div class="group-heading"><h3>${g}</h3><span>${items.length} 个页面</span></div><div class="grid">${items.map(card).join('')}</div></section>`).join(''):`<div class="empty">没有匹配页面。清空筛选或换一个关键词。</div>`;$('#table').innerHTML=`<div class="table-row header"><span>ID</span><span>页面</span><span>状态</span><span>当前缺口</span><span></span></div>${pages.map(p=>`<div class="table-row"><span class="id">${p.id}</span><strong>${p.name}</strong><span class="pill ${statusClass(p)}">${p.statusLabel}</span><span class="missing">${p.missing}</span><button data-open="${p.id}">详情</button></div>`).join('')}`;$('#groups').style.display=listMode?'none':'';$('#table').style.display=listMode?'block':'none';$$('[data-open]').forEach(b=>b.onclick=()=>openDetail(b.dataset.open));}
|
||
function openDetail(id){const p=DATA.pages.find(x=>x.id===id);if(!p)return;const runtimeLabel=p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 历史候选';const workflow=p.stageDetails.map(s=>`<li class="${s.phase}"><div><b>${s.label} <small>${s.id}</small></b><span>${s.deliverable}</span></div></li>`).join('');const images=[p.draft?`<figure><img src="${p.draft}" alt="${p.id} AI design"><figcaption>AI / 设计稿</figcaption></figure>`:'',p.deconstruction?`<figure><img src="${p.deconstruction}" alt="${p.id} deconstruction board"><figcaption>解构板 / 图层、文字与交互映射</figcaption></figure>`:'',p.deconstructionDetail?`<figure><img src="${p.deconstructionDetail}" alt="${p.id} detailed deconstruction"><figcaption>解构细图 / 顶部状态栏子部件</figcaption></figure>`:'',p.assets?`<figure><img src="${p.assets}" alt="${p.id} asset contact sheet"><figcaption>素材清单 / 生成与处理结果</figcaption></figure>`:'',(p.runtime||p.approved)?`<figure><img src="${p.runtime||p.approved}" alt="${p.id} runtime"><figcaption>${p.runtime?runtimeLabel:'批准设计稿'}</figcaption></figure>`:''].join('');$('#detail').innerHTML=`<span class="pill ${statusClass(p)} detail-status">${p.statusLabel}</span><h2 id="detail-title">${p.id} · ${p.name}</h2><p class="lead">${p.group} · 生产门 ${p.stage}/${p.stageTotal}</p><div class="detail-images">${images}</div><div class="detail-block"><h3>制作流程</h3><ol class="workflow">${workflow}</ol></div><div class="detail-block"><h3>当前缺口</h3><p>${p.missing}</p></div><div class="detail-block"><h3>页面资料(仓库路径)</h3><ul class="refs">${p.references.length?p.references.map(r=>`<li><span>${r.label} · ${r.path}</span></li>`).join(''):'<li>尚无个人页面资料包。</li>'}</ul></div>`;$('#drawer').classList.add('is-open');$('#drawer').setAttribute('aria-hidden','false');}
|
||
function openDetail(id){const p=DATA.pages.find(x=>x.id===id);if(!p)return;const runtimeLabel=p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 运行候选';const workflow=p.stageDetails.map(s=>`<li class="${s.phase}"><div><b>${s.label} <small>${s.id}</small></b><span>${s.deliverable}</span></div></li>`).join('');const images=[p.draft?`<figure><img src="${p.draft}" alt="${p.id} AI design"><figcaption>AI / 设计稿</figcaption></figure>`:'',p.deconstruction?`<figure><img src="${p.deconstruction}" alt="${p.id} deconstruction board"><figcaption>解构板 / 图层、文字与交互映射</figcaption></figure>`:'',p.assets?`<figure><img src="${p.assets}" alt="${p.id} asset contact sheet"><figcaption>素材清单 / 生成与处理结果</figcaption></figure>`:'',(p.runtime||p.approved)?`<figure><img src="${p.runtime||p.approved}" alt="${p.id} runtime"><figcaption>${p.runtime?runtimeLabel:'批准设计稿'}</figcaption></figure>`:''].join('');const captures=p.runtimeCaptures?.length?`<div class="detail-block"><h3>Runtime 验证截图(${p.runtimeCaptures.length} 张)</h3><div class="capture-grid">${p.runtimeCaptures.map((c,i)=>`<figure><img src="${c.src}" alt="${p.id} runtime capture ${i+1}" loading="lazy"><figcaption>${c.path.split('/').pop()}</figcaption></figure>`).join('')}</div></div>`:'';$('#detail').innerHTML=`<span class="pill ${statusClass(p)} detail-status">${p.statusLabel}</span><h2 id="detail-title">${p.id} · ${p.name}</h2><p class="lead">${p.group} · 生产门 ${p.stage}/${p.stageTotal}</p><div class="detail-images">${images}</div>${captures}<div class="detail-block"><h3>制作流程</h3><ol class="workflow">${workflow}</ol></div><div class="detail-block"><h3>当前缺口</h3><p>${p.missing}</p></div><div class="detail-block"><h3>页面资料(仓库路径)</h3><ul class="refs">${p.references.length?p.references.map(r=>`<li><span>${r.label} · ${r.path}</span></li>`).join(''):'<li>尚无个人页面资料包。</li>'}</ul></div>`;$('#drawer').classList.add('is-open');$('#drawer').setAttribute('aria-hidden','false');}
|
||
function setFilter(value){filter=value;$$('[data-filter]').forEach(b=>b.classList.toggle('is-active',b.dataset.filter===value));render()}
|
||
function updateMetrics(){const c=DATA.counts;$('#metric-complete').textContent=c.complete;$('#metric-active').textContent=c.active;$('#metric-not').textContent=c.notStarted;$('#metric-art').textContent=c.withDesign;$('#metric-runtime').textContent=c.withRuntime;$('#nav-all').textContent=c.total;$('#nav-complete').textContent=c.complete;$('#nav-active').textContent=c.active;$('#nav-not').textContent=c.notStarted;$('#nav-art').textContent=c.withDesign;const pct=Math.round(c.complete/c.total*100);$('#progress-value').textContent=`${pct}%`;$('#progress-fill').style.width=`${pct}%`;$('#progress-note').textContent=`还差 ${c.total-c.complete} 个页面`}
|
||
$$('[data-filter]').forEach(b=>b.onclick=()=>{setFilter(b.dataset.filter);if(window.innerWidth<=800)$('#toggle-side').click()});$('#search').oninput=e=>{query=e.target.value.trim();render()};$('#reset-filter').onclick=()=>{$('#search').value='';query='';setFilter('all')};$('#toggle-table').onclick=()=>{listMode=!listMode;$('#toggle-table').textContent=listMode?'看板视图':'列表视图';render()};$('#toggle-side').onclick=()=>{const open=$('.side').classList.toggle('is-open');$('#toggle-side').setAttribute('aria-expanded',String(open))};$('#drawer-close').onclick=()=>{$('#drawer').classList.remove('is-open');$('#drawer').setAttribute('aria-hidden','true')};$('#drawer').onclick=e=>{if(e.target.id==='drawer')$('#drawer-close').click()};document.addEventListener('keydown',e=>{if(e.key==='Escape')$('#drawer-close').click()});$('#generated').textContent=`数据生成于 ${DATA.generatedAt}`;updateMetrics();render();const initialPage=decodeURIComponent(location.hash.slice(1));if(initialPage)openDetail(initialPage);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
|
||
def main() -> None:
|
||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
payload = json.dumps(gallery_data(), ensure_ascii=False, separators=(",", ":"))
|
||
OUTPUT.write_text(HTML.replace("__DATA__", payload), encoding="utf-8")
|
||
print(f"ABG_DESIGN_GALLERY_OK pages={len(json.loads(payload)['pages'])} output={OUTPUT}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|