Grid组件是ArkTS提供的网格布局容器,columnsTemplate(‘1fr 1fr‘)定义了两列等宽的网格模板——1fr表示一份可用空间,两个1fr即两列各占50%宽度
一、引言:美妆电商的移动端技术演进
在当今移动互联网时代,美妆电商行业正经历着前所未有的蓬勃发展。根据行业数据统计,全球美妆市场规模已突破五千亿美元,而其中线上渠道的占比正在以每年两位数的速度持续增长。越来越多的消费者选择在移动端完成从浏览、比价到下单的完整购物流程,这对移动端应用的用户体验、性能表现和功能完整性提出了极高的要求。

美妆类电商应用与普通电商应用相比有着显著的特殊性。首先,美妆产品高度依赖视觉呈现,用户在购买前需要直观地感受产品的色彩、质感和包装设计,这就要求应用在 UI 层面具备极强的视觉表现力。其次,美妆产品的分类体系复杂,从护肤、彩妆到香水、面膜,每个品类下又有细分的子类别,需要清晰的分类导航体系来引导用户。此外,色号选择是美妆购物中极具特色的一环,同一款口红可能有数十种色号,如何让用户便捷地浏览和选择色号,是提升转化率的关键因素之一。
华为 HarmonyOS 作为新一代分布式操作系统,其应用开发框架 ArkTS 提供了声明式 UI 编程范式,让开发者能够以更加简洁、高效的方式构建复杂的用户界面。ArkTS 基于 TypeScript 语言扩展,在保留了类型安全特性的同时,引入了状态管理、组件化构建等响应式编程能力。通过 @Component、@Builder、@State 等装饰器,开发者可以轻松实现数据驱动视图的更新机制,无需手动操作 DOM 节点。
本文将对一个完整的美妆商城应用进行逐段、逐行的深度技术解析。该应用涵盖了首页推荐、商品分类、购物车管理、收藏夹、个人中心五大核心功能模块,以及加入购物车弹窗、写评价弹窗、商品详情弹窗等多个交互场景。通过本文的详细剖析,读者将深入理解 HarmonyOS ArkTS 的组件化架构思想、状态管理机制、布局系统以及各种 UI 组件的使用方法,从而能够举一反三地构建出属于自己的优质移动端应用。
二、接口定义层:类型安全的数据契约
2.1 色彩配置接口
在任何 UI 驱动的应用中,颜色管理都是最基础也是最重要的环节之一。良好的颜色管理体系能够确保整个应用在视觉上保持一致性,同时也便于后续的主题切换和品牌色调整。
interface ColorPalette {
primary: string;
light: string;
bg: string;
card: string;
textPrimary: string;
textSecondary: string;
border: string;
sale: string;
gold: string;
new: string;
}

这段代码定义了一个名为 ColorPalette 的接口,它描述了应用中使用的完整色彩体系。让我们逐行分析每个字段的含义和用途:
primary:主色调,在美妆类应用中通常选择粉红色系,这里用于按钮、高亮文字、导航栏选中态等核心视觉元素。light:浅色调,作为主色的辅助色,用于渐变效果的过渡色或浅色背景。bg:页面背景色,通常是非常浅的粉色,为整个应用营造柔和的视觉基调。card:卡片背景色,这里使用纯白色,确保商品卡片在浅色背景上有清晰的层次感。textPrimary:主要文字颜色,使用深粉色,既保持了品牌一致性又确保了足够的对比度。textSecondary:次要文字颜色,用于辅助信息的展示。border:边框颜色,与浅色调一致,营造细腻的分隔效果。sale:促销标签颜色,使用高饱和度的红色来吸引注意力。gold:评分星星颜色,金色是评分系统的通用色彩语言。new:新品标签颜色,使用绿色传达"新鲜"的语义。
2.2 商品分类元数据接口
interface ProductCategoryMeta {
id: string;
name: string;
icon: string;
desc: string;
}

ProductCategoryMeta 接口定义了商品分类的元数据结构。其中 id 字段是分类的唯一标识符,采用字符串类型而非数字,这样可以使用语义化的命名(如 'skincare'、'makeup'),提高代码可读性。name 字段存储分类的中文名称,icon 字段使用 emoji 字符作为分类图标,这是一种轻量级的图标方案,无需引入额外的图片资源。desc 字段存储分类的简短描述语,用于在界面上提供额外的上下文信息。
2.3 品牌元数据接口
interface BrandMeta {
id: string;
name: string;
icon: string;
desc: string;
}

BrandMeta 接口与分类元数据接口结构相同,但语义不同。这里的 id 采用品牌英文名称的小写形式(如 'dior'、'chanel'),name 保留品牌的官方名称(包括大小写和特殊字符,如 'Estée Lauder'),desc 存储品牌的简短定位描述(如"法式优雅"、“经典永恒”)。
2.4 肤质信息接口
interface SkinTypeMeta {
id: string;
label: string;
value: string;
}

SkinTypeMeta 接口用于描述用户的肤质档案信息。这是美妆类应用中非常独特的功能模块——通过记录用户的肤质类型、肤色和护肤关注点,可以为用户提供个性化的商品推荐。label 字段是显示标签(如"肤质"、“肤色”),value 字段是具体的值(如"混合偏干"、“自然偏白”)。
2.5 评价标签接口
interface ReviewTagMeta {
id: string;
label: string;
count: number;
}

ReviewTagMeta 接口定义了用户评价的标签体系。在美妆商品的评价中,标签化的评价方式能够让其他消费者快速了解商品的口碑特点。label 存储标签文本(如"持久不脱妆"、“显色度高”),count 记录该标签被使用的次数,这个数据可以用于标签的排序和展示热度。
2.6 核心商品数据接口
interface BeautyProduct {
id: string;
name: string;
brand: string;
category: string;
price: number;
originalPrice: number;
rating: number;
reviewCount: number;
soldCount: number;
coverColor: string;
coverColor2: string;
tags: string[];
isHot: boolean;
isNew: boolean;
isOnSale: boolean;
discount: number;
description: string;
shadeColors: string[];
volume: string;
isFavorite: boolean;
}

这是整个应用中最核心的数据接口,定义了美妆商品的完整数据模型。让我们逐字段深入分析:
id:商品唯一标识,使用'p1'、'p2'这样的命名约定,简洁且易于引用。name:商品名称,使用中文命名,如"烈艳蓝金唇膏"、“小黑瓶精华”。brand:品牌名称,直接存储品牌的显示名称。category:商品所属分类,如"口红"、“精华”、"彩妆"等。price:当前售价,使用number类型精确到分。originalPrice:原价,用于显示划线价和计算折扣力度。rating:评分,浮点数类型,支持小数点后一位(如 4.9)。reviewCount:评价总数,反映商品的受欢迎程度。soldCount:销量数据,展示商品的市场表现。coverColor和coverColor2:封面渐变色的起始色和结束色。这是一个非常巧妙的设计——由于应用不使用真实图片,而是通过渐变色块来模拟商品封面,这两个字段定义了每个商品的独特色彩组合。tags:标签数组,存储商品的特性标签。isHot、isNew、isOnSale:三个布尔值标记,分别标识热销、新品和促销状态。discount:折扣值,如 8 表示 8 折。description:商品描述文本。shadeColors:色号颜色数组,存储该商品所有可选色号的颜色值。这是美妆应用的核心特色字段。volume:规格信息,如"3.5g"、“50ml”。isFavorite:是否已收藏,用于收藏状态的同步管理。
2.7 分类项接口
interface CategoryItem {
id: string;
name: string;
icon: string;
color: string;
bg: string;
count: number;
subCategories: string[];
}

CategoryItem 接口定义了分类页面中每个分类项的完整数据。与前文的 ProductCategoryMeta 不同,这个接口用于分类页面的具体展示,包含了更多的视觉属性。color 和 bg 分别定义了分类的主色和背景色,每个分类都有自己独特的色彩主题。count 字段记录该分类下的商品数量,subCategories 是子分类名称数组,用于展示更细粒度的分类导航。
2.8 购物车项接口
interface CartItem {
id: string;
productId: string;
productName: string;
brand: string;
price: number;
quantity: number;
coverColor: string;
coverColor2: string;
shade: string;
isSelected: boolean;
stock: number;
}

CartItem 接口定义了购物车中商品的数据结构。这里有几个关键字段值得注意:productId 建立了购物车项与商品数据的关联;shade 字段存储用户选择的色号信息(如"色号#999"),这是美妆购物车区别于普通电商的重要特征;isSelected 布尔值控制该商品是否被选中参与结算;stock 字段记录库存数量,用于限制用户购买数量上限。
2.9 评价数据接口
interface ReviewItem {
id: string;
productId: string;
author: string;
avatar: string;
rating: number;
content: string;
date: string;
images: string[];
likes: number;
isVerified: boolean;
}

ReviewItem 接口定义了用户评价的完整数据模型。avatar 字段使用 emoji 字符作为用户头像,这是一种极简但有效的方案。images 数组存储评价图片的颜色值(而非真实图片 URL),与应用整体的渐变色块设计风格保持一致。isVerified 布尔值标识是否为已验证购买者的评价,这在电商应用中是建立评价可信度的重要机制。
2.10 订单记录与销售数据接口
interface OrderRecord {
id: string;
orderNo: string;
date: string;
status: string;
total: number;
count: number;
}
interface WeeklySaleData {
day: string;
sales: number;
}

OrderRecord 接口定义了订单历史记录的数据结构,包含订单号、日期、状态、总金额和商品数量等关键信息。WeeklySaleData 接口用于周销售趋势图表的数据展示,每天包含星期标签和销售数量两个字段,是数据可视化的基础数据结构。
三、颜色配置与静态配置体系
3.1 全局颜色配置
const COLORS: Record<string, string> = {
'primary': '#E91E63',
'light': '#F8BBD0',
'bg': '#FCE4EC',
'card': '#FFFFFF',
'textPrimary': '#880E4F',
'textSecondary': '#E91E63',
'border': '#F8BBD0',
'sale': '#FF1744',
'gold': '#FFD700',
'new': '#4CAF50',
'white': '#FFFFFF',
'gray': '#9E9E9E',
'grayLight': '#E0E0E0',
'grayBg': '#F5F5F5',
'textDark': '#333333',
'textSub': '#666666',
};

