HarmonyOS 6效果实现:原生数据可视化开发:搭建柱状图可视化组件,实现数值数据动态渲染与样式适配
一囊一季,荷包香事。香囊,古称"容臭",又称"香缨"“香包”,是中国传统手工艺的瑰宝,距今已有两千余年历史。香囊不仅是佩戴饰物,更是中医药理、刺绣技艺、民俗文化的综合载体——一囊一季,四时之香不同;一针一线,吉祥寓意相连。从端午辟秽到合欢安神,从平针绣到盘金绣,香囊承载着中国人的生活美学与健康智慧。本文完整记录一个基于鸿蒙HarmonyOS API 24与ArkTS声明式框架构建的「香囊坊」非遗手作平台,从玫瑰粉荷花色墨绿奶油的香囊色彩主题,到头部虚线圆环旋转动画与浮动光点translate位移呼吸效果,再到8Tab差异化布局(在售香囊列表/香料用量对比横条/香方集收录/绣样图库/绣娘名录/订单台账/香客名录/分铺经营评分)、多组件架构、@ObjectLink数据共享、@Observed聚合数据类NangData、3个工具函数(useBarWidth/useColor/scoreColor)、PillTag标签子组件封装、三大弹窗(新增香方/入库香料/编辑香囊)、条件渲染模态遮罩、PlayMode.Alternate呼吸动画体系,附完整源码与运行效果截图。

一、引言:一囊一季,荷包香事
香囊,古称"容臭",又称"香缨"“香包”,是中国传统手工艺的瑰宝。香囊的起源可追溯至先秦,《礼记·内则》记载"男女未冠笄者,鸡初鸣,咸盥漱,拂髦总角,衿缨,皆佩容臭",可见先秦少年已佩戴香囊。香囊不仅是佩戴饰物,更是中医药理、刺绣技艺、民俗文化的综合载体——一囊一季,四时之香不同;一针一线,吉祥寓意相连。端午佩香囊辟秽驱蚊,安神香囊助眠宁心,提神香囊醒脑明目,养颜香囊润肤悦色,香囊承载着中国人的生活美学与健康智慧。
中国香囊材质丰富,工艺精湛。香囊内填香料种类繁多——艾草辛温辟秽、白芷辛温祛湿、丁香辛温温中、薄荷辛凉醒脑、薰衣草甘平安神、桂花甘温凝香、合欢花甘平解郁、檀香辛温静心、陈皮苦温理气、茉莉花甘凉清雅,十味香料各具性味功效,产地从南阳到亳州到海南到太仓到伊犁到咸宁到安庆到广州到新会到福州,覆盖中国大江南北。香方更是博大精深——合欢安神方安神助眠、艾蒲辟秽方驱蚊辟秽、青梅醒脑方提神醒脑、白芷祛湿方祛湿健脾、檀香静心方静心冥想、玫瑰润颜方养颜悦色、陈皮理气方开胃理气、清心净莲方清心除烦,八方香方对症施治,配伍精当。绣样纹样更是琳琅满目——并蒂莲寓意并蒂同心、双鱼吉庆象征年年有余、五福捧寿代表福寿绵长、石榴多子寓意多子多福、如意云头象征万事如意、蝴蝶恋花寓意美满姻缘、葫芦万代代表子孙万代、喜鹊登梅象征喜上眉梢,一针一线皆祝福。绣娘更是四大名绣齐聚——苏绣双面绣、湘绣鬅毛针、蜀绣晕针、粤绣金银线、顾绣画绣、苗绣打籽绣、十字绣、贴布绣,八种绣法各擅胜场。
然而,随着现代生活节奏加快,香囊这门需要刺绣与中药双重技艺的传统手作正面临传承困境——年轻人不了解香囊文化,手工绣娘缺乏展示和销售渠道,香囊产品配方不透明、绣法混乱、产地信息缺失,正成为中国香囊非遗传承的现实痛点。一个优秀的香囊手作平台,不仅需要覆盖从"在售香囊"到"香料库"再到"香方集"的完整产品展示闭环,还要在纹样图库、绣娘名录、订单管理、香客档案、分铺经营等维度做到精准而高效。它需要以在售香囊为核心入口,横向扩展出香料、香方、纹样、绣娘、订单、香客、铺面八大功能模块,每个模块采用完全不同的布局风格,形成一个完整闭环的中国香囊非遗手作生态。
正是在这样的背景下,香囊坊类非遗手作平台应运而生。一个优秀的香囊应用,需要以在售香囊为核心入口,横向扩展出香料用量对比、香方收录管理、绣样纹样图库、绣娘名录展示、订单台账追踪、香客档案管理、分铺经营评分八大功能模块,每个模块采用完全不同的布局风格——香囊=在售香囊列表+编辑交互、香料=用量对比横条+香料库列表、香方=香方集列表+新增交互、纹样=绣样图库列表、绣娘=绣娘名录列表、订单=订单台账列表+交期标识、香客=香客名录列表、铺面=分铺经营列表+评分着色,形成了一个完整闭环的中国香囊非遗手作生态。它不仅是一个功能完备的演示项目,更是一份典型的ArkUI工程实践范本,涵盖了多组件架构设计、@ObjectLink数据共享、@Observed聚合数据类、颜色分级函数、PillTag子组件封装、头部旋转动画与浮动光点、条件渲染模态弹窗、layoutWeight弹性进度条、ForEach列表渲染等关键技术点。
二、应用整体架构概览
整个应用采用了"多组件架构 + @ObjectLink数据共享 + 配置驱动 + 子组件封装"的四层设计模式。入口组件NangApp作为整个应用的根容器,负责管理当前激活的Tab状态、特效开关、三个弹窗的显隐状态、表单数据以及聚合数据对象。与单组件内@Builder构建所有页面的架构不同,本应用将整个应用拆分为10个独立组件:1个入口组件NangApp + 1个标签子组件PillTag + 8个内容子组件(SachetTab香囊/HerbTab香料/FormulaTab香方/PatternTab纹样/EmbroiderTab绣娘/OrderTab订单/CustomerTab香客/ShopTab铺面)。每个内容子组件独立封装,职责单一,通过@ObjectLink共享聚合数据,通过回调函数(onEdit/onAdd)实现父子组件通信,是本应用工程化设计的最大亮点,也是ArkUI多组件架构的典型实践。
应用一共划分为八个一级页面:香囊(Tab 0)、香料(Tab 1)、香方(Tab 2)、纹样(Tab 3)、绣娘(Tab 4)、订单(Tab 5)、香客(Tab 6)、铺面(Tab 7)。底部bottomBar采用双行四列布局——ROW1_IDX [0,1,2,3] 为第一行,ROW2_IDX [4,5,6,7] 为第二行,每个Tab项由@Builder bottomItem(i)构建,包含图标(16号Emoji)、标签文字(9号)、选中指示条(16x3圆角2玫瑰色小条)三部分。激活态文字roseDeep深玫瑰色加粗+指示条显示,非激活态textHint浅灰粉色普通+指示条透明隐藏,点击切换curTab触发contentArea重新渲染。八个Tab每个布局完全不同——香囊采用在售香囊列表+编辑交互(12款香囊,配方绣法信息,✏️点击编辑弹窗)、香料采用用量对比横条+香料库列表(10种香料,useBarWidth用量宽度映射,useColor三级着色)、香方采用香方集列表+新增交互(8方香方,✚点击新增弹窗)、纹样采用绣样图库列表(8种纹样,寓意绣法信息)、绣娘采用绣娘名录列表(8位绣娘,四大名绣齐聚,绣龄技能信息)、订单采用订单台账列表(10笔订单,交期warning着色)、香客采用香客名录列表(8位香客,城市偏好信息)、铺面采用分铺经营列表+评分着色(6家分铺,scoreColor三级着色),这种"一页一型"的差异化设计最大程度还原了真实中国香囊非遗手作平台的交互体验。
在数据层面,应用独创了**@Observed聚合数据类**设计——NangData一个类包含sachets/herbs/formulas/patterns/embroiderers/orders/customers/shops八个数组,每个数组6-12条数据,用对象字面量初始化(非new构造函数),@Observed装饰整个类,配合@ObjectLink在子组件间共享引用,实现"一个数据对象贯穿所有页面"的聚合数据模式。这种设计将所有业务数据集中管理,子组件通过@ObjectLink data: NangData接收共享引用,修改数据时(如addFormula/addHerb/editSachet)所有引用该数据的子组件自动响应式刷新,是ArkUI跨组件数据共享的高级实践。应用还定义了8个元数据接口(SachetItem/HerbItem/FormulaItem/PatternItem/EmbroiderItem/OrderItem/CustomerItem/ShopItem)和1个标签子组件PillTag,将数据结构与UI表现层解耦,实现配置驱动设计。
在颜色逻辑层面,应用定义了3个颜色与尺寸映射函数(useBarWidth用量宽度映射/useColor用量三级着色/scoreColor评分三级着色),将颜色和尺寸映射逻辑从UI层抽离,每个函数根据不同维度(用量/评分)返回对应的颜色值或百分比宽度,实现"数据即配色、数据即尺寸"的动态映射模式。应用配色采用"玫瑰粉荷花色墨绿奶油"香囊非遗主题——bg #FAF4EC 米杏色(模拟古绢温润质感),rose #C08A9E 玫瑰粉(香囊主色调),roseDeep #8E5A72 深玫瑰(深色强调),lotus #E8D5DC 荷花粉(浅色装饰),green #3E5C50 墨绿(香料辅助色),cream #FDF6F1 奶油白(表单背景),textMain #4A3340 深褐紫(深色文字),五色各司其职形成"以玫瑰粉定囊、以荷花色定饰、以墨绿定香、以奶油定表、以米杏定底"的中国香囊语义着色体系。
在交互层面,应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:addFormulaModal新增香方(三字段TextInput表单:香方名称/主治功效/配伍香料数,收录香方rose玫瑰底)、addHerbModal入库香料(三字段TextInput表单:香料名称/性味/每囊用量,入库香料green墨绿底)、editSachetModal编辑香囊(三字段TextInput表单:香囊名称/搭配香方/售价,保存修改roseDeep深玫瑰底)。三个弹窗都是@Builder方法内联在入口组件中,通过if条件渲染控制显隐(showAddF/showAddH/showEdit三个@State布尔变量),遮罩用rgba(40,25,35,0.5)(50%深紫)硬编码,弹窗主体与遮罩用Column层叠,整体zIndex(999)覆盖主内容。香囊页✏️点击触发editSachetModal(editIdx记录目标索引,预填当前值),香料页✚点击触发addHerbModal,香方页✚点击触发addFormulaModal,形成了完整的"编辑香囊→入库香料→新增香方"交互闭环。
下面逐页展示应用的八大功能模块运行效果。
三、香囊页:在售香囊列表与编辑交互
香囊页以"在售香囊"为核心,采用"香囊列表+编辑交互"的单层布局。

