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

5 回复

添加存储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中,景点选择器可通过ArkUI声明式开发实现。使用Picker组件,结合自定义数据模型,可展示景点列表。通过@State@Link装饰器管理选中状态,并利用条件渲染或循环渲染动态更新界面。事件处理函数响应用户选择,完成交互逻辑。

这是一个结构清晰的HarmonyOS Next景点选择器应用,实现了引导页、主列表页和详情页的三页面架构。代码整体质量不错,但有几个关键点需要注意:

  1. 路由导入问题:在Index页面中,同时使用了import { router } from '@kit.ArkUI'router: Router = new Router()两种方式。建议统一使用import { router } from '@kit.ArkUI',这是HarmonyOS Next推荐的单例模式。

  2. Detail页面代码重复:Detail页面的代码与Index页面几乎完全相同,这可能是粘贴错误。Detail页面应该专注于显示从Index页面传递过来的参数(ParamsBean),而不是重新加载JSON数据和渲染完整列表。

  3. 数据状态管理@Local装饰器用于组件内状态管理是合适的,但要注意对于复杂数据结构,使用UIUtils.makeObserved()创建可观察对象是正确的做法,确保了UI的响应式更新。

  4. 资源文件读取:使用resourceManager.getRawFileContent()读取JSON数据是标准做法,但需要注意错误处理的完整性。

  5. Checkbox状态管理checkedItem方法实现了单选逻辑(选中一个时取消其他选中),这种交互设计符合景点选择的实际场景。

建议修正Detail页面的实现,使其真正接收并显示路由参数,而不是重复Index页面的功能。整体架构合理,代码可维护性良好。

回到顶部