个人页通常从几个按钮起步:我的作品、收藏、设置、关于。功能少时,用 string 传递目标看起来最省事;但当字符串同时控制底部页签和个人页面板,任何拼写差异都可能把页面带到一个不存在的状态。
本文以 profilePanel、handleProfileTarget() 和 openProfilePanel() 为例,把松散字符串改造成类型化动作协议,让每次点击只有一条明确路径,并让新增入口时的遗漏在编译阶段暴露出来。

一、字符串路由的问题不只是容易拼错
下面这类签名允许调用方传入任意内容:
ts
private handleProfileTarget(target: string): void
private openProfilePanel(panel: string): void
若传入 work 而不是 works,代码仍能编译。更隐蔽的问题是,target 有时表示底部页签,有时表示个人页面板,两个不同概念被塞进了同一字符串通道。最终表现可能是:
- 未知值被赋给
activeTab,导航没有对应内容; - 没有作品时跳到创建页,有作品时却打开面板,规则藏在点击函数里;
- 面板关闭使用空字符串,空值既表示"关闭"又可能表示"未初始化";
- 新增菜单后忘记同步某个
if分支。
类型化改造的第一步,是承认"页签""面板""菜单动作"是三个领域对象。
二、分别定义页签和面板
使用枚举能让状态值集中、可搜索,也不需要用空字符串表示关闭:
ts
export enum MainTab {
Gallery = 'gallery',
Create = 'create',
Profile = 'profile'
}
export enum ProfilePanel {
None = 'none',
Account = 'account',
Works = 'works',
Settings = 'settings',
About = 'about'
}
@State activeTab: MainTab = MainTab.Gallery
@State profilePanel: ProfilePanel = ProfilePanel.None
None 是一个明确状态,不再依赖 '' 的隐含约定。页面渲染时也可以直接比较枚举成员。
三、菜单传递动作而不是目标字符串
"我的作品"不是简单的导航目标:有记录时打开作品面板,没有记录时进入创建页。它更接近一条命令。
ts
export enum ProfileAction {
OpenAccount = 'open-account',
OpenWorks = 'open-works',
OpenFavorites = 'open-favorites',
OpenSettings = 'open-settings',
OpenAbout = 'open-about'
}
export interface ProfileMenuItem {
title: string
subtitle: string
action: ProfileAction
}
这样菜单配置只声明用户意图,不必知道最终是切页还是开面板。
四、集中路由并穷尽每个动作
将动作解释集中到一个方法,所有前置条件都能在同一处阅读:
ts
private routeProfileAction(action: ProfileAction): void {
switch (action) {
case ProfileAction.OpenAccount:
this.toggleProfilePanel(ProfilePanel.Account)
return
case ProfileAction.OpenWorks:
this.openWorksOrCreate()
return
case ProfileAction.OpenFavorites:
this.profilePanel = ProfilePanel.None
this.activeTab = MainTab.Gallery
this.galleryMode = 'favorites'
return
case ProfileAction.OpenSettings:
this.toggleProfilePanel(ProfilePanel.Settings)
return
case ProfileAction.OpenAbout:
this.toggleProfilePanel(ProfilePanel.About)
return
}
}
每个 case 都以 return 结束,避免误落入后续分支。新增 ProfileAction 时,路由方法就是唯一需要补齐的入口。

