HarmonyOS 6 ArkUI - selectedItem采用可空类型GoodsItem | null,用于在各Tab页面与详情弹窗之间传递选中数据
引言

在当今移动互联网与智能座驾深度融合的时代,汽车后市场服务正经历着前所未有的数字化转型。汽车贴膜改色作为后市场服务中高频且高客单价的业务品类,其线上化平台的设计与实现质量直接决定了用户的消费决策效率与服务体验。本文将深入剖析一款基于HarmonyOS ArkTS声明式UI框架构建的汽车贴膜改色工坊应用页面,该页面涵盖了改色集市、隐形车衣、隔热膜、施工门店、案例秀、消息中心及个人中心七大功能模块,通过单文件结构实现了完整的业务闭环。
从技术架构层面来看,该应用采用了HarmonyOS ArkTS的声明式编程范式,利用@Entry和@Component装饰器构建了标准入口组件结构。整个页面通过@State状态驱动机制实现了响应式数据流,包含当前Tab索引、各类弹窗显隐状态、选中商品对象、芯片选择索引、步进器数值及输入文本等十余个状态变量。这种集中式的状态管理模式使得UI层与数据层之间形成了清晰的绑定关系,任何状态变更都能自动触发对应视图的精确刷新,避免了命令式编程中手动操作DOM带来的性能开销与维护成本。
在业务设计层面,该应用围绕"选膜-比价-预约-施工-分享"这一核心链路展开,精心设计了24款改色膜、12款隐形车衣、10款隔热膜、10家施工门店、10个案例帖及12条消息通知的数据模型。每个数据项都包含名称、价格、描述、评分、数量、标签和热度七个维度,通过统一的GoodsItem接口定义实现了数据结构的一致性。应用在视觉设计上采用了霓虹紫(#8E24AA)、钛银、炭黑、浅紫底(#F3E5F5)和荧光青(#00BFA5)的配色方案,营造出科技感与潮流感并存的视觉氛围,精准契合了汽车改色这一个性化消费场景的用户审美预期。
数据模型与状态定义

应用首先定义了一个全局的数据接口GoodsItem,作为所有商品列表的统一数据契约。随后在主组件中声明了丰富的状态变量和业务数据源,这些构成了整个页面的数据基座。
interface GoodsItem {
name: string
price: number
desc: string
score: number
num: number
tag: string
hot: string
}
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showBuyModal: boolean = false
@State showShopModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedItem: GoodsItem | null = null
@State chipA: number = 0
@State chipB: number = 0
@State stepNum: number = 1
@State phoneText: string = ''
private tabs: string[] = ['改色集市', '隐形车衣', '隔热膜', '施工门店', '案例秀', '消息', '我的']
private colorChips: string[] = ['钢琴黑', '电光金属', '液态银丝', '哑光军绿', '宝石蓝']
private partChips: string[] = ['全车', '车顶', '引擎盖', '后视镜']
private specChips: string[] = ['前杠版', '半车版', '全车版']
private storeChips: string[] = ['朝阳旗舰店', '海淀店', '丰台店', '通州店']
private timeChips: string[] = ['上午', '下午', '晚间']
private topicChips: string[] = ['全站', '电光金属', '哑光系列', '碳纤维纹', '婚车白', '复古绿', '荧光色']
private applyChips: string[] = ['全车', '车顶', '引擎盖', '后视镜', '门把手', '内饰件']
private filmColors: string[] = ['#1A1A1A', '#42A5F5', '#EC407A', '#00BFA5', '#FFD600', '#5E35B1', '#AB47BC', '#546E7A', '#FF7043', '#26C6DA', '#EF5350', '#66BB6A']
private avatars: string[] = ['🚗', '🏎', '🚙', '🚕', '🚓', '🚗', '🏎', '🚙', '🚕', '🚓', '🚗', '🏎']
GoodsItem接口定义了七个字段:name为商品名称,price为价格,desc为描述信息,score为评分或热度百分比,num根据不同列表语义各异(如热度值、厚度微米、隔热率百分比、距离公里数、点赞数等),tag为标签,hot为热度标记。这种复用同一接口的设计策略既保证了代码结构的一致性,也带来了一定的灵活性挑战——不同列表中num字段的语义差异需要在渲染逻辑中做区分处理。
状态变量方面,currentTab控制当前展示的Tab页面,五个show*Modal布尔值分别控制五种弹窗的显隐。selectedItem采用可空类型GoodsItem | null,用于在各Tab页面与详情弹窗之间传递选中数据。chipA和chipB作为通用选择索引,被多个弹窗复用来记录当前选中的芯片项。stepNum为步进器数值,phoneText绑定输入框文本。这些状态变量的设计体现了"最小状态集"原则,通过复用而非冗余声明来控制状态复杂度。
商品数据源:改色膜与隐形车衣

改色膜列表是应用的核心商品数据,包含了24款不同色彩与材质的改色膜产品,覆盖了从入门到高端的完整价格带。
// 改色膜 24 条
private mainList: GoodsItem[] = [
{ name: '电光金属 · 液态银丝蓝', price: 6800, desc: '进口 TPU 基材 · 5 年质保', score: 96, num: 452, tag: '热销', hot: '爆' },
{ name: '钢琴黑 · 哑光曜石', price: 5200, desc: '日本铸级膜 · 哑光低反射', score: 94, num: 388, tag: '经典', hot: '黑' },
{ name: '宝石蓝 · 炫彩幻变', price: 7500, desc: '多层镀膜 · 变色效果', score: 92, num: 341, tag: '新品', hot: '蓝' },
{ name: '哑光军绿 · 战地迷彩', price: 6300, desc: '军工级 PET · 抗刮', score: 90, num: 275, tag: '潮流', hot: '绿' },
{ name: '液态金属银 · 流光溢彩', price: 8800, desc: '纳米电镀 · 高镜面', score: 97, num: 501, tag: '奢选', hot: '银' },
{ name: '火焰红 · 法拉利同款', price: 5900, desc: '高饱和色浆 · 持久', score: 95, num: 415, tag: '热销', hot: '红' },
{ name: '翡翠绿 · 皇家绿改', price: 6600, desc: '进口基材 · 4 年质保', score: 91, num: 233, tag: '个性', hot: '绿' },
{ name: '香槟金 · 商务轻奢', price: 6100, desc: '金属颗粒 · 细腻', score: 89, num: 198, tag: '稳重', hot: '金' },
{ name: '哑光灰 · 纳多灰同款', price: 5400, desc: '进口哑光 · 抗污', score: 93, num: 362, tag: '热销', hot: '灰' },
{ name: '电光紫 · 霓虹幻影', price: 7200, desc: '电光系列 · 夜间流光', score: 96, num: 429, tag: '潮流', hot: '紫' },
{ name: '深海蓝 · 布鲁克林蓝', price: 5700, desc: '双拼色可选 · 耐候', score: 88, num: 175, tag: '经典', hot: '蓝' },
{ name: '岩石灰 · 水泥灰改色', price: 5000, desc: '国产优质 · 性价比', score: 90, num: 287, tag: '实惠', hot: '灰' },
{ name: '落日橙 · 撼路者橙', price: 6400, desc: '高亮色泽 · 防晒', score: 92, num: 312, tag: '个性', hot: '橙' },
{ name: '薄荷绿 · 清新马卡龙', price: 6200, desc: '低饱和色系 · 温和', score: 87, num: 154, tag: '新品', hot: '绿' },
{ name: '曜石黑 · 亮面高黑', price: 5300, desc: '经典黑膜 · 万能款', score: 95, num: 401, tag: '热销', hot: '黑' },
{ name: '玫瑰金 · 粉调金属', price: 6900, desc: '渐变金属 · 女神款', score: 91, num: 219, tag: '潮流', hot: '粉' },
{ name: '冰川蓝 · 冷调金属', price: 7100, desc: '冰感涂层 · 隔热', score: 94, num: 358, tag: '奢选', hot: '蓝' },
{ name: '磨砂黑 · 哑光高级感', price: 5600, desc: '磨砂工艺 · 抗指纹', score: 93, num: 377, tag: '热销', hot: '黑' },
{ name: '柠檬黄 · 警示亮黄', price: 4800, desc: '高可见度 · 防刮', score: 86, num: 132, tag: '个性', hot: '黄' },
{ name: '酒红 · 勃艮第红', price: 6700, desc: '深邃酒红 · 商务', score: 95, num: 389, tag: '经典', hot: '红' },
{ name: '碳纤维纹 · 黑色碳纤', price: 7800, desc: '仿碳纤维 · 运动风', score: 96, num: 466, tag: '高端', hot: '碳' },
{ name: '珍珠白 · 珍珠贝母', price: 5800, desc: '珍珠光泽 · 温润', score: 90, num: 241, tag: '婚车', hot: '白' },
{ name: '军械绿 · 复古绿', price: 5100, desc: '复古色调 · 文艺', score: 88, num: 168, tag: '小众', hot: '绿' },
{ name: '霓虹粉 · 荧光少女', price: 6100, desc: '荧光色系 · 夜光', score: 89, num: 187, tag: '潮流', hot: '粉' }
]
// 隐形车衣 12 条 num 为厚度微米
private ppList: GoodsItem[] = [
{ name: '龙膜 · 钻石系列', price: 12800, desc: '全车 TPU · 10 年质保', score: 97, num: 220, tag: '旗舰', hot: '钻' },
{ name: '威固 V70+', price: 11800, desc: '光学级 TPU · 抗划痕', score: 96, num: 200, tag: '旗舰', hot: '威' },
{ name: 'XPEL · 极致系列', price: 13800, desc: '美国原装 · 自修复', score: 98, num: 230, tag: '尊享', hot: 'X' },
{ name: '3M · 漆面保护膜', price: 9800, desc: '经典款 · 7 年质保', score: 93, num: 180, tag: '热销', hot: '3' },
{ name: '圣科 · 铂金系列', price: 8800, desc: '高透亮 · 5 年质保', score: 91, num: 160, tag: '实惠', hot: '圣' },
{ name: '艺卡 · ARES 系列', price: 7800, desc: '意大利基材 · 抗黄变', score: 90, num: 150, tag: '实惠', hot: '艺' },
{ name: '量子 · Q 系列', price: 10800, desc: '纳米涂层 · 疏水', score: 94, num: 190, tag: '高端', hot: '量' },
{ name: '固驰 · 哑光车衣', price: 9200, desc: '哑光车衣 · 改色一体', score: 92, num: 170, tag: '潮流', hot: '固' },
{ name: 'UPPF · 拓弗系列', price: 11200, desc: '军工级 · 高韧性', score: 95, num: 210, tag: '高端', hot: 'U' },
{ name: '诺盾 · ND-X 系列', price: 8200, desc: '性价比之选 · 5 年', score: 89, num: 140, tag: '实惠', hot: '诺' },
{ name: '康得新 · 新锐系列', price: 6800, desc: '国产优质 · 4 年质保', score: 87, num: 130, tag: '入门', hot: '康' },
{ name: '琥珀 · 光学系列', price: 10200, desc: '高清晰度 · 增亮', score: 93, num: 185, tag: '热销', hot: '琥' }
]
改色膜数据的设计颇具匠心:价格区间从4800元的入门款到8800元的奢选款,覆盖了不同消费能力的用户群体。每条数据都包含详细的材质描述(如"进口TPU基材"“日本铸级膜”“纳米电镀"等),这些描述信息在列表渲染时会直接展示给用户,帮助其快速了解产品特性。hot字段使用单字标记(如"爆”“黑”"蓝"等),在UI上会以彩色背景方块的形式呈现,既起到了视觉标识的作用,又为列表增加了色彩层次感。
隐形车衣列表则聚焦于TPU材质的漆面保护膜产品,共12款,价格从6800元到13800元不等。这里num字段的语义变更为厚度微米值(130μm-230μm),在Tab2页面中会被用于绘制柱状图对比各品牌车衣的厚度差异。这种数据复用策略虽然增加了字段语义的多义性,但有效减少了数据接口的定义数量,是声明式UI中常见的权衡设计。
隔热膜、门店与案例数据源

应用还定义了隔热膜、施工门店、案例帖和消息通知四类数据源,每类数据都有其独特的业务含义和渲染方式。
// 隔热膜 10 条 num 为隔热率百分比
private tintList: GoodsItem[] = [
{ name: '威固 VK70 前挡', price: 3280, desc: '光学膜 · 高清晰', score: 97, num: 88, tag: '顶级', hot: '挡' },
{ name: '龙膜 · 畅悦80', price: 2680, desc: '陶瓷膜 · 不屏蔽信号', score: 95, num: 80, tag: '热销', hot: '龙' },
{ name: '3M · 晶锐70', price: 2980, desc: '光学微附 · 隔热强', score: 94, num: 85, tag: '高端', hot: '晶' },
{ name: '量子 · 钻石70', price: 2380, desc: '磁控溅射 · 持久', score: 92, num: 76, tag: '热销', hot: '量' },
{ name: '圣科 · RC70', price: 2180, desc: '性价比 · 隔紫外线', score: 90, num: 72, tag: '实惠', hot: '圣' },
{ name: '强生 · 环球70', price: 1980, desc: '经典款 · 耐用', score: 89, num: 68, tag: '实惠', hot: '强' },
{ name: 'FSK · 冰钻前挡', price: 2580, desc: '日本进口 · 高透', score: 93, num: 79, tag: '高端', hot: '冰' },
{ name: '琥珀光学 · A70', price: 2780, desc: '陶瓷颗粒 · 低反光', score: 94, num: 82, tag: '高端', hot: '琥' },
{ name: '康得新 · K70', price: 1780, desc: '国产优质 · 三年质保', score: 87, num: 62, tag: '入门', hot: '康' },
{ name: '蓝光 · 前挡套装', price: 1580, desc: '入门之选 · 隔紫外线', score: 85, num: 58, tag: '入门', hot: '蓝' }
]
// 施工门店 10 条 num 为距离公里 score 为门店评分
private shopList: GoodsItem[] = [
{ name: '朝阳旗舰店 · 奥特莱斯', price: 0, desc: '朝阳区东四环中路 · 12 个施工工位', score: 96, num: 3, tag: '旗舰', hot: '朝' },
{ name: '海淀店 · 五棵松', price: 0, desc: '海淀区复兴路 · 8 个施工工位', score: 94, num: 7, tag: '直营', hot: '海' },
{ name: '丰台店 · 世界公园', price: 0, desc: '丰台区丰葆路 · 6 个施工工位', score: 92, num: 11, tag: '直营', hot: '丰' },
{ name: '通州店 · 万达广场', price: 0, desc: '通州区新华西街 · 6 个施工工位', score: 91, num: 15, tag: '直营', hot: '通' },
{ name: '昌平店 · 回龙观', price: 0, desc: '昌平区回龙观西大街 · 5 个施工工位', score: 90, num: 21, tag: '加盟', hot: '昌' },
{ name: '大兴店 · 亦庄', price: 0, desc: '大兴区荣华中路 · 7 个施工工位', score: 93, num: 18, tag: '加盟', hot: '大' },
{ name: '顺义店 · 后沙峪', price: 0, desc: '顺义区安泰大街 · 4 个施工工位', score: 89, num: 26, tag: '加盟', hot: '顺' },
{ name: '房山店 · 长阳', price: 0, desc: '房山区长阳镇 · 4 个施工工位', score: 88, num: 32, tag: '加盟', hot: '房' },
{ name: '石景山店 · 苹果园', price: 0, desc: '石景山区苹果园南路 · 5 个施工工位', score: 90, num: 24, tag: '直营', hot: '石' },
{ name: '望京店 · 酒仙桥', price: 0, desc: '朝阳区酒仙桥路 · 9 个施工工位', score: 95, num: 9, tag: '直营', hot: '望' }
]
// 案例帖子 10 条 num 为点赞数 score 为人气
private caseList: GoodsItem[] = [
{ name: '车友老王', price: 0, desc: '电光紫 Model 3 全车改色,夜间流光效果一绝,附三天施工全记录', score: 96, num: 1286, tag: '新帖', hot: '紫' },
{ name: '女神座驾Lisa', price: 0, desc: '香槟金 Mini 全车膜,回头率拉满,姐妹们冲鸭', score: 94, num: 986, tag: '精选', hot: '金' },
{ name: '玩车大叔', price: 0, desc: '哑光军绿牧马人,硬派风格首选,附洗车养护小贴士', score: 92, num: 875, tag: '热门', hot: '绿' },
{ name: '城市猎人', price: 0, desc: '碳纤维纹 GTI,运动感翻倍,全车花了四天半', score: 91, num: 764, tag: '热门', hot: '碳' },
{ name: '京郊小飞', price: 0, desc: '宝石蓝 BRZ 幻变效果实拍,阳光下会变色', score: 90, num: 652, tag: '新帖', hot: '蓝' },
{ name: '星城车痴', price: 0, desc: '钢琴黑奔驰 E 哑光高级感,商务人士低调之选', score: 93, num: 598, tag: '精选', hot: '黑' },
{ name: '半岛铁盒', price: 0, desc: '液态金属银保时捷,镜面效果像镀了一层水银', score: 95, num: 534, tag: '精选', hot: '银' },
{ name: '朝阳阿伟', price: 0, desc: '翡翠绿老甲壳虫复古改造,文艺范儿拉满', score: 88, num: 487, tag: '新帖', hot: '绿' },
{ name: '五环车神', price: 0, desc: '磨砂黑奥迪 S4 深度作业,含隐形车衣叠加方案', score: 90, num: 433, tag: '热门', hot: '黑' },
{ name: '三里屯喵', price: 0, desc: '玫瑰金甲壳虫女神座驾,粉色金属渐变太温柔', score: 89, num: 396, tag: '新帖', hot: '粉' }
]
// 消息 12 条 tag 为时间
private msgList: GoodsItem[] = [
{ name: '预约成功通知', price: 0, desc: '您的贴膜预约已确认,请在 3 月 18 日上午到店', score: 0, num: 0, tag: '2小时前', hot: '✅' },
{ name: '施工进度提醒', price: 0, desc: '您的爱车已完成表面清洁,进入贴膜工序', score: 0, num: 0, tag: '4小时前', hot: '🛠' },
{ name: '门店客服', price: 0, desc: '您咨询的钢琴黑膜片已到货,可预约施工', score: 0, num: 0, tag: '1天前', hot: '💬' },
{ name: '优惠活动', price: 0, desc: '本月改色节:全场 8 折起,晒图返 200 元', score: 0, num: 0, tag: '1天前', hot: '🎁' },
{ name: '膜品上架通知', price: 0, desc: '新款电光金属系列到店,欢迎预约试色', score: 0, num: 0, tag: '2天前', hot: '🆕' },
{ name: '质保提醒', price: 0, desc: '您的隐形车衣质保剩余 3 年 2 个月', score: 0, num: 0, tag: '3天前', hot: '🛡' },
{ name: '点赞与关注', price: 0, desc: '车友老王赞了你的案例电光紫作业', score: 0, num: 0, tag: '3天前', hot: '❤' },
{ name: '评论回复', price: 0, desc: '玩车大叔回复了你:改色膜建议选进口基材', score: 0, num: 0, tag: '5天前', hot: '💬' },
{ name: '物流通知', price: 0, desc: '您购买的车身装饰件已发货,预计明日达', score: 0, num: 0, tag: '6天前', hot: '🚚' },
{ name: '会员权益', price: 0, desc: '黄金会员专享:每年两次免费洗车已到账', score: 0, num: 0, tag: '1周前', hot: '👑' },
{ name: '系统公告', price: 0, desc: '贴膜工坊 3.0 版本上线,体验全新案例秀', score: 0, num: 0, tag: '1周前', hot: '📢' },
{ name: '售后回访', price: 0, desc: '感谢您选择本店施工,邀请您评价本次服务', score: 0, num: 0, tag: '2周前', hot: '⭐' }
]
隔热膜列表中num表示隔热率百分比(58%-88%),tag字段按照"顶级"“热销”“高端”“实惠”“入门"五个层级划分产品定位,帮助用户根据预算快速筛选。施工门店列表中price字段被置零,num表示距离用户的公里数,score表示门店评分,tag区分"旗舰”“直营”"加盟"三种门店类型。案例帖列表中price同样为零,num为点赞数,score为人气值,desc字段则是一段生动的用户故事描述。消息通知列表中所有数值字段均为零,tag存储时间信息,hot字段使用Emoji图标来区分消息类型。
辅助方法与取值函数

应用定义了若干辅助方法用于安全地访问selectedItem的属性以及计算列表中的最大值,这些方法在各个Tab页面和弹窗中被频繁调用。
selName(): string {
if (this.selectedItem !== null) {
return this.selectedItem.name
}
return ''
}
selPrice(): number {
if (this.selectedItem !== null) {
return this.selectedItem.price
}
return 0
}
selScore(): number {
if (this.selectedItem !== null) {
return this.selectedItem.score
}
return 0
}
maxNumOf(list: GoodsItem[]): number {
let m: number = 1
for (let i = 0; i < list.length; i++) {
if (list[i].num > m) {
m = list[i].num
}
}
return m
}
maxScoreOf(list: GoodsItem[]): number {
let m: number = 1
for (let i = 0; i < list.length; i++) {
if (list[i].score > m) {
m = list[i].score
}
}
return m
}
selName、selPrice、selScore三个方法都遵循相同的模式:首先检查selectedItem是否为null,如果不为空则返回对应属性,否则返回默认空值。这种防御性编程模式确保了在用户未选中任何商品时,详情弹窗不会因访问空对象属性而崩溃。maxNumOf和maxScoreOf方法通过遍历列表找到num或score字段的最大值,这些最大值在柱状图渲染中用于计算各柱条的相对高度比例,确保图表在数据范围变化时能够自适应缩放。
静态头部与底部导航栏

头部组件是页面的视觉入口,集成了品牌标识、搜索入口和消息通知入口;底部导航栏则通过Tab切换实现七大功能模块之间的快速跳转。
// 静态头部 无任何动画
@Builder headerBuilder() {
Column({ space: 10 }) {
Row() {
Text('贴膜改色工坊')
.fontSize(20)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Column() {
Text('🎧')
.fontSize(16)
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#6A1B9A')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Stack() {
Column() {
Text('💬')
.fontSize(15)
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#6A1B9A')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Text('6')
.fontSize(8)
.fontColor('#FFFFFF')
.padding(2)
.borderRadius(8)
.backgroundColor('#FF5252')
.border({ width: 1, color: '#FFFFFF' })
.translate({ x: 10, y: -10 })
}
.width(32)
.height(32)
.margin({ left: 10 })
}
.width('100%')
Row({ space: 8 }) {
Text('🎨')
.fontSize(14)
Text('搜改色膜 / 门店')
.fontSize(12)
.fontColor('#8E24AA')
Column().layoutWeight(1)
Text('🔍')
.fontSize(14)
}
.width('100%')
.height(36)
.padding({ left: 14, right: 14 })
.borderRadius(18)
.backgroundColor('#F3E5F5')
.alignItems(VerticalAlign.Center)
Row({ space: 6 }) {
Text('🔥')
.fontSize(11)
Text('本月热贴:钢琴黑 · 电光金属漆已服务 1286 台车')
.fontSize(10)
.fontColor('#E1BEE7')
.maxLines(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 12 })
.backgroundColor('#8E24AA')
}
头部采用三层垂直布局:顶部为品牌标题行,包含"贴膜改色工坊"文字标识以及客服和消息入口按钮,消息按钮通过Stack容器叠加了红色未读数角标,利用translate属性将角标偏移到右上角位置。中间层是搜索胶囊条,使用圆角矩形背景模拟搜索框的视觉效果,内部包含搜索图标和提示文字。底部为热门信息条,展示本月热门贴膜服务的统计数据。整个头部以霓虹紫(#8E24AA)为背景色,营造出品牌色调的统一感。
底部导航栏的实现则更为精炼,通过ForEach遍历tabs数组生成七个Tab项,每个Tab包含文字标签和底部指示条,指示条颜色根据currentTab是否等于当前索引来动态切换。
@Builder bottomBuilder() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column({ space: 2 }) {
Text(tab)
.fontSize(10)
.fontColor(this.currentTab === index ? '#8E24AA' : '#999999')
Column()
.width(18)
.height(3)
.borderRadius(2)
.backgroundColor(this.currentTab === index ? '#8E24AA' : '#FFFFFF00')
}
.onClick(() => { this.currentTab = index })
}, (tab: string) => tab)
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
}
底部导航栏的关键设计在于条件渲染:当currentTab === index时,文字颜色变为品牌紫色#8E24AA,底部指示条显示为同色实心条;否则文字为灰色#999999,指示条为透明色#FFFFFF00。这种通过状态驱动样式切换的方式是声明式UI的典型范式,开发者无需手动操作DOM样式,只需修改状态变量即可自动触发视图更新。每个Tab项绑定onClick事件,点击时将currentTab设置为对应索引值。
改色集市Tab:统计卡片与商品列表

改色集市是应用的首页Tab,展示了运营数据概览和改色膜商品列表,是用户进入应用后首先看到的页面。
// Tab1 改色集市
@Builder tab0() {
Column() {
Row({ space: 10 }) {
Column({ space: 6 }) {
Text('🎨')
.fontSize(18)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('328')
.fontSize(20)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Text('在售色系')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Column({ space: 6 }) {
Text('🛠')
.fontSize(18)
Text('1286')
.fontSize(20)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Text('本月施工')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Column({ space: 6 }) {
Text('🔁')
.fontSize(18)
Text('87%')
.fontSize(20)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
Text('复购率')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
.width('100%')
三栏统计卡片采用Row水平布局配合layoutWeight(1)实现等宽分配,每个卡片包含图标、数字和说明文字三层信息。首个卡片的Emoji图标添加了scale缩放动画和animation属性,实现无限循环的脉冲呼吸效果,增强了页面的动态视觉吸引力。动画参数中duration: 700表示700毫秒一个周期,iterations: -1表示无限循环,curve: Curve.EaseInOut定义了先缓后急再缓的动画曲线。
ForEach(this.mainList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(item.hot)
.fontSize(12)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(46)
.height(46)
.borderRadius(10)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(item.name)
.fontSize(14)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(10)
.fontColor('#90A4AE')
.maxLines(1)
Row() {
Column()
.width(item.score + '%')
.height(4)
.borderRadius(2)
.backgroundColor('#8E24AA')
}
.width(110)
.height(4)
.borderRadius(2)
.backgroundColor('#F3E5F5')
Text('好评 ' + item.score + '% · 热度 ' + item.num)
.fontSize(9)
.fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text('¥' + item.price)
.fontSize(14)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Text(item.tag)
.fontSize(9)
.fontColor('#00BFA5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#E0F2F1')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
商品列表通过ForEach遍历mainList数组渲染每条改色膜数据。每行商品卡片采用三段式布局:左侧为46x46的彩色方块标识,使用filmColors数组按索引取模循环取色,确保24款商品拥有多样化的颜色标识;中间为商品信息区,包含名称、描述、好评率进度条和统计数据,进度条通过内层Column的width属性设为item.score + '%'来实现百分比宽度填充;右侧为价格和标签区域。每行卡片绑定onClick事件,点击时将当前item赋值给selectedItem并显示详情弹窗。
隐形车衣Tab:柱状图与网格卡片
隐形车衣Tab展示了一个厚度对比柱状图和12款车衣产品的网格卡片列表,通过数据可视化增强用户的比较决策能力。
// Tab2 隐形车衣
@Builder tab1() {
Column() {
Row({ space: 6 }) {
Text('🛡')
.fontSize(14)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('车衣厚度对比(单位 μm)')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
Row({ space: 8 }) {
ForEach(this.ppList, (item: GoodsItem) => {
Column({ space: 4 }) {
Text(item.num + '').fontSize(8).fontColor('#8E24AA')
Column().width(14)
.height(item.num / this.maxNumOf(this.ppList) * 120)
.borderRadius(4).backgroundColor('#8E24AA')
Text(item.name).fontSize(7).fontColor('#90A4AE').maxLines(1)
}
}, (item: GoodsItem) => item.name)
}
.width('100%')
.height(170)
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.SpaceBetween)
柱状图的实现是该Tab页面的技术亮点。通过ForEach遍历ppList数组,每个柱条由三部分组成:顶部的数值标签、中间的柱条本体和底部的名称标签。柱条高度通过item.num / this.maxNumOf(this.ppList) * 120公式计算,其中maxNumOf方法返回列表中最大的num值(即最大厚度),120为柱条最大像素高度。这种归一化算法确保所有柱条按比例缩放到0-120像素范围内,最高柱条恰好达到120像素。容器使用alignItems(VerticalAlign.End)实现柱条底部对齐,justifyContent(FlexAlign.SpaceBetween)实现柱条之间的均匀间距分布。
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.ppList, (item: GoodsItem) => {
Column({ space: 8 }) {
Row() {
Text(item.tag + '质保')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#00BFA5')
Column().layoutWeight(1)
Text(item.num + 'μm')
.fontSize(9)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
}
.width('100%')
Column() {
Text('🛡')
.fontSize(26)
}
.width('100%')
.height(60)
.borderRadius(10)
.backgroundColor('#F3E5F5')
.justifyContent(FlexAlign.Center)
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(10)
.fontColor('#90A4AE')
.maxLines(1)
Row() {
Text('¥' + item.price)
.fontSize(13)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('购买')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor('#8E24AA')
.onClick(() => {
this.selectedItem = item
this.stepNum = 1
this.showBuyModal = true
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('48%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
}, (item: GoodsItem) => item.name)
}
.width('100%')
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
车衣商品卡片采用Flex容器配合wrap: FlexWrap.Wrap实现自动换行的双列网格布局。每个卡片宽度设为48%,通过justifyContent: FlexAlign.SpaceBetween在两列之间留出4%的间距。卡片内部结构包含质保标签行、盾牌图标区域、商品名称、描述和价格购买行。购买按钮绑定onClick事件,点击时将选中商品赋值给selectedItem,重置步进器为1,并显示购买弹窗。
页面交互流程
以下是用户在应用中的核心交互流程,从浏览商品到完成预约的完整链路:
隔热膜Tab:数据看板与横向滑动列表
隔热膜Tab设计了一个深色数据看板和横向滑动的膜品精选列表,通过深色背景营造科技感。
// Tab3 隔热膜
@Builder tab2() {
Column() {
Row() {
Column({ space: 6 }) {
Text('46')
.fontSize(22)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('今日施工(台)')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 6 }) {
Text('82%')
.fontSize(22)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('平均隔热率')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 6 }) {
Text('99%')
.fontSize(22)
.fontColor('#AB47BC')
.fontWeight(FontWeight.Bold)
Text('紫外线阻隔')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ top: 18, bottom: 18 })
.borderRadius(16)
.backgroundColor('#263238')
深色数据看板以炭黑色#263238为背景,三栏数据分别使用荧光青#00BFA5、白色#FFFFFF和紫色#AB47BC作为数字颜色,形成了冷暖对比的视觉效果。首个数字"46"添加了脉冲动画效果,吸引视觉焦点。
Scroll() {
Row({ space: 12 }) {
ForEach(this.tintList, (item: GoodsItem) => {
Column({ space: 8 }) {
Text(item.name)
.fontSize(12)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.num + '%')
.fontSize(28)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('隔热率')
.fontSize(9)
.fontColor('#B0BEC5')
Text(item.desc)
.fontSize(9)
.fontColor('#78909C')
.maxLines(1)
Text('¥' + item.price)
.fontSize(13)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(140)
.padding(14)
.borderRadius(14)
.backgroundColor('#263238')
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.padding({ left: 2, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
横向滑动列表使用Scroll组件配合scrollable(ScrollDirection.Horizontal)实现水平滚动。每个卡片固定宽度140像素,以深色背景呈现隔热率百分比大字数据,数字添加了脉冲缩放动画。这种深色卡片列表的设计不仅增加了页面的视觉层次(与上方的白色背景柱状图形成对比),也突出了隔热率这一核心参数指标。
施工门店与案例秀Tab
施工门店Tab通过门店评分柱状图和门店列表卡片展示服务网络信息,案例秀Tab则以社交信息流形式展示车友改色案例。
// Tab4 施工门店
@Builder tab3() {
Column() {
Row({ space: 6 }) {
Text('🏪')
.fontSize(14)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('门店评分一览')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('满分 100')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
Row({ space: 10 }) {
ForEach(this.shopList, (item: GoodsItem) => {
Column({ space: 4 }) {
Text(item.score + '')
.fontSize(8)
.fontColor('#8E24AA')
Column()
.width(16)
.height(item.score / this.maxScoreOf(this.shopList) * 120)
.borderRadius(4)
.backgroundColor('#8E24AA')
Text(item.name)
.fontSize(7)
.fontColor('#90A4AE')
.maxLines(1)
}
}, (item: GoodsItem) => item.name)
}
.width('100%')
.height(170)
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.SpaceBetween)
门店评分柱状图使用maxScoreOf方法计算最大评分值,柱条高度公式为item.score / this.maxScoreOf(this.shopList) * 120。与隐形车衣Tab的柱状图类似,这里同样采用了归一化算法确保柱条按比例缩放。不同之处在于,门店列表中每条数据包含距离信息,因此在门店列表卡片中会展示距离公里数和预约按钮。
// Tab5 案例秀
@Builder tab4() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.topicChips, (c: string, idx: number) => {
Text('# ' + c)
.fontSize(11)
.fontColor(idx === 0 ? '#FFFFFF' : '#8E24AA')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(idx === 0 ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
}, (c: string) => c)
}
.width('100%')
ForEach(this.caseList, (item: GoodsItem, index: number) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text(this.avatars[index % this.avatars.length])
.fontSize(18)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Text(item.tag + ' · 人气 ' + item.score)
.fontSize(9)
.fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('关注')
.fontSize(10)
.fontColor('#8E24AA')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor('#F3E5F5')
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(12)
.fontColor('#455A64')
.maxLines(2)
.lineHeight(18)
.width('100%')
Row({ space: 8 }) {
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor(this.filmColors[index % this.filmColors.length])
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor('#E1BEE7')
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor('#D1C4E9')
}
.width('100%')
Row() {
Text('❤ ' + item.num)
.fontSize(12)
.fontColor('#E91E63')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Text('💬 ' + Math.floor(item.num / 8))
.fontSize(11)
.fontColor('#B0BEC5')
Text('分享')
.fontSize(11)
.fontColor('#B0BEC5')
.margin({ left: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
案例秀Tab的设计模仿了社交平台的帖子信息流。顶部是话题标签栏,通过ForEach渲染topicChips数组,首个标签以品牌紫色填充表示当前选中。每个案例帖卡片包含用户头像行(使用Emoji头像和彩色背景)、帖子正文(两行文字描述)、三列图片占位区域(使用不同紫色系背景色模拟图片)和底部互动栏(点赞数、评论数和分享按钮)。点赞数添加了脉冲动画效果,评论数通过Math.floor(item.num / 8)公式根据点赞数估算得出。
消息中心与个人中心Tab
消息中心Tab以列表形式展示各类通知消息,个人中心Tab展示用户信息和功能入口菜单。
// Tab6 消息
@Builder tab5() {
Column() {
Row({ space: 6 }) {
Text('📨')
.fontSize(14)
Text('消息中心 · 12 条未读')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('全部已读')
.fontSize(11)
.fontColor('#8E24AA')
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
ForEach(this.msgList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(item.hot)
.fontSize(18)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(11)
.fontColor('#90A4AE')
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 8 }) {
Text(item.tag)
.fontSize(9)
.fontColor('#B0BEC5')
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#FF5252')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
消息列表中每条消息的hot字段存储Emoji图标作为消息类型标识,右侧时间标签下方有一个红色未读指示点,该指示点添加了脉冲缩放动画效果以提醒用户注意未读消息。个人中心Tab则展示了用户头像、昵称、会员信息和三栏统计数据(预约数、施工记录数、收藏膜色数),以及多个功能入口行(我的预约、施工记录、收藏膜色、地址管理、客服中心、设置),每个入口行右侧带有箭头图标和附加信息。
弹窗系统:预约与购买流程
应用设计了五类弹窗,分别用于预约贴膜、车衣购买、门店预约、删除确认和商品详情展示。以下展示预约贴膜弹窗和购买弹窗的实现。
// 弹框1 预约贴膜 底部弹出
@Builder addModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
Text('预约贴膜')
.fontSize(18)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('选择膜品')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.colorChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipA === idx ? '#FFFFFF' : '#8E24AA')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipA === idx ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipA = idx })
}, (c: string) => c)
}
.width('100%')
Text('施工部位')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.partChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipB === idx ? '#FFFFFF' : '#00BFA5')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipB === idx ? '#00BFA5' : '#E0F2F1')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipB = idx })
}, (c: string) => c)
}
.width('100%')
Text('联系手机号')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
TextInput({ placeholder: '请输入 11 位手机号', text: this.phoneText })
.onChange((value: string) => { this.phoneText = value })
.width('100%')
.height(44)
.padding(10)
.borderRadius(10)
.backgroundColor('#F5F5F7')
.fontSize(13)
Text('提交预约')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(46)
.borderRadius(23)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
.onClick(() => { this.showAddModal = false })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => { event.stopPropagation() })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { this.showAddModal = false })
}
预约弹窗采用底部弹出模式,外层Column使用layoutWeight(1)的空白占位将内容推至底部,半透明遮罩层使用rgba(0,0,0,0.5)实现。弹窗内容区包含一个拖拽指示条、标题、膜品选择芯片组、施工部位芯片组和手机号输入框。芯片选择通过chipA和chipB状态变量记录选中索引,选中时文字变白、背景变实色。TextInput组件绑定phoneText状态变量,实现双向数据绑定。内容区域使用constraintSize({ maxHeight: '80%' })限制最大高度,防止内容过多时撑满全屏。内层onClick使用event.stopPropagation()阻止事件冒泡,确保点击弹窗内容区不会关闭弹窗,而点击遮罩层则会关闭弹窗。
// 弹框2 车衣购买 底部弹出
@Builder buyModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
Text(this.selName())
.fontSize(17)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('单价 ¥' + this.selPrice() + ' / 套 · 限购 3 套')
.fontSize(11)
.fontColor('#B0BEC5')
.width('100%')
Text('选择规格')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.specChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipA === idx ? '#FFFFFF' : '#8E24AA')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipA === idx ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipA = idx })
}, (c: string) => c)
}
.width('100%')
Text('购买数量')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Row({ space: 14 }) {
Text('-')
.fontSize(16)
.fontColor('#8E24AA')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#F3E5F5')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum > 1) {
this.stepNum -= 1
}
})
Text(this.stepNum + ' 套')
.fontSize(14)
.fontColor('#263238')
.width(60)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(16)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum < 3) {
this.stepNum += 1
}
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Text('合计')
.fontSize(13)
.fontColor('#90A4AE')
Column().layoutWeight(1)
Text('¥' + Math.floor(this.selPrice() * this.stepNum))
.fontSize(26)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text('立即购买')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(46)
.borderRadius(23)
.backgroundColor('#00BFA5')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
.onClick(() => { this.showBuyModal = false })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => { event.stopPropagation() })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { this.showBuyModal = false })
}
购买弹窗与预约弹窗的结构类似,但增加了规格选择和数量步进器。步进器通过减号和加号按钮实现,减号按钮在stepNum > 1时才允许递减,加号按钮在stepNum < 3时才允许递增,实现了"限购3套"的业务约束。合计金额通过Math.floor(this.selPrice() * this.stepNum)实时计算单价乘以数量,并添加了脉冲缩放动画以吸引用户注意价格变化。
详情弹窗与主页面构建
详情弹窗采用右侧滑出的全高面板设计,是商品详情展示的核心载体。主页面build方法则将所有组件组装成完整的页面结构。
// 弹框5 详情 右侧滑出全高面板
@Builder detailModal() {
Column() {
Column() {
Column({ space: 8 }) {
Text('🎨')
.fontSize(52)
Text('正品授权 · 假一赔十')
.fontSize(10)
.fontColor('#E1BEE7')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(0,0,0,0.25)')
}
.width('100%')
.height(180)
.backgroundColor('#4A148C')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 12 }) {
Text(this.selName())
.fontSize(18)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('★★★★★')
.fontSize(14)
.fontColor('#FFB300')
.width('100%')
Text('¥' + this.selPrice())
.fontSize(28)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
.width('100%')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column({ space: 6 }) {
Row() {
Column()
.width(this.selScore() + '%')
.height(6)
.borderRadius(3)
.backgroundColor('#8E24AA')
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#F3E5F5')
Text('好评率 ' + this.selScore() + '% · 已服务 452 台车')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
Text('适用部位')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.applyChips, (c: string) => {
Text(c)
.fontSize(11)
.fontColor('#263238')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor('#ECEFF1')
.margin({ right: 8, bottom: 8 })
}, (c: string) => c)
}
.width('100%')
Text('进口基材 · 支持全车 / 局部施工 · 完工免费镀晶一次')
.fontSize(10)
.fontColor('#90A4AE')
.width('100%')
.lineHeight(16)
}
.width('100%')
.padding(16)
Column().layoutWeight(1)
Text('立即预约')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.margin({ left: 16, right: 16, bottom: 20 })
.onClick(() => {
this.showDetailModal = false
this.showAddModal = true
})
}
.width('80%')
.height('100%')
.backgroundColor('#FFFFFF')
.onClick((event: ClickEvent) => { event.stopPropagation() })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.End)
.onClick(() => { this.showDetailModal = false })
}
详情弹窗的外层容器使用justifyContent(FlexAlign.End)和alignItems(HorizontalAlign.End)将内容面板定位到屏幕右侧,面板宽度为80%,高度为100%,实现了右侧滑出的全高面板效果。面板顶部为深紫色(#4A148C)的商品图占位区,包含大号Emoji图标和品质保障标签。中间为商品信息区,包含名称、五星评分、价格(带脉冲动画)、好评率进度条和适用部位标签组。底部为"立即预约"按钮,点击后关闭详情弹窗并打开预约弹窗,实现了弹窗之间的级联跳转。
build() {
Stack() {
Column() {
this.headerBuilder()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tab0()
} else if (this.currentTab === 1) {
this.tab1()
} else if (this.currentTab === 2) {
this.tab2()
} else if (this.currentTab === 3) {
this.tab3()
} else if (this.currentTab === 4) {
this.tab4()
} else if (this.currentTab === 5) {
this.tab5()
} else {
this.tab6()
}
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.backgroundColor('#F5F5F7')
.edgeEffect(EdgeEffect.Spring)
this.bottomBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F7')
if (this.showAddModal) {
this.addModal()
}
if (this.showBuyModal) {
this.buyModal()
}
if (this.showShopModal) {
this.shopModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
if (this.showDetailModal) {
this.detailModal()
}
}
.width('100%')
.height('100%')
}
}
build方法是整个组件的渲染入口,采用Stack容器实现弹窗的层叠效果。Stack内部首先放置主内容区(Column包含头部、可滚动内容区和底部导航栏),然后通过条件判断依次叠加五个弹窗组件。当某个show*Modal状态为true时,对应的弹窗会被渲染到Stack上层,覆盖在主内容区之上。中间内容区使用Scroll组件实现纵向滚动,并添加了edgeEffect(EdgeEffect.Spring)弹簧边界效果,提升了滚动交互的物理真实感。Tab切换通过if-else条件链实现,根据currentTab的值决定渲染哪个Tab页面的Builder方法。
技术点对比
| 技术维度 | 实现方式 | 设计特点 | 适用场景 |
|---|---|---|---|
| 状态管理 | @State装饰器声明式状态 | 集中式管理十余个状态变量,自动触发UI刷新 | 中等复杂度单页面应用 |
| 组件复用 | @Builder方法构建可复用UI段 | 七个Tab页面和五个弹窗均以Builder方法封装 | 多Tab多弹窗页面结构 |
| 数据模型 | 单一GoodsItem接口复用 | num字段多义性(热度/厚度/隔热率/距离/点赞数) | 统一数据结构的多场景列表 |
| 图表渲染 | ForEach动态生成柱状图 | 归一化算法按比例缩放柱条高度 | 数据对比可视化 |
| 弹窗系统 | Stack层叠+条件渲染 | 五种弹窗类型,支持级联跳转 | 多类型交互弹窗 |
| 滚动布局 | Scroll+layoutWeight弹性布局 | 弹簧边界效果增强交互真实感 | 长列表与可滚动内容区 |
| 网格布局 | Flex+FlexWrap.Wrap自动换行 | 48%宽度双列卡片,SpaceBetween间距分配 | 商品卡片双列展示 |
| 动画效果 | scale+animation属性组合 | 700ms EaseInOut无限循环脉冲动画 | 强调关键数据与交互元素 |
| 事件处理 | onClick+stopPropagation | 遮罩层关闭弹窗,内容区阻止冒泡 | 模态弹窗交互控制 |
| 主题色系 | 霓虹紫为主色调 | #8E24AA品牌色贯穿全局 | 个性化潮流消费场景 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 汽车贴膜改色工坊 汽车之家风格 ArkTS 页面
// 配色:霓虹紫 钛银 炭黑 浅紫底 荧光青
interface GoodsItem {
name: string
price: number
desc: string
score: number
num: number
tag: string
hot: string
}
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showBuyModal: boolean = false
@State showShopModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedItem: GoodsItem | null = null
@State chipA: number = 0
@State chipB: number = 0
@State stepNum: number = 1
@State phoneText: string = ''
private tabs: string[] = ['改色集市', '隐形车衣', '隔热膜', '施工门店', '案例秀', '消息', '我的']
private colorChips: string[] = ['钢琴黑', '电光金属', '液态银丝', '哑光军绿', '宝石蓝']
private partChips: string[] = ['全车', '车顶', '引擎盖', '后视镜']
private specChips: string[] = ['前杠版', '半车版', '全车版']
private storeChips: string[] = ['朝阳旗舰店', '海淀店', '丰台店', '通州店']
private timeChips: string[] = ['上午', '下午', '晚间']
private topicChips: string[] = ['全站', '电光金属', '哑光系列', '碳纤维纹', '婚车白', '复古绿', '荧光色']
private applyChips: string[] = ['全车', '车顶', '引擎盖', '后视镜', '门把手', '内饰件']
private filmColors: string[] = ['#1A1A1A', '#42A5F5', '#EC407A', '#00BFA5', '#FFD600', '#5E35B1', '#AB47BC', '#546E7A', '#FF7043', '#26C6DA', '#EF5350', '#66BB6A']
private avatars: string[] = ['🚗', '🏎', '🚙', '🚕', '🚓', '🚗', '🏎', '🚙', '🚕', '🚓', '🚗', '🏎']
// 改色膜 24 条
private mainList: GoodsItem[] = [
{ name: '电光金属 · 液态银丝蓝', price: 6800, desc: '进口 TPU 基材 · 5 年质保', score: 96, num: 452, tag: '热销', hot: '爆' },
{ name: '钢琴黑 · 哑光曜石', price: 5200, desc: '日本铸级膜 · 哑光低反射', score: 94, num: 388, tag: '经典', hot: '黑' },
{ name: '宝石蓝 · 炫彩幻变', price: 7500, desc: '多层镀膜 · 变色效果', score: 92, num: 341, tag: '新品', hot: '蓝' },
{ name: '哑光军绿 · 战地迷彩', price: 6300, desc: '军工级 PET · 抗刮', score: 90, num: 275, tag: '潮流', hot: '绿' },
{ name: '液态金属银 · 流光溢彩', price: 8800, desc: '纳米电镀 · 高镜面', score: 97, num: 501, tag: '奢选', hot: '银' },
{ name: '火焰红 · 法拉利同款', price: 5900, desc: '高饱和色浆 · 持久', score: 95, num: 415, tag: '热销', hot: '红' },
{ name: '翡翠绿 · 皇家绿改', price: 6600, desc: '进口基材 · 4 年质保', score: 91, num: 233, tag: '个性', hot: '绿' },
{ name: '香槟金 · 商务轻奢', price: 6100, desc: '金属颗粒 · 细腻', score: 89, num: 198, tag: '稳重', hot: '金' },
{ name: '哑光灰 · 纳多灰同款', price: 5400, desc: '进口哑光 · 抗污', score: 93, num: 362, tag: '热销', hot: '灰' },
{ name: '电光紫 · 霓虹幻影', price: 7200, desc: '电光系列 · 夜间流光', score: 96, num: 429, tag: '潮流', hot: '紫' },
{ name: '深海蓝 · 布鲁克林蓝', price: 5700, desc: '双拼色可选 · 耐候', score: 88, num: 175, tag: '经典', hot: '蓝' },
{ name: '岩石灰 · 水泥灰改色', price: 5000, desc: '国产优质 · 性价比', score: 90, num: 287, tag: '实惠', hot: '灰' },
{ name: '落日橙 · 撼路者橙', price: 6400, desc: '高亮色泽 · 防晒', score: 92, num: 312, tag: '个性', hot: '橙' },
{ name: '薄荷绿 · 清新马卡龙', price: 6200, desc: '低饱和色系 · 温和', score: 87, num: 154, tag: '新品', hot: '绿' },
{ name: '曜石黑 · 亮面高黑', price: 5300, desc: '经典黑膜 · 万能款', score: 95, num: 401, tag: '热销', hot: '黑' },
{ name: '玫瑰金 · 粉调金属', price: 6900, desc: '渐变金属 · 女神款', score: 91, num: 219, tag: '潮流', hot: '粉' },
{ name: '冰川蓝 · 冷调金属', price: 7100, desc: '冰感涂层 · 隔热', score: 94, num: 358, tag: '奢选', hot: '蓝' },
{ name: '磨砂黑 · 哑光高级感', price: 5600, desc: '磨砂工艺 · 抗指纹', score: 93, num: 377, tag: '热销', hot: '黑' },
{ name: '柠檬黄 · 警示亮黄', price: 4800, desc: '高可见度 · 防刮', score: 86, num: 132, tag: '个性', hot: '黄' },
{ name: '酒红 · 勃艮第红', price: 6700, desc: '深邃酒红 · 商务', score: 95, num: 389, tag: '经典', hot: '红' },
{ name: '碳纤维纹 · 黑色碳纤', price: 7800, desc: '仿碳纤维 · 运动风', score: 96, num: 466, tag: '高端', hot: '碳' },
{ name: '珍珠白 · 珍珠贝母', price: 5800, desc: '珍珠光泽 · 温润', score: 90, num: 241, tag: '婚车', hot: '白' },
{ name: '军械绿 · 复古绿', price: 5100, desc: '复古色调 · 文艺', score: 88, num: 168, tag: '小众', hot: '绿' },
{ name: '霓虹粉 · 荧光少女', price: 6100, desc: '荧光色系 · 夜光', score: 89, num: 187, tag: '潮流', hot: '粉' }
]
// 隐形车衣 12 条 num 为厚度微米
private ppList: GoodsItem[] = [
{ name: '龙膜 · 钻石系列', price: 12800, desc: '全车 TPU · 10 年质保', score: 97, num: 220, tag: '旗舰', hot: '钻' },
{ name: '威固 V70+', price: 11800, desc: '光学级 TPU · 抗划痕', score: 96, num: 200, tag: '旗舰', hot: '威' },
{ name: 'XPEL · 极致系列', price: 13800, desc: '美国原装 · 自修复', score: 98, num: 230, tag: '尊享', hot: 'X' },
{ name: '3M · 漆面保护膜', price: 9800, desc: '经典款 · 7 年质保', score: 93, num: 180, tag: '热销', hot: '3' },
{ name: '圣科 · 铂金系列', price: 8800, desc: '高透亮 · 5 年质保', score: 91, num: 160, tag: '实惠', hot: '圣' },
{ name: '艺卡 · ARES 系列', price: 7800, desc: '意大利基材 · 抗黄变', score: 90, num: 150, tag: '实惠', hot: '艺' },
{ name: '量子 · Q 系列', price: 10800, desc: '纳米涂层 · 疏水', score: 94, num: 190, tag: '高端', hot: '量' },
{ name: '固驰 · 哑光车衣', price: 9200, desc: '哑光车衣 · 改色一体', score: 92, num: 170, tag: '潮流', hot: '固' },
{ name: 'UPPF · 拓弗系列', price: 11200, desc: '军工级 · 高韧性', score: 95, num: 210, tag: '高端', hot: 'U' },
{ name: '诺盾 · ND-X 系列', price: 8200, desc: '性价比之选 · 5 年', score: 89, num: 140, tag: '实惠', hot: '诺' },
{ name: '康得新 · 新锐系列', price: 6800, desc: '国产优质 · 4 年质保', score: 87, num: 130, tag: '入门', hot: '康' },
{ name: '琥珀 · 光学系列', price: 10200, desc: '高清晰度 · 增亮', score: 93, num: 185, tag: '热销', hot: '琥' }
]
// 隔热膜 10 条 num 为隔热率百分比
private tintList: GoodsItem[] = [
{ name: '威固 VK70 前挡', price: 3280, desc: '光学膜 · 高清晰', score: 97, num: 88, tag: '顶级', hot: '挡' },
{ name: '龙膜 · 畅悦80', price: 2680, desc: '陶瓷膜 · 不屏蔽信号', score: 95, num: 80, tag: '热销', hot: '龙' },
{ name: '3M · 晶锐70', price: 2980, desc: '光学微附 · 隔热强', score: 94, num: 85, tag: '高端', hot: '晶' },
{ name: '量子 · 钻石70', price: 2380, desc: '磁控溅射 · 持久', score: 92, num: 76, tag: '热销', hot: '量' },
{ name: '圣科 · RC70', price: 2180, desc: '性价比 · 隔紫外线', score: 90, num: 72, tag: '实惠', hot: '圣' },
{ name: '强生 · 环球70', price: 1980, desc: '经典款 · 耐用', score: 89, num: 68, tag: '实惠', hot: '强' },
{ name: 'FSK · 冰钻前挡', price: 2580, desc: '日本进口 · 高透', score: 93, num: 79, tag: '高端', hot: '冰' },
{ name: '琥珀光学 · A70', price: 2780, desc: '陶瓷颗粒 · 低反光', score: 94, num: 82, tag: '高端', hot: '琥' },
{ name: '康得新 · K70', price: 1780, desc: '国产优质 · 三年质保', score: 87, num: 62, tag: '入门', hot: '康' },
{ name: '蓝光 · 前挡套装', price: 1580, desc: '入门之选 · 隔紫外线', score: 85, num: 58, tag: '入门', hot: '蓝' }
]
// 施工门店 10 条 num 为距离公里 score 为门店评分
private shopList: GoodsItem[] = [
{ name: '朝阳旗舰店 · 奥特莱斯', price: 0, desc: '朝阳区东四环中路 · 12 个施工工位', score: 96, num: 3, tag: '旗舰', hot: '朝' },
{ name: '海淀店 · 五棵松', price: 0, desc: '海淀区复兴路 · 8 个施工工位', score: 94, num: 7, tag: '直营', hot: '海' },
{ name: '丰台店 · 世界公园', price: 0, desc: '丰台区丰葆路 · 6 个施工工位', score: 92, num: 11, tag: '直营', hot: '丰' },
{ name: '通州店 · 万达广场', price: 0, desc: '通州区新华西街 · 6 个施工工位', score: 91, num: 15, tag: '直营', hot: '通' },
{ name: '昌平店 · 回龙观', price: 0, desc: '昌平区回龙观西大街 · 5 个施工工位', score: 90, num: 21, tag: '加盟', hot: '昌' },
{ name: '大兴店 · 亦庄', price: 0, desc: '大兴区荣华中路 · 7 个施工工位', score: 93, num: 18, tag: '加盟', hot: '大' },
{ name: '顺义店 · 后沙峪', price: 0, desc: '顺义区安泰大街 · 4 个施工工位', score: 89, num: 26, tag: '加盟', hot: '顺' },
{ name: '房山店 · 长阳', price: 0, desc: '房山区长阳镇 · 4 个施工工位', score: 88, num: 32, tag: '加盟', hot: '房' },
{ name: '石景山店 · 苹果园', price: 0, desc: '石景山区苹果园南路 · 5 个施工工位', score: 90, num: 24, tag: '直营', hot: '石' },
{ name: '望京店 · 酒仙桥', price: 0, desc: '朝阳区酒仙桥路 · 9 个施工工位', score: 95, num: 9, tag: '直营', hot: '望' }
]
// 案例帖子 10 条 num 为点赞数 score 为人气
private caseList: GoodsItem[] = [
{ name: '车友老王', price: 0, desc: '电光紫 Model 3 全车改色,夜间流光效果一绝,附三天施工全记录', score: 96, num: 1286, tag: '新帖', hot: '紫' },
{ name: '女神座驾Lisa', price: 0, desc: '香槟金 Mini 全车膜,回头率拉满,姐妹们冲鸭', score: 94, num: 986, tag: '精选', hot: '金' },
{ name: '玩车大叔', price: 0, desc: '哑光军绿牧马人,硬派风格首选,附洗车养护小贴士', score: 92, num: 875, tag: '热门', hot: '绿' },
{ name: '城市猎人', price: 0, desc: '碳纤维纹 GTI,运动感翻倍,全车花了四天半', score: 91, num: 764, tag: '热门', hot: '碳' },
{ name: '京郊小飞', price: 0, desc: '宝石蓝 BRZ 幻变效果实拍,阳光下会变色', score: 90, num: 652, tag: '新帖', hot: '蓝' },
{ name: '星城车痴', price: 0, desc: '钢琴黑奔驰 E 哑光高级感,商务人士低调之选', score: 93, num: 598, tag: '精选', hot: '黑' },
{ name: '半岛铁盒', price: 0, desc: '液态金属银保时捷,镜面效果像镀了一层水银', score: 95, num: 534, tag: '精选', hot: '银' },
{ name: '朝阳阿伟', price: 0, desc: '翡翠绿老甲壳虫复古改造,文艺范儿拉满', score: 88, num: 487, tag: '新帖', hot: '绿' },
{ name: '五环车神', price: 0, desc: '磨砂黑奥迪 S4 深度作业,含隐形车衣叠加方案', score: 90, num: 433, tag: '热门', hot: '黑' },
{ name: '三里屯喵', price: 0, desc: '玫瑰金甲壳虫女神座驾,粉色金属渐变太温柔', score: 89, num: 396, tag: '新帖', hot: '粉' }
]
// 消息 12 条 tag 为时间
private msgList: GoodsItem[] = [
{ name: '预约成功通知', price: 0, desc: '您的贴膜预约已确认,请在 3 月 18 日上午到店', score: 0, num: 0, tag: '2小时前', hot: '✅' },
{ name: '施工进度提醒', price: 0, desc: '您的爱车已完成表面清洁,进入贴膜工序', score: 0, num: 0, tag: '4小时前', hot: '🛠' },
{ name: '门店客服', price: 0, desc: '您咨询的钢琴黑膜片已到货,可预约施工', score: 0, num: 0, tag: '1天前', hot: '💬' },
{ name: '优惠活动', price: 0, desc: '本月改色节:全场 8 折起,晒图返 200 元', score: 0, num: 0, tag: '1天前', hot: '🎁' },
{ name: '膜品上架通知', price: 0, desc: '新款电光金属系列到店,欢迎预约试色', score: 0, num: 0, tag: '2天前', hot: '🆕' },
{ name: '质保提醒', price: 0, desc: '您的隐形车衣质保剩余 3 年 2 个月', score: 0, num: 0, tag: '3天前', hot: '🛡' },
{ name: '点赞与关注', price: 0, desc: '车友老王赞了你的案例电光紫作业', score: 0, num: 0, tag: '3天前', hot: '❤' },
{ name: '评论回复', price: 0, desc: '玩车大叔回复了你:改色膜建议选进口基材', score: 0, num: 0, tag: '5天前', hot: '💬' },
{ name: '物流通知', price: 0, desc: '您购买的车身装饰件已发货,预计明日达', score: 0, num: 0, tag: '6天前', hot: '🚚' },
{ name: '会员权益', price: 0, desc: '黄金会员专享:每年两次免费洗车已到账', score: 0, num: 0, tag: '1周前', hot: '👑' },
{ name: '系统公告', price: 0, desc: '贴膜工坊 3.0 版本上线,体验全新案例秀', score: 0, num: 0, tag: '1周前', hot: '📢' },
{ name: '售后回访', price: 0, desc: '感谢您选择本店施工,邀请您评价本次服务', score: 0, num: 0, tag: '2周前', hot: '⭐' }
]
selName(): string {
if (this.selectedItem !== null) {
return this.selectedItem.name
}
return ''
}
selPrice(): number {
if (this.selectedItem !== null) {
return this.selectedItem.price
}
return 0
}
selScore(): number {
if (this.selectedItem !== null) {
return this.selectedItem.score
}
return 0
}
maxNumOf(list: GoodsItem[]): number {
let m: number = 1
for (let i = 0; i < list.length; i++) {
if (list[i].num > m) {
m = list[i].num
}
}
return m
}
maxScoreOf(list: GoodsItem[]): number {
let m: number = 1
for (let i = 0; i < list.length; i++) {
if (list[i].score > m) {
m = list[i].score
}
}
return m
}
// 静态头部 无任何动画
@Builder headerBuilder() {
Column({ space: 10 }) {
Row() {
Text('贴膜改色工坊')
.fontSize(20)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Column() {
Text('🎧')
.fontSize(16)
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#6A1B9A')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Stack() {
Column() {
Text('💬')
.fontSize(15)
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#6A1B9A')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Text('6')
.fontSize(8)
.fontColor('#FFFFFF')
.padding(2)
.borderRadius(8)
.backgroundColor('#FF5252')
.border({ width: 1, color: '#FFFFFF' })
.translate({ x: 10, y: -10 })
}
.width(32)
.height(32)
.margin({ left: 10 })
}
.width('100%')
Row({ space: 8 }) {
Text('🎨')
.fontSize(14)
Text('搜改色膜 / 门店')
.fontSize(12)
.fontColor('#8E24AA')
Column().layoutWeight(1)
Text('🔍')
.fontSize(14)
}
.width('100%')
.height(36)
.padding({ left: 14, right: 14 })
.borderRadius(18)
.backgroundColor('#F3E5F5')
.alignItems(VerticalAlign.Center)
Row({ space: 6 }) {
Text('🔥')
.fontSize(11)
Text('本月热贴:钢琴黑 · 电光金属漆已服务 1286 台车')
.fontSize(10)
.fontColor('#E1BEE7')
.maxLines(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 12 })
.backgroundColor('#8E24AA')
}
@Builder bottomBuilder() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column({ space: 2 }) {
Text(tab)
.fontSize(10)
.fontColor(this.currentTab === index ? '#8E24AA' : '#999999')
Column()
.width(18)
.height(3)
.borderRadius(2)
.backgroundColor(this.currentTab === index ? '#8E24AA' : '#FFFFFF00')
}
.onClick(() => { this.currentTab = index })
}, (tab: string) => tab)
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
}
// Tab1 改色集市
@Builder tab0() {
Column() {
Row({ space: 10 }) {
Column({ space: 6 }) {
Text('🎨')
.fontSize(18)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('328')
.fontSize(20)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Text('在售色系')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Column({ space: 6 }) {
Text('🛠')
.fontSize(18)
Text('1286')
.fontSize(20)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Text('本月施工')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Column({ space: 6 }) {
Text('🔁')
.fontSize(18)
Text('87%')
.fontSize(20)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
Text('复购率')
.fontSize(10)
.fontColor('#90A4AE')
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
.width('100%')
Row({ space: 6 }) {
Text('🔥')
.fontSize(14)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('热门改色膜 · 24 款')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('查看全部 ›')
.fontSize(11)
.fontColor('#8E24AA')
}
.width('100%')
.margin({ top: 16, bottom: 10 })
.alignItems(VerticalAlign.Center)
ForEach(this.mainList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(item.hot)
.fontSize(12)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(46)
.height(46)
.borderRadius(10)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(item.name)
.fontSize(14)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(10)
.fontColor('#90A4AE')
.maxLines(1)
Row() {
Column()
.width(item.score + '%')
.height(4)
.borderRadius(2)
.backgroundColor('#8E24AA')
}
.width(110)
.height(4)
.borderRadius(2)
.backgroundColor('#F3E5F5')
Text('好评 ' + item.score + '% · 热度 ' + item.num)
.fontSize(9)
.fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text('¥' + item.price)
.fontSize(14)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Text(item.tag)
.fontSize(9)
.fontColor('#00BFA5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#E0F2F1')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab2 隐形车衣
@Builder tab1() {
Column() {
Row({ space: 6 }) {
Text('🛡')
.fontSize(14)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('车衣厚度对比(单位 μm)')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
Row({ space: 8 }) {
ForEach(this.ppList, (item: GoodsItem) => {
Column({ space: 4 }) {
Text(item.num + '').fontSize(8).fontColor('#8E24AA')
Column().width(14)
.height(item.num / this.maxNumOf(this.ppList) * 120)
.borderRadius(4).backgroundColor('#8E24AA')
Text(item.name).fontSize(7).fontColor('#90A4AE').maxLines(1)
}
}, (item: GoodsItem) => item.name)
}
.width('100%')
.height(170)
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.SpaceBetween)
Row({ space: 6 }) {
Text('💎')
.fontSize(14)
Text('隐形车衣 · 12 款在售')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('厚度越高越抗石击')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.margin({ top: 16, bottom: 10 })
.alignItems(VerticalAlign.Center)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.ppList, (item: GoodsItem) => {
Column({ space: 8 }) {
Row() {
Text(item.tag + '质保')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#00BFA5')
Column().layoutWeight(1)
Text(item.num + 'μm')
.fontSize(9)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
}
.width('100%')
Column() {
Text('🛡')
.fontSize(26)
}
.width('100%')
.height(60)
.borderRadius(10)
.backgroundColor('#F3E5F5')
.justifyContent(FlexAlign.Center)
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(10)
.fontColor('#90A4AE')
.maxLines(1)
Row() {
Text('¥' + item.price)
.fontSize(13)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('购买')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor('#8E24AA')
.onClick(() => {
this.selectedItem = item
this.stepNum = 1
this.showBuyModal = true
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('48%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
}, (item: GoodsItem) => item.name)
}
.width('100%')
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab3 隔热膜
@Builder tab2() {
Column() {
Row() {
Column({ space: 6 }) {
Text('46')
.fontSize(22)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('今日施工(台)')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 6 }) {
Text('82%')
.fontSize(22)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('平均隔热率')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 6 }) {
Text('99%')
.fontSize(22)
.fontColor('#AB47BC')
.fontWeight(FontWeight.Bold)
Text('紫外线阻隔')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ top: 18, bottom: 18 })
.borderRadius(16)
.backgroundColor('#263238')
Row({ space: 6 }) {
Text('🌡')
.fontSize(14)
Text('隔热率排行 · 10 款')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: 16, bottom: 10 })
.alignItems(VerticalAlign.Center)
Row({ space: 10 }) {
ForEach(this.tintList, (item: GoodsItem) => {
Column({ space: 4 }) {
Text(item.num + '%').fontSize(8).fontColor('#00BFA5')
Column().width(16)
.height(item.num / this.maxNumOf(this.tintList) * 120)
.borderRadius(4).backgroundColor('#00BFA5')
Text(item.name).fontSize(7).fontColor('#90A4AE').maxLines(1)
}
}, (item: GoodsItem) => item.name)
}
.width('100%')
.height(170)
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.SpaceBetween)
Row({ space: 6 }) {
Text('🧊')
.fontSize(14)
Text('膜品精选 · 横向滑动')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('前挡专用')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.margin({ top: 16, bottom: 10 })
.alignItems(VerticalAlign.Center)
Scroll() {
Row({ space: 12 }) {
ForEach(this.tintList, (item: GoodsItem) => {
Column({ space: 8 }) {
Text(item.name)
.fontSize(12)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.num + '%')
.fontSize(28)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('隔热率')
.fontSize(9)
.fontColor('#B0BEC5')
Text(item.desc)
.fontSize(9)
.fontColor('#78909C')
.maxLines(1)
Text('¥' + item.price)
.fontSize(13)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(140)
.padding(14)
.borderRadius(14)
.backgroundColor('#263238')
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.padding({ left: 2, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab4 施工门店
@Builder tab3() {
Column() {
Row({ space: 6 }) {
Text('🏪')
.fontSize(14)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('门店评分一览')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('满分 100')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
Row({ space: 10 }) {
ForEach(this.shopList, (item: GoodsItem) => {
Column({ space: 4 }) {
Text(item.score + '')
.fontSize(8)
.fontColor('#8E24AA')
Column()
.width(16)
.height(item.score / this.maxScoreOf(this.shopList) * 120)
.borderRadius(4)
.backgroundColor('#8E24AA')
Text(item.name)
.fontSize(7)
.fontColor('#90A4AE')
.maxLines(1)
}
}, (item: GoodsItem) => item.name)
}
.width('100%')
.height(170)
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.SpaceBetween)
Row({ space: 6 }) {
Text('📍')
.fontSize(14)
Text('附近门店 · 10 家')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('距离由近到远')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.margin({ top: 16, bottom: 10 })
.alignItems(VerticalAlign.Center)
ForEach(this.shopList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column()
.width(4)
.height(52)
.borderRadius(2)
.backgroundColor(this.filmColors[index % this.filmColors.length])
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(item.name)
.fontSize(14)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.tag)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor('#8E24AA')
}
.alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(10)
.fontColor('#90A4AE')
.maxLines(1)
Row() {
Column()
.width(item.score + '%')
.height(4)
.borderRadius(2)
.backgroundColor('#00BFA5')
}
.width(120)
.height(4)
.borderRadius(2)
.backgroundColor('#ECEFF1')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text(item.num + ' km')
.fontSize(12)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
Text('预约')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor('#00BFA5')
.onClick(() => {
this.showShopModal = true
})
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.showShopModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab5 案例秀
@Builder tab4() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.topicChips, (c: string, idx: number) => {
Text('# ' + c)
.fontSize(11)
.fontColor(idx === 0 ? '#FFFFFF' : '#8E24AA')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(idx === 0 ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
}, (c: string) => c)
}
.width('100%')
ForEach(this.caseList, (item: GoodsItem, index: number) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text(this.avatars[index % this.avatars.length])
.fontSize(18)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Text(item.tag + ' · 人气 ' + item.score)
.fontSize(9)
.fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('关注')
.fontSize(10)
.fontColor('#8E24AA')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor('#F3E5F5')
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(12)
.fontColor('#455A64')
.maxLines(2)
.lineHeight(18)
.width('100%')
Row({ space: 8 }) {
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor(this.filmColors[index % this.filmColors.length])
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor('#E1BEE7')
Column()
.layoutWeight(1)
.height(110)
.borderRadius(10)
.backgroundColor('#D1C4E9')
}
.width('100%')
Row() {
Text('❤ ' + item.num)
.fontSize(12)
.fontColor('#E91E63')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Text('💬 ' + Math.floor(item.num / 8))
.fontSize(11)
.fontColor('#B0BEC5')
Text('分享')
.fontSize(11)
.fontColor('#B0BEC5')
.margin({ left: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab6 消息
@Builder tab5() {
Column() {
Row({ space: 6 }) {
Text('📨')
.fontSize(14)
Text('消息中心 · 12 条未读')
.fontSize(15)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('全部已读')
.fontSize(11)
.fontColor('#8E24AA')
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
ForEach(this.msgList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(item.hot)
.fontSize(18)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor(this.filmColors[index % this.filmColors.length])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(item.name)
.fontSize(13)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.desc)
.fontSize(11)
.fontColor('#90A4AE')
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 8 }) {
Text(item.tag)
.fontSize(9)
.fontColor('#B0BEC5')
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#FF5252')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.currentTab = this.currentTab
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// Tab7 我的
@Builder tab6() {
Column() {
Column({ space: 14 }) {
Row({ space: 14 }) {
Column() {
Text('🧑🔧')
.fontSize(26)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#37474F')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.border({ width: 2, color: '#00BFA5' })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column({ space: 6 }) {
Text('David 车主')
.fontSize(18)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('爱车:特斯拉 Model 3 · 珍珠白')
.fontSize(11)
.fontColor('#B0BEC5')
Text('黄金会员 · 积分 2680')
.fontSize(9)
.fontColor('#00BFA5')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Column({ space: 4 }) {
Text('3')
.fontSize(18)
.fontColor('#00BFA5')
.fontWeight(FontWeight.Bold)
Text('我的预约')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('12')
.fontSize(18)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('施工记录')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('28')
.fontSize(18)
.fontColor('#AB47BC')
.fontWeight(FontWeight.Bold)
Text('收藏膜色')
.fontSize(10)
.fontColor('#B0BEC5')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ top: 12 })
.border({ width: 1, color: '#37474F' })
}
.width('100%')
.padding(18)
.borderRadius(16)
.backgroundColor('#263238')
Row() {
Text('📅').fontSize(16)
Text('我的预约').fontSize(14).fontColor('#263238').margin({ left: 10 })
Column().layoutWeight(1)
Text('3 条待施工').fontSize(10).fontColor('#B0BEC5')
Text('›').fontSize(20).fontColor('#B0BEC5').margin({ left: 8 })
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ top: 14, bottom: 8 }).alignItems(VerticalAlign.Center)
.onClick(() => { this.showAddModal = true })
Row() {
Text('🗂')
.fontSize(16)
Text('施工记录')
.fontSize(14)
.fontColor('#263238')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('共 12 次')
.fontSize(10)
.fontColor('#B0BEC5')
Text('›')
.fontSize(20)
.fontColor('#B0BEC5')
.margin({ left: 8 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.showDeleteModal = true
})
Row() {
Text('🎨')
.fontSize(16)
Text('收藏膜色')
.fontSize(14)
.fontColor('#263238')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('28 款')
.fontSize(10)
.fontColor('#B0BEC5')
Text('›')
.fontSize(20)
.fontColor('#B0BEC5')
.margin({ left: 8 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = this.mainList[0]
this.showDetailModal = true
})
Row() {
Text('📍')
.fontSize(16)
Text('地址管理')
.fontSize(14)
.fontColor('#263238')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('2 个常用地址')
.fontSize(10)
.fontColor('#B0BEC5')
Text('›')
.fontSize(20)
.fontColor('#B0BEC5')
.margin({ left: 8 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.showShopModal = true
})
Row() {
Text('🎧')
.fontSize(16)
Text('客服中心')
.fontSize(14)
.fontColor('#263238')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('9:00 - 21:00 在线')
.fontSize(10)
.fontColor('#B0BEC5')
Text('›')
.fontSize(20)
.fontColor('#B0BEC5')
.margin({ left: 8 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.currentTab = 5
})
Row() {
Text('⚙')
.fontSize(16)
Text('设置')
.fontSize(14)
.fontColor('#263238')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('v3.0.1')
.fontSize(10)
.fontColor('#B0BEC5')
Text('›')
.fontSize(20)
.fontColor('#B0BEC5')
.margin({ left: 8 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.showBuyModal = true
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
}
// 弹框1 预约贴膜 底部弹出
@Builder addModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
Text('预约贴膜')
.fontSize(18)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('选择膜品')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.colorChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipA === idx ? '#FFFFFF' : '#8E24AA')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipA === idx ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipA = idx })
}, (c: string) => c)
}
.width('100%')
Text('施工部位')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.partChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipB === idx ? '#FFFFFF' : '#00BFA5')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipB === idx ? '#00BFA5' : '#E0F2F1')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipB = idx })
}, (c: string) => c)
}
.width('100%')
Text('联系手机号')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
TextInput({ placeholder: '请输入 11 位手机号', text: this.phoneText })
.onChange((value: string) => { this.phoneText = value })
.width('100%')
.height(44)
.padding(10)
.borderRadius(10)
.backgroundColor('#F5F5F7')
.fontSize(13)
Text('提交预约')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(46)
.borderRadius(23)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
.onClick(() => { this.showAddModal = false })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { this.showAddModal = false })
}
// 弹框2 车衣购买 底部弹出
@Builder buyModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
Text(this.selName())
.fontSize(17)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('单价 ¥' + this.selPrice() + ' / 套 · 限购 3 套')
.fontSize(11)
.fontColor('#B0BEC5')
.width('100%')
Text('选择规格')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.specChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipA === idx ? '#FFFFFF' : '#8E24AA')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipA === idx ? '#8E24AA' : '#F3E5F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipA = idx })
}, (c: string) => c)
}
.width('100%')
Text('购买数量')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Row({ space: 14 }) {
Text('-')
.fontSize(16)
.fontColor('#8E24AA')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#F3E5F5')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum > 1) {
this.stepNum -= 1
}
})
Text(this.stepNum + ' 套')
.fontSize(14)
.fontColor('#263238')
.width(60)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(16)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum < 3) {
this.stepNum += 1
}
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Text('合计')
.fontSize(13)
.fontColor('#90A4AE')
Column().layoutWeight(1)
Text('¥' + Math.floor(this.selPrice() * this.stepNum))
.fontSize(26)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text('立即购买')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(46)
.borderRadius(23)
.backgroundColor('#00BFA5')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
.onClick(() => { this.showBuyModal = false })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { this.showBuyModal = false })
}
// 弹框3 门店预约 底部弹出
@Builder shopModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
Text('门店预约')
.fontSize(18)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('选择门店')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.storeChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipA === idx ? '#FFFFFF' : '#263238')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipA === idx ? '#263238' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipA = idx })
}, (c: string) => c)
}
.width('100%')
Text('选择时段')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.timeChips, (c: string, idx: number) => {
Text(c)
.fontSize(12)
.fontColor(this.chipB === idx ? '#FFFFFF' : '#00BFA5')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.chipB === idx ? '#00BFA5' : '#E0F2F1')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.chipB = idx })
}, (c: string) => c)
}
.width('100%')
Text('施工工位(限 1 - 2 位)')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Row({ space: 14 }) {
Text('-')
.fontSize(16)
.fontColor('#263238')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#ECEFF1')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum > 1) {
this.stepNum -= 1
}
})
Text(this.stepNum + ' 位')
.fontSize(14)
.fontColor('#263238')
.width(60)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(16)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#263238')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.stepNum < 2) {
this.stepNum += 1
}
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text('确认预约工位')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(46)
.borderRadius(23)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
.onClick(() => { this.showShopModal = false })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { this.showShopModal = false })
}
// 弹框4 删除确认 居中小卡
@Builder deleteModal() {
Column() {
Column({ space: 16 }) {
Column({ space: 10 }) {
Text('⚠️')
.fontSize(30)
Text('确认删除该预约记录?')
.fontSize(15)
.fontColor('#D32F2F')
.fontWeight(FontWeight.Bold)
Text('删除后无法恢复,请谨慎操作')
.fontSize(11)
.fontColor('#B0BEC5')
}
.width('100%')
.padding({ top: 22, bottom: 18 })
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor('#FFEBEE')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Row({ space: 12 }) {
Text('取消')
.fontSize(14)
.fontColor('#78909C')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F7')
.textAlign(TextAlign.Center)
.onClick(() => { this.showDeleteModal = false })
Text('删除')
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#E53935')
.textAlign(TextAlign.Center)
.onClick(() => { this.showDeleteModal = false })
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('72%')
.borderRadius(12)
.backgroundColor('#FFFFFF')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.showDeleteModal = false })
}
// 弹框5 详情 右侧滑出全高面板
@Builder detailModal() {
Column() {
Column() {
Column({ space: 8 }) {
Text('🎨')
.fontSize(52)
Text('正品授权 · 假一赔十')
.fontSize(10)
.fontColor('#E1BEE7')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(0,0,0,0.25)')
}
.width('100%')
.height(180)
.backgroundColor('#4A148C')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 12 }) {
Text(this.selName())
.fontSize(18)
.fontColor('#263238')
.fontWeight(FontWeight.Bold)
.width('100%')
Text('★★★★★')
.fontSize(14)
.fontColor('#FFB300')
.width('100%')
Text('¥' + this.selPrice())
.fontSize(28)
.fontColor('#8E24AA')
.fontWeight(FontWeight.Bold)
.width('100%')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column({ space: 6 }) {
Row() {
Column()
.width(this.selScore() + '%')
.height(6)
.borderRadius(3)
.backgroundColor('#8E24AA')
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#F3E5F5')
Text('好评率 ' + this.selScore() + '% · 已服务 452 台车')
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
Text('适用部位')
.fontSize(12)
.fontColor('#8E24AA')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.applyChips, (c: string) => {
Text(c)
.fontSize(11)
.fontColor('#263238')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor('#ECEFF1')
.margin({ right: 8, bottom: 8 })
}, (c: string) => c)
}
.width('100%')
Text('进口基材 · 支持全车 / 局部施工 · 完工免费镀晶一次')
.fontSize(10)
.fontColor('#90A4AE')
.width('100%')
.lineHeight(16)
}
.width('100%')
.padding(16)
Column().layoutWeight(1)
Text('立即预约')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor('#8E24AA')
.textAlign(TextAlign.Center)
.margin({ left: 16, right: 16, bottom: 20 })
.onClick(() => {
this.showDetailModal = false
this.showAddModal = true
})
}
.width('80%')
.height('100%')
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.End)
.onClick(() => { this.showDetailModal = false })
}
build() {
Stack() {
Column() {
this.headerBuilder()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tab0()
} else if (this.currentTab === 1) {
this.tab1()
} else if (this.currentTab === 2) {
this.tab2()
} else if (this.currentTab === 3) {
this.tab3()
} else if (this.currentTab === 4) {
this.tab4()
} else if (this.currentTab === 5) {
this.tab5()
} else {
this.tab6()
}
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.backgroundColor('#F5F5F7')
.edgeEffect(EdgeEffect.Spring)
this.bottomBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F7')
if (this.showAddModal) {
this.addModal()
}
if (this.showBuyModal) {
this.buyModal()
}
if (this.showShopModal) {
this.shopModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
if (this.showDetailModal) {
this.detailModal()
}
}
.width('100%')
.height('100%')
}
}
总结

