第4篇:引导流程与用户画像
教程目标
通过本篇教程,你将学会:
- 设计应用引导流程
- 实现欢迎页面
- 创建模式选择页面
- 收集用户信息(位置、环境、目标)
- 建立用户画像
- 管理引导流程状态
完成本教程后,你将拥有一个完整的用户引导系统。
一、引导流程设计
1.1 引导流程概述
我们的应用引导流程包含以下步骤:
欢迎页面 → 模式选择 → 位置信息 → 环境条件 → 种植目标 → 完成引导 → 地图首页
↓ ↓ ↓ ↓ ↓ ↓
2秒展示 选择模式 收集位置 收集环境 收集目标 保存数据
设计要点:
- 简洁明了,不超过5个步骤
- 每个步骤都有明确的目标
- 支持跳过和返回
- 数据持久化保存
1.2 创建引导服务
在 ets/services/ 目录下创建 OnboardingService.ets:
typescript
/**
* 引导流程服务
* 管理用户引导流程的状态和数据
*/
import { StorageUtil, StorageKeys } from '../utils/StorageUtil';
import { UserProfile, AppMode, LocationInfo } from '../models/CommonModels';
export class OnboardingService {
private static instance: OnboardingService;
private constructor() {}
static getInstance(): OnboardingService {
if (!OnboardingService.instance) {
OnboardingService.instance = new OnboardingService();
}
return OnboardingService.instance;
}
/**
* 检查是否已完成引导
*/
async isOnboardingCompleted(): Promise<boolean> {
return await StorageUtil.getBoolean(StorageKeys.ONBOARDING_COMPLETED, false);
}
/**
* 保存用户资料
*/
async saveUserProfile(profile: UserProfile): Promise<boolean> {
try {
await StorageUtil.saveObject('user_profile', profile);
await StorageUtil.saveBoolean(StorageKeys.ONBOARDING_COMPLETED, true);
await StorageUtil.saveString(StorageKeys.USER_MODE, profile.mode);
await StorageUtil.saveString(StorageKeys.USER_NICKNAME, profile.nickname);
console.info('[OnboardingService] User profile saved');
return true;
} catch (error) {
console.error('[OnboardingService] Failed to save user profile:', error);
return false;
}
}
/**
* 获取用户资料
*/
async getUserProfile(): Promise<UserProfile | null> {
try {
const profile = await StorageUtil.getObject<UserProfile>('user_profile', null);
return profile;
} catch (error) {
console.error('[OnboardingService] Failed to get user profile:', error);
return null;
}
}
/**
* 重置引导流程
*/
async resetOnboarding(): Promise<void> {
await StorageUtil.saveBoolean(StorageKeys.ONBOARDING_COMPLETED, false);
console.info('[OnboardingService] Onboarding reset');
}
}
二、创建欢迎页面
2.1 创建 WelcomePage.ets
在 ets/pages/ 目录下创建 WelcomePage.ets:
typescript
import { router } from '@kit.ArkUI';
import { StorageUtil, StorageKeys } from '../utils/StorageUtil';
import { AppConstants } from '../constants/AppConstants';
@Entry
@ComponentV2
struct WelcomePage {
@Local isLoading: boolean = true;
async aboutToAppear(): Promise<void> {
// 检查是否已完成引导
setTimeout(async () => {
const onboardingCompleted = await StorageUtil.getBoolean(StorageKeys.ONBOARDING_COMPLETED, false);
if (onboardingCompleted) {
// 已完成引导,直接跳转到主页面
router.replaceUrl({
url: 'pages/Index'
});
} else {
// 未完成引导,跳转到模式选择页
router.replaceUrl({
url: 'pages/OnboardingFlow/ModeSelectionPage'
});
}
}, 2000); // 显示欢迎页2秒
}
build() {
Column() {
// Logo和标语
Column({ space: 24 }) {
// App图标
Text('🌱')
.fontSize(80)
.fontWeight(FontWeight.Bold)
Text(AppConstants.APP_NAME)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(AppConstants.APP_SLOGAN)
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
}
.justifyContent(FlexAlign.Center)
.layoutWeight(1)
// 加载指示器
if (this.isLoading) {
LoadingProgress()
.width(40)
.height(40)
.color($r('app.color.primary_professional'))
.margin({ bottom: 48 })
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
页面特点:
- 显示应用Logo和标语
- 2秒后自动跳转
- 根据引导状态决定跳转目标
- 使用
router.replaceUrl避免返回
三、创建模式选择页面
3.1 创建 OnboardingFlow 目录
在 ets/pages/ 下创建 OnboardingFlow 目录,用于存放引导流程相关页面。
3.2 创建 ModeSelectionPage.ets
在 OnboardingFlow/ 目录下创建 ModeSelectionPage.ets:
typescript
import { router } from '@kit.ArkUI';
import { AppMode } from '../../models/CommonModels';
import { CommonCard } from '../../components/CommonComponents';
@Entry
@ComponentV2
struct ModeSelectionPage {
@Local selectedMode: AppMode | null = null;
build() {
Column() {
// 标题
Column({ space: 12 }) {
Text('选择使用模式')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('根据您的需求选择合适的模式')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.padding({ top: 60, bottom: 40 })
.alignItems(HorizontalAlign.Start)
// 模式选择卡片
Column({ space: 16 }) {
// 专业农业模式
this.buildModeCard(
'🌾',
'专业农业模式',
'适合农业从业者,提供地块管理、作物管理、农事记录等专业功能',
AppMode.PROFESSIONAL_AGRICULTURE,
['地图定位', '地块管理', '产量预测', '成本核算']
)
// 家庭园艺模式
this.buildModeCard(
'🏡',
'家庭园艺模式',
'适合家庭用户,管理阳台、庭院植物,提供简单易用的种植指导',
AppMode.HOME_GARDENING,
['植物管理', '浇水提醒', '生长记录', '知识学习']
)
}
.layoutWeight(1)
// 继续按钮
Button('继续')
.width('100%')
.height(48)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor($r('app.color.primary_professional'))
.borderRadius(8)
.enabled(this.selectedMode !== null)
.opacity(this.selectedMode !== null ? 1 : 0.5)
.onClick(() => {
this.onContinue();
})
.margin({ bottom: 24 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor($r('app.color.background'))
}
}
3.3 配置页面路由
打开 entry/src/main/resources/base/profile/main_pages.json,添加页面路由:
json
{
"src": [
"pages/WelcomePage",
"pages/Index",
"pages/OnboardingFlow/ModeSelectionPage",
"pages/OnboardingFlow/LocationPage",
"pages/OnboardingFlow/ConditionsPage",
"pages/OnboardingFlow/GoalsPage"
]
}
3.4 添加模式卡片构建方法
在 ModeSelectionPage 中添加:
typescript
@Builder
buildModeCard(
icon: string,
title: string,
description: string,
mode: AppMode,
features: string[]
) {
CommonCard({
backgroundColor: this.selectedMode === mode ?
'#E6F7FF' : $r('app.color.card_background'),
onClick: () => {
this.selectedMode = mode;
}
}) {
Column({ space: 12 }) {
// 图标和标题
Row({ space: 12 }) {
Text(icon)
.fontSize(40)
Column({ space: 4 }) {
Text(title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(description)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
// 特性列表
Column({ space: 8 }) {
ForEach(features, (feature: string) => {
Row({ space: 8 }) {
Text('✓')
.fontSize(14)
.fontColor($r('app.color.primary_professional'))
Text(feature)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
})
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 选中标记
if (this.selectedMode === mode) {
Row() {
Text('已选择')
.fontSize(12)
.fontColor($r('app.color.primary_professional'))
.fontWeight(FontWeight.Medium)
}
.width('100%')
.justifyContent(FlexAlign.End)
}
}
}
}
onContinue() {
if (this.selectedMode) {
// 保存选择的模式到临时存储
AppStorage.setOrCreate('temp_selected_mode', this.selectedMode);
console.info('[ModeSelection] Selected mode:', this.selectedMode);
// 跳转到位置信息页面
router.pushUrl({
url: 'pages/OnboardingFlow/LocationPage'
});
}
}
四、创建位置信息页面
4.1 创建 LocationPage.ets
在 OnboardingFlow/ 目录下创建 LocationPage.ets:
typescript
import { router } from '@kit.ArkUI';
import { LocationInfo } from '../../models/CommonModels';
import { CommonCard, ActionButton } from '../../components/CommonComponents';
@Entry
@ComponentV2
struct LocationPage {
@Local province: string = '';
@Local city: string = '';
@Local district: string = '';
@Local climateZone: string = '';
// 省份列表(简化版)
private provinces: string[] = ['北京', '上海', '广东', '浙江', '江苏', '山东', '河南', '四川', '湖北', '湖南'];
// 气候带列表
private climateZones: string[] = ['温带季风气候', '亚热带季风气候', '热带季风气候', '温带大陆性气候', '高原山地气候'];
build() {
Column() {
// 标题和进度
Column({ space: 12 }) {
Row() {
Text('位置信息')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
Text('1/3')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
Text('告诉我们您的位置,以便提供更精准的种植建议')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.padding({ top: 60, bottom: 40 })
.alignItems(HorizontalAlign.Start)
// 表单内容
Scroll() {
Column({ space: 16 }) {
// 省份选择
CommonCard({ title: '省份', icon: '📍' }) {
TextInput({ placeholder: '请输入省份' })
.onChange((value: string) => {
this.province = value;
})
}
// 城市输入
CommonCard({ title: '城市', icon: '🏙️' }) {
TextInput({ placeholder: '请输入城市名称' })
.onChange((value: string) => {
this.city = value;
})
}
// 区县输入
CommonCard({ title: '区/县(可选)', icon: '🗺️' }) {
TextInput({ placeholder: '请输入区县名称' })
.onChange((value: string) => {
this.district = value;
})
}
// 气候带输入
CommonCard({ title: '气候带', icon: '🌡️' }) {
TextInput({ placeholder: '如:温带季风气候' })
.onChange((value: string) => {
this.climateZone = value;
})
}
}
}
.layoutWeight(1)
// 按钮组
Row({ space: 12 }) {
Button('上一步')
.width('30%')
.height(48)
.fontSize(16)
.backgroundColor($r('app.color.background'))
.fontColor($r('app.color.text_primary'))
.onClick(() => {
router.back();
})
ActionButton({
text: '下一步',
type: 'primary',
onClick: () => {
this.onNext();
}
})
.layoutWeight(1)
}
.width('100%')
.margin({ bottom: 24 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor($r('app.color.background'))
}
onNext() {
// 验证必填项
if (!this.province || !this.city || !this.climateZone) {
console.warn('请填写完整的位置信息');
return;
}
// 保存位置信息到临时存储
const locationInfo: LocationInfo = {
province: this.province,
city: this.city,
district: this.district,
climateZone: this.climateZone
};
AppStorage.setOrCreate('temp_location_info', JSON.stringify(locationInfo));
console.info('[LocationPage] Location info saved');
// 跳转到完成页面(简化流程)
router.pushUrl({
url: 'pages/OnboardingFlow/GoalsPage'
});
}
}
五、创建目标设置页面
5.1 创建 GoalsPage.ets
typescript
import { router } from '@kit.ArkUI';
import { AppMode, UserProfile, LocationInfo } from '../../models/CommonModels';
import { OnboardingService } from '../../services/OnboardingService';
import { CommonCard, ActionButton } from '../../components/CommonComponents';
@Entry
@ComponentV2
struct GoalsPage {
@Local nickname: string = '';
@Local isLoading: boolean = false;
private onboardingService = OnboardingService.getInstance();
build() {
Column() {
// 标题
Column({ space: 12 }) {
Text('完成设置')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('最后一步,设置您的昵称')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.padding({ top: 60, bottom: 40 })
.alignItems(HorizontalAlign.Start)
// 表单
Column({ space: 16 }) {
CommonCard({ title: '昵称', icon: '👤' }) {
TextInput({ placeholder: '请输入昵称' })
.onChange((value: string) => {
this.nickname = value;
})
}
}
.layoutWeight(1)
// 完成按钮
ActionButton({
text: '完成',
type: 'primary',
loading: this.isLoading,
onClick: () => {
this.onComplete();
}
})
.margin({ bottom: 24 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor($r('app.color.background'))
}
async onComplete() {
if (!this.nickname) {
console.warn('请输入昵称');
return;
}
this.isLoading = true;
try {
// 获取临时存储的数据
const selectedMode = AppStorage.get<AppMode>('temp_selected_mode') || AppMode.PROFESSIONAL_AGRICULTURE;
const locationInfoStr = AppStorage.get<string>('temp_location_info') || '{}';
const locationInfo: LocationInfo = JSON.parse(locationInfoStr);
// 创建用户资料
const userProfile: UserProfile = {
id: Date.now().toString(),
nickname: this.nickname,
mode: selectedMode,
location: locationInfo,
createdAt: Date.now(),
lastUpdatedAt: Date.now(),
onboardingCompleted: true
};
// 保存用户资料
const success = await this.onboardingService.saveUserProfile(userProfile);
if (success) {
console.info('[GoalsPage] User profile saved successfully');
// 跳转到主页面
router.replaceUrl({
url: 'pages/Index'
});
} else {
console.error('[GoalsPage] Failed to save user profile');
}
} catch (error) {
console.error('[GoalsPage] Error:', error);
} finally {
this.isLoading = false;
}
}
}
六、扩展 StorageUtil
在 StorageUtil.ets 中添加对象和布尔值存储方法:
typescript
/**
* 保存对象
*/
static async saveObject<T>(key: string, value: T): Promise<void> {
try {
const jsonString = JSON.stringify(value);
await StorageUtil.saveString(key, jsonString);
} catch (error) {
console.error(`[StorageUtil] Failed to save object for key ${key}:`, error);
throw error;
}
}
/**
* 获取对象
*/
static async getObject<T>(key: string, defaultValue: T | null = null): Promise<T | null> {
try {
const jsonString = await StorageUtil.getString(key, '');
if (jsonString) {
return JSON.parse(jsonString) as T;
}
return defaultValue;
} catch (error) {
console.error(`[StorageUtil] Failed to get object for key ${key}:`, error);
return defaultValue;
}
}
/**
* 保存布尔值
*/
static async saveBoolean(key: string, value: boolean): Promise<void> {
try {
if (!StorageUtil.dataPreferences) {
throw Error('Storage not initialized');
}
await StorageUtil.dataPreferences.put(key, value);
await StorageUtil.dataPreferences.flush();
} catch (error) {
console.error(`[StorageUtil] Failed to save boolean:`, error);
throw error;
}
}
/**
* 获取布尔值
*/
static async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
try {
if (!StorageUtil.dataPreferences) {
throw Error('Storage not initialized');
}
const value = await StorageUtil.dataPreferences.get(key, defaultValue);
return value as boolean;
} catch (error) {
console.error(`[StorageUtil] Failed to get boolean:`, error);
return defaultValue;
}
}
七、运行与测试
7.1 完整测试流程
- 清除应用数据
- 运行应用
- 按照以下步骤测试:
步骤1:欢迎页面
- ✅ 显示应用Logo和标语
- ✅ 2秒后自动跳转
步骤2:模式选择
- ✅ 显示两种模式选项
- ✅ 点击卡片可以选择模式
- ✅ 选中后显示"已选择"标记
- ✅ 点击"继续"按钮跳转
步骤3:位置信息
- ✅ 显示进度"1/3"
- ✅ 可以输入省份、城市、气候带
- ✅ 点击"上一步"返回
- ✅ 点击"下一步"跳转
步骤4:完成设置
- ✅ 输入昵称
- ✅ 点击"完成"保存数据
- ✅ 跳转到主页面
7.2 验证数据持久化
重启应用,应该直接进入主页面,不再显示引导流程。
八、总结
本篇教程完成了:
- ✅ 欢迎页面
- ✅ 模式选择页面
- ✅ 位置信息页面
- ✅ 目标设置页面
- ✅ 引导服务
- ✅ 用户画像建立
- ✅ 数据持久化
九、下一步
在下一篇教程中,我们将学习:
- 高德地图SDK集成
- 地图首页布局设计
- 地图显示与交互
- 定位服务实现
教程版本 :v1.0
更新日期 :2026-01
适用版本:DevEco Studio 5.0+, HarmonyOS API 17+