const state = { data: null, view: "overview", screenId: "SYS-004", screenCategory: "ALL", chapter: 0, flowIndex: 0, queries: {}, }; const viewNames = { overview: "总览", screens: "页面规格与缺口", flow: "流程规格链", chapters: "八章画面", characters: "角色图鉴", professions: "职业系统", enemies: "敌人图鉴", items: "装备与物品", records: "成就与收藏", production: "美术与音频生产", readiness: "开发准备度", }; const screenCategoryNames = { ALL: "全部", SYS: "系统", GLD: "公会", WRK: "工坊", REC: "档案", WLD: "世界", EXP: "远征", BAT: "战斗", RES: "结果", SET: "设置", }; const localizedScreenNames = { "SYS-001": "启动画面", "SYS-002": "首次无障碍设置", "SYS-003": "语言与安全区校准", "SYS-004": "标题首页", "SYS-005": "游戏存档", "SYS-006": "新游戏设置", "SYS-007": "回归摘要", "SYS-008": "存档恢复", "SYS-009": "完成度总览", "SYS-010": "制作人员、许可与数据说明", }; function screenName(screen) { return localizedScreenNames[screen.id] || screen.name; } function layoutKind(screen) { const prefix = screen.id.slice(0, 3); if (screen.id === "SYS-001") return "dedicated"; if (screen.id === "GLD-005") return "family-formation"; if (["EXP-002", "EXP-003", "WLD-001", "WLD-002", "WLD-003"].includes(screen.id)) return "family-route"; if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) return "family-battle"; if (prefix === "SET") return "family-settings"; if (prefix === "REC") return "family-records"; return "missing"; } function coverage(screen) { const kind = layoutKind(screen); if (kind === "dedicated") return { key: "dedicated", label: "逐页设计" }; if (kind.startsWith("family-")) return { key: "family", label: "页面族示意" }; return { key: "missing", label: "未设计" }; } function reviewCopy(screen) { if (screen.id === "SYS-001") { return { focal: "游戏标志、加载光线、当前载入状态", action: "无需操作;完成后自动进入首次设置或标题首页", variants: "首次启动、内容迁移、可恢复载入错误;错误时才显示重试与诊断入口", }; } return screen; } const chapterImages = [ "assets/web/ch_ch01.webp", "assets/web/ch_ch02.webp", "assets/web/ch_ch03.webp", "assets/web/ch_ch04.webp", "assets/web/ch_ch05.webp", "assets/web/ch_ch06.webp", "assets/web/ch_ch07.webp", "assets/web/ch_ch08.webp", ]; const flowIds = [ "SYS-004", "GLD-001", "GLD-005", "WLD-001", "EXP-002", "EXP-004", "BAT-002", "BAT-005", "BAT-006", "BAT-007", "RES-001", "RES-002", "EXP-008", "EXP-009", "EXP-010", "RES-005", "GLD-001", ]; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; function escapeHtml(value = "") { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function splitIdentity(identity) { const match = identity.match(/^([^ ]+)\s+(.+)$/); return match ? { id: match[1], name: match[2] } : { id: identity, name: identity }; } function getScreen(id) { return state.data.screens.find((screen) => screen.id === id); } function chapterNumberFromIdentity(identity) { const match = identity.match(/CH-(\d{2})/); return match ? Number(match[1]) : 1; } function setView(view, options = {}) { state.view = view; if (options.screenId) state.screenId = options.screenId; if (Number.isInteger(options.chapter)) state.chapter = options.chapter; history.replaceState(null, "", `#${view}`); render(); closeMobileNav(); $("#content")?.focus({ preventScroll: true }); } function viewHeading(eyebrow, title, description, aside = "") { return `
${escapeHtml(eyebrow)}

${escapeHtml(title)}

${escapeHtml(description)}

${aside}
`; } function metric(value, label) { return `
${escapeHtml(value)}${escapeHtml(label)}
`; } function chip(label) { return `${escapeHtml(label)}`; } function renderOverview() { const counts = state.data.counts; return `
${viewHeading( "Complete pre-production", "先把整款游戏设计清楚,再进入开发", "Aetherbound Guild 是一款横屏 2D 日系手绘的六人编队策略 RPG。玩家不是连续点技能,而是读懂威胁、调整编队与规则、让方案自动执行,并在关键时刻用两次 Directive 改变战局。", `${chip("25-35 小时主线")} ${chip("一次性买断")} ${chip("iOS / Android / PC / Console")}`, )}
第一章风车中继战斗概念图:三名冒险者面对机械钟兽
CH-01 DYNAMIC SLICE

看预告、改规则、验证判断

第一视觉焦点是敌方的中心线预告,第二是 Mara 保护后排,第三是可用的 Retune Directive。概念图只确定方向,HUD 和所有文字由真实界面实现。

${metric(counts.screens, "页面需求定义;UI 尚未逐页完成")} ${metric("8 × 3", "章节路线 / 战场 / 修复画面族")} ${metric(counts.characters, "有姓名、有成长弧的角色")} ${metric(counts.professions, "12 基础 + 24 进阶职业")} ${metric(counts.enemies, "具有独立机制的敌人")} ${metric(counts.items, "逐件定义效果与视觉的装备")} ${metric("30", "自适应音乐套件")} ${metric("710", "需要被验证的 SFX 事件身份")}
PLAYER LOOP

玩家真正反复做什么

读懂问题

先看敌人的目标、倒计时、路线压力与奖励风险。所有必要信息在承诺前可见,随机只提出问题,不直接判玩家输。

改变方案

调整六人位置、职业、技能、装备、目标策略和 Standing Orders。每个选择都必须改变首个失败点或后续机会。

形成世界后果

战斗执行准备,奖励进入 Unsecured Cache;到 Anchor 决定安全与风险。每章修复一种公共服务,并改变下一轮路线。

REVIEW PATH

建议你怎么检查

1. 先看核心流程

从 Title 点到 Guild、编队、地图、路线、战斗、诊断、奖励、Anchor 与修复,判断每一步是否知道为什么要点。

2. 再看八章差异

检查每章是否具有不同的环境、学习目标、敌人问题、Boss 测试和永久世界变化,而不是换背景和数值。

3. 最后看完整内容

搜索 30 角色、36 职业、100 敌人、320 装备,确认它们都有玩法用途、反制关系和生产需求。

`; } function artForScreen(screen) { const prefix = screen.id.slice(0, 3); if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) { return "assets/web/ch01_battle.webp"; } if (prefix === "REC") { if (screen.id === "REC-002") return "assets/web/el_enemies.webp"; if (screen.id === "REC-003") return "assets/web/el_items.webp"; return "assets/web/el_heroes.webp"; } if (["GLD-003", "GLD-004", "GLD-007", "GLD-008", "GLD-009", "GLD-011", "GLD-012"].includes(screen.id)) { return screen.id === "GLD-007" ? "assets/web/el_profession_kits.webp" : "assets/web/el_heroes.webp"; } if (prefix === "WLD" || prefix === "EXP" || prefix === "WRK" || prefix === "GLD" || prefix === "SYS" || prefix === "RES") { return "assets/web/ch_ch01.webp"; } return ""; } function stageTop(screen) { return `
${escapeHtml(screen.id)} 目标 · 修复风车中继
Saved Cache 180
`; } function renderRouteStage(screen) { return ` ${stageTop(screen)}
CH-01 · 风车盆地${escapeHtml(screen.name)}

路线提前显示威胁、奖励、Cache 风险与 Anchor 距离。

`; } function renderFormationStage(screen) { const slots = ["Mara · Front", "Oren · Mid", "Tamsin · Rear", "Suri · Front", "Ilyo · Rear", "空位"]; return ` ${stageTop(screen)}
WEAVE GRID${escapeHtml(screen.name)}

点角色再点目标位置;拖动只是可选快捷方式。

${slots.map((slot, index) => ``).join("")}

首次受击预测

Mara 拦截中心线
Tamsin 暴露于 Gust
角色标签 4/6 不重复
`; } function renderBattleStage(screen) { const directiveFlow = ["BAT-005", "BAT-006", "BAT-007"].includes(screen.id); return ` ${stageTop(screen)}
UNSECURED CACHE180
`; } function renderSettingsStage(screen) { return `
SETTINGS显示与大字动效与闪烁音频与字幕控制方式存档与云端

${escapeHtml(screen.name)}

UI 与文字缩放 · 130%
减少动态
高对比目标轮廓
字幕与事件标签
低功耗模式
`; } function renderRecordsStage(screen, art) { return ` ${stageTop(screen)}
概念元素图
${escapeHtml(screen.name)}

图鉴同时展示身份、机制、反制、获取与完成状态。

已研究 · 机制完整
可用反制 · 2
章节关联 · CH-01
生产状态 · Concept
`; } function renderBootStage() { return `
AETHERBOUND GUILD裂脉远征团

让每一次修复,改变下一段远征。

正在校验远征档案 · 68%

完成后自动进入,无需点击
`; } function renderMissingStage(screen) { const copy = reviewCopy(screen); return `
UI COVERAGE GAP · ${escapeHtml(screen.id)} ${escapeHtml(screenName(screen))}尚未完成页面设计

目前只有页面需求,不能把这块内容当作游戏画面审核。

玩家首先需要看见
${escapeHtml(copy.focal)}
页面必须支持
${escapeHtml(copy.action || "无需操作")}
`; } function renderGameStage(screen) { const prefix = screen.id.slice(0, 3); const kind = layoutKind(screen); const art = artForScreen(screen); const light = prefix === "SET" || prefix === "REC"; let content; if (kind === "dedicated") content = renderBootStage(); else if (kind === "family-formation") content = renderFormationStage(screen); else if (kind === "family-route") content = renderRouteStage(screen); else if (kind === "family-battle") content = renderBattleStage(screen); else if (kind === "family-settings") content = renderSettingsStage(screen); else if (kind === "family-records") content = renderRecordsStage(screen, art); else content = renderMissingStage(screen); const decorativeArt = art && prefix !== "REC" && kind !== "dedicated" && kind !== "missing" ? `` : ""; return `
${decorativeArt}${content}
`; } function screenMeta(screen, index, screens) { const copy = reviewCopy(screen); const status = coverage(screen); const previous = screens[(index - 1 + screens.length) % screens.length]; const next = screens[(index + 1) % screens.length]; return `
${escapeHtml(screen.id)}

${escapeHtml(screenName(screen))}

${chip(status.label)} ${chip("人类审核未通过")}
玩家首先需要看见

${escapeHtml(copy.focal)}

玩家在这里做什么

${escapeHtml(copy.action || "无需操作")}

异常与安全退出

${escapeHtml(copy.variants)}

`; } function renderScreens() { const category = state.screenCategory; const filtered = state.data.screens.filter((screen) => category === "ALL" || screen.id.startsWith(`${category}-`)); if (!filtered.some((screen) => screen.id === state.screenId)) state.screenId = filtered[0].id; const screen = getScreen(state.screenId); const index = filtered.findIndex((entry) => entry.id === screen.id); const coverageCounts = state.data.screens.reduce((result, entry) => { result[coverage(entry).key] += 1; return result; }, { dedicated: 0, family: 0, missing: 0 }); return `
${viewHeading("UI COVERAGE AUDIT", "82 个页面需求,不是 82 张完成的 UI", "当前只有启动页完成逐页设计;部分页面仅有共用页面族示意,其余仍是规格缺口。这里用于识别要继续设计什么,不能作为完整 UI 验收。", `${chip(`${coverageCounts.dedicated} 逐页设计`)} ${chip(`${coverageCounts.family} 页面族示意`)} ${chip(`${coverageCounts.missing} 未设计`)}`)}
${Object.entries(screenCategoryNames).map(([key, label]) => ``).join("")}
${renderGameStage(screen)}
${screenMeta(screen, index, filtered)}
`; } function renderFlow() { const flowScreens = flowIds.map(getScreen); const screen = flowScreens[state.flowIndex]; return `
${viewHeading("NAVIGATION SPECIFICATION", "从首页到章节修复的流程规格链", "这条链只证明 17 个需求节点和顺序已经定义;其中多个节点仍没有独立 UI,因此它不是可以进行理解或乐趣验收的产品原型。", `${chip(`${state.flowIndex + 1} / ${flowScreens.length}`)} ${chip("UI 未完成")}`)}
${renderGameStage(screen)}
${screenMeta(screen, state.flowIndex, flowScreens)}
`; } function renderChapters() { const chapter = state.data.chapters[state.chapter]; const { id, name } = splitIdentity(chapter.identity); return `
${viewHeading("8 CHAPTERS · 24 VISUAL FAMILIES", "每一章都提出新的策略问题", "每章都有独立的路线/聚落、代表战场和修复/抉择画面。概念图用于确认环境方向,完整细节以章节策划文档为准。")}
${state.data.chapters.map((entry, index) => ``).join("")}
${escapeHtml(name)} 章节环境概念图
${id}A · 路线 / 聚落${escapeHtml(chapter.region)} · ${escapeHtml(chapter.route)}
${id}B · 代表战场${escapeHtml(chapter.learning)},Boss 通过真实机制检验这一章。
${id}C · 修复 / 抉择${escapeHtml(chapter.change)}
${escapeHtml(id)}

${escapeHtml(name)}

本章学习目标

${escapeHtml(chapter.learning)}

Boss 测试与奖励

${escapeHtml(chapter.boss)}

永久变化

${escapeHtml(chapter.change)}

${chip("Concept art")} ${chip("Owner review open")}
`; } function searchableValues(entries, query) { const normalized = query.trim().toLocaleLowerCase(); if (!normalized) return entries; return entries.filter((entry) => Object.values(entry).join(" ").toLocaleLowerCase().includes(normalized)); } function renderCatalog(view, entries, config) { const query = state.queries[view] || ""; const filtered = searchableValues(entries, query); const shown = filtered.slice(0, config.limit || 320); return `
${viewHeading(config.eyebrow, config.title, config.description, `${chip(`${filtered.length} / ${entries.length}`)} ${chip("完整权威索引")}`)}
${shown.length ? shown.map((entry) => config.row(entry)).join("") : `
没有匹配内容。请清除搜索条件。
`}
`; } function catalogEntry(identity, meta, body) { const { id, name } = splitIdentity(identity); return `
${escapeHtml(id)}${escapeHtml(name)}${escapeHtml(meta)}

${escapeHtml(body)}

`; } function renderCharacters() { return renderCatalog("characters", state.data.characters, { eyebrow: "30 NAMED ADVENTURERS", title: "每个角色都有玩法、动机与成长弧", description: "角色不是随机兵种皮肤。每个人有稳定身份、职业亲和、招募路径、关系、个人弧线、视觉和双语声音需求。", image: "assets/web/el_heroes.webp", alt: "Mara、Oren 和 Ilyo 三名核心角色概念图", visualNote: "概念板用于锁定成熟比例、职业仍保留人物身份、以及 Guild 修复者的工作质感。并非最终角色立绘或 sprite。", row: (entry) => catalogEntry(entry.identity, "Character", `${entry.gameplay} ${entry.story}`), }); } function renderProfessions() { return renderCatalog("professions", state.data.professions, { eyebrow: "12 BASE · 24 ADVANCED", title: "36 个职业都改变行动逻辑", description: "每个职业拥有资源、两项主动技能、一项被动、AI 优先级、反制面、晋升证明,以及可生产的武器、动作、VFX 与声音身份。", image: "assets/web/el_profession_kits.webp", alt: "六套职业装备与资源装置概念图", visualNote: "职业采用模块化装备和动作套件叠加在 30 个稳定人物身份上,不为每个角色重画一套通用职业人。", row: (entry) => catalogEntry(entry.identity, entry.role.split(".")[0], `${entry.skills} ${entry.build}`), }); } function renderEnemies() { return renderCatalog("enemies", state.data.enemies, { eyebrow: "56 ORDINARY · 24 ELITE · 12 SUB-BOSS · 8 BOSS", title: "100 个敌人不是换色数值包", description: "每个敌人通过独立目标、时序、空间、资源或信息规则改变准备方案,并具有可读预告、至少两个回应和生产规格。", image: "assets/web/el_enemies.webp", alt: "第一章机械敌人、暗潮危险和预告几何概念图", visualNote: "第一章敌人来自被破坏的公共机械,轮廓和动作直接表达机制。后续七章保留各自材料与社会功能。", row: (entry) => catalogEntry(entry.identity, "Enemy mechanic", `${entry.mechanic} 反制读取:${entry.read}`), }); } function renderItems() { return renderCatalog("items", state.data.items, { eyebrow: "320 AUTHORED ITEMS", title: "装备必须改变判断,而不是只抬战力", description: "320 件物品逐件定义槽位、章节、效果、状态依赖、协同、反制、获取、升级与美术/声音。160 件属于行为变化物品。", image: "assets/web/el_items.webp", alt: "Guild 装备、工具、Cache 与 Anchor 元素概念图", visualNote: "物品按真实物件展示,稀有度不会用巨大光效抢走机制信息。最终 320 件都需要独立透明物件图。", row: (entry) => catalogEntry(entry.identity, `${entry.slot} · ${entry.tier} · ${entry.flag}`, `${entry.effect} ${entry.build}`), }); } function renderRecords() { const achievements = state.data.achievements; const collections = state.data.collections; return `
${viewHeading("48 ACHIEVEMENTS · 20 COLLECTIONS", "完成度记录玩家学会了什么", "成就验证真实机制、构筑和章节结果;收藏把区域证据变成 Guild 中可见的物理陈列。它们不靠重复刷次数延长游戏。")}
${metric(48, "机制与叙事成就")}${metric(20, "区域收藏")}${metric("100 h", "全收集与高难完成目标")}${metric("0", "每日任务、通行证与抽卡")}

成就

${achievements.map((entry) => catalogEntry(entry.identity, "Achievement", `${entry.requirement} 奖励:${entry.reward}`)).join("")}

收藏

${collections.map((entry) => catalogEntry(entry.identity, "Collection", `${entry.entries} 陈列:${entry.display}`)).join("")}
`; } function renderProduction() { return `
${viewHeading("COMPLETE PRODUCTION INVENTORY", "美术、动画、音乐和声音怎么做", "完整数量已经拆成可生产的身份包、模块、动作、场景和事件。当前四张 AI 图都是概念方向,不是最终运行时资源。")}
${metric("30", "角色身份 rig + 6 表情")}${metric("72", "主动技能动作 / VFX 合同")}${metric("752+", "非 Boss 敌人动作状态")}${metric("88", "章节环境主状态")}${metric("101", "模块化剧情构图")}${metric("30", "音乐套件")}${metric("94", "环境声配置")}${metric("2,602", "双语最低角色录音资产")}

角色与职业

30 套稳定角色身份 + 36 套职业装备、资源装置和动作模块。职业改变人物的战斗轮廓,但不把角色替换成无名 class sprite。

敌人与装备

100 个敌人各自拥有机制轮廓、动作与预告;320 件物品都有独立物件图,160 件行为物品还必须显示真实触发。

环境与章节

八章共 88 个环境状态和至少 200 个独立道具/结构族。背景、交互物、前景、危险与修复层分开,不能用一张平图冒充地图。

音乐

30 套自适应音乐包含 base、pulse、tension 和 resolution;八章保留不同材料与节拍,最终章组合而不抹平差异。

语音

角色圣经定义 773 条战斗/Hub 语义额度和 144 个角色场景。英文与简中最低 2,602 个录音资产,语义语音严格控制并发。

SFX 与无障碍

710 个事件身份必须与真实提交状态一致。静音、字幕、单声道、输出切换、低功耗和振动关闭都不能改变玩法信息。

先做哪一块

正式批量生产前只做 CH-01 风车中继动态切片:Mara / Oren / Ilyo、Bell-Feeder、两名支援敌人、三条战线、Retune Directive、诊断、Reward → Cache → Anchor,以及 Clinic / Warning Tower 两个永久修复结果。

`; } function renderReadiness() { const rows = [ ["产品与玩家体验", "完整", "Product contract, 25-35h campaign, onboarding and evidence gates", "设计一致性已校验;人类理解/乐趣开放"], ["战斗 / 职业 / 装备", "完整", "Deterministic combat, 12+24 professions, 320 items", "需通过可玩动态切片验证"], ["经济 / 难度 / 存档", "完整", "1700-min authored budget, five traces, atomic save/failure", "公式静态校验通过;运行时未实现"], ["世界与内容", "完整", "30 characters, 100 enemies, 8 chapters, ending/postgame", "数量与链接校验通过;无人工完整试玩"], ["页面与 UI", "未完成", "82 个页面需求已定义;1 页逐页设计、40 页页面族示意", "41 页没有独立 UI;其余页面族也必须逐页深化"], ["美术与动画", "完整清单", "Identity/modular production and CH-01 slice", "仅 4 张概念图;无 final/integrated/runtime"], ["音乐 / 环境 / 语音 / SFX", "完整清单", "30 music, 94 ambience, 2602 voice, 710 events", "未生成、未试听、未授权批量 SFX/music"], ["HTML 产品原型", "失败待修", "内容索引和响应式外壳可用", "Owner 已证明页面表达不可理解;当前只能用于缺口审计"], ["引擎开发", "冻结", "No Godot project or runtime code", "完整设计 Owner 批准后另开 Goal"], ["设备 / 打包 / 上架", "开放", "No build, signing, device or store action", "必须在运行时完成后分别授权"], ]; const blockedStatuses = new Set(["冻结", "开放", "未完成", "失败待修"]); return `
${viewHeading("PRE-DEVELOPMENT GATE", "UI 设计没有完成,不能进入正式开发", "系统和内容文档已经建立,但 82 页 UI 只有 1 页逐页设计。Owner 已确认现有页面难以理解,因此完整产品原型验收失败。")} ${rows.map(([area, status, ready, open]) => ``).join("")}
领域状态已经具备仍然开放
${escapeHtml(area)}${escapeHtml(status)}${escapeHtml(ready)}${escapeHtml(open)}

下一步不是让你继续猜

先逐页完成标题、公会、编队、地图、路线、战斗、结算、奖励、锚点和修复等核心链 UI,再扩展其余页面。新的审核候选必须让页面目的、当前状态和唯一主操作直接可见。

`; } function render() { if (!state.data) return; const renderers = { overview: renderOverview, screens: renderScreens, flow: renderFlow, chapters: renderChapters, characters: renderCharacters, professions: renderProfessions, enemies: renderEnemies, items: renderItems, records: renderRecords, production: renderProduction, readiness: renderReadiness, }; $("#content").innerHTML = renderers[state.view](); $("#view-title").textContent = viewNames[state.view]; $$(".nav-item").forEach((button) => button.classList.toggle("is-active", button.dataset.view === state.view)); bindViewEvents(); } function bindViewEvents() { $$('[data-open-view]').forEach((button) => button.addEventListener("click", () => setView(button.dataset.openView))); $$('[data-screen-category]').forEach((button) => button.addEventListener("click", () => { state.screenCategory = button.dataset.screenCategory; render(); })); $$('[data-screen-id]').forEach((button) => button.addEventListener("click", () => { state.screenId = button.dataset.screenId; render(); })); $$('[data-chapter]').forEach((button) => button.addEventListener("click", () => { state.chapter = Number(button.dataset.chapter); render(); })); $$('[data-flow-index]').forEach((button) => button.addEventListener("click", () => { state.flowIndex = Number(button.dataset.flowIndex); render(); })); $$('[data-flow-next]').forEach((button) => button.addEventListener("click", () => { state.flowIndex = (state.flowIndex + 1) % flowIds.length; if (state.view !== "flow") setView("flow"); else render(); })); $$('[data-flow-prev]').forEach((button) => button.addEventListener("click", () => { state.flowIndex = (state.flowIndex - 1 + flowIds.length) % flowIds.length; render(); })); $$('[data-catalog-search]').forEach((input) => input.addEventListener("input", () => { const view = input.dataset.catalogSearch; state.queries[view] = input.value; render(); const replacement = $(`[data-catalog-search="${view}"]`); replacement?.focus(); replacement?.setSelectionRange(replacement.value.length, replacement.value.length); })); } function openMobileNav() { $("#sidebar").classList.add("is-open"); $("#nav-scrim").hidden = false; } function closeMobileNav() { $("#sidebar").classList.remove("is-open"); $("#nav-scrim").hidden = true; } function openCurrentStage() { const stage = $(".game-stage"); if (!stage) return; const dialog = $("#stage-dialog"); $("#dialog-stage").replaceChildren(stage.cloneNode(true)); dialog.showModal(); } async function init() { try { const embeddedData = $("#prototype-data"); if (embeddedData) { state.data = JSON.parse(embeddedData.textContent); } else { const response = await fetch("data/catalog.json"); if (!response.ok) throw new Error(`HTTP ${response.status}`); state.data = await response.json(); } const hashView = location.hash.replace("#", ""); if (viewNames[hashView]) state.view = hashView; render(); } catch (error) { $("#content").innerHTML = `

设计索引载入失败

${escapeHtml(error.message)}

请通过本地 HTTP 服务或 Artifact Gallery 打开此页面。

`; } } $("#primary-nav").addEventListener("click", (event) => { const button = event.target.closest("[data-view]"); if (button) setView(button.dataset.view); }); $("#open-nav").addEventListener("click", openMobileNav); $("#close-nav").addEventListener("click", closeMobileNav); $("#nav-scrim").addEventListener("click", closeMobileNav); $("#fullscreen-current").addEventListener("click", openCurrentStage); $("#close-dialog").addEventListener("click", () => $("#stage-dialog").close()); $("#stage-dialog").addEventListener("click", (event) => { if (event.target === event.currentTarget) event.currentTarget.close(); }); window.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMobileNav(); }); init();