引言

在这里插入图片描述

随着新能源汽车保有量的持续攀升,充电服务场景的数字化重构已成为出行生态中不可忽视的一环。一辆纯电动车主在日常通勤中,可能同时涉及快充补能、社区慢充、谷段电价优化、充电卡券消费、社区内容互动等多种诉求。传统的单一功能充电应用已经无法满足用户在"找桩—比价—预约—充电—社交"全链路中的复合需求,因此构建一个多Tab聚合型充电服务大厅成为提升用户体验的关键技术路径。本文将围绕一套完整的新能源充电驿站主页面源码,逐段拆解其声明式UI架构、状态管理策略、数据模型设计以及弹层交互逻辑。

从技术架构层面来看,本应用采用了HarmonyOS ArkTS的声明式UI范式,通过@Entry@Component装饰器将整个页面声明为一个可复用的组件入口。页面内部以@State装饰器管理大量可变状态,包括当前选中的Tab索引、各弹层显示布尔值、选中数据项引用、Chip选择索引、步进计数器等,共计十余个响应式状态变量。这种集中式状态管理方式确保了当任意状态发生变化时,ArkUI框架能够自动触发相关UI组件的重新渲染,从而实现数据驱动视图的核心设计理念。同时,页面通过@Builder装饰器将不同功能区域的UI拆分为多个构建器函数,实现了视图层的模块化解耦。

从业务设计层面来看,应用围绕充电驿站这一核心场景,构建了七大功能Tab:充电大厅、快充专区、慢充社区、充电卡券、里程社区、消息通知和个人中心。每个Tab承载不同维度的数据列表和可视化组件,如柱状图、进度条、横向滚动画廊、双列网格等。此外,页面集成了五种弹层交互:发布充电桩、预约充电、办理充电卡、删除确认和充电站详情,涵盖了从内容发布到消费决策的完整交互闭环。整体配色以电光蓝(#1565C0)为主色调,辅以新能源绿(#00C853)强调环保属性,深蓝(#0D47A1)用于慢充区域,警告红(#FF5252)用于提示和警示,形成了层次分明的视觉语言体系。

一、数据模型与状态管理基础

在这里插入图片描述

1.1 GoodsItem接口定义

应用首先定义了一个全局接口GoodsItem,作为所有列表项数据的统一数据契约。该接口包含七个字段,分别承载商品名称、价格、描述、评分、数值、标签和热度标识。这种统一数据模型的设计使得不同功能Tab下的列表渲染逻辑可以高度复用,只需要在展示层对不同字段赋予不同的语义解释即可。

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: string
}

这里需要特别说明的是,price字段在不同场景下的语义是不同的。在充电大厅中,price代表单次充电费用(以分为单位);在快充专区中,price表示每度电的价格(同样以分为单位);在里程社区帖子列表中,price则被复用为评论数量。这种字段语义复用的设计虽然减少了接口数量,但也要求开发者在渲染时做精确的语义映射。score字段在充电大厅中表示空闲率,在社区帖子中表示帖子热度,在卡券列表中表示热度百分比。num字段同样具有多义性:在充电大厅中表示距离(米),在快充专区中表示功率(kW),在社区帖子中表示点赞数。hot字段用于显示热门标识标签,如"爆"“热”“新”"荐"等。

1.2 组件声明与状态变量

在这里插入图片描述

页面的核心组件通过@Entry@Component装饰器声明为应用入口组件。组件内部定义了大量的@State变量来管理页面状态。

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showAddModal: boolean = false
  @State showBookModal: boolean = false
  @State showCardModal: boolean = false
  @State showDeleteModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State chipA: number = 0
  @State chipB: number = 0
  @State stepNum: number = 1
  @State kindIdx: number = 0
  @State powerIdx: number = 0
  @State periodIdx: number = 0
  @State cardTypeIdx: number = 0
  @State vendorIdx: number = 0

上述状态变量可以分为三类。第一类是导航状态,currentTab控制当前激活的Tab页面。第二类是弹层显示状态,五个布尔变量分别控制五种弹层的显示与隐藏,初始值均为false,确保页面加载时不会意外弹出任何弹层。第三类是交互选择状态,selectedItem保存当前被用户点击选中的列表项,chipAchipB用于控制头部筛选Chip的选中索引,stepNum用于预约充电时长和卡券数量的步进控制,kindIdxvendorIdx这一组索引变量则分别用于发布充电桩弹层中桩类型、充电功率、充电时段、卡种和桩企的选择。

1.3 静态数据数组与Chip配置

在这里插入图片描述

组件内部定义了大量private数组,作为模拟数据源和筛选选项配置。

  private tabs: string[] = ['充电大厅', '快充专区', '慢充社区', '充电卡券', '里程社区', '消息', '我的']
  private priceChips: string[] = ['尖峰', '高峰', '平段', '谷段']
  private kindChips: string[] = ['直流快充', '交流慢充', '超级快充']
  private powerChips: string[] = ['60kW', '120kW', '480kW']
  private periodChips: string[] = ['立即充电', '低谷时段', '预约时段']
  private cardTypeChips: string[] = ['次卡', '月卡', '季卡', '年卡']
  private vendorChips: string[] = ['特来电', '星星充电', '小桔', '国网']
  private topicChips: string[] = ['续航实测', '充电省钱', '电车露营', '冬季续航', '桩企评测']
  private facilityChips: string[] = ['休息室', '卫生间', '便利店', '洗车', 'Wi-Fi', '餐饮']
  private menuList: string[] = ['充电订单', '我的卡券', '余额钱包', '发票管理', '常用充电站', '设置']
  private cardPriceArr: number[] = [19, 29, 69, 199]

tabs数组定义了底部导航栏的七个Tab标签。priceChips对应电价时段,这与国内电网分时电价政策一致,谷段充电是用户省钱的核心场景。kindChipspowerChips分别定义了充电桩类型和功率档位,从60kW到480kW覆盖了当前主流的充电功率范围。vendorChips列出了四大充电桩运营商。topicChips用于里程社区的话题筛选。facilityChips定义了充电站配套服务设施。cardPriceArrcardTypeChips配合使用,为不同卡种提供价格映射。

1.4 充电大厅数据列表

在这里插入图片描述

充电大厅是应用的第一个Tab,承载了24条充电站数据,是目前所有列表中数据量最大的一个。

  // 充电大厅: 24 条充电站 price 为单次费用(分) num 为距离(米) score 为空闲率
  private mainList: GoodsItem[] = [
    { name: '特来电超充站(望京店)', price: 128, desc: '直流快充桩 24 个', score: 96, num: 480, tag: '超快充', hot: '爆' },
    { name: '星星充电站(朝阳大悦城)', price: 96, desc: '交流慢充桩 18 个', score: 82, num: 320, tag: '会员9折', hot: '热' },
    { name: '小桔充电站(酒仙桥)', price: 156, desc: '直流快充桩 32 个', score: 91, num: 460, tag: '新客立减', hot: '新' },
    { name: '国家电网充电站(朝阳公园)', price: 88, desc: '直流快充桩 16 个', score: 74, num: 210, tag: '谷段优惠', hot: '热' },
    { name: '蔚来超充站(亦庄)', price: 188, desc: '超级快充桩 12 个', score: 98, num: 490, tag: '支持换电', hot: '爆' },
    { name: '特斯拉超充站(国贸)', price: 198, desc: '超级快充桩 20 个', score: 95, num: 350, tag: '车主专享', hot: '爆' },
    // ... 更多充电站数据
  ]

每条数据包含充电站名称、单次费用(以分为单位,128分即1.28元)、桩数量描述、空闲率评分(96表示96%空闲)、距离(以米为单位,480米)、标签和热度标识。数据覆盖了特来电、星星充电、小桔、国家电网、蔚来、特斯拉六大品牌运营商,覆盖了北京朝阳区多个商圈。这种高密度数据模拟为列表渲染性能测试和UI布局验证提供了真实的压力场景。

二、辅助方法与业务逻辑函数

在这里插入图片描述

2.1 选中项取值方法

应用定义了一系列辅助方法来安全地从selectedItem中提取字段值,避免空指针异常。

  selName(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.name
    }
    return ''
  }

  selPrice(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.price
    }
    return 0
  }

  selScore(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.score
    }
    return 0
  }

  selTag(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.tag
    }
    return ''
  }

每个方法都遵循相同的模式:先检查selectedItem是否为null,如果不为空则返回对应字段值,否则返回该类型的默认值(空字符串或零)。这种防御性编程模式确保了在用户尚未点击任何列表项时,弹层中不会出现undefined或运行时错误。这些方法在详情弹层、预约弹层和卡券弹层中被广泛调用,是弹层数据绑定的核心数据源。

2.2 最大值计算与品牌颜色映射

在这里插入图片描述

maxNumOf方法用于计算列表中num字段的最大值,主要服务于柱状图渲染场景中的比例缩放计算。brandColor方法则根据充电站名称中的品牌关键词返回对应的品牌主色调。

  maxNumOf(list: GoodsItem[]): number {
    let m: number = 1
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }

  brandColor(item: GoodsItem): string {
    if (item.name.indexOf('特来电') >= 0) {
      return '#1565C0'
    }
    if (item.name.indexOf('星星') >= 0) {
      return '#00C853'
    }
    if (item.name.indexOf('小桔') >= 0) {
      return '#FF5252'
    }
    if (item.name.indexOf('国') >= 0) {
      return '#0D47A1'
    }
    if (item.name.indexOf('蔚来') >= 0) {
      return '#00BFA5'
    }
    if (item.name.indexOf('特斯拉') >= 0) {
      return '#7B1FA2'
    }
    return '#FF9800'
  }

