当前位置: 首页 > ops >正文

实现商品列表

HarmonyOS ArkTS提供了丰富的接口和组件,开发者可以根据实际场景和开发需求,选用不同的组件和接口。在本案例中,我们使用Scroll组件、List组件以及LazyForEach组件实现一个商品列表的页面,并且拥有下拉刷新、懒加载和到底提示的效果。

1. 案例效果截图

2. 案例运用到的知识点

2.1. 核心知识点

  • Scroll:可滚动的容器组件,当子组件的布局尺寸超过父组件的视口时,内容可以滚动。
  • List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
  • Tabs:一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
  • LazyForEach:开发框架提供数据懒加载(LazyForEach组件)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

2.2. 其他知识点

  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Provider/@Consumer
  • 自定义组件和组件生命周期
  • 内置组件:Column/Button
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

3. 代码结构

├──entry/src/main/ets                      // 代码区
│  ├──common
│  │  └──CommonConstants.ets               // 常量集合文件
│  ├──entryability
│  │  └──EntryAbility.ets                  // 应用入口,承载应用的生命周期
│  ├──pages
│  │  └──ListIndex.ets                     // 页面入口
│  ├──view
│  │  ├──GoodsListComponent.ets            // 商品列表组件
│  │  ├──PutDownRefreshLayout.ets          // 下拉刷新组件
│  │  └──TabBarsComponent.ets              // Tabs组件
│  └──viewmodel
│     ├──InitialData.ets                   // 初始化数据
│     └──ListDataSource.ets                // List使用的相关数据加载
└──entry/src/main/resources                // 资源文件目录

4. 公共文件与资源

本案例涉及到的常量类代码如下:

// entry/src/main/ets/common/CommonConstants.ets
export const GOODS_LIST_HEIGHT: string = '20%'
export const GOODS_IMAGE_WIDTH: string = '40%'
export const GOODS_FONT_WIDTH: string = '60%'
export const GOODS_LIST_WIDTH: string = '94%'
export const LAYOUT_WIDTH_OR_HEIGHT: string = '100%'// font-size
export const GOODS_LIST_PADDING: number = 8
export const GOODS_EVALUATE_FONT_SIZE: number = 12
export const NORMAL_FONT_SIZE: number = 16
export const BIGGER_FONT_SIZE: number = 20
export const MAX_FONT_SIZE: number = 32// margin
export const REFRESH_ICON_MARGIN_RIGHT: number = 20
export const MARGIN_RIGHT: number = 32// width or height
export const ICON_WIDTH: number = 40
export const ICON_HEIGHT: number = 40// list space
export const LIST_ITEM_SPACE: number = 16// navigation title
export const STORE: string = '商城'// offset range
export const MAX_OFFSET_Y: number = 100// refresh time
export const REFRESH_TIME: number = 1500// Magnification
export const MAGNIFICATION: number = 2
export const MAX_DATA_LENGTH: number = 12

本案例涉及到的资源文件如下:

4.1. string.json

// entry/src/main/resources/base/element/string.json
{"string": [{"name": "entry_desc","value": "description"},{"name": "EntryAbility_desc","value": "description"},{"name": "EntryAbility_label","value": "List组件"},{"name": "selected","value": "精选"},{"name": "mobile_phone","value": "手机"},{"name": "clothes","value": "服饰"},{"name": "wear","value": "穿搭"},{"name": "home_furnishing","value": "家居"},{"name": "goodsName","value": "【新品上市】畅乐冰晶绿低脂新品"},{"name": "another_goodsName","value": "【新品上市】奶茶自然清新亲近自然"},{"name": "advertising_language","value": "重磅推荐,MD新品试用中!"},{"name": "evaluate","value": "6662人评价 95%好评"},{"name": "price_199","value": "¥199"},{"name": "price_265","value": "¥265"},{"name": "price_810","value": "¥810"},{"name": "price_999","value": "¥999"},{"name": "to_bottom","value": "-- 已经到底了 --"},{"name": "refresh_text","value": "正在刷新"}]
}

4.2. color.json

// entry/src/main/resources/base/element/color.json
{"color": [{"name": "white","value": "#FFFFFF"},{"name": "primaryBgColor","value": "#F1F3F5"},{"name": "gray","value": "#989A9C"},{"name": "deepGray","value": "#182431"},{"name": "freshRed","value": "#E92F4F"}]
}

其他资源请到源码中获取。

5. 首页

