醋坊非遗酿醋应用基于HarmonyOS API 24深度解析——采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 VinegarApp 作为整个应用的根容器

本文基于 HarmonyOS NEXT + ArkTS 声明式开发框架,完整实现一个「醋坊」非遗酿醋主题 App,涵盖配色系统、状态管理、多 Tab 页面、自定义弹窗、动态颜色映射、深棕色货架视觉等核心知识点,并附完整源码与运行效果截图。


一、引言:非遗酿醋的数字化传承

在移动互联网与智能设备高速发展的今天,非物质文化遗产的传承与传播已经从单纯的线下演示,演变为一种高度数字化、流程化、个性化的线上线下融合体验。酿醋,作为中国传统饮食文化的核心技艺,距今已有两千余年历史。一坛好醋,从蒸料、糖化、发酵、熏醅到淋醋、陈酿,需经数十道工序,凝聚着匠人的心血与粮食的醇厚。三年陈的酸香、五年陈的醇厚、十年陈的绵柔、糯米香醋的鲜甜、红曲米醋的微酸、苹果醋的果香,每一款醋都承载着中国人对味的追求与讲究。然而,这门古老的技艺正面临着传承人老龄化、受众面狭窄、销售渠道单一等现实困境。

正是在这样的背景下,醋坊类数字化应用应运而生,并迅速成为非遗酿醋"活态传承"的数字载体。一个优秀的醋坊应用,不仅需要覆盖从"醋品货架"到"客评口碑"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到细致入微,让用户在指尖滑动间便能感受到粮食的醇厚与陈酿的雅致。

本文将深入剖析一个基于鸿蒙 ArkUI 声明式开发框架构建的醋坊非遗酿醋应用。该应用以"醋品货架"为核心,横向扩展出醋坛墙、醋方册、订单管理、客评口碑五大功能模块,形成了一个完整闭环的酿醋管理生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、模态弹窗、列表渲染、图表可视化、动态颜色映射、深棕色货架视觉等关键技术点。

醋坊应用整体运行效果 - 大酿坊每日出货柱状图与醋品货架列表


二、应用整体架构概览

整个应用采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 VinegarApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态,并根据状态切换不同的内容区组件。

应用一共划分为五个一级页面:醋品货架(VINEGAR)、醋坛墙(JAR)、醋方册(RECIPE)、订单管理(ORDER)以及客评口碑(REVIEW)。每个页面都是一个独立的 @Component 装饰的自定义组件,内部封装了自己的状态、构建逻辑和子组件。这种"高内聚、低耦合"的组件化设计,使得每个页面可以独立开发、独立测试,同时也便于后续的扩展与维护。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、醋品元数据、醋坛元数据、醋方元数据、订单元数据、客评元数据、周销量元数据、醋型构成元数据、工艺热度元数据、TOP榜元数据等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类 VinegarData 为载体,配合五组各 12 条模拟数据,模拟了一个真实的醋坊数据环境。

在交互层面,应用通过条件渲染(if-else)实现了四类弹窗的叠加显示:新酿醋品、修改订单、撤下醋坛、醋方详情。弹窗以全屏半透明遮罩 + 居中卡片的形式呈现,与主内容共享同一根 Column,通过布尔状态控制显隐。

