在消费升级与户外生活方式深度融合的当下,精致露营(Glamping)已从小众圈层走向大众视野。一个优秀的露营装备电商应用,不仅需要承载帐篷、炊具、灯具、睡袋等多品类的复杂数据维度,更需要通过视觉语言传递山系复古的温度感与可靠感。

ArkTS 的声明式 UI 范式为多品类电商场景提供了天然契合的架构能力。通过 @Component 装饰器的组件化拆分、@State 的状态驱动重渲染、纯函数的数据处理层,开发者可以在单一代码文件中构建出包含六个完整功能标签页、数十个交互弹窗、多维度数据可视化的综合应用。

本应用的墨松绿 × 暖沙米 × 赭石三色体系,不仅是视觉风格的选择,更是工程层面的设计令牌化实践。全局色彩通过 CpTheme 接口和 CP 常量集中管控,每一处组件的色彩引用都指向语义化的属性名,为未来品牌色迭代与多主题扩展奠定了架构基础。

引言

精致露营(Glamping)作为户外运动与品质生活的交汇点,其装备选购场景具有品类跨度大、专业参数多、决策链路长的特点。一款帐篷需要考量容纳人数、重量、季节适应性、防水等级;一口锅需要区分材质(钛/铝/铸铁/不锈钢)、重量、导热性能;一盏灯需要对比亮度(流明)、续航时间、供电方式(充电/煤油/燃气/太阳能);一条睡袋需要关注温标、填充物(鹅绒/鸭绒/棉)、重量和舒适度。这种多维度的专业数据展示需求,对移动端 UI 框架的信息密度管理和可视化表达能力提出了严峻挑战。

本文分析的精致露营装备馆应用,基于 HarmonyOS ArkTS API 24 声明式开发范式构建,覆盖帐篷、炊具、灯具、睡袋四大核心品类,延伸至营地推荐、装备清单、会员体系、优惠券领取等社区化运营场景。应用采用墨松绿(#1F4B3F)为主色、赭石(#D98E4A)为强调色、暖沙米(#F4F1E8)为背景色的山系复古配色方案,在视觉上传递出沉稳、温暖、可信赖的户外装备调性。

从技术架构维度审视,该应用的核心设计决策体现在三个层面。第一,数据处理层完全函数化——全部过滤、排序、格式化、最大值计算逻辑均以全局纯函数形式存在,不依赖组件状态,保证了逻辑的可测试性和复用性。第二,组件化拆分粒度精准——六个标签页各为独立 @Component,每个组件管理自身的时间器、筛选状态、弹窗状态和表单状态,形成高内聚低耦合的模块边界。第三,动效系统以数学函数为驱动核心——通过全局递增的 wave 计数器配合正弦、绝对值、取模等数学运算,在无专业动画框架依赖下实现了浮动、弹跳、脉冲、涟漪、扫光、闪烁、烟雾七种有机动态效果。

一、主题系统与设计令牌架构

interface CpTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
}

const CP: CpTheme = {
  bg: '#F4F1E8',
  primary: '#1F4B3F',
  accent: '#D98E4A',
  card: '#FFFFFF',
  text: '#2E2A22',
  sub: '#8C8471',
  line: '#E2DCC9',
  glow: '#3E7C6B'
}

在这里插入图片描述

应用的主题系统由 CpTheme 接口定义类型结构,CP 常量对象实例化全部八个色彩维度。背景色 #F4F1E8 是暖沙米色调,营造出帐篷内壁的温暖质感;主色 #1F4B3F 是深墨松绿,象征针叶林的沉稳;强调色 #D98E4A 是赭石橙,点缀于按钮、标签、价格等关键交互元素;卡片色为纯白,与暖沙米背景形成层次;辉光色 #3E7C6B 是介于主色与强调色之间的过渡绿,用于渐变终点和次要强调。

这种设计令牌化方案的核心价值在于色彩语义化。在组件代码中读到 CP.primary 时,开发者立即理解这是品牌主色;读到 CP.accent 时,知道这是行动召唤按钮的强调色。相较于直接使用 #1F4B3F,语义化引用大幅降低了代码的认知负荷,也为未来实现"林间晨雾""沙漠落日"等主题包切换提供了架构条件。

二、多品类数据模型与静态数据体系

interface CpTent {
  name: string
  brand: string
  capacity: number
  weight: number
  price: number
  orig: number
  sale: number
  season: string
  icon: string
}

const CP_TENTS: CpTent[] = [
  { name: '云顶隧道帐 3 人', brand: 'NEMO', capacity: 3, weight: 3.8, price: 2899, orig: 3299, sale: 156, season: '三季帐', icon: '⛺' },
  { name: '胡迪尼轻量双人帐', brand: 'Hilleberg', capacity: 2, weight: 1.9, price: 4599, orig: 5199, sale: 88, season: '四季帐', icon: '🏔' },
  { name: '黑标重型狩猎帐', brand: 'Tentipi', capacity: 8, weight: 12.5, price: 6999, orig: 7999, sale: 43, season: '四季帐', icon: '🌲' },
  // ... 共 10 款帐篷
]

interface CpCook {
  name: string
  brand: string
  material: string
  weight: number
  price: number
  sale: number
  icon: string
}

interface CpLamp {
  name: string
  brand: string
  lumens: number
  runtime: number
  price: number
  sale: number
  mode: string
  icon: string
}

interface CpBag {
  name: string
  brand: string
  limit: number
  fill: string
  weight: number
  price: number
  orig: number
  sale: number
  icon: string
}

在这里插入图片描述

应用的数据模型覆盖四大品类各具特色的专业参数。CpTent 定义帐篷结构,包含容纳人数(capacity,1-8 人)、重量(weight,0.9-12.5kg)、季节类型(三季帐/四季帐/遮阳幕)——这些参数直接决定了帐篷的使用场景和价格定位。CpCook 定义炊具结构,材质字段(material)区分钛、铝、铸铁、不锈钢,每种材质的导热性、重量和价格差异巨大。CpLamp 定义灯具结构,包含流明数(lumens,60-800lm)、续航时间(runtime,8-40 小时)和供电方式(充电/煤油/燃气/太阳能)。CpBag 定义睡袋结构,核心参数是舒适温标(limit,-25 至 15 度)和填充物类型(900 蓬鹅绒/850 蓬鸭绒/中空棉等)。