// entry/src/main/ets/pages/Index.ets
import TabBar from '../view/TabBarsComponent'
import { LAYOUT_WIDTH_OR_HEIGHT, STORE } from '../common/CommonConstants'@Entry
@Component
struct ListIndex {build() {Row() {Navigation() {Column() {TabBar()}.width(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}.size({ width: LAYOUT_WIDTH_OR_HEIGHT, height: LAYOUT_WIDTH_OR_HEIGHT }).title(STORE).titleMode(NavigationTitleMode.Mini)}.height(LAYOUT_WIDTH_OR_HEIGHT).backgroundColor($r('app.color.primaryBgColor'))}
}

6. 实现Tabs

// entry/src/main/ets/view/TabBarsComponent.ets
import { initTabBarData } from '../viewmodel/InitialData'
import {LAYOUT_WIDTH_OR_HEIGHT,NORMAL_FONT_SIZE,BIGGER_FONT_SIZE,MAX_FONT_SIZE
} from '../common/CommonConstants'@Component
export default struct TabBar {@State tabsIndex: number = 0@BuilderfirstTabBar() {Column() {Text($r('app.string.selected')).fontSize(this.tabsIndex === 0 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === 0 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}@BuilderotherTabBar(content: Resource, index: number) {Column() {Text(content).fontSize(this.tabsIndex === index + 1 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === index + 1 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}build() {Tabs() {TabContent() {Scroll() {Column() {Text('商品列表')}.width(LAYOUT_WIDTH_OR_HEIGHT)}.scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT)}.tabBar(this.firstTabBar)ForEach(initTabBarData, (item: Resource, index?: number) => {TabContent() {Column() {Text(item).fontSize(MAX_FONT_SIZE)}.justifyContent(FlexAlign.Center).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT)}.tabBar(this.otherTabBar(item, index !== undefined ? index : 0))})}.onChange((index: number) => {this.tabsIndex = index}).vertical(false)}
}

7. 商品列表和懒加载

7.1. 添加入口

// entry/src/main/ets/view/TabBarsComponent.ets
import { initTabBarData } from '../viewmodel/InitialData'
import {LAYOUT_WIDTH_OR_HEIGHT,NORMAL_FONT_SIZE,BIGGER_FONT_SIZE,MAX_FONT_SIZE
} from '../common/CommonConstants'
import GoodsList from './GoodsListComponent'@Component
export default struct TabBar {@State tabsIndex: number = 0@BuilderfirstTabBar() {Column() {Text($r('app.string.selected')).fontSize(this.tabsIndex === 0 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === 0 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}@BuilderotherTabBar(content: Resource, index: number) {Column() {Text(content).fontSize(this.tabsIndex === index + 1 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === index + 1 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}build() {Tabs() {TabContent() {Scroll() {Column() {GoodsList()}.width(LAYOUT_WIDTH_OR_HEIGHT)}.scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT)}.tabBar(this.firstTabBar)ForEach(initTabBarData, (item: Resource, index?: number) => {TabContent() {Column() {Text(item).fontSize(MAX_FONT_SIZE)}.justifyContent(FlexAlign.Center).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT)}.tabBar(this.otherTabBar(item, index !== undefined ? index : 0))})}.onChange((index: number) => {this.tabsIndex = index}).vertical(false)}
}

7.2. 商品列表懒加载

// entry/src/main/ets/view/GoodsListComponent.ets
import * as commonConst from '../common/CommonConstants'
import { GoodsListItemType } from '../viewmodel/InitialData'
import { ListDataSource } from '../viewmodel/ListDataSource'@Component
export default struct GoodsList {@Provide goodsListData: ListDataSource = new ListDataSource()private startTouchOffsetY: number = 0private endTouchOffsetY: number = 0build() {Row() {List({ space: commonConst.LIST_ITEM_SPACE }) {LazyForEach(this.goodsListData, (item: GoodsListItemType) => {ListItem() {Row() {Column() {Image(item?.goodsImg).width(commonConst.LAYOUT_WIDTH_OR_HEIGHT).height(commonConst.LAYOUT_WIDTH_OR_HEIGHT).draggable(false)}.width(commonConst.GOODS_IMAGE_WIDTH).height(commonConst.LAYOUT_WIDTH_OR_HEIGHT)Column() {Text(item?.goodsName).fontSize(commonConst.NORMAL_FONT_SIZE).margin({ bottom: commonConst.BIGGER_FONT_SIZE })Text(item?.advertisingLanguage).fontColor($r('app.color.gray')).fontSize(commonConst.GOODS_EVALUATE_FONT_SIZE).margin({ right: commonConst.MARGIN_RIGHT, bottom: commonConst.BIGGER_FONT_SIZE })Row() {Text(item?.evaluate).fontSize(commonConst.GOODS_EVALUATE_FONT_SIZE).fontColor($r('app.color.deepGray'))Text(item?.price).fontSize(commonConst.NORMAL_FONT_SIZE).fontColor($r('app.color.freshRed'))}.justifyContent(FlexAlign.SpaceAround).width(commonConst.GOODS_LIST_WIDTH)}.padding(commonConst.GOODS_LIST_PADDING).width(commonConst.GOODS_FONT_WIDTH).height(commonConst.LAYOUT_WIDTH_OR_HEIGHT)}.justifyContent(FlexAlign.SpaceBetween).height(commonConst.GOODS_LIST_HEIGHT).width(commonConst.LAYOUT_WIDTH_OR_HEIGHT)}.onTouch((event?: TouchEvent) => {if (event === undefined) {return}switch (event.type) {case TouchType.Down:this.startTouchOffsetY = event.touches[0].ybreakcase TouchType.Up:this.startTouchOffsetY = event.touches[0].ybreakcase TouchType.Move:if (this.startTouchOffsetY - this.endTouchOffsetY > 0) {this.goodsListData.pushData()}break}})})}.width(commonConst.GOODS_LIST_WIDTH)}.justifyContent(FlexAlign.Center).width(commonConst.LAYOUT_WIDTH_OR_HEIGHT)}
}

7.3. 定义数据类型

// entry/src/main/ets/viewmodel/ListDataSource.ets
import { goodsInitialList, GoodsListItemType } from './InitialData'
import { MAGNIFICATION, MAX_DATA_LENGTH } from '../common/CommonConstants'/*** 创建一个范围列表。*/
const createListRange = (): GoodsListItemType[] => {let result = new Array<GoodsListItemType>()for (let i = 0; i < MAGNIFICATION; i++) {result = result.concat(goodsInitialList)}return result
}/*** LazyLoad 类实现*/
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = []public totalCount(): number {return 0}public getData(index: number): GoodsListItemType | undefined {return undefined}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener)}}unregisterDataChangeListener(listener: DataChangeListener): void {const position = this.listeners.indexOf(listener);if (position >= 0) {this.listeners.splice(position, 1)}}notifyDataReload(): void {this.listeners.forEach((listener: DataChangeListener) => {listener.onDataReloaded()})}notifyDataAdd(index: number): void {this.listeners.forEach((listener: DataChangeListener) => {listener.onDataAdd(index)})}notifyDataChange(index: number): void {this.listeners.forEach((listener: DataChangeListener) => {listener.onDataChange(index)})}notifyDataDelete(index: number): void {this.listeners.forEach((listener: DataChangeListener) => {listener.onDataDelete(index)})}notifyDataMove(from: number, to: number): void {this.listeners.forEach((listener: DataChangeListener) => {listener.onDataMove(from, to)})}
}export class ListDataSource extends BasicDataSource {private listData = createListRange()public totalCount(): number {return this.listData.length}public getData(index: number): GoodsListItemType {return this.listData[index]}public pushData(): void {if (this.listData.length < MAX_DATA_LENGTH) {this.listData = this.listData.concat(goodsInitialList)this.notifyDataAdd(this.listData.length - 1)}}
}

7.4. 数据初始化

// entry/src/main/ets/viewmodel/InitialData.ets
export const initTabBarData = [$r('app.string.mobile_phone'),$r('app.string.clothes'),$r('app.string.wear'),$r('app.string.home_furnishing')
]export class GoodsListItemType {goodsImg: ResourcegoodsName: ResourceadvertisingLanguage: Resourceevaluate: Resourceprice: Resourceconstructor(goodsImg: Resource, goodsName: Resource, price: Resource) {this.goodsImg = goodsImgthis.goodsName = goodsNamethis.advertisingLanguage = $r('app.string.advertising_language')this.evaluate = $r('app.string.evaluate')this.price = price}
}export const goodsInitialList: GoodsListItemType[] = [new GoodsListItemType($r('app.media.goodsImg'), $r('app.string.goodsName'), $r('app.string.price_199')),new GoodsListItemType($r('app.media.goodsImg_2'), $r('app.string.another_goodsName'), $r('app.string.price_199')),new GoodsListItemType($r('app.media.goodsImg_3'), $r('app.string.goodsName'), $r('app.string.price_199')),new GoodsListItemType($r('app.media.goodsImg_4'), $r('app.string.another_goodsName'), $r('app.string.price_199'))
]

8. 下拉刷新与到底提示

8.1. 下拉刷新

// entry/src/main/ets/view/TabBarsComponent.ets
import { initTabBarData } from '../viewmodel/InitialData'
import {LAYOUT_WIDTH_OR_HEIGHT,NORMAL_FONT_SIZE,BIGGER_FONT_SIZE,MAX_FONT_SIZE,MAX_OFFSET_Y,REFRESH_TIME
} from '../common/CommonConstants'
import GoodsList from './GoodsListComponent'
import PutDownRefresh from './PutDownRefreshLayout'@Component
export default struct TabBar {private timer: number = 0private currentOffsetY: number = 0@State tabsIndex: number = 0@State refreshStatus: boolean = false@State refreshText: Resource = $r('app.string.refresh_text')@BuilderfirstTabBar() {Column() {Text($r('app.string.selected')).fontSize(this.tabsIndex === 0 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === 0 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}@BuilderotherTabBar(content: Resource, index: number) {Column() {Text(content).fontSize(this.tabsIndex === index + 1 ? BIGGER_FONT_SIZE : NORMAL_FONT_SIZE).fontColor(this.tabsIndex === index + 1 ? Color.Black : $r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).justifyContent(FlexAlign.Center)}putDownRefresh(event?: TouchEvent): void {if (event === undefined) {return}switch (event.type) {// 记录手指按下时的 y 坐标。case TouchType.Down:this.currentOffsetY = event.touches[0].ybreakcase TouchType.Move:// 根据下拉偏移量确定是否刷新。this.refreshStatus = event.touches[0].y - this.currentOffsetY > MAX_OFFSET_Ybreakcase TouchType.Cancel:breakcase TouchType.Up:// 仅模拟效果,不进行数据请求。this.timer = setTimeout(() => {this.refreshStatus = false}, REFRESH_TIME)breakdefault:break}}aboutToDisappear() {clearTimeout(this.timer)}build() {Tabs() {TabContent() {Scroll() {Column() {if (this.refreshStatus) {PutDownRefresh({ refreshText: $refreshText })}GoodsList()Text($r('app.string.to_bottom')).fontSize(NORMAL_FONT_SIZE).fontColor($r('app.color.gray'))}.width(LAYOUT_WIDTH_OR_HEIGHT)}.scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT).onTouch((event?: TouchEvent) => {this.putDownRefresh(event)})}.tabBar(this.firstTabBar)ForEach(initTabBarData, (item: Resource, index?: number) => {TabContent() {Column() {Text(item).fontSize(MAX_FONT_SIZE)}.justifyContent(FlexAlign.Center).width(LAYOUT_WIDTH_OR_HEIGHT).height(LAYOUT_WIDTH_OR_HEIGHT)}.tabBar(this.otherTabBar(item, index !== undefined ? index : 0))})}.onChange((index: number) => {this.tabsIndex = index}).vertical(false)}
}

8.2. 到底显示

// entry/src/main/ets/view/PutDownRefreshLayout.ets
import * as commonConst from '../common/CommonConstants'@Component
export default struct PutDownRefresh {@Link refreshText: Resourcebuild() {Row() {Image($r('app.media.refreshing')).width(commonConst.ICON_WIDTH).height(commonConst.ICON_HEIGHT)Text(this.refreshText).fontSize(commonConst.NORMAL_FONT_SIZE)}.justifyContent(FlexAlign.Center).width(commonConst.GOODS_LIST_WIDTH).height(commonConst.GOODS_LIST_HEIGHT)}
}

9. 代码与视频教程

完整案例代码与视频教程请参见:

代码:Code-06-01.zip。

视频:《实现商品列表》。

http://www.xdnf.cn/news/7655.html

相关文章:

