鸿蒙基础
- 1.自定义构建函数
- 1. 构建函数-[@Builder ](/Builder )
- 2. 构建函数-传参传递(单向)
- 3. 构建函数-传递参数(双向)
- 4. 构建函数-传递参数练习
- 5. 构建函数-[@BuilderParam ](/BuilderParam ) 传递UI
- 2.组件状态共享
- 1. 状态共享-父子单向
- 2. 状态共享-父子双向
- 3. 状态共享-后代组件
- 4. 状态共享-状态监听器
- 5. 综合案例 - 相册图片选取
- 1-页面布局,准备一个选择图片的按钮并展示
- 2-准备弹层,点击时展示弹层
- 3-添加点击事件,设置选中状态
- 4-点击确定同步给页面
- 5.关闭弹层
- 6. @Observed与[@ObjectLink ](/ObjectLink )
- 7. Next新增修饰符-Require-Track
- 3.应用状态
- 1. UIAbility内状态-LocalStorage
- 2. 应用状态-AppStorage
- 概述
- 3. 状态持久化-PersistentStorage
- 限制条件
- 4. 状态持久化-preferences首选项
- 5. 设备状态-Environment(了解)
- 4.网络管理(需要模拟器)
- 1. 应用权限
- 2. HTTP请求(需要模拟器)
- request接口开发步骤
- 5.今日案例-美团外卖
- 1. 目录结构-入口页面
- 2. 页面结构-底部组件
- 3. 顶部结构-MTTop(复制粘贴)
- 4. 页面结构-商品菜单和商品列表
- 5. 页面结构-购物车
- 6. 业务逻辑-渲染商品菜单和列表
- 7. 业务逻辑-封装新增加菜和减菜组件
- 8. 业务逻辑-加入购物车
- 9.加菜和减菜按钮加入购物车
- 10.清空购物车
- 11.底部内容汇总
- 美团案例完整代码
1.自定义构建函数
1. 构建函数-@Builder
:::info
如果你不想在直接抽象组件,ArkUI还提供了一种更轻量的UI元素复用机制 @Builder
,可以将重复使用的UI元素抽象成一个方法,在 build
方法里调用。称之为自定义构建函数
:::
只要使用Builder修饰符修饰的内容,都可以做成对应的UI描述
@Entry
@Component
struct BuilderCase {@Statelist: string[] = ["A", "B","C", "D", "E", "F"]@BuildergetItemBuilder (itemName: string) {Row() {Text(`${itemName}. 选项`)}.height(60).backgroundColor("#ffe0dede").borderRadius(8).width("100%").padding({left: 20,right: 20})}build() {Column({ space: 10 }) {ForEach(this.list, (item: string) => {this.getItemBuilder(item)})}.padding(20)}
}
- 用法- 使用@Builder修饰符修饰
@Entry
@Component
struct BuilderCase02 {build() {Row() {Column() {Row() {Row() {Text("异常时间")Text("2023-12-12")}.width('100%').justifyContent(FlexAlign.SpaceBetween).padding({left: 15,right: 15}).borderRadius(8).height(40).backgroundColor(Color.White)}.padding({left: 10,right: 10})}.width('100%')}.height('100%').backgroundColor('#ccc')}
}
:::info
假设你有N个这样的单个元素,但是重复的去写会浪费大量的代码,丧失代码的可读性,此时我们就可以使用
builder构建函数
:::
- 全局定义- @Builder function name () {}
@Builder
function getCellContent(leftTitle: string, rightValue: string) {Row() {Row() {Text(leftTitle)Text(rightValue)}.width('100%').justifyContent(FlexAlign.SpaceBetween).padding({left: 15,right: 15}).borderRadius(8).height(40).backgroundColor(Color.White)}.padding({left: 10,right: 10})}
- 在组件中使用
Column({ space: 10 }) {getCellContent("异常时间", "2023-12-12")getCellContent("异常位置", "回龙观")getCellContent("异常类型", "漏油")}.width('100%')
Next里面最大的变化就是全局的自定义Builder函数可以被引用,也就是你的一些公共的builder函数可以抽提出来,像使用函数那样来复用一些样式
2. 构建函数-传参传递(单向)
:::success
传的参数是按值的话,那个builder不具备响应式特征
传的参数是复杂数据, 而且复杂数据类型中的参数有响应式修饰符修饰,那么具备响应式特征
:::
@Entry
@Component
struct BuilderTransCase {@Statearea: string = "望京"@BuildergetCardItem (leftTitle: string, rightValue: string) {Row() {Text(leftTitle)Text(rightValue)}.justifyContent(FlexAlign.SpaceBetween).width('100%').height(50).borderRadius(8).backgroundColor(Color.White).padding({left: 20,right: 20})}@BuildergetCardItemObj (item: ICardItem) {Row() {Text(item.leftTitle)Text(item.rightValue)}.justifyContent(FlexAlign.SpaceBetween).width('100%').height(50).borderRadius(8).backgroundColor(Color.White).padding({left: 20,right: 20})}build() {Column({ space: 20 }) {Text(this.area)this.getCardItem("异常位置", this.area) // 按值传递不具备响应式this.getCardItemObj({ leftTitle: '异常位置', rightValue: this.area }) // 按照引用传递可以实现数据更新this.getCardItem("异常时间", "2023-12-12")this.getCardItem("异常类型", "漏油")Button("上报位置").onClick(() => {this.area = "厦门"})}.justifyContent(FlexAlign.Center).width('100%').height('100%').padding(20).backgroundColor(Color.Gray)}
}
interface ICardItem {leftTitle: stringrightValue: string
}
:::info
自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:
- 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
- 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link。
- @Builder内UI语法遵循UI语法规则。
:::
我们发现上一个案例,使用了string这种基础数据类型,即使它属于用State修饰的变量,也不会引起UI的变化
- 按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供**$$**作为按引用传递参数的范式。
ABuilder( $$ : 类型 );
:::info
- 也就是我们需要在builder中传入一个对象, 该对象使用$$(可使用其他字符)的符号来修饰,此时数据具备响应式了
:::
class CellParams {leftTitle: string = ""rightValue: string = ""
}
@Builder
function getCellContent($$: CellParams) {Row() {Row() {Text($$.leftTitle)Text($$.rightValue)}.width('100%').justifyContent(FlexAlign.SpaceBetween).padding({left: 15,right: 15}).borderRadius(8).height(40).backgroundColor(Color.White)}.padding({left: 10,right: 10})}
- 传值
this.getCellContent({ leftTitle: '异常位置', rightValue: this.formData.location })
this.getCellContent({ leftTitle: '异常时间', rightValue: this.formData.time })
this.getCellContent({ leftTitle: '异常类型', rightValue: this.formData.type })
:::info
同样的,全局Builder同样支持这种用法
:::
@Entry
@Component
struct BuilderCase {@State formData: CardClass = {time: "2023-12-12",location: '回龙观',type: '漏油'}@BuildergetCellContent($$: CellParams) {Row() {Row() {Text($$.leftTitle)Text($$.rightValue)}.width('100%').justifyContent(FlexAlign.SpaceBetween).padding({left: 15,right: 15}).borderRadius(8).height(40).backgroundColor(Color.White)}.padding({left: 10,right: 10})}build() {Row() {Column() {Column({ space: 10 }) {this.getCellContent({ leftTitle: '异常时间', rightValue: this.formData.time })this.getCellContent({ leftTitle: '异常位置', rightValue: this.formData.location })this.getCellContent({ leftTitle: '异常类型', rightValue: this.formData.type })}.width('100%')Button("修改数据").onClick(() => {this.formData.location = "望京"})}.width('100%')}.height('100%').backgroundColor('#ccc')}
}class CardClass {time: string = ""location: string = ""type: string = ""
}
class CellParams {leftTitle: string = ""rightValue: string = ""
}
@Builder
function getCellContent($$: CellParams ) {Row() {Row() {Text($$.leftTitle)Text($$.rightValue)}.width('100%').justifyContent(FlexAlign.SpaceBetween).padding({left: 15,right: 15}).borderRadius(8).height(40).backgroundColor(Color.White)}.padding({left: 10,right: 10})}
:::info
- 使用
@Builder
复用逻辑的时候,支持传参可以更灵活的渲染UI - 参数可以使用
状态数据
,不过建议通过对象的方式传入@Builder
:::
3. 构建函数-传递参数(双向)
:::info
之前我们做过这样一个表单,$$不能绑定整个对象,有没有什么解决办法呢?
:::
新建一个的builder -FormBuilder
@Entry
@Component
struct BuilderCase03 {@StateformData: FormData = {name: '张三',age: '18',bank: '中国银行',money: '999'}@BuilderFormBuilder(formData:FormData) {Column({ space: 20 }) {TextInput({ placeholder: '请输入姓名',text:formData.name})TextInput({ placeholder: '请输入年龄',text:formData.age})TextInput({ placeholder: '请输入银行',text:formData.bank })TextInput({ placeholder: '请输入银行卡余额',text:formData.money})}.width('100%')}build() {Row() {Column({space:20}) {this.FormBuilder(this.formData)Row({space:20}){Button('重置').onClick(()=>{this.formData = {name: '',age: '',bank: '',money: ''}})Button('注册')}}.width('100%').padding(20)}.height('100%')}
}interface FormData {name: stringage: stringbank: stringmoney: string
}
:::danger
在页面上尝试使用builder,传入需要展示的数据,点击重置时,会发现UI并不能更新!
因为传递参数必须是{ params1:数据 }格式,params1才是响应式的
:::
改造传值,发现此时响应式了
@Entry
@Component
struct BuilderCase03 {@StateformData: FormData = {name: '张三',age: '18',bank: '中国银行',money: '999'}@BuilderFormBuilder(formData:FormDataInfo) {Column({ space: 20 }) {TextInput({ placeholder: '请输入姓名',text:formData.data.name})TextInput({ placeholder: '请输入年龄',text:formData.data.age})TextInput({ placeholder: '请输入银行',text:formData.data.bank })TextInput({ placeholder: '请输入银行卡余额',text:formData.data.money})}.width('100%')}build() {Row() {Column({space:20}) {this.FormBuilder({data:this.formData})Row({space:20}){Button('重置').onClick(()=>{this.formData = {name: '',age: '',bank: '',money: ''}})Button('注册')}}.width('100%').padding(20)}.height('100%')}
}interface FormData {name: stringage: stringbank: stringmoney: string
}
interface FormDataInfo{data:FormData
}
改造成双向绑定,builder内部改变时也能通知外层
@Entry
@Component
struct BuilderCase03 {@StateformData: FormData = {name: '张三',age: '18',bank: '中国银行',money: '999'}@BuilderFormBuilder($$:FormDataInfo) {Column({ space: 20 }) {TextInput({ placeholder: '请输入姓名',text:$$.data.name})TextInput({ placeholder: '请输入年龄',text:$$.data.age})TextInput({ placeholder: '请输入银行',text:$$.data.bank })TextInput({ placeholder: '请输入银行卡余额',text:$$.data.money})}.width('100%')}build() {Row() {Column({space:20}) {Text(JSON.stringify(this.formData))this.FormBuilder({data:this.formData})Row({space:20}){Button('重置').onClick(()=>{this.formData = {name: '',age: '',bank: '',money: ''}})Button('注册')}}.width('100%').padding(20)}.height('100%')}
}interface FormData {name: stringage: stringbank: stringmoney: string
}
interface FormDataInfo{data:FormData
}
4. 构建函数-传递参数练习
上图中,是tabs组件中的tabbar属性,支持自定义builder,意味着我们可以定制它的样式
- 准备八个图标放到资源目录下
图片.zip
- 新建一个页面, 声明一个interface并建立四个数据的状态
interface TabInterface {name: stringicon: ResourceStrselectIcon: ResourceStrtitle: string
}
- 循环生成对应的TabContent
@Entry
@Component
struct TabBarBuilderCase {@Statelist: TabInterface[] = [{icon: $r("app.media.ic_public_message"),selectIcon: $r('app.media.ic_public_message_filled'),name: 'wechat',title: '微信',}, {icon: $r('app.media.ic_public_contacts_group'),selectIcon: $r('app.media.ic_public_contacts_group_filled'),name: 'connect',title: '联系人',}, {icon: $r('app.media.ic_gallery_discover'),selectIcon: $r('app.media.ic_gallery_discover_filled'),name: 'discover',title: '发现',}, {icon: $r('app.media.ic_public_contacts'),selectIcon: $r('app.media.ic_public_contacts_filled'),name: 'my',title: '我的',}]build() {Tabs() {ForEach(this.list, (item: TabInterface) => {TabContent() {Text(item.title)}.tabBar(item.title)})}.barPosition(BarPosition.End)}
}
interface TabInterface {name: stringicon: ResourceStrselectIcon: ResourceStrtitle: string
}
此时,如果我们想实现图中对应的效果,就需要使用自定义Builder来做,因为TabContent的tabBar属性支持CustomBuilder类型,CustomBuilder类型就是builder修饰的函数
- 在当前组件中声明一个builder函数
@BuilderCommonTabBar (item: TabInterface) {Column () {Image(item.icon).width(20).height(20)Text(item.title).fontSize(12).fontColor("#1AAD19").margin({top: 5})}}
- 定义一个数据来绑定当前tabs的激活索引
@StatecurrentIndex: number = 0
- 根据当前激活索引设置不同的颜色的图标
@BuilderCommonTabBar (item: TabInterface) {Column () {Image(item.name === this.list[this.currentIndex].name ? item.selectIcon : item.icon).width(20).height(20)Text(item.title).fontSize(12).fontColor(item.name === this.list[this.currentIndex].name ? "#1AAD19": "#2A2929").margin({top: 5})}}
5. 构建函数-@BuilderParam 传递UI
:::success
插槽-Vue-Slot React-RenderProps
-
把UI结构体的函数(Builder修饰的函数)当成参数传入到组件中,让组件放入固定的位置去渲染
-
子组件接收传入的函数的修饰符/装饰器叫做BuilderParam
:::
:::info -
Component可以抽提组件
-
Builder可以实现轻量级的UI复用
完善了吗? 其实还不算,比如下面这个例子
:::
- BuilderParam的基本使用 - 如何实现定制化Header?
:::success
使用BuilderParam的步骤
- 前提:需要出现父子组件的关系
- 前提:BuilderParam应出现在子组件中
-
- 子组件声明 @BuilderParam getConent: () => void
-
- BuilderParam的参数可以不给初始值,如果给了初始值, 就是没有内容的默认内容
-
- 父组件传入的时候,它需要用builder修饰的函数又或者是 一个箭头函数中包裹着
-
- 调用builder函数的逻辑
:::
- 调用builder函数的逻辑
@Entry
@Component
struct BuildParamCase {// 声明的一个要传递的内容!@BuilderLeftBuilder() {Image($r('sys.media.ohos_ic_compnent_titlebar_back')).width(20)}@BuilderCenterBuilder(){Row(){Text('最新推荐')Text('🔥')}.layoutWeight(1).justifyContent(FlexAlign.Center)}@BuilderRightBuilder(){Image($r('sys.media.ohos_ic_public_scan')).width(20)}build() {Row() {Column() {// Header容器MyBuilderParamChild()}.width('100%')}.height('100%')}
}@Component
struct MyBuilderParamChild {@BuilderdefaultLeftParam(){Text('返回')}@BuilderParamleftContent:()=>void = this.defaultLeftParam@BuilderdefaultCenterParam(){Text('首页').layoutWeight(1).textAlign(TextAlign.Center)}@BuilderParamcenterContent:()=>void = this.defaultCenterParam@BuilderdefaultRightParam(){Text('确定')}@BuilderParamrightContent:()=>void = this.defaultRightParambuild() {Row() {// 左this.leftContent()// 中this.centerContent()// 右this.rightContent()}.width('100%').backgroundColor(Color.Pink).padding(20)}
}
-
builderParam传值
:::success -
当我们使用builderParam的时候,又需要拿到渲染的数据该怎么办?
场景: 当我们有一个列表组件,该组件的列表格式是固定的,但是每个选项的内容由传入的结构决定怎么搞?
- 列表组件可以渲染数据-但是每一个选项的UI结构由使用者决定
- 拷贝图片到assets
图片.zip
:::
- 封装一个列表的组件,可以渲染传入的数组
@Preview
@Component
// 列表组件
struct HmList {@Statelist: object[] = [] // 不知道传入的是什么类型 统一认为是object@BuilderParamrenderItem: (obj: object) => voidbuild() {// Grid List WaterFlow// 渲染数组List ({ space: 10 }) {ForEach(this.list, (item: object) => {ListItem() {// 自定义的结构if(this.renderItem) {this.renderItem(item)// 函数中的this始终指向调用者}}})}.padding(20)}
}
export { HmList }// WaterFlow FlowItem Grid GirdItem List ListItem
- 父组件调用
import { BuilderParamChild } from './components/BuilderParamChild'
@Entry
@Component
struct BuilderParamCase {@Statelist: GoodItem[] = [{"id": 1,"goods_name": "班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣","goods_img": "assets/1.webp","goods_price": 108,"goods_count": 1,},{"id": 2,"goods_name": "嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮","goods_img": "assets/2.webp","goods_price": 129,"goods_count": 1,},{"id": 3,"goods_name": "思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套","goods_img": "assets/3.webp","goods_price": 198,"goods_count": 1,},{"id": 4,"goods_name": "思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套","goods_img": "assets/4.webp","goods_price": 99,"goods_count": 1,},{"id": 5,"goods_name": "幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮","goods_img": "assets/5.webp","goods_price": 156,"goods_count": 1,},{"id": 6,"goods_name": "ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女","goods_img": "assets/6.webp","goods_price": 142.8,"goods_count": 1,},{"id": 7,"goods_name": "幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套","goods_img": "assets/7.webp","goods_price": 219,"goods_count": 2,},{"id": 8,"goods_name": "依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套","goods_img": "assets/8.webp","goods_price": 178,"goods_count": 1,},{"id": 9,"goods_name": "芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬","goods_img": "assets/9.webp","goods_price": 128,"goods_count": 1,},{"id": 10,"goods_name": "Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫","goods_img": "assets/10.webp","goods_price": 153,"goods_count": 1,}]@BuilderrenderItem (item: GoodItem) {Row({ space: 10 }) {Image(item.goods_img).borderRadius(8).width(120).height(200)Column() {Text(item.goods_name).fontWeight(FontWeight.Bold)Text("¥ "+item.goods_price.toString()).fontColor(Color.Red).fontWeight(FontWeight.Bold)}.padding({top: 5,bottom: 5}).alignItems(HorizontalAlign.Start).justifyContent(FlexAlign.SpaceBetween).height(200).layoutWeight(1)}.width('100%')}build() {Row() {Column() {BuilderParamChild({list:this.list,builderItem:(item:object)=>{this.renderItem(item as GoodItem)}})}.width('100%')}.height('100%')}
}
interface GoodItem {goods_name: stringgoods_price: numbergoods_img: stringgoods_count: numberid: number
}
:::success
1.BuildParam可以没有默认值,但是调用的时候最好判断一下
2.BuildParam可以声明参数,调用的时候传递的参数最后回传给父组件传递的Builder
:::
-
尾随闭包
:::success
Column () { } 中大括号就是尾随闭包的写法
:::
:::info
当我们的组件只有一个BuilderParam的时候,此时可以使用尾随闭包的语法 也就是像我们原来使用Column或者Row组件时一样,直接在大括号中传入
::: -
父组件使用尾随闭包传入
神领物流中有很多这样的Panel栏
我们用尾随闭包来封装这样的组件,理解一下BuildParam的使用
首先封装一个Panel组件
@Component
struct PanelComp {@StateleftText:string = '左侧标题'@BuilderParamrightContent:()=>void = this.defaultContent@BuilderdefaultContent(){Row({space:16}){Checkbox().select(true).shape(CheckBoxShape.CIRCLE)Text('是')}}build() {Row(){Text(this.leftText)this.rightContent()}.width('100%').padding(20).backgroundColor('#ccc').borderRadius(8).justifyContent(FlexAlign.SpaceBetween)}
}export { PanelComp }
- 接下来父组件使用,并分别传递左侧文字和右侧的结构
import { PanelComp } from './components/PanelComp'@Entry
@Component
struct BuilderParamClosure {@StateisOn:boolean = falsebuild() {Row() {Column() {Text(''+this.isOn)PanelComp({// 数据leftText:'低电量模式'}){// 结构Toggle({type:ToggleType.Switch,isOn:$$this.isOn})}}.width('100%').padding(20)}.height('100%')}
}
:::success
只有一个BuilderParam且不需要传参的时候,可以使用尾随闭包
注意:尾随闭包用空大括号就代表传递空内容,会替代默认内容
:::
2.组件状态共享
State是当前组件的状态, 用State修饰的数据变化会驱动UI的更新(只有第一层)
父传子的时候,子组件定义变量的时候,如果没有任何的修饰符,那么该值只会在第一次渲染时生效
:::info
接下来,我们学习组件状态传递
我们知道 State是当前组件的状态,它的数据变化可以驱动UI,但是子组件接收的数据没办法更新,我们需要
更多的修饰符来帮助我们完成数据的响应式传递
:::
1. 状态共享-父子单向
比如我们希望实现这样一个效果,粉色区域是一个子组件,父组件有一个值
如何让父子同时可以进行修改,且保持同步呢?
- 先写页面
@Entry
@Component
struct ComponentQuestionCase {@State money: number = 999999;build() {Column() {Text('father:' + this.money)Button('存100块')CompQsChild()}.padding(20).width('100%').height('100%')}
}@Component
struct CompQsChild {@State money: number = 0build() {Column() {Text('child:' + this.money)Button('花100块')}.padding(20).backgroundColor(Color.Pink)}
}
- 传递值给子组件,绑定点击事件修改money,此时会发现,父子组件各改各的
@Entry
@Component
struct PropCase {@Statemoney: number = 999999build() {Column() {Text('father:' + this.money)Button('存100块').onClick(() => {this.money += 100})// ---------// 父给子传值,默认只生效一次PropChild({money:this.money})}.width('100%')}
}@Component
struct PropChild {// @State// 用于和传入的值保持同步(单向),如果传入的值改变也会引起UI的更新// 自身可以进行修改,但是不推荐// 因为父组件再次改变会覆盖自己的内容@Propmoney: number = 0build() {Column() {Text('father:' + this.money)Button('花100块').onClick(() => {this.money -= 100})}.padding(20).backgroundColor(Color.Pink)}
}
此时,我们就可以学习一个新的修饰符@Prop
,被@Prop修饰过的数据可以自动监听传递的值,同步保持更新,修改子组件的money修饰符为@Prop
,此时就能实现父组件改变,子组件同步更新
:::success
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
:::
@Entry
@Component
struct ComponentQuestionCase {@State money: number = 999999;build() {Column() {Text('father:' + this.money)Button('存100块').onClick(()=>{this.money+=100})CompQsChild({money:this.money})}.padding(20).width('100%').height('100%')}
}@Component
struct CompQsChild {@Prop money: number = 0build() {Column() {Text('child:' + this.money)Button('花100块').onClick(()=>{this.money-=100})}.padding(20).backgroundColor(Color.Pink)}
}
:::info
Prop 支持类型和State修饰符基本一致,并且Prop可以给初始值,也可以不给
注意:子组件仍然可以改自己,更新UI,但不会通知父组件(单向),父组件改变后会覆盖子组件自己的值
:::
2. 状态共享-父子双向
-
Prop修饰符- 父组件数据更新-让子组件更新- 子组件更新-父组件不为所动
:::info
Prop是单向的,而Link修饰符则是双向的数据传递,只要使用Link修饰了传递过来的数据,这个时候就是双向同步了
注意点:
Link修饰符不允许给初始值
::: -
将刚刚的案例改造成双向的
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。
@Entry
@Component
struct ComponentQuestionCase {@Statemoney: number = 999999;build() {Column() {Text('father:' + this.money)Button('存100块').onClick(()=>{this.money+=100})CompQsChild({money:this.money})}.padding(20).width('100%').height('100%')}
}@Component
struct CompQsChild {// 各玩各的// @State money: number = 0// 听爸爸的话// @Prop money: number// 团结一心@Link money: numberbuild() {Column() {Text('child:' + this.money)Button('花100块').onClick(()=>{this.money-=100})}.padding(20).backgroundColor(Color.Pink)}
}
:::danger
Link修饰符的要求- 你的父组件传值时传的必须是Link或者State修饰的数据
:::
下面这段代码的问题出现在哪里?
@Entry
@Component
struct ComponentQuestionCase {@StatedataInfo: MoneyInfo = {money: 99999,bank: '中国银行'}build() {Column() {Text('father:' + this.dataInfo.money)Button('存100块').onClick(() => {this.dataInfo.money += 100})CompQsChild({ dataInfo: this.dataInfo })}.padding(20).width('100%').height('100%')}
}@Component
struct CompQsChild {// 各玩各的// @State money: number = 0// 听爸爸的话// @Prop money: number// 团结一心@Link dataInfo: MoneyInfobuild() {Column() {Text('child:' + this.dataInfo.money)Button('花100块').onClick(() => {this.dataInfo.money -= 100})ChildChild({ money: this.dataInfo.money })}.padding(20).backgroundColor(Color.Pink)}
}@Component
struct ChildChild {// 各玩各的// @State money: number = 0// 听爸爸的话// @Prop money: number// 团结一心@Link money: number// @Link dataInfo: MoneyInfobuild() {Column() {Text('ChildChild:' + this.money)Button('花100块').onClick(() => {this.money -= 100})}.padding(20).backgroundColor(Color.Red)}
}interface MoneyInfo {money: numberbank: string
}
3. 状态共享-后代组件
:::info
如果我们的组件层级特别多,ArkTS支持跨组件传递状态数据来实现双向同步@Provide和 @Consume
这特别像Vue中的依赖注入
:::
- 改造刚刚的案例,不再层层传递,仍然可以实现效果
@Entry
@Component
struct ComponentQuestionCase1 {@ProvidedataInfo: MoneyInfo1 = {money: 99999,bank: '中国银行'}build() {Column() {Text('father:' + this.dataInfo.money)Button('存100块').onClick(() => {this.dataInfo.money += 100})CompQsChild1()}.padding(20).width('100%').height('100%')}
}@Component
struct CompQsChild1 {// 各玩各的// @State money: number = 0// 听爸爸的话// @Prop money: number// 团结一心@ConsumedataInfo: MoneyInfo1build() {Column() {Text('child:' + this.dataInfo.money)Button('花100块').onClick(() => {this.dataInfo.money -= 100})ChildChild1()}.padding(20).backgroundColor(Color.Pink)}
}@Component
struct ChildChild1 {// 各玩各的// @State money: number = 0// 听爸爸的话// @Prop money: number// 团结一心@ConsumedataInfo: MoneyInfo1// @Link dataInfo: MoneyInfobuild() {Column() {Text('ChildChild:' + this.dataInfo.money)Button('花100块').onClick(() => {this.dataInfo.money -= 100})}.padding(20).backgroundColor(Color.Red)}
}interface MoneyInfo1 {money: numberbank: string
}
:::info
注意: 在不指定Provide名称的情况下,你需要使用相同的名字来定义和接收数据
:::
如果组件已有该命名变量,可以起别名进行提供/接收
:::info
1.提供起别名
@Provide(‘newName’) 重起一个别名叫newName,后代就只能接收newName
:::
:::info
2.接收起别名
@Consume(‘ProvideName’)
newName:类型
提供的时候没有起别名,接收的时候重起一个别名叫newName
:::
:::info
3.同理,提供的时候起了别名,接收的时候也需要起别名该怎么做呢?
:::
:::danger
注意:@Consume代表数据是接收的,不能有默认值
不要想太多,ArkTS所有内容都不支持深层数据更新 UI渲染
:::
- 后代传值-案例
:::success
黑马云音乐-播放状态传递
:::
:::info
各个页面共享同一个播放状态,而且可以互相控制,如果传递来传递去会非常的麻烦,但是他们都是Tabs组件内的,我们在index页面提供一个状态,在各个组件接收即可
:::
借用之前的TabbarCase进行改造
- 创建两个子组件,一个是播放控制的子组件,一个是背景播放的子组件
背景播放组件
@Component
struct BackPlayComp {@ConsumeisPlay:booleanbuild() {Row(){Row({space:20}){Image($r('app.media.b')).width(40)Text('耍猴的 - 二手月季')}Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play')).width(20).aspectRatio(1).onClick(()=>{this.isPlay=!this.isPlay})}.width('100%').padding({left:20,right:20,top:6,bottom:6}).backgroundColor(Color.Grey).justifyContent(FlexAlign.SpaceBetween)}
}
export {BackPlayComp}
播放控制组件
@Component
struct PlayControlComp {@ConsumeisPlay:booleanbuild() {Row({space:20}){Image($r('sys.media.ohos_ic_public_play_last')).width(20).aspectRatio(1)Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play')).width(20).aspectRatio(1).onClick(()=>{this.isPlay=!this.isPlay})Image($r('sys.media.ohos_ic_public_play_next')).width(20).aspectRatio(1)}.width('100%').padding(20).backgroundColor(Color.Pink).justifyContent(FlexAlign.Center)}
}
export {PlayControlComp}
首页引用
import { BackPlayComp } from './components/ConnectComp'
import { PlayControlComp } from './components/WechatComp'@Entry
@Component
struct TabBarCase {@Statelist: TabInterface[] = [{icon: $r("app.media.ic_public_message"),selectIcon: $r('app.media.ic_public_message_filled'),name: 'wechat',title: '微信',},{icon: $r('app.media.ic_public_contacts_group'),selectIcon: $r('app.media.ic_public_contacts_group_filled'),name: 'connect',title: '联系人',}, {icon: $r('app.media.ic_gallery_discover'),selectIcon: $r('app.media.ic_gallery_discover_filled'),name: 'discover',title: '发现',}, {icon: $r('app.media.ic_public_contacts'),selectIcon: $r('app.media.ic_public_contacts_filled'),name: 'my',title: '我的',}]// 组件内的@StatecurrenIndex: number = 0@ProvideisPlay:boolean = false@BuildertabBarItem(item: TabInterface) {Column({ space: 6 }) {Image(item.name === this.list[this.currenIndex].name ? item.selectIcon : item.icon).width(20)Text(item.title).fontSize(12).fontColor(item.name === this.list[this.currenIndex].name ? '#1caa20' : '#000')}}build() {Row() {Stack({alignContent:Alignment.Bottom}) {Tabs({ index: $$this.currenIndex }) {ForEach(this.list, (item: TabInterface) => {TabContent() {// 切换展示的内容放这里// Text(item.title)if (item.name === 'wechat') {PlayControlComp()} else if (item.name === 'connect') {PlayControlComp()}}.tabBar(this.tabBarItem(item))})}.barPosition(BarPosition.End)BackPlayComp().translate({y:-60})}.width('100%')}.height('100%')}
}interface TabInterface {name: stringicon: ResourceStrselectIcon: ResourceStrtitle: string
}
:::info
此时,各个页面共享了播放状态,只要任意地方进行改变,都能保持同步
:::
4. 状态共享-状态监听器
如果开发者需要关注某个状态变量的值是否改变,可以使用
@Watch
为状态变量设置回调函数。
Watch(“回调函数名”)中的回调必须在组件中声明,该函数接收一个参数,参数为修改的属性名
注意:Watch修饰符要写在 State Prop Link Provide的修饰符下面,否则会有问题
- 在第一次初始化的时候,@Watch装饰的方法不会被调用
前面我们做了一个‘抖音’文字抖动效果,如果希望播放的时候希望文字抖动,暂停的时候文字暂停,如下
改造我们的播放控制组件,添加层叠的文字,并将写死的x,y方向的值设置为变量
@Component
struct PlayControlComp {@StateshakenX:number = 0@StateshakenY:number = 0@ConsumeisPlay:booleanbuild() {Column(){Stack(){Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ff2d83b3').translate({x:this.shakenX,y:this.shakenY}).zIndex(1)Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ffe31fa9').translate({x:this.shakenY,y:this.shakenX}).zIndex(2)Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ff030000').translate({x:0,y:0}).zIndex(3)}Row({space:20}){Image($r('sys.media.ohos_ic_public_play_last')).width(20).aspectRatio(1)Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play')).width(20).aspectRatio(1).onClick(()=>{this.isPlay=!this.isPlay})Image($r('sys.media.ohos_ic_public_play_next')).width(20).aspectRatio(1)}.width('100%').padding(20).backgroundColor(Color.Pink).justifyContent(FlexAlign.Center)}}
}
export {PlayControlComp}
:::info
此时我们就可以用@Watch需要观察isPlay的属性了,只要isPlay变了就开始抖动文字
:::
@Consume@Watch('update') //watch写在要监听的数据下方isPlay:boolean//监听的数据改变时会触发这个函数update(){if(this.isPlay){this.timer = setInterval(()=>{this.shakenX = 2 - Math.random()*4this.shakenY = 2 - Math.random()*4},100)}else{clearInterval(this.timer)this.shakenX = 0this.shakenY = 0}}
- 完整代码
@Component
struct PlayControlComp {@StateshakenX:number = 0@StateshakenY:number = 0timer:number = -1@Consume@Watch('update')isPlay:booleanupdate(){if(this.isPlay){this.timer = setInterval(()=>{this.shakenX = 2 - Math.random()*4this.shakenY = 2 - Math.random()*4},100)}else{clearInterval(this.timer)this.shakenX = 0this.shakenY = 0}}build() {Column(){Stack(){Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ff2d83b3').translate({x:this.shakenX,y:this.shakenY}).zIndex(1)Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ffe31fa9').translate({x:this.shakenY,y:this.shakenX}).zIndex(2)Text('抖音').fontSize(50).fontWeight(FontWeight.Bold).fontColor('#ff030000').translate({x:0,y:0}).zIndex(3)}Row({space:20}){Image($r('sys.media.ohos_ic_public_play_last')).width(20).aspectRatio(1)Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play')).width(20).aspectRatio(1).onClick(()=>{this.isPlay=!this.isPlay})Image($r('sys.media.ohos_ic_public_play_next')).width(20).aspectRatio(1)}.width('100%').padding(20).backgroundColor(Color.Pink).justifyContent(FlexAlign.Center)}}
}
export {PlayControlComp}
:::info
简单点说@Watch可以用于主动检测数据变化,需要绑定一个函数,当数据变化时会触发这个函数
:::
5. 综合案例 - 相册图片选取
基于我们已经学习过的单向、双向、后台、状态监听,我们来做一个综合案例,感受一下有了新的修饰符加成,再进行复杂的案例传值时,是否还想之前的知乎一样绕人
:::info
分析:
1.准备一个用于选择图片的按钮,点击展示弹层
2.准备弹层,渲染所有图片
3.图片添加点击事件,点击时检测选中数量后添加选中状态
4.点击确定,将选中图片同步给页面并关闭弹层
5.取消时,关闭弹层
:::
1-页面布局,准备一个选择图片的按钮并展示
- 选择图片Builder
@Builder
export function SelectImageIcon() {Row() {Image($r('sys.media.ohos_ic_public_add')).width('100%').height('100%').fillColor(Color.Gray)}.width('100%').height('100%').padding(20).backgroundColor('#f5f7f8').border({width: 1,color: Color.Gray,style: BorderStyle.Dashed})
}
- 页面布局,使用Builder
import { SelectImageIcon } from './builders/SelectBuilder'
@Entry
@Component
struct ImageSelectCase {build() {Grid() {GridItem() {SelectImageIcon()}.aspectRatio(1)}.padding(20).width('100%').height('100%').rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr')}
}
2-准备弹层,点击时展示弹层
:::info
弹层的使用分为3步
1.声明弹层
2.注册弹层
3.使用弹层
:::
- 弹层组件
// 1.声明一个弹层
@CustomDialog
struct MyDialog {controller:CustomDialogControllerbuild() {Column() {Text('默认内容')}.width('100%').padding(20).backgroundColor('#fff')}
}export { MyDialog }
- 使用弹层
import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
@Entry
@Component
struct ImageSelectCase {// 2.注册弹层myDialogController:CustomDialogController = new CustomDialogController({builder:MyDialog()})build() {Grid() {GridItem() {SelectImageIcon()}.aspectRatio(1).onClick(()=>{// 3.使用弹层this.myDialogController.open()})}.padding(20).width('100%').height('100%').rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr')}
}
:::info
理想很丰满,显示很骨感,不论如何使用弹层,下方都会有一个空白边
这种下半屏或者全屏的展示不适合用CustomDialog,这里只做学习即可
我们看到的效果,更适合用通用属性bindSheet,半模态转场
需要传入三个参数:
第一个,是否显示模态框
第二个,模态框自定义构建函数
第三个(非必传),模态框的配置项
所以,我们进行改造
:::
import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
import { SelectImage } from './components/SelectImage'@Entry
@Component
struct ImageSelectCase {// 2.注册弹层// myDialogController:CustomDialogController = new CustomDialogController({// builder:MyDialog(),// customStyle:true// })// 下方有留白,取消不了,换一种方案@StateshowDialog: boolean = false@StateimageList: ResourceStr[] = ["assets/1.webp","assets/2.webp","assets/3.webp","assets/4.webp","assets/5.webp","assets/6.webp","assets/7.webp","assets/8.webp","assets/9.webp","assets/10.webp"]@StateselectList: ResourceStr[] = []@StateselectedList: ResourceStr[] = []@BuilderImageListBuilder() {// 大坑:最外层必须得是容器组件Column(){SelectImage({imageList:this.imageList})}}build() {Grid() {GridItem() {SelectImageIcon()}.aspectRatio(1).onClick(() => {// 3.使用弹层// this.myDialogController.open()this.showDialog = true})}.padding(20).width('100%').height('100%').rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr').bindSheet($$this.showDialog, this.ImageListBuilder(), { showClose: false, height: '60%' })}
}
:::info
犹豫bindSheet需要一个builder,所以我们声明了一个builder
但是又考虑到了复用,如果其他地方也要选取图片怎么办?我们把内部又抽离成了一个组件
注意:builder内部根级必须是内置组件
:::
@Component
struct SelectImage {@PropimageList:ResourceStr[] = []build() {Column() {Row() {Text('取消')Text('已选中 0/9 张').layoutWeight(1).textAlign(TextAlign.Center)Text('确定')}.width('100%').padding(20)Grid() {ForEach(this.imageList, (item: ResourceStr) => {GridItem() {Image(item)}.aspectRatio(1)})}.padding(20).layoutWeight(1).rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr')}.width('100%').height('100%').backgroundColor('#f5f7f8')}
}
export { SelectImage }
3-添加点击事件,设置选中状态
- 对图片进行改造,统一添加点击事件,并声明一个选中的列表用来收集选中的图片
@Component
struct SelectImage {@PropimageList: ResourceStr[] = []@StateselectList: ResourceStr[] = []build() {Column() {Row() {Text('取消')Text(`已选中${this.selectList.length}/9 张`).layoutWeight(1).textAlign(TextAlign.Center)Text('确定')}.width('100%').padding(20)Grid() {ForEach(this.imageList, (item: ResourceStr) => {GridItem() {Stack({ alignContent: Alignment.BottomEnd }) {Image(item)if (this.selectList.includes(item)) {Image($r('sys.media.ohos_ic_public_select_all')).width(30).aspectRatio(1).fillColor('#ff397204').margin(4)}}}.aspectRatio(1).onClick(() => {this.selectList.push(item)})})}.padding(20).layoutWeight(1).rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr')}.width('100%').height('100%').backgroundColor('#f5f7f8')}
}export { SelectImage }
:::info
选是能选了,但是选的多了,没有加限制,而且不一定每次都是选多张,所以把能选几张控制一下
包括选中的,要可以取消才行
:::
4-点击确定同步给页面
这个就类似于知乎的点赞了,子组件声明一个可以接收父组件传递过来改数据的方法,点确定的时候调用即可
但是,我们学习那么多的修饰符了,就没必要这么麻烦了,既然想子改父,完全可以父传子,用Link接收直接改
父传
子改
:::info
到这效果基本就完成了,最后一个关闭弹层,你能想到怎么做了吗?
:::
5.关闭弹层
:::info
再添加一个预览图片的需求,添加后的图片可以点击预览查看,该如何实现呢?
:::
绑定添加事件,用弹层展示图片
- 自定义弹层
// 1.声明一个弹层
@CustomDialog
struct MyDialog {controller:CustomDialogController@PropselectedList:ResourceStr[] = []@StateselectIndex:number = 0build() {Column() {Swiper(){ForEach(this.selectedList,(item:ResourceStr)=>{Image(item).width('100%')})}.index($$this.selectIndex)Text(`${this.selectIndex+1}/${this.selectedList.length}`).fontColor('#fff').margin(20)}.width('100%').height('100%').backgroundColor('#000').justifyContent(FlexAlign.Center).onClick(()=>{this.controller.close()})}
}export { MyDialog }
- 使用弹层
import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
import { SelectImage } from './components/SelectImage'@Entry
@Component
struct ImageSelectCase {@StateselectedList: ResourceStr[] = []// 2.注册弹层myDialogController:CustomDialogController = new CustomDialogController({builder:MyDialog({// 传递的属性必须先声明selectedList:this.selectedList}),customStyle:true})// 下方有留白,取消不了,换一种方案@StateshowDialog: boolean = false@StateimageList: ResourceStr[] = ["assets/1.webp","assets/2.webp","assets/3.webp","assets/4.webp","assets/5.webp","assets/6.webp","assets/7.webp","assets/8.webp","assets/9.webp","assets/10.webp"]@BuilderImageListBuilder() {// 大坑:最外层必须得是容器组件Column(){SelectImage({imageList:this.imageList,selectedList:this.selectedList,showDialog:this.showDialog})}}build() {Grid() {ForEach(this.selectedList,(item:ResourceStr)=>{GridItem() {Image(item)}.aspectRatio(1).onClick(()=>{this.myDialogController.open()})})GridItem() {SelectImageIcon()}.aspectRatio(1).onClick(() => {// 3.使用弹层// this.myDialogController.open()this.showDialog = true})}.padding(20).width('100%').height('100%').rowsGap(10).columnsGap(10).columnsTemplate('1fr 1fr 1fr').bindSheet($$this.showDialog, this.ImageListBuilder(), { showClose: false, height: '60%' })}
}
6. @Observed与@ObjectLink
:::info
之前讲解Link的时候,我们说了一个要求,就是只有@State或者@Link修饰的数据才能用,
如果是一个数组内有多个对象,将对象传递给子组件的时候就没有办法使用Link了
ArtTS支持 Observed和@ObjectLink来实现这个需求
:::
例如美团点菜,菜品肯定是一个数组,如果我们将每个菜品封装成组件
当对菜品进行修改的时候,就没法再用Link同步了
使用步骤:
- 使用
@Observed
修饰这个类- 初始化数据:数据确保是通过
@Observed
修饰的类new
出来的- 通过
@ObjectLink
修饰传递的数据,可以直接修改被关联对象来更新UI
模拟一个点菜的案例来演示用法
@Entry
@Component
struct ObservedObjectLinkCase {@StategoodsList:GoodsTypeModel[] = [new GoodsTypeModel({name:'瓜子',price:3,count:0}),new GoodsTypeModel({name:'花生',price:3,count:0}),new GoodsTypeModel({name:'矿泉水',price:3,count:0})]build() {Column(){ForEach(this.goodsList,(item:GoodsTypeModel)=>{// 2.确保传递的对象是new过observed修饰的GoodItemLink({goodItem:item})})}}
}@Component
struct GoodItemLink {// 3.用ObjectLink修饰@ObjectLinkgoodItem:GoodsTypeModelbuild() {Row({space:20}){Text(this.goodItem.name)Text('¥'+this.goodItem.price)Image($r('sys.media.ohos_ic_public_remove_filled')).width(20).aspectRatio(1).onClick(()=>{this.goodItem.count--})Text(this.goodItem.count.toString())Image($r('sys.media.ohos_ic_public_add_norm_filled')).width(20).aspectRatio(1).onClick(()=>{this.goodItem.count++})}.width('100%').padding(20)}
}interface GoodsType {name:stringprice:numbercount:number
}
// 1.使用observed修饰一个类
@Observed
export class GoodsTypeModel implements GoodsType {name: string = ''price: number = 0count: number = 0constructor(model: GoodsType) {this.name = model.namethis.price = model.pricethis.count = model.count}
}
:::success
改造-知乎案例
点赞- 需求是当前数据的点赞量+1或者-1, 之前实际实现是: 把一条数据 给到父组件-替换了父组件的整行的数据, 并且造成了案例中头像的闪烁-因为这个组件数据被销毁然后被创建
理想效果: 其他一切都不动,只动数量的部分-也就是UI视图的局部更新- 需要使用Observed和ObjectLink
:::
@Observed
export class ReplyItemModel implements ReplyItem {avatar: ResourceStr = ''author: string = ''id: number = 0content: string = ''time: string = ''area: string = ''likeNum: number = 0likeFlag: boolean | null = nullconstructor(model: ReplyItem) {this.avatar = model.avatarthis.author = model.authorthis.id = model.idthis.content = model.contentthis.time = model.timethis.area = model.areathis.likeNum = model.likeNumthis.likeFlag = model.likeFlag}
}
- 给知乎的评论组件增加一个ObjectLink修饰符
// 接收渲染的选项@ObjectLinkitem: ReplyItemModel
- 评论子组件实现点赞的方法
// 更新逻辑changeLike () {if(this.item.likeFlag) {// 点过赞this.item.likeNum--}else {// 没有点过赞this.item.likeNum++}this.item.likeFlag = !this.item.likeFlag // 取反}
- 父组件传值优化
ForEach(this.commentList, (item: ReplyItemModel) => {ListItem() {HmCommentItem({item: item})}})
:::info
细节:此时,我们的头像不再闪动,说明数据已经不需要去更新整条数据来让父组件完成UI的更新,而是子组件内部局部的更新
:::
:::info
注意点:
- ObjectLink只能修饰被Observed修饰的class类型
- Observed修饰的class的数据如果是复杂数据类型,需要采用赋值的方式才可以具备响应式特性-因为它只能监听到第一层
- 如果出现复杂类型嵌套,只需要Observed我们需要的class即可
- ObjectLink修饰符不能用在Entry修饰的组件中
:::
:::info
此知识点不太好理解,同学们一定一定多敲几遍!!!!!
:::
7. Next新增修饰符-Require-Track
:::success
Require修饰符
4.0的编辑器中- 如果子组件定义了Prop,那么父组件必须得传,不传则报错
Next版本中,如果你想让父组件必须传递一个属性给你的Prop,作为强制性的约束条件,可以使用Require修饰符
:::
:::success
Require修饰符只能作用在两个修饰符前面Prop BuilderParam
:::
@Entry
@Component
struct RequireCase {@Statemessage: string = 'Hello World';@BuilderparentContent(){Text('builderParam')}build() {Row() {Column() {RequireChild({message: this.message}){this.parentContent()}}.width('100%')}.height('100%')}
}@Component
struct RequireChild {// 1.Prop@Require@Propmessage: string// 2.BuilderParam@Require@BuilderParamdefaultContent: () => voidbuild() {Column() {Text(this.message)this.defaultContent()}}
}
:::success
Track修饰符- 只针对对象中的某个属性的更新起作用,其余没修饰的属性不能进行UI展示
:::
该修饰符不存在新的视觉效果,属于性能优化级的,改造知乎点赞,对数据添加@Track查看效果
export interface ReplyItem {avatar: ResourceStr // 头像author: string // 作者id: number // 评论的idcontent: string // 评论内容time: string // 发表时间area: string // 地区likeNum: number // 点赞数量likeFlag: boolean | null // 当前用户是否点过赞
}
@Observed
export class ReplyItemModel implements ReplyItem {@Trackavatar: ResourceStr = ''@Trackauthor: string = ''@Trackid: number = 0@Trackcontent: string = ''@Tracktime: string = ''@Trackarea: string = ''@TracklikeNum: number = 0@TracklikeFlag: boolean | null = nullconstructor(model: ReplyItem) {this.avatar = model.avatarthis.author = model.authorthis.id = model.idthis.content = model.contentthis.time = model.timethis.area = model.areathis.likeNum = model.likeNumthis.likeFlag = model.likeFlag}
}
:::success
Track的作用只更新对象中的某些字段, Track修饰符用来作用在class中的某些字段,只有被标记的字段才会更新,并且没有被Track标记的字段不能被使用
场景:
假如只想根据对象中某个字段来更新或者渲染视图 就可以使用Track
:::
3.应用状态
:::success
State 组件内状态
Prop 父组件传入
Link 父组件传入
Provide 跨级组件传入
Consume 跨级组件接收
ObjectLink 父组件传入局部更新状态
:::
:::info
ArtTS提供了好几种状态用来帮助我们管理我们的全局数据
- LocalStorage-UIAbility状态(内存- 注意:和前端的区分开,它非持久化,非全应用)
- AppStorage- 应用内状态-多UIAbility共享-(内存-非持久化-退出应用同样消失)
- PersistentStorage-全局持久化状态(写入磁盘-持久化状态-退出应用 数据同样存在)
- 首选项- 写入磁盘
- 关系型数据库 - 写入磁盘
- 端云数据库
- 接口调用-云端数据(服务器数据)
:::
1. UIAbility内状态-LocalStorage
:::info
LocalStorage
是页面级的UI状态存储,通过 @Entry
装饰器接收的参数可以在页面内共享同一个 LocalStorage
实例。 LocalStorage
也可以在 UIAbility
内,页面间共享状态。
用法
-
创建
LocalStorage
实例:const storage = new LocalStorage({ key: value })
-
单向
@LocalStorageProp('user')
组件内可变 -
双向
@LocalStorageLink('user')
全局均可变
:::
案例-修改用户信息 -
创建一个LocalStorage,用于各个页面间共享数据
:::info
步骤:
1.准备一个含有类型声明的对象作为共享数据
2.将数据传入new LocalStorage(),得到可以共享的对象
3.导入共享对象,在需要使用的页面导入该对象,并传入@Entry
4.声明一个变量,用@LocalStorageProp或@LocalStorageLink修饰进行接收
5.使用声明的变量进行渲染
::: -
LocalStorage的声明与导出
// self是要共享的数据
const self: Record<string, ResourceStr> = {'age': '18','nickName': '一介码农','gender': '男','avtar': $r('app.media.b')
}
// localUserInfo是共享的数据
export const localUserInfo = new LocalStorage(self)
页面结构直接复制粘贴即可
@Entry
@Component
struct LocalStorageCase01 {build() {Column() {Row() {Image($r('sys.media.ohos_ic_back')).width(20).aspectRatio(1)Text('个人信息1').fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('确定')}.width('100%').padding(20).alignItems(VerticalAlign.Center)Row() {Text('头像:')Image('').width(40)}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('昵称:')TextInput({ text: '' }).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('性别:')TextInput({ text: '' }).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('年龄:')TextInput({ text: '' }).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)}.width('100%').height('100%')}
}
- 页面引用并传递共享的数据进行使用
// 1.引入可以共享的数据
import { localUserInfo } from './LocalStorageModel'
import { router } from '@kit.ArkUI'// 2.传递给页面
@Entry(localUserInfo)
@Component
struct LocalStorageCase02 {// 3.使用localUserInfo@LocalStorageLink('avtar')avtar: ResourceStr = ''@LocalStorageLink('nickName')nickName: ResourceStr = ''@LocalStorageLink('gender')gender: ResourceStr = ''@LocalStorageLink('age')age: ResourceStr = ''build() {Column() {Row() {Image($r('sys.media.ohos_ic_back')).width(20).aspectRatio(1).onClick(()=>{router.back()})Text('个人信息2').fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('确定')}.width('100%').padding(20).alignItems(VerticalAlign.Center)Row() {Text('头像:')Image(this.avtar).width(40)}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('昵称:')TextInput({ text: $$this.nickName }).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('性别:')TextInput({ text: $$this.gender}).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)Row() {Text('年龄:')TextInput({ text: $$this.age }).textAlign(TextAlign.End).layoutWeight(1).backgroundColor('#fff').padding({right: 0})}.width('100%').padding(20).justifyContent(FlexAlign.SpaceBetween)}.width('100%').height('100%')}
}
- 新建一个页面,将共享的数据同时作用到两个页面,router.pushUrl可以跳转页面
//跳转
Text('修改').onClick(()=>{router.pushUrl({url:'pages/08/LocalStorageDemo/LocalStorageCase01'})})
//返回Image($r('sys.media.ohos_ic_back')).width(20).aspectRatio(1).onClick(()=>{router.back()})
- 使用LocalStorageLink实现双向绑定
@LocalStorageLink('nickName')nickName:string = ''
:::info
- 将LocalStorage实例从UIAbility共享到一个或多个视图,参考 官方示例
- 使用场景:
服务卡片-只能通过LocalStorage进行接收参数
:::
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';export default class EntryAbility extends UIAbility {// self是要共享的数据self: Record<string, ResourceStr> = {'age': '19','nickName': '一介码农','gender': '男','avtar': $r('app.media.b')}// localUserInfo是共享的数据localUserInfo:LocalStorage = new LocalStorage(this.self)onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');}onDestroy(): void {hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');}onWindowStageCreate(windowStage: window.WindowStage): void {windowStage.loadContent('pages/08/LocalStorage/LocalStorage02',this.localUserInfo );}onWindowStageDestroy(): void {// Main window is destroyed, release UI related resourceshilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');}onForeground(): void {// Ability has brought to foregroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');}
}
2. 应用状态-AppStorage
LocalStorage是针对UIAbility的状态共享- 一个UIAbility有个页面
一个应用可能有若干个UIAbility
:::success
概述
AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。
AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。
AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。
AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。
:::
:::info
AppStorage
是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。-注意它也是内存数据,不会写入磁盘
第一种用法-使用UI修饰符
- **如果是初始化使用 **
**AppStorage.setOrCreate(key,value)**
- 单向
**@StorageProp('user')**
组件内可变 - 双向
**@StorageLink('user')**
全局均可变
第二种用法 使用API方法
-
**AppStorage.get<ValueType>(key)**
** 获取数据** -
**AppStorage.set<ValueType>(key,value)**
** 覆盖数据**
:::
:::success
AppStorage.setOrCreate(“”, T) // 创建或者设置某个字段的属性
AppStorage.get(“”) // 获取的全局状态类型
如果遇到获取数据的类型为空,可以用if判断,也可以用非空断言来解决
StorageLink . - 直接修改-自动同步到全局状态
StorageProp- 可以改,只会在当前组件生效,只是改的全局状态的副本,不会对全局状态产生影响
:::
准备两个页面,A页面登录获取用户信息,B页面展示修改 -
A页面登录模版,用于存入AppStorage
@Entry
@Component
struct AppStorageCase01 {@Stateusername: string = ""@Statepassword: string = ""build() {Row() {Column({ space: 20 }) {TextInput({ placeholder: '请输入用户名', text: $$this.username })TextInput({ placeholder: '请输入密码', text: $$this.password }).type(InputType.Password)Button("登录").width('100%')}.padding(20).width('100%')}.height('100%')}
}
- B页面登录模版,用于展示AppStorage
@Entry
@Component
struct AppStorageCase02 {build() {Column() {Row({ space: 20 }) {Image($r('app.media.b')).width(60).aspectRatio(1).borderRadius(30)Column({ space: 10 }) {Text('姓名:老潘')Text(`年龄:18岁`)}}.alignItems(VerticalAlign.Center).padding(20).width('100%')Button("退出")}.width('100%').height('100%')}
}
- A页面点击登录
import { router } from '@kit.ArkUI'@Entry
@Component
struct AppStorageCase01 {@Stateusername: string = ""@Statepassword: string = ""login(){const userInfo:Record<string,string> = {'name':'一介码农','age':'99',}AppStorage.setOrCreate<Record<string,string>>('userInfo',userInfo)router.pushUrl({url:'pages/08/AppStorageDemo/AppStorageCase1'})}build() {Row() {Column({ space: 20 }) {TextInput({ placeholder: '请输入用户名', text: $$this.username })TextInput({ placeholder: '请输入密码', text: $$this.password }).type(InputType.Password)Button("登录").width('100%').onClick(()=>{this.login()})}.padding(20).width('100%')}.height('100%')}
}
- B页面展示登录信息
@Entry
@Component
struct AppStorageCase02 {// 用法1// @StorageProp('userInfo')// userInfo:Record<string,string> = {}// 用法2@StateuserInfo:Record<string,string> = {}aboutToAppear(): void {const userInfo = AppStorage.get<Record<string,string>>('userInfo')this.userInfo = userInfo!}build() {Column() {Row({ space: 20 }) {Image($r('app.media.b')).width(60).aspectRatio(1).borderRadius(30)Column({ space: 10 }) {Text(`姓名:${this.userInfo.name}`)Text(`年龄:${this.userInfo.age}岁`)}}.alignItems(VerticalAlign.Center).padding(20).width('100%')Button("退出").onClick(()=>{AppStorage.set('userInfo',null)router.back()})}.width('100%').height('100%')}
}
新建一个Ability,打开新的UIAbility查看状态
let want:Want = {'deviceId': '', // deviceId为空表示本设备'bundleName': 'com.example.harmonyos_next_base','abilityName': 'EntryAbility1',};(getContext() as common.UIAbilityContext).startAbility(want)
3. 状态持久化-PersistentStorage
:::info
前面讲的所有状态均为内存状态,也就是应用退出便消失,所以如果我们想持久化的保留一些数据,应该使用
PersistentStorage
。
注意:
UI和业务逻辑不直接访问 PersistentStorage
中的属性,所有属性访问都是对 AppStorage
的访问,AppStorage
中的更改会自动同步到 PersistentStorage
。
也就是,我们和之前访问AppStorage是一样的,只不过需要提前使用PersistentStorage来声明
:::
PersistentStorage
将选定的AppStorage
属性保留在设备磁盘上。
:::warning
- 支持:number, string, boolean, enum 等简单类型;
- 如果:要支持对象类型,可以转换成json字符串
- 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。
用法:
PersistentStorage.PersistProp(‘属性名’, 值)
注意: 如果用了持久化, 那么AppStorage读取出来的对象实际上是PersistentStorage存储的json字符串
如果没用持久化 。那么读取出来的对象就是AppStorage对象
:::
将刚刚的token直接持久化存储
PersistentStorage.PersistProp("user", '123') // 初始化磁盘,给一个读取不到时加载的默认值
:::info
只要初始化了数据,我们以后使用AppStorage就可以读取和设置,它会自动同步到我们的磁盘上
目前不支持复杂对象的持久化,如果你需要存储,你需要把它序列化成功字符串
- 测试:需要在真机或模拟器调试
:::
大家可以在上一个例子之前添加 PersistentStorage.PersistProp(‘属性名’, 值)
然后直接使用AppStorage进行set就可以了,设置完成之后,使用模拟器先把任务销毁,然后再查看数据是否显示
:::success
限制条件
PersistentStorage允许的类型和值有:
- number, string, boolean, enum 等简单类型。
- 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。
PersistentStorage不允许的类型和值有:
- 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
- 不支持undefined 和 null 。
持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:
- 持久化大型数据集。
- 持久化经常变化的变量。
PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。
PersistentStorage只能在UI页面内使用,否则将无法持久化数据。
:::
4. 状态持久化-preferences首选项
:::success
此时此刻,需要做一件事, 有token跳转到主页,没有token跳转到登录
:::
:::success
首选项
- 每一个key的value的长度最大为8kb
- 创建首选项-仓库的概念- 应用可以有N个仓库,一个仓库中可以有N个key
:::
import { Context } from '@kit.AbilityKit'
import { preferences } from '@kit.ArkData'
// 两种方式引入的是同一个东西
// import preferences from '@ohos.data.preferences'export class PreferencesClass {// static代表的是静态,可以直接通过类访问// store名称static defaultStore: string = 'DEFAULT_STORE'static firstStore: string = 'FIRST_STORE'// 字段名称,一个字段配2个方法,读取和写入static tokenKey:string = 'TOKEN_KEY'// 仓库中存储字段static setToken(content:Context,token:string,storeName:string=PreferencesClass.defaultStore){const store = preferences.getPreferencesSync(content,{name:storeName})store.putSync(PreferencesClass.tokenKey,token)store.flush()}// 读取仓库中字段static getToken(content:Context,storeName:string=PreferencesClass.defaultStore){const store = preferences.getPreferencesSync(content,{name:storeName})return store.getSync(PreferencesClass.tokenKey,'')}
}
- 在ability中判断
5. 设备状态-Environment(了解)
:::info
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。
:::
- 1.将设备的色彩模式存入AppStorage,默认值为Color.LIGHT
Environment.EnvProp('colorMode', Color.LIGHT);
- 2.可以使用@StorageProp进行查询,从而实现不同UI
@StorageProp('colorMode')
lang : bgColor = Color.White';
- 该环境变量只能查询后写入AppStorage,可以在AppStorage中进行修改,改目前使用场景比较鸡肋,作为面试知识点储备即可
// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中;
Environment.EnvProp('colorMode', 'en');
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('colorMode');if (lang.get() === Color.LIGHT) {console.info('亮色');
} else {console.info('暗色');
}
4.网络管理(需要模拟器)
1. 应用权限
ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力
应用权限保护的对象可以分为数据和功能:
- 数据包含了个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)、应用数据。
- 功能则包括了设备功能(如打电话、发短信、联网等)、应用功能(如弹出悬浮框、创建快捷方式等)等。
根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权)。
- 配置文件权限声明
- 向用户申请授权
例如:访问网络需要联网权限
system_grant(系统授权)配置后直接生效
{"module" : {// ..."requestPermissions":[{"name" : "ohos.permission.INTERNET"}]}
}
例如:获取地址位置权限
user_grant(用户授权)向用户申请
1.首先在module.json5中配置权限申请地址位置权限
{"module" : {// ..."requestPermissions":[{"name" : "ohos.permission.INTERNET"},{"name": "ohos.permission.APPROXIMATELY_LOCATION","reason": "$string:permission_location","usedScene": {"abilities": ["EntryAbility"]}}]}
}
2.在ability中申请用户授权
通过abilityAccessCtrl创建管理器进行申请权限
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {const manager = abilityAccessCtrl.createAtManager() // 创建程序控制管理器await manager.requestPermissionsFromUser(this.context,["ohos.permission.APPROXIMATELY_LOCATION"])}
开启权限后可以获取经纬度坐标
import { geoLocationManager } from '@kit.LocationKit';@Entry
@Component
struct HuaweiMapDemo {@Stateresult:geoLocationManager.Location = {} as geoLocationManager.Locationbuild() {Column() {Button('获取经纬度').onClick(async ()=>{this.result = await geoLocationManager.getCurrentLocation()})Text('经度:'+this.result.latitude)Text('纬度:'+this.result.longitude)}.height('100%')}
}
2. HTTP请求(需要模拟器)
:::success
request接口开发步骤
- 从@ohos.net.http.d.ts中导入http命名空间。
- 调用createHttp()方法,创建一个HttpRequest对象。
- 调用该对象的on()方法,订阅http响应头事件,此接口会比request请求先返回。可以根据业务需要订阅此消息。
- 调用该对象的request()方法,传入http请求的url地址和可选参数,发起网络请求。
- 按照实际业务需要,解析返回结果。
- 调用该对象的off()方法,取消订阅http响应头事件。
- 当该请求使用完毕时,调用destroy()方法主动销毁。
:::
// 引入包名
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定"EXAMPLE_URL",{method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET// 开发者根据自身业务需要添加header字段header: [{'Content-Type': 'application/json'}],// 当使用POST请求时此字段用于传递内容extraData: "data to send",expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型usingCache: true, // 可选,默认为truepriority: 1, // 可选,默认为1connectTimeout: 60000, // 可选,默认为60000msreadTimeout: 60000, // 可选,默认为60000msusingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定usingProxy: false, // 可选,默认不使用网络代理,自API 10开始支持该属性caPath:'/path/to/cacert.pem', // 可选,默认使用系统预制证书,自API 10开始支持该属性clientCert: { // 可选,默认不使用客户端证书,自API 11开始支持该属性certPath: '/path/to/client.pem', // 默认不使用客户端证书,自API 11开始支持该属性keyPath: '/path/to/client.key', // 若证书包含Key信息,传入空字符串,自API 11开始支持该属性certType: http.CertType.PEM, // 可选,默认使用PEM,自API 11开始支持该属性keyPassword: "passwordToKey" // 可选,输入key文件的密码,自API 11开始支持该属性},multiFormDataList: [ // 可选,仅当Header中,'content-Type'为'multipart/form-data'时生效,自API 11开始支持该属性{name: "Part1", // 数据名,自API 11开始支持该属性contentType: 'text/plain', // 数据类型,自API 11开始支持该属性data: 'Example data', // 可选,数据内容,自API 11开始支持该属性remoteFileName: 'example.txt' // 可选,自API 11开始支持该属性}, {name: "Part2", // 数据名,自API 11开始支持该属性contentType: 'text/plain', // 数据类型,自API 11开始支持该属性// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.txtfilePath: `${getContext(this).filesDir}/fileName.txt`, // 可选,传入文件路径,自API 11开始支持该属性remoteFileName: 'fileName.txt' // 可选,自API 11开始支持该属性}]}, (err: BusinessError, data: http.HttpResponse) => {if (!err) {// data.result为HTTP响应内容,可根据业务需要进行解析console.info('Result:' + JSON.stringify(data.result));console.info('code:' + JSON.stringify(data.responseCode));// data.header为HTTP响应头,可根据业务需要进行解析console.info('header:' + JSON.stringify(data.header));console.info('cookies:' + JSON.stringify(data.cookies)); // 8+// 当该请求使用完毕时,调用destroy方法主动销毁httpRequest.destroy();} else {console.error('error:' + JSON.stringify(err));// 取消订阅HTTP响应头事件httpRequest.off('headersReceive');// 当该请求使用完毕时,调用destroy方法主动销毁httpRequest.destroy();}}
);
美团外卖接口地址: https://zhousg.atomgit.net/harmonyos-next/takeaway.json
2)使用 @ohos.net.http
模块发请求
import http from '@ohos.net.http'@Entry
@Component
struct HttpCase {aboutToAppear() {this.getMeiTuanData()}async getMeiTuanData() {try {const req = http.createHttp()const res = await req.request("https://zhousg.atomgit.net/harmonyos-next/takeaway.json")AlertDialog.show({message: res.result as string})} catch (e) {}}build() {Row() {Column() {}.width('100%')}.height('100%')}
}
:::success
使用第三方包 axios
:::
:::success
openharmony中心仓地址
:::
- 安装axios
$ ohpm install @ohos/axios
- 发起请求
import axios, { AxiosResponse } from '@ohos/axios'
import { promptAction } from '@kit.ArkUI';@Entry
@Component
struct HttpCase {@State message: string = 'Hello World';async getData() {const result = await axios.get<object, AxiosResponse<object,null>>("https://zhousg.atomgit.net/harmonyos-next/takeaway.json")promptAction.showToast({ message: JSON.stringify(result) })}build() {Row() {Column() {Text(this.message).fontSize(50).fontWeight(FontWeight.Bold)Button("测试请求").onClick(() => {this.getData()})}.width('100%')}.height('100%')}
}
interface Data {name: string
}
5.今日案例-美团外卖
:::success
准备基础色值
在一个标准项目中,应该会有几套标准的配色,此时可以使用resources/base/element/color.json来帮我们统一管理,使用时使用$r(“app.color.xxx”)来取值即可
:::
- 将color赋值到resources/base/element/color.json中
{"color": [{"name": "start_window_background","value": "#FFFFFF"},{"name": "white","value": "#FFFFFF"},{"name": "black","value": "#000000"},{"name": "bottom_back","value": "#222426"},{"name": "main_color","value": "#f8c74e"},{"name": "select_border_color","value": "#fa0"},{"name": "un_select_color","value": "#666"},{"name": "search_back_color","value": "#eee"},{"name": "search_font_color","value": "#999"},{"name": "food_item_second_color","value": "#333"},{"name": "food_item_label_color","value": "#fff5e2"},{"name": "top_border_color","value": "#e4e4e4"},{"name": "left_back_color","value": "#f5f5f5"},{"name": "font_main_color","value": "#ff8000"}]
}
!
1. 目录结构-入口页面
:::success
新建如下目录结构
pages
-MeiTuan
-api
-components
-models
-utils
-MTIndex.ets(Page)
:::
- 在MTIndex.ets中设置基础布局
@Entry@Componentstruct MTIndex {build() {Column() {}.width('100%').height("100%").backgroundColor($r("app.color.white"))}}
- 新建MTTop-MTMain-MTBottom三个组件-在components目录下
@Component
struct MTMain {build() {Text("MTMain")}
}
export default MTMain
@Component
struct MTTop {build() {Text("MTTop")}
}
export default MTTop
@Component
struct MTBottom {build() {Text("MTBottom")}
}
export default MTBottom
- 在MTIndex.ets中放入
import MTBottom from './components/MTBottom'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'@Entry
@Component
struct MTIndex {build() {Column() {Stack({ alignContent: Alignment.Bottom }) {Column() {MTTop()MTMain()}.height("100%")MTBottom()}.layoutWeight(1)}.width('100%').height("100%").backgroundColor($r("app.color.white"))}
}
2. 页面结构-底部组件
:::success
将图片资源 图片.zip放入到资源目录下 resources/media
:::
@Preview
@Component
struct MTBottom {build() {Row () {Row() {// 小哥的显示Badge({value: '0',position: BadgePosition.Right,style: {badgeSize: 18}}){Image($r("app.media.ic_public_cart")).width(47).height(69).position({y: -20})}.margin({left: 25,right: 10})// 显示费用Column() {Text(){// span imageSpanSpan("¥").fontSize(12)Span("0.00").fontSize(24)}.fontColor($r("app.color.white"))Text("预估另需配送费¥5元").fontColor($r("app.color.search_font_color")).fontSize(14)}.alignItems(HorizontalAlign.Start).layoutWeight(1)Text("去结算").height(50).width(100).backgroundColor($r("app.color.main_color")).textAlign(TextAlign.Center).borderRadius({topRight: 25,bottomRight: 25})}.height(50).backgroundColor($r("app.color.bottom_back")).width('100%').borderRadius(25)}.width('100%').padding({left: 20,right: 20,bottom: 20})}
}
export default MTBottom
3. 顶部结构-MTTop(复制粘贴)
@Component
struct MTTop {@BuilderNavItem(active: boolean, title: string, subTitle?: string) {Column() {Text() {Span(title)if (subTitle) {Span(' ' + subTitle).fontSize(10).fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))}}.layoutWeight(1).fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color")).fontWeight(active ? FontWeight.Bold : FontWeight.Normal)Text().height(1).width(20).margin({ left: 6 }).backgroundColor(active ? $r("app.color.select_border_color") : 'transparent')}.width(73).alignItems(HorizontalAlign.Start).padding({ top: 3 })}build() {Row() {this.NavItem(true, '点菜')this.NavItem(false, '评价', '1796')this.NavItem(false, '商家')Row() {Image($r('app.media.ic_public_search')).width(14).aspectRatio(1).fillColor($r("app.color.search_font_color"))Text('请输入菜品名称').fontSize(12).fontColor($r("app.color.search_back_color"))}.backgroundColor($r("app.color.search_back_color")).height(25).borderRadius(13).padding({ left: 5, right: 5 }).layoutWeight(1)}.padding({ left: 15, right: 15 }).height(40).border({ width: { bottom: 0.5 }, color: $r("app.color.top_border_color") })}
}export default MTTop
4. 页面结构-商品菜单和商品列表
- 抽提MTFoodItem组件(粘贴)
@Preview
@Component
struct MTFoodItem {build() {Row() {Image('https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/1.jpg').width(90).aspectRatio(1)Column({ space: 5 }) {Text('小份酸汤莜面鱼鱼+肉夹馍套餐').textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(2).fontWeight(600)Text('酸汤莜面鱼鱼,主料:酸汤、莜面 肉夹馍,主料:白皮饼、猪肉').textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(1).fontSize(12).fontColor($r("app.color.food_item_second_color"))Text('点评网友推荐').fontSize(10).backgroundColor($r("app.color.food_item_label_color")).fontColor($r("app.color.font_main_color")).padding({ top: 2, bottom: 2, right: 5, left: 5 }).borderRadius(2)Text() {Span('月销售40')Span(' ')Span('好评度100%')}.fontSize(12).fontColor($r("app.color.black"))Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span('34.23').fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.padding(10).alignItems(VerticalAlign.Top)}
}
export default MTFoodItem
- 在MTMain中使用
import MTFoodItem from './MTFoodItem'@Component
struct MTMain {list: string[] = ['一人套餐', '特色烧烤', '杂粮主食']@StateactiveIndex: number = 0build() {Row() {Column() {ForEach(this.list, (item: string, index: number) => {Text(item).height(50).width('100%').textAlign(TextAlign.Center).fontSize(14).backgroundColor(this.activeIndex === index ? $r("app.color.white") : $r("app.color.left_back_color")).onClick(() => {this.activeIndex = index})})}.width(90)// 右侧内容List() {ForEach([1,2,3,4,5,6,7,8,9], () => {ListItem() {MTFoodItem()}})}.layoutWeight(1).backgroundColor('#fff').padding({bottom: 80})}.layoutWeight(1).alignItems(VerticalAlign.Top).width('100%')}
}
export default MTMain
5. 页面结构-购物车
- 新建MTCart组件
import MTCartItem from './MTCartItem'@Component
struct MTCart {build() {Column() {Column() {Row() {Text('购物车').fontSize(12).fontWeight(600)Text('清空购物车').fontSize(12).fontColor($r("app.color.search_font_color"))}.width('100%').height(40).justifyContent(FlexAlign.SpaceBetween).border({ width: { bottom: 0.5 }, color: $r("app.color.left_back_color") }).margin({ bottom: 10 }).padding({ left: 15, right: 15 })List({ space: 30 }) {ForEach([1,2,3,4], () => {ListItem() {MTCartItem()}})}.divider({strokeWidth: 0.5,color: $r("app.color.left_back_color")}).padding({ left: 15, right: 15, bottom: 100 })}.backgroundColor($r("app.color.white")).borderRadius({topLeft: 16,topRight: 16})}.height('100%').width('100%').justifyContent(FlexAlign.End).backgroundColor('rgba(0,0,0,0.5)')}
}
export default MTCart
- 新建MTCartItem组件(粘贴)
@Component
struct MTCartItem {build() {Row() {Image('https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/4.jpeg').width(60).aspectRatio(1).borderRadius(8)Column({ space: 5 }) {Text('小份酸汤莜面鱼鱼+肉夹馍套餐').fontSize(14).textOverflow({overflow: TextOverflow.Ellipsis}).maxLines(2)Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span('34.23').fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.alignItems(VerticalAlign.Top)}
}
export default MTCartItem
- 在MTIndex.ets中声明管控显示购物车变量
@Provide showCart: boolean = false
- 在MTIndex.ets中控制显示
import MTBottom from './components/MTBottom'
import MTCart from './components/MTCart'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'@Entry
@Component
struct MTIndex {@Provide showCart: boolean = falsebuild() {Column() {Stack({ alignContent: Alignment.Bottom }) {Column() {MTTop()MTMain()}.height("100%")if(this.showCart) {MTCart()}MTBottom()}.layoutWeight(1)}.width('100%').height("100%").backgroundColor($r("app.color.white"))}
}
:::success
这里MTCart要放在MTBottom前面 利用层级的先后关系实现底部内容挡在购物车前面的效果
:::
- 点击购物车图标显示隐藏购物车-MTBottom.ets
@Component
struct MTBottom {@ConsumeshowCart: booleanbuild() {Row() {Row() {Badge({value: '0',position: BadgePosition.Right,style: { badgeSize: 18 }}) {Image($r("app.media.ic_public_cart")).width(47).height(69).position({ y: -19 })}.width(50).height(50).margin({ left: 25, right: 10 }).onClick(() => {this.showCart = !this.showCart})Column() {Text() {Span('¥').fontColor('#fff').fontSize(12)Span('0.00').fontColor('#fff').fontSize(24)}Text('预估另需配送费 ¥5').fontSize(12).fontColor('#999')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text('去结算').backgroundColor($r("app.color.main_color")).alignSelf(ItemAlign.Stretch).padding(15).borderRadius({topRight: 25,bottomRight: 25})}.height(50).width('100%').backgroundColor($r("app.color.bottom_back")).borderRadius(25)}.width('100%').padding({ left: 20, right: 20, bottom: 20 })}
}
export default MTBottom
- 返回键关闭购物车
组件生命周期有一个方法叫onBackPress,可以在Index监听这个方法进行关闭
onBackPress(): boolean | void {this.showCart = false}
6. 业务逻辑-渲染商品菜单和列表
- 准备结构返回的数据模型(粘贴)
export class FoodItem {id: number = 0name: string = ""like_ratio_desc: string = ""food_tag_list: string[] = []price: number = 0picture: string = ""description: string = ""tag: string = ""month_saled: number = 0count: number = 0
}export class Category {tag: string = ""name: string =""foods: FoodItem[] = []
}
- api/index.ets 使用
http
发送请求,获取数据
import { http } from '@kit.NetworkKit'
export class FoodItem {id: number = 0name: string = ""like_ratio_desc: string = ""food_tag_list: string[] = []price: number = 0picture: string = ""description: string = ""tag: string = ""month_saled: number = 0count: number = 0
}
export class Category {tag: string = ""name: string =""foods: FoodItem[] = []
}
export const getData =async () => {const req = http.createHttp()const res = await req.request('https://zhousg.atomgit.net/harmonyos-next/takeaway.json')return JSON.parse(res.result as string) as Category[]
}
- 在MTMain.ets中获取数据
@State
list: Category[] = []async aboutToAppear(){this.list = await getAllData()}
- MTMain循环内容渲染
import { getAllData } from '../api'
import { Category, FoodItem } from '../models'
import MTFoodItem from './MTFoodItem'@Component
struct MTMain {@StateactiveIndex: number = 0@Statelist: Category[] = []async aboutToAppear(){this.list = await getAllData()}build() {Row() {Column() {ForEach(this.list, (item: Category, index: number) => {Text(item.name).height(50).width('100%').textAlign(TextAlign.Center).fontSize(14).backgroundColor(this.activeIndex === index ? $r("app.color.white") : $r("app.color.left_back_color")).onClick(() => {this.activeIndex = index})})}.width(90)// 右侧内容List() {ForEach(this.list[this.activeIndex]?.foods || [], (item: FoodItem) => {ListItem() {MTFoodItem({ item })}})}.layoutWeight(1).backgroundColor($r("app.color.white")).padding({bottom: 80})}.layoutWeight(1).alignItems(VerticalAlign.Top).width('100%')}
}
export default MTMain
- MTFoodItem组件使用属性接收数据
import { FoodItem } from '../models'@Preview
@Component
struct MTFoodItem {item: FoodItem = new FoodItem()build() {Row() {Image(this.item.picture).width(90).aspectRatio(1)Column({ space: 5 }) {Text(this.item.name).textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(2).fontWeight(600)Text(this.item.description).textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(1).fontSize(12).fontColor($r("app.color.food_item_second_color"))ForEach(this.item.food_tag_list, (tag: string) => {Text(tag).fontSize(10).backgroundColor($r("app.color.food_item_label_color")).fontColor($r("app.color.font_main_color")).padding({ top: 2, bottom: 2, right: 5, left: 5 }).borderRadius(2)})Text() {Span('月销售' + this.item.month_saled)Span(' ')Span(this.item.like_ratio_desc)}.fontSize(12).fontColor($r("app.color.black"))Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span(this.item.price?.toString()).fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.padding(10).alignItems(VerticalAlign.Top)}
}export default MTFoodItem
7. 业务逻辑-封装新增加菜和减菜组件
- 准备组件的静态结构(粘贴)
@Preview
@Component
struct MTAddCut {build() {Row({ space: 8 }) {Row() {Image($r('app.media.ic_screenshot_line')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.white")).borderRadius(4).border({ width: 0.5 , color: $r("app.color.main_color")})Text('0').fontSize(14)Row() {Image($r('app.media.ic_public_add_filled')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.main_color")).borderRadius(4)}}
}
export default MTAddCut
- 放置在MTFoodItem中
8. 业务逻辑-加入购物车
:::info
设计购物车模型
我们需要持久化的数据,使用 PersistentStorage.persistProp(CART_KEY, [])
:::
- 购物车数据更新
import { FoodItem } from '../api'
PersistentStorage.persistProp('cart_list', [])
export class CartStore {static addCutCart(item: FoodItem, flag: boolean = true) {const list = AppStorage.get<FoodItem[]>('cart_list')!const index = list.findIndex(listItem => listItem.id === item.id)if (flag) {if (index < 0) {item.count = 1// 新增list.unshift(item)} else {list[index].count++// 让第一层发生变化list.splice(index, 1,list[index])}} else {list[index].count--// 如果减到0就删掉if (list[index].count === 0){list.splice(index, 1)}else{// 让第一层发生变化list.splice(index, 1,list[index])}}AppStorage.setOrCreate('cart_list',list)}
}
:::success
切记:改第二层UI是不会响应式更新的,所以一定是数组自身,或者数组的第一层要变化才行!
:::
- 现在我们有了加菜-减菜的方法-也可以调用加入菜品
- 购物车视图更新
:::info
在MTCart中使用StorageLink直接取出购物车数据进行双向绑定
:::
import { FoodItem } from '../api'
import MTAddCut from './MTAddCut'@Component
struct MTCartItem {item:FoodItem = new FoodItem()build() {Row() {Image(this.item.picture).width(60).aspectRatio(1).borderRadius(8)Column({ space: 5 }) {Text(this.item.name).fontSize(14).textOverflow({overflow: TextOverflow.Ellipsis}).maxLines(2)Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span(this.item.price.toString()).fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}MTAddCut({food:this.item})}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.alignItems(VerticalAlign.Top)}
}
export default MTCartItem
- MTCartItem中使用item
import { FoodItem } from '../api'
import MTAddCut from './MTAddCut'@Component
struct MTCartItem {item:FoodItem = new FoodItem()build() {Row() {Image(this.item.picture).width(60).aspectRatio(1).borderRadius(8)Column({ space: 5 }) {Text(this.item.name).fontSize(14).textOverflow({overflow: TextOverflow.Ellipsis}).maxLines(2)Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span(this.item.price.toString()).fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}MTAddCut({food:this.item})}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.alignItems(VerticalAlign.Top)}
}
export default MTCartItem
9.加菜和减菜按钮加入购物车
:::info
-
使用AppStorage接收所有购物车数据
-
根据数量显示减菜按钮和数量元素
:::
import { FoodItem } from '../api'
import { CarCalcClass } from '../utils/CartCalcClass'@Preview
@Component
struct MTAddCut {@StorageLink('cart_list')cartList: FoodItem[] = []food: FoodItem = new FoodItem()getCount(): number {const index = this.cartList.findIndex(listItem => listItem.id === this.food.id)return index < 0 ? 0 : this.cartList[index].count}build() {Row({ space: 8 }) {Row() {Image($r('app.media.ic_screenshot_line')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.white")).borderRadius(4).border({ width: 0.5, color: $r("app.color.main_color") }).visibility(this.getCount()>0?Visibility.Visible:Visibility.Hidden).onClick(() => {CartStore.addCutCart(this.food, false)})Text(this.getCount().toString()).fontSize(14)Row() {Image($r('app.media.ic_public_add_filled')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.main_color")).borderRadius(4).onClick(() => {CartStore.addCutCart(this.food)})}}
}export default MTAddCut
- 给AddCutCart传入Item
MTAddCut({ item: this.item })
:::success
在MTCartItem中同样需要放置AddCutCart
:::
MTAddCut({ item: this.item })
:::success
解决在购物车中添加图片卡的问题
:::
ForEach(this.cartList, (item: FoodItem) => {ListItem() {MTCartItem({ item })}}, (item: FoodItem) => item.id.toString())
10.清空购物车
Text('清空购物车').fontSize(12).fontColor('#999').onClick(() => {CartStore.clearCart()})
- 清空方法
static clearCarts () {AppStorage.set<FoodItem[]>("cart_list", [])}
11.底部内容汇总
import { FoodItem } from '../api'@Component
struct MTBottom {@ConsumeshowCart: boolean@StorageLink('cart_list')cartList: FoodItem[] = []getAllCount () {return this.cartList.reduce((preValue, item) => preValue + item.count, 0).toString()}getAllPrice () {return this.cartList.reduce((preValue, item) => preValue + item.count * item.price, 0).toFixed(2)}build() {Row() {Row() {Badge({value: '0',position: BadgePosition.Right,style: {badgeSize: 18}}) {Image($r('app.media.ic_public_cart')).width(48).height(70).position({y: -20,})}.margin({left:25,right:10}).onClick(() => {this.showCart = !this.showCart})Column() {Text(){// span imageSpanSpan("¥").fontSize(12)Span("0.00").fontSize(24)}.fontColor($r("app.color.white"))Text("预估另需配送费¥5元").fontColor($r("app.color.search_font_color")).fontSize(14)}.alignItems(HorizontalAlign.Start).layoutWeight(1)Text("去结算").height(50).width(100).backgroundColor($r("app.color.main_color")).textAlign(TextAlign.Center).borderRadius({topRight: 25,bottomRight: 25})}.height(50).width('100%').backgroundColor($r('app.color.bottom_back')).borderRadius(25)}.width('100%').padding(20)}
}export default MTBottom
美团案例完整代码
- MTIndex.ets
import { Category, getData } from './models'
import MTBottom from './components/MTBottom'
import MTCart from './components/MTCart'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'
import { promptAction } from '@kit.ArkUI'@Entry
@Component
struct MTIndex {@Provide showCart: boolean = false@Statelist: Category[] = []onBackPress(): boolean | void {this.showCart = false}async aboutToAppear() {this.list = await getData()}build() {Stack({ alignContent: Alignment.Bottom }) {Column() {MTTop()MTMain({list: this.list})}.height('100%').width('100%')if (this.showCart) {MTCart()}MTBottom()}.width('100%').height('100%')}
}
- components/MTTop.ets
@Component
struct MTTop {@BuilderNavItem(active: boolean, title: string, subTitle?: string) {Column() {Text() {Span(title)if (subTitle) {Span(' ' + subTitle).fontSize(10).fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))}}.layoutWeight(1).fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color")).fontWeight(active ? FontWeight.Bold : FontWeight.Normal)Text().height(1).width(20).margin({ left: 6 }).backgroundColor(active ? $r("app.color.select_border_color") : 'transparent')}.width(73).alignItems(HorizontalAlign.Start).padding({ top: 3 })}build() {Row() {this.NavItem(true, '点菜')this.NavItem(false, '评价', '1796')this.NavItem(false, '商家')Row() {Image($r('app.media.ic_public_search')).width(14).aspectRatio(1).fillColor($r("app.color.search_font_color"))Text('请输入菜品名称').fontSize(12).fontColor($r("app.color.search_back_color"))}.backgroundColor($r("app.color.search_back_color")).height(25).borderRadius(13).padding({ left: 5, right: 5 }).layoutWeight(1)}.padding({ left: 15, right: 15 }).height(40).border({ width: { bottom: 0.5 }, color: $r("app.color.top_border_color") })}
}export default MTTop
- components/MTMain.ets
import { Category, FoodItem } from '../models'
import MTFoodItem from './MTFoodItem'@Component
struct MTMain {@Linklist:Category[]@StateactiveIndex:number = 0build() {Row() {Column() {ForEach(this.list, (item: Category,index:number) => {Text(item.name).width('100%').fontSize(14).textAlign(TextAlign.Center).height(50).backgroundColor(this.activeIndex===index?$r("app.color.white") : $r("app.color.left_back_color")).onClick(() => {this.activeIndex = index})})}.width(90).height('100%').backgroundColor($r("app.color.left_back_color"))// 右侧内容List() {if(this.list.length>0){ForEach(this.list[this.activeIndex].foods, (food:FoodItem) => {ListItem() {MTFoodItem({food:food})}})}else{ListItem(){Text('暂无商品~').width('100%').padding(20).textAlign(TextAlign.Center).fontColor($r('app.color.left_back_color'))}}}.layoutWeight(1).backgroundColor('#fff').padding({bottom: 80})}.width('100%').layoutWeight(1).alignItems(VerticalAlign.Top)}
}export default MTMain
- components/MTBottom.ets
import { FoodItem } from '../models'@Component
struct MTBottom {@ConsumeshowCart: boolean@StorageLink('cart_list')cartList: FoodItem[] = []getAllCount () {return this.cartList.reduce((preValue, item) => preValue + item.count, 0).toString()}getAllPrice () {return this.cartList.reduce((preValue, item) => preValue + item.count * item.price, 0).toFixed(2)}build() {Row() {Row() {Badge({value: '0',position: BadgePosition.Right,style: {badgeSize: 18}}) {Image($r('app.media.ic_public_cart')).width(48).height(70).position({y: -20,})}.margin({left:25,right:10}).onClick(() => {this.showCart = !this.showCart})Column() {Text(){// span imageSpanSpan("¥").fontSize(12)Span("0.00").fontSize(24)}.fontColor($r("app.color.white"))Text("预估另需配送费¥5元").fontColor($r("app.color.search_font_color")).fontSize(14)}.alignItems(HorizontalAlign.Start).layoutWeight(1)Text("去结算").height(50).width(100).backgroundColor($r("app.color.main_color")).textAlign(TextAlign.Center).borderRadius({topRight: 25,bottomRight: 25})}.height(50).width('100%').backgroundColor($r('app.color.bottom_back')).borderRadius(25)}.width('100%').padding(20)}
}export default MTBottom
- components/MTCart.ets
import { FoodItem } from '../models'
import MTCartItem from './MTCartItem'
@Component
struct MTCart {@ConsumeshowCart:boolean@StorageLink('cart_list')cartList:FoodItem[] = []build() {Column() {Blank().backgroundColor('rgba(0,0,0,0.5)').onClick(()=>{this.showCart = false})Column() {Row() {Text('购物车').fontSize(12).fontWeight(600)Text('清空购物车').fontSize(12).fontColor($r("app.color.search_font_color"))}.width('100%').height(40).justifyContent(FlexAlign.SpaceBetween).border({ width: { bottom: 0.5 }, color: $r("app.color.left_back_color") }).margin({ bottom: 10 }).padding({ left: 15, right: 15 })List({ space: 30 }) {ForEach(this.cartList, (item:FoodItem) => {ListItem() {MTCartItem({item:item})}},(item:FoodItem)=>item.id.toString())}.divider({strokeWidth: 0.5,color: $r("app.color.left_back_color")}).padding({ left: 15, right: 15, bottom: 100 })}.backgroundColor($r("app.color.white")).borderRadius({topLeft: 16,topRight: 16})}.height('100%').width('100%')}
}
export default MTCart
- components/MTFoodItem.ets
import { FoodItem } from '../models'
import MTAddCut from './MTAddCut'@Preview
@Component
struct MTFoodItem {food:FoodItem = new FoodItem()build() {Row() {Image(this.food.picture).width(90).aspectRatio(1)Column({ space: 5 }) {Text(this.food.name).textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(2).fontWeight(600)Text(this.food.description).textOverflow({overflow: TextOverflow.Ellipsis,}).maxLines(1).fontSize(12).fontColor($r("app.color.food_item_second_color"))Text(this.food.tag).fontSize(10).backgroundColor($r("app.color.food_item_label_color")).fontColor($r("app.color.font_main_color")).padding({ top: 2, bottom: 2, right: 5, left: 5 }).borderRadius(2)Text() {Span('月销售'+this.food.month_saled)Span(' ')Span(`好评度${this.food.like_ratio_desc}%`)}.fontSize(12).fontColor($r("app.color.black"))Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span(this.food.price.toString()).fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}MTAddCut({food:this.food})}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.padding(10).alignItems(VerticalAlign.Top)}
}
export default MTFoodItem
- components/MTCartItem.ets
import { FoodItem } from '../models'
import MTAddCut from './MTAddCut'@Component
struct MTCartItem {item:FoodItem = new FoodItem()build() {Row() {Image(this.item.picture).width(60).aspectRatio(1).borderRadius(8)Column({ space: 5 }) {Text(this.item.name).fontSize(14).textOverflow({overflow: TextOverflow.Ellipsis}).maxLines(2)Row() {Text() {Span('¥ ').fontColor($r("app.color.font_main_color")).fontSize(10)Span(this.item.price.toString()).fontColor($r("app.color.font_main_color")).fontWeight(FontWeight.Bold)}MTAddCut({food:this.item})}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10, right: 10 })}.alignItems(VerticalAlign.Top)}
}
export default MTCartItem
- components/MTAddCut.ets
import { FoodItem } from '../models'
import { CartStore } from '../utils/CartCalcClass'@Preview
@Component
struct MTAddCut {@StorageLink('cart_list')cartList: FoodItem[] = []food: FoodItem = new FoodItem()getCount(): number {const index = this.cartList.findIndex(listItem => listItem.id === this.food.id)return index < 0 ? 0 : this.cartList[index].count}build() {Row({ space: 8 }) {Row() {Image($r('app.media.ic_screenshot_line')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.white")).borderRadius(4).border({ width: 0.5, color: $r("app.color.main_color") }).visibility(this.getCount()>0?Visibility.Visible:Visibility.Hidden).onClick(() => {CartStore.addCutCart(this.food, false)})Text(this.getCount().toString()).fontSize(14)Row() {Image($r('app.media.ic_public_add_filled')).width(10).aspectRatio(1)}.width(16).aspectRatio(1).justifyContent(FlexAlign.Center).backgroundColor($r("app.color.main_color")).borderRadius(4).onClick(() => {CartStore.addCutCart(this.food)})}}
}export default MTAddCut
- api/index.ets
import { http } from '@kit.NetworkKit'
import { Category } from '../models'export const getAllData = async () => {const req = http.createHttp()const res = await req.request(" https://zhousg.atomgit.net/harmonyos-next/takeaway.json")return JSON.parse(res.result as string) as Category[]
}
- models/index.ets
export class FoodItem {id: number = 0name: string = ""like_ratio_desc: string = ""food_tag_list: string[] = []price: number = 0picture: string = ""description: string = ""tag: string = ""month_saled: number = 0count: number = 0
}
export class Category {tag: string = ""name: string =""foods: FoodItem[] = []
}
- utils/index.ets
import { FoodItem } from '../models'
PersistentStorage.persistProp('cart_list', [])
export class CartStore {static addCutCart(item: FoodItem, flag: boolean = true) {const list = AppStorage.get<FoodItem[]>('cart_list')!const index = list.findIndex(listItem => listItem.id === item.id)if (flag) {if (index < 0) {item.count = 1// 新增list.unshift(item)} else {list[index].count++list.splice(index, 1,list[index])}} else {list[index].count--// 如果减到0就删掉if (list[index].count === 0){list.splice(index, 1)}else{list.splice(index, 1,list[index])}}AppStorage.setOrCreate('cart_list',list)}static clearCarts () {AppStorage.set<FoodItem[]>("cart_list", [])}
}