此外还有 CpOrder(订单,含状态和日期)、CpFav(收藏夹,含标签如"轻量"“重装”“氛围”“极寒”)、CpMonth(月度消费数据)和 CpTab(导航栏配置)。全部数据以 const 常量数组声明,在应用内充当内存数据库。

三、纯函数式数据处理与可视化计算层

function cpTentFilter(arr: CpTent[], t: string): CpTent[] {
  let r: CpTent[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].season === t) {
      r.push(arr[i])
    }
  }
  return r
}

function cpTentTop(): CpTent[] {
  let r: CpTent[] = CP_TENTS.slice()
  r.sort((a: CpTent, b: CpTent) => b.price - a.price)
  return r.slice(0, 6)
}

function cpMaxTentPrice(): number {
  let m: number = 1
  for (let i = 0; i < CP_TENTS.length; i++) {
    if (CP_TENTS[i].price > m) {
      m = CP_TENTS[i].price
    }
  }
  return m
}

function cpLumenW(l: number): string {
  return Math.min(100, Math.round(l / 800 * 100)) + '%'
}

function cpTempW(t: number): string {
  return Math.min(100, Math.max(8, Math.round((t + 30) / 45 * 100))) + '%'
}

function cpWeightW(w: number, m: number): string {
  return Math.min(100, Math.round(w / Math.max(1, m) * 100)) + '%'
}

在这里插入图片描述

数据处理层是该应用架构的工程核心。全部数据处理逻辑被提取为独立的全局纯函数,不依赖任何组件的 @State 状态。cpTentFilter 根据季节标签过滤帐篷列表,cpTentTop 按价格降序取前六用于价格对比,cpMaxTentPrice 遍历全量数据获取最大价格作为条形图归一化分母。

可视化计算函数是该层的亮点。cpLumenW 将流明值映射为 0-100% 的进度条宽度,以 800 流明为满刻度。cpTempW 将温标映射为百分比,公式 (t + 30) / 45 * 100 将 -30 度到 15 度的区间映射为 0-100%,同时 Math.max(8, ...) 保证最小宽度不低于 8%,避免极端低温时进度条完全消失。cpWeightW 将重量按参考最大值归一化。这些函数的设计哲学是"数据到视觉的线性映射",使得用户可以通过进度条长度直观感知参数的相对量级。

纯函数的架构决策不仅提升了代码可测试性,更重要的是保证了数据流的单向性和可预测性。在组件的 build() 方法中调用 cpTentFilter(this.tents, this.chip) 时,函数接收当前状态快照作为输入,返回新的过滤结果数组,不修改原始数据的引用——这种不可变性保证了 ArkTS 状态驱动渲染机制的正确性。

四、帧动画函数体系

function cpFloatY(w: number, i: number): number {
  return Math.sin((w + i * 14) * 0.1) * 5
}

function cpPulse(w: number): number {
  return 34 + Math.sin(w * 0.18) * 16
}

function cpRipple(w: number): number {
  return (w % 60) * 2
}

function cpShineO(w: number): number {
  return ((w * 2) % 260) - 70
}

function cpBlink(w: number, i: number): number {
  return (Math.floor(w / 10) + i) % 2
}

function cpBounceY(w: number, i: number): number {
  return -Math.abs(Math.sin((w + i * 8) * 0.12)) * 7
}

function cpSmokeX(w: number, i: number): number {
  return Math.sin((w + i * 20) * 0.06) * 8
}

应用的动效引擎由七个帧函数构成,全部以全局递增的 wave 计数器为时间轴输入。cpFloatY 生成正弦浮动位移,通过 i * 14 的索引偏移制造相位差。cpPulse 生成 18-50 之间的脉冲值,用于灯具卡片的呼吸光晕。cpRipple 通过取模生成 0-118 的递增涟漪半径。cpShineO 生成 -70 到 190 的扫光位移值。cpBlink 生成 0/1 交替的闪烁信号。

cpBounceY 是弹跳动画的核心,使用 Math.abs 包裹正弦函数生成非对称轨迹——上升快、下落慢的抛物线感觉,配合负号使方向向上偏移。cpSmokeX 是该应用独有的"烟雾飘散"动画函数,频率较低(0.06)且振幅较大(8),用于首页 Banner 中篝火 Emoji 的烟雾飘散效果,增添了露营场景的氛围感。

五、主入口组件与导航架构

@Entry
@Component
struct CpApp {
  @State curTab: number = 0

