当前位置: 首页 > article >正文

【搜索页】- 功能流程

【搜索页】- 功能流程

【搜索组件】- 改造搜索组件HdSearch

src/main/ets/common/components/HdSearch.ets

课程目标

  • 直接将搜索关键字写死在keywords数组中:keywords:string[]=['html','css','js','vue','react']
  • 使用setInterval实现每隔3秒完成题目分类数据的切换
  • 使用router传参的方式将题目分类名称传给搜索页SearchPage.ets

步骤:

  1. 改造HdSearch.ets组件
import { router } from '@kit.ArkUI'

@Component
export struct HdSearch {
  @State
  readonlyMode: boolean = true
  @State
  ph: string = 'html'
  bg: string = ''
  color: string = ''
  keywords: string[] = ['html', 'css3', 'javascript', 'vue', 'react'] // 用来给轮播使用的数据
  index: number = 0 //表示取得数组的索引号

  aboutToAppear(): void {
    // 页面加载完毕就开启定时器
    setInterval(() => {
      // 索引先+
      this.index++
      // 判断等于数组长度就重置索引
      if (this.index === this.keywords.length) {
        this.index = 0
      }
      // 从数组中获取当前索引的关键字
      this.ph = this.keywords[this.index]
    }, 3000)
  }

  build() {
    Row() {
      Row({ space: 4 }) {
        Image($r("app.media.ic_common_search"))
          .width($r('app.float.hd_search_icon_size'))
          .aspectRatio(1)
          .fillColor(this.color || $r('app.color.common_gray_02'))
        Text(this.ph || $r('app.string.hd_search_placeholder'))
          .fontColor(this.color || $r('app.color.common_gray_02'))
          .fontSize($r('app.float.common_font14'))
      }
      .layoutWeight(1)
      .height($r('app.float.hd_search_height'))
      .backgroundColor(this.bg || $r('app.color.common_gray_border'))
      .borderRadius($r('app.float.hd_search_radius'))
      .justifyContent(FlexAlign.Center)
    }
    .onClick(() => {
      router.pushUrl({ url: 'pages/SearchPage',params:{keywordText:this.ph} })
    })
  }
}
  1. 新建SearchPage.ets 接受路由传参
import { router } from '@kit.ArkUI';

interface iRouterParams {
  keywordText: string
}

@Entry
@Component
struct SearchPage {
  @State message: string = 'Hello World';

  aboutToAppear(): void {
    let params = router.getParams() as iRouterParams
    this.message = params.keywordText
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

【搜索页】- 静态结构(SearchPage.ets)

课程目标

  • 首选项综合案例上代码作为静态模版
import { promptAction } from '@kit.ArkUI'
import { preferences } from '@kit.ArkData'

@Entry
@Component
struct PreDemo {
  @State keyword: string = ''
  @StorageProp('topHeight') topHeight: number = 0
  @State keywords: string[] = []

  aboutToAppear() {
    this.getData()
  }

  @Builder
  itemBuilder(text: string) {
    Row({ space: 20 }) {
      Text(text)
      Text('x')
        .height(30)
        .onClick(async () => {
          //  删除指定关键字
          await this.delData(text)
          await this.getData()
        })
    }
    .margin({ right: 10, top: 10 })
    .padding({ left: 15, right: 15, top: 5, bottom: 5 })
    .backgroundColor('rgba(0,0,0,0.05)')
    .borderRadius(20)
  }

  // 1.0 新增方法
  async saveData(text: string) {
    // 非空验证
    if (!text) {
      return
    }

    // 1.0 获取首选项对象实例
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    // 2.0 调用putsync方法保存数组
    // 2.0.1 先从首选项中获取老数据
    let dataArr = pre.getSync('keyword', []) as string[]
    // 判断如果首选项中已经有了该关键词,不再保存
    if (dataArr.includes(text)) {
      // 该关键字已经存在了,不保存
      return
    }

    dataArr.push(text)
    // 2.0.2 存数据
    pre.putSync('keyword', dataArr)

    // 3.0 调用flush写入到磁盘
    await pre.flush()
  }

  // 2.0 获取首选项数据
  async getData() {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    this.keywords = pre.getSync('keyword', []) as string[]
  }

  // 3.0 删除指定关键字和全部关键字
  async delData(text?: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    //   1.0 删除全部关键
    if (!text) {
      pre.deleteSync('keyword')
    } else {
      // 2.0 删除指定关键字  获取 -> 删除内存数组的关键字 -> 写回
      let datas = pre.getSync('keyword', []) as string[]
      let cindex = datas.findIndex(item => item === text)
      // 当关键字索引为-1的时候,表示没有找到
      if (cindex < 0) {
        return
      }
      // 如果有删除
      datas.splice(cindex, 1) // 返回值表示删除的元素
      //   保存回去
      pre.putSync('keyword', datas)
    }
    // 写回磁盘
    pre.flush()
  }

  build() {
    Navigation() {
      Column({ space: 15 }) {
        // 1. 搜索关键字
        TextInput({ placeholder: '输入回车保存数据', text: $$this.keyword })
          .onSubmit(async () => {
            // AlertDialog.show({ message: this.keyword })
            //   将关键词数据保存到首选项中
            await this.saveData(this.keyword)
            await this.getData()
          })

        // 2. 关键字列表
        Row() {
          Text('搜索记录').fontSize(20).fontWeight(800)

          Row() {
            Text('全部删除')
              .onClick(async () => {
                // 删除全局数据
                await this.delData()
                await this.getData()
              })
            Text(' | ')
            Text('取消删除')

            Image($r('app.media.ic_common_delete'))
              .height(28)
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        //   3. 关键字列表
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.keywords, (item: string) => {
            this.itemBuilder(item)
          })
        }
      }
      .padding(15)
    }
    .padding({ top: this.topHeight })
    .titleMode(NavigationTitleMode.Mini)
    .title('搜索页面')
  }
}


【搜索页】- 向首选项中存储路由传入的关键字

课程目标

  • HdSearch.ets组件通过路由给SearchPage传入关键字
  • SearchPage接收路由参数关键字,将数据保存到首选项

注意:不能将@State修饰的状态变量数组存储到首选项,会报错

import { router } from '@kit.ArkUI'

@Component
export struct HdSearch {
  @State
  readonlyMode: boolean = true
  @State
  ph: string = 'html'
  bg: string = ''
  color: string = ''
  keywords: string[] = ['html', 'css3', 'javascript', 'vue', 'react'] // 用来给轮播使用的数据
  index: number = 0 //表示取得数组的索引号

  aboutToAppear(): void {
    // 页面加载完毕就开启定时器
    setInterval(() => {
      // 索引先+
      this.index++
      // 判断等于数组长度就重置索引
      if (this.index === this.keywords.length) {
        this.index = 0
      }
      // 从数组中获取当前索引的关键字
      this.ph = this.keywords[this.index]
    }, 3000)
  }

  build() {
    Row() {
      Row({ space: 4 }) {
        Image($r("app.media.ic_common_search"))
          .width($r('app.float.hd_search_icon_size'))
          .aspectRatio(1)
          .fillColor(this.color || $r('app.color.common_gray_02'))
        Text(this.ph || $r('app.string.hd_search_placeholder'))
          .fontColor(this.color || $r('app.color.common_gray_02'))
          .fontSize($r('app.float.common_font14'))
      }
      .layoutWeight(1)
      .height($r('app.float.hd_search_height'))
      .backgroundColor(this.bg || $r('app.color.common_gray_border'))
      .borderRadius($r('app.float.hd_search_radius'))
      .justifyContent(FlexAlign.Center)
    }
    .onClick(() => {
      router.pushUrl({ url: 'pages/SearchPage',params:{keywordText:this.ph} })
    })
  }
}
import { promptAction } from '@kit.ArkUI'
import { preferences } from '@kit.ArkData'
import { router } from '@kit.ArkUI';

interface iRouterParams {
  keywordText: string
}

@Entry
@Component
struct SearchPage {
  @State keyword: string = ''
  @StorageProp('topHeight') topHeight: number = 0
  @State keywords: string[] = []
  @State isdel: boolean = false

  async aboutToAppear() {
    let params = router.getParams() as iRouterParams
    this.keyword = params.keywordText // 接收路由参数

    // 将路由传入的参数关键字保存到首选项
    await this.saveData(params.keywordText)
    await this.getData()
  }

  @Builder
  itemBuilder(text: string) {
    Row({ space: 20 }) {
      Text(text)
      if (this.isdel) {
        Text('x')
          .height(30)
          .onClick(async () => {
            //  删除指定关键字
            await this.delData(text)
            await this.getData()
          })
      }
    }
    .margin({ right: 10, top: 10 })
    .padding({ left: 15, right: 15, top: 5, bottom: 5 })
    .backgroundColor('rgba(0,0,0,0.05)')
    .borderRadius(20)
  }

  // 1.0 新增方法
  async saveData(text: string) {
    // 非空验证
    if (!text) {
      return
    }

    // 1.0 获取首选项对象实例
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    // 2.0 调用putsync方法保存数组
    // 2.0.1 先从首选项中获取老数据
    let dataArr = pre.getSync('keyword', []) as string[]
    // 判断如果首选项中已经有了该关键词,不再保存
    if (dataArr.includes(text)) {
      // 该关键字已经存在了,不保存
      return
    }

    dataArr.push(text)
    // 2.0.2 存数据
    pre.putSync('keyword', dataArr)

    // 3.0 调用flush写入到磁盘
    await pre.flush()
  }

  // 2.0 获取首选项数据
  async getData() {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    this.keywords = pre.getSync('keyword', []) as string[]
  }

  // 3.0 删除指定关键字和全部关键字
  async delData(text?: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'store1' })
    //   1.0 删除全部关键
    if (!text) {
      pre.deleteSync('keyword')
    } else {
      // 2.0 删除指定关键字  获取 -> 删除内存数组的关键字 -> 写回
      let datas = pre.getSync('keyword', []) as string[]
      let cindex = datas.findIndex(item => item === text)
      // 当关键字索引为-1的时候,表示没有找到
      if (cindex < 0) {
        return
      }
      // 如果有删除
      datas.splice(cindex, 1) // 返回值表示删除的元素
      //   保存回去
      pre.putSync('keyword', datas)
    }
    // 写回磁盘
    pre.flush()
  }

