云计算百科
云计算领域专业知识百科平台

HarmonyOS7 ArkUI 商品规格选择 - SKU多维度规格选择与价格联动实战:把案例真正写懂

文章目录

      • 前言
      • 业务场景先说清
      • 使用方法
      • 状态和列表怎么配合
      • 核心逻辑拆读
        • 片段 1:`getCurrentSku(): SkuItem | undefined {`
        • 片段 2:`get currentPrice(): number {`
      • 必读小结
      • 完整代码
      • 复盘

前言

这篇我不打算空讲概念,直接贴着代码往下拆。页面怎么跑起来、状态怎么变、交互怎么收口,都会在文章里说清楚。

这个案例的主线是 商品规格选择,后面的 SKU多维度规格选择与价格联动 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。

业务场景先说清

ProductSkuSelectorPage 对应的是 商品规格选择 – SKU多维度规格选择与价格联动 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。

观察点在这个案例里的表现
页面主题 商品规格选择
主要文案 颜色, 星光银, 深空黑, 玫瑰金, 暗夜绿, 存储
常用组件 Button, Column, Flex, ForEach, Row, Scroll, Stack, Text
适合练习 状态驱动 UI、局部刷新、事件回调、页面结构拆分

使用方法

把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。

如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。

Ink-note style technical diagram illustrating a Ha

状态和列表怎么配合

ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。

状态字段类型说明
quantity number 用户输入值,会直接参与计算、筛选或提交
specGroups SpecGroup[] 记录当前展示状态或页面选择
showPanel boolean 布尔状态,控制弹层、模式或显示隐藏
addedToCart boolean 记录当前展示状态或页面选择
cartCount number 记录当前展示状态或页面选择

我自己看这类示例时,会先把 @State 和事件函数圈出来,再去看布局。这样不会被大段 Column、Row 搞乱节奏。

核心逻辑拆读

