泠泠七弦上,静听松风寒。古琴,中国最古老的弹拨乐器之一,三千年来与文人雅士相伴,从九霄环佩到焦尾绿绮,从伏羲式到仲尼式,每一张琴都藏着一段故事。本文完整记录一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式框架构建的「古琴坊」非遗手作平台,从木褐琴徽金宣纸底的色彩主题,到深木褐头部🎵旋转与✨scale缩放交互动画,再到12点木褐竖线琴徽金圆古琴装饰纹、5Tab差异化布局(古琴本周销量柱状图+名器列表/琴弦名琴TOP6+弦库韧度/琴徽制琴工艺热度+琴徽名录/订单琴式构成+订单台账/客评口碑列表)、多组件11组件架构、@ObjectLink数据共享、@Observed聚合数据类、5个颜色分级函数、十六进制透明度拼接、木褐woodBrown与琴徽金huiGold装饰、layoutWeight动态横向进度条、古琴图标区3根琴弦竖线、四大弹窗(定制古琴/修改订单/撤下琴弦/琴徽详情),附完整源码与运行效果截图。


一、引言:泠泠七弦,松风入耳

古琴,又称瑶琴、玉琴、七弦琴,是中国最古老的弹拨乐器之一,有文字可考的历史超过三千年。从《诗经》"窈窕淑女,琴瑟友之"到伯牙子期高山流水遇知音,从嵇康《广陵散》到司马相如《凤求凰》,古琴早已超越乐器本身,成为中国文人精神的象征——"琴棋书画"四艺之首,士无故不撤琴瑟。一张好琴,从选材听音开始,经开料制形、挖槽纳音、合琴裹布、灰胎打磨、上弦调音,历时数月乃至数年,方能成就一张可传百年的名器。九霄环佩、玉壶冰、碧涧流泉、沧海龙吟、太古遗音、鹤鸣秋月、绿绮、焦尾……这些流传千年的琴名,每一个背后都藏着一段制琴人与琴的故事。

然而,随着现代生活节奏加快,古琴这门古老的艺术正面临传承困境——年轻人不了解古琴文化,制琴匠人缺乏展示和销售渠道,古琴产品标准化程度低、琴式不透明、琴弦选择困难,正成为古琴非遗传承的现实痛点。一个优秀的古琴手作平台,不仅需要覆盖从"古琴名器"到"琴弦库"再到"琴徽名录"的完整产品展示闭环,还要在订单管理、客户评价、匠人评级等维度做到精准而高效。它需要以古琴陈列为核心入口,横向扩展出琴弦、琴徽、订单、客评五大功能模块,每个模块采用完全不同的布局风格,形成一个完整闭环的古琴非遗手作生态。

正是在这样的背景下,古琴坊类非遗手作平台应运而生,并迅速成为古琴爱好者和汉服圈层的文化入口。一个优秀的古琴应用,需要以古琴名器陈列为核心入口,横向扩展出琴弦、琴徽、订单、客评五大功能模块,每个模块采用完全不同的布局风格——古琴=本周销量柱状图+古琴名器列表、琴弦=名琴销量TOP6横向进度条+琴弦库韧度列表、琴徽=制琴工艺热度横向进度条+琴徽名录列表、订单=琴式构成横向进度条+订单台账列表、客评=客评口碑列表,形成了一个完整闭环的古琴非遗手作生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了多组件架构设计、@ObjectLink数据共享、@Observed聚合数据类、颜色分级函数、十六进制透明度拼接、木褐woodBrown与琴徽金huiGold装饰、头部scale缩放动画、奇偶行交替条纹、子组件封装等关键技术点。

古琴坊应用整体运行效果 - 深木褐头部古琴坊标题与12点木褐竖线琴徽金圆装饰纹及本周销量柱状图古琴名器列表


二、应用整体架构概览

整个应用采用了"多组件架构 + @ObjectLink数据共享 + 配置驱动 + 子组件封装"的四层设计模式。入口组件 GuqinApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态、四个弹窗的显隐状态、头部交互动画状态以及聚合数据对象。与单组件内@Builder构建所有页面的架构不同,本应用将整个应用拆分为 11 个独立组件:1 个入口组件 GuqinApp + 1 个标签子组件 GuqinTag + 5 个内容子组件(GuqinContent古琴/StringContent琴弦/HuiContent琴徽/GuqinOrderContent订单/GuqinReviewContent客评)+ 4 个弹窗子组件(AddGuqinModal定制古琴/EditGuqinOrderModal修改订单/DeleteStringModal撤下琴弦/DetailHuiModal琴徽详情)。每个子组件独立封装,职责单一,通过 @ObjectLink 共享聚合数据,通过回调函数(onAdd/onDel/onDetail/onEdit/onClose)实现父子组件通信,是本应用工程化设计的最大亮点,也是 ArkUI 多组件架构的典型实践。

应用一共划分为五个一级页面:古琴(GUQIN)、琴弦(STRING)、琴徽(HUI)、订单(ORDER)以及客评(REVIEW)。底部 tabBar 采用非遗设计——5个Tab(古琴🎼/琴弦🪢/琴徽✨/订单📦/客评⭐)单排布局,激活态文字 t.color 加粗 + 背景 t.color+‘14’(10%透明色底)圆角10 + border 1px t.color,非激活态 textHint 灰字 + 透明背景,tabBar 顶部 3px primary 木褐条。五个Tab每个布局完全不同——古琴采用本周销量柱状图+古琴名器列表(12款历史名琴,价格分级着色,奇偶行交替,图标装饰区带3根琴弦竖线)、琴弦采用名琴销量TOP6横向进度条+琴弦库列表(12种弦,韧度进度条,🗑️点击删除弹窗)、琴徽采用制琴工艺热度横向进度条+琴徽名录列表(12徽,五音十二律,👁️点击详情弹窗)、订单采用琴式构成横向进度条+订单台账列表(12订单,金额分级着色,✏️点击编辑弹窗)、客评采用客评口碑列表(12条评价,⭐星星repeat,标签分级着色),这种"一页一型"的差异化设计最大程度还原了真实古琴非遗手作平台的交互体验。

在数据层面,应用独创了**@Observed聚合数据类**设计——GuqinData 一个类包含 guqins/strings/huis/orders/reviews 五个数组,每个数组 12 条数据,用对象字面量初始化(非 new 构造函数),@Observed 装饰整个类,配合 @ObjectLink 在子组件间共享引用,实现"一个数据对象贯穿所有页面"的聚合数据模式。这种设计将所有业务数据集中管理,子组件通过 @ObjectLink data: GuqinData 接收共享引用,修改数据时所有引用该数据的子组件自动响应式刷新,是 ArkUI 跨组件数据共享的高级实践。应用还定义了 9 个元数据接口(GuqinItem/StringItem/HuiItem/GuqinOrderItem/GuqinReviewItem/WeekGuqinMeta/StyleShareMeta/GuqinHotMeta/GuqinTopMeta)和 4 个统计常量数组(WEEK_SOLD 7天销量/STYLE_SHARE 4种琴式构成/GUQIN_HOT 6种制琴工艺热度/GUQIN_TOP 6款名琴销量排行),将统计数据与业务数据分离,实现配置驱动设计。

在颜色逻辑层面,应用定义了 5 个颜色分级函数(getStyleColor琴式5类/getStringColor韧度3级/getGuqinColor价格4级/getOrderColor金额4级/getReviewTagColor标签5类),将颜色映射逻辑从 UI 层抽离,每个函数根据不同维度(琴式/韧度/价格/金额/标签)返回对应的颜色值,实现"数据即配色"的动态着色模式。特别的是,本应用大量使用十六进制透明度拼接技巧——color + 十六进制透明度后缀('14’10%/'40’25%/'55’33%/'66’40%/'80’50%/'0D’5%/'33’20%/'2E’18%),用十六进制透明度后缀实现半透明背景和边框,无需额外定义半透明颜色常量,是 ArkUI 颜色处理的实用技巧。本应用配色最大特色是木褐 woodBrown #5A4030 与琴徽金 huiGold #C9A24B——木褐用于装饰竖线(模拟琴弦),琴徽金用于装饰圆(模拟琴徽,带1px白边),与宣纸底 bg #F2EDE0 形成"木褐琴徽金宣纸"的经典古琴配色,是本应用视觉设计的灵魂。这种将所有色彩集中管理、配合5个颜色分级函数和十六进制透明度拼接与木褐琴徽金装饰与多维度动态着色的做法保证了全局一致性,降低了维护成本。

在交互层面,应用通过四个独立弹窗子组件实现了完整的增删改查交互闭环:AddGuqinModal 定制古琴(琴名/琴式/预算三行展示,确认定制 primary 木褐底)、EditGuqinOrderModal 修改订单(客户/数量60→66/金额39.6万,保存修改)、DeleteStringModal 撤下琴弦(@Prop title 传入弦名,确认撤下 danger 红底)、DetailHuiModal 琴徽详情(@Prop name 传入徽名,大图标+琴徽螺钿玉石描述+音位+匠人评级,知道了按钮)。四个弹窗各自独立组件,通过 @Prop 接收参数(DeleteStringModal的title、DetailHuiModal的name),通过 onClose 回调关闭,遮罩用 #66000000(40%黑)硬编码。古琴页➕点击触发 AddGuqinModal,琴弦页🗑️点击触发 DeleteStringModal(delStringName 记录弦名),琴徽页👁️点击触发 DetailHuiModal(detailHuiName 记录徽名),订单页✏️点击触发 EditGuqinOrderModal,形成了完整的"定制古琴→撤下琴弦→查看琴徽→修改订单"交互闭环。

古琴坊琴徽详情弹窗运行效果 - 弹窗叠加琴徽页显示大图标琴徽名螺钿玉石描述及音位匠人评级

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


三、古琴页:本周销量柱状图与古琴名器列表

古琴页以"古琴名器陈列"为核心,采用"本周销量柱状图+古琴名器列表"的两层布局。

古琴模块 - 本周古琴销量柱状图周六30张峰值与12款历史名器列表价格分级着色及3根琴弦竖线装饰

页面顶部第一部分是本周销量卡,Column 包裹,padding 14,cardBg 白底,圆角20,border 1px primary+‘55’(33%透明木褐):

  • 装饰行:Row,3x14木褐竖线(woodBrown,圆角2,模拟琴弦)+ layoutWeight(1) 2px primary+'55’横线 + 9x9琴徽金圆(huiGold,圆角5,带1px白边,模拟琴徽)+ 3x14木褐竖线 + layoutWeight(1) 2px横线 + 3x14木褐竖线,木褐琴弦琴徽金装饰分隔线。
  • 标题行:Row,"📈 本周古琴销量"textPrimary深褐字13号加粗 + "周六30张"primary木褐字9号。
  • 柱状图行:Row,ForEach WEEK_SOLD 7天,每天 Column layoutWeight 1 alignItems Center:
    • 销量数字:w.value + ‘’,8号,fontColor w.value>=26 ? primary木褐 : accentLight赭石金(周末≥26用木褐高亮)。
    • 柱子:Column width 16,height = w.value(直接用数值当高度!最大30px),backgroundColor w.value>=26 ? primary木褐 : accentLight赭石金,圆角8,margin top 4。
    • 星期:w.day textSecondary灰字9号,margin top 4。
  • 柱状图容器:width 100%,height 150,alignItems VerticalAlign.Bottom(柱子从底部生长),margin top 10。

7 天销量数据:周一8/周二12/周三15/周四18/周五22/周六30(峰值,primary木褐高亮!)/周日26(也≥26,primary木褐高亮)。柱子高度直接用 w.value 数值(最大30px),非比例映射,是本应用柱状图设计的特色——数值即高度,简单直观。古琴是高端手工商品,周销量仅8-30张,与之前扎染周销量26-70件形成鲜明对比,体现了古琴"小众高端"的产品定位。周末(周六30/周日26)用 primary 木褐色高亮,工作日用 accentLight 赭石金色,形成"周末高峰木褐/工作日赭石金"的视觉区分,周六30张峰值是本周最高点。