  build() {
    Navigation() {
      Column({ space: 15 }) {
        // 1. 搜索关键字
        TextInput({ placeholder: '输入回车保存数据', text: $$this.keyword })
          .onSubmit(async () => {
            // AlertDialog.show({ message: this.keyword })
            //   将关键词数据保存到首选项中
            await this.saveData(this.keyword)
            await this.getData()
          })

        // 2. 关键字列表
        Row() {
          Text('搜索记录').fontSize(20).fontWeight(800)

          Row() {
            if (this.isdel) {
              Text('全部删除')
                .onClick(async () => {
                  // 删除全局数据
                  await this.delData()
                  await this.getData()
                })
              Text(' | ')
              Text('取消删除')
                .onClick(() => {
                  this.isdel = false
                })
            }

            Image($r('app.media.ic_common_delete'))
              .height(28)
              .onClick(() => {
                this.isdel = true
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        //   3. 关键字列表
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.keywords, (item: string) => {
            this.itemBuilder(item)
          })
        }
      }
      .padding(15)
    }
    .padding({ top: this.topHeight })
    .titleMode(NavigationTitleMode.Mini)
    .title('搜索页面')
  }
}


【搜索页】- 根据关键字搜索数据

课程目标

  • 复用src/main/ets/views/home/QuestionListComp.ets页面,在QuestionListComp.ets里面的请求url上增加keyword关键字参数

import { preferences } from '@kit.ArkData'
import promptAction from '@ohos.promptAction';
import { router } from '@kit.ArkUI';
import { QuestionListComp } from '../views/home/QuestionListComp';

interface iRouterParams {
  keywrod: string
}

@Entry
@Component
struct SearchPage {
  @State keyword: string = '' // 搜索关键字
  @State kdList: string[] = []
  @StorageProp('topHeight') topHeight: number = 0
  @State isdel: boolean = false
  @State isSearch: boolean = false

  async aboutToAppear() {
    // 获取路由传入的关键字
    let routerObj = router.getParams() as iRouterParams
    this.keyword = routerObj.keywrod
    this.isSearch = true //打开搜索页面

    // 将关键字保存到用户首选项中
    await this.saveKeyWord(routerObj.keywrod)

    // 页面进入即获取关键字数组显示
    this.kdList = await this.getKeyWords()
  }

  // 1. 保存用户首选项中的关键
  async saveKeyWord(keyword: string) {
    //   1. 去用户首选项中获取key为keyword的数据,字符串数组
    let pre = preferences.getPreferencesSync(getContext(), { name: 'store' })
    //   2. 向首选项中获取数据
    let kdArr = pre.getSync('keyword', []) as string[]
    // 判断kdArr数组中如果已经存在了keyword变量的值,则不再追加
    // ['html5','css3'] -> css3 --> 数组的 find,findindex,indexOf, includes
    if (kdArr.includes(keyword)) {
      return //不再追加
    }
    //   3. 利用push方法将用户输入的关键字追加到kdArr中
    kdArr.push(keyword)
    //   4. 利用putSync+flush方法完成写入
    pre.putSync('keyword', kdArr)
    await pre.flush()
  }

  // 2. 获取用户首选项中的关键字数组
  async getKeyWords() {
    let pre = preferences.getPreferencesSync(getContext(), { name: 'store' })
    return pre.getSync('keyword', []) as string[]
  }

  // 3. 删除(① 删除指定关键字  ② 全部删除)
  async delKeyWord(keywrod?: string) {
    let pre = preferences.getPreferencesSync(getContext(), { name: 'store' })

    if (keywrod) {
      //  删除指定的关键字
      //   1. 获取首选项的所有数据到内存中(字符串数组)
      let kdArr = pre.getSync('keyword', []) as string[]
      //   2. 根据已知的关键字keywrod 查找它在这个数组中的索引,同时调用splice删除之
      let index = kdArr.findIndex(item => item == keywrod)
      // ✨注意:如果 index < 0 则不删除
      if (index < 0) {
        promptAction.showToast({ message: '当前要删除掉关键字不存在' })
        return
      }
      kdArr.splice(index, 1)

      //   3. 调用 putSync将最新的数组写回文件
      pre.putSync('keyword', kdArr)
    } else {
      // 删除全部
      pre.deleteSync('keyword')
    }

    await pre.flush()
  }

  @Builder
  itemBuilder(text: string) {
    Row({ space: 20 }) {
      Text(text)
      if (this.isdel) {
        Text('x')
          .height(30)
          .onClick(async () => {
            // 删除指定关键字
            await this.delKeyWord(text)
            // 重新筛选页面
            this.kdList = await this.getKeyWords()
          })
      }
    }
    .margin({ right: 10, top: 10 })
    .padding({
      left: 15,
      right: 15,
      top: 5,
      bottom: 5
    })
    .backgroundColor('rgba(0,0,0,0.05)')
    .borderRadius(20)
  }

  build() {
    Navigation() {
      Column({ space: 15 }) {
        // 1. 搜索关键字
        TextInput({ placeholder: '输入回车保存数据', text: $$this.keyword })
          .onSubmit(async () => {
            // 关键字保存到首选项中
            await this.saveKeyWord(this.keyword)

            this.isSearch = true

            // 获取关键字数组显示
            this.kdList = await this.getKeyWords()
          })
          .onClick(() => {
            // 点击文本框关闭搜索组件
            this.isSearch = false
          })

        if (this.isSearch) {
          // 显示搜索记录组件(List)
          Row() {
            QuestionListComp({ keyword: this.keyword })
          }

        } else {
          // 2. 搜索记录
          Row() {
            Text('搜索记录').fontSize(20).fontWeight(800)

            Row() {
              if (this.isdel) {
                Text('全部删除')
                  .onClick(async () => {
                    //  全部删除所以无需传参数
                    await this.delKeyWord()
                    // 刷新页面
                    this.kdList = await this.getKeyWords()
                  })
                Text(' | ')
                Text('取消删除')
                  .onClick(() => {
                    this.isdel = false
                  })
              } else {
                Image($r('app.media.ic_common_delete'))
                  .height(28)
                  .onClick(() => {
                    this.isdel = true
                  })
              }
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)

          //   3. 关键字列表
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.kdList, (item: string) => {
              this.itemBuilder(item)
            })
          }
        }

      }
      .padding(15)
    }
    .padding({ top: this.topHeight })
    .titleMode(NavigationTitleMode.Mini)
    .title('搜索页面')
  }
}
import { HdLoadingDialog } from '../../components/HdLoadingDialog'
import { HdHttp } from '../../utils/request'
import { QuestionItemComp, QuestionItem, iQuestion } from '../QuestionItemComp'
import { FilterParams } from './QuestionFilterComp'
import { router } from '@kit.ArkUI'

@Component
export struct QuestionListComp {
  // 这个数据是从HomeCategoryComp中传入的(父 传 子)
  @Prop typeid: number = 0
  @State list: QuestionItem[] = []
  // 定义一个状态属性,用来和Refresh组件进行双向数据绑定
  @State isRefreshing: boolean = false
  page: number = 0 //当前列表数据的页码
  @State isloadMore: boolean = false // 加载更多
  @Prop keyword: string = ''
  // 实例化自定义弹窗对象
  dialog = new CustomDialogController({
    builder: HdLoadingDialog({ message: '正在加载中...' }),
    customStyle: true, //表示关闭CustomDialogController自己的样式,只显示组件定义的样式
    alignment: DialogAlignment.Center  //弹窗的位置在中间
  })
  // 定义一个参数,用@Watch来监听其变化,从而主动执行getList方法
  @Prop @Watch('getList') params: FilterParams = { index: 0, sort: 0 } as FilterParams
  selfIndex: number = 0 // 应该由Cate组件传入,表示当前List组件是属于哪个分类索引下的组件

