提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0 及以上版本开发一款具有实用性和创新性的移动应用软件。
一、项目名称:黑马健康运动
黑马健康是一款流行的运动健身软件,旨在帮助用户保持健康和锻炼,致力于提供健身教学、跑步、骑行、交友及健身饮食指导一站式运动解决方案。健身、跑步、骑行,运动品类更丰富,个性化推荐更加精细。
二、应用运行过程3
1.食物列表页
1.1. 分析列表页面结构:从上到下列式布局,第一部分第一行是一个导航条,左侧是一个返回按钮,右侧是一个文本,第二部分是Tabs
当用户点击饮食记录页面某一个分组时,跳转到食物列表页面,创建一个新页面ItemIndex
1.2 头部导航 行式布局,封装到Builder
1.3 列表,创建一个组件,view/item/ItemList
运行截图:
代码如下(示例):
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import RecordService from '../service/RecordService'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NUmberKeyboard from '../view/item/NumberKeyboard'
import RecordItem from '../viewmodel/RecordItem'
import RecordType from '../viewmodel/RecordType'
@Entry
@Component
struct ItemIndex {
@State amount:number = 1
@State value: string = ''//按键记录
@State showPanel: boolean = false
@State item: RecordItem = null
@State type: RecordType = RecordTypes[0]
@State isFood: boolean = true
onPanelShow(item: RecordItem){//定义函数,当panel要展示时做一些事情
this.amount = 1
this.value = ''
this.item = item
this.showPanel = true//控制弹窗展示
}
onPageShow(){
//1.获取跳转时的参数
let params:any = router.getParams()
//2.获取点击的饮食记录类型
this.type = params.type
this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
}
build() {
Column() {
//1.头部导航
this.Header()
//2.列表
ItemList({showPanel: this.onPanelShow.bind(this), isFood: this.isFood})
.layoutWeight(1)
//3.底部面板
Panel(this.showPanel) { //使用时需要传入一个布尔类型的参数
//3.1.顶部日期
ItemPanelHeader()
//3.2.记录项卡片
if (this.item) {
ItemCard({ amount: this.amount, item: $item })
}
//3.3.数字键盘
NUmberKeyboard({ amount: $amount, value: $value })
//3.4.按钮
this.PanelButton()
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray'))
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
@Builder PanelButton(){
Row({space:CommonConstants.SPACE_6}){
Button('取消')
.width(120)
.backgroundColor($r('app.color.light_gray'))
.type(ButtonType.Normal)//样式
.borderRadius(6)//边角
.onClick(() => this.showPanel = false)
Button('提交')
.width(120)
.backgroundColor($r('app.color.primary_color'))
.type(ButtonType.Normal)//样式
.borderRadius(6)//边角
.onClick(() => {
//1.持久化保存
RecordService.insert(this.type.id, this.item.id, this.amount)
.then(() => {
//2.关闭弹窗
this.showPanel = false
})
})
}
.margin({top: 10})
}
@Builder Header(){
Row(){
Image($r('app.media.ic_public_back'))
.width(24)
.onClick(() => router.back())//点击事件 返回
Blank()
Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
2.食物列表—底部Panel
底部弹出面板,列式布局,去食物列表页设置点击事件,当点击时弹出来
2.1.顶部日期,新建组件view/item/ItemPanelHeader
文本内容Text(‘2024年1月25日 早晨’),居中
2.2记录项卡片,新建组件view/item/ItemCard
首先为图片信息(先写固定);然后名称信息 食物名称:Text(‘全麦吐司’)
下划线:Divider()
再 营养素信息NutrientInfo(需要传参)定义在Builder:label,value(数值)
最后 食物或运动数量:定义一个状态变量@Prop amount(用prop接收)
运行截图:
代码如下(示例):
itemPanelHeader.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemPanelHeader {
build() {
Row(){
Text('2024年1月25日 早餐')
.fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
.width(CommonConstants.THOUSANDTH_940)
}
}
itemCard.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemCard {
@Prop amount:number//用Prop接收,和键盘不在一个组件中
@Link item: RecordItem
build() {
Column({space:CommonConstants.SPACE_8}){
//1.图片信息
Image(this.item.image).width(150)
//2.名称
Row(){
Text(this.item.name).fontWeight(CommonConstants.FONT_WEIGHT_700)
}
.backgroundColor($r('app.color.lightest_primary_color'))
.padding({top: 5, bottom: 5, left:12, right: 12})
Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)//下划线
//3.营养素信息
Row({space:CommonConstants.SPACE_8}){
this.NutrientInfo({label: '热量(千卡)', value: this.item.calorie})
if (this.item.id < 10000){
this.NutrientInfo({label: '碳水(克)', value: this.item.carbon})
this.NutrientInfo({label: '蛋白质(克)', value: this.item.protein})
this.NutrientInfo({label: '脂肪(克)', value: this.item.fat})
}
}
Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)//下划线
//4.数量信息
Row(){
Column({space:CommonConstants.SPACE_4}){
Text(this.amount.toFixed(1)).fontSize(50).fontColor($r('app.color.primary_color'))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
Divider().color($r('app.color.primary_color'))
}
.width(150)
Text(this.item.unit)
.fontColor($r('app.color.light_gray'))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
}
}
}
@Builder NutrientInfo($$:{label:string,value:number}) { //营养素信息
Column({space:CommonConstants.SPACE_8}){
Text($$.label).fontSize(14).fontColor($r('app.color.light_gray'))
Text(($$.value * this.amount).toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
}
}
}
3.食物列表—数字键盘
view/item/NumberKeyboard
Grid组件:容器型组件,网格容器,由行和列分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
接口 Grid(scroller ?: Scroller)
定义value记录用户输入数字,增加点击事件:点击数字,拼接用户输入的内容;校验输入格式是否正确:小数点数量不能超过两个,小数不能超过两位;将字符串转为数值(有上限);保存
运行截图:
代码如下:
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NUmberKeyboard {
numbers: string [] = ['1', '2', '3', '4','5', '6', '7', '8', '9','0', '.']
@Link amount: number//双向,修改amount的值
@Link value: string
@Styles keyBoxStyle(){
.backgroundColor(Color.White)
.height(60)
.borderRadius(8)
}
build() {
Grid(){
ForEach(this.numbers, num => {
GridItem(){
Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickNumber(num))
})
GridItem(){
Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickDelete())
}
.width('100%')
.height(280)
.backgroundColor($r('app.color.index_page_background'))
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(8)//列间距
.rowsGap(8)//行间距
.padding(8)//内边距
.margin({top:10})//外边距
}
clickNumber(num: string){
//1.拼接用户输入的内容
let val = this.value + num//拼接
//2.校验输入格式是否正确
let firstIndex = val.indexOf('.')
let lastIndex = val.lastIndexOf('.')
if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length -2)){
//非法输入
return
}
//3.将字符串转为数值
let amount = this.parseFloat(val)
//4.保存
if(amount >= 999.9){
this.amount = 999.0
this.value = '999'
}else{
this.amount = amount
this.value = val
}
}
clickDelete(){//删除
if(this.value.length <= 0){
this.value = ''
this.amount = 0
return
}
this.value = this.value.substring(0, this.value.length - 1)
this.amount = this.parseFloat(this.value)
}
parseFloat(str: string){
if(!str){
return 0
}
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}
return parseFloat(str)
}
}
总结
1.食物列表页
分析排版
1.头部导航
2.列表