  build() {
    Stack() {
      Column() {
        Row({ space: 10 }) {
          Column() {
            Text('🏕').fontSize(24)
          }
          .width(38).height(38).borderRadius(19).backgroundColor(CP.primary)
            .justifyContent(FlexAlign.Center)
          Column({ space: 2 }) {
            Text('山系露营仓').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('GLAMPING GEAR · 今晚就去野').fontSize(8).fontColor('#D8E5DF')
          }.alignItems(HorizontalAlign.Start)
          Column().layoutWeight(1)
          Text('🗓').fontSize(20)
          Text('❤').fontSize(20).margin({ left: 10 })
        }
        .width('100%').height(58).padding({ left: 16, right: 16 }).backgroundColor(CP.primary)

        Row({ space: 8 }) {
          Text('🌲').fontSize(14)
          Text('今晚气温 22° · 微风 · 适合露营指数 ★★★★★')
            .fontSize(10).fontColor(CP.primary).layoutWeight(1).maxLines(1)
          Text('查营地').fontSize(10).fontColor('#FFFFFF').backgroundColor(CP.accent)
            .borderRadius(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
        }
        .width('92%').height(30).borderRadius(15).backgroundColor('#E7EFE9')
          .padding({ left: 12, right: 6 }).margin({ top: 8, bottom: 4 })

        Column() {
          if (this.curTab === 0) {
            CpHomeTab()
          } else if (this.curTab === 1) {
            CpTentTab()
          } else if (this.curTab === 2) {
            CpCookTab()
          } else if (this.curTab === 3) {
            CpLampTab()
          } else if (this.curTab === 4) {
            CpBagTab()
          } else {
            CpMineTab()
          }
        }.layoutWeight(1).width('100%')

        Row() {
          ForEach(CP_TABS, (t: CpTab, i: number) => {
            Column({ space: 3 }) {
              Text(t.icon).fontSize(19)
              Text(t.label).fontSize(10).fontColor(this.curTab === i ? CP.primary : CP.sub)
              if (this.curTab === i) {
                Row().width(16).height(3).borderRadius(2).backgroundColor(CP.accent)
              }
            }
            .justifyContent(FlexAlign.Center)
            .layoutWeight(1)
            .onClick(() => {
              this.curTab = i
            })
          }, (t: CpTab) => t.label)
        }
        .width('100%').height(58).backgroundColor(CP.card)
        .border({ width: { top: 1 }, color: CP.line })
      }.width('100%').height('100%').backgroundColor(CP.bg)
    }
  }
}

在这里插入图片描述

CpApp 入口组件的布局结构与陆冲公园应用类似但有关键差异。标题栏使用墨松绿背景配白色文字,左侧 Logo 容器使用圆角 19 的墨松绿圆形包裹帐篷 Emoji,右侧是日历和收藏入口。标题栏下方增加了一条"天气露营指数"信息条——浅绿底色配墨松绿文字,右侧赭石色"查营地"按钮,这条信息条是露营场景特有的"出行决策辅助"设计。

导航栏的激活指示条宽度为 16dp(比陆冲公园的 14dp 更宽),配赭石色。六个标签页通过 if-else 条件链挂载,非激活组件不参与渲染树。

六、首页组件:Banner 动效、营地推荐与炊具热销榜

@Component
struct CpHomeTab {
  @State wave: number = 0
  @State timer: number = 0
  @State showGuide: boolean = false
  @State showCheck: boolean = false
  @State showCoupon: boolean = false
  @State showVip: boolean = false

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 1000
    }, 60)
  }

  aboutToDisappear(): void {
    if (this.timer > 0) {
      clearInterval(this.timer)
    }
  }

在这里插入图片描述

首页组件管理五个状态:wave 动画计数器、timer 定时器 ID、四个弹窗显隐布尔值(营地攻略、装备清单、优惠券、山系会员)。生命周期管理与其他组件一致:aboutToAppear 启动 60ms 定时器,aboutToDisappear 清理。

          Stack() {
            Column() {
              Row({ space: 14 }) {
                Column({ space: 2 }) {
                  Text('秋日露营季').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                  Text('帐篷 8 折 · 睡袋满 1000 减 150').fontSize(10)
                    .fontColor('#E8DFC8').margin({ top: 4 })
                  Text('去选装备').fontSize(11).fontColor(CP.primary).backgroundColor('#F4F1E8')
                    .borderRadius(13).padding({ left: 14, right: 14, top: 5, bottom: 5 }).margin({ top: 10 })
                }.alignItems(HorizontalAlign.Start)
                Column().layoutWeight(1)
                Text('🔥').fontSize(40).translate({ y: cpBounceY(this.wave, 0) })
                  .opacity(cpBlink(this.wave, 0) === 1 ? 1 : 0.7)
              }.width('100%')
            }
            .width('100%').padding({ left: 20, right: 20, top: 22, bottom: 22 }).borderRadius(18)
            .linearGradient({ angle: 160, colors: [[CP.primary, 0], ['#0F2E26', 1]] })

            Text('💨').fontSize(16).translate({ x: cpSmokeX(this.wave, 0), y: -cpBounceY(this.wave, 1) })
              .position({ x: '72%', y: 10 }).opacity(0.5)
            Text('🍂').fontSize(18).translate({ y: cpFloatY(this.wave, 2) })
              .position({ x: 30, y: 80 }).opacity(0.8)
            Text('⭐').fontSize(14).translate({ y: cpFloatY(this.wave, 3) })
              .position({ x: '85%', y: 70 }).opacity(0.7)
          }.width('100%').height(132)

在这里插入图片描述

Banner 区域是该应用动效最密集的区域。底层是 160 度角从墨松绿到更深的 #0F2E26 渐变卡片,内含"秋日露营季"标题和促销文案。右侧的篝火 Emoji 使用 cpBounceY 弹跳动画配合 cpBlink 闪烁透明度,模拟火焰的跳动。篝火上方的烟雾 Emoji 使用 cpSmokeX 水平飘移和反向 cpBounceY 位移,营造出烟雾上升飘散的效果。左下角的落叶 Emoji 和右上角的星星分别使用 cpFloatY 浮动,整体形成一幅动态的秋日露营场景画。

          Scroll() {
            Row({ space: 10 }) {
              Column({ space: 5 }) {
                Text('🌲').fontSize(30)
                Text('莫干山竹林营地').fontSize(11).fontWeight(FontWeight.Medium).fontColor(CP.text)
                Text('杭州 · 车位 30 · 海拔 480m').fontSize(8).fontColor(CP.sub)
                Text('¥128/晚').fontSize(11).fontWeight(FontWeight.Bold).fontColor(CP.accent)
              }.width(124).padding(10).borderRadius(14).backgroundColor(CP.card).alignItems(HorizontalAlign.Center)
              // ... 千岛湖、四明山、崇明岛
            }
          }.scrollable(ScrollDirection.Horizontal).width('100%')

在这里插入图片描述

营地推荐区域使用水平滚动 Scroll 容器,展示四个长三角地区的精选营地卡片。每张卡片包含营地 Emoji、名称、位置信息(城市·车位·特色)和每晚价格,价格使用赭石色突出。水平滚动使用户可以快速浏览多个营地选项,是露营场景特有的"选址决策"辅助功能。

