
引言
启动页(Splash Screen)是用户打开应用时看到的第一个界面,虽然只停留短短2秒,但它决定了用户对应用的第一印象。一个好的启动页应该简洁美观,同时利用这段时间预加载数据,提升用户体验。
为什么启动页如此重要?
用户在点击应用图标后,会经历一个"冷启动"过程。在这个过程中:
- 系统需要加载应用框架(耗时约300-500ms)
- 应用需要初始化基础服务(耗时约200-500ms)
- 最后才显示启动页
如果这个等待时间没有内容展示,用户会看到一片空白,产生"应用没反应"的错觉。而一个精心设计的启动页可以:
- 填补等待空白:让用户知道应用正在启动
- 传递品牌价值:通过Logo和slogan强化品牌形象
- 预加载数据:利用等待时间准备首屏内容
通过本文,你将掌握在鸿蒙中:
- 如何创建启动页并实现自动跳转
- 如何使用定时器控制页面切换
- 如何根据季节动态设置背景图
- 如何优化启动页的视觉体验
学习目标
完成本文后,你将能够:
- ✅ 使用Timer实现定时跳转
- ✅ 实现季节动态背景图
- ✅ 设计简洁美观的启动页UI
- ✅ 处理页面跳转的路由异常
- ✅ 避免启动页的性能问题
- ✅ 设计可复用的启动页组件
需求分析
功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| UI展示 | Logo、名称、背景图 | Stack布局、Image组件 |
| 季节背景 | 根据季节动态切换背景 | Date API、条件判断 |
| 定时跳转 | 2秒后自动跳转 | setTimeout、router |
| 异常处理 | 路由跳转失败处理 | try-catch |
| 数据预加载 | 提前加载首屏数据 | async/await |
架构设计
启动页在应用架构中的位置
启动页是用户旅程的起点,但它并不是孤立的。在整体架构中,启动页需要与以下模块协作:
┌─────────────────────────────────────────────────────────────┐
│ 应用启动流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 系统启动 │
│ └── AbilityStage.onCreate() │
│ │ │
│ ▼ │
│ 2. 应用初始化 │
│ └── 初始化日志服务、存储服务、网络服务等 │
│ │ │
│ ▼ │
│ 3. 启动页显示 │
│ └── Splash.ets (本文重点) │
│ ├── 显示Logo和品牌信息 │
│ ├── 预加载首屏数据 │
│ └── 定时/条件触发跳转 │
│ │ │
│ ▼ │
│ 4. 首页显示 │
│ └── Index.ets │
│ └── 使用预加载数据快速渲染 │
│ │
└─────────────────────────────────────────────────────────────┘
状态管理架构
启动页涉及的状态相对简单,但仍需要合理设计:
typescript
// 启动页状态分类
interface SplashState {
// UI状态 - 使用@State
opacity: number; // 淡入动画透明度
isReady: boolean; // 是否可以跳转
// 持久化状态 - 使用@StorageLink
hasOnboarded: boolean; // 是否已完成引导
lastVisitTime: number; // 上次访问时间
// 只读配置 - 使用@StorageProp
appName: string; // 应用名称
enableSeasonBg: boolean; // 是否启用季节背景
}
状态选择原则:
| 状态类型 | 装饰器 | 说明 |
|---|---|---|
| 动画透明度 | @State |
组件私有,只在启动页使用 |
| 引导状态 | @StorageLink |
需要持久化,跨页面共享 |
| 应用配置 | @StorageProp |
只读配置,不需要修改 |
生命周期管理
启动页的生命周期需要特别注意资源管理:
typescript
@Entry
@Component
struct Splash {
private timerId: number = -1; // 定时器ID
private preloadTask: Promise<void> | null = null; // 预加载任务
aboutToAppear() {
// 1. 启动页显示时,开始预加载数据
this.preloadTask = this.preloadData();
// 2. 启动淡入动画
this.playFadeInAnimation();
// 3. 设置跳转定时器
this.scheduleNavigation();
}
aboutToDisappear() {
// 4. 清理定时器,防止内存泄漏
this.clearTimer();
// 5. 取消未完成的预加载任务(可选)
// 如果预加载耗时过长,页面已经跳转,可以选择取消
}
}
为什么生命周期顺序重要?
aboutToAppear:页面即将显示,可以开始异步操作build:渲染UI,不应执行耗时操作aboutToDisappear:页面即将销毁,必须清理资源
异常处理架构
启动页可能遇到的异常及处理策略:
| 异常场景 | 影响 | 处理策略 |
|---|---|---|
| 路由跳转失败 | 用户无法进入首页 | 记录日志,显示错误提示 |
| 数据预加载失败 | 首页可能显示空白 | 使用缓存数据降级 |
| 图片资源缺失 | 显示默认背景 | 提供备选资源 |
| 定时器未清理 | 内存泄漏 | 在aboutToDisappear中清理 |
核心实现
步骤1: 创建启动页基础结构
功能说明
创建Splash页面,包含季节动态背景图、Logo、应用名称,使用Stack布局实现层叠效果。
运行效果