本文详细剖析了一款基于HarmonyOS ArkTS声明式UI框架构建的汽车贴膜改色工坊应用页面的完整实现。该应用以单文件结构实现了七大功能模块和五种弹窗交互,通过@State状态驱动机制实现了响应式数据流,利用@Builder方法封装实现了UI组件的高度复用。从数据模型定义、辅助方法设计、头部底部导航、七个Tab页面到五类弹窗系统,每个模块都遵循了声明式编程的核心范式——开发者只需描述UI与状态的映射关系,框架自动处理状态变更后的视图刷新。
在数据可视化方面,应用通过ForEach遍历配合归一化算法实现了三种柱状图(车衣厚度对比、隔热率排行、门店评分一览),通过Scroll组件实现了隔热膜横向滑动列表和施工门店列表。在交互设计方面,弹窗系统采用了底部弹出(预约、购买、门店预约)、居中警示(删除确认)和右侧滑出(详情)三种模式,通过Stack容器的层叠渲染和条件判断实现了弹窗的灵活管理。芯片选择器、步进器和输入框等表单组件通过chipA、chipB、stepNum、phoneText等状态变量实现了双向数据绑定。
从工程实践角度来看,该应用的设计体现了HarmonyOS ArkTS开发中的多项最佳实践:使用统一的GoodsItem接口降低数据结构复杂度、通过防御性编程方法确保空值安全、利用脉冲缩放动画增强关键信息的视觉吸引力、采用stopPropagation精确控制事件冒泡行为。这些设计模式和技术实践对于从事HarmonyOS应用开发的工程师具有重要的参考价值,也为汽车后市场服务类应用的UI架构设计提供了可借鉴的实现方案。
更多推荐



所有评论(0)