HarmonyOS NEXT 5.0鸿蒙开发一套影院APP(附带源码)

448 阅读5分钟

鸿蒙开发HarmonyOS NEXT5.0开发一套影院APP

鸿蒙开发HarmonyOS NEXT5.0开发一套影院APP效果图创建项目tabs菜单实现第三方axios引入和基本使用简介下载安装需要权限axios二次封装和类型定义正在热映和即将上映实现首页api封装首页内容电影列表内容和样式问题影院列表页面内容影院api定义

效果图

电影影院
Screenshot_2024-12-17T162132Screenshot_2024-12-17T162148

创建项目

image-20241217174535668

image-20241217174824839

image-20241217174927934

tabs菜单实现

在Tabs中使用TabContent,对应一个切换页签的内容视图。改写Index.ets,实现tabs菜单的切换

@Entry
@Component
struct Index {
  build() {
    Tabs({ barPosition: BarPosition.End }) {
      TabContent() {
        Text('电影页面')
      }.tabBar('电影')
​
      TabContent() {
        Text('影院页面')
      }.tabBar('影院')
    }
  }
}

image-20241217175125704

第三方axios引入和基本使用

简介

Axios ,是一个基于 promise 的网络请求库,可以运行 node.js 和浏览器中。本库基于Axios 原库v1.3.4版本进行适配,使其可以运行在 OpenHarmony,并沿用其现有用法和特性。

  • http 请求
  • Promise API
  • request 和 response 拦截器
  • 转换 request 和 response 的 data 数据
  • 自动转换 JSON data 数据

axios三方库封装的意义 对axios进行封装的意义在于提供更高层次的抽象,以便简化网络请求的使用和管理。以下是一些具体的理由:

1.统一接口:封装后,可以统一管理所有的网络请求接口,使得在应用中调用网络请求时更加一致,减少重复代码。

2.简化配置:封装可以避免每次请求都需要重复配置相似的参数(例如headers、请求方式等),通过配置对象直接传入更简洁。

3.请求和响应拦截器:封装允许在发送请求之前或收到响应之后,对请求或响应进行处理,比如添加公共的请求头、处理错误、数据格式化等。

4.错误处理:通过自定义的错误处理机制,可以实现统一的错误处理逻辑,比如根据状态码处理特定的错误(例如401未登录、403权限不足等)。

5.增强功能:可以根据项目需求添加额外的功能,例如显示加载状态、处理用户登录状态等。

6.提高可维护性:将网络请求相关的逻辑集中管理,可以让代码更加清晰,降低维护成本。

7.支持特定业务需求:可根据实际的业务需求扩展功能,比如提供缓存机制、重试机制等,增强请求的灵活性。

下载安装

ohpm install @ohos/axios

OpenHarmony ohpm 环境配置等更多内容,请参考如何安装 OpenHarmony ohpm 包

需要权限

ohos.permission.INTERNET

module.json5文件中添加以下内容,开启网络请求权限。

    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]

axios二次封装和类型定义

创建request.ets,实现axios的二次封装

import axios, { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from '@ohos/axios'
import { ResultEnum } from '../enums/ResultEnum';
​
// 创建 axios 实例
const request = axios.create({
  baseURL: "https://m.maizuo.com",
  timeout: 50000,
  headers: {
    "Content-Type": "application/json;charset=utf-8",
    'x-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"1734338281267481973260289","bc":"320800"}'
  }
})
​
// 添加请求拦截器
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  // 对请求数据做点什么 如 添加token
  return config;
}, (error: AxiosError) => {
  // 对请求错误做些什么
  return Promise.reject(error);
});
​
​
// 添加响应拦截器
request.interceptors.response.use((response: AxiosResponse) => {
  const result: AxiosResponse<R> = response.data
​
  console.info(JSON.stringify(response.data))
  if (result.status === ResultEnum.SUCCESS) {
​
    return response.data.data;
  }
​
  return Promise.reject(new Error(result.data.msg || "Error"));
​
}, (error: AxiosError) => {
  // 对响应错误做点什么
  return Promise.reject(error.message);
});
​
​
// 统一返回类型
interface R {
  status: number,
  msg?: string,
  data: object
}
​
export default request;

正在热映和即将上映实现

创建Home.etsCinema.ets组件,以及index.etsapi组件

首页api封装