炊具热销榜使用 ForEach 遍历 cpHotTop() 函数返回的 TOP5 炊具数据,排名徽章前三名分别使用墨松绿、辉光绿和赭石色,体现了品牌色系的一致性运用。

七、帐篷标签页:价格对比、季节筛选与 CRUD

          Column({ space: 12 }) {
            ForEach(cpTentFilter(this.tents, this.chip), (t: CpTent, i: number) => {
              Column({ space: 10 }) {
                Row({ space: 12 }) {
                  Stack() {
                    Column().width(64).height(52).borderRadius(14)
                      .linearGradient({ angle: 135, colors: [['#DDE8E2', 0], ['#C4D6CC', 1]] })
                    Text(t.icon).fontSize(28)
                      .translate({ y: cpFloatY(this.wave, i) })
                  }.alignContent(Alignment.Center)
                  Column({ space: 4 }) {
                    Row({ space: 6 }) {
                      Text(t.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(CP.text).maxLines(1)
                      Text(t.season).fontSize(8).fontColor('#FFFFFF')
                        .backgroundColor(cpSeasonColor(t.season)).borderRadius(6)
                        .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    }
                    Text(t.brand + ' · ' + t.capacity + ' 人 · ' + t.weight + 'kg')
                      .fontSize(10).fontColor(CP.sub)
                    Row({ space: 6 }) {
                      Text(cpPrice(t.price)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(CP.primary)
                      Text(cpPrice(t.orig)).fontSize(9).fontColor(CP.sub)
                        .decoration({ type: TextDecorationType.LineThrough })
                      Text(cpOff(t.price, t.orig)).fontSize(9).fontColor('#FFFFFF')
                        .backgroundColor(CP.accent).borderRadius(8)
                        .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                    }
                  }.layoutWeight(1).alignItems(HorizontalAlign.Start)
                }.width('100%')
                Row({ space: 6 }) {
                  Text('月销 ' + t.sale).fontSize(9).fontColor(CP.sub)
                  Column().layoutWeight(1)
                  Text(t.capacity + ' 人入住').fontSize(9).fontColor(CP.sub)
                  Column().layoutWeight(1)
                  Text('防雨 3000mm').fontSize(9).fontColor(CP.sub)
                }.width('100%')
              }
              .width('100%').padding(14).borderRadius(16).backgroundColor(CP.card)
              .border({ width: 1, color: CP.line })
              .onClick(() => {
                this.selIdx = cpFindTentIdx(this.tents, t.name)
                this.showDetail = true
              })
            }, (t: CpTent) => t.name)
          }.width('100%')

在这里插入图片描述

帐篷列表项的设计体现了信息密度的精心编排。每个列表项是一个带边框的圆角卡片,顶部行左侧是 64×52 的渐变图标容器(帐篷 Emoji 使用 cpFloatY 浮动动画),右侧是商品信息:名称旁附季节标签(颜色由 cpSeasonColor 映射:四季帐=墨松绿,三季帐=辉光绿,遮阳幕=赭石),品牌、人数、重量一行展示,价格行包含现价、原价删除线和折扣标签。底部行均分三个月销、人数、防水参数。整张卡片的信息密度高但层次分明,通过字号差异(13/10/9)和色彩区分(正文/辅助/强调)实现了高效的信息组织。

上架表单的校验逻辑确保容量在 1-8 人范围内,价格大于 0。删除操作通过 splice 移除并显示确认弹窗。

八、炊具标签页:材质着色与户外厨房排行榜

          Column({ space: 8 }) {
            ForEach(cpCookFilter(this.cooks, this.chip), (c: CpCook, i: number) => {
              Row({ space: 10 }) {
                Stack() {
                  Text((i + 1).toString()).fontSize(13).fontWeight(FontWeight.Bold)
                    .fontColor(i < 3 ? '#FFFFFF' : CP.sub)
                  Row()
                    .width(30).height(30).borderRadius(15)
                    .backgroundColor(i < 3 ? cpMaterialColor(c.material) : CP.line)
                    .opacity(cpBlink(this.wave, i) === 1 ? 1 : 0.6)
                }.alignContent(Alignment.Center).width(30).height(30)
                Stack() {
                  Column().width(46).height(46).borderRadius(15).backgroundColor('#F1EEE2')
                  Text(c.icon).fontSize(22)
                    .translate({ y: cpBounceY(this.wave, i) })
                }.alignContent(Alignment.Center)
                Column({ space: 3 }) {
                  Row({ space: 6 }) {
                    Text(c.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(CP.text).maxLines(1)
                    Text(c.material).fontSize(8).fontColor('#FFFFFF')
                      .backgroundColor(cpMaterialColor(c.material)).borderRadius(6)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  }
                  Text(c.brand + ' · ' + c.weight + 'g · 月销 ' + c.sale).fontSize(9).fontColor(CP.sub)
                }.layoutWeight(1).alignItems(HorizontalAlign.Start)
                Text(cpPrice(c.price)).fontSize(12).fontWeight(FontWeight.Bold).fontColor(CP.primary)
              }
              .width('100%').padding(10).borderRadius(14).backgroundColor(CP.card)
            }, (c: CpCook) => c.name)
          }.width('100%')

炊具标签页的排行榜设计独具特色。排名徽章是一个 Stack 叠加结构:底层是材质色圆盘(前三名使用 cpMaterialColor 返回的钛=墨松绿、铝=辉光绿、铸铁=深灰、不锈钢=暖灰),上层是排名数字,通过 cpBlink 驱动透明度脉冲。商品图标使用 cpBounceY 弹跳,材质标签使用与徽章一致的颜色编码。这种"颜色即类别"的视觉编码使用户无需阅读文字即可识别材质类型。

0

1

2

3

4

5

应用启动

CpApp 入口渲染

标题栏 + 天气信息条

底部导航 ForEach

curTab 值判断

CpHomeTab 首页

CpTentTab 帐篷

CpCookTab 炊具

CpLampTab 灯具

CpBagTab 睡袋

CpMineTab 我的

Banner篝火+烟雾+落叶动效

快捷入口4弹窗

营地横向滚动推荐

炊具热销榜TOP5

帐篷价格对比条形图

季节筛选Chips

帐篷列表+浮动动画

详情/上架/编辑/删除

材质筛选Chips

排行榜+材质色编码

销量柱状图

生火小课堂

亮度对比条形图

灯具网格+脉冲光晕

供电方式柱状图

详情/上架/编辑/删除

温标对比双色条

季节筛选

睡袋列表+雪花浮动

详情/上架/删除

用户卡+扫光特效

消费趋势月度图

订单列表

愿望单网格

设置3弹窗

九、灯具标签页:亮度对比与脉冲光晕网格

          Column({ space: 10 }) {
            Row() {
              Text('💡 亮度对比 TOP6').fontSize(15).fontWeight(FontWeight.Bold).fontColor(CP.text)
              Column().layoutWeight(1)
              Text('单位: 流明').fontSize(9).fontColor(CP.sub)
            }.width('100%')
            Column({ space: 8 }) {
              ForEach(cpLampTop(), (l: CpLamp, i: number) => {
                Row({ space: 8 }) {
                  Text(l.name).fontSize(10).fontColor(CP.text).width(100).maxLines(1)
                  Stack({ alignContent: Alignment.Start }) {
                    Row().height(14).width('100%').borderRadius(7).backgroundColor(CP.line)
                    Row().height(14).width(cpLumenW(l.lumens)).borderRadius(7)
                      .linearGradient({ angle: 0, colors: [[CP.accent, 0], ['#FBBF24', 1]] })
                  }.layoutWeight(1)
                  Text(l.lumens.toString()).fontSize(10).fontWeight(FontWeight.Bold)
                    .fontColor('#B45309').width(36).textAlign(TextAlign.End)
                }.width('100%')
              }, (l: CpLamp) => l.name)
            }.width('100%').padding(12).borderRadius(14).backgroundColor(CP.card)
          }.width('100%')

灯具标签页的核心可视化是"亮度对比 TOP6"水平条形图。进度条宽度由 cpLumenW 函数计算,以 800 流明为满刻度。渐变从赭石到琥珀黄,传达"越亮越黄"的暖光语义——这与露营灯具追求温暖氛围光的使用场景高度契合。

灯具网格使用 Flex 两列布局,每个卡片图标区域使用 Stack 三层叠加:底层是 cpPulse 驱动的呼吸光晕(半透明赭石色圆形),中层是渐变底色,顶层是灯具 Emoji。脉冲光晕使每盏灯都像在持续发光,将静态的商品卡片转化为有温度的场景化展示。

          Row({ space: 10 }).alignItems(VerticalAlign.Bottom).height(100) {
            ForEach(cpLampFilter(this.lamps, this.chip), (l: CpLamp, i: number) => {
              Column({ space: 4 }) {
                Column().width(18).height(cpVBarH(l.sale, cpMaxLampSale())).borderRadius(5)
                  .backgroundColor(cpModeColor(l.mode))
                Text(l.mode).fontSize(8).fontColor(CP.sub)
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            }, (l: CpLamp) => l.name)
          }.width('100%').padding(12).borderRadius(14).backgroundColor('#E7EFE9')

供电方式柱状图使用 cpModeColor 为不同供电方式的柱体着色:煤油=赭石棕、燃气=红色、太阳能=橙色、充电=辉光绿。柱状图的色彩编码与商品标签一致,使用户在柱状图和列表之间建立视觉关联。

十、睡袋标签页:温标双色条与雪花动效

            Column({ space: 8 }) {
              ForEach(cpBagTop(), (b: CpBag, i: number) => {
                Row({ space: 8 }) {
                  Text(b.name).fontSize(10).fontColor(CP.text).width(100).maxLines(1)
                  Stack({ alignContent: Alignment.Start }) {
                    Row().height(14).width('100%').borderRadius(7)
                      .linearGradient({ angle: 0, colors: [['#93C5FD', 0], ['#DC2626', 1]] })
                    Row().height(14).width(cpTempW(b.limit)).borderRadius(7)
                      .linearGradient({ angle: 0, colors: [['#1D4ED8', 0], ['#93C5FD', 1]] })
                    Row().width(2).height(18).backgroundColor(CP.text)
                      .position({ x: cpTempW(b.limit) })
                  }.layoutWeight(1)
                  Text(b.limit + '°').fontSize(10).fontWeight(FontWeight.Bold)
                    .fontColor('#1D4ED8').width(36).textAlign(TextAlign.End)
                }.width('100%')
              }, (b: CpBag) => b.name)
            }.width('100%').padding(12).borderRadius(14).backgroundColor(CP.card)

睡袋温标对比条是该应用最具创意的可视化设计。每个温标条是一个三层 Stack:最底层是从浅蓝到红色的渐变底条(代表从寒冷到温暖的完整温标光谱 -30 至 15 度),第二层是蓝色渐变进度条(宽度由 cpTempW 计算),代表该睡袋的舒适温标位置,第三层是一个 2dp 宽的指示线标记精确位置。

这种设计使用户一眼就能看到每个睡袋在温度光谱中的位置——越偏左表示越耐寒,越偏右表示越适合温暖季节。蓝色进度条覆盖的区域代表"从最低温到该睡袋温标的范围",直观传达了"该睡袋能应对多冷的天气"。

睡袋列表项叠加了雪花 Emoji 的浮动动效(cpFloatY),位置在卡片右上方,半透明,为低温睡袋营造了寒冷氛围的视觉暗示。

十一、个人中心:会员体系与消费报告

          Stack() {
            Column({ space: 6 }) {
              Row({ space: 10 }) {
                Text('🌲').fontSize(32)
                Column({ space: 3 }) {
                  Text(this.nick).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                  Text('ID: CP2026888 · 松果会员 · 露营 32 晚').fontSize(10).fontColor('#D8E5DF')
                }.alignItems(HorizontalAlign.Start)
                Column().layoutWeight(1)
                Text('VIP').fontSize(11).fontColor(CP.primary).backgroundColor('#F4F1E8')
                  .borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 })
              }.width('100%')
              Row() {
                Text('装备积分 2560').fontSize(10).fontColor('#D8E5DF')
                Column().layoutWeight(1)
                Text('优惠券 2 张').fontSize(10).fontColor('#D8E5DF')
              }.width('100%').margin({ top: 8 })
            }
            .width('100%').padding(18).borderRadius(18)
            .linearGradient({ angle: 135, colors: [['#0F2E26', 0], [CP.primary, 1]] })

            Column()
              .width(44).height(132).borderRadius(22)
              .backgroundColor('rgba(244, 241, 232, 0.2)').rotate({ angle: 22 })
              .translate({ x: cpShineO(this.wave) })
              .position({ x: -28, y: 0 })
          }.width('100%').height(132).clip(true)

个人中心的用户卡片使用了从更深的 #0F2E26CP.primary 的渐变,比其他标签页的 Banner 更深沉,传达会员的尊享质感。扫光特效使用暖沙米色的半透明层(rgba(244, 241, 232, 0.2)),旋转 22 度后通过 cpShineO 驱动水平位移。clip(true) 确保光带不溢出卡片边界。用户信息显示昵称、ID、会员等级(松果会员)和露营晚数(32 晚),这些数据是露营社区特有的用户画像维度。

消费趋势月度柱状图展示半年消费数据,每根柱子使用墨松绿到辉光绿的垂直渐变。柱顶标注金额,底部标注月份。半年累计 ¥11710 和"五月露营高峰"的文字摘要帮助用户理解消费趋势。

会员体系的弹窗展示了"松果会员"到"雪松会员"的升级路径,包含专属营地折扣、免押金租借、免费保养、观星优先预约等权益,未解锁权益以黄色标签提示,形成了清晰的用户激励体系。

技术点对比表格

技术维度 实现方案 设计优势 适用场景
主题管理 CpTheme 接口 + CP 常量 山系复古色集中管控 全局色彩引用
温标可视化 双层渐变底条+蓝色进度条+指示线 温度光谱直觉感知 睡袋温标对比
材质色编码 cpMaterialColor 函数映射 颜色即类别,无需阅读 炊具排行榜/标签
烟雾动效 cpSmokeX 低频高幅正弦 营地篝火氛围营造 首页 Banner
脉冲光晕 cpPulse 驱动圆形缩放 灯具持续发光暗示 灯具网格卡片
弹窗管理 @State 布尔值 + if 条件渲染 多弹窗互斥控制 4个功能弹窗
亮度映射 cpLumenW 以800lm为满刻度 流明值百分比化 灯具亮度对比
会员体系 等级+权益+解锁路径 用户激励可视化 个人中心弹窗
扫光特效 半透明层+旋转+位移+裁剪 尊享质感微交互 会员卡区域
水平滚动 Scroll + Horizontal方向 营地快速浏览 营地推荐区

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface CpTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
}

const CP: CpTheme = {
  bg: '#F4F1E8',
  primary: '#1F4B3F',
  accent: '#D98E4A',
  card: '#FFFFFF',
  text: '#2E2A22',
  sub: '#8C8471',
  line: '#E2DCC9',
  glow: '#3E7C6B'
}

interface CpTent {
  name: string
  brand: string
  capacity: number
  weight: number
  price: number
  orig: number
  sale: number
  season: string
  icon: string
}

const CP_TENTS: CpTent[] = [
  { name: '云顶隧道帐 3 人', brand: 'NEMO', capacity: 3, weight: 3.8, price: 2899, orig: 3299, sale: 156, season: '三季帐', icon: '⛺' },
  { name: '胡迪尼轻量双人帐', brand: 'Hilleberg', capacity: 2, weight: 1.9, price: 4599, orig: 5199, sale: 88, season: '四季帐', icon: '🏔' },
  { name: '星野天幕 5 米', brand: 'Snow Peak', capacity: 6, weight: 4.2, price: 3299, orig: 3899, sale: 132, season: '遮阳幕', icon: '🌟' },
  { name: '山脊穹顶帐 4 人', brand: 'Coleman', capacity: 4, weight: 5.6, price: 1599, orig: 1899, sale: 288, season: '三季帐', icon: '🏕' },
  { name: '骑脊超轻单人帐', brand: 'MSR', capacity: 1, weight: 1.2, price: 3199, orig: 3599, sale: 121, season: '三季帐', icon: '⛺' },
  { name: '营地屋形棉布帐', brand: 'DOD', capacity: 4, weight: 8.4, price: 2399, orig: 2799, sale: 195, season: '四季帐', icon: '🏠' },
  { name: '清风速开帐 2 人', brand: 'Naturehike', capacity: 2, weight: 2.8, price: 499, orig: 699, sale: 642, season: '三季帐', icon: '💨' },
  { name: '黑标重型狩猎帐', brand: 'Tentipi', capacity: 8, weight: 12.5, price: 6999, orig: 7999, sale: 43, season: '四季帐', icon: '🌲' },
  { name: '晨雾印第安帐', brand: 'DOD', capacity: 3, weight: 6.1, price: 1899, orig: 2199, sale: 174, season: '三季帐', icon: '🪶' },
  { name: '轻量金字塔帐', brand: 'Luxosil', capacity: 2, weight: 0.9, price: 3799, orig: 4299, sale: 96, season: '三季帐', icon: '🔺' }
]

interface CpCook {
  name: string
  brand: string
  material: string
  weight: number
  price: number
  sale: number
  icon: string
}

const CP_COOKS: CpCook[] = [
  { name: '钛合金一体锅 900ml', brand: 'TOAKS', material: '钛', weight: 108, price: 329, sale: 386, icon: '🍳' },
  { name: '蜂巢折叠炉 G2', brand: 'BRS', material: '钛', weight: 68, price: 259, sale: 512, icon: '🔥' },
  { name: '铝合金套锅 4 件套', brand: 'Fire Maple', material: '铝', weight: 420, price: 199, sale: 421, icon: '🥘' },
  { name: '雪峰铸铁煎盘 26cm', brand: 'Snow Peak', material: '铸铁', weight: 1250, price: 599, sale: 187, icon: '🥞' },
  { name: '钛杯双耳 450ml', brand: 'TOAKS', material: '钛', weight: 92, price: 169, sale: 598, icon: '☕' },
  { name: '防风柴火炉折叠款', brand: 'Bushcraft', material: '不锈钢', weight: 680, price: 289, sale: 264, icon: '🌪' },
  { name: '便携咖啡手冲套装', brand: 'Hario', material: '玻璃', weight: 350, price: 439, sale: 176, icon: '🫖' },
  { name: '野营烤肉网架', brand: 'DOD', material: '不锈钢', weight: 890, price: 159, sale: 343, icon: '🍢' }
]

interface CpLamp {
  name: string
  brand: string
  lumens: number
  runtime: number
  price: number
  sale: number
  mode: string
  icon: string
}

const CP_LAMPS: CpLamp[] = [
  { name: '榉木煤油灯', brand: 'Petromax', lumens: 300, runtime: 20, price: 899, sale: 92, mode: '煤油', icon: '🏮' },
  { name: '气灯 HT-5', brand: 'Fire Maple', lumens: 350, runtime: 8, price: 459, sale: 154, mode: '燃气', icon: '💡' },
  { name: '棉花糖串灯 10 米', brand: 'DOD', lumens: 60, runtime: 30, price: 129, sale: 687, mode: '充电', icon: '✨' },
  { name: '营地棒灯 500 流明', brand: 'BioLite', lumens: 500, runtime: 24, price: 399, sale: 231, mode: '充电', icon: '🔦' },
  { name: '复古桅杆灯', brand: 'Snow Peak', lumens: 180, runtime: 40, price: 689, sale: 118, mode: '充电', icon: '🕯' },
  { name: '太阳能庭院灯', brand: 'Naturehike', lumens: 220, runtime: 12, price: 189, sale: 402, mode: '太阳能', icon: '🔋' },
  { name: '头灯夜跑款 800 流明', brand: 'Black Diamond', lumens: 800, runtime: 10, price: 529, sale: 289, mode: '充电', icon: '⛑' },
  { name: '篝火氛围投影灯', brand: 'DOD', lumens: 100, runtime: 18, price: 259, sale: 356, mode: '充电', icon: '🔥' }
]

interface CpBag {
  name: string
  brand: string
  limit: number
  fill: string
  weight: number
  price: number
  orig: number
  sale: number
  icon: string
}


const CP_BAGS: CpBag[] = [
  { name: '北极鹅绒 -25° 木乃伊', brand: 'Western Mountaineering', limit: -25, fill: '900 蓬鹅绒', weight: 1180, price: 4899, orig: 5499, sale: 66, icon: '🥶' },
  { name: '信封棉睡袋 5°', brand: 'Coleman', limit: 5, fill: '中空棉', weight: 1450, price: 259, orig: 359, sale: 488, icon: '🛌' },
  { name: '超轻羽绒 0° 款', brand: 'Sea to Summit', limit: 0, fill: '850 蓬鸭绒', weight: 720, price: 2199, orig: 2599, sale: 142, icon: '羽绒' },
  { name: '四季抗风 -10° 妈咪', brand: 'Marmot', limit: -10, fill: '650 蓬鸭绒', weight: 1350, price: 1599, orig: 1899, sale: 187, icon: '❄️' },
  { name: '车中泊专用被枕', brand: 'DOD', limit: 8, fill: '棉+纤维', weight: 1600, price: 499, orig: 599, sale: 312, icon: '🚐' },
  { name: '夏季凉感睡袋 15°', brand: 'Naturehike', limit: 15, fill: '轻量棉', weight: 850, price: 179, orig: 229, sale: 521, icon: '🌙' }
]

interface CpOrder {
  id: string
  name: string
  state: string
  price: number
  date: string
  icon: string
}

const CP_ORDERS: CpOrder[] = [
  { id: 'CP20260812', name: '云顶隧道帐 3 人', state: '已完成', price: 2899, date: '08-12', icon: '⛺' },
  { id: 'CP20260805', name: '棉花糖串灯 10 米', state: '待收货', price: 129, date: '08-05', icon: '✨' },
  { id: 'CP20260728', name: '钛杯双耳 450ml', state: '已发货', price: 169, date: '07-28', icon: '☕' },
  { id: 'CP20260719', name: '超轻羽绒 0° 款', state: '已完成', price: 2199, date: '07-19', icon: '🥶' },
  { id: 'CP20260706', name: '星野天幕 5 米', state: '已完成', price: 3299, date: '07-06', icon: '🌟' },
  { id: 'CP20260622', name: '营地棒灯 500 流明', state: '已取消', price: 399, date: '06-22', icon: '🔦' }
]

interface CpFav {
  name: string
  price: number
  tag: string
  icon: string
}

const CP_FAVS: CpFav[] = [
  { name: '胡迪尼轻量双人帐', price: 4599, tag: '轻量', icon: '🏔' },
  { name: '黑标重型狩猎帐', price: 6999, tag: '重装', icon: '🌲' },
  { name: '榉木煤油灯', price: 899, tag: '氛围', icon: '🏮' },
  { name: '北极鹅绒 -25° 木乃伊', price: 4899, tag: '极寒', icon: '🥶' },
  { name: '便携咖啡手冲套装', price: 439, tag: '手冲', icon: '🫖' },
  { name: '头灯夜跑款 800 流明', price: 529, tag: '夜行', icon: '⛑' }
]

interface CpMonth {
  m: string
  v: number
}

const CP_MONTHS: CpMonth[] = [
  { m: '2月', v: 620 },
  { m: '3月', v: 980 },
  { m: '4月', v: 2450 },
  { m: '5月', v: 3100 },
  { m: '6月', v: 1890 },
  { m: '7月', v: 2670 }
]

interface CpTab {
  icon: string
  label: string
}

const CP_TABS: CpTab[] = [
  { icon: '🏕', label: '首页' },
  { icon: '⛺', label: '帐篷' },
  { icon: '🍳', label: '炊具' },
  { icon: '💡', label: '灯具' },
  { icon: '🛌', label: '睡袋' },
  { icon: '🧭', label: '我的' }
]

function cpPrice(v: number): string {
  return '¥' + v.toFixed(0)
}

function cpOff(p: number, o: number): string {
  return (Math.round(p / o * 100) / 10).toFixed(1) + '折'
}

function cpBarW(v: number, m: number): string {
  return Math.min(100, Math.round(v / Math.max(1, m) * 100)) + '%'
}

function cpLumenW(l: number): string {
  return Math.min(100, Math.round(l / 800 * 100)) + '%'
}

function cpTempW(t: number): string {
  return Math.min(100, Math.max(8, Math.round((t + 30) / 45 * 100))) + '%'
}

function cpWeightW(w: number, m: number): string {
  return Math.min(100, Math.round(w / Math.max(1, m) * 100)) + '%'
}

function cpVBarH(v: number, m: number): number {
  return Math.max(6, Math.round(v / Math.max(1, m) * 90))
}

function cpStatusColor(s: string): string {
  if (s === '已完成') {
    return CP.accent
  }
  if (s === '待收货') {
    return '#B45309'
  }
  if (s === '已发货') {
    return '#3E7C6B'
  }
  return '#9CA3AF'
}

function cpSeasonColor(s: string): string {
  if (s === '四季帐') {
    return CP.primary
  }
  if (s === '三季帐') {
    return CP.glow
  }
  if (s === '遮阳幕') {
    return '#B45309'
  }
  return '#8C8471'
}

function cpMaterialColor(m: string): string {
  if (m === '钛') {
    return CP.primary
  }
  if (m === '铝') {
    return '#3E7C6B'
  }
  if (m === '铸铁') {
    return '#57534E'
  }
  if (m === '不锈钢') {
    return '#78716C'
  }
  return '#B45309'
}

function cpModeColor(m: string): string {
  if (m === '煤油') {
    return '#B45309'
  }
  if (m === '燃气') {
    return '#DC2626'
  }
  if (m === '太阳能') {
    return '#D97706'
  }
  return CP.glow
}

function cpTentFilter(arr: CpTent[], t: string): CpTent[] {
  let r: CpTent[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].season === t) {
      r.push(arr[i])
    }
  }
  return r
}

