HarmonyOS NEXT应用开发边学边玩系列:从零实现一影视APP (三、影视搜索页功能实现)

2025-01-14 16:37:02
9次阅读
0个评论
最后修改时间:2025-01-14 16:45:03

在HarmonyOS NEXT开发环境中,我们可以使用@nutpi/axios库来简化网络请求的操作。本文将展示如何使用HarmonyOS NEXT框架和@nutpi/axios库,从零开始实现一个简单的影视APP,主要关注影视搜索页的功能实现。

为什么选择@nutpi/axios

nutpi/axios是坚果派出品的,对axios封装过的鸿蒙HTTP客户端库,用于简化axios库的使用和以最简单的形式写代码。使用nutpi/axios库可以大大简化代码,使网络接口变得简单直观。

项目开源地址:https://atomgit.com/csdn-qq8864/hmmovie

安装nutpi/axios

首先,我们需要在项目中安装nutpi/axios库。

ohpm install @ohos/axios

a5b9c842be5cf69c2728883a5c2cd3d.jpg

实现电影搜索接口

我们使用nutpi/axios库来实现电影搜索接口。先封装一个工具类:

//axiosClient.ets
import {AxiosHttpRequest,HttpPromise} from '@nutpi/axios'
import {AxiosHeaders,AxiosRequestHeaders,AxiosError } from '@nutpi/axios';
import { Log } from './logutil';
import { promptAction } from "@kit.ArkUI";

function showToast(msg:string){
  Log.debug(msg)
  promptAction.showToast({ message: msg })
}

function showLoadingDialog(msg:string){
  Log.debug(msg)
  promptAction.showToast({ message: msg })
}

function hideLoadingDialog() {

}
/**
 * axios请求客户端创建
 */
const axiosClient = new AxiosHttpRequest({
  baseURL: "http://120.27.146.247:8000/api/v1",
  timeout: 10 * 1000,
  checkResultCode: false,
  showLoading:true,
  headers: new AxiosHeaders({
    'Content-Type': 'application/json'
  }) as AxiosRequestHeaders,
  interceptorHooks: {
    requestInterceptor: async (config) => {
      // 在发送请求之前做一些处理,例如打印请求信息
      Log.debug('网络请求Request 请求方法:', `${config.method}`);
      Log.debug('网络请求Request 请求链接:', `${config.url}`);
      Log.debug('网络请求Request Params:', `\n${JSON.stringify(config.params)}`);
      Log.debug('网络请求Request Data:', `${JSON.stringify(config.data)}`);
      // 动态添加或修改header
      //config.headers['X-ATOMGIT-POP-COMMUNITY'] = 'openatom';
      axiosClient.config.showLoading = config.showLoading
      if (config.showLoading) {
        showLoadingDialog("加载中...")
      }
      if (config.checkLoginState) {
        //let hasLogin = await StorageUtils.get(StorageKeys.USER_LOGIN, false)
        //Log.debug('网络请求Request 登录状态校验>>>', `${hasLogin.toString()}`);
        // if (hasLogin) {
        //   return config
        // } else {
        //   if (config.needJumpToLogin) {
        //     //Router.push(RoutePath.TestPage)
        //   }
        //   throw new AxiosError("请登录")
        // }
      }
      return config;
    },
    requestInterceptorCatch: (err) => {
      Log.error("网络请求RequestError", err.toString())
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      return err;
    },
    responseInterceptor: (response) => {
      //优先执行自己的请求响应拦截器,在执行通用请求request的
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      Log.debug('网络请求响应Response:', `\n${JSON.stringify(response.data)}`);
      if (response.status === 200) {
        // @ts-ignore
        const checkResultCode = response.config.checkResultCode
        if (checkResultCode && response.data.errorCode != 0) {
          showToast(response.data.errorMsg)
          return Promise.reject(response)
        }
        return Promise.resolve(response);
      } else {
        return Promise.reject(response);
      }
    },
    responseInterceptorCatch: (error) => {
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      Log.error("网络请求响应异常", error.toString());
      errorHandler(error);
      return Promise.reject(error);
    },
  }
});

function errorHandler(error: any) {
  if (error instanceof AxiosError) {
    //showToast(error.message)
  } else if (error != undefined && error.response != undefined && error.response.status) {
    switch (error.response.status) {
    // 401: 未登录
    // 未登录则跳转登录页面,并携带当前页面的路径
    // 在登录成功后返回当前页面,这一步需要在登录页操作。
      case 401:

        break;
    // 403 token过期
    // 登录过期对用户进行提示
    // 清除本地token和清空vuex中token对象
    // 跳转登录页面
      case 403:
        //showToast("登录过期,请重新登录")
      // 清除token
      // localStorage.removeItem('token');
        break;
    // 404请求不存在
      case 404:
        //showToast("网络请求不存在")
        break;

    // 其他错误,直接抛出错误提示
      default:
        //showToast(error.response.data.message)
    }

  }
}

export  {axiosClient,HttpPromise};

接口的实现很简单啦:

// 定义电影搜索接口
// 7.电影搜索接口
export const movieSearch =  (q:string,start:number,count:number): HttpPromise<SearchResp> => axiosClient.post({url:'/searchmovie',data: {  q:q,start:start,count:count }});

代码讲解

  1. 创建axios客户端:我们封装了一个axios客户端axiosClient工具类,并设置了基础URL和请求超时时间。
  2. 定义接口函数movieSearch函数接收三个参数:q(查询字符串)、start(起始位置)和count(数量),并返回一个Promise对象。

Search组件和List组件使用