第二部分是古琴名器,Column,margin top 12:

  • 标题行:Row,"🎼 古琴名器"textPrimary深褐字13号加粗 + "点击➕制琴"textHint灰字9号 + "➕"14号,点击触发 onAdd 回调(showAddGuqin=true)。
  • 名琴列表:ForEach this.data.guqins 12款,每款 Row,padding 10,backgroundColor i%2===0 ? cardBg白 : cardAlt浅米黄(奇偶行交替条纹!),圆角18,border 1px line,margin top 8:
    • 图标装饰区:Column width 60,padding 上下10/6,backgroundColor getGuqinColor(p.price)+‘0D’(5%透明色底),圆角22,border 1px getGuqinColor(p.price)+‘33’(20%透明色边):
      • 圆形图标:Column width 44 height 44,justifyContent Center,alignItems Center,backgroundColor getGuqinColor(p.price)+‘14’(10%透明色底),圆角22,border 1px getGuqinColor(p.price)+‘66’(40%透明色边),Text p.emoji 18号居中。
      • 3根琴弦竖线:Row width 30 justifyContent Center margin top 5,3根 2x10 getGuqinColor(p.price) 竖线(圆角1,间距2px,模拟古琴七弦,这里简化为3根代表)。
    • 信息区:Column layoutWeight 1 alignItems Start margin left 10:
      • 名称价格行:Row width 100% justifyContent SpaceBetween,p.name textPrimary深褐字13号加粗 + “¥”+p.price getGuqinColor(p.price)色10号加粗 margin left 8。
      • 标签行:Row width 100% justifyContent Start margin top 6,GuqinTag({text: p.style, color: getStyleColor(p.style)})(琴式标签,琴式分级色)+ GuqinTag({text: p.wood+‘面’, color: accentLight赭石金})(木材标签,赭石金固定色)+ "七弦十三徽"textHint灰字9号 margin left 8。

12 款历史名琴:九霄环佩(伏羲式,杉木,¥6800,🎼)、玉壶冰(仲尼式,杉木,¥5800,🧊)、碧涧流泉(蕉叶式,桐木,¥7200,🌊)、沧海龙吟(落霞式,杉木,¥6600,🐉)、太古遗音(伏羲式,桐木,¥6200,📜)、鹤鸣秋月(仲尼式,杉木,¥5500,🦢)、绿绮(神农式,桐木,¥5000,💚)、焦尾(师旷式,梓木,¥7800,🔥)、绕梁(列子式,杉木,¥5400,🌀)、灵机(灵机式,桐木,¥4600,⚙️)、凤势(凤势式,杉木,¥5900,🪶)、连珠(连珠式,桐木,¥4800,📿)。这些都是中国古琴史上的经典名琴——九霄环佩是唐代雷威所斫、现藏故宫的传世名琴,焦尾是东汉蔡邕所斫的四大名琴之一,绿绮是司马相如的琴,绕梁是春秋战国时期的名琴,每一张琴都有深厚的历史文化底蕴。价格从¥4600(灵机)到¥7800(焦尾),getGuqinColor 四级着色(≥7000 danger红/≥6000 primary木褐/≥5200 primaryLight浅木褐/其他 success墨绿),¥7800焦尾/¥7200碧涧流泉用 danger 红色(≥7000最高端,焦尾是四大名琴之一价格最高),¥6800九霄环佩/¥6600沧海龙吟/¥6200太古遗音用 primary 木褐(≥6000中高端,九霄环佩是传世名琴),¥5800玉壶冰/¥5500鹤鸣秋月/¥5400绕梁/¥5900凤势用 primaryLight 浅木褐(≥5200中端),¥5000绿绮/¥4600灵机/¥4800连珠用 success 墨绿色(其他入门级),形成"以价格定颜色"的动态着色模式。奇偶行交替(i%2=0白/i%2=1浅米黄)营造列表条纹效果,图标装饰区的3根琴弦竖线(2x10 getGuqinColor竖线,间距2px)模拟古琴七弦,是本应用古琴名器列表设计的核心亮点。GuqinTag 子组件封装标签样式(9号字+color+'14’背景+color+'40’边框+圆角10),实现标签组件复用。琴式标签用 getStyleColor 琴式分级着色(伏羲式木褐/仲尼式浅木褐/蕉叶式赭石金/落霞式朱砂红/其他墨绿),木材标签用 accentLight 赭石金固定色,"七弦十三徽"标注古琴制式,完整呈现古琴名器的文化信息。


四、琴弦页:名琴销量TOP6与琴弦库韧度

琴弦页以"古琴用弦"为核心,采用"名琴销量TOP6横向进度条+琴弦库韧度列表"的两层布局。

琴弦模块 - 名琴销量TOP6横向进度条九霄环佩居首与12种琴弦库列表韧度进度条

页面顶部第一部分是名琴销量TOP6卡,Column 包裹,padding 14,cardBg 白底,圆角20,border 1px primary+‘55’:

  • 标题行:Row,"🏆 名琴销量 TOP6"textPrimary深褐字13号加粗 + "九霄环佩居首"textHint灰字9号。
  • TOP6列表:ForEach GUQIN_TOP 6款,每款 Row width 100% margin top 6:
    • 名称:t.name textPrimary深褐字10号,width 90。
    • 横向进度条:Row layoutWeight 1,Column layoutWeight(t.sold) height 12 backgroundColor t.color 圆角6(已售部分,动态layoutWeight!)+ Column layoutWeight(100-t.sold) height 12 backgroundColor line 圆角6(未售部分)。
    • 销量:t.sold+‘把’ t.color色9号,width 40 textAlign End。

6 款名琴销量排行:九霄环佩96把(primary木褐)、玉壶冰92把(primaryLight浅木褐)、碧涧流泉89把(accentLight赭石金)、沧海龙吟85把(success墨绿)、太古遗音81把(accent朱砂红)、鹤鸣秋月77把(textSecondary灰褐)。横向进度条用 layoutWeight 动态分配(t.sold 和 100-t.sold),非固定宽度非百分比,是 ArkUI 特有的弹性布局进度条实现——已售部分占 t.sold 份,未售部分占 100-t.sold 份,总份数100,自动按比例分配宽度。九霄环佩96把居首,进度条几乎满格,是本应用琴弦页TOP6设计的核心亮点。注意这里TOP6标题是"名琴销量"但放在琴弦页,是因为琴弦页需要一个统计卡作为页面头部,用名琴销量排行作为引流,引导用户关注名琴搭配的琴弦。

第二部分是琴弦库,Column,margin top 12:

  • 标题行:Row,"🪢 琴弦库"textPrimary深褐字13号加粗 + "点击🗑去弦"textHint灰字9号。
  • 琴弦列表:ForEach this.data.strings 12种,每种 Row,padding 10,backgroundColor i%2===0 ? cardBg白 : cardAlt浅米黄(奇偶行交替),圆角18,border 1px line,margin top 8:
    • 图标装饰区:Column width 56,padding 上下10/6,backgroundColor getStringColor(c.level)+‘0D’,圆角16,border 1px getStringColor(c.level)+‘33’:
      • 图标:Text c.emoji 18号居中。
      • 装饰点行:Row width 28 justifyContent Center margin top 5,10x10 getStringColor圆(圆角5,带2px白边,模拟琴徽)+ 8x8 huiGold琴徽金圆(圆角4,margin left 4)。
    • 信息区:Column layoutWeight 1 alignItems Start margin left 10:
      • 名称张力行:Row width 100% justifyContent SpaceBetween,c.name textPrimary深褐字13号加粗 + c.tension textSecondary灰字9号 margin left 8(中张/高张/低张)。
      • 韧度行:Row width 100% margin top 6,"韧度"textHint灰字9号 + Row layoutWeight 1 margin left 6,Column layoutWeight(c.level) height 6 backgroundColor getStringColor(c.level) 圆角3(韧度进度条!)+ Column layoutWeight(100-c.level) height 6 backgroundColor line 圆角3。
    • 删除按钮:Text '🗑️’14号,点击触发 onDel(c.name) 回调(delStringName=c.name,showDeleteString=true)。

12 种琴弦:太古丝弦(蚕丝,96,中张,🪢)、冰弦(冰丝,95,高张,🧵)、缠丝弦(蚕丝缠弦,94,低张,🕸️)、钢弦一弦(钢弦,93,高张,🔩)、钢弦七弦(钢弦,92,中张,⚙️)、梅花丝弦(蚕丝,95,中张,🌸)、玄丝弦(蚕丝,91,低张,⚫)、云纹钢弦(钢弦,90,中张,☁️)、织锦丝弦(蚕丝,94,高张,🧣)、青竹弦(竹丝,89,低张,🎋)、金丝弦(金缠丝,93,高张,✨)、玉弦(蚕丝,95,中张,💠)。韧度从89到96,getStringColor 三级着色(≥95 primary木褐/≥92 primaryLight浅木褐/其他 accentLight赭石金),太古丝弦96/冰弦95/梅花丝弦95/玉弦95用 primary 木褐(最高韧度,丝弦精品),缠丝弦94/钢弦一弦93/钢弦七弦92/织锦丝弦94/金丝弦93用 primaryLight 浅木褐(中高韧度),玄丝弦91/云纹钢弦90/青竹弦89用 accentLight 赭石金(基础韧度),形成"以韧度定颜色"的动态着色模式。韧度进度条用 layoutWeight 动态分配(c.level 和 100-c.level),内嵌在列表行中,是本应用琴弦库设计的核心亮点。12种琴弦覆盖古琴常用弦材(蚕丝/冰丝/钢弦/竹丝/金缠丝等),张力分中张/高张/低张三种,完整呈现古琴用弦生态。点击🗑️触发 DeleteStringModal 撤下琴弦弹窗(@Prop title 传入弦名,确认后从弦库移除),形成"浏览→撤下"交互闭环。


五、琴徽页:制琴工艺热度与琴徽名录

琴徽页以"古琴琴徽"为核心,采用"制琴工艺热度横向进度条+琴徽名录列表"的两层布局。

琴徽模块 - 制琴工艺热度横向进度条选材听音97居首与12琴徽名录列表五音十二律

页面顶部第一部分是制琴工艺热度卡,Column 包裹,padding 14,cardBg 白底,圆角20,border 1px primary+‘55’:

  • 标题行:Row,"🔥 制琴工艺热度"textPrimary深褐字13号加粗 + "选材听音97"primary木褐字9号。
  • 热度列表:ForEach GUQIN_HOT 6工艺,每款 Row width 100% margin top 6:
    • 名称:d.name textPrimary深褐字10号,width 80。
    • 横向进度条:Row layoutWeight 1,Column layoutWeight(d.heat) height 14 backgroundColor d.color 圆角7(热度部分)+ Column layoutWeight(100-d.heat) height 14 backgroundColor line 圆角7(剩余部分)。
    • 热度值:d.heat+‘’ d.color色9号,width 32 textAlign End。

6 种制琴工艺热度:选材听音97(primary木褐)、开料制形95(primaryLight浅木褐)、挖槽纳音94(accentLight赭石金)、合琴裹布93(success墨绿)、灰胎打磨92(accent朱砂红)、上弦调音90(textSecondary灰褐)。横向进度条用 layoutWeight 动态分配(d.heat 和 100-d.heat),选材听音97居首,进度条几乎满格。6种工艺覆盖古琴制作全流程(选材听音→开料制形→挖槽纳音→合琴裹布→灰胎打磨→上弦调音),从选木材听音色开始,经开料制琴形、挖槽腹纳音、合琴板裹夏布、上灰胎打磨、最后上弦调音,历时数月方能成琴,完整呈现古琴制作的工艺流程和匠人精神,是本应用琴徽页制琴工艺热度设计的核心亮点。

第二部分是琴徽名录,Column,margin top 12:

  • 标题行:Row,"✨ 琴徽名录"textPrimary深褐字13号加粗 + "点击👁️了解"textHint灰字9号。
  • 琴徽列表:ForEach this.data.huis 12徽,每徽 Row,padding 10,backgroundColor i%2===0 ? cardBg白 : cardAlt浅米黄(奇偶行交替),圆角18,border 1px line,margin top 8:
    • 图标装饰区:Column width 56,padding 上下10/6,backgroundColor getStringColor(d.level)+‘0D’,圆角16,border 1px getStringColor(d.level)+‘33’:
      • 图标:Text d.emoji 18号居中。
      • 琴徽圆:Column width 14 height 14 backgroundColor getStringColor(d.level) 圆角7,带2px huiGold琴徽金边框(模拟琴徽螺钿镶嵌,琴徽金边框是特色),margin top 5。
    • 信息区:Column layoutWeight 1 alignItems Start margin left 10:
      • 名称音位行:Row width 100% justifyContent SpaceBetween,d.name textPrimary深褐字13号加粗 + d.sound getStringColor(d.level)色9号 margin left 8(宫音/商音/角音/徵音/羽音/清角/中吕/蕤宾/林钟/夷则/南吕/应钟,五音十二律!)。
      • 位置准度行:Row width 100% justifyContent Start margin top 6,GuqinTag({text: d.pos, color: textSecondary灰})(琴面位置标签,灰色固定色)+ “准度”+d.level textHint灰字9号 margin left 8。
    • 详情按钮:Text '👁️’14号,点击触发 onDetail(d.name) 回调(detailHuiName=d.name,showDetailHui=true)。

