№ · 玩法设计 CH.07.30
主页 · 🎲 玩法设计 · 页面层级与导航规范 v1

页面层级与导航规范 v1

📅 2026-07-30 · 📦 4.6 KB · 🎲 玩法设计 · 📝 MD 文档 🆕 14d 🏷️ 终局🏷️ 规范🏷️ 玩法设计🏷️ 界面

适用范围:src/utils/modalStack/(弹窗体系)、src/utils/navigation/(二级页面视图栈)、src/data/ui/navigateHooks.ts(跨系统导航意图)。 目标:把散落在 App.tsx 的弹窗 useState 与 prop drilling 收敛为三条正交的层级通道,遵循项目三层架构(数据 / 逻辑 / UI)。


1. 三条通道各管一层

| 通道 | 数据层 | 逻辑层 | UI 层 | 语义 | | :-- | :-- | :-- | :-- | :-- | | Modal 弹窗 | data/ui/modal.ts | utils/modalStack/ | components/primitives/Modal/ModalBase | 覆盖在当前页之上的浮层(对账 / 结算 / 确认) | | View 视图栈 | data/ui/navigation.ts | utils/navigation/ | components/primitives/ViewStackHost | 跨系统跳转的二级页面(半屏抽屉 / 全屏 push) | | NavigateHooks | data/ui/navigateHooks.ts | —(意图声明,无逻辑) | 各域组件透传 + App wire | "点了某个实体想干嘛"的意图声明 |

三者不互相联通:Modal 栈 / View 栈是平级独立的 reducer;NavigateHooks 只是把点击意图外抛,由宿主决定落到哪条通道。


2. zIndex 分层约定

统一由逻辑层解析,UI 不写魔法数字。

| 层 | zIndex | 来源 | | :-- | :-- | :-- | | View 视图栈基准 | 80 + 每层 ×2 | data/ui/navigation.ts VIEW_STACK_BASE_Z_INDEX / resolveViewZIndex() | | Modal base | 50 | data/ui/modal.ts MODAL_PRIORITY_Z_INDEX | | Modal elevated | 90 | 同上 | | Modal critical | 120 | 同上(继续周目确认等) | | Modal top | 9998 | 同上(终局 / 调试压顶) | | 嵌套弹窗叠加 | +10 | MODAL_STACKED_Z_OFFSET / resolveModalZIndex(priority, stacked) |

视图栈落在弹窗之下、终局压顶层之上:二级页面是"页面",弹窗是"页面上的浮层"。


3. Modal 接入方式

import { ModalBase } from '../primitives/Modal';

<ModalBase open={open} onClose={onClose} priority="base" placement="center" ariaLabel="秀才对账">
  <div className="...">卡片内容</div>
</ModalBase>
  • placementcenter(居中卡片)/ sheet(底部半屏)/ fullscreen(全屏)。
  • priority:决定 zIndex 档位(见 §2)。
  • stacked:叠加在已有弹窗之上时置 true,自动 +10
  • App 级弹窗状态统一走 useModalStack() + APP_MODAL_ID,不再新增 useState

4. View 视图栈接入方式

// 打开二级页面(收敛所有"打开 X"按钮)
const navigate = useNavigate();
navigate.push(NAV_VIEW.itemDetail, { params: { itemId }, presentation: 'drawer' });

// 宿主:把 viewId + params 映射成带真实 props 的组件(App / 域组件负责,宿主不感知业务数据)
<ViewStackHost
  renderView={({ entry }) => entry.id === NAV_VIEW.itemDetail
    ? <ItemDetailBody itemId={entry.params.itemId as string} {...data} />
    : null}
  resolveTitle={() => '宝具详情'}
/>
  • 竖屏(useAppViewportMode().isPortraitViewport)走 drawer push;宽屏保留内联双栏。
  • presentationdrawer 从底部升起 / fullscreen 横向推入。
  • 返回条 / Esc / 遮罩点击统一由 ViewStackHost 处理(navigate.pop)。

5. NavigateHooks 与红点联动

数据层只声明意图形状(onItemClick / onStaffClick / onDishClick),不引 UI、不写逻辑

export interface NavigateHooks {
  readonly onItemClick?: (itemId: string) => void;
  readonly onStaffClick?: (staffId: StaffId) => void;
  readonly onDishClick?: (dishId: string) => void;
}

UI 层 wire 行为(以行囊装配为例):

  • 装配到门客 → 清门客红点 redDot.clear(RD_TAB.staff) / redDot.clear(RD_ROOT.staff)(缺省行为,组件自带)。
  • 透传 navigateHooks.onStaffClick(staffId) → 由 App 决定是否切到门客名册 Tab(跳转行为在 App 层 wire,组件不硬编码 Tab 切换)。

红点联动准则:谁消费了信息谁清红点;清红点走 redDot 统一 API,禁止散落字符串路径(用 RD_* 常量)。


6. 约束清单

  1. 数据层(data/ui/*.ts)只放 type / const,禁止函数。
  2. zIndex / 栈操作只在逻辑层解析,UI 不写魔法数字、不直接改栈数组。
  3. 新增二级页面:在 NAV_VIEW 补一行 + 在对应宿主 renderView 注册,不再 prop drilling 开关。
  4. 新增弹窗:优先 ModalBase + useModalStack,不再手写 fixed inset-0 z-50 遮罩。
  5. 跨系统跳转意图走 NavigateHooks,Tab 切换等副作用在 App 层 wire。