COLORS 是一个全局常量对象,使用 Record<string, string> 类型声明,表示一个从字符串键到字符串值的映射。这种设计模式将所有颜色值集中管理,带来了几个显著的优势:
第一,一致性保障。整个应用中所有组件引用同一套颜色定义,避免了散落在各处的硬编码颜色值导致的不一致问题。第二,可维护性。当需要调整品牌色或适配深色模式时,只需修改这一处配置即可全局生效。第三,可读性。使用 COLORS['primary'] 比直接写 '#E91E63' 更具语义化,代码阅读者能立即理解颜色的用途。
从配色方案来看,这套色彩体系以粉红色(#E91E63,Material Design 的 Pink 500)为核心,搭配浅粉色背景和白色卡片,营造出了美妆应用特有的柔美、精致的视觉氛围。深粉色(#880E4F)用于文字和渐变终止色,增加了视觉层次。促销红色(#FF1744)、金色(#FFD700)和绿色(#4CAF50)分别用于特定场景的状态标识。
3.2 分类元数据配置
const CATEGORY_METAS: Record<string, ProductCategoryMeta> = {
'skincare': { id: 'skincare', name: '护肤', icon: '🧴', desc: '滋养每一寸肌肤' },
'makeup': { id: 'makeup', name: '彩妆', icon: '💄', desc: '绽放你的美丽' },
'perfume': { id: 'perfume', name: '香水', icon: '🌸', desc: '独特的香氛记忆' },
'mask': { id: 'mask', name: '面膜', icon: '🎭', desc: '密集修护焕亮' },
'lipstick': { id: 'lipstick', name: '口红', icon: '💋', desc: '一抹倾心' },
'essence': { id: 'essence', name: '精华', icon: '✨', desc: '深层焕颜修护' },
'cream': { id: 'cream', name: '面霜', icon: '🫙', desc: '锁住水润光泽' },
'sunscreen': { id: 'sunscreen', name: '防晒', icon: '☀️', desc: '阳光下的守护' },
};

CATEGORY_METAS 定义了八大商品分类的元数据。每个分类都包含语义化的 ID、中文名称、emoji 图标和诗意的描述语。这种使用 emoji 作为图标的方案在实际开发中是一个值得讨论的权衡:优点是零资源依赖、跨平台一致性高、色彩丰富;缺点是无法精细控制图标的样式和动画效果。对于原型开发或轻量级应用来说,这是一个非常高效的选择。
3.3 品牌与肤质配置
const BRAND_METAS: Record<string, BrandMeta> = {
'dior': { id: 'dior', name: 'Dior', icon: '🌹', desc: '法式优雅' },
'chanel': { id: 'chanel', name: 'Chanel', icon: '👜', desc: '经典永恒' },
'lancome': { id: 'lancome', name: 'Lancome', icon: '🌸', desc: '玫瑰之美' },
'esteelauder': { id: 'esteelauder', name: 'Estée Lauder', icon: '💎', desc: '奢华护肤' },
'skii': { id: 'skii', name: 'SK-II', icon: '⭐', desc: '晶莹剔透' },
'ysl': { id: 'ysl', name: 'YSL', icon: '👑', desc: '前卫时尚' },
};
const SKIN_TYPE_METAS: Record<string, SkinTypeMeta> = {
'type': { id: 'type', label: '肤质', value: '混合偏干' },
'tone': { id: 'tone', label: '肤色', value: '自然偏白' },
'concern': { id: 'concern', label: '关注', value: '保湿·抗初老' },
};
const REVIEW_TAG_METAS: Record<string, ReviewTagMeta> = {
't1': { id: 't1', label: '持久不脱妆', count: 1280 },
't2': { id: 't2', label: '显色度高', count: 960 },
't3': { id: 't3', label: '滋润不拔干', count: 850 },
't4': { id: 't4', label: '性价比高', count: 720 },
't5': { id: 't5', label: '包装精美', count: 630 },
};
品牌配置涵盖了六大国际一线美妆品牌,每个品牌都配有独特的 emoji 图标和简短的品牌定位描述。肤质配置定义了用户的三个肤质维度。评价标签配置定义了五个常用的评价标签及其使用次数,这些次数数据可以用于标签的热度排序——使用次数越多的标签排在越前面,帮助用户快速了解商品最受好评的特点。
四、数据模型类与响应式状态
4.1 可观察的商品数据类
@Observed
class ProductData {
id: string = '';
name: string = '';
brand: string = '';
category: string = '';
price: number = 0;
originalPrice: number = 0;
rating: number = 0;
reviewCount: number = 0;
soldCount: number = 0;
coverColor: string = '#E91E63';
coverColor2: string = '#F8BBD0';
tags: string[] = [];
isHot: boolean = false;
isNew: boolean = false;
isOnSale: boolean = false;
discount: number = 0;
description: string = '';
shadeColors: string[] = [];
volume: string = '';
isFavorite: boolean = false;
}
@Observed 装饰器是 ArkTS 状态管理体系中的重要组成部分。被 @Observed 装饰的类会成为"可观察对象",这意味着当该类的实例属性发生变化时,框架能够自动检测到变化并触发与之绑定的 UI 组件重新渲染。
这个 ProductData 类与前面定义的 BeautyProduct 接口在字段结构上完全一致,但有关键的区别:接口只是类型声明,不包含实际值也不具备响应式能力;而 @Observed 类则为每个字段提供了默认值,并且赋予了数据变化自动驱动视图更新的能力。
值得注意的是,coverColor 和 coverColor2 字段的默认值分别设为主色和浅色,确保即使数据未完全初始化,UI 也能以合理的颜色渲染。所有布尔值默认为 false,数值默认为 0,字符串默认为空字符串,数组默认为空数组——这些都是安全的默认值,避免了 undefined 或 null 导致的运行时错误。
4.2 可观察的购物车数据类
@Observed
class CartData {
id: string = '';
productId: string = '';
productName: string = '';
brand: string = '';
price: number = 0;
quantity: number = 1;
coverColor: string = '#E91E63';
coverColor2: string = '#F8BBD0';
shade: string = '';
isSelected: boolean = true;
stock: number = 99;
}
CartData 类同样使用了 @Observed 装饰器。在购物车场景中,响应式数据尤为重要——当用户修改商品数量、切换选中状态时,底部的合计金额和已选数量需要实时更新。quantity 默认值为 1,isSelected 默认值为 true(新加入购物车的商品默认选中),stock 默认值为 99(一个足够大的库存数,避免频繁触发库存限制)。
五、主组件架构与状态管理
5.1 组件声明与状态变量
@Entry
@Component
struct BeautyShopping {
@State currentTab: number = 0
@State showAddCartModal: boolean = false
@State showReviewModal: boolean = false
@State showRemoveFavModal: boolean = false
@State showProductDetail: boolean = false
@State selectedProductIndex: number = 0
@State selectedShadeIndex: number = 0
@State reviewRating: number = 5
@State reviewText: string = ''
@State removeFavIndex: number = 0
@State cartTotal: number = 0
@State selectedCartCount: number = 0
@State countdownHours: number = 8
@State countdownMinutes: number = 42
@State countdownSeconds: number = 15
这段代码是整个应用的架构核心。@Entry 装饰器标识这是应用的入口组件,@Component 装饰器声明这是一个自定义组件。struct 关键字定义了组件的结构体,在 ArkTS 中,组件以结构体的形式组织。
@State 装饰器用于声明组件的内部状态变量。当 @State 修饰的变量值发生变化时,框架会自动重新调用 build() 方法中依赖该变量的部分进行 UI 更新。让我们逐一分析每个状态变量的作用:
currentTab:当前激活的标签页索引,0-4 分别对应首页、分类、购物车、收藏、我的。初始值为 0,即应用启动时默认显示首页。showAddCartModal、showReviewModal、showRemoveFavModal、showProductDetail:四个布尔值,分别控制四种弹窗的显示与隐藏。这种"每个弹窗一个独立状态"的设计简洁明了,便于精确控制。selectedProductIndex:当前选中的商品在商品数组中的索引,用于商品详情弹窗和加入购物车弹窗中定位具体商品。selectedShadeIndex:当前选中的色号索引,用于色号选择的交互。reviewRating:用户在写评价时选择的评分,默认 5 分(满分)。reviewText:用户输入的评价文本内容。removeFavIndex:待移除收藏的商品索引。cartTotal:购物车选中商品的总价,实时计算更新。selectedCartCount:购物车中选中商品的总件数。countdownHours、countdownMinutes、countdownSeconds:闪购倒计时的时、分、秒,初始值分别为 8、42、15。
5.2 商品数据集合
商品数据是应用的核心数据源,包含了 22 个商品对象,覆盖了口红、精华、彩妆、面霜、香水、面膜、防晒、护肤等多个品类。每个商品对象都严格按照 BeautyProduct 接口的结构定义,包含完整的价格、评分、销量、色号等信息。
private products: BeautyProduct[] = [
{ id: 'p1', name: '烈艳蓝金唇膏', brand: 'Dior', category: '口红',
price: 350, originalPrice: 420, rating: 4.9, reviewCount: 2380,
soldCount: 15600, coverColor: '#C2185B', coverColor2: '#E91E63',
tags: ['热销', '持久'], isHot: true, isNew: false, isOnSale: true,
discount: 8, description: '经典丝绒质感唇膏,显色饱满持久',
shadeColors: ['#880E4F', '#C2185B', '#E91E63', '#F06292'],
volume: '3.5g', isFavorite: true },
// ... 更多商品数据
];
以第一个商品"烈艳蓝金唇膏"为例来详细分析:该商品属于 Dior 品牌的口红品类,售价 350 元(原价 420 元,相当于 8 折优惠)。评分高达 4.9 分,累计评价 2380 条,销量达到 15600 件。封面使用深粉色到亮粉色的渐变(#C2185B 到 #E91E63),配有"热销"和"持久"两个标签。它同时标记为热销商品和促销商品。提供了四种色号选择,颜色从深紫红到浅粉红递进。规格为 3.5g,已被当前用户收藏。
这种数据设计有几个值得学习的要点:每个商品的 coverColor 和 coverColor2 都经过精心选择,与商品本身的品类和品牌调性相符——口红类用红色系、精华类用紫色或蓝色系、香水类用金色系,这种色彩语义化设计让用户即使没有看到真实图片也能对商品有直观的感受。
5.3 分类、购物车与收藏数据
private categories: CategoryItem[] = [
{ id: 'c1', name: '护肤', icon: '🧴', color: '#E91E63', bg: '#FCE4EC',
count: 320, subCategories: ['洁面', '爽肤水', '精华', '面霜', '眼霜', '面膜'] },
// ... 更多分类
];
分类数据包含 8 个主分类,每个分类配有独立的主题色和背景色,以及 6 个子分类。例如"护肤"分类下包含洁面、爽肤水、精华、面霜、眼霜、面膜六个子分类,覆盖了完整的护肤流程。count 字段表示该分类下有 320 款商品,这个数字虽然在此应用中是静态的,但在真实场景中应从后端接口动态获取。
private cartItems: CartItem[] = [
{ id: 'cart1', productId: 'p1', productName: '烈艳蓝金唇膏', brand: 'Dior',
price: 350, quantity: 1, coverColor: '#C2185B', coverColor2: '#E91E63',
shade: '色号#999', isSelected: true, stock: 50 },
// ... 更多购物车项
];
购物车数据包含 12 个商品项,每个项都通过 productId 与商品列表建立关联。shade 字段记录了用户选择的具体色号(如"色号#999"),这是美妆购物车的特色字段。stock 字段设置了每个商品的库存上限,当用户增加数量时会进行校验。
收藏数据包含 15 个商品,结构与商品数据一致,但 isFavorite 字段全部为 true。评价数据包含 4 条用户评价,每条评价都包含作者、头像、评分、内容、日期、图片色值数组和点赞数。周销售数据包含 7 天的销售数据,用于首页的销售趋势图表展示。订单数据包含 3 条历史订单记录。
5.4 辅助计算方法
private calcCartTotal(): number {
let total: number = 0;
for (let i = 0; i < this.cartItems.length; i++) {
if (this.cartItems[i].isSelected) {
total += this.cartItems[i].price * this.cartItems[i].quantity;
}
}
return total;
}
calcCartTotal() 方法计算购物车中所有选中商品的总价。它遍历 cartItems 数组,对每个 isSelected 为 true 的商品项,将其 price 与 quantity 相乘后累加到 total 变量中。这个方法在用户切换选中状态或修改数量时被调用,确保底部结算栏显示的金额始终准确。
private calcSelectedCount(): number {
let count: number = 0;
for (let i = 0; i < this.cartItems.length; i++) {
if (this.cartItems[i].isSelected) {
count += this.cartItems[i].quantity;
}
}
return count;
}
calcSelectedCount() 方法的逻辑与总价计算类似,但它累加的是选中商品的数量而非金额。这个值显示在结算栏的"已选 X 件"文本中。
private getProductById(id: string): BeautyProduct {
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === id) {
return this.products[i];
}
}
return this.products[0];
}
getProductById() 方法通过商品 ID 查找商品对象。如果找不到匹配的 ID,则返回数组中的第一个商品作为默认值。这种"安全回退"的设计避免了返回 undefined 可能导致的空指针异常。
六、主构建方法与布局架构
6.1 build 方法与整体布局
build() {
Stack({ alignContent: Alignment.TopStart }) {
Column() {
if (this.currentTab === 0) {
this.homeTab()
} else if (this.currentTab === 1) {
this.categoryTab()
} else if (this.currentTab === 2) {
this.cartTab()
} else if (this.currentTab === 3) {
this.favoriteTab()
} else {
this.profileTab()
}
this.bottomTabBar()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS['bg'])
if (this.showAddCartModal || this.showReviewModal ||
this.showRemoveFavModal || this.showProductDetail) {
this.modalOverlay()
}
}
.width('100%')
.height('100%')
}
build() 方法是每个 ArkTS 组件的核心,它定义了组件的 UI 结构。这里使用了 Stack 作为最外层容器,Stack 是一种层叠布局容器,子元素按照声明顺序从下到上层叠排列。
alignContent: Alignment.TopStart 参数设置子元素的对齐方式为左上角对齐。在 Stack 内部,首先是一个 Column 容器,它包含了两个部分:根据 currentTab 状态值条件渲染的五个标签页内容构建器,以及始终显示的底部导航栏。
这种通过 if-else 条件语句来切换标签页内容的方式简洁直观。当 currentTab 的值改变时,@State 装饰器会触发 build() 方法重新执行,自动切换到对应的标签页内容。
在 Column 之上,Stack 的层叠特性使得 modalOverlay() 能够覆盖在所有内容之上。模态遮罩的显示条件是四个弹窗状态中任意一个为 true,使用逻辑或运算符 || 连接。这种设计确保了任何时候最多只有一个弹窗类型的遮罩层显示(虽然多个状态可以同时为 true,但在实际交互中它们是互斥的)。
外层 Stack 设置了 width('100%') 和 height('100%'),确保组件占满整个屏幕空间。内层 Column 也设置了相同的尺寸,并使用 COLORS['bg'](浅粉色 #FCE4EC)作为背景色,为整个应用奠定了柔和的视觉基调。
6.2 模态遮罩系统
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Center }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(136,14,79,0.5)')
.onClick(() => {
this.showAddCartModal = false;
this.showReviewModal = false;
this.showRemoveFavModal = false;
this.showProductDetail = false;
})
if (this.showAddCartModal) {
this.addCartModalContent()
}
if (this.showReviewModal) {
this.reviewModalContent()
}
if (this.showRemoveFavModal) {
this.removeFavModalContent()
}
if (this.showProductDetail) {
this.productDetailModalContent()
}
}
.width('100%')
.height('100%')
}
@Builder 装饰器用于声明一个构建器方法,它可以包含 UI 组件的声明,类似于一个轻量级的子组件。modalOverlay() 构建器实现了模态遮罩的通用框架。
Stack 设置了居中对齐(Alignment.Center),使得弹窗内容在屏幕中央显示。首先声明的是一个全屏的 Column,它作为半透明遮罩背景,使用 rgba(136,14,79,0.5) 颜色——这是深粉色(#880E4F)的半透明版本,50% 的透明度既能看到底下的内容,又能制造出聚焦弹窗的视觉效果。
遮罩背景绑定了 onClick 事件,点击遮罩区域会将所有四个弹窗状态设为 false,关闭当前弹窗。这是移动端弹窗交互的标准模式——点击遮罩区域关闭弹窗。
在遮罩背景之上,通过四个独立的 if 条件语句分别渲染对应的弹窗内容。虽然这种写法看起来有四个独立的判断,但由于实际使用中只有一个状态为 true,所以只会渲染一个弹窗。这种设计的好处是各弹窗之间完全解耦,添加或移除弹窗不会影响其他弹窗的逻辑。
6.3 底部导航栏
@Builder
bottomTabBar() {
Row() {
ForEach([0, 1, 2, 3, 4], (tabIndex: number) => {
Column() {
Text(this.getTabIcon(tabIndex))
.fontSize(24)
Text(this.getTabName(tabIndex))
.fontSize(10)
.fontColor(this.currentTab === tabIndex ? COLORS['primary'] : COLORS['gray'])
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = tabIndex;
})
})
}
.width('100%')
.height(56)
.backgroundColor(COLORS['card'])
.border({ width: { top: 1 }, color: COLORS['border'], radius: 0, style: BorderStyle.Solid })
.justifyContent(FlexAlign.SpaceAround)
.alignItems(VerticalAlign.Center)
}
底部导航栏是移动端应用的核心导航组件。这里使用 Row 作为水平容器,通过 ForEach 遍历 [0, 1, 2, 3, 4] 数组生成五个标签按钮。
每个标签按钮是一个 Column,包含图标 Text 和名称 Text 两个子元素。layoutWeight(1) 让五个按钮等分导航栏的宽度。名称文字的颜色通过三元表达式动态设置:当前选中的标签使用主色(COLORS['primary']),未选中的使用灰色(COLORS['gray']),实现了清晰的状态视觉反馈。
onClick 事件将 currentTab 设置为被点击的标签索引,触发 @State 的响应式更新,自动切换标签页内容。
导航栏整体高度为 56 像素(这是一个符合触控友好设计的最小高度),背景色为白色(COLORS['card']),顶部有 1 像素的浅色边框线作为分隔。justifyContent(FlexAlign.SpaceAround) 让五个标签均匀分布。
private getTabIcon(index: number): string {
if (index === 0) return '💄';
if (index === 1) return '📂';
if (index === 2) return '🛒';
if (index === 3) return '❤️';
return '👤';
}
private getTabName(index: number): string {
if (index === 0) return '首页';
if (index === 1) return '分类';
if (index === 2) return '购物车';
if (index === 3) return '收藏';
return '我的';
}
这两个辅助方法分别返回标签的图标和名称。使用 emoji 作为图标,与整个应用的视觉风格保持一致。五个标签分别是首页(口红图标)、分类(文件夹图标)、购物车(购物车图标)、收藏(心形图标)和我的(人像图标),覆盖了美妆电商应用的核心功能入口。
七、首页标签页深度解析
7.1 首页整体结构
@Builder
homeTab() {
Scroll() {
Column() {
this.flashSaleBanner()
this.brandRow()
this.productGridSection()
this.recommendSection()
this.weeklySalesChart()
Column().height(20)
}
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
首页使用 Scroll 组件作为可滚动容器,内部是一个 Column 垂直布局,依次包含五个内容区块:闪购横幅、品牌快捷入口、商品网格、为你推荐和周销售图表。最后添加了一个高度为 20 的空 Column 作为底部留白,避免内容被底部导航栏遮挡。
scrollBar(BarState.Off) 隐藏了滚动条,让界面更加简洁。layoutWeight(1) 让 Scroll 占据除了底部导航栏之外的所有剩余空间。align(Alignment.TopStart) 设置内容从顶部开始排列。
7.2 闪购倒计时横幅
@Builder
flashSaleBanner() {
Column() {
Row() {
Text('💄 美妆商城')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Column().layoutWeight(1)
Text('🔍')
.fontSize(22)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
闪购横幅的顶部是搜索栏区域。左侧是应用标题"美妆商城",使用 22 号粗体白色字体;中间用一个 layoutWeight(1) 的空 Column 占据剩余空间,将搜索图标推到右侧;右侧是搜索图标。
Row() {
Column() {
Text('⚡ 限时闪购')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Text('精选美妆 低至5折')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row() {
Text('距结束')
.fontSize(11)
.fontColor('rgba(255,255,255,0.8)')
Text(this.countdownHours.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 4 })
Text(':')
.fontSize(14)
.fontColor(COLORS['card'])
.margin({ left: 2, right: 2 })
Text(this.countdownMinutes.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(':')
.fontSize(14)
.fontColor(COLORS['card'])
.margin({ left: 2, right: 2 })
Text(this.countdownSeconds.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
}
.width('100%')
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
}
闪购区域是横幅的核心部分,左侧显示"限时闪购"标题和"精选美妆 低至5折"副标题,右侧是倒计时显示。倒计时的设计非常精巧:每个数字(时、分、秒)都放在一个白色背景、圆角为 4 的小方块中,数字使用深粉色粗体,与白色背景形成强烈对比,数字之间用白色冒号分隔。这种"翻牌式"的倒计时设计是电商促销场景的经典 UI 模式,能够有效营造紧迫感,促使用户尽快下单。
整个横幅容器使用了从主色到深色的水平渐变背景(linearGradient),底部左右圆角为 24,配合向下的阴影效果,营造出了悬浮卡片式的立体视觉感受。shadow 属性的 color 使用了主色的半透明版本,让阴影带有粉色色调,与整体配色融为一体。
7.3 品牌快捷入口
@Builder
brandRow() {
Column() {
Text('🔥 热门品牌')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.alignSelf(ItemAlign.Start)
.margin({ left: 16, top: 16, bottom: 12 })
Scroll() {
Column() {
Row() {
ForEach(this.brandKeys, (key: string) => {
Column() {
Column() {
Text(BRAND_METAS[key].icon)
.fontSize(24)
}
.width(52)
.height(52)
.borderRadius(26)
.backgroundColor(COLORS['bg'])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 6, color: 'rgba(233,30,99,0.15)', offsetX: 0, offsetY: 2 })
Text(BRAND_METAS[key].name)
.fontSize(10)
.fontColor(COLORS['textPrimary'])
.margin({ top: 6 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.margin({ right: 12 })
.onClick(() => {
this.currentTab = 1;
})
})
}
.padding({ left: 16, right: 16 })
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
}
.width('100%')
}
品牌快捷入口区域展示六大国际美妆品牌的圆形图标。使用横向 Scroll 组件实现可滑动的品牌列表,这在品牌数量较多时尤为重要——用户可以通过左右滑动浏览所有品牌。
每个品牌图标是一个 52x52 的圆形容器(borderRadius(26)),背景使用浅粉色,内部居中显示品牌对应的 emoji 图标。圆形容器下方是品牌名称,使用 10 号字体,设置了 maxLines(1) 和 textOverflow 为省略号,确保长品牌名不会破坏布局。
点击品牌图标会将 currentTab 设为 1(跳转到分类页),这是品牌入口的常见交互模式——点击品牌后展示该品牌下的所有商品。阴影效果使用了粉色的半透明色,让品牌图标有了轻微的浮起感。
7.4 商品网格区域
@Builder
productGridSection() {
Column() {
Row() {
Text('🎁 精选好物')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
Column().layoutWeight(1)
Text('查看全部 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
Grid() {
ForEach(this.products, (product: BeautyProduct, index: number) => {
GridItem() {
this.productCard(product, index)
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding({ left: 16, right: 16 })
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
}
商品网格区域是首页的核心内容区域。标题行使用 Row 布局,左侧是"精选好物"标题,右侧是"查看全部"链接,中间用 layoutWeight(1) 的空 Column 分隔。
Grid 组件是 ArkTS 提供的网格布局容器,columnsTemplate('1fr 1fr') 定义了两列等宽的网格模板——1fr 表示一份可用空间,两个 1fr 即两列各占 50% 宽度。columnsGap(12) 和 rowsGap(12) 分别设置列间距和行间距为 12 像素。ForEach 遍历所有商品数据,为每个商品生成一个 GridItem,内部调用 productCard 构建器渲染商品卡片。
7.5 商品卡片组件
商品卡片是整个应用中复用度最高的 UI 组件,其设计精密度直接影响用户的购物体验。
@Builder
productCard(product: BeautyProduct, index: number) {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column()
.width('100%')
.height(120)
.linearGradient({
direction: GradientDirection.Left,
colors: [[product.coverColor, 0], [product.coverColor2, 1]]
})
.borderRadius({ topLeft: 12, topRight: 12 })
卡片顶部是商品封面区域,使用 Stack 层叠布局。最底层是一个高度为 120 的 Column,应用了从商品 coverColor 到 coverColor2 的左向渐变,顶部左右圆角为 12。这个渐变色块就是商品的"虚拟图片"——通过精心选择的颜色组合来模拟商品的视觉外观。
if (product.isOnSale) {
Column() {
Text(product.discount + '折')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.backgroundColor(COLORS['sale'])
.borderRadius(20)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.margin({ top: 8, right: 8 })
}
if (product.isNew) {
Column() {
Text('NEW')
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.backgroundColor(COLORS['new'])
.borderRadius(20)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ top: 8, left: 8 })
.position({ x: 0, y: 0 })
}
在封面之上层叠了两个条件渲染的徽章。促销徽章位于右上角(Stack 的 TopEnd 对齐),红色背景显示折扣信息(如"8折")。新品徽章通过 position({ x: 0, y: 0 }) 定位在左上角,绿色背景显示"NEW"文字。两个徽章都使用 borderRadius(20) 实现药丸形状,通过条件渲染确保只在对应状态为 true 时显示。
Column() {
Text(product.brand)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.height(120)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
封面中央居中显示品牌名称,使用 16 号粗体白色字体,90% 不透明度。这个全屏覆盖的 Column 与渐变背景层叠,品牌名位于渐变色的正中央。
Column() {
Text(product.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.alignSelf(ItemAlign.Start)
.margin({ top: 8, left: 8, right: 8 })
Row() {
Text('★' + product.rating.toFixed(1))
.fontSize(11)
.fontColor(COLORS['gold'])
Text('已售' + product.soldCount.toString())
.fontSize(10)
.fontColor(COLORS['gray'])
.margin({ left: 6 })
}
.width('100%')
.padding({ left: 8, right: 8, top: 4 })
.justifyContent(FlexAlign.SpaceBetween)
卡片下半部分是商品信息区域。商品名称使用 14 号中等粗细的深色字体,最多显示两行,超出部分以省略号结尾。评分行左侧是金色星星加评分值(toFixed(1) 确保显示一位小数),右侧是灰色销量文字,两者通过 SpaceBetween 布局分列两端。
Row() {
Text('¥' + product.price.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('¥' + product.originalPrice.toString())
.fontSize(11)
.fontColor(COLORS['gray'])
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
}
.width('100%')
.padding({ left: 8, right: 8, top: 4 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
价格行展示了当前售价(16 号粗体粉色)和原价(11 号灰色带删除线)。TextDecorationType.LineThrough 给原价添加了横向删除线,这是电商应用中表达折扣信息的标准视觉语言,让用户一眼就能看出优惠力度。
Row() {
Text('🛒 加入购物车')
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['card'])
}
.width('100%')
.height(32)
.backgroundColor(COLORS['primary'])
.justifyContent(FlexAlign.Center)
.borderRadius({ bottomLeft: 12, bottomRight: 12 })
.margin({ top: 8 })
.onClick(() => {
this.selectedProductIndex = index;
this.selectedShadeIndex = 0;
this.showAddCartModal = true;
})
}
.width('100%')
.backgroundColor(COLORS['card'])
.borderRadius({ bottomLeft: 12, bottomRight: 12 })
}
.width('100%')
.borderRadius(12)
.backgroundColor(COLORS['card'])
.shadow({ radius: 8, color: 'rgba(233,30,99,0.1)', offsetX: 0, offsetY: 2 })
.onClick(() => {
this.selectedProductIndex = index;
this.showProductDetail = true;
})
}
卡片底部是"加入购物车"按钮,高度为 32 像素,使用主色背景和白色文字,底部左右圆角为 12,与卡片整体保持一致。点击按钮会设置 selectedProductIndex 为当前商品索引,重置 selectedShadeIndex 为 0(默认选中第一个色号),然后显示加入购物车弹窗。
整个卡片外层也绑定了 onClick 事件,点击卡片任意区域(非按钮区域)会打开商品详情弹窗。这里有一个值得注意的交互设计:按钮的 onClick 和卡片的 onClick 是两个独立的事件处理器,点击按钮时只会触发按钮的事件(因为按钮在 Stack/Column 的内层),不会冒泡到卡片外层的事件。这种设计让"加入购物车"和"查看详情"两个操作互不干扰。
7.6 为你推荐区域
@Builder
recommendSection() {
Column() {
Row() {
Text('✨ 为你推荐')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
Column().layoutWeight(1)
Text('换一批 🔄')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.padding({ left: 16, right: 16, top: 20, bottom: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
Scroll() {
Column() {
Row() {
ForEach(this.products, (product: BeautyProduct, index: number) => {
if (index < 8) {
Column() {
Column()
.width(100)
.height(100)
.linearGradient({
direction: GradientDirection.Left,
colors: [[product.coverColor, 0], [product.coverColor2, 1]]
})
.borderRadius(12)
Text(product.name)
.fontSize(11)
.fontColor(COLORS['textDark'])
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
.width(100)
Text('¥' + product.price.toString())
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
.margin({ top: 2 })
}
.width(100)
.margin({ right: 12 })
.alignItems(HorizontalAlign.Start)
}
})
}
.padding({ left: 16, right: 16 })
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
}
.width('100%')
}
"为你推荐"区域采用横向滚动布局,展示前 8 个商品的精简卡片。与商品网格中的完整卡片不同,推荐区域的卡片更加简洁——只有 100x100 的渐变色块封面、商品名称和价格,没有评分、销量和加入购物车按钮。这种精简设计适合横向滚动的场景,用户可以快速浏览多个商品,点击后进入详情页了解更多信息。
if (index < 8) 条件限制了只展示前 8 个商品,避免横向列表过长。标题栏右侧的"换一批"按钮是推荐系统的常见交互入口,在真实应用中点击后会请求后端返回新的推荐列表。
7.7 周销售趋势图表
@Builder
weeklySalesChart() {
Column() {
Text('📊 本周销售趋势')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.alignSelf(ItemAlign.Start)
.margin({ left: 16, top: 20, bottom: 12 })
Row() {
ForEach(this.weeklySales, (data: WeeklySaleData, index: number) => {
Column() {
Column()
.width(24)
.height(data.sales / 20)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[COLORS['primary'], 0], [COLORS['light'], 1]]
})
.borderRadius({ topLeft: 4, topRight: 4 })
Text(data.day)
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
})
}
.width('90%')
.height(200)
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding({ top: 16, bottom: 12 })
.alignItems(VerticalAlign.Top)
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08)', offsetX: 0, offsetY: 2 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
这是一个纯 ArkTS 实现的柱状图组件,无需引入任何第三方图表库。其实现原理非常巧妙:使用 Row 作为图表容器,ForEach 遍历 weeklySales 数据数组,为每一天生成一个 Column 柱状条。
每个柱状条的高度通过 data.sales / 20 计算得出——将销售数值除以 20 进行缩放,使其适合 200 像素的图表高度。例如,周日销量为 3200,则柱状条高度为 160 像素。柱状条使用从底部到顶部的渐变色(主色到浅色),顶部圆角为 4,下方显示星期标签。
layoutWeight(1) 让七个柱状条等分图表宽度,justifyContent(FlexAlign.End) 确保柱状条从容器的底部开始向上生长。整个图表容器使用白色背景、16 的圆角和轻微的阴影,呈现出一个精致的数据卡片。
这种纯原生方式实现图表的优势在于:完全可控的样式定制、无需额外的依赖库、性能表现优秀。对于简单的柱状图需求来说,这比引入图表库更加轻量和高效。
八、分类标签页解析
8.1 分类页面整体结构
@Builder
categoryTab() {
Scroll() {
Column() {
Column() {
Text('📂 全部分类')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
.alignSelf(ItemAlign.Start)
Text('找到你的专属美妆')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
.alignSelf(ItemAlign.Start)
}
.width('100%')
.padding({ left: 16, top: 16, bottom: 20 })
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
分类页面的顶部同样是渐变色头部,显示"全部分类"标题和"找到你的专属美妆"副标题。渐变方向、圆角和阴影的设置与首页横幅保持一致,确保了应用整体的视觉统一性。
8.2 分类卡片与子分类网格
Column() {
ForEach(this.categories, (cat: CategoryItem, index: number) => {
Column() {
Row() {
Column() {
Text(cat.icon)
.fontSize(28)
}
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor(cat.bg)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Text(cat.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Text(cat.count.toString() + '款商品')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Text('查看全部 >')
.fontSize(11)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.alignItems(VerticalAlign.Center)
每个分类卡片是一个独立的 Column,顶部是分类头部行。左侧是 48x48 的圆形分类图标容器,使用分类自身的背景色(cat.bg),内部居中显示 emoji 图标。右侧是分类名称和商品数量,最右侧是"查看全部"链接。使用 layoutWeight(1) 的空 Column 将链接推到右侧。
Grid() {
ForEach(cat.subCategories, (sub: string, subIndex: number) => {
GridItem() {
Column() {
Text(sub)
.fontSize(12)
.fontColor(COLORS['textPrimary'])
}
.width('100%')
.height(40)
.backgroundColor(cat.bg)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.margin({ top: 12 })
}
.width('100%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(12)
.margin({ left: 16, right: 16, top: 12 })
.shadow({ radius: 6, color: 'rgba(233,30,99,0.08)', offsetX: 0, offsetY: 2 })
})
}
.padding({ top: 4, bottom: 20 })
}
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
每个分类卡片下方是三列子分类网格(columnsTemplate('1fr 1fr 1fr'))。每个子分类是一个高度为 40 的圆角色块,背景使用分类的主题色,文字使用主色。这种"浅色背景+主题色文字"的设计既保持了分类的色彩辨识度,又不会过于刺眼。
整个分类卡片使用白色背景、16 的圆角和轻微阴影,通过 margin 设置了左右各 16 的边距和顶部 12 的间距,让多个分类卡片之间有清晰的视觉分隔。
九、购物车标签页深度解析
9.1 购物车头部与列表
@Builder
cartTab() {
Column() {
Row() {
Text('🛒 购物车')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Column().layoutWeight(1)
Text(this.cartItems.length.toString() + '件')
.fontSize(14)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
购物车页面的头部与其他标签页保持一致的渐变色设计,显示"购物车"标题和商品总数。this.cartItems.length.toString() 动态计算并显示购物车中的商品种类数量。
9.2 购物车商品项
购物车商品项是交互最复杂的 UI 组件之一,包含了选择、展示、数量调整等多种交互。
Scroll() {
Column() {
ForEach(this.cartItems, (item: CartItem, index: number) => {
Row() {
Column() {
if (item.isSelected) {
Text('✓')
.fontSize(14)
.fontColor(COLORS['card'])
}
}
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor(item.isSelected ? COLORS['primary'] : COLORS['card'])
.border({ width: 1, color: COLORS['border'], radius: 12, style: BorderStyle.Solid })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
item.isSelected = !item.isSelected;
this.cartTotal = this.calcCartTotal();
this.selectedCartCount = this.calcSelectedCount();
})
每个购物车商品项的最左侧是选择圆形复选框。当 isSelected 为 true 时,背景为主色并显示白色对勾;为 false 时,背景为白色并显示浅色边框。点击复选框会切换 isSelected 状态,并立即重新计算购物车总价和选中数量——这两个计算结果分别驱动底部结算栏的金额和件数显示。
Column()
.width(60)
.height(60)
.linearGradient({
direction: GradientDirection.Left,
colors: [[item.coverColor, 0], [item.coverColor2, 1]]
})
.borderRadius(12)
.margin({ left: 8 })
选择框右侧是 60x60 的商品渐变色块封面,与商品卡片中的封面设计保持一致,但尺寸更小,适合列表场景。
Column() {
Text(item.productName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.brand)
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
.alignSelf(ItemAlign.Start)
Text(item.shade)
.fontSize(10)
.fontColor(COLORS['textSecondary'])
.margin({ top: 2 })
.alignSelf(ItemAlign.Start)
商品信息区域包含三行文字:商品名称(14号中等粗细深色)、品牌名称(11号灰色)和色号信息(10号次要色)。色号信息的展示是美妆购物车的特色——让用户清楚地知道购物车里每件商品选择的具体色号。
9.3 数量加减器
Row() {
Text('¥' + item.price.toString())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Column().layoutWeight(1)
Row() {
Text('−')
.fontSize(16)
.fontColor(COLORS['textPrimary'])
.width(28)
.height(28)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['bg'])
.borderRadius({ topLeft: 6, bottomLeft: 6 })
.onClick(() => {
if (item.quantity > 1) {
item.quantity--;
this.cartTotal = this.calcCartTotal();
this.selectedCartCount = this.calcSelectedCount();
}
})
Text(item.quantity.toString())
.fontSize(14)
.fontColor(COLORS['textDark'])
.width(32)
.height(28)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['card'])
Text('+')
.fontSize(16)
.fontColor(COLORS['textPrimary'])
.width(28)
.height(28)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['bg'])
.borderRadius({ topRight: 6, bottomRight: 6 })
.onClick(() => {
if (item.quantity < item.stock) {
item.quantity++;
this.cartTotal = this.calcCartTotal();
this.selectedCartCount = this.calcSelectedCount();
}
})
}
.height(28)
.borderRadius(6)
.alignItems(VerticalAlign.Center)
}
.width('100%')
.margin({ top: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
价格行右侧是数量加减器,由减号按钮、数量显示和加号按钮三部分组成。减号按钮的 onClick 事件首先检查 item.quantity > 1,确保数量不会减到 0 以下;加号按钮检查 item.quantity < item.stock,防止超过库存上限。两个按钮在修改数量后都立即重新计算总价和选中数量。
加减器的设计细节值得注意:减号按钮只有左上和左下圆角(topLeft: 6, bottomLeft: 6),加号按钮只有右上和右下圆角,中间的数量显示没有圆角——三个部分拼接在一起形成了一个完整的圆角矩形控件。减号和加号按钮使用浅粉色背景,数量显示使用白色背景,通过背景色的差异区分了可点击区域和不可点击区域。
9.4 底部结算栏
Row() {
Column() {
Row() {
Text('全选')
.fontSize(12)
.fontColor(COLORS['textSub'])
}
}
.margin({ left: 16 })
Column().layoutWeight(1)
Column() {
Row() {
Text('合计: ')
.fontSize(13)
.fontColor(COLORS['textSub'])
Text('¥' + this.cartTotal.toString())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
}
.alignItems(VerticalAlign.Bottom)
Text('已选' + this.selectedCartCount.toString() + '件')
.fontSize(10)
.fontColor(COLORS['gray'])
.alignSelf(ItemAlign.End)
}
.alignItems(HorizontalAlign.End)
.margin({ right: 12 })
Row() {
Text('去结算')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.height(44)
.padding({ left: 28, right: 28 })
.backgroundColor(COLORS['primary'])
.borderRadius(22)
.justifyContent(FlexAlign.Center)
.margin({ right: 16 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 2 })
}
.width('100%')
.height(64)
.backgroundColor(COLORS['card'])
.border({ width: { top: 1 }, color: COLORS['border'], radius: 0, style: BorderStyle.Solid })
.alignItems(VerticalAlign.Center)
}
.width('100%')
.layoutWeight(1)
}
}
底部结算栏是购物车页面的关键交互区域,高度为 64 像素,使用白色背景和顶部边框线。从左到右依次是"全选"按钮、合计金额和"去结算"按钮。
合计金额区域包含两行文字:上方的"合计: ¥XXX"(18号粗体粉色金额)和下方的"已选X件"(10号灰色辅助信息)。this.cartTotal 和 this.selectedCartCount 是 @State 变量,当用户修改商品数量或选择状态时会自动更新,结算栏的显示也会随之实时变化。
"去结算"按钮高度为 44 像素,使用主色背景和白色粗体文字,左右各 28 的内边距让按钮有足够的可点击区域,圆角为 22 实现了药丸形状,配合粉色阴影营造出了强烈的行动召唤效果。
十、收藏标签页解析
10.1 收藏页面结构
@Builder
favoriteTab() {
Column() {
Row() {
Text('❤️ 我的收藏')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Column().layoutWeight(1)
Text(this.favoriteProducts.length.toString() + '件')
.fontSize(14)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
Scroll() {
Column() {
Grid() {
ForEach(this.favoriteProducts, (product: BeautyProduct, index: number) => {
GridItem() {
this.favoriteCard(product, index)
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding({ left: 16, right: 16, top: 12, bottom: 20 })
.constraintSize({ maxHeight: '100%' })
Column().height(20)
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
.width('100%')
.layoutWeight(1)
}
收藏页面的整体结构与首页商品网格类似,使用两列网格布局展示收藏的商品。头部同样使用渐变色设计,显示"我的收藏"标题和收藏数量。
10.2 收藏卡片组件
@Builder
favoriteCard(product: BeautyProduct, index: number) {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column()
.width('100%')
.height(110)
.linearGradient({
direction: GradientDirection.Left,
colors: [[product.coverColor, 0], [product.coverColor2, 1]]
})
.borderRadius({ topLeft: 12, topRight: 12 })
Column() {
Text('❤️')
.fontSize(14)
}
.backgroundColor(COLORS['card'])
.borderRadius(14)
.width(28)
.height(28)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.margin({ top: 6, right: 6 })
.shadow({ radius: 4, color: 'rgba(0,0,0,0.1)', offsetX: 0, offsetY: 1 })
Column() {
Text(product.brand)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.height(110)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
收藏卡片与商品卡片的结构类似,但有几个差异。首先是封面高度从 120 降为 110,略微紧凑。其次是右上角的徽章换成了收藏心标——一个 28x28 的白色圆形容器内显示红色心形 emoji,配合轻微的阴影,清晰地标示出该商品已被收藏。
Column() {
Text(product.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.alignSelf(ItemAlign.Start)
.margin({ top: 6, left: 8, right: 8 })
Row() {
Text('★' + product.rating.toFixed(1))
.fontSize(10)
.fontColor(COLORS['gold'])
Column().layoutWeight(1)
Text('¥' + product.price.toString())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
}
.width('100%')
.padding({ left: 8, right: 8, top: 4 })
.alignItems(VerticalAlign.Center)
Row() {
Text('🛒 加购')
.fontSize(11)
.fontColor(COLORS['card'])
}
.width('100%')
.height(28)
.backgroundColor(COLORS['primary'])
.justifyContent(FlexAlign.Center)
.borderRadius({ bottomLeft: 12, bottomRight: 12 })
.margin({ top: 6 })
.onClick(() => {
this.removeFavIndex = index;
this.showRemoveFavModal = true;
})
}
.width('100%')
.backgroundColor(COLORS['card'])
.borderRadius({ bottomLeft: 12, bottomRight: 12 })
}
.width('100%')
.borderRadius(12)
.backgroundColor(COLORS['card'])
.shadow({ radius: 6, color: 'rgba(233,30,99,0.1)', offsetX: 0, offsetY: 2 })
}
收藏卡片的信息区域更加简洁,评分和价格放在同一行(左侧评分,右侧价格),商品名称只显示一行。底部按钮文字改为"加购",点击后不是直接加入购物车,而是弹出取消收藏的确认弹窗——这是一种保护性的交互设计,避免用户误触取消收藏。
十一、个人中心标签页解析
11.1 个人信息头部与会员积分
@Builder
profileTab() {
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('👑')
.fontSize(48)
}
.width(72)
.height(72)
.borderRadius(36)
.backgroundColor('rgba(255,255,255,0.3)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Text('美丽小仙女')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Text('会员等级: 钻石VIP 💎')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.margin({ left: 16 })
.alignItems(HorizontalAlign.Start)
}
.alignItems(VerticalAlign.Center)
.padding({ left: 20, top: 20, bottom: 16 })
}
.width('100%')
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
个人中心头部展示了用户的头像和基本信息。头像区域是一个 72x72 的圆形容器,背景使用半透明白色(rgba(255,255,255,0.3)),内部居中显示皇冠 emoji。头像右侧是用户名"美丽小仙女"和会员等级"钻石VIP",使用白色和半透明白色字体在渐变背景上显示。
Row() {
Column() {
Text('8,920')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('美丽积分')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('12')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('优惠券')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('3')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('收藏夹')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding({ top: 16, bottom: 16 })
.margin({ top: 12 })
.justifyContent(FlexAlign.SpaceAround)
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08)', offsetX: 0, offsetY: 2 })
会员积分卡片使用三等分布局展示美丽积分(8920)、优惠券(12张)和收藏夹(3个)三项数据。每项数据的数字使用 22 号粗体粉色,标签使用 11 号灰色。这个卡片悬浮在渐变头部下方,使用负边距效果(通过 margin({ top: 12 }) 实现间距),白色背景配合圆角和阴影形成了卡片悬浮的视觉层次。
11.2 美妆档案模块
Column() {
Row() {
Text('🧴 美妆档案')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('编辑 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
Row() {
ForEach(this.skinKeys, (key: string) => {
Column() {
Text(SKIN_TYPE_METAS[key].label)
.fontSize(11)
.fontColor(COLORS['gray'])
Text(SKIN_TYPE_METAS[key].value)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textPrimary'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS['bg'])
.borderRadius(12)
.margin({ left: 4, right: 4 })
})
}
.width('100%')
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(16)
.margin({ top: 12 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08)', offsetX: 0, offsetY: 2 })
美妆档案是美妆应用中非常独特的功能模块。通过 ForEach 遍历 skinKeys 数组,从 SKIN_TYPE_METAS 配置中读取肤质类型(“混合偏干”)、肤色(“自然偏白”)和护肤关注点(“保湿·抗初老”)三项信息。每项信息以浅粉色圆角色块的形式展示,上方是灰色标签,下方是粉色值。这种个性化的美妆档案可以为商品推荐系统提供数据基础。
11.3 订单管理模块
Column() {
Row() {
Text('📦 我的订单')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('全部 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
Row() {
Column() {
Text('💳')
.fontSize(24)
Text('待付款')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
// ... 其他订单状态入口
}
.width('100%')
.margin({ bottom: 12 })
订单管理模块包含两部分:顶部的五个订单状态快捷入口(待付款、待发货、待收货、待评价、退换),每个入口使用 emoji 图标加文字的形式;下方的订单历史列表。
ForEach(this.orders, (order: OrderRecord, index: number) => {
Row() {
Column() {
Text(order.orderNo)
.fontSize(12)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Text(order.date + ' · ' + order.count.toString() + '件商品')
.fontSize(10)
.fontColor(COLORS['gray'])
.margin({ top: 4 })
.alignSelf(ItemAlign.Start)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + order.total.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text(order.status)
.fontSize(10)
.fontColor(COLORS['textSecondary'])
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: 1, color: COLORS['border'], radius: 0, style: BorderStyle.Solid })
.alignItems(VerticalAlign.Center)
.margin({ top: 4 })
})
订单列表使用 ForEach 遍历 orders 数组,每条订单记录显示为左右两栏。左侧是订单号和日期(“2026-07-28 · 2件商品”),右侧是总金额和订单状态。订单之间用浅色边框线分隔,金额使用粉色粗体,状态使用次要色文字。
11.4 美妆日记模块
Column() {
Row() {
Text('📔 美妆日记')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('写日记 ✍️')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
Column() {
Row() {
Text('📅')
.fontSize(20)
Column() {
Text('今日护肤记录')
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
Text('已完成5步护肤流程,肌肤状态良好')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS['bg'])
.borderRadius(12)
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(16)
.margin({ top: 12, bottom: 20 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08)', offsetX: 0, offsetY: 2 })
美妆日记是应用的特色功能之一,鼓励用户记录每日的护肤过程。日记卡片内部展示了一条"今日护肤记录"条目,包含日历图标、标题和描述文字。这种功能设计将单纯的购物应用升级为美妆生活管家,增加了用户的粘性和使用频率。
十二、弹窗系统深度解析
12.1 加入购物车弹窗
@Builder
addCartModalContent() {
Column() {
Row() {
Text('加入购物车')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('✕')
.fontSize(20)
.fontColor(COLORS['gray'])
.onClick(() => {
this.showAddCartModal = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.padding({ left: 20, right: 20, top: 20, bottom: 12 })
加入购物车弹窗的顶部是标题行,左侧"加入购物车"标题,右侧关闭按钮"✕"。点击关闭按钮将 showAddCartModal 设为 false 关闭弹窗。
Row() {
Column()
.width(80)
.height(80)
.linearGradient({
direction: GradientDirection.Left,
colors: [[this.products[this.selectedProductIndex].coverColor, 0],
[this.products[this.selectedProductIndex].coverColor2, 1]]
})
.borderRadius(12)
Column() {
Text(this.products[this.selectedProductIndex].name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Row() {
Text('¥' + this.products[this.selectedProductIndex].price.toString())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('¥' + this.products[this.selectedProductIndex].originalPrice.toString())
.fontSize(12)
.fontColor(COLORS['gray'])
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
}
.alignItems(VerticalAlign.Bottom)
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 12 })
.alignItems(VerticalAlign.Center)
商品预览区域通过 this.products[this.selectedProductIndex] 动态获取当前选中商品的数据。左侧是 80x80 的渐变色块封面,右侧是商品名称和价格信息(当前售价和带删除线的原价)。这种通过索引动态获取数据的方式使得同一个弹窗模板可以服务于所有商品。
12.2 色号选择网格
Grid() {
ForEach(this.products[this.selectedProductIndex].shadeColors, (shade: string, sIndex: number) => {
GridItem() {
Column() {
Column()
.width(40)
.height(40)
.backgroundColor(shade)
.borderRadius(20)
.border({
width: this.selectedShadeIndex === sIndex ? 3 : 0,
color: COLORS['primary'],
radius: 22,
style: BorderStyle.Solid
})
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.selectedShadeIndex = sIndex;
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding({ left: 20, right: 20 })
.height(100)
色号选择是美妆购物中最具特色的功能。这里使用五列网格布局展示色号选项,每个色号是一个 40x40 的圆形色块,背景色直接使用色号的颜色值。
选中状态通过 border 的 width 属性动态控制:当 selectedShadeIndex === sIndex 时,边框宽度为 3(显示粉色选中边框);否则为 0(无边框)。点击色号会更新 selectedShadeIndex,触发 UI 重新渲染,选中边框自动移动到新的色号上。这种通过状态驱动 UI 变化的方式正是声明式 UI 的核心优势。
12.3 确认加入按钮
Row() {
Text('确认加入购物车')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.width('90%')
.height(48)
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.justifyContent(FlexAlign.Center)
.borderRadius(24)
.margin({ bottom: 20 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.3)', offsetX: 0, offsetY: 4 })
.onClick(() => {
this.showAddCartModal = false;
})
}
.width('88%')
.backgroundColor(COLORS['card'])
.borderRadius(24)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15)', offsetX: 0, offsetY: 8 })
}
确认按钮使用与头部横幅相同的渐变色,高度为 48 像素,圆角为 24 形成药丸形状。点击后关闭弹窗。弹窗容器宽度为屏幕的 88%,使用 24 的圆角和较大的阴影(radius: 20),在半透明遮罩上呈现为一个悬浮的白色卡片。
12.4 写评价弹窗
@Builder
reviewModalContent() {
Column() {
Row() {
Text('✍️ 写评价')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('✕')
.fontSize(20)
.fontColor(COLORS['gray'])
.onClick(() => {
this.showReviewModal = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.padding({ left: 20, right: 20, top: 20, bottom: 16 })
写评价弹窗的标题行与加入购物车弹窗结构一致,但标题文字不同。
Row() {
ForEach([1, 2, 3, 4, 5], (star: number) => {
Text(star <= this.reviewRating ? '⭐' : '☆')
.fontSize(32)
.margin({ right: 8 })
.onClick(() => {
this.reviewRating = star;
})
})
}
.width('100%')
.padding({ left: 20, bottom: 16 })
评分星星使用 ForEach 遍历 [1, 2, 3, 4, 5] 数组生成五颗星。每颗星的显示通过三元表达式控制:如果 star <= reviewRating,显示实心星(⭐),否则显示空心星(☆)。点击任意一颗星会将 reviewRating 设为对应的值,自动更新星星的显示状态。这种交互模式是评分组件的标准实现方式。
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.reviewTagKeys, (key: string) => {
Text(REVIEW_TAG_METAS[key].label)
.fontSize(12)
.fontColor(COLORS['primary'])
.backgroundColor(COLORS['bg'])
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
})
}
.width('88%')
.padding({ left: 4, right: 4 })
.margin({ bottom: 16 })
评价标签区域使用 Flex 布局并启用了换行(FlexWrap.Wrap),标签会根据可用宽度自动排列换行。每个标签是一个浅粉色背景、主色文字的圆角药丸,使用 ForEach 从 REVIEW_TAG_METAS 配置中读取。Flex 换行布局是处理数量不定的标签元素的理想方案——无论标签数量多少,都能自动适应布局。
12.5 取消收藏确认弹窗
@Builder
removeFavModalContent() {
Column() {
Column() {
Text('💔')
.fontSize(48)
Text('取消收藏')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
.margin({ top: 12 })
Text('确定要将这个商品从收藏中移除吗?')
.fontSize(13)
.fontColor(COLORS['gray'])
.margin({ top: 8 })
.textAlign(TextAlign.Center)
}
.width('100%')
.padding({ top: 28, bottom: 20 })
.alignItems(HorizontalAlign.Center)
Row() {
Text('再想想')
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textSub'])
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(44)
.backgroundColor(COLORS['bg'])
.borderRadius(22)
.onClick(() => {
this.showRemoveFavModal = false;
})
Text('确定移除')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(44)
.backgroundColor(COLORS['sale'])
.borderRadius(22)
.margin({ left: 12 })
.onClick(() => {
this.showRemoveFavModal = false;
})
}
.width('85%')
.margin({ bottom: 24 })
}
.width('76%')
.backgroundColor(COLORS['card'])
.borderRadius(24)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15)', offsetX: 0, offsetY: 8 })
}
取消收藏确认弹窗是一个典型的对话框组件,宽度为屏幕的 76%,比其他弹窗更窄,呈现出居中对话框的标准比例。
弹窗内容分为两部分:上方的信息区域显示破碎心形 emoji、标题"取消收藏"和提示文字;下方的按钮行包含"再想想"(浅色背景)和"确定移除"(红色背景)两个等宽按钮,通过 layoutWeight(1) 实现等分。两个按钮的高度均为 44 像素,圆角为 22 形成药丸形状。这种双按钮对话框设计是移动端确认操作的标准模式——通过视觉强弱的对比(浅色取消按钮 vs 红色确认按钮)引导用户做出审慎的决定。
12.6 商品详情弹窗
商品详情弹窗是所有弹窗中最复杂的一个,它几乎复现了一个完整的商品详情页面。
@Builder
productDetailModalContent() {
Scroll() {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column()
.width('100%')
.height(200)
.linearGradient({
direction: GradientDirection.Left,
colors: [[this.products[this.selectedProductIndex].coverColor, 0],
[this.products[this.selectedProductIndex].coverColor2, 1]]
})
.borderRadius({ topLeft: 24, topRight: 24 })
Column() {
Text('✕')
.fontSize(18)
.fontColor(COLORS['card'])
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('rgba(0,0,0,0.3)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.margin({ top: 12, right: 12 })
.onClick(() => {
this.showProductDetail = false;
})
Column() {
Text(this.products[this.selectedProductIndex].brand)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('rgba(255,255,255,0.9)')
Text(this.products[this.selectedProductIndex].category)
.fontSize(14)
.fontColor('rgba(255,255,255,0.7)')
.margin({ top: 4 })
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
详情弹窗的顶部是高度为 200 的商品大图区域——使用与商品封面相同的渐变色块,但尺寸更大。右上角有一个半透明黑色背景的关闭按钮,中央居中显示品牌名(28号粗体)和分类名(14号)。borderRadius({ topLeft: 24, topRight: 24 }) 让弹窗顶部呈现圆角,与弹窗容器的圆角匹配。
Column() {
Text(this.products[this.selectedProductIndex].name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Text(this.products[this.selectedProductIndex].description)
.fontSize(13)
.fontColor(COLORS['textSub'])
.margin({ top: 8 })
.alignSelf(ItemAlign.Start)
商品信息区域从上到下依次展示商品名称(20号粗体)、商品描述(13号灰色)、评分行(金色星星、评价数量、销量)、价格行(大号粉色售价、灰色删除线原价、红色折扣标签)和标签行(Flex 换行布局的标签药丸)。
ForEach(this.reviews, (review: ReviewItem, rIndex: number) => {
Column() {
Row() {
Column() {
Text(review.avatar)
.fontSize(24)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(COLORS['bg'])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(review.author)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
if (review.isVerified) {
Text('✓ 已验证')
.fontSize(9)
.fontColor(COLORS['new'])
.backgroundColor('#E8F5E9')
.borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
.margin({ left: 6 })
}
}
.alignItems(VerticalAlign.Center)
Text('★'.repeat(review.rating))
.fontSize(11)
.fontColor(COLORS['gold'])
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(review.date)
.fontSize(10)
.fontColor(COLORS['gray'])
}
.width('100%')
.alignItems(VerticalAlign.Center)
评价列表区域使用 ForEach 遍历 reviews 数组,每条评价展示用户头像(emoji)、作者名、验证标记("✓ 已验证"绿色标签)、评分星星(通过 '★'.repeat(review.rating) 生成对应数量的星星)、评价日期、评价内容、评价图片(颜色色块)和点赞数。
'★'.repeat(review.rating) 是一个非常巧妙的实现——利用 JavaScript/TypeScript 的字符串 repeat 方法,根据评分值生成对应数量的星星字符。例如评分为 5 则生成"★★★★★",评分为 3 则生成"★★★"。
整个详情弹窗使用 Scroll 包裹,constraintSize({ maxHeight: '80%' }) 限制最大高度为屏幕的 80%,确保弹窗不会完全覆盖屏幕,底部留出一定的遮罩区域,让用户知道可以通过点击遮罩关闭弹窗。
十三、关键特性对比总结
下表对应用中各个核心模块的关键特性进行了系统性对比总结:
| 模块名称 | 核心功能 | 状态管理 | 布局组件 | 交互特色 | 数据来源 |
|---|---|---|---|---|---|
| 首页标签 | 商品展示与推荐 | currentTab | Scroll + Column + Grid | 闪购倒计时、横向推荐、柱状图 | products 数组 |
| 分类标签 | 分类导航浏览 | currentTab | Scroll + Column + Grid | 三列子分类网格、独立主题色 | categories 数组 |
| 购物车标签 | 购物车管理 | cartTotal, selectedCartCount | Column + Scroll + Row | 选中/取消、数量加减、实时结算 | cartItems 数组 |
| 收藏标签 | 收藏商品展示 | removeFavIndex | Column + Scroll + Grid | 取消收藏确认弹窗 | favoriteProducts 数组 |
| 个人中心 | 用户信息与订单 | 无额外状态 | Scroll + Column + Row | 会员积分、美妆档案、订单列表 | orders, SKIN_TYPE_METAS |
| 加入购物车弹窗 | 色号与数量选择 | selectedProductIndex, selectedShadeIndex | Column + Grid + Row | 五列色号网格、动态选中边框 | products 数组 |
| 写评价弹窗 | 评价输入 | reviewRating, reviewText | Column + Flex + Row | 五星评分、Flex 换行标签 | REVIEW_TAG_METAS |
| 取消收藏弹窗 | 操作确认 | showRemoveFavModal | Column + Row | 双按钮确认对话框 | 静态文案 |
| 商品详情弹窗 | 商品详情展示 | selectedProductIndex, showReviewModal | Scroll + Stack + Column | 可滚动详情、评价列表、写评价入口 | products, reviews 数组 |
| 底部导航栏 | 标签页切换 | currentTab | Row + ForEach | 五标签等分布局、选中色变化 | 静态配置 |
| 模态遮罩 | 弹窗背景 | 四个弹窗布尔值 | Stack + Column | 点击遮罩关闭弹窗 | 无 |
| 闪购横幅 | 促销展示 | countdownHours/Minutes/Seconds | Column + Row | 渐变背景、翻牌倒计时 | 静态初始值 |
| 周销售图表 | 数据可视化 | 无 | Row + ForEach | 纯原生柱状图、渐变色柱 | weeklySales 数组 |
| 品牌入口 | 品牌快捷导航 | currentTab | Scroll + Row + ForEach | 横向滚动、点击跳转分类 | BRAND_METAS |
| 商品卡片 | 商品信息展示 | selectedProductIndex | Column + Stack + Row | 渐变封面、促销/新品徽章、加购按钮 | products 数组 |
十四、总结与技术展望
14.1 架构设计总结
通过对这个美妆商城应用的完整代码解析,我们可以总结出以下几个关键的架构设计思想:
第一,接口先行的类型安全设计。 应用在正式开发组件之前,首先定义了十余个接口类型,从色彩配置到商品数据、从评价记录到订单信息,所有数据结构都有明确的类型约束。这种"接口先行"的设计方法论确保了代码的类型安全性,在编译阶段就能发现类型不匹配的错误,大幅降低了运行时异常的风险。同时,完善的接口定义也起到了文档的作用——其他开发者通过阅读接口定义就能快速理解应用的数据模型。
第二,集中式的配置管理。 所有的颜色值、分类元数据、品牌信息、肤质配置和评价标签都集中在全局常量对象中管理。这种设计模式带来了三个显著的好处:视觉一致性有保障(所有组件引用同一套颜色定义)、维护成本低(修改一处即可全局生效)、代码可读性高(COLORS['primary'] 比 '#E91E63' 更具语义)。在实际项目中,这种配置可以进一步扩展为主题系统,支持深色模式、品牌换肤等功能。
第三,响应式状态驱动的 UI 更新。 应用大量使用 @State 装饰器声明状态变量,通过修改状态值来触发 UI 的自动更新,完全遵循声明式 UI 的编程范式。例如在购物车中,用户修改商品数量后,只需更新 cartTotal 和 selectedCartCount 两个状态值,底部结算栏的金额和件数就会自动更新——开发者无需手动操作任何 DOM 节点或调用任何更新方法。这种数据驱动视图的模式大幅简化了状态管理的复杂度。
第四,组件化的构建器架构。 应用通过 @Builder 装饰器将复杂的 UI 拆分为多个独立的构建器方法,每个构建器负责一个特定的 UI 区块。这种组件化拆分使得代码结构清晰、职责分明、复用性强。例如 productCard 构建器被首页商品网格和详情弹窗共同复用,modalOverlay 构建器统一管理所有弹窗的遮罩层。
14.2 视觉设计亮点
在视觉设计方面,这个应用有几个值得特别提及的亮点:
渐变色块的虚拟图片方案。 由于不使用真实图片资源,应用创造性地使用双色渐变色块来代替商品图片。每个商品都有独特的 coverColor 和 coverColor2 组合,这些颜色经过精心选择,与商品的品类和品牌调性相符——口红用红色系、精华用紫色系、香水用金色系。这种方案虽然无法展示商品的真实外观,但在原型开发和功能演示阶段非常高效,而且视觉效果统一美观。
纯原生柱状图实现。 周销售趋势图表完全使用 ArkTS 的基础布局组件实现,无需引入任何第三方图表库。通过 ForEach 遍历数据数组,将每个数据项的值除以缩放系数得到柱状条高度,配合渐变色和圆角,呈现出专业的数据可视化效果。这种方案的优势在于完全可控的样式定制和优异的性能表现。
一致的渐变头部设计。 五个标签页的头部区域都采用了相同的渐变色 + 底部圆角 + 阴影的设计语言,营造了强烈的品牌一致性。粉色到深粉色的水平渐变成为了应用的视觉签名,让用户在不同页面之间切换时始终感受到统一的品牌氛围。
14.3 交互设计亮点
在交互设计方面,应用同样体现了多个优秀的设计实践:
数量加减器的边界保护。 购物车中的数量加减器在减少时检查 quantity > 1(不允许减到 0),在增加时检查 quantity < stock(不允许超过库存),这种边界保护机制避免了无效数据的产生。
操作确认弹窗。 取消收藏操作前弹出确认对话框,避免了用户误触导致的数据丢失。双按钮设计中,取消按钮使用浅色背景、确认按钮使用红色背景,通过视觉强弱对比引导用户审慎操作。
色号选择的视觉反馈。 色号选择通过动态边框宽度(选中时 3px,未选中时 0px)提供清晰的选中状态反馈,配合 @State 的响应式更新,选中边框能够即时跟随用户的点击操作移动。
评分组件的实时反馈。 写评价弹窗中的五星评分组件,点击任意星星即可即时更新评分值和星星的显示状态(实心/空心),为用户提供了流畅的评分体验。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
// 美妆商城 - HarmonyOS ArkTS
// Beauty & Cosmetics Shopping App
// ==================== 接口定义 ====================
interface ColorPalette {
primary: string;
light: string;
bg: string;
card: string;
textPrimary: string;
textSecondary: string;
border: string;
sale: string;
gold: string;
new: string;
}
interface ProductCategoryMeta {
id: string;
name: string;
icon: string;
desc: string;
}
interface BrandMeta {
id: string;
name: string;
icon: string;
desc: string;
}
interface SkinTypeMeta {
id: string;
label: string;
value: string;
}
interface ReviewTagMeta {
id: string;
label: string;
count: number;
}
interface BeautyProduct {
id: string;
name: string;
brand: string;
category: string;
price: number;
originalPrice: number;
rating: number;
reviewCount: number;
soldCount: number;
coverColor: string;
coverColor2: string;
tags: string[];
isHot: boolean;
isNew: boolean;
isOnSale: boolean;
discount: number;
description: string;
shadeColors: string[];
volume: string;
isFavorite: boolean;
}
interface CategoryItem {
id: string;
name: string;
icon: string;
color: string;
bg: string;
count: number;
subCategories: string[];
}
interface CartItem {
id: string;
productId: string;
productName: string;
brand: string;
price: number;
quantity: number;
coverColor: string;
coverColor2: string;
shade: string;
isSelected: boolean;
stock: number;
}
interface ReviewItem {
id: string;
productId: string;
author: string;
avatar: string;
rating: number;
content: string;
date: string;
images: string[];
likes: number;
isVerified: boolean;
}
interface OrderRecord {
id: string;
orderNo: string;
date: string;
status: string;
total: number;
count: number;
}
interface WeeklySaleData {
day: string;
sales: number;
}
// ==================== 颜色配置 ====================
const COLORS: Record<string, string> = {
'primary': '#E91E63',
'light': '#F8BBD0',
'bg': '#FCE4EC',
'card': '#FFFFFF',
'textPrimary': '#880E4F',
'textSecondary': '#E91E63',
'border': '#F8BBD0',
'sale': '#FF1744',
'gold': '#FFD700',
'new': '#4CAF50',
'white': '#FFFFFF',
'gray': '#9E9E9E',
'grayLight': '#E0E0E0',
'grayBg': '#F5F5F5',
'textDark': '#333333',
'textSub': '#666666',
};
// ==================== 静态配置 ====================
const CATEGORY_METAS: Record<string, ProductCategoryMeta> = {
'skincare': { id: 'skincare', name: '护肤', icon: '🧴', desc: '滋养每一寸肌肤' },
'makeup': { id: 'makeup', name: '彩妆', icon: '💄', desc: '绽放你的美丽' },
'perfume': { id: 'perfume', name: '香水', icon: '🌸', desc: '独特的香氛记忆' },
'mask': { id: 'mask', name: '面膜', icon: '🎭', desc: '密集修护焕亮' },
'lipstick': { id: 'lipstick', name: '口红', icon: '💋', desc: '一抹倾心' },
'essence': { id: 'essence', name: '精华', icon: '✨', desc: '深层焕颜修护' },
'cream': { id: 'cream', name: '面霜', icon: '🫙', desc: '锁住水润光泽' },
'sunscreen': { id: 'sunscreen', name: '防晒', icon: '☀️', desc: '阳光下的守护' },
};
const BRAND_METAS: Record<string, BrandMeta> = {
'dior': { id: 'dior', name: 'Dior', icon: '🌹', desc: '法式优雅' },
'chanel': { id: 'chanel', name: 'Chanel', icon: '👜', desc: '经典永恒' },
'lancome': { id: 'lancome', name: 'Lancome', icon: '🌸', desc: '玫瑰之美' },
'esteelauder': { id: 'esteelauder', name: 'Estée Lauder', icon: '💎', desc: '奢华护肤' },
'skii': { id: 'skii', name: 'SK-II', icon: '⭐', desc: '晶莹剔透' },
'ysl': { id: 'ysl', name: 'YSL', icon: '👑', desc: '前卫时尚' },
};
const SKIN_TYPE_METAS: Record<string, SkinTypeMeta> = {
'type': { id: 'type', label: '肤质', value: '混合偏干' },
'tone': { id: 'tone', label: '肤色', value: '自然偏白' },
'concern': { id: 'concern', label: '关注', value: '保湿·抗初老' },
};
const REVIEW_TAG_METAS: Record<string, ReviewTagMeta> = {
't1': { id: 't1', label: '持久不脱妆', count: 1280 },
't2': { id: 't2', label: '显色度高', count: 960 },
't3': { id: 't3', label: '滋润不拔干', count: 850 },
't4': { id: 't4', label: '性价比高', count: 720 },
't5': { id: 't5', label: '包装精美', count: 630 },
};
// ==================== 数据模型类 ====================
@Observed
class ProductData {
id: string = '';
name: string = '';
brand: string = '';
category: string = '';
price: number = 0;
originalPrice: number = 0;
rating: number = 0;
reviewCount: number = 0;
soldCount: number = 0;
coverColor: string = '#E91E63';
coverColor2: string = '#F8BBD0';
tags: string[] = [];
isHot: boolean = false;
isNew: boolean = false;
isOnSale: boolean = false;
discount: number = 0;
description: string = '';
shadeColors: string[] = [];
volume: string = '';
isFavorite: boolean = false;
}
@Observed
class CartData {
id: string = '';
productId: string = '';
productName: string = '';
brand: string = '';
price: number = 0;
quantity: number = 1;
coverColor: string = '#E91E63';
coverColor2: string = '#F8BBD0';
shade: string = '';
isSelected: boolean = true;
stock: number = 99;
}
// ==================== 主组件 ====================
@Entry
@Component
struct BeautyShopping {
@State currentTab: number = 0
@State showAddCartModal: boolean = false
@State showReviewModal: boolean = false
@State showRemoveFavModal: boolean = false
@State showProductDetail: boolean = false
@State selectedProductIndex: number = 0
@State selectedShadeIndex: number = 0
@State reviewRating: number = 5
@State reviewText: string = ''
@State removeFavIndex: number = 0
@State cartTotal: number = 0
@State selectedCartCount: number = 0
@State countdownHours: number = 8
@State countdownMinutes: number = 42
@State countdownSeconds: number = 15
// 商品数据 - 20+ items
private products: BeautyProduct[] = [
{ id: 'p1', name: '烈艳蓝金唇膏', brand: 'Dior', category: '口红', price: 350, originalPrice: 420, rating: 4.9, reviewCount: 2380, soldCount: 15600, coverColor: '#C2185B', coverColor2: '#E91E63', tags: ['热销', '持久'], isHot: true, isNew: false, isOnSale: true, discount: 8, description: '经典丝绒质感唇膏,显色饱满持久', shadeColors: ['#880E4F', '#C2185B', '#E91E63', '#F06292'], volume: '3.5g', isFavorite: true },
{ id: 'p2', name: '小黑瓶精华', brand: 'Estée Lauder', category: '精华', price: 780, originalPrice: 980, rating: 4.8, reviewCount: 4560, soldCount: 28000, coverColor: '#4A148C', coverColor2: '#7B1FA2', tags: ['修护', '抗老'], isHot: true, isNew: false, isOnSale: true, discount: 8, description: '夜间修护精华,焕活肌底', shadeColors: ['#311B92', '#512DA8'], volume: '50ml', isFavorite: true },
{ id: 'p3', name: '神仙水', brand: 'SK-II', category: '精华', price: 1450, originalPrice: 1690, rating: 4.9, reviewCount: 8900, soldCount: 45000, coverColor: '#006064', coverColor2: '#00838F', tags: ['亮肤', '口碑'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: 'PITERA™核心成分,晶莹剔透', shadeColors: ['#004D40', '#00695C'], volume: '230ml', isFavorite: false },
{ id: 'p4', name: '金管口红', brand: 'YSL', category: '口红', price: 380, originalPrice: 420, rating: 4.7, reviewCount: 3200, soldCount: 18000, coverColor: '#D32F2F', coverColor2: '#FF5252', tags: ['滋润', '显色'], isHot: false, isNew: true, isOnSale: false, discount: 9, description: '金管设计,丝滑滋润唇膏', shadeColors: ['#B71C1C', '#D32F2F', '#E53935', '#EF5350'], volume: '3.5g', isFavorite: true },
{ id: 'p5', name: '粉底液', brand: 'Chanel', category: '彩妆', price: 560, originalPrice: 620, rating: 4.8, reviewCount: 5600, soldCount: 22000, coverColor: '#37474F', coverColor2: '#546E7A', tags: ['轻薄', '遮瑕'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '轻薄透气粉底液,自然遮瑕', shadeColors: ['#3E2723', '#4E342E', '#5D4037', '#6D4C41'], volume: '30ml', isFavorite: false },
{ id: 'p6', name: '红石榴面霜', brand: 'Lancome', category: '面霜', price: 680, originalPrice: 850, rating: 4.6, reviewCount: 1800, soldCount: 9500, coverColor: '#BF360C', coverColor2: '#E64A19', tags: ['保湿', '提亮'], isHot: false, isNew: false, isOnSale: true, discount: 8, description: '红石榴精粹,提亮肤色', shadeColors: ['#BF360C'], volume: '50ml', isFavorite: true },
{ id: 'p7', name: '小棕瓶精华', brand: 'Estée Lauder', category: '精华', price: 950, originalPrice: 1080, rating: 4.9, reviewCount: 6700, soldCount: 32000, coverColor: '#1A237E', coverColor2: '#283593', tags: ['修护', '经典'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '多效修护精华,全面抗老', shadeColors: ['#1A237E'], volume: '50ml', isFavorite: false },
{ id: 'p8', name: '香水', brand: 'Chanel', category: '香水', price: 890, originalPrice: 990, rating: 4.9, reviewCount: 3400, soldCount: 12000, coverColor: '#FFD700', coverColor2: '#FFA000', tags: ['经典', '持久'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '经典五号香水,永恒优雅', shadeColors: ['#F57F17', '#F9A825'], volume: '100ml', isFavorite: true },
{ id: 'p9', name: '补水面膜', brand: 'SK-II', category: '面膜', price: 120, originalPrice: 180, rating: 4.5, reviewCount: 2200, soldCount: 35000, coverColor: '#004D40', coverColor2: '#00695C', tags: ['补水', '平价'], isHot: false, isNew: true, isOnSale: true, discount: 7, description: '深层补水面膜,即刻焕亮', shadeColors: ['#004D40'], volume: '25ml*5片', isFavorite: false },
{ id: 'p10', name: '防晒霜', brand: 'Lancome', category: '防晒', price: 320, originalPrice: 380, rating: 4.7, reviewCount: 1500, soldCount: 8500, coverColor: '#FF6F00', coverColor2: '#FF8F00', tags: ['清爽', '高倍'], isHot: false, isNew: false, isOnSale: true, discount: 8, description: '清爽高倍防晒,SPF50+', shadeColors: ['#E65100', '#FF6F00'], volume: '30ml', isFavorite: true },
{ id: 'p11', name: '腮红盘', brand: 'Dior', category: '彩妆', price: 420, originalPrice: 480, rating: 4.8, reviewCount: 980, soldCount: 6500, coverColor: '#E91E63', coverColor2: '#F06292', tags: ['自然', '显色'], isHot: false, isNew: true, isOnSale: false, discount: 9, description: '自然红润腮红盘', shadeColors: ['#C2185B', '#E91E63', '#F06292'], volume: '6g', isFavorite: false },
{ id: 'p12', name: '眼影盘', brand: 'YSL', category: '彩妆', price: 580, originalPrice: 650, rating: 4.9, reviewCount: 4300, soldCount: 15000, coverColor: '#4A148C', coverColor2: '#6A1B9A', tags: ['闪耀', '多色'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '十二色眼影盘,闪耀迷人', shadeColors: ['#4A148C', '#6A1B9A', '#7B1FA2'], volume: '12色', isFavorite: true },
{ id: 'p13', name: '润唇膏', brand: 'Dior', category: '口红', price: 280, originalPrice: 320, rating: 4.6, reviewCount: 1200, soldCount: 9800, coverColor: '#FF4081', coverColor2: '#FF80AB', tags: ['滋润', '日常'], isHot: false, isNew: false, isOnSale: true, discount: 9, description: '变色润唇膏,自然润泽', shadeColors: ['#E91E63', '#FF4081', '#FF80AB'], volume: '3.5g', isFavorite: false },
{ id: 'p14', name: '美白精华', brand: 'SK-II', category: '精华', price: 1280, originalPrice: 1480, rating: 4.8, reviewCount: 5600, soldCount: 18000, coverColor: '#01579B', coverColor2: '#0277BD', tags: ['美白', '提亮'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '美白淡斑精华,均匀肤色', shadeColors: ['#01579B'], volume: '30ml', isFavorite: true },
{ id: 'p15', name: '修护面霜', brand: 'Lancome', category: '面霜', price: 920, originalPrice: 1100, rating: 4.7, reviewCount: 3200, soldCount: 11000, coverColor: '#880E4F', coverColor2: '#AD1457', tags: ['修护', '滋润'], isHot: false, isNew: true, isOnSale: true, discount: 8, description: '深层修护面霜,紧致肌肤', shadeColors: ['#880E4F'], volume: '50ml', isFavorite: false },
{ id: 'p16', name: '睫毛膏', brand: 'Chanel', category: '彩妆', price: 350, originalPrice: 400, rating: 4.7, reviewCount: 2800, soldCount: 14000, coverColor: '#212121', coverColor2: '#424242', tags: ['纤长', '卷翘'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '纤长卷翘睫毛膏', shadeColors: ['#212121'], volume: '10ml', isFavorite: true },
{ id: 'p17', name: '护手霜', brand: 'Lancome', category: '护肤', price: 180, originalPrice: 220, rating: 4.5, reviewCount: 890, soldCount: 12000, coverColor: '#F48FB1', coverColor2: '#F8BBD0', tags: ['滋润', '便携'], isHot: false, isNew: false, isOnSale: true, discount: 8, description: '深层滋润护手霜', shadeColors: ['#F48FB1'], volume: '50ml', isFavorite: false },
{ id: 'p18', name: '卸妆油', brand: 'Dior', category: '护肤', price: 260, originalPrice: 300, rating: 4.6, reviewCount: 1100, soldCount: 7800, coverColor: '#FF8A65', coverColor2: '#FFAB91', tags: ['温和', '深层清洁'], isHot: false, isNew: false, isOnSale: true, discount: 9, description: '温和深层卸妆油', shadeColors: ['#FF8A65'], volume: '150ml', isFavorite: true },
{ id: 'p19', name: '保湿水', brand: 'SK-II', category: '护肤', price: 1190, originalPrice: 1390, rating: 4.8, reviewCount: 4500, soldCount: 16000, coverColor: '#00838F', coverColor2: '#00ACC1', tags: ['保湿', '平衡'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '保湿平衡爽肤水', shadeColors: ['#00838F'], volume: '230ml', isFavorite: false },
{ id: 'p20', name: '唇釉', brand: 'YSL', category: '口红', price: 360, originalPrice: 400, rating: 4.9, reviewCount: 5600, soldCount: 25000, coverColor: '#D32F2F', coverColor2: '#EF5350', tags: ['镜面', '持久'], isHot: true, isNew: true, isOnSale: true, discount: 9, description: '镜面唇釉,水光感十足', shadeColors: ['#B71C1C', '#D32F2F', '#E53935'], volume: '6ml', isFavorite: true },
{ id: 'p21', name: '散粉', brand: 'Chanel', category: '彩妆', price: 480, originalPrice: 540, rating: 4.7, reviewCount: 2100, soldCount: 9500, coverColor: '#F5F5DC', coverColor2: '#FFF8DC', tags: ['控油', '轻薄'], isHot: false, isNew: false, isOnSale: true, discount: 9, description: '轻薄控油散粉', shadeColors: ['#F5F5DC'], volume: '15g', isFavorite: false },
{ id: 'p22', name: '香水小样', brand: 'Dior', category: '香水', price: 199, originalPrice: 280, rating: 4.5, reviewCount: 680, soldCount: 5600, coverColor: '#FFD54F', coverColor2: '#FFE082', tags: ['便携', '多种香型'], isHot: false, isNew: true, isOnSale: true, discount: 7, description: '四款经典香水小样套装', shadeColors: ['#FFD54F', '#FFE082'], volume: '4*5ml', isFavorite: true },
];
// 分类数据 - 8+ items
private categories: CategoryItem[] = [
{ id: 'c1', name: '护肤', icon: '🧴', color: '#E91E63', bg: '#FCE4EC', count: 320, subCategories: ['洁面', '爽肤水', '精华', '面霜', '眼霜', '面膜'] },
{ id: 'c2', name: '彩妆', icon: '💄', color: '#C2185B', bg: '#F8BBD0', count: 280, subCategories: ['粉底', '遮瑕', '腮红', '眼影', '睫毛膏', '散粉'] },
{ id: 'c3', name: '香水', icon: '🌸', color: '#7B1FA2', bg: '#E1BEE7', count: 150, subCategories: ['女香', '男香', '中性香', '淡香水', '浓香水', '香氛礼盒'] },
{ id: 'c4', name: '面膜', icon: '🎭', color: '#00838F', bg: '#B2EBF2', count: 180, subCategories: ['贴片面膜', '涂抹面膜', '睡眠面膜', '清洁面膜', '补水面膜', '美白面膜'] },
{ id: 'c5', name: '口红', icon: '💋', color: '#D32F2F', bg: '#FFCDD2', count: 220, subCategories: ['哑光', '滋润', '唇釉', '唇泥', '变色唇膏', '唇线笔'] },
{ id: 'c6', name: '精华', icon: '✨', color: '#1565C0', bg: '#BBDEFB', count: 190, subCategories: ['保湿精华', '美白精华', '抗老精华', '修护精华', '淡斑精华', '紧致精华'] },
{ id: 'c7', name: '面霜', icon: '🫙', color: '#6A1B9A', bg: '#E1BEE7', count: 130, subCategories: ['日霜', '晚霜', '保湿霜', '修护霜', '抗老霜', '提亮霜'] },
{ id: 'c8', name: '防晒', icon: '☀️', color: '#FF6F00', bg: '#FFE0B2', count: 95, subCategories: ['面部防晒', '身体防晒', '隔离', '防晒喷雾', '晒后修复', '儿童防晒'] },
];
// 购物车数据 - 12+ items
private cartItems: CartItem[] = [
{ id: 'cart1', productId: 'p1', productName: '烈艳蓝金唇膏', brand: 'Dior', price: 350, quantity: 1, coverColor: '#C2185B', coverColor2: '#E91E63', shade: '色号#999', isSelected: true, stock: 50 },
{ id: 'cart2', productId: 'p2', productName: '小黑瓶精华', brand: 'Estée Lauder', price: 780, quantity: 1, coverColor: '#4A148C', coverColor2: '#7B1FA2', shade: '50ml', isSelected: true, stock: 30 },
{ id: 'cart3', productId: 'p3', productName: '神仙水', brand: 'SK-II', price: 1450, quantity: 1, coverColor: '#006064', coverColor2: '#00838F', shade: '230ml', isSelected: false, stock: 20 },
{ id: 'cart4', productId: 'p4', productName: '金管口红', brand: 'YSL', price: 380, quantity: 2, coverColor: '#D32F2F', coverColor2: '#FF5252', shade: '色号#21', isSelected: true, stock: 40 },
{ id: 'cart5', productId: 'p5', productName: '粉底液', brand: 'Chanel', price: 560, quantity: 1, coverColor: '#37474F', coverColor2: '#546E7A', shade: '色号#20', isSelected: true, stock: 25 },
{ id: 'cart6', productId: 'p6', productName: '红石榴面霜', brand: 'Lancome', price: 680, quantity: 1, coverColor: '#BF360C', coverColor2: '#E64A19', shade: '50ml', isSelected: false, stock: 35 },
{ id: 'cart7', productId: 'p7', productName: '小棕瓶精华', brand: 'Estée Lauder', price: 950, quantity: 1, coverColor: '#1A237E', coverColor2: '#283593', shade: '50ml', isSelected: true, stock: 15 },
{ id: 'cart8', productId: 'p8', productName: '香水', brand: 'Chanel', price: 890, quantity: 1, coverColor: '#FFD700', coverColor2: '#FFA000', shade: '100ml', isSelected: true, stock: 10 },
{ id: 'cart9', productId: 'p12', productName: '眼影盘', brand: 'YSL', price: 580, quantity: 1, coverColor: '#4A148C', coverColor2: '#6A1B9A', shade: '12色盘', isSelected: false, stock: 28 },
{ id: 'cart10', productId: 'p14', productName: '美白精华', brand: 'SK-II', price: 1280, quantity: 1, coverColor: '#01579B', coverColor2: '#0277BD', shade: '30ml', isSelected: true, stock: 18 },
{ id: 'cart11', productId: 'p16', productName: '睫毛膏', brand: 'Chanel', price: 350, quantity: 2, coverColor: '#212121', coverColor2: '#424242', shade: '纤长款', isSelected: true, stock: 45 },
{ id: 'cart12', productId: 'p20', productName: '唇釉', brand: 'YSL', price: 360, quantity: 1, coverColor: '#D32F2F', coverColor2: '#EF5350', shade: '色号#416', isSelected: false, stock: 33 },
];
// 收藏数据 - 15+ items
private favoriteProducts: BeautyProduct[] = [
{ id: 'f1', name: '烈艳蓝金唇膏', brand: 'Dior', category: '口红', price: 350, originalPrice: 420, rating: 4.9, reviewCount: 2380, soldCount: 15600, coverColor: '#C2185B', coverColor2: '#E91E63', tags: ['热销'], isHot: true, isNew: false, isOnSale: true, discount: 8, description: '经典丝绒唇膏', shadeColors: ['#880E4F', '#C2185B'], volume: '3.5g', isFavorite: true },
{ id: 'f2', name: '小黑瓶精华', brand: 'Estée Lauder', category: '精华', price: 780, originalPrice: 980, rating: 4.8, reviewCount: 4560, soldCount: 28000, coverColor: '#4A148C', coverColor2: '#7B1FA2', tags: ['修护'], isHot: true, isNew: false, isOnSale: true, discount: 8, description: '夜间修护精华', shadeColors: ['#311B92'], volume: '50ml', isFavorite: true },
{ id: 'f3', name: '金管口红', brand: 'YSL', category: '口红', price: 380, originalPrice: 420, rating: 4.7, reviewCount: 3200, soldCount: 18000, coverColor: '#D32F2F', coverColor2: '#FF5252', tags: ['滋润'], isHot: false, isNew: true, isOnSale: false, discount: 9, description: '金管滋润唇膏', shadeColors: ['#B71C1C', '#D32F2F'], volume: '3.5g', isFavorite: true },
{ id: 'f4', name: '红石榴面霜', brand: 'Lancome', category: '面霜', price: 680, originalPrice: 850, rating: 4.6, reviewCount: 1800, soldCount: 9500, coverColor: '#BF360C', coverColor2: '#E64A19', tags: ['保湿'], isHot: false, isNew: false, isOnSale: true, discount: 8, description: '红石榴面霜', shadeColors: ['#BF360C'], volume: '50ml', isFavorite: true },
{ id: 'f5', name: '香水', brand: 'Chanel', category: '香水', price: 890, originalPrice: 990, rating: 4.9, reviewCount: 3400, soldCount: 12000, coverColor: '#FFD700', coverColor2: '#FFA000', tags: ['经典'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '经典五号香水', shadeColors: ['#F57F17'], volume: '100ml', isFavorite: true },
{ id: 'f6', name: '眼影盘', brand: 'YSL', category: '彩妆', price: 580, originalPrice: 650, rating: 4.9, reviewCount: 4300, soldCount: 15000, coverColor: '#4A148C', coverColor2: '#6A1B9A', tags: ['闪耀'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '十二色眼影盘', shadeColors: ['#4A148C'], volume: '12色', isFavorite: true },
{ id: 'f7', name: '唇釉', brand: 'YSL', category: '口红', price: 360, originalPrice: 400, rating: 4.9, reviewCount: 5600, soldCount: 25000, coverColor: '#D32F2F', coverColor2: '#EF5350', tags: ['镜面'], isHot: true, isNew: true, isOnSale: true, discount: 9, description: '镜面唇釉', shadeColors: ['#D32F2F'], volume: '6ml', isFavorite: true },
{ id: 'f8', name: '润唇膏', brand: 'Dior', category: '口红', price: 280, originalPrice: 320, rating: 4.6, reviewCount: 1200, soldCount: 9800, coverColor: '#FF4081', coverColor2: '#FF80AB', tags: ['滋润'], isHot: false, isNew: false, isOnSale: true, discount: 9, description: '变色润唇膏', shadeColors: ['#FF4081'], volume: '3.5g', isFavorite: true },
{ id: 'f9', name: '美白精华', brand: 'SK-II', category: '精华', price: 1280, originalPrice: 1480, rating: 4.8, reviewCount: 5600, soldCount: 18000, coverColor: '#01579B', coverColor2: '#0277BD', tags: ['美白'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '美白精华', shadeColors: ['#01579B'], volume: '30ml', isFavorite: true },
{ id: 'f10', name: '卸妆油', brand: 'Dior', category: '护肤', price: 260, originalPrice: 300, rating: 4.6, reviewCount: 1100, soldCount: 7800, coverColor: '#FF8A65', coverColor2: '#FFAB91', tags: ['温和'], isHot: false, isNew: false, isOnSale: true, discount: 9, description: '温和卸妆油', shadeColors: ['#FF8A65'], volume: '150ml', isFavorite: true },
{ id: 'f11', name: '睫毛膏', brand: 'Chanel', category: '彩妆', price: 350, originalPrice: 400, rating: 4.7, reviewCount: 2800, soldCount: 14000, coverColor: '#212121', coverColor2: '#424242', tags: ['纤长'], isHot: true, isNew: false, isOnSale: true, discount: 9, description: '纤长睫毛膏', shadeColors: ['#212121'], volume: '10ml', isFavorite: true },
{ id: 'f12', name: '护手霜', brand: 'Lancome', category: '护肤', price: 180, originalPrice: 220, rating: 4.5, reviewCount: 890, soldCount: 12000, coverColor: '#F48FB1', coverColor2: '#F8BBD0', tags: ['滋润'], isHot: false, isNew: false, isOnSale: true, discount: 8, description: '滋润护手霜', shadeColors: ['#F48FB1'], volume: '50ml', isFavorite: true },
{ id: 'f13', name: '香水小样', brand: 'Dior', category: '香水', price: 199, originalPrice: 280, rating: 4.5, reviewCount: 680, soldCount: 5600, coverColor: '#FFD54F', coverColor2: '#FFE082', tags: ['便携'], isHot: false, isNew: true, isOnSale: true, discount: 7, description: '四款香水小样', shadeColors: ['#FFD54F'], volume: '4*5ml', isFavorite: true },
{ id: 'f14', name: '腮红盘', brand: 'Dior', category: '彩妆', price: 420, originalPrice: 480, rating: 4.8, reviewCount: 980, soldCount: 6500, coverColor: '#E91E63', coverColor2: '#F06292', tags: ['自然'], isHot: false, isNew: true, isOnSale: false, discount: 9, description: '自然腮红盘', shadeColors: ['#C2185B'], volume: '6g', isFavorite: true },
{ id: 'f15', name: '修护面霜', brand: 'Lancome', category: '面霜', price: 920, originalPrice: 1100, rating: 4.7, reviewCount: 3200, soldCount: 11000, coverColor: '#880E4F', coverColor2: '#AD1457', tags: ['修护'], isHot: false, isNew: true, isOnSale: true, discount: 8, description: '修护面霜', shadeColors: ['#880E4F'], volume: '50ml', isFavorite: true },
];
// 评价数据
private reviews: ReviewItem[] = [
{ id: 'r1', productId: 'p1', author: '美丽小仙女', avatar: '🌸', rating: 5, content: '颜色超级正!上嘴很舒服,持久度也很好,会回购!', date: '2026-07-28', images: ['#C2185B', '#E91E63'], likes: 128, isVerified: true },
{ id: 'r2', productId: 'p1', author: '化妆师Lily', avatar: '💄', rating: 5, content: '专业推荐!丝绒质感一流,显色度满分', date: '2026-07-25', images: ['#880E4F'], likes: 95, isVerified: true },
{ id: 'r3', productId: 'p1', author: '爱美的猫', avatar: '🐱', rating: 4, content: '颜色好看,就是稍微有点干,搭配润唇膏更好', date: '2026-07-20', images: [], likes: 32, isVerified: false },
{ id: 'r4', productId: 'p2', author: '护肤达人', avatar: '✨', rating: 5, content: '用了一个月,皮肤明显变好了,修护效果很棒', date: '2026-07-22', images: ['#4A148C'], likes: 210, isVerified: true },
];
// 周销售数据
private weeklySales: WeeklySaleData[] = [
{ day: '周一', sales: 1200 },
{ day: '周二', sales: 1800 },
{ day: '周三', sales: 1500 },
{ day: '周四', sales: 2200 },
{ day: '周五', sales: 2800 },
{ day: '周六', sales: 3500 },
{ day: '周日', sales: 3200 },
];
// 订单数据
private orders: OrderRecord[] = [
{ id: 'o1', orderNo: 'DD20260728001', date: '2026-07-28', status: '已发货', total: 730, count: 2 },
{ id: 'o2', orderNo: 'DD20260720002', date: '2026-07-20', status: '已完成', total: 1450, count: 1 },
{ id: 'o3', orderNo: 'DD20260710003', date: '2026-07-10', status: '已完成', total: 940, count: 3 },
];
private brandKeys: string[] = ['dior', 'chanel', 'lancome', 'esteelauder', 'skii', 'ysl'];
private categoryKeys: string[] = ['skincare', 'makeup', 'perfume', 'mask', 'lipstick', 'essence', 'cream', 'sunscreen'];
private skinKeys: string[] = ['type', 'tone', 'concern'];
private reviewTagKeys: string[] = ['t1', 't2', 't3', 't4', 't5'];
// 计算购物车总价
private calcCartTotal(): number {
let total: number = 0;
for (let i = 0; i < this.cartItems.length; i++) {
if (this.cartItems[i].isSelected) {
total += this.cartItems[i].price * this.cartItems[i].quantity;
}
}
return total;
}
private calcSelectedCount(): number {
let count: number = 0;
for (let i = 0; i < this.cartItems.length; i++) {
if (this.cartItems[i].isSelected) {
count += this.cartItems[i].quantity;
}
}
return count;
}
private getProductById(id: string): BeautyProduct {
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === id) {
return this.products[i];
}
}
return this.products[0];
}
// ==================== 主构建 ====================
build() {
Stack({ alignContent: Alignment.TopStart }) {
Column() {
if (this.currentTab === 0) {
this.homeTab()
} else if (this.currentTab === 1) {
this.categoryTab()
} else if (this.currentTab === 2) {
this.cartTab()
} else if (this.currentTab === 3) {
this.favoriteTab()
} else {
this.profileTab()
}
this.bottomTabBar()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS['bg'])
if (this.showAddCartModal || this.showReviewModal || this.showRemoveFavModal || this.showProductDetail) {
this.modalOverlay()
}
}
.width('100%')
.height('100%')
}
// ==================== 模态遮罩 ====================
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Center }) {
// 遮罩背景
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(136,14,79,0.5)')
.onClick(() => {
this.showAddCartModal = false;
this.showReviewModal = false;
this.showRemoveFavModal = false;
this.showProductDetail = false;
})
if (this.showAddCartModal) {
this.addCartModalContent()
}
if (this.showReviewModal) {
this.reviewModalContent()
}
if (this.showRemoveFavModal) {
this.removeFavModalContent()
}
if (this.showProductDetail) {
this.productDetailModalContent()
}
}
.width('100%')
.height('100%')
}
// ==================== 底部导航 ====================
@Builder
bottomTabBar() {
Row() {
ForEach([0, 1, 2, 3, 4], (tabIndex: number) => {
Column() {
Text(this.getTabIcon(tabIndex))
.fontSize(24)
Text(this.getTabName(tabIndex))
.fontSize(10)
.fontColor(this.currentTab === tabIndex ? COLORS['primary'] : COLORS['gray'])
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = tabIndex;
})
})
}
.width('100%')
.height(56)
.backgroundColor(COLORS['card'])
.border({ width: { top: 1 }, color: COLORS['border'], radius: 0, style: BorderStyle.Solid })
.justifyContent(FlexAlign.SpaceAround)
.alignItems(VerticalAlign.Center)
}
private getTabIcon(index: number): string {
if (index === 0) return '💄';
if (index === 1) return '📂';
if (index === 2) return '🛒';
if (index === 3) return '❤️';
return '👤';
}
private getTabName(index: number): string {
if (index === 0) return '首页';
if (index === 1) return '分类';
if (index === 2) return '购物车';
if (index === 3) return '收藏';
return '我的';
}
// ==================== 首页 Tab ====================
@Builder
homeTab() {
Scroll() {
Column() {
// 粉色渐变头部 - 闪购倒计时
this.flashSaleBanner()
// 品牌快捷入口
this.brandRow()
// 热门商品网格
this.productGridSection()
// 为你推荐
this.recommendSection()
// 周销售图表
this.weeklySalesChart()
// 底部留白
Column().height(20)
}
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
@Builder
flashSaleBanner() {
Column() {
// 搜索栏
Row() {
Text('💄 美妆商城')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Column().layoutWeight(1)
Text('🔍')
.fontSize(22)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
// 闪购标题
Row() {
Column() {
Text('⚡ 限时闪购')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
Text('精选美妆 低至5折')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
// 倒计时
Row() {
Text('距结束')
.fontSize(11)
.fontColor('rgba(255,255,255,0.8)')
Text(this.countdownHours.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 4 })
Text(':')
.fontSize(14)
.fontColor(COLORS['card'])
.margin({ left: 2, right: 2 })
Text(this.countdownMinutes.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(':')
.fontSize(14)
.fontColor(COLORS['card'])
.margin({ left: 2, right: 2 })
Text(this.countdownSeconds.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.backgroundColor(COLORS['card'])
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
}
.width('100%')
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.shadow({ radius: 12, color: 'rgba(233,30,99,0.3', offsetX: 0, offsetY: 4 })
}
@Builder
brandRow() {
Column() {
Text('🔥 热门品牌')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
.alignSelf(ItemAlign.Start)
.margin({ left: 16, top: 16, bottom: 12 })
Scroll() {
Column() {
Row() {
ForEach(this.brandKeys, (key: string) => {
Column() {
Column() {
Text(BRAND_METAS[key].icon)
.fontSize(24)
}
.width(52)
.height(52)
.borderRadius(26)
.backgroundColor(COLORS['bg'])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 6, color: 'rgba(233,30,99,0.15', offsetX: 0, offsetY: 2 })
Text(BRAND_METAS[key].name)
.fontSize(10)
.fontColor(COLORS['textPrimary'])
.margin({ top: 6 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.margin({ right: 12 })
.onClick(() => {
this.currentTab = 1;
})
})
}
.padding({ left: 16, right: 16 })
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
}
.width('100%')
}
@Builder
productGridSection() {
Column() {
Row() {
Text('🎁 精选好物')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textPrimary'])
Column().layoutWeight(1)
Text('查看全部 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
// 2列商品网格
Grid() {
ForEach(this.products, (product: BeautyProduct, index: number) => {
GridItem() {
this.productCard(product, index)
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding({ left: 16, right: 16 })
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
}
// 美妆档案
Column() {
Row() {
Text('🧴 美妆档案')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('编辑 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
Row() {
ForEach(this.skinKeys, (key: string) => {
Column() {
Text(SKIN_TYPE_METAS[key].label)
.fontSize(11)
.fontColor(COLORS['gray'])
Text(SKIN_TYPE_METAS[key].value)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textPrimary'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS['bg'])
.borderRadius(12)
.margin({ left: 4, right: 4 })
})
}
.width('100%')
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(16)
.margin({ top: 12 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08', offsetX: 0, offsetY: 2 })
// 我的订单
Column() {
Row() {
Text('📦 我的订单')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('全部 >')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
// 订单状态快捷入口
Row() {
Column() {
Text('💳')
.fontSize(24)
Text('待付款')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('🚚')
.fontSize(24)
Text('待发货')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('📦')
.fontSize(24)
Text('待收货')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('⭐')
.fontSize(24)
Text('待评价')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('↩️')
.fontSize(24)
Text('退换')
.fontSize(10)
.fontColor(COLORS['textSub'])
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.margin({ bottom: 12 })
// 订单列表
ForEach(this.orders, (order: OrderRecord, index: number) => {
Row() {
Column() {
Text(order.orderNo)
.fontSize(12)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Text(order.date + ' · ' + order.count.toString() + '件商品')
.fontSize(10)
.fontColor(COLORS['gray'])
.margin({ top: 4 })
.alignSelf(ItemAlign.Start)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + order.total.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text(order.status)
.fontSize(10)
.fontColor(COLORS['textSecondary'])
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: 1, color: COLORS['border'], radius: 0, style: BorderStyle.Solid })
.alignItems(VerticalAlign.Center)
.margin({ top: 4 })
})
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(16)
.margin({ top: 12 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08', offsetX: 0, offsetY: 2 })
// 美妆日记
Column() {
Row() {
Text('📔 美妆日记')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('写日记 ✍️')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 12 })
Column() {
Row() {
Text('📅')
.fontSize(20)
Column() {
Text('今日护肤记录')
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
Text('已完成5步护肤流程,肌肤状态良好')
.fontSize(11)
.fontColor(COLORS['gray'])
.margin({ top: 2 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS['bg'])
.borderRadius(12)
}
.width('92%')
.backgroundColor(COLORS['card'])
.borderRadius(16)
.padding(16)
.margin({ top: 12, bottom: 20 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.08', offsetX: 0, offsetY: 2 })
Column().height(20)
}
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
// ==================== 加入购物车弹窗 ====================
@Builder
addCartModalContent() {
Column() {
// 标题行
Row() {
Text('加入购物车')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('✕')
.fontSize(20)
.fontColor(COLORS['gray'])
.onClick(() => {
this.showAddCartModal = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.padding({ left: 20, right: 20, top: 20, bottom: 12 })
// 商品预览
Row() {
Column()
.width(80)
.height(80)
.linearGradient({
direction: GradientDirection.Left,
colors: [[this.products[this.selectedProductIndex].coverColor, 0], [this.products[this.selectedProductIndex].coverColor2, 1]]
})
.borderRadius(12)
Column() {
Text(this.products[this.selectedProductIndex].name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Row() {
Text('¥' + this.products[this.selectedProductIndex].price.toString())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('¥' + this.products[this.selectedProductIndex].originalPrice.toString())
.fontSize(12)
.fontColor(COLORS['gray'])
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
}
.alignItems(VerticalAlign.Bottom)
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 12 })
.alignItems(VerticalAlign.Center)
// 规格信息
Row() {
Text('规格')
.fontSize(13)
.fontColor(COLORS['textSub'])
Text(this.products[this.selectedProductIndex].volume)
.fontSize(13)
.fontColor(COLORS['textDark'])
.margin({ left: 8 })
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 8 })
// 色号选择
Text('选择色号')
.fontSize(13)
.fontColor(COLORS['textSub'])
.alignSelf(ItemAlign.Start)
.margin({ left: 20, bottom: 8 })
// 色号网格
Grid() {
ForEach(this.products[this.selectedProductIndex].shadeColors, (shade: string, sIndex: number) => {
GridItem() {
Column() {
Column()
.width(40)
.height(40)
.backgroundColor(shade)
.borderRadius(20)
.border({
width: this.selectedShadeIndex === sIndex ? 3 : 0,
color: COLORS['primary'],
radius: 22,
style: BorderStyle.Solid
})
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.selectedShadeIndex = sIndex;
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding({ left: 20, right: 20 })
.height(100)
// 数量选择
Row() {
Text('数量')
.fontSize(14)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Row() {
Text('−')
.fontSize(18)
.fontColor(COLORS['textPrimary'])
.width(36)
.height(36)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['bg'])
.borderRadius({ topLeft: 8, bottomLeft: 8 })
Text('1')
.fontSize(16)
.fontColor(COLORS['textDark'])
.width(44)
.height(36)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['card'])
Text('+')
.fontSize(18)
.fontColor(COLORS['textPrimary'])
.width(36)
.height(36)
.textAlign(TextAlign.Center)
.backgroundColor(COLORS['bg'])
.borderRadius({ topRight: 8, bottomRight: 8 })
}
.height(36)
.borderRadius(8)
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 16 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
// 确认按钮
Row() {
Text('确认加入购物车')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.width('90%')
.height(48)
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.justifyContent(FlexAlign.Center)
.borderRadius(24)
.margin({ bottom: 20 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.3', offsetX: 0, offsetY: 4 })
.onClick(() => {
this.showAddCartModal = false;
})
}
.width('88%')
.backgroundColor(COLORS['card'])
.borderRadius(24)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15', offsetX: 0, offsetY: 8 })
}
// ==================== 写评价弹窗 ====================
@Builder
reviewModalContent() {
Column() {
Row() {
Text('✍️ 写评价')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('✕')
.fontSize(20)
.fontColor(COLORS['gray'])
.onClick(() => {
this.showReviewModal = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.padding({ left: 20, right: 20, top: 20, bottom: 16 })
// 评分星星
Row() {
Text('你的评分')
.fontSize(14)
.fontColor(COLORS['textSub'])
}
.width('100%')
.padding({ left: 20, bottom: 8 })
Row() {
ForEach([1, 2, 3, 4, 5], (star: number) => {
Text(star <= this.reviewRating ? '⭐' : '☆')
.fontSize(32)
.margin({ right: 8 })
.onClick(() => {
this.reviewRating = star;
})
})
}
.width('100%')
.padding({ left: 20, bottom: 16 })
// 评价内容输入
Text('分享你的使用体验')
.fontSize(14)
.fontColor(COLORS['textSub'])
.alignSelf(ItemAlign.Start)
.margin({ left: 20, bottom: 8 })
Column() {
Text(this.reviewText)
.fontSize(14)
.fontColor(this.reviewText.length > 0 ? COLORS['textDark'] : COLORS['gray'])
.width('100%')
.textAlign(TextAlign.Start)
}
.width('88%')
.height(120)
.backgroundColor(COLORS['bg'])
.borderRadius(12)
.padding(12)
.margin({ bottom: 16 })
// 评价标签
Text('选择标签')
.fontSize(13)
.fontColor(COLORS['textSub'])
.alignSelf(ItemAlign.Start)
.margin({ left: 20, bottom: 8 })
// 使用Flex布局标签
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.reviewTagKeys, (key: string) => {
Text(REVIEW_TAG_METAS[key].label)
.fontSize(12)
.fontColor(COLORS['primary'])
.backgroundColor(COLORS['bg'])
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
})
}
.width('88%')
.padding({ left: 4, right: 4 })
.margin({ bottom: 16 })
// 提交按钮
Row() {
Text('发布评价')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
}
.width('90%')
.height(48)
.linearGradient({
direction: GradientDirection.Right,
colors: [[COLORS['primary'], 0], [COLORS['textPrimary'], 1]]
})
.justifyContent(FlexAlign.Center)
.borderRadius(24)
.margin({ bottom: 20 })
.shadow({ radius: 8, color: 'rgba(233,30,99,0.3', offsetX: 0, offsetY: 4 })
.onClick(() => {
this.showReviewModal = false;
})
}
.width('88%')
.backgroundColor(COLORS['card'])
.borderRadius(24)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15', offsetX: 0, offsetY: 8 })
}
// ==================== 移除收藏确认弹窗 ====================
@Builder
removeFavModalContent() {
Column() {
Column() {
Text('💔')
.fontSize(48)
Text('取消收藏')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
.margin({ top: 12 })
Text('确定要将这个商品从收藏中移除吗?')
.fontSize(13)
.fontColor(COLORS['gray'])
.margin({ top: 8 })
.textAlign(TextAlign.Center)
}
.width('100%')
.padding({ top: 28, bottom: 20 })
.alignItems(HorizontalAlign.Center)
// 按钮行
Row() {
Text('再想想')
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textSub'])
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(44)
.backgroundColor(COLORS['bg'])
.borderRadius(22)
.onClick(() => {
this.showRemoveFavModal = false;
})
Text('确定移除')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(44)
.backgroundColor(COLORS['sale'])
.borderRadius(22)
.margin({ left: 12 })
.onClick(() => {
this.showRemoveFavModal = false;
})
}
.width('85%')
.margin({ bottom: 24 })
}
.width('76%')
.backgroundColor(COLORS['card'])
.borderRadius(24)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15', offsetX: 0, offsetY: 8 })
}
// ==================== 商品详情弹窗 ====================
@Builder
productDetailModalContent() {
Scroll() {
Column() {
// 商品大图
Stack({ alignContent: Alignment.TopEnd }) {
Column()
.width('100%')
.height(200)
.linearGradient({
direction: GradientDirection.Left,
colors: [[this.products[this.selectedProductIndex].coverColor, 0], [this.products[this.selectedProductIndex].coverColor2, 1]]
})
.borderRadius({ topLeft: 24, topRight: 24 })
// 关闭按钮
Column() {
Text('✕')
.fontSize(18)
.fontColor(COLORS['card'])
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('rgba(0,0,0,0.3)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.margin({ top: 12, right: 12 })
.onClick(() => {
this.showProductDetail = false;
})
// 品牌名
Column() {
Text(this.products[this.selectedProductIndex].brand)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('rgba(255,255,255,0.9)')
Text(this.products[this.selectedProductIndex].category)
.fontSize(14)
.fontColor('rgba(255,255,255,0.7)')
.margin({ top: 4 })
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
// 商品信息
Column() {
Text(this.products[this.selectedProductIndex].name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
Text(this.products[this.selectedProductIndex].description)
.fontSize(13)
.fontColor(COLORS['textSub'])
.margin({ top: 8 })
.alignSelf(ItemAlign.Start)
// 评分行
Row() {
Text('★' + this.products[this.selectedProductIndex].rating.toFixed(1))
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['gold'])
Text(this.products[this.selectedProductIndex].reviewCount.toString() + '条评价')
.fontSize(12)
.fontColor(COLORS['gray'])
.margin({ left: 8 })
Text('已售' + this.products[this.selectedProductIndex].soldCount.toString())
.fontSize(12)
.fontColor(COLORS['gray'])
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 12 })
.alignItems(VerticalAlign.Center)
// 价格行
Row() {
Text('¥' + this.products[this.selectedProductIndex].price.toString())
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['primary'])
Text('¥' + this.products[this.selectedProductIndex].originalPrice.toString())
.fontSize(14)
.fontColor(COLORS['gray'])
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 8 })
Column().layoutWeight(1)
if (this.products[this.selectedProductIndex].isOnSale) {
Text(this.products[this.selectedProductIndex].discount.toString() + '折')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['card'])
.backgroundColor(COLORS['sale'])
.borderRadius(12)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
}
}
.width('100%')
.margin({ top: 8 })
.alignItems(VerticalAlign.Bottom)
// 标签行
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.products[this.selectedProductIndex].tags, (tag: string, tIndex: number) => {
Text(tag)
.fontSize(11)
.fontColor(COLORS['primary'])
.backgroundColor(COLORS['bg'])
.borderRadius(10)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.margin({ right: 6, bottom: 6 })
})
}
.width('100%')
.margin({ top: 12 })
// 色号选择
Text('色号选择')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
.alignSelf(ItemAlign.Start)
.margin({ top: 16, bottom: 8 })
Row() {
ForEach(this.products[this.selectedProductIndex].shadeColors, (shade: string, sIndex: number) => {
Column()
.width(36)
.height(36)
.backgroundColor(shade)
.borderRadius(18)
.border({
width: 2,
color: COLORS['border'],
radius: 20,
style: BorderStyle.Solid
})
.margin({ right: 8 })
})
}
.width('100%')
// 规格信息
Row() {
Text('规格: ')
.fontSize(13)
.fontColor(COLORS['textSub'])
Text(this.products[this.selectedProductIndex].volume)
.fontSize(13)
.fontColor(COLORS['textDark'])
}
.width('100%')
.margin({ top: 12 })
// 用户评价标题
Row() {
Text('用户评价')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS['textDark'])
Column().layoutWeight(1)
Text('写评价 ✍️')
.fontSize(12)
.fontColor(COLORS['textSecondary'])
.onClick(() => {
this.showReviewModal = true;
})
}
.width('100%')
.margin({ top: 20, bottom: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
// 评价列表
ForEach(this.reviews, (review: ReviewItem, rIndex: number) => {
Column() {
Row() {
Column() {
Text(review.avatar)
.fontSize(24)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(COLORS['bg'])
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(review.author)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS['textDark'])
if (review.isVerified) {
Text('✓ 已验证')
.fontSize(9)
.fontColor(COLORS['new'])
.backgroundColor('#E8F5E9')
.borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
.margin({ left: 6 })
}
}
.alignItems(VerticalAlign.Center)
Text('★'.repeat(review.rating))
.fontSize(11)
.fontColor(COLORS['gold'])
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(review.date)
.fontSize(10)
.fontColor(COLORS['gray'])
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(review.content)
.fontSize(13)
.fontColor(COLORS['textDark'])
.margin({ top: 8 })
.alignSelf(ItemAlign.Start)
// 评价图片
if (review.images.length > 0) {
Row() {
ForEach(review.images, (img: string, iIndex: number) => {
Column()
.width(48)
.height(48)
.backgroundColor(img)
.borderRadius(8)
.margin({ right: 6 })
})
}
.width('100%')
.margin({ top: 8 })
}
Row() {
Text('👍 ' + review.likes.toString())
.fontSize(11)
.fontColor(COLORS['gray'])
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS['bg'])
.borderRadius(12)
.margin({ bottom: 8 })
})
Column().height(20)
}
.width('100%')
.padding(16)
}
}
.width('90%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor(COLORS['card'])
.borderRadius(24)
.scrollBar(BarState.Off)
.shadow({ radius: 20, color: 'rgba(0,0,0,0.15', offsetX: 0, offsetY: 8 })
}
}
14.4 未来优化方向
虽然这个应用在功能和视觉上已经相当完善,但在实际产品化时还有以下几个可以优化的方向:

数据层的异步化。 当前所有数据都是硬编码在组件中的静态数组,在真实应用中应该通过 HTTP 请求从后端接口异步获取。可以引入数据仓库层,统一管理数据的获取、缓存和更新逻辑。
倒计时的实时化。 闪购倒计时目前使用静态初始值,在实际应用中应该使用定时器(setInterval)每秒递减 countdownSeconds,并在倒计时归零时触发促销结束的逻辑。
状态管理的升级。 当应用规模增大时,可以考虑使用 @Observed 和 @ObjectLink 实现跨组件的状态共享,或者引入全局状态管理方案,避免通过组件层级传递状态。
图片资源的引入。 在产品化阶段,应该将渐变色块替换为真实的商品图片,引入图片加载库处理网络图片的加载、缓存和占位图。
搜索功能的实现。 首页搜索栏目前只是视觉展示,应该实现搜索功能,支持按商品名称、品牌、分类等维度进行搜索过滤。
个性化推荐算法。 "为你推荐"区域目前展示的是固定的前 8 个商品,在真实应用中应该基于用户的浏览历史、购买记录和美妆档案数据,通过推荐算法返回个性化的商品列表。
更多推荐
所有评论(0)