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

简介: 本项目基于HarmonyOS NEXT 5.0开发了一款影院应用程序,主要实现了电影和影院信息的展示功能。应用包括首页、电影列表、影院列表等模块。首页包含轮播图与正在热映及即将上映的电影切换显示;电影列表模块通过API获取电影数据并以网格形式展示,用户可以查看电影详情;影院列表则允许用户选择城市后查看对应影院信息,并支持城市选择弹窗。此外,项目中还集成了Axios用于网络请求,并进行了二次封装以简化接口调用流程,同时添加了请求和响应拦截器来处理通用逻辑。整体代码结构清晰,使用了组件化开发方式,便于维护和扩展。该简介概括了提供的内容,但请注意实际开发中还需考虑UI优化、性能提升等方面的工作。

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


效果图

电影 影院

创建项目

tabs菜单实现

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

@Entry

@Component

struct Index {

 build() {

   Tabs({ barPosition: BarPosition.End }) {

     TabContent() {

       Text('电影页面')

     }.tabBar('电影')

     TabContent() {

       Text('影院页面')

     }.tabBar('影院')

   }

 }

}

第三方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

影院列表

页面内容

这里主要分为两块内容,影院列表和城市选择,城市选择这里我们使用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;

}

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


公众号搜“Harry技术”,关注我,带你看不一样的人间烟火!回复“鸿蒙”获取源码地址。

目录
相关文章
|
5天前
鸿蒙开发:V2版本装饰器@Once
@Once装饰器只能与@Param搭配使用,仅此一个组合,无其他使用方式,还有就是,必须在V2版本,也就是@ComponentV2装饰的自定义组件中,否则会报异常。
鸿蒙开发:V2版本装饰器@Once
|
3天前
|
安全 API
鸿蒙开发:实现AOP代码插桩能力
正确的运用AOP,可以提升代码的模块化、复用性、可维护性和灵活性,同时降低了耦合度,使系统更易于扩展和维护。
28 13
鸿蒙开发:实现AOP代码插桩能力
|
6天前
鸿蒙开发:熟知@BuilderParam装饰器
在实际的开发中,我们经常会遇到自定义组件的情况,比如通用的列表组件,选项卡组件等等,由于使用方的样式不一,子组件是动态变化的,针对这一情况,就不得不让使用方把子组件视图传递过来,如何来接收这个UI视图,这就是@BuilderParam装饰器的作用。
鸿蒙开发:熟知@BuilderParam装饰器
|
4天前
|
存储 文件存储 Android开发
仿第八区APP分发下载打包封装系统源码
该系统为仿第八区APP分发下载打包封装系统源码,支持安卓、iOS及EXE程序分发,自动判断并稳定安装。智能提取应用信息,自动生成PLIST文件和图标,提供合理的点数扣除机制。支持企业签名在线提交、专属下载页面生成、云端存储(阿里云、七牛云),并优化签名流程,支持中文包及合并分发,确保高效稳定的下载体验。 [点击查看源码](https://download.csdn.net/download/huayula/90463452)
41 22
|
5天前
|
安全
鸿蒙开发:校验构造传参装饰器@Require
@Require装饰器依赖ArkTs的类型检查,仅在编译阶段拦截类型错误和缺失参数,对于运行时才能确定的动态值,如从网络请求获取的数据,仍需在生命周期函数中进行二次校验。
41 18
|
1天前
鸿蒙开发:wrapBuilder来封装全局@Builder
首先第一点,在同一个UI组件内,同一个wrapBuilder只能初始化一次,第二点就是WrappedBuilder对象的builder属性方法只能在struct内部使用。
|
6天前
|
API 开发者
鸿蒙开发:V2版本装饰器之@Monitor装饰器
如果要实现@Monitor监听,其变量一定要被@Local、@Param、@Provider、@Consumer、@Computed装饰,未被修饰则无法被监听,还有,如果监听对象的变化,则不建议在一个类中对同一个属性进行多次@Monitor的监听,多次监听,只有最后一个定义的监听方法才会有效。
|
7天前
|
数据采集 人工智能 数据可视化
Harmony os next~鸿蒙系统开发
鸿蒙系统开发简介: 鸿蒙系统(HarmonyOS)是华为自主研发的面向全场景的分布式操作系统。它旨在为不同设备提供统一的操作平台,支持手机、平板、智能穿戴、车机等多种终端。鸿蒙系统具备微内核架构、低时延和高安全性等特性,通过分布式技术实现设备间无缝协同。开发者可以利用其丰富的API和工具链,构建跨平台应用,提升用户体验。
22 0
|
文字识别 安全 JavaScript
713页鸿蒙巨作!《鸿蒙HarmonyOS手机应用开发实战》简介
《鸿蒙HarmonyOS手机应用开发实战》一书由清华大学出版社出版,已经于2022年1月上市。拿到了样书,第一时间希望与读者朋友们分享下这本书里面的内容。
404 0
|
13天前
|
前端开发 JavaScript 开发工具
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
75 5
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡

热门文章

最新文章

  • 1
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
  • 2
    EMAS 性能分析全面适配HarmonyOS NEXT,开启原生应用性能优化新纪元
  • 3
    鸿蒙开发:了解@Builder装饰器
  • 4
    【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
  • 5
    【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
  • 6
    Harmony os next~HarmonyOS Ability与页面跳转开发详解
  • 7
    HarmonyOS使用系统图标
  • 8
    【02】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-准备工具安装-编译器DevEco Studio安装-arkts编程语言认识-编译器devco-鸿蒙SDK安装-模拟器环境调试-hyper虚拟化开启-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
  • 9
    鸿蒙H5离线包技术分享
  • 10
    一文轻松拿下HarmonyOS NEXT的自定义组件