  // // source这个参数代表的是哪个参数的改变引起的监听器执行
  // loadData(source:string) {
  //   AlertDialog.show({ message: JSON.stringify('watch触发了' + source) })
  // }

  aboutToAppear(): void {
    // 注释原因:因为在List组件上增加了触底加载更多的方法onReachEnd,此时这个方法在页面进入的时候会执行一次,所以注释掉
    // aboutToAppear中的主动请求数据方法
    // this.getList()
  }

  async getList(p?: string) {
    // 优化:如果当前params中的index 和 List组件本身的index一致,应该执行请求,否则不允许执行
    if (this.params.index != this.selfIndex) {
      return
    }
    if (p == 'params') {
      this.page = 1
    }
    try {
      // 打开自定义弹窗
      this.dialog.open()

      let res = await HdHttp.Get<iQuestion>('hm/question/list', new Object({
        questionBankType: 10, //接口必选参数,默认填写10
        type: this.typeid, // 由于我们是要查看某个分类下面的数据,所以必须填写type参数
        page: this.page,
        sort: this.params.sort, //浏览量:20从低到高21从高到底  ,0:默认
        keyword: this.keyword  // 这是搜索关键字
      }))

      // 判断当前如果是触底加载更多,才追加到数据后面,如果是下拉刷新,应该直接赋值
      if (this.isloadMore) {
        this.list.push(...res.data.rows)
      } else {
        this.list = res.data.rows
      }

      // 增加将this.list中的所有id变成一个数组保存到AppStroage中
      // 1. 使用map将this.list数组中的id变成一个 ['12','13','25']
      let ids = this.list.map(item => item.id)
      // 2. 将ids数组保存到AppStroage中
      AppStorage.setOrCreate('list', ids)

      // 关闭下拉动画
      this.isRefreshing = false
      this.isloadMore = false //数据触底加载完成

      // 关闭自定义弹窗
      this.dialog.close()

    } catch (err) {
      // 关闭下拉动画
      this.isRefreshing = false
      this.isloadMore = false //数据触底加载完成
      // 关闭自定义弹窗
      this.dialog.close()
    }
  }

