页面层级与导航规范 v1
适用范围:
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>
placement:center(居中卡片)/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)走drawerpush;宽屏保留内联双栏。 presentation:drawer从底部升起 /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. 约束清单
- 数据层(
data/ui/*.ts)只放 type / const,禁止函数。 - zIndex / 栈操作只在逻辑层解析,UI 不写魔法数字、不直接改栈数组。
- 新增二级页面:在
NAV_VIEW补一行 + 在对应宿主renderView注册,不再 prop drilling 开关。 - 新增弹窗:优先
ModalBase+useModalStack,不再手写fixed inset-0 z-50遮罩。 - 跨系统跳转意图走
NavigateHooks,Tab 切换等副作用在 App 层 wire。