葡萄酒品鉴记录应用基于HarmonyOS API 24使用了 Stack 布局来叠加滚动内容和弹窗,滚动容器设置了 scrollBar(BarState.Off) 隐藏滚动条
引言:当葡萄酒文化遇见移动端技术
葡萄酒,作为一种承载了千年文明的饮品,早已超越了单纯的消费属性,成为生活方式与品位的象征。对于真正的葡萄酒爱好者与专业品酒师而言,每一瓶酒背后都蕴含着产地风土、酿造工艺、陈年潜力等多维度的信息。如何系统化地管理这些信息、记录每一次品鉴体验、追踪酒窖库存变化,并从中提炼出有价值的数据洞察,一直是一个既有生活温度、又有技术深度的命题。

随着移动端开发技术的不断演进,尤其是声明式 UI 范式的成熟,我们终于有机会以一种结构清晰、可维护性强的方式,构建出一款真正贴合品酒场景的应用。本文将围绕一款名为「Vinothèque」(私人酒窖管理)的葡萄酒品鉴记录应用,展开一次从顶层架构到底层细节的全景式技术剖析。
这款应用并非简单的列表展示工具,而是一个集酒款管理、品鉴记录、活动追踪、数据统计与个人中心于一体的综合性平台。它面向的是 WSET(Wine & Spirit Education Trust)认证品酒师、葡萄酒收藏家以及资深爱好者群体,因此在数据结构的精细程度、视觉表达的层次感以及交互逻辑的严谨性上,都有着较高的要求。
应用背景与设计理念
业务场景的复杂性
葡萄酒品鉴本身是一门高度结构化的学问。一瓶酒的信息维度极为丰富:从最基础的酒名、酒庄、产区、国家、年份、葡萄品种,到更深层次的酒精度、价格、颜色描述、香气轮盘、酒体、单宁、酸度、甜度,再到配餐建议、品鉴日期、品鉴地点、品鉴笔记、外观评分、香气评分、口感评分、综合评分,以及库存数量、购买日期、适饮期限等酒窖管理维度。
这些字段之间并非孤立存在,而是相互关联、相互印证的。例如,单宁与酸度的组合会影响酒体的感知;产区与葡萄品种的组合往往决定了酒款的基本风格走向;评分维度的拆分则让品鉴记录更具专业性和可追溯性。如何在数据模型中准确反映这些关联,是设计的第一个难点。
视觉风格的选择
应用的视觉风格选择了"酒红 + 香槟金"的经典搭配,背景采用淡粉色营造柔和氛围。这种配色并非随意为之:酒红色(#880E4F)呼应葡萄酒本身的色泽,传递出沉稳与高贵;香槟金色(#FFD54F)则象征着品质与庆典感,常用于评分、价格等关键信息的强调;淡粉色背景(#FCE4EC)作为基底,避免了纯白带来的冷峻感,让整体界面更显温润。
技术架构的选型思路
应用采用 ArkTS 声明式开发范式,这是一种基于 TypeScript 扩展的语言,专门为 HarmonyOS 应用开发而设计。它具备类型安全、声明式 UI、状态驱动等特性,非常适合构建这种数据密集型、多页面交互的应用。整个应用以"接口定义 -> 数据模型 -> 配置常量 -> 模拟数据 -> 组件实现"的层次组织代码,每一层各司其职,形成了清晰的关注点分离。
一、接口定义层:为数据建立契约
在大型应用中,数据结构的稳定性是整个系统的基石。应用首先通过一系列 interface 定义,为所有参与流转的数据建立了严格的类型契约。这种做法的好处在于:编译期即可发现类型不匹配的问题,IDE 能够提供精准的智能提示,同时也为后续的数据校验和文档化奠定了基础。
核心酒款接口
酒款是整个应用的核心实体,其接口定义如下:
interface WineItem {
id: number
name: string
winery: string
region: string
country: string
vintage: number
grape: string
type: string
alcohol: number
price: number
rating: number
color: string
aroma: string[]
body: string
tannin: string
acidity: string
sweetness: string
pairing: string[]
tastingDate: string
tastingLocation: string
tastingNotes: string
appearanceScore: number
aromaScore: number
palateScore: number
overallScore: number
isFavorite: boolean
image: string
stock: number
purchaseDate: string
drinkBy: number
}

下面对每一个字段进行逐行解读:
id: number:酒款的唯一标识符,采用数字类型,用于列表渲染时的 key 标识和数据查找。name: string:酒款完整名称,通常包含酒庄名、系列名和年份,例如"拉菲古堡红葡萄酒 2018"。winery: string:酒庄名称,保留了原始的外文名(如 Château Lafite Rothschild),以体现专业性。region: string:具体产区,包含中文与外文对照(如"波亚克 (Pauillac)"),便于专业用户识别。country: string:国家信息,用于统计页的产区分布图表。vintage: number:年份,用数字存储而非字符串,方便后续的数值比较与排序;对于无年份酒款(如部分波特),用 0 表示。grape: string:葡萄品种,可能为单一品种或混酿描述(如"霞多丽/黑皮诺")。type: string:葡萄酒类型,对应配置表中的键值,取值范围为"红葡萄酒"“白葡萄酒”“桃红”“起泡酒”“甜酒”“加强酒”。alcohol: number:酒精度,保留一位小数,如 13.5。price: number:价格,以人民币为单位,用于酒窖估值计算。rating: number:总体评分,范围 0-5,支持小数。color: string:颜色描述文本,如"深宝石红"“浅金黄”,用于品鉴笔记的视觉描述。aroma: string[]:香气标签数组,存储多个香气特征(如[‘黑醋栗’,‘雪松’,‘烟草’]),这是香气轮盘的简化表达。body: string:酒体,取值为"轻盈"“中等”“饱满”,对应不同的数值映射。tannin: string:单宁强度,取值为"低"“中”“高”。acidity: string:酸度,取值为"低"“中”“高”。sweetness: string:甜度,取值为"干型"“半干”“半甜”“甜”,这是葡萄酒标准的甜度分级。pairing: string[]:配餐建议数组,存储适合搭配的食物。tastingDate: string:品鉴日期,采用 ISO 格式字符串。tastingLocation: string:品鉴地点,通常是高端酒店或专业品鉴场所。tastingNotes: string:品鉴笔记正文,记录品酒师的详细感受。appearanceScore: number:外观评分,单独评估酒液色泽与澄清度。aromaScore: number:香气评分,评估香气的复杂度与层次。palateScore: number:口感评分,评估酒体结构与平衡感。overallScore: number:综合评分,由前三项综合得出。isFavorite: boolean:是否收藏,用于在卡片上显示心形标记。image: string:图片资源标识符,目前使用占位符。stock: number:库存瓶数,用于酒窖管理与低库存预警。purchaseDate: string:购买日期。drinkBy: number:适饮期截止年份,用于判断酒款是否进入最佳饮用窗口。
辅助配置接口
除了核心酒款,应用还定义了多个辅助接口,用于规范各类配置数据的结构:
interface WineTypeConfig {
label: string
color: string
bgColor: string
}
interface BodyConfig {
label: string
value: number
}
interface TanninConfig {
label: string
value: number
}
interface AcidityConfig {
label: string
value: number
}
interface SweetnessConfig {
label: string
value: number
}
interface TabItemConfig {
label: string
icon: string
}

逐行解释如下:
WineTypeConfig:包含三个字段。label是显示文案,color是该类型对应的主色(用于卡片左侧色块),bgColor是对应的浅色背景(用于标签底色)。这种设计将视觉表现与数据类型绑定,实现了一致的色彩语言。BodyConfig/TanninConfig/AcidityConfig:结构相同,都是label加value。label用于显示,value是一个 0-100 的数值,用于在卡片上绘制进度条。将文字描述映射为数值,是数据可视化的常见手法。SweetnessConfig:与其他三个略有不同,value的取值为 25/50/75/100,因为甜度有四个等级而非三个。TabItemConfig:底部导航栏每一项的配置,label是文字,icon是 emoji 图标。
品鉴记录接口
品鉴记录与酒款信息有一定的重叠,但侧重点不同。它更关注"一次品鉴行为"本身:
interface TastingRecordItem {
id: number
wineName: string
vintage: number
winery: string
type: string
tastingDate: string
tastingLocation: string
appearanceScore: number
aromaScore: number
palateScore: number
overallScore: number
notes: string
taster: string
temperature: number
decantTime: number
}

这里有几个值得注意的字段:
taster: string:品鉴者姓名,记录是谁进行了这次品鉴,便于多人协作场景。temperature: number:侍酒温度,以摄氏度为单位。不同类型的酒有各自的理想侍酒温度(如红葡萄酒 16-18°C,白葡萄酒 8-12°C),这个字段体现了专业性。decantTime: number:醒酒时间,以分钟为单位。对于年轻的大酒,醒酒是释放香气和柔化单宁的关键步骤。
活动与其他接口
应用还定义了活动、统计和个人中心相关的接口:
interface ActivityItem {
id: number
title: string
date: string
time: string
location: string
description: string
participantCount: number
wineCount: number
status: string
organizer: string
fee: number
}
interface MonthlyTastingData {
month: string
count: number
}
interface RegionData {
region: string
count: number
percentage: number
}
interface RatingData {
range: string
count: number
percentage: number
}
interface ProfileMenuData {
icon: string
title: string
subtitle: string
value: string
}
interface SummaryCardData {
label: string
value: string
unit: string
color: string
}

这些接口的设计思路是:每一个数据展示场景都有对应的结构定义。ActivityItem 涵盖了品鉴活动的所有信息维度;MonthlyTastingData、RegionData、RatingData 分别为不同类型的统计图表服务;ProfileMenuData 和 SummaryCardData 则服务于个人中心和首页的概览卡片。
二、数据模型层:观察者模式的实践
在 ArkTS 中,@Observed 装饰器用于声明一个可观察的类。当类的属性发生变化时,与之绑定的 UI 组件会自动刷新。应用中定义了一个 WineModel 类:
@Observed
class WineModel {
id: number = 0
name: string = ''
winery: string = ''
region: string = ''
country: string = ''
vintage: number = 0
grape: string = ''
type: string = ''
alcohol: number = 0
price: number = 0
rating: number = 0
color: string = ''
aroma: string[] = []
body: string = ''
tannin: string = ''
acidity: string = ''
sweetness: string = ''
pairing: string[] = []
tastingDate: string = ''
tastingLocation: string = ''
tastingNotes: string = ''
appearanceScore: number = 0
aromaScore: number = 0
palateScore: number = 0
overallScore: number = 0
isFavorite: boolean = false
image: string = ''
stock: number = 0
purchaseDate: string = ''
drinkBy: number = 0
constructor(item: WineItem) {
this.id = item.id
this.name = item.name
this.winery = item.winery
this.region = item.region
this.country = item.country
this.vintage = item.vintage
this.grape = item.grape
this.type = item.type
this.alcohol = item.alcohol
this.price = item.price
this.rating = item.rating
this.color = item.color
this.aroma = item.aroma
this.body = item.body
this.tannin = item.tannin
this.acidity = item.acidity
this.sweetness = item.sweetness
this.pairing = item.pairing
this.tastingDate = item.tastingDate
this.tastingLocation = item.tastingLocation
this.tastingNotes = item.tastingNotes
this.appearanceScore = item.appearanceScore
this.aromaScore = item.aromaScore
this.palateScore = item.palateScore
this.overallScore = item.overallScore
this.isFavorite = item.isFavorite
this.image = item.image
this.stock = item.stock
this.purchaseDate = item.purchaseDate
this.drinkBy = item.drinkBy
}
}

这一层的设计意图值得深入探讨:
@Observed装饰器:它将这个类标记为可观察对象。在 ArkTS 的响应式体系中,当WineModel实例的属性被修改时,所有引用了该实例的@ObjectLink组件都会收到更新通知并重新渲染。这是实现"数据驱动 UI"的核心机制。- 默认值初始化:每个属性在声明时都赋予了默认值。这是 TypeScript 的良好实践,确保即使构造函数未完整赋值,对象也处于一个合法的状态。
- 构造函数接收
WineItem:这是一种"接口到模型"的转换模式。WineItem是纯数据契约,而WineModel是带有响应式能力的领域对象。通过构造函数进行逐字段赋值,完成了从原始数据到可观察对象的封装。
这种"接口定义数据形状、类赋予数据行为"的分层方式,是面向对象设计中"数据与行为分离"原则的体现,也为未来扩展业务方法(如计算性价比、判断是否进入适饮期等)预留了空间。
三、配置常量层:将规则与数据分离
应用将所有"规则性"的数据抽取为常量配置,这是一种非常值得称道的工程实践。它使得业务规则集中管理,修改时只需调整一处。
葡萄酒类型配置
const WINE_TYPE_CONFIG: Record<string, WineTypeConfig> = {
'红葡萄酒': { label: '红葡萄酒', color: '#880E4F', bgColor: '#FCE4EC' },
'白葡萄酒': { label: '白葡萄酒', color: '#F9A825', bgColor: '#FFFDE7' },
'桃红': { label: '桃红', color: '#EC407A', bgColor: '#FCE4EC' },
'起泡酒': { label: '起泡酒', color: '#7B1FA2', bgColor: '#F3E5F5' },
'甜酒': { label: '甜酒', color: '#E65100', bgColor: '#FFF3E0' },
'加强酒': { label: '加强酒', color: '#5D4037', bgColor: '#EFEBE9' }
}

这里使用 Record<string, WineTypeConfig> 作为类型,表示一个以字符串为键、WineTypeConfig 为值的映射表。每一种葡萄酒类型都对应一个主色和浅色背景:
- 红葡萄酒用深酒红
#880E4F,与品牌主色一致,凸显核心地位。 - 白葡萄酒用琥珀黄
#F9A825,呼应白葡萄酒常见的金黄色泽。 - 桃红用粉色
#EC407A,直观对应其外观。 - 起泡酒用紫色
#7B1FA2,传递优雅与庆典感。 - 甜酒用橙色
#E65100,象征甜蜜与温暖。 - 加强酒用棕色
#5D4037,呼应波特酒、雪莉酒的陈年色泽。
口感维度配置
const BODY_CONFIG: Record<string, BodyConfig> = {
'轻盈': { label: '轻盈', value: 33 },
'中等': { label: '中等', value: 66 },
'饱满': { label: '饱满', value: 100 }
}
const TANNIN_CONFIG: Record<string, TanninConfig> = {
'低': { label: '低', value: 33 },
'中': { label: '中', value: 66 },
'高': { label: '高', value: 100 }
}
const ACIDITY_CONFIG: Record<string, AcidityConfig> = {
'低': { label: '低', value: 33 },
'中': { label: '中', value: 66 },
'高': { label: '高', value: 100 }
}
const SWEETNESS_CONFIG: Record<string, SweetnessConfig> = {
'干型': { label: '干型', value: 25 },
'半干': { label: '半干', value: 50 },
'半甜': { label: '半甜', value: 75 },
'甜': { label: '甜', value: 100 }
}
这四组配置的逻辑一致:将文字描述映射为百分比数值。酒体、单宁、酸度各分三级,每级间隔 33%;甜度分四级,每级间隔 25%。这些数值直接用于卡片上迷你进度条的宽度计算,让用户一眼就能感知酒款的口感轮廓。
导航与选项配置
const TAB_CONFIG: Record<string, TabItemConfig> = {
'Wines': { label: '酒款', icon: '🍷' },
'Tasting': { label: '品鉴', icon: '📝' },
'Events': { label: '活动', icon: '📅' },
'Stats': { label: '统计', icon: '📊' },
'Profile': { label: '我的', icon: '👤' }
}
const WINE_TYPE_FILTERS: string[] = ['全部', '红葡萄酒', '白葡萄酒', '桃红', '起泡酒', '甜酒', '加强酒']
const WINE_TYPE_OPTIONS: string[] = ['红葡萄酒', '白葡萄酒', '桃红', '起泡酒', '甜酒', '加强酒']
const BODY_OPTIONS: string[] = ['轻盈', '中等', '饱满']
const TANNIN_OPTIONS: string[] = ['低', '中', '高']
const ACIDITY_OPTIONS: string[] = ['低', '中', '高']
const SWEETNESS_OPTIONS: string[] = ['干型', '半干', '半甜', '甜']
TAB_CONFIG 定义了底部五个页签的文案与图标;其余数组则是各类表单选项的数据源。值得注意的是,WINE_TYPE_FILTERS 比 WINE_TYPE_OPTIONS 多了一个"全部"选项,因为筛选器需要支持"不筛选"的状态,而新增表单中类型是必选项。
四、模拟数据层:构建真实的业务样本
为了让应用在无后端的情况下也能完整运转,开发者在代码中内置了大量的模拟数据。这些数据并非随意编造,而是参考了真实的葡萄酒市场信息,涵盖了波尔多、勃艮第、纳帕谷、托斯卡纳、巴罗莎谷等世界知名产区,酒款从几千元到近万元不等,具有极高的真实感。
酒款数据示例
以第一款酒为例:
{
id: 1, name: '拉菲古堡红葡萄酒 2018', winery: 'Château Lafite Rothschild',
region: '波亚克 (Pauillac)', country: '法国', vintage: 2018, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 13.5, price: 8800, rating: 4.8, color: '深宝石红',
aroma: ['黑醋栗', '雪松', '烟草', '皮革', '石墨'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['牛排', '羊排', '陈年奶酪'], tastingDate: '2025-03-15', tastingLocation: '上海半岛酒店',
tastingNotes: '经典波尔多左岸风格,单宁强劲而细腻,余味悠长达30秒以上,带有明显的石墨和黑巧克力气息。需醒酒2小时以上。',
appearanceScore: 4.8, aromaScore: 4.9, palateScore: 4.7, overallScore: 4.8,
isFavorite: true, image: 'wine_01', stock: 3, purchaseDate: '2024-01-20', drinkBy: 2045
}
这组数据的真实性体现在多个细节:拉菲古堡确实是波尔多左岸波亚克产区的一级庄,2018 是一个优秀年份,赤霞珠是其主要葡萄品种,8800 元的价格区间符合市场行情,"需醒酒2小时以上"的品鉴笔记也符合年轻拉菲的实际情况。适饮期至 2045 年,体现了顶级波尔多的陈年潜力。
应用共内置了 20 款酒,覆盖了法国(8 款)、美国(3 款)、意大利(2 款)、澳大利亚(2 款)、新西兰、德国、匈牙利、中国、葡萄牙各 1 款,类型涵盖红葡萄酒、白葡萄酒、桃红、起泡酒、甜酒和加强酒,构成了一个完整的样本集。
品鉴记录与活动数据
品鉴记录共 15 条,每条都关联了一款具体的酒,并记录了品鉴者、侍酒温度、醒酒时间等专业信息。例如拉菲的品鉴记录中,温度为 18°C(红葡萄酒的理想侍酒温度),醒酒时间为 120 分钟,品鉴者为"李明"。
活动数据共 12 条,包含了波尔多一级庄垂直品鉴晚宴、勃艮第特级园品鉴会、WSET Level 3 认证课程等真实存在的活动类型。每条活动都标注了状态(报名中/已结束/进行中/未开始),并附有费用、人数、酒款数等关键信息。
统计数据
统计页所需的数据被预先聚合好:
const MONTHLY_TASTING_DATA: MonthlyTastingData[] = [
{ month: '1月', count: 8 },
{ month: '2月', count: 12 },
{ month: '3月', count: 15 },
{ month: '4月', count: 10 },
{ month: '5月', count: 14 },
{ month: '6月', count: 18 },
{ month: '7月', count: 22 },
{ month: '8月', count: 16 },
{ month: '9月', count: 20 },
{ month: '10月', count: 11 },
{ month: '11月', count: 9 },
{ month: '12月', count: 13 }
]
const MAX_TASTING_COUNT: number = 22
MAX_TASTING_COUNT 是一个关键常量,代表全年单月最高品鉴次数(7 月的 22 次)。柱状图的高度以此为基准进行归一化计算,确保最高的柱子恰好填满图表区域。
五、主入口组件:应用骨架与导航架构
枚举定义
enum WineTab {
Wines,
Tasting,
Events,
Stats,
Profile
}
使用枚举而非字符串常量来管理页签状态,是类型安全的最佳实践。枚举值从 0 开始递增,比较时直接对比数值,性能优于字符串比较,且拼写错误能在编译期被发现。
主组件结构
@Entry
@Component
struct WineApp {
@State activeTab: WineTab = WineTab.Wines
@Builder
contentArea() {
if (this.activeTab === WineTab.Wines) {
WineListPage()
} else if (this.activeTab === WineTab.Tasting) {
TastingPage()
} else if (this.activeTab === WineTab.Events) {
EventPage()
} else if (this.activeTab === WineTab.Stats) {
WineStatsPage()
} else {
WineProfilePage()
}
}
@Builder
bottomTabItem(tab: WineTab, icon: string, label: string) {
Column() {
Text(icon)
.fontSize(22)
Text(label)
.fontSize(10)
.fontColor(this.activeTab === tab ? '#880E4F' : '#9E9E9E')
.margin({ top: 2 })
}
.justifyContent(FlexAlign.Center)
.layoutWeight(1)
.height('100%')
.onClick(() => {
this.activeTab = tab
})
}
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem(WineTab.Wines, '🍷', '酒款')
this.bottomTabItem(WineTab.Tasting, '📝', '品鉴')
this.bottomTabItem(WineTab.Events, '📅', '活动')
this.bottomTabItem(WineTab.Stats, '📊', '统计')
this.bottomTabItem(WineTab.Profile, '👤', '我的')
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.borderWidth(1)
.borderColor('#E0E0E0')
}
.width('100%')
.height('100%')
.backgroundColor('#FCE4EC')
}
}
这段代码是整个应用的骨架,逐部分解析:
@Entry装饰器标记这是应用的入口组件,HarmonyOS 框架会从这里开始构建 UI 树。@Component声明这是一个自定义组件。@State activeTab: WineTab = WineTab.Wines:声明一个状态变量,初始值为酒款页。当activeTab变化时,contentArea和所有bottomTabItem中引用了它的地方都会重新渲染。contentArea是一个@Builder方法,根据当前activeTab的值条件性地渲染对应的页面组件。这种"if-else 链"在页签数量较少时清晰直观。bottomTabItem是一个参数化的@Builder,接收页签枚举值、图标和文案,构建单个底部导航项。关键在于fontColor的三元判断:当前激活的页签显示酒红色,未激活的显示灰色。build()方法中,外层Column包含内容区和底部导航栏Row。底部栏高度固定为 56vp,白色背景,顶部有一像素的浅灰分隔线,整体视觉干净利落。
六、酒款列表页:信息密度与交互的平衡
酒款列表页是应用的核心页面,承担了酒款展示、筛选、新增、编辑和删除等多重职责。
状态变量群
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedWine: string = ''
@State filterType: string = '全部'
@State addName: string = ''
@State addWinery: string = ''
@State addRegion: string = ''
@State addCountry: string = ''
@State addVintage: string = ''
@State addGrape: string = ''
@State addAlcohol: string = ''
@State addPrice: string = ''
@State addType: string = '红葡萄酒'
@State addBody: string = '中等'
@State addTannin: string = '中'
@State addAcidity: string = '中'
@State addSweetness: string = '干型'
@State editNotes: string = ''
@State editRating: string = ''
这里的状态管理值得细看。三个布尔变量 showAddModal、showEditModal、showDeleteModal 分别控制三个弹窗的显示与隐藏。filterType 记录当前筛选的类型。add* 系列变量绑定新增表单的各个输入框。editNotes 和 editRating 绑定编辑弹窗。
一个值得讨论的细节是:年份、酒精度等本应是数字的字段,状态变量却声明为 string。这是因为在表单输入场景中,用户可能输入未完整的值(如输入到一半的"202"),如果用数字类型会导致中间状态无法正确表示。最终提交时再做类型转换是更稳健的做法。
弹窗遮罩层
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => onClose())
}
这是一个可复用的遮罩层构建器。它接收一个 onClose 回调函数,点击遮罩区域时触发关闭。半透明黑色背景营造了模态对话框的视觉焦点效果,是移动端弹窗的标准做法。
新增酒款弹窗
新增弹窗是整个应用中最复杂的表单结构之一:
@Builder
addWineModal() {
Column() {
Column() {
Text('新增酒款')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.margin({ bottom: 16 })
Text('酒款名称')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入酒款名称', text: this.addName })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addName = value })
// ...后续字段结构类似
}
.width('88%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
逐行解析其设计要点:
- 外层
Column撑满全屏,通过justifyContent(FlexAlign.Center)让弹窗内容垂直居中。 - 内层
Column宽度为 88%,白色背景,16 圆角,20 内边距,形成了悬浮卡片效果。 - 每个表单字段都遵循"标签 + 输入框"的模式:标签左对齐(
alignSelf(ItemAlign.Start)),字号 13,灰色;输入框高度 40,浅灰背景,8 圆角,通过onChange回调实时更新状态变量。 - 年份与酒精度并排显示,使用
Row包裹两个Column,各占layoutWeight(1),中间留 8 的间距,这种双列布局在有限宽度下提升了信息密度。 - 葡萄酒类型、酒体、单宁、酸度、甜度等选项采用"标签胶囊"的形式:用
ForEach遍历选项数组,每个选项是一个可点击的Text,选中时填充酒红色背景与白色文字,未选中时为浅灰背景与灰色文字。这种交互方式比下拉选择器更直观,尤其适合选项数量较少的场景。 - 底部按钮区:取消按钮为浅灰风格,添加按钮为酒红主色风格,两者各占一半宽度,通过
layoutWeight(1)实现。
酒款卡片构建器
酒款卡片是信息密度最高的 UI 单元:
@Builder
wineCardBuilder(item: WineItem) {
Column() {
Row() {
Column() {
Text('🍷')
.fontSize(28)
Text(WINE_TYPE_CONFIG[item.type].label)
.fontSize(8)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width(56)
.height(70)
.borderRadius(10)
.backgroundColor(WINE_TYPE_CONFIG[item.type].color)
.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.layoutWeight(1)
if (item.isFavorite) {
Text('♥')
.fontSize(16)
.fontColor('#C62828')
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.winery)
.fontSize(11)
.fontColor('#757575')
.margin({ top: 3 })
Row() {
Text(item.country)
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text(item.vintage > 0 ? item.vintage + '年' : 'NV')
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text(item.grape)
.fontSize(10)
.fontColor('#9E9E9E')
}
.margin({ top: 2 })
Row() {
this.starBuilder(item.rating)
Text(' ' + item.rating.toFixed(1))
.fontSize(12)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Bold)
.margin({ left: 4 })
Row()
.layoutWeight(1)
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Top)
// ...后续还有香气标签、口感进度条、配餐信息等
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => {
this.selectedWine = item.name
this.editNotes = item.tastingNotes
this.editRating = item.rating.toFixed(1)
this.showEditModal = true
})
}
这张卡片的信息架构可以分为四个层次:
第一层:标识区。左侧是一个 56x70 的彩色色块,背景色取自 WINE_TYPE_CONFIG,顶部是酒杯 emoji,底部是类型文字。这个色块既起装饰作用,又通过颜色快速传递酒款类型信息。
第二层:基本信息区。酒名加粗显示,收藏酒款右侧显示红色心形。下方依次是酒庄名、国家/年份/品种的组合信息(用中点分隔),以及评分星级加价格。item.vintage > 0 ? item.vintage + '年' : 'NV' 这个三元表达式处理了无年份酒款的情况,"NV"是"Non-Vintage"的国际通用缩写。
第三层:口感维度区。四个迷你进度条横向排列,分别对应酒体、单宁、酸度、甜度:
Column() {
Text('酒体')
.fontSize(9)
.fontColor('#9E9E9E')
Row() {
Column()
.width(BODY_CONFIG[item.body].value + '%')
.height(4)
.backgroundColor('#880E4F')
.borderRadius(2)
}
.width(50)
.height(4)
.backgroundColor('#E0E0E0')
.borderRadius(2)
.margin({ top: 2 })
Text(item.body)
.fontSize(9)
.fontColor('#616161')
.margin({ top: 2 })
}
进度条的原理是:外层 Row 固定宽度 50、浅灰背景;内层 Column 的宽度根据配置值动态计算(如"饱满"对应 100%,即 50 的满宽),酒红色填充。甜度的填充色单独使用香槟金,以示区别。最右侧还有库存信息,当库存 ≤3 时显示红色预警,否则显示绿色。
第四层:配餐与适饮期。底部一行展示配餐建议(用 ForEach 遍历 pairing 数组)和适饮期截止年份。
星级评分构建器
@Builder
starBuilder(rating: number) {
Row() {
if (rating >= 1) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
if (rating >= 2) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
// ...第3、4、5颗星逻辑相同
}
}
这个构建器用最朴素的方式实现了星级显示:五颗星,每颗根据评分阈值判断是亮色(香槟金)还是暗色(浅灰)。虽然代码略显重复,但逻辑极其清晰,且性能优于循环方案(避免了动态列表的开销)。
页面主体与弹窗管理
build() {
Stack() {
Scroll() {
Column() {
Text('葡 萄 酒 品 鉴')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('Vinothèque · 私人酒窖管理')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Row() {
this.summaryCard(SUMMARY_DATA[0])
this.summaryCard(SUMMARY_DATA[1])
this.summaryCard(SUMMARY_DATA[2])
this.summaryCard(SUMMARY_DATA[3])
}
.width('92%')
.margin({ bottom: 12 })
// ...筛选条与酒款卡片列表
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
if (this.showAddModal) {
this.modalOverlay(() => { this.showAddModal = false })
this.addWineModal()
}
if (this.showEditModal) {
this.modalOverlay(() => { this.showEditModal = false })
this.editNotesModal()
}
if (this.showDeleteModal) {
this.modalOverlay(() => { this.showDeleteModal = false })
this.deleteConfirmModal()
}
}
.width('100%')
.height('100%')
}
这里使用了 Stack 布局来叠加滚动内容和弹窗。Stack 的特性是子元素按顺序堆叠,后声明的元素覆盖在前面元素之上。因此,当 showAddModal 为 true 时,遮罩层和弹窗会覆盖在滚动列表之上,形成模态效果。滚动容器设置了 scrollBar(BarState.Off) 隐藏滚动条,保持界面整洁。
七、品鉴记录页:专业评分的可视化
品鉴记录页聚焦于"品鉴行为"本身,其核心特色是四维评分体系。
品鉴卡片构建
@Builder
tastingItemBuilder(item: TastingRecordItem) {
Column() {
Row() {
Column() {
Text('🍷')
.fontSize(24)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor(WINE_TYPE_CONFIG[item.type].bgColor)
.justifyContent(FlexAlign.Center)
Column() {
Text(item.wineName)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text(item.wine + ' · ' + (item.vintage > 0 ? item.vintage + '年' : 'NV'))
.fontSize(11)
.fontColor('#757575')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(item.overallScore.toFixed(1))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
this.starBuilder(item.overallScore)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('外观')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.appearanceScore.toFixed(1))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.margin({ top: 2 })
this.starBuilder(item.appearanceScore)
}
.layoutWeight(1)
// ...香气、口感、综合三列结构相同
}
.width('100%')
.margin({ top: 12 })
Text(item.notes)
.fontSize(12)
.fontColor('#616161')
.margin({ top: 10 })
.width('100%')
Row() {
Text('📅 ' + item.tastingDate)
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text('📍 ' + item.tastingLocation)
.fontSize(10)
.fontColor('#9E9E9E')
Row()
.layoutWeight(1)
Text('侍酒师: ' + item.taster)
.fontSize(10)
.fontColor('#9E9E9E')
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 8 })
Row() {
Text('🌡️ ' + item.temperature + '°C')
.fontSize(10)
.fontColor('#757575')
.backgroundColor('#F5F5F5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
if (item.decantTime > 0) {
Text('⏱️ 醒酒' + item.decantTime + '分钟')
.fontSize(10)
.fontColor('#757575')
.backgroundColor('#F5F5F5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ left: 6 })
}
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
}
卡片的信息层次设计得非常考究:
- 顶部行:圆形头像(带类型背景色)+ 酒名与酒庄年份 + 右侧综合评分与星级。
- 四维评分行:外观、香气、口感、综合四列均分(各
layoutWeight(1)),每列包含标签、分数和星级。这种四列对比让品鉴者能快速判断酒款在各个维度的表现。 - 笔记正文:品鉴笔记全文展示,灰色文字降低视觉权重,避免喧宾夺主。
- 元信息行:日期、地点、侍酒师信息,使用 emoji 图标增强可读性。
- 专业参数行:侍酒温度和醒酒时间以胶囊标签形式展示。
if (item.decantTime > 0)的条件判断意味着只有需要醒酒的酒款才会显示醒酒时间标签,避免了显示"醒酒0分钟"这种无意义信息。
八、活动页:状态化的事件管理
活动页的设计重点是状态管理与信息分层。
活动卡片构建
@Builder
activityItemBuilder(item: ActivityItem) {
Column() {
Row() {
Column() {
Text(item.date.substring(5, 7))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(item.date.substring(8, 10) + '日')
.fontSize(11)
.fontColor('#FCE4EC')
.margin({ top: 2 })
}
.width(56)
.height(56)
.borderRadius(12)
.backgroundColor('#880E4F')
.justifyContent(FlexAlign.Center)
Column() {
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text('🕐 ' + item.time + ' · 📍 ' + item.location)
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Column() {
Text(item.status)
.fontSize(10)
.fontColor(item.status === '报名中' ? '#2E7D32' : item.status === '已结束' ? '#9E9E9E' : item.status === '进行中' ? '#FF8F00' : '#757575')
.backgroundColor(item.status === '报名中' ? '#E8F5E9' : item.status === '已结束' ? '#F5F5F5' : item.status === '进行中' ? '#FFF8E1' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.description)
.fontSize(12)
.fontColor('#616161')
.margin({ top: 10 })
.width('100%')
Row() {
Column() {
Text(item.participantCount + '人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('参与人数')
.fontSize(9)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
// ...品鉴酒款数、活动费用列结构类似
Row()
.layoutWeight(1)
Column() {
Text(item.organizer)
.fontSize(10)
.fontColor('#9E9E9E')
if (item.status === '报名中') {
Text('立即报名 >')
.fontSize(11)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 12 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
}
几个关键设计点:
- 日期提取:
item.date.substring(5, 7)从 ISO 格式日期字符串中截取月份(第 5-7 位),substring(8, 10)截取日期。这种字符串操作虽然简单,但避免了引入日期解析库的 overhead。 - 状态颜色映射:通过嵌套三元运算符,根据活动状态动态设置文字颜色和背景色。报名中(绿色)、已结束(灰色)、进行中(橙色)、未开始(灰色)各有对应的配色方案,让用户一眼即可区分。
- 条件性行动按钮:只有状态为"报名中"的活动才显示"立即报名 >"链接,其他状态不显示,避免无效操作。
九、统计页:纯 CSS 实现的数据可视化
统计页是应用中最具技术亮点的部分——它完全用 ArkTS 的布局能力实现了柱状图,没有引入任何图表库。
月度柱状图
@Builder
monthlyBarBuilder(data: MonthlyTastingData) {
Column() {
Column() {
Column()
.width(20)
.height(data.count / MAX_TASTING_COUNT * 100)
.backgroundColor('#880E4F')
.borderRadius({ topLeft: 4, topRight: 4 })
}
.height(100)
.justifyContent(FlexAlign.End)
Text(data.count.toString())
.fontSize(9)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.margin({ top: 2 })
Text(data.month)
.fontSize(9)
.fontColor('#9E9E9E')
}
.layoutWeight(1)
}
这个柱状图的实现思路非常巧妙:
- 外层
Column通过layoutWeight(1)在Row中均分宽度,12 个月份各占一份。 - 中间是一个固定高度 100 的容器,
justifyContent(FlexAlign.End)让子元素贴底对齐。 - 内层的柱子是一个
Column,宽度固定 20,高度通过data.count / MAX_TASTING_COUNT * 100动态计算。例如 7 月的 count 为 22,等于 MAX,所以高度为 100(满高);1 月 count 为 8,高度为 36.36。 - 柱子顶部圆角通过
borderRadius({ topLeft: 4, topRight: 4 })实现,营造胶囊感。 - 柱子下方显示数值和月份标签。
横向条形图
产区分布和类型分布采用横向条形图:
@Builder
regionBarBuilder(data: RegionData) {
Row() {
Text(data.region)
.fontSize(11)
.fontColor('#616161')
.width(70)
Row() {
Column()
.width(data.percentage + '%')
.height(16)
.backgroundColor('#880E4F')
.borderRadius(8)
}
.layoutWeight(1)
.height(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Text(data.count + '款')
.fontSize(11)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
}
原理与酒款卡片上的迷你进度条相同,但这里作为一个独立的统计图表组件使用。左侧是固定宽度 70 的标签,中间是 layoutWeight(1) 的进度条容器,右侧是固定宽度 40 的数值。类型分布的构建器 typeBarBuilder 还多了一步:从 WINE_TYPE_CONFIG 中读取对应类型的颜色作为填充色,让图表的色彩与酒款类型保持一致。
统计页布局
build() {
Scroll() {
Column() {
Text('品鉴统计')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('2025年度 · 品鉴数据分析')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Column() {
Text('月度品鉴数量')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
Row() {
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[0])
// ...共12个月份
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('全年品鉴')
.fontSize(12)
.fontColor('#757575')
Row()
.layoutWeight(1)
Text('168次')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
}
.width('100%')
.margin({ top: 12 })
.alignItems(VerticalAlign.Center)
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
// ...类型分布、产区分布、评分分布、酒窖概览四个卡片
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}
统计页的整体布局采用了"卡片式分段"的设计:每一个统计维度(月度趋势、类型分布、产区分布、评分分布、酒窖概览)都封装在一个白色圆角卡片中,卡片之间用 12 的间距分隔。这种设计让复杂的数据信息有了清晰的视觉边界。
十、个人中心页:用户身份与功能入口
个人中心页采用了"头部信息卡 + 分组菜单列表"的经典布局。
头部信息卡
Column() {
Row() {
Column() {
Text('L')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
}
.width(70)
.height(70)
.borderRadius(35)
.backgroundColor('#4A148C')
.justifyContent(FlexAlign.Center)
Column() {
Text('李明 · WSET Level 4')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('WSET Diploma Candidate')
.fontSize(11)
.fontColor('#FCE4EC')
.margin({ top: 4 })
Row() {
Text('🌟 资深品酒师')
.fontSize(10)
.fontColor('#FFD54F')
.backgroundColor('rgba(255,213,79,0.2)')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
}
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 16 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 16 })
Row() {
Column() {
Text('168')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('品鉴次数')
.fontSize(10)
.fontColor('#FCE4EC')
.margin({ top: 4 })
}
.layoutWeight(1)
// ...收藏酒款、参加活动两列
}
.width('100%')
}
.width('92%')
.backgroundColor('#880E4F')
.borderRadius(16)
.padding(20)
.margin({ top: 12, bottom: 12 })
头部卡片的设计要点:
- 整个卡片使用酒红色背景,与品牌主色一致,形成强烈的视觉焦点。
- 头像是一个 70x70 的圆形(
borderRadius(35)),深紫色背景(#4A148C),内含用户姓名首字母"L",香槟金色文字。 - 用户信息包含姓名、WSET 等级(Level 4 是 WSET 最高级别)、候选身份说明和"资深品酒师"徽章。徽章使用半透明香槟金背景,呼应整体配色。
- 底部三列数据(品鉴次数、收藏酒款、参加活动)使用白色文字,其中"参加活动"的数字使用香槟金强调,形成层次。
菜单选项构建器
@Builder
menuOptionBuilder(menu: ProfileMenuData) {
Row() {
Column() {
Text(menu.icon)
.fontSize(16)
.fontColor('#880E4F')
.fontWeight(FontWeight.Bold)
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#FCE4EC')
.justifyContent(FlexAlign.Center)
Column() {
Text(menu.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#212121')
Text(menu.subtitle)
.fontSize(11)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
if (menu.value.length > 0) {
Text(menu.value)
.fontSize(12)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
}
Text(' >')
.fontSize(14)
.fontColor('#BDBDBD')
.margin({ left: 4 })
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.Center)
}
这里有一个细节值得注意:菜单图标使用的是汉字(如"窖"“笔”“购”),而非传统的图标字体或图片。这是一种巧妙的做法——在不引入图标资源的前提下,用单个汉字的表意性来承担图标功能,既轻量又有文化特色。每个图标放在一个 36x36 的圆形浅粉背景中,酒红色文字加粗,视觉上与整体风格和谐统一。
if (menu.value.length > 0) 的条件判断确保只有带有数值的菜单项(如"20款"“168篇”“¥58,600”)才显示右侧的数值标签,没有数值的项(如品鉴指南、意见反馈)只显示箭头。
分组菜单与版本信息
Column() {
this.menuOptionBuilder(PROFILE_MENU_DATA[0])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[1])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[2])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[3])
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 12 })
// ...第二组菜单结构相同
Text('Vinothèque v3.2.1')
.fontSize(11)
.fontColor('#BDBDBD')
.margin({ top: 8, bottom: 20 })
菜单被分为两组:第一组是核心功能(酒窖、品鉴记录、购买记录、评分),第二组是辅助功能(消息通知、品鉴指南、意见反馈、设置)。组与组之间通过独立的白色卡片和间距自然分隔,组内各项之间用 1 像素的浅灰分隔线区分。页面最底部是版本号信息,浅灰色小字,是应用 footer 的标准写法。
十一、关键特性对比总结
下表对应用中各个核心页面的功能定位、数据来源、可视化手段和交互特色进行了横向对比:
| 维度 | 酒款列表页 | 品鉴记录页 | 活动页 | 统计页 | 个人中心页 |
|---|---|---|---|---|---|
| 核心功能 | 酒款展示与管理 | 品鉴笔记归档 | 活动报名与浏览 | 数据分析与洞察 | 用户身份与功能入口 |
| 数据来源 | WINE_DATA(20条) | TASTING_DATA(15条) | ACTIVITY_DATA(12条) | 多组聚合统计常量 | 静态用户信息+菜单配置 |
| 可视化手段 | 迷你进度条、星级、色彩标签 | 四维评分卡片、星级 | 状态色彩标签、日期色块 | 柱状图、横向条形图 | 数据统计行、徽章 |
| 弹窗交互 | 新增/编辑/删除三弹窗 | 无 | 无 | 无 | 无 |
| 筛选能力 | 类型横向滚动筛选 | 无 | 无 | 无 | 无 |
| 卡片主色 | 按酒款类型动态变化 | 酒红色为主 | 酒红色+状态色 | 酒红色+香槟金 | 酒红色背景头部 |
| 滚动方式 | 垂直滚动 | 垂直滚动 | 垂直滚动 | 垂直滚动 | 垂直滚动 |
| 信息密度 | 高(单卡片含10+字段) | 中高(四维评分+笔记) | 中(活动信息+统计) | 中高(多图表) | 中(头部+菜单) |
| 专业元素 | 香气轮盘、配餐、适饮期 | 侍酒温度、醒酒时间 | WSET课程、米其林晚宴 | 月度趋势、产区分布 | WSET等级、品酒师徽章 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:

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

完整代码:
// ==================== 葡萄酒品鉴记录APP ====================
// 主题色:酒红 #880E4F + 香槟金 #FFD54F,背景 #FCE4EC
// ==================== 接口定义 ====================
interface WineItem {
id: number
name: string
winery: string
region: string
country: string
vintage: number
grape: string
type: string
alcohol: number
price: number
rating: number
color: string
aroma: string[]
body: string
tannin: string
acidity: string
sweetness: string
pairing: string[]
tastingDate: string
tastingLocation: string
tastingNotes: string
appearanceScore: number
aromaScore: number
palateScore: number
overallScore: number
isFavorite: boolean
image: string
stock: number
purchaseDate: string
drinkBy: number
}
interface WineTypeConfig {
label: string
color: string
bgColor: string
}
interface BodyConfig {
label: string
value: number
}
interface TanninConfig {
label: string
value: number
}
interface AcidityConfig {
label: string
value: number
}
interface SweetnessConfig {
label: string
value: number
}
interface TabItemConfig {
label: string
icon: string
}
interface TastingRecordItem {
id: number
wineName: string
vintage: number
winery: string
type: string
tastingDate: string
tastingLocation: string
appearanceScore: number
aromaScore: number
palateScore: number
overallScore: number
notes: string
taster: string
temperature: number
decantTime: number
}
interface ActivityItem {
id: number
title: string
date: string
time: string
location: string
description: string
participantCount: number
wineCount: number
status: string
organizer: string
fee: number
}
interface MonthlyTastingData {
month: string
count: number
}
interface RegionData {
region: string
count: number
percentage: number
}
interface RatingData {
range: string
count: number
percentage: number
}
interface ProfileMenuData {
icon: string
title: string
subtitle: string
value: string
}
interface SummaryCardData {
label: string
value: string
unit: string
color: string
}
// ==================== 数据模型 ====================
@Observed
class WineModel {
id: number = 0
name: string = ''
winery: string = ''
region: string = ''
country: string = ''
vintage: number = 0
grape: string = ''
type: string = ''
alcohol: number = 0
price: number = 0
rating: number = 0
color: string = ''
aroma: string[] = []
body: string = ''
tannin: string = ''
acidity: string = ''
sweetness: string = ''
pairing: string[] = []
tastingDate: string = ''
tastingLocation: string = ''
tastingNotes: string = ''
appearanceScore: number = 0
aromaScore: number = 0
palateScore: number = 0
overallScore: number = 0
isFavorite: boolean = false
image: string = ''
stock: number = 0
purchaseDate: string = ''
drinkBy: number = 0
constructor(item: WineItem) {
this.id = item.id
this.name = item.name
this.winery = item.winery
this.region = item.region
this.country = item.country
this.vintage = item.vintage
this.grape = item.grape
this.type = item.type
this.alcohol = item.alcohol
this.price = item.price
this.rating = item.rating
this.color = item.color
this.aroma = item.aroma
this.body = item.body
this.tannin = item.tannin
this.acidity = item.acidity
this.sweetness = item.sweetness
this.pairing = item.pairing
this.tastingDate = item.tastingDate
this.tastingLocation = item.tastingLocation
this.tastingNotes = item.tastingNotes
this.appearanceScore = item.appearanceScore
this.aromaScore = item.aromaScore
this.palateScore = item.palateScore
this.overallScore = item.overallScore
this.isFavorite = item.isFavorite
this.image = item.image
this.stock = item.stock
this.purchaseDate = item.purchaseDate
this.drinkBy = item.drinkBy
}
}
// ==================== 配置常量 ====================
const WINE_TYPE_CONFIG: Record<string, WineTypeConfig> = {
'红葡萄酒': { label: '红葡萄酒', color: '#880E4F', bgColor: '#FCE4EC' },
'白葡萄酒': { label: '白葡萄酒', color: '#F9A825', bgColor: '#FFFDE7' },
'桃红': { label: '桃红', color: '#EC407A', bgColor: '#FCE4EC' },
'起泡酒': { label: '起泡酒', color: '#7B1FA2', bgColor: '#F3E5F5' },
'甜酒': { label: '甜酒', color: '#E65100', bgColor: '#FFF3E0' },
'加强酒': { label: '加强酒', color: '#5D4037', bgColor: '#EFEBE9' }
}
const BODY_CONFIG: Record<string, BodyConfig> = {
'轻盈': { label: '轻盈', value: 33 },
'中等': { label: '中等', value: 66 },
'饱满': { label: '饱满', value: 100 }
}
const TANNIN_CONFIG: Record<string, TanninConfig> = {
'低': { label: '低', value: 33 },
'中': { label: '中', value: 66 },
'高': { label: '高', value: 100 }
}
const ACIDITY_CONFIG: Record<string, AcidityConfig> = {
'低': { label: '低', value: 33 },
'中': { label: '中', value: 66 },
'高': { label: '高', value: 100 }
}
const SWEETNESS_CONFIG: Record<string, SweetnessConfig> = {
'干型': { label: '干型', value: 25 },
'半干': { label: '半干', value: 50 },
'半甜': { label: '半甜', value: 75 },
'甜': { label: '甜', value: 100 }
}
const TAB_CONFIG: Record<string, TabItemConfig> = {
'Wines': { label: '酒款', icon: '🍷' },
'Tasting': { label: '品鉴', icon: '📝' },
'Events': { label: '活动', icon: '📅' },
'Stats': { label: '统计', icon: '📊' },
'Profile': { label: '我的', icon: '👤' }
}
const WINE_TYPE_FILTERS: string[] = ['全部', '红葡萄酒', '白葡萄酒', '桃红', '起泡酒', '甜酒', '加强酒']
const WINE_TYPE_OPTIONS: string[] = ['红葡萄酒', '白葡萄酒', '桃红', '起泡酒', '甜酒', '加强酒']
const BODY_OPTIONS: string[] = ['轻盈', '中等', '饱满']
const TANNIN_OPTIONS: string[] = ['低', '中', '高']
const ACIDITY_OPTIONS: string[] = ['低', '中', '高']
const SWEETNESS_OPTIONS: string[] = ['干型', '半干', '半甜', '甜']
// ==================== 模拟数据 - 酒款列表20条 ====================
const WINE_DATA: WineItem[] = [
{
id: 1, name: '拉菲古堡红葡萄酒 2018', winery: 'Château Lafite Rothschild',
region: '波亚克 (Pauillac)', country: '法国', vintage: 2018, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 13.5, price: 8800, rating: 4.8, color: '深宝石红',
aroma: ['黑醋栗', '雪松', '烟草', '皮革', '石墨'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['牛排', '羊排', '陈年奶酪'], tastingDate: '2025-03-15', tastingLocation: '上海半岛酒店',
tastingNotes: '经典波尔多左岸风格,单宁强劲而细腻,余味悠长达30秒以上,带有明显的石墨和黑巧克力气息。需醒酒2小时以上。',
appearanceScore: 4.8, aromaScore: 4.9, palateScore: 4.7, overallScore: 4.8,
isFavorite: true, image: 'wine_01', stock: 3, purchaseDate: '2024-01-20', drinkBy: 2045
},
{
id: 2, name: '唐培里侬香槟 2015', winery: 'Moët & Chandon',
region: '香槟 (Champagne)', country: '法国', vintage: 2015, grape: '霞多丽/黑皮诺',
type: '起泡酒', alcohol: 12.5, price: 2200, rating: 4.5, color: '浅金黄色',
aroma: ['白花', '柑橘', '烤面包', '杏仁', '矿物质'], body: '中等', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['生蚝', '鱼子酱', '海鲜'], tastingDate: '2025-04-02', tastingLocation: '北京CAFA葡萄酒学院',
tastingNotes: '气泡细腻持久,入口清爽活泼,带有明显的烤面包和坚果风味,酸度挺拔,结构优雅。适合作为开胃酒。',
appearanceScore: 4.6, aromaScore: 4.5, palateScore: 4.4, overallScore: 4.5,
isFavorite: true, image: 'wine_02', stock: 6, purchaseDate: '2024-03-15', drinkBy: 2030
},
{
id: 3, name: '作品一号 2017', winery: 'Opus One',
region: '纳帕谷 (Napa Valley)', country: '美国', vintage: 2017, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 14.5, price: 6500, rating: 4.7, color: '深紫红色',
aroma: ['黑莓', '蓝莓', '香草', '摩卡', '甘草'], body: '饱满', tannin: '高', acidity: '中', sweetness: '干型',
pairing: ['和牛', '鹿肉', '蓝纹奶酪'], tastingDate: '2025-02-20', tastingLocation: '广州四季酒店',
tastingNotes: '纳帕谷顶级酒款,波尔多混酿风格与美国果味的完美结合,单宁如丝绒般顺滑,酒体宏大而均衡。',
appearanceScore: 4.7, aromaScore: 4.8, palateScore: 4.6, overallScore: 4.7,
isFavorite: true, image: 'wine_03', stock: 2, purchaseDate: '2024-02-10', drinkBy: 2037
},
{
id: 4, name: '奔富葛兰许 2017', winery: 'Penfolds',
region: '巴罗莎谷 (Barossa Valley)', country: '澳大利亚', vintage: 2017, grape: '设拉子',
type: '红葡萄酒', alcohol: 14.5, price: 4500, rating: 4.6, color: '深紫黑色',
aroma: ['黑果', '巧克力', '咖啡豆', '黑胡椒', '橡木'], body: '饱满', tannin: '高', acidity: '中', sweetness: '干型',
pairing: ['烤羊排', '炖牛肉', '硬质奶酪'], tastingDate: '2025-05-10', tastingLocation: '深圳瑞吉酒店',
tastingNotes: '澳洲酒王,浓郁而复杂,美国橡木桶带来的香草和椰子气息与设拉子的黑果风味完美融合,余味悠长。',
appearanceScore: 4.6, aromaScore: 4.7, palateScore: 4.5, overallScore: 4.6,
isFavorite: false, image: 'wine_04', stock: 4, purchaseDate: '2024-04-20', drinkBy: 2040
},
{
id: 5, name: '木桐酒庄红葡萄酒 2016', winery: 'Château Mouton Rothschild',
region: '波亚克 (Pauillac)', country: '法国', vintage: 2016, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 13.5, price: 6800, rating: 4.7, color: '深红宝石色',
aroma: ['黑醋栗', '雪松', '香料', '薄荷', '烟熏'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['鹿肉', '松露', '陈年帕尔马奶酪'], tastingDate: '2025-03-28', tastingLocation: '上海外滩会',
tastingNotes: '1855一级庄,力量与优雅并存,单宁紧致而有结构感,木桐标志性的强烈个性和艺术气息。',
appearanceScore: 4.8, aromaScore: 4.7, palateScore: 4.7, overallScore: 4.7,
isFavorite: true, image: 'wine_05', stock: 2, purchaseDate: '2024-01-15', drinkBy: 2045
},
{
id: 6, name: '路易王妃水晶香槟 2014', winery: 'Louis Roederer',
region: '香槟 (Champagne)', country: '法国', vintage: 2014, grape: '霞多丽/黑皮诺',
type: '起泡酒', alcohol: 12, price: 3500, rating: 4.6, color: '明亮金色',
aroma: ['柠檬', '白桃', '榛子', '奶油', '矿物质'], body: '中等', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['龙虾', '扇贝', '寿司'], tastingDate: '2025-04-18', tastingLocation: '杭州柏悦酒店',
tastingNotes: '水晶香槟的精致与力量,细腻的气泡,浓郁的柑橘和白花香气,矿物质感强烈,收尾干净利落。',
appearanceScore: 4.7, aromaScore: 4.6, palateScore: 4.5, overallScore: 4.6,
isFavorite: false, image: 'wine_06', stock: 5, purchaseDate: '2024-05-01', drinkBy: 2030
},
{
id: 7, name: '云雾之湾长相思 2021', winery: 'Cloudy Bay',
region: '马尔堡 (Marlborough)', country: '新西兰', vintage: 2021, grape: '长相思',
type: '白葡萄酒', alcohol: 13, price: 380, rating: 4.3, color: '浅黄绿色',
aroma: ['百香果', '青柠', '西番莲', '青草', '矿物质'], body: '中等', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['海鲜沙拉', '生蚝', '山羊奶酪'], tastingDate: '2025-06-05', tastingLocation: '厦门华尔道夫酒店',
tastingNotes: '新西兰长相思的标杆,热带水果香气奔放,酸度清脆活泼,清新而易饮,适合夏日享用。',
appearanceScore: 4.2, aromaScore: 4.4, palateScore: 4.3, overallScore: 4.3,
isFavorite: false, image: 'wine_07', stock: 12, purchaseDate: '2024-06-10', drinkBy: 2026
},
{
id: 8, name: '红颜容酒庄 2015', winery: 'Château Haut-Brion',
region: '佩萨克-雷奥良 (Pessac-Léognan)', country: '法国', vintage: 2015, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 14, price: 7200, rating: 4.8, color: '深石榴红色',
aroma: ['黑樱桃', '泥土', '雪茄盒', '松露', '香料'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['烤鸭', '松露料理', '陈年奶酪'], tastingDate: '2025-01-25', tastingLocation: '北京瑰丽酒店',
tastingNotes: '唯一一个不在梅多克的1855一级庄,独特的泥土和矿物气息,优雅而深邃,陈年潜力极强。',
appearanceScore: 4.9, aromaScore: 4.8, palateScore: 4.8, overallScore: 4.8,
isFavorite: true, image: 'wine_08', stock: 1, purchaseDate: '2024-01-05', drinkBy: 2050
},
{
id: 9, name: '天娜红葡萄酒 2018', winery: 'Tenuta Tignanello',
region: '托斯卡纳 (Tuscany)', country: '意大利', vintage: 2018, grape: '桑娇维塞',
type: '红葡萄酒', alcohol: 13.5, price: 1200, rating: 4.4, color: '深红宝石色',
aroma: ['红樱桃', '紫罗兰', '皮革', '烟草', '香料'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['意面', '披萨', '烤肉'], tastingDate: '2025-05-22', tastingLocation: '成都丽思卡尔顿',
tastingNotes: '超级托斯卡纳经典之作,桑娇维塞的优雅与小比例赤霞珠的力量感完美融合,余味带有樱桃利口酒般的甜美。',
appearanceScore: 4.4, aromaScore: 4.5, palateScore: 4.3, overallScore: 4.4,
isFavorite: false, image: 'wine_09', stock: 8, purchaseDate: '2024-03-20', drinkBy: 2035
},
{
id: 10, name: '蒙大菲赤霞珠 2018', winery: 'Robert Mondavi',
region: '纳帕谷 (Napa Valley)', country: '美国', vintage: 2018, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 14.5, price: 880, rating: 4.4, color: '深紫红色',
aroma: ['黑加仑', '薄荷', '香草', '可可', '橡木'], body: '饱满', tannin: '中', acidity: '中', sweetness: '干型',
pairing: ['牛排', '汉堡', '烤鸡'], tastingDate: '2025-06-12', tastingLocation: '上海静安香格里拉',
tastingNotes: '纳帕谷经典风格,成熟水果的甜美与橡木桶的香草气息相得益彰,单宁柔和,易饮性好。',
appearanceScore: 4.3, aromaScore: 4.4, palateScore: 4.4, overallScore: 4.4,
isFavorite: false, image: 'wine_10', stock: 10, purchaseDate: '2024-04-05', drinkBy: 2030
},
{
id: 11, name: '璞立酒庄黑皮诺 2019', winery: 'Domaine Drouhin',
region: '威拉米特谷 (Willamette Valley)', country: '美国', vintage: 2019, grape: '黑皮诺',
type: '红葡萄酒', alcohol: 13.5, price: 520, rating: 4.3, color: '浅红宝石色',
aroma: ['红樱桃', '覆盆子', '玫瑰花瓣', '森林地表', '香料'], body: '中等', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['三文鱼', '鸭胸', '蘑菇料理'], tastingDate: '2025-04-25', tastingLocation: '苏州W酒店',
tastingNotes: '俄勒冈黑皮诺的优雅典范,红色水果香气精致,酸度活泼,带有明显的森林地表气息,勃艮第风格。',
appearanceScore: 4.2, aromaScore: 4.4, palateScore: 4.3, overallScore: 4.3,
isFavorite: false, image: 'wine_11', stock: 7, purchaseDate: '2024-05-15', drinkBy: 2029
},
{
id: 12, name: '西施佳雅 2018', winery: 'Tenuta San Guido',
region: '博格利 (Bolgheri)', country: '意大利', vintage: 2018, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 14, price: 5200, rating: 4.7, color: '深紫红色',
aroma: ['黑莓', '蓝莓', '香草', '甘草', '烟草'], body: '饱满', tannin: '高', acidity: '中', sweetness: '干型',
pairing: ['和牛', '野味', '蓝纹奶酪'], tastingDate: '2025-02-14', tastingLocation: '上海米其林三星餐厅',
tastingNotes: '意大利酒王,超级托斯卡纳的先驱,赤霞珠的浓郁与托斯卡纳风土的结合,单宁丝滑,结构宏大。',
appearanceScore: 4.7, aromaScore: 4.8, palateScore: 4.6, overallScore: 4.7,
isFavorite: true, image: 'wine_12', stock: 3, purchaseDate: '2024-02-14', drinkBy: 2040
},
{
id: 13, name: '亨兹勒雷司令 2020', winery: 'Henschke',
region: '伊顿谷 (Eden Valley)', country: '澳大利亚', vintage: 2020, grape: '雷司令',
type: '白葡萄酒', alcohol: 12, price: 420, rating: 4.4, color: '浅稻草黄色',
aroma: ['青柠', '白桃', '茉莉花', '蜂蜜', '汽油'], body: '轻盈', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['泰国菜', '海鲜', '辣味料理'], tastingDate: '2025-06-20', tastingLocation: '广州文华东方酒店',
tastingNotes: '澳洲顶级雷司令,酸度令人振奋,柑橘和白花香气纯净,带有标志性的汽油气息,陈年潜力出色。',
appearanceScore: 4.3, aromaScore: 4.5, palateScore: 4.3, overallScore: 4.4,
isFavorite: false, image: 'wine_13', stock: 9, purchaseDate: '2024-06-01', drinkBy: 2035
},
{
id: 14, name: '皇家托卡伊阿苏5篓 2017', winery: 'Royal Tokaji',
region: '托卡伊 (Tokaji)', country: '匈牙利', vintage: 2017, grape: '富尔民特',
type: '甜酒', alcohol: 11, price: 980, rating: 4.5, color: '琥珀金色',
aroma: ['杏干', '蜂蜜', '橘子酱', '焦糖', '香料'], body: '饱满', tannin: '低', acidity: '高', sweetness: '甜',
pairing: ['鹅肝', '蓝纹奶酪', '甜点'], tastingDate: '2025-03-08', tastingLocation: '北京华尔道夫酒店',
tastingNotes: '甜酒之王,贵腐葡萄带来的浓郁杏脯和蜂蜜风味,酸度完美平衡甜度,余味绵长不绝。',
appearanceScore: 4.6, aromaScore: 4.6, palateScore: 4.4, overallScore: 4.5,
isFavorite: true, image: 'wine_14', stock: 5, purchaseDate: '2024-03-08', drinkBy: 2060
},
{
id: 15, name: '美讯酒庄红葡萄酒 2014', winery: 'Château La Mission Haut-Brion',
region: '佩萨克-雷奥良 (Pessac-Léognan)', country: '法国', vintage: 2014, grape: '赤霞珠',
type: '红葡萄酒', alcohol: 14, price: 5500, rating: 4.6, color: '深石榴红色',
aroma: ['黑樱桃', '黑橄榄', '烟熏', '松露', '甘草'], body: '饱满', tannin: '高', acidity: '高', sweetness: '干型',
pairing: ['烤羊排', '松露料理', '硬质奶酪'], tastingDate: '2025-01-18', tastingLocation: '上海浦东丽思卡尔顿',
tastingNotes: '红颜容的姊妹酒庄,风格更加雄壮有力,烟熏和黑橄榄的气息独特,单宁强劲需长时间陈年。',
appearanceScore: 4.6, aromaScore: 4.6, palateScore: 4.6, overallScore: 4.6,
isFavorite: false, image: 'wine_15', stock: 2, purchaseDate: '2024-01-10', drinkBy: 2040
},
{
id: 16, name: '威廉费尔夏布利 2020', winery: 'William Fèvre',
region: '夏布利 (Chablis)', country: '法国', vintage: 2020, grape: '霞多丽',
type: '白葡萄酒', alcohol: 12.5, price: 360, rating: 4.2, color: '浅金黄色',
aroma: ['青苹果', '柠檬', '燧石', '白花', '牡蛎壳'], body: '中等', tannin: '低', acidity: '高', sweetness: '干型',
pairing: ['生蚝', '海鲜', '山羊奶酪'], tastingDate: '2025-05-05', tastingLocation: '大连君悦酒店',
tastingNotes: '经典夏布利风格,纯净的矿物质感,酸度清冽如刀,带有明显的燧石和牡蛎壳气息,配生蚝绝佳。',
appearanceScore: 4.1, aromaScore: 4.3, palateScore: 4.2, overallScore: 4.2,
isFavorite: false, image: 'wine_16', stock: 15, purchaseDate: '2024-04-12', drinkBy: 2028
},
{
id: 17, name: '西施瀑桃红 2021', winery: 'Whispering Angel',
region: '普罗旺斯 (Provence)', country: '法国', vintage: 2021, grape: '歌海娜',
type: '桃红', alcohol: 13, price: 280, rating: 4.1, color: '浅三文鱼色',
aroma: ['草莓', '西瓜', '柑橘', '玫瑰', '草本'], body: '轻盈', tannin: '低', acidity: '中', sweetness: '干型',
pairing: ['沙拉', '海鲜', '地中海料理'], tastingDate: '2025-06-15', tastingLocation: '三亚亚特兰蒂斯',
tastingNotes: '普罗旺斯桃红的代表作,颜色淡雅如洋葱皮,口感清爽柔顺,红色水果和柑橘的香气轻盈飘逸。',
appearanceScore: 4.3, aromaScore: 4.1, palateScore: 4.0, overallScore: 4.1,
isFavorite: false, image: 'wine_17', stock: 18, purchaseDate: '2024-05-20', drinkBy: 2025
},
{
id: 18, name: '泰勒10年茶色波特', winery: "Taylor's",
region: '波尔图 (Porto)', country: '葡萄牙', vintage: 0, grape: '国图瑞加',
type: '加强酒', alcohol: 20, price: 520, rating: 4.4, color: '琥珀褐色',
aroma: ['无花果', '焦糖', '坚果', '肉桂', '可可'], body: '饱满', tannin: '低', acidity: '中', sweetness: '甜',
pairing: ['巧克力', '坚果', '蓝纹奶酪'], tastingDate: '2025-02-28', tastingLocation: '澳门丽思卡尔顿',
tastingNotes: '经典茶色波特,氧化陈年带来的坚果和焦糖风味,甜而不腻,余味温暖悠长,适合餐后慢饮。',
appearanceScore: 4.5, aromaScore: 4.4, palateScore: 4.4, overallScore: 4.4,
isFavorite: false, image: 'wine_18', stock: 6, purchaseDate: '2024-02-25', drinkBy: 2060
},
{
id: 19, name: '莱茵高雷司令 2019', winery: 'Weingut Robert Weil',
region: '莱茵高 (Rheingau)', country: '德国', vintage: 2019, grape: '雷司令',
type: '白葡萄酒', alcohol: 12, price: 480, rating: 4.3, color: '浅金黄色',
aroma: ['青苹果', '白桃', '柠檬皮', '板岩', '蜂蜜'], body: '中等', tannin: '低', acidity: '高', sweetness: '半干',
pairing: ['亚洲料理', '猪肉', '海鲜'], tastingDate: '2025-04-10', tastingLocation: '青岛香格里拉',
tastingNotes: '德国莱茵高经典雷司令,板岩土壤带来的矿物感,酸度精准,半干的残糖与高酸度完美平衡。',
appearanceScore: 4.2, aromaScore: 4.4, palateScore: 4.3, overallScore: 4.3,
isFavorite: false, image: 'wine_19', stock: 8, purchaseDate: '2024-04-01', drinkBy: 2032
},
{
id: 20, name: '紫色马瑟兰 2020', winery: '中法庄园',
region: '山东烟台', country: '中国', vintage: 2020, grape: '马瑟兰',
type: '红葡萄酒', alcohol: 14, price: 358, rating: 4.2, color: '深紫红色',
aroma: ['蓝莓', '桑葚', '薄荷', '黑巧克力', '香料'], body: '饱满', tannin: '中', acidity: '中', sweetness: '干型',
pairing: ['北京烤鸭', '红烧肉', '中式炖菜'], tastingDate: '2025-05-30', tastingLocation: '北京瑰丽酒店',
tastingNotes: '中国马瑟兰的标杆之作,蓝莓和桑葚的果香奔放,带有薄荷的清凉感,单宁柔顺,展现了中国风土的独特魅力。',
appearanceScore: 4.3, aromaScore: 4.3, palateScore: 4.1, overallScore: 4.2,
isFavorite: true, image: 'wine_20', stock: 20, purchaseDate: '2024-05-25', drinkBy: 2032
}
]
// ==================== 模拟数据 - 品鉴记录15条 ====================
const TASTING_DATA: TastingRecordItem[] = [
{ id: 1, wineName: '拉菲古堡红葡萄酒 2018', vintage: 2018, winery: 'Château Lafite Rothschild', type: '红葡萄酒', tastingDate: '2025-03-15', tastingLocation: '上海半岛酒店', appearanceScore: 4.8, aromaScore: 4.9, palateScore: 4.7, overallScore: 4.8, notes: '经典波尔多左岸风格,单宁强劲而细腻,余味悠长。', taster: '李明', temperature: 18, decantTime: 120 },
{ id: 2, wineName: '唐培里侬香槟 2015', vintage: 2015, winery: 'Moët & Chandon', type: '起泡酒', tastingDate: '2025-04-02', tastingLocation: '北京CAFA葡萄酒学院', appearanceScore: 4.6, aromaScore: 4.5, palateScore: 4.4, overallScore: 4.5, notes: '气泡细腻持久,入口清爽活泼,烤面包和坚果风味明显。', taster: '王芳', temperature: 8, decantTime: 0 },
{ id: 3, wineName: '作品一号 2017', vintage: 2017, winery: 'Opus One', type: '红葡萄酒', tastingDate: '2025-02-20', tastingLocation: '广州四季酒店', appearanceScore: 4.7, aromaScore: 4.8, palateScore: 4.6, overallScore: 4.7, notes: '纳帕谷顶级,波尔多混酿与美国果味完美结合,单宁丝滑。', taster: '张伟', temperature: 18, decantTime: 90 },
{ id: 4, wineName: '奔富葛兰许 2017', vintage: 2017, winery: 'Penfolds', type: '红葡萄酒', tastingDate: '2025-05-10', tastingLocation: '深圳瑞吉酒店', appearanceScore: 4.6, aromaScore: 4.7, palateScore: 4.5, overallScore: 4.6, notes: '浓郁复杂,香草椰子与黑果完美融合,余味悠长。', taster: '陈静', temperature: 18, decantTime: 60 },
{ id: 5, wineName: '木桐酒庄红葡萄酒 2016', vintage: 2016, winery: 'Château Mouton Rothschild', type: '红葡萄酒', tastingDate: '2025-03-28', tastingLocation: '上海外滩会', appearanceScore: 4.8, aromaScore: 4.7, palateScore: 4.7, overallScore: 4.7, notes: '力量与优雅并存,单宁紧致有结构,木桐标志性个性。', taster: '李明', temperature: 18, decantTime: 120 },
{ id: 6, wineName: '路易王妃水晶香槟 2014', vintage: 2014, winery: 'Louis Roederer', type: '起泡酒', tastingDate: '2025-04-18', tastingLocation: '杭州柏悦酒店', appearanceScore: 4.7, aromaScore: 4.6, palateScore: 4.5, overallScore: 4.6, notes: '精致与力量,矿物质感强烈,收尾干净利落。', taster: '王芳', temperature: 10, decantTime: 0 },
{ id: 7, wineName: '云雾之湾长相思 2021', vintage: 2021, winery: 'Cloudy Bay', type: '白葡萄酒', tastingDate: '2025-06-05', tastingLocation: '厦门华尔道夫酒店', appearanceScore: 4.2, aromaScore: 4.4, palateScore: 4.3, overallScore: 4.3, notes: '热带水果奔放,酸度清脆,清新易饮。', taster: '刘洋', temperature: 10, decantTime: 0 },
{ id: 8, wineName: '红颜容酒庄 2015', vintage: 2015, winery: 'Château Haut-Brion', type: '红葡萄酒', tastingDate: '2025-01-25', tastingLocation: '北京瑰丽酒店', appearanceScore: 4.9, aromaScore: 4.8, palateScore: 4.8, overallScore: 4.8, notes: '独特泥土矿物气息,优雅深邃,陈年潜力极强。', taster: '张伟', temperature: 18, decantTime: 150 },
{ id: 9, wineName: '西施佳雅 2018', vintage: 2018, winery: 'Tenuta San Guido', type: '红葡萄酒', tastingDate: '2025-02-14', tastingLocation: '上海米其林三星餐厅', appearanceScore: 4.7, aromaScore: 4.8, palateScore: 4.6, overallScore: 4.7, notes: '意大利酒王,赤霞珠与托斯卡纳风土的完美结合。', taster: '李明', temperature: 18, decantTime: 90 },
{ id: 10, wineName: '皇家托卡伊阿苏5篓 2017', vintage: 2017, winery: 'Royal Tokaji', type: '甜酒', tastingDate: '2025-03-08', tastingLocation: '北京华尔道夫酒店', appearanceScore: 4.6, aromaScore: 4.6, palateScore: 4.4, overallScore: 4.5, notes: '甜酒之王,杏脯蜂蜜浓郁,酸度完美平衡甜度。', taster: '陈静', temperature: 12, decantTime: 0 },
{ id: 11, wineName: '天娜红葡萄酒 2018', vintage: 2018, winery: 'Tenuta Tignanello', type: '红葡萄酒', tastingDate: '2025-05-22', tastingLocation: '成都丽思卡尔顿', appearanceScore: 4.4, aromaScore: 4.5, palateScore: 4.3, overallScore: 4.4, notes: '桑娇维塞的优雅与赤霞珠的力量完美融合。', taster: '刘洋', temperature: 18, decantTime: 60 },
{ id: 12, wineName: '亨兹勒雷司令 2020', vintage: 2020, winery: 'Henschke', type: '白葡萄酒', tastingDate: '2025-06-20', tastingLocation: '广州文华东方酒店', appearanceScore: 4.3, aromaScore: 4.5, palateScore: 4.3, overallScore: 4.4, notes: '酸度令人振奋,柑橘白花纯净,汽油气息标志性。', taster: '王芳', temperature: 10, decantTime: 0 },
{ id: 13, wineName: '泰勒10年茶色波特', vintage: 0, winery: "Taylor's", type: '加强酒', tastingDate: '2025-02-28', tastingLocation: '澳门丽思卡尔顿', appearanceScore: 4.5, aromaScore: 4.4, palateScore: 4.4, overallScore: 4.4, notes: '坚果焦糖风味,甜而不腻,余味温暖悠长。', taster: '张伟', temperature: 16, decantTime: 0 },
{ id: 14, wineName: '莱茵高雷司令 2019', vintage: 2019, winery: 'Weingut Robert Weil', type: '白葡萄酒', tastingDate: '2025-04-10', tastingLocation: '青岛香格里拉', appearanceScore: 4.2, aromaScore: 4.4, palateScore: 4.3, overallScore: 4.3, notes: '板岩矿物感,酸度精准,半干残糖与高酸完美平衡。', taster: '刘洋', temperature: 10, decantTime: 0 },
{ id: 15, wineName: '紫色马瑟兰 2020', vintage: 2020, winery: '中法庄园', type: '红葡萄酒', tastingDate: '2025-05-30', tastingLocation: '北京瑰丽酒店', appearanceScore: 4.3, aromaScore: 4.3, palateScore: 4.1, overallScore: 4.2, notes: '蓝莓桑葚果香奔放,薄荷清凉感,中国风土独特魅力。', taster: '陈静', temperature: 18, decantTime: 30 }
]
// ==================== 模拟数据 - 品鉴活动12条 ====================
const ACTIVITY_DATA: ActivityItem[] = [
{ id: 1, title: '波尔多一级庄垂直品鉴晚宴', date: '2025-08-15', time: '19:00', location: '上海半岛酒店 · 珍宝厅', description: '五大一级庄垂直年份品鉴,由Master Wine李明主持,配以米其林三星晚宴。', participantCount: 24, wineCount: 15, status: '报名中', organizer: 'Wine Club Shanghai', fee: 8800 },
{ id: 2, title: '勃艮第特级园品鉴会', date: '2025-08-22', time: '14:00', location: '北京瑰丽酒店 · 天宝阁', description: '精选勃艮第特级园酒款,包括罗曼尼康帝、拉塔希等传奇酒款。', participantCount: 18, wineCount: 12, status: '报名中', organizer: 'CAFA葡萄酒学院', fee: 6800 },
{ id: 3, title: '纳帕谷 vs 波尔多盲品大赛', date: '2025-07-20', time: '15:00', location: '广州四季酒店 · 宴会厅', description: '经典对决!纳帕谷顶级酒款对阵波尔多列级庄,盲品猜产区。', participantCount: 30, wineCount: 10, status: '已结束', organizer: 'Wine Club Guangzhou', fee: 2800 },
{ id: 4, title: '意大利超级托斯卡纳主题品鉴', date: '2025-09-05', time: '19:00', location: '深圳瑞吉酒店 · 品酒阁', description: '西施佳雅、天娜、索拉雅等超级托斯卡纳酒款深度品鉴。', participantCount: 20, wineCount: 8, status: '报名中', organizer: 'Italian Wine Society', fee: 3600 },
{ id: 5, title: '香槟大师班 - 年份香槟专题', date: '2025-07-12', time: '14:00', location: '杭州柏悦酒店 · 悦轩', description: '年份香槟深度解析,唐培里侬、水晶、库克等顶级年份对比品鉴。', participantCount: 25, wineCount: 10, status: '已结束', organizer: 'Champagne Bureau', fee: 3200 },
{ id: 6, title: '德国雷司令风土品鉴会', date: '2025-09-20', time: '15:00', location: '青岛香格里拉 · 大宴会厅', description: '摩泽尔、莱茵高、法尔兹三大产区雷司令对比品鉴。', participantCount: 28, wineCount: 12, status: '报名中', organizer: 'German Wine Institute', fee: 1500 },
{ id: 7, title: '澳洲设拉子大师班', date: '2025-08-08', time: '14:00', location: '成都丽思卡尔顿 · 天阁', description: '巴罗莎谷、猎人谷、麦克拉伦谷设拉子横向品鉴对比。', participantCount: 22, wineCount: 10, status: '报名中', organizer: 'Wine Australia', fee: 1800 },
{ id: 8, title: '中国精品酒庄品鉴会', date: '2025-07-28', time: '15:00', location: '北京华尔道夫 · 荣萃厅', description: '中法庄园、银色高地、迦南美地等中国顶级酒庄酒款品鉴。', participantCount: 35, wineCount: 12, status: '已结束', organizer: '中国葡萄酒协会', fee: 800 },
{ id: 9, title: '甜酒与加强酒晚宴', date: '2025-10-10', time: '19:00', location: '澳门丽思卡尔顿 · 雅园', description: '托卡伊、苏玳、波特、雪莉等甜酒与加强酒搭配甜点晚宴。', participantCount: 16, wineCount: 8, status: '报名中', organizer: 'Sweet Wine Society', fee: 2600 },
{ id: 10, title: '黑皮诺风土品鉴 - 勃艮第 vs 俄勒冈', date: '2025-09-15', time: '15:00', location: '苏州W酒店 · 宴会厅', description: '勃艮第夜丘与俄勒冈威拉米特谷黑皮诺风土对比品鉴。', participantCount: 26, wineCount: 12, status: '报名中', organizer: 'Pinot Noir Club', fee: 2400 },
{ id: 11, title: 'WSET Level 3 葡萄酒认证课程', date: '2025-08-01', time: '09:00', location: '上海CAFA葡萄酒学院', description: 'WSET三级葡萄酒认证系统课程,含品鉴实操和考试。', participantCount: 20, wineCount: 80, status: '进行中', organizer: 'CAFA China', fee: 9800 },
{ id: 12, title: '年终名酒拍卖预展品鉴', date: '2025-12-05', time: '18:00', location: '香港半岛酒店 · 大宴会厅', description: '年度名酒拍卖会预展,顶级藏品品鉴与投资讲座。', participantCount: 50, wineCount: 30, status: '未开始', organizer: 'Sotheby\'s Wine', fee: 5000 }
]
// ==================== 统计数据 ====================
const MONTHLY_TASTING_DATA: MonthlyTastingData[] = [
{ month: '1月', count: 8 },
{ month: '2月', count: 12 },
{ month: '3月', count: 15 },
{ month: '4月', count: 10 },
{ month: '5月', count: 14 },
{ month: '6月', count: 18 },
{ month: '7月', count: 22 },
{ month: '8月', count: 16 },
{ month: '9月', count: 20 },
{ month: '10月', count: 11 },
{ month: '11月', count: 9 },
{ month: '12月', count: 13 }
]
const MAX_TASTING_COUNT: number = 22
const REGION_DISTRIBUTION: RegionData[] = [
{ region: '法国', count: 8, percentage: 40 },
{ region: '美国', count: 3, percentage: 15 },
{ region: '意大利', count: 2, percentage: 10 },
{ region: '澳大利亚', count: 2, percentage: 10 },
{ region: '新西兰', count: 1, percentage: 5 },
{ region: '德国', count: 1, percentage: 5 },
{ region: '匈牙利', count: 1, percentage: 5 },
{ region: '中国', count: 1, percentage: 5 },
{ region: '葡萄牙', count: 1, percentage: 5 }
]
const RATING_DISTRIBUTION: RatingData[] = [
{ range: '4.5-5.0', count: 8, percentage: 40 },
{ range: '4.0-4.4', count: 10, percentage: 50 },
{ range: '3.5-3.9', count: 2, percentage: 10 },
{ range: '3.0-3.4', count: 0, percentage: 0 },
{ range: '3.0以下', count: 0, percentage: 0 }
]
const WINE_TYPE_DISTRIBUTION: RegionData[] = [
{ region: '红葡萄酒', count: 12, percentage: 60 },
{ region: '白葡萄酒', count: 4, percentage: 20 },
{ region: '起泡酒', count: 2, percentage: 10 },
{ region: '甜酒', count: 1, percentage: 5 },
{ region: '桃红', count: 1, percentage: 5 },
{ region: '加强酒', count: 1, percentage: 5 }
]
const PROFILE_MENU_DATA: ProfileMenuData[] = [
{ icon: '窖', title: '我的酒窖', subtitle: '管理收藏酒款', value: '20款' },
{ icon: '笔', title: '品鉴记录', subtitle: '历史品鉴笔记', value: '168篇' },
{ icon: '购', title: '购买记录', subtitle: '购酒历史记录', value: '¥58,600' },
{ icon: '星', title: '我的评分', subtitle: '查看我的评分', value: '168条' },
{ icon: '铃', title: '消息通知', subtitle: '活动提醒通知', value: '5条' },
{ icon: '书', title: '品鉴指南', subtitle: '葡萄酒知识库', value: '' },
{ icon: '议', title: '意见反馈', subtitle: '反馈和建议', value: '' },
{ icon: '设', title: '设置', subtitle: '应用设置', value: '' }
]
const SUMMARY_DATA: SummaryCardData[] = [
{ label: '酒款收藏', value: '20', unit: '款', color: '#880E4F' },
{ label: '品鉴次数', value: '168', unit: '次', color: '#FFD54F' },
{ label: '平均评分', value: '4.4', unit: '分', color: '#7B1FA2' },
{ label: '酒窖估值', value: '¥58K', unit: '', color: '#2E7D32' }
]
// ==================== 枚举定义 ====================
enum WineTab {
Wines,
Tasting,
Events,
Stats,
Profile
}
// ==================== 主入口组件 ====================
@Entry
@Component
struct WineApp {
@State activeTab: WineTab = WineTab.Wines
@Builder
contentArea() {
if (this.activeTab === WineTab.Wines) {
WineListPage()
} else if (this.activeTab === WineTab.Tasting) {
TastingPage()
} else if (this.activeTab === WineTab.Events) {
EventPage()
} else if (this.activeTab === WineTab.Stats) {
WineStatsPage()
} else {
WineProfilePage()
}
}
@Builder
bottomTabItem(tab: WineTab, icon: string, label: string) {
Column() {
Text(icon)
.fontSize(22)
Text(label)
.fontSize(10)
.fontColor(this.activeTab === tab ? '#880E4F' : '#9E9E9E')
.margin({ top: 2 })
}
.justifyContent(FlexAlign.Center)
.layoutWeight(1)
.height('100%')
.onClick(() => {
this.activeTab = tab
})
}
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem(WineTab.Wines, '🍷', '酒款')
this.bottomTabItem(WineTab.Tasting, '📝', '品鉴')
this.bottomTabItem(WineTab.Events, '📅', '活动')
this.bottomTabItem(WineTab.Stats, '📊', '统计')
this.bottomTabItem(WineTab.Profile, '👤', '我的')
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.borderWidth(1)
.borderColor('#E0E0E0')
}
.width('100%')
.height('100%')
.backgroundColor('#FCE4EC')
}
}
// ==================== 酒款页 ====================
@Component
struct WineListPage {
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedWine: string = ''
@State filterType: string = '全部'
@State addName: string = ''
@State addWinery: string = ''
@State addRegion: string = ''
@State addCountry: string = ''
@State addVintage: string = ''
@State addGrape: string = ''
@State addAlcohol: string = ''
@State addPrice: string = ''
@State addType: string = '红葡萄酒'
@State addBody: string = '中等'
@State addTannin: string = '中'
@State addAcidity: string = '中'
@State addSweetness: string = '干型'
@State editNotes: string = ''
@State editRating: string = ''
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => onClose())
}
@Builder
addWineModal() {
Column() {
Column() {
Text('新增酒款')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.margin({ bottom: 16 })
Text('酒款名称')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入酒款名称', text: this.addName })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addName = value })
Text('酒庄')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入酒庄名称', text: this.addWinery })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addWinery = value })
Text('产区')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入产区', text: this.addRegion })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addRegion = value })
Text('国家')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入国家', text: this.addCountry })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addCountry = value })
Row() {
Column() {
Text('年份')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '2020', text: this.addVintage })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4 })
.onChange((value: string) => { this.addVintage = value })
}
.layoutWeight(1)
.margin({ right: 8 })
Column() {
Text('酒精度%')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '13.5', text: this.addAlcohol })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4 })
.onChange((value: string) => { this.addAlcohol = value })
}
.layoutWeight(1)
}
.width('100%')
.margin({ bottom: 10 })
Text('葡萄品种')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入葡萄品种', text: this.addGrape })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addGrape = value })
Text('价格(元)')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextInput({ placeholder: '请输入价格', text: this.addPrice })
.width('100%')
.height(40)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 10 })
.onChange((value: string) => { this.addPrice = value })
Text('葡萄酒类型')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(WINE_TYPE_OPTIONS, (type: string) => {
Text(type)
.fontSize(11)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.addType === type ? '#880E4F' : '#F5F5F5')
.fontColor(this.addType === type ? '#FFFFFF' : '#757575')
.margin({ right: 6, bottom: 4 })
.onClick(() => { this.addType = type })
}, (type: string) => type)
}
.width('100%')
.margin({ top: 4, bottom: 10 })
Text('酒体')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(BODY_OPTIONS, (body: string) => {
Text(body)
.fontSize(11)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.addBody === body ? '#880E4F' : '#F5F5F5')
.fontColor(this.addBody === body ? '#FFFFFF' : '#757575')
.margin({ right: 6 })
.onClick(() => { this.addBody = body })
}, (body: string) => body)
}
.width('100%')
.margin({ top: 4, bottom: 10 })
Text('单宁')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(TANNIN_OPTIONS, (tannin: string) => {
Text(tannin)
.fontSize(11)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.addTannin === tannin ? '#880E4F' : '#F5F5F5')
.fontColor(this.addTannin === tannin ? '#FFFFFF' : '#757575')
.margin({ right: 6 })
.onClick(() => { this.addTannin = tannin })
}, (tannin: string) => tannin)
}
.width('100%')
.margin({ top: 4, bottom: 10 })
Text('酸度')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(ACIDITY_OPTIONS, (acidity: string) => {
Text(acidity)
.fontSize(11)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.addAcidity === acidity ? '#880E4F' : '#F5F5F5')
.fontColor(this.addAcidity === acidity ? '#FFFFFF' : '#757575')
.margin({ right: 6 })
.onClick(() => { this.addAcidity = acidity })
}, (acidity: string) => acidity)
}
.width('100%')
.margin({ top: 4, bottom: 10 })
Text('甜度')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(SWEETNESS_OPTIONS, (sweet: string) => {
Text(sweet)
.fontSize(11)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.addSweetness === sweet ? '#880E4F' : '#F5F5F5')
.fontColor(this.addSweetness === sweet ? '#FFFFFF' : '#757575')
.margin({ right: 6 })
.onClick(() => { this.addSweetness = sweet })
}, (sweet: string) => sweet)
}
.width('100%')
.margin({ top: 4, bottom: 16 })
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.backgroundColor('#F5F5F5')
.fontColor('#757575')
.fontSize(15)
.borderRadius(8)
.margin({ right: 8 })
.onClick(() => { this.showAddModal = false })
Button('添加酒款')
.layoutWeight(1)
.height(42)
.backgroundColor('#880E4F')
.fontColor('#FFFFFF')
.fontSize(15)
.borderRadius(8)
.onClick(() => { this.showAddModal = false })
}
.width('100%')
}
.width('88%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder
editNotesModal() {
Column() {
Column() {
Text('编辑品鉴笔记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.margin({ bottom: 4 })
Text(this.selectedWine)
.fontSize(13)
.fontColor('#9E9E9E')
.margin({ bottom: 16 })
Text('品鉴笔记')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
TextArea({ placeholder: '请输入品鉴笔记', text: this.editNotes })
.width('100%')
.height(100)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.fontSize(14)
.margin({ top: 4, bottom: 12 })
.onChange((value: string) => { this.editNotes = value })
Text('评分')
.fontSize(13)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
Row() {
ForEach(['5.0', '4.5', '4.0', '3.5', '3.0'], (score: string) => {
Text(score)
.fontSize(13)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.editRating === score ? '#880E4F' : '#F5F5F5')
.fontColor(this.editRating === score ? '#FFFFFF' : '#757575')
.margin({ right: 8 })
.onClick(() => { this.editRating = score })
}, (score: string) => score)
}
.width('100%')
.margin({ top: 4, bottom: 16 })
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.backgroundColor('#F5F5F5')
.fontColor('#757575')
.fontSize(15)
.borderRadius(8)
.margin({ right: 8 })
.onClick(() => { this.showEditModal = false })
Button('保存')
.layoutWeight(1)
.height(42)
.backgroundColor('#880E4F')
.fontColor('#FFFFFF')
.fontSize(15)
.borderRadius(8)
.onClick(() => { this.showEditModal = false })
}
.width('100%')
}
.width('88%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder
deleteConfirmModal() {
Column() {
Column() {
Text('确认删除')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
.margin({ bottom: 12 })
Text('确定要删除 ' + this.selectedWine + ' 吗?')
.fontSize(14)
.fontColor('#616161')
.textAlign(TextAlign.Center)
.margin({ bottom: 20 })
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.backgroundColor('#F5F5F5')
.fontColor('#757575')
.fontSize(15)
.borderRadius(8)
.margin({ right: 8 })
.onClick(() => { this.showDeleteModal = false })
Button('确认删除')
.layoutWeight(1)
.height(42)
.backgroundColor('#C62828')
.fontColor('#FFFFFF')
.fontSize(15)
.borderRadius(8)
.onClick(() => { this.showDeleteModal = false })
}
.width('100%')
}
.width('75%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(24)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder
starBuilder(rating: number) {
Row() {
if (rating >= 1) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
if (rating >= 2) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
if (rating >= 3) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
if (rating >= 4) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
if (rating >= 5) {
Text('★').fontSize(13).fontColor('#FFD54F')
} else {
Text('★').fontSize(13).fontColor('#E0E0E0')
}
}
}
@Builder
summaryCard(data: SummaryCardData) {
Column() {
Text(data.value)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(data.color)
Text(data.unit)
.fontSize(10)
.fontColor('#9E9E9E')
.margin({ top: 2 })
Text(data.label)
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 12, bottom: 12 })
}
@Builder
wineCardBuilder(item: WineItem) {
Column() {
Row() {
Column() {
Text('🍷')
.fontSize(28)
Text(WINE_TYPE_CONFIG[item.type].label)
.fontSize(8)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width(56)
.height(70)
.borderRadius(10)
.backgroundColor(WINE_TYPE_CONFIG[item.type].color)
.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.layoutWeight(1)
if (item.isFavorite) {
Text('♥')
.fontSize(16)
.fontColor('#C62828')
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.winery)
.fontSize(11)
.fontColor('#757575')
.margin({ top: 3 })
Row() {
Text(item.country)
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text(item.vintage > 0 ? item.vintage + '年' : 'NV')
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text(item.grape)
.fontSize(10)
.fontColor('#9E9E9E')
}
.margin({ top: 2 })
Row() {
this.starBuilder(item.rating)
Text(' ' + item.rating.toFixed(1))
.fontSize(12)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Bold)
.margin({ left: 4 })
Row()
.layoutWeight(1)
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Top)
Row() {
ForEach(item.aroma, (a: string) => {
Text(a)
.fontSize(9)
.fontColor(WINE_TYPE_CONFIG[item.type].color)
.backgroundColor(WINE_TYPE_CONFIG[item.type].bgColor)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ right: 4 })
}, (a: string) => a)
}
.width('100%')
.margin({ top: 10 })
Row() {
Column() {
Text('酒体')
.fontSize(9)
.fontColor('#9E9E9E')
Row() {
Column()
.width(BODY_CONFIG[item.body].value + '%')
.height(4)
.backgroundColor('#880E4F')
.borderRadius(2)
}
.width(50)
.height(4)
.backgroundColor('#E0E0E0')
.borderRadius(2)
.margin({ top: 2 })
Text(item.body)
.fontSize(9)
.fontColor('#616161')
.margin({ top: 2 })
}
Column() {
Text('单宁')
.fontSize(9)
.fontColor('#9E9E9E')
Row() {
Column()
.width(TANNIN_CONFIG[item.tannin].value + '%')
.height(4)
.backgroundColor('#880E4F')
.borderRadius(2)
}
.width(50)
.height(4)
.backgroundColor('#E0E0E0')
.borderRadius(2)
.margin({ top: 2 })
Text(item.tannin)
.fontSize(9)
.fontColor('#616161')
.margin({ top: 2 })
}
.margin({ left: 16 })
Column() {
Text('酸度')
.fontSize(9)
.fontColor('#9E9E9E')
Row() {
Column()
.width(ACIDITY_CONFIG[item.acidity].value + '%')
.height(4)
.backgroundColor('#880E4F')
.borderRadius(2)
}
.width(50)
.height(4)
.backgroundColor('#E0E0E0')
.borderRadius(2)
.margin({ top: 2 })
Text(item.acidity)
.fontSize(9)
.fontColor('#616161')
.margin({ top: 2 })
}
.margin({ left: 16 })
Column() {
Text('甜度')
.fontSize(9)
.fontColor('#9E9E9E')
Row() {
Column()
.width(SWEETNESS_CONFIG[item.sweetness].value + '%')
.height(4)
.backgroundColor('#FFD54F')
.borderRadius(2)
}
.width(50)
.height(4)
.backgroundColor('#E0E0E0')
.borderRadius(2)
.margin({ top: 2 })
Text(item.sweetness)
.fontSize(9)
.fontColor('#616161')
.margin({ top: 2 })
}
.margin({ left: 16 })
Row()
.layoutWeight(1)
Column() {
Text('库存')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.stock + '瓶')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(item.stock <= 3 ? '#C62828' : '#2E7D32')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.margin({ top: 10 })
Row() {
Text('配餐: ')
.fontSize(10)
.fontColor('#9E9E9E')
ForEach(item.pairing, (p: string) => {
Text(p + ' ')
.fontSize(10)
.fontColor('#757575')
}, (p: string) => p)
Row()
.layoutWeight(1)
Text('适饮期至' + item.drinkBy)
.fontSize(9)
.fontColor('#9E9E9E')
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => {
this.selectedWine = item.name
this.editNotes = item.tastingNotes
this.editRating = item.rating.toFixed(1)
this.showEditModal = true
})
}
build() {
Stack() {
Scroll() {
Column() {
Text('葡 萄 酒 品 鉴')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('Vinothèque · 私人酒窖管理')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Row() {
this.summaryCard(SUMMARY_DATA[0])
this.summaryCard(SUMMARY_DATA[1])
this.summaryCard(SUMMARY_DATA[2])
this.summaryCard(SUMMARY_DATA[3])
}
.width('92%')
.margin({ bottom: 12 })
Row() {
Text('我的酒窖')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Row()
.layoutWeight(1)
Text('+ 添加酒款')
.fontSize(12)
.fontColor('#880E4F')
.backgroundColor('#FCE4EC')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.onClick(() => { this.showAddModal = true })
}
.width('92%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 8 })
Scroll() {
Row() {
ForEach(WINE_TYPE_FILTERS, (type: string) => {
Text(type)
.fontSize(11)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(this.filterType === type ? '#880E4F' : '#FFFFFF')
.fontColor(this.filterType === type ? '#FFFFFF' : '#757575')
.margin({ right: 6 })
.onClick(() => { this.filterType = type })
}, (type: string) => type)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 8 })
this.wineCardBuilder(WINE_DATA[0])
this.wineCardBuilder(WINE_DATA[1])
this.wineCardBuilder(WINE_DATA[2])
this.wineCardBuilder(WINE_DATA[3])
this.wineCardBuilder(WINE_DATA[4])
this.wineCardBuilder(WINE_DATA[5])
this.wineCardBuilder(WINE_DATA[6])
this.wineCardBuilder(WINE_DATA[7])
this.wineCardBuilder(WINE_DATA[8])
this.wineCardBuilder(WINE_DATA[9])
this.wineCardBuilder(WINE_DATA[10])
this.wineCardBuilder(WINE_DATA[11])
this.wineCardBuilder(WINE_DATA[12])
this.wineCardBuilder(WINE_DATA[13])
this.wineCardBuilder(WINE_DATA[14])
this.wineCardBuilder(WINE_DATA[15])
this.wineCardBuilder(WINE_DATA[16])
this.wineCardBuilder(WINE_DATA[17])
this.wineCardBuilder(WINE_DATA[18])
this.wineCardBuilder(WINE_DATA[19])
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
if (this.showAddModal) {
this.modalOverlay(() => { this.showAddModal = false })
this.addWineModal()
}
if (this.showEditModal) {
this.modalOverlay(() => { this.showEditModal = false })
this.editNotesModal()
}
if (this.showDeleteModal) {
this.modalOverlay(() => { this.showDeleteModal = false })
this.deleteConfirmModal()
}
}
.width('100%')
.height('100%')
}
}
// ==================== 品鉴页 ====================
@Component
struct TastingPage {
@Builder
starBuilder(score: number) {
Row() {
if (score >= 1) {
Text('★').fontSize(11).fontColor('#FFD54F')
} else {
Text('★').fontSize(11).fontColor('#E0E0E0')
}
if (score >= 2) {
Text('★').fontSize(11).fontColor('#FFD54F')
} else {
Text('★').fontSize(11).fontColor('#E0E0E0')
}
if (score >= 3) {
Text('★').fontSize(11).fontColor('#FFD54F')
} else {
Text('★').fontSize(11).fontColor('#E0E0E0')
}
if (score >= 4) {
Text('★').fontSize(11).fontColor('#FFD54F')
} else {
Text('★').fontSize(11).fontColor('#E0E0E0')
}
if (score >= 5) {
Text('★').fontSize(11).fontColor('#FFD54F')
} else {
Text('★').fontSize(11).fontColor('#E0E0E0')
}
}
}
@Builder
tastingItemBuilder(item: TastingRecordItem) {
Column() {
Row() {
Column() {
Text('🍷')
.fontSize(24)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor(WINE_TYPE_CONFIG[item.type].bgColor)
.justifyContent(FlexAlign.Center)
Column() {
Text(item.wineName)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text(item.winery + ' · ' + (item.vintage > 0 ? item.vintage + '年' : 'NV'))
.fontSize(11)
.fontColor('#757575')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(item.overallScore.toFixed(1))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
this.starBuilder(item.overallScore)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('外观')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.appearanceScore.toFixed(1))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.margin({ top: 2 })
this.starBuilder(item.appearanceScore)
}
.layoutWeight(1)
Column() {
Text('香气')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.aromaScore.toFixed(1))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.margin({ top: 2 })
this.starBuilder(item.aromaScore)
}
.layoutWeight(1)
Column() {
Text('口感')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.palateScore.toFixed(1))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.margin({ top: 2 })
this.starBuilder(item.palateScore)
}
.layoutWeight(1)
Column() {
Text('综合')
.fontSize(9)
.fontColor('#9E9E9E')
Text(item.overallScore.toFixed(1))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.margin({ top: 2 })
this.starBuilder(item.overallScore)
}
.layoutWeight(1)
}
.width('100%')
.margin({ top: 12 })
Text(item.notes)
.fontSize(12)
.fontColor('#616161')
.margin({ top: 10 })
.width('100%')
Row() {
Text('📅 ' + item.tastingDate)
.fontSize(10)
.fontColor('#9E9E9E')
Text(' · ')
.fontSize(10)
.fontColor('#9E9E9E')
Text('📍 ' + item.tastingLocation)
.fontSize(10)
.fontColor('#9E9E9E')
Row()
.layoutWeight(1)
Text('侍酒师: ' + item.taster)
.fontSize(10)
.fontColor('#9E9E9E')
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 8 })
Row() {
Text('🌡️ ' + item.temperature + '°C')
.fontSize(10)
.fontColor('#757575')
.backgroundColor('#F5F5F5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
if (item.decantTime > 0) {
Text('⏱️ 醒酒' + item.decantTime + '分钟')
.fontSize(10)
.fontColor('#757575')
.backgroundColor('#F5F5F5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ left: 6 })
}
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
}
build() {
Scroll() {
Column() {
Text('品鉴记录')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('记录每一次品酒的美好时刻')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Row() {
Column() {
Text('168')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('总品鉴次数')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
Column() {
Text('4.4')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('平均评分')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
.margin({ left: 8 })
Column() {
Text('12')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1FA2')
Text('本月品鉴')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
.margin({ left: 8 })
}
.width('92%')
.margin({ bottom: 12 })
Row() {
Text('品鉴笔记')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Row()
.layoutWeight(1)
Text('共15条')
.fontSize(11)
.fontColor('#9E9E9E')
}
.width('92%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 8 })
this.tastingItemBuilder(TASTING_DATA[0])
this.tastingItemBuilder(TASTING_DATA[1])
this.tastingItemBuilder(TASTING_DATA[2])
this.tastingItemBuilder(TASTING_DATA[3])
this.tastingItemBuilder(TASTING_DATA[4])
this.tastingItemBuilder(TASTING_DATA[5])
this.tastingItemBuilder(TASTING_DATA[6])
this.tastingItemBuilder(TASTING_DATA[7])
this.tastingItemBuilder(TASTING_DATA[8])
this.tastingItemBuilder(TASTING_DATA[9])
this.tastingItemBuilder(TASTING_DATA[10])
this.tastingItemBuilder(TASTING_DATA[11])
this.tastingItemBuilder(TASTING_DATA[12])
this.tastingItemBuilder(TASTING_DATA[13])
this.tastingItemBuilder(TASTING_DATA[14])
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}
}
// ==================== 活动页 ====================
@Component
struct EventPage {
@Builder
activityItemBuilder(item: ActivityItem) {
Column() {
Row() {
Column() {
Text(item.date.substring(5, 7))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(item.date.substring(8, 10) + '日')
.fontSize(11)
.fontColor('#FCE4EC')
.margin({ top: 2 })
}
.width(56)
.height(56)
.borderRadius(12)
.backgroundColor('#880E4F')
.justifyContent(FlexAlign.Center)
Column() {
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text('🕐 ' + item.time + ' · 📍 ' + item.location)
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Column() {
Text(item.status)
.fontSize(10)
.fontColor(item.status === '报名中' ? '#2E7D32' : item.status === '已结束' ? '#9E9E9E' : item.status === '进行中' ? '#FF8F00' : '#757575')
.backgroundColor(item.status === '报名中' ? '#E8F5E9' : item.status === '已结束' ? '#F5F5F5' : item.status === '进行中' ? '#FFF8E1' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.description)
.fontSize(12)
.fontColor('#616161')
.margin({ top: 10 })
.width('100%')
Row() {
Column() {
Text(item.participantCount + '人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('参与人数')
.fontSize(9)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
Column() {
Text(item.wineCount + '款')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('品鉴酒款')
.fontSize(9)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
.margin({ left: 24 })
Column() {
Text('¥' + item.fee)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('活动费用')
.fontSize(9)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
.margin({ left: 24 })
Row()
.layoutWeight(1)
Column() {
Text(item.organizer)
.fontSize(10)
.fontColor('#9E9E9E')
if (item.status === '报名中') {
Text('立即报名 >')
.fontSize(11)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ top: 12 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 8 })
}
build() {
Scroll() {
Column() {
Text('品鉴活动')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('与志同道合的酒友一起品鉴美酒')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Row() {
Column() {
Text('4')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#2E7D32')
Text('报名中')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
Column() {
Text('3')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#9E9E9E')
Text('已结束')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
.margin({ left: 8 })
Column() {
Text('1')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF8F00')
Text('进行中')
.fontSize(11)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 16, bottom: 16 })
.margin({ left: 8 })
}
.width('92%')
.margin({ bottom: 12 })
Row() {
Text('活动列表')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Row()
.layoutWeight(1)
Text('共12场')
.fontSize(11)
.fontColor('#9E9E9E')
}
.width('92%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 8 })
this.activityItemBuilder(ACTIVITY_DATA[0])
this.activityItemBuilder(ACTIVITY_DATA[1])
this.activityItemBuilder(ACTIVITY_DATA[2])
this.activityItemBuilder(ACTIVITY_DATA[3])
this.activityItemBuilder(ACTIVITY_DATA[4])
this.activityItemBuilder(ACTIVITY_DATA[5])
this.activityItemBuilder(ACTIVITY_DATA[6])
this.activityItemBuilder(ACTIVITY_DATA[7])
this.activityItemBuilder(ACTIVITY_DATA[8])
this.activityItemBuilder(ACTIVITY_DATA[9])
this.activityItemBuilder(ACTIVITY_DATA[10])
this.activityItemBuilder(ACTIVITY_DATA[11])
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}
}
// ==================== 统计页 ====================
@Component
struct WineStatsPage {
@Builder
monthlyBarBuilder(data: MonthlyTastingData) {
Column() {
Column() {
Column()
.width(20)
.height(data.count / MAX_TASTING_COUNT * 100)
.backgroundColor('#880E4F')
.borderRadius({ topLeft: 4, topRight: 4 })
}
.height(100)
.justifyContent(FlexAlign.End)
Text(data.count.toString())
.fontSize(9)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.margin({ top: 2 })
Text(data.month)
.fontSize(9)
.fontColor('#9E9E9E')
}
.layoutWeight(1)
}
@Builder
regionBarBuilder(data: RegionData) {
Row() {
Text(data.region)
.fontSize(11)
.fontColor('#616161')
.width(70)
Row() {
Column()
.width(data.percentage + '%')
.height(16)
.backgroundColor('#880E4F')
.borderRadius(8)
}
.layoutWeight(1)
.height(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Text(data.count + '款')
.fontSize(11)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
}
@Builder
typeBarBuilder(data: RegionData) {
Row() {
Text(data.region)
.fontSize(11)
.fontColor('#616161')
.width(70)
Row() {
Column()
.width(data.percentage + '%')
.height(16)
.backgroundColor(WINE_TYPE_CONFIG[data.region] ? WINE_TYPE_CONFIG[data.region].color : '#880E4F')
.borderRadius(8)
}
.layoutWeight(1)
.height(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Text(data.percentage + '%')
.fontSize(11)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
.width(36)
.textAlign(TextAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
}
@Builder
ratingBarBuilder(data: RatingData) {
Row() {
Text(data.range)
.fontSize(11)
.fontColor('#616161')
.width(60)
Row() {
Column()
.width(data.percentage + '%')
.height(16)
.backgroundColor('#FFD54F')
.borderRadius(8)
}
.layoutWeight(1)
.height(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Text(data.count + '款')
.fontSize(11)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Medium)
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
}
build() {
Scroll() {
Column() {
Text('品鉴统计')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
.alignSelf(ItemAlign.Start)
.margin({ top: 12, left: 16 })
Text('2025年度 · 品鉴数据分析')
.fontSize(12)
.fontColor('#757575')
.alignSelf(ItemAlign.Start)
.margin({ top: 2, left: 16, bottom: 12 })
Column() {
Text('月度品鉴数量')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
Row() {
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[0])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[1])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[2])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[3])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[4])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[5])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[6])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[7])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[8])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[9])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[10])
this.monthlyBarBuilder(MONTHLY_TASTING_DATA[11])
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('全年品鉴')
.fontSize(12)
.fontColor('#757575')
Row()
.layoutWeight(1)
Text('168次')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
}
.width('100%')
.margin({ top: 12 })
.alignItems(VerticalAlign.Center)
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
Column() {
Text('葡萄酒类型分布')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[0])
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[1])
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[2])
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[3])
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[4])
this.typeBarBuilder(WINE_TYPE_DISTRIBUTION[5])
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
Column() {
Text('产区分布')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
this.regionBarBuilder(REGION_DISTRIBUTION[0])
this.regionBarBuilder(REGION_DISTRIBUTION[1])
this.regionBarBuilder(REGION_DISTRIBUTION[2])
this.regionBarBuilder(REGION_DISTRIBUTION[3])
this.regionBarBuilder(REGION_DISTRIBUTION[4])
this.regionBarBuilder(REGION_DISTRIBUTION[5])
this.regionBarBuilder(REGION_DISTRIBUTION[6])
this.regionBarBuilder(REGION_DISTRIBUTION[7])
this.regionBarBuilder(REGION_DISTRIBUTION[8])
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
Column() {
Text('评分分布')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
this.ratingBarBuilder(RATING_DISTRIBUTION[0])
this.ratingBarBuilder(RATING_DISTRIBUTION[1])
this.ratingBarBuilder(RATING_DISTRIBUTION[2])
this.ratingBarBuilder(RATING_DISTRIBUTION[3])
this.ratingBarBuilder(RATING_DISTRIBUTION[4])
Row() {
Text('平均评分')
.fontSize(12)
.fontColor('#757575')
Row()
.layoutWeight(1)
Text('4.4 / 5.0')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
}
.width('100%')
.margin({ top: 12 })
.alignItems(VerticalAlign.Center)
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
Column() {
Text('酒窖概览')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
.alignSelf(ItemAlign.Start)
.margin({ bottom: 12 })
Row() {
Column() {
Text('20')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('酒款总数')
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('124')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('库存瓶数')
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('¥58.6K')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#880E4F')
Text('酒窖估值')
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('6')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
Text('低库存')
.fontSize(10)
.fontColor('#757575')
.margin({ top: 4 })
}
.layoutWeight(1)
}
.width('100%')
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.margin({ bottom: 12 })
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}
}
// ==================== 我的页 ====================
@Component
struct WineProfilePage {
@Builder
menuOptionBuilder(menu: ProfileMenuData) {
Row() {
Column() {
Text(menu.icon)
.fontSize(16)
.fontColor('#880E4F')
.fontWeight(FontWeight.Bold)
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#FCE4EC')
.justifyContent(FlexAlign.Center)
Column() {
Text(menu.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#212121')
Text(menu.subtitle)
.fontSize(11)
.fontColor('#9E9E9E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
if (menu.value.length > 0) {
Text(menu.value)
.fontSize(12)
.fontColor('#880E4F')
.fontWeight(FontWeight.Medium)
}
Text(' >')
.fontSize(14)
.fontColor('#BDBDBD')
.margin({ left: 4 })
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.Center)
}
build() {
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('L')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
}
.width(70)
.height(70)
.borderRadius(35)
.backgroundColor('#4A148C')
.justifyContent(FlexAlign.Center)
Column() {
Text('李明 · WSET Level 4')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('WSET Diploma Candidate')
.fontSize(11)
.fontColor('#FCE4EC')
.margin({ top: 4 })
Row() {
Text('🌟 资深品酒师')
.fontSize(10)
.fontColor('#FFD54F')
.backgroundColor('rgba(255,213,79,0.2)')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
}
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 16 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
.margin({ bottom: 16 })
Row() {
Column() {
Text('168')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('品鉴次数')
.fontSize(10)
.fontColor('#FCE4EC')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('20')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('收藏酒款')
.fontSize(10)
.fontColor('#FCE4EC')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('12')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('参加活动')
.fontSize(10)
.fontColor('#FCE4EC')
.margin({ top: 4 })
}
.layoutWeight(1)
}
.width('100%')
}
.width('92%')
.backgroundColor('#880E4F')
.borderRadius(16)
.padding(20)
.margin({ top: 12, bottom: 12 })
Column() {
this.menuOptionBuilder(PROFILE_MENU_DATA[0])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[1])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[2])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[3])
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 12 })
Column() {
this.menuOptionBuilder(PROFILE_MENU_DATA[4])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[5])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[6])
Column().width('100%').height(1).backgroundColor('#F0F0F0')
this.menuOptionBuilder(PROFILE_MENU_DATA[7])
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 12 })
Text('Vinothèque v3.2.1')
.fontSize(11)
.fontColor('#BDBDBD')
.margin({ top: 8, bottom: 20 })
}
.width('100%')
}
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}
}
十二、总结
通过对这款葡萄酒品鉴记录应用的完整代码剖析,我们可以提炼出以下几个层面的技术洞察。
在架构层面,应用遵循了"接口契约 -> 领域模型 -> 配置常量 -> 模拟数据 -> 组件实现"的分层架构。每一层都有明确的职责边界:接口层定义数据形状,模型层赋予响应式能力,配置层集中管理业务规则,数据层提供真实样本,组件层负责视觉呈现与交互逻辑。这种分层方式使得代码的可读性和可维护性都处于较高水平,任何一层的修改都不会对其他层造成连锁影响。

在数据建模层面,应用展现了对葡萄酒领域的深入理解。WineItem 接口的 30 个字段涵盖了从基础信息到专业评分的完整维度,四维评分体系(外观、香气、口感、综合)与国际通用的 WSET 品鉴方法论保持一致,侍酒温度、醒酒时间等字段的引入进一步提升了专业性。配置常量将葡萄酒类型与色彩绑定、将口感描述与数值映射的做法,既是工程上的解耦,也是领域知识的形式化表达。
在 UI 实现层面,应用最大的技术亮点在于完全用原生布局能力实现了数据可视化。月度柱状图通过 height 的动态计算和 FlexAlign.End 的底部对齐实现;横向条形图通过嵌套 Row 的宽度百分比实现;星级评分通过条件渲染实现。这些方案避免了引入第三方图表库的依赖,保持了应用的轻量性,同时也展示了声明式 UI 在数据可视化场景下的潜力。
在交互设计层面,应用采用了"卡片式信息架构 + 模态弹窗"的成熟模式。每张卡片都是一个自包含的信息单元,通过 @Builder 方法参数化构建,保证了视觉一致性。弹窗系统通过 Stack 叠加遮罩层和内容层实现模态效果,状态变量控制显示隐藏,逻辑清晰。筛选条采用横向滚动设计,在不占用垂直空间的前提下容纳了全部筛选选项。
在视觉设计层面,"酒红 + 香槟金 + 淡粉"的色彩体系贯穿始终,既有品牌识别度,又与葡萄酒主题高度契合。色彩的使用遵循"主色用于强调、浅色用于背景"的原则,通过 WINE_TYPE_CONFIG 等配置实现了数据驱动的动态配色,让不同类型的酒款在视觉上自然区分。
在工程实践层面,应用展示了多项值得借鉴的习惯:枚举替代字符串常量管理页签状态、@Observed 装饰器实现响应式数据流、@Builder 方法实现 UI 复用、默认值初始化保证对象合法性、条件渲染避免无意义信息展示。这些细节累积起来,构成了代码质量的基础保障。
当然,作为一个以教学和演示为目的的实现,应用也有一些可以进一步优化的方向。例如,酒款列表目前是直接展开 20 张卡片,在大数据量场景下应考虑使用 LazyForEach 实现懒加载;表单提交目前只是关闭弹窗,未实际写入数据源,可以接入持久化存储;筛选功能目前只更新了 filterType 状态,但未对列表进行实际过滤渲染。这些都是从演示原型走向生产级应用时需要补齐的环节。
更多推荐
所有评论(0)