HarmonyOS 鸿蒙Next中一个简单的景点选择器
HarmonyOS 鸿蒙Next中一个简单的景点选择器

三个页面{
Guide页面:
import { router } from '[@kit](/user/kit).ArkUI';
[@Entry](/user/Entry)
@Component
struct Guide {
@State taskId: number = 0;
@State count: number = 5;
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.huaweiyun'))
.width('100%')
.height('100%')
Text(`跳过${this.count}`)
.width(80)
.height(30)
.backgroundColor('#ffb8b8b8')
.borderRadius(15)
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.margin({ top: 30, right: 30 })
.onClick(() => {
this.redirect()
})
}
.height('100%')
.width('100%')
.backgroundColor(Color.White)
}
aboutToAppear(): void {
this.satrkTaskId()
}
//开启倒计时
satrkTaskId() {
this.taskId = setInterval(() => {
this.count--
if (this.count == 0) {
this.redirect()
}
}, 1000)
}
//跳转
redirect() {
clearInterval(this.taskId)
router.pushUrl({ url: "pages/Index" })
}
}
index页面:
import { common } from '[@kit](/user/kit).AbilityKit';
import { BusinessError } from '[@kit](/user/kit).BasicServicesKit';
import { buffer } from '[@kit](/user/kit).ArkTS';
import { Title } from './TitleData';
import { guideData, scenicSpotType } from './GuideData';
import { PromptAction, Router, UIUtils } from '[@kit](/user/kit).ArkUI';
import { ParamsBean } from './ParamsBean';
[@Entry](/user/Entry)
[@ComponentV2](/user/ComponentV2)
struct Index {
router: Router = new Router()
[@Local](/user/Local) TitleList: Title[] = [];
[@Local](/user/Local) GuideList: guideData[] = [];
uiContext: UIContext = this.getUIContext();
promptAction: PromptAction = this.uiContext.getPromptAction();
build() {
Column() {
// 标题
Text(this.TitleList[0]?.title || '鸿蒙之家')
.fontSize(20)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 20, bottom: 10 })
// 使用 List 组件替代 Column 来显示卡片列表
List({ space: 10 }) {
ForEach(this.GuideList, (item: guideData, index: number) => {
ListItem() {
// 卡片容器
Column() {
// 卡片内容行
Row() {
// 卡片图标 - 使用城市图标
Text(item.icon)//
.fontSize(24)
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.margin({ right: 10 })
// 卡片标签和描述 - 使用城市名和城市描述
Column() {
Text(item.城市名) // 卡片标签
.fontSize(18)
.textAlign(TextAlign.Start)
.width('100%')
Text(item.城市描述) // 卡片描述
.fontSize(14)
.textAlign(TextAlign.Start)
.width('100%')
.margin({ top: 2 })
}
.layoutWeight(1) // 占据剩余空间
// 更多选项图标
Image($r('app.media.icon_down'))
.width(20)
.height(20)
.onClick(() => {
//是否展开,取反
item.isShow = !item.isShow
console.log("mapItem:" + JSON.stringify(item))
})
}
.width('100%')
// 可选择的内容列表(条件渲染)- 使用景点作为查询内容
Column() {
// 适宜游玩时间
Row() {
Text("适宜游玩时间:")
.fontSize(14)
Text(item.适宜游玩时间)
.fontSize(14)
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
.padding(10)
Divider()
.strokeWidth(1)
.color('#E0E0E0')
// 景点列表标题
Text("推荐景点:")
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 10, top: 10, bottom: 5 })
// 景点列表 - 使用另一个 List 组件显示景点
List() {
ForEach(item.景点, (spot: scenicSpotType, spotIndex: number) => {
ListItem() {
Column() {
// 景点名称和描述
Row() {
Checkbox({ name:spot.名称, group: 'checkboxGroup' })
.select(spot.isSelected || false)
.selectedColor(0xed6f21)
.shape(CheckBoxShape.CIRCLE)
.onChange(() => {
// 直接调用 checkedItem 方法切换状态
this.checkedItem(spot.名称)
})
Column() {
Text(spot.名称)
.fontSize(16)
.textAlign(TextAlign.Start)
Text(spot.描述)
.fontSize(14)
.fontColor(Color.Gray)
}
.layoutWeight(1)
Text(spot.推荐指数)
.fontSize(14)
.fontColor(Color.Orange)
}
.width('100%')
.padding(10)
Divider()
.strokeWidth(1)
.color('#E0E0E0')
}
.width('100%')
}
})
}
.visibility(item.isShow? Visibility.Visible : Visibility.None)
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(5)
.margin({ bottom: 10 })
}
.width('100%')
.margin({ top: 10 })
.backgroundColor('#F8F8F8')
.borderRadius(5)
}
// 卡片样式 - 严格按照要求
.width('100%')
.padding(5)
.backgroundColor(Color.White)
.borderRadius(5)
.border({
width: 1,
color: '#E0E0E0'
})
}
})
}
.width('100%')
.layoutWeight(1) // 占据剩余空间
.alignListItem(ListItemAlign.Center)
Button("查询")
.onClick(() => {
this.jumptoPage2()
})
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding(10)
}
jumptoPage2() {
let name = "" //选中的标签
let checkCityName = "" //选中的内容
this.GuideList.forEach((mapItem:guideData, index: number) => {
mapItem.景点.forEach((cityItem: scenicSpotType, position: number) => {
//如果存在选中的话,就赋值给checkCityItem
if (cityItem.isSelected) {
name = mapItem.icon
checkCityName = cityItem.名称
}
})
})
//checkCityItem为空,则表示没选中
if (checkCityName == "") {
this.promptAction.openToast({
message: '请选择需要查询的选项',
duration: 3000,
})
return
}
let content1 = `周边搜索-位置:${checkCityName},搜索关键词:餐厅`
let content2 = `骑行规划-出发地:桂林学院,目的地:${checkCityName}`
//构建参数
let params: ParamsBean = {
label:name,
content1: content1,
content2: content2
}
//跳转到详情页
this.router.pushUrl({
url: "pages/Detail",
params: params
})
}
checkedItem(Name: string) {
// 遍历数组,切换指定景点的选中状态
this.GuideList.forEach((GuideItem: guideData, index: number) => {
GuideItem.景点.forEach((PlaceItem: scenicSpotType, position: number) => {
if (PlaceItem.名称 == Name) {
// 切换当前点击景点的选中状态
PlaceItem.isSelected = !PlaceItem.isSelected
}
else {
PlaceItem.isSelected = false
}
})
})
}
getTitlejson() {
try {
// "test.txt"仅作示例,请替换为实际使用的资源
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
context.resourceManager.getRawFileContent("title.json", (error: BusinessError, value: Uint8Array) => {
if (error != null) {
console.error("error is " + error);
} else {
let str = buffer.from(value.buffer).toString()
this.TitleList = JSON.parse(str)
console.log('this.titlelist' + str)
}
});
} catch (error) {
let code = (error as BusinessError).code;
let message = (error as BusinessError).message;
console.error(`callback getRawFileContent failed, error code: ${code}, message: ${message}.`);
}
}
getGuidejson() {
try {
// "test.txt"仅作示例,请替换为实际使用的资源
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
context.resourceManager.getRawFileContent("guide.json", (error: BusinessError, value: Uint8Array) => {
if (error != null) {
console.error("error is " + error);
} else {
let str = buffer.from(value.buffer).toString()
let data: guideData[] = JSON.parse(str)
this.GuideList = UIUtils.makeObserved(data)
console.log('this.guide' + str)
this.GuideList.forEach((guideItem: guideData, index: number) => {
guideItem.isShow = false
guideItem.景点.forEach((placeItem: scenicSpotType, position: number) => {
placeItem.isSelected = false
})
})
}
});
} catch (error) {
}
}
aboutToAppear(): void {
this.getTitlejson()
this.getGuidejson()
}
}
Detail页面:
import { common } from '[@kit](/user/kit).AbilityKit';
import { BusinessError } from '[@kit](/user/kit).BasicServicesKit';
import { buffer } from '[@kit](/user/kit).ArkTS';
import { Title } from './TitleData';
import { guideData, scenicSpotType } from './GuideData';
import { PromptAction, Router, UIUtils } from '[@kit](/user/kit).ArkUI';
import { ParamsBean } from './ParamsBean';
[@Entry](/user/Entry)
[@ComponentV2](/user/ComponentV2)
struct Index {
router: Router = new Router()
[@Local](/user/Local) TitleList: Title[] = [];
[@Local](/user/Local) GuideList: guideData[] = [];
uiContext: UIContext = this.getUIContext();
promptAction: PromptAction = this.uiContext.getPromptAction();
build() {
Column() {
// 标题
Text(this.TitleList[0]?.title || '鸿蒙之家')
.fontSize(20)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 20, bottom: 10 })
// 使用 List 组件替代 Column 来显示卡片列表
List({ space: 10 }) {
ForEach(this.GuideList, (item: guideData, index: number) => {
ListItem() {
// 卡片容器
Column() {
// 卡片内容行
Row() {
// 卡片图标 - 使用城市图标
Text(item.icon)//
.fontSize(24)
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.margin({ right: 10 })
// 卡片标签和描述 - 使用城市名和城市描述
Column() {
Text(item.城市名) // 卡片标签
.fontSize(18)
.textAlign(TextAlign.Start)
.width('100%')
Text(item.城市描述) // 卡片描述
.fontSize(14)
.textAlign(TextAlign.Start)
.width('100%')
.margin({ top: 2 })
}
.layoutWeight(1) // 占据剩余空间
// 更多选项图标
Image($r('app.media.icon_down'))
.width(20)
.height(20)
.onClick(() => {
//是否展开,取反
item.isShow = !item.isShow
console.log("mapItem:" + JSON.stringify(item))
})
}
.width('100%')
// 可选择的内容列表(条件渲染)- 使用景点作为查询内容
Column() {
// 适宜游玩时间
Row() {
Text("适宜游玩时间:")
.fontSize(14)
Text(item.适宜游玩时间)
.fontSize(14)
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
.padding(10)
Divider()
.strokeWidth(1)
.color('#E0E0E0')
// 景点列表标题
Text("推荐景点:")
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 10, top: 10, bottom: 5 })
// 景点列表 - 使用另一个 List 组件显示景点
List() {
ForEach(item.景点, (spot: scenicSpotType, spotIndex: number) => {
ListItem() {
Column() {
// 景点名称和描述
Row() {
Checkbox({ name:spot.名称, group: 'checkboxGroup' })
.select(spot.isSelected || false)
.selectedColor(0xed6f21)
.shape(CheckBoxShape.CIRCLE)
.onChange(() => {
// 直接调用 checkedItem 方法切换状态
this.checkedItem(spot.名称)
})
Column() {
Text(spot.名称)
.fontSize(16)
.textAlign(TextAlign.Start)
Text(spot.描述)
.fontSize(14)
.fontColor(Color.Gray)
}
.layoutWeight(1)
Text(spot.推荐指数)
.fontSize(14)
.fontColor(Color.Orange)
}
.width('100%')
.padding(10)
Divider()
.strokeWidth(1)
.color('#E0E0E0')
}
.width('100%')
}
})
}
.visibility(item.isShow? Visibility.Visible : Visibility.None)
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(5)
.margin({ bottom: 10 })
}
.width('100%')
.margin({ top: 10 })
.backgroundColor('#F8F8F8')
.borderRadius(5)
}
// 卡片样式 - 严格按照要求
.width('100%')
.padding(5)
.backgroundColor(Color.White)
.borderRadius(5)
.border({
width: 1,
color: '#E0E0E0'
})
}
})
}
.width('100%')
.layoutWeight(1) // 占据剩余空间
.alignListItem(ListItemAlign.Center)
Button("查询")
.onClick(() => {
this.jumptoPage2()
})
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding(10)
}
jumptoPage2() {
let name = "" //选中的标签
let checkCityName = "" //选中的内容
this.GuideList.forEach((mapItem:guideData, index: number) => {
mapItem.景点.forEach((cityItem: scenicSpotType, position: number) => {
//如果存在选中的话,就赋值给checkCityItem
if (cityItem.isSelected) {
name = mapItem.icon
checkCityName = cityItem.名称
}
})
})
//checkCityItem为空,则表示没选中
if (checkCityName == "") {
this.promptAction.openToast({
message: '请选择需要查询的选项',
duration: 3000,
})
return
}
let content1 = `周边搜索-位置:${checkCityName},搜索关键词:餐厅`
let content2 = `骑行规划-出发地:桂林学院,目的地:${checkCityName}`
//构建参数
let params: ParamsBean = {
label:name,
content1: content1,
content2: content2
}
//跳转到详情页
this.router.pushUrl({
url: "pages/Detail",
params: params
})
}
checkedItem(Name: string) {
// 遍历数组,切换指定景点的选中状态
this.GuideList.forEach((GuideItem: guideData, index: number) => {
GuideItem.景点.forEach((PlaceItem: scenicSpotType, position: number) => {
if (PlaceItem.名称 == Name) {
更多关于HarmonyOS 鸿蒙Next中一个简单的景点选择器的实战教程也可以访问 https://www.itying.com/category-93-b0.html
添加存储checkbox的选择
1. 在 aboutToAppear 方法中添加恢复逻辑
typescript
aboutToAppear(): void {
let options: preferences.Options = { name: 'test' }
this.dataPreferences = preferences.getPreferencesSync(this.getUIContext().getHostContext(), options)
this.getCityDataFromDb()
this.getTitlejson()
this.getGuidejson()
// 添加:延迟恢复选中状态,确保数据已加载
setTimeout(() => {
this.restoreCheckboxState();
}, 100);
}
2. 添加恢复选中状态的新方法
typescript
/**
* 恢复checkbox选中状态
*/
restoreCheckboxState() {
try {
const savedSpotName = this.getDataToDb('selectedSpotName');
console.log(`恢复checkbox状态: ${savedSpotName}`);
if (savedSpotName) {
this.GuideList.forEach((guideItem: guideData) => {
guideItem.景点.forEach((spot: scenicSpotType) => {
if (spot.名称 === savedSpotName) {
spot.isSelected = true;
console.log(`成功恢复选中: ${savedSpotName}`);
}
});
});
}
} catch (error) {
console.error('恢复checkbox状态失败:', error);
}
}
3. 在 checkedItem 方法中添加存储逻辑
typescript
checkedItem(Name: string) {
// 遍历数组,切换指定景点的选中状态
this.GuideList.forEach((GuideItem: guideData, index: number) => {
GuideItem.景点.forEach((PlaceItem: scenicSpotType, position: number) => {
if (PlaceItem.名称 == Name) {
// 切换当前点击景点的选中状态
PlaceItem.isSelected = !PlaceItem.isSelected
// 新增:存储选中状态
if (PlaceItem.isSelected) {
this.saveDataToDb('selectedSpotName', Name);
console.log(`存储选中景点: ${Name}`);
} else {
this.saveDataToDb('selectedSpotName', '');
console.log('清除选中景点');
}
} else {
PlaceItem.isSelected = false
}
})
})
}
加入以下组件:
radioIndex: number = 0
//用户首选项管理本地数据库
dataPreferences: preferences.Preferences | null = null
/**
*存数据到本地数据库
* @param key
* @param value
*/
saveDataToDb(key: string, value: string) {
this.dataPreferences!!.putSync(key, value)
//通过flush将Preferences实例持久化
this.dataPreferences!!.flushSync()
}
/**
* 从本地取数据
* @param key
*/
getDataToDb(key: string): string {
let value = this.dataPreferences!!.getSync(key, "")
console.log( `getDataToDb key:${key},value:${value}`)
return value as string
}
前往HarmontOS数据封装类工具获取数据封装工具
更多关于HarmonyOS 鸿蒙Next中一个简单的景点选择器的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
谢谢分享
加油,,
这是一个结构清晰的HarmonyOS Next景点选择器应用,实现了引导页、主列表页和详情页的三页面架构。代码整体质量不错,但有几个关键点需要注意:
-
路由导入问题:在Index页面中,同时使用了
import { router } from '@kit.ArkUI'和router: Router = new Router()两种方式。建议统一使用import { router } from '@kit.ArkUI',这是HarmonyOS Next推荐的单例模式。 -
Detail页面代码重复:Detail页面的代码与Index页面几乎完全相同,这可能是粘贴错误。Detail页面应该专注于显示从Index页面传递过来的参数(ParamsBean),而不是重新加载JSON数据和渲染完整列表。
-
数据状态管理:
@Local装饰器用于组件内状态管理是合适的,但要注意对于复杂数据结构,使用UIUtils.makeObserved()创建可观察对象是正确的做法,确保了UI的响应式更新。 -
资源文件读取:使用
resourceManager.getRawFileContent()读取JSON数据是标准做法,但需要注意错误处理的完整性。 -
Checkbox状态管理:
checkedItem方法实现了单选逻辑(选中一个时取消其他选中),这种交互设计符合景点选择的实际场景。
建议修正Detail页面的实现,使其真正接收并显示路由参数,而不是重复Index页面的功能。整体架构合理,代码可维护性良好。