12 琴徽:一徽(岳山右,96,宫音,✨)、二徽(琴面左,95,商音,🌟)、三徽(琴面中,95,角音,💫)、四徽(琴面右,94,徵音,⭐)、五徽(岳山侧,96,羽音,🌠)、六徽(琴面左,93,清角,🪐)、七徽(龙龈左,96,中吕,🌙)、八徽(琴面右,92,蕤宾,☄️)、九徽(琴面左,94,林钟,💎)、十徽(琴面中,92,夷则,🔷)、十一徽(琴面右,91,南吕,🌌)、十三徽(岳山左,90,应钟,🪩)。注意这里列了一到十一徽和十三徽,跳过了十二徽——古琴实际有十三徽(一至十三),但模拟数据中十二徽未列出,可能是数据设计的小特点,也可能是故意留出"十二徽"作为隐藏彩蛋。准度从90到96,getStringColor 三级着色(≥95 primary木褐/≥92 primaryLight浅木褐/其他 accentLight赭石金),一徽96/二徽95/三徽95/五徽96/七徽96用 primary 木褐(最高准度,一五七徽是古琴常用泛音位),四徽94/六徽93/九徽94/八徽92/十徽92用 primaryLight 浅木褐(中高准度),十一徽91/十三徽90用 accentLight 赭石金(基础准度)。音位对应五音十二律——宫商角徵羽五音(一至五徽)+ 清角(六徽,变徵之音)+ 中吕蕤宾林钟夷则南吕应钟十二律(七至十三徽,对应十二律吕),完整呈现古琴琴徽的音律体系,是本应用琴徽名录设计的核心文化亮点。琴徽圆带2px huiGold琴徽金边框,模拟琴徽用螺钿或玉石镶嵌于琴面的工艺,琴徽金边框是本应用琴徽页的视觉特色。点击👁️触发 DetailHuiModal 琴徽详情弹窗(@Prop name 传入徽名,显示琴徽以螺钿或玉石嵌于琴面,十三徽分定泛音之位,与岳山、龙龈相望,一至七徽主泛音清亮,八至十三徽渐沉厚,取音须准,分毫之差音即失位,音位宫音·岳山右,匠人评级甲级·镶徽廿载),形成"浏览→查看详情"交互闭环。


六、订单页:琴式构成与订单台账

订单页以"订单管理"为核心,采用"琴式构成横向进度条+订单台账列表"的两层布局。

订单模块 - 琴式构成横向进度条伏羲式占比最高与12笔订单台账列表金额分级着色

页面顶部第一部分是琴式构成卡,Column 包裹,padding 14,cardBg 白底,圆角20,border 1px primary+‘55’:

  • 标题行:Row,"🧩 琴式构成"textPrimary深褐字13号加粗 + "伏羲式占比最高"textHint灰字9号。
  • 构成列表:ForEach STYLE_SHARE 4种,每款 Row width 100% margin top 6:
    • 色点:Column width 8 height 8 backgroundColor s.color 圆角4。
    • 名称:s.name textPrimary深褐字10号 margin left 6。
    • 数量:‘×’+s.count textSecondary灰字9号 margin left 4。
    • 横向进度条:Row layoutWeight 1 margin left 10,Column layoutWeight(s.count) height 10 backgroundColor s.color 圆角5(占比部分)+ Column layoutWeight(20-s.count) height 10 backgroundColor line 圆角5(剩余部分,总份数20!)。

4 种琴式构成:伏羲式×5(primary木褐)、仲尼式×4(primaryLight浅木褐)、蕉叶式×3(accentLight赭石金)、落霞式×2(accent朱砂红)。横向进度条用 layoutWeight 动态分配(s.count 和 20-s.count,总份数20非100),伏羲式×5占比最高,是本应用订单页琴式构成设计的核心亮点。4种琴式是古琴最经典的形制——伏羲式相传为伏羲所造,造型古朴;仲尼式相传为孔子所造,造型端方,是最常见的琴式;蕉叶式造型如蕉叶,灵秀别致;落霞式造型如落霞,纹理优美。4种琴式覆盖古琴经典形制全品类,完整呈现古琴形制构成。

第二部分是订单台账,Column,margin top 12:

  • 标题行:Row,"📦 订单台账"textPrimary深褐字13号加粗 + "点击✏️改单"textHint灰字9号 + "✏️"14号 margin left 8,点击触发 onEdit 回调(showEditOrder=true)。
  • 订单列表:ForEach this.data.orders 12笔,每笔 Row,padding 10,backgroundColor i%2===0 ? cardBg白 : cardAlt浅米黄(奇偶行交替),圆角18,border 1px line,margin top 8:
    • 图标:Text o.emoji 16号。
    • 信息区:Column layoutWeight 1 alignItems Start margin left 10,o.name textPrimary深褐字12号加粗 + o.region+’ · '+o.count+‘张’ textSecondary灰字9号 margin top 2。
    • 金额:Text ‘¥’+o.amount getOrderColor(o.amount)色12号加粗。
    • 编辑按钮:Text '✏️’12号 margin left 10,点击触发 onEdit 回调。

12 笔订单:雅集琴馆(华东,60张,¥360000,🏛️)、国乐学院(华中,45张,¥270000,🎓)、琴学研究会(华北,38张,¥228000,📖)、汉服品牌(华东,50张,¥240000,👘)、影视道具组(华南,25张,¥175000,🎬)、海外琴社(海外,20张,¥160000,🌏)、茶空间连锁(华中,30张,¥150000,🍵)、非遗展馆(西南,35张,¥210000,🏺)、古琴班(华东,55张,¥275000,🎶)、文创买手店(华南,28张,¥112000,🎁)、景区演艺团(西南,32张,¥160000,🏞️)、私人定制(华北,12张,¥120000,💎)。金额从¥112000(文创买手店)到¥360000(雅集琴馆),getOrderColor 四级着色(≥30万 danger红/≥20万 primary木褐/≥13万 primaryLight浅木褐/其他 success墨绿),¥360000雅集琴馆用 danger 红色(≥30万最大额,雅集琴馆是古琴高端客户),¥270000国乐学院/¥228000琴学研究会/¥240000汉服品牌/¥210000非遗展馆/¥275000古琴班用 primary 木褐(≥20万中大额),¥175000影视道具/¥160000海外琴社/¥150000茶空间/¥160000景区演艺/¥120000私人定制用 primaryLight 浅木褐(≥13万中等),¥112000文创买手店用 success 墨绿色(其他小额),形成"以金额定颜色"的动态着色模式。点击✏️触发 EditGuqinOrderModal 修改订单弹窗(显示客户雅集琴馆,数量60→66张,金额¥396000,保存修改),是本应用订单页设计的核心亮点。订单覆盖12种客户类型(雅集琴馆/国乐学院/琴学研究会/汉服品牌/影视道具/海外琴社/茶空间/非遗展馆/古琴班/文创买手店/景区演艺/私人定制),完整呈现古琴B端客户生态,古琴作为高端手工乐器,客户以琴馆、学院、研究会、汉服品牌等文化机构为主,与之前扎染的民宿/文创/直播等大众客户形成鲜明对比。


七、客评页:客评口碑列表

客评页以"客户评价"为核心,采用"客评口碑列表"的单层布局。

客评模块 - 12条客评口碑列表含星级重复标签分级着色及评价内容

页面顶部第一部分是标题行,Row width 100% justifyContent SpaceBetween margin bottom 6:

  • "⭐ 客评口碑"textPrimary深褐字13号加粗。
  • "12条好评 · 平均4.9"textHint灰字9号。

第二部分是客评列表,ForEach this.data.reviews 12条,每条 Column,padding 12,backgroundColor i%2===0 ? cardBg白 : cardAlt浅米黄(奇偶行交替),圆角18,border 1px line,margin top 8:

  • 用户行:Row width 100%,
    • 头像:Text r.emoji 15号。
    • 用户信息:Column layoutWeight 1 alignItems Start margin left 8,r.name textPrimary深褐字12号加粗 + r.date textHint灰字9号 margin top 1。
    • 星级:Text ‘⭐’.repeat(r.score)(⭐字符串重复r.score次!)10号 warning赭石色。
    • 标签:GuqinTag({text: r.tag, color: getReviewTagColor(r.tag)})(评价标签,标签分级色)。
  • 评价内容:Text r.content textSecondary灰字11号,lineHeight 16,margin top 6。

12 条客评:琴馆馆主(5星,09-03,九霄环佩音色清越,徽位取音极准,开箱即能入调,音色清越,🏛️)、音乐学院教授(5星,09-02,仲尼式形制端方,弦距适中,学生练琴爱不释手,形制端方,🎓)、琴学会长(5星,09-01,灰胎打磨细腻,走手音圆润无燥,大漆光泽内敛,打磨细腻,📖)、汉服主理(5星,08-31,蕉叶式造型灵秀,置于茶席旁拍照极有韵味,造型灵秀,👘)、道具组组长(5星,08-30,剧组取景用琴,木纹做旧自然,镜头感十足,做旧自然,🎬)、海外琴友(5星,08-29,漂洋过海琴面无恙,装裱妥帖,丝弦音色温暖,包装妥帖,🌏)、茶空间老板(5星,08-28,琴声配茶香,客人驻足聆听,氛围感拉满,氛围感好,🍵)、展馆策展人(4星,08-27,琴身比例合度,若配琴匣展示更添仪式感,比例合度,🏺)、琴课老师(4星,08-26,弦张力稳定,若附备弦一根更贴心,张力稳定,🎶)、买手店主(5星,08-25,小琴桌款销量好,客人当摆件也当乐器用,一物两用,🎁)、演艺团长(5星,08-24,舞台灯光下琴漆流光,演出效果超出预期,上镜出彩,🏞️)、老琴友(5星,08-23,第二把了,焦尾式音质厚重,越弹越有味道,越弹越爱,💎)。10条5星+2条4星,平均4.9分,星级用 ‘⭐’.repeat(r.score) 字符串重复生成(非循环非组件,纯字符串乘法),是轻量级星级展示的实用技巧。标签 getReviewTagColor 五级着色(音色清越/打磨细腻/比例合度/上镜出彩用 primary木褐,形制端方/做旧自然/一物两用用 primaryLight浅木褐,造型灵秀/氛围感好/越弹越爱用 success墨绿,包装妥帖/张力稳定用 accentLight赭石金,其他 danger红),形成"以标签定颜色"的动态着色模式。评价内容都是真实古琴用户的使用反馈,覆盖12种客户类型与订单台账客户类型呼应,从琴馆馆主到音乐学院教授,从汉服主理到海外琴友,从茶空间老板到老琴友,完整呈现古琴客户的口碑生态,古琴作为文人乐器,评价关键词集中在"音色清越"“形制端方”“打磨细腻”“越弹越爱"等文化性描述,与之前扎染的"花色晕染”"走量飞快"等商业性描述形成鲜明对比。奇偶行交替条纹,是本应用客评页设计的标准布局。


八、色彩主题系统:木褐琴徽金宣纸底与十六进制透明度拼接

8.1 色彩接口定义

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

interface GuqinPalette {
  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; woodBrown: string; huiGold: string;
}
  • primary:主色 #5A4030,木褐,古琴非遗主题色,用于标题强调、价格、按钮、柱状图高峰、tabBar顶部条。
  • primaryLight:浅主色 #8A6B52,浅木褐,用于渐变和浅色强调、中端价格。
  • primaryDark:深主色 #3A2A1E,深木褐,用于头部背景(营造浓郁古琴木坊氛围)。
  • accent:辅助色 #A63A2B,朱砂红,用于琴式落霞式着色、制琴工艺灰胎打磨。
  • accentLight:浅辅助色 #C98A4E,赭石金,用于头部副标题文字、柱状图工作日、木材标签。
  • bg:页面背景 #F2EDE0,米白宣纸底,古琴浅色背景(模拟宣纸质感)。
  • cardBg:卡片背景 #FFFFFF,白,用于奇数行卡片和主要卡片。
  • cardAlt:交替卡片背景 #EFE6D6,浅米黄,用于偶数行卡片(奇偶行交替条纹)。
  • textPrimary:文字主色 #33291E,深褐,古琴深色文字(非纯黑,更有木质感和文化感)。
  • textSecondary:次要文字 #7A6A52,灰褐,用于次要信息。
  • textHint:提示文字 #AC9F88,浅灰褐,用于辅助信息和非激活tab。
  • border:边框色 #E2D8C2,浅米黄边,用于按钮边框。
  • line:分割线 #F0EADC,极浅米黄,用于卡片边框和进度条背景。
  • success:成功色 #6E8B5E,墨绿,用于成功状态和入门级价格、制琴工艺合琴裹布。
  • warning:警告色 #C98A4E,赭石,用于星级评分。
  • danger:危险色 #9E2E20,深红,用于删除按钮和最高端价格/大额订单。
  • woodBrown:木褐 #5A4030,与 primary 同色,用于装饰竖线(模拟琴弦,3x12/3x14木褐竖线)。
  • huiGold:琴徽金 #C9A24B,琴徽金,用于装饰圆(模拟琴徽,9x9琴徽金圆带1px白边,琴徽圆带2px琴徽金边框)。

古琴坊撤下琴弦弹窗运行效果 - 弹窗叠加琴弦页显示撤下琴弦确认及再想想确认撤下按钮

8.2 色彩实例化