接下来,我们将实现影视搜索页的组件,包括Search组件和List组件的使用。

import { movieSearch } from '../../common/api/movie';
import { SearchRespData } from '../../common/bean/SearchResp';
import { Log } from '../../utils/logutil';
import { BusinessError } from '@kit.BasicServicesKit';

@Builder
export function SearchPageBuilder() {
  SearchPage()
}

@Component
struct SearchPage {
  pageStack: NavPathStack = new NavPathStack()
  controller: SearchController = new SearchController()
  @State changeValue: string = ''
  @State submitValue: string = ''
  @State searchList: SearchRespData[] = []

  // 组件生命周期
  aboutToAppear() {
    Log.info('SearchPage aboutToAppear');
  }

  onPageShow(): void {
    this.controller.caretPosition(0)
  }

  build() {
    NavDestination() {
      Column({ space: 0 }) {
        Search({ controller: this.controller, value: this.changeValue, placeholder: '请输入片名' })
          .searchButton('搜索')
          .width('95%')
          .height(45)
          .maxLength(30)
          .backgroundColor('#F5F5F5')
          .placeholderColor(Color.Grey)
          .placeholderFont({ size: 14, weight: 400 })
          .textFont({ size: 14, weight: 400 })
          .focusable(true)
          .defaultFocus(true)
          .onSubmit((value: string) => {
            this.submitValue = value
          })
          .onChange((value: string) => {
            this.changeValue = value
            movieSearch(value, 1, 10).then((res) => {
              Log.debug(res.data.message)
              Log.debug("request", "res.data.code:%{public}d", res.data.code)
              if (res.data.code == 0) {
                this.searchList = res.data.data
              }
            })
              .catch((err: BusinessError) => {
                Log.debug("request", "err.data.code:%d", err.code)
                Log.debug("request", err.message)
              });
          })
          .margin({ left: 20, right: 20 })

        // list组件
        List({ space: 10 }) {
          ForEach(this.searchList, (item: SearchRespData, idx) => {
            ListItem() {
              Column({ space: 0 }) {
                Row() {
                  Stack() {
                    Image(item.cover).objectFit(ImageFit.Cover)
                      .borderRadius(5).zIndex(1)

                    Text(item.year.substring(0, 10))
                      .padding(5)
                      .margin({ top: 80 })
                      .width('100%')
                      .height(20)
                      .textAlign(TextAlign.Center)
                      .maxLines(2)
                      .textOverflow({ overflow: TextOverflow.Clip })
                      .fontSize(12)
                      .fontColor(Color.White)
                      .opacity(100) // 设置标题的透明度
                      .backgroundColor('#808080AA') // 背景颜色设为透明
                      .zIndex(2)
                  }.width(100).height(100).margin({ left: 10 })

                  Column({ space: 15 }) {
                    Text(item.title).fontSize(16).fontWeight(FontWeight.Bold).align(Alignment.Start).width('100%')
                    Text(item.genre).fontSize(12).align(Alignment.Start).width('100%')
                    Text('评分: ' + item.rate).fontSize(12)
                      .align(Alignment.Start).width('100%')
                  }.justifyContent(FlexAlign.Start).padding(5).margin({ left: 10 })
                }.size({ width: '100%', height: 100 })
              }.size({ width: '100%', height: 100 })
            }.onClick(() => {
              this.pageStack.pushDestinationByName("MovieDetailPage", { id: item.id }).catch((e: Error) => {
                console.log(`catch exception: ${JSON.stringify(e)}`)
              }).then(() => {
                // 跳转成功
              });
            })
          }, (itm: SearchRespData) => itm.id)
        }
        .divider({ strokeWidth: 2, color: '#F1F3F5' })
        .listDirection(Axis.Vertical)
        .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })

      }
      .width('100%')
      .height('100%')
    }.title("影视搜索")
    .width('100%')
    .height('100%')
    .onReady(ctx => {
      this.pageStack = ctx.pathStack
    })
  }
}

代码讲解

  1. 导入模块:我们导入了之前定义的movieSearch函数,以及一些其他必要的模块。
  2. 定义组件状态
    • changeValue:用于存储当前搜索框中的输入值。
    • submitValue:用于存储用户提交的搜索值。
    • searchList:用于存储搜索结果的列表。
  3. 组件生命周期
    • aboutToAppear:组件即将出现在页面时执行的日志记录。
    • onPageShow:组件显示时重置光标位置。
  4. 构建页面
    • NavDestination:定义页面的导航目的地。
    • Column:垂直布局容器。
    • Search:搜索框组件,设置了搜索按钮、宽度、高度、最大长度等属性,并绑定了onSubmitonChange事件。
    • List:列表组件,用于显示搜索结果。
      • ForEach:遍历searchList数组,为每个搜索结果项创建一个ListItem
      • ListItem:列表项组件,包含电影的封面、年份、标题、类型和评分。
      • onClick:点击列表项时,导航到电影详情页。
  5. 列表样式
    • divider:设置列表项之间的分隔线。
    • listDirection:设置列表的方向为垂直。
    • edgeEffect:设置边缘效果为弹簧效果。

总结

通过本文,我们展示了如何使用HarmonyOS NEXT框架和nutpi/axios库来实现一个简单的影视搜索页。nutpi/axios库的使用大大简化了网络请求的操作,使代码更加简洁易读。希望这篇文章对你有所帮助,让你在开发HarmonyOS NEXT应用时更加得心应手。

如果你有任何问题或建议,欢迎在评论区留言交流!

收藏00

登录 后评论。没有帐号? 注册 一个。