HarmonyOS的声明式UI范式将状态与视图深度绑定,通过@State装饰器实现了数据驱动的高效渲染机制,让开发者只需关注状态变化,视图便会自动完成差量更新。这是鸿蒙原生开发区别于传统命令式编程的核心设计哲学。

ArkTS语言在TypeScript基础上扩展了装饰器系统与构建器模式,为组件化开发提供了更强大的支撑。@Entry、@Component、@Builder三件套构成了声明式UI的骨架,配合ForEach、if条件渲染,能够构建出层次分明、逻辑清晰的大型应用界面。

动画与状态管理的融合是HarmonyOS ArkUI框架的精髓所在。通过定时器周期性更新@State变量,配合animation属性与scale、rotate等变换函数,无需引入复杂的动画框架即可实现呼吸、旋转、粒子掉落等丰富的视觉效果,这种轻量级的动画方案极大降低了开发门槛。

引言

随着宠物经济的蓬勃发展和智能化生活方式的普及,宠物服务类应用正成为移动端的重要场景。一个完整的宠物生活服务平台需要覆盖领养、用品商城、美容预约、社区活动和个人中心等多个业务域,这对应用的架构分层和模块解耦提出了较高要求。本文将深入剖析一个基于HarmonyOS ArkTS语言构建的宠物潮流生活馆应用,该应用采用得物风格的薄荷绿与珊瑚橙双主题配色方案,通过底部六Tab导航架构将复杂业务拆解为六个独立组件,每个Tab对应一个完整的业务功能模块,实现了界面隔离与状态隔离。

从技术架构层面来看,该应用充分利用了ArkTS的类型系统与声明式UI特性。首先通过interface定义了宠物、商品、美容服务、活动、订单、收藏等九个数据模型接口,确保了数据流动过程中的类型安全。其次采用const常量集中管理配色方案PET_C和各类静态数据数组,实现了设计令牌(Design Token)与业务数据的统一管理。应用还封装了多个纯函数工具方法,如分类过滤、数据查找、颜色映射等,将可复用逻辑从组件中剥离,提升了代码的可维护性。

在交互体验设计上,该应用展现了HarmonyOS动画体系的丰富表现力。首页通过setInterval定时器驱动breath状态变量在1与1.06之间交替变化,配合scale变换和EaseInOut动画曲线实现了猫咪充电式呼吸效果;零食图标借助fall变量的持续累加与rotate旋转属性呈现出粒子掉落的动态感;宠物详情页的爪印动画利用paw变量驱动四个爪子以不同尺寸、透明度和旋转角度呈现错落效果;美容详情页的沙漏通过hourglass变量驱动360度旋转,营造出排班等待的视觉反馈。这些动画方案均遵循"定时器更新状态——状态驱动属性——属性绑定动画"的统一模式,简洁而高效。

弹窗体系是该应用的另一大亮点。整个应用共设计了十二处弹窗交互,覆盖领养登记、宠物详情、用品详情、美容预约、美容详情、活动详情、活动报名、订单详情、删除收藏、个人资料编辑、新人礼包、确认购买等场景。所有弹窗统一采用Stack+Column+modalOverlay三段式结构,通过布尔状态变量控制显隐,配合zIndex层级管理和position绝对定位实现覆盖层效果,点击遮罩层即可关闭弹窗,交互逻辑清晰统一。

数据模型与接口定义:类型安全的基石

应用首先定义了一组interface接口,为整个数据流建立了严格的类型约束。这些接口覆盖了应用的所有业务实体,是组件间数据传递的契约。

interface PetItem {
  name: string;
  breed: string;
  cat: string;
  age: string;
  price: number;
  hot: number;
  tag: string;
  desc: string;
}

interface PetGoods {
  name: string;
  cat: string;
  price: number;
  sales: number;
  stock: number;
  tag: string;
  desc: string;
}

interface PetBeauty {
  name: string;
  style: string;
  price: number;
  score: number;
  time: string;
  desc: string;
}

interface PetEvent {
  name: string;
  city: string;
  date: string;
  people: number;
  mode: string;
  status: string;
  desc: string;
}

PetItem接口定义了宠物实体的完整字段,包含名称、品种、分类、年龄、价格、人气值、标签和描述八个属性,其中price和hot为数值类型,用于后续的热度榜进度条计算和价格展示。PetGoods接口在宠物商品的基础上增加了sales销量和stock库存字段,这两个字段在商品详情弹窗中以三列数据卡片形式展示。PetBeauty接口引入了style美容风格和score好评率字段,score值用于口碑榜排序。PetEvent接口则定义了活动名称、城市、日期、预计人数、活动形式和报名状态等字段,status字段通过纯函数映射为不同颜色标识。

ArkTS的interface定义不同于运行时类型检查,它在编译期就能捕获类型不匹配的错误,配合ForEach的键值生成器,能够确保列表渲染的唯一性和更新效率。这种静态类型约束在大型应用中价值尤为突出。

除了业务实体接口,应用还定义了PetColor配色接口和PetTab导航接口。PetColor接口是一个包含16个字符串字段的结构体,涵盖了背景色、卡片色、主色调、主色深色、薄荷色、文字主色、文字副色、边框色、金色、危险色、绿色等全套设计令牌。通过将配色方案抽象为接口,应用实现了主题色与组件代码的彻底解耦,若需切换主题只需修改PET_C常量即可,无需触碰任何组件代码。

配色方案与静态数据:设计令牌的集中管理

应用采用const声明了一个全局配色对象PET_C,将薄荷绿与珊瑚橙双色主题的所有色值集中管理,这是典型的设计令牌(Design Token)实践。

const PET_C: PetColor = {
  bg: '#F0FBF6',
  card: '#FFFFFF',
  primary: '#FF8C69',
  primaryDark: '#F26B45',
  mint: '#7FD8BE',
  mintDark: '#4FB899',
  textMain: '#2C3E3E',
  textSub: '#7A8F8A',
  textHint: '#A8C3BC',
  border: '#DFF0EA',
  gold: '#F5B942',
  danger: '#E5484D',
  green: '#2FB368',
  white: '#FFFFFF',
  soft: '#FFF3EC'
};

在这里插入图片描述