maxNumOf方法初始化最大值为1(避免除零错误),遍历列表比较更新。这一方法在快充功率分布柱状图、慢充距离分布柱状图、卡券折扣力度柱状图中均有使用。brandColor方法通过字符串搜索匹配品牌名称,为每个品牌分配独特的色彩标识:特来电使用电光蓝,星星充电使用新能源绿,小桔使用警告红,国家电网使用深蓝,蔚来使用青绿色,特斯拉使用深紫色。这种品牌色彩映射不仅增强了视觉辨识度,也为用户快速定位偏好的运营商提供了直觉化引导。

2.3 预约费用计算

在这里插入图片描述

bookFee方法用于在预约充电弹层中实时计算预估费用,将选中充电站的单位价格乘以用户选择的充电时长。

  headChar(item: GoodsItem): string {
    return item.name.substring(0, 1)
  }

  bookFee(): number {
    return Math.floor((this.selPrice() / 100) * this.stepNum)
  }

headChar方法提取充电站名称的首字符,用于在列表头像区域显示品牌首字母缩写。bookFee方法将selPrice()返回的以分为单位的价格转换为元,再乘以stepNum(用户通过步进器选择的充电小时数),最后通过Math.floor取整。这一计算逻辑与stepNum状态变量形成联动:当用户点击步进器的加号或减号按钮时,stepNum发生变化,ArkUI框架自动重新调用bookFee()并更新弹层中显示的预估费用,体现了声明式UI数据驱动视图的核心优势。

三、头部与底部导航构建

3.1 静态头部Builder

头部区域是应用的视觉锚点,集成了定位、标题搜索和电价时段筛选三个核心功能区块。

  @Builder
  headerBuilder() {
    Column() {
      Row() {
        Row({ space: 4 }) {
          Text('北京·朝阳区')
            .fontSize(13)
            .fontColor('#FFFFFF')
          Text('▾')
            .fontSize(12)
            .fontColor('#FFFFFF')
        }

        Column()
          .layoutWeight(1)

        Stack({ alignContent: Alignment.TopEnd }) {
          Text('🔔')
            .fontSize(20)
            .fontColor('#FFFFFF')
          Text('3')
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF5252')
            .borderRadius(9)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10 })

      Row() {
        Text('充电驿站')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Column()
          .layoutWeight(1)

        Row({ space: 6 }) {
          Text('🔍')
            .fontSize(13)
          Text('搜充电站/桩企')
            .fontSize(12)
            .fontColor('#999999')
          Text('⌗')
            .fontSize(14)
            .fontColor('#666666')
        }
        .height(30)
        .padding({ left: 12, right: 12 })
        .backgroundColor('#F5F5F5')
        .borderRadius(15)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10, bottom: 10 })

      Row({ space: 8 }) {
        ForEach(this.priceChips, (chip: string, index: number) => {
          Text(chip)
            .fontSize(11)
            .padding({ left: 12, right: 12, top: 4, bottom: 4 })
            .borderRadius(12)
            .fontColor(this.chipA === index ? '#1565C0' : '#E3F2FD')
            .backgroundColor(this.chipA === index ? '#FFFFFF' : 'rgba(255,255,255,0.15)')
            .onClick(() => {
              this.chipA = index
            })
        }, (chip: string) => chip)
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#1565C0')
  }

头部构建器分为三层结构。第一层是定位行,左侧显示"北京·朝阳区"并附带下拉箭头,右侧使用Stack容器将通知铃铛图标与未读数角标进行层叠布局,Alignment.TopEnd使角标对齐到右上角。第二层是标题搜索行,左侧加粗显示"充电驿站"品牌名称,右侧是圆角搜索框,使用灰色背景和圆角营造输入框视觉。第三层是电价时段Chip行,通过ForEach遍历priceChips数组渲染四个电价时段选项,选中的Chip使用白色背景和蓝色文字,未选中的使用半透明白色背景。

Chip的选中状态通过this.chipA === index三元表达式实现条件渲染,点击事件将chipA更新为当前索引。整个头部使用电光蓝背景色#1565C0,与白色文字形成高对比度,确保在户外强光环境下的可读性。

3.2 底部导航栏Builder

底部导航栏是页面间切换的核心交互组件,通过Tab列表实现七个功能区域的切换。

  @Builder
  bottomBuilder() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column({ space: 2 }) {
          Text(tab)
            .fontSize(10)
            .fontColor(this.currentTab === index ? '#1565C0' : '#999999')
          Column()
            .width(18)
            .height(3)
            .borderRadius(2)
            .backgroundColor(this.currentTab === index ? '#1565C0' : '#FFFFFF00')
        }
        .layoutWeight(1)
        .onClick(() => {
          this.currentTab = index
        })
      }, (tab: string) => tab)
    }
    .width('100%')
    .height(56)
    .backgroundColor('#FFFFFF')
  }

底部导航栏使用Row容器水平排列七个Tab项,每个Tab通过layoutWeight(1)等分宽度。每个Tab项内部是一个Column,上方是文字标签,下方是3px高的指示条。选中状态下文字变为电光蓝、指示条显示蓝色背景;未选中状态下文字为灰色、指示条透明。点击事件直接更新currentTab状态变量,触发ArkUI框架重新渲染build()方法中的条件分支,从而切换显示对应Tab的内容区域。指示条的圆角设计和适度宽度(18vp)为底部栏增添了精致感。

四、充电大厅Tab内容构建

4.1 数据概览与发布入口