const COLORS: GuqinPalette = {
  primary: '#5A4030', primaryLight: '#8A6B52', primaryDark: '#3A2A1E',
  accent: '#A63A2B', accentLight: '#C98A4E',
  bg: '#F2EDE0', cardBg: '#FFFFFF', cardAlt: '#EFE6D6',
  textPrimary: '#33291E', textSecondary: '#7A6A52', textHint: '#AC9F88',
  border: '#E2D8C2', line: '#F0EADC',
  success: '#6E8B5E', warning: '#C98A4E', danger: '#9E2E20',
  white: '#FFFFFF', woodBrown: '#5A4030', huiGold: '#C9A24B'
};

本应用采用"木褐琴徽金宣纸底"非遗主题设计——bg #F2EDE0 米白宣纸底(模拟宣纸质感,古琴文化载体),cardBg #FFFFFF 白,cardAlt #EFE6D6 浅米黄(奇偶行交替用),textPrimary #33291E 深褐(古琴深色文字,非纯黑,更有木质感和文化感),primary #5A4030 木褐主色,primaryDark #3A2A1E 深木褐(头部背景,营造浓郁古琴木坊氛围),accent #A63A2B 朱砂红辅助色,accentLight #C98A4E 赭石金(头部副标题),woodBrown #5A4030 木褐(装饰竖线用,模拟琴弦),huiGold #C9A24B 琴徽金(装饰圆用,模拟琴徽)。主色体系采用"木褐+琴徽金+宣纸底"三色组合——木褐用于主色/价格/按钮/高峰/装饰竖线,琴徽金用于装饰圆/琴徽边框,宣纸底用于页面背景/交替卡片,三色各司其职形成"以木褐定主、以琴徽金定饰、以宣纸底定底"的古琴语义着色体系。

本应用最大的色彩处理特色是木褐 woodBrown #5A4030 与琴徽金 huiGold #C9A24B——木褐用于装饰竖线(3x12/3x14木褐竖线,圆角2,模拟古琴琴弦),琴徽金用于装饰圆(9x9琴徽金圆带1px白边,模拟琴徽螺钿镶嵌;琴徽圆带2px琴徽金边框),与宣纸底 bg #F2EDE0 形成"木褐琴徽金宣纸"的经典古琴配色,是本应用视觉设计的灵魂。头部12点装饰纹中 r%3=0 用3x12木褐竖线(模拟琴弦,竖线形态区别于之前的圆点),r%3=1 用9x9琴徽金圆带1px白边(模拟琴徽,琴徽金是特色色),r%3===2 用5x5白小圆,木褐竖线+琴徽金圆+白小圆三色交替,营造浓郁的古琴装饰纹效果。古琴图标装饰区的3根琴弦竖线(2x10 getGuqinColor竖线,间距2px)模拟古琴七弦,琴弦库图标装饰区的10x10 getStringColor圆带2px白边+8x8琴徽金圆,琴徽图标装饰区的14x14 getStringColor圆带2px琴徽金边框,都是"木褐琴弦琴徽金"古琴纹样的模拟,是本应用视觉设计的核心特色。woodBrown 木褐与 primary 同色但语义不同——primary 是主题主色,woodBrown 是装饰专用色(模拟琴弦),huiGold 琴徽金是独立的装饰色(模拟琴徽),这种"同色异义+独立装饰色"的设计体现了古琴"琴弦+琴徽"的核心视觉元素。

本应用另一个色彩处理特色是十六进制透明度拼接技巧——大量使用 color + 十六进制透明度后缀实现半透明背景和边框,无需额外定义半透明颜色常量:

  • color + ‘14’ = 10% 透明度(用于 GuqinTag 背景、图标区背景、tabBar激活背景)
  • color + ‘40’ = 25% 透明度(用于 GuqinTag 边框)
  • color + ‘55’ = 33% 透明度(用于卡片边框、装饰横线)
  • color + ‘66’ = 40% 透明度(用于圆形图标边框)
  • color + ‘80’ = 50% 透明度(用于头部右侧图标区边框)
  • color + ‘0D’ = 5% 透明度(用于古琴图标装饰区背景)
  • color + ‘33’ = 20% 透明度(用于古琴图标装饰区边框)
  • color + ‘2E’ = 18% 透明度(用于头部右侧图标区背景)

这种技巧的原理是:ArkUI 的颜色值支持 #RRGGBBAA 格式(8位十六进制,最后两位是透明度),通过字符串拼接 color(#RRGGBB)+ 透明度后缀(AA),即可动态生成任意透明度的同色系颜色,无需为每个颜色定义多个透明度变体。例如 primary #5A4030 + ‘14’ = #5A403014(10%透明木褐),primary + ‘66’ = #5A403066(40%透明木褐)。这种技巧在本应用中被广泛使用(GuqinTag子组件、图标装饰区、tabBar激活态、卡片边框、头部图标区等),是 ArkUI 颜色处理的高级实用技巧,大幅减少了颜色常量的定义数量。

本应用的动态颜色逻辑也十分丰富——5个颜色分级函数(getStyleColor琴式5类/getStringColor韧度3级/getGuqinColor价格4级/getOrderColor金额4级/getReviewTagColor标签5类),将颜色映射逻辑从 UI 层抽离,每个函数根据不同维度返回对应颜色值,实现"数据即配色"的动态着色模式。getGuqinColor 价格四级(≥7000 danger红/≥6000 primary木褐/≥5200 primaryLight浅木褐/其他 success墨绿),getOrderColor 金额四级(≥30万 danger红/≥20万 primary木褐/≥13万 primaryLight浅木褐/其他 success墨绿),getStringColor 韧度三级(≥95 primary木褐/≥92 primaryLight浅木褐/其他 accentLight赭石金),getStyleColor 琴式五类(伏羲式→木褐/仲尼式→浅木褐/蕉叶式→赭石金/落霞式→朱砂红/其他→墨绿),getReviewTagColor 标签五类(音色类→木褐/形制类→浅木褐/体验类→墨绿/服务类→赭石金/其他→红)。这种将所有色彩集中管理、配合5个颜色分级函数和十六进制透明度拼接与木褐琴徽金装饰与多维度动态着色的做法保证了全局一致性,降低了维护成本。

古琴坊定制古琴弹窗运行效果 - 弹窗叠加古琴页显示琴名琴式预算三行及取消确认定制按钮


九、类型定义:@Observed聚合数据类与多组件数据共享

9.1 @Observed聚合数据类 GuqinData

应用独创了 @Observed 聚合数据类设计——GuqinData 一个类包含 guqins/strings/huis/orders/reviews 五个数组,每个数组 12 条数据,用对象字面量初始化(非 new 构造函数),@Observed 装饰整个类:

@Observed
export class GuqinData {
  guqins: GuqinItem[] = [ /* 12款历史名琴 */ ];
  strings: StringItem[] = [ /* 12种琴弦 */ ];
  huis: HuiItem[] = [ /* 12琴徽 */ ];
  orders: GuqinOrderItem[] = [ /* 12笔订单 */ ];
  reviews: GuqinReviewItem[] = [ /* 12条客评 */ ];
}

与每个数据类独立定义+独立常量数组初始化的设计不同,本应用将所有业务数据聚合到一个 GuqinData 类中,用类属性初始化(guqins: GuqinItem[] = […]),非 constructor 构造函数。@Observed 装饰整个类,使得 GuqinData 的所有属性变化都能被观察到。配合 @ObjectLink 在子组件间共享引用,实现"一个数据对象贯穿所有页面"的聚合数据模式。入口组件 GuqinApp 中 @State data: GuqinData = new GuqinData() 创建数据实例,5个内容子组件通过 @ObjectLink data: GuqinData 接收共享引用,修改数据时所有引用该数据的子组件自动响应式刷新,是 ArkUI 跨组件数据共享的高级实践。

9.2 元数据接口

应用定义了 9 个元数据接口,将数据结构与 UI 表现层解耦:

  • GuqinItem:古琴项(name/style/wood/price/emoji,12款历史名琴)
  • StringItem:琴弦项(name/material/level/tension/emoji,12种琴弦)
  • HuiItem:琴徽项(name/pos/level/sound/emoji,12琴徽,五音十二律)
  • GuqinOrderItem:订单项(name/region/count/amount/emoji,12笔)
  • GuqinReviewItem:客评项(name/score/date/content/tag/emoji,12条)
  • WeekGuqinMeta:周销量元数据(day/value,7天)
  • StyleShareMeta:琴式构成元数据(name/count/color,4种)
  • GuqinHotMeta:制琴工艺热度元数据(name/heat/color,6种)
  • GuqinTopMeta:名琴销量排行元数据(name/sold/color,6款)

9.3 5个颜色分级函数

function getStyleColor(style: string): string { /* 琴式5类:伏羲式木褐/仲尼式浅木褐/蕉叶式赭石金/落霞式朱砂红/其他墨绿 */ }
function getStringColor(level: number): string { /* 韧度3级:≥95木褐/≥92浅木褐/其他赭石金 */ }
function getGuqinColor(price: number): string { /* 价格4级:≥7000红/≥6000木褐/≥5200浅木褐/其他墨绿 */ }
function getOrderColor(amount: number): string { /* 金额4级:≥30万红/≥20万木褐/≥13万浅木褐/其他墨绿 */ }
function getReviewTagColor(tag: string): string { /* 标签5类 */ }

5 个颜色分级函数将颜色映射逻辑从 UI 层抽离,每个函数根据不同维度(琴式/韧度/价格/金额/标签)返回对应颜色值,实现"数据即配色"的动态着色模式。getStringColor 被琴弦页和琴徽页共用(韧度/准度都用三级着色),getGuqinColor 用于古琴页价格和图标区着色,getOrderColor 用于订单页金额着色,getReviewTagColor 用于客评页标签着色,getStyleColor 用于古琴页琴式标签着色。

所有数据类用对象字面量初始化(非 new 构造函数),@Observed 装饰聚合类,@ObjectLink 在子组件间共享引用,5个颜色分级函数将逻辑从 UI 层抽离,9个元数据接口实现数据结构规范化,4个统计常量数组(WEEK_SOLD/STYLE_SHARE/GUQIN_HOT/GUQIN_TOP)将统计数据与业务数据分离,是本应用数据层设计的核心亮点。数据规模:5个业务数组各12条=60条业务数据,4个统计数组共23条统计数据,总计83条数据,完整呈现古琴非遗手作平台的数据生态。12款历史名琴(九霄环佩/玉壶冰/碧涧流泉/沧海龙吟/太古遗音/鹤鸣秋月/绿绮/焦尾/绕梁/灵机/凤势/连珠)都是中国古琴史上的经典名琴,12琴徽对应五音十二律(宫商角徵羽+清角+中吕蕤宾林钟夷则南吕应钟),6种制琴工艺覆盖制琴全流程(选材听音→开料制形→挖槽纳音→合琴裹布→灰胎打磨→上弦调音),4种琴式覆盖古琴经典形制(伏羲式/仲尼式/蕉叶式/落霞式),数据文化含量极高,是本应用区别于之前主题的最大特色。


十、多组件架构与@ObjectLink数据共享:11组件协同设计

本应用采用了多组件架构设计,将整个应用拆分为 11 个独立组件,组件数量多、架构复杂。

10.1 11个组件清单

  1. GuqinApp(入口组件):@Entry装饰,管理curTab/data/4个弹窗显隐/2个头部动画状态/delStringName/detailHuiName,包含头部+内容区切换+tabBar+4个弹窗条件渲染。
  2. GuqinTag(标签子组件):@Component,@Prop text+@Prop color,封装标签样式(9号字+color+'14’背景+color+'40’边框+圆角10),被古琴/琴弦/琴徽/客评页共用。
  3. GuqinContent(古琴内容子组件):@Component,@ObjectLink data+onAdd回调,本周销量柱状图+古琴名器列表。
  4. StringContent(琴弦内容子组件):@Component,@ObjectLink data+onDel回调,名琴销量TOP6横向进度条+琴弦库列表。
  5. HuiContent(琴徽内容子组件):@Component,@ObjectLink data+onDetail回调,制琴工艺热度横向进度条+琴徽名录列表。
  6. GuqinOrderContent(订单内容子组件):@Component,@ObjectLink data+onEdit回调,琴式构成横向进度条+订单台账列表。
  7. GuqinReviewContent(客评内容子组件):@Component,@ObjectLink data,客评口碑列表。
  8. AddGuqinModal(定制古琴弹窗子组件):@Component,onClose回调,琴名/琴式/预算三行展示+确认定制。
  9. EditGuqinOrderModal(修改订单弹窗子组件):@Component,onClose回调,客户/数量/金额三行+保存修改。
  10. DeleteStringModal(撤下琴弦弹窗子组件):@Component,@Prop title+onClose回调,撤下确认+确认撤下danger红底。
  11. DetailHuiModal(琴徽详情弹窗子组件):@Component,@Prop name+onClose回调,大图标+琴徽螺钿玉石描述+音位+匠人评级+知道了。