  build() {
    Column() {
      Text(this.typeid.toString()).fontSize(30)
      Refresh({ refreshing: $$this.isRefreshing }) {
        List() {
          ForEach(this.list, (item: QuestionItem) => {
            ListItem() {
              QuestionItemComp({
                item: item
              })
                .padding({ left: 10, right: 10 })
            }
            .onClick(() => {
              router.pushUrl({ url: 'pages/QuestionDetailPage', params: { id: item.id } })
            })

          })
        }
        .edgeEffect(EdgeEffect.None) // 关闭list组件的回弹效果,从而优化触底会发2次请求的问题
        .onReachEnd(() => {
          // 防止用户鼠标或者手指抖动
          if (this.isloadMore == false) {
            this.isloadMore = true
            // 页面进入即执行一次
            this.page++
            this.getList()
          }
        })
      }
      .onRefreshing(() => {
        // 重新请求服务器数据
        this.page = 1 //下拉刷新的时候,将当前页码重置为1
        this.getList()
      })

    }
  }
}

【搜索页】-2个优化

  1. SearchPage页面软键盘优化
import { promptAction, router } from '@kit.ArkUI'
import { preferences } from '@kit.ArkData'
import { QuestionListComp } from '../views/home/QuestionListComp'
import { FilterParams } from '../views/home/QuestionFilterComp'
import { Logger } from '../common/utils/Logger'

interface iRouterParams {
  keywordText: string
}

@Entry
@Component
struct SearchPage {
  @State keyword: string = ''
  @StorageProp('topHeight') topHeight: number = 0
  @State isdel: boolean = false
  @State kdList: string[] = []
  @State isSearch: boolean = true
  @StorageLink('filter')  params: FilterParams = { sort: 0, index: 0 }
  @State fsable:boolean = false
  @StorageLink('startInterval') startIntervalFlag : boolean = true

  async aboutToAppear() {
    this.kdList = await this.getData()

    // 接收搜索关键字
    const p = router.getParams() as iRouterParams
    this.keyword = p.keywordText
  }

  // 准备用户首选项的 保存数据方法+删除方法 + 获取方法
  async getData() {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })
    return pre.getSync('keyword', []) as string[] //['html5','css3'] -> 如果没有 []
  }

  // 保存数据
  async saveData(kdText: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })
    // 1. 先从用户首选项获取老数据
    const oldData = pre.getSync('keyword', []) as string[]
    // 2. 如果kdText在用户首选项中存来了,就不存了
    if (oldData.includes(kdText)) {
      return
    }

    oldData.push(kdText)

    // 3. 将新数据保存到首选项中
    pre.putSync('keyword', oldData)

    //   4. 写入到文件
    pre.flushSync()
  }

  // 删除数据(指定文本删除,全部删除)
  async delData(kdText?: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })

    if (kdText) {
      // 指定文本删除
      //   1. 获取老数据 2 删除指定的文本(内存中的数据) 3 将数据写回到磁盘
      const oldData = pre.getSync('keyword', []) as string[]
      const index = oldData.findIndex(item => item === kdText)
      // 对未找到的关键字不执行删除动作
      if (index < 0) {
        return
      }
      oldData.splice(index, 1)
      pre.putSync('keyword', oldData)
      pre.flushSync()
    } else {
      //   全部删除
      pre.deleteSync('keyword')
      pre.flushSync()
    }
  }

  @Builder
  itemBuilder(text: string) {
    Row({ space: 20 }) {
      Text(text)
        .onClick(()=>{
          this.isSearch = true
          this.keyword = text
          this.fsable = false
        })
      if (this.isdel) {
        Text('x')
          .height(30)
          .onClick(async () => {
            //   1. 调用删除方法
            await this.delData(text)
            //   2. 重新获取数据
            this.kdList = await this.getData()
          })
      }
    }
    .margin({ right: 10, top: 10 })
    .padding({
      left: 15,
      right: 15,
      top: 5,
      bottom: 5
    })
    .backgroundColor('rgba(0,0,0,0.05)')
    .borderRadius(20)
  }

  onBackPress(): boolean | void {
    Logger.debug('onBackPress')
    this.startIntervalFlag = false
    this.startIntervalFlag = true
    return false  // 使用原有的路由方式来回退
  }

  build() {
    Navigation() {
      Column({ space: 15 }) {
        // 1. 搜索关键字
        TextInput({ placeholder: '输入回车保存数据', text: $$this.keyword })// 回车的时候会触发
          .onSubmit(async () => {
            // AlertDialog.show({ message: this.keyword })
            // 1. 将关键字保存到首选项中
            await this.saveData(this.keyword)

            // 2. 获取用户首选项数据回显在页面上
            this.kdList = await this.getData()

            this.isSearch = true

            // AlertDialog.show({message:JSON.stringify('保存成功')})
          })
          .onClick(()=>{
            this.isSearch = false
            this.fsable = true
          })
          .focusable(this.fsable)

        if (this.isSearch) {
          //   使用搜索组件
          Column(){
              QuestionListComp({keyword:this.keyword,selfIndex:this.params.index})
          }
        } else {
          // 2. 关键字列表
          Row() {
            Text('搜索记录').fontSize(20).fontWeight(800)

            Row() {
              if (this.isdel) {
                Text('全部删除')
                  .onClick(async () => {
                    // AlertDialog.show({ message: '补上全部删除逻辑' })
                    //   1. 调用删除方法
                    await this.delData()
                    //   2. 重新获取数据
                    this.kdList = await this.getData()
                  })
                Text(' | ')
                Text('取消删除')
                  .onClick(() => {
                    this.isdel = false
                  })
              } else {
                Image($r('app.media.ic_common_delete'))
                  .height(28)
                  .onClick(() => {
                    this.isdel = true
                  })
              }
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)

          //   3. 关键字列表
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.kdList, (item: string) => {
              this.itemBuilder(item)
            })
          }
        }

      }
      .padding(15)
    }
    .padding({ top: this.topHeight })
    .titleMode(NavigationTitleMode.Mini)
    .title('搜索页面')
  }
}
  1. HdSearch组件定时器优化