function cpCookFilter(arr: CpCook[], t: string): CpCook[] {
  let r: CpCook[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].material === t) {
      r.push(arr[i])
    }
  }
  return r
}

function cpLampFilter(arr: CpLamp[], t: string): CpLamp[] {
  let r: CpLamp[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].mode === t) {
      r.push(arr[i])
    }
  }
  return r
}

function cpBagFilter(arr: CpBag[], t: string): CpBag[] {
  let r: CpBag[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || (t === '冬季款' && arr[i].limit <= 0) || (t === '春秋款' && arr[i].limit > 0)) {
      r.push(arr[i])
    }
  }
  return r
}

function cpTentTop(): CpTent[] {
  let r: CpTent[] = CP_TENTS.slice()
  r.sort((a: CpTent, b: CpTent) => b.price - a.price)
  return r.slice(0, 6)
}

function cpCookTop(): CpCook[] {
  let r: CpCook[] = CP_COOKS.slice()
  r.sort((a: CpCook, b: CpCook) => b.sale - a.sale)
  return r
}

function cpLampTop(): CpLamp[] {
  let r: CpLamp[] = CP_LAMPS.slice()
  r.sort((a: CpLamp, b: CpLamp) => b.lumens - a.lumens)
  return r.slice(0, 6)
}