10.2 @ObjectLink数据共享机制

入口组件 GuqinApp 中 @State data: GuqinData = new GuqinData() 创建聚合数据实例,5个内容子组件通过 @ObjectLink data: GuqinData 接收共享引用:

// 入口组件传递
GuqinContent({ data: this.data, onAdd: () => { this.showAddGuqin = true; } })

// 子组件接收
@Component
struct GuqinContent {
  @ObjectLink data: GuqinData;
  onAdd: () => void = () => {};
}

@ObjectLink 是 ArkUI 中用于跨组件共享 @Observed 装饰对象引用的装饰器,与 @Prop(值传递,单向同步)不同,@ObjectLink 是引用传递,双向同步——子组件修改 data.guqins 等属性时,入口组件和其他引用同一 data 的子组件自动响应式刷新。这种设计实现了"一个数据对象贯穿所有页面"的聚合数据模式,避免了每个页面独立 @State 数据数组的重复定义,是 ArkUI 跨组件数据共享的高级实践。

10.3 回调函数父子通信

4个内容子组件通过回调函数与入口组件通信,实现弹窗触发:

  • GuqinContent 的 onAdd:点击➕触发,入口组件设置 showAddGuqin=true。
  • StringContent 的 onDel(n: string):点击🗑️触发,入口组件设置 delStringName=n + showDeleteString=true。
  • HuiContent 的 onDetail(n: string):点击👁️触发,入口组件设置 detailHuiName=n + showDetailHui=true。
  • GuqinOrderContent 的 onEdit:点击✏️触发,入口组件设置 showEditOrder=true。

4个弹窗子组件通过 onClose 回调与入口组件通信,实现弹窗关闭:

  • AddGuqinModal/EditGuqinOrderModal/DeleteStringModal/DetailHuiModal 都有 onClose: () => void 回调,点击取消/确认/✕/知道了时调用 onClose(),入口组件设置对应弹窗显隐为 false。

DeleteStringModal 和 DetailHuiModal 通过 @Prop 接收参数(title/name),@Prop 是值传递单向同步,入口组件设置 delStringName/detailHuiName 后传递给弹窗子组件,弹窗内显示对应弦名/徽名。

10.4 头部交互动画

头部右侧 🎵✨🪕 三个图标,其中 🎵 和 ✨ 可点击触发动画:

  • 🎵 旋转动画:@State noteRotate: boolean = false,点击🎵 this.noteRotate = !this.noteRotate,.rotate({ angle: this.noteRotate ? 90 : 0 }),.animation({ duration: 700, curve: Curve.EaseOut }),点击旋转90度,700ms EaseOut 动画,模拟音符旋转。
  • ✨ scale缩放动画:@State starScale: boolean = false,点击✨ this.starScale = !this.starScale,.scale({ x: this.starScale ? 1.6 : 1, y: this.starScale ? 1.6 : 1 }),.animation({ duration: 620, curve: Curve.EaseOut }),点击✨从1倍放大到1.6倍,620ms EaseOut 动画,模拟琴徽星光闪烁放大,非旋转非translate,是scale缩放!

头部🎵旋转和✨scale缩放动画是本应用最具创意的交互设计,点击🎵旋转模拟音符跳动,点击✨scale缩放模拟琴徽星光闪烁,增加了应用的趣味性和互动感,也体现了 ArkUI 显式动画(animation属性配合状态变化,支持rotate/translate/scale多种变换)的使用。🪕 图标纯装饰不可点击,三个图标区用 accentLight+‘2E’(18%透明赭石金底)+border accentLight+‘80’(50%透明赭石金边)+圆角24,营造古琴木坊装饰感。与之前823扎染坊的🌀旋转+💧translate位移不同,本应用用了🎵旋转+✨scale缩放,每次主题换一种动画组合,体现了 ArkUI 显式动画的多样性。

10.5 12点古琴装饰纹

头部下方 12 个古琴装饰点(GUQIN_COL 0-11),用 ForEach 遍历,每个 Row width 14 justifyContent Center,根据 r%3 显示不同形状:

  • r%3===0:3x12 木褐竖线(woodBrown,圆角2,模拟古琴琴弦,竖线形态区别于之前的圆点)。
  • r%3===1:9x9 琴徽金圆(huiGold,圆角5,带1px white白边,模拟琴徽螺钿镶嵌,琴徽金是特色色)。
  • r%3===2:5x5 白小圆(white,圆角3)。

12 个装饰点用 SpaceBetween 均匀分布,木褐竖线+琴徽金圆带白边+白小圆三色交替,营造浓郁的古琴装饰纹效果,是本应用头部设计的特色细节。与之前822绒花坊的粉/绿/金圆点、823扎染坊的靛蓝圆带染白边框不同,本应用用了木褐竖线(模拟琴弦)+琴徽金圆带白边(模拟琴徽),装饰纹形态与主题高度契合——古琴的核心视觉元素就是琴弦和琴徽,用竖线模拟琴弦、用金圆模拟琴徽,是本应用装饰纹设计的精妙之处。下方"🎶 七弦为益友 · 两耳是知音 ✨"分隔线,用左右两条 accentLight+‘66’(40%透明赭石金)横线+中间文字,"七弦为益友,两耳是知音"是古琴经典诗句(出自白居易《船夜援琴》),营造古琴文化氛围。


十一、四大弹窗子组件:增删改查交互闭环

应用通过四个独立弹窗子组件实现了完整的增删改查交互闭环,每个弹窗都是独立 @Component 组件,通过 @Prop 接收参数、onClose 回调关闭。

11.1 AddGuqinModal 定制古琴

@Component
struct AddGuqinModal {
  onClose: () => void = () => {};
  build() {
    Column() { // 全屏遮罩 #66000000
      Column() { // 弹窗卡片 width 86%
        Row() { Text('🎼 定制古琴') + Text('✕').onClick(onClose) }
        Row() { Text('琴名') + Text('九霄环佩') } // cardAlt底
        Row() { Text('琴式') + Text('伏羲式 · 杉木面 · 七弦十三徽') } // cardAlt底
        Row() { Text('预算') + Text('¥6800') } // primary色
        Row() { Text('取消') + Text('确认定制') } // primary木褐底
      }
    }
  }
}

定制古琴弹窗显示琴名(九霄环佩)、琴式(伏羲式·杉木面·七弦十三徽)、预算(¥6800,primary木褐色)三行信息,每行用 cardAlt 浅米黄底,确认定制按钮用 primary 木褐底白字。点击✕/取消/确认定制都调用 onClose() 关闭弹窗。本弹窗为展示型弹窗(无输入框),预设定制信息,点击确认即关闭。

11.2 EditGuqinOrderModal 修改订单

修改订单弹窗显示客户(雅集琴馆)、数量(60 → 66 张,primary木褐色显示变更)、金额(¥396000,accent朱砂红色)三行信息,每行 cardAlt 浅米黄底,数量变更用 primary 木褐色突出显示(从60增至66),金额用 accent 朱砂红色。保存修改按钮用 primary 木褐底白字。点击✕/取消/保存修改都调用 onClose() 关闭。本弹窗同样为展示型,预设修改信息(数量从60增至66,金额对应增至39.6万)。

11.3 DeleteStringModal 撤下琴弦

@Component
struct DeleteStringModal {
  @Prop title: string; // 接收弦名
  onClose: () => void = () => {};
  build() {
    Column() { // 全屏遮罩
      Column() { // 弹窗卡片
        Text('🗑️ 撤下琴弦')
        Text('确定撤下「' + this.title + '」吗?') // 动态弦名
        Text('撤下的琴弦将从弦库移除。')
        Row() { Text('再想想') + Text('确认撤下') } // danger红底!
      }
    }
  }
}

撤下琴弦弹窗通过 @Prop title 接收弦名(入口组件设置 delStringName 后传入),显示"确定撤下「弦名」吗?"动态文案,确认撤下按钮用 danger 深红底白字(删除操作的危险色),再想想按钮用 border 边框。点击再想想/确认撤下都调用 onClose() 关闭。@Prop 是值传递单向同步,入口组件 delStringName 变化时自动同步到弹窗。

11.4 DetailHuiModal 琴徽详情

琴徽详情弹窗通过 @Prop name 接收徽名,显示大图标(✨30号)+徽名(this.name,textPrimary深褐字加粗)+描述文字(琴徽以螺钿或玉石嵌于琴面,十三徽分定泛音之位,与岳山、龙龈相望。一至七徽主泛音清亮,八至十三徽渐沉厚,取音须准,分毫之差音即失位。)+音位(宫音·岳山右)+匠人评级(甲级·镶徽廿载,primary木褐色),知道了按钮用 primary 木褐底白字。描述文字是固定的琴徽工艺描述(本项目未根据徽名动态切换描述,预设琴徽文案),音位和匠人评级也是预设值。点击✕/知道了调用 onClose() 关闭。@Prop name 接收徽名用于标题显示。

四个弹窗遮罩都用 #66000000(40%黑色,硬编码非 COLORS.mask),弹窗卡片 width 86%,cardBg 白底,圆角18,constraintSize maxHeight 78%。四个弹窗各自独立组件,通过 @Prop 接收参数(DeleteStringModal的title、DetailHuiModal的name),通过 onClose 回调关闭,形成了完整的"定制古琴→撤下琴弦→查看琴徽→修改订单"增删改查交互闭环,是本应用多组件架构设计的核心亮点。


十二、关键技术特性对比