import { router } from '@kit.ArkUI'
import { Logger } from '../utils/Logger'

@Component
export struct HdSearch {
  @State
  readonlyMode: boolean = true
  @State ph: string = 'html'
  bg: string = ''
  color: string = ''
  // 1.准备用户喜好的关键字
  keywords: string[] = ['html', 'css', 'js', 'vue', 'react']
  index: number = 0
  intervalNum: number = -1
  @StorageLink('startInterval') @Watch('startInterval') startIntervalFlag : boolean = true

  // 2. 进入这个组件之后,开启定时器(setInterval(()=>{},3000))每隔三秒中轮播里面的关键字
  aboutToAppear(): void {
    // Logger.debug('aboutToAppear')
    this.startInterval()
  }

  startInterval(){
    if(!this.startIntervalFlag) return

    this.intervalNum = setInterval(() => {
      Logger.debug('setInterval')
      //   1. 定义一个索引
      //   2. 索引自增后从keywords中取值赋值给 ph这个状态变量 ->自动刷新UI
      this.index++
      if (this.index >= this.keywords.length) {
        this.index = 0
      }
      this.ph = this.keywords[this.index]

    }, 3000)
  }

  build() {
    Row() {
      Row({ space: 4 }) {
        Image($r("app.media.ic_common_search"))
          .width($r('app.float.hd_search_icon_size'))
          .aspectRatio(1)
          .fillColor(this.color || $r('app.color.common_gray_02'))
        Text(this.ph || $r('app.string.hd_search_placeholder'))
          .fontColor(this.color || $r('app.color.common_gray_02'))
          .fontSize($r('app.float.common_font14'))
          .onClick(() => {
            // 跳转之前,将定时器移除
            clearInterval(this.intervalNum)

            router.pushUrl({ url: 'pages/SearchPage', params: { keywordText: this.ph } })
          })
      }
      .layoutWeight(1)
      .height($r('app.float.hd_search_height'))
      .backgroundColor(this.bg || $r('app.color.common_gray_border'))
      .borderRadius($r('app.float.hd_search_radius'))
      .justifyContent(FlexAlign.Center)
    }.onClick(() => {
      // router.replaceUrl({ url: 'pages/SearchPage' })
    })
  }
}
import { promptAction, router } from '@kit.ArkUI'
import { preferences } from '@kit.ArkData'
import { QuestionListComp } from '../views/home/QuestionListComp'
import { FilterParams } from '../views/home/QuestionFilterComp'
import { Logger } from '../common/utils/Logger'

interface iRouterParams {
  keywordText: string
}

@Entry
@Component
struct SearchPage {
  @State keyword: string = ''
  @StorageProp('topHeight') topHeight: number = 0
  @State isdel: boolean = false
  @State kdList: string[] = []
  @State isSearch: boolean = true
  @StorageLink('filter')  params: FilterParams = { sort: 0, index: 0 }
  @State fsable:boolean = false
  @StorageLink('startInterval') startIntervalFlag : boolean = true

  async aboutToAppear() {
    this.kdList = await this.getData()

    // 接收搜索关键字
    const p = router.getParams() as iRouterParams
    this.keyword = p.keywordText
  }

  // 准备用户首选项的 保存数据方法+删除方法 + 获取方法
  async getData() {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })
    return pre.getSync('keyword', []) as string[] //['html5','css3'] -> 如果没有 []
  }

  // 保存数据
  async saveData(kdText: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })
    // 1. 先从用户首选项获取老数据
    const oldData = pre.getSync('keyword', []) as string[]
    // 2. 如果kdText在用户首选项中存来了,就不存了
    if (oldData.includes(kdText)) {
      return
    }

    oldData.push(kdText)

    // 3. 将新数据保存到首选项中
    pre.putSync('keyword', oldData)

    //   4. 写入到文件
    pre.flushSync()
  }

  // 删除数据(指定文本删除,全部删除)
  async delData(kdText?: string) {
    const pre = preferences.getPreferencesSync(getContext(), { name: 'keywordStore' })

    if (kdText) {
      // 指定文本删除
      //   1. 获取老数据 2 删除指定的文本(内存中的数据) 3 将数据写回到磁盘
      const oldData = pre.getSync('keyword', []) as string[]
      const index = oldData.findIndex(item => item === kdText)
      // 对未找到的关键字不执行删除动作
      if (index < 0) {
        return
      }
      oldData.splice(index, 1)
      pre.putSync('keyword', oldData)
      pre.flushSync()
    } else {
      //   全部删除
      pre.deleteSync('keyword')
      pre.flushSync()
    }
  }

  @Builder
  itemBuilder(text: string) {
    Row({ space: 20 }) {
      Text(text)
        .onClick(()=>{
          this.isSearch = true
          this.keyword = text
          this.fsable = false
        })
      if (this.isdel) {
        Text('x')
          .height(30)
          .onClick(async () => {
            //   1. 调用删除方法
            await this.delData(text)
            //   2. 重新获取数据
            this.kdList = await this.getData()
          })
      }
    }
    .margin({ right: 10, top: 10 })
    .padding({
      left: 15,
      right: 15,
      top: 5,
      bottom: 5
    })
    .backgroundColor('rgba(0,0,0,0.05)')
    .borderRadius(20)
  }

  onBackPress(): boolean | void {
    Logger.debug('onBackPress')
    this.startIntervalFlag = false
    this.startIntervalFlag = true
    return false  // 使用原有的路由方式来回退
  }

  build() {
    Navigation() {
      Column({ space: 15 }) {
        // 1. 搜索关键字
        TextInput({ placeholder: '输入回车保存数据', text: $$this.keyword })// 回车的时候会触发
          .onSubmit(async () => {
            // AlertDialog.show({ message: this.keyword })
            // 1. 将关键字保存到首选项中
            await this.saveData(this.keyword)

            // 2. 获取用户首选项数据回显在页面上
            this.kdList = await this.getData()

            this.isSearch = true

            // AlertDialog.show({message:JSON.stringify('保存成功')})
          })
          .onClick(()=>{
            this.isSearch = false
            this.fsable = true
          })
          .focusable(this.fsable)

        if (this.isSearch) {
          //   使用搜索组件
          Column(){
              QuestionListComp({keyword:this.keyword,selfIndex:this.params.index})
          }
        } else {
          // 2. 关键字列表
          Row() {
            Text('搜索记录').fontSize(20).fontWeight(800)

            Row() {
              if (this.isdel) {
                Text('全部删除')
                  .onClick(async () => {
                    // AlertDialog.show({ message: '补上全部删除逻辑' })
                    //   1. 调用删除方法
                    await this.delData()
                    //   2. 重新获取数据
                    this.kdList = await this.getData()
                  })
                Text(' | ')
                Text('取消删除')
                  .onClick(() => {
                    this.isdel = false
                  })
              } else {
                Image($r('app.media.ic_common_delete'))
                  .height(28)
                  .onClick(() => {
                    this.isdel = true
                  })
              }
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)

          //   3. 关键字列表
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.kdList, (item: string) => {
              this.itemBuilder(item)
            })
          }
        }

      }
      .padding(15)
    }
    .padding({ top: this.topHeight })
    .titleMode(NavigationTitleMode.Mini)
    .title('搜索页面')
  }
}