五、把条件跳转提炼成业务方法
作品入口的规则值得单独命名,避免在多处复制"记录数量大于零"的判断:
ts
private openWorksOrCreate(): void {
if (this.generatedRecords.length === 0) {
this.profilePanel = ProfilePanel.None
this.activeTab = MainTab.Create
this.createStatus = '先生成一张作品吧'
return
}
this.activeTab = MainTab.Profile
this.toggleProfilePanel(ProfilePanel.Works)
}
方法名直接表达产品规则。未来即使"无作品"改成展示空面板,也只需要修改这一处。
六、面板切换只接受 ProfilePanel
切换函数不再接受任意字符串,并显式拒绝打开 None:
ts
private toggleProfilePanel(panel: ProfilePanel): void {
if (panel === ProfilePanel.None) {
this.profilePanel = ProfilePanel.None
return
}
this.profilePanel = this.profilePanel === panel
? ProfilePanel.None
: panel
}
调用方不能误把 MainTab.Create 传进来。这个小约束切断了页签值和面板值混用的可能。
七、渲染层也使用穷尽分支
多个独立 if 容易在状态调整后同时渲染或漏掉回退。可以让面板内容只由一个状态分派:
ts
@Builder
private ProfilePanelContent() {
if (this.profilePanel === ProfilePanel.Account) {
this.AccountPanel()
} else if (this.profilePanel === ProfilePanel.Works) {
this.WorksPanel()
} else if (this.profilePanel === ProfilePanel.Settings) {
this.SettingsPanel()
} else if (this.profilePanel === ProfilePanel.About) {
this.AboutPanel()
} else {
this.ProfileOverview()
}
}
最后的 else 对应 None,页面始终有明确内容,不会出现空白区域。
八、导航状态要一次提交
一次路由往往会同时改变 activeTab、profilePanel 和筛选模式。若更新顺序分散,界面可能短暂进入非法组合。可以用一个导航快照统一提交:
ts
export interface NavigationState {
tab: MainTab
panel: ProfilePanel
galleryMode: 'all' | 'favorites'
}
private applyNavigation(next: NavigationState): void {
this.activeTab = next.tab
this.profilePanel = next.panel
this.galleryMode = next.galleryMode
}
例如进入收藏时一次提交 { tab: Gallery, panel: None, galleryMode: favorites },不会保留上一个个人页面板。
九、为非法组合建立不变量
即使字段都有类型,不同字段之间仍可能形成不合理组合。可以把关系写进断言:
ts
export function validateNavigation(state: NavigationState): string[] {
const issues: string[] = []
if (state.tab !== MainTab.Profile && state.panel !== ProfilePanel.None) {
issues.push('非个人页不能保持个人页面板')
}
if (state.tab !== MainTab.Gallery && state.galleryMode === 'favorites') {
issues.push('收藏筛选只能属于图库页')
}
return issues
}
调试构建或单元测试中运行这些断言,可以更早发现"值合法但组合非法"的状态。
十、测试关注结果状态而不是点击实现
路由测试不需要渲染完整页面,只要给定动作和前置数据,验证最终状态:
| 前置条件 | 动作 | 期望页签 | 期望面板 |
|---|---|---|---|
| 无生成记录 | 我的作品 | Create | None |
| 有生成记录 | 我的作品 | Profile | Works |
| 任意 | 设置 | Profile | Settings |
| 任意 | 再点设置 | Profile | None |
| 任意 | 收藏 | Gallery | None |
另外要验证每次离开个人页后,profilePanel 必须回到 None,避免返回时恢复了用户并未重新选择的面板。

十一、落地检查清单
- 页签、面板和菜单动作使用不同类型;
- 空面板由
ProfilePanel.None表示; - 菜单项只携带
ProfileAction; - 条件跳转规则集中在具名方法;
- 离开个人页时关闭个人页面板;
- 导航快照不会产生非法组合;
- 新增动作时同步补充路由与测试。
改造完成后应从每个底部页签进入个人页,再依次点击所有菜单并返回。除了验证目标内容,还要确认旧面板和旧筛选已经清理;否则类型虽然合法,跨页残留仍可能形成不符合产品规则的组合状态。
总结
字符串路由最大的代价不是代码长,而是状态边界不可见。把个人页交互拆成 MainTab、ProfilePanel 和 ProfileAction 后,点击意图、业务规则和最终界面状态各司其职。Harmony os 页面即使继续增长,也能沿着一条可读、可验证的协议演进。
标签:Harmony os、ArkTS、ArkUI、类型建模、页面路由