醋坊非遗酿醋应用基于HarmonyOS API 24深度解析——采用了“单入口 + 多内容区 + 底部导航“的经典移动端布局模式。入口组件 VinegarApp 作为整个应用的根容器
醋坊非遗酿醋应用基于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 卡片通过横向进度条展示销量排名前六的醋品品种,三年陈醋以 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。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

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

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

16.4 选择项目模板
新建一个空白模板,在模板市场中选择 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 · 华为 · 非遗酿醋 · 移动端开发
更多推荐


所有评论(0)