自适应网站建设价格wordpress 热门标签
1. 购物车
咱们购物车基于 V2 装饰器进行开发,底气来源于
自定义组件混用场景指导
1.1. 素材整合
observedv2和Trace
- 数据模型和页面
// 其他略
// 购物车
export interface CartGoods {count: number;id: string;name: string;picture: string;price: number;selected: boolean;skuId: string;stock: number;attrsText: string;
}@ObservedV2
export class CartGoodsModel implements CartGoods {@Tracecount: number = 0id: string = ''name: string = ''picture: string = ''price: number = 0@Traceselected: boolean = falseskuId: string = ''stock: number = 0attrsText: string = ''constructor(model: CartGoods) {this.count = model.countthis.id = model.idthis.name = model.namethis.picture = model.picturethis.price = model.pricethis.selected = model.selectedthis.skuId = model.skuIdthis.stock = model.stockthis.attrsText = model.attrsText}
}
- 静态页面
import { auth, MkGuess, MkNavbar, MkUser, CartGoodsModel, CartGoods } from 'basic'@Component
export struct CartView {// 用户信息@StorageProp(auth.KEY) user: MkUser = {} as MkUser// 顶部安全区域@StorageProp('safeTop') safeTop: number = 0// ObserveV2 无法直接和@State 使用 会报错 但是包到数组中是可以使用的// @State cartData: CartGoodsModel = new CartGoodsModel({} as CartGoods)// 商品信息@State cartList: CartGoodsModel[] = []aboutToAppear(): void {}onCheckOrder() {AlertDialog.show({message: '去结算'})}@BuilderDeleteBuilder(onDelete: () => void) {Text('删除').fontSize(14).width(60).height(100).backgroundColor($r('[basic].color.red')).fontColor($r('[basic].color.white')).textAlign(TextAlign.Center).onClick(() => {onDelete()})}build() {Column() {MkNavbar({ title: '购物袋', showLeftIcon: false, showRightIcon: true }).border({width: { bottom: 0.5 },color: '#e4e4e4'})List() {if (this.user.token) {if (this.cartList.length) {ForEach(this.cartList, (cart: CartGoodsModel) => {ListItem() {CartItemComp({cart})}.backgroundColor($r('[basic].color.under')).padding({ left: 8, right: 8 }).transition({ type: TransitionType.Delete, opacity: 0 }).swipeAction({end: this.DeleteBuilder(async () => {AlertDialog.show({message: 'clickDel'})})})})} else {ListItem() {Text('无商品')}}} else {// 未登录ListItem() {Text('未登录')}}ListItem() {MkGuess().margin({ top: 8, bottom: 8 })}}.contentStartOffset(8).width('100%').layoutWeight(1).scrollBar(BarState.Off)if (this.cartList.length) {Row() {// TODO 添加自定义 CheckBoxText('全选').fontSize(14).fontColor($r('[basic].color.black')).margin({ right: 20 })Text('合计:').fontSize(14).fontColor($r('[basic].color.black')).margin({ right: 2 })Text('998').fontSize(16).fontWeight(500).fontColor($r('[basic].color.red')).layoutWeight(1)Button('去结算').fontSize(14).height(36).backgroundColor($r('[basic].color.red')).onClick(() => {})}.height(50).width('100%').backgroundColor($r('[basic].color.white')).border({width: { top: 0.5, bottom: 0.5 },color: '#e4e4e4'}).padding({ left: 16, right: 16 })}}.height('100%').width('100%').backgroundColor($r('[basic].color.under')).padding({ top: this.safeTop })}
}@ComponentV2
struct CartItemComp {@Param // v1 Prop 自己的状态 @Localcart: CartGoodsModel = new CartGoodsModel({} as CartGoods)build() {Row({ space: 10 }) {// TODO 添加自定义 CheckBoxImage(this.cart.picture).width(80).height(80)Column({ space: 8 }) {Text(this.cart.name).textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize(14).fontColor($r('[basic].color.black')).width('100%')Row() {Text(this.cart.attrsText).textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize(12).fontColor($r('[basic].color.text')).layoutWeight(1)Image($r('sys.media.ohos_ic_public_arrow_down')).fillColor($r('[basic].color.gray')).width(16).height(16)}.padding({ left: 6, right: 4 }).width(100).height(24).backgroundColor($r('[basic].color.under')).borderRadius(2)Row() {Text(`¥${this.cart.price}`).fontSize(14).fontWeight(500)Counter()}.width('100%').justifyContent(FlexAlign.SpaceBetween)}.layoutWeight(1).alignItems(HorizontalAlign.Start)}.width('100%').height(100).padding(10).border({ width: { bottom: 0.5 }, color: '#e4e4e4' }).backgroundColor($r('[basic].color.white'))}
}
- 在 oh-package.json5 文件中导入 common/basic
{"name": "cart","version": "1.0.0","description": "Please describe the basic information.","main": "Index.ets","author": "","license": "Apache-2.0","packageType": "InterfaceHar","dependencies": {"basic": "file:../../commons/basic"}
}
git 记录
Cart-素材整合
1.2. MkEmpty 组件封装(V2实现)
V2装饰器
先来搞定购物车中间部分的显示效果,通过组件来完成
核心步骤:
- 创建组件并暴露
- 根据文档实现功能
- 页面中导入并测试使用
-
- 已登录,无商品,点击去第一个 Tab(下一节做)
- 未登录,点击去登录页
基础模版
@ComponentV2export struct MkEmpty {build() {Column({ space: 20 }) {Image($r('app.media.ic_public_empty')).width(120).aspectRatio(1)Text('信息').fontSize(12).fontColor($r('app.color.gray'))Button('去逛逛').fontSize(12).height(26).padding({ left: 20, right: 20 }).linearGradient({angle: 90,colors: [['#FD3F8F', 0], ['#FF773C', 1]]}).margin({ bottom: 20 })}.padding(40).width('100%')}}
属性名 | 类型 | 说明 | 默认值 |
tip | string | 提示信息 | 空字符串 |
buttonText | string | 按钮文本 | 空字符串 |
icon | ResourceStr | 中间图标 | ic_public_empty |
onClickButton | ()=>void | 点击按钮的逻辑 | 空方法 |
参考代码
@ComponentV2
export struct MkEmpty {@Paramtip: string = '空空如也'@ParambuttonText: string = '去逛逛'@Paramicon: ResourceStr = $r('app.media.ic_public_empty')@EventonClickButton: () => void = () => {}build() {Column({ space: 20 }) {Image(this.icon).width(120).aspectRatio(1)Text(this.tip).fontSize(12).fontColor($r('app.color.gray'))Button(this.buttonText).fontSize(12).height(26).padding({ left: 20, right: 20 }).linearGradient({angle: 90,colors: [['#FD3F8F', 0], ['#FF773C', 1]]}).margin({ bottom: 20 }).onClick(() => {this.onClickButton()})}.padding(40).width('100%')}
}
List() {if (this.user.token) {if (this.cartList.length) {// 略} else {ListItem() {MkEmpty({tip: '购物袋是空的~',buttonText: '去逛逛',onClickButton: () => {// 去第一个 Tab}})}}} else {// 未登录ListItem() {MkEmpty({tip: '您还未登陆~',buttonText: '请先登录',onClickButton: () => {// 去登录页}})}}ListItem() {MkGuess().margin({ top: 8, bottom: 8 })}
}
git记录
Cart-MkEmpty组件封装
1.3. 去首页
使用Emitter进行线程间通信
需求及分析:
- 已登录,购物车没有商品的时候,点击切换 tab 到第一个【首页】
- feature/cart -> products/phone 可以通过??? 来实现通讯
- products/phone 如何控制 tab 切换
核心步骤:
- 增加 emmit 的事件 id 常量,并导出 (方便管理)
- phone/index 注册事件,接收参数
- feature/cart 触发事件,传递数据
- phone/index 控制 tab 切换
// 其他略
export class EmitterKey {// 切换到指定的 tabstatic CHANGE_TAB: emitter.InnerEvent = { eventId: 10001 }
}
aboutToAppear(): void {this.registerEvent()
}// 注册 emmiter
registerEvent() {// 订阅事件emitter.on(EmitterKey.CHANGE_TAB, (eventData) => {this.activeIndex = eventData.data?.index || 0 // 不传值也能回首页})
}Tabs({ barPosition: BarPosition.End, index: this.activeIndex }){// 其他略
}
MkEmpty({tip: '购物袋是空的',buttonText: '去逛逛',onClickButton: () => {// 发送事件emitter.emit(EmitterKey.CHANGE_TAB, {data: {index: 0}})}
})
git 记录
Cart-去首页
1.4. 加入购物车
接口文档
需求:
- 详情页点击添加到购物车
- 更新服务器数据
核心步骤:
- 抽取 api,丢到 common
- 页面实现交互逻辑
-
- token 校验-》sku 校验-》提交-》api调用-》
- 通过一个类管理所有购物车接口,因为有一整套的 CRUD
import { RequestAxios } from '../utils/Request'interface AddParams {skuId: stringcount: number
}class Cart {// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}
}export const cart = new Cart()
- 导出
// 导出购物车封装购物车请求 API 的类,这个类只需导出一次即可
export { cart } from './src/main/ets/apis/cart'
- 整合业务,在商品详情页调用 加入购物车 接口
// 加入购物车async addToCart() {// 1. 判断用户是否登录,未登录需要登录const token = auth.getUser().tokenif (!token) {promptAction.showToast({ message: '请登录后下单' })this.pageStack.pushPath({ name: 'LoginView' })return}// 2. 判断用户是否有选择商品规则(是否完整)if (!this.sku.id) {promptAction.showToast({ message: '请选择要购买的商品规格' })return}// 3. 加入购物车this.loading = true// await 等待请求成功await cart.add({ skuId: this.sku.id, count: this.count })this.loading = falsepromptAction.showToast({ message: '加入购物袋成功' })// 4. 关闭半模态弹窗this.showSheet = false}// 其他略
Button(this.loading ? '加入中...' : '加入购物袋').buyButton($r('[basic].color.black'), true).onClick(async () => {this.addToCart()})
git 记录
Cart-加入购物车
1.5. 个数及页面跳转
多个地方需要用到个数,并且在很多时候都需要刷新
1.5.1. 详情页面个数更新
需求:
- 获取最新个数(通过接口)
- 更新购物车个数(考虑其他界面获取)
核心步骤:
- 抽取 api:
-
- 有 Token
-
-
- 获取数据,具体的值
-
-
- 没 Token
-
-
- 格式为 0
-
-
- 更新到AppStorage
- 组件中获取并渲染
import { RequestAxios } from '../utils/Request'interface AddParams {skuId: stringcount: number
}interface CountResult {count: number
}export class Cart {// AppStorage 的 KeyCartKey: string = 'cartCount'// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}// 获取购物车数量async count() {let count = 0// 用户已登录才发请求获取const userInfo = auth.getUser()if (userInfo.token) {// 获取购物车数量const res = await RequestAxios.get<CountResult>('/member/cart/count')// 更新变量,用于全局存储count = res.count}// 全局存储购物车数量AppStorage.setOrCreate(this.CartKey, count)}
}export const cart = new Cart()
// 购物车商品数量
@StorageProp(cart.CartKey) cartCount: number = 0Badge({count: this.cartCount,style: {},position: { x: 30, y: 4 }
}) /*** 加入购物车* */
async addToCart() {// 登录判断if (!auth.getUser().token) {promptAction.showToast({message: '请登录后下单'})return pathStack.pushPathByName('LoginView', null)}// 是否选择商品if (!this.sku.id) {return promptAction.showToast({message: '请选择商品规格'})}this.loading = true// 加入购物车await cart.add({skuId: this.sku.id,count: this.count})promptAction.showToast({message: '加入购物袋成功'})// 关闭半模态this.showSheet = falsethis.loading = false// 更新数量cart.count()
}Button(this.loading ? '加入中...' : '加入购物袋').buyButton($r('app.color.black'), true).onClick(async () => {this.addToCart()})
git 记录
Cart-详情页个数更新
1.5.2. 首页更新数量
需求:
- 首页增加角标
- 获取个数并渲染
- 如果是首次打开页面,获取个数(已登录的情况)
核心步骤:
- 增加 Badge 组件
- 通过 AppStorage 获取数据并渲染
- 生命周期钩子中获取数据
- 更新首页购物车数量
@StorageProp(cart.CartKey) cartCount: number = 0aboutToAppear(): void {this.breakpointSystem.register()// 获取购物车数量cart.count()
}@BuilderTabItemBuilder(item: TabItem, index: number) {Badge({count: index === 2 ? this.cartCount : 0,style: {},}) {Column() {Image(this.activeIndex === index ? item.active : item.normal).width(24).aspectRatio(1)Text(item.text).fontColor($r('app.color.black')).fontSize(12)}.justifyContent(FlexAlign.SpaceEvenly).height(50)}
}
git记录
Cart-首页个数更新
1.5.3. 登录和登出更新数量
登录和登出:
- 登录更新:
-
- 账号密码登录,华为登录之后,保存用户信息,同时更新购物车数量
- 登出更新:
-
- 设置页,退出登录时,删除用户信息,同时更新购物车数量
- cart.count() --> 0
- 保存用户信息、删除用户信息时,都要更新购物车数量
import { cart } from "../apis/cart"export interface MkUser {token: stringnickname: stringavatar: stringaccount: string
}class Auth {KEY: string = 'user'initUser() {// 把 AppStorage 的用户信息进行持久化 UI 状态存储,状态改变自动同步到本地磁盘PersistentStorage.persistProp(this.KEY, {})}getUser() {return AppStorage.get<MkUser>(this.KEY) || {} as MkUser}saveUser(user: MkUser) {AppStorage.setOrCreate<MkUser>(this.KEY, user)// 登录时,更新购物车数量cart.count()}removeUser() {AppStorage.setOrCreate<MkUser>(this.KEY, {} as MkUser)// 登出时,更新购物车数量cart.count()}
}// 导出实例化对象,单例
export const auth = new Auth()
git记录
Cart-登录登出更新购物车数量
1.6. 跳转购物车页
需求:
- 点击购物车图标,打开购物袋页面
- tabs打开时,没有返回按钮
- 跳转打开时,显示返回按钮
注意:
- 之前购物袋只是 Tabs 渲染的组件,并不是页面,没有注册页面路由。
核心步骤:
- feature/CartView
-
- 增加 Builder 入口函数 和 NavDestination 组件
- 配置路由表 route_map.json
- 配置 module.json5
- 详情页,点击跳转即可
-
- 优化:详情页跳转的时候【显示返回按钮】,并可以点击【返回上一页】
- 通过携带参数实现效果
- 增加 Builder 入口函数 和 NavDestination 组件
// 1.1 路由入口函数
@Builder
function CartViewBuilder() {// 1.2 子路由组件NavDestination() {CartView()}.hideTitleBar(true)
}@Component
export struct CartView {}
- 新建路由表文件,并配置路由表
{"routerMap": [{"name": "CartView","pageSourceFile": "src/main/ets/views/CartView.ets","buildFunction": "CartViewBuilder"}]
}
- 配置 module.json5,让模块的路由表生效
{"module": {"name": "cart","type": "shared","description": "$string:shared_desc","deviceTypes": ["phone","tablet","2in1"],"deliveryWithInstall": true,"pages": "$profile:main_pages","routerMap": "$profile:route_map"}
}
- 页面跳转,并传参
// 购物袋角标
Badge({count: this.cartCount,style: {},position: { x: 30, y: 4 }
}) {Image($r('[basic].media.ic_public_cart')).iconButton().onClick(() => {pathStack.pushPathByName('CartView', true)})
}
- 获取参数
@State isShowLeftIcon: boolean = falseaboutToAppear(): void {const params = pathStack.getParamByName('CartView') as boolean[]if (params.length > 0) {this.isShowLeftIcon = params.pop() as boolean}
}// 其他内容略
MkNavbar({title: '购物袋',showRightIcon: true,showLeftIcon: this.isShowLeftIcon,leftClickHandler: () => {pathStack.pop()}
})
git 记录
Cart-去购物车页
1.7. 购物车列表渲染
接口文档
需求:
- 获取购物车数据并渲染
核心步骤:
- 抽取接口
- 获取数据之后通过 new 转对象(嵌套数据更新)
- 这个过程需要在 2 种情况下执行(分两步完成)
-
- 首次进入,有 token 就获取(生命周期钩子)
- 添加商品到购物车中(详情页--》购物车页)
1.7.1. 默认渲染
import { RequestAxios } from '../utils/Request'
import { CartGoods } from '../viewmodel'interface AddParams {skuId: stringcount: number
}interface CountResult {count: number
}class Cart {CartKey: string = 'cartCount'// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}// 获取个数async count() {const res = await RequestAxios.get<CountResult>('/member/cart/count')AppStorage.setOrCreate(this.CartKey, res.data.result.count)return res.data.result.count}// 获取列表async list() {return RequestAxios.get<CartGoods[]>('/member/cart')}}export const cart = new Cart()
aboutToAppear(): void {const params = pathStack.getParamByName('CartView') as boolean[]if (params.length > 0) {this.isShowLeftIcon = params.pop() as boolean}this.getData()
}async getData() {if (!auth.getUser().token) {return}const res = await cart.list()this.cartList = res.map((item) => new CartGoodsModel(item))
}
git 记录
Cart-默认渲染
1.7.2. 更新购物车列表
组件可见区域变化事件
问题分析:
- 购物车,每次打开重新获取列表数据
-
- 不是页面,只有 aboutToAppear 钩子
- 第一次打开之后就不会被销毁,不会再次触发
- 详情页
-
- 跳转去购物车是新页面,会触发 aboutToAppear 可以刷新
- tab 页
-
- 切换到 购物车 tab 时需要获取列表(不会再次触发aboutToAppear)
思考:如何实现 购物车页面数据刷新?
- 通过 emitter 触发刷新
- 组件可视区域改变时刷新 onVisibleAreaChange
@Component
export struct CartView {// ...省略aboutToAppear(): void {const params = pathStack.getParamByName('CartView') as boolean[]if (params.length > 0) {this.isShowLeftIcon = params.pop() as boolean}// this.getData() 拿掉}// 获取购物袋列表async getData() {// 用户已登录的情况下,才获取购物袋列表const userInfo = auth.getUser()if (userInfo.token) {// 显示加载框this.dialog.open()const res = await cart.list()// map 把每个普通对象转换成 CartGoodsModel 对象,更新对象时图片不会闪动this.cartList = res.data.result.map(item => new CartGoodsModel(item))// 隐藏加载框this.dialog.close()}}// ...省略build() {Column() {// ...}.height('100%').width('100%').backgroundColor($r('[basic].color.under')).onVisibleAreaChange([0, 1], (isVisible) => {if (isVisible) {this.getData()}})}
}
git 记录
Cart-更新购物车列表
1.8. 个数更新
接口文档
需求:
- 修改【个数】时
- 更新服务器,更新本地
- 更新购物车个数
核心步骤:
- 实现Count组件的个数渲染及禁用外观
- 累加及递减逻辑:
-
- 修改服务器数据(*)
- 更新本地数据
- 更新个数
- 伴随 loading 效果
- 选中:
-
- 修改服务器数据(*)
- 更新本地数据
import { auth } from '../../../../Index'
import { RequestAxios } from '../utils/Request'
import { CartGoods } from '../viewmodel'// 导出 页面中 会用到
export interface UpdateParams {selected?: booleancount?: number
}interface AddParams {skuId: stringcount: number
}interface CountResult {count: number
}export const CartKey: string = 'cartCount'export class Cart {// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}// 获取个数async count() {let count = 0if (auth.getUser().token) {const res = await RequestAxios.get<CountResult>('/member/cart/count')count = res.data.result.count}AppStorage.setOrCreate(CartKey, count)}// 获取列表async list() {return RequestAxios.get<CartGoods[]>('/member/cart')}// 更新商品async update(skuId: string, data: UpdateParams) {return RequestAxios.put<CartGoods>(`/member/cart/${skuId}`, data)}
}export const cart = new Cart()
// 其他略
async updateCart(params: UpdateParams) {await cart.update(this.cart.skuId, params)// 如果是更新个数 不传递时为 undefinedif (params.count) {this.cart.count = params.countcart.count()}
}// 计数器
Counter() {Text(this.cart.count.toString())
}
.enableDec(this.cart.count > 1) // 控制减号是否可用
.enableInc(this.cart.count < this.cart.stock) // 控制加号是否可用
.onInc(() => {this.updateCart({ count: this.cart.count + 1 })
})
.onDec(() => {this.updateCart({ count: this.cart.count - 1 })
})
git 记录
Cart-个数及选中状态更新
1.9. MkCheckBox组件
checkBox
原生的 CheckBox 使用起来有一些不便,咱们来:
- 试试这个不便之处
- 然后自己实现个组件,替换到 购物车页面的 【列表项】和【全选】
如果不用双向绑定那么 选中状态 和 绑定的状态变量就没有关联
@Entry
@Component
struct Page06 {@State isChecked: boolean = falsebuild() {Column() {Text(JSON.stringify(this.isChecked))Checkbox().select(this.isChecked).onClick(() => {this.isChecked = true})}.height('100%').width('100%').backgroundColor(Color.Orange).padding(40)}
}
基础模版
@ComponentV2
export struct MkCheckBox {build() {// $r('app.media.ic_public_check_filled')选中 $r('app.media.ic_public_check') 未选中Image($r('app.media.ic_public_check_filled')).width(18)// $r('app.color.red') 选中 $r('app.color.gray') 默认.fillColor($r('app.color.red')).aspectRatio(1).margin(2)}
}
// 其他略 记得导出
export { MkCheckBox } from './src/main/ets/components/MkCheckBox'
// 添加全选 其他略
MkCheckBox({
})
Text('全选').fontSize(14).fontColor($r('[basic].color.black')).margin({ right: 20 })@ComponentV2
struct CartItemComp {@Paramcart: CartGoodsModel = new CartGoodsModel({} as CartGoods)async updateCart(params: UpdateParams) {await cart.update(this.cart.skuId, params)if (params.selected !== undefined) {this.cart.selected = params.selected}if (params.count) {this.cart.count = params.countcart.count()}}build() {Row({ space: 10 }) {// 添加单选 其他略MkCheckBox({})Image(this.cart.picture).width(80).height(80)}.width('100%').height(100).padding(10).border({ width: { bottom: 0.5 }, color: '#e4e4e4' }).backgroundColor($r('[basic].color.white'))}
}
属性名 | 类型 | 说明 | 默认值 |
checked | boolean | 是否选中 | false |
boxSize | number | 大小 | 18 |
onClickBox | ()=>void | 点击组件 | ()=>{} |
@ComponentV2
export struct MkCheckBox {@Param checked: boolean = false@Param boxSize: number = 18@EventonClickBox: () => void = () => {}build() {Image(this.checked ? $r('app.media.ic_public_check_filled') : $r('app.media.ic_public_check')).width(this.boxSize).fillColor(this.checked ? $r('app.color.red') : $r('app.color.gray')).aspectRatio(1).margin(2).onClick(() => this.onClickBox())}
}
git记录
Cart-MkCheckBox组件封装
1.10. 选中状态更新
接口文档
需求:
- 修改【选中状态】时
- 更新服务器,更新本地
核心步骤:
- 点击选中:
-
- 修改服务器数据(*)
- 更新本地数据
@ComponentV2
struct CartItemComp {@Paramcart: CartGoodsModel = new CartGoodsModel({} as CartGoods)async updateCart(params: UpdateParams) {await cart.update(this.cart.skuId, params)if (params.selected !== undefined) {this.cart.selected = params.selected}if (params.count) {this.cart.count = params.countcart.count()}}build() {Row({ space: 10 }) {MkCheckBox({checked: this.cart.selected, onClickBox: () => {this.updateCart({ selected: !this.cart.selected })}})Image(this.cart.picture).width(80).height(80)Column({ space: 8 }) {Text(this.cart.name).textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize(14).fontColor($r('[basic].color.black')).width('100%')Row() {Text(this.cart.attrsText).textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize(12).fontColor($r('[basic].color.text')).layoutWeight(1)Image($r('sys.media.ohos_ic_public_arrow_down')).fillColor($r('[basic].color.gray')).width(16).height(16)}.padding({ left: 6, right: 4 }).width(100).height(24).backgroundColor($r('[basic].color.under')).borderRadius(2)Row() {Text(`¥${this.cart.price}`).fontSize(14).fontWeight(500)Counter() {Text(this.cart.count.toString())}.enableDec(this.cart.count > 0).onInc(() => {this.updateCart({ count: this.cart.count + 1 })}).onDec(() => {this.updateCart({ count: this.cart.count - 1 })})}.width('100%').justifyContent(FlexAlign.SpaceBetween)}.layoutWeight(1).alignItems(HorizontalAlign.Start)}.width('100%').height(100).padding(10).border({ width: { bottom: 0.5 }, color: '#e4e4e4' }).backgroundColor($r('[basic].color.white'))}
}
git记录
Cart-选中状态更新
1.11. 全选
接口文档
Array.prototype.every() - JavaScript | MDN
需求:
- 点击列表项目,同步更新全选状态
- 点击全选,批量设置每一项的选中状态
核心步骤:
- 计算全选状态并绑定(方法):
-
- 返回每一项是否被选中(selected==true)(every)
- 点击子组件(CheckBox)通过回调函数通知父组件(强制刷新页面)
- 抽取 api
- 点击全选:
-
- 获取本地选中状态
- 调用接口更新服务器
- 设置列表项的选中状态
import { auth } from '../../../../Index'
import { RequestAxios } from '../utils/Request'
import { CartGoods } from '../viewmodel'interface UpdateParams {selected?: booleancount?: number
}interface AddParams {skuId: stringcount: number
}interface CountResult {count: number
}interface CheckAllParams {selected: boolean
}export const CartKey: string = 'cartCount'export class Cart {// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}// 获取个数async count() {let count = 0if (auth.getUser().token) {const res = await RequestAxios.get<CountResult>('/member/cart/count')count = res.data.result.count}AppStorage.setOrCreate(CartKey, count)}// 获取列表list() {return RequestAxios.get<CartGoods[]>('/member/cart')}// 更新商品update(skuid: string, data: UpdateParams) {return RequestAxios.put<CartGoods>(`/member/cart/${skuid}`, data)}// 全选checkAll(data: CheckAllParams) {return RequestAxios.put<null>('/member/cart/selected', data)}
}export const cart = new Cart()
// 全选
MkCheckBox({checked: this.cartList.every(v => v.selected),onClickBox: async () => {const isCheckedAll = this.cartList.every(v => v.selected)await cart.checkAll({ selected: !isCheckedAll })this.cartList.forEach(v => v.selected = !isCheckedAll)}})
git 记录
Cart-全选
1.12. 总价格
链接
需求:
- 根据选中状态,个数,单价计算总价格
核心步骤:
- 实现方法:
-
- 筛选-》累加-》小数点 2 位-》转字符串
- 组件中使用该方法
// 定义计算总价的函数
/*** 总价格* */totalCount() {return this.cartList.filter(v => v.selected).reduce((acc, cur) => acc + cur.price * cur.count, 0).toFixed().toString()}Text('合计:').fontSize(14).fontColor($r('[basic].color.black')).margin({ right: 2 })
Text(this.totalCount()).fontSize(16).fontWeight(500).fontColor($r('[basic].color.red')).layoutWeight(1)
git 记录
Cart-总价格
1.13. 删除
接口文档
需求:
- 点击删除,删除购物车数据
核心步骤:
- 抽取接口(坑,delete 请求方法第二个参数和之前的不同)
- 点击删除调用接口,删除服务器数据
- 更新个数
- 删除本地
- 伴随 loading 效果
export class RequestAxios {// get -> params -> { params: {} }// T 响应的内容 的类型!!static get<T>(url: string, params?: object): Promise<T> {return axiosInstance.get<null, T>(url, { params })}static getPlus<T>(url: string, config?: AxiosRequestConfig): Promise<T> {return axiosInstance.get<null, T>(url, config)}// post -> data -> { data: {} }static post<T>(url: string, data?: object): Promise<T> {return axiosInstance.post<null, T>(url, data)}static delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {return axiosInstance.delete<null, T>(url, config)}static put<T>(url: string, data?: object): Promise<T> {return axiosInstance.put<null, T>(url, data)}
}
import { Log } from '@abner/log'
import { auth, Logger } from '../../../../Index'
import { RequestAxios } from '../utils/Request'
import { CartGoods } from '../viewmodel'interface UpdateParams {selected?: booleancount?: number
}interface AddParams {skuId: stringcount: number
}interface CountResult {count: number
}interface CheckAllParams {selected: boolean
}// 删除的参数
interface DeleteParams {ids: string[]
}export const CartKey: string = 'cartCount'export class Cart {// 添加add(data: AddParams) {return RequestAxios.post<null>('/member/cart', data)}// 获取个数async count() {let count = 0if (auth.getUser().token) {const res = await RequestAxios.get<CountResult>('/member/cart/count')count = res.data.result.count}AppStorage.setOrCreate(CartKey, count)}// 获取列表async list() {return RequestAxios.get<CartGoods[]>('/member/cart')}// 更新商品async update(skuid: string, data: UpdateParams) {return RequestAxios.put<CartGoods>(`/member/cart/${skuid}`, data)}// 全选async checkAll(data: CheckAllParams) {return RequestAxios.put<null>('/member/cart/selected', data)}// 删除async remove(data: DeleteParams) {return RequestAxios.delete<null>('/member/cart', { data: data })}
}export const cart = new Cart()
// 其他略
ForEach(this.cartList,(item: CartGoodsModel, index: number) => {ListItem() {CartItemComp({cart: item})}.backgroundColor($r('[basic].color.under')).padding({ left: 8, right: 8 }).transition({ type: TransitionType.Delete, opacity: 0 }).swipeAction({end: this.DeleteBuilder(async () => {AlertDialog.show({title: '温馨提示',message: '确定删除该商品吗?',buttons: [{value: '取消', fontColor: $r('[basic].color.gray'), action: () => {}},{value: '确定', fontColor: $r('[basic].color.black'), action: async () => {// 服务器端删除,根据 skuId 删除商品await cart.remove({ ids: [item.skuId] })// 本地删除this.cartList.splice(index, 1)// 更新购物袋数量await cart.count()}},]})})})})
git 记录
Cart-删除购物袋商品
2. bug-ForEach 的keyGenerator参数
ForEach 键值规则
ForEach的第三个参数如果不给,那么默认值
bug 复现
- 保证购物车有数据,测试交互效果
- 切换到其他tab,再切换购物车,测试交互效果
- 修改选中状态,修改个数,无法触发【全选】 和 【价格】的重新计算
import { promptAction } from '@kit.ArkUI'
import { JSON } from '@kit.ArkTS'@ObservedV2
class Dog {id: number@Tracename: string@Tracechecked: booleanconstructor(id: number, name: string) {this.id = idthis.name = namethis.checked = false}
}@Entry
@Component
struct Parent {@StatesimpleList: Array<Dog> = [new Dog(1, '1狗'),new Dog(2, '2狗'),new Dog(3, '3狗'),];isCheckedAll() {console.log('计算是否全部选中')return this.simpleList.every(v => v.checked)}build() {Row() {Column() {Button('打印数据').margin(10).onClick(() => {promptAction.showToast({message: JSON.stringify(this.simpleList)})})Text('点击替换成相同的数据').fontSize(24).fontColor(Color.Red).onClick(() => {this.simpleList = this.simpleList.map(v => {return new Dog(v.id, v.name)})})Text(this.isCheckedAll() ? '全部选中' : '未全选').fontSize(24).fontColor(Color.Blue).margin(10)ForEach(this.simpleList, (item: Dog, index: number) => {ChildItem({ item: item }).margin({ top: 20 })}, (item: Dog, index: number) => {return JSON.stringify(item) + ' ' + index})}.justifyContent(FlexAlign.Center).width('100%').height('100%')}.height('100%').backgroundColor(0xF1F3F5)}
}@ComponentV2
struct ChildItem {@Param item: Dog = new Dog(0, '')aboutToAppear(): void {console.log('aboutToAppear' + this.item.id)}build() {Row() {Text(this.item.checked ? '√' : 'x').fontSize(20).margin({ right: 10 }).onClick(() => {this.item.checked = !this.item.checked})Text(this.item.id + '|' + this.item.name).fontSize(20).onClick(() => {this.item.name += '1'})}}
}
结论:
- 默认的键值生成逻辑,在使用相同数据覆盖的时候不会触发子组件的重新生成
- 子组件交互时修改的依旧是上一份数据,但是数据源已经更改所以无法触发 UI 更新
解决方案:
- 自定义 keyGenerator 函数,希望子组件全部重新创建时,使生成结果和上一次不同
- 可以使用时间戳,随机数,累加的数。。核心就是保证 循环的 key 发生改变
优化代码
import { promptAction } from '@kit.ArkUI'
import { JSON } from '@kit.ArkTS'
import { data } from '@kit.TelephonyKit'@ObservedV2
class Dog {id: number@Tracename: string@Tracechecked: booleanconstructor(id: number, name: string) {this.id = idthis.name = namethis.checked = false}
}@Entry
@Component
struct Parent {@StatesimpleList: Array<Dog> = [new Dog(1, '1狗'),new Dog(2, '2狗'),new Dog(3, '3狗'),];num: number = 0isCheckedAll() {console.log('计算是否全部选中')return this.simpleList.every(v => v.checked)}build() {Row() {Column() {Button('打印数据').margin(10).onClick(() => {promptAction.showToast({message: JSON.stringify(this.simpleList)})})Text('点击替换成相同的数据').fontSize(24).fontColor(Color.Red).onClick(() => {this.num = Date.now()// 基于原始数据 生成新数据this.simpleList = this.simpleList.map(v => {return new Dog(v.id, v.name)})})Text(this.isCheckedAll() ? '全部选中' : '未全选').fontSize(24).fontColor(Color.Blue).margin(10)ForEach(this.simpleList, (item: Dog, index: number) => {ChildItem({ item: item }).margin({ top: 20 })}, (item: Dog, index: number) => {console.log('ForEach' + item.id)return JSON.stringify(item)+this.num})}.justifyContent(FlexAlign.Center).width('100%').height('100%')}.height('100%').backgroundColor(0xF1F3F5)}
}@ComponentV2
struct ChildItem {@Param item: Dog = new Dog(0, '')aboutToAppear(): void {console.log('aboutToAppear' + this.item.id)}build() {Row() {Text(this.item.checked ? '√' : 'x').fontSize(20).margin({ right: 10 }).onClick(() => {this.item.checked = !this.item.checked})Text(this.item.id + '|' + this.item.name).fontSize(20).onClick(() => {this.item.name += '1'})}}
}
// 额外添加到随机数中
refreshNum: number = 0/*** 获取数据* */
async getData() {if (!auth.getUser().token) {return}const res = await cart.list()this.cartList = res.map((item) => new CartGoodsModel(item))this.refreshNum = Date.now()
}ForEach(this.cartList,(item: CartGoodsModel, index: number) => {// 略},(item: CartGoodsModel, index: number) => {return JSON.stringify(item) + this.refreshNum})
3. 自定义弹窗优化
3.1. 弹窗错误优化
错误信息
目前使用的弹窗会有错误提示,错误原因是:
- 103302: 内容节点对应自定义弹窗已存在
- 103303:无法找到内容节点对应的自定义弹窗
咱们来优化一下弹框的逻辑
- 多次请求只有第一次弹框
- 多次请求最后一次结束的时候关闭弹框
// 其他略let count =0// 添加请求拦截器
axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {// 如果用户信息中有 token 就在请求头中携带 tokenconst user = auth.getUser()if (user.token) {config.headers.Authorization = `Bearer ${user.token}`}// 对请求数据做点什么if (count === 0) {PromptActionClass.openDialog()}count++return config;
}, (error: AxiosError) => {// 对请求错误做些什么return Promise.reject(error);
});interface ErrorType {message: stringmsg: stringcode: string
}// 添加响应拦截器
axiosInstance.interceptors.response.use((response: AxiosResponse) => {// 对响应数据做点什么count--if (count <= 0) {PromptActionClass.closeDialog()}return response.data.result
}, (error: AxiosError<ErrorType>) => {// 只要 http 状态码 不在 200-299 以内 就会进到这个异常// 400 参数错误 (用户名密码登录 输入错误密码)// Logger.info(error.response?.status)if (error.response?.status === 400) {// Logger.info( error.response.data as object)promptAction.showToast({message: error.response.data.message})} else if (error.response?.status === 401) {// 删除 tokenauth.removeUser()// 提示用户promptAction.showToast({message: error.response.data.message})// 去登陆pathStack.pushPathByName('LoginView', null)}count--if (count <= 0) {PromptActionClass.closeDialog()}// 401 登录过期 (给错误的 token)// 对响应错误做点什么return Promise.reject(error);
});
git 记录
fix-优化全局弹框错误提示