页面顶部第一部分是标题行,Row包裹:
Row() {
Text('🎐 在售香囊').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text(this.data.sachets.length.toString() + ' 款 · 一囊一季')
.fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行采用左右分布——左侧"🎐 在售香囊"14号粗体深褐紫色,右侧"12 款 · 一囊一季"10号浅灰粉色,用Column().layoutWeight(1)撑开中间空间实现两端对齐。"一囊一季"点明香囊四时之香不同的时令特色——春佩梅花醒脑、夏挂艾蒲辟秽、秋佩桂花凝香、冬藏檀香静心,一囊对应一季,是中国香囊文化的核心意象。
第二部分是香囊列表,Scroll包裹ForEach遍历this.data.sachets数组,每款香囊渲染为一张卡片:
Scroll() {
Column() {
ForEach(this.data.sachets, (s: SachetItem, i: number) => {
Row() {
Column() {
Text(s.emoji).fontSize(18)
}
.width(42).height(42).borderRadius(21)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(s.formula + ' · ' + s.stitch).fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Column() {
Text('¥' + s.price).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(COLORS.roseDeep)
Text('✏️').fontSize(12).fontColor(COLORS.rose).margin({ top: 4 })
.onClick(() => { this.onEdit(i) })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (s: SachetItem) => s.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
每张香囊卡片采用三列布局——第一列是42x42圆形荷花粉底Emoji图标区(backgroundColor(COLORS.lotus)荷花粉底+borderRadius(21)圆形),第二列是layoutWeight(1)弹性信息区(香囊名称13号粗体深褐紫色+配方绣法信息10号灰紫色),第三列是价格与编辑区(“¥”+price 13号粗体深玫瑰色+✏️编辑按钮12号玫瑰色)。卡片用cardBg白底圆角14+shadow阴影+margin top 7间距,animation 350ms EaseOut入场动画。
12款香囊数据完整呈现了中国香囊的文化生态:合欢安神香囊(合欢安神方,平针绣,¥68,🎐)、端午辟秽香囊(艾蒲辟秽方,锁针绣,¥46,🌿)、青梅提神香囊(青梅醒脑方,打籽绣,¥58,🍋)、桂花香囊(金桂凝香方,平针绣,¥62,🌼)、薰衣安眠香囊(薰衣安神方,辫子绣,¥52,💜)、丁香驱寒香囊(丁香温中方,齐针绣,¥48,🧣)、莲花清心香囊(清心净莲方,滚针绣,¥72,🪷)、白芷祛湿香囊(白芷祛湿方,平针绣,¥56,🍃)、茉莉清雅香囊(茉莉清雅方,网绣,¥66,🤍)、檀香静思香囊(檀香静心方,盘金绣,¥88,🪔)、陈皮开胃香囊(陈皮理气方,锁针绣,¥44,🍊)、玫瑰养颜香囊(玫瑰润颜方,打籽绣,¥78,🌹)。价格从¥44(陈皮开胃香囊)到¥88(檀香静思香囊),檀香静思香囊¥88最贵(盘金绣工艺最复杂,檀香静心方配伍珍贵),陈皮开胃香囊¥44最实惠(锁针绣工艺常见,陈皮理气方配伍简单)。12款香囊覆盖安神助眠/驱蚊辟秽/提神醒脑/祛湿健脾/静心冥想/养颜悦色/开胃理气/清心除烦八大功效,绣法覆盖平针绣/锁针绣/打籽绣/辫子绣/齐针绣/滚针绣/网绣/盘金绣八种绣法,完整呈现中国香囊"一囊一季一绣一香"的文化内涵。点击✏️触发editSachetModal编辑香囊弹窗(editIdx记录目标索引,formSName/formSFormula/formSPrice预填当前值),形成"浏览→编辑"交互闭环。
四、香料页:用量对比横条与香料库
香料页以"香料库"为核心,采用"每囊用量对比横条+香料库列表"的两层布局。

页面顶部第一部分是用量对比卡,Column包裹:
Column() {
Text('⚖️ 每囊用量对比(克)').fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain).width('100%')
ForEach(this.data.herbs, (h: HerbItem) => {
Row() {
Text(h.emoji).fontSize(13)
Text(h.name).fontSize(10).fontColor(COLORS.textMain).width(56).margin({ left: 5 })
Column() {
Column().width(useBarWidth(h.use)).height(8)
.backgroundColor(useColor(h.use)).borderRadius(4)
}
.layoutWeight(1).height(8).backgroundColor('#F0E5E9').borderRadius(4)
Text(h.use.toString() + 'g').fontSize(9).fontColor(COLORS.textHint).width(28)
.textAlign(TextAlign.End)
}
.width('100%').margin({ top: 8 })
}, (h: HerbItem) => h.name)
}
.width('100%').padding(12)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 6, color: '#0F000000', offsetY: 2 })
.margin({ top: 10 })
用量对比卡是本应用香料页的核心亮点。每行采用四列布局——Emoji图标(13号)+香料名称(10号width 56固定宽度)+横向进度条(layoutWeight(1)弹性宽度,外层#F0E5E9浅粉灰底+内层useBarWidth(h.use)宽度useColor(h.use)颜色进度)+用量数值(9号"Xg"右对齐width 28)。进度条宽度通过useBarWidth函数映射:
function useBarWidth(use: number): string {
return Math.round(use / 20 * 100).toString() + '%';
}
useBarWidth将用量值除以20再乘100四舍五入取整,转换为百分比字符串——用量20g对应100%满格,用量15g对应75%,用量10g对应50%,用量4g对应20%。这种"数值除以基准值映射百分比"的方式简洁直观,20g是香囊单味香料用量的上限参考值。颜色通过useColor函数三级着色:
function useColor(use: number): string {
if (use >= 12) {
return '#C08A9E'; // 玫瑰粉——高用量
}
if (use >= 7) {
return '#3E5C50'; // 墨绿——中用量
}
return '#C4AAB6'; // 浅灰粉——低用量
}
useColor三级着色——用量≥12g用玫瑰粉#C08A9E(高用量,如薰衣草18g/艾草15g/薄荷12g,是香囊主药用量大),用量≥7g用墨绿#3E5C50(中用量,如白芷10g/合欢花10g/丁香8g/陈皮9g,是辅助药材中等用量),用量<7g用浅灰粉#C4AAB6(低用量,如茉莉花5g/桂花6g/檀香4g,是芳香调料少量即可)。这种"以用量定颜色"的动态着色模式让用量对比一目了然,薰衣草18g用量最高进度条最长且玫瑰粉色最醒目,檀香4g用量最低进度条最短且浅灰粉色最淡。
10种香料用量数据:薰衣草18g(伊犁,最高!玫瑰粉)、艾草15g(南阳,玫瑰粉)、薄荷12g(太仓,玫瑰粉)、白芷10g(亳州,墨绿)、合欢花10g(安庆,墨绿)、陈皮9g(新会,墨绿)、丁香8g(海南,墨绿)、桂花6g(咸宁,浅灰粉)、茉莉花5g(福州,浅灰粉)、檀香4g(广州,最低!浅灰粉)。产地覆盖南阳/亳州/海南/太仓/伊犁/咸宁/安庆/广州/新会/福州十地,性味覆盖辛温/辛凉/甘平/甘温/甘凉/苦温六种,完整呈现中国香料产地分布与性味分类。
第二部分是香料库列表,ForEach遍历this.data.herbs数组:
ForEach(this.data.herbs, (h: HerbItem) => {
Row() {
Column() {
Text(h.emoji).fontSize(17)
}
.width(38).height(38).borderRadius(19)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(h.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(h.taste + ' · 产自' + h.from).fontSize(9)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text(h.use.toString() + 'g/囊').fontSize(10).fontColor(COLORS.roseDeep)
}
.width('100%').padding(10)
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (h: HerbItem) => h.name)
香料库列表每行采用三列布局——38x38圆形奶油白底Emoji图标区(backgroundColor(COLORS.cream)奶油白底+borderRadius(19)圆形,比香囊卡片的荷花粉底更柔和)+弹性信息区(香料名称12号粗体+性味产地信息9号灰紫色"辛温 · 产自南阳")+用量标注(10号深玫瑰色"Xg/囊")。卡片用cardBg白底圆角12(比用量对比卡14略小形成层次)+margin top 7间距+animation 350ms EaseOut。香料页顶部还有"+ 入库香料"按钮:
Text('+ 入库香料').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.green).borderRadius(10)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.onClick(() => { this.onAdd() })
"+ 入库香料"按钮用green墨绿底白字圆角10,点击触发addHerbModal入库香料弹窗,形成"浏览→入库"交互闭环。
五、香方页:香方集与新增交互
香方页以"香方集"为核心,采用"香方列表+新增交互"的单层布局。

页面顶部第一部分是标题行,Row包裹:
Row() {
Text('📖 香方集').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('+ 新增香方').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.rose).borderRadius(10)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.onClick(() => { this.onAdd() })
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"📖 香方集"14号粗体深褐紫色,右侧"+ 新增香方"按钮用rose玫瑰粉底白字圆角10,点击触发addFormulaModal新增香方弹窗。"香方集"是中国传统香学的核心概念——香方是香囊配伍的灵魂,一方香方对应一种功效,配伍数味香料协同增效,是中国中医药"君臣佐使"理论在香囊领域的实践。
第二部分是香方列表,Scroll包裹ForEach遍历this.data.formulas数组:
ForEach(this.data.formulas, (f: FormulaItem) => {
Row() {
Column() {
Text(f.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(f.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(f.purpose + ' · 配伍 ' + f.count + ' 味').fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('📖').fontSize(14).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (f: FormulaItem) => f.name)
每张香方卡片采用三列布局——40x40圆形荷花粉底Emoji图标区+弹性信息区(香方名称13号粗体+主治功效配伍数信息10号灰紫色"安神助眠 · 配伍 6 味")+📖图标(14号opacity 0.6半透明)。卡片用cardBg白底圆角14+shadow阴影+animation 350ms EaseOut。
8方香方数据:合欢安神方(安神助眠,6味,🌙)、艾蒲辟秽方(驱蚊辟秽,8味,🛡️)、青梅醒脑方(提神醒脑,5味,⚡)、白芷祛湿方(祛湿健脾,7味,💧)、檀香静心方(静心冥想,4味,🧘)、玫瑰润颜方(养颜悦色,6味,🌹)、陈皮理气方(开胃理气,5味,🍊)、清心净莲方(清心除烦,7味,🪷)。配伍数从4味(檀香静心方,配伍精简专注静心)到8味(艾蒲辟秽方,配伍丰富驱蚊全面),艾蒲辟秽方8味配伍最多(艾草+白芷+丁香+薄荷+藿香+菖蒲+苍术+雄黄,全面辟秽),檀香静心方4味配伍最少(檀香+沉香+乳香+没药,精简专注静心)。8方香方覆盖安神助眠/驱蚊辟秽/提神醒脑/祛湿健脾/静心冥想/养颜悦色/开胃理气/清心除烦八大功效,与12款香囊的配方一一对应,完整呈现中国香方"对症施治、配伍精当"的中医药理论。点击"+ 新增香方"触发addFormulaModal新增香方弹窗,通过data.addFormula(name, purpose, count)将新香方unshift插入数组头部,形成"浏览→新增"交互闭环。
六、纹样页:绣样图库
纹样页以"绣样图库"为核心,采用"纹样列表"的单层布局。

页面顶部第一部分是标题行:
Row() {
Text('🪡 绣样图库').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('一针一线皆祝福').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"🪡 绣样图库"14号粗体深褐紫色,右侧"一针一线皆祝福"10号浅灰粉色,点明纹样的文化寓意——每一种绣样纹样都承载着吉祥祝福,是中国民俗文化的视觉表达。
第二部分是纹样列表,Scroll包裹ForEach遍历this.data.patterns数组:
ForEach(this.data.patterns, (p: PatternItem) => {
Row() {
Column() {
Text(p.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(p.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(p.meaning + ' · ' + p.stitch).fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🪡').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (p: PatternItem) => p.name)
每张纹样卡片采用三列布局——40x40圆形奶油白底Emoji图标区(backgroundColor(COLORS.cream)奶油白底,比香方卡片的荷花粉底更素雅,呼应纹样"绣样"的素净气质)+弹性信息区(纹样名称13号粗体+寓意绣法信息10号灰紫色"并蒂同心 · 平针绣")+🪡图标(13号opacity 0.6半透明)。卡片用cardBg白底圆角14+shadow阴影+animation 350ms EaseOut。
8种纹样数据:并蒂莲(并蒂同心,平针绣,🪷)、双鱼吉庆(年年有余,打籽绣,🐟)、五福捧寿(福寿绵长,盘金绣,🦇)、石榴多子(多子多福,锁针绣,🍎)、如意云头(万事如意,滚针绣,☁️)、蝴蝶恋花(美满姻缘,齐针绣,🦋)、葫芦万代(子孙万代,网绣,🎋)、喜鹊登梅(喜上眉梢,辫子绣,🐦)。8种纹样覆盖并蒂同心/年年有余/福寿绵长/多子多福/万事如意/美满姻缘/子孙万代/喜上眉梢八大吉祥寓意,绣法覆盖平针绣/打籽绣/盘金绣/锁针绣/滚针绣/齐针绣/网绣/辫子绣八种绣法(与12款香囊的8种绣法完全一致),完整呈现中国香囊绣样"一针一线皆祝福"的文化内涵。并蒂莲用平针绣(平针绣是最基础的绣法,适合莲花线条流畅)、五福捧寿用盘金绣(盘金绣是最华丽的绣法,适合福寿纹样金碧辉煌)、喜鹊登梅用辫子绣(辫子绣立体感强,适合喜鹊羽毛纹理),纹样与绣法匹配考究,体现了中国刺绣"因纹施绣"的工艺智慧。
七、绣娘页:绣娘名录
绣娘页以"绣娘名录"为核心,采用"绣娘列表"的单层布局。

页面顶部第一部分是标题行:
Row() {
Text('🧵 绣娘名录').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('四大名绣齐聚').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"🧵 绣娘名录"14号粗体深褐紫色,右侧"四大名绣齐聚"10号浅灰粉色,点明绣娘名录的核心特色——苏绣、湘绣、蜀绣、粤绣四大名绣的绣娘齐聚一堂,是中国刺绣最高水准的集中展示。
第二部分是绣娘列表,Scroll包裹ForEach遍历this.data.embroiderers数组:
ForEach(this.data.embroiderers, (e: EmbroiderItem) => {
Row() {
Column() {
Text(e.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(e.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(e.skill + ' · 绣龄 ' + e.years + '年').fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🧵').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (e: EmbroiderItem) => e.name)
每张绣娘卡片采用三列布局——40x40圆形荷花粉底Emoji图标区+弹性信息区(绣娘名称13号粗体"苏绣·顾"+技能绣龄信息10号灰紫色"双面绣 · 绣龄 30年")+🧵图标(13号opacity 0.6半透明)。卡片用cardBg白底圆角14+shadow阴影+animation 350ms EaseOut。
8位绣娘数据:苏绣·顾(30年,双面绣,🧵)、湘绣·谭(26年,鬅毛针,🪡)、蜀绣·冯(24年,晕针,🌸)、粤绣·梁(22年,金银线,✨)、顾绣·韩(28年,画绣,🎨)、苗绣·石(20年,打籽绣,🌀)、十字绣·王(12年,十字绣,❌)、贴布绣·杨(15年,贴布绣,🧩)。绣龄从12年(十字绣·王,最年轻)到30年(苏绣·顾,最资深),苏绣·顾30年绣龄居首(双面绣是苏绣最高技艺,双面绣正反两面图案一致颜色不同,需要极高技艺),湘绣·谭26年次之(鬅毛针是湘绣特色针法,擅长绣虎狮毛发),顾绣·韩28年第三(画绣是顾绣特色,以画入绣绣画合一)。8位绣娘覆盖苏绣/湘绣/蜀绣/粤绣四大名绣+顾绣/苗绣/十字绣/贴布绣四种民间绣法,技能覆盖双面绣/鬅毛针/晕针/金银线/画绣/打籽绣/十字绣/贴布绣八种绣法,完整呈现中国刺绣"四大名绣齐聚、八种绣法各擅胜场"的技艺生态。
八、订单页:订单台账
订单页以"订单管理"为核心,采用"订单台账列表+交期标识"的单层布局。

页面顶部第一部分是标题行:
Row() {
Text('📜 订单台账').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('在排 ' + this.data.orders.length.toString() + ' 单').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"📜 订单台账"14号粗体深褐紫色,右侧"在排 10 单"10号浅灰粉色(动态显示当前在排订单数量),点明订单台账的核心功能——追踪在排订单的交期与数量。
第二部分是订单列表,Scroll包裹ForEach遍历this.data.orders数组:
ForEach(this.data.orders, (o: OrderItem) => {
Row() {
Text(o.emoji).fontSize(15)
Column() {
Text(o.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(o.item + ' × ' + o.qty).fontSize(9)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Column() {
Text(o.days.toString() + '天').fontSize(11).fontWeight(FontWeight.Bold)
.fontColor(COLORS.warning)
Text('交期').fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (o: OrderItem) => o.name + o.item)
每张订单卡片采用三列布局——Emoji图标(15号,无圆形底色直接显示,比其他Tab更简洁)+弹性信息区(客户名称12号粗体+商品数量信息9号灰紫色"端午辟秽香囊 × 200")+交期区(天数11号粗体warning赭金色"20天"+8号"交期"标签)。交期用warning赭金色#C08A1E着色是本应用订单页的核心亮点——交期越短越紧迫,用warning色提醒掌柜优先排产,交期越长越从容,用warning色的轻重变化暗示紧急程度。卡片用cardBg白底圆角14+shadow阴影+animation 350ms EaseOut。ForEach的key用o.name + o.item组合键(客户名+商品名),避免同名客户不同订单的key冲突。
10笔订单数据:汉服社团(端午辟秽香囊,200个,20天,👘)、文创书店·知止(合欢安神香囊,80个,15天,📚)、禅意民宿(檀香静思香囊,60个,18天,🏡)、国风美妆(玫瑰养颜香囊,120个,25天,💄)、研学课堂(艾蒲辟秽套装,150个,30天,🎓)、私人订制·陆(并蒂莲婚庆香囊,36个,40天,💍)、中医馆(白芷祛湿香囊,90个,22天,🏥)、茶空间(清心净莲香囊,45个,16天,🍵)、酒店集团(薰衣安眠香囊,300个,35天,🏨)、节气礼盒商(四季香囊礼盒,500个,45天,🎁)。数量从36个(私人订制·陆,并蒂莲婚庆香囊定制量小但工期长40天)到500个(节气礼盒商,四季香囊礼盒批量最大工期最长45天),交期从15天(文创书店·知止,最紧迫)到45天(节气礼盒商,最从容)。10笔订单覆盖汉服社团/文创书店/禅意民宿/国风美妆/研学课堂/私人订制/中医馆/茶空间/酒店集团/节气礼盒商十种客户类型,完整呈现中国香囊B端客户生态,香囊作为文化手作品,客户以文化机构/民宿/美妆/教育/酒店等文创类机构为主,体现香囊"文化手作"的产品定位。
九、香客页:香客名录
香客页以"香客档案"为核心,采用"香客列表"的单层布局。

页面顶部第一部分是标题行:
Row() {
Text('👥 香客名录').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('老客回购率 58%').fontSize(10).fontColor(COLORS.green)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"👥 香客名录"14号粗体深褐紫色,右侧"老客回购率 58%"10号墨绿色(用COLORS.green墨绿色着色,与warning赭金色的交期标识形成对比,绿色代表回购率健康良好),点明香客档案的核心指标——老客回购率58%体现了香囊产品的高复购率和文化粘性。
第二部分是香客列表,Scroll包裹ForEach遍历this.data.customers数组:
ForEach(this.data.customers, (c: CustomerItem) => {
Row() {
Column() {
Text(c.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(c.name + ' · ' + c.city).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain)
Text('偏爱 ' + c.fav).fontSize(9).fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🎐').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (c: CustomerItem) => c.name)
每张香客卡片采用三列布局——40x40圆形奶油白底Emoji头像区(backgroundColor(COLORS.cream)奶油白底,比绣娘卡片的荷花粉底更素净,呼应香客"素人"身份)+弹性信息区(姓名城市12号粗体"林晚 · 杭州"+偏爱系别9号灰紫色"偏爱 安神系")+🎐图标(13号opacity 0.6半透明,香囊Emoji呼应香客与香囊的关联)。卡片用cardBg白底圆角14+shadow阴影+animation 350ms EaseOut。
8位香客数据:林晚(杭州,安神系,👩)、程远(北京,提神系,🧑)、苏芮(苏州,花系,👩🦰)、阿澈(成都,祛湿系,🧔)、白露(南京,静心系,👱♀️)、许安然(上海,安神系,👨)、顾清和(扬州,清雅系,🧑🦱)、温故(广州,养颜系,👩🦳)。城市覆盖杭州/北京/苏州/成都/南京/上海/扬州/广州八城,偏爱系别覆盖安神系/提神系/花系/祛湿系/静心系/清雅系/养颜系七大系别,完整呈现中国香囊C端客户的偏好分布。林晚和许安然都偏爱安神系(安神系是最受欢迎的系别,现代人睡眠质量普遍不佳),苏芮偏爱花系(花系香囊如桂花香囊/玫瑰养颜香囊,以花香为主),白露偏爱静心系(静心系如檀香静思香囊,适合冥想瑜伽人群),温故偏爱养颜系(养颜系如玫瑰养颜香囊,以养颜悦色为主)。8位香客姓名富有诗意——林晚、程远、苏芮、阿澈、白露、许安然、顾清和、温故,每个名字都带有中国古典文学意境,呼应香囊的文化气质。
十、铺面页:分铺经营与评分着色
铺面页以"分铺经营"为核心,采用"分铺列表+评分着色"的单层布局。

页面顶部第一部分是标题行:
Row() {
Text('🏮 分铺经营').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('6 城联动').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
标题行左侧"🏮 分铺经营"14号粗体深褐紫色,右侧"6 城联动"10号浅灰粉色,点明分铺经营的核心特色——6家分铺分布在6座文化名城,形成"6城联动"的连锁经营格局。
第二部分是分铺列表,Scroll包裹ForEach遍历this.data.shops数组:
ForEach(this.data.shops, (s: ShopItem) => {
Row() {
Column() {
Text(s.emoji).fontSize(18)
}
.width(42).height(42).borderRadius(21)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(s.name + ' · ' + s.city).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain)
Text('客流 ' + s.flow).fontSize(9).fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text(s.score.toString() + ' ★').fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(scoreColor(s.score))
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (s: ShopItem) => s.name)
每张分铺卡片采用三列布局——42x42圆形荷花粉底Emoji图标区+弹性信息区(分铺名称城市13号粗体"姑苏荷包铺 · 苏州"+客流信息9号灰紫色"客流 1200人/月")+评分区("X.X ★"12号粗体,颜色通过scoreColor函数动态着色)。评分着色是本应用铺面页的核心亮点:
function scoreColor(score: number): string {
if (score >= 4.8) {
return '#C08A9E'; // 玫瑰粉——高分
}
if (score >= 4.6) {
return '#3E5C50'; // 墨绿——中高分
}
return '#C4AAB6'; // 浅灰粉——中分
}
scoreColor三级着色——评分≥4.8用玫瑰粉#C08A9E(高分,如姑苏荷包铺4.9/长安香事4.8/夫子庙香铺4.8,是香囊坊旗舰分铺),评分≥4.6用墨绿#3E5C50(中高分,如锦里香囊庄4.7/南锣鼓巷香店4.7,是香囊坊优质分铺),评分<4.6用浅灰粉#C4AAB6(中分,如西湖香囊集4.6,是香囊坊新开分铺)。这种"以评分定颜色"的动态着色模式与香料页的useColor函数逻辑一致,形成了统一的"三级着色"设计语言——高用玫瑰粉、中用墨绿、低用浅灰粉。
6家分铺数据:姑苏荷包铺(苏州,1200人/月,4.9,🏮)、长安香事(西安,980人/月,4.8,🏯)、锦里香囊庄(成都,1500人/月,4.7,🎋)、夫子庙香铺(南京,1300人/月,4.8,⛩️)、西湖香囊集(杭州,1100人/月,4.6,🌊)、南锣鼓巷香店(北京,900人/月,4.7,🏮)。客流从900人/月(南锣鼓巷香店,北京南锣鼓巷游客相对较少)到1500人/月(锦里香囊庄,成都锦里是热门旅游景点客流量最大),评分从4.6(西湖香囊集,杭州新开业评分略低)到4.9(姑苏荷包铺,苏州老字号旗舰分铺评分最高)。6家分铺分布在苏州/西安/成都/南京/杭州/北京六座文化名城,每座城市都是中国历史文化名城,与香囊的文化定位高度契合。姑苏荷包铺4.9分居首(苏州是苏绣发源地,荷包铺文化底蕴深厚),锦里香囊庄1500人/月客流最大(成都锦里是热门旅游景点),体现了"以文化名城选址、以旅游景点引流"的分铺经营策略。
十一、色彩主题系统:玫瑰粉荷花色墨绿奶油
11.1 色彩接口定义
任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个NangPalette接口,将整个应用所需的色彩抽象为一个结构化的契约,共16个字段:
interface NangPalette {
bg: string;
cardBg: string;
rose: string;
roseDeep: string;
lotus: string;
green: string;
cream: string;
textMain: string;
textSub: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
}
- bg:背景色 #FAF4EC,米杏色,模拟古绢温润质感,是整个应用的底色基调。
- cardBg:卡片背景 #FFFFFF,纯白,与米杏色底形成层次。
- rose:玫瑰粉 #C08A9E,香囊主色调,用于按钮、图标、指示条、高分着色。
- roseDeep:深玫瑰 #8E5A72,深色强调,用于价格、标题强调、保存按钮。
- lotus:荷花粉 #E8D5DC,浅色装饰,用于图标圆形底色。
- green:墨绿 #3E5C50,辅助色,用于入库按钮、回购率、中高分着色。
- cream:奶油白 #FDF6F1,表单背景,用于TextInput输入框底色。
- textMain:深褐紫 #4A3340,主文字色,非纯黑更有文化感。
- textSub:灰紫 #8A6B7A,副文字色,用于描述信息。
- textHint:浅灰粉 #C4AAB6,提示文字色,用于辅助信息。
11.2 色彩常量定义
const COLORS: NangPalette = {
bg: '#FAF4EC',
cardBg: '#FFFFFF',
rose: '#C08A9E',
roseDeep: '#8E5A72',
lotus: '#E8D5DC',
green: '#3E5C50',
cream: '#FDF6F1',
textMain: '#4A3340',
textSub: '#8A6B7A',
textHint: '#C4AAB6',
border: '#EFE0E4',
success: '#4E7A5A',
warning: '#C08A1E',
danger: '#B23A3A',
white: '#FFFFFF'
};
本应用配色最大特色是玫瑰粉 rose #C08A9E 与荷花粉 lotus #E8D5DC——玫瑰粉用于按钮/指示条/高分着色/价格(是香囊丝缎的玫瑰粉色调),荷花粉用于图标圆形底色(是荷花的浅粉色调,与中国香囊"荷包"的"荷"字呼应),与米杏色底bg #FAF4EC形成"玫瑰粉荷花色墨绿奶油"的中国香囊经典配色。墨绿green #3E5C50用于入库按钮和回购率(是艾草的墨绿色调,与香料页的艾草🌿呼应),奶油白cream #FDF6F1用于表单背景(是丝绸的奶油白色调),五色各司其职,营造既玫瑰粉柔美又墨绿沉稳的中国香囊非遗氛围。
11.3 颜色分级函数体系
应用定义了3个颜色与尺寸映射函数,将颜色映射逻辑从UI层抽离:
| 函数名 | 输入维度 | 分级逻辑 | 用途 |
|---|---|---|---|
| useBarWidth | 用量(g) | use/20*100 取整+% | 香料用量进度条宽度映射 |
| useColor | 用量(g) | ≥12玫瑰粉/≥7墨绿/其他浅灰粉 | 香料用量进度条颜色分级 |
| scoreColor | 评分 | ≥4.8玫瑰粉/≥4.6墨绿/其他浅灰粉 | 分铺评分颜色分级 |
三个函数形成了统一的"三级着色"设计语言——高用玫瑰粉、中用墨绿、低用浅灰粉,useColor和scoreColor的分级逻辑完全一致(只是阈值不同),保证了视觉一致性。
十二、数据模型与状态管理
12.1 @Observed聚合数据类
应用独创了@Observed聚合数据类设计——NangData一个类包含八个数组,用对象字面量初始化:
@Observed
export class NangData {
sachets: SachetItem[] = [
{ name: '合欢安神香囊', formula: '合欢安神方', stitch: '平针绣', price: 68, emoji: '🎐' },
{ name: '端午辟秽香囊', formula: '艾蒲辟秽方', stitch: '锁针绣', price: 46, emoji: '🌿' },
// ... 12款香囊
];
herbs: HerbItem[] = [
{ name: '艾草', taste: '辛温', use: 15, from: '南阳', emoji: '🌿' },
// ... 10种香料
];
formulas: FormulaItem[] = [
{ name: '合欢安神方', purpose: '安神助眠', count: 6, emoji: '🌙' },
// ... 8方香方
];
patterns: PatternItem[] = [
{ name: '并蒂莲', meaning: '并蒂同心', stitch: '平针绣', emoji: '🪷' },
// ... 8种纹样
];
embroiderers: EmbroiderItem[] = [
{ name: '苏绣·顾', years: 30, skill: '双面绣', emoji: '🧵' },
// ... 8位绣娘
];
orders: OrderItem[] = [
{ name: '汉服社团', item: '端午辟秽香囊', qty: 200, days: 20, emoji: '👘' },
// ... 10笔订单
];
customers: CustomerItem[] = [
{ name: '林晚', city: '杭州', fav: '安神系', emoji: '👩' },
// ... 8位香客
];
shops: ShopItem[] = [
{ name: '姑苏荷包铺', city: '苏州', flow: '1200人/月', score: 4.9, emoji: '🏮' },
// ... 6家分铺
];
}
@Observed装饰整个类,配合@ObjectLink在子组件间共享引用。数据规模达76条(12+10+8+8+8+10+8+6),完整呈现中国香囊非遗手作平台的数据生态。
12.2 数据操作方法
NangData类还定义了3个数据操作方法:
addFormula(name: string, purpose: string, count: number): void {
this.formulas.unshift({ name: name, purpose: purpose, count: count, emoji: '📖' });
}
addHerb(name: string, taste: string, use: number): void {
this.herbs.unshift({ name: name, taste: taste, use: use, from: '新采', emoji: '🌿' });
}
editSachet(idx: number, name: string, formula: string, price: number): void {
this.sachets.splice(idx, 1, {
name: name,
formula: formula,
stitch: this.sachets[idx].stitch,
price: price,
emoji: this.sachets[idx].emoji
});
}
- addFormula:新增香方,
unshift插入数组头部,新香方emoji默认📖。 - addHerb:入库香料,
unshift插入数组头部,新香料产地默认"新采"、emoji默认🌿。 - editSachet:编辑香囊,
splice替换指定索引项,保留原绣法stitch和emoji不变(只改名称、配方、价格)。
12.3 状态管理架构
入口组件NangApp管理15个@State状态变量——curTab(当前Tab索引)、fx(特效开关布尔)、spin(旋转动画布尔)、floatDot(浮动光点布尔)、data(NangData实例)、showAddF/showAddH/showEdit(三个弹窗显隐布尔)、editIdx(编辑索引)、formFName/formFPurpose/formFCount(新增香方表单三字段)、formHName/formHTaste/formHUse(入库香料表单三字段)、formSName/formSFormula/formSPrice(编辑香囊表单三字段)。子组件通过@ObjectLink接收data共享引用,通过@Prop接收fx特效开关,通过回调函数(onEdit/onAdd)与入口组件通信。
十三、交互与动画系统
13.1 头部动画:虚线圆环旋转与浮动光点
头部区域是本应用最具创意的动画设计——虚线圆环旋转动画+浮动光点呼吸动画:
Stack() {
Column()
.width(62).height(62).borderRadius(31)
.border({ width: 1.5, color: COLORS.rose })
.borderStyle(BorderStyle.Dashed)
.animation({ duration: 6000, curve: Curve.Linear, iterations: -1 })
Column() {
Text('🎐').fontSize(26)
}
.width(52).height(52).borderRadius(26)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column().width(8).height(8).borderRadius(4)
.backgroundColor(COLORS.warning)
.translate({ y: this.floatDot ? -10 : 2 })
.opacity(this.floatDot ? 0.3 : 1)
.animation({ duration: 1600, curve: Curve.EaseInOut, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 52, y: 4 })
}
.width(64).height(64)
Stack层叠三部分——最外层是62x62虚线圆环(borderStyle(BorderStyle.Dashed)虚线边框+animation 6000ms Linear无限循环,虚线圆环持续旋转模拟香囊丝线缠绕的动态效果),中间层是52x52荷花粉底实心圆(🎐香囊Emoji 26号居中),最上层是8x8warning赭金色浮动光点(translate y: -10↔2上下位移+opacity: 0.3↔1透明度变化+animation 1600ms EaseInOut Alternate往返呼吸,模拟香囊散发的香气光点浮动飘逸)。虚线圆环旋转6000ms一周(缓慢旋转,模拟丝线缠绕),浮动光点1600ms一次往返(快速呼吸,模拟香气飘逸),两者节奏不同形成"慢旋快浮"的动画层次。浮动光点用position({ x: 52, y: 4 })定位在圆环右上角,模拟香气从香囊上方飘出的视觉效果。
13.2 特效开关
头部还有一个特效开关按钮:
Text(this.fx ? '✨ 特效开' : '✨ 特效关')
.fontSize(9).fontColor(COLORS.white).backgroundColor(COLORS.rose)
.borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 })
.onClick(() => { this.fx = !this.fx })
点击切换fx布尔值,通过@Prop传递给子组件,子组件可根据fx值决定是否显示动画效果。特效开时显示"✨ 特效开"玫瑰粉底白字,特效关时显示"✨ 特效关",是本应用的交互亮点之一。
13.3 三大弹窗系统
应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:
三个弹窗结构一致——modalOverlay遮罩层(全屏半透明背景,点击关闭)+弹窗主体(90%宽度,圆角16,cardBg白底,position居中偏上8%)。每个弹窗包含标题行(标题+✕关闭)、Divider分割线、表单区(3个TextInput输入框,每个带label标签+placeholder提示)、底部按钮区(取消+确认双按钮)。确认按钮颜色各不相同——编辑香囊用roseDeep深玫瑰底"保存修改"、入库香料用green墨绿底"入库香料"、新增香方用rose玫瑰底"收录香方",通过按钮颜色区分操作类型。遮罩统一用rgba(40,25,35,0.5)(50%深紫色),弹窗整体zIndex(999)覆盖主内容。
十四、完整源代码
以下是应用的完整源代码,涵盖了色彩接口定义、常量配置、数据模型、工具函数、标签子组件、入口组件及八大内容子组件的全部实现:
// ============ 香囊坊 · 荷包香事 ============
interface NangPalette {
bg: string;
cardBg: string;
rose: string;
roseDeep: string;
lotus: string;
green: string;
cream: string;
textMain: string;
textSub: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
}
const COLORS: NangPalette = {
bg: '#FAF4EC',
cardBg: '#FFFFFF',
rose: '#C08A9E',
roseDeep: '#8E5A72',
lotus: '#E8D5DC',
green: '#3E5C50',
cream: '#FDF6F1',
textMain: '#4A3340',
textSub: '#8A6B7A',
textHint: '#C4AAB6',
border: '#EFE0E4',
success: '#4E7A5A',
warning: '#C08A1E',
danger: '#B23A3A',
white: '#FFFFFF'
};
interface NangTabMeta {
key: string;
icon: string;
label: string;
}
const TAB_LIST: NangTabMeta[] = [
{ key: 'sachet', icon: '🎐', label: '香囊' },
{ key: 'herb', icon: '🌿', label: '香料' },
{ key: 'formula', icon: '📖', label: '香方' },
{ key: 'pattern', icon: '🪡', label: '纹样' },
{ key: 'embroider', icon: '🧵', label: '绣娘' },
{ key: 'order', icon: '📜', label: '订单' },
{ key: 'customer', icon: '👥', label: '香客' },
{ key: 'shop', icon: '🏮', label: '铺面' }
];
const ROW1_IDX: number[] = [0, 1, 2, 3];
const ROW2_IDX: number[] = [4, 5, 6, 7];
const HOT_TAGS: string[] = ['安神助眠', '驱蚊辟秽', '提神醒脑', '祛湿健脾'];
interface SachetItem {
name: string;
formula: string;
stitch: string;
price: number;
emoji: string;
}
interface HerbItem {
name: string;
taste: string;
use: number;
from: string;
emoji: string;
}
interface FormulaItem {
name: string;
purpose: string;
count: number;
emoji: string;
}
interface PatternItem {
name: string;
meaning: string;
stitch: string;
emoji: string;
}
interface EmbroiderItem {
name: string;
years: number;
skill: string;
emoji: string;
}
interface OrderItem {
name: string;
item: string;
qty: number;
days: number;
emoji: string;
}
interface CustomerItem {
name: string;
city: string;
fav: string;
emoji: string;
}
interface ShopItem {
name: string;
city: string;
flow: string;
score: number;
emoji: string;
}
@Observed
export class NangData {
sachets: SachetItem[] = [
{ name: '合欢安神香囊', formula: '合欢安神方', stitch: '平针绣', price: 68, emoji: '🎐' },
{ name: '端午辟秽香囊', formula: '艾蒲辟秽方', stitch: '锁针绣', price: 46, emoji: '🌿' },
{ name: '青梅提神香囊', formula: '青梅醒脑方', stitch: '打籽绣', price: 58, emoji: '🍋' },
{ name: '桂花香囊', formula: '金桂凝香方', stitch: '平针绣', price: 62, emoji: '🌼' },
{ name: '薰衣安眠香囊', formula: '薰衣安神方', stitch: '辫子绣', price: 52, emoji: '💜' },
{ name: '丁香驱寒香囊', formula: '丁香温中方', stitch: '齐针绣', price: 48, emoji: '🧣' },
{ name: '莲花清心香囊', formula: '清心净莲方', stitch: '滚针绣', price: 72, emoji: '🪷' },
{ name: '白芷祛湿香囊', formula: '白芷祛湿方', stitch: '平针绣', price: 56, emoji: '🍃' },
{ name: '茉莉清雅香囊', formula: '茉莉清雅方', stitch: '网绣', price: 66, emoji: '🤍' },
{ name: '檀香静思香囊', formula: '檀香静心方', stitch: '盘金绣', price: 88, emoji: '🪔' },
{ name: '陈皮开胃香囊', formula: '陈皮理气方', stitch: '锁针绣', price: 44, emoji: '🍊' },
{ name: '玫瑰养颜香囊', formula: '玫瑰润颜方', stitch: '打籽绣', price: 78, emoji: '🌹' }
];
herbs: HerbItem[] = [
{ name: '艾草', taste: '辛温', use: 15, from: '南阳', emoji: '🌿' },
{ name: '白芷', taste: '辛温', use: 10, from: '亳州', emoji: '🤍' },
{ name: '丁香', taste: '辛温', use: 8, from: '海南', emoji: '🧣' },
{ name: '薄荷', taste: '辛凉', use: 12, from: '太仓', emoji: '🌱' },
{ name: '薰衣草', taste: '甘平', use: 18, from: '伊犁', emoji: '💜' },
{ name: '桂花', taste: '甘温', use: 6, from: '咸宁', emoji: '🌼' },
{ name: '合欢花', taste: '甘平', use: 10, from: '安庆', emoji: '🌸' },
{ name: '檀香', taste: '辛温', use: 4, from: '广州', emoji: '🪵' },
{ name: '陈皮', taste: '苦温', use: 9, from: '新会', emoji: '🍊' },
{ name: '茉莉花', taste: '甘凉', use: 5, from: '福州', emoji: '🤍' }
];
formulas: FormulaItem[] = [
{ name: '合欢安神方', purpose: '安神助眠', count: 6, emoji: '🌙' },
{ name: '艾蒲辟秽方', purpose: '驱蚊辟秽', count: 8, emoji: '🛡️' },
{ name: '青梅醒脑方', purpose: '提神醒脑', count: 5, emoji: '⚡' },
{ name: '白芷祛湿方', purpose: '祛湿健脾', count: 7, emoji: '💧' },
{ name: '檀香静心方', purpose: '静心冥想', count: 4, emoji: '🧘' },
{ name: '玫瑰润颜方', purpose: '养颜悦色', count: 6, emoji: '🌹' },
{ name: '陈皮理气方', purpose: '开胃理气', count: 5, emoji: '🍊' },
{ name: '清心净莲方', purpose: '清心除烦', count: 7, emoji: '🪷' }
];
patterns: PatternItem[] = [
{ name: '并蒂莲', meaning: '并蒂同心', stitch: '平针绣', emoji: '🪷' },
{ name: '双鱼吉庆', meaning: '年年有余', stitch: '打籽绣', emoji: '🐟' },
{ name: '五福捧寿', meaning: '福寿绵长', stitch: '盘金绣', emoji: '🦇' },
{ name: '石榴多子', meaning: '多子多福', stitch: '锁针绣', emoji: '🍎' },
{ name: '如意云头', meaning: '万事如意', stitch: '滚针绣', emoji: '☁️' },
{ name: '蝴蝶恋花', meaning: '美满姻缘', stitch: '齐针绣', emoji: '🦋' },
{ name: '葫芦万代', meaning: '子孙万代', stitch: '网绣', emoji: '🎋' },
{ name: '喜鹊登梅', meaning: '喜上眉梢', stitch: '辫子绣', emoji: '🐦' }
];
embroiderers: EmbroiderItem[] = [
{ name: '苏绣·顾', years: 30, skill: '双面绣', emoji: '🧵' },
{ name: '湘绣·谭', years: 26, skill: '鬅毛针', emoji: '🪡' },
{ name: '蜀绣·冯', years: 24, skill: '晕针', emoji: '🌸' },
{ name: '粤绣·梁', years: 22, skill: '金银线', emoji: '✨' },
{ name: '顾绣·韩', years: 28, skill: '画绣', emoji: '🎨' },
{ name: '苗绣·石', years: 20, skill: '打籽绣', emoji: '🌀' },
{ name: '十字绣·王', years: 12, skill: '十字绣', emoji: '❌' },
{ name: '贴布绣·杨', years: 15, skill: '贴布绣', emoji: '🧩' }
];
orders: OrderItem[] = [
{ name: '汉服社团', item: '端午辟秽香囊', qty: 200, days: 20, emoji: '👘' },
{ name: '文创书店·知止', item: '合欢安神香囊', qty: 80, days: 15, emoji: '📚' },
{ name: '禅意民宿', item: '檀香静思香囊', qty: 60, days: 18, emoji: '🏡' },
{ name: '国风美妆', item: '玫瑰养颜香囊', qty: 120, days: 25, emoji: '💄' },
{ name: '研学课堂', item: '艾蒲辟秽套装', qty: 150, days: 30, emoji: '🎓' },
{ name: '私人订制·陆', item: '并蒂莲婚庆香囊', qty: 36, days: 40, emoji: '💍' },
{ name: '中医馆', item: '白芷祛湿香囊', qty: 90, days: 22, emoji: '🏥' },
{ name: '茶空间', item: '清心净莲香囊', qty: 45, days: 16, emoji: '🍵' },
{ name: '酒店集团', item: '薰衣安眠香囊', qty: 300, days: 35, emoji: '🏨' },
{ name: '节气礼盒商', item: '四季香囊礼盒', qty: 500, days: 45, emoji: '🎁' }
];
customers: CustomerItem[] = [
{ name: '林晚', city: '杭州', fav: '安神系', emoji: '👩' },
{ name: '程远', city: '北京', fav: '提神系', emoji: '🧑' },
{ name: '苏芮', city: '苏州', fav: '花系', emoji: '👩🦰' },
{ name: '阿澈', city: '成都', fav: '祛湿系', emoji: '🧔' },
{ name: '白露', city: '南京', fav: '静心系', emoji: '👱♀️' },
{ name: '许安然', city: '上海', fav: '安神系', emoji: '👨' },
{ name: '顾清和', city: '扬州', fav: '清雅系', emoji: '🧑🦱' },
{ name: '温故', city: '广州', fav: '养颜系', emoji: '👩🦳' }
];
shops: ShopItem[] = [
{ name: '姑苏荷包铺', city: '苏州', flow: '1200人/月', score: 4.9, emoji: '🏮' },
{ name: '长安香事', city: '西安', flow: '980人/月', score: 4.8, emoji: '🏯' },
{ name: '锦里香囊庄', city: '成都', flow: '1500人/月', score: 4.7, emoji: '🎋' },
{ name: '夫子庙香铺', city: '南京', flow: '1300人/月', score: 4.8, emoji: '⛩️' },
{ name: '西湖香囊集', city: '杭州', flow: '1100人/月', score: 4.6, emoji: '🌊' },
{ name: '南锣鼓巷香店', city: '北京', flow: '900人/月', score: 4.7, emoji: '🏮' }
];
addFormula(name: string, purpose: string, count: number): void {
this.formulas.unshift({ name: name, purpose: purpose, count: count, emoji: '📖' });
}
addHerb(name: string, taste: string, use: number): void {
this.herbs.unshift({ name: name, taste: taste, use: use, from: '新采', emoji: '🌿' });
}
editSachet(idx: number, name: string, formula: string, price: number): void {
this.sachets.splice(idx, 1, {
name: name,
formula: formula,
stitch: this.sachets[idx].stitch,
price: price,
emoji: this.sachets[idx].emoji
});
}
}
function useBarWidth(use: number): string {
return Math.round(use / 20 * 100).toString() + '%';
}
function useColor(use: number): string {
if (use >= 12) {
return '#C08A9E';
}
if (use >= 7) {
return '#3E5C50';
}
return '#C4AAB6';
}
function scoreColor(score: number): string {
if (score >= 4.8) {
return '#C08A9E';
}
if (score >= 4.6) {
return '#3E5C50';
}
return '#C4AAB6';
}
@Component
struct PillTag {
label: string = '';
build() {
Text(this.label)
.fontSize(10).fontColor(COLORS.roseDeep)
.backgroundColor(COLORS.lotus)
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ left: 4, right: 4 })
}
}
@Entry
@Component
struct NangApp {
@State curTab: number = 0
@State fx: boolean = true
@State spin: boolean = false
@State floatDot: boolean = false
@State data: NangData = new NangData()
@State showAddF: boolean = false
@State showAddH: boolean = false
@State showEdit: boolean = false
@State editIdx: number = 0
@State formFName: string = ''
@State formFPurpose: string = ''
@State formFCount: string = '5'
@State formHName: string = ''
@State formHTaste: string = ''
@State formHUse: string = '8'
@State formSName: string = ''
@State formSFormula: string = ''
@State formSPrice: string = '60'
@Builder topArea() {
Column() {
Row() {
Stack() {
Column()
.width(62).height(62).borderRadius(31)
.border({ width: 1.5, color: COLORS.rose })
.borderStyle(BorderStyle.Dashed)
.animation({ duration: 6000, curve: Curve.Linear, iterations: -1 })
Column() {
Text('🎐').fontSize(26)
}
.width(52).height(52).borderRadius(26)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column().width(8).height(8).borderRadius(4)
.backgroundColor(COLORS.warning)
.translate({ y: this.floatDot ? -10 : 2 })
.opacity(this.floatDot ? 0.3 : 1)
.animation({ duration: 1600, curve: Curve.EaseInOut, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 52, y: 4 })
}
.width(64).height(64)
Column() {
Text('香囊坊').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text('荷包香事 · 一囊一季').fontSize(10).fontColor(COLORS.textSub).margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
Text(this.fx ? '✨ 特效开' : '✨ 特效关')
.fontSize(9).fontColor(COLORS.white).backgroundColor(COLORS.rose)
.borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 })
.onClick(() => { this.fx = !this.fx })
}
.width('100%').padding({ left: 14, right: 14, top: 14 })
.onAppear(() => {
this.spin = true;
this.floatDot = true;
})
Row() {
Text('🔍').fontSize(13)
Text('搜索香囊 / 香料 / 香方…').fontSize(11).fontColor(COLORS.textHint).margin({ left: 6 })
}
.width('100%').height(34)
.padding({ left: 12, right: 12 })
.backgroundColor(COLORS.white).borderRadius(17)
.shadow({ radius: 5, color: '#10000000', offsetY: 2 })
.margin({ left: 14, right: 14, top: 12 })
Row() {
ForEach(HOT_TAGS, (t: string) => {
PillTag({ label: t })
}, (t: string) => t)
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ top: 10, bottom: 10 })
}
}
@Builder contentArea() {
Column() {
if (this.curTab === 0) {
SachetTab({ data: this.data, fx: this.fx, onEdit: (i: number) => {
this.editIdx = i;
this.formSName = this.data.sachets[i].name;
this.formSFormula = this.data.sachets[i].formula;
this.formSPrice = this.data.sachets[i].price.toString();
this.showEdit = true;
} })
} else if (this.curTab === 1) {
HerbTab({ data: this.data, fx: this.fx, onAdd: () => {
this.showAddH = true;
} })
} else if (this.curTab === 2) {
FormulaTab({ data: this.data, fx: this.fx, onAdd: () => {
this.showAddF = true;
} })
} else if (this.curTab === 3) {
PatternTab({ data: this.data, fx: this.fx })
} else if (this.curTab === 4) {
EmbroiderTab({ data: this.data, fx: this.fx })
} else if (this.curTab === 5) {
OrderTab({ data: this.data, fx: this.fx })
} else if (this.curTab === 6) {
CustomerTab({ data: this.data, fx: this.fx })
} else {
ShopTab({ data: this.data, fx: this.fx })
}
}
.layoutWeight(1)
.width('100%')
}
@Builder bottomItem(i: number) {
Column() {
Text(TAB_LIST[i].icon).fontSize(16)
.opacity(this.curTab === i ? 1 : 0.55)
Text(TAB_LIST[i].label).fontSize(9)
.fontColor(this.curTab === i ? COLORS.roseDeep : COLORS.textHint)
.fontWeight(this.curTab === i ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 1 })
Column().width(16).height(3).borderRadius(2)
.backgroundColor(this.curTab === i ? COLORS.rose : '#00000000')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 5, bottom: 6 })
.animation({ duration: 250, curve: Curve.EaseOut })
.onClick(() => { this.curTab = i })
}
@Builder bottomBar() {
Column() {
Row() {
ForEach(ROW1_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => TAB_LIST[i].key)
}
.width('100%').padding({ left: 6, right: 6, top: 5 })
Row() {
ForEach(ROW2_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => TAB_LIST[i].key)
}
.width('100%').padding({ left: 6, right: 6, bottom: 6 })
}
.width('100%')
.backgroundColor(COLORS.cardBg)
.shadow({ radius: 10, color: '#1A000000', offsetY: -3 })
}
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(40,25,35,0.5)')
.onClick(onClose)
}
@Builder addFormulaModal() {
Column() {
this.modalOverlay(() => { this.showAddF = false })
Column() {
Row() {
Text('📖 新增香方').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint)
.onClick(() => { this.showAddF = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('香方名称').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '如:合欢安神方' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formFName = v })
Text('主治功效').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '如:安神助眠' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formFPurpose = v })
Text('配伍香料数').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '5' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formFCount = v })
Text('温馨提示:香方需经掌柜试香定稿').fontSize(10)
.fontColor(COLORS.textHint).width('100%').margin({ top: 10, left: 20 })
}
.width('100%')
.constraintSize({ maxHeight: '58%' })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSub)
.backgroundColor('#F3E9EE').borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.onClick(() => { this.showAddF = false })
Text('收录香方').fontSize(14).fontColor(COLORS.white)
.backgroundColor(COLORS.rose).borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => {
this.data.addFormula(this.formFName, this.formFPurpose,
this.formFCount === '' ? 5 : Number(this.formFCount));
this.formFName = '';
this.formFPurpose = '';
this.formFCount = '5';
this.showAddF = false;
})
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor(COLORS.cardBg).borderRadius(16)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '8%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
@Builder addHerbModal() {
Column() {
this.modalOverlay(() => { this.showAddH = false })
Column() {
Row() {
Text('🌿 新增香料').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint)
.onClick(() => { this.showAddH = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('香料名称').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '如:艾草' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formHName = v })
Text('性味').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '如:辛温' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formHTaste = v })
Text('每囊用量(克)').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ placeholder: '8' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formHUse = v })
Text('温馨提示:新香料入库前需阴干七日').fontSize(10)
.fontColor(COLORS.textHint).width('100%').margin({ top: 10, left: 20 })
}
.width('100%')
.constraintSize({ maxHeight: '58%' })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSub)
.backgroundColor('#F3E9EE').borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.onClick(() => { this.showAddH = false })
Text('入库香料').fontSize(14).fontColor(COLORS.white)
.backgroundColor(COLORS.green).borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => {
this.data.addHerb(this.formHName, this.formHTaste,
this.formHUse === '' ? 8 : Number(this.formHUse));
this.formHName = '';
this.formHTaste = '';
this.formHUse = '8';
this.showAddH = false;
})
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor(COLORS.cardBg).borderRadius(16)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '8%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
@Builder editSachetModal() {
Column() {
this.modalOverlay(() => { this.showEdit = false })
Column() {
Row() {
Text('✏️ 编辑香囊').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint)
.onClick(() => { this.showEdit = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('香囊名称').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ text: this.formSName, placeholder: '香囊名称' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formSName = v })
Text('搭配香方').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ text: this.formSFormula, placeholder: '搭配香方' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formSFormula = v })
Text('售价(元)').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
TextInput({ text: this.formSPrice, placeholder: '售价' })
.placeholderColor('#BBBBBB').fontSize(14).width('100%')
.backgroundColor(COLORS.cream).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.formSPrice = v })
Text('提示:改价后各分铺同步更新').fontSize(10)
.fontColor(COLORS.textHint).width('100%').margin({ top: 10, left: 20 })
}
.width('100%')
.constraintSize({ maxHeight: '58%' })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSub)
.backgroundColor('#F3E9EE').borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.onClick(() => { this.showEdit = false })
Text('保存修改').fontSize(14).fontColor(COLORS.white)
.backgroundColor(COLORS.roseDeep).borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => {
this.data.editSachet(this.editIdx, this.formSName, this.formSFormula,
this.formSPrice === '' ? 60 : Number(this.formSPrice));
this.showEdit = false;
})
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor(COLORS.cardBg).borderRadius(16)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '8%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
build() {
Stack() {
Column() {
this.topArea()
this.contentArea()
this.bottomBar()
}
.width('100%').height('100%').backgroundColor(COLORS.bg)
if (this.showAddF) {
this.addFormulaModal()
}
if (this.showAddH) {
this.addHerbModal()
}
if (this.showEdit) {
this.editSachetModal()
}
}
.width('100%').height('100%')
}
}
// ============ 香囊 ============
@Component
struct SachetTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
onEdit: (idx: number) => void = () => {}
build() {
Column() {
Row() {
Text('🎐 在售香囊').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text(this.data.sachets.length.toString() + ' 款 · 一囊一季').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.sachets, (s: SachetItem, i: number) => {
Row() {
Column() {
Text(s.emoji).fontSize(18)
}
.width(42).height(42).borderRadius(21)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(s.formula + ' · ' + s.stitch).fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Column() {
Text('¥' + s.price).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(COLORS.roseDeep)
Text('✏️').fontSize(12).fontColor(COLORS.rose).margin({ top: 4 })
.onClick(() => { this.onEdit(i) })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (s: SachetItem) => s.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 香料(含用量条) ============
@Component
struct HerbTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
onAdd: () => void = () => {}
build() {
Column() {
Row() {
Row() {
Text('🌿 香料库').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('+ 入库香料').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.green).borderRadius(10)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.onClick(() => { this.onAdd() })
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
}
Scroll() {
Column() {
Column() {
Text('⚖️ 每囊用量对比(克)').fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain).width('100%')
ForEach(this.data.herbs, (h: HerbItem) => {
Row() {
Text(h.emoji).fontSize(13)
Text(h.name).fontSize(10).fontColor(COLORS.textMain).width(56).margin({ left: 5 })
Column() {
Column().width(useBarWidth(h.use)).height(8)
.backgroundColor(useColor(h.use)).borderRadius(4)
}
.layoutWeight(1).height(8).backgroundColor('#F0E5E9').borderRadius(4)
Text(h.use.toString() + 'g').fontSize(9).fontColor(COLORS.textHint).width(28)
.textAlign(TextAlign.End)
}
.width('100%').margin({ top: 8 })
}, (h: HerbItem) => h.name)
}
.width('100%').padding(12)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 6, color: '#0F000000', offsetY: 2 })
.margin({ top: 10 })
ForEach(this.data.herbs, (h: HerbItem) => {
Row() {
Column() {
Text(h.emoji).fontSize(17)
}
.width(38).height(38).borderRadius(19)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(h.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(h.taste + ' · 产自' + h.from).fontSize(9)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text(h.use.toString() + 'g/囊').fontSize(10).fontColor(COLORS.roseDeep)
}
.width('100%').padding(10)
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (h: HerbItem) => h.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 香方 ============
@Component
struct FormulaTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
onAdd: () => void = () => {}
build() {
Column() {
Row() {
Row() {
Text('📖 香方集').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('+ 新增香方').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.rose).borderRadius(10)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.onClick(() => { this.onAdd() })
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
}
Scroll() {
Column() {
ForEach(this.data.formulas, (f: FormulaItem) => {
Row() {
Column() {
Text(f.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(f.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(f.purpose + ' · 配伍 ' + f.count + ' 味').fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('📖').fontSize(14).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (f: FormulaItem) => f.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 纹样 ============
@Component
struct PatternTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
build() {
Column() {
Row() {
Text('🪡 绣样图库').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('一针一线皆祝福').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.patterns, (p: PatternItem) => {
Row() {
Column() {
Text(p.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(p.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(p.meaning + ' · ' + p.stitch).fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🪡').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (p: PatternItem) => p.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 绣娘 ============
@Component
struct EmbroiderTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
build() {
Column() {
Row() {
Text('🧵 绣娘名录').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('四大名绣齐聚').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.embroiderers, (e: EmbroiderItem) => {
Row() {
Column() {
Text(e.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(e.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(e.skill + ' · 绣龄 ' + e.years + '年').fontSize(10)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🧵').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (e: EmbroiderItem) => e.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 订单 ============
@Component
struct OrderTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
build() {
Column() {
Row() {
Text('📜 订单台账').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('在排 ' + this.data.orders.length.toString() + ' 单').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.orders, (o: OrderItem) => {
Row() {
Text(o.emoji).fontSize(15)
Column() {
Text(o.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Text(o.item + ' × ' + o.qty).fontSize(9)
.fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Column() {
Text(o.days.toString() + '天').fontSize(11).fontWeight(FontWeight.Bold)
.fontColor(COLORS.warning)
Text('交期').fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (o: OrderItem) => o.name + o.item)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 香客 ============
@Component
struct CustomerTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
build() {
Column() {
Row() {
Text('👥 香客名录').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('老客回购率 58%').fontSize(10).fontColor(COLORS.green)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.customers, (c: CustomerItem) => {
Row() {
Column() {
Text(c.emoji).fontSize(17)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.cream).justifyContent(FlexAlign.Center)
Column() {
Text(c.name + ' · ' + c.city).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain)
Text('偏爱 ' + c.fav).fontSize(9).fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text('🎐').fontSize(13).opacity(0.6)
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (c: CustomerItem) => c.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
// ============ 铺面 ============
@Component
struct ShopTab {
@ObjectLink data: NangData
@Prop fx: boolean = false
@State pulse: boolean = false
build() {
Column() {
Row() {
Text('🏮 分铺经营').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
Column().layoutWeight(1)
Text('6 城联动').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ left: 12, right: 12, top: 10 })
Scroll() {
Column() {
ForEach(this.data.shops, (s: ShopItem) => {
Row() {
Column() {
Text(s.emoji).fontSize(18)
}
.width(42).height(42).borderRadius(21)
.backgroundColor(COLORS.lotus).justifyContent(FlexAlign.Center)
Column() {
Text(s.name + ' · ' + s.city).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textMain)
Text('客流 ' + s.flow).fontSize(9).fontColor(COLORS.textSub).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 9 })
Text(s.score.toString() + ' ★').fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(scoreColor(s.score))
}
.width('100%').padding(11)
.backgroundColor(COLORS.cardBg).borderRadius(14)
.shadow({ radius: 5, color: '#0F000000', offsetY: 2 })
.margin({ top: 7 })
.animation({ duration: 350, curve: Curve.EaseOut })
}, (s: ShopItem) => s.name)
}
.width('100%').padding({ left: 10, right: 10, bottom: 10 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.onAppear(() => { this.pulse = true })
}
.width('100%').height('100%')
}
}
十五、开发环境搭建
15.1 安装 DevEco Studio
DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

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

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

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

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

十六、总结
通过对这款香囊坊非遗手作平台的深度记录,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、组件化工程、文化内涵等多维度能力的综合体现。
在架构层面,该应用采用了多组件架构设计——将整个应用拆分为10个独立组件(1入口+1标签+8内容),每个组件独立封装、职责单一,通过@ObjectLink共享聚合数据、通过回调函数实现父子通信、通过@Prop传递特效开关,组件数量多、架构清晰。与单组件内@Builder构建所有页面的架构不同,本应用将内容页都抽离为独立@Component子组件,入口组件NangApp只负责状态管理和组件调度,实现了真正的高内聚低耦合。这种多组件架构设计虽然增加了组件间通信的复杂度(@ObjectLink/@Prop/回调函数),但大幅提升了代码的可维护性和可复用性,是中大型ArkUI应用的标准架构实践,也是本应用工程化设计的最大亮点。
在数据层面,应用独创了**@Observed聚合数据类设计——NangData一个类包含sachets/herbs/formulas/patterns/embroiderers/orders/customers/shops八个数组,每个数组6-12条数据,用对象字面量初始化(非new构造函数),@Observed装饰整个类,配合@ObjectLink在子组件间共享引用,实现"一个数据对象贯穿所有页面"的聚合数据模式。这种设计将所有业务数据集中管理,子组件通过@ObjectLink data: NangData接收共享引用,修改数据时所有引用该数据的子组件自动响应式刷新,避免了每个页面独立@State数据数组的重复定义,是ArkUI跨组件数据共享的高级实践。应用还定义了3个颜色与尺寸映射函数(useBarWidth用量宽度映射/useColor用量三级着色/scoreColor评分三级着色),将颜色和尺寸映射逻辑从UI层抽离,每个函数根据不同维度返回对应的颜色值或百分比宽度,实现"数据即配色、数据即尺寸"的动态映射模式。8个元数据接口实现数据结构规范化,数据规模达76条,完整呈现中国香囊非遗手作平台的数据生态。本应用数据最大的特色是中国传统香囊文化全覆盖**——12款香囊覆盖安神助眠/驱蚊辟秽/提神醒脑/祛湿健脾/静心冥想/养颜悦色/开胃理气/清心除烦八大功效,绣法覆盖平针绣/锁针绣/打籽绣/辫子绣/齐针绣/滚针绣/网绣/盘金绣八种绣法;10种香料产地覆盖南阳/亳州/海南/太仓/伊犁/咸宁/安庆/广州/新会/福州十地;8方香方配伍数从4味到8味;8种纹样寓意覆盖并蒂同心/年年有余/福寿绵长/多子多福/万事如意/美满姻缘/子孙万代/喜上眉梢八大吉祥寓意;8位绣娘覆盖四大名绣+顾绣+苗绣+十字绣+贴布绣八种绣法。这些文化数据让应用不只是一个技术演示,更是一个中国香囊文化的数字载体。
在视觉层面,本应用采用"玫瑰粉荷花色墨绿奶油"中国香囊非遗主题设计——bg #FAF4EC 米杏色(模拟古绢温润质感,香囊文化载体),cardBg #FFFFFF 白,rose #C08A9E 玫瑰粉(香囊主色调,模拟丝缎玫瑰粉),roseDeep #8E5A72 深玫瑰(深色强调),lotus #E8D5DC 荷花粉(浅色装饰,与"荷包"的"荷"呼应),green #3E5C50 墨绿(辅助色,与艾草🌿呼应),cream #FDF6F1 奶油白(表单背景,模拟丝绸奶油白),textMain #4A3340 深褐紫(深色文字,非纯黑更有文化感)。主色体系采用"玫瑰粉+荷花粉+墨绿+奶油+米杏"五色组合,五色各司其职形成"以玫瑰粉定囊、以荷花色定饰、以墨绿定香、以奶油定表、以米杏定底"的中国香囊语义着色体系。本应用最大的视觉特色是头部虚线圆环旋转动画与浮动光点呼吸效果——62x62虚线圆环6000ms线性无限旋转模拟香囊丝线缠绕,8x8warning赭金色光点1600ms EaseInOut Alternate往返呼吸模拟香气飘逸,"慢旋快浮"的动画层次营造了香囊"丝线缠绕、香气飘逸"的动态意境。统一的色彩系统是整个应用的视觉基石,玫瑰粉荷花色墨绿奶油主题配合3个颜色分级函数和多维度动态着色,营造出既玫瑰粉柔美又墨绿沉稳的中国香囊非遗氛围。
在交互层面,应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:addFormulaModal新增香方(三字段TextInput表单:香方名称/主治功效/配伍香料数,收录香方rose玫瑰底)、addHerbModal入库香料(三字段TextInput表单:香料名称/性味/每囊用量,入库香料green墨绿底)、editSachetModal编辑香囊(三字段TextInput表单:香囊名称/搭配香方/售价,保存修改roseDeep深玫瑰底)。三个弹窗都是@Builder方法内联在入口组件中,通过if条件渲染控制显隐,遮罩用rgba(40,25,35,0.5)(50%深紫)硬编码,弹窗主体与遮罩用Column层叠,整体zIndex(999)覆盖主内容。香囊页✏️点击触发editSachetModal(editIdx记录目标索引,预填当前值),香料页✚点击触发addHerbModal,香方页✚点击触发addFormulaModal,形成了完整的"编辑香囊→入库香料→新增香方"交互闭环。特效开关fx通过@Prop传递给子组件,点击切换特效状态,增加了应用的趣味性和互动感。用量对比横条用useBarWidth百分比映射+useColor三级着色(高用玫瑰粉/中用墨绿/低用浅灰粉),分铺评分用scoreColor三级着色(与useColor逻辑一致保证视觉统一),交期用warning赭金色着色提醒排产,回购率用green墨绿色着色表示健康,都是本应用交互设计的精细之处。
技术特性对比表
| 维度 | 香囊坊 | 筷子坊 | 内画鼻烟壶坊 | 折扇坊 |
|---|---|---|---|---|
| 架构模式 | 多组件(10组件) | 多组件(11组件) | 单组件@Builder | 单组件@Builder |
| Tab数量 | 8 Tab双行四列 | 5 Tab单排 | 8 Tab双行四列 | 8 Tab双行四列 |
| 数据类 | @Observed NangData | @Observed ChopstickData | @Observed SnuffShopData | @Observed FanShopData |
| 数据规模 | 76条(8数组) | 83条(5数组+4统计) | 76条(8数组) | 76条(8数组) |
| 颜色函数 | 3个(useBarWidth/useColor/scoreColor) | 5个 | 6个 | 6个 |
| 弹窗数量 | 3个(新增香方/入库香料/编辑香囊) | 4个 | 3个 | 3个 |
| 主题色 | 玫瑰粉#C08A9E | 乌木黑#2F2A26 | 温润玉色#7FA08C | 竹青#7A8B5A |
| 辅助色 | 荷花粉#E8D5DC | 朱红#B03A2E | 琥珀#B08D57 | 金#C9A227 |
| 头部动画 | 虚线圆环旋转+浮动光点 | 🥢旋转90°+🏮位移 | 瓶身scale呼吸+星点闪烁 | 扇骨辐线开合+流苏 |
| 特效开关 | ✨ fx @Prop传递 | 无 | 无 | 无 |
| 文化主题 | 香囊·荷包香事 | 筷子·一箸千年 | 鼻烟壶·壶中丹青 | 折扇·扇开清凉 |
| 统计图表 | 用量对比横条 | 柱状图+TOP6+进度条 | 柱状图+硬度横条 | 柱状图+难度横条 |

标签: HarmonyOS · ArkTS · API 24 · 中国香囊 · 玫瑰粉荷花色 · 多组件架构 · @ObjectLink · 一囊一季 · 移动端开发
更多推荐


所有评论(0)