充电大厅Tab是用户进入应用后看到的首屏内容,顶部展示今日充电量、节省费用和碳减排三项核心数据指标。

  @Builder
  tab0() {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 4 }) {
          Text('28.6')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1565C0')
          Text('今日充电量(度)')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(34)
          .backgroundColor('#E3F2FD')

        Column({ space: 4 }) {
          Text('¥46')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00C853')
          Text('节省费用')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(34)
          .backgroundColor('#E3F2FD')

        Column({ space: 4 }) {
          Text('12.4kg')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00C853')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('碳减排')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Row({ space: 6 }) {
        Text('+')
          .fontSize(13)
          .fontColor('#FFFFFF')
        Text('发布充电桩')
          .fontSize(13)
          .fontColor('#FFFFFF')
      }
      .padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('#00C853')
      .borderRadius(18)
      .onClick(() => {
        this.showAddModal = true
      })

数据概览区使用三等分布局,每个指标之间用1px宽的浅蓝色分割线隔开。今日充电量使用蓝色突出显示,节省费用和碳减排使用绿色显示。碳减排数值附加了scale缩放动画和iterations: -1无限循环,以脉冲效果吸引用户注意环保贡献。下方的"发布充电桩"按钮使用绿色胶囊形设计,点击后触发showAddModaltrue,从而在build()方法的Stack层叠中渲染发布弹层。

4.2 充电站列表卡片

充电站列表是充电大厅Tab的核心内容区域,通过ForEach遍历mainList渲染24个充电站卡片。

      Column({ space: 10 }) {
        ForEach(this.mainList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column() {
              Text(this.headChar(item))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(44)
            .height(44)
            .borderRadius(22)
            .backgroundColor(this.brandColor(item))

            Column({ space: 5 }) {
              Row({ space: 6 }) {
                Text(item.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                  .maxLines(1)
                Text(item.hot)
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF5252')
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
              }

              Text(item.desc)
                .fontSize(11)
                .fontColor('#999999')
                .maxLines(1)

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .borderRadius(3)
                  .backgroundColor('#E3F2FD')
                Column()
                  .width(`${item.score}%`)
                  .height(6)
                  .borderRadius(3)
                  .backgroundColor('#00C853')
              }
              .width('100%')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column({ space: 5 }) {
              Text(`¥${(item.price / 100).toFixed(2)}`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1565C0')
              Text(item.tag)
                .fontSize(10)
                .fontColor('#1565C0')
                .backgroundColor('#E3F2FD')
                .borderRadius(8)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(`${(item.num / 100).toFixed(1)}km`)
                .fontSize(10)
                .fontColor('#999999')
            }
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

每个卡片采用三段式横向布局:左侧是44x44的圆形品牌头像,通过brandColor方法获取品牌色作为背景,通过headChar方法提取品牌首字符作为头像内容。中间区域通过layoutWeight(1)占据剩余空间,展示充电站名称(加粗、单行)、桩描述、空闲率进度条。进度条使用Stack层叠布局,底层是浅蓝色背景条,上层是绿色填充条,宽度通过模板字符串${item.score}%动态设置。右侧区域展示价格(将分转换为元并保留两位小数)、标签和距离(将米转换为公里)。点击卡片时将selectedItem设为当前项并打开详情弹层。

五、快充专区与慢充社区Tab构建

5.1 快充专区功率分布柱状图

快充专区Tab顶部展示本月充电功率分布柱状图,通过计算每个桩的功率值占最大值的比例来渲染柱形高度。

  @Builder
  tab1() {
    Column({ space: 12 }) {
      Text('本月充电功率分布 (kW)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Row({ space: 6 }) {
        ForEach(this.fastList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(`${item.num}`)
              .fontSize(9)
              .fontColor('#1565C0')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.fastList) * 120)
              .backgroundColor(this.brandColor(item))
              .borderRadius(4)
            Text(item.name)
              .fontSize(8)
              .fontColor('#999999')
              .maxLines(1)
          }
          .width(20)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

柱状图通过ForEach遍历fastList数据数组,每个柱子是一个Column容器,内部从上到下依次排列功率数值文字、柱形条和名称文字。柱形高度通过item.num / this.maxNumOf(this.fastList) * 120计算得出,其中120是柱子的最大高度基准值。maxNumOf方法确保最大值的柱子高度恰好为120vp,其余按比例缩放。整个Row容器设置alignItems(VerticalAlign.End)使所有柱子底部对齐,形成标准的柱状图视觉效果。柱形条颜色通过brandColor方法按品牌区分,增强了数据的可辨识性。

5.2 快充桩双列网格卡片

在柱状图下方,快充专区使用Flex容器以双列网格布局展示空闲快充桩卡片。

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.fastList, (item: GoodsItem) => {
          Column({ space: 8 }) {
            Row({ space: 6 }) {
              Column()
                .width(8)
                .height(8)
                .borderRadius(4)
                .backgroundColor(item.tag === '空闲' ? '#00C853' : '#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.tag)
                .fontSize(10)
                .fontColor(item.tag === '空闲' ? '#00C853' : '#FF5252')
            }

            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(1)

            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)

            Text(`${item.num}kW · 空闲${item.score}%`)
              .fontSize(11)
              .fontColor('#1565C0')

            Text(`¥${(item.price / 100).toFixed(2)}/度`)
              .fontSize(11)
              .fontColor('#FF5252')
          }
          .width('48%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

Flex容器设置wrap: FlexWrap.Wrap允许子元素自动换行,justifyContent: FlexAlign.SpaceBetween使双列卡片之间均匀分布间距。每个卡片宽度设为48%,留出4%的间距空间。卡片顶部的状态指示灯是一个8x8的圆形色块,空闲状态为绿色并附带脉冲动画,占用状态为红色无动画。卡片内容依次展示桩名称、位置描述、功率与空闲率、每度电价格。这种双列网格布局在有限屏幕空间内最大化了信息密度,同时通过圆角卡片和白色背景保持了良好的视觉呼吸感。

5.3 慢充社区数据展示

慢充社区Tab以深蓝色卡片为视觉特色,展示谷段电价、空闲桩数量和谷段开始时间三项关键信息。

  @Builder
  tab2() {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 4 }) {
          Text('0.31')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('谷段电价(元/度)')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(36)
          .backgroundColor('rgba(255,255,255,0.3)')

        Column({ space: 4 }) {
          Text('86')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('空闲慢充桩(个)')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(36)
          .backgroundColor('rgba(255,255,255,0.3)')

        Column({ space: 4 }) {
          Text('23:00')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('谷段开始')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0D47A1')
      .borderRadius(12)

慢充社区使用深蓝色#0D47A1作为主背景色,与充电大厅的电光蓝形成色调层次差异,从视觉上区分快充与慢充两种场景。谷段电价数值附加了脉冲缩放动画,强调其经济性优势。分割线使用半透明白色rgba(255,255,255,0.3),在深色背景上呈现柔和的分隔效果。三项数据均使用白色文字搭配浅蓝色副标题,确保在深蓝背景上的高可读性。

六、充电流程与交互架构流程图

以下流程图展示了用户从进入充电驿站应用到完成充电预约的完整交互流程,涵盖了Tab切换、列表浏览、详情查看和预约提交等核心环节。

快充专区

慢充社区

充电卡券

里程社区

消息

我的

用户启动应用

加载充电大厅Tab

查看充电站列表

浏览充电站卡片

点击充电站卡片

打开详情弹层

查看详情信息

是否预约充电?

打开预约弹层

关闭弹层返回列表

选择充电时段

调整充电时长

查看预估费用

确认预约?

关闭弹层完成预约

返回充电大厅

切换Tab?

查看功率分布

查看谷段电价

浏览卡券列表

浏览社区帖子

查看消息通知

进入个人中心

领取卡券?

打开卡券弹层

选择卡种与桩企

确认办理

七、充电卡券与里程社区Tab构建

7.1 充电卡券折扣柱状图与列表

充电卡券Tab顶部展示卡券折扣力度柱状图,下方以列表形式展示可领取的卡券。

  @Builder
  tab3() {
    Column({ space: 12 }) {
      Text('卡券折扣力度一览')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Row({ space: 6 }) {
        ForEach(this.cardList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(item.tag)
              .fontSize(9)
              .fontColor('#FF5252')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.cardList) * 100)
              .backgroundColor('#FF5252')
              .borderRadius(4)
          }
          .width(22)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Column({ space: 10 }) {
        ForEach(this.cardList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column()
              .width(4)
              .height(56)
              .borderRadius(2)
              .backgroundColor(this.brandColor(item))

            Column({ space: 5 }) {
              Text(item.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
              Text(item.desc)
                .fontSize(11)
                .fontColor('#999999')
              Text(`热度 ${item.score}%`)
                .fontSize(10)
                .fontColor('#1565C0')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column({ space: 6 }) {
              Text(item.tag)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text('领取')
                .fontSize(12)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF5252')
                .borderRadius(12)
                .padding({ left: 16, right: 16, top: 5, bottom: 5 })
                .onClick(() => {
                  this.selectedItem = item
                  this.showCardModal = true
                })
            }
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

卡券列表每条记录左侧使用4px宽的品牌色竖条作为视觉装饰,与充电站列表的品牌头像设计形成呼应。右侧的折扣标签(如"5折"“8折”)使用20px大字号红色加粗显示,并附带脉冲动画吸引注意力。领取按钮为红色胶囊形设计,点击后将选中项设为当前卡券并打开办理充电卡弹层。柱状图的最大高度基准为100vp,小于快充专区的120vp,视觉上更紧凑,因为卡券信息本身比功率数据更短小。

7.2 里程社区帖子列表

里程社区Tab以话题Chip筛选和帖子信息流为核心内容,模拟了充电用户社区的互动场景。

  @Builder
  tab4() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        ForEach(this.topicChips, (chip: string, index: number) => {
          Text(`# ${chip}`)
            .fontSize(11)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(12)
            .fontColor(this.chipB === index ? '#FFFFFF' : '#1565C0')
            .backgroundColor(this.chipB === index ? '#1565C0' : '#E3F2FD')
            .onClick(() => {
              this.chipB = index
            })
        }, (chip: string) => chip)
      }
      .width('100%')

      Column({ space: 10 }) {
        ForEach(this.postList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column() {
              Text(this.headChar(item))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(40)
            .height(40)
            .borderRadius(20)
            .backgroundColor(this.brandColor(item))

            Column({ space: 6 }) {
              Row({ space: 8 }) {
                Text(item.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                Text(item.tag)
                  .fontSize(10)
                  .fontColor('#BBBBBB')
              }

              Text(item.desc)
                .fontSize(12)
                .fontColor('#666666')
                .maxLines(2)

              Row({ space: 18 }) {
                Row({ space: 4 }) {
                  Text('👍')
                    .fontSize(12)
                  Text(`${item.num}`)
                    .fontSize(11)
                    .fontColor('#1565C0')
                    .scale({ x: 1.05, y: 1.05 })
                    .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                }

                Row({ space: 4 }) {
                  Text('💬')
                    .fontSize(12)
                  Text(`${item.price}`)
                    .fontSize(11)
                    .fontColor('#999999')
                }

                Text(`#${item.hot}`)
                  .fontSize(10)
                  .fontColor('#00C853')
              }
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

话题Chip行使用chipB状态变量控制选中索引,选中时为蓝底白字,未选中时为浅蓝底蓝字,与头部电价时段Chip的设计风格保持一致。帖子卡片采用左侧头像加右侧内容的经典信息流布局。内容区域包含用户名和时间、帖子正文(最多两行)、底部互动数据(点赞数附带脉冲动画、评论数和话题标签)。这里复用了price字段作为评论数、num字段作为点赞数、hot字段作为话题标签,是GoodsItem接口字段语义复用的典型案例。

八、消息与个人中心Tab构建

8.1 消息列表与未读标识

消息Tab以简洁的消息列表形式展示系统通知和用户互动消息,通过红点标识未读消息。

  @Builder
  tab5() {
    Column({ space: 10 }) {
      ForEach(this.msgList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(this.headChar(item))
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(44)
          .height(44)
          .borderRadius(22)
          .backgroundColor(this.brandColor(item))

          Column({ space: 5 }) {
            Text(item.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 8 }) {
            Text(item.tag)
              .fontSize(10)
              .fontColor('#BBBBBB')
            if (item.hot === '未') {
              Column()
                .width(10)
                .height(10)
                .borderRadius(5)
                .backgroundColor('#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
          }
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .onClick(() => {
          this.selectedItem = item
          this.showDeleteModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
  }

消息列表的未读标识通过条件渲染if (item.hot === '未')实现,当消息热度字段为"未"时渲染一个10x10的红色圆点并附带脉冲动画。已读消息则不显示红点。点击消息项会打开删除确认弹层(模拟消息删除操作)。消息时间通过tag字段展示,如"刚刚""10分钟前"等。消息头像同样复用brandColor方法获取颜色,但由于消息来源并非充电站品牌,这里实际上是根据消息名称中的关键词匹配返回的颜色,是一种简化处理。

8.2 个人中心Tab

个人中心Tab展示用户信息卡片、统计数据和菜单列表,是应用的设置与信息入口。

  @Builder
  tab6() {
    Column({ space: 12 }) {
      Column({ space: 14 }) {
        Row({ space: 12 }) {
          Column() {
            Text('D')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1565C0')
          }
          .width(56)
          .height(56)
          .borderRadius(28)
          .backgroundColor('#FFFFFF')

          Column({ space: 6 }) {
            Text('充电达人David')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('铂金会员')
              .fontSize(10)
              .fontColor('#0D47A1')
              .backgroundColor('#FFD54F')
              .borderRadius(8)
              .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Text('›')
            .fontSize(20)
            .fontColor('#FFFFFF')
        }
        .width('100%')

        Row() {
          Column({ space: 4 }) {
            Text('186')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('充电次数')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)

          Column()
            .width(1)
            .height(30)
            .backgroundColor('rgba(255,255,255,0.3)')

          Column({ space: 4 }) {
            Text('2468')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('累计度数')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)

          Column()
            .width(1)
            .height(30)
            .backgroundColor('rgba(255,255,255,0.3)')

          Column({ space: 4 }) {
            Text('312kg')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('碳减排')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)
        }
        .width('100%')
      }
      .width('100%')
      .padding(18)
      .borderRadius(16)
      .linearGradient({ angle: 135, colors: [['#0D47A1', 0], ['#1565C0', 1]] })

个人中心顶部用户卡片使用linearGradient线性渐变背景,135度角从深蓝#0D47A1过渡到电光蓝#1565C0,营造出高级感的渐变视觉效果。用户头像为白色圆形背景加蓝色"D"字母,会员标签使用金色背景#FFD54F并附带脉冲动画。统计区采用三等分布局展示充电次数、累计度数和碳减排量,分割线使用半透明白色适配深色渐变背景。

九、弹层交互构建

9.1 发布充电桩弹层

发布充电桩弹层从底部弹出,提供桩类型选择、功率选择和站点名称输入功能。

  @Builder
  addModal() {
    Column() {
      Column()
        .layoutWeight(1)

      Column({ space: 14 }) {
        Row() {
          Text('发布充电桩')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Column()
            .layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#999999')
            .onClick(() => {
              this.showAddModal = false
            })
        }
        .width('100%')

        Text('选择桩类型')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.kindChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.kindIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.kindIdx === index ? '#1565C0' : '#F5F5F5')
              .onClick(() => {
                this.kindIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择充电功率')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.powerChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.powerIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.powerIdx === index ? '#00C853' : '#F5F5F5')
              .onClick(() => {
                this.powerIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('站点名称')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        TextInput({ placeholder: '请输入站点名称' })
          .height(42)
          .fontSize(13)
          .backgroundColor('#F5F5F5')
          .borderRadius(8)

        Text('确认发布')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
          .width('100%')
          .padding(12)
          .backgroundColor('#00C853')
          .borderRadius(22)
          .onClick(() => {
            this.showAddModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showAddModal = false
    })
  }

弹层采用全屏遮罩加底部弹出面板的经典模式。外层Column设置半透明黑色背景rgba(0,0,0,0.5)作为遮罩,顶部使用layoutWeight(1)的空Column将内容面板推至底部。内容面板设置borderRadius({ topLeft: 16, topRight: 16 })实现顶部圆角效果,constraintSize({ maxHeight: '80%' })限制最大高度为屏幕的80%。桩类型Chip使用蓝色选中态,功率Chip使用绿色选中态,通过不同颜色区分选择层级。遮罩点击事件关闭弹层,内容面板内部使用event.stopPropagation()阻止事件冒泡,确保点击面板内容时不会意外关闭弹层。

9.2 预约充电弹层

预约充电弹层提供充电时段选择、时长步进和实时费用预估功能。

  @Builder
  bookModal() {
    Column() {
      Column()
        .layoutWeight(1)

      Column({ space: 14 }) {
        Row() {
          Text('预约充电')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Column()
            .layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#999999')
            .onClick(() => {
              this.showBookModal = false
            })
        }
        .width('100%')

        Text(this.selName() === '' ? '特来电超充站(望京店)' : this.selName())
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1565C0')

        Text('充电时段')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.periodChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.periodIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.periodIdx === index ? '#1565C0' : '#F5F5F5')
              .onClick(() => {
                this.periodIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('充电时长(1-12小时)')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 20 }) {
          Text('-')
            .fontSize(16)
            .fontColor('#1565C0')
            .width(30)
            .height(30)
            .borderRadius(15)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E3F2FD')
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum -= 1
              }
            })

          Text(`${this.stepNum} 小时`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Text('+')
            .fontSize(16)
            .fontColor('#1565C0')
            .width(30)
            .height(30)
            .borderRadius(15)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E3F2FD')
            .onClick(() => {
              if (this.stepNum < 12) {
                this.stepNum += 1
              }
            })
        }

        Row({ space: 8 }) {
          Text('预估费用')
            .fontSize(13)
            .fontColor('#666666')
          Text('¥' + this.bookFee())
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF5252')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')

        Text('确认预约')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
          .width('100%')
          .padding(12)
          .backgroundColor('#1565C0')
          .borderRadius(22)
          .onClick(() => {
            this.showBookModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showBookModal = false
    })
  }

步进器由减号按钮、数值显示和加号按钮三部分组成。减号按钮在stepNum > 1时递减,加号按钮在stepNum < 12时递增。预估费用通过bookFee()方法实时计算,由于stepNum@State变量,每次步进变化时ArkUI框架自动重新计算并更新费用显示文本。费用数值附带脉冲缩放动画,增强价格变动的视觉反馈。充电站名称通过selName()方法获取,如果为空则显示默认值"特来电超充站(望京店)",确保即使用户未从列表点击进入也能看到合理的内容。

9.3 详情弹层与主构建方法

详情弹层从右侧滑出,占满全高,展示充电站详细信息和服务设施标签。

  @Builder
  detailModal() {
    Column() {
      Column() {
        Column({ space: 6 }) {
          Text('⚡')
            .fontSize(44)
          Text(this.selTag() === '' ? '超充站实景' : this.selTag())
            .fontSize(12)
            .fontColor('#FFFFFF')
        }
        .width('100%')
        .height(170)
        .justifyContent(FlexAlign.Center)
        .linearGradient({ angle: 135, colors: [['#0D47A1', 0], ['#00C853', 1]] })

        Column({ space: 12 }) {
          Text(this.selName() === '' ? '特来电超充站(望京店)' : this.selName())
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .maxLines(2)

          Row({ space: 6 }) {
            Text('★')
              .fontSize(16)
              .fontColor('#FF9800')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text(`${this.selScore()}`)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF9800')
            Text('超2000人充电')
              .fontSize(11)
              .fontColor('#999999')
          }

          // ... 电价、空闲率、进度条、服务设施标签等

          Text('预约充电')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .textAlign(TextAlign.Center)
            .width('100%')
            .padding(12)
            .backgroundColor('#1565C0')
            .borderRadius(22)
            .onClick(() => {
              this.showDetailModal = false
              this.showBookModal = true
            })
        }
        .width('100%')
        .padding(14)
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }
      .width('80%')
      .height('100%')
      .backgroundColor('#FFFFFF')
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .alignItems(HorizontalAlign.End)
    .onClick(() => {
      this.showDetailModal = false
    })
  }

详情弹层的视觉特色在于顶部170vp高的渐变头图区域,使用135度角从深蓝到绿色的线性渐变linearGradient({ angle: 135, colors: [['#0D47A1', 0], ['#00C853', 1]] }),配合闪电emoji图标营造出充电主题的视觉氛围。弹层宽度为80%,从右侧滑入(通过justifyContent(FlexAlign.End)alignItems(HorizontalAlign.End)实现右对齐)。服务设施标签通过Flex容器以FlexWrap.Wrap自动换行方式展示六个设施Chip。底部"预约充电"按钮点击后先关闭详情弹层再打开预约弹层,实现了弹层间的跳转流转。

9.4 主构建方法build()

最终的build()方法是组件的渲染入口,通过Stack层叠容器将主页面和所有弹层组合在一起。

  build() {
    Stack() {
      Column() {
        this.headerBuilder()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tab0()
            } else if (this.currentTab === 1) {
              this.tab1()
            } else if (this.currentTab === 2) {
              this.tab2()
            } else if (this.currentTab === 3) {
              this.tab3()
            } else if (this.currentTab === 4) {
              this.tab4()
            } else if (this.currentTab === 5) {
              this.tab5()
            } else {
              this.tab6()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .width('100%')

        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F2F4F7')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showBookModal) {
        this.bookModal()
      }
      if (this.showCardModal) {
        this.cardModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }

Stack容器是最外层容器,子元素按声明顺序层叠排列。第一个子元素是主页面Column,包含头部、可滚动内容区和底部导航栏。内容区使用Scroll包裹,内部通过if-else条件分支根据currentTab的值渲染对应的Tab构建器。layoutWeight(1)使内容区占据头部和底部之间的全部剩余空间。五个弹层通过各自的条件渲染语句if (this.showXxxModal)控制显示,由于Stack的层叠特性,后声明的弹层会覆盖在先声明的弹层之上。页面背景色设置为#F2F4F7浅灰色,与白色卡片形成微妙的层次对比。

十、技术点对比分析

技术维度 充电大厅Tab 快充专区Tab 慢充社区Tab 充电卡券Tab 里程社区Tab 消息Tab 个人中心Tab
布局容器 Column + ForEach Flex双列网格 Scroll横向滚动 Column + ForEach Column + ForEach Column + ForEach Column + linearGradient
数据量 24条 12条 10条 10条 10条 12条 静态
可视化组件 进度条 柱状图 + 网格卡片 柱状图 + 横滑卡片 柱状图 + 品牌色条 话题Chip 红点未读标识 渐变卡片 + 统计
主色调 #1565C0 #1565C0/#00C853 #0D47A1 #FF5252 #1565C0 #FF5252 #0D47A1→#1565C0
动画效果 碳减排脉冲 状态灯脉冲 谷段电价脉冲 折扣标签脉冲 点赞数脉冲 红点脉冲 会员标签脉冲
点击交互 打开详情弹层 打开充电 打开卡券弹层 话题筛选 打开删除弹层 菜单跳转
字段语义 price=费用/num=距离 price=电价/num=功率 price=费用/num=距离 price=面值/num=折扣 price=评论/num=点赞 price=0/num=排序 静态文本

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 新能源充电驿站主页面
// 配色: 电光蓝 #1565C0 新能源绿 #00C853 深蓝 #0D47A1 浅蓝底 #E3F2FD 警告红 #FF5252

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: string
}

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showAddModal: boolean = false
  @State showBookModal: boolean = false
  @State showCardModal: boolean = false
  @State showDeleteModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State chipA: number = 0
  @State chipB: number = 0
  @State stepNum: number = 1
  @State kindIdx: number = 0
  @State powerIdx: number = 0
  @State periodIdx: number = 0
  @State cardTypeIdx: number = 0
  @State vendorIdx: number = 0

  private tabs: string[] = ['充电大厅', '快充专区', '慢充社区', '充电卡券', '里程社区', '消息', '我的']
  private priceChips: string[] = ['尖峰', '高峰', '平段', '谷段']
  private kindChips: string[] = ['直流快充', '交流慢充', '超级快充']
  private powerChips: string[] = ['60kW', '120kW', '480kW']
  private periodChips: string[] = ['立即充电', '低谷时段', '预约时段']
  private cardTypeChips: string[] = ['次卡', '月卡', '季卡', '年卡']
  private vendorChips: string[] = ['特来电', '星星充电', '小桔', '国网']
  private topicChips: string[] = ['续航实测', '充电省钱', '电车露营', '冬季续航', '桩企评测']
  private facilityChips: string[] = ['休息室', '卫生间', '便利店', '洗车', 'Wi-Fi', '餐饮']
  private menuList: string[] = ['充电订单', '我的卡券', '余额钱包', '发票管理', '常用充电站', '设置']
  private cardPriceArr: number[] = [19, 29, 69, 199]

  // 充电大厅: 24 条充电站 price 为单次费用(分) num 为距离(米) score 为空闲率
  private mainList: GoodsItem[] = [
    { name: '特来电超充站(望京店)', price: 128, desc: '直流快充桩 24 个', score: 96, num: 480, tag: '超快充', hot: '爆' },
    { name: '星星充电站(朝阳大悦城)', price: 96, desc: '交流慢充桩 18 个', score: 82, num: 320, tag: '会员9折', hot: '热' },
    { name: '小桔充电站(酒仙桥)', price: 156, desc: '直流快充桩 32 个', score: 91, num: 460, tag: '新客立减', hot: '新' },
    { name: '国家电网充电站(朝阳公园)', price: 88, desc: '直流快充桩 16 个', score: 74, num: 210, tag: '谷段优惠', hot: '热' },
    { name: '蔚来超充站(亦庄)', price: 188, desc: '超级快充桩 12 个', score: 98, num: 490, tag: '支持换电', hot: '爆' },
    { name: '特斯拉超充站(国贸)', price: 198, desc: '超级快充桩 20 个', score: 95, num: 350, tag: '车主专享', hot: '爆' },
    { name: '特来电充电站(三里屯)', price: 118, desc: '直流快充桩 28 个', score: 88, num: 260, tag: '24小时', hot: '热' },
    { name: '星星充电站(双井)', price: 102, desc: '交流慢充桩 22 个', score: 76, num: 180, tag: '夜间5折', hot: '热' },
    { name: '小桔充电站(望京西)', price: 136, desc: '直流快充桩 15 个', score: 69, num: 330, tag: '新客立减', hot: '新' },
    { name: '国家电网充电站(东坝)', price: 92, desc: '直流快充桩 12 个', score: 62, num: 420, tag: '谷段优惠', hot: '荐' },
    { name: '蔚来超充站(中关村)', price: 176, desc: '超级快充桩 10 个', score: 93, num: 240, tag: '支持换电', hot: '爆' },
    { name: '特斯拉超充站(五棵松)', price: 186, desc: '超级快充桩 16 个', score: 90, num: 390, tag: '车主专享', hot: '热' },
    { name: '特来电充电站(西二旗)', price: 124, desc: '直流快充桩 36 个', score: 85, num: 310, tag: '24小时', hot: '荐' },
    { name: '星星充电站(回龙观)', price: 98, desc: '交流慢充桩 26 个', score: 78, num: 460, tag: '夜间5折', hot: '热' },
    { name: '小桔充电站(天通苑)', price: 132, desc: '直流快充桩 20 个', score: 71, num: 150, tag: '新客立减', hot: '新' },
    { name: '国家电网充电站(通州)', price: 86, desc: '直流快充桩 18 个', score: 66, num: 440, tag: '谷段优惠', hot: '荐' },
    { name: '蔚来超充站(亦庄东)', price: 168, desc: '超级快充桩 8 个', score: 89, num: 290, tag: '支持换电', hot: '热' },
    { name: '特斯拉超充站(顺义)', price: 192, desc: '超级快充桩 14 个', score: 92, num: 370, tag: '车主专享', hot: '爆' },
    { name: '特来电充电站(大兴)', price: 114, desc: '直流快充桩 30 个', score: 80, num: 410, tag: '24小时', hot: '荐' },
    { name: '星星充电站(方庄)', price: 106, desc: '交流慢充桩 16 个', score: 73, num: 220, tag: '夜间5折', hot: '热' },
    { name: '小桔充电站(十里河)', price: 142, desc: '直流快充桩 25 个', score: 87, num: 270, tag: '新客立减', hot: '新' },
    { name: '国家电网充电站(石景山)', price: 90, desc: '直流快充桩 14 个', score: 64, num: 470, tag: '谷段优惠', hot: '荐' },
    { name: '蔚来超充站(首钢园)', price: 178, desc: '超级快充桩 9 个', score: 94, num: 200, tag: '支持换电', hot: '爆' },
    { name: '特斯拉超充站(丽泽)', price: 194, desc: '超级快充桩 18 个', score: 97, num: 340, tag: '车主专享', hot: '爆' }
  ]

  // 快充专区: 12 条快充桩 num 为功率(kW) tag 为状态
  private fastList: GoodsItem[] = [
    { name: '特来电A01', price: 128, desc: '望京店 · 直流快充', score: 92, num: 480, tag: '空闲', hot: '爆' },
    { name: '星星B02', price: 96, desc: '大悦城 · 直流快充', score: 60, num: 120, tag: '占用', hot: '热' },
    { name: '小桔C03', price: 136, desc: '酒仙桥 · 直流快充', score: 85, num: 250, tag: '空闲', hot: '新' },
    { name: '国网D04', price: 88, desc: '朝阳公园 · 直流快充', score: 45, num: 180, tag: '占用', hot: '热' },
    { name: '蔚来E05', price: 158, desc: '亦庄 · 超级快充', score: 95, num: 480, tag: '空闲', hot: '爆' },
    { name: '特斯拉F06', price: 186, desc: '国贸 · 超级快充', score: 90, num: 250, tag: '空闲', hot: '爆' },
    { name: '特来电G07', price: 132, desc: '三里屯 · 直流快充', score: 50, num: 60, tag: '占用', hot: '荐' },
    { name: '星星H08', price: 102, desc: '双井 · 直流快充', score: 78, num: 120, tag: '空闲', hot: '热' },
    { name: '小桔I09', price: 144, desc: '望京西 · 直流快充', score: 66, num: 250, tag: '占用', hot: '新' },
    { name: '国网J10', price: 92, desc: '东坝 · 直流快充', score: 88, num: 60, tag: '空闲', hot: '荐' },
    { name: '蔚来K11', price: 162, desc: '中关村 · 超级快充', score: 82, num: 480, tag: '空闲', hot: '爆' },
    { name: '特斯拉L12', price: 176, desc: '五棵松 · 超级快充', score: 40, num: 250, tag: '占用', hot: '热' }
  ]

  // 慢充社区: 10 条社区慢充桩 num 为距离(米)
  private slowList: GoodsItem[] = [
    { name: '望京西园慢充站', price: 76, desc: '交流慢充桩 12 个', score: 88, num: 320, tag: '谷段0.31元', hot: '荐' },
    { name: '大悦城地下车库', price: 82, desc: '交流慢充桩 8 个', score: 92, num: 450, tag: '夜间5折', hot: '热' },
    { name: '酒仙桥小区慢充站', price: 70, desc: '交流慢充桩 15 个', score: 75, num: 180, tag: '谷段0.31元', hot: '荐' },
    { name: '三里屯公寓慢充站', price: 88, desc: '交流慢充桩 6 个', score: 68, num: 260, tag: '包月优惠', hot: '新' },
    { name: '双井家园慢充站', price: 79, desc: '交流慢充桩 10 个', score: 81, num: 390, tag: '夜间5折', hot: '热' },
    { name: '回龙观东大街慢充站', price: 74, desc: '交流慢充桩 20 个', score: 86, num: 470, tag: '谷段0.31元', hot: '荐' },
    { name: '天通苑北区慢充站', price: 72, desc: '交流慢充桩 18 个', score: 79, num: 140, tag: '包月优惠', hot: '荐' },
    { name: '通州万达公寓慢充站', price: 84, desc: '交流慢充桩 9 个', score: 90, num: 330, tag: '夜间5折', hot: '热' },
    { name: '方庄芳古园慢充站', price: 77, desc: '交流慢充桩 11 个', score: 72, num: 220, tag: '谷段0.31元', hot: '荐' },
    { name: '大兴星光社区慢充站', price: 81, desc: '交流慢充桩 14 个', score: 84, num: 410, tag: '包月优惠', hot: '新' }
  ]

  // 充电卡券: 10 条卡券 num 为折扣力度 score 为热度
  private cardList: GoodsItem[] = [
    { name: '新客充电立减券', price: 500, desc: '有效期至 2026-09-30', score: 60, num: 500, tag: '5折', hot: '爆' },
    { name: '谷段充电8折券', price: 800, desc: '有效期至 2026-10-15', score: 80, num: 200, tag: '8折', hot: '热' },
    { name: '快充满减券', price: 1000, desc: '有效期至 2026-08-31', score: 90, num: 350, tag: '满30减10', hot: '热' },
    { name: '慢充包月9折券', price: 900, desc: '有效期至 2026-12-31', score: 75, num: 100, tag: '9折', hot: '荐' },
    { name: '周末充电7折券', price: 700, desc: '有效期至 2026-09-20', score: 70, num: 300, tag: '7折', hot: '爆' },
    { name: '会员专享85折券', price: 850, desc: '有效期至 2026-11-11', score: 95, num: 150, tag: '8.5折', hot: '热' },
    { name: '夜间充电6折券', price: 600, desc: '有效期至 2026-10-08', score: 65, num: 400, tag: '6折', hot: '爆' },
    { name: '邀友得50元券', price: 5000, desc: '有效期至 2026-12-31', score: 100, num: 450, tag: '50元', hot: '新' },
    { name: '换电体验券', price: 300, desc: '有效期至 2026-09-05', score: 55, num: 80, tag: '免费体验', hot: '新' },
    { name: '节假日5折券', price: 500, desc: '有效期至 2026-10-01', score: 85, num: 500, tag: '5折', hot: '爆' }
  ]

  // 里程社区: 10 条帖子 num 为点赞数 price 为评论数
  private postList: GoodsItem[] = [
    { name: '电动老王', price: 66, desc: '周末满电出发跑了趟郊区,来回368公里,剩余电量还有18%,续航实测靠谱。', score: 88, num: 420, tag: '2小时前', hot: '续航实测' },
    { name: '充电小达人', price: 42, desc: '谷段充电真香,昨晚23点去充电,一度电才三毛一,充满花了不到20块。', score: 92, num: 380, tag: '5小时前', hot: '充电省钱' },
    { name: '北漂电车君', price: 88, desc: '冬季续航大跳水,零下五度开暖风续航直接打六折,冬天一定要规划好补能。', score: 76, num: 460, tag: '昨天 18:20', hot: '冬季续航' },
    { name: '高速老兵', price: 35, desc: '京藏高速服务区新上了480kW超充桩,一杯咖啡的时间电量从20%到90%。', score: 84, num: 340, tag: '昨天 09:12', hot: '续航实测' },
    { name: '露营爱好者', price: 51, desc: '开着电车去露营,外放电煮火锅真的太爽了,一晚上用电不到5度。', score: 90, num: 410, tag: '2天前', hot: '电车露营' },
    { name: '桩企观察员', price: 27, desc: '实测三家桩企的充电速度,峰值功率和宣传还是有差距的,附详细数据表。', score: 80, num: 300, tag: '2天前', hot: '桩企评测' },
    { name: '省钱喵喵', price: 19, desc: '办理了慢充包月卡,一个月下来电费省了小两百,推荐给小区里有桩的朋友。', score: 86, num: 260, tag: '3天前', hot: '充电省钱' },
    { name: '东北电车哥', price: 44, desc: '零下二十度充电体验:预热电池后充电速度明显提升,分享我的操作流程。', score: 78, num: 280, tag: '3天前', hot: '冬季续航' },
    { name: '草根评测君', price: 73, desc: '五千字长文对比五家充电App的价格和体验,结论有点意外,建议收藏。', score: 94, num: 480, tag: '4天前', hot: '桩企评测' },
    { name: '山野车友', price: 31, desc: '自驾进山全程靠超充站补能,路线规划和站点分享都在正文里了。', score: 82, num: 240, tag: '5天前', hot: '电车露营' }
  ]

  // 消息: 12 条消息 hot 为未读标记
  private msgList: GoodsItem[] = [
    { name: '充电完成通知', price: 0, desc: '您在特来电超充站(望京店)的充电订单已完成,本次充电28.6度。', score: 100, num: 100, tag: '刚刚', hot: '未' },
    { name: '卡券到期提醒', price: 0, desc: '您有一张谷段充电8折券将于3天后到期,记得及时使用。', score: 90, num: 96, tag: '10分钟前', hot: '未' },
    { name: '桩友圈评论', price: 0, desc: '电动老王评论了你的帖子:数据很详细,收藏了!', score: 85, num: 92, tag: '30分钟前', hot: '未' },
    { name: '系统公告', price: 0, desc: '8月谷段充电活动上线,23:00-7:00充电全场8折。', score: 80, num: 88, tag: '1小时前', hot: '未' },
    { name: '预约成功通知', price: 0, desc: '您已成功预约小桔充电站(酒仙桥) 明早7:00 的充电时段。', score: 75, num: 80, tag: '2小时前', hot: '已' },
    { name: '余额不足提醒', price: 0, desc: '您的充电余额不足10元,为不影响使用请及时充值。', score: 70, num: 76, tag: '3小时前', hot: '已' },
    { name: '新站上线通知', price: 0, desc: '您附近新增蔚来超充站(首钢园),超级快充桩9个。', score: 68, num: 72, tag: '5小时前', hot: '已' },
    { name: '点赞通知', price: 0, desc: '充电小达人赞了你的帖子《谷段充电真香》。', score: 66, num: 68, tag: '昨天 20:11', hot: '已' },
    { name: '发票开具成功', price: 0, desc: '您申请的充电发票已开具成功,可前往邮箱查收。', score: 60, num: 64, tag: '昨天 14:30', hot: '已' },
    { name: '会员等级变动', price: 0, desc: '恭喜您升级为铂金会员,快充服务费享88折。', score: 58, num: 60, tag: '2天前', hot: '已' },
    { name: '安全提醒', price: 0, desc: '夏季高温充电请注意:避免暴晒后立即快充。', score: 55, num: 56, tag: '2天前', hot: '已' },
    { name: '活动邀请', price: 0, desc: '邀您参加周末充电驿站车主线下聚会,报名从速。', score: 52, num: 52, tag: '3天前', hot: '已' }
  ]

  selName(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.name
    }
    return ''
  }

  selPrice(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.price
    }
    return 0
  }

  selScore(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.score
    }
    return 0
  }

  selTag(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.tag
    }
    return ''
  }

  maxNumOf(list: GoodsItem[]): number {
    let m: number = 1
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }

  brandColor(item: GoodsItem): string {
    if (item.name.indexOf('特来电') >= 0) {
      return '#1565C0'
    }
    if (item.name.indexOf('星星') >= 0) {
      return '#00C853'
    }
    if (item.name.indexOf('小桔') >= 0) {
      return '#FF5252'
    }
    if (item.name.indexOf('国') >= 0) {
      return '#0D47A1'
    }
    if (item.name.indexOf('蔚来') >= 0) {
      return '#00BFA5'
    }
    if (item.name.indexOf('特斯拉') >= 0) {
      return '#7B1FA2'
    }
    return '#FF9800'
  }

  headChar(item: GoodsItem): string {
    return item.name.substring(0, 1)
  }

  bookFee(): number {
    return Math.floor((this.selPrice() / 100) * this.stepNum)
  }

  // 静态头部: 定位行 + 标题搜索行 + 电价时段chips
  @Builder
  headerBuilder() {
    Column() {
      Row() {
        Row({ space: 4 }) {
          Text('北京·朝阳区')
            .fontSize(13)
            .fontColor('#FFFFFF')
          Text('▾')
            .fontSize(12)
            .fontColor('#FFFFFF')
        }

        Column()
          .layoutWeight(1)

        Stack({ alignContent: Alignment.TopEnd }) {
          Text('🔔')
            .fontSize(20)
            .fontColor('#FFFFFF')
          Text('3')
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF5252')
            .borderRadius(9)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10 })

      Row() {
        Text('充电驿站')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Column()
          .layoutWeight(1)

        Row({ space: 6 }) {
          Text('🔍')
            .fontSize(13)
          Text('搜充电站/桩企')
            .fontSize(12)
            .fontColor('#999999')
          Text('⌗')
            .fontSize(14)
            .fontColor('#666666')
        }
        .height(30)
        .padding({ left: 12, right: 12 })
        .backgroundColor('#F5F5F5')
        .borderRadius(15)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10, bottom: 10 })

      Row({ space: 8 }) {
        ForEach(this.priceChips, (chip: string, index: number) => {
          Text(chip)
            .fontSize(11)
            .padding({ left: 12, right: 12, top: 4, bottom: 4 })
            .borderRadius(12)
            .fontColor(this.chipA === index ? '#1565C0' : '#E3F2FD')
            .backgroundColor(this.chipA === index ? '#FFFFFF' : 'rgba(255,255,255,0.15)')
            .onClick(() => {
              this.chipA = index
            })
        }, (chip: string) => chip)
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#1565C0')
  }

  @Builder
  bottomBuilder() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column({ space: 2 }) {
          Text(tab)
            .fontSize(10)
            .fontColor(this.currentTab === index ? '#1565C0' : '#999999')
          Column()
            .width(18)
            .height(3)
            .borderRadius(2)
            .backgroundColor(this.currentTab === index ? '#1565C0' : '#FFFFFF00')
        }
        .layoutWeight(1)
        .onClick(() => {
          this.currentTab = index
        })
      }, (tab: string) => tab)
    }
    .width('100%')
    .height(56)
    .backgroundColor('#FFFFFF')
  }

  // Tab1 充电大厅
  @Builder
  tab0() {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 4 }) {
          Text('28.6')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1565C0')
          Text('今日充电量(度)')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(34)
          .backgroundColor('#E3F2FD')

        Column({ space: 4 }) {
          Text('¥46')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00C853')
          Text('节省费用')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(34)
          .backgroundColor('#E3F2FD')

        Column({ space: 4 }) {
          Text('12.4kg')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00C853')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('碳减排')
            .fontSize(10)
            .fontColor('#999999')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Row({ space: 6 }) {
        Text('+')
          .fontSize(13)
          .fontColor('#FFFFFF')
        Text('发布充电桩')
          .fontSize(13)
          .fontColor('#FFFFFF')
      }
      .padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('#00C853')
      .borderRadius(18)
      .onClick(() => {
        this.showAddModal = true
      })

      Text('附近充电站')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Column({ space: 10 }) {
        ForEach(this.mainList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column() {
              Text(this.headChar(item))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(44)
            .height(44)
            .borderRadius(22)
            .backgroundColor(this.brandColor(item))

            Column({ space: 5 }) {
              Row({ space: 6 }) {
                Text(item.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                  .maxLines(1)
                Text(item.hot)
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF5252')
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
              }

              Text(item.desc)
                .fontSize(11)
                .fontColor('#999999')
                .maxLines(1)

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .borderRadius(3)
                  .backgroundColor('#E3F2FD')
                Column()
                  .width(`${item.score}%`)
                  .height(6)
                  .borderRadius(3)
                  .backgroundColor('#00C853')
              }
              .width('100%')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column({ space: 5 }) {
              Text(`¥${(item.price / 100).toFixed(2)}`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1565C0')
              Text(item.tag)
                .fontSize(10)
                .fontColor('#1565C0')
                .backgroundColor('#E3F2FD')
                .borderRadius(8)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(`${(item.num / 100).toFixed(1)}km`)
                .fontSize(10)
                .fontColor('#999999')
            }
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

  // Tab2 快充专区
  @Builder
  tab1() {
    Column({ space: 12 }) {
      Text('本月充电功率分布 (kW)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Row({ space: 6 }) {
        ForEach(this.fastList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(`${item.num}`)
              .fontSize(9)
              .fontColor('#1565C0')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.fastList) * 120)
              .backgroundColor(this.brandColor(item))
              .borderRadius(4)
            Text(item.name)
              .fontSize(8)
              .fontColor('#999999')
              .maxLines(1)
          }
          .width(20)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Text('空闲快充桩')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.fastList, (item: GoodsItem) => {
          Column({ space: 8 }) {
            Row({ space: 6 }) {
              Column()
                .width(8)
                .height(8)
                .borderRadius(4)
                .backgroundColor(item.tag === '空闲' ? '#00C853' : '#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.tag)
                .fontSize(10)
                .fontColor(item.tag === '空闲' ? '#00C853' : '#FF5252')
            }

            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(1)

            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)

            Text(`${item.num}kW · 空闲${item.score}%`)
              .fontSize(11)
              .fontColor('#1565C0')

            Text(`¥${(item.price / 100).toFixed(2)}/度`)
              .fontSize(11)
              .fontColor('#FF5252')
          }
          .width('48%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

  // Tab3 慢充社区
  @Builder
  tab2() {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 4 }) {
          Text('0.31')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('谷段电价(元/度)')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(36)
          .backgroundColor('rgba(255,255,255,0.3)')

        Column({ space: 4 }) {
          Text('86')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('空闲慢充桩(个)')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)

        Column()
          .width(1)
          .height(36)
          .backgroundColor('rgba(255,255,255,0.3)')

        Column({ space: 4 }) {
          Text('23:00')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('谷段开始')
            .fontSize(10)
            .fontColor('#E3F2FD')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0D47A1')
      .borderRadius(12)

      Text('社区慢充桩距离分布')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Row({ space: 6 }) {
        ForEach(this.slowList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(`${(item.num / 100).toFixed(1)}`)
              .fontSize(9)
              .fontColor('#0D47A1')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.slowList) * 120)
              .backgroundColor('#0D47A1')
              .borderRadius(4)
            Text(item.name)
              .fontSize(7)
              .fontColor('#999999')
              .maxLines(1)
          }
          .width(22)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Text('小区充电卡')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.slowList, (item: GoodsItem) => {
            Column({ space: 8 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
              Text(item.desc)
                .fontSize(11)
                .fontColor('#999999')
                .maxLines(1)
              Text(`${(item.num / 100).toFixed(1)}km · 空闲${item.score}%`)
                .fontSize(11)
                .fontColor('#0D47A1')
              Text('去充电')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#1565C0')
                .borderRadius(12)
                .padding({ left: 14, right: 14, top: 5, bottom: 5 })
            }
            .width(130)
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

  // Tab4 充电卡券
  @Builder
  tab3() {
    Column({ space: 12 }) {
      Text('卡券折扣力度一览')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .width('100%')

      Row({ space: 6 }) {
        ForEach(this.cardList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(item.tag)
              .fontSize(9)
              .fontColor('#FF5252')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.cardList) * 100)
              .backgroundColor('#FF5252')
              .borderRadius(4)
          }
          .width(22)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      Column({ space: 10 }) {
        ForEach(this.cardList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column()
              .width(4)
              .height(56)
              .borderRadius(2)
              .backgroundColor(this.brandColor(item))

            Column({ space: 5 }) {
              Text(item.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
              Text(item.desc)
                .fontSize(11)
                .fontColor('#999999')
              Text(`热度 ${item.score}%`)
                .fontSize(10)
                .fontColor('#1565C0')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column({ space: 6 }) {
              Text(item.tag)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text('领取')
                .fontSize(12)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF5252')
                .borderRadius(12)
                .padding({ left: 16, right: 16, top: 5, bottom: 5 })
                .onClick(() => {
                  this.selectedItem = item
                  this.showCardModal = true
                })
            }
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

  // Tab5 里程社区
  @Builder
  tab4() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        ForEach(this.topicChips, (chip: string, index: number) => {
          Text(`# ${chip}`)
            .fontSize(11)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(12)
            .fontColor(this.chipB === index ? '#FFFFFF' : '#1565C0')
            .backgroundColor(this.chipB === index ? '#1565C0' : '#E3F2FD')
            .onClick(() => {
              this.chipB = index
            })
        }, (chip: string) => chip)
      }
      .width('100%')

      Column({ space: 10 }) {
        ForEach(this.postList, (item: GoodsItem) => {
          Row({ space: 10 }) {
            Column() {
              Text(this.headChar(item))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(40)
            .height(40)
            .borderRadius(20)
            .backgroundColor(this.brandColor(item))

            Column({ space: 6 }) {
              Row({ space: 8 }) {
                Text(item.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                Text(item.tag)
                  .fontSize(10)
                  .fontColor('#BBBBBB')
              }

              Text(item.desc)
                .fontSize(12)
                .fontColor('#666666')
                .maxLines(2)

              Row({ space: 18 }) {
                Row({ space: 4 }) {
                  Text('👍')
                    .fontSize(12)
                  Text(`${item.num}`)
                    .fontSize(11)
                    .fontColor('#1565C0')
                    .scale({ x: 1.05, y: 1.05 })
                    .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                }

                Row({ space: 4 }) {
                  Text('💬')
                    .fontSize(12)
                  Text(`${item.price}`)
                    .fontSize(11)
                    .fontColor('#999999')
                }

                Text(`#${item.hot}`)
                  .fontSize(10)
                  .fontColor('#00C853')
              }
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }

  // Tab6 消息
  @Builder
  tab5() {
    Column({ space: 10 }) {
      ForEach(this.msgList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(this.headChar(item))
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(44)
          .height(44)
          .borderRadius(22)
          .backgroundColor(this.brandColor(item))

          Column({ space: 5 }) {
            Text(item.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 8 }) {
            Text(item.tag)
              .fontSize(10)
              .fontColor('#BBBBBB')
            if (item.hot === '未') {
              Column()
                .width(10)
                .height(10)
                .borderRadius(5)
                .backgroundColor('#FF5252')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
          }
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .onClick(() => {
          this.selectedItem = item
          this.showDeleteModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
  }

  // Tab7 我的
  @Builder
  tab6() {
    Column({ space: 12 }) {
      Column({ space: 14 }) {
        Row({ space: 12 }) {
          Column() {
            Text('D')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1565C0')
          }
          .width(56)
          .height(56)
          .borderRadius(28)
          .backgroundColor('#FFFFFF')

          Column({ space: 6 }) {
            Text('充电达人David')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('铂金会员')
              .fontSize(10)
              .fontColor('#0D47A1')
              .backgroundColor('#FFD54F')
              .borderRadius(8)
              .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Text('›')
            .fontSize(20)
            .fontColor('#FFFFFF')
        }
        .width('100%')

        Row() {
          Column({ space: 4 }) {
            Text('186')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('充电次数')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)

          Column()
            .width(1)
            .height(30)
            .backgroundColor('rgba(255,255,255,0.3)')

          Column({ space: 4 }) {
            Text('2468')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('累计度数')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)

          Column()
            .width(1)
            .height(30)
            .backgroundColor('rgba(255,255,255,0.3)')

          Column({ space: 4 }) {
            Text('312kg')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('碳减排')
              .fontSize(10)
              .fontColor('#E3F2FD')
          }
          .layoutWeight(1)
        }
        .width('100%')
      }
      .width('100%')
      .padding(18)
      .borderRadius(16)
      .linearGradient({ angle: 135, colors: [['#0D47A1', 0], ['#1565C0', 1]] })

      Column() {
        ForEach(this.menuList, (menu: string, index: number) => {
          Row({ space: 10 }) {
            Text('⚡')
              .fontSize(16)
            Text(menu)
              .fontSize(14)
              .fontColor('#333333')
            Column()
              .layoutWeight(1)
            Text('›')
              .fontSize(18)
              .fontColor('#CCCCCC')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14, left: 14, right: 14 })
          .onClick(() => {
            if (index === 0) {
              this.showDeleteModal = true
            } else if (index === 1) {
              this.showCardModal = true
            } else if (index === 2) {
              this.showCardModal = true
            } else if (index === 5) {
              this.showAddModal = true
            }
          })
        }, (menu: string) => menu)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
    }
    .width('100%')
    .padding(12)
  }

  // 弹框1: 发布充电桩(底部弹出)
  @Builder
  addModal() {
    Column() {
      Column()
        .layoutWeight(1)

      Column({ space: 14 }) {
        Row() {
          Text('发布充电桩')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Column()
            .layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#999999')
            .onClick(() => {
              this.showAddModal = false
            })
        }
        .width('100%')

        Text('选择桩类型')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.kindChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.kindIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.kindIdx === index ? '#1565C0' : '#F5F5F5')
              .onClick(() => {
                this.kindIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择充电功率')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.powerChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.powerIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.powerIdx === index ? '#00C853' : '#F5F5F5')
              .onClick(() => {
                this.powerIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('站点名称')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        TextInput({ placeholder: '请输入站点名称' })
          .height(42)
          .fontSize(13)
          .backgroundColor('#F5F5F5')
          .borderRadius(8)

        Text('确认发布')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
          .width('100%')
          .padding(12)
          .backgroundColor('#00C853')
          .borderRadius(22)
          .onClick(() => {
            this.showAddModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showAddModal = false
    })
  }

  // 弹框2: 预约充电(底部弹出)
  @Builder
  bookModal() {
    Column() {
      Column()
        .layoutWeight(1)

      Column({ space: 14 }) {
        Row() {
          Text('预约充电')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Column()
            .layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#999999')
            .onClick(() => {
              this.showBookModal = false
            })
        }
        .width('100%')

        Text(this.selName() === '' ? '特来电超充站(望京店)' : this.selName())
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1565C0')

        Text('充电时段')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.periodChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.periodIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.periodIdx === index ? '#1565C0' : '#F5F5F5')
              .onClick(() => {
                this.periodIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('充电时长(1-12小时)')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 20 }) {
          Text('-')
            .fontSize(16)
            .fontColor('#1565C0')
            .width(30)
            .height(30)
            .borderRadius(15)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E3F2FD')
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum -= 1
              }
            })

          Text(`${this.stepNum} 小时`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Text('+')
            .fontSize(16)
            .fontColor('#1565C0')
            .width(30)
            .height(30)
            .borderRadius(15)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E3F2FD')
            .onClick(() => {
              if (this.stepNum < 12) {
                this.stepNum += 1
              }
            })
        }

        Row({ space: 8 }) {
          Text('预估费用')
            .fontSize(13)
            .fontColor('#666666')
          Text('¥' + this.bookFee())
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF5252')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')

        Text('确认预约')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
          .width('100%')
          .padding(12)
          .backgroundColor('#1565C0')
          .borderRadius(22)
          .onClick(() => {
            this.showBookModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showBookModal = false
    })
  }

  // 弹框3: 办理充电卡(底部弹出)
  @Builder
  cardModal() {
    Column() {
      Column()
        .layoutWeight(1)

      Column({ space: 14 }) {
        Row() {
          Text('办理充电卡')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Column()
            .layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#999999')
            .onClick(() => {
              this.showCardModal = false
            })
        }
        .width('100%')

        Column({ space: 4 }) {
          Text('办理卡券')
            .fontSize(11)
            .fontColor('#999999')
          Text(this.selName() === '' ? '新客专享充电卡' : this.selName())
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1565C0')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)

        Text('选择卡种')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.cardTypeChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.cardTypeIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.cardTypeIdx === index ? '#1565C0' : '#F5F5F5')
              .onClick(() => {
                this.cardTypeIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择桩企')
          .fontSize(13)
          .fontColor('#666666')
          .width('100%')

        Row({ space: 8 }) {
          ForEach(this.vendorChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .fontColor(this.vendorIdx === index ? '#FFFFFF' : '#666666')
              .backgroundColor(this.vendorIdx === index ? '#00C853' : '#F5F5F5')
              .onClick(() => {
                this.vendorIdx = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('办理价格')
            .fontSize(13)
            .fontColor('#666666')
          Text('¥' + Math.floor(this.cardPriceArr[this.cardTypeIdx]))
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF5252')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')

        Text('立即办理')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
          .width('100%')
          .padding(12)
          .backgroundColor('#FF5252')
          .borderRadius(22)
          .onClick(() => {
            this.showCardModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showCardModal = false
    })
  }

  // 弹框4: 删除确认(居中警示小卡)
  @Builder
  deleteModal() {
    Column() {
      Column({ space: 14 }) {
        Column() {
          Text('!')
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        .width(56)
        .height(56)
        .borderRadius(28)
        .backgroundColor('#FF5252')

        Text('确认删除该充电记录?')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')

        Text('删除后无法恢复,请谨慎操作')
          .fontSize(11)
          .fontColor('#999999')

        Row({ space: 12 }) {
          Text('取消')
            .fontSize(14)
            .fontColor('#666666')
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .padding(10)
            .borderRadius(20)
            .backgroundColor('#F5F5F5')
            .onClick(() => {
              this.showDeleteModal = false
            })

          Text('删除')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .padding(10)
            .borderRadius(20)
            .backgroundColor('#FF5252')
            .onClick(() => {
              this.showDeleteModal = false
            })
        }
        .width('100%')
      }
      .width('72%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .onClick(() => {
      this.showDeleteModal = false
    })
  }

  // 弹框5: 详情(右侧滑出全高面板)
  @Builder
  detailModal() {
    Column() {
      Column() {
        Column({ space: 6 }) {
          Text('⚡')
            .fontSize(44)
          Text(this.selTag() === '' ? '超充站实景' : this.selTag())
            .fontSize(12)
            .fontColor('#FFFFFF')
        }
        .width('100%')
        .height(170)
        .justifyContent(FlexAlign.Center)
        .linearGradient({ angle: 135, colors: [['#0D47A1', 0], ['#00C853', 1]] })

        Column({ space: 12 }) {
          Text(this.selName() === '' ? '特来电超充站(望京店)' : this.selName())
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .maxLines(2)

          Row({ space: 6 }) {
            Text('★')
              .fontSize(16)
              .fontColor('#FF9800')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text(`${this.selScore()}`)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF9800')
            Text('超2000人充电')
              .fontSize(11)
              .fontColor('#999999')
          }

          Row({ space: 12 }) {
            Column({ space: 2 }) {
              Text('电价')
                .fontSize(10)
                .fontColor('#999999')
              Text(`¥${(this.selPrice() / 100).toFixed(2)}/度`)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1565C0')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column({ space: 2 }) {
              Text('空闲率')
                .fontSize(10)
                .fontColor('#999999')
              Text(`${this.selScore()}%`)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#00C853')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')

          Column({ space: 6 }) {
            Text('空闲进度')
              .fontSize(11)
              .fontColor('#666666')
            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width('100%')
                .height(8)
                .borderRadius(4)
                .backgroundColor('#E3F2FD')
              Column()
                .width(`${this.selScore()}%`)
                .height(8)
                .borderRadius(4)
                .backgroundColor('#00C853')
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          Text('服务设施')
            .fontSize(11)
            .fontColor('#666666')

          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.facilityChips, (chip: string) => {
              Text(chip)
                .fontSize(11)
                .fontColor('#1565C0')
                .backgroundColor('#E3F2FD')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ bottom: 6 })
            }, (chip: string) => chip)
          }
          .width('100%')

          Column()
            .layoutWeight(1)

          Text('预约充电')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .textAlign(TextAlign.Center)
            .width('100%')
            .padding(12)
            .backgroundColor('#1565C0')
            .borderRadius(22)
            .onClick(() => {
              this.showDetailModal = false
              this.showBookModal = true
            })
        }
        .width('100%')
        .padding(14)
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }
      .width('80%')
      .height('100%')
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .alignItems(HorizontalAlign.End)
    .onClick(() => {
      this.showDetailModal = false
    })
  }

  build() {
    Stack() {
      Column() {
        this.headerBuilder()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tab0()
            } else if (this.currentTab === 1) {
              this.tab1()
            } else if (this.currentTab === 2) {
              this.tab2()
            } else if (this.currentTab === 3) {
              this.tab3()
            } else if (this.currentTab === 4) {
              this.tab4()
            } else if (this.currentTab === 5) {
              this.tab5()
            } else {
              this.tab6()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .width('100%')

        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F2F4F7')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showBookModal) {
        this.bookModal()
      }
      if (this.showCardModal) {
        this.cardModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }
}


总结

在这里插入图片描述

本文完整拆解了一套基于HarmonyOS API 24声明式UI范式构建的新能源充电驿站聚合大厅应用。从数据模型定义到状态管理策略,从七大功能Tab的逐段构建到五种弹层交互的详细实现,涵盖了ArkTS声明式UI开发的全部核心环节。应用通过统一的GoodsItem接口和@State状态变量体系,实现了高度复用的列表渲染逻辑和弹层数据绑定机制。@Builder装饰器将复杂页面拆分为可维护的构建器函数单元,每个构建器聚焦单一职责,既保证了代码可读性,也便于后续功能扩展。

从UI设计层面来看,应用充分利用了ArkUI框架提供的丰富布局容器和样式属性。FlexFlexWrap.Wrap实现了双列网格自动换行布局,StackAlignment枚举实现了角标和进度条等层叠效果,linearGradient实现了个人中心和详情弹层的渐变背景。动画方面,通过scale配合animationiterations: -1参数实现了多处脉冲效果,为关键数据指标(碳减排、谷段电价、折扣标签、点赞数、未读红点等)提供了持续性的视觉引导。柱状图通过ForEach遍历数据数组并动态计算柱形高度比例,是纯声明式UI实现数据可视化的典型范例。

从交互架构层面来看,build()方法中的Stack层叠容器配合条件渲染if语句,构建了一个弹层管理系统。每个弹层通过独立的@State布尔变量控制显示,点击遮罩区域或关闭按钮时将状态置为false,ArkUI框架自动移除对应弹层的渲染。弹层之间的跳转(如详情弹层点击预约按钮跳转至预约弹层)通过在点击事件中先关闭当前弹层再打开目标弹层实现。事件冒泡控制通过event.stopPropagation()防止内容区域的点击意外触发遮罩关闭逻辑。这种设计模式简洁高效,适用于中等复杂度的移动端弹层交互场景,为类似的多功能聚合型应用提供了可直接参考的架构范本。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