在视觉层面,应用独创了"深棕色货架"的醋品页设计——醋品页整个列表卡片都使用深棕色背景(primary #5A3A28 与 brew #5A3A28 奇偶交替),白色文字,金棕色价格,模仿醋坊的深色货架,形成了强烈的视觉识别特征。全局圆角极小(圆角2/4/6),模仿醋坛的直方造型。

醋坊订单模块运行效果 - 酿醋工艺热度排行与醋品订单列表(真机运行)

下面逐页展示应用的五大功能模块运行效果。


三、醋品页:深棕色货架的核心陈列中枢

醋品页是整个应用的默认首页,也是视觉设计最独特的页面。它包含了大酿坊每日出货柱状图、醋品货架列表,以及新酿醋品弹窗。

醋品页顶部是"大酿坊 · 每日出货"柱状图卡片,采用了独创的深棕色背景设计——背景色为 primaryDark #2E1E14,标题文字为白色,柱状图使用金棕色 warning #B0893F,数值和日期文字为浅棕色 primaryLight,卡片圆角 4,带有 2px 金棕色半透明边框。通过 ForEach 遍历一周七天的出货数据,柱子高度直接绑定数值,周六以 70 坛的出货量居首,符合醋品作为日常调味品在周末热销的真实节奏。

下方是醋品货架列表,12 款醋品卡片采用了独创的"深棕色货架"设计——卡片背景使用 primary #5A3A28 和 brew #5A3A28 奇偶交替(都是深棕色!),文字为白色,价格为金棕色,标签为金棕和浅棕。左侧是一个 48×48 的图标方块,背景色通过 getVinegarColor 函数按醋品名称分档着色,圆角 4,展示醋品 emoji 图标。右侧信息区展示醋品名称、价格(金棕色)、醋型标签(金棕色)和陈酿标签(浅棕色,“陈3年”/"陈5年"等)。

12 款醋品覆盖了从 16 元(白米醋)到 168 元(十年陈醋)的完整价格梯队,醋型涵盖陈醋、香醋、米醋、果醋、养生醋、调味醋六大类,陈酿从 1 年到 10 年不等,每款都配有独特的 emoji 图标(三年陈醋用🍶、十年陈醋用⚱️、糯米香醋用🍚、苹果醋用🍎、梅子醋用🫐等),让醋品不仅是调味品,更是味觉与视觉的双重体验。右上角 ➕ 按钮点击可弹出"新酿醋品"弹窗。


四、醋坛页:销量排行与撤坛管理

醋坛页以"醋坛"为核心,顶部是醋品销量 TOP6 横向条形图,下方是醋坛墙列表,支持撤坛操作。

醋坛模块 - 醋品销量TOP6横向条形图与醋坛墙列表

顶部 TOP6 卡片通过横向进度条展示销量排名前六的醋品品种,三年陈醋以 96 的销量居首,五年陈醋、糯米香醋紧随其后。进度条使用 layoutWeight 比例法实现,已售部分与剩余部分按比例分配宽度,颜色直接取数据中的配置色,六种醋品各有特色。

下方醋坛墙列表展示 12 款醋坛,卡片采用"左图标右文字"布局。左侧是一个 42×42 的图标方块,背景色通过 getJarColor 函数按窑品级别分档着色(≥95用深棕、≥91用赭红、其余用金棕),圆角 4,展示醋坛 emoji 图标。右侧展示醋坛名称、材质(粗陶/紫砂/黑釉瓷等)、窑品级别(“窑品96级”,颜色按级别动态着色)、产地(“宜兴 · 窑口”)和 🗑️ 撤坛按钮。

12 款醋坛材质覆盖粗陶、紫砂、黑釉瓷、青瓷、麻陶、黄泥陶、柿釉瓷、酱釉瓷、土釉陶、绞胎陶、冰裂纹陶、陈年泥陶十二种,窑品级别从 88(土釉大缸)到 97(老陈泥瓮)不等,老陈泥瓮以 97 级居首,紫砂醋坛和绞胎坛 95 级次之,符合"坛以陈为尊、以紫砂为贵"的行业共识。产地从宜兴、磁州、龙泉、榆次、镇江、淄博、佛山、邯郸、焦作、景德镇到清徐,几乎囊括了中国古代制陶的全部名窑口。点击 🗑️ 按钮弹出"撤下醋坛"二次确认弹窗,采用警示型设计语言,"确认撤下"按钮使用危险色赭红,防止误操作。


五、醋方页:醋型构成与醋方详情

醋方页以"醋方"为核心,顶部是醋型构成占比图,下方是醋方册列表,支持查看详情。

醋方模块 - 醋型构成占比图与醋方册列表

顶部醋型构成卡片展示三种醋型的分布占比:陈醋 5 款、米醋 4 款、果醋 3 款,合计 12 款,恰好对应醋品货架的醋型总数。进度条以 12 为满刻度进行比例渲染,比百分比更直观地传达了"几醋型"的语义。

下方醋方册列表展示 12 款醋方,卡片采用"左图标右文字"布局。左侧是一个 42×42 的图标方块,背景统一使用 accent 赭红 14% 透明度,圆角 4,展示菜品 emoji 图标。右侧展示醋方名称、主料(排骨/土豆/花生等)、用法与流派(“炒糖色 · 无锡味”)和 👁️ 详情按钮。

12 款醋方覆盖糖醋排骨、酸辣土豆丝、老醋花生、醋熘白菜、西湖醋鱼、醋泡黑豆、酸汤肥牛、醋蘸饺子、姜醋猪蹄、醋腌萝卜、果醋沙拉、醋熏卤味十二道经典用醋菜肴,用法涵盖炒糖色、起锅淋、凉拌、炝锅、浇汁、浸泡、调汤、蘸食、炖煮、腌制、拌酱、熏制十二种,流派从无锡、四川、山西、北京、杭州、民间、贵州、北方、广东、潮汕、西式到江南,呈现出"醋通天下味"的格局。点击 👁️ 按钮弹出"醋方详情"弹窗,包含醋方展示区、文艺风格描述文案(“醋要后放、沿锅边淋,高温激出醋香而不留死酸。糖醋菜则先炒糖色,再下主料,末了烹醋翻匀,一锅出琉璃亮汁。老厨师的分寸,全在这半勺醋的先后。”)、流派源地(“山西 · 太原”)和主厨评级(“特一级 · 掌勺卅年”)。


六、订单页:酿醋工艺热度与订单改单

订单页以"订单"为核心,顶部是酿醋工艺热度排行,下方是订单列表,支持修改订单。

订单模块 - 酿醋工艺热度排行与醋品订单列表

顶部酿醋工艺热度卡片展示六门工序的热度指数:蒸料 95、糖化 94、发酵 96、熏醅 92、淋醋 90、陈酿 93,恰好对应酿醋制作的完整流程(蒸料→糖化→发酵→熏醅→淋醋→陈酿),其中发酵以 96 的热度居首(不是第一道工序),呈现出"发酵为核心工序"的合理分布。六根进度条颜色从深棕到赭红到金棕到蓝色到浅灰到竹叶绿,形成了一条色彩渐变的工艺流水线。

下方订单列表展示 12 条订单,卡片采用"左图标右文字"布局。左侧是一个 40×40 的图标方块,背景色通过 getOrderColor 函数按订单金额分档着色(15万以上深棕、11-15万赭红、8-11万金棕、8万以下蓝色),圆角 4,展示客户 emoji 图标。右侧展示客户名称、✏️ 编辑按钮、区域标签和订单详情(“· 1400 坛 · ¥118000”)。

12 类客户覆盖老字号饭店、火锅连锁、饺子馆联盟、商超供货商、拌菜连锁、面馆集团、养生会所、月子中心、学校食堂、电商自营店、机场免税店、海外中餐厅。商超供货商以 5200 坛的数量和 188000 元的金额双居首,海外中餐厅虽然只有 800 坛,但金额高达 176000,单价最高,体现了非遗酿醋在海外市场的高溢价。点击 ✏️ 按钮弹出"修改订单"弹窗,可调整数量和金额,数量行使用箭头符号"→"直观展示修改前后的对比。


七、客评页:口碑展示与标签体系

客评页是应用中最具"人情味"的页面,展示了 12 条用户好评。

客评模块 - 客评口碑列表

页面顶部是标题行"⭐ 客评口碑"和统计摘要"12条好评 · 平均4.9"。下方 12 条评价卡片采用"左头像右信息"的横向布局:最左侧是用户 emoji 头像,中间是用户名称和日期,右侧是星级评分(通过 String.repeat 一行代码生成对应数量的星星)和评价标签(通过动态颜色函数按标签语义分档着色)。评价内容正文在卡片下方,使用次要色文字和 16px 行高。

12 条评价每条都有独特的标签:“一勺定味”、“回头翻倍”、“下酒绝配”、“每晚好方”、“醇酸地道”、“代饮佳品”、“镇店之味”、“清爽不抢”、“客人点名”、“供货稳定”、“开坛名场面”、“三十年老客”,这些标签既概括了评价核心,又形成了醋坊的口碑关键词云。

评价内容充满生活气息,如"五年陈酸香醇厚,糖醋菜一勺定味"、“蘸料台换了这款,客人回头率翻倍”、“老醋花生按方子做,下酒绝配”、“酸汤面就认这口老陈醋的醇”、“打了三十年散醋,如今孙子接着打”,让数据看起来不是冷冰冰的测试桩,而是真实食客的心声。卡片背景使用白色与浅米色奇偶交替,整体视觉温馨而雅致。


八、色彩主题系统:深棕赭红的酿醋视觉语言

8.1 色彩接口定义

任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 VinegarPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约:

interface VinegarPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  cardAlt: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  line: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  brew: string;
  mashWhite: string;
}
  • primary:主色调,深棕 #5A3A28,用于按钮、强调文字、激活状态、醋品页货架背景等高优先级视觉元素,是陈醋的标志性色彩。
  • primaryDark:主色深色变体 #2E1E14,作为顶部头部背景和醋品页柱状图卡片背景,营造深醋瓮的沉稳感。
  • accent:强调色,赭红 #8E3B2E,与深棕形成暖调对比,用于醋坛、熏醅等元素。
  • bg:页面整体背景色 #F6F1EB,米白暖调,营造醋坊作坊的温润质感。
  • brew:酿色 #5A3A28,专用于醋品页货架交替背景和头部装饰,语义上强调"酿"的质感。
  • mashWhite:醪白 #FAF6F0,用于装饰性背景,营造出醪糟发酵的柔和质感。

醋坊醋品模块真机运行效果 - 深棕色货架背景与金棕色价格的视觉呈现

8.2 色彩实例化

const COLORS: VinegarPalette = {
  primary: '#5A3A28',
  primaryLight: '#B08A6E',
  primaryDark: '#2E1E14',
  accent: '#8E3B2E',
  accentLight: '#C98A7E',
  bg: '#F6F1EB',
  cardBg: '#FFFFFF',
  cardAlt: '#EDE3D8',
  textPrimary: '#33241A',
  textSecondary: '#7C6A5C',
  textHint: '#A9998A',
  border: '#E0D3C4',
  line: '#EBE1D4',
  success: '#6E8F5A',
  warning: '#B0893F',
  danger: '#8E3B2E',
  white: '#FFFFFF',
  brew: '#5A3A28',
  mashWhite: '#FAF6F0'
};

主色 #5A3A28 是一种醇厚的深棕色,既契合"酿醋"主题(陈醋的标志性色彩),又具有传统、沉稳的气质。强调色 #8E3B2E 赭红与深棕形成暖调对比,呼应熏醅、红曲米醋的色彩。warning 金棕 #B0893F 用于醋品页价格和柱状图,营造出"金标陈醋"的高级感。背景色 #F6F1EB 米白暖调与主色同暖色系,营造出醋坊作坊的温润氛围。这种将所有色彩集中管理的做法保证了全局一致性,支持主题切换,降低了维护成本。