import request from '../utils/request'export interface MoveData {
  films?: Films[],
  total?: number
}
​
export interface Films {
  filmId: number;
  name: string;
  poster: string;
  actors: Array<Actors>;
  director: string;
  category: string;
  synopsis: string;
  filmType: FilmType;
  nation: string;
  language?: string;
  videoId?: string;
  premiereAt: number;
  timeType: number;
  runtime: number;
  grade: string;
  item: Item;
  isPresale: boolean;
  isSale: boolean;
};
​
export interface Actors {
  name: string;
  role: string;
  avatarAddress: string;
};
​
export interface FilmType {
  name: string;
  value: number;
};
​
export interface Item {
  name: string;
  type: number;
};
​
export const getIndex = (page: number) => {
  return request<string,MoveData>({
    url: `/gateway?cityId=320800&pageNum=${page}&pageSize=10&type=1&k=5393095`,
    method: "get",
    headers:{
      "X-Host": "mall.film-ticket.film.list",
    }
  })
}
首页内容

这里分两个部分,轮播图和电影列表,使用Swiper组件实现轮播图功能,Flex组件实现正在热映和即将上映得功能,配合Scroll组件实现内容滚动、点击返回顶部,并刷新请求接口

import { Films, getIndex, MoveData } from '../api/index'
import MovieList from '../components/MovieList'
​
@Component
struct Home {
  // 显示正在热映/即将上映
  @State flag: boolean = true
  // 分页参数
  @State page: number = 1
  // 正在热映的电影数据
  @State moveData: MoveData = {}
  @State len: number = 0
  scroll: Scroller = new Scroller()
​
  aboutToAppear(): void {
    this.getData()
  }
​
  // 请求电影列表功能单独封装
  getData() {
    getIndex(this.page).then((data:MoveData) => {
      // 替换图片地址
      data.films?.map((item: Films) => {
        item.poster = item.poster.replace("pic.maizuo.com", "static.maizuo.com/pc/v5")
        return item
      }) as Array<Films>
​
      if (this.page === 1) {
        this.moveData = data
      } else {
        (this.moveData as MoveData).films = (this.moveData as MoveData).films?.concat(data.films as Films[])
      }
    })
  }
​
  build() {
    Column() {
      Scroll(this.scroll) {
        Column() {
          // 轮播图
          Swiper() {
            Image($r('app.media.h5_01')).width('100%').height(180)
            Image($r('app.media.h5_02')).width('100%').height(180)
            Image($r('app.media.h5_03')).width('100%').height(180)
          }.autoPlay(true).loop(true)
​
          // 即将上映
          Flex({ justifyContent: FlexAlign.SpaceAround }) {
            Text('正在热映')
              .padding(10)
              .border({ width: { bottom: this.flag ? 3 : 0 } })
              .borderColor(Color.Red)
              .onClick(() => {
                this.flag = true
              })
            Text('即将上映').padding(10)
              .border({ width: { bottom: !this.flag ? 3 : 0 } })
              .borderColor(Color.Red)
              .onClick(() => {
                this.flag = false
              })
          }
​
          if (this.flag) {
            // Text('正在热映内容')
            MovieList({ moveData: this.moveData, page: this.page })
          } else {
            Text('即将上映内容')
          }
        }
      }.onScrollEdge((size) => {
        if (size === 2) {
          this.page++
          this.getData()
        }
      })
      // 滚动停止时触发
      .onScrollStop(() => {
        //  this.scroll.currentOffset() 返回当前的滚动偏移量 yOffset是y轴的偏移量,赋值给len
        this.len = this.scroll.currentOffset().yOffset
      })
​
      if (this.len > 250) {
        Text('顶部')
          .fontWeight(FontWeight.Bold)
          .width(40)
          .height(40)
          .backgroundColor(Color.White)
          .position({ x: '80%', y: '80%' })
          .onClick(() => {
            this.scroll.scrollTo({ yOffset: 0, xOffset: 0 })
          })
      }
    }
  }
}
​
export default Home

电影列表内容和样式问题

将电影列表作为一个组件,写一个MovieList.ets组件

import { Actors, Films, MoveData } from "../api"
​
@Component
struct MovieList {
  // 定义moveData数据
  @Prop moveData: MoveData
  @Link page: number
​
  filter_actors(arr: Actors[]) {
    // 如果arr为空
    if (!arr){
      return '暂无主演'
    }
    return arr.map((item: Actors) => {
      return item.name
    }).join(' ') // 通过数组转为字符串,用空格进行拼接
  }
​
  aboutToAppear(): void {
​
  }
​
  build() {
    Column() {
      GridRow({
        gutter: { y: 20 }
      }) {
        ForEach(this.moveData.films, (item: Films) => {
          GridCol({
            span: {
              sm: 12,
              md: 6,
              lg: 3
            }
          }) {
            Flex({ justifyContent: FlexAlign.SpaceBetween }) {
              Image(item.poster).width('20%').height(100).margin({ right: 10 })
              Column() {
                Text(item.name).fontSize(20).fontWeight(FontWeight.Bold).lineHeight(30)
                Text(this.filter_actors(item.actors))
                Row(){
                  Text(item.nation+'|').fontSize(15)
                  Text(`${item.runtime}`).fontSize(15)
                }
              }.alignItems(HorizontalAlign.Start).width('80%')
            }.padding(10)
          }
        })
      }
    }
  }
}
​
export default MovieList