完整代码
typescript
// pages/Splash.ets
import router from '@ohos.router';
@Entry
@Component
struct Splash {
// 根据季节获取背景图
private getSeasonBg(): string {
const month = new Date().getMonth() + 1; // 1-12
if (month >= 3 && month <= 5) {
return 'bg/seasons/chun.png'; // 春季
} else if (month >= 6 && month <= 8) {
return 'bg/seasons/xia.png'; // 夏季
} else if (month >= 9 && month <= 11) {
return 'bg/seasons/qiu.png'; // 秋季
} else {
return 'bg/seasons/dong.png'; // 冬季
}
}
build() {
Stack({ alignContent: Alignment.Center }) {
// 根据季节显示背景图
Image($rawfile(this.getSeasonBg()))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover);
Column({ space: 16 }) {
// 标题区域 - 使用毛玻璃效果
Column({ space: 8 }) {
Text('节气通')
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('把握时令之美,传承东方智慧')
.fontSize(16)
.fontColor('#FFFFFF')
.opacity(0.9)
}
.padding({ top: 20, right: 32, bottom: 20, left: 32 })
.backgroundColor('rgba(0, 0, 0, 0.1)')
.borderRadius(20)
.backdropBlur(10)
// Logo
Image($r('app.media.logo'))
.width(120)
.height(120)
.borderRadius(60)
.backgroundColor('rgba(255, 255, 255, 0.2)')
.padding(8)
.shadow({ radius: 12, color: 'rgba(0, 0, 0, 0.3)', offsetX: 0, offsetY: 4 })
}
}
.width('100%')
.height('100%')
.onAppear(() => {
setTimeout(() => {
try {
router.replaceUrl({
url: 'pages/Index'
});
} catch (err) {
console.error(`Router error: ${JSON.stringify(err)}`);
}
}, 2000);
})
}
}
步骤2: 实现定时跳转
typescript
.onAppear(() => {
setTimeout(() => {
try {
router.replaceUrl({
url: 'pages/Index'
});
} catch (err) {
console.error(`Router error: ${JSON.stringify(err)}`);
}
}, 2000);
})
步骤3: 数据预加载
typescript
import common from '@ohos.app.ability.common';
import { StorageService } from '../services/StorageService';
@Entry
@Component
struct Splash {
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
aboutToAppear() {
// 预加载数据(后台执行)
this.preloadData();
// 定时跳转
setTimeout(() => {
try {
router.replaceUrl({ url: 'pages/Index' });
} catch (err) {
console.error(`Router error: ${JSON.stringify(err)}`);
}
}, 2000);
}
async preloadData() {
try {
const storageService = new StorageService(this.context);
await storageService.init();
} catch (error) {
console.error('预加载数据失败:', error);
}
}
build() {
// ... UI代码
}
}
步骤4: 淡入动画
typescript
@Entry
@Component
struct Splash {
@State opacity: number = 0;
aboutToAppear() {
animateTo({
duration: 800,
curve: Curve.EaseOut
}, () => {
this.opacity = 1;
});
setTimeout(() => {
try {
router.replaceUrl({ url: 'pages/Index' });
} catch (err) {
console.error(`Router error: ${JSON.stringify(err)}`);
}
}, 2000);
}
build() {
Stack() {
Image($rawfile(this.getSeasonBg()))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
Column() {
// 内容...
}
}
.width('100%')
.height('100%')
.opacity(this.opacity)
}
}
设计思路
方案对比
在实现启动页时,我们面临多个设计决策。以下是方案对比分析:
决策1: 背景图方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定背景图 | 实现简单,体积小 | 缺乏动态感 | 简单应用 |
| 季节背景图 | 动态变化,贴合主题 | 需准备4套图片 | 推荐 |
| 轮播背景图 | 内容丰富 | 开发复杂,可能喧宾夺主 | 内容型应用 |
选择理由:应用主题是二十四节气文化,季节背景能增强用户对"时令"概念的感知,让首次打开应用的用户立刻感受到应用的核心价值。
决策2: 跳转时机
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定时间跳转 | 实现简单,用户可预期 | 可能浪费用户等待时间 | 推荐 |
| 数据加载完成后跳转 | 不浪费等待时间 | 用户可能等待过久 | 数据量大的应用 |
| 用户点击跳转 | 完全由用户控制 | 用户可能忘记点击 | 需要强制展示场景 |
选择理由:当前版本数据量较小,2秒足够完成数据预加载,固定时间跳转能给用户稳定的预期。
决策3: 跳转方式
| 方式 | 特点 | 是否保留历史 |
|---|---|---|
router.pushUrl |
推入新页面 | 保留,用户可返回 |
router.replaceUrl |
替换当前页面 | 不保留,无法返回 |
router.back |
返回上一页 | 需要有历史记录 |
选择理由:启动页是应用的"入口",用户不应该能够返回到启动页。replaceUrl确保启动页不会留在导航栈中。
决策4: 动画效果
| 动画类型 | 效果 | 推荐度 |
|---|---|---|
| 无动画 | 立即显示 | ❌ 突兀 |
| 淡入动画 | 透明度渐变 | ✅ 推荐 |
| 缩放动画 | Logo从小放大 | ⚠️ 可能分散注意力 |
| 组合动画 | 多个动画同时播放 | ⚠️ 过于复杂 |
选择理由:简单的淡入动画既能平滑过渡,又不会过于花哨,适合作为品牌展示。
关键实现细节
Stack布局的层叠原理
typescript
Stack({ alignContent: Alignment.Center }) {
// 第一个子组件:背景图(最底层)
Image($rawfile('bg.png'))
// 第二个子组件:半透明遮罩(中间层)
Column()
.backgroundColor('rgba(0,0,0,0.3)')
// 第三个子组件:内容(最上层)
Column() {
Text('节气通')
Image($r('app.media.logo'))
}
}
为什么使用Stack而不是Column嵌套?
如果用Column实现:
typescript
// ❌ 不推荐:Column嵌套实现背景
Column() {
Image($rawfile('bg.png')) // 会被内容撑开
Column() { // 需要绝对定位
Text('节气通')
}
}
Stack的优势:
- 自动填满父容器
- 子组件按z-index层叠
- 不需要手动计算位置
毛玻璃效果的实现原理
typescript
Column() {
// 内容...
}
.backgroundColor('rgba(0, 0, 0, 0.1)') // 半透明黑色背景
.backdropBlur(10) // 模糊半径10
.borderRadius(20) // 圆角
参数说明:
backgroundColor:使用rgba设置半透明背景,确保文字可读性backdropBlur:模糊半径,值越大越模糊borderRadius:圆角,让毛玻璃效果边缘更柔和
组件变体
变体1: 带品牌动画的启动页
如果需要更炫酷的效果,可以添加Logo入场动画:
typescript
@Entry
@Component
struct AnimatedSplash {
@State logoScale: number = 0.5;
@State logoOpacity: number = 0;
@State contentOpacity: number = 0;
aboutToAppear() {
// 1. Logo缩放+淡入
animateTo({
duration: 600,
curve: Curve.EaseOut
}, () => {
this.logoScale = 1;
this.logoOpacity = 1;
});
// 2. 内容淡入(延迟200ms)
setTimeout(() => {
animateTo({ duration: 400 }, () => {
this.contentOpacity = 1;
});
}, 200);
// 3. 定时跳转
setTimeout(() => {
router.replaceUrl({ url: 'pages/Index' });
}, 2500);
}
build() {
Stack() {
Image($rawfile(this.getSeasonBg()))
.width('100%')
.height('100%')
Column({ space: 24 }) {
Image($r('app.media.logo'))
.width(120)
.height(120)
.scale({ x: this.logoScale, y: this.logoScale })
.opacity(this.logoOpacity)
Column({ space: 8 }) {
Text('节气通')
.fontSize(36)
.fontOpacity(this.contentOpacity)
}
}
}
.width('100%')
.height('100%')
}
}
动画时序:
0ms ──────────────────────────────────► 2500ms
│ │
├─ Logo动画 ─────────► │
│ 0ms → 600ms │ │
│ ├─ 内容动画 ───────► │
│ │ 200ms → 600ms │
│ │ │
│ └─ 定时跳转 ────────────────────────►
│ 2500ms
变体2: 带进度指示的启动页
如果预加载数据量较大,可以显示加载进度:
typescript
@Entry
@Component
struct ProgressSplash {
@State progress: number = 0;
@State statusText: string = '正在加载...';
aboutToAppear() {
this.preloadWithProgress();
}
async preloadWithProgress() {
const steps = [
{ text: '正在加载配置...', weight: 20 },
{ text: '正在加载数据...', weight: 50 },
{ text: '正在准备界面...', weight: 80 },
{ text: '即将进入...', weight: 100 }
];
for (const step of steps) {
this.statusText = step.text;
await this.simulateLoad(step.weight);
}
router.replaceUrl({ url: 'pages/Index' });
}
async simulateLoad(targetProgress: number) {
while (this.progress < targetProgress) {
this.progress += 5;
await new Promise(resolve => setTimeout(resolve, 50));
}
}
build() {
Stack() {
Image($rawfile(this.getSeasonBg()))
.width('100%')
.height('100%')
Column({ space: 24 }) {
Image($r('app.media.logo'))
.width(100)
.height(100)
Text(this.statusText)
.fontSize(14)
.fontColor('#FFFFFF')
Progress({ value: this.progress, total: 100 })
.width(200)
.color('#4A9B6D')
}
}
.width('100%')
.height('100%')
}
}
变体3: 判断是否显示启动页
如果用户已经完成引导,不想每次都显示启动页:
typescript
@Entry
@Component
struct SmartSplash {
@StorageLink('hasOnboarded') hasOnboarded: boolean = false;
aboutToAppear() {
if (this.hasOnboarded) {
// 已完成引导,直接跳转
router.replaceUrl({ url: 'pages/Index' });
} else {
// 显示启动页
this.startSplashFlow();
}
}
startSplashFlow() {
// 正常启动页流程
setTimeout(() => {
router.replaceUrl({ url: 'pages/Onboarding' });
}, 2000);
}
build() {
// 启动页UI
}
}
常见问题与解决方案
问题1: 启动页白屏闪烁
现象描述 :
应用启动时先显示白屏,然后才显示启动页。
原因分析:
- 系统默认使用白色窗口背景
- 应用初始化需要时间,期间没有内容可显示
- 启动页加载完成前,用户看到空白
解决方案:
json
// 在 entry/src/main/resources/base/element/color.json 中配置窗口背景色
{
"color": [
{
"name": "window_background",
"value": "#F8F7F2"
}
]
}
同时在启动页加载完成前,可以在 AbilityStage 中设置背景:
typescript
// AbilityStage.ets
import AbilityStage from '@ohos.app.ability.AbilityStage';
export default class MyAbilityStage extends AbilityStage {
onCreate() {
// 设置启动背景色,避免白屏
const window = this.context.getWindow();
window.setWindowBackgroundColor('#F8F7F2');
}
}
问题2: 定时器内存泄漏
现象描述 :
页面跳转后,定时器仍在后台运行,可能导致内存泄漏。
原因分析:
- setTimeout 是异步操作
- 即使页面已经销毁,回调函数可能仍被执行
- 如果回调中引用了已销毁的组件,会导致问题
解决方案:
typescript
@Entry
@Component
struct Splash {
private timerId: number = -1;
aboutToAppear() {
// 保存定时器ID
this.timerId = setTimeout(() => {
router.replaceUrl({ url: 'pages/Index' });
}, 2000);
}
aboutToDisappear() {
// 清理定时器
if (this.timerId !== -1) {
clearTimeout(this.timerId);
this.timerId = -1;
}
}
build() {
// UI代码
}
}
最佳实践:
- 始终保存定时器ID
- 在aboutToDisappear中清理
- 使用-1作为"未启动"的标识
问题3: 预加载失败导致卡住
现象描述 :
预加载数据失败后,启动页一直停留在2秒后的状态,不跳转也不报错。
原因分析:
- 如果在aboutToAppear中await预加载
- 预加载失败会导致异常中断
- 后续的setTimeout永远不会执行
错误示例:
typescript
// ❌ 错误:等待数据加载完成才跳转
async aboutToAppear() {
await this.preloadData(); // 如果失败,永远不跳转
setTimeout(() => {
router.replaceUrl({ url: 'pages/Index' });
}, 2000);
}
正确做法:
typescript
// ✅ 正确:并行执行,不互相阻塞
aboutToAppear() {
this.preloadData(); // 后台执行,不await
setTimeout(() => { // 定时跳转正常执行
router.replaceUrl({ url: 'pages/Index' });
}, 2000);
}
// ✅ 更好的做法:添加错误处理
async preloadData() {
try {
const data = await DataService.load();
AppStorage.set('cachedData', data);
} catch (error) {
// 即使失败,也记录日志,不影响跳转
console.error('预加载失败:', error);
}
}
问题4: 路由跳转失败
现象描述 :
router.replaceUrl调用后,页面没有跳转,控制台报错。
常见原因及解决:
| 原因 | 解决方案 |
|---|---|
| 页面路径错误 | 检查main_pages.json中的路径 |
| 页面未注册 | 确保目标页在src数组中 |
| 在build中调用路由 | 只在生命周期中调用 |
json
// entry/src/main/resources/base/profile/main_pages.json
{
"src": [
"pages/Splash",
"pages/Index",
"pages/Detail"
]
}
正确调用时机:
typescript
// ✅ 正确:在aboutToAppear中调用
aboutToAppear() {
setTimeout(() => {
router.replaceUrl({ url: 'pages/Index' });
}, 2000);
}
// ❌ 错误:在build中调用
build() {
Column() {
// ...
}
.onClick(() => {
router.replaceUrl({ url: 'pages/Index' }); // 不要这样做!
})
}
问题5: 图片资源路径错误
现象描述 :
报错 "No such resource",图片无法显示。
原因分析:
- 资源文件不存在
- 路径格式错误
- 扩展名不匹配
排查步骤:
-
确认文件位置:
entry/src/main/resources/rawfile/
└── bg/
└── seasons/
├── chun.png
├── xia.png
├── qiu.png
└── dong.png -
确认路径格式:
typescript
// ✅ rawfile使用 $rawfile()
Image($rawfile('bg/seasons/chun.png'))
// ✅ media资源使用 $r()
Image($r('app.media.logo'))
// ❌ 不要混用
Image($r('app.media.bg/seasons/chun')) // 错误
- 确认扩展名:
typescript
// 如果文件是 chun.jpg,就不能用 .png 结尾
return 'bg/seasons/chun.jpg'; // 不是 .png
问题6: 季节判断不准确
现象描述 :
用户反馈当前季节和背景图不匹配。
原因分析:
- 节气中的"季节"和天文季节有差异
- 简单按月份判断可能不准确
- 没有考虑闰年等因素
改进方案:
typescript
// 使用节气判断季节,而非月份
private getSolarTermSeason(): string {
const month = new Date().getMonth() + 1;
const day = new Date().getDate();
// 节气分界点(近似)
// 立春:约2月4日
// 立夏:约5月5日
// 立秋:约8月7日
// 立冬:约11月7日
if (month < 2 || (month === 2 && day < 4)) {
return 'dong'; // 冬季
} else if (month < 5 || (month === 5 && day < 5)) {
return 'chun'; // 春季
} else if (month < 8 || (month === 8 && day < 7)) {
return 'xia'; // 夏季
} else if (month < 11 || (month === 11 && day < 7)) {
return 'qiu'; // 秋季
} else {
return 'dong'; // 冬季
}
}
性能优化
1. 图片资源优化
启动页图片是首先加载的资源,需要特别注意:
typescript
// ✅ 使用适当尺寸的图片
// 不要使用4000x3000的原图
// 推荐使用 1920x1080 或更小的图片
// ✅ 考虑图片格式
// 照片类:使用 JPG 或 WebP
// 图标类:使用 PNG
// WebP 格式体积更小,但需要确认设备支持
推荐尺寸:
| 设备类型 | 推荐尺寸 | 说明 |
|---|---|---|
| 手机 | 1080x1920 | 主流分辨率 |
| 平板 | 1920x1200 | 考虑横屏 |
2. 减少启动时间
typescript
// ✅ 延迟初始化非必要操作
aboutToAppear() {
// 立即显示UI
this.startAnimation();
// 延迟执行重量级操作
setTimeout(() => {
this.preloadHeavyData();
}, 500);
}
// ✅ 使用骨架屏提升感知速度
// 如果数据加载需要等待,可以先显示骨架UI
3. 避免阻塞主线程
typescript
// ❌ 不好:在启动页执行耗时同步操作
aboutToAppear() {
this.processLargeData(); // 可能阻塞UI
}
// ✅ 好:将耗时操作设为异步
aboutToAppear() {
this.startAnimation();
setTimeout(() => {
this.loadDataInBackground();
}, 100);
}
本章小结
核心知识点
本文详细讲解了启动页的实现,主要涵盖以下核心知识:
1. UI布局架构
- Stack层叠布局实现背景+内容叠加
- 毛玻璃效果增强文字可读性
- 季节动态背景图提升品牌一致性
2. 生命周期管理
- aboutToAppear:初始化、启动动画、设置定时器
- aboutToDisappear:清理定时器、取消预加载
- 避免在build中执行异步操作
3. 状态管理策略
- @State:组件私有状态(动画透明度)
- @StorageLink:持久化状态(引导状态)
- @StorageProp:只读配置
4. 异常处理机制
- try-catch捕获路由异常
- 预加载失败不影响跳转
- 资源路径错误降级处理
最佳实践清单
| 检查项 | 说明 | 状态 |
|---|---|---|
| 设置窗口背景色 | 避免白屏闪烁 | ✅ |
| 清理定时器 | 防止内存泄漏 | ✅ |
| 预加载并行执行 | 不阻塞跳转 | ✅ |
| 路由异常处理 | 防止崩溃 | ✅ |
| 图片资源验证 | 确保路径正确 | ✅ |
| 动画性能优化 | 使用硬件加速 | ✅ |
代码模板
typescript
// 标准启动页模板
@Entry
@Component
struct Splash {
@State opacity: number = 0;
private timerId: number = -1;
aboutToAppear() {
// 1. 淡入动画
animateTo({ duration: 800, curve: Curve.EaseOut }, () => {
this.opacity = 1;
});
// 2. 预加载数据(可选)
this.preloadData();
// 3. 定时跳转
this.timerId = setTimeout(() => {
router.replaceUrl({ url: 'pages/Index' });
}, 2000);
}
aboutToDisappear() {
if (this.timerId !== -1) {
clearTimeout(this.timerId);
}
}
async preloadData() {
// 预加载逻辑
}
private getSeasonBg(): string {
const month = new Date().getMonth() + 1;
if (month >= 3 && month <= 5) return 'bg/seasons/chun.png';
if (month >= 6 && month <= 8) return 'bg/seasons/xia.png';
if (month >= 9 && month <= 11) return 'bg/seasons/qiu.png';
return 'bg/seasons/dong.png';
}
build() {
Stack() {
Image($rawfile(this.getSeasonBg()))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
Column({ space: 16 }) {
Text('节气通').fontSize(36)
Text('把握时令之美').fontSize(16)
}
}
.width('100%')
.height('100%')
.opacity(this.opacity)
}
}
下一步预告
下一篇文章将讲解首页开发(上),实现应用的核心导航架构:
- Tabs底部导航容器
- 自定义TabBar组件
- 页面切换状态管理
- 主题色动态应用
相关链接
- 项目源码 : Atomgit仓库