醋坊醋坛模块真机运行效果 - 米白背景与浅米色交替卡片的层次效果


九、类型定义:构建严谨的数据契约

应用通过一组接口定义,为每一类业务对象建立了明确的数据契约。

9.1 醋品、醋坛、醋方元数据

interface VinegarItem {
  name: string; type: string; age: number; price: number; emoji: string;
}
interface JarItem {
  name: string; material: string; level: number; place: string; emoji: string;
}
interface RecipeItem {
  name: string; dish: string; usage: string; origin: string; emoji: string;
}

VinegarItem 描述醋品的名称、醋型、陈酿年数、价格和图标。JarItem 描述醋坛的名称、材质、窑品级别、产地和图标。RecipeItem 描述醋方的名称、主料、用法、流派源地和图标。

9.2 订单、客评与图表元数据

interface VinegarOrderItem {
  name: string; region: string; count: number; amount: number; emoji: string;
}
interface VinegarReviewItem {
  name: string; score: number; date: string; content: string; tag: string; emoji: string;
}
interface WeekVinegarMeta { day: string; value: number; }
interface TypeShareMeta { name: string; count: number; color: string; }
interface BrewHotMeta { name: string; heat: number; color: string; }
interface VinegarTopMeta { name: string; sold: number; color: string; }

四组图表元数据(WeekVinegarMeta/TypeShareMeta/BrewHotMeta/VinegarTopMeta)都自带 color 字段,实现了"数据即配色"的图表渲染模式,组件渲染时无需额外的颜色映射逻辑。


十、配置驱动:常量数据与可视化配置

10.1 Tab 导航配置

enum VinegarTab { VINEGAR = 0, JAR = 1, RECIPE = 2, ORDER = 3, REVIEW = 4 }