这套配色方案以珊瑚橙(#FF8C69)作为主色调primary,搭配薄荷绿(#7FD8BE)作为辅助色mint,形成了得物风格的清新活力视觉语言。背景色采用极浅的薄荷绿(#F0FBF6)营造柔和氛围,卡片色使用纯白保证内容层次清晰。文字色彩分为主文字(深灰绿)、副文字(中灰绿)和提示文字(浅灰绿)三级,形成了清晰的视觉层级。danger红色用于删除操作和爆款标签,green绿色用于已完成状态和免运费提示,gold金色则用于人气值和好评率展示,色彩语义高度统一。

静态数据数组是该应用的另一核心资产。PET_ITEMS数组包含16条宠物数据,覆盖布偶猫、柯基、英短蓝猫、金毛、橘猫、柴犬、龙猫、边牧、狸花猫、蜜袋鼯等多个品种,每条数据都包含完整的名称、品种、分类、年龄、价格、人气值、标签和描述信息。PET_GOODS数组收录了10款宠物用品,从冻干猫粮到逗猫棒、从自动饮水机到猫砂豆腐砂,覆盖主粮、玩具、器具、清洁、户外、家居、零食、护理八个品类。PET_BEAUTY数组定义了8种美容服务套餐,PET_EVENTS数组规划了6场萌宠活动,PET_ORDERS数组模拟了8条订单记录,PET_FAVS数组初始化了6条收藏数据。

将大量静态业务数据以const数组形式集中声明,是ArkTS应用中常见的Mock数据管理策略。在实际工程中,这些数据通常来自网络请求或本地数据库,但在原型开发和组件逻辑验证阶段,集中式静态数据能极大提升开发效率,同时保证了组件逻辑的可测试性。

纯函数工具区:可复用逻辑的提取

应用在数据定义之后封装了一组纯函数工具方法,这些函数不依赖任何组件状态,接收输入参数并返回确定性输出,是可复用逻辑的典范。

function petHotWidth(h: number): number {
  let w = h * 2.6;
  if (w > 100) {
    w = 100;
  }
  return w;
}

function petTagColor(tag: string): string {
  if (tag === '爆款') {
    return PET_C.danger;
  }
  if (tag === '热销') {
    return PET_C.primary;
  }
  if (tag === '平价') {
    return PET_C.green;
  }
  if (tag === '领养' || tag === '高性价比') {
    return PET_C.mintDark;
  }
  return PET_C.gold;
}

function petStatusColor(s: string): string {
  if (s === '报名中') {
    return PET_C.primary;
  }
  if (s === '即将开始') {
    return PET_C.gold;
  }
  return PET_C.textHint;
}

function petOrderColor(s: string): string {
  if (s === '已完成') {
    return PET_C.green;
  }
  if (s === '已发货') {
    return '#4FB899';
  }
  if (s === '退款中') {
    return PET_C.danger;
  }
  if (s === '待付款') {
    return PET_C.gold;
  }
  return PET_C.primary;
}

在这里插入图片描述

petHotWidth函数将人气值(0-100范围)映射为进度条宽度百分比,通过乘以2.6系数放大并在超过100时截断,确保高人气宠物的进度条不会溢出容器。petTagColor函数是一个典型的标签-颜色映射器,根据标签文本返回对应的语义色彩:爆款用红色突出紧迫感,热选用主色珊瑚橙,平价用绿色传达实惠感,领养和高性价比用薄荷深色传达公益温度,其余标签默认使用金色。petStatusColor和petOrderColor函数分别处理活动状态和订单状态的颜色映射,已完成对应绿色、退款中对应红色、待付款对应金色,这种状态-颜色映射模式在列表渲染中被频繁调用。

除了颜色映射函数,应用还封装了数据查询函数。petListByCat和petGoodsList函数根据分类参数过滤宠物列表和商品列表,当传入"全部"时返回完整数组,否则使用filter方法按cat字段筛选。petFindPet、petFindGoods、petFindBeauty、petFindEvent、petFindOrder五个查找函数通过名称或编号在对应数组中线性查找,找到则返回该对象,找不到则返回数组首个元素作为兜底默认值。这种线性查找虽然时间复杂度为O(n),但在数据量较小(十几条记录)的场景下性能完全足够,且代码简洁可读。

入口组件与Tab导航:六模块架构的骨架

PetApp作为应用的@Entry入口组件,承载了底部六Tab导航的核心逻辑,通过activeTab状态变量控制当前激活的Tab索引,实现模块间的切换。

@Entry
@Component
struct PetApp {
  @State activeTab: number = 0;

  @Builder
  tabIcon(item: PetTab, idx: number) {
    Column() {
      Text(item.icon)
        .fontSize(20)
        .margin({ top: 6 })
      Text(item.name)
        .fontSize(11)
        .fontColor(this.activeTab === idx ? PET_C.primary : PET_C.textHint)
        .fontWeight(this.activeTab === idx ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 3, bottom: 4 })
    }
    .width('100%')
    .layoutWeight(1)
    .borderRadius(14)
    .backgroundColor(this.activeTab === idx ? PET_C.soft : Color.Transparent)
    .onClick(() => {
      this.activeTab = idx;
    })
  }

  @Builder
  tabContent() {
    if (this.activeTab === 0) {
      PetHomeTab()
    } else if (this.activeTab === 1) {
      PetItemTab()
    } else if (this.activeTab === 2) {
      PetGoodsTab()
    } else if (this.activeTab === 3) {
      PetBeautyTab()
    } else if (this.activeTab === 4) {
      PetEventTab()
    } else {
      PetMineTab()
    }
  }

  build() {
    Column() {
      Stack() {
        this.tabContent()
      }
      .layoutWeight(1)
      .width('100%')

      Row() {
        ForEach(PET_TABS, (item: PetTab, idx: number) => {
          this.tabIcon(item, idx)
        }, (item: PetTab, idx: number) => item.name + idx)
      }
      .width('100%')
      .height(62)
      .backgroundColor(PET_C.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .shadow({ radius: 12, color: '#1A7FD8BE', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(PET_C.bg)
  }
}

在这里插入图片描述

tabIcon是一个@Builder方法,它接收PetTab项和索引参数,构建单个Tab按钮。激活状态下的Tab文字使用珊瑚橙主色加粗显示,背景填充浅橙soft色形成选中高亮效果;非激活Tab则使用提示灰色常规字重,背景透明。点击事件简单地将activeTab赋值为当前索引,ArkUI框架自动处理状态变化引发的视图更新。

tabContent方法通过if-else条件链根据activeTab值渲染对应的Tab组件。这种条件渲染方式在Tab数量较少时清晰直观,每个Tab是一个独立的@Component组件,各自维护自己的@State状态。底部导航栏使用ForEach遍历PET_TABS数组渲染六个Tab按钮,通过item.name + idx组合键确保每个Tab的唯一标识。导航栏整体使用圆角顶部设计配合薄荷绿阴影,视觉上与上方内容区形成柔和过渡。

@Entry装饰器标记的组件是应用的入口页面,每个ArkTS工程只能有一个@Entry组件。@Component装饰的普通组件可以被多次复用,而@Builder方法则用于在组件内部抽取可复用的UI片段,三者的层级关系构成了ArkTS组件化开发的基础架构。

首页模块:呼吸动画与热度榜进度条

PetHomeTab组件是应用的首页,集成了搜索栏、功能入口、呼吸动画横幅、新人礼包粒子掉落、宠物热度榜进度条、待领养萌宠横向滚动和养宠指南入口等多个功能区块。

@Component
struct PetHomeTab {
  @State adoptShow: boolean = false;
  @State couponShow: boolean = false;
  @State breath: number = 1;
  @State fall: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.breath = this.breath > 1 ? 1 : 1.06;
      this.fall = this.fall + 16;
    }, 700);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

在这里插入图片描述

组件声明了四个状态变量:adoptShow和couponShow控制两个弹窗的显隐,breath驱动猫咪呼吸动画的缩放比例,fall驱动零食图标的旋转角度。timer变量持有定时器ID,初始值-1表示未启动。在aboutToAppear生命周期回调中,通过setInterval注册了一个700毫秒间隔的定时器,每次回调将breath在1和1.06之间切换实现呼吸效果,同时fall持续累加16用于驱动零食掉落旋转。aboutToDisappear回调中调用clearInterval清除定时器,防止组件销毁后定时器继续执行导致内存泄漏。

// 猫咪呼吸动画横幅
Row() {
  Column() {
    Text('🐱')
      .fontSize(34)
      .scale({ x: this.breath, y: this.breath })
      .animation({ duration: 700, curve: Curve.EaseInOut })
  }
  .width(64)
  .height(64)
  .justifyContent(FlexAlign.Center)
  .backgroundColor(PET_C.mint)
  .borderRadius(32)

  Column() {
    Text('萌宠在呼吸 · 安心陪伴')
      .fontSize(14)
      .fontColor(PET_C.white)
      .fontWeight(FontWeight.Bold)
    Text('本月已有 328 只毛孩子找到新家')
      .fontSize(11)
      .fontColor('#E8FFF6')
      .margin({ top: 3 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)
  .margin({ left: 12 })

  Text('领养')
    .fontSize(12)
    .fontColor(PET_C.mintDark)
    .textAlign(TextAlign.Center)
    .width(56)
    .height(30)
    .backgroundColor(PET_C.white)
    .borderRadius(15)
    .onClick(() => {
      this.adoptShow = true;
    })
}
.width('100%')
.height(66)
.padding({ left: 14, right: 12 })
.linearGradient({ colors: [['#7FD8BE', 0], ['#4FB899', 1]] })
.borderRadius(18)
.margin({ left: 14, right: 14, top: 12 })

在这里插入图片描述

呼吸动画横幅是首页的视觉焦点。猫咪emoji通过scale变换绑定breath状态变量,当breath从1变为1.06时,猫咪图标放大6%,配合animation属性设置的700毫秒EaseInOut动画曲线,产生平滑的缩放过渡效果,模拟呼吸的起伏节奏。整个横幅使用薄荷绿到薄荷深绿的线性渐变背景,圆角矩形设计,右侧的"领养"按钮点击后弹出领养登记弹窗。零食掉落横幅则使用fall变量驱动🍖图标的rotate旋转,fall % 30取模确保旋转角度在0-30度之间循环,配合Linear动画曲线产生持续旋转的粒子掉落视觉效果。

// 宠物热度榜图表
Column() {
  ForEach([0, 1, 2, 3, 4], (idx: number) => {
    Row() {
      Text(`${idx + 1}`)
        .fontSize(15)
        .fontColor(idx < 3 ? PET_C.primary : PET_C.textHint)
        .fontWeight(FontWeight.Bold)
        .width(28)
      Text(PET_ITEMS[idx].name)
        .fontSize(13)
        .fontColor(PET_C.textMain)
        .width(104)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Stack() {
        Row() {
          Row()
            .width(`${petHotWidth(PET_ITEMS[idx].hot)}%`)
            .height(10)
            .borderRadius(5)
            .linearGradient({ colors: [[PET_C.primary, 0], ['#FFC3AC', 1]] })
        }
        .width('100%')
        .height(10)
        .backgroundColor('#EDF6F1')
        .borderRadius(5)
      }
      .layoutWeight(1)
      Text(`${PET_ITEMS[idx].hot}`)
        .fontSize(12)
        .fontColor(PET_C.primary)
        .fontWeight(FontWeight.Bold)
        .width(32)
        .textAlign(TextAlign.End)
    }
    .width('100%')
    .height(40)
  }, (idx: number) => 'rank' + idx)
}

在这里插入图片描述

热度榜进度条采用了Stack+Row嵌套的经典进度条结构。外层Stack作为进度条容器,宽度通过layoutWeight弹性占据剩余空间。内层第一个Row设置浅灰绿背景作为轨道,第二个Row通过petHotWidth函数计算出的百分比宽度作为进度填充,配合珊瑚橙到浅橙的线性渐变,视觉效果鲜明。前三名排名数字使用珊瑚橙主色加粗,后两名使用提示灰色,形成了清晰的排名视觉层级。宠物名称设置了maxLines为1和Ellipsis省略号截断,防止长名称破坏布局。

0

1

2

3

4

5

应用启动 PetApp

aboutToAppear 生命周期

setInterval 注册定时器 700ms

activeTab 值判断

PetHomeTab 首页

PetItemTab 宠物

PetGoodsTab 用品

PetBeautyTab 美容

PetEventTab 活动

PetMineTab 我的

呼吸动画 breath 1↔1.06

粒子掉落 fall 累加16

热度榜进度条 petHotWidth

弹窗 adoptShow/couponShow

爪印动画 paw 累加10

分类筛选 selCat

详情弹窗 detailShow

销量进度条 Stack

分类筛选 横向滚动

沙漏动画 hourglass 累加6

状态筛选 selStatus

收藏管理 favList

删除确认 delShow

animation EaseInOut 700ms

modalOverlay 遮罩+modal

aboutToDisappear clearInterval

首页弹窗体系:遮罩层与模态对话框

首页集成了两个弹窗:领养登记弹窗和新人礼包弹窗。两者均采用统一的三段式结构:modalOverlay遮罩层 + 具体弹窗内容 + zIndex层级管理。

@Builder
modalOverlay() {
  Column() {
    Column() {
      Text('')
        .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#55000000')
    .onClick(() => {
      this.adoptShow = false;
      this.couponShow = false;
    })
  }
  .width('100%')
  .height('100%')
}

在这里插入图片描述

modalOverlay是一个共享的遮罩层构建器,它渲染一个全屏半透明黑色背景(#55000000,约33%不透明度),点击任意区域即可关闭所有弹窗。这种设计将遮罩逻辑抽取为独立Builder,在多个弹窗间复用,避免了重复代码。在build方法中,弹窗的渲染通过if条件判断实现。

if (this.adoptShow) {
  Column() {
    this.modalOverlay()
    this.adoptModal()
  }
  .width('100%')
  .height('100%')
  .position({ x: 0, y: 0 })
  .zIndex(999)
}

当adoptShow为true时,渲染一个全屏Column容器,内部分两层:底层是modalOverlay遮罩,上层是adoptModal弹窗内容。容器通过position绝对定位锚定到(0,0)坐标,zIndex设为999确保浮于所有内容之上。领养登记弹窗内部包含意向宠物输入框、居住环境三选一选择器(自有住房/租住/合租)、养宠经验输入框和提交按钮,其中居住环境选择器通过ForEach渲染三个选项,第一个默认选中使用珊瑚橙主色背景,其余使用浅绿背景。

新人礼包弹窗couponModal则采用居中卡片设计,顶部大号礼物emoji,中间展示"满300减50"优惠券信息,底部"去逛逛"按钮使用薄荷深色背景。弹窗卡片宽度设为84%,配合borderRadius圆角和padding内边距,视觉上精致圆润。整个弹窗体系的关闭逻辑统一:点击遮罩层关闭、点击关闭按钮关闭、点击确认按钮关闭,三种关闭路径覆盖了用户的各类操作意图。

在ArkTS中实现弹窗并不依赖系统Dialog API,而是通过条件渲染+绝对定位+zIndex的组合方案自行构建。这种方式的灵活性远高于系统弹窗,可以完全自定义弹窗的布局、动画和交互逻辑,但需要开发者自行管理弹窗状态和层级关系。

宠物Tab模块:爪印动画与详情弹窗

PetItemTab组件负责宠物列表的展示与筛选,包含品种信息卡、分类筛选器、宠物列表和两个弹窗(详情弹窗+领养确认弹窗),并集成了爪印动画效果。

@Component
struct PetItemTab {
  @State selCat: string = '全部';
  @State selPet: string = '';
  @State detailShow: boolean = false;
  @State adoptShow: boolean = false;
  @State paw: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.paw = this.paw + 10;
    }, 300);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

组件维护了selCat分类筛选状态、selPet当前选中宠物名称、detailShow和adoptShow两个弹窗状态以及paw爪印动画驱动变量。定时器以300毫秒间隔递增paw值,比首页的700毫秒更快的频率使爪印动画更为灵动。

// 分类筛选
Row() {
  ForEach(['全部', '猫咪', '狗狗', '异宠'], (c: string) => {
    Text(c)
      .fontSize(12)
      .fontColor(this.selCat === c ? PET_C.white : PET_C.textMain)
      .textAlign(TextAlign.Center)
      .layoutWeight(1)
      .height(34)
      .backgroundColor(this.selCat === c ? PET_C.primary : PET_C.card)
      .borderRadius(17)
      .margin({ right: c === '异宠' ? 0 : 8 })
      .onClick(() => {
        this.selCat = c;
      })
  }, (c: string) => c)
}

分类筛选器通过ForEach渲染四个分类按钮,选中状态使用珊瑚橙背景白字,未选中使用白底深字。点击按钮将selCat赋值为对应分类名,ArkUI框架自动触发petListByCat函数重新计算过滤后的列表数据并更新渲染。宠物列表的每张卡片采用左图标右内容的横向布局:左侧76x76圆角方块展示宠物emoji和品种名,右侧展示名称、标签、年龄描述和价格人气信息。

详情弹窗detailModal是宠物信息的完整展示面板,顶部展示宠物emoji、名称和品种年龄,中部展示描述文字,下方三列数据卡片分别展示领养价、人气值和特点标签。弹窗中嵌入了爪印动画。

// 小爪印动画
Row() {
  ForEach([0, 1, 2, 3], (i: number) => {
    Text('🐾')
      .fontSize(14 + i * 3)
      .opacity(0.95 - i * 0.2)
      .rotate({ angle: (i % 2 === 0 ? 1 : -1) * (this.paw % 30) })
      .animation({ duration: 500, curve: Curve.EaseOut })
      .margin({ right: 10 })
  }, (i: number) => 'p' + i)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 14 })

爪印动画通过ForEach渲染四个🐾emoji,每个爪子的字号从14递增到23(14 + i * 3),透明度从0.95递减到0.35(0.95 - i * 0.2),旋转角度根据索引奇偶性取正负方向,整体呈现出由大到小、由清晰到模糊、左右摆动的渐变序列效果。paw变量每300毫秒递增10,取模30后角度在0-30度间循环,配合500毫秒EaseOut动画曲线,四个爪子产生错落有致的摇摆动画。底部"申请领养"和"收藏"两个按钮采用主色实底和薄荷描边的对比设计,点击申请领养会关闭详情弹窗并打开领养确认弹窗。

用品Tab模块:销量进度条与购买流程

PetGoodsTab组件是宠物用品商城模块,包含品类销量进度条图表、横向滚动分类筛选器、商品列表、详情弹窗和确认订单弹窗。

// 销量 Stack 进度条图表
Column() {
  Row() {
    Text('📊 品类销量')
      .fontSize(14)
      .fontColor(PET_C.textMain)
      .fontWeight(FontWeight.Bold)
    Text('实时数据')
      .fontSize(11)
      .fontColor(PET_C.textHint)
      .margin({ left: 8 })
  }
  .width('100%')
  .justifyContent(FlexAlign.SpaceBetween)

  ForEach(['主粮', '玩具', '器具', '清洁', '户外', '家居', '零食', '护理'], (c: string, i: number) => {
    Row() {
      Text(c)
        .fontSize(12)
        .fontColor(PET_C.textSub)
        .width(44)
      Stack() {
        Row() {
          Row()
            .width(`${Math.min(petGoodsList(c).length * 12, 100)}%`)
            .height(9)
            .borderRadius(5)
            .backgroundColor(i % 2 === 0 ? PET_C.primary : PET_C.mintDark)
        }
        .width('100%')
        .height(9)
        .backgroundColor('#EDF6F1')
        .borderRadius(5)
      }
      .layoutWeight(1)
      Text(`${petGoodsList(c).length}`)
        .fontSize(11)
        .fontColor(PET_C.primary)
        .width(40)
        .textAlign(TextAlign.End)
    }
    .width('100%')
    .height(26)
  }, (c: string) => 'cat' + c)
}

品类销量图表通过ForEach遍历八个品类,每个品类渲染一行进度条。进度条宽度通过Math.min(petGoodsList©.length * 12, 100)计算,即将该品类商品数量乘以12并限制在100以内。这里使用Math.min函数确保进度条不会超过容器宽度。偶数索引品类使用珊瑚橙进度条,奇数索引使用薄荷深色,形成了交替配色视觉节奏。右侧显示该品类的商品数量。

商品列表的卡片布局与宠物列表类似,但图标根据品类动态选择:玩具显示🧸、主粮显示🍚、其他显示🛍️。每张卡片点击后打开详情弹窗,详情弹窗展示商品完整信息,底部"立即购买"和"加入购物车"两个按钮。点击立即购买会关闭详情弹窗并弹出确认订单弹窗buyModal。

@Builder
buyModal() {
  Stack() {
    Column() {
      Row() {
        Text('确认订单')
          .fontSize(18)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
        Text('✕')
          .fontSize(16)
          .fontColor(PET_C.textHint)
          .onClick(() => {
            this.buyShow = false;
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('🛍️')
          .fontSize(30)
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor('#EAFBF3')
          .borderRadius(14)
        Column() {
          Text(petFindGoods(this.selGoods).name)
            .fontSize(15)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('数量 x 1 · 默认地址')
            .fontSize(12)
            .fontColor(PET_C.textSub)
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 10 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#F7FDFA')
      .borderRadius(14)
      .margin({ top: 14 })

      Row() {
        Text('商品金额')
          .fontSize(13)
          .fontColor(PET_C.textSub)
        Text(`¥${petFindGoods(this.selGoods).price}`)
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')
      .margin({ top: 14 })

      Row() {
        Text('运费')
          .fontSize(13)
          .fontColor(PET_C.textSub)
        Text('免运费')
          .fontSize(13)
          .fontColor(PET_C.green)
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')
      .margin({ top: 8 })

      Row() {
        Text('合计')
          .fontSize(13)
          .fontColor(PET_C.textSub)
        Text(`¥${petFindGoods(this.selGoods).price}`)
          .fontSize(20)
          .fontColor(PET_C.primary)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')
      .margin({ top: 10 })

      Text('提交订单')
        .fontSize(15)
        .fontColor(PET_C.white)
        .fontWeight(FontWeight.Bold)
        .textAlign(TextAlign.Center)
        .width('100%')
        .height(46)
        .backgroundColor(PET_C.mintDark)
        .borderRadius(23)
        .margin({ top: 18 })
        .onClick(() => {
          this.buyShow = false;
        })
    }
    .padding(20)
    .backgroundColor(PET_C.card)
    .borderRadius(20)
    .width('90%')
  }
  .width('100%')
  .height('100%')
}

确认订单弹窗是一个完整的订单确认流程界面。顶部标题栏含关闭按钮,中部商品信息卡片展示商品图标、名称、数量和地址,下方三行明细分别展示商品金额、运费(免运费用绿色标注)和合计金额(大号珊瑚橙字体突出显示),底部"提交订单"按钮使用薄荷深色背景。整个弹窗的信息架构清晰,价格信息的视觉层级通过字号和颜色的差异分明区分。

美容Tab模块:沙漏动画与预约流程

PetBeautyTab组件是宠物美容院模块,包含口碑榜列表、沙漏旋转动画、美容详情弹窗和预约弹窗。

@Component
struct PetBeautyTab {
  @State selBeauty: string = '';
  @State detailShow: boolean = false;
  @State bookShow: boolean = false;
  @State hourglass: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.hourglass = this.hourglass + 6;
    }, 400);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

沙漏动画以400毫秒间隔递增hourglass值,每次增加6度。在详情弹窗中,⏳沙漏emoji通过rotate变换绑定hourglass % 360,实现持续旋转效果,配合800毫秒EaseInOut动画曲线,旋转过渡平滑自然,传达出"高级美容师排班确认中"的等待氛围。

// 沙漏动画
Row() {
  Text('⏳')
    .fontSize(28)
    .rotate({ angle: this.hourglass % 360 })
    .animation({ duration: 800, curve: Curve.EaseInOut })
  Text('高级美容师排班确认中')
    .fontSize(12)
    .fontColor(PET_C.textSub)
    .margin({ left: 10 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 14 })

美容列表按PET_BEAUTY数组顺序展示,每项包含排名序号(前三名珊瑚橙高亮)、服务名称、风格标签(薄荷深色背景)、描述文字、价格和好评率。列表底部有一行"金牌美容师均持证上岗"的居中提示文字,增强信任感。点击列表项打开详情弹窗,详情弹窗展示服务完整信息,包含价格、好评率和耗时三列数据卡片,中间嵌入沙漏动画,底部"立即预约"按钮点击后打开预约弹窗bookModal。

预约弹窗包含门店选择器(旗舰店/城市店/社区店三选一,默认选中旗舰店)和预约时间输入框,展示"明天 10:30 · 宠物需提前 10 分钟到店"的提示信息,底部"确认预约"按钮使用薄荷深色背景。整个预约流程从列表点击到详情查看再到预约确认,形成了完整的三级交互链路。

沙漏动画是状态驱动动画的典型应用。hourglass变量持续递增但通过取模360限制在0-360度范围内,这种"无限递增+取模截断"的模式避免了数值溢出问题,同时保证了动画的连续性。配合EaseInOut曲线,沙漏在旋转过程中产生先加速后减速的自然节奏感。

活动Tab模块:状态筛选与报名流程

PetEventTab组件是萌宠活动模块,包含活动状态筛选器、活动卡片列表、活动详情弹窗和报名成功弹窗。

// 状态筛选
Row() {
  ForEach(['全部', '报名中', '即将开始', '筹备中'], (s: string) => {
    Text(s)
      .fontSize(12)
      .fontColor(this.selStatus === s ? PET_C.white : PET_C.textMain)
      .textAlign(TextAlign.Center)
      .layoutWeight(1)
      .height(34)
      .backgroundColor(this.selStatus === s ? PET_C.primary : PET_C.card)
      .borderRadius(17)
      .margin({ right: s === '筹备中' ? 0 : 8 })
      .onClick(() => {
        this.selStatus = s;
      })
  }, (s: string) => s)
}

状态筛选器提供四个筛选选项,点击后更新selStatus状态,petEventList函数根据状态过滤活动列表。活动卡片采用纵向布局,顶部行展示活动名称和状态标签(通过petStatusColor函数映射颜色),中部行展示城市、日期和人数三个信息标签,下方展示描述文字(最多两行省略号截断),底部行展示活动形式标签和"查看详情"入口。点击卡片打开详情弹窗。

活动详情弹窗展示活动完整信息,包含预计人数、活动形式和报名状态三列数据卡片、活动描述和温馨提示(提示用户为宠物佩戴牵引绳、携带免疫证明)。底部"报名参加"按钮点击后关闭详情弹窗并打开报名成功弹窗joinModal,显示"报名成功"和"活动前1天推送入场码"的提示。

我的Tab模块:收藏管理与删除确认

PetMineTab组件是个人中心模块,包含资料卡、数据统计、订单列表、收藏列表和三个弹窗(资料编辑、订单详情、删除收藏确认)。

@Component
struct PetMineTab {
  @State profileShow: boolean = false;
  @State orderShow: boolean = false;
  @State selOrder: string = '';
  @State delShow: boolean = false;
  @State delName: string = '';
  @State favList: PetFav[] = PET_FAVS;

该组件维护了三个弹窗状态、当前选中订单号、待删除收藏名称以及favList收藏列表。favList初始化为PET_FAVS常量数组,是应用中唯一一个通过状态变量管理动态列表数据的组件,其他组件的数据均为静态常量直接渲染。

@Builder
delModal() {
  Stack() {
    Column() {
      Text('💔')
        .fontSize(44)
      Text('删除收藏?')
        .fontSize(18)
        .fontColor(PET_C.textMain)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 10 })
      Text('将移除「' + this.delName + '」')
        .fontSize(12)
        .fontColor(PET_C.textSub)
        .margin({ top: 8 })
      Row() {
        Text('再想想')
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .textAlign(TextAlign.Center)
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#F0F7F4')
          .borderRadius(22)
          .onClick(() => {
            this.delShow = false;
          })
        Text('确认删除')
          .fontSize(14)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .layoutWeight(1)
          .height(44)
          .backgroundColor(PET_C.danger)
          .borderRadius(22)
          .margin({ left: 12 })
          .onClick(() => {
            this.favList = this.favList.filter((o: PetFav, oi: number) => oi !== this.favList.findIndex((f: PetFav) => f.name === this.delName));
            this.delShow = false;
          })
      }
      .width('100%')
      .margin({ top: 20 })
    }
    .width('82%')
    .padding(24)
    .backgroundColor(PET_C.card)
    .borderRadius(22)
  }
  .width('100%')
  .height('100%')
}

删除收藏弹窗是应用中唯一的二次确认交互。弹窗顶部展示💔大号emoji和"删除收藏?"标题,中间显示待删除的收藏名称。底部两个按钮形成对比设计:"再想想"使用浅灰绿背景的次要按钮风格,"确认删除"使用danger红色背景的主要按钮风格。确认删除按钮的点击回调中,通过filter方法配合findIndex过滤掉匹配delName的收藏项,重新赋值favList触发列表更新。这种通过不可变更新(Immutable Update)的方式重新赋值整个数组,是ArkTS中触发@State数组更新的推荐做法。

收藏列表的每项右侧都有一个💔图标,点击后设置delName并打开删除确认弹窗。订单列表则展示订单商品、日期、件数、金额和状态,状态颜色通过petOrderColor函数映射。点击订单项打开订单详情弹窗,展示订单号、商品信息、下单时间和订单状态等完整信息。

ArkTS中@State装饰的数组变量,当直接通过push、splice等方法修改原数组时,可能不会触发视图更新。最佳实践是通过filter、map等返回新数组的方法重新赋值,确保ArkUI框架能正确检测到状态变化并执行差量渲染。

动画体系横向对比与技术要点

该应用共集成了四种动画方案,分别应用于首页、宠物详情、美容详情等不同场景,下表对四种动画的技术实现进行横向对比:

动画类型 驱动状态变量 定时器间隔 变换属性 动画曲线 动画时长 应用场景 视觉效果
呼吸动画 breath (1↔1.06) 700ms scale (x,y) EaseInOut 700ms 首页猫咪横幅 周期性缩放模拟呼吸起伏
粒子掉落 fall (累加16) 700ms rotate (angle) Linear 700ms 首页零食横幅 持续旋转模拟粒子掉落
爪印动画 paw (累加10) 300ms rotate+opacity+fontSize EaseOut 500ms 宠物详情弹窗 四爪错落摇摆渐变序列
沙漏动画 hourglass (累加6) 400ms rotate (angle) EaseInOut 800ms 美容详情弹窗 360度旋转模拟沙漏等待

四种动画方案虽然变换属性和参数各异,但核心实现模式高度统一:通过setInterval定时器周期性更新@State状态变量,状态变量绑定到UI组件的变换属性上,配合animation属性指定动画曲线和时长,框架自动在状态变化时执行属性过渡动画。这种"定时器驱动状态——状态绑定属性——属性触发动画"的三段式模式,是ArkTS轻量级动画的标准范式,无需引入Lottie等第三方动画库即可实现丰富的动态效果。

下表对比了应用中各Tab模块的核心技术特征:

模块 组件名 状态变量数 弹窗数量 动画类型 数据筛选方式 列表渲染 特色交互
首页 PetHomeTab 4 2 呼吸+粒子 热度榜+横向滚动 进度条热度可视化
宠物 PetItemTab 5 2 爪印 selCat分类过滤 纵向列表+图标卡片 三段式详情弹窗链
用品 PetGoodsTab 4 2 selCat横向滚动筛选 纵向列表+销量图表 确认订单流程弹窗
美容 PetBeautyTab 4 2 沙漏 无(按好评排序) 纵向口碑榜列表 预约门店选择器
活动 PetEventTab 4 2 selStatus状态过滤 纵向卡片列表 报名成功反馈弹窗
我的 PetMineTab 6 3 favList动态过滤 订单+收藏双列表 删除二次确认交互

从上表可以看出,六个Tab模块在状态管理、弹窗交互和数据筛选方面形成了统一的设计范式,但又根据各自业务特点进行了差异化定制。PetMineTab是状态变量和弹窗数量最多的模块,体现了个人中心功能的复杂度;PetHomeTab和PetBeautyTab集成了动画效果,增强了交互的生动感;PetItemTab和PetGoodsTab提供了分类筛选能力,适应了宠物品种和商品品类多样性的需求。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 场景:宠物领养/用品/美容/活动社区
// 底部 6 Tab:首页/宠物/用品/美容/活动/我的
// 包含:宠物热度榜进度条、用品销量 Stack 进度条、猫咪充电呼吸动画、零食掉落粒子、美容沙漏动画
// 弹框:领养登记/宠物详情/用品详情/美容预约/美容详情/活动详情/订单详情/删除收藏/我的资料/满减弹框 共12处

interface PetTab {
  name: string;
  icon: string;
}

interface PetItem {
  name: string;
  breed: string;
  cat: string;
  age: string;
  price: number;
  hot: number;
  tag: string;
  desc: string;
}

interface PetGoods {
  name: string;
  cat: string;
  price: number;
  sales: number;
  stock: number;
  tag: string;
  desc: string;
}

interface PetBeauty {
  name: string;
  style: string;
  price: number;
  score: number;
  time: string;
  desc: string;
}

interface PetEvent {
  name: string;
  city: string;
  date: string;
  people: number;
  mode: string;
  status: string;
  desc: string;
}

interface PetOrder {
  no: string;
  name: string;
  cnt: number;
  amount: number;
  date: string;
  status: string;
}

interface PetFav {
  name: string;
  cat: string;
  price: number;
  hot: number;
}

interface PetBreed {
  name: string;
  count: number;
  desc: string;
}

interface PetColor {
  bg: string;
  card: string;
  primary: string;
  primaryDark: string;
  mint: string;
  mintDark: string;
  textMain: string;
  textSub: string;
  textHint: string;
  border: string;
  gold: string;
  danger: string;
  green: string;
  white: string;
  soft: string;
}

const PET_C: PetColor = {
  bg: '#F0FBF6',
  card: '#FFFFFF',
  primary: '#FF8C69',
  primaryDark: '#F26B45',
  mint: '#7FD8BE',
  mintDark: '#4FB899',
  textMain: '#2C3E3E',
  textSub: '#7A8F8A',
  textHint: '#A8C3BC',
  border: '#DFF0EA',
  gold: '#F5B942',
  danger: '#E5484D',
  green: '#2FB368',
  white: '#FFFFFF',
  soft: '#FFF3EC'
};

const PET_TABS: PetTab[] = [
  { name: '首页', icon: '🏠' },
  { name: '宠物', icon: '🐾' },
  { name: '用品', icon: '🛍️' },
  { name: '美容', icon: '💇' },
  { name: '活动', icon: '🎪' },
  { name: '我的', icon: '👤' }
];

const PET_BREEDS: PetBreed[] = [
  { name: '猫咪', count: 8, desc: '布偶/英短/橘猫/狸花' },
  { name: '狗狗', count: 6, desc: '柯基/金毛/柴犬/边牧' },
  { name: '异宠', count: 2, desc: '龙猫/蜜袋鼯' }
];

const PET_ITEMS: PetItem[] = [
  { name: '布偶猫·雪球', breed: '布偶', cat: '猫咪', age: '8个月', price: 6800, hot: 98, tag: '热门', desc: '蓝双色布偶,性格温顺粘人,疫苗齐全已绝育' },
  { name: '柯基·奶糖', breed: '柯基', cat: '狗狗', age: '1岁', price: 5200, hot: 96, tag: '活泼', desc: '三色柯基,短腿圆臀,精力旺盛爱撒娇' },
  { name: '英短蓝猫·团子', breed: '英短', cat: '猫咪', age: '5个月', price: 2800, hot: 94, tag: '热门', desc: '蓝灰色英短,圆脸大眼,性格安静易相处' },
  { name: '金毛·阳光', breed: '金毛', cat: '狗狗', age: '2岁', price: 4200, hot: 92, tag: '温顺', desc: '纯种金毛巡回犬,亲人聪明,适合家庭陪伴' },
  { name: '橘猫·大橘', breed: '橘猫', cat: '猫咪', age: '1岁', price: 800, hot: 90, tag: '高性价比', desc: '中华田园橘猫,能吃能睡能撒娇,健康皮实' },
  { name: '柴犬·豆豆', breed: '柴犬', cat: '狗狗', age: '1岁半', price: 7800, hot: 88, tag: '网红', desc: '赤色柴犬,表情包大户,独立又忠诚' },
  { name: '龙猫·团团', breed: '龙猫', cat: '异宠', age: '6个月', price: 3600, hot: 84, tag: '稀有', desc: '银斑龙猫,毛茸茸圆滚滚,夜间活跃' },
  { name: '边牧·闪电', breed: '边牧', cat: '狗狗', age: '1岁', price: 6500, hot: 82, tag: '聪明', desc: '黑白边牧,智商超群,学指令飞快' },
  { name: '狸花猫·小虎', breed: '狸花', cat: '猫咪', age: '9个月', price: 500, hot: 80, tag: '领养', desc: '标准狸花,捕猎小能手,健康易养' },
  { name: '蜜袋鼯·果冻', breed: '蜜袋鼯', cat: '异宠', age: '4个月', price: 1800, hot: 76, tag: '袖珍', desc: '亮灰色蜜袋鼯,会滑翔的小可爱,亲近人' },
  { name: '蓝猫·薄荷', breed: '英短', cat: '猫咪', age: '10个月', price: 3200, hot: 74, tag: '可爱', desc: '蓝白英短,薄荷绿眼睛,软糯好rua' },
  { name: '泰迪·卷卷', breed: '泰迪', cat: '狗狗', age: '2岁', price: 2600, hot: 70, tag: '不掉毛', desc: '棕色玩具贵宾,卷毛造型,聪明活泼' },
  { name: '布偶·奶茶', breed: '布偶', cat: '猫咪', age: '1岁', price: 7200, hot: 68, tag: '精品', desc: '海双布偶,大围脖爆毛,仙女气质' },
  { name: '萨摩耶·棉花', breed: '萨摩耶', cat: '狗狗', age: '1岁半', price: 8800, hot: 64, tag: '微笑天使', desc: '纯白萨摩耶,微笑天使,蓬松雪白' },
  { name: '折耳猫·汤圆', breed: '苏格兰折耳', cat: '猫咪', age: '7个月', price: 4500, hot: 60, tag: '软萌', desc: '折耳立耳随机,圆滚滚自带腮红' },
  { name: '二哈·拆家', breed: '哈士奇', cat: '狗狗', age: '2岁', price: 3500, hot: 56, tag: '搞笑', desc: '蓝色眼眸哈士奇,表情帝,自带戏精属性' }
];

const PET_GOODS: PetGoods[] = [
  { name: '冻干双拼猫粮 5kg', cat: '主粮', price: 158, sales: 3200, stock: 500, tag: '爆款', desc: '鸡肉冻干+全价粮,含牛磺酸,护肠胃' },
  { name: '全价幼犬粮 4kg', cat: '主粮', price: 128, sales: 2800, stock: 600, tag: '热销', desc: '高蛋白配方,DHA助力幼犬大脑发育' },
  { name: '逗猫棒三件套', cat: '玩具', price: 29, sales: 5600, stock: 900, tag: '平价', desc: '羽毛+铃铛+激光三合一,猫咪疯狂上头' },
  { name: '自动饮水机 2L', cat: '器具', price: 99, sales: 1800, stock: 300, tag: '智能', desc: '循环活水+四重过滤,静音马达设计' },
  { name: '猫砂豆腐砂 6L', cat: '清洁', price: 39, sales: 8000, stock: 1500, tag: '爆款', desc: '豆腐砂吸水结团快,低尘可冲厕所' },
  { name: '牵引绳胸背套装', cat: '户外', price: 89, sales: 1200, stock: 400, tag: '安全', desc: '反光材质胸背带,爆冲缓冲减压' },
  { name: '宠物窝垫冬夏两用', cat: '家居', price: 129, sales: 980, stock: 250, tag: '舒适', desc: '可拆洗绒布窝垫,四季通用' },
  { name: '冻干鸡肉零食 200g', cat: '零食', price: 45, sales: 4300, stock: 800, tag: '热销', desc: '纯肉冻干无添加,训练奖励神器' },
  { name: '宠物指甲剪套装', cat: '护理', price: 35, sales: 2100, stock: 700, tag: '必备', desc: '防误剪设计,含磨甲锉与止血粉' },
  { name: '猫抓板瓦楞纸', cat: '玩具', price: 25, sales: 6500, stock: 1200, tag: '平价', desc: '双面可用猫抓板,附赠猫薄荷' }
];

const PET_BEAUTY: PetBeauty[] = [
  { name: '洗护美容套餐', style: '全套', price: 168, score: 98, time: '90分钟', desc: '洗护+吹干+造型修剪+指甲护理' },
  { name: '宠物SPA深层护理', style: 'SPA', price: 258, score: 96, time: '120分钟', desc: '进口精油SPA,深层清洁滋养皮毛' },
  { name: '赛级造型修剪', style: '造型', price: 328, score: 95, time: '150分钟', desc: '资深美容师,赛级标准造型设计' },
  { name: '猫咪去浮毛护理', style: '护理', price: 88, score: 94, time: '45分钟', desc: '专业去浮毛,减少猫毛满屋飞' },
  { name: '宠物洁牙护理', style: '护理', price: 138, score: 92, time: '60分钟', desc: '超声波洁牙,清新口气防牙结石' },
  { name: '狗狗香薰浴', style: 'SPA', price: 118, score: 90, time: '70分钟', desc: '香薰泡泡浴,舒缓放松皮毛护理' },
  { name: '猫咪美容一条龙', style: '全套', price: 198, score: 89, time: '100分钟', desc: '洗剪吹一条龙,含剪指甲清耳道' },
  { name: '小宠清洁美容', style: '护理', price: 68, score: 87, time: '40分钟', desc: '龙猫/仓鼠等小宠专属清洁服务' }
];

const PET_EVENTS: PetEvent[] = [
  { name: '萌宠领养日', city: '上海', date: '09-06', people: 300, mode: '线下领养', status: '报名中', desc: '现场 50+ 待领养毛孩子,专业顾问一对一' },
  { name: '宠物运动会', city: '成都', date: '09-14', people: 200, mode: '趣味赛跑', status: '报名中', desc: '狗狗障碍跑/猫咪敏捷赛,冠军有神秘大礼' },
  { name: '猫咪咖啡馆集会', city: '杭州', date: '09-21', people: 120, mode: '撸猫聚会', status: '报名中', desc: '网红猫咖包场,50 只猫咪陪玩一下午' },
  { name: '宠物摄影展', city: '广州', date: '10-11', people: 500, mode: '摄影展览', status: '即将开始', desc: '萌宠大片展览,现场有专业摄影师' },
  { name: '狗狗水上乐园', city: '三亚', date: '10-26', people: 150, mode: '水上狂欢', status: '筹备中', desc: '宠物专属泳池,救生员全程守护' },
  { name: '异宠科普节', city: '北京', date: '11-08', people: 260, mode: '科普展览', status: '筹备中', desc: '蜥蜴/龙猫/蜜袋鼯科普,安全近距离观察' }
];

const PET_ORDERS: PetOrder[] = [
  { no: 'P260815001', name: '冻干双拼猫粮 5kg', cnt: 2, amount: 316, date: '08-15 10:23', status: '已发货' },
  { no: 'P260815002', name: '逗猫棒三件套', cnt: 1, amount: 29, date: '08-15 11:40', status: '待收货' },
  { no: 'P260816003', name: '宠物洗护美容套餐', cnt: 1, amount: 168, date: '08-16 09:12', status: '已完成' },
  { no: 'P260817004', name: '猫砂豆腐砂 6L', cnt: 4, amount: 156, date: '08-17 14:05', status: '待发货' },
  { no: 'P260818005', name: '自动饮水机 2L', cnt: 1, amount: 99, date: '08-18 16:48', status: '已发货' },
  { no: 'P260820006', name: '牵引绳胸背套装', cnt: 1, amount: 89, date: '08-20 08:30', status: '已完成' },
  { no: 'P260821007', name: '冻干鸡肉零食', cnt: 3, amount: 135, date: '08-21 19:22', status: '待付款' },
  { no: 'P260822008', name: '猫咪去浮毛护理', cnt: 1, amount: 88, date: '08-22 13:15', status: '退款中' }
];

const PET_FAVS: PetFav[] = [
  { name: '布偶猫·雪球', cat: '宠物', price: 6800, hot: 98 },
  { name: '冻干双拼猫粮', cat: '用品', price: 158, hot: 95 },
  { name: '柯基·奶糖', cat: '宠物', price: 5200, hot: 96 },
  { name: '宠物窝垫', cat: '用品', price: 129, hot: 88 },
  { name: '赛级造型修剪', cat: '美容', price: 328, hot: 92 },
  { name: '猫砂豆腐砂', cat: '用品', price: 39, hot: 90 }
];

// 纯函数工具区
function petHotWidth(h: number): number {
  let w = h * 2.6;
  if (w > 100) {
    w = 100;
  }
  return w;
}

function petTagColor(tag: string): string {
  if (tag === '爆款') {
    return PET_C.danger;
  }
  if (tag === '热销') {
    return PET_C.primary;
  }
  if (tag === '平价') {
    return PET_C.green;
  }
  if (tag === '领养' || tag === '高性价比') {
    return PET_C.mintDark;
  }
  return PET_C.gold;
}

function petStatusColor(s: string): string {
  if (s === '报名中') {
    return PET_C.primary;
  }
  if (s === '即将开始') {
    return PET_C.gold;
  }
  return PET_C.textHint;
}

function petOrderColor(s: string): string {
  if (s === '已完成') {
    return PET_C.green;
  }
  if (s === '已发货') {
    return '#4FB899';
  }
  if (s === '退款中') {
    return PET_C.danger;
  }
  if (s === '待付款') {
    return PET_C.gold;
  }
  return PET_C.primary;
}

@Entry
@Component
struct PetApp {
  @State activeTab: number = 0;

  @Builder
  tabIcon(item: PetTab, idx: number) {
    Column() {
      Text(item.icon)
        .fontSize(20)
        .margin({ top: 6 })
      Text(item.name)
        .fontSize(11)
        .fontColor(this.activeTab === idx ? PET_C.primary : PET_C.textHint)
        .fontWeight(this.activeTab === idx ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 3, bottom: 4 })
    }
    .width('100%')
    .layoutWeight(1)
    .borderRadius(14)
    .backgroundColor(this.activeTab === idx ? PET_C.soft : Color.Transparent)
    .onClick(() => {
      this.activeTab = idx;
    })
  }

  @Builder
  tabContent() {
    if (this.activeTab === 0) {
      PetHomeTab()
    } else if (this.activeTab === 1) {
      PetItemTab()
    } else if (this.activeTab === 2) {
      PetGoodsTab()
    } else if (this.activeTab === 3) {
      PetBeautyTab()
    } else if (this.activeTab === 4) {
      PetEventTab()
    } else {
      PetMineTab()
    }
  }

  build() {
    Column() {
      Stack() {
        this.tabContent()
      }
      .layoutWeight(1)
      .width('100%')

      Row() {
        ForEach(PET_TABS, (item: PetTab, idx: number) => {
          this.tabIcon(item, idx)
        }, (item: PetTab, idx: number) => item.name + idx)
      }
      .width('100%')
      .height(62)
      .backgroundColor(PET_C.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .shadow({ radius: 12, color: '#1A7FD8BE', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(PET_C.bg)
  }
}

@Component
struct PetHomeTab {
  @State adoptShow: boolean = false;
  @State couponShow: boolean = false;
  @State breath: number = 1;
  @State fall: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.breath = this.breath > 1 ? 1 : 1.06;
      this.fall = this.fall + 16;
    }, 700);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.adoptShow = false;
        this.couponShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  adoptModal() {
    Stack() {
      Column() {
        Row() {
          Text('🐾 领养登记')
            .fontSize(18)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.adoptShow = false;
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Text('意向宠物')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 16 })
        Text('例如:布偶猫 / 柯基')
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .width('100%')
          .height(44)
          .padding({ left: 14 })
          .backgroundColor('#F4FAF7')
          .borderRadius(10)
          .margin({ top: 8 })

        Text('居住环境')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
        Row() {
          ForEach(['自有住房', '租住', '合租'], (h: string, i: number) => {
            Text(h)
              .fontSize(13)
              .fontColor(i === 0 ? PET_C.white : PET_C.textMain)
              .textAlign(TextAlign.Center)
              .layoutWeight(1)
              .height(38)
              .backgroundColor(i === 0 ? PET_C.primary : '#F4FAF7')
              .borderRadius(10)
              .margin({ right: i < 2 ? 8 : 0 })
          }, (h: string) => h)
        }
        .width('100%')
        .margin({ top: 8 })

        Text('养宠经验')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
        Text('例如:养过 2 只猫咪')
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .width('100%')
          .height(44)
          .padding({ left: 14 })
          .backgroundColor('#F4FAF7')
          .borderRadius(10)
          .margin({ top: 8 })

        Text('提交领养申请')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.primary)
          .borderRadius(23)
          .margin({ top: 20 })
          .onClick(() => {
            this.adoptShow = false;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('88%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  couponModal() {
    Stack() {
      Column() {
        Text('🎁')
          .fontSize(44)
        Text('新人礼包到账')
          .fontSize(19)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 10 })
        Text('满 300 减 50 券 · 已放入卡包')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .margin({ top: 8 })
        Row() {
          Text('¥50')
            .fontSize(26)
            .fontColor(PET_C.primary)
            .fontWeight(FontWeight.Bold)
          Text('满300元可用')
            .fontSize(12)
            .fontColor(PET_C.textHint)
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding({ top: 16, bottom: 16 })
        .backgroundColor('#FFF3EC')
        .borderRadius(14)
        .margin({ top: 14 })
        Text('去逛逛')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 18 })
          .onClick(() => {
            this.couponShow = false;
          })
      }
      .width('84%')
      .padding(24)
      .backgroundColor(PET_C.card)
      .borderRadius(22)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 浅色圆角头部
          Column() {
            Row() {
              Column() {
                Text('🐾 宠物潮流馆')
                  .fontSize(22)
                  .fontColor(PET_C.textMain)
                  .fontWeight(FontWeight.Bold)
                Text('领养 · 用品 · 美容一站式')
                  .fontSize(12)
                  .fontColor(PET_C.textSub)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              Text('🔔')
                .fontSize(20)
                .width(40)
                .height(40)
                .textAlign(TextAlign.Center)
                .backgroundColor(PET_C.card)
                .borderRadius(20)
                .shadow({ radius: 6, color: '#1A7FD8BE', offsetY: 2 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

            Row() {
              Text('🔍')
                .fontSize(16)
                .margin({ left: 14 })
              Text('搜索宠物/用品/美容服务')
                .fontSize(13)
                .fontColor(PET_C.textHint)
                .margin({ left: 8 })
              Text('搜索')
                .fontSize(12)
                .fontColor(PET_C.white)
                .fontWeight(FontWeight.Bold)
                .textAlign(TextAlign.Center)
                .width(64)
                .height(32)
                .backgroundColor(PET_C.primary)
                .borderRadius(16)
                .margin({ right: 6 })
            }
            .width('100%')
            .height(44)
            .backgroundColor(PET_C.card)
            .borderRadius(22)
            .justifyContent(FlexAlign.SpaceBetween)
            .margin({ top: 14 })
            .shadow({ radius: 8, color: '#1A7FD8BE', offsetY: 3 })
          }
          .width('100%')
          .padding({ left: 18, right: 18, top: 14, bottom: 16 })
          .linearGradient({ colors: [['#EAFBF3', 0], ['#F7FFF9', 1]] })

          // 功能入口(圆角气泡)
          Row() {
            ForEach(['🐱 领养', '🛍️ 商城', '💇 美容', '🎪 活动'], (f: string, i: number) => {
              Text(f)
                .fontSize(13)
                .fontColor(i === 0 ? PET_C.white : PET_C.textMain)
                .textAlign(TextAlign.Center)
                .layoutWeight(1)
                .height(60)
                .backgroundColor(i === 0 ? PET_C.primary : PET_C.card)
                .borderRadius(20)
                .margin({ right: i < 3 ? 8 : 0 })
                .shadow({ radius: 6, color: '#14A8C3BC', offsetY: 2 })
                .onClick(() => {
                  if (i === 0) {
                    this.adoptShow = true;
                  }
                })
            }, (f: string) => f)
          }
          .width('100%')
          .padding({ left: 14, right: 14, top: 16 })

          // 猫咪呼吸动画横幅
          Row() {
            Column() {
              Text('🐱')
                .fontSize(34)
                .scale({ x: this.breath, y: this.breath })
                .animation({ duration: 700, curve: Curve.EaseInOut })
            }
            .width(64)
            .height(64)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(PET_C.mint)
            .borderRadius(32)

            Column() {
              Text('萌宠在呼吸 · 安心陪伴')
                .fontSize(14)
                .fontColor(PET_C.white)
                .fontWeight(FontWeight.Bold)
              Text('本月已有 328 只毛孩子找到新家')
                .fontSize(11)
                .fontColor('#E8FFF6')
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 12 })

            Text('领养')
              .fontSize(12)
              .fontColor(PET_C.mintDark)
              .textAlign(TextAlign.Center)
              .width(56)
              .height(30)
              .backgroundColor(PET_C.white)
              .borderRadius(15)
              .onClick(() => {
                this.adoptShow = true;
              })
          }
          .width('100%')
          .height(66)
          .padding({ left: 14, right: 12 })
          .linearGradient({ colors: [['#7FD8BE', 0], ['#4FB899', 1]] })
          .borderRadius(18)
          .margin({ left: 14, right: 14, top: 12 })

          // 新人礼包横幅(零食掉落粒子)
          Row() {
            Text('🍖')
              .fontSize(24)
              .rotate({ angle: this.fall % 30 })
              .animation({ duration: 700, curve: Curve.Linear })
            Column() {
              Text('新人礼包 · 满300减50')
                .fontSize(14)
                .fontColor(PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text('限时领取 72 小时内有效')
                .fontSize(11)
                .fontColor(PET_C.textSub)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 10 })
            Text('领取')
              .fontSize(12)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
              .width(56)
              .height(30)
              .backgroundColor(PET_C.primary)
              .borderRadius(15)
              .onClick(() => {
                this.couponShow = true;
              })
          }
          .width('100%')
          .height(64)
          .padding({ left: 16, right: 12 })
          .backgroundColor(PET_C.card)
          .borderRadius(18)
          .border({ width: 1, color: '#FFD9C4' })
          .margin({ left: 14, right: 14, top: 12 })

          // 宠物热度榜图表
          Row() {
            Text('🔥 宠物热度榜')
              .fontSize(16)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('本周人气')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .padding({ left: 16, top: 18 })
          .justifyContent(FlexAlign.SpaceBetween)

          Column() {
            ForEach([0, 1, 2, 3, 4], (idx: number) => {
              Row() {
                Text(`${idx + 1}`)
                  .fontSize(15)
                  .fontColor(idx < 3 ? PET_C.primary : PET_C.textHint)
                  .fontWeight(FontWeight.Bold)
                  .width(28)
                Text(PET_ITEMS[idx].name)
                  .fontSize(13)
                  .fontColor(PET_C.textMain)
                  .width(104)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Stack() {
                  Row() {
                    Row()
                      .width(`${petHotWidth(PET_ITEMS[idx].hot)}%`)
                      .height(10)
                      .borderRadius(5)
                      .linearGradient({ colors: [[PET_C.primary, 0], ['#FFC3AC', 1]] })
                  }
                  .width('100%')
                  .height(10)
                  .backgroundColor('#EDF6F1')
                  .borderRadius(5)
                }
                .layoutWeight(1)
                Text(`${PET_ITEMS[idx].hot}`)
                  .fontSize(12)
                  .fontColor(PET_C.primary)
                  .fontWeight(FontWeight.Bold)
                  .width(32)
                  .textAlign(TextAlign.End)
              }
              .width('100%')
              .height(40)
            }, (idx: number) => 'rank' + idx)
          }
          .padding({ left: 16, right: 16, top: 6, bottom: 10 })
          .backgroundColor(PET_C.card)
          .borderRadius(18)
          .margin({ left: 14, right: 14, top: 10 })

          // 精选宠物横向
          Row() {
            Text('⭐ 待领养萌宠')
              .fontSize(16)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('全部 >')
              .fontSize(12)
              .fontColor(PET_C.textHint)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 18 })
          .justifyContent(FlexAlign.SpaceBetween)

          Scroll() {
            Row() {
              ForEach([2, 5, 9, 13], (idx: number) => {
                Column() {
                  Text(PET_ITEMS[idx].cat === '猫咪' ? '🐱' : (PET_ITEMS[idx].cat === '狗狗' ? '🐶' : '🐭'))
                    .fontSize(34)
                  Text(PET_ITEMS[idx].name)
                    .fontSize(14)
                    .fontColor(PET_C.textMain)
                    .fontWeight(FontWeight.Bold)
                    .margin({ top: 6 })
                  Text(`${PET_ITEMS[idx].breed} · ${PET_ITEMS[idx].age}`)
                    .fontSize(11)
                    .fontColor(PET_C.textHint)
                    .margin({ top: 3 })
                  Row() {
                    Text(PET_ITEMS[idx].tag)
                      .fontSize(10)
                      .fontColor(PET_C.white)
                      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                      .backgroundColor(petTagColor(PET_ITEMS[idx].tag))
                      .borderRadius(8)
                    Text(`¥${PET_ITEMS[idx].price}`)
                      .fontSize(14)
                      .fontColor(PET_C.primary)
                      .fontWeight(FontWeight.Bold)
                      .margin({ left: 6 })
                  }
                  .width('100%')
                  .justifyContent(FlexAlign.SpaceBetween)
                  .margin({ top: 8 })
                }
                .width(158)
                .padding(14)
                .backgroundColor(PET_C.card)
                .borderRadius(18)
                .margin({ right: 10 })
                .shadow({ radius: 8, color: '#147FD8BE', offsetY: 3 })
              }, (idx: number) => 'pick' + idx)
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
          .padding({ left: 14, right: 14, top: 10 })

          // 品牌故事入口
          Row() {
            Column() {
              Text('📖 养宠指南')
                .fontSize(15)
                .fontColor(PET_C.white)
                .fontWeight(FontWeight.Bold)
              Text('新手养宠避坑手册 · 免费领取')
                .fontSize(11)
                .fontColor('#E8FFF6')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('查看')
              .fontSize(13)
              .fontColor(PET_C.mintDark)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
              .width(64)
              .height(32)
              .backgroundColor(PET_C.white)
              .borderRadius(16)
          }
          .width('100%')
          .height(64)
          .padding({ left: 18, right: 14 })
          .backgroundColor(PET_C.mintDark)
          .borderRadius(18)
          .margin({ left: 14, right: 14, top: 18, bottom: 24 })
        }
        .width('100%')
      }
      .scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')

      if (this.adoptShow) {
        Column() {
          this.modalOverlay()
          this.adoptModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.couponShow) {
        Column() {
          this.modalOverlay()
          this.couponModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

function petListByCat(cat: string): PetItem[] {
  if (cat === '全部') {
    return PET_ITEMS;
  }
  return PET_ITEMS.filter((p: PetItem) => p.cat === cat);
}

function petFindPet(n: string): PetItem {
  for (let i = 0; i < PET_ITEMS.length; i++) {
    if (PET_ITEMS[i].name === n) {
      return PET_ITEMS[i];
    }
  }
  return PET_ITEMS[0];
}

@Component
struct PetItemTab {
  @State selCat: string = '全部';
  @State selPet: string = '';
  @State detailShow: boolean = false;
  @State adoptShow: boolean = false;
  @State paw: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.paw = this.paw + 10;
    }, 300);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.detailShow = false;
        this.adoptShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  detailModal() {
    Stack() {
      Column() {
        Row() {
          Text(petFindPet(this.selPet).cat === '猫咪' ? '🐱' : (petFindPet(this.selPet).cat === '狗狗' ? '🐶' : '🐭'))
            .fontSize(40)
          Column() {
            Text(petFindPet(this.selPet).name)
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text(petFindPet(this.selPet).breed + ' · ' + petFindPet(this.selPet).age)
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')

        Text(petFindPet(this.selPet).desc)
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
          .lineHeight(20)

        Row() {
          Column() {
            Text(`¥${petFindPet(this.selPet).price}`)
              .fontSize(20)
              .fontColor(PET_C.primary)
              .fontWeight(FontWeight.Bold)
            Text('领养价')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(`${petFindPet(this.selPet).hot}`)
              .fontSize(20)
              .fontColor(PET_C.gold)
              .fontWeight(FontWeight.Bold)
            Text('人气值')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(petFindPet(this.selPet).tag)
              .fontSize(15)
              .fontColor(PET_C.mintDark)
              .fontWeight(FontWeight.Bold)
            Text('特点')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .backgroundColor('#F4FAF7')
        .borderRadius(14)
        .margin({ top: 14 })

        // 小爪印动画
        Row() {
          ForEach([0, 1, 2, 3], (i: number) => {
            Text('🐾')
              .fontSize(14 + i * 3)
              .opacity(0.95 - i * 0.2)
              .rotate({ angle: (i % 2 === 0 ? 1 : -1) * (this.paw % 30) })
              .animation({ duration: 500, curve: Curve.EaseOut })
              .margin({ right: 10 })
          }, (i: number) => 'p' + i)
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .margin({ top: 14 })

        Row() {
          Text('申请领养')
            .fontSize(14)
            .fontColor(PET_C.white)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .backgroundColor(PET_C.primary)
            .borderRadius(22)
            .onClick(() => {
              this.detailShow = false;
              this.adoptShow = true;
            })
          Text('收藏')
            .fontSize(14)
            .fontColor(PET_C.mintDark)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .border({ width: 1, color: PET_C.mint })
            .borderRadius(22)
            .margin({ left: 10 })
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  adoptModal() {
    Stack() {
      Column() {
        Text('🎉')
          .fontSize(46)
        Text('领养申请已提交')
          .fontSize(19)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 12 })
        Text('客服将在 24 小时内联系您进行家访')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .margin({ top: 8 })
        Text('确定')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 22 })
          .onClick(() => {
            this.adoptShow = false;
          })
      }
      .width('82%')
      .padding(24)
      .backgroundColor(PET_C.card)
      .borderRadius(22)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        // 头部
        Column() {
          Row() {
            Text('🐾 宠物之家')
              .fontSize(20)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
            Text(`${petListByCat(this.selCat).length} 只待领养`)
              .fontSize(12)
              .fontColor('#E8FFF6')
              .margin({ left: 8 })
            Text('+ 登记')
              .fontSize(12)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
              .width(64)
              .height(30)
              .backgroundColor(PET_C.primary)
              .borderRadius(15)
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .linearGradient({ colors: [['#7FD8BE', 0], ['#4FB899', 1]] })
        .borderRadius({ bottomLeft: 22, bottomRight: 22 })

        // 品种信息卡
        Row() {
          ForEach(PET_BREEDS, (b: PetBreed, i: number) => {
            Column() {
              Text(b.name)
                .fontSize(14)
                .fontColor(PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text(`${b.count}`)
                .fontSize(12)
                .fontColor(PET_C.primary)
                .margin({ top: 3 })
              Text(b.desc)
                .fontSize(9)
                .fontColor(PET_C.textHint)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .margin({ top: 3 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(PET_C.card)
            .borderRadius(16)
            .margin({ right: i < 2 ? 8 : 0 })
          }, (b: PetBreed) => b.name)
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 14 })

        // 分类筛选
        Row() {
          ForEach(['全部', '猫咪', '狗狗', '异宠'], (c: string) => {
            Text(c)
              .fontSize(12)
              .fontColor(this.selCat === c ? PET_C.white : PET_C.textMain)
              .textAlign(TextAlign.Center)
              .layoutWeight(1)
              .height(34)
              .backgroundColor(this.selCat === c ? PET_C.primary : PET_C.card)
              .borderRadius(17)
              .margin({ right: c === '异宠' ? 0 : 8 })
              .onClick(() => {
                this.selCat = c;
              })
          }, (c: string) => c)
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 12 })

        Scroll() {
          Column() {
            ForEach(petListByCat(this.selCat), (item: PetItem, idx: number) => {
              Row() {
                Column() {
                  Text(item.cat === '猫咪' ? '🐱' : (item.cat === '狗狗' ? '🐶' : '🐭'))
                    .fontSize(34)
                  Text(item.breed)
                    .fontSize(10)
                    .fontColor(PET_C.textHint)
                    .margin({ top: 4 })
                }
                .width(76)
                .height(76)
                .justifyContent(FlexAlign.Center)
                .backgroundColor('#EAFBF3')
                .borderRadius(16)

                Column() {
                  Row() {
                    Text(item.name)
                      .fontSize(15)
                      .fontColor(PET_C.textMain)
                      .fontWeight(FontWeight.Bold)
                    Text(item.tag)
                      .fontSize(10)
                      .fontColor(PET_C.white)
                      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                      .backgroundColor(petTagColor(item.tag))
                      .borderRadius(8)
                      .margin({ left: 8 })
                  }
                  .width('100%')

                  Text(`${item.age} · ${item.desc}`)
                    .fontSize(11)
                    .fontColor(PET_C.textSub)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .width('100%')
                    .margin({ top: 4 })

                  Row() {
                    Text(`¥${item.price}`)
                      .fontSize(16)
                      .fontColor(PET_C.primary)
                      .fontWeight(FontWeight.Bold)
                    Text(`人气 ${item.hot}`)
                      .fontSize(11)
                      .fontColor(PET_C.gold)
                      .margin({ left: 8 })
                    Text('详情 >')
                      .fontSize(12)
                      .fontColor(PET_C.mintDark)
                      .fontWeight(FontWeight.Bold)
                      .margin({ left: 8 })
                  }
                  .width('100%')
                  .margin({ top: 6 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 12 })
              }
              .width('100%')
              .padding(12)
              .backgroundColor(PET_C.card)
              .borderRadius(16)
              .margin({ bottom: 10 })
              .onClick(() => {
                this.selPet = item.name;
                this.detailShow = true;
              })
            }, (item: PetItem) => item.name)
          }
          .width('100%')
          .padding({ left: 14, right: 14, top: 10, bottom: 20 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.Top)
      }
      .width('100%')
      .height('100%')

      if (this.detailShow) {
        Column() {
          this.modalOverlay()
          this.detailModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.adoptShow) {
        Column() {
          this.modalOverlay()
          this.adoptModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

function petGoodsList(cat: string): PetGoods[] {
  if (cat === '全部') {
    return PET_GOODS;
  }
  return PET_GOODS.filter((g: PetGoods) => g.cat === cat);
}

function petFindGoods(n: string): PetGoods {
  for (let i = 0; i < PET_GOODS.length; i++) {
    if (PET_GOODS[i].name === n) {
      return PET_GOODS[i];
    }
  }
  return PET_GOODS[0];
}

@Component
struct PetGoodsTab {
  @State selCat: string = '全部';
  @State selGoods: string = '';
  @State detailShow: boolean = false;
  @State buyShow: boolean = false;

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.detailShow = false;
        this.buyShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  detailModal() {
    Stack() {
      Column() {
        Row() {
          Text('🛍️')
            .fontSize(38)
          Column() {
            Text(petFindGoods(this.selGoods).name)
              .fontSize(17)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text(petFindGoods(this.selGoods).cat + ' · ' + petFindGoods(this.selGoods).tag)
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')

        Text(petFindGoods(this.selGoods).desc)
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
          .lineHeight(20)

        Row() {
          Column() {
            Text(`¥${petFindGoods(this.selGoods).price}`)
              .fontSize(20)
              .fontColor(PET_C.primary)
              .fontWeight(FontWeight.Bold)
            Text('售价')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(`${petFindGoods(this.selGoods).sales}`)
              .fontSize(18)
              .fontColor(PET_C.gold)
              .fontWeight(FontWeight.Bold)
            Text('已售')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(`${petFindGoods(this.selGoods).stock}`)
              .fontSize(18)
              .fontColor(PET_C.green)
              .fontWeight(FontWeight.Bold)
            Text('库存')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .backgroundColor('#F4FAF7')
        .borderRadius(14)
        .margin({ top: 14 })

        Row() {
          Text('立即购买')
            .fontSize(14)
            .fontColor(PET_C.white)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .backgroundColor(PET_C.primary)
            .borderRadius(22)
            .onClick(() => {
              this.detailShow = false;
              this.buyShow = true;
            })
          Text('加入购物车')
            .fontSize(14)
            .fontColor(PET_C.mintDark)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .border({ width: 1, color: PET_C.mint })
            .borderRadius(22)
            .margin({ left: 10 })
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buyModal() {
    Stack() {
      Column() {
        Row() {
          Text('确认订单')
            .fontSize(18)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.buyShow = false;
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          Text('🛍️')
            .fontSize(30)
            .width(56)
            .height(56)
            .textAlign(TextAlign.Center)
            .backgroundColor('#EAFBF3')
            .borderRadius(14)
          Column() {
            Text(petFindGoods(this.selGoods).name)
              .fontSize(15)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('数量 x 1 · 默认地址')
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F7FDFA')
        .borderRadius(14)
        .margin({ top: 14 })

        Row() {
          Text('商品金额')
            .fontSize(13)
            .fontColor(PET_C.textSub)
          Text(`¥${petFindGoods(this.selGoods).price}`)
            .fontSize(14)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 14 })

        Row() {
          Text('运费')
            .fontSize(13)
            .fontColor(PET_C.textSub)
          Text('免运费')
            .fontSize(13)
            .fontColor(PET_C.green)
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 8 })

        Row() {
          Text('合计')
            .fontSize(13)
            .fontColor(PET_C.textSub)
          Text(`¥${petFindGoods(this.selGoods).price}`)
            .fontSize(20)
            .fontColor(PET_C.primary)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 10 })

        Text('提交订单')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 18 })
          .onClick(() => {
            this.buyShow = false;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        // 头部
        Column() {
          Row() {
            Text('🛍️ 宠物商城')
              .fontSize(20)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
            Text('全场满300减50')
              .fontSize(12)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
              .width(96)
              .height(30)
              .backgroundColor(PET_C.primary)
              .borderRadius(15)
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .linearGradient({ colors: [['#FFB39A', 0], ['#FF8C69', 1]] })
        .borderRadius({ bottomLeft: 22, bottomRight: 22 })

        // 销量 Stack 进度条图表
        Column() {
          Row() {
            Text('📊 品类销量')
              .fontSize(14)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('实时数据')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)

          ForEach(['主粮', '玩具', '器具', '清洁', '户外', '家居', '零食', '护理'], (c: string, i: number) => {
            Row() {
              Text(c)
                .fontSize(12)
                .fontColor(PET_C.textSub)
                .width(44)
              Stack() {
                Row() {
                  Row()
                    .width(`${Math.min(petGoodsList(c).length * 12, 100)}%`)
                    .height(9)
                    .borderRadius(5)
                    .backgroundColor(i % 2 === 0 ? PET_C.primary : PET_C.mintDark)
                }
                .width('100%')
                .height(9)
                .backgroundColor('#EDF6F1')
                .borderRadius(5)
              }
              .layoutWeight(1)
              Text(`${petGoodsList(c).length}`)
                .fontSize(11)
                .fontColor(PET_C.primary)
                .width(40)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .height(26)
          }, (c: string) => 'cat' + c)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(PET_C.card)
        .borderRadius(16)
        .margin({ left: 14, right: 14, top: 12 })

        // 分类筛选
        Row() {
          Scroll() {
            Row() {
              ForEach(['全部', '主粮', '玩具', '器具', '清洁', '户外', '家居', '零食', '护理'], (c: string) => {
                Text(c)
                  .fontSize(12)
                  .fontColor(this.selCat === c ? PET_C.white : PET_C.textMain)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .backgroundColor(this.selCat === c ? PET_C.primary : PET_C.card)
                  .borderRadius(16)
                  .margin({ right: 8 })
                  .onClick(() => {
                    this.selCat = c;
                  })
              }, (c: string) => c)
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
        }
        .width('100%')
        .padding({ top: 12, bottom: 4 })

        Scroll() {
          Column() {
            ForEach(petGoodsList(this.selCat), (item: PetGoods, idx: number) => {
              Row() {
                Text(item.cat === '玩具' ? '🧸' : (item.cat === '主粮' ? '🍚' : '🛍️'))
                  .fontSize(28)
                  .width(64)
                  .height(64)
                  .textAlign(TextAlign.Center)
                  .backgroundColor('#EAFBF3')
                  .borderRadius(14)

                Column() {
                  Row() {
                    Text(item.name)
                      .fontSize(14)
                      .fontColor(PET_C.textMain)
                      .fontWeight(FontWeight.Bold)
                    Text(item.tag)
                      .fontSize(10)
                      .fontColor(PET_C.white)
                      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                      .backgroundColor(petTagColor(item.tag))
                      .borderRadius(8)
                      .margin({ left: 8 })
                  }
                  .width('100%')

                  Text(item.desc)
                    .fontSize(11)
                    .fontColor(PET_C.textSub)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .width('100%')
                    .margin({ top: 4 })

                  Row() {
                    Text(`¥${item.price}`)
                      .fontSize(16)
                      .fontColor(PET_C.primary)
                      .fontWeight(FontWeight.Bold)
                    Text(`已售 ${item.sales}`)
                      .fontSize(11)
                      .fontColor(PET_C.gold)
                      .margin({ left: 8 })
                    Text('详情 >')
                      .fontSize(12)
                      .fontColor(PET_C.mintDark)
                      .fontWeight(FontWeight.Bold)
                      .margin({ left: 8 })
                  }
                  .width('100%')
                  .margin({ top: 6 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 12 })
              }
              .width('100%')
              .padding(12)
              .backgroundColor(PET_C.card)
              .borderRadius(16)
              .margin({ bottom: 10 })
              .onClick(() => {
                this.selGoods = item.name;
                this.detailShow = true;
              })
            }, (item: PetGoods) => item.name)
          }
          .width('100%')
          .padding({ left: 14, right: 14, top: 8, bottom: 20 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.Top)
      }
      .width('100%')
      .height('100%')

      if (this.detailShow) {
        Column() {
          this.modalOverlay()
          this.detailModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.buyShow) {
        Column() {
          this.modalOverlay()
          this.buyModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

function petFindBeauty(n: string): PetBeauty {
  for (let i = 0; i < PET_BEAUTY.length; i++) {
    if (PET_BEAUTY[i].name === n) {
      return PET_BEAUTY[i];
    }
  }
  return PET_BEAUTY[0];
}

@Component
struct PetBeautyTab {
  @State selBeauty: string = '';
  @State detailShow: boolean = false;
  @State bookShow: boolean = false;
  @State hourglass: number = 0;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.hourglass = this.hourglass + 6;
    }, 400);
  }

  aboutToDisappear(): void {
    clearInterval(this.timer);
  }

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.detailShow = false;
        this.bookShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  detailModal() {
    Stack() {
      Column() {
        Row() {
          Text('💇')
            .fontSize(38)
          Column() {
            Text(petFindBeauty(this.selBeauty).name)
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text(petFindBeauty(this.selBeauty).style + ' · ' + petFindBeauty(this.selBeauty).time)
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')

        Text(petFindBeauty(this.selBeauty).desc)
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
          .lineHeight(20)

        Row() {
          Column() {
            Text(`¥${petFindBeauty(this.selBeauty).price}`)
              .fontSize(20)
              .fontColor(PET_C.primary)
              .fontWeight(FontWeight.Bold)
            Text('价格')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(`${petFindBeauty(this.selBeauty).score}`)
              .fontSize(20)
              .fontColor(PET_C.gold)
              .fontWeight(FontWeight.Bold)
            Text('好评率')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(petFindBeauty(this.selBeauty).time)
              .fontSize(16)
              .fontColor(PET_C.mintDark)
              .fontWeight(FontWeight.Bold)
            Text('耗时')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .backgroundColor('#F4FAF7')
        .borderRadius(14)
        .margin({ top: 14 })

        // 沙漏动画
        Row() {
          Text('⏳')
            .fontSize(28)
            .rotate({ angle: this.hourglass % 360 })
            .animation({ duration: 800, curve: Curve.EaseInOut })
          Text('高级美容师排班确认中')
            .fontSize(12)
            .fontColor(PET_C.textSub)
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .margin({ top: 14 })

        Text('立即预约')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.primary)
          .borderRadius(23)
          .margin({ top: 16 })
          .onClick(() => {
            this.detailShow = false;
            this.bookShow = true;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  bookModal() {
    Stack() {
      Column() {
        Row() {
          Text('预约美容')
            .fontSize(18)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.bookShow = false;
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Text(petFindBeauty(this.selBeauty).name)
          .fontSize(15)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .margin({ top: 14 })

        Text('选择门店')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
        Row() {
          ForEach(['旗舰店', '城市店', '社区店'], (s: string, i: number) => {
            Text(s)
              .fontSize(13)
              .fontColor(i === 0 ? PET_C.white : PET_C.textMain)
              .textAlign(TextAlign.Center)
              .layoutWeight(1)
              .height(38)
              .backgroundColor(i === 0 ? PET_C.primary : '#F4FAF7')
              .borderRadius(10)
              .margin({ right: i < 2 ? 8 : 0 })
          }, (s: string) => s)
        }
        .width('100%')
        .margin({ top: 8 })

        Text('预约时间')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
        Text('明天 10:30 · 宠物需提前 10 分钟到店')
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .width('100%')
          .height(44)
          .padding({ left: 14 })
          .backgroundColor('#F4FAF7')
          .borderRadius(10)
          .margin({ top: 8 })

        Text('确认预约')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 20 })
          .onClick(() => {
            this.bookShow = false;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('88%')
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        // 头部
        Column() {
          Row() {
            Text('💇 宠物美容院')
              .fontSize(20)
              .fontColor(PET_C.white)
              .fontWeight(FontWeight.Bold)
            Text('星级美容师 · 上门服务')
              .fontSize(12)
              .fontColor('#FFE9DE')
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .linearGradient({ colors: [['#FFA98C', 0], ['#FF8C69', 0.7], ['#F26B45', 1]] })
        .borderRadius({ bottomLeft: 22, bottomRight: 22 })

        Row() {
          Text('⭐ 口碑榜')
            .fontSize(14)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('按好评率排序')
            .fontSize(11)
            .fontColor(PET_C.textHint)
            .margin({ left: 8 })
        }
        .width('100%')
        .padding({ left: 16, top: 16 })
        .justifyContent(FlexAlign.SpaceBetween)

        Scroll() {
          Column() {
            ForEach(PET_BEAUTY, (item: PetBeauty, idx: number) => {
              Row() {
                Text(`${idx + 1}`)
                  .fontSize(15)
                  .fontColor(idx < 3 ? PET_C.primary : PET_C.textHint)
                  .fontWeight(FontWeight.Bold)
                  .width(26)
                Column() {
                  Row() {
                    Text(item.name)
                      .fontSize(15)
                      .fontColor(PET_C.textMain)
                      .fontWeight(FontWeight.Bold)
                    Text(item.style)
                      .fontSize(10)
                      .fontColor(PET_C.white)
                      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                      .backgroundColor(PET_C.mintDark)
                      .borderRadius(8)
                      .margin({ left: 8 })
                  }
                  .width('100%')
                  Text(item.desc)
                    .fontSize(11)
                    .fontColor(PET_C.textSub)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .width('100%')
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 8 })

                Column() {
                  Text(`¥${item.price}`)
                    .fontSize(16)
                    .fontColor(PET_C.primary)
                    .fontWeight(FontWeight.Bold)
                  Text(`好评 ${item.score}`)
                    .fontSize(11)
                    .fontColor(PET_C.gold)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.End)
                .margin({ left: 8 })
              }
              .width('100%')
              .padding(12)
              .backgroundColor(PET_C.card)
              .borderRadius(16)
              .margin({ bottom: 10 })
              .onClick(() => {
                this.selBeauty = item.name;
                this.detailShow = true;
              })
            }, (item: PetBeauty) => item.name)

            Text('— 金牌美容师均持证上岗 —')
              .fontSize(12)
              .fontColor(PET_C.textHint)
              .width('100%')
              .textAlign(TextAlign.Center)
              .margin({ top: 4, bottom: 20 })
          }
          .width('100%')
          .padding({ left: 14, right: 14, top: 10 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.Top)
      }
      .width('100%')
      .height('100%')

      if (this.detailShow) {
        Column() {
          this.modalOverlay()
          this.detailModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.bookShow) {
        Column() {
          this.modalOverlay()
          this.bookModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

function petFindEvent(n: string): PetEvent {
  for (let i = 0; i < PET_EVENTS.length; i++) {
    if (PET_EVENTS[i].name === n) {
      return PET_EVENTS[i];
    }
  }
  return PET_EVENTS[0];
}

function petEventList(status: string): PetEvent[] {
  if (status === '全部') {
    return PET_EVENTS;
  }
  return PET_EVENTS.filter((e: PetEvent) => e.status === status);
}

@Component
struct PetEventTab {
  @State selStatus: string = '全部';
  @State selEvent: string = '';
  @State detailShow: boolean = false;
  @State joinShow: boolean = false;

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.detailShow = false;
        this.joinShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  detailModal() {
    Stack() {
      Column() {
        Row() {
          Text('🎪')
            .fontSize(36)
          Column() {
            Text(petFindEvent(this.selEvent).name)
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text(petFindEvent(this.selEvent).city + ' · ' + petFindEvent(this.selEvent).date)
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.detailShow = false;
            })
        }
        .width('100%')

        Row() {
          Column() {
            Text(`${petFindEvent(this.selEvent).people}`)
              .fontSize(20)
              .fontColor(PET_C.primary)
              .fontWeight(FontWeight.Bold)
            Text('预计人数')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(petFindEvent(this.selEvent).mode)
              .fontSize(14)
              .fontColor(PET_C.mintDark)
              .fontWeight(FontWeight.Bold)
            Text('活动形式')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          Column() {
            Text(petFindEvent(this.selEvent).status)
              .fontSize(14)
              .fontColor(petStatusColor(petFindEvent(this.selEvent).status))
              .fontWeight(FontWeight.Bold)
            Text('报名状态')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .backgroundColor('#F4FAF7')
        .borderRadius(14)
        .margin({ top: 14 })

        Text(petFindEvent(this.selEvent).desc)
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
          .lineHeight(20)

        Text('温馨提示:请为宠物佩戴牵引绳,携带免疫证明入场,现场提供饮水与宠物急救包。')
          .fontSize(12)
          .fontColor(PET_C.textHint)
          .width('100%')
          .margin({ top: 12 })
          .lineHeight(19)

        Text('报名参加')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.primary)
          .borderRadius(23)
          .margin({ top: 18 })
          .onClick(() => {
            this.detailShow = false;
            this.joinShow = true;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  joinModal() {
    Stack() {
      Column() {
        Text('🎉')
          .fontSize(46)
        Text('报名成功')
          .fontSize(19)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 12 })
        Text(petFindEvent(this.selEvent).name + ' · 记得带上毛孩子')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .margin({ top: 8 })
        Text('活动前 1 天推送入场码')
          .fontSize(12)
          .fontColor(PET_C.textHint)
          .margin({ top: 6 })
        Text('好的')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 22 })
          .onClick(() => {
            this.joinShow = false;
          })
      }
      .width('82%')
      .padding(24)
      .backgroundColor(PET_C.card)
      .borderRadius(22)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        // 头部
        Column() {
          Text('🎪 萌宠活动')
            .fontSize(20)
            .fontColor(PET_C.white)
            .fontWeight(FontWeight.Bold)
          Text('领养日 · 运动会 · 摄影展 · 科普节')
            .fontSize(12)
            .fontColor('#FFE9DE')
            .margin({ top: 6 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 16, right: 16, top: 16, bottom: 18 })
        .linearGradient({ colors: [['#F26B45', 0], ['#FF8C69', 0.7], ['#FFA98C', 1]] })

        // 状态筛选
        Row() {
          ForEach(['全部', '报名中', '即将开始', '筹备中'], (s: string) => {
            Text(s)
              .fontSize(12)
              .fontColor(this.selStatus === s ? PET_C.white : PET_C.textMain)
              .textAlign(TextAlign.Center)
              .layoutWeight(1)
              .height(34)
              .backgroundColor(this.selStatus === s ? PET_C.primary : PET_C.card)
              .borderRadius(17)
              .margin({ right: s === '筹备中' ? 0 : 8 })
              .onClick(() => {
                this.selStatus = s;
              })
          }, (s: string) => s)
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 12 })

        Scroll() {
          Column() {
            ForEach(petEventList(this.selStatus), (item: PetEvent, idx: number) => {
              Column() {
                Row() {
                  Text(item.name)
                    .fontSize(16)
                    .fontColor(PET_C.textMain)
                    .fontWeight(FontWeight.Bold)
                  Text(item.status)
                    .fontSize(11)
                    .fontColor(PET_C.white)
                    .padding({ left: 10, right: 10, top: 3, bottom: 3 })
                    .backgroundColor(petStatusColor(item.status))
                    .borderRadius(10)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)

                Row() {
                  Text('📍 ' + item.city)
                    .fontSize(12)
                    .fontColor(PET_C.textSub)
                  Text('📅 ' + item.date)
                    .fontSize(12)
                    .fontColor(PET_C.textSub)
                    .margin({ left: 14 })
                  Text('👥 ' + item.people + '人')
                    .fontSize(12)
                    .fontColor(PET_C.textSub)
                    .margin({ left: 14 })
                }
                .width('100%')
                .margin({ top: 8 })

                Text(item.desc)
                  .fontSize(12)
                  .fontColor(PET_C.textSub)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width('100%')
                  .margin({ top: 8 })
                  .lineHeight(18)

                Row() {
                  Text(item.mode)
                    .fontSize(12)
                    .fontColor(PET_C.mintDark)
                    .fontWeight(FontWeight.Bold)
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                    .backgroundColor('#EAFBF3')
                    .borderRadius(12)
                  Text('查看详情 >')
                    .fontSize(13)
                    .fontColor(PET_C.primary)
                    .fontWeight(FontWeight.Bold)
                    .layoutWeight(1)
                    .textAlign(TextAlign.End)
                }
                .width('100%')
                .margin({ top: 10 })
              }
              .width('100%')
              .padding(14)
              .backgroundColor(PET_C.card)
              .borderRadius(16)
              .margin({ bottom: 10 })
              .onClick(() => {
                this.selEvent = item.name;
                this.detailShow = true;
              })
            }, (item: PetEvent) => item.name)
          }
          .width('100%')
          .padding({ left: 14, right: 14, top: 10, bottom: 20 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.Top)
      }
      .width('100%')
      .height('100%')

      if (this.detailShow) {
        Column() {
          this.modalOverlay()
          this.detailModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.joinShow) {
        Column() {
          this.modalOverlay()
          this.joinModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

function petFindOrder(n: string): PetOrder {
  for (let i = 0; i < PET_ORDERS.length; i++) {
    if (PET_ORDERS[i].no === n) {
      return PET_ORDERS[i];
    }
  }
  return PET_ORDERS[0];
}

@Component
struct PetMineTab {
  @State profileShow: boolean = false;
  @State orderShow: boolean = false;
  @State selOrder: string = '';
  @State delShow: boolean = false;
  @State delName: string = '';
  @State favList: PetFav[] = PET_FAVS;

  @Builder
  modalOverlay() {
    Column() {
      Column() {
        Text('')
          .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
      .onClick(() => {
        this.profileShow = false;
        this.orderShow = false;
        this.delShow = false;
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  profileModal() {
    Stack() {
      Column() {
        Row() {
          Text('👤')
            .fontSize(44)
          Column() {
            Text('毛孩子的铲屎官')
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('Lv.8 资深铲屎官')
              .fontSize(12)
              .fontColor(PET_C.primary)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.profileShow = false;
            })
        }
        .width('100%')

        Row() {
          Column() {
            Text('2')
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('我的毛孩子')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 3 })
          }
          .layoutWeight(1)
          Column() {
            Text('18')
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('收藏好物')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 3 })
          }
          .layoutWeight(1)
          Column() {
            Text('3260')
              .fontSize(18)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('积分')
              .fontSize(11)
              .fontColor(PET_C.textHint)
              .margin({ top: 3 })
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .backgroundColor('#F4FAF7')
        .borderRadius(14)
        .margin({ top: 16 })

        Text('我的宠物宣言')
          .fontSize(13)
          .fontColor(PET_C.textSub)
          .width('100%')
          .margin({ top: 14 })
        Text('一旦选择,一生负责 🐾')
          .fontSize(14)
          .fontColor(PET_C.textMain)
          .width('100%')
          .height(44)
          .padding({ left: 14 })
          .backgroundColor('#F4FAF7')
          .borderRadius(10)
          .margin({ top: 8 })

        Text('保存资料')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.primary)
          .borderRadius(23)
          .margin({ top: 20 })
          .onClick(() => {
            this.profileShow = false;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('88%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  orderModal() {
    Stack() {
      Column() {
        Row() {
          Text('订单详情')
            .fontSize(18)
            .fontColor(PET_C.textMain)
            .fontWeight(FontWeight.Bold)
          Text('✕')
            .fontSize(16)
            .fontColor(PET_C.textHint)
            .onClick(() => {
              this.orderShow = false;
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Text('订单号:' + petFindOrder(this.selOrder).no)
          .fontSize(12)
          .fontColor(PET_C.textHint)
          .width('100%')
          .margin({ top: 14 })

        Row() {
          Text('🛍️')
            .fontSize(30)
            .width(56)
            .height(56)
            .textAlign(TextAlign.Center)
            .backgroundColor('#EAFBF3')
            .borderRadius(14)
          Column() {
            Text(petFindOrder(this.selOrder).name)
              .fontSize(15)
              .fontColor(PET_C.textMain)
              .fontWeight(FontWeight.Bold)
            Text('数量 x ' + petFindOrder(this.selOrder).cnt)
              .fontSize(12)
              .fontColor(PET_C.textSub)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text(`¥${petFindOrder(this.selOrder).amount}`)
            .fontSize(17)
            .fontColor(PET_C.primary)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F7FDFA')
        .borderRadius(14)
        .margin({ top: 12 })

        Row() {
          Text('下单时间')
            .fontSize(13)
            .fontColor(PET_C.textSub)
          Text(petFindOrder(this.selOrder).date)
            .fontSize(13)
            .fontColor(PET_C.textMain)
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 14 })

        Row() {
          Text('订单状态')
            .fontSize(13)
            .fontColor(PET_C.textSub)
          Text(petFindOrder(this.selOrder).status)
            .fontSize(13)
            .fontColor(petOrderColor(petFindOrder(this.selOrder).status))
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 10 })

        Text('确认')
          .fontSize(15)
          .fontColor(PET_C.white)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(46)
          .backgroundColor(PET_C.mintDark)
          .borderRadius(23)
          .margin({ top: 20 })
          .onClick(() => {
            this.orderShow = false;
          })
      }
      .padding(20)
      .backgroundColor(PET_C.card)
      .borderRadius(20)
      .width('90%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  delModal() {
    Stack() {
      Column() {
        Text('💔')
          .fontSize(44)
        Text('删除收藏?')
          .fontSize(18)
          .fontColor(PET_C.textMain)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 10 })
        Text('将移除「' + this.delName + '」')
          .fontSize(12)
          .fontColor(PET_C.textSub)
          .margin({ top: 8 })
        Row() {
          Text('再想想')
            .fontSize(14)
            .fontColor(PET_C.textMain)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .backgroundColor('#F0F7F4')
            .borderRadius(22)
            .onClick(() => {
              this.delShow = false;
            })
          Text('确认删除')
            .fontSize(14)
            .fontColor(PET_C.white)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .layoutWeight(1)
            .height(44)
            .backgroundColor(PET_C.danger)
            .borderRadius(22)
            .margin({ left: 12 })
            .onClick(() => {
              this.favList = this.favList.filter((o: PetFav, oi: number) => oi !== this.favList.findIndex((f: PetFav) => f.name === this.delName));
              this.delShow = false;
            })
        }
        .width('100%')
        .margin({ top: 20 })
      }
      .width('82%')
      .padding(24)
      .backgroundColor(PET_C.card)
      .borderRadius(22)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        // 头部资料卡
        Column() {
          Row() {
            Column() {
              Text('👤')
                .fontSize(40)
            }
            .width(64)
            .height(64)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#EAFBF3')
            .borderRadius(32)

            Column() {
              Text('毛孩子的铲屎官')
                .fontSize(18)
                .fontColor(PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text('Lv.8 资深铲屎官 · 积分 3260')
                .fontSize(12)
                .fontColor(PET_C.textSub)
                .margin({ top: 5 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 14 })

            Text('编辑')
              .fontSize(12)
              .fontColor(PET_C.white)
              .textAlign(TextAlign.Center)
              .width(56)
              .height(30)
              .backgroundColor(PET_C.primary)
              .borderRadius(15)
              .onClick(() => {
                this.profileShow = true;
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 18, bottom: 18 })
        .backgroundColor(PET_C.card)

        // 数据卡片
        Row() {
          ForEach(['毛孩子 2', '收藏 18', '订单 26', '活动 5'], (d: string, i: number) => {
            Column() {
              Text(d.split(' ')[0])
                .fontSize(16)
                .fontColor(i === 0 ? PET_C.primary : PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text(d.split(' ')[1])
                .fontSize(10)
                .fontColor(PET_C.textHint)
                .margin({ top: 3 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(PET_C.bg)
            .borderRadius(14)
            .margin({ right: i < 3 ? 8 : 0 })
          }, (d: string) => d)
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 14 })

        Scroll() {
          Column() {
            // 订单列表
            Row() {
              Text('📦 我的订单')
                .fontSize(16)
                .fontColor(PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text('全部订单 >')
                .fontSize(12)
                .fontColor(PET_C.textHint)
            }
            .width('100%')
            .padding({ top: 16, bottom: 8 })
            .justifyContent(FlexAlign.SpaceBetween)

            ForEach(PET_ORDERS, (item: PetOrder, idx: number) => {
              Row() {
                Text('📦')
                  .fontSize(24)
                  .width(52)
                  .height(52)
                  .textAlign(TextAlign.Center)
                  .backgroundColor('#EAFBF3')
                  .borderRadius(12)
                Column() {
                  Text(item.name)
                    .fontSize(14)
                    .fontColor(PET_C.textMain)
                    .fontWeight(FontWeight.Bold)
                  Text(item.date + ' · 共 ' + item.cnt + ' 件')
                    .fontSize(11)
                    .fontColor(PET_C.textHint)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })
                Column() {
                  Text(`¥${item.amount}`)
                    .fontSize(14)
                    .fontColor(PET_C.primary)
                    .fontWeight(FontWeight.Bold)
                  Text(item.status)
                    .fontSize(10)
                    .fontColor(petOrderColor(item.status))
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')
              .padding(10)
              .backgroundColor(PET_C.card)
              .borderRadius(14)
              .margin({ bottom: 8 })
              .onClick(() => {
                this.selOrder = item.no;
                this.orderShow = true;
              })
            }, (item: PetOrder) => item.no)

            // 收藏列表
            Row() {
              Text('❤️ 我的收藏')
                .fontSize(16)
                .fontColor(PET_C.textMain)
                .fontWeight(FontWeight.Bold)
              Text(`${this.favList.length} 件收藏`)
                .fontSize(12)
                .fontColor(PET_C.textHint)
            }
            .width('100%')
            .padding({ top: 16, bottom: 8 })
            .justifyContent(FlexAlign.SpaceBetween)

            ForEach(this.favList, (item: PetFav, idx: number) => {
              Row() {
                Text(item.cat === '宠物' ? '🐾' : (item.cat === '美容' ? '💇' : '🛍️'))
                  .fontSize(24)
                  .width(52)
                  .height(52)
                  .textAlign(TextAlign.Center)
                  .backgroundColor('#EAFBF3')
                  .borderRadius(12)
                Column() {
                  Text(item.name)
                    .fontSize(14)
                    .fontColor(PET_C.textMain)
                    .fontWeight(FontWeight.Bold)
                  Row() {
                    Text(`¥${item.price}`)
                      .fontSize(13)
                      .fontColor(PET_C.primary)
                      .fontWeight(FontWeight.Bold)
                    Text(`热度 ${item.hot}`)
                      .fontSize(11)
                      .fontColor(PET_C.gold)
                      .margin({ left: 8 })
                  }
                  .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })
                Text('💔')
                  .fontSize(18)
                  .onClick(() => {
                    this.delName = item.name;
                    this.delShow = true;
                  })
              }
              .width('100%')
              .padding(10)
              .backgroundColor(PET_C.card)
              .borderRadius(14)
              .margin({ bottom: 8 })
            }, (item: PetFav) => item.name)

            Text('— 领养代替购买,给它一个家 —')
              .fontSize(12)
              .fontColor(PET_C.textHint)
              .width('100%')
              .textAlign(TextAlign.Center)
              .margin({ top: 6, bottom: 20 })
          }
          .width('100%')
          .padding({ left: 14, right: 14 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.Top)
      }
      .width('100%')
      .height('100%')

      if (this.profileShow) {
        Column() {
          this.modalOverlay()
          this.profileModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.orderShow) {
        Column() {
          this.modalOverlay()
          this.orderModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }

      if (this.delShow) {
        Column() {
          this.modalOverlay()
          this.delModal()
        }
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
        .zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}



在这里插入图片描述

总结

本文深入剖析了一个基于HarmonyOS ArkTS构建的宠物潮流生活馆应用,该应用以六Tab导航架构为骨架,将宠物领养、用品商城、美容服务、社区活动和个人中心五大业务域拆解为五个独立组件,配合首页共六个Tab模块,实现了业务逻辑的清晰隔离与状态管理的独立自治。应用采用薄荷绿与珊瑚橙双主题配色方案,通过PET_C设计令牌对象集中管理16个色值,实现了主题色与组件代码的彻底解耦,具备良好的主题切换扩展能力。

在技术实现层面,该应用展现了ArkTS声明式UI开发的多项核心能力。数据层通过九个interface接口定义了严格的类型约束,配合const静态数组和纯函数工具方法,构建了类型安全、逻辑清晰的数据处理体系。组件层充分利用@State状态管理驱动视图更新,配合@Builder方法抽取可复用UI片段,通过if条件渲染和ForEach列表渲染构建了层次分明的界面结构。动画层通过setInterval定时器驱动状态变量变化,配合scale、rotate变换和animation动画属性,实现了呼吸、粒子掉落、爪印摇摆、沙漏旋转四种轻量级动画效果,无需引入第三方动画库即可满足丰富的动态交互需求。

采用"modalOverlay遮罩层 + 具体弹窗内容 + zIndex层级管理"的三段式架构,通过布尔状态变量控制显隐,点击遮罩或按钮即可关闭,交互逻辑清晰统一。从领养登记到订单确认、从美容预约到收藏删除,每个业务流程都形成了"列表点击——详情查看——操作确认"的完整三级交互链路。总体而言,该应用完整地展示了HarmonyOS ArkTS在多模块应用架构、状态管理、动画交互和弹窗体系等方面的工程实践,为鸿蒙原生应用开发提供了具有参考价值的实现范式。通过类型系统约束数据、状态驱动视图、定时器驱动动画、条件渲染管理弹窗这四大技术支柱,开发者能够在HarmonyOS 6.1.1平台上构建出功能完整、交互丰富、架构清晰的原生应用。

Logo

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

更多推荐