片段 1:getCurrentSku(): SkuItem | undefined {

这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。

getCurrentSku(): SkuItem | undefined {
const selectedSpecs = this.specGroups.map((g: SpecGroup) => g.selected)
return this.skuTable.find((sku: SkuItem) =>
sku.specs.every((spec: string, i: number) => spec === selectedSpecs[i])
)
}

片段 2:get currentPrice(): number {

Ink-note style framework diagram showing the state

这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。

get currentPrice(): number {
return this.getCurrentSku()?.price ?? 5999
}

必读小结

适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。

推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高
我会重点检查的几个地方

  • 点击或输入之后,界面有没有立即更新
  • 列表渲染是否依赖了稳定的数据结构
  • 筛选、统计、派生数据是不是单独放进函数里
  • 是否还有重复布局可以继续抽出去
  • 文案、颜色、间距是不是同一套风格

完整代码

Ink-note style flowchart detailing the logic of ge

// ProductSkuSelectorPage – 商品规格选择 – SKU多维度规格选择与价格联动

interface SpecOption {
value: string
available: boolean
}

interface SpecGroup {
name: string
options: SpecOption[]
selected: string
}

interface SkuItem {
specs: string[]
price: number
stock: number
skuId: string
}

@Entry
@Component
struct ProductSkuSelectorPage {
@State quantity: number = 1
@State specGroups: SpecGroup[] = [
{
name: '颜色',
options: [
{ value: '星光银', available: true },
{ value: '深空黑', available: true },
{ value: '玫瑰金', available: true },
{ value: '暗夜绿', available: false },
],
selected: '星光银',
},
{
name: '存储',
options: [
{ value: '128GB', available: true },
{ value: '256GB', available: true },
{ value: '512GB', available: true },
{ value: '1TB', available: false },
],
selected: '128GB',
},
{
name: '版本',
options: [
{ value: '标准版', available: true },
{ value: '联名款', available: true },
],
selected: '标准版',
},
]
@State showPanel: boolean = false
@State addedToCart: boolean = false
@State cartCount: number = 0

private skuTable: SkuItem[] = [
{ specs: ['星光银', '128GB', '标准版'], price: 5999, stock: 50, skuId: 'sku001' },
{ specs: ['星光银', '256GB', '标准版'], price: 6999, stock: 30, skuId: 'sku002' },
{ specs: ['星光银', '512GB', '标准版'], price: 8499, stock: 20, skuId: 'sku003' },
{ specs: ['深空黑', '128GB', '标准版'], price: 5999, stock: 45, skuId: 'sku004' },
{ specs: ['深空黑', '256GB', '标准版'], price: 6999, stock: 25, skuId: 'sku005' },
{ specs: ['玫瑰金', '128GB', '联名款'], price: 6499, stock: 10, skuId: 'sku006' },
{ specs: ['玫瑰金', '256GB', '联名款'], price: 7499, stock: 8, skuId: 'sku007' },
]

getCurrentSku(): SkuItem | undefined {
const selectedSpecs = this.specGroups.map((g: SpecGroup) => g.selected)
return this.skuTable.find((sku: SkuItem) =>
sku.specs.every((spec: string, i: number) => spec === selectedSpecs[i])
)
}

get currentPrice(): number {
return this.getCurrentSku()?.price ?? 5999
}

get currentStock(): number {
return this.getCurrentSku()?.stock ?? 0
}

get selectedDesc(): string {
return this.specGroups.map((g: SpecGroup) => g.selected).join(' · ')
}

selectSpec(groupIdx: number, value: string) {
this.specGroups = this.specGroups.map((g: SpecGroup, idx: number) => {
if (idx === groupIdx) g.selected = value
return g
return g
})
}

addToCart() {
this.cartCount += this.quantity
this.addedToCart = true
this.showPanel = false
setTimeout(() => { this.addedToCart = false }, 2000)
}

@Builder
SpecPanel() {
Column({ space: 0 }) {
// 商品摘要
Row({ space: 12 }) {
Column()
.width(80)
.height(80)
.borderRadius(8)
.linearGradient({ angle: 135, colors: [['#667eea', 0], ['#764ba2', 1]] })

Column({ space: 4 }) {
Text(`¥${this.currentPrice}`)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#ff4d4f')
Text(`库存 ${this.currentStock}`).fontSize(12).fontColor('#aaaaaa')
Text(this.selectedDesc).fontSize(12).fontColor('#666666').maxLines(2)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(16)
.backgroundColor('#f8f8f8')

Scroll() {
Column({ space: 20 }) {
// 规格选择
ForEach(this.specGroups, (group: SpecGroup, groupIdx: number) => {
Column({ space: 12 }) {
Text(group.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1a1a1a')
.width('100%')

Flex({ wrap: FlexWrap.Wrap}) {
ForEach(group.options, (opt: SpecOption) => {
Text(opt.value)
.fontSize(13)
.fontColor(
!opt.available ? '#cccccc' :
group.selected === opt.value ? '#1890ff' : '#444444'
)
.backgroundColor(
!opt.available ? '#f8f8f8' :
group.selected === opt.value ? '#e8f4ff' : '#f5f5f5'
)
.border({
width: 1.5,
color: !opt.available ? '#e8e8e8' :
group.selected === opt.value ? '#1890ff' : 'transparent',
})
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.borderRadius(20)
.enabled(opt.available)
.onClick(() => {
if (opt.available) this.selectSpec(groupIdx, opt.value)
})
})
}
.width('100%')
}
})

// 数量选择
Row() {
Text('数量')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1a1a1a')
.layoutWeight(1)

Row({ space: 0 }) {
Text('-')
.fontSize(20)
.fontColor(this.quantity <= 1 ? '#cccccc' : '#1a1a1a')
.width(40).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f5f5')
.borderRadius({ topLeft: 8, bottomLeft: 8 })
.enabled(this.quantity > 1)
.onClick(() => { if (this.quantity > 1) this.quantity })

Text(`${this.quantity}`)
.fontSize(16)
.fontColor('#1a1a1a')
.fontWeight(FontWeight.Medium)
.width(50)
.height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f5f5')

Text('+')
.fontSize(20)
.fontColor(this.quantity >= this.currentStock ? '#cccccc' : '#1a1a1a')
.width(40).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f5f5')
.borderRadius({ topRight: 8, bottomRight: 8 })
.enabled(this.quantity < this.currentStock)
.onClick(() => {
if (this.quantity < this.currentStock) this.quantity++
})
}
}
.width('100%')

// 操作按钮
Row({ space: 12 }) {
Button('加入购物车')
.layoutWeight(1)
.height(48)
.fontSize(15)
.backgroundColor('#ff7a00')
.onClick(() => this.addToCart())

Button('立即购买')
.layoutWeight(1)
.height(48)
.fontSize(15)
.backgroundColor('#ff4d4f')
.onClick(() => this.addToCart())
}
.width('100%')

Column().height(20)
}
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
}
.height(440)
}
.backgroundColor('#ffffff')
.borderRadius({ topLeft: 20, topRight: 20 })
.shadow({ radius: 20, color: '#00000020', offsetX: 0, offsetY: 4 })
}

build() {
Stack({ alignContent: Alignment.Bottom }) {
Column({ space: 0 }) {
// 商品图
Stack() {
Column()
.width('100%')
.height(320)
.backgroundColor('#764BA2')
Row() {
if (this.cartCount > 0) {
Stack({ alignContent: Alignment.TopEnd }) {
Text('🛒').fontSize(26)
Text(this.cartCount + '')
.fontSize(10)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(9)
.width(18)
.height(18)
.textAlign(TextAlign.Center)
.margin({ top: 4, right: 6 })
}
} else {
Text('🛒').fontSize(26)
}
}
.position({ x: '100%', y: 16 })
.translate({ x: 60 })
}

// 商品信息
Column({ space: 12 }) {
Row() {
Column({ space: 4 }) {
Text(`¥${this.currentPrice}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ff4d4f')
Text('¥6999 原价').fontSize(13).fontColor('#aaaaaa')
.decoration({ type: TextDecorationType.LineThrough })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)

Column({ space: 2 }) {
Text('🏷️ 限时折扣').fontSize(12).fontColor('#ff4d4f')
Text('每人限购3件').fontSize(12).fontColor('#aaaaaa')
}
.alignItems(HorizontalAlign.End)
}

Text('Pro Max 旗舰智能手机 5G双卡双待 超清摄像')
.fontSize(16)
.fontColor('#1a1a1a')
.fontWeight(FontWeight.Medium)
.lineHeight(24)

Text(`已选:${this.selectedDesc}`)
.fontSize(13)
.fontColor('#888888')

// 评分
Row({ space: 8 }) {
Text('⭐⭐⭐⭐⭐').fontSize(14)
Text('4.9分').fontSize(13).fontColor('#fa8c16').fontWeight(FontWeight.Medium)
Text('(8,234条评价)').fontSize(12).fontColor('#aaaaaa')
}
}
.padding({ left: 16, right: 16, top: 16 })
.backgroundColor('#ffffff')

Blank()

// 底部操作栏
Row({ space: 0 }) {
Column({ space: 2 }) {
Text('🏠').fontSize(20)
Text('首页').fontSize(10).fontColor('#888888')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })

Column({ space: 2 }) {
if (this.cartCount > 0) {
Stack({ alignContent: Alignment.TopEnd }) {
Text('🛒').fontSize(20)
Text(this.cartCount + '')
.fontSize(10)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(9)
.width(18)
.height(18)
.textAlign(TextAlign.Center)
.margin({ top: 4, right: 6 })
}
} else {
Text('🛒').fontSize(20)
}
Text('购物车').fontSize(10).fontColor('#888888')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })

Button('加入购物车')
.layoutWeight(2)
.height(44)
.fontSize(14)
.backgroundColor('#ff7a00')
.borderRadius(0)
.onClick(() => { this.showPanel = true })

Button('立即购买')
.layoutWeight(2)
.height(44)
.fontSize(14)
.backgroundColor('#ff4d4f')
.borderRadius(0)
.onClick(() => { this.showPanel = true })
}
.width('100%')
.backgroundColor('#ffffff')
.border({ width: { top: 0.5 }, color: '#f0f0f0' })
}
.width('100%')
.height('100%')
.backgroundColor('#f5f7fa')

// 规格面板
if (this.showPanel) {
Column({ space: 0 }) {
Row() {
Blank()
Text('✕').fontSize(20).fontColor('#999999')
.onClick(() => { this.showPanel = false })
}
.padding({ right: 16, top: 12 })
this.SpecPanel()
}
.width('100%')
}

// 加购成功提示
if (this.addedToCart) {
Text('✓ 已加入购物车')
.fontSize(14)
.fontColor('#ffffff')
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor('#333333cc')
.borderRadius(20)
.position({ x: '50%', y: '85%' })
.translate({ x: '-50%' })
}
}
.width('100%')
.height('100%')
}
}

复盘

这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。

赞(0)
未经允许不得转载:网硕互联帮助中心 » HarmonyOS7 ArkUI 商品规格选择 - SKU多维度规格选择与价格联动实战:把案例真正写懂
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!