基于HarmonyOS API 24的ArkTS宠物潮流生活馆六Tab架构声明式UI动画交互与弹窗体系深度解析——HarmonyOS 6.1.1声明式UI实战
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省略号截断,防止长名称破坏布局。
首页弹窗体系:遮罩层与模态对话框
首页集成了两个弹窗:领养登记弹窗和新人礼包弹窗。两者均采用统一的三段式结构: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平台上构建出功能完整、交互丰富、架构清晰的原生应用。
更多推荐


所有评论(0)