特性维度实现方式技术要点设计价值
色彩管理GuqinPalette接口 + COLORS常量集中定义20种语义色,含木褐琴徽金宣纸底主题保证视觉一致性,支持主题切换
木褐琴徽金宣纸底主题primary #5A4030 + huiGold #C9A24B + bg #F2EDE0木褐主色/琴徽金装饰/宣纸底背景,textPrimary #33291E深褐非纯黑古琴非遗氛围,木褐琴徽金经典配色
深木褐头部primaryDark #3A2A1E底 + 白字标题 + accentLight赭石金副标题深木褐背景营造浓郁古琴木坊氛围,右侧🎵✨🪕图标区头部视觉焦点,古琴木坊氛围
头部🎵旋转动画noteRotate状态 + rotate angle 90 + animation 700ms EaseOut点击🎵旋转90度,模拟音符跳动,显式动画交互趣味性,动效实践
头部✨scale缩放动画starScale状态 + scale x/y 1.6倍 + animation 620ms点击✨从1倍放大到1.6倍,模拟琴徽星光闪烁,非旋转非translate交互趣味性,scale变换实践
12点古琴装饰纹GUQIN_COL 0-11 + r%3三种形状3x12木褐竖线(模拟琴弦)/9x9琴徽金圆带1px白边(模拟琴徽)/5x5白小圆古琴纹样模拟,琴弦琴徽核心元素
木褐woodBrown装饰#5A4030用于装饰竖线(3x12/3x14)与primary同色但语义不同,模拟古琴琴弦竖线装饰专用色,琴弦模拟
琴徽金huiGold装饰#C9A24B用于装饰圆(9x9带白边)和琴徽圆边框(2px)独立装饰色,模拟琴徽螺钿镶嵌,琴徽金边框是特色装饰专用色,琴徽模拟
十六进制透明度拼接color+‘14’/‘40’/‘55’/‘66’/‘80’/‘0D’/‘33’/‘2E’#RRGGBBAA格式,字符串拼接动态生成半透明色减少颜色常量,高级技巧
多组件架构1入口+1标签+5内容+4弹窗=11组件每个组件独立封装,职责单一,@Component装饰高内聚低耦合,工程化
@Observed聚合数据类GuqinData一个类含5个数组@Observed装饰整个类,对象字面量初始化非constructor数据集中管理,聚合模式
@ObjectLink数据共享子组件@ObjectLink data: GuqinData引用传递双向同步,一个数据对象贯穿所有页面跨组件数据共享,高级实践
回调函数父子通信onAdd/onDel/onDetail/onEdit/onClose子组件通过回调触发入口弹窗,入口通过onClose关闭组件解耦,通信清晰
@Prop参数传递DeleteStringModal的title/DetailHuiModal的name值传递单向同步,入口设置后自动同步到弹窗弹窗动态参数,灵活
GuqinTag子组件@Prop text + @Prop color封装标签样式,color+'14’背景+color+'40’边框标签组件复用,一致性
5个颜色分级函数getStyleColor/getStringColor/getGuqinColor/getOrderColor/getReviewTagColor按琴式/韧度/价格/金额/标签分级返回颜色数据即配色,逻辑抽离
琴式五级着色getStyleColor 伏羲式木褐/仲尼式浅木褐/蕉叶式赭石金/落霞式朱砂红/其他墨绿古琴4种经典琴式+其他,琴式标签动态着色以琴式定颜色,文化着色
价格四级着色getGuqinColor ≥7000红/≥6000木褐/≥5200浅木褐/其他墨绿古琴价格4600-7800,焦尾7800用danger红最高端以价格定颜色,动态着色
金额四级着色getOrderColor ≥30万红/≥20万木褐/≥13万浅木褐/其他墨绿订单金额11.2万-36万,雅集琴馆36万用danger红最大额以金额定颜色,动态着色
韧度三级着色getStringColor ≥95木褐/≥92浅木褐/其他赭石金琴弦韧度89-96,太古丝弦96用木褐最高以韧度定颜色,动态着色
本周销量柱状图WEEK_SOLD 7天 + height=w.value直接用数值柱子高度=数值非比例映射,周末≥26用primary木褐/其他accentLight赭石金,最大30px数值即高度,古琴高端低销量
横向进度条layoutWeightlayoutWeight(t.sold) + layoutWeight(100-t.sold)弹性布局动态分配宽度,非固定宽度非百分比ArkUI特有进度条实现
琴式构成进度条layoutWeight(s.count) + layoutWeight(20-s.count)总份数20非100,伏羲式×5占比最高灵活比例,适配不同基数
名琴销量TOP6GUQIN_TOP 6款 + 横向进度条九霄环佩96居首,进度条几乎满格名琴排行一目了然
制琴工艺热度GUQIN_HOT 6工艺 + 横向进度条选材听音97居首,覆盖制琴全流程6工艺工艺热度排行,非遗文化
奇偶行交替条纹i%2=0 cardBg白 / i%2=1 cardAlt浅米黄所有列表页共用,营造条纹效果列表可读性,视觉层次
古琴图标3根琴弦竖线3根2x10 getGuqinColor竖线,间距2px古琴图标装饰区下方,模拟古琴七弦(简化为3根)琴弦模拟,古琴特色
琴徽圆带琴徽金边框14x14 getStringColor圆 + 2px huiGold边框琴徽图标装饰区,模拟琴徽螺钿镶嵌琴徽模拟,琴徽金特色
韧度内嵌进度条内嵌列表行 + layoutWeight(c.level)琴弦库每行内嵌韧度进度条,getStringColor着色韧度可视化,专业感
星级字符串重复‘⭐’.repeat(r.score)纯字符串乘法生成星级,非循环非组件轻量级星级展示,实用技巧
tabBar激活态t.color文字+背景t.color+'14’圆角10+border 1px t.color5个Tab各自颜色激活,顶部3px primary木褐条非遗导航,色彩丰富
四大弹窗子组件AddGuqinModal/EditGuqinOrderModal/DeleteStringModal/DetailHuiModal独立@Component,@Prop参数,onClose回调,#66000000遮罩增删改查闭环,组件复用
弹窗@Prop动态参数DeleteStringModal的title / DetailHuiModal的name入口设置delStringName/detailHuiName后@Prop同步弹窗内容动态化,灵活
modalOverlay特殊实现width1 height1 Column+透明Text+透明Button占位用,实际弹窗用独立背景#66000000兼容框架要求,特殊处理
12款历史名琴九霄环佩/玉壶冰/碧涧流泉/沧海龙吟/太古遗音/鹤鸣秋月/绿绮/焦尾/绕梁/灵机/凤势/连珠都是中国古琴史上经典名琴,价格4600-7800,杉木/桐木/梓木历史文化名琴,非遗底蕴
12琴徽五音十二律一至十一徽+十三徽,宫商角徵羽+清角+中吕蕤宾林钟夷则南吕应钟跳过十二徽(可能是彩蛋),准度90-96,五音十二律音律体系琴徽音律文化,专业深度
制琴全流程6工艺选材听音→开料制形→挖槽纳音→合琴裹布→灰胎打磨→上弦调音从选木到调音完整制琴流程,热度97-90制琴工艺文化,匠人精神
4种经典琴式伏羲式/仲尼式/蕉叶式/落霞式古琴最经典形制,伏羲式×5占比最高琴式文化,形制多样
12种琴弦生态蚕丝/冰丝/钢弦/竹丝/金缠丝等,中张/高张/低张古琴常用弦材,韧度89-96琴弦生态,专业感
12种B端客户生态雅集琴馆/国乐学院/琴学研究会/汉服品牌/影视道具/海外琴社等古琴高端客户以文化机构为主,与扎染大众客户对比古琴商业生态,真实感
数据规模5业务数组各12条=60条+4统计数组23条=83条古琴/琴弦/琴徽/订单/客评各12,统计7+4+6+6完整数据生态,古琴非遗

完整代码

interface GuqinPalette {
  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;
  woodBrown: string;
  huiGold: string;
}

const COLORS: GuqinPalette = {
  primary: '#5A4030',
  primaryLight: '#8A6B52',
  primaryDark: '#3A2A1E',
  accent: '#A63A2B',
  accentLight: '#C98A4E',
  bg: '#F2EDE0',
  cardBg: '#FFFFFF',
  cardAlt: '#EFE6D6',
  textPrimary: '#33291E',
  textSecondary: '#7A6A52',
  textHint: '#AC9F88',
  border: '#E2D8C2',
  line: '#F0EADC',
  success: '#6E8B5E',
  warning: '#C98A4E',
  danger: '#9E2E20',
  white: '#FFFFFF',
  woodBrown: '#5A4030',
  huiGold: '#C9A24B'
};

enum GuqinTab {
  GUQIN = 0,
  STRING = 1,
  HUI = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'guqin', icon: '🎼', label: '古琴', color: '#5A4030' },
  { key: 'string', icon: '🪢', label: '琴弦', color: '#8A6B52' },
  { key: 'hui', icon: '✨', label: '琴徽', color: '#C98A4E' },
  { key: 'order', icon: '📦', label: '订单', color: '#C98A4E' },
  { key: 'review', icon: '⭐', label: '客评', color: '#9E2E20' }
];

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

interface GuqinItem {
  name: string;
  style: string;
  wood: string;
  price: number;
  emoji: string;
}

interface StringItem {
  name: string;
  material: string;
  level: number;
  tension: string;
  emoji: string;
}

interface HuiItem {
  name: string;
  pos: string;
  level: number;
  sound: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekGuqinMeta[] = [
  { day: '周一', value: 8 },
  { day: '周二', value: 12 },
  { day: '周三', value: 15 },
  { day: '周四', value: 18 },
  { day: '周五', value: 22 },
  { day: '周六', value: 30 },
  { day: '周日', value: 26 }
];

const STYLE_SHARE: StyleShareMeta[] = [
  { name: '伏羲式', count: 5, color: '#5A4030' },
  { name: '仲尼式', count: 4, color: '#8A6B52' },
  { name: '蕉叶式', count: 3, color: '#C98A4E' },
  { name: '落霞式', count: 2, color: '#A63A2B' }
];

const GUQIN_HOT: GuqinHotMeta[] = [
  { name: '选材听音', heat: 97, color: '#5A4030' },
  { name: '开料制形', heat: 95, color: '#8A6B52' },
  { name: '挖槽纳音', heat: 94, color: '#C98A4E' },
  { name: '合琴裹布', heat: 93, color: '#6E8B5E' },
  { name: '灰胎打磨', heat: 92, color: '#A63A2B' },
  { name: '上弦调音', heat: 90, color: '#7A6A52' }
];

const GUQIN_TOP: GuqinTopMeta[] = [
  { name: '九霄环佩', sold: 96, color: '#5A4030' },
  { name: '玉壶冰', sold: 92, color: '#8A6B52' },
  { name: '碧涧流泉', sold: 89, color: '#C98A4E' },
  { name: '沧海龙吟', sold: 85, color: '#6E8B5E' },
  { name: '太古遗音', sold: 81, color: '#A63A2B' },
  { name: '鹤鸣秋月', sold: 77, color: '#7A6A52' }
];

@Observed
export class GuqinData {
  guqins: GuqinItem[] = [
    { name: '九霄环佩', style: '伏羲式', wood: '杉木', price: 6800, emoji: '🎼' },
    { name: '玉壶冰', style: '仲尼式', wood: '杉木', price: 5800, emoji: '🧊' },
    { name: '碧涧流泉', style: '蕉叶式', wood: '桐木', price: 7200, emoji: '🌊' },
    { name: '沧海龙吟', style: '落霞式', wood: '杉木', price: 6600, emoji: '🐉' },
    { name: '太古遗音', style: '伏羲式', wood: '桐木', price: 6200, emoji: '📜' },
    { name: '鹤鸣秋月', style: '仲尼式', wood: '杉木', price: 5500, emoji: '🦢' },
    { name: '绿绮', style: '神农式', wood: '桐木', price: 5000, emoji: '💚' },
    { name: '焦尾', style: '师旷式', wood: '梓木', price: 7800, emoji: '🔥' },
    { name: '绕梁', style: '列子式', wood: '杉木', price: 5400, emoji: '🌀' },
    { name: '灵机', style: '灵机式', wood: '桐木', price: 4600, emoji: '⚙️' },
    { name: '凤势', style: '凤势式', wood: '杉木', price: 5900, emoji: '🪶' },
    { name: '连珠', style: '连珠式', wood: '桐木', price: 4800, emoji: '📿' }
  ];

  strings: StringItem[] = [
    { name: '太古丝弦', material: '蚕丝', level: 96, tension: '中张', emoji: '🪢' },
    { name: '冰弦', material: '冰丝', level: 95, tension: '高张', emoji: '🧵' },
    { name: '缠丝弦', material: '蚕丝缠弦', level: 94, tension: '低张', emoji: '🕸️' },
    { name: '钢弦一弦', material: '钢弦', level: 93, tension: '高张', emoji: '🔩' },
    { name: '钢弦七弦', material: '钢弦', level: 92, tension: '中张', emoji: '⚙️' },
    { name: '梅花丝弦', material: '蚕丝', level: 95, tension: '中张', emoji: '🌸' },
    { name: '玄丝弦', material: '蚕丝', level: 91, tension: '低张', emoji: '⚫' },
    { name: '云纹钢弦', material: '钢弦', level: 90, tension: '中张', emoji: '☁️' },
    { name: '织锦丝弦', material: '蚕丝', level: 94, tension: '高张', emoji: '🧣' },
    { name: '青竹弦', material: '竹丝', level: 89, tension: '低张', emoji: '🎋' },
    { name: '金丝弦', material: '金缠丝', level: 93, tension: '高张', emoji: '✨' },
    { name: '玉弦', material: '蚕丝', level: 95, tension: '中张', emoji: '💠' }
  ];

  huis: HuiItem[] = [
    { name: '一徽', pos: '岳山右', level: 96, sound: '宫音', emoji: '✨' },
    { name: '二徽', pos: '琴面左', level: 95, sound: '商音', emoji: '🌟' },
    { name: '三徽', pos: '琴面中', level: 95, sound: '角音', emoji: '💫' },
    { name: '四徽', pos: '琴面右', level: 94, sound: '徵音', emoji: '⭐' },
    { name: '五徽', pos: '岳山侧', level: 96, sound: '羽音', emoji: '🌠' },
    { name: '六徽', pos: '琴面左', level: 93, sound: '清角', emoji: '🪐' },
    { name: '七徽', pos: '龙龈左', level: 96, sound: '中吕', emoji: '🌙' },
    { name: '八徽', pos: '琴面右', level: 92, sound: '蕤宾', emoji: '☄️' },
    { name: '九徽', pos: '琴面左', level: 94, sound: '林钟', emoji: '💎' },
    { name: '十徽', pos: '琴面中', level: 92, sound: '夷则', emoji: '🔷' },
    { name: '十一徽', pos: '琴面右', level: 91, sound: '南吕', emoji: '🌌' },
    { name: '十三徽', pos: '岳山左', level: 90, sound: '应钟', emoji: '🪩' }
  ];

  orders: GuqinOrderItem[] = [
    { name: '雅集琴馆', region: '华东', count: 60, amount: 360000, emoji: '🏛️' },
    { name: '国乐学院', region: '华中', count: 45, amount: 270000, emoji: '🎓' },
    { name: '琴学研究会', region: '华北', count: 38, amount: 228000, emoji: '📖' },
    { name: '汉服品牌', region: '华东', count: 50, amount: 240000, emoji: '👘' },
    { name: '影视道具组', region: '华南', count: 25, amount: 175000, emoji: '🎬' },
    { name: '海外琴社', region: '海外', count: 20, amount: 160000, emoji: '🌏' },
    { name: '茶空间连锁', region: '华中', count: 30, amount: 150000, emoji: '🍵' },
    { name: '非遗展馆', region: '西南', count: 35, amount: 210000, emoji: '🏺' },
    { name: '古琴班', region: '华东', count: 55, amount: 275000, emoji: '🎶' },
    { name: '文创买手店', region: '华南', count: 28, amount: 112000, emoji: '🎁' },
    { name: '景区演艺团', region: '西南', count: 32, amount: 160000, emoji: '🏞️' },
    { name: '私人定制', region: '华北', count: 12, amount: 120000, emoji: '💎' }
  ];

