引言:当葡萄酒文化遇见移动端技术

葡萄酒,作为一种承载了千年文明的饮品,早已超越了单纯的消费属性,成为生活方式与品位的象征。对于真正的葡萄酒爱好者与专业品酒师而言,每一瓶酒背后都蕴含着产地风土、酿造工艺、陈年潜力等多维度的信息。如何系统化地管理这些信息、记录每一次品鉴体验、追踪酒窖库存变化,并从中提炼出有价值的数据洞察,一直是一个既有生活温度、又有技术深度的命题。

在这里插入图片描述

随着移动端开发技术的不断演进,尤其是声明式 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:结构相同,都是 labelvaluelabel 用于显示,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 涵盖了品鉴活动的所有信息维度;MonthlyTastingDataRegionDataRatingData 分别为不同类型的统计图表服务;ProfileMenuDataSummaryCardData 则服务于个人中心和首页的概览卡片。


二、数据模型层:观察者模式的实践

在 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_FILTERSWINE_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 = ''

这里的状态管理值得细看。三个布尔变量 showAddModalshowEditModalshowDeleteModal 分别控制三个弹窗的显示与隐藏。filterType 记录当前筛选的类型。add* 系列变量绑定新增表单的各个输入框。editNoteseditRating 绑定编辑弹窗。

一个值得讨论的细节是:年份、酒精度等本应是数字的字段,状态变量却声明为 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 状态,但未对列表进行实际过滤渲染。这些都是从演示原型走向生产级应用时需要补齐的环节。

更多推荐