  • 建站系统哪个好?
  • 基于CATIA参数化圆锥建模的自动化插件开发实践——NX建模之圆锥体命令的参考与移植(二)
  • 笔记:显示实现接口如何实现,作用是什么
  • ollama部署模型
  • 工单派单应用:5 大核心功能提升协作效率
  • Ai学习之LangChain框架
  • STM32外设应用详解——从基础到高级应用的全面指南
  • 差分数组:原理与应用
  • 文献分享-临床预测模型-基于围手术期时间数据肝切除术后肝衰竭早期检测
  • CSS 背景全解析:从基础属性到视觉魔法
  • MinIO集群故障,其中一块driver-4异常
  • 网络安全之带正常数字签名的后门样本分析
  • 软件测试之环境搭建及测试流程
  • 见多识广10:大模型的一些基础概念
  • Python训练营打卡——DAY31(2025.5.20)
  • 类和对象------2
  • Leetcode百题斩-字典树
  • MySQL 安全更新大量数据
  • MySQL高可用之ProxySQL + MGR 实现读写分离实战
  • 面向AI研究的模块化即插即用架构综述与资源整理全覆盖
  • 数据库实验——备份与恢复
  • 【普及−】洛谷P1862 ——输油管道问题
  • 【latex】文本颜色修改
  • 【QT】QTableWidget获取width为100,与真实值不符问题解决
  • C++ 网络编程(9)字节序处理和消息队列的控制
  • 缺乏进度跟踪机制,如何掌握项目状态?
  • MyBatis常用方法
  • 零售EDI:Belk Stores EDI需求分析
  • 阅读笔记---城市计算中用于预测学习的时空图神经网络研究综述
  • 《从零开始构建高可用MySQL架构:全流程实战指南》