  reviews: GuqinReviewItem[] = [
    { name: '琴馆馆主', score: 5, date: '09-03', content: '九霄环佩音色清越,徽位取音极准,开箱即能入调。', tag: '音色清越', emoji: '🏛️' },
    { name: '音乐学院教授', score: 5, date: '09-02', content: '仲尼式形制端方,弦距适中,学生练琴爱不释手。', tag: '形制端方', emoji: '🎓' },
    { name: '琴学会长', score: 5, 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: 5, date: '08-28', content: '琴声配茶香,客人驻足聆听,氛围感拉满。', tag: '氛围感好', emoji: '🍵' },
    { name: '展馆策展人', score: 4, date: '08-27', content: '琴身比例合度,若配琴匣展示更添仪式感。', tag: '比例合度', emoji: '🏺' },
    { name: '琴课老师', score: 4, date: '08-26', content: '弦张力稳定,若附备弦一根更贴心。', tag: '张力稳定', emoji: '🎶' },
    { name: '买手店主', score: 5, date: '08-25', content: '小琴桌款销量好,客人当摆件也当乐器用。', tag: '一物两用', emoji: '🎁' },
    { name: '演艺团长', score: 5, date: '08-24', content: '舞台灯光下琴漆流光,演出效果超出预期。', tag: '上镜出彩', emoji: '🏞️' },
    { name: '老琴友', score: 5, date: '08-23', content: '第二把了,焦尾式音质厚重,越弹越有味道。', tag: '越弹越爱', emoji: '💎' }
  ];
}

function getStyleColor(style: string): string {
  if (style === '伏羲式') {
    return '#5A4030';
  } else if (style === '仲尼式') {
    return '#8A6B52';
  } else if (style === '蕉叶式') {
    return '#C98A4E';
  } else if (style === '落霞式') {
    return '#A63A2B';
  }
  return '#6E8B5E';
}

function getStringColor(level: number): string {
  if (level >= 95) {
    return '#5A4030';
  } else if (level >= 92) {
    return '#8A6B52';
  }
  return '#C98A4E';
}

function getGuqinColor(price: number): string {
  if (price >= 7000) {
    return '#9E2E20';
  } else if (price >= 6000) {
    return '#5A4030';
  } else if (price >= 5200) {
    return '#8A6B52';
  }
  return '#6E8B5E';
}

function getOrderColor(amount: number): string {
  if (amount >= 300000) {
    return '#9E2E20';
  } else if (amount >= 200000) {
    return '#5A4030';
  } else if (amount >= 130000) {
    return '#8A6B52';
  }
  return '#6E8B5E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '音色清越' || tag === '打磨细腻' || tag === '比例合度' || tag === '上镜出彩') {
    return '#5A4030';
  } else if (tag === '形制端方' || tag === '做旧自然' || tag === '一物两用') {
    return '#8A6B52';
  } else if (tag === '造型灵秀' || tag === '氛围感好' || tag === '越弹越爱') {
    return '#6E8B5E';
  } else if (tag === '包装妥帖' || tag === '张力稳定') {
    return '#C98A4E';
  }
  return '#9E2E20';
}