影院列表

页面内容

Screenshot_2024-12-19T075505

这里主要分为两块内容,影院列表和城市选择,城市选择这里我们使用CustomDialogController自定义一个弹窗,并实现影院列表的刷新

import { Cinemas, Cities, getCinema, getCity } from '../api/cinema'
​
// 创建一个选择城市的弹窗
@CustomDialog
struct CityDialog {
  controller: CustomDialogController = new CustomDialogController({
    builder: CityDialog({})
  })
  @State cities: Cities[] = []
  // 定义父组件传入的通信函数
  updateCity: (item: Cities) => void = () => {
  }
​
  aboutToAppear(): void {
    getCity().then(data => {
      this.cities = data.cities
    })
  }
​
  build() {
    Scroll() {
      Column() {
        Text('请选择城市')
          .fontSize(20)
          .fontColor(Color.Red)
          .margin(5)
          .padding(5)
          .border({ width: { bottom: 1 } })
​
        ForEach(this.cities, (item: Cities) => {
          Text(item.name).width('100%').height(50).textAlign(TextAlign.Center)
            .onClick(() => {
              this.updateCity(item)
            })
        })
      }
    }
  }
}
​
​
@Component
struct Cinema {
  @State cityName: string = '上海'
  @State cityId: number = 310100
  @State cinemas: Cinemas[] = []
  // 弹窗属性
  cityDialog: CustomDialogController = new CustomDialogController({
    builder: CityDialog({
      updateCity: (city: Cities) => {
        this.updateCity(city)
      }
    })
  })
​
  updateCity(city: Cities) {
    console.info(JSON.stringify(city))
    this.cityName = city.name
    this.cityDialog.close()
    this.getData(city.cityId)
  }
​
  aboutToAppear(): void {
    this.getData(this.cityId)
  }
​
  getData(cityId: number) {
    getCinema(cityId).then(data => {
      console.info(JSON.stringify(data))
      this.cinemas = data.cinemas
    })
  }
​
  build() {
    Scroll() {
      Column() {
        Text(this.cityName).fontColor(Color.Red).fontSize(20).fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.cityDialog.open()
          })
​
        ForEach(this.cinemas, (item: Cinemas) => {
          Column({ space: 10 }) {
            Text(item.name).fontSize(18).width('100%')
            Text(item.address)
              .fontSize(15)
              .fontColor(Color.Gray)
              .width('100%')
          }.margin(10).justifyContent(FlexAlign.Start)
        })
      }
    }
  }
}
​
export default Cinema

影院api定义

创建cinema.etsapi组件

import request from '../utils/request'
​
​
// 获取影院列表
export const getCinema = (cityId: number) => {
  return request<string, CinemasData>({
    url: `/gateway?cityId=${cityId}&ticketFlag=1&k=2500238`,
    method: 'GET',
    headers: {
      "X-Host": "mall.film-ticket.cinema.list",
    }
  })
}
​
export const getCity = () => {
  return request<string,CitiesData>({
    url: `/gateway?k=9628046`,
    method: 'GET',
    headers: {
      "X-Host": "mall.film-ticket.city.list",
    }
  })
}
​
// 影院数据
export interface CinemasData {
  cinemas: Array<Cinemas>;
}
​
export interface Cinemas {
  cinemaId: number;
  name: string;
  address: string;
  longitude: number;
  latitude: number;
  gpsAddress: string;
  cityId: number;
  cityName: string;
  districtId: number;
  districtName: string;
  district: District;
  phone: string;
  telephones: Array<string>;
  isVisited: number;
  lowPrice: number;
  Distance: number;
  eTicketFlag: number;
  seatFlag: number;
}
​
export interface District {
  districtId: number;
  name: string;
}
​
// 城市列表数据
export interface CitiesData {
  cities: Array<Cities>;
}
​
export interface Cities {
  cityId: number;
  name: string;
  pinyin: string;
  isHot: number;
}

到这里,影院的基本功能已经完成,还有很多地方需要优化,你可以根据自己的需求,进行完善。

image-20241217181216815

总结

更多内容请参考文章

鸿蒙应用开发

一、鸿蒙应用开发快速体验
二、ArkTS 快速入门
三、HarmonyOS NEXT应用开发:ArkTS工程目录结构(Stage模型)

公众号搜“Harry技术”,关注我,带你看不一样的人间烟火!

源码地址:关注公众号“Harry技术”,回复“鸿蒙”获取源码地址。