鸿蒙开发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.ets
和 Cinema.ets
组件,以及index.ets
api组件
首页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.ets
api组件
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技术”,关注我,带你看不一样的人间烟火!回复“鸿蒙”获取源码地址。