【我的】- 功能流程

课程目标

  • 完成编辑个人信息:① 上传头像 ② 修改昵称
  • 完成累计学时统计
  • 完成单词用法高亮、AVPlayer朗读单词功能

【我的-编辑个人信息页】-静态页

课程目标

  • 迁移下面代码到ProfileEditPage.ets页面中
  • 使用@StorageProp('user') 同步用户头像和昵称数据
import { promptAction } from '@kit.ArkUI'
import { iLoginUserModel } from '../models/AccountModel'

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(() => {
                  // 选择头像并上传this.pickerAvatar()

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({ left: (45), right: (45), top: (15), bottom: (15) })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【我的-编辑个人信息页】- 头像上传

课程目标

  • 完成 利用picker api选择1张图片
  • 完成利用 request.uploadFile 进行图片上传
  • get请求userInfo接口刷新用户数据,更新AppStorage("user")中的用户缓存数据

【头像上传】- 使用photoAccessHelper选择一张图片

步骤:

  1. 实例化选择器参数(使用new PhotoSelectOptions())
  2. 实例化图片选择器 (使用newPhotoViewPicker() )
  3. 调用图片选择器的select方法传入选择器参数完成图片选取获得结果
import { iLoginUserModel } from '../models/datamodel'
// import { picker } from '@kit.CoreFileKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit'


@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 1. 选择系统相册的图片
  async selectImage(maxnum: number) {
    //   1. 定义打开系统相册的相关参数
    //   const options = new picker.PhotoSelectOptions()
    const options = new photoAccessHelper.PhotoSelectOptions()
    options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
    options.maxSelectNumber = maxnum

    //   2. 打开和选择
    let viewer = new photoAccessHelper.PhotoViewPicker()
   const res = await viewer.select(options)
    AlertDialog.show({message:JSON.stringify(res.photoUris,null,2)})
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 选择头像并上传this.pickerAvatar()
                  // 文件的本地存储路径。
                 await this.selectImage(6)

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【头像上传】- 拷贝选择图片到缓存目录

步骤:

  1. 用上下文获取当前应用的缓存目录-> getContext().cacheDir
  2. 利用Date.now()随机生成图片名字filename,扩展名为jpg
  3. 利用 fs.openSync方法打开图片,准备拷贝到缓存目录 ->const file = fs.openSync(uri, fs.OpenMode.READ_ONLY)
  4. 利用fs.copyFileSync(file.fd, fullPath) 拷贝文件到缓存目录

此时可以获得当前图片的一个缓存地址供reqeust.uploadFile使用:'internal://cache/' + filename

import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【头像上传】- 利用request.uploadFile 进行图片上传

上传接口文档

步骤:

  1. 准备好参数调用request.uploadFile()获得上传对象 uploader
 'Content-Type': 'multipart/form-data'
  1. 给uploader对象注册progress事件,监听上传进度 requestRes.on("progress", (uploadedSize: number, totalSize: number)=>{})
  2. 给uploader对象注册fail事件,监听报错信息requestRes.on('fail', (taskStates) => {})
import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageProp('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', (res) => {
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【头像上传】- 重新调用接口获取最新用户数据

步骤:

    1. get请求userInfo接口 重新获取用户数据
    2. 通过@StorageLink(''user")完成 用户头像字段avatar的修改

import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', async (res) => {
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
      //  这个方法一旦触发,那么服务器的头像已经上传完毕并且更新了
      //  这是去重新获取https://api-harmony-teach.itheima.net/hm/userInfo中的头像地址就是我们上传以后的新的头像地址
      let newUserInfo = await HdHttp.Get<object>('hm/userInfo')
      this.currentUser.avatar = newUserInfo.data['avatar']

      // AlertDialog.show({ message: JSON.stringify('老头像地址:'+this.currentUser.avatar +' 新的头像地址:' + newUserInfo.data['avatar']) })
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【头像上传】- 上传百分比实时更新

业务分析:

  1. ✔️需要在上传的时候获取上传进度百分比(request.uploadFile的progress事件来完成 )

  1. 需要使用emiiter来将上传进度百分数字从ProfileEditPage.ets 传给 HdLoadingDialog.ets页面

问题:使用传统的响应式方式是无法向@CustomDialog组件实时更新数据的

解决:需要使用emitter来实时更新@CustomDialog组件中的变量,从而达到页面数据的实时更新

progress事件监听上传进度
import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', async (res) => {
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
      //  这个方法一旦触发,那么服务器的头像已经上传完毕并且更新了
      //  这是去重新获取https://api-harmony-teach.itheima.net/hm/userInfo中的头像地址就是我们上传以后的新的头像地址
      let newUserInfo = await HdHttp.Get<object>('hm/userInfo')
      this.currentUser.avatar = newUserInfo.data['avatar']

      // AlertDialog.show({ message: JSON.stringify('老头像地址:'+this.currentUser.avatar +' 新的头像地址:' + newUserInfo.data['avatar']) })
    })

    //   3. 监控当前的上传进度
    uploador.on('progress', (uploadedSize, totalSize) => {
      // uploadedSize  -> 当前已经上传的大小
      // totalSize  -> 要上传的总大小
      Logger.info(uploadedSize.toString(), totalSize.toString())
      let pnum = (uploadedSize / totalSize * 100).toFixed(0) // 计算出百分数 %
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

打开自定义弹窗
import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';
import { HdLoadingDialog } from '../components/HdLoadingDialog';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0

  // 实例化自定义弹窗对象
  dialog = new CustomDialogController({
    builder: HdLoadingDialog({ message: '上传:' }),
    customStyle: true
  })

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    // 打开上传提示弹窗
    this.dialog.open()

    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
      // 关闭上传提示弹窗
      this.dialog.close()
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', async (res) => {
      // 关闭上传提示弹窗
      this.dialog.close()
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
      //  这个方法一旦触发,那么服务器的头像已经上传完毕并且更新了
      //  这是去重新获取https://api-harmony-teach.itheima.net/hm/userInfo中的头像地址就是我们上传以后的新的头像地址
      let newUserInfo = await HdHttp.Get<object>('hm/userInfo')
      this.currentUser.avatar = newUserInfo.data['avatar']

      // AlertDialog.show({ message: JSON.stringify('老头像地址:'+this.currentUser.avatar +' 新的头像地址:' + newUserInfo.data['avatar']) })
    })

    //   3. 监控当前的上传进度
    uploador.on('progress', (uploadedSize, totalSize) => {
      // uploadedSize  -> 当前已经上传的大小
      // totalSize  -> 要上传的总大小
      Logger.info(uploadedSize.toString(), totalSize.toString())
      let pnum = (uploadedSize / totalSize * 100).toFixed(0) // 计算出百分数 %
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}

【新知识】- emitter核心api

课程目标

  • 理解emitter核心api的作用和工作机制

emitter什么是?emitter主要提供发送和处理事件的能力,包括订阅事件(on)、发送事件(emit)、取消订阅(off)事件的功能。我们通过发送事件,来触发所有订阅的事件的执行。

应用场景:可以通过emitter向@CustomDialog组件来实时传递数据

【头像上传】- 使用emitter实现上传进度百分比更新

使用CustomDialogController 弹出正在上传中的提示,并且需要展示上传百分比

步骤:

  1. 在src/main/ets/pages/ProfileEditPage.ets中利用emitter发送事件并传递数据
  2. 在LoadingDialog.ets 中利用emitter 完成事件订阅emitter.on(),来接收上传进度数据,并在页面上更新上传进度数据

import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { emitter, request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';
import { HdLoadingDialog } from '../components/HdLoadingDialog';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0
  // 实例化自定义弹窗对象
  dialog = new CustomDialogController({
    builder: HdLoadingDialog({ message: '上传:' }),
    customStyle: true
  })

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    // 打开上传提示弹窗
    this.dialog.open()

    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
      // 关闭上传提示弹窗
      this.dialog.close()
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', async (res) => {
      // 关闭上传提示弹窗
      this.dialog.close()
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
      //  这个方法一旦触发,那么服务器的头像已经上传完毕并且更新了
      //  这是去重新获取https://api-harmony-teach.itheima.net/hm/userInfo中的头像地址就是我们上传以后的新的头像地址
      let newUserInfo = await HdHttp.Get<object>('hm/userInfo')
      this.currentUser.avatar = newUserInfo.data['avatar']

      // AlertDialog.show({ message: JSON.stringify('老头像地址:'+this.currentUser.avatar +' 新的头像地址:' + newUserInfo.data['avatar']) })
    })

    //   3. 监控当前的上传进度
    uploador.on('progress', (uploadedSize, totalSize) => {
      // uploadedSize  -> 当前已经上传的大小
      // totalSize  -> 要上传的总大小
      Logger.info(uploadedSize.toString(), totalSize.toString())
      let pnum = (uploadedSize / totalSize * 100).toFixed(0) // 计算出百分数 %

      // 使用emitter将pnum这个数据发送出去
      emitter.emit({ eventId: 0 }, { data: { pstr: pnum + '%' } })
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: this.currentUser.nickName || '昵称' })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(() => {
                  // 修改昵称 this.updateNickName()

                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}
/*
 * 自定义弹窗有规则:
 * 1. 必须有 @CustomDialog
 * 2.里面使用  controller: CustomDialogController 定义一个固定的控制器对象
 * */
import { emitter } from '@kit.BasicServicesKit'
import { Logger } from '../utils/Logger'

@CustomDialog
export struct HdLoadingDialog {
  @Prop message: string = ''
  controller: CustomDialogController

  aboutToAppear(): void {
    // 注册emitter的on事件来监听emit发送过来的数据
    emitter.on({ eventId: 0 }, (rec) => {
      let pstr = rec.data!['pstr'] as string
      this.message = '上传:' + pstr
    })
  }

  build() {
    Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      LoadingProgress().width(30).height(30).color('#fff')
      if (this.message) {
        Text(this.message).fontSize((14)).fontColor('#fff')
      }
    }
    .width(150)
    .height(50)
    .padding(10)
    .backgroundColor('rgba(0,0,0,0.5)')
    .borderRadius(8)
  }
}

【我的-编辑个人信息页】-昵称更新

课程目标

  • post请求userInfo/profile接口(接口文档)完成用户昵称更新

import { iLoginUserModel } from '../models/datamodel'
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { emitter, request } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { HdHttp } from '../utils/request';
import { HdLoadingDialog } from '../components/HdLoadingDialog';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct ProfileEditPage {
  // 获取登录用户数据
  @StorageLink('user') currentUser: iLoginUserModel = {} as iLoginUserModel
  @State nickName: string = this.currentUser.nickName
  // 获取安全区域高度数据
  @StorageProp("topHeight") topHeight: number = 0
  // 实例化自定义弹窗对象
  dialog = new CustomDialogController({
    builder: HdLoadingDialog({ message: '上传:' }),
    customStyle: true
  })

  // 1. 使用picker选择相册中的图片
  async selectImage(maxnum: number) {
    // 1.1 实例化选择参数
    let opts = new picker.PhotoSelectOptions()
    opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    opts.maxSelectNumber = maxnum

    // 1.2 打开相册来选择照片返回(选择相册照片的数组)
    let viewer = new picker.PhotoViewPicker()
    let res = await viewer.select(opts)
    return res.photoUris
  }

  // 2. 拷贝到应用程序缓存目录
  async copyToCacheDir(photoImagePath: string) {
    //   1. 使用openSync将相册中的图片加载到内存中得到内存的数字指向
    let file = fs.openSync(photoImagePath, fs.OpenMode.READ_ONLY)

    //   2. 使用copyFileSync完成图片拷贝到应用程序缓存中
    let dir = getContext().cacheDir
    let type = 'jpg'
    let filename = Date.now() + '.' + type
    let fullpath = dir + '/' + filename

    fs.copyFileSync(file.fd, fullpath)

    //   3. 返回文件名和文件的扩展名
    // ['123123234.jpg','jpg']
    return [filename, type]
  }

  // 3. 头像上传
  async upload(filename: string, type: string) {
    // 打开上传提示弹窗
    this.dialog.open()

    let uploador = await request.uploadFile(getContext(), {
      method: 'POST',
      url: 'https://api-harmony-teach.itheima.net/hm/userInfo/avatar',
      header: {
        'Content-Type': 'multipart/form-data',
        'Authorization': `Bearer ${this.currentUser.token}`
      },
      files: [
        {
          filename: filename,
          type: type,
          name: 'file',
          uri: 'internal://cache/' + filename
        }
      ],
      data: []
    })

    //  1.监控文件上传失败事件
    // 不能监听所有异常
    uploador.on('fail', (err) => {
      // AlertDialog.show({ message: 'fail-->' + JSON.stringify(err, null, 2) })
      Logger.error('头像上传失败', JSON.stringify(err))
      // 关闭上传提示弹窗
      this.dialog.close()
    })

    //  2. 监控服务器响应回来的数据
    uploador.on('headerReceive', async (res) => {
      // 关闭上传提示弹窗
      this.dialog.close()
      // AlertDialog.show({ message: '完成-->' + JSON.stringify(res, null, 2) })
      //  这个方法一旦触发,那么服务器的头像已经上传完毕并且更新了
      //  这是去重新获取https://api-harmony-teach.itheima.net/hm/userInfo中的头像地址就是我们上传以后的新的头像地址
      let newUserInfo = await HdHttp.Get<object>('hm/userInfo')
      this.currentUser.avatar = newUserInfo.data['avatar']

      // AlertDialog.show({ message: JSON.stringify('老头像地址:'+this.currentUser.avatar +' 新的头像地址:' + newUserInfo.data['avatar']) })
    })

    //   3. 监控当前的上传进度
    uploador.on('progress', (uploadedSize, totalSize) => {
      // uploadedSize  -> 当前已经上传的大小
      // totalSize  -> 要上传的总大小
      Logger.info(uploadedSize.toString(), totalSize.toString())
      let pnum = (uploadedSize / totalSize * 100).toFixed(0) // 计算出百分数 %

      // 使用emitter将pnum这个数据发送出去
      emitter.emit({ eventId: 0 }, { data: { pstr: pnum + '%' } })
    })
  }

  build() {
    Navigation() {
      Stack() {
        List() {
          ListItem() {
            Row() {
              Text('头像')
              // 回显用户头像
              Image(this.currentUser.avatar || $rawfile('avatar.png'))
                .width((40))
                .width((40))
                .borderRadius((40))
                .border({ width: 0.5, color: '#e4e4e4' })
                .onClick(async () => {
                  // 1. 使用picker选择相册中的图片
                  let urls = await this.selectImage(1)
                  // AlertDialog.show({ message: JSON.stringify(urls[0]) })

                  //  2. 利用fs将相册图片拷贝到缓存目录中
                  let fileInfo = await this.copyToCacheDir(urls[0])
                  // AlertDialog.show({ message: JSON.stringify(fileInfo, null, 2) })

                  //  3. 利用reqeust.uploadFile完成图片上传
                  await this.upload(fileInfo[0], fileInfo[1])

                })
            }.width('100%').height((60)).justifyContent(FlexAlign.SpaceBetween)
          }

          ListItem() {
            Row() {
              Text('昵称')
              // 回显用户昵称
              TextInput({ text: $$this.nickName })
                .textAlign(TextAlign.End)
                .layoutWeight(1)
                .padding(0)
                .height((60))
                .backgroundColor(Color.Transparent)
                .borderRadius(0)
                .onSubmit(async () => {
                  //1. 调用接口修改昵称
                  //   https://api-harmony-teach.itheima.net/hm/userInfo/profile
                  //   传参:nickName
                  await HdHttp.Post<object>('hm/userInfo/profile', new Object({
                    nickName: this.nickName
                  }))

                  //  2. 将最新的昵称同步给 currentUser中的nickName
                  this.currentUser.nickName = this.nickName
                  promptAction.showToast({ message: '昵称修改成功 ' })
                })
            }.width('100%').height(60).justifyContent(FlexAlign.SpaceBetween)
          }
        }
        .width('100%')
        .height('100%')
        .padding({
          left: (45),
          right: (45),
          top: (15),
          bottom: (15)
        })
        .divider({ strokeWidth: 0.5, color: '#f5f5f5' })

      }.width('100%')
      .height('100%')
    }
    .padding({ top: this.topHeight + 10 })
    .title('完善个人信息')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .linearGradient({
      colors: [['#FFB071', 0], ['#f3f4f5', 0.3], ['#f3f4f5', 1]]
    })
  }
}


http://www.kler.cn/a/595731.html

相关文章:

  • 模型整合-cherry studio+mysql_mcp_server服务配置
  • FreeRTOS移植并实现一个多任务程序
  • 如何借助es的snapshot跨集群迁移部分索引
  • git原理与常用命令及其使用
  • electron框架(4.0)electron-builde和electron Forge的打包方式
  • 鸿蒙开发工程师简历项目撰写全攻略
  • 开源模型应用落地-LangGraph101-多智能体协同实践(六)
  • 蓝桥杯 第十天 2019国赛第4题 矩阵计数
  • 《Python实战进阶》No42: 多线程与多进程编程详解(下)
  • PowerBI纯小白如何驾驭DAX公式一键生成:copilot for fabric
  • Docker学习笔记(十)搭建Docker私有仓库
  • 密码协议与网络安全——引言
  • 零基础搭建智能法律知识库!腾讯云HAI实战教程
  • 基于Arm GNU Toolchain编译生成的.elf转hex/bin文件格式方法
  • 数学建模 绘图 图表 可视化(3)
  • 星越L_超速报警功能使用讲解
  • Java Web开发技术解析:从基础到实践的全栈指南
  • 手撸一个 deepseek 聊天历史对话、多轮对话(ollama + deepseek + langchain + flask)
  • 基于图像处理和机器学习实现的压差表数值读取
  • HarmonyOS鸿蒙开发 BuilderParam在父组件的Builder的点击事件报错:Error message:is not callable