HarmonyOS 分层架构 — phone 壳工程与 HdsTabs 沉浸光感框架实战
效果
一、phone 壳工程职责
在分层架构中,phone 模块是应用的入口壳工程(Entry Module),它不承载任何业务逻辑,而是承担以下四项职责:
| 职责 | 说明 |
|---|---|
| 应用入口 | EntryAbility 作为 UIAbility 启动点,负责窗口初始化、安全区域采集 |
| 依赖聚合 | 通过 oh-package.json5 汇聚 common + 所有 features/* 模块 |
| Tab 导航 | MainPage 使用 HdsTabs 组装首页/游园/地图/我的四个 TabContent |
| 路由注册与分发 | RouterConfig 注册全局路由表,routeMapBuilder 根据路由名分发页面 |
核心原则:phone 模块只做"胶水"——把各 feature 模块的页面"粘"到导航框架上,自身不包含业务 UI 组件。
二、工程配置
2.1 module.json5 — Entry 类型声明
{
"module": {
"name": "phone",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:startIcon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
"requestPermissions": [
{ "name": "ohos.permission.GET_NETWORK_INFO" },
{ "name": "ohos.permission.INTERNET" },
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:module_desc",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:module_desc",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
关键配置解读:
"type": "entry":标识为入口模块,每个 HAP 包有且仅有一个 entry。"mainElement": "EntryAbility":指定 Ability 入口类,系统启动时自动调用其onCreate。"pages": "$profile:main_pages":路由表指向resources/base/profile/main_pages.json,注册所有可导航页面。- 权限声明:网络信息 + 互联网访问 + 模糊定位 + 精确定位,覆盖旅游 App 的地图与数据请求场景。
2.2 oh-package.json5 — 依赖聚合
{
"name": "phone",
"version": "1.0.0",
"description": "手机端壳模块",
"main": "",
"author": "",
"license": "",
"dependencies": {
"common": "file:../common",
"home": "file:../features/home",
"ParkService": "file:../features/ParkService",
"MapService": "file:../features/MapService",
"AccountCenter": "file:../features/AccountCenter",
"PersonalCenter": "file:../features/PersonalCenter"
}
}
架构意义:
- phone 模块是唯一同时依赖所有 feature 模块的层,它扮演"总装车间"角色。
- 各 feature 之间互不依赖——ParkService 不知道 MapService 的存在,它们通过 common 层的
RouterService间接通信。 "file:../"路径引用方式适用于单仓库多模块(Monorepo)结构,hvigor 构建系统自动处理依赖解析。
2.3 build-profile.json5 — 构建配置
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": ["./obfuscation-rules.txt"]
}
}
}
}
],
"targets": [
{ "name": "default" },
{ "name": "ohosTest" }
]
}
"apiType": "stageMode":使用 Stage 模型(推荐),区别于旧版 FA 模型。"copyCodeResource": { "enable": false }:不拷贝代码资源,减小包体积。- targets:
default(正式构建)+ohosTest(仪器测试),两套 target 共用同一份构建配置。
三、EntryAbility — 入口能力
EntryAbility 是整个 App 的生命周期起点,完整代码如下:
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { RdbService } from 'common';
const TAG: string = 'EntryAbility';
const DOMAIN: number = 0x0001;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);
hilog.info(DOMAIN, TAG, 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, TAG, 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, 'Ability onWindowStageCreate');
windowStage.getMainWindow().then((windowClass: window.Window) => {
// ① 窗口布局配置(非全屏模式,系统自动处理状态栏安全区域)
windowClass.setWindowLayoutFullScreen(false).then(() => {
hilog.info(DOMAIN, TAG, 'Window layout configured');
});
// ② 安全区域采集
const navArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('bottomRectHeight', navArea.bottomRect.height);
const sysArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', sysArea.topRect.height);
});
// ③ RDB 本地存储初始化
RdbService.getInstance().initStore(this.context);
// ④ 加载主页面
windowStage.loadContent('pages/MainPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, TAG, 'Succeeded in loading the content');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, TAG, 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, TAG, 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, TAG, 'Ability onBackground');
}
}
3.1 关键步骤拆解
| 步骤 | 代码位置 | 说明 |
|---|---|---|
| ① 窗口布局 | setWindowLayoutFullScreen(false) | 使用非全屏模式,系统自动处理状态栏安全区域,避免内容被遮挡 |
| ② 安全区域采集 | getWindowAvoidArea() × 2 | 获取顶部系统栏高度和底部导航指示器高度,存入 AppStorage 供全局使用 |
| ③ RDB 初始化 | RdbService.getInstance().initStore() | 在页面加载前完成本地数据库初始化,确保页面 aboutToAppear 时可立即查询 |
| ④ 页面加载 | loadContent('pages/MainPage') | 加载入口页面,即下文详述的 MainPage |
3.2 颜色模式
this.context.getApplicationContext().setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);
COLOR_MODE_NOT_SET 表示跟随系统设置(亮色/暗色自适应),不强制指定。如需锁定亮色模式,可改为 COLOR_MODE_LIGHT。
四、HdsNavigation + HdsTabs 沉浸光感框架
MainPage 是整个 App 的核心页面,它使用 HarmonyOS 6.1 新增的 @kit.UIDesignKit 中的 HdsNavigation 和 HdsTabs 组件,构建了一套沉浸光感导航框架。
4.1 完整代码
import {
HdsNavigation, HdsNavigationTitleMode, HdsNavigationTitleBarOptions,
HdsTabs, HdsTabsController, hdsMaterial
} from '@kit.UIDesignKit';
import { RouterService, RouteConstants, UserInfo, ThemeConstants } from 'common';
import { HomePage } from 'home';
import { ParkListPage, ParkDetailPage } from 'ParkService';
import { MapPage } from 'MapService';
import { LoginPage, RegisterPage, ProfileEditPage } from 'AccountCenter';
import {
PersonalCenterPage,
FavoritesPage, HistoryPage, SettingsPage, WebViewPage
} from 'PersonalCenter';
import { RouterConfig } from '../router/RouterConfig';
@Entry
@ComponentV2
struct MainPage {
@Local currentTabIndex: number = 0;
@Local navPathStack: NavPathStack = new NavPathStack();
@Local topRectHeight: number = 0;
@Local bottomRectHeight: number = 0;
@Provider('UserInfo') userInfo: UserInfo = new UserInfo();
private tabsController: HdsTabsController = new HdsTabsController();
aboutToAppear(): void {
RouterService.registerNavPathStack(this.navPathStack);
RouterConfig.registerAllRoutes();
}
@Builder
routeMapBuilder(name: string, param: object) {
if (name === RouteConstants.PARK_DETAIL_PAGE) {
ParkDetailPage({ spotId: (param as Record<string, string>)['spotId'] ?? '' })
} else if (name === RouteConstants.LOGIN_PAGE) {
LoginPage()
} else if (name === RouteConstants.REGISTER_PAGE) {
RegisterPage()
} else if (name === RouteConstants.FAVORITES_PAGE) {
FavoritesPage({ filterType: (param as Record<string, string>)['filterType'] ?? '' })
} else if (name === RouteConstants.HISTORY_PAGE) {
HistoryPage()
} else if (name === RouteConstants.SETTINGS_PAGE) {
SettingsPage()
} else if (name === RouteConstants.WEB_VIEW_PAGE) {
WebViewPage({
url: (param as Record<string, string>)['url'] ?? '',
title: (param as Record<string, string>)['title'] ?? ''
})
} else if (name === RouteConstants.PROFILE_EDIT_PAGE) {
ProfileEditPage()
}
}
build() {
Column() {
HdsNavigation() {
HdsTabs({ controller: this.tabsController }) {
TabContent() {
Column() {
HomePage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83C\uDFE0', '\uD83C\uDFE0', '首页', 0))
TabContent() {
Column() {
ParkListPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83C\uDF3F', '\uD83C\uDF3F', '游园', 1))
TabContent() {
Column() {
MapPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83D\uDDFA\uFE0F', '\uD83D\uDDFA\uFE0F', '地图', 2))
TabContent() {
Column() {
PersonalCenterPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83D\uDC64', '\uD83D\uDC64', '我的', 3))
}
.barOverlap(true)
.vertical(false)
.barPosition(BarPosition.End)
.barFloatingStyle({
barBottomMargin: 28,
systemMaterialEffect: {
materialType: hdsMaterial.MaterialType.ADAPTIVE,
materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}
})
.onChange((index: number) => {
this.currentTabIndex = index;
})
}
.mode(NavigationMode.Stack)
.navDestination(this.routeMapBuilder)
.titleBar({
content: { title: { mainTitle: '广州旅游住宿' } },
style: {
systemMaterialEffect: {
materialType: hdsMaterial.MaterialType.ADAPTIVE,
materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}
}
} as HdsNavigationTitleBarOptions)
.titleMode(HdsNavigationTitleMode.MINI)
.hideBackButton(true)
}
.width('100%')
.height('100%')
}
@Builder
tabBarBuilder(normalEmoji: string, selectedEmoji: string, label: string, index: number) {
Column() {
Text(this.currentTabIndex === index ? selectedEmoji : normalEmoji)
.fontSize(22)
.fontColor(this.currentTabIndex === index ? '#1A73E8' : '#5F6368')
Text(label)
.fontSize(10)
.fontColor(this.currentTabIndex === index ? '#1A73E8' : '#5F6368')
.margin({ top: 2 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTabIndex = index;
this.tabsController.changeIndex(index);
})
}
}
4.2 HdsNavigation 导航容器
HdsNavigation 是 @kit.UIDesignKit 提供的高阶导航容器,等价于 Navigation 组件的 HDS(HarmonyOS Design System)增强版:
HdsNavigation() {
// 内部放置 HdsTabs
}
.mode(NavigationMode.Stack) // 栈式导航,支持 push/pop
.navDestination(this.routeMapBuilder) // 路由目标页面构建器
.titleBar({
content: { title: { mainTitle: '广州旅游住宿' } },
style: {
systemMaterialEffect: {
materialType: hdsMaterial.MaterialType.ADAPTIVE,
materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}
}
} as HdsNavigationTitleBarOptions)
.titleMode(HdsNavigationTitleMode.MINI)
.hideBackButton(true) // 隐藏标题栏左侧返回按钮
核心配置:
| 属性 | 值 | 说明 |
|---|---|---|
mode | NavigationMode.Stack | 栈式导航,新页面从右侧滑入,返回时 pop 出栈 |
navDestination | routeMapBuilder | 路由分发 Builder,根据路由名匹配页面 |
titleBar.content | mainTitle: '广州旅游住宿' | 顶部标题栏文字 |
titleBar.style.systemMaterialEffect | ADAPTIVE | 标题栏使用自适应光感材质,滚动时呈现毛玻璃效果 |
titleBar 类型断言 | as HdsNavigationTitleBarOptions | ArkTS 严格模式要求对象字面量必须对应显式类型 |
titleMode | MINI | 始终显示小型标题栏,不随滚动展开/收缩 |
hideBackButton | true | 隐藏标题栏左侧的默认返回按钮,主页面不需要返回操作 |
4.3 HdsTabs 底部导航 + 浮动样式
HdsTabs({ controller: this.tabsController }) {
// 四个 TabContent
}
.barOverlap(true)
.vertical(false)
.barPosition(BarPosition.End)
.barFloatingStyle({
barBottomMargin: 28,
systemMaterialEffect: {
materialType: hdsMaterial.MaterialType.ADAPTIVE,
materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}
})
浮动导航栏是本项目的视觉亮点:
barOverlap(true):TabBar 浮在内容之上,不占用页面空间。barFloatingStyle:启用浮动模式,barBottomMargin: 28让 TabBar 距离屏幕底部 28vp,形成"胶囊"效果。systemMaterialEffect:使用hdsMaterial.MaterialType.ADAPTIVE自适应材质——TabBar 背景会根据底层内容自动调整明暗,实现类似 iOS 毛玻璃的通透感。
4.4 四个 TabContent
TabContent() {
Column() {
HomePage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83C\uDFE0', '\uD83C\uDFE0', '首页', 0))
TabContent() {
Column() {
ParkListPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83C\uDF3F', '\uD83C\uDF3F', '游园', 1))
TabContent() {
Column() {
MapPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83D\uDDFA\uFE0F', '\uD83D\uDDFA\uFE0F', '地图', 2))
TabContent() {
Column() {
PersonalCenterPage()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.tabBar(this.tabBarBuilder('\uD83D\uDC64', '\uD83D\uDC64', '我的', 3))
每个 TabContent 内部用 Column 包裹页面组件,并设置 .justifyContent(FlexAlign.Start) 确保内容顶部对齐,避免 TabContent 默认垂直居中导致页面内容下沉。各页面组件直接实例化对应 feature 模块的入口页面:
HomePage←home模块ParkListPage←ParkService模块MapPage←MapService模块PersonalCenterPage←PersonalCenter模块
4.5 自定义 tabBarBuilder
重要:HarmonyOS 6.1 中
sys.symbol.*和sys.media.ohos_ic_public_*系统图标资源大量废弃或不可用。本项目采用 Unicode emoji 文本 替代所有系统图标,确保在所有设备上稳定显示。
@Builder
tabBarBuilder(normalEmoji: string, selectedEmoji: string, label: string, index: number) {
Column() {
Text(this.currentTabIndex === index ? selectedEmoji : normalEmoji)
.fontSize(22)
.fontColor(this.currentTabIndex === index ? '#1A73E8' : '#5F6368')
Text(label)
.fontSize(10)
.fontColor(this.currentTabIndex === index ? '#1A73E8' : '#5F6368')
.margin({ top: 2 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTabIndex = index;
this.tabsController.changeIndex(index);
})
}
Tab 图标映射:
| Tab | Emoji | Unicode |
|---|---|---|
| 首页 | 🏠 | \uD83C\uDFE0 |
| 游园 | 🌿 | \uD83C\uDF3F |
| 地图 | 🗺️ | \uD83D\uDDFA\uFE0F |
| 我的 | 👤 | \uD83D\uDC64 |
选中态切换逻辑:
- 颜色切换:选中
#1A73E8(品牌蓝),未选中#5F6368(中性灰)。 tabsController.changeIndex(index):通过控制器驱动 Tab 切换,与@Local currentTabIndex双向同步。
4.6 @Provider(UserInfo) 全局状态注入
@Provider('UserInfo') userInfo: UserInfo = new UserInfo();
@Provider 是 @ComponentV2 体系中的跨层级状态共享装饰器。在 MainPage 中声明后,所有子组件(包括深层嵌套的 feature 页面)都可以通过 @Consumer('UserInfo') 获取用户信息,无需逐层 @Prop 传递。
注意:
@Provider和@Consumer的参数必须使用字符串字面量(如'UserInfo'),不能直接使用类型名。这是 ArkTS 严格模式的要求。
// 子组件中使用
@ComponentV2
struct UserAvatar {
@Consumer('UserInfo') userInfo: UserInfo = new UserInfo();
// 直接使用 this.userInfo.nickname 等属性
}
五、路由注册与分发
5.1 RouterConfig 路由表
重要:ArkTS 严格模式下,
@Builder装饰器不能用于 class 内部,只能在全局作用域或struct中使用。因此路由页面的构建改为在MainPage的routeMapBuilder中内联实现。
import { RouteConstants } from 'common';
export class RouterConfig {
static registerAllRoutes(): void {
// 路由页面通过 MainPage 的 navDestination Builder 直接构建
}
}
RouterConfig 简化为仅保留 registerAllRoutes() 方法(预留扩展),所有页面构建逻辑统一在 MainPage.routeMapBuilder 中内联实现:
@Builder
routeMapBuilder(name: string, param: object) {
if (name === RouteConstants.PARK_DETAIL_PAGE) {
ParkDetailPage({ spotId: (param as Record<string, string>)['spotId'] ?? '' })
} else if (name === RouteConstants.LOGIN_PAGE) {
LoginPage()
}
// ... 其他路由
}
5.2 路由分发流程
路由分发的完整链路如下:
feature 模块调用 RouterService.push(路由名, 参数)
↓
RouterService 内部调用 NavPathStack.pushByName(路由名, 参数)
↓
HdsNavigation 的 navDestination 触发 routeMapBuilder
↓
routeMapBuilder 根据路由名直接内联构建页面组件
以“查看景点详情”为例:
// 1. 在 ScenicCard 的点击回调中(home 模块)
RouterService.push(RouteConstants.PARK_DETAIL_PAGE, { spotId: 'gz-001' });
// 2. routeMapBuilder 直接构建页面(无需调用 RouterConfig)
if (name === RouteConstants.PARK_DETAIL_PAGE) {
ParkDetailPage({ spotId: (param as Record<string, string>)['spotId'] ?? '' })
}
参数传递约定:所有路由参数统一使用 Record<string, string> 格式,通过 (param as Record<string, string>)['key'] 取值,并用 ?? '' 提供默认值防止 undefined。
六、安全区域处理
全屏模式下,页面内容会延伸到状态栏和底部导航指示器区域,因此需要精确获取安全区域高度,避免内容被系统 UI 遮挡。
6.1 EntryAbility 中的采集
// 底部导航指示器高度
const navArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('bottomRectHeight', navArea.bottomRect.height);
// 顶部系统栏高度(状态栏 + 挖孔区域)
const sysArea = windowClass.getWindowAvoidArea(
window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', sysArea.topRect.height);
6.2 MainPage 中的消费
由于采用非全屏模式(setWindowLayoutFullScreen(false)),系统自动处理状态栏安全区域,MainPage 无需手动添加顶部 padding。AppStorage 中的 topRectHeight 和 bottomRectHeight 仍可供 feature 模块的页面使用,用于自定义标题栏或底部操作栏的 padding 计算。
// feature 页面中可按需获取
const topHeight = AppStorage.get<number>('topRectHeight');
七、关键注意事项
7.1 NavPathStack 注册时机
aboutToAppear(): void {
RouterService.registerNavPathStack(this.navPathStack);
RouterConfig.registerAllRoutes();
}
NavPathStack 必须在 aboutToAppear 中注册到 RouterService,而不是在构造函数或 build() 中。原因:
aboutToAppear在组件挂载前调用,此时NavPathStack实例已创建。- 如果在
build()中注册,每次 UI 刷新都会重复注册,导致路由栈异常。 - 注册顺序:先注册 NavPathStack(让 RouterService 持有栈引用),再注册路由表(声明可导航页面列表)。
7.2 路由 Builder 内联构建
注意:ArkTS 严格模式下,
@Builder装饰器只能用于全局作用域或struct(组件)中,不能用于普通 class 的静态方法。因此路由页面的构建不能放在RouterConfig类中,而是改为在MainPage的routeMapBuilder中直接内联构建:
@Builder
routeMapBuilder(name: string, param: object) {
if (name === RouteConstants.PARK_DETAIL_PAGE) {
ParkDetailPage({ spotId: (param as Record<string, string>)['spotId'] ?? '' })
} else if (name === RouteConstants.LOGIN_PAGE) {
LoginPage()
}
// ...
}
这种内联方式完全符合 ArkTS 严格模式要求,且路由参数直接传递给页面组件,无需中间转发。
7.3 @ComponentV2 + @Entry
@Entry
@ComponentV2
struct MainPage {
@Entry 标记此组件为页面入口(对应 module.json5 中 pages 配置的页面路径),@ComponentV2 启用 V2 装饰器体系。两者必须同时使用:
- 仅有
@Entry没有@ComponentV2:使用 V1 装饰器体系(@State、@Prop等)。 - 仅有
@ComponentV2没有@Entry:该组件是子组件,不能作为页面入口。 @Entry+@ComponentV2:V2 体系下的页面入口,支持@Local、@Param、@Event、@Provider/@Consumer等新版装饰器。
7.4 HdsTabsController 与 @Local 的协作
@Local currentTabIndex: number = 0;
private tabsController: HdsTabsController = new HdsTabsController();
currentTabIndex用@Local声明为组件本地状态,变更时触发 UI 刷新(tabBarBuilder 中的选中态切换依赖此变量)。tabsController用private声明(非@Local),因为控制器本身不需要触发 UI 刷新——它只是一个"命令式"操作的句柄(如tabsController.changeIndex(index))。
附:页面路由注册文件
在 resources/base/profile/main_pages.json 中注册所有可导航页面:
{
"src": [
"pages/MainPage"
]
}
由于本项目使用 HdsNavigation + navDestination 的动态路由分发模式,只需注册 MainPage 一个页面。其他页面(景点详情、登录、注册等)都通过 routeMapBuilder 动态构建,无需在 main_pages.json 中声明。
277

被折叠的 条评论
为什么被折叠?