@Entry
@Component
struct GuqinApp {
  @State curTab: number = 0;
  @State data: GuqinData = new GuqinData();
  @State showAddGuqin: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteString: boolean = false;
  @State showDetailHui: boolean = false;
  @State delStringName: string = '';
  @State detailHuiName: string = '';
  @State noteRotate: boolean = false;
  @State starScale: 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('Guqin · 泠泠七弦 松风入耳')
                .fontSize(10)
                .fontColor(COLORS.accentLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🎵')
                .fontSize(20)
                .onClick(() => {
                  this.noteRotate = !this.noteRotate;
                })
                .rotate({ angle: this.noteRotate ? 90 : 0 })
                .animation({ duration: 700, curve: Curve.EaseOut })
              Text('✨')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.starScale = !this.starScale;
                })
                .scale({ x: this.starScale ? 1.6 : 1, y: this.starScale ? 1.6 : 1 })
                .animation({ duration: 620, curve: Curve.EaseOut })
              Text('🪕')
                .fontSize(14)
                .margin({ left: 6 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(COLORS.accentLight + '2E')
            .borderRadius(24)
            .border({ width: 1, color: COLORS.accentLight + '80' })
          }
          .width('100%')

          Row() {
            ForEach(GUQIN_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(3)
                    .height(12)
                    .backgroundColor(COLORS.woodBrown)
                    .borderRadius(2)
                } else if (r % 3 === 1) {
                  Column()
                    .width(9)
                    .height(9)
                    .backgroundColor(COLORS.huiGold)
                    .borderRadius(5)
                    .border({ width: 1, color: COLORS.white })
                } else {
                  Column()
                    .width(5)
                    .height(5)
                    .backgroundColor(COLORS.white)
                    .borderRadius(3)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'guqin' + r)
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 12 })

          Row() {
            Column()
              .layoutWeight(1)
              .height(1)
              .backgroundColor(COLORS.accentLight + '66')
            Text('🎶 七弦为益友 · 两耳是知音 ✨')
              .fontSize(9)
              .fontColor(COLORS.accentLight)
              .margin({ left: 8, right: 8 })
            Column()
              .layoutWeight(1)
              .height(1)
              .backgroundColor(COLORS.accentLight + '66')
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(COLORS.primaryDark)

        if (this.curTab === GuqinTab.GUQIN) {
          GuqinContent({ data: this.data, onAdd: () => {
            this.showAddGuqin = true;
          } })
        } else if (this.curTab === GuqinTab.STRING) {
          StringContent({ data: this.data, onDel: (n: string) => {
            this.delStringName = n;
            this.showDeleteString = true;
          } })
        } else if (this.curTab === GuqinTab.HUI) {
          HuiContent({ data: this.data, onDetail: (n: string) => {
            this.detailHuiName = n;
            this.showDetailHui = true;
          } })
        } else if (this.curTab === GuqinTab.ORDER) {
          GuqinOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          GuqinReviewContent({ 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(10)
            .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.showAddGuqin) {
        AddGuqinModal({
          onClose: () => {
            this.showAddGuqin = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditGuqinOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteString) {
        DeleteStringModal({
          title: this.delStringName, onClose: () => {
            this.showDeleteString = false;
          }
        })
      }
      if (this.showDetailHui) {
        DetailHuiModal({
          name: this.detailHuiName, onClose: () => {
            this.showDetailHui = false;
          }
        })
      }
    }
  }
}

@Component
struct GuqinTag {
  @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(10)
      .border({ width: 1, color: this.color + '40' })
  }
}

@Component
struct GuqinContent {
  @ObjectLink data: GuqinData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(3)
              .height(14)
              .backgroundColor(COLORS.woodBrown)
              .borderRadius(2)
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primary + '55')
              .margin({ left: 4, right: 4 })
            Column()
              .width(9)
              .height(9)
              .backgroundColor(COLORS.huiGold)
              .borderRadius(5)
              .border({ width: 1, color: COLORS.white })
              .margin({ right: 4 })
            Column()
              .width(3)
              .height(14)
              .backgroundColor(COLORS.woodBrown)
              .borderRadius(2)
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primary + '55')
              .margin({ left: 4, right: 4 })
            Column()
              .width(3)
              .height(14)
              .backgroundColor(COLORS.woodBrown)
              .borderRadius(2)
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 本周古琴销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六30张')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekGuqinMeta) => {
              Column() {
                Text(w.value + '')
                  .fontSize(8)
                  .fontColor(w.value >= 26 ? COLORS.primary : COLORS.accentLight)
                Column()
                  .width(16)
                  .height(w.value)
                  .backgroundColor(w.value >= 26 ? COLORS.primary : COLORS.accentLight)
                  .borderRadius(8)
                  .margin({ top: 4 })
                Text(w.day)
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (w: WeekGuqinMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .border({ width: 1, color: COLORS.primary + '55' })

        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.guqins, (p: GuqinItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(44)
                .height(44)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getGuqinColor(p.price) + '14')
                .borderRadius(22)
                .border({ width: 1, color: getGuqinColor(p.price) + '66' })
                Row() {
                  Column()
                    .width(2)
                    .height(10)
                    .backgroundColor(getGuqinColor(p.price))
                    .borderRadius(1)
                  Column()
                    .width(2)
                    .height(10)
                    .backgroundColor(getGuqinColor(p.price))
                    .borderRadius(1)
                    .margin({ left: 2 })
                  Column()
                    .width(2)
                    .height(10)
                    .backgroundColor(getGuqinColor(p.price))
                    .borderRadius(1)
                    .margin({ left: 2 })
                }
                .width(30)
                .justifyContent(FlexAlign.Center)
                .margin({ top: 5 })
              }
              .width(60)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getGuqinColor(p.price) + '0D')
              .borderRadius(22)
              .border({ width: 1, color: getGuqinColor(p.price) + '33' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('¥' + p.price)
                    .fontSize(10)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(getGuqinColor(p.price))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  GuqinTag({ text: p.style, color: getStyleColor(p.style) })
                  GuqinTag({ text: p.wood + '面', color: COLORS.accentLight })
                  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(18)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: GuqinItem, i: number) => 'gq' + 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 StringContent {
  @ObjectLink data: GuqinData;
  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(GUQIN_TOP, (t: GuqinTopMeta) => {
            Row() {
              Text(t.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .width(90)
              Row() {
                Column()
                  .layoutWeight(t.sold)
                  .height(12)
                  .backgroundColor(t.color)
                  .borderRadius(6)
                Column()
                  .layoutWeight(100 - t.sold)
                  .height(12)
                  .backgroundColor(COLORS.line)
                  .borderRadius(6)
              }
              .layoutWeight(1)
              Text(t.sold + '把')
                .fontSize(9)
                .fontColor(t.color)
                .width(40)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (t: GuqinTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .border({ width: 1, color: COLORS.primary + '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.strings, (c: StringItem, i: number) => {
            Row() {
              Column() {
                Text(c.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(getStringColor(c.level))
                    .borderRadius(5)
                    .border({ width: 2, color: COLORS.white })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.huiGold)
                    .borderRadius(4)
                    .margin({ left: 4 })
                }
                .width(28)
                .justifyContent(FlexAlign.Center)
                .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getStringColor(c.level) + '0D')
              .borderRadius(16)
              .border({ width: 1, color: getStringColor(c.level) + '33' })

              Column() {
                Row() {
                  Text(c.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(c.tension)
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  Text('韧度')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                  Row() {
                    Column()
                      .layoutWeight(c.level)
                      .height(6)
                      .backgroundColor(getStringColor(c.level))
                      .borderRadius(3)
                    Column()
                      .layoutWeight(100 - c.level)
                      .height(6)
                      .backgroundColor(COLORS.line)
                      .borderRadius(3)
                  }
                  .layoutWeight(1)
                  .margin({ left: 6 })
                }
                .width('100%')
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })

              Text('🗑️')
                .fontSize(14)
                .onClick(() => {
                  this.onDel(c.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(18)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (c: StringItem, i: number) => 'st' + c.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 HuiContent {
  @ObjectLink data: GuqinData;
  onDetail: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🔥 制琴工艺热度')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('选材听音97')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(GUQIN_HOT, (d: GuqinHotMeta) => {
            Row() {
              Text(d.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .width(80)
              Row() {
                Column()
                  .layoutWeight(d.heat)
                  .height(14)
                  .backgroundColor(d.color)
                  .borderRadius(7)
                Column()
                  .layoutWeight(100 - d.heat)
                  .height(14)
                  .backgroundColor(COLORS.line)
                  .borderRadius(7)
              }
              .layoutWeight(1)
              Text(d.heat + '')
                .fontSize(9)
                .fontColor(d.color)
                .width(32)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (d: GuqinHotMeta) => d.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .border({ width: 1, color: COLORS.primary + '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.huis, (d: HuiItem, i: number) => {
            Row() {
              Column() {
                Text(d.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Column()
                  .width(14)
                  .height(14)
                  .backgroundColor(getStringColor(d.level))
                  .borderRadius(7)
                  .border({ width: 2, color: COLORS.huiGold })
                  .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getStringColor(d.level) + '0D')
              .borderRadius(16)
              .border({ width: 1, color: getStringColor(d.level) + '33' })

              Column() {
                Row() {
                  Text(d.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(d.sound)
                    .fontSize(9)
                    .fontColor(getStringColor(d.level))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  GuqinTag({ text: d.pos, color: COLORS.textSecondary })
                  Text('准度' + d.level)
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })

              Text('👁️')
                .fontSize(14)
                .onClick(() => {
                  this.onDetail(d.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(18)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (d: HuiItem, i: number) => 'hu' + d.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 GuqinOrderContent {
  @ObjectLink data: GuqinData;
  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(STYLE_SHARE, (s: StyleShareMeta) => {
            Row() {
              Column()
                .width(8)
                .height(8)
                .backgroundColor(s.color)
                .borderRadius(4)
              Text(s.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .margin({ left: 6 })
              Text('×' + s.count)
                .fontSize(9)
                .fontColor(COLORS.textSecondary)
                .margin({ left: 4 })
              Row() {
                Column()
                  .layoutWeight(s.count)
                  .height(10)
                  .backgroundColor(s.color)
                  .borderRadius(5)
                Column()
                  .layoutWeight(20 - s.count)
                  .height(10)
                  .backgroundColor(COLORS.line)
                  .borderRadius(5)
              }
              .layoutWeight(1)
              .margin({ left: 10 })
            }
            .width('100%')
            .margin({ top: 6 })
          }, (s: StyleShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .border({ width: 1, color: COLORS.primary + '55' })

        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.onEdit();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.orders, (o: GuqinOrderItem, i: number) => {
            Row() {
              Text(o.emoji)
                .fontSize(16)
              Column() {
                Text(o.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(o.region + ' · ' + o.count + '张')
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              .margin({ left: 10 })
              Text('¥' + o.amount)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(getOrderColor(o.amount))
              Text('✏️')
                .fontSize(12)
                .margin({ left: 10 })
                .onClick(() => {
                  this.onEdit();
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(18)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: GuqinOrderItem, 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 GuqinReviewContent {
  @ObjectLink data: GuqinData;

  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: GuqinReviewItem, 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)
              GuqinTag({ 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(18)
          .border({ width: 1, color: COLORS.line })
          .margin({ top: 8 })
        }, (r: GuqinReviewItem, i: number) => 'rv' + r.name + i)
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct AddGuqinModal {
  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(10)
        .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(10)
        .margin({ top: 8 })
        Row() {
          Text('预算')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥6800')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(12)
            .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(12)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditGuqinOrderModal {
  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(10)
        .margin({ top: 14 })
        Row() {
          Text('数量')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('60 → 66 张')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('金额')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥396000')
            .fontSize(11)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(12)
            .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(12)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeleteStringModal {
  @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(12)
            .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(12)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailHuiModal {
  @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(12)
        .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(10)
        .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(10)
        .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(12)
          .margin({ top: 12 })
          .onClick(() => {
            this.onClose();
          })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

十四、开发环境搭建

14.1 安装 DevEco Studio

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

DevEco Studio 安装欢迎界面

14.2 选择安装路径

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

选择安装位置

14.3 配置安装选项

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

安装选项配置

14.4 选择项目模板

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

选择 Empty Ability 模板

14.5 配置项目信息

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

配置项目信息


十五、总结

通过对这款古琴坊非遗手作平台的深度记录,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、组件化工程、文化内涵等多维度能力的综合体现。

在架构层面,该应用采用了多组件架构设计——将整个应用拆分为 11 个独立组件(1入口+1标签+5内容+4弹窗),每个组件独立封装、职责单一,通过 @ObjectLink 共享聚合数据、通过回调函数实现父子通信、通过 @Prop 传递弹窗参数,组件数量多、架构复杂。与单组件内@Builder构建所有页面的架构不同,本应用将内容页和弹窗都抽离为独立 @Component 子组件,入口组件 GuqinApp 只负责状态管理和组件调度,实现了真正的高内聚低耦合。这种多组件架构设计虽然增加了组件间通信的复杂度(@ObjectLink/@Prop/回调函数),但大幅提升了代码的可维护性和可复用性,是中大型 ArkUI 应用的标准架构实践,也是本应用工程化设计的最大亮点。

在数据层面,应用独创了**@Observed 聚合数据类设计——GuqinData 一个类包含 guqins/strings/huis/orders/reviews 五个数组,每个数组 12 条数据,用对象字面量初始化(非 new 构造函数),@Observed 装饰整个类,配合 @ObjectLink 在子组件间共享引用,实现"一个数据对象贯穿所有页面"的聚合数据模式。这种设计将所有业务数据集中管理,子组件通过 @ObjectLink data: GuqinData 接收共享引用,修改数据时所有引用该数据的子组件自动响应式刷新,避免了每个页面独立 @State 数据数组的重复定义,是 ArkUI 跨组件数据共享的高级实践。应用还定义了 5 个颜色分级函数(getStyleColor琴式5类/getStringColor韧度3级/getGuqinColor价格4级/getOrderColor金额4级/getReviewTagColor标签5类),将颜色映射逻辑从 UI 层抽离,每个函数根据不同维度返回对应颜色值,实现"数据即配色"的动态着色模式。9 个元数据接口实现数据结构规范化,4 个统计常量数组将统计数据与业务数据分离,数据规模达 83 条(60业务+23统计),完整呈现古琴非遗手作平台的数据生态。本应用数据最大的特色是文化含量极高**——12款历史名琴(九霄环佩/玉壶冰/碧涧流泉/沧海龙吟/太古遗音/鹤鸣秋月/绿绮/焦尾/绕梁/灵机/凤势/连珠)都是中国古琴史上的经典名琴,九霄环佩是唐代雷威所斫现藏故宫的传世名琴,焦尾是东汉蔡邕所斫的四大名琴之一,绿绮是司马相如的琴,每一张琴都有深厚的历史文化底蕴;12琴徽对应五音十二律(宫商角徵羽+清角+中吕蕤宾林钟夷则南吕应钟),完整呈现古琴琴徽的音律体系;6种制琴工艺覆盖制琴全流程(选材听音→开料制形→挖槽纳音→合琴裹布→灰胎打磨→上弦调音),从选木材听音色到上弦调音历时数月方能成琴,体现匠人精神;4种琴式覆盖古琴经典形制(伏羲式/仲尼式/蕉叶式/落霞式),伏羲式相传为伏羲所造,仲尼式相传为孔子所造。这些文化数据让应用不只是一个技术演示,更是一个古琴文化的数字载体。

在视觉层面,本应用采用"木褐琴徽金宣纸底"非遗主题设计——bg #F2EDE0 米白宣纸底(模拟宣纸质感,古琴文化载体),cardBg #FFFFFF 白,cardAlt #EFE6D6 浅米黄(奇偶行交替用),textPrimary #33291E 深褐(古琴深色文字,非纯黑,更有木质感和文化感),primary #5A4030 木褐主色,primaryDark #3A2A1E 深木褐(头部背景,营造浓郁古琴木坊氛围),accent #A63A2B 朱砂红辅助色,accentLight #C98A4E 赭石金(头部副标题),woodBrown #5A4030 木褐(装饰竖线用,模拟琴弦),huiGold #C9A24B 琴徽金(装饰圆用,模拟琴徽)。主色体系采用"木褐+琴徽金+宣纸底"三色组合,三色各司其职形成"以木褐定主、以琴徽金定饰、以宣纸底定底"的古琴语义着色体系。本应用最大的视觉特色是木褐 woodBrown 与琴徽金 huiGold——木褐用于装饰竖线(3x12/3x14木褐竖线,模拟古琴琴弦),琴徽金用于装饰圆(9x9琴徽金圆带1px白边,模拟琴徽螺钿镶嵌;琴徽圆带2px琴徽金边框),与宣纸底形成"木褐琴徽金宣纸"的经典古琴配色。头部12点装饰纹中 r%3=0 用3x12木褐竖线(模拟琴弦,竖线形态区别于之前的圆点),r%3=1 用9x9琴徽金圆带1px白边(模拟琴徽,琴徽金是特色色),r%3===2 用5x5白小圆,木褐竖线+琴徽金圆+白小圆三色交替,营造浓郁的古琴装饰纹效果。古琴图标装饰区的3根琴弦竖线(2x10 getGuqinColor竖线,间距2px)模拟古琴七弦,琴弦库图标装饰区的10x10 getStringColor圆带2px白边+8x8琴徽金圆,琴徽图标装饰区的14x14 getStringColor圆带2px琴徽金边框,都是"木褐琴弦琴徽金"古琴纹样的模拟。woodBrown 木褐与 primary 同色但语义不同——primary 是主题主色,woodBrown 是装饰专用色(模拟琴弦),huiGold 琴徽金是独立的装饰色(模拟琴徽),这种"同色异义+独立装饰色"的设计体现了古琴"琴弦+琴徽"的核心视觉元素。本应用另一个色彩处理特色是十六进制透明度拼接技巧——大量使用 color + 十六进制透明度后缀实现半透明背景和边框,无需额外定义半透明颜色常量,是 ArkUI 颜色处理的高级实用技巧。统一的色彩系统是整个应用的视觉基石,木褐琴徽金宣纸底主题配合十六进制透明度拼接和木褐琴徽金装饰与5个颜色分级函数与多维度动态着色,营造出既沉稳木褐又琴徽金辉的古琴非遗氛围。

在交互层面,应用通过四个独立弹窗子组件实现了完整的增删改查交互闭环:AddGuqinModal 定制古琴(琴名/琴式/预算三行展示,确认定制 primary 木褐底)、EditGuqinOrderModal 修改订单(客户/数量60→66/金额39.6万,保存修改)、DeleteStringModal 撤下琴弦(@Prop title 传入弦名,确认撤下 danger 红底)、DetailHuiModal 琴徽详情(@Prop name 传入徽名,大图标+琴徽螺钿玉石描述+音位+匠人评级,知道了按钮)。四个弹窗各自独立组件,通过 @Prop 接收参数(DeleteStringModal的title、DetailHuiModal的name),通过 onClose 回调关闭,遮罩用 #66000000(40%黑)硬编码。古琴页➕点击触发 AddGuqinModal,琴弦页🗑️点击触发 DeleteStringModal(delStringName 记录弦名),琴徽页👁️点击触发 DetailHuiModal(detailHuiName 记录徽名),订单页✏️点击触发 EditGuqinOrderModal,形成了完整的"定制古琴→撤下琴弦→查看琴徽→修改订单"交互闭环。头部🎵旋转动画(点击旋转90度模拟音符跳动)和✨scale缩放动画(点击✨从1倍放大到1.6倍模拟琴徽星光闪烁)是本应用最具创意的交互设计,点击图标触发显式动画,增加了应用的趣味性和互动感,也体现了 ArkUI 显式动画(animation属性配合状态变化,支持rotate/translate/scale多种变换)的使用。与之前823扎染坊的🌀旋转+💧translate位移不同,本应用用了🎵旋转+✨scale缩放,每次主题换一种动画组合,体现了 ArkUI 显式动画的多样性。本周销量柱状图柱子高度直接用数值(最大30px,非比例映射,古琴是高端手工商品周销量仅8-30张,与之前扎染周销量26-70件形成鲜明对比,体现古琴"小众高端"定位),周末≥26用 primary 木褐高亮,横向进度条用 layoutWeight 动态分配(非固定宽度非百分比,是 ArkUI 特有的弹性布局进度条实现),琴式构成进度条总份数20非100(灵活适配不同基数),韧度进度条内嵌在列表行中,星级用 ‘⭐’.repeat(r.score) 字符串重复生成(非循环非组件,纯字符串乘法),奇偶行交替条纹(i%2=0白/i%2=1浅米黄),tabBar激活态用 t.color 文字+背景 t.color+'14’圆角10+border 1px t.color(5个Tab各自颜色激活),顶部3px primary 木褐条,都是本应用交互设计的精细之处。5个内容子组件通过回调函数(onAdd/onDel/onDetail/onEdit)与入口组件通信触发弹窗,4个弹窗子组件通过 onClose 回调关闭,@Prop 实现弹窗动态参数,形成了完整的多组件交互通信体系。

该应用展现的多组件架构设计、@ObjectLink数据共享、@Observed聚合数据类、@Prop参数传递、回调函数父子通信、5个颜色分级函数、十六进制透明度拼接技巧、木褐woodBrown与琴徽金huiGold装饰、12点木褐竖线琴徽金圆古琴装饰纹、layoutWeight动态横向进度条、头部🎵旋转+✨scale缩放动画、古琴图标区3根琴弦竖线、琴徽圆带琴徽金边框、本周销量数值高度柱状图、韧度内嵌进度条、星级字符串重复、四大弹窗子组件增删改查闭环、GuqinTag子组件封装、12款历史名琴文化、12琴徽五音十二律、6种制琴工艺全流程、4种经典琴式以及木褐琴徽金宣纸底古琴视觉设计意识,都值得开发者在自己的项目中借鉴与传承。无论是初学者还是有经验的鸿蒙开发者,都能从中获得关于"如何用多组件架构和声明式语法构建高质量古琴非遗手作移动应用"的有益启示。泠泠七弦上,静听松风寒——愿这张古琴坊应用,能让更多人在代码中感受到三千古琴的文化温度。

古琴坊应用整体运行效果 - 深木褐头部古琴坊标题与12点木褐竖线琴徽金圆装饰纹及本周销量柱状图古琴名器列表


标签: HarmonyOS · API 24 · ArkTS · 华为 · 古琴非遗 · 木褐琴徽金 · 多组件架构 · @ObjectLink · 五音十二律 · 移动端开发

Logo

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

更多推荐