function cpBagTop(): CpBag[] {
  let r: CpBag[] = CP_BAGS.slice()
  r.sort((a: CpBag, b: CpBag) => b.sale - a.sale)
  return r
}


    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(18, 30, 26, 0.55)')
      .position({ x: 0, y: 0 }).zIndex(999)
      .onClick(() => {
        this.showNick = false
        this.showAddr = false
        this.showAbout = false
      })
  }
}


在这里插入图片描述

总结

本文系统性剖析了基于 HarmonyOS ArkTS 构建的精致露营装备馆电商应用,从主题系统、数据模型、纯函数处理层、帧动画引擎、六大组件架构、CRUD 全流程、四种特色数据可视化(价格对比条形图、亮度对比、温标双色条、材质色编码柱状图)到社区化运营要素(营地推荐、装备清单、会员体系、优惠券)进行了全面解析。

该应用最具创意的技术亮点在于多维度的数据可视化设计。温标双色条通过底层渐变温度光谱加蓝色进度条加指示线的三层叠加,将抽象的温度参数转化为直觉化的视觉位置感知;材质色编码通过纯函数将四种炊具材质映射为四种颜色,使颜色本身成为信息载体而非纯装饰;脉冲光晕使灯具卡片从静态商品图变为持续发光的场景化展示。这些可视化方案不依赖任何图表库,仅凭 ArkTS 原生组件的 Stack 叠加、linearGradient 渐变和数学函数计算就实现了专业级的数据可视化效果。

从工程架构角度,该应用的"纯函数数据层 + 声明式视图层"二元分离架构在六个标签页中保持了一致性。全部过滤器、排序器、格式化器、最大值计算器和动画函数均以全局纯函数形式存在,组件仅负责状态持有和视图编排。这种设计使数据处理逻辑可以脱离 UI 独立测试和复用,同时保证了 ArkTS 状态驱动渲染机制的正确性。每个组件的 aboutToAppearaboutToDisappear 生命周期配对管理定时器,杜绝了内存泄漏隐患。七个帧动画函数以正弦、绝对值、取模等基础数学运算为驱动核心,在无专业动画框架依赖下实现了覆盖 Banner、图标、光晕、涟漪、扫光等场景的丰富有机动态效果,展现了 ArkTS 在动效表达上的原生能力。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