const TAB_LIST: TabMeta[] = [
  { key: 'vinegar', icon: '🍶', label: '醋品', color: '#5A3A28' },
  { key: 'jar', icon: '🏺', label: '醋坛', color: '#8E3B2E' },
  { key: 'recipe', icon: '📜', label: '醋方', color: '#B0893F' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#5A3A28' }
];

枚举提供类型安全的 Tab 标识,TAB_LIST 为每个 Tab 配置独立的图标、标签和主题色:醋品用深棕、醋坛用赭红、醋方用金棕、订单用蓝色、客评用深棕,"一 Tab 一色"让每个页面有鲜明的视觉身份。

10.2 周销量、醋型构成、工艺热度与 TOP 榜

const WEEK_SOLD = [
  { day: '周一', value: 20 }, { day: '周二', value: 26 },
  { day: '周三', value: 34 }, { day: '周四', value: 42 },
  { day: '周五', value: 55 }, { day: '周六', value: 70 }, { day: '周日', value: 62 }
];
const TYPE_SHARE = [
  { name: '陈醋', count: 5, color: '#5A3A28' },
  { name: '米醋', count: 4, color: '#8E3B2E' },
  { name: '果醋', count: 3, color: '#B0893F' }
];
const BREW_HOT = [
  { name: '蒸料', heat: 95 }, { name: '糖化', heat: 94 }, { name: '发酵', heat: 96 },
  { name: '熏醅', heat: 92 }, { name: '淋醋', heat: 90 }, { name: '陈酿', heat: 93 }
];
const VINEGAR_TOP = [
  { name: '三年陈醋', sold: 96 }, { name: '五年陈醋', sold: 93 },
  { name: '糯米香醋', sold: 90 }, { name: '苹果醋', sold: 87 },
  { name: '桂花醋', sold: 84 }, { name: '梅子醋', sold: 81 }
];

BREW_HOT 的六门工艺恰好对应酿醋制作的完整流程(蒸料→糖化→发酵→熏醅→淋醋→陈酿),其中发酵以 96 的热度居首,呈现出"发酵为核心工序"的合理分布。VINEGAR_COL 常量 [0…11] 用于顶部头部的装饰带渲染,按索引模 3 分别渲染:酿色圆角矩形 10×14(模0)、赭红方块 8×8 圆角1(模1)、竹绿叶圆 8×8(模2),形成一条高低错落的装饰线,呼应醋坛的"直方"视觉元素。


十一、数据模型:可观察的醋品实体

@Observed
export class VinegarData {
  vinegars: VinegarItem[] = [ /* 12 款醋品 */ ];
  jars: JarItem[] = [ /* 12 款醋坛 */ ];
  recipes: RecipeItem[] = [ /* 12 款醋方 */ ];
  orders: VinegarOrderItem[] = [ /* 12 条订单 */ ];
  reviews: VinegarReviewItem[] = [ /* 12 条客评 */ ];
}

VinegarData 是核心数据模型,以五个数组承载醋坊全部业务信息。@Observed 装饰器使得该类实例在被 @State 或 @ObjectLink 引用时,属性变化能自动触发 UI 刷新。每个数组精确包含 12 条数据,"五五对称、各十二条"的设计保证了每个页面都有足够的内容密度。所有数据在类定义时直接初始化,"字段即默认值"的写法避免了构造函数中重复赋值。


十二、模拟数据:构建真实的醋坊数据环境

12.1 醋品模拟数据

12 款醋品覆盖了从 16 元(白米醋)到 168 元(十年陈醋)的完整价格梯队,醋型涵盖陈醋、香醋、米醋、果醋、养生醋、调味醋六大类,陈酿从 1 年到 10 年不等。三年陈醋¥38、五年陈醋¥68、十年陈醋¥168、糯米香醋¥28、红曲米醋¥22、白米醋¥16、苹果醋¥32、梅子醋¥46、桂花醋¥36、枸杞醋¥52、姜汁醋¥26、蒜蓉醋¥24,emoji 图标与醋品名称呼应(三年陈醋用🍶、十年陈醋用⚱️、糯米香醋用🍚、苹果醋用🍎等)。

12.2 醋坛与醋方模拟数据

醋坛材质覆盖粗陶、紫砂、黑釉瓷、青瓷、麻陶、黄泥陶、柿釉瓷、酱釉瓷、土釉陶、绞胎陶、冰裂纹陶、陈年泥陶十二种,窑品级别从 88 到 97 不等,老陈泥瓮以 97 级居首,产地从宜兴到清徐几乎囊括中国古代制陶全部名窑口。醋方覆盖糖醋排骨、酸辣土豆丝、老醋花生等十二道经典用醋菜肴,用法涵盖炒糖色、起锅淋、凉拌等十二种,流派从无锡到江南呈现出"醋通天下味"的格局。

12.3 订单与客评模拟数据

订单覆盖 12 类客户,商超供货商以 5200 坛和 188000 元双居首,海外中餐厅单价最高。客评 12 条各有独特标签(一勺定味、回头翻倍、下酒绝配、每晚好方、醇酸地道、代饮佳品、镇店之味、清爽不抢、客人点名、供货稳定、开坛名场面、三十年老客),评价内容充满生活气息,如"五年陈酸香醇厚,糖醋菜一勺定味"、“打了三十年散醋,如今孙子接着打”。


十三、动态颜色函数:数据驱动的视觉映射

应用通过五个动态颜色函数将业务数据映射为视觉色彩,实现"数据即配色"。

function getVinegarColor(name: string): string {
  if (name === '三年陈醋' || name === '五年陈醋' || name === '十年陈醋') return '#5A3A28';
  else if (name === '糯米香醋' || name === '红曲米醋' || name === '白米醋') return '#8E3B2E';
  else if (name === '苹果醋' || name === '梅子醋' || name === '桂花醋') return '#B0893F';
  return '#4E7BA8';
}
function getJarColor(level: number): string {
  if (level >= 95) return '#5A3A28';
  else if (level >= 91) return '#8E3B2E';
  return '#B0893F';
}
function getPriceColor(price: number): string {
  if (price >= 100) return '#5A3A28';
  else if (price >= 40) return '#8E3B2E';
  else if (price >= 25) return '#B0893F';
  return '#4E7BA8';
}
function getOrderColor(amount: number): string {
  if (amount >= 150000) return '#5A3A28';
  else if (amount >= 110000) return '#8E3B2E';
  else if (amount >= 80000) return '#B0893F';
  return '#4E7BA8';
}
function getReviewTagColor(tag: string): string { /* 按标签语义分四档着色 */ }
  • getVinegarColor:按醋品名称分四组色彩(三年/五年/十年陈醋类深棕、糯米香醋/红曲米醋/白米醋类赭红、苹果醋/梅子醋/桂花醋类金棕、其他蓝色)。
  • getJarColor:按窑品级别分三档(95以上深棕、91-94赭红、其余金棕),“以色定级”。
  • getPriceColor:按价格分四档(100以上深棕、40-99赭红、25-39金棕、25以下蓝色)。
  • getOrderColor:按订单金额分四档(15万以上深棕、11-15万赭红、8-11万金棕、8万以下蓝色)。
  • getReviewTagColor:按评价标签语义分四色系。

值得注意的是,醋品页的图标方块背景色调用 getVinegarColor,而卡片整体背景则固定使用 primary 和 brew 深棕色(奇偶交替),形成了"图标多彩、货架统一深棕"的视觉层次。这种"深棕色货架 + 动态颜色图标"的组合是本应用最具辨识度的视觉设计特征。


十四、入口组件:应用的骨架与导航中枢

14.1 组件声明与状态

@Entry
@Component
struct VinegarApp {
  @State curTab: number = 0;
  @State data: VinegarData = new VinegarData();
  @State showAddVinegar: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteJar: boolean = false;
  @State showDetailRecipe: boolean = false;
  @State delJarName: string = '';
  @State detailRecipeName: string = '';
  @State jarFloat: boolean = false;
  @State dropPulse: boolean = false;
}

10 个 @State 状态分四类:导航状态(curTab)、数据状态(data)、弹窗控制状态(4个布尔+2个参数字符串)、交互动效状态(jarFloat、dropPulse)。

14.2 顶部头部构建

顶部头部采用 primaryDark #2E1E14 极深棕色背景。第一层标题栏左侧是标题"醋坊"和副标题"Vinegar · 千日陈酿 一口醇酸",右侧是动效区:醋坛🏺点击触发上下浮动动画(translate Y 从 3 到 -10,duration 700ms)、水滴💧点击触发缩放脉冲动画(scale 从 0.85 到 1.25,duration 600ms)、麦穗🌾静态装饰。第二层是"装饰带",通过 ForEach 遍历 VINEGAR_COL,按模 3 渲染酿色圆角矩形 10×14、赭红方块 8×8 圆角1、竹绿叶圆 8×8,形成一条高低错落的装饰线,呼应醋坛的"直方"视觉元素。第三层是标语分割线"💧 蒸料熏醅 · 夏伏晒冬捞冰 🏺"。

14.3 底部 Tab 导航

底部导航顶部有一条 3px 高的深棕色装饰条,与极深棕色头部形成上下呼应。每个 Tab 激活态有三重视觉反馈:标签文字主题色加粗、背景填充主题色 8% 透明度、主题色实线边框,圆角 2(醋坛直方风格)。非激活态用灰色文字、透明背景、无边框。导航栏高度 60。

14.4 条件渲染弹窗

应用采用条件渲染(if-else)实现四个弹窗:新酿醋品(AddVinegarModal)、修改订单(EditVinegarOrderModal)、撤下醋坛(DeleteJarModal)、醋方详情(DetailRecipeModal)。弹窗直接叠加在根 Column 末尾,通过布尔状态控制显隐,每个弹窗内部是全屏半透明遮罩(#66000000)+ 居中白色卡片。弹窗卡片统一使用圆角 6,信息行圆角 4。


十五、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 VinegarPalette 接口 + COLORS 常量 集中定义 20 种语义色,含 brew/mashWhite 装饰色 保证视觉一致性,支持主题切换
数据建模 @Observed 装饰的 VinegarData 类 5 个数组各 12 条数据,覆盖酿醋全业务维度 数据与 UI 双向联动,响应式更新
配置驱动 常量数组 + 元数据接口 WEEK_SOLD/TYPE_SHARE/BREW_HOT/VINEGAR_TOP 自带 color 数据与表现解耦,扩展零成本
动态颜色 5 个全局映射函数 按名称/级别/价格/金额/标签分档着色,醋品页图标多彩货架统一深棕 数据即配色,信息密度翻倍
深棕色货架 primary/brew 交替背景 醋品页整个列表深棕色背景,白色文字,金棕色价格 强烈视觉识别,酿醋氛围浓
状态管理 @State + @ObjectLink 10 个状态分四类(导航/数据/弹窗/动效) 细粒度状态控制,自动触发渲染
组件化 @Component + @Builder 5 个页面组件 + 4 个弹窗组件 + VinegarTag 通用组件 高内聚低耦合,代码复用率高
模态交互 条件渲染弹窗 + 全屏遮罩 4 个弹窗 if-else 叠加,#66000000 半透明遮罩 灵活可控,完全自定义布局
列表渲染 ForEach + 奇偶交替背景 12 条数据 × 5 页面,cardBg/cardAlt 或 primary/brew 交替 数据驱动渲染,长列表可读性强
数据可视化 柱状图 + 横向条形图 height 绑定数据、layoutWeight 比例法、深棕卡片金棕柱 多维度呈现,直观易读
星级渲染 String.repeat 技巧 ‘⭐’.repeat(score) 一行代码生成星级 简洁高效,无需循环
防御编程 字段默认值 + 类型安全 所有数据字段声明即初始化,枚举替代魔法数字 健壮性强,无运行时异常
布局系统 Column/Row + layoutWeight 弹性权重分配、条件渲染弹窗、Scroll 滚动容器 自适应布局,层次分明
交互动效 translate + scale + animation 醋坛浮动 700ms、水滴缩放脉冲 600ms、EaseOut 曲线 静态头部灵动化,趣味性强
视觉层次 装饰带/圆角2/深棕货架 VINEGAR_COL 模3渲染高低错落装饰、全局极小圆角、醋品页深棕货架 精致细腻,酿醋氛围感强

完整代码

interface VinegarPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  cardAlt: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  line: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  brew: string;
  mashWhite: string;
}

const COLORS: VinegarPalette = {
  primary: '#5A3A28',
  primaryLight: '#B08A6E',
  primaryDark: '#2E1E14',
  accent: '#8E3B2E',
  accentLight: '#C98A7E',
  bg: '#F6F1EB',
  cardBg: '#FFFFFF',
  cardAlt: '#EDE3D8',
  textPrimary: '#33241A',
  textSecondary: '#7C6A5C',
  textHint: '#A9998A',
  border: '#E0D3C4',
  line: '#EBE1D4',
  success: '#6E8F5A',
  warning: '#B0893F',
  danger: '#8E3B2E',
  white: '#FFFFFF',
  brew: '#5A3A28',
  mashWhite: '#FAF6F0'
};

enum VinegarTab {
  VINEGAR = 0,
  JAR = 1,
  RECIPE = 2,
  ORDER = 3,
  REVIEW = 4
}

interface TabMeta {
  key: string;
  icon: string;
  label: string;
  color: string;
}

const TAB_LIST: TabMeta[] = [
  { key: 'vinegar', icon: '🍶', label: '醋品', color: '#5A3A28' },
  { key: 'jar', icon: '🏺', label: '醋坛', color: '#8E3B2E' },
  { key: 'recipe', icon: '📜', label: '醋方', color: '#B0893F' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#5A3A28' }
];

const VINEGAR_COL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

interface VinegarItem {
  name: string;
  type: string;
  age: number;
  price: number;
  emoji: string;
}

interface JarItem {
  name: string;
  material: string;
  level: number;
  place: string;
  emoji: string;
}

interface RecipeItem {
  name: string;
  dish: string;
  usage: string;
  origin: string;
  emoji: string;
}

interface VinegarOrderItem {
  name: string;
  region: string;
  count: number;
  amount: number;
  emoji: string;
}

interface VinegarReviewItem {
  name: string;
  score: number;
  date: string;
  content: string;
  tag: string;
  emoji: string;
}

interface WeekVinegarMeta {
  day: string;
  value: number;
}

interface TypeShareMeta {
  name: string;
  count: number;
  color: string;
}

interface BrewHotMeta {
  name: string;
  heat: number;
  color: string;
}

interface VinegarTopMeta {
  name: string;
  sold: number;
  color: string;
}

const WEEK_SOLD: WeekVinegarMeta[] = [
  { day: '周一', value: 20 },
  { day: '周二', value: 26 },
  { day: '周三', value: 34 },
  { day: '周四', value: 42 },
  { day: '周五', value: 55 },
  { day: '周六', value: 70 },
  { day: '周日', value: 62 }
];

const TYPE_SHARE: TypeShareMeta[] = [
  { name: '陈醋', count: 5, color: '#5A3A28' },
  { name: '米醋', count: 4, color: '#8E3B2E' },
  { name: '果醋', count: 3, color: '#B0893F' }
];

const BREW_HOT: BrewHotMeta[] = [
  { name: '蒸料', heat: 95, color: '#5A3A28' },
  { name: '糖化', heat: 94, color: '#8E3B2E' },
  { name: '发酵', heat: 96, color: '#B0893F' },
  { name: '熏醅', heat: 92, color: '#4E7BA8' },
  { name: '淋醋', heat: 90, color: '#7C8AA0' },
  { name: '陈酿', heat: 93, color: '#6E8F5A' }
];

const VINEGAR_TOP: VinegarTopMeta[] = [
  { name: '三年陈醋', sold: 96, color: '#5A3A28' },
  { name: '五年陈醋', sold: 93, color: '#8E3B2E' },
  { name: '糯米香醋', sold: 90, color: '#B0893F' },
  { name: '苹果醋', sold: 87, color: '#7C8AA0' },
  { name: '桂花醋', sold: 84, color: '#4E7BA8' },
  { name: '梅子醋', sold: 81, color: '#6E8F5A' }
];

@Observed
export class VinegarData {
  vinegars: VinegarItem[] = [
    { name: '三年陈醋', type: '陈醋', age: 3, price: 38, emoji: '🍶' },
    { name: '五年陈醋', type: '陈醋', age: 5, price: 68, emoji: '🍶' },
    { name: '十年陈醋', type: '陈醋', age: 10, price: 168, emoji: '⚱️' },
    { name: '糯米香醋', type: '香醋', age: 2, price: 28, emoji: '🍚' },
    { name: '红曲米醋', type: '米醋', age: 1, price: 22, emoji: '🔴' },
    { name: '白米醋', type: '米醋', age: 1, price: 16, emoji: '🤍' },
    { name: '苹果醋', type: '果醋', age: 1, price: 32, emoji: '🍎' },
    { name: '梅子醋', type: '果醋', age: 2, price: 46, emoji: '🫐' },
    { name: '桂花醋', type: '果醋', age: 1, price: 36, emoji: '🍂' },
    { name: '枸杞醋', type: '养生醋', age: 2, price: 52, emoji: '🔴' },
    { name: '姜汁醋', type: '养生醋', age: 1, price: 26, emoji: '🫚' },
    { name: '蒜蓉醋', type: '调味醋', age: 1, price: 24, emoji: '🧄' }
  ];

  jars: JarItem[] = [
    { name: '粗陶大坛', material: '粗陶', level: 90, place: '宜兴', emoji: '🏺' },
    { name: '紫砂醋坛', material: '紫砂', level: 96, place: '宜兴', emoji: '🫖' },
    { name: '黑釉瓮', material: '黑釉瓷', level: 93, place: '磁州', emoji: '⚫' },
    { name: '青瓷醋瓮', material: '青瓷', level: 94, place: '龙泉', emoji: '🟢' },
    { name: '榆次老坛', material: '麻陶', level: 92, place: '榆次', emoji: '🪵' },
    { name: '镇江泥坛', material: '黄泥陶', level: 91, place: '镇江', emoji: '🟤' },
    { name: '柿釉小坛', material: '柿釉瓷', level: 89, place: '淄博', emoji: '🟠' },
    { name: '酱釉瓮', material: '酱釉瓷', level: 92, place: '佛山', emoji: '🟫' },
    { name: '土釉大缸', material: '土釉陶', level: 88, place: '邯郸', emoji: '🛢️' },
    { name: '绞胎坛', material: '绞胎陶', level: 95, place: '焦作', emoji: '🌀' },
    { name: '冰裂坛', material: '冰裂纹陶', level: 94, place: '景德镇', emoji: '❄️' },
    { name: '老陈泥瓮', material: '陈年泥陶', level: 97, place: '清徐', emoji: '🏛️' }
  ];

  recipes: RecipeItem[] = [
    { name: '糖醋排骨', dish: '排骨', usage: '炒糖色', origin: '无锡', emoji: '🍖' },
    { name: '酸辣土豆丝', dish: '土豆', usage: '起锅淋', origin: '四川', emoji: '🥔' },
    { name: '老醋花生', dish: '花生', usage: '凉拌', origin: '山西', emoji: '🥜' },
    { name: '醋熘白菜', dish: '白菜', usage: '炝锅', origin: '北京', emoji: '🥬' },
    { name: '西湖醋鱼', dish: '草鱼', usage: '浇汁', origin: '杭州', emoji: '🐟' },
    { name: '醋泡黑豆', dish: '黑豆', usage: '浸泡', origin: '民间', emoji: '⚫' },
    { name: '酸汤肥牛', dish: '肥牛', usage: '调汤', origin: '贵州', emoji: '🍲' },
    { name: '醋蘸饺子', dish: '饺子', usage: '蘸食', origin: '北方', emoji: '🥟' },
    { name: '姜醋猪蹄', dish: '猪蹄', usage: '炖煮', origin: '广东', emoji: '🐖' },
    { name: '醋腌萝卜', dish: '萝卜', usage: '腌制', origin: '潮汕', emoji: '🥕' },
    { name: '果醋沙拉', dish: '果蔬', usage: '拌酱', origin: '西式', emoji: '🥗' },
    { name: '醋熏卤味', dish: '卤味', usage: '熏制', origin: '江南', emoji: '🍳' }
  ];

  orders: VinegarOrderItem[] = [
    { name: '老字号饭店', region: '华北', count: 1400, amount: 118000, emoji: '🍽️' },
    { name: '火锅连锁', region: '西南', count: 2600, amount: 156000, emoji: '🌶️' },
    { name: '饺子馆联盟', region: '东北', count: 1800, amount: 96000, emoji: '🥟' },
    { name: '商超供货商', region: '华东', count: 5200, amount: 188000, emoji: '🛒' },
    { name: '拌菜连锁', region: '华中', count: 940, amount: 78000, emoji: '🥗' },
    { name: '面馆集团', region: '西北', count: 2100, amount: 112000, emoji: '🍜' },
    { name: '养生会所', region: '华南', count: 620, amount: 88000, emoji: '💆' },
    { name: '月子中心', region: '华东', count: 480, amount: 66000, emoji: '👶' },
    { name: '学校食堂', region: '华北', count: 3600, amount: 92000, emoji: '🏫' },
    { name: '电商自营店', region: '华东', count: 4400, amount: 168000, emoji: '📱' },
    { name: '机场免税店', region: '华南', count: 360, amount: 142000, emoji: '✈️' },
    { name: '海外中餐厅', region: '海外', count: 800, amount: 176000, emoji: '🌏' }
  ];

  reviews: VinegarReviewItem[] = [
    { name: '饭店大厨', score: 5, date: '09-04', content: '五年陈酸香醇厚,糖醋菜一勺定味。', tag: '一勺定味', emoji: '🍽️' },
    { name: '火锅店主', score: 5, date: '09-03', content: '蘸料台换了这款,客人回头率翻倍。', tag: '回头翻倍', emoji: '🌶️' },
    { name: '家庭主妇', score: 5, date: '09-02', content: '老醋花生按方子做,下酒绝配。', tag: '下酒绝配', emoji: '🥜' },
    { name: '养生达人', score: 4, date: '09-01', content: '醋泡黑豆每晚吃,就是瓶口再大点好。', tag: '每晚好方', emoji: '⚫' },
    { name: '面馆老板', score: 5, date: '08-31', content: '酸汤面就认这口老陈醋的醇。', tag: '醇酸地道', emoji: '🍜' },
    { name: '宝妈用户', score: 5, date: '08-30', content: '果醋孩子当饮料喝,不再闹着要汽水。', tag: '代饮佳品', emoji: '🍎' },
    { name: '海外厨子', score: 5, date: '08-29', content: '中餐镇店就靠它,玻璃坛一路无损。', tag: '镇店之味', emoji: '🌏' },
    { name: '拌菜师傅', score: 4, date: '08-28', content: '米醋清爽不抢味,盼出大桶装。', tag: '清爽不抢', emoji: '🥗' },
    { name: '饺子馆掌柜', score: 5, date: '08-27', content: '客人问了几次醋名,干脆贴了牌子。', tag: '客人点名', emoji: '🥟' },
    { name: '采购主管', score: 5, date: '08-26', content: '商超供货稳定,旺季从没断过货。', tag: '供货稳定', emoji: '🛒' },
    { name: '美食博主', score: 5, date: '08-25', content: '十年陈开了坛拍视频,粉丝全馋哭了。', tag: '开坛名场面', emoji: '🎥' },
    { name: '老街坊', score: 5, date: '08-24', content: '打了三十年散醋,如今孙子接着打。', tag: '三十年老客', emoji: '🍶' }
  ];
}

function getVinegarColor(name: string): string {
  if (name === '三年陈醋' || name === '五年陈醋' || name === '十年陈醋') {
    return '#5A3A28';
  } else if (name === '糯米香醋' || name === '红曲米醋' || name === '白米醋') {
    return '#8E3B2E';
  } else if (name === '苹果醋' || name === '梅子醋' || name === '桂花醋') {
    return '#B0893F';
  }
  return '#4E7BA8';
}

function getJarColor(level: number): string {
  if (level >= 95) {
    return '#5A3A28';
  } else if (level >= 91) {
    return '#8E3B2E';
  }
  return '#B0893F';
}

function getPriceColor(price: number): string {
  if (price >= 100) {
    return '#5A3A28';
  } else if (price >= 40) {
    return '#8E3B2E';
  } else if (price >= 25) {
    return '#B0893F';
  }
  return '#4E7BA8';
}

function getOrderColor(amount: number): string {
  if (amount >= 150000) {
    return '#5A3A28';
  } else if (amount >= 110000) {
    return '#8E3B2E';
  } else if (amount >= 80000) {
    return '#B0893F';
  }
  return '#4E7BA8';
}

function getReviewTagColor(tag: string): string {
  if (tag === '一勺定味' || tag === '醇酸地道' || tag === '镇店之味' || tag === '三十年老客') {
    return '#5A3A28';
  } else if (tag === '回头翻倍' || tag === '下酒绝配' || tag === '开坛名场面') {
    return '#8E3B2E';
  } else if (tag === '每晚好方' || tag === '代饮佳品' || tag === '清爽不抢' || tag === '供货稳定' || tag === '客人点名') {
    return '#B0893F';
  }
  return '#4E7BA8';
}

@Entry
@Component
struct VinegarApp {
  @State curTab: number = 0;
  @State data: VinegarData = new VinegarData();
  @State showAddVinegar: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteJar: boolean = false;
  @State showDetailRecipe: boolean = false;
  @State delJarName: string = '';
  @State detailRecipeName: string = '';
  @State jarFloat: boolean = false;
  @State dropPulse: boolean = false;

  @Builder modalOverlay(onClose: () => void) {
    Column() {
      Text('')
        .width(0)
        .height(0)
        .opacity(0)
      Button('')
        .width(1)
        .height(1)
        .opacity(0)
        .onClick(() => {
          onClose();
        })
    }
    .width(1)
    .height(1)
  }

  build() {
    Column() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('🍶 醋坊')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.white)
              Text('Vinegar · 千日陈酿 一口醇酸')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🏺')
                .fontSize(18)
                .onClick(() => {
                  this.jarFloat = !this.jarFloat;
                })
                .translate({ x: 0, y: this.jarFloat ? -10 : 3 })
                .animation({ duration: 700, curve: Curve.EaseOut })
              Text('💧')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.dropPulse = !this.dropPulse;
                })
                .scale({ x: this.dropPulse ? 1.25 : 0.85, y: this.dropPulse ? 1.25 : 0.85 })
                .animation({ duration: 600, curve: Curve.EaseOut })
              Text('🌾')
                .fontSize(14)
                .margin({ left: 6 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(COLORS.primaryLight + '2E')
            .borderRadius(4)
            .border({ width: 1, color: COLORS.primaryLight + '80' })
          }
          .width('100%')

          Row() {
            ForEach(VINEGAR_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(10)
                    .height(14)
                    .backgroundColor(COLORS.brew)
                    .borderRadius(5)
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(1)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.success)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'vinegar' + r)
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 12 })

          Row() {
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primaryLight + '66')
            Text('💧 蒸料熏醅 · 夏伏晒冬捞冰 🏺')
              .fontSize(9)
              .fontColor(COLORS.primaryLight)
              .margin({ left: 8, right: 8 })
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primaryLight + '66')
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(COLORS.primaryDark)

        if (this.curTab === VinegarTab.VINEGAR) {
          VinegarContent({ data: this.data, onAdd: () => {
            this.showAddVinegar = true;
          } })
        } else if (this.curTab === VinegarTab.JAR) {
          JarContent({ data: this.data, onDel: (n: string) => {
            this.delJarName = n;
            this.showDeleteJar = true;
          } })
        } else if (this.curTab === VinegarTab.RECIPE) {
          RecipeContent({ data: this.data, onDetail: (n: string) => {
            this.detailRecipeName = n;
            this.showDetailRecipe = true;
          } })
        } else if (this.curTab === VinegarTab.ORDER) {
          VinegarOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          VinegarReviewContent({ data: this.data })
        }
      }
      .width('100%')
      .height('100%')

      Column() {
        Column()
          .width('100%')
          .height(3)
          .backgroundColor(COLORS.primary)
        Row() {
          ForEach(TAB_LIST, (t: TabMeta) => {
            Column() {
              Text(t.icon)
                .fontSize(19)
              Text(t.label)
                .fontSize(10)
                .fontColor(this.curTab === TAB_LIST.indexOf(t) ? t.color : COLORS.textHint)
                .fontWeight(this.curTab === TAB_LIST.indexOf(t) ? FontWeight.Bold : FontWeight.Normal)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .justifyContent(FlexAlign.Center)
            .padding({ top: 7, bottom: 7 })
            .backgroundColor(this.curTab === TAB_LIST.indexOf(t) ? t.color + '14' : '#00000000')
            .borderRadius(2)
            .border(this.curTab === TAB_LIST.indexOf(t) ? { width: 1, color: t.color } : { width: 0 })
            .onClick(() => {
              this.curTab = TAB_LIST.indexOf(t);
            })
          }, (t: TabMeta) => t.key)
        }
        .width('100%')
        .height(60)
        .padding({ left: 8, right: 8 })
        .backgroundColor(COLORS.cardBg)
      }
      .width('100%')

      if (this.showAddVinegar) {
        AddVinegarModal({
          onClose: () => {
            this.showAddVinegar = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditVinegarOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteJar) {
        DeleteJarModal({
          title: this.delJarName, onClose: () => {
            this.showDeleteJar = false;
          }
        })
      }
      if (this.showDetailRecipe) {
        DetailRecipeModal({
          name: this.detailRecipeName, onClose: () => {
            this.showDetailRecipe = false;
          }
        })
      }
    }
  }
}

@Component
struct VinegarTag {
  @Prop text: string;
  @Prop color: string;

  build() {
    Text(this.text)
      .fontSize(9)
      .fontColor(this.color)
      .padding({ left: 7, right: 7, top: 2, bottom: 2 })
      .backgroundColor(this.color + '14')
      .borderRadius(2)
      .border({ width: 1, color: this.color + '40' })
  }
}

@Component
struct VinegarContent {
  @ObjectLink data: VinegarData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🍶 大酿坊 · 每日出货')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.white)
            Text('周六70坛')
              .fontSize(9)
              .fontColor(COLORS.primaryLight)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekVinegarMeta) => {
              Column() {
                Text(w.value + '')
                  .fontSize(8)
                  .fontColor(COLORS.primaryLight)
                Column()
                  .width(18)
                  .height(w.value)
                  .backgroundColor(COLORS.warning)
                  .borderRadius(2)
                  .margin({ top: 4 })
                Text(w.day)
                  .fontSize(9)
                  .fontColor(COLORS.primaryLight)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (w: WeekVinegarMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(16)
        .backgroundColor(COLORS.primaryDark)
        .borderRadius(4)
        .border({ width: 2, color: COLORS.warning + '66' })

        Column() {
          Row() {
            Text('🍶 醋品货架')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击➕添醋')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
            Text('➕')
              .fontSize(14)
              .margin({ left: 8 })
              .onClick(() => {
                this.onAdd();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.vinegars, (p: VinegarItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(20)
                  .textAlign(TextAlign.Center)
              }
              .width(48)
              .height(48)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getVinegarColor(p.name))
              .borderRadius(4)

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.white)
                  Text('¥' + p.price)
                    .fontSize(10)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.warning)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  VinegarTag({ text: p.type, color: COLORS.warning })
                  VinegarTag({ text: '陈' + p.age + '年', color: COLORS.primaryLight })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.primary : COLORS.brew)
            .borderRadius(4)
            .border({ width: 1, color: COLORS.primaryLight + '33' })
            .margin({ top: 8 })
          }, (p: VinegarItem, i: number) => 'vc' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct JarContent {
  @ObjectLink data: VinegarData;
  onDel: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🏆 醋品销量 TOP6')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('三年陈居首')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(VINEGAR_TOP, (t: VinegarTopMeta) => {
            Row() {
              Text(t.name)
                .fontSize(10)
                .width(64)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(t.sold)
                  .height(12)
                  .backgroundColor(t.color)
                  .borderRadius(2)
                Row()
                  .layoutWeight(100 - t.sold)
                  .height(12)
                  .backgroundColor(COLORS.line)
                  .borderRadius(2)
              }
              .layoutWeight(1)
              .height(12)
              .margin({ left: 6 })
              Text(t.sold + '')
                .fontSize(10)
                .width(28)
                .textAlign(TextAlign.End)
                .fontColor(t.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (t: VinegarTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(4)
        .border({ width: 1, color: COLORS.accent + '55' })

        Column() {
          Row() {
            Text('🏺 醋坛墙')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击🗑️撤坛')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.jars, (p: JarItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getJarColor(p.level) + '14')
              .borderRadius(4)
              .border({ width: 1, color: getJarColor(p.level) + '66' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.material)
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                  Text('🗑️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onDel(p.name);
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text('窑品' + p.level + '级')
                    .fontSize(9)
                    .fontColor(getJarColor(p.level))
                  Text(p.place + ' · 窑口')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(4)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: JarItem, i: number) => 'jr' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct RecipeContent {
  @ObjectLink data: VinegarData;
  onDetail: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🫙 醋型构成')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('陈醋为骨')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(TYPE_SHARE, (s: TypeShareMeta) => {
            Row() {
              Text(s.name)
                .fontSize(10)
                .width(40)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(s.count)
                  .height(14)
                  .backgroundColor(s.color)
                  .borderRadius(2)
                Row()
                  .layoutWeight(12 - s.count)
                  .height(14)
                  .backgroundColor(COLORS.line)
                  .borderRadius(2)
              }
              .layoutWeight(1)
              .height(14)
              .margin({ left: 6 })
              Text(s.count + ' 款')
                .fontSize(10)
                .width(36)
                .textAlign(TextAlign.End)
                .fontColor(s.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (s: TypeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(4)
        .border({ width: 1, color: COLORS.accent + '55' })

        Column() {
          Row() {
            Text('📜 醋方册')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击👁️看方')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.recipes, (p: RecipeItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(COLORS.accent + '14')
              .borderRadius(4)
              .border({ width: 1, color: COLORS.accent + '66' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.dish)
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                  Text('👁️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onDetail(p.name);
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text(p.usage + ' · ' + p.origin + '味')
                    .fontSize(9)
                    .fontColor(COLORS.accent)
                  Text('醋方')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(4)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: RecipeItem, i: number) => 'rc' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct VinegarOrderContent {
  @ObjectLink data: VinegarData;
  onEdit: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🔥 酿醋工艺热度')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('发酵最旺')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(BREW_HOT, (h: BrewHotMeta) => {
            Row() {
              Text(h.name)
                .fontSize(10)
                .width(52)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(h.heat)
                  .height(10)
                  .backgroundColor(h.color)
                  .borderRadius(2)
                Row()
                  .layoutWeight(100 - h.heat)
                  .height(10)
                  .backgroundColor(COLORS.line)
                  .borderRadius(2)
              }
              .layoutWeight(1)
              .height(10)
              .margin({ left: 6 })
              Text(h.heat + '')
                .fontSize(10)
                .width(28)
                .textAlign(TextAlign.End)
                .fontColor(h.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (h: BrewHotMeta) => h.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(4)
        .border({ width: 1, color: COLORS.accent + '55' })

        Column() {
          Row() {
            Text('📦 醋品订单')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击✏️改单')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.orders, (o: VinegarOrderItem, i: number) => {
            Row() {
              Column() {
                Text(o.emoji)
                  .fontSize(16)
                  .textAlign(TextAlign.Center)
              }
              .width(40)
              .height(40)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getOrderColor(o.amount) + '14')
              .borderRadius(4)
              .border({ width: 1, color: getOrderColor(o.amount) + '66' })

              Column() {
                Row() {
                  Text(o.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('✏️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onEdit();
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text(o.region)
                    .fontSize(9)
                    .fontColor(getOrderColor(o.amount))
                  Text('· ' + o.count + ' 坛 · ¥' + o.amount)
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 6 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(4)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: VinegarOrderItem, i: number) => 'od' + o.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct VinegarReviewContent {
  @ObjectLink data: VinegarData;

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('⭐ 客评口碑')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('12条好评 · 平均4.9')
            .fontSize(9)
            .fontColor(COLORS.textHint)
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ bottom: 6 })
        ForEach(this.data.reviews, (r: VinegarReviewItem, i: number) => {
          Column() {
            Row() {
              Text(r.emoji)
                .fontSize(15)
              Column() {
                Text(r.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(r.date)
                  .fontSize(9)
                  .fontColor(COLORS.textHint)
                  .margin({ top: 1 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              .margin({ left: 8 })
              Text('⭐'.repeat(r.score))
                .fontSize(10)
                .fontColor(COLORS.warning)
              VinegarTag({ text: r.tag, color: getReviewTagColor(r.tag) })
            }
            .width('100%')
            Text(r.content)
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .lineHeight(16)
              .margin({ top: 6 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
          .borderRadius(4)
          .border({ width: 1, color: COLORS.line })
          .margin({ top: 8 })
        }, (r: VinegarReviewItem, i: number) => 'rv' + r.name + i)
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct AddVinegarModal {
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('🍶 新酿醋品')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Row() {
          Text('品名')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('桑葚果醋')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 14 })
        Row() {
          Text('工艺')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('果汁发酵 · 陈酿一年 · 紫檀色')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Row() {
          Text('定价')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥42')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(4)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('确认添醋')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.primary)
            .borderRadius(4)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(6)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditVinegarOrderModal {
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('✏️ 修改订单')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Row() {
          Text('客户')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('商超供货商')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 14 })
        Row() {
          Text('数量')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('5200 → 6400 坛')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Row() {
          Text('金额')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥236000')
            .fontSize(11)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(4)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('保存修改')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.primary)
            .borderRadius(4)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(6)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeleteJarModal {
  @Prop title: string;
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Text('🗑️ 撤下醋坛')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Text('确定撤下「' + this.title + '」吗?')
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
          .margin({ top: 10 })
        Text('撤下的醋坛将从坛墙移除。')
          .fontSize(10)
          .fontColor(COLORS.textHint)
          .margin({ top: 4 })
        Row() {
          Text('再想想')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(4)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('确认撤下')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.danger)
            .borderRadius(4)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(6)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailRecipeModal {
  @Prop name: string;
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('👁️ 醋方详情')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Column() {
          Text('📜')
            .fontSize(30)
          Text(this.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .margin({ top: 6 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 12 })
        Text('醋要后放、沿锅边淋,高温激出醋香而不留死酸。糖醋菜则先炒糖色,再下主料,末了烹醋翻匀,一锅出琉璃亮汁。老厨师的分寸,全在这半勺醋的先后。')
          .fontSize(11)
          .fontColor(COLORS.textSecondary)
          .lineHeight(17)
          .margin({ top: 10 })
        Row() {
          Text('流派源地')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('山西 · 太原')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Row() {
          Text('主厨评级')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('特一级 · 掌勺卅年')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(4)
        .margin({ top: 8 })
        Text('知道了')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 18, right: 18, top: 7, bottom: 7 })
          .backgroundColor(COLORS.primary)
          .borderRadius(4)
          .margin({ top: 12 })
          .onClick(() => {
            this.onClose();
          })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(6)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

十六、开发环境搭建

16.1 安装 DevEco Studio

DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

DevEco Studio 安装欢迎界面

16.2 选择安装路径

选择目标安装目录,建议安装到非系统盘,确认所需空间后点击「下一步」。

选择安装位置

16.3 配置安装选项

设置环境变量,勾选创建桌面快捷方式、将 bin 文件夹添加到 PATH、添加上下文菜单,但是需要重启一下电脑使环境变量生效。

安装选项配置

16.4 选择项目模板

新建一个空白模板,在模板市场中选择 Empty Ability。

选择 Empty Ability 模板

16.5 配置项目信息

设置 API 为 24 的模板项目,项目名称 kfschool,Compatible SDK 6.1.1(24),设备类型勾选 Phone,点击完成后初始化项目,自动下载相关依赖。

配置项目信息


十七、总结

通过对这款醋坊非遗酿醋应用的深度剖析,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、防御性编程等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Component,每个组件内部通过 @Builder 进一步复用 UI 片段;状态层利用 ArkUI 的 @State、@Observed 和 @ObjectLink 机制,实现了数据变化到 UI 刷新的自动响应。

在数据层面,VinegarData 类作为核心数据模型,以 5 个数组各 12 条数据完整描述了醋坊的全部业务信息。5 个动态颜色映射函数将业务数据转化为视觉色彩,实现了"数据即配色"的高级渲染模式,这是该应用最具特色的技术亮点之一。

在视觉层面,本应用独创了"深棕色货架"的醋品页设计——醋品页整个列表卡片都使用深棕色背景(primary #5A3A28 与 brew #5A3A28 奇偶交替),白色文字,金棕色价格,模仿醋坊的深色货架,形成了强烈的视觉识别特征。顶部"大酿坊·每日出货"柱状图卡片也采用深棕色背景配金棕色柱状图,与货架列表形成统一的视觉语言。全局圆角极小(圆角2/4/6),模仿醋坛的直方造型。统一的色彩系统是整个应用的视觉基石,深棕主色搭配赭红强调色,配合金棕语义色和蓝色装饰色,营造出既传统又醇厚的酿醋主题氛围。顶部头部的高低错落装饰带、醋坛浮动+水滴缩放脉冲微动效、极小圆角等细节处理,使得界面在信息密度较高的同时依然保持精致与舒适,处处透露出"醋坊"的主题气质。

在交互层面,应用实现了完整的 CRUD 闭环——新酿醋品、修改订单、撤下醋坛、醋方详情四类弹窗各司其职,配合统一的条件渲染机制和全屏半透明遮罩,交互流畅且一致。撤坛操作的二次确认、订单金额的动态着色、出货量阈值的高亮显示等细节,都体现了对用户体验的深入考量。

醋坊应用整体运行效果 - 大酿坊每日出货柱状图与醋品货架列表


标签: HarmonyOS · ArkTS · 华为 · 非遗酿醋 · 移动端开发

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