丝弦雅韵:基于HarmonyOS ArkTS API 24的古筝坊应用木色琥珀墨色主题与单组件八Tab架构全记录
丝弦雅韵:基于HarmonyOS ArkTS API 24的古筝坊应用木色琥珀墨色主题与单组件八Tab架构全记录
丝弦雅韵,十三弦动。古筝,古称"秦筝",始于秦代,距今已有两千余年历史,是中国最古老的弹拨乐器之一。古筝不仅是乐器,更是中华礼乐文明、文人雅趣、匠心技艺的综合载体——一弦一音,五声调式不同;一柱一码,承弦定音相连。从先秦五弦到唐宋十三弦再到现代二十一弦,从山东筝派到河南筝派到客家筝派到潮州筝派到浙江筝派,古筝承载着中国人的音乐美学与精神追求。本文完整记录一个基于鸿蒙HarmonyOS 6.1.1 API 24与ArkTS声明式框架构建的「古筝坊」非遗手作平台,从木色琥珀丝色墨色的古筝色彩主题,到头部琴弦opacity呼吸与雁柱translate振动动画效果,再到8Tab差异化布局(传世古筝列表/琴弦张力标定横条/雁柱琴码承弦/经典琴谱分级/筝界名家流派/制筝工序工时横条/雅集活动报名/订单台账柱状图)、单组件@Builder架构、@Observed可观察数据类、9个工具函数(stringX/bridgeX/tensionBarW/tensionColor/craftBarW/stageColor/orderBarH/levelColor/schoolColor)、三大弹窗(新增订单/编辑琴弦/删除雅集)、条件渲染模态遮罩、PlayMode.Alternate呼吸动画体系、柱状图数据可视化,附完整源码与开发环境搭建指南。
一、引言:丝弦雅韵,十三弦动
古筝,古称"秦筝",是中国最古老的弹拨乐器之一,距今已有两千余年历史。古筝的起源可追溯至秦代,《史记·李斯列传》记载"夫击瓮叩缶弹筝搏髀,而歌呼呜呜快耳者,真秦之声也",可见秦代筝已盛行于秦国宫廷与民间。古筝不仅是乐器,更是中华礼乐文明、文人雅趣、匠心技艺的综合载体——一弦一音,五声调式(宫商角徵羽)不同;一柱一码,承弦定音相连。从先秦五弦筝到唐代十三弦筝再到现代二十一弦筝,从高山流水到渔舟唱晚到战台风,古筝承载着中国人的音乐美学与精神追求。
中国古筝形制演变丰富,工艺精湛。古筝的弦数经历了从5弦到12弦到13弦到14弦到15弦到16弦到18弦到21弦乃至25弦的演变——先秦五弦古朴苍劲、唐代十三弦清雅悠远、现代二十一弦丰富宏阔,弦数的增加扩展了音域和表现力。古筝面板选材讲究——桐木面板音色温润共鸣好(敦煌·蜶恋花桐木面板)、泡桐面板音色明亮穿透力强(朱雀·凤鸣岐山泡桐)、楠木面板音色沉稳深沉(龙凤·荷塘月色楠木)、红木面板音色华丽圆润(韵牌·高山流水红木)、紫檀面板音色厚重大气(弦昇·紫檀嵌螺钿)、杉木面板音色清亮通透(琼花·古法断纹杉木)、楸木面板音色中正平和(鼎韵·素面新筝楸木)、花梨面板音色温暖醇厚(宏声·花梨木纹花梨)、楠竹筝身轻盈便携(中州·竹节筝楠竹)、酸枝面板音色华丽辉煌(盛世·金玉满堂酸枝)、百年桐木整挖音色浑厚通透(名匠·整挖筝百年桐木),十二款古筝覆盖桐木/泡桐/楠木/红木/紫檀/杉木/楸木/花梨/楠竹/酸枝/百年桐木十一种材质。琴弦规格从0.12mm到0.72mm——钢丝尼龙弦音色明亮、缠丝钢弦音色厚重,张力从45%到88%分为三级着色。雁柱(琴码)材质覆盖紫檀/牛骨/红木/乌木/楠竹/青玉/黄杨木/花梨木八种,高度从4.0cm到4.5cm。制筝工序从选材开料到整体修整共八道——选材开料、面板烘烤、木框成型、面板刨制、挖膛调音、涂漆晾干、装弦校音、整体修整,工时从16小时到120小时,"初序-中序-精序-末序"四阶段层层递进。
中国古筝演奏流派更是博大精深——山东筝派刚健明快(赵玉斋质朴浑厚)、河南筝派高亢苍劲(中州竹节筝)、客家筝派古淡典雅(罗九香含蓄悠远)、潮州筝派清丽细腻(郭鹰清丽细腻、林毛根含蓄悠远)、浙江筝派华丽流畅(曹正质朴浑厚、王昌元气势磅礴),五大流派各具特色,从北方到南方、从学院到民间,形成了完整的中国古筝演奏文化生态。经典琴谱更是流传千古——渔舟唱晚(河南筝曲,入门)、高山流水(浙江筝曲,中级)、战台风(创作曲,高级)、出水莲(客家筝曲,中级)、寒鸦戏水(潮州筝曲,高级)、茉莉芬芳(江苏民歌改编,入门)、浏阳河(湖南民歌改编,中级)、林冲夜奔(筝独奏,高难),八首琴谱覆盖入门到高难四个级别。制筝名家更是名家辈出——曹正(浙派,42部作品)、郭鹰(潮州派,30部作品)、赵玉斋(山东派,35部作品)、罗九香(客家派,28部作品)、王昌元(浙派,22部作品)、项斯华(学院派,26部作品)、林毛根(潮州派,18部作品)、李萌(现代派,24部作品),八位名家覆盖浙派/潮州派/山东派/客家派/学院派/现代派六大流派。
然而,随着现代生活节奏加快,古筝这门需要木工与调音双重技艺的传统手作正面临传承困境——年轻人不了解古筝文化,制筝匠人缺乏展示和销售渠道,古筝产品材质不透明、琴弦规格混乱、工序信息缺失,正成为中国古筝非遗传承的现实痛点。一个优秀的古筝手作平台,不仅需要覆盖从"传世古筝"到"琴弦规格"再到"雁柱琴码"的完整产品展示闭环,还要在经典琴谱、筝界名家、制筝工序、雅集活动、订单管理等维度做到精准而高效。它需要以传世古筝为核心入口,横向扩展出琴弦、雁柱、琴谱、名家、工序、雅集、订单八大功能模块,每个模块采用完全不同的布局风格,形成一个完整闭环的中国古筝非遗手作生态。
正是在这样的背景下,古筝坊类非遗手作平台应运而生。一个优秀的古筝应用,需要以传世古筝为核心入口,横向扩展出琴弦张力标定、雁柱承弦定音、经典琴谱分级、筝界名家流派、制筝工时横条、雅集活动报名、订单台账柱状图八大功能模块,每个模块采用完全不同的布局风格——古筝=传世古筝列表+订购交互、琴弦=张力标定横条+琴弦规格列表、雁柱=琴码列表+承弦支撑、琴谱=经典琴谱列表+难度分级、名家=筝界名家列表+流派着色、工序=制筝工时横条+阶段着色、雅集=雅集活动列表+报名删除、订单=柱状图+订单台账列表,形成了一个完整闭环的中国古筝非遗手作生态。它不仅是一个功能完备的演示项目,更是一份典型的ArkUI工程实践范本,涵盖了单组件@Builder架构设计、@Observed可观察数据类、颜色分级函数、头部琴弦雁柱动画、条件渲染模态弹窗、layoutWeight弹性进度条、ForEach列表渲染、柱状图数据可视化等关键技术点。

二、应用整体架构概览
整个应用采用了"单组件架构 + @Builder方法构建 + @State状态管理 + 配置驱动"的四层设计模式。入口组件ZhengPage作为整个应用的唯一组件,负责管理当前激活的Tab状态、呼吸动画布尔值、三个弹窗的显隐状态、表单数据以及八个@Observed数据数组。与多组件架构(如香囊坊将应用拆分为10个独立组件)不同,本应用将所有页面内容都通过@Builder方法内联构建在一个@Entry @Component struct中——1个入口组件ZhengPage包含18个@Builder方法(pageHeader/sectionTitle/zhengRow/stringRow/bridgeRow/scoreRow/masterRow/craftRow/salonRow/orderRow/orderChart/tabContent/bottomItem/bottomBar/addModal/editModal/delModal/modalOverlay),每个@Builder方法负责一个独立的UI区块,通过if-else条件渲染在tabContent中切换八个Tab页面。这种单组件@Builder架构虽然组件粒度较粗,但所有状态和UI逻辑集中在一处,便于快速开发和维护,是中小型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)构建,包含图标(17号Emoji)、标签文字(11号)两部分。激活态文字amber琥珀色+背景色#4A3420深棕高亮,非激活态sub浅棕色普通+tabBg深棕底,点击切换curTab触发tabContent重新渲染。八个Tab每个布局完全不同——古筝采用传世古筝列表+订购交互(12款古筝,材质长度信息,"订"点击新增订单弹窗)、琴弦采用张力标定横条+琴弦规格列表(8种琴弦,tensionBarW张力宽度映射,tensionColor三级着色)、雁柱采用琴码列表+承弦支撑(8种雁柱,材质高度信息,translate振动动画)、琴谱采用经典琴谱列表+难度分级(8首琴谱,levelColor三级着色)、名家采用筝界名家列表+流派着色(8位名家,schoolColor流派着色)、工序采用制筝工时横条+阶段着色(8道工序,craftBarW工时宽度映射,stageColor阶段着色)、雅集采用雅集活动列表+报名删除(8场雅集,"删"点击删除确认弹窗)、订单采用柱状图+订单台账列表(10笔订单,orderBarH金额高度映射),这种"一页一型"的差异化设计最大程度还原了真实中国古筝非遗手作平台的交互体验。
在数据层面,应用采用了**@Observed可观察数据类**设计——八个独立的@Observed类(ZhengItem/StringItem/BridgeItem/ScoreItem/MasterItem/CraftItem/SalonItem/OrderItem),每个类有独立的constructor构造函数,通过new关键字实例化,存储在@State数组中(zhengs/strings/bridges/scores/masters/crafts/salons/orders)。数据规模达70条(12+8+8+8+8+8+8+10),完整呈现中国古筝非遗手作平台的数据生态。与香囊坊的聚合数据类NangData不同,本应用将八组数据分散为八个@State数组直接管理在入口组件中,修改数据时通过数组方法(splice/unshift)直接操作@State数组触发响应式刷新,是ArkUI单组件状态管理的基础实践。应用还定义了8个数据接口(ZhengItem/StringItem/BridgeItem/ScoreItem/MasterItem/CraftItem/SalonItem/OrderItem)和1个Tab元数据接口(TabMeta),将数据结构与UI表现层解耦,实现配置驱动设计。
在颜色逻辑层面,应用定义了9个工具函数(stringX/bridgeX定位函数+tensionBarW/tensionColor张力函数+craftBarW/stageColor工序函数+orderBarH金额高度函数+levelColor难度函数+schoolColor流派函数),将颜色、尺寸、定位映射逻辑从UI层抽离,每个函数根据不同维度(张力/工时/金额/难度/流派/阶段)返回对应的颜色值或像素宽度或定位坐标,实现"数据即配色、数据即尺寸、数据即位置"的动态映射模式。应用配色采用"木色琥珀丝色墨色"古筝非遗主题——bg #F7F1E6 宣纸色(模拟古筝谱宣纸质感),wood #8A5A2B 木色(古筝面板木色调),amber #D9A441 琥珀色(古筝装饰主色调),silk #E8C87A 丝色(古筝琴弦丝色调),ink #2B2118 墨色(古筝面板墨色调),五色各司其职形成"以木色定筝、以琥珀定饰、以丝色定弦、以墨色定底、以宣纸定景"的中国古筝语义着色体系。
在交互层面,应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:addModal新增订单(三字段TextInput表单:客户名称/订单金额/交付日期,保存accent木色底)、editModal编辑琴弦(紧弦+5%/松弦-5%两按钮调节张力,紧弦win红色底/松弦amber琥珀底)、delModal删除雅集(确认删除danger危险红底)。三个弹窗都是@Builder方法内联在入口组件中,通过if条件渲染控制显隐(showAdd/showEdit/showDel三个@State布尔变量),遮罩用rgba(0,0,0,0.45)(45%黑色),弹窗主体与遮罩用Stack层叠,整体zIndex(999)覆盖主内容。古筝页"订"点击触发addModal(新增订单splice插入数组头部),琴弦页"改"点击触发editModal(selString记录目标项,调节张力),雅集页"删"点击触发delModal(selSalon记录目标项,确认删除splice移除),形成了完整的"订购古筝→调节琴弦→删除雅集"交互闭环。
下面逐页展示应用的八大功能模块运行效果与技术实现。
三、配色体系:木色琥珀丝色墨色
3.1 色彩接口定义
任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个ColorPalette接口,将整个应用所需的色彩抽象为一个结构化的契约,共20个字段:
interface ColorPalette {
bg: string;
cardBg: string;
header1: string;
header2: string;
wood: string;
amber: string;
silk: string;
ink: string;
title: string;
sub: string;
text1: string;
text2: string;
text3: string;
accent: string;
win: string;
lose: string;
danger: string;
tabBg: string;
tabOn: string;
mask: string;
}
ColorPalette接口定义了20个色彩字段,覆盖了应用的全部视觉需求——背景色(bg/cardBg)、头部渐变(header1/header2)、主题色(wood/amber/silk/ink)、文字层级(title/sub/text1/text2/text3)、功能色(accent/win/lose/danger)、导航色(tabBg/tabOn)和遮罩色(mask)。这种"接口先行"的设计模式确保了色彩使用的类型安全和一致性——所有颜色都通过COLORS常量引用,编译器能在开发阶段就发现拼写错误或遗漏。与香囊坊的15个色彩字段相比,古筝坊扩展到20个字段,新增了header1/header2头部渐变色、silk丝色、ink墨色等古筝专用色,更精准地还原了古筝的材质质感。
3.2 色彩常量定义
const COLORS: ColorPalette = {
bg: '#F7F1E6',
cardBg: '#FFFFFF',
header1: '#3E2A1A',
header2: '#1F140C',
wood: '#8A5A2B',
amber: '#D9A441',
silk: '#E8C87A',
ink: '#2B2118',
title: '#F5EAD0',
sub: '#C9B28A',
text1: '#3E2A1A',
text2: '#7A6A52',
text3: '#A89A80',
accent: '#8A5A2B',
win: '#C0392B',
lose: '#2E7D32',
danger: '#D9534F',
tabBg: '#3E2A1A',
tabOn: '#D9A441',
mask: 'rgba(0,0,0,0.45)'
};

本应用配色最大特色是木色wood #8A5A2B 与琥珀amber #D9A441——木色用于古筝面板色调、按钮强调、雁柱色块(是古筝桐木面板的温润木色调),琥珀色用于头部装饰条、雁柱琴码、Tab选中态、统计数字(是古筝琥珀色琴码的明亮色调),与宣纸色底bg #F7F1E6形成"木色琥珀丝色墨色"的中国古筝经典配色。丝色silk #E8C87A用于头部琴弦线条(是古筝丝弦的淡金色调),墨色ink #2B2118用于深色文字和面板暗部(是古筝墨色面板的沉穆色调),四色各司其职,营造既木色温润又琥珀明亮的古筝非遗氛围。
下面逐一解析每个色值的文化含义:
-
bg #F7F1E6 宣纸色:整个应用的底色基调,模拟古筝谱宣纸的温润质感。宣纸是中国传统书画用纸,色淡黄温润,与古筝琴谱的文化载体高度契合。这个色值不是纯白也不是纯黄,而是带有微微暖调的米黄色,让人联想到宣纸上书写的减字谱和工尺谱。
-
cardBg #FFFFFF 纯白:卡片背景色,与宣纸色底形成层次。白色卡片在米黄底色上显得干净利落,模拟古筝面板的白色桐木纹理。
-
header1 #3E2A1A 深木色:头部渐变起始色,是古筝边框红木的深木色调。这个色值接近红木家具的深棕红色,与古筝侧板的红木材质呼应。
-
header2 #1F140C 墨黑色:头部渐变终止色,是古筝底板的墨黑色调。这个色值比header1更深,模拟古筝底部面板的沉穆色调,header1到header2的160度线性渐变营造了古筝从侧板到底板的材质过渡效果。
-
wood #8A5A2B 木色:古筝面板的核心色值,是桐木面板的温润木色调。用于头部琴体色块、雁柱行色块、按钮背景,是整个应用最核心的主题色。#8A5A2B是一个中等明度的棕橙色,既不过亮也不过暗,完美还原了桐木面板打磨后的温润光泽。
-
amber #D9A441 琥珀色:古筝装饰的主色调,是琥珀色琴码的明亮色调。用于头部装饰边框、雁柱色块、Tab选中态、统计数字、张力高阶着色。#D9A441是一个明亮的金橙色,模拟琥珀的透亮光泽,与木色形成明暗对比。
-
silk #E8C87A 丝色:古筝琴弦的丝色调,是丝弦的淡金色调。用于头部琴弦线条、侧弦色块。#E8C87A是一个柔和的淡金色,模拟蚕丝琴弦在灯光下的丝光质感。
-
ink #2B2118 墨色:古筝面板的墨色调,是墨色文字的深底色。虽然在本应用中ink字段定义了但直接使用较少,text1 #3E2A1A承担了大部分深色文字的功能。#2B2118是一个极深的棕黑色,接近墨汁的颜色,呼应古筝面板上的墨色题字。
-
title #F5EAD0 浅金色:头部标题文字色,是宣纸上淡金题字的色调。用于头部"古筝坊"标题文字。#F5EAD0是一个极浅的暖金色,在深色头部背景上显得醒目而温润。
-
sub #C9B28A 棕色:头部副标题文字色,是古筝木纹的棕色调。用于头部"丝弦雅韵 · 十三弦动"副标题和统计标签文字。#C9B28A是一个中明度的棕色,与title形成主次层次。
-
text1 #3E2A1A 深木色文字:主文字色,与header1相同。用于卡片标题、按钮文字。非纯黑更有文化感,模拟宣纸上的墨色书写。
-
text2 #7A6A52 中木色文字:副文字色,用于卡片描述信息。#7A6A52是一个中明度的灰棕色,与text1形成主次层次。
-
text3 #A89A80 浅木色文字:提示文字色,用于辅助信息、统计标签、单位标注。#A89A80是一个浅灰棕色,在白色卡片上显得柔和。
-
accent #8A5A2B 强调色:与wood相同的木色,用于按钮背景、统计数字、工时横条。accent和wood值相同但语义不同——accent表示"强调用途",wood表示"材质用途",这种语义化命名便于后续独立调整。
-
win #C0392B 朱红色:高阶/高值着色,用于高张力、高难度、高分值、价格。#C0392B是一个朱红色,模拟古筝朱漆的色调,用于警示和强调高值数据。在tensionColor中张力≥80%用朱红色(高张力紧张),在levelColor中含"难"用朱红色(高难度),在orderRow中金额用朱红色(高价值)。
-
lose #2E7D32 竹青色:低阶/低值着色,用于低张力、低难度。#2E7D32是一个竹青色,模拟竹子的青绿色调,用于表示安全和低值。在tensionColor中张力<60%用竹青色(低张力松弛),在levelColor中不含"难"和"中"用竹青色(入门级)。
-
danger #D9534F 危险红:删除按钮背景色。#D9534F是一个明亮的红色,比win朱红色更浅更亮,用于危险操作(删除雅集)的视觉警示。
-
tabBg #3E2A1A Tab背景色:与header1相同的深木色,底部导航栏背景。模拟古筝底板的深木色调,与白色内容区形成鲜明对比。
-
tabOn #D9A441 Tab选中色:与amber相同的琥珀色,Tab选中态文字色和高亮背景。模拟古筝琥珀色琴码的明亮色调,在深色Tab背景上显得醒目。
-
mask rgba(0,0,0,0.45) 遮罩色:弹窗遮罩,45%透明度的纯黑色。比香囊坊的50%深紫遮罩更暗更中性,突出弹窗内容的白底卡片。
3.3 颜色分级函数体系
应用定义了9个工具函数,其中6个涉及颜色和尺寸映射,将映射逻辑从UI层抽离:
| 函数名 | 输入维度 | 分级逻辑 | 用途 |
|---|---|---|---|
| stringX | 弦索引i | 26 + i * 15 | 头部琴弦X轴定位 |
| bridgeX | 柱索引i | 30 + i * 15 | 头部雁柱X轴定位 |
| tensionBarW | 张力t | 16 + min(t,100) * 1.3 | 琴弦张力进度条宽度 |
| tensionColor | 张力t | ≥80朱红/≥60琥珀/其他竹青 | 琴弦张力颜色分级 |
| craftBarW | 工时h | 18 + min(h,200) * 0.55 | 工序进度条宽度 |
| stageColor | 阶段 | 含"精"朱红/含"中"琥珀/其他竹青 | 工序阶段颜色分级 |
| orderBarH | 金额v | 20 + min(v,20000) / 250 | 订单柱状图高度 |
| levelColor | 难度 | 含"难"朱红/含"中"琥珀/其他竹青 | 琴谱难度颜色分级 |
| schoolColor | 流派 | 含"浙"琥珀/含"潮"朱红/含"客家"竹青/其他木色 | 名家流派颜色分级 |
六个函数形成了统一的"三级着色"设计语言——高用朱红色win、中用琥珀色amber、低用竹青色lose,tensionColor/stageColor/levelColor的分级逻辑完全一致(只是判断维度不同),schoolColor则额外增加了accent木色作为默认值,保证了视觉一致性。这种"数据即配色、数据即尺寸"的动态映射模式让每个数据项都能根据自身属性获得对应的视觉表
四、导航Tab设计
4.1 Tab元数据接口与列表
应用定义了TabMeta接口和TAB_LIST数组,将八个Tab的标签和图标配置化:
interface TabMeta {
label: string;
icon: string;
}
const TAB_LIST: TabMeta[] = [
{ label: '古筝', icon: '🎼' },
{ label: '琴弦', icon: '🧵' },
{ label: '雁柱', icon: '🪵' },
{ label: '琴谱', icon: '📖' },
{ label: '名家', icon: '🏆' },
{ label: '工序', icon: '🔨' },
{ label: '雅集', icon: '🍵' },
{ label: '订单', icon: '📦' }
];
TabMeta接口只有两个字段——label标签文字和icon图标Emoji,简洁明了。TAB_LIST数组按顺序定义了八个Tab:古筝(🎼乐谱图标)、琴弦(🧵线轴图标)、雁柱(🪵木块图标)、琴谱(📖书本图标)、名家(🏆奖杯图标)、工序(🔨锤子图标)、雅集(🍵茶杯图标)、订单(📦包裹图标)。每个图标都精准呼应了对应模块的文化内涵——🎼对应古筝演奏、🧵对应琴弦材质、🪵对应雁柱木质、📖对应琴谱典籍、🏆对应名家成就、🔨对应制筝工艺、🍵对应雅集茶话、📦对应订单物流。这种"配置驱动"的设计模式让Tab的增删改只需修改TAB_LIST数组,无需改动UI代码。
4.2 双行四列索引与专用索引
const ROW1_IDX: number[] = [0, 1, 2, 3];
const ROW2_IDX: number[] = [4, 5, 6, 7];
const STR_IDX: number[] = [0, 1, 2, 3, 4, 5];
const BRIDGE_IDX: number[] = [0, 1, 2, 3, 4, 5];
const ORDER_BAR_IDX: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
ROW1_IDX和ROW2_IDX将八个Tab分为两行四列——第一行[0,1,2,3]是古筝/琴弦/雁柱/琴谱(产品与内容),第二行[4,5,6,7]是名家/工序/雅集/订单(人物与运营)。这种分组布局让用户一眼就能区分"产品维度"和"运营维度",符合古筝坊"前四Tab展示产品、后四Tab管理运营"的信息架构。
STR_IDX [0,1,2,3,4,5] 和 BRIDGE_IDX [0,1,2,3,4,5] 是头部动画的专用索引——分别用于ForEach渲染6根琴弦和6个雁柱。虽然古筝实际有21弦,但头部动画空间有限,用6根弦和6个雁柱即可表达古筝"弦柱交错"的视觉意象。ORDER_BAR_IDX [0,1,2,3,4,5,6,7,8,9] 是订单柱状图的专用索引,对应10笔订单的柱子编号。
4.3 底部导航构建

@Builder
bottomItem(i: number) {
Column() {
Text(TAB_LIST[i].icon)
.fontSize(17)
Text(TAB_LIST[i].label)
.fontSize(11)
.fontColor(this.curTab === i ? COLORS.amber : COLORS.sub)
.margin({ top: 2 })
}
.width('25%')
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === i ? '#4A3420' : COLORS.tabBg)
.borderRadius(10)
.onClick(() => {
this.curTab = i;
})
}
每个Tab项采用Column纵向布局——图标17号Emoji在上,标签11号文字在下,间距margin top 2。每个Tab项宽度25%(四等分一行),padding上下7。选中态文字amber琥珀色+背景#4A3420深棕高亮(比tabBg #3E2A1A略亮,形成层次),非选中态文字sub浅棕色+背景tabBg深棕底。borderRadius 10圆角让每个Tab项像独立的"琴码"按钮。点击设置curTab = i触发tabContent条件渲染重新构建。
@Builder
bottomBar() {
Column() {
Row() {
ForEach(ROW1_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r1' + i)
}
.width('100%')
Row() {
ForEach(ROW2_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r2' + i)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.tabBg)
.borderRadius({ topLeft: 18, topRight: 18, bottomLeft: 0, bottomRight: 0 })
}
bottomBar采用Column包裹两个Row——第一行ForEach遍历ROW1_IDX [0,1,2,3]渲染古筝/琴弦/雁柱/琴谱四个Tab,第二行ForEach遍历ROW2_IDX [4,5,6,7]渲染名家/工序/雅集/订单四个Tab,行间距margin top 6。整体padding 10+tabBg深木色背景+顶部圆角18(模拟古筝面板弧线)。ForEach的key用’r1’+i和’r2’+i前缀区分两行避免key冲突。这种双行四列布局让8个Tab在有限屏幕空间内合理排布,比单排8Tab更紧凑,比单排4Tab更完整。
五、数据接口定义
应用定义了8个数据接口,将每个业务实体的数据结构规范化:
5.1 古筝项 ZhengItem
interface ZhengItem {
name: string;
wood: string;
strings: number;
length: number;
price: number;
}
ZhengItem定义了古筝的五个属性——name名称(如"敦煌 · 蜶恋花")、wood面板材质(如"桐木面板")、strings弦数(如21)、length琴长cm(如163)、price价格(如16800)。五个属性完整描述了一把古筝的核心参数,从材质到尺寸到价格一目了然。
5.2 琴弦项 StringItem
interface StringItem {
name: string;
gauge: string;
material: string;
tension: number;
price: number;
}
StringItem定义了琴弦的五个属性——name名称(如"钢丝尼龙弦 · 1号")、gauge线径(如"0.12mm")、material材质(如"钢丝缠尼龙")、tension张力百分比(如88)、price价格(如36)。tension是琴弦的核心参数——张力越高音色越明亮紧张,张力越低音色越松弛柔和。
5.3 雁柱项 BridgeItem
interface BridgeItem {
name: string;
wood: string;
height: number;
price: number;
}
BridgeItem定义了雁柱(琴码)的四个属性——name名称(如"紫檀雁柱")、wood材质(如"紫檀")、height高度cm(如4.2)、price价格(如68)。雁柱是古筝承弦定音的关键部件——不同材质的雁柱影响音色传导,不同高度影响弦距手感。
5.4 琴谱项 ScoreItem
interface ScoreItem {
title: string;
genre: string;
level: string;
pages: number;
}
ScoreItem定义了琴谱的四个属性——title曲名(如"渔舟唱晚")、genre流派体裁(如"河南筝曲")、level难度(如"入门"/“中级”/“高级”/“高难”)、pages页数(如6)。level字段是分级着色的核心——含"难"用朱红色、含"中"用琥珀色、其他用竹青色。
5.5 名家项 MasterItem

interface MasterItem {
name: string;
school: string;
works: number;
style: string;
}
MasterItem定义了筝界名家的四个属性——name姓名(如"曹正")、school流派(如"浙派")、works作品数(如42)、style演奏风格(如"质朴浑厚")。school字段是流派着色的核心——含"浙"用琥珀色、含"潮"用朱红色、含"客家"用竹青色、其他用木色。
5.6 工序项 CraftItem

interface CraftItem {
name: string;
hours: number;
stage: string;
}
CraftItem定义了制筝工序的三个属性——name工序名(如"挖膛调音")、hours工时(如120)、stage阶段(如"精序")。hours驱动工时横条宽度(craftBarW),stage驱动阶段标签颜色(stageColor)——含"精"用朱红色、含"中"用琥珀色、其他用竹青色。
5.7 雅集项 SalonItem

interface SalonItem {
name: string;
place: string;
date: string;
members: number;
}
SalonItem定义了雅集活动的四个属性——name活动名(如"三秋雅集")、place地点(如"杭州西湖")、date日期(如"2026-08-22")、members报名人数(如36)。雅集是古筝文化的重要社交形式——以琴会友、切磋技艺,从西湖到个园到网师园到碑林,每场雅集都选址文化胜地。
5.8 订单项 OrderItem

interface OrderItem {
no: string;
customer: string;
amount: number;
date: string;
}
OrderItem定义了订单的四个属性——no订单号(如"SO-260801")、customer客户(如"岭南琴社")、amount金额(如16800)、date日期(如"08-01")。amount驱动柱状图高度(orderBarH),是订单台账的核心数据维度。
六、@Observed可观察数据类
6.1 数据类定义模式
与香囊坊的聚合数据类NangData不同,古筝坊将八个数据实体各自定义为独立的@Observed类,每个类有独立的constructor构造函数。这种设计让每个数据实体独立可观察、独立可实例化,便于在不同场景下灵活组合。
@Observed
export class ZhengItem {
name: string;
wood: string;
strings: number;
length: number;
price: number;
constructor(name: string, wood: string, strings: number, length: number, price: number) {
this.name = name;
this.wood = wood;
this.strings = strings;
this.length = length;
this.price = price;
}
}
@Observed装饰器让ZhengItem类的实例变为可观察对象——当实例的属性被修改时(如editModal中修改tension),引用该实例的UI会自动响应式刷新。constructor构造函数接收五个参数并赋值给实例属性,通过new关键字实例化。这种"class + constructor + new"的模式是ArkTS/TypeScript标准的面向对象实践,比香囊坊的"对象字面量初始化"更规范、更适合复杂数据场景。
其余七个@Observed类(StringItem/BridgeItem/ScoreItem/MasterItem/CraftItem/SalonItem/OrderItem)结构一致——@Observed装饰+属性声明+constructor构造函数,此处不再逐一展示,它们的完整代码参见文末完整源码部分。
6.2 @State数据数组初始化
入口组件ZhengPage通过八个@State数组管理全部业务数据:
@State zhengs: ZhengItem[] = [
new ZhengItem('敦煌 · 蝶恋花', '桐木面板', 21, 163, 16800),
new ZhengItem('朱雀 · 凤鸣岐山', '泡桐', 21, 163, 9800),
new ZhengItem('龙凤 · 荷塘月色', '楠木', 21, 165, 7200),
new ZhengItem('韵牌 · 高山流水', '红木', 21, 163, 5600),
new ZhengItem('弦昇 · 紫檀嵌螺钿', '紫檀', 21, 166, 32000),
new ZhengItem('琼花 · 古法断纹', '杉木', 21, 163, 12800),
new ZhengItem('鼎韵 · 素面新筝', '楸木', 21, 162, 3800),
new ZhengItem('宏声 · 花梨木纹', '花梨', 21, 164, 6800),
new ZhengItem('中州 · 竹节筝', '楠竹', 18, 150, 2600),
new ZhengItem('盛世 · 金玉满堂', '酸枝', 21, 166, 25800),
new ZhengItem('清韵 · 少儿练习筝', '桐木', 16, 135, 1800),
new ZhengItem('名匠 · 整挖筝', '百年桐木', 21, 168, 45000)
];
12款古筝数据完整呈现了中国古筝的品牌与材质生态。价格从¥1800(清韵·少儿练习筝,16弦135cm桐木入门款)到¥45000(名匠·整挖筝,21弦168cm百年桐木整挖顶级款),名匠·整挖筝¥45000最贵(百年桐木整挖工艺最复杂,整挖筝是一体成型音色浑厚),清韵·少儿练习筝¥1800最实惠(16弦135cm桐木入门款,适合少儿初学)。弦数从16弦(清韵·少儿练习筝,入门练习用)到21弦(标准现代筝),18弦(中州·竹节筝,便携款)。琴长从135cm(清韵·少儿练习筝,短款便携)到168cm(名匠·整挖筝,加长款共鸣更佳)。材质覆盖桐木面板/泡桐/楠木/红木/紫檀/杉木/楸木/花梨/楠竹/酸枝/百年桐木十一种,从经济型桐木到顶级百年桐木到奢华紫檀嵌螺钿,完整呈现中国古筝从入门到收藏的全价位产品线。
@State strings: StringItem[] = [
new StringItem('钢丝尼龙弦 · 1号', '0.12mm', '钢丝缠尼龙', 88, 36),
new StringItem('钢丝尼龙弦 · 3号', '0.18mm', '钢丝缠尼龙', 82, 38),
new StringItem('钢丝尼龙弦 · 5号', '0.24mm', '钢丝缠尼龙', 74, 40),
new StringItem('钢丝尼龙弦 · 8号', '0.32mm', '钢丝缠尼龙', 66, 44),
new StringItem('钢丝尼龙弦 · 12号', '0.42mm', '钢丝缠尼龙', 58, 48),
new StringItem('缠丝钢弦 · 16号', '0.55mm', '铜丝缠绕', 52, 55),
new StringItem('缠丝钢弦 · 21号', '0.72mm', '铜丝缠绕', 45, 62),
new StringItem('文化衫 · 丝弦套装', '全弦21根', '尼龙/钢丝', 60, 460)
];
8种琴弦数据覆盖了从1号到21号的完整规格。线径从0.12mm(1号弦最细,高音区)到0.72mm(21号弦最粗,低音区),张力从88%(1号弦最高,高音区张力大)到45%(21号弦最低,低音区张力小)。前5种是钢丝尼龙弦(钢丝缠尼龙材质,1-12号高音中音区),后2种是缠丝钢弦(铜丝缠绕材质,16-21号低音区),最后1种是套装(全弦21根)。张力分级着色——88%和82%≥80用win朱红色(高张力紧张),74%和66%≥60用amber琥珀色(中张力适中),58%/52%/45%<60用lose竹青色(低张力松弛)。
其余六组数据(bridges/scores/masters/crafts/salons/orders)结构一致——@State数组+new实例化初始化,此处不再逐一展示完整代码,它们的完整代码参见文末完整源码部分。数据规模统计:zhengs 12款 + strings 8种 + bridges 8种 + scores 8首 + masters 8位 + crafts 8道 + salons 8场 + orders 10笔 = 70条数据,完整呈现中国古筝非遗手作平台的数据生态。
七、工具函数
应用定义了9个工具函数,将定位、颜色、尺寸映射逻辑从UI层抽离。前文已在颜色分级函数体系中展示了函数总览表和数据流图,此处逐一深入解析每个函数的实现。
7.1 定位函数 stringX 与 bridgeX
function stringX(i: number): number {
return 26 + i * 15;
}
function bridgeX(i: number): number {
return 30 + i * 15;
}
stringX和bridgeX是头部动画的X轴定位函数。stringX以26为起始偏移、15为间距,计算第i根琴弦的X坐标——第0根x=26、第1根x=41、第2根x=56…第5根x=101。bridgeX以30为起始偏移(比stringX偏移4px,让雁柱位于两根琴弦之间)、15为间距,计算第i个雁柱的X坐标——第0个x=30、第1个x=45…第5个x=105。两个函数的间距相同(15px),但起始偏移不同(26 vs 30),实现了"琴弦与雁柱交错排列"的视觉效果——每根琴弦位于两个雁柱之间的中点,模拟古筝"弦穿柱上"的真实物理结构。
7.2 张力函数 tensionBarW 与 tensionColor
function tensionBarW(t: number): number {
return 16 + Math.min(t, 100) * 1.3;
}
function tensionColor(t: number): string {
if (t >= 80) {
return COLORS.win;
}
if (t >= 60) {
return COLORS.amber;
}
return COLORS.lose;
}
tensionBarW将张力值映射为进度条像素宽度——基础宽度16px + min(张力,100) * 1.3。张力88%对应16+881.3=16+114.4=130.4px,张力45%对应16+451.3=16+58.5=74.5px。Math.min(t, 100)确保张力超过100时进度条不会溢出(封顶100%),16px基础宽度确保即使张力为0也有最小可见宽度。tensionColor三级着色——张力≥80%用win朱红色#C0392B(高张力紧张,如1号弦88%/3号弦82%,是高音区弦距紧音色明亮)、张力≥60%用amber琥珀色#D9A441(中张力适中,如5号弦74%/8号弦66%,是中音区弦距适中)、张力<60%用lose竹青色#2E7D32(低张力松弛,如12号弦58%/16号弦52%/21号弦45%,是低音区弦距松音色沉)。这种"以张力定颜色"的动态着色模式让琴弦张力分布一目了然。
7.3 工序函数 craftBarW 与 stageColor
function craftBarW(h: number): number {
return 18 + Math.min(h, 200) * 0.55;
}
function stageColor(stage: string): string {
if (stage.indexOf('精') >= 0) {
return COLORS.win;
}
if (stage.indexOf('中') >= 0) {
return COLORS.amber;
}
return COLORS.lose;
}
craftBarW将工时值映射为进度条像素宽度——基础宽度18px + min(工时,200) * 0.55。工时120小时对应18+1200.55=18+66=84px,工时16小时对应18+160.55=18+8.8=26.8px。Math.min(h, 200)封顶200小时,0.55的系数让最长工时120小时(挖膛调音)的进度条约为84px,在卡片宽度内合理显示。stageColor根据阶段字符串包含的关键字着色——含"精"用win朱红色(精序阶段,如挖膛调音120h/涂漆晾干72h/装弦校音16h,是制筝核心精修工序)、含"中"用amber琥珀色(中序阶段,如木框成型40h/面板刨制28h,是制筝中期成型工序)、其他用lose竹青色(初序/末序阶段,如选材开料24h/面板烘烤36h/整体修整20h,是制筝基础和收尾工序)。注意stageColor用的是indexOf字符串包含匹配而非精确匹配,"精序"包含"精"字所以匹配。
7.4 金额高度函数 orderBarH
function orderBarH(v: number): number {
return 20 + Math.min(v, 20000) / 250;
}
orderBarH将订单金额映射为柱状图柱子像素高度——基础高度20px + min(金额,20000) / 250。金额45000对应20+min(45000,20000)/250=20+20000/250=20+80=100px,金额1800对应20+1800/250=20+7.2=27.2px。Math.min(v, 20000)封顶20000元(超过20000的柱子高度不再增长,避免过高),/250的除数让20000元对应80px高度增量,加上基础20px最高100px。这种"数值除以基准值映射高度"的方式简洁直观,让金额差异一目了然——民乐团采购45000(封顶20000计算,最高柱)vs 少儿培训中心1800(最矮柱)。
7.5 难度函数 levelColor
function levelColor(level: string): string {
if (level.indexOf('难') >= 0) {
return COLORS.win;
}
if (level.indexOf('中') >= 0) {
return COLORS.amber;
}
return COLORS.lose;
}
levelColor根据难度字符串包含的关键字着色——含"难"用win朱红色(高难度,如"高级"含"难"字?不,注意"高级"不含"难"。“高难"含"难"字,如林冲夜奔"高难"用朱红色)、含"中"用amber琥珀色(中级,如高山流水"中级”/出水莲"中级"/浏阳河"中级")、其他用lose竹青色(入门,如渔舟唱晚"入门"/茉莉芬芳"入门")。注意"高级"不含"难"字也不含"中"字,所以"高级"会落到else分支用lose竹青色——这是一个值得注意的边界情况。实际数据中"战台风"是"高级"、“寒鸦戏水"是"高级”,它们不含"难"和"中"所以用竹青色,只有"林冲夜奔"是"高难"含"难"字用朱红色。
7.6 流派函数 schoolColor
function schoolColor(school: string): string {
if (school.indexOf('浙') >= 0) {
return COLORS.amber;
}
if (school.indexOf('潮') >= 0) {
return COLORS.win;
}
if (school.indexOf('客家') >= 0) {
return COLORS.lose;
}
return COLORS.accent;
}
schoolColor根据流派字符串包含的关键字着色——含"浙"用amber琥珀色(浙江派,如曹正"浙派"/王昌元"浙派",浙江派华丽流畅用琥珀色明亮)、含"潮"用win朱红色(潮州派,如郭鹰"潮州派"/林毛根"潮州派",潮州派清丽细腻用朱红色醒目)、含"客家"用lose竹青色(客家派,如罗九香"客家派",客家派古淡典雅用竹青色沉静)、其他用accent木色(如赵玉斋"山东派"/项斯华"学院派"/李萌"现代派",用木色作为默认中性色)。schoolColor是六个分级函数中唯一有四个级别的函数(其他都是三级),多了一个accent木色默认分支,体现了古筝流派"五大流派+学院+现代"的多样性。
八、头部动画:琴弦opacity呼吸与雁柱translate振动
头部区域pageHeader是本应用最具创意的动画设计——琴弦opacity呼吸动画+雁柱translate振动动画+共振点scale缩放动画,通过Stack层叠构建了一个微缩的古筝面板视觉意象。
8.1 头部容器与渐变背景
@Builder
pageHeader() {
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.borderRadius(22)
.linearGradient({
angle: 160,
colors: [[COLORS.header1, 0], [COLORS.header2, 1]]
})
Stack层叠的第一层是全屏渐变背景——100%宽高+borderRadius 22圆角+linearGradient 160度线性渐变,从header1 #3E2A1A深木色(0%起始)到header2 #1F140C墨黑色(100%终止)。160度渐变角度让色彩从左上到右下过渡,模拟古筝面板从侧板到底板的光影变化。这个深色渐变背景为后续的琴弦、雁柱、标题提供了高对比度的暗色画布。
8.2 琴体色块与侧弦
Column()
.width('86%')
.height(64)
.borderRadius(30)
.backgroundColor(COLORS.wood)
.border({ width: 2, color: COLORS.amber })
.position({ x: 24, y: 26 })
Column()
.width(8)
.height(48)
.backgroundColor(COLORS.silk)
.borderRadius(4)
.position({ x: 14, y: 34 })
第二层是琴体色块——86%宽、64高、borderRadius 30圆角、wood木色#8A5A2B底色+amber琥珀色2px边框,position定位在x=24,y=26。这个圆角矩形模拟古筝面板的桐木色块,琥珀色边框模拟古筝侧板的装饰线条。第三层是侧弦——8宽、48高、silk丝色#E8C87A底色+borderRadius 4圆角,position定位在x=14,y=34。这个窄长条模拟古筝左侧的侧弦(岳山弦),丝色淡金模拟蚕丝光泽。琴体色块和侧弦的位置关系——侧弦在琴体左侧紧贴,模拟古筝"弦从左侧岳山引出、穿过面板上的雁柱"的物理结构。
8.3 琴弦opacity呼吸动画
ForEach(STR_IDX, (i: number) => {
Column()
.width(2)
.height(46)
.backgroundColor(COLORS.silk)
.opacity(this.breath ? 1 : 0.55)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: stringX(i), y: 35 })
}, (i: number) => 'str' + i)
第四层是6根琴弦——ForEach遍历STR_IDX [0,1,2,3,4,5],每根弦是2宽、46高、silk丝色#E8C87A底色的窄长条。核心动画是opacity呼吸——this.breath ? 1 : 0.55,breath为true时完全不透明(opacity 1),breath为false时半透明(opacity 0.55),配合animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })实现520ms一次往返的无限呼吸。PlayMode.Alternate让动画在1↔0.55之间往返播放而非跳回起点,模拟琴弦在拨动后的震颤余韵。6根弦的X坐标由stringX(i)计算——第0根x=26、第1根x=41…第5根x=101,均匀分布在琴体色块上方。ForEach的key用’str’+i确保每根弦独立渲染。
8.4 雁柱translate振动动画
ForEach(BRIDGE_IDX, (i: number) => {
Column()
.width(7)
.height(9)
.borderRadius(2)
.backgroundColor(COLORS.amber)
.translate({ y: this.breath ? -2 : 2 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: bridgeX(i), y: 56 })
}, (i: number) => 'brg' + i)
第五层是6个雁柱——ForEach遍历BRIDGE_IDX [0,1,2,3,4,5],每个雁柱是7宽、9高、borderRadius 2圆角、amber琥珀色#D9A441底色的小方块。核心动画是translate振动——this.breath ? -2 : 2,breath为true时向上位移2px(translate y:-2),breath为false时向下位移2px(translate y:2),配合520ms Alternate往返动画,模拟雁柱在琴弦震动时的上下微颤。6个雁柱的X坐标由bridgeX(i)计算——第0个x=30、第1个x=45…第5个x=105,与琴弦交错排列(雁柱在两根弦之间的中点)。Y坐标56比琴弦Y坐标35低21px,让雁柱位于琴弦下方(模拟古筝"弦在柱上、柱在面板上"的结构)。
8.5 共振点scale缩放动画
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor(COLORS.amber)
.scale({ x: this.breath ? 1.4 : 1, y: this.breath ? 1.4 : 1 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 208, y: 30 })
第六层是1个共振点——10宽、10高、borderRadius 5圆形、amber琥珀色底色。核心动画是scale缩放——this.breath ? 1.4 : 1,breath为true时放大到1.4倍,breath为false时恢复原大小1倍,配合520ms Alternate往返动画,模拟古筝共鸣箱的脉动呼吸。共振点position定位在x=208,y=30,位于琴体色块右上方,模拟古筝右侧的共鸣点(右侧是弹奏区域,共鸣最强烈)。三种动画(opacity呼吸/translate振动/scale缩放)同步以520ms节奏播放,形成"弦颤、柱振、箱鸣"的三重动态意境。
8.6 标题文字层
Column() {
Text('古筝坊')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Text('丝弦雅韵 · 十三弦动')
.fontSize(12)
.fontColor(COLORS.sub)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 250, y: 40 })
}
.width('100%')
.height(136)
第七层是标题文字——Column纵向布局,"古筝坊"24号粗体title浅金色#F5EAD0+"丝弦雅韵 · 十三弦动"12号sub棕色#C9B28A,margin top 6间距。alignItems Start左对齐,position定位在x=250,y=40(位于动画元素右侧)。"十三弦动"点明古筝唐宋十三弦的历史——唐代古筝为十三弦,是古筝发展史上的经典形制,"十三弦动"寓意弦弦皆动、音音传情。整个Stack高度136,包含了渐变背景、琴体、侧弦、6根琴弦、6个雁柱、共振点、标题文字七层视觉元素,构建了一个完整的古筝面板微缩意象。
.position({ x: 250, y: 40 })
}
.width('100%')
.height(136)
第七层是标题文字——Column纵向布局,"古筝坊"24号粗体title浅金色#F5EAD0+"丝弦雅韵 · 十三弦动"12号sub棕色#C9B28A,margin top 6间距。alignItems Start左对齐,position定位在x=250,y=40(位于动画元素右侧)。"十三弦动"点明古筝唐宋十三弦的历史——唐代古筝为十三弦,是古筝发展史上的经典形制,"十三弦动"寓意弦弦皆动、音音传情。整个Stack高度136,包含了渐变背景、琴体、侧弦、6根琴弦、6个雁柱、共振点、标题文字七层视觉元素,构建了一个完整的古筝面板微缩意象。
### 8.7 统计行
```typescript
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('传世古筝')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('21')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('标准弦数')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('制筝工序')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('10')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('本月订单')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 8, right: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 16, topRight: 16, bottomLeft: 0, bottomRight: 0 })
.margin({ top: 10 })
统计行是pageHeader的第二部分——Row包裹四个Column,每个layoutWeight(1)等分宽度。四个统计项:12传世古筝、21标准弦数、8制筝工序、10本月订单。数字20号粗体amber琥珀色+标签10号sub棕色。cardBg白底+顶部圆角16+margin top 10与头部Stack分隔。四个统计数字概括了古筝坊的核心数据——12款古筝产品、21弦标准形制、8道制筝工序、10笔本月订单,让用户一进入应用就能掌握全局概况。
8.8 呼吸动画驱动
aboutToAppear(): void {
setInterval(() => {
this.breath = !this.breath;
}, 520);
}
aboutToAppear是组件生命周期钩子,在组件创建后、渲染前调用。通过setInterval每520ms翻转breath布尔值——true↔false循环切换。520ms的间隔与所有animation的duration 520ms完全同步,确保breath值变化时所有动画元素同步进入下一帧。PlayMode.Alternate让动画在两个状态之间往返,配合breath的翻转,实现了"breath翻true→动画从false态过渡到true态→520ms后breath翻false→动画从true态过渡回false态→无限循环"的呼吸效果。这个单一的breath状态变量驱动了头部6根琴弦opacity呼吸、6个雁柱translate振动、1个共振点scale缩放,以及后续列表中张力横条opacity呼吸、工时横条opacity呼吸、柱状图柱子opacity呼吸,是整个应用的动画心脏。
上图为呼吸动画的驱动流程图——一个setInterval定时器+一个breath布尔值驱动了8组动画元素,实现了"一变量控全场"的高效动画架构。所有动画的duration都是520ms、iterations都是-1(无限循环)、playMode都是Alternate(往返),确保了全局动画的节奏统一。这种"单一状态变量驱动全局动画"的设计模式是ArkUI呼吸动画的经典实践。
九、列表渲染:各@Builder行渲染方法
应用定义了8个@Builder行渲染方法,每个Tab对应一个行渲染方法,构建各具特色的列表卡片。
9.1 古筝行 zhengRow
@Builder
zhengRow(item: ZhengItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.wood + ' · ' + item.length + 'cm')
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.strings + '弦')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('¥' + item.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 6 })
Text('订')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.accent)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.showAdd = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
古筝行采用三列布局——第一列是layoutWeight(1)弹性信息区(古筝名称15号Medium木色text1+材质长度信息11号灰棕text2"桐木面板 · 163cm"),第二列是弦数价格区("21弦"12号粗体木色accent+“¥16800"13号粗体朱红色win,右对齐),第三列是"订"按钮(12号白字木色底圆角10,点击触发showAdd=true打开新增订单弹窗)。卡片cardBg白底+borderRadius 14圆角+padding 12+margin bottom 8间距。zhengRow的核心交互是"订"按钮——点击后showAdd变为true,build()中的if (this.showAdd)条件渲染addModal弹窗,用户填写客户名称/订单金额/交付日期后点击"保存”,通过this.orders.splice(0, 0, new OrderItem(...))在订单数组头部插入新订单,形成"浏览古筝→订购→新增订单"的交互闭环。
9.2 琴弦行 stringRow
@Builder
stringRow(item: StringItem, i: number) {
Column() {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.gauge + ' · ' + item.material)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('¥' + item.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(item.tension + '% 张力')
.fontSize(10)
.fontColor(tensionColor(item.tension))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 6 })
Text('订')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.accent)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.showAdd = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
stringRow(item: StringItem, i: number) {
Column() {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.gauge + ' · ' + item.material)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('¥' + item.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(item.tension + '% 张力')
.fontSize(10)
.fontColor(tensionColor(item.tension))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('改')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.amber)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selString = item;
this.showEdit = true;
})
}
.width('100%')
Row() {
Column()
.width(tensionBarW(item.tension))
.height(8)
.borderRadius(4)
.backgroundColor(tensionColor(item.tension))
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text('张力标定')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
bridgeRow(item: BridgeItem, i: number) {
Row() {
Column()
.width(14)
.height(18)
.borderRadius(3)
.backgroundColor(COLORS.wood)
.translate({ y: this.breath ? -2 : 2 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text('高 ' + item.height + 'cm · 承弦支撑')
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Text('¥' + item.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
scoreRow(item: ScoreItem, i: number) {
Row() {
Column() {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.genre)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.level)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.backgroundColor(levelColor(item.level))
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text(item.pages + ' 页')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
masterRow(item: MasterItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.style)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.school)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.backgroundColor(schoolColor(item.school))
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text(item.works + ' 部作品')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
craftRow(item: CraftItem, i: number) {
Column() {
Row() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.stage)
.fontSize(10)
.fontColor(COLORS.cardBg)
.backgroundColor(stageColor(item.stage))
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.hours + ' 工时')
.fontSize(11)
.fontColor(COLORS.text2)
}
.width('100%')
Row() {
Column()
.width(craftBarW(item.hours))
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.accent)
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text('工序时长')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
salonRow(item: SalonItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.place + ' · ' + item.date)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.members + ' 人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('报名')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('删')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selSalon = item;
this.showDel = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
orderRow(item: OrderItem, i: number) {
Row() {
Column() {
Text(item.customer)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.no + ' · ' + item.date)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('¥' + item.amount)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
orderChart() {
Column() {
Row() {
Text('📊')
.fontSize(15)
Text('订单金额分布')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ left: 6 })
Column().layoutWeight(1)
Text('单位:元')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.padding({ left: 4, right: 4, bottom: 10 })
Row() {
ForEach(ORDER_BAR_IDX, (i: number) => {
Column() {
Column()
.width(18)
.height(orderBarH(this.orders[i].amount))
.borderRadius({ topLeft: 5, topRight: 5 })
.backgroundColor(i % 2 === 0 ? COLORS.amber : COLORS.accent)
.opacity(this.breath ? 1 : 0.65)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Text((i + 1) + '')
.fontSize(9)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (i: number) => 'bar' + i)
}
.width('100%')
.padding({ left: 6, right: 6, top: 10, bottom: 10 })
.backgroundColor('#FAF4E8')
.borderRadius(12)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 10 })
}
@Builder
tabContent() {
if (this.curTab === 0) {
Column() {
this.sectionTitle('🎼', '传世古筝', '共 ' + this.zhengs.length + ' 款')
ForEach(this.zhengs, (item: ZhengItem, i: number) => {
this.zhengRow(item, i)
}, (item: ZhengItem) => item.name)
}
.width('100%')
} else if (this.curTab === 1) {
Column() {
this.sectionTitle('🧵', '琴弦规格', '张力标定')
ForEach(this.strings, (item: StringItem, i: number) => {
this.stringRow(item, i)
}, (item: StringItem) => item.name)
}
.width('100%')
} else if (this.curTab === 2) {
Column() {
this.sectionTitle('🪵', '雁柱 · 琴码', '承弦定音')
ForEach(this.bridges, (item: BridgeItem, i: number) => {
this.bridgeRow(item, i)
}, (item: BridgeItem) => item.name)
}
.width('100%')
} else if (this.curTab === 3) {
Column() {
this.sectionTitle('📖', '经典琴谱', '按难度分级')
ForEach(this.scores, (item: ScoreItem, i: number) => {
this.scoreRow(item, i)
}, (item: ScoreItem) => item.title)
}
.width('100%')
} else if (this.curTab === 4) {
Column() {
this.sectionTitle('🏆', '筝界名家', '流派传承')
ForEach(this.masters, (item: MasterItem, i: number) => {
this.masterRow(item, i)
}, (item: MasterItem) => item.name)
}
.width('100%')
} else if (this.curTab === 5) {
Column() {
this.sectionTitle('🔨', '制筝工序', '工时横条')
ForEach(this.crafts, (item: CraftItem, i: number) => {
this.craftRow(item, i)
}, (item: CraftItem) => item.name)
}
.width('100%')
} else if (this.curTab === 6) {
Column() {
this.sectionTitle('🍵', '雅集活动', '以琴会友')
ForEach(this.salons, (item: SalonItem, i: number) => {
this.salonRow(item, i)
}, (item: SalonItem) => item.name)
}
.width('100%')
} else {
Column() {
this.sectionTitle('📦', '订单列表', '共 ' + this.orders.length + ' 单')
this.orderChart()
ForEach(this.orders, (item: OrderItem, i: number) => {
this.orderRow(item, i)
}, (item: OrderItem) => item.no)
}
.width('100%')
}
}
@Builder
bottomItem(i: number) {
Column() {
Text(TAB_LIST[i].icon)
.fontSize(17)
Text(TAB_LIST[i].label)
.fontSize(11)
.fontColor(this.curTab === i ? COLORS.amber : COLORS.sub)
.margin({ top: 2 })
}
.width('25%')
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === i ? '#4A3420' : COLORS.tabBg)
.borderRadius(10)
.onClick(() => {
this.curTab = i;
})
}
@Builder
bottomBar() {
Column() {
Row() {
ForEach(ROW1_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r1' + i)
}
.width('100%')
Row() {
ForEach(ROW2_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r2' + i)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.tabBg)
.borderRadius({ topLeft: 18, topRight: 18, bottomLeft: 0, bottomRight: 0 })
}
@Builder
addModal() {
if (this.showAdd) {
Stack() {
this.modalOverlay(() => {
this.showAdd = false;
})
Column() {
Text('新增订单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('录入一笔购筝订单')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('客户名称')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formCustomer, placeholder: '如:某某琴社' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formCustomer = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('订单金额')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formAmount, placeholder: '如:6800' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formAmount = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Column() {
Text('交付日期')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formDate, placeholder: '如:2026-08-20' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formDate = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showAdd = false;
})
Text('保存')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.accent)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
this.orders.splice(0, 0, new OrderItem('SO-NEW', this.formCustomer, 6800, this.formDate));
this.showAdd = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
@Builder
editModal() {
if (this.showEdit) {
Stack() {
this.modalOverlay(() => {
this.showEdit = false;
})
Column() {
Text('编辑琴弦')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('调整弦张力标定')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('琴弦')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selString === null ? '—' : this.selString.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('当前张力')
.fontSize(12)
.fontColor(COLORS.text2)
Text((this.selString === null ? 0 : this.selString.tension) + '%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(tensionColor(this.selString === null ? 0 : this.selString.tension))
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Row() {
Text('紧弦 +5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.win)
.borderRadius(10)
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.min(this.selString.tension + 5, 100);
}
this.showEdit = false;
})
Text('松弦 -5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.amber)
.borderRadius(10)
.margin({ left: 10 })
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.max(this.selString.tension - 5, 10);
}
this.showEdit = false;
})
}
.width('100%')
.margin({ top: 18 })
Text('关闭')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.margin({ top: 12 })
.onClick(() => {
this.showEdit = false;
})
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
@Builder
delModal() {
if (this.showDel) {
Stack() {
this.modalOverlay(() => {
this.showDel = false;
})
Column() {
Text('删除雅集')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('此操作不可恢复,请确认')
.fontSize(12)
.fontColor(COLORS.text3)
.margin({ top: 6 })
Column() {
Text('将删除:')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selSalon === null ? '—' : this.selSalon.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.backgroundColor('#FBEDEC')
.borderRadius(12)
.margin({ top: 14 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showDel = false;
})
Text('确认删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.danger)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
if (this.selSalon !== null) {
this.salons.splice(this.salons.indexOf(this.selSalon), 1);
}
this.showDel = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
build() {
Stack() {
Column() {
Scroll() {
Column() {
this.pageHeader()
this.tabContent()
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 12 })
}
.scrollable(ScrollDirection.Vertical)
.layoutWeight(1)
.backgroundColor(COLORS.bg)
this.bottomBar()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
if (this.showAdd) {
this.addModal()
}
if (this.showEdit) {
this.editModal()
}
if (this.showDel) {
this.delModal()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
十、弹窗交互:三大弹窗系统
应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环。三个弹窗结构一致——modalOverlay遮罩层+弹窗主体,通过if条件渲染控制显隐。
10.1 模态遮罩 modalOverlay
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => {
onClose();
})
}
modalOverlay是三个弹窗共用的遮罩层Builder——接收一个onClose回调函数。全屏Column+mask遮罩色rgba(0,0,0,0.45)(45%黑色半透明),点击遮罩调用onClose关闭弹窗。这种"遮罩+回调"的设计模式让三个弹窗的遮罩行为统一——点击遮罩任意区域即可关闭弹窗,符合移动端弹窗交互惯例。modalOverlay被addModal/editModal/delModal三个弹窗通过this.modalOverlay(() => { this.showAdd = false })调用,每个弹窗传入不同的关闭回调(showAdd=false/showEdit=false/showDel=false)。
10.2 新增订单弹窗 addModal
@Builder
addModal() {
if (this.showAdd) {
Stack() {
this.modalOverlay(() => {
this.showAdd = false;
})
Column() {
Text('新增订单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('录入一笔购筝订单')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('客户名称')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formCustomer, placeholder: '如:某某琴社' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formCustomer = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
// ... 订单金额、交付日期字段
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showAdd = false;
})
Text('保存')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.accent)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
this.orders.splice(0, 0, new OrderItem('SO-NEW', this.formCustomer, 6800, this.formDate));
this.showAdd = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
addModal是新增订单弹窗——if(this.showAdd)条件渲染,Stack层叠modalOverlay遮罩+弹窗主体。弹窗主体88%宽+padding 18+cardBg白底+borderRadius 18圆角+constraintSize maxHeight 80%(防止内容过多超出屏幕)。标题"新增订单"17号Bold+“录入一笔购筝订单"11号副标题。三个TextInput表单字段——客户名称(placeholder"如:某某琴社”)、订单金额(placeholder"如:6800")、交付日期(placeholder"如:2026-08-20",默认值formDate=‘2026-08-19’),每个字段通过onChange回调更新对应的@State变量(formCustomer/formAmount/formDate)。底部双按钮——“取消”#F0E9DB浅棕底text2灰字layoutWeight(1)+"保存"accent木色底白字Bold layoutWeight(1) margin left 12。点击"保存"执行this.orders.splice(0, 0, new OrderItem('SO-NEW', this.formCustomer, 6800, this.formDate))在订单数组头部插入新订单(订单号’SO-NEW’,金额硬编码6800),然后showAdd=false关闭弹窗。splice(0, 0, …)是新项插入数组头部的标准操作,新订单会出现在订单列表和柱状图的最前面。
10.3 编辑琴弦弹窗 editModal
@Builder
editModal() {
if (this.showEdit) {
Stack() {
this.modalOverlay(() => {
this.showEdit = false;
})
Column() {
Text('编辑琴弦')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('调整弦张力标定')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('琴弦')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selString === null ? '—' : this.selString.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('当前张力')
.fontSize(12)
.fontColor(COLORS.text2)
Text((this.selString === null ? 0 : this.selString.tension) + '%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(tensionColor(this.selString === null ? 0 : this.selString.tension))
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Row() {
Text('紧弦 +5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.win)
.borderRadius(10)
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.min(this.selString.tension + 5, 100);
}
this.showEdit = false;
})
Text('松弦 -5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.amber)
.borderRadius(10)
.margin({ left: 10 })
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.max(this.selString.tension - 5, 10);
}
this.showEdit = false;
})
}
.width('100%')
.margin({ top: 18 })
Text('关闭')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.margin({ top: 12 })
.onClick(() => {
this.showEdit = false;
})
}
// ... Stack属性
}
}
}
editModal是编辑琴弦弹窗——与addModal不同,editModal不是表单输入而是按钮操作。弹窗显示选中琴弦的名称(selString.name,空值显示"—")和当前张力(selString.tension + “%”,22号Bold粗体+ tensionColor动态着色)。两个操作按钮——"紧弦 +5%"win朱红色底(点击后tension = Math.min(tension + 5, 100),张力增加5%但不超过100%)和"松弦 -5%“amber琥珀色底(点击后tension = Math.max(tension - 5, 10),张力减少5%但不低于10%)。Math.min/max确保张力在10-100范围内,防止过度调节。底部"关闭"按钮#F0E9DB浅棕底。editModal的核心价值是直接修改@Observed对象的属性——selString是StringItem实例的引用,修改tension后由于StringItem是@Observed类,UI自动响应式刷新,张力横条宽度和颜色实时更新。这种”@Observed对象属性修改→UI自动刷新"是ArkUI可观察对象的核心能力。
10.4 删除雅集弹窗 delModal
@Builder
delModal() {
if (this.showDel) {
Stack() {
this.modalOverlay(() => {
this.showDel = false;
})
Column() {
Text('删除雅集')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('此操作不可恢复,请确认')
.fontSize(12)
.fontColor(COLORS.text3)
.margin({ top: 6 })
Column() {
Text('将删除:')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selSalon === null ? '—' : this.selSalon.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.backgroundColor('#FBEDEC')
.borderRadius(12)
.margin({ top: 14 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showDel = false;
})
Text('确认删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.danger)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
if (this.selSalon !== null) {
this.salons.splice(this.salons.indexOf(this.selSalon), 1);
}
this.showDel = false;
})
}
.width('100%')
.margin({ top: 18 })
}
// ... Stack属性
}
}
}
delModal是删除雅集确认弹窗——标题"删除雅集"17号Bold+警告"此操作不可恢复,请确认"12号text3。中间有一个#FBEDEC浅红底的信息区——"将删除:“12号text2+选中雅集名称16号Bold danger危险红色,让用户明确看到即将删除的内容。底部双按钮——“再想想”#F0E9DB浅棕底(取消删除)和"确认删除"danger危险红色底白字Bold(点击后this.salons.splice(this.salons.indexOf(this.selSalon), 1)从数组中移除该雅集)。splice(indexOf(selSalon), 1)先通过indexOf找到选中雅集在数组中的索引,然后splice移除1项。delModal的核心价值是"删除前确认”——通过danger红色底+不可恢复警告+显示删除目标名称,确保用户不会误删,是移动端删除操作的标准交互模式。
10.5 弹窗系统架构
上图为三大弹窗系统的交互流程图——三个触发入口(古筝页"订"/琴弦页"改"/雅集页"删")分别触发三个弹窗(addModal/editModal/delModal),每个弹窗执行不同的数据操作(splice插入/属性修改/splice移除)。三个弹窗的按钮颜色各不相同——addModal"保存"用accent木色底、editModal"紧弦"用win朱红色底/"松弦"用amber琥珀色底、delModal"确认删除"用danger危险红色底,通过按钮颜色区分操作类型(新增=木色、调节=朱红/琥珀、删除=危险红)。三个弹窗都通过if条件渲染控制显隐,zIndex(999)覆盖主内容,modalOverlay遮罩点击关闭。
10.6 build 主构建方法
build() {
Stack() {
Column() {
Scroll() {
Column() {
this.pageHeader()
this.tabContent()
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 12 })
}
.scrollable(ScrollDirection.Vertical)
.layoutWeight(1)
.backgroundColor(COLORS.bg)
this.bottomBar()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
if (this.showAdd) {
this.addModal()
}
if (this.showEdit) {
this.editModal()
}
if (this.showDel) {
this.delModal()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
build()是入口组件的主构建方法——Stack最外层包裹Column主内容+三个if条件弹窗。Column内Scroll包裹pageHeader头部+tabContent内容,padding left/right 14+bottom 12,scrollable Vertical垂直滚动,layoutWeight(1)弹性填充剩余空间(bottomBar固定底部不滚动),bg宣纸色背景。bottomBar在Scroll下方固定底部。三个弹窗通过if条件渲染叠在Stack最上层——showAdd渲染addModal、showEdit渲染editModal、showDel渲染delModal。Stack的层叠顺序是"后渲染在上"——主内容先渲染在底层,弹窗后渲染在上层,zIndex(999)确保弹窗覆盖主内容。这种"Stack主内容+if条件弹窗"的架构是ArkUI弹窗管理的经典模式。
十一、图表可视化:订单金额柱状图
11.1 orderChart 柱状图构建
@Builder
orderChart() {
Column() {
Row() {
Text('📊')
.fontSize(15)
Text('订单金额分布')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ left: 6 })
Column().layoutWeight(1)
Text('单位:元')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.padding({ left: 4, right: 4, bottom: 10 })
Row() {
ForEach(ORDER_BAR_IDX, (i: number) => {
Column() {
Column()
.width(18)
.height(orderBarH(this.orders[i].amount))
.borderRadius({ topLeft: 5, topRight: 5 })
.backgroundColor(i % 2 === 0 ? COLORS.amber : COLORS.accent)
.opacity(this.breath ? 1 : 0.65)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Text((i + 1) + '')
.fontSize(9)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (i: number) => 'bar' + i)
}
.width('100%')
.padding({ left: 6, right: 6, top: 10, bottom: 10 })
.backgroundColor('#FAF4E8')
.borderRadius(12)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 10 })
}
orderChart是订单页的柱状图Builder——双层布局,上层是标题行(📊图标+"订单金额分布"14号Bold+layoutWeight(1)+ "单位:元"10号text3),下层是柱状图区域。柱状图通过ForEach遍历ORDER_BAR_IDX [0,1,2,3,4,5,6,7,8,9]渲染10根柱子,每根柱子是Column包裹——内层Column是柱子本体(width 18固定宽度+height由orderBarH(this.orders[i].amount)计算+borderRadius topLeft/topRight 5顶部圆角+backgroundColor根据i%2交替使用amber琥珀色和accent木色+opacity由breath驱动1↔0.65呼吸),外层Column的底部是序号Text((i+1)编号9号text3)。每根柱子layoutWeight(1)等分宽度+alignItems Center居中。柱状图区域#FAF4E8浅米底色+borderRadius 12圆角+padding 10,与cardBg白底卡片形成层次。orderBarH将金额映射为柱高——¥45000(民乐团采购,封顶20000计算)柱最高约100px,¥1800(少儿培训中心)柱最矮约27px,金额差异一目了然。amber/accent交替色让相邻柱子颜色不同便于区分,breath驱动的opacity呼吸让柱状图"跳动"模拟数据脉动。
11.2 柱状图数据映射
上图为柱状图的数据映射流程——10笔订单金额通过orderBarH函数映射为柱子高度,同时i%2交替映射为amber/accent颜色。注意¥32000、¥45000、¥25800三笔超过20000的订单都被Math.min封顶为20000计算(柱高100px),这三笔订单在柱状图中高度相同无法区分金额差异——这是orderBarH的设计取舍,封顶20000避免柱子过高溢出图表区域,但损失了高金额订单之间的差异信息。¥1800(少儿培训中心)柱最矮27px,¥16800(岭南琴社)柱最高(未封顶)87px,金额从¥1800到¥45000的23倍跨度在柱状图中通过高度差异直观呈现。
十二、完整源代码
以下是古筝坊应用的完整源代码,涵盖了色彩接口定义、常量配置、Tab元数据、索引常量、八个@Observed可观察数据类、九个工具函数、入口组件及十八个@Builder方法的全部实现:
interface ColorPalette {
bg: string;
cardBg: string;
header1: string;
header2: string;
wood: string;
amber: string;
silk: string;
ink: string;
title: string;
sub: string;
text1: string;
text2: string;
text3: string;
accent: string;
win: string;
lose: string;
danger: string;
tabBg: string;
tabOn: string;
mask: string;
}
const COLORS: ColorPalette = {
bg: '#F7F1E6',
cardBg: '#FFFFFF',
header1: '#3E2A1A',
header2: '#1F140C',
wood: '#8A5A2B',
amber: '#D9A441',
silk: '#E8C87A',
ink: '#2B2118',
title: '#F5EAD0',
sub: '#C9B28A',
text1: '#3E2A1A',
text2: '#7A6A52',
text3: '#A89A80',
accent: '#8A5A2B',
win: '#C0392B',
lose: '#2E7D32',
danger: '#D9534F',
tabBg: '#3E2A1A',
tabOn: '#D9A441',
mask: 'rgba(0,0,0,0.45)'
};
interface TabMeta {
label: string;
icon: string;
}
const TAB_LIST: TabMeta[] = [
{ label: '古筝', icon: '🎼' },
{ label: '琴弦', icon: '🧵' },
{ label: '雁柱', icon: '🪵' },
{ label: '琴谱', icon: '📖' },
{ label: '名家', icon: '🏆' },
{ label: '工序', icon: '🔨' },
{ label: '雅集', icon: '🍵' },
{ label: '订单', icon: '📦' }
];
const ROW1_IDX: number[] = [0, 1, 2, 3];
const ROW2_IDX: number[] = [4, 5, 6, 7];
const STR_IDX: number[] = [0, 1, 2, 3, 4, 5];
const BRIDGE_IDX: number[] = [0, 1, 2, 3, 4, 5];
const ORDER_BAR_IDX: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@Observed
export class ZhengItem {
name: string;
wood: string;
strings: number;
length: number;
price: number;
constructor(name: string, wood: string, strings: number, length: number, price: number) {
this.name = name;
this.wood = wood;
this.strings = strings;
this.length = length;
this.price = price;
}
}
@Observed
export class StringItem {
name: string;
gauge: string;
material: string;
tension: number;
price: number;
constructor(name: string, gauge: string, material: string, tension: number, price: number) {
this.name = name;
this.gauge = gauge;
this.material = material;
this.tension = tension;
this.price = price;
}
}
@Observed
export class BridgeItem {
name: string;
wood: string;
height: number;
price: number;
constructor(name: string, wood: string, height: number, price: number) {
this.name = name;
this.wood = wood;
this.height = height;
this.price = price;
}
}
@Observed
export class ScoreItem {
title: string;
genre: string;
level: string;
pages: number;
constructor(title: string, genre: string, level: string, pages: number) {
this.title = title;
this.genre = genre;
this.level = level;
this.pages = pages;
}
}
@Observed
export class MasterItem {
name: string;
school: string;
works: number;
style: string;
constructor(name: string, school: string, works: number, style: string) {
this.name = name;
this.school = school;
this.works = works;
this.style = style;
}
}
@Observed
export class CraftItem {
name: string;
hours: number;
stage: string;
constructor(name: string, hours: number, stage: string) {
this.name = name;
this.hours = hours;
this.stage = stage;
}
}
@Observed
export class SalonItem {
name: string;
place: string;
date: string;
members: number;
constructor(name: string, place: string, date: string, members: number) {
this.name = name;
this.place = place;
this.date = date;
this.members = members;
}
}
@Observed
export class OrderItem {
no: string;
customer: string;
amount: number;
date: string;
constructor(no: string, customer: string, amount: number, date: string) {
this.no = no;
this.customer = customer;
this.amount = amount;
this.date = date;
}
}
function stringX(i: number): number {
return 26 + i * 15;
}
function bridgeX(i: number): number {
return 30 + i * 15;
}
function tensionBarW(t: number): number {
return 16 + Math.min(t, 100) * 1.3;
}
function tensionColor(t: number): string {
if (t >= 80) {
return COLORS.win;
}
if (t >= 60) {
return COLORS.amber;
}
return COLORS.lose;
}
function craftBarW(h: number): number {
return 18 + Math.min(h, 200) * 0.55;
}
function stageColor(stage: string): string {
if (stage.indexOf('精') >= 0) {
return COLORS.win;
}
if (stage.indexOf('中') >= 0) {
return COLORS.amber;
}
return COLORS.lose;
}
function orderBarH(v: number): number {
return 20 + Math.min(v, 20000) / 250;
}
function levelColor(level: string): string {
if (level.indexOf('难') >= 0) {
return COLORS.win;
}
if (level.indexOf('中') >= 0) {
return COLORS.amber;
}
return COLORS.lose;
}
function schoolColor(school: string): string {
if (school.indexOf('浙') >= 0) {
return COLORS.amber;
}
if (school.indexOf('潮') >= 0) {
return COLORS.win;
}
if (school.indexOf('客家') >= 0) {
return COLORS.lose;
}
return COLORS.accent;
}
@Entry
@Component
struct ZhengPage {
@State curTab: number = 0;
@State breath: boolean = false;
@State showAdd: boolean = false;
@State showEdit: boolean = false;
@State showDel: boolean = false;
@State selString: StringItem | null = null;
@State selSalon: SalonItem | null = null;
@State formCustomer: string = '';
@State formAmount: string = '';
@State formDate: string = '2026-08-19';
@State zhengs: ZhengItem[] = [
new ZhengItem('敦煌 · 蝶恋花', '桐木面板', 21, 163, 16800),
new ZhengItem('朱雀 · 凤鸣岐山', '泡桐', 21, 163, 9800),
new ZhengItem('龙凤 · 荷塘月色', '楠木', 21, 165, 7200),
new ZhengItem('韵牌 · 高山流水', '红木', 21, 163, 5600),
new ZhengItem('弦昇 · 紫檀嵌螺钿', '紫檀', 21, 166, 32000),
new ZhengItem('琼花 · 古法断纹', '杉木', 21, 163, 12800),
new ZhengItem('鼎韵 · 素面新筝', '楸木', 21, 162, 3800),
new ZhengItem('宏声 · 花梨木纹', '花梨', 21, 164, 6800),
new ZhengItem('中州 · 竹节筝', '楠竹', 18, 150, 2600),
new ZhengItem('盛世 · 金玉满堂', '酸枝', 21, 166, 25800),
new ZhengItem('清韵 · 少儿练习筝', '桐木', 16, 135, 1800),
new ZhengItem('名匠 · 整挖筝', '百年桐木', 21, 168, 45000)
];
@State strings: StringItem[] = [
new StringItem('钢丝尼龙弦 · 1号', '0.12mm', '钢丝缠尼龙', 88, 36),
new StringItem('钢丝尼龙弦 · 3号', '0.18mm', '钢丝缠尼龙', 82, 38),
new StringItem('钢丝尼龙弦 · 5号', '0.24mm', '钢丝缠尼龙', 74, 40),
new StringItem('钢丝尼龙弦 · 8号', '0.32mm', '钢丝缠尼龙', 66, 44),
new StringItem('钢丝尼龙弦 · 12号', '0.42mm', '钢丝缠尼龙', 58, 48),
new StringItem('缠丝钢弦 · 16号', '0.55mm', '铜丝缠绕', 52, 55),
new StringItem('缠丝钢弦 · 21号', '0.72mm', '铜丝缠绕', 45, 62),
new StringItem('文化衫 · 丝弦套装', '全弦21根', '尼龙/钢丝', 60, 460)
];
@State bridges: BridgeItem[] = [
new BridgeItem('紫檀雁柱', '紫檀', 4.2, 68),
new BridgeItem('牛骨雁柱', '牛骨', 4.0, 88),
new BridgeItem('红木雁柱', '红木', 4.2, 42),
new BridgeItem('乌木雁柱', '乌木', 4.5, 76),
new BridgeItem('竹制雁柱', '楠竹', 4.0, 18),
new BridgeItem('玉石雁柱', '青玉', 4.3, 128),
new BridgeItem('黄杨雁柱', '黄杨木', 4.1, 36),
new BridgeItem('花梨雁柱', '花梨木', 4.2, 48)
];
@State scores: ScoreItem[] = [
new ScoreItem('渔舟唱晚', '河南筝曲', '入门', 6),
new ScoreItem('高山流水', '浙江筝曲', '中级', 12),
new ScoreItem('战台风', '创作曲', '高级', 16),
new ScoreItem('出水莲', '客家筝曲', '中级', 10),
new ScoreItem('寒鸦戏水', '潮州筝曲', '高级', 14),
new ScoreItem('茉莉芬芳', '江苏民歌改编', '入门', 8),
new ScoreItem('浏阳河', '湖南民歌改编', '中级', 9),
new ScoreItem('林冲夜奔', '筝独奏', '高难', 18)
];
@State masters: MasterItem[] = [
new MasterItem('曹正', '浙派', 42, '质朴浑厚'),
new MasterItem('郭鹰', '潮州派', 30, '清丽细腻'),
new MasterItem('赵玉斋', '山东派', 35, '刚健明快'),
new MasterItem('罗九香', '客家派', 28, '古淡典雅'),
new MasterItem('王昌元', '浙派', 22, '气势磅礴'),
new MasterItem('项斯华', '学院派', 26, '华丽流畅'),
new MasterItem('林毛根', '潮州派', 18, '含蓄悠远'),
new MasterItem('李萌', '现代派', 24, '创新多元')
];
@State crafts: CraftItem[] = [
new CraftItem('选材开料', 24, '初序'),
new CraftItem('面板烘烤', 36, '初序'),
new CraftItem('木框成型', 40, '中序'),
new CraftItem('面板刨制', 28, '中序'),
new CraftItem('挖膛调音', 120, '精序'),
new CraftItem('涂漆晾干', 72, '精序'),
new CraftItem('装弦校音', 16, '精序'),
new CraftItem('整体修整', 20, '末序')
];
@State salons: SalonItem[] = [
new SalonItem('三秋雅集', '杭州西湖', '2026-08-22', 36),
new SalonItem('桐木夜话', '扬州个园', '2026-08-25', 24),
new SalonItem('流水知音会', '苏州网师园', '2026-09-02', 40),
new SalonItem('弦上春秋', '西安碑林', '2026-09-10', 28),
new SalonItem('月下筝鸣', '成都望江楼', '2026-09-18', 32),
new SalonItem('古韵新声', '北京恭王府', '2026-10-01', 50),
new SalonItem('竹林听筝', '广州兰圃', '2026-10-12', 22),
new SalonItem('秋月琴会', '南京瞻园', '2026-10-20', 30)
];
@State orders: OrderItem[] = [
new OrderItem('SO-260801', '岭南琴社', 16800, '08-01'),
new OrderItem('SO-260803', '西安音乐学院', 9800, '08-03'),
new OrderItem('SO-260805', '个人定制 · 王女士', 32000, '08-05'),
new OrderItem('SO-260808', '浙江古筝协会', 7200, '08-08'),
new OrderItem('SO-260810', '苏州艺校', 5600, '08-10'),
new OrderItem('SO-260812', '民乐团采购', 45000, '08-12'),
new OrderItem('SO-260814', '合肥琴行', 3800, '08-14'),
new OrderItem('SO-260816', '日本筝友会', 25800, '08-16'),
new OrderItem('SO-260817', '北京古筝馆', 12800, '08-17'),
new OrderItem('SO-260818', '少儿培训中心', 1800, '08-18')
];
aboutToAppear(): void {
setInterval(() => {
this.breath = !this.breath;
}, 520);
}
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => {
onClose();
})
}
@Builder
pageHeader() {
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.borderRadius(22)
.linearGradient({
angle: 160,
colors: [[COLORS.header1, 0], [COLORS.header2, 1]]
})
Column()
.width('86%')
.height(64)
.borderRadius(30)
.backgroundColor(COLORS.wood)
.border({ width: 2, color: COLORS.amber })
.position({ x: 24, y: 26 })
Column()
.width(8)
.height(48)
.backgroundColor(COLORS.silk)
.borderRadius(4)
.position({ x: 14, y: 34 })
ForEach(STR_IDX, (i: number) => {
Column()
.width(2)
.height(46)
.backgroundColor(COLORS.silk)
.opacity(this.breath ? 1 : 0.55)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: stringX(i), y: 35 })
}, (i: number) => 'str' + i)
ForEach(BRIDGE_IDX, (i: number) => {
Column()
.width(7)
.height(9)
.borderRadius(2)
.backgroundColor(COLORS.amber)
.translate({ y: this.breath ? -2 : 2 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: bridgeX(i), y: 56 })
}, (i: number) => 'brg' + i)
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor(COLORS.amber)
.scale({ x: this.breath ? 1.4 : 1, y: this.breath ? 1.4 : 1 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 208, y: 30 })
Column() {
Text('古筝坊')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Text('丝弦雅韵 · 十三弦动')
.fontSize(12)
.fontColor(COLORS.sub)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 250, y: 40 })
}
.width('100%')
.height(136)
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('传世古筝')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('21')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('标准弦数')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('制筝工序')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('10')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.amber)
Text('本月订单')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 8, right: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 16, topRight: 16, bottomLeft: 0, bottomRight: 0 })
.margin({ top: 10 })
}
.width('100%')
}
@Builder
sectionTitle(icon: string, title: string, sub: string) {
Row() {
Text(icon)
.fontSize(15)
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ left: 6 })
Column().layoutWeight(1)
Text(sub)
.fontSize(11)
.fontColor(COLORS.text3)
}
.width('100%')
.padding({ left: 4, right: 4, bottom: 10 })
}
@Builder
zhengRow(item: ZhengItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.wood + ' · ' + item.length + 'cm')
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.strings + '弦')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('¥' + item.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 6 })
Text('订')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.accent)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.showAdd = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
stringRow(item: StringItem, i: number) {
Column() {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.gauge + ' · ' + item.material)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('¥' + item.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(item.tension + '% 张力')
.fontSize(10)
.fontColor(tensionColor(item.tension))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('改')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.amber)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selString = item;
this.showEdit = true;
})
}
.width('100%')
Row() {
Column()
.width(tensionBarW(item.tension))
.height(8)
.borderRadius(4)
.backgroundColor(tensionColor(item.tension))
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text('张力标定')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
bridgeRow(item: BridgeItem, i: number) {
Row() {
Column()
.width(14)
.height(18)
.borderRadius(3)
.backgroundColor(COLORS.wood)
.translate({ y: this.breath ? -2 : 2 })
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text('高 ' + item.height + 'cm · 承弦支撑')
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Text('¥' + item.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
scoreRow(item: ScoreItem, i: number) {
Row() {
Column() {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.genre)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.level)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.backgroundColor(levelColor(item.level))
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text(item.pages + ' 页')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
masterRow(item: MasterItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.style)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.school)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.backgroundColor(schoolColor(item.school))
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text(item.works + ' 部作品')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
craftRow(item: CraftItem, i: number) {
Column() {
Row() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.stage)
.fontSize(10)
.fontColor(COLORS.cardBg)
.backgroundColor(stageColor(item.stage))
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.hours + ' 工时')
.fontSize(11)
.fontColor(COLORS.text2)
}
.width('100%')
Row() {
Column()
.width(craftBarW(item.hours))
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.accent)
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text('工序时长')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
salonRow(item: SalonItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.place + ' · ' + item.date)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.members + ' 人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('报名')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('删')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selSalon = item;
this.showDel = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
orderRow(item: OrderItem, i: number) {
Row() {
Column() {
Text(item.customer)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.no + ' · ' + item.date)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('¥' + item.amount)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.win)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
orderChart() {
Column() {
Row() {
Text('📊')
.fontSize(15)
Text('订单金额分布')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ left: 6 })
Column().layoutWeight(1)
Text('单位:元')
.fontSize(10)
.fontColor(COLORS.text3)
}
.width('100%')
.padding({ left: 4, right: 4, bottom: 10 })
Row() {
ForEach(ORDER_BAR_IDX, (i: number) => {
Column() {
Column()
.width(18)
.height(orderBarH(this.orders[i].amount))
.borderRadius({ topLeft: 5, topRight: 5 })
.backgroundColor(i % 2 === 0 ? COLORS.amber : COLORS.accent)
.opacity(this.breath ? 1 : 0.65)
.animation({ duration: 520, iterations: -1, playMode: PlayMode.Alternate })
Text((i + 1) + '')
.fontSize(9)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (i: number) => 'bar' + i)
}
.width('100%')
.padding({ left: 6, right: 6, top: 10, bottom: 10 })
.backgroundColor('#FAF4E8')
.borderRadius(12)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 10 })
}
@Builder
tabContent() {
if (this.curTab === 0) {
Column() {
this.sectionTitle('🎼', '传世古筝', '共 ' + this.zhengs.length + ' 款')
ForEach(this.zhengs, (item: ZhengItem, i: number) => {
this.zhengRow(item, i)
}, (item: ZhengItem) => item.name)
}
.width('100%')
} else if (this.curTab === 1) {
Column() {
this.sectionTitle('🧵', '琴弦规格', '张力标定')
ForEach(this.strings, (item: StringItem, i: number) => {
this.stringRow(item, i)
}, (item: StringItem) => item.name)
}
.width('100%')
} else if (this.curTab === 2) {
Column() {
this.sectionTitle('🪵', '雁柱 · 琴码', '承弦定音')
ForEach(this.bridges, (item: BridgeItem, i: number) => {
this.bridgeRow(item, i)
}, (item: BridgeItem) => item.name)
}
.width('100%')
} else if (this.curTab === 3) {
Column() {
this.sectionTitle('📖', '经典琴谱', '按难度分级')
ForEach(this.scores, (item: ScoreItem, i: number) => {
this.scoreRow(item, i)
}, (item: ScoreItem) => item.title)
}
.width('100%')
} else if (this.curTab === 4) {
Column() {
this.sectionTitle('🏆', '筝界名家', '流派传承')
ForEach(this.masters, (item: MasterItem, i: number) => {
this.masterRow(item, i)
}, (item: MasterItem) => item.name)
}
.width('100%')
} else if (this.curTab === 5) {
Column() {
this.sectionTitle('🔨', '制筝工序', '工时横条')
ForEach(this.crafts, (item: CraftItem, i: number) => {
this.craftRow(item, i)
}, (item: CraftItem) => item.name)
}
.width('100%')
} else if (this.curTab === 6) {
Column() {
this.sectionTitle('🍵', '雅集活动', '以琴会友')
ForEach(this.salons, (item: SalonItem, i: number) => {
this.salonRow(item, i)
}, (item: SalonItem) => item.name)
}
.width('100%')
} else {
Column() {
this.sectionTitle('📦', '订单列表', '共 ' + this.orders.length + ' 单')
this.orderChart()
ForEach(this.orders, (item: OrderItem, i: number) => {
this.orderRow(item, i)
}, (item: OrderItem) => item.no)
}
.width('100%')
}
}
@Builder
bottomItem(i: number) {
Column() {
Text(TAB_LIST[i].icon)
.fontSize(17)
Text(TAB_LIST[i].label)
.fontSize(11)
.fontColor(this.curTab === i ? COLORS.amber : COLORS.sub)
.margin({ top: 2 })
}
.width('25%')
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === i ? '#4A3420' : COLORS.tabBg)
.borderRadius(10)
.onClick(() => {
this.curTab = i;
})
}
@Builder
bottomBar() {
Column() {
Row() {
ForEach(ROW1_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r1' + i)
}
.width('100%')
Row() {
ForEach(ROW2_IDX, (i: number) => {
this.bottomItem(i)
}, (i: number) => 'r2' + i)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.tabBg)
.borderRadius({ topLeft: 18, topRight: 18, bottomLeft: 0, bottomRight: 0 })
}
@Builder
addModal() {
if (this.showAdd) {
Stack() {
this.modalOverlay(() => {
this.showAdd = false;
})
Column() {
Text('新增订单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('录入一笔购筝订单')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('客户名称')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formCustomer, placeholder: '如:某某琴社' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formCustomer = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('订单金额')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formAmount, placeholder: '如:6800' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formAmount = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Column() {
Text('交付日期')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formDate, placeholder: '如:2026-08-20' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formDate = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showAdd = false;
})
Text('保存')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.accent)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
this.orders.splice(0, 0, new OrderItem('SO-NEW', this.formCustomer, 6800, this.formDate));
this.showAdd = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
@Builder
editModal() {
if (this.showEdit) {
Stack() {
this.modalOverlay(() => {
this.showEdit = false;
})
Column() {
Text('编辑琴弦')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('调整弦张力标定')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 3 })
Column() {
Text('琴弦')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selString === null ? '—' : this.selString.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('当前张力')
.fontSize(12)
.fontColor(COLORS.text2)
Text((this.selString === null ? 0 : this.selString.tension) + '%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(tensionColor(this.selString === null ? 0 : this.selString.tension))
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Row() {
Text('紧弦 +5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.win)
.borderRadius(10)
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.min(this.selString.tension + 5, 100);
}
this.showEdit = false;
})
Text('松弦 -5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.amber)
.borderRadius(10)
.margin({ left: 10 })
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = Math.max(this.selString.tension - 5, 10);
}
this.showEdit = false;
})
}
.width('100%')
.margin({ top: 18 })
Text('关闭')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.margin({ top: 12 })
.onClick(() => {
this.showEdit = false;
})
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
@Builder
delModal() {
if (this.showDel) {
Stack() {
this.modalOverlay(() => {
this.showDel = false;
})
Column() {
Text('删除雅集')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text('此操作不可恢复,请确认')
.fontSize(12)
.fontColor(COLORS.text3)
.margin({ top: 6 })
Column() {
Text('将删除:')
.fontSize(12)
.fontColor(COLORS.text2)
Text(this.selSalon === null ? '—' : this.selSalon.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.backgroundColor('#FBEDEC')
.borderRadius(12)
.margin({ top: 14 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#F0E9DB')
.borderRadius(12)
.onClick(() => {
this.showDel = false;
})
Text('确认删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.danger)
.borderRadius(12)
.margin({ left: 12 })
.onClick(() => {
if (this.selSalon !== null) {
this.salons.splice(this.salons.indexOf(this.selSalon), 1);
}
this.showDel = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('88%')
.padding(18)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
build() {
Stack() {
Column() {
Scroll() {
Column() {
this.pageHeader()
this.tabContent()
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 12 })
}
.scrollable(ScrollDirection.Vertical)
.layoutWeight(1)
.backgroundColor(COLORS.bg)
this.bottomBar()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
if (this.showAdd) {
this.addModal()
}
if (this.showEdit) {
this.editModal()
}
if (this.showDel) {
this.delModal()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
十三、开发环境搭建
安装 DevEco Studio
DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

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

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

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

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

十四、总结
通过对这款古筝坊非遗手作平台的深度记录,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、数据可视化、文化内涵等多维度能力的综合体现。
在架构层面,该应用采用了单组件@Builder架构设计——将整个应用的所有页面内容通过18个@Builder方法内联构建在一个@Entry @Component struct ZhengPage中,每个@Builder方法负责一个独立的UI区块(pageHeader/sectionTitle/zhengRow/stringRow/bridgeRow/scoreRow/masterRow/craftRow/salonRow/orderRow/orderChart/tabContent/bottomItem/bottomBar/addModal/editModal/delModal/modalOverlay),通过if-else条件渲染在tabContent中切换八个Tab页面。与多组件架构(如香囊坊将应用拆分为10个独立组件)不同,本应用所有状态和UI逻辑集中在一处,通过17个@State状态变量直接管理,无需@ObjectLink跨组件数据共享和回调函数父子通信。这种单组件@Builder架构虽然组件粒度较粗,但开发效率高、状态管理简单、调试方便,是中小型ArkUI应用的高效实践模式。所有@Builder方法共享同一个组件作用域的this引用,可以直接访问全部@State变量和@Observed数据数组,无需prop传递和link连接,代码更直观更易维护。
在数据层面,应用采用了八个独立@Observed可观察数据类设计——ZhengItem/StringItem/BridgeItem/ScoreItem/MasterItem/CraftItem/SalonItem/OrderItem各自独立定义,每个类有独立的constructor构造函数,通过new关键字实例化,存储在八个@State数组中(zhengs/strings/bridges/scores/masters/crafts/salons/orders)。与香囊坊的聚合数据类NangData不同,本应用将八组数据分散为八个@State数组直接管理在入口组件中,修改数据时通过数组方法(splice/unshift)直接操作@State数组触发响应式刷新,或通过修改@Observed对象属性(如editModal中修改selString.tension)触发UI自动刷新。@Observed装饰器让每个数据类的实例变为可观察对象——当实例属性被修改时,引用该实例的UI会自动响应式刷新,这是ArkUI可观察对象的核心能力。数据规模达70条(12+8+8+8+8+8+8+10),完整呈现中国古筝非遗手作平台的数据生态。应用还定义了9个工具函数(stringX/bridgeX定位+tensionBarW/tensionColor张力+craftBarW/stageColor工序+orderBarH金额高度+levelColor难度+schoolColor流派),将颜色、尺寸、定位映射逻辑从UI层抽离,每个函数根据不同维度返回对应的颜色值或像素宽度或定位坐标,实现"数据即配色、数据即尺寸、数据即位置"的动态映射模式。本应用数据最大的特色是中国传统古筝文化全覆盖——12款古筝覆盖桐木/泡桐/楠木/红木/紫檀/杉木/楸木/花梨/楠竹/酸枝/百年桐木十一种材质,价格从¥1800到¥45000;8种琴弦覆盖钢丝尼龙弦和缠丝钢弦两大类,线径从0.12mm到0.72mm,张力从45%到88%;8种雁柱覆盖紫檀/牛骨/红木/乌木/楠竹/青玉/黄杨木/花梨木八种材质;8首琴谱覆盖河南/浙江/客家/潮州四大流派+创作曲+民歌改编,难度从入门到高难;8位名家覆盖浙派/潮州派/山东派/客家派/学院派/现代派六大流派;8道工序覆盖初序/中序/精序/末序四阶段,工时从16小时到120小时。这些文化数据让应用不只是一个技术演示,更是一个中国古筝文化的数字载体。
在视觉层面,本应用采用"木色琥珀丝色墨色"中国古筝非遗主题设计——bg #F7F1E6 宣纸色(模拟古筝谱宣纸质感),wood #8A5A2B 木色(古筝面板桐木色调),amber #D9A441 琥珀色(古筝装饰琴码色调),silk #E8C87A 丝色(古筝琴弦丝色调),ink #2B2118 墨色(古筝面板墨色调),header1 #3E2A1A 深木色+header2 #1F140C 墨黑色头部渐变(模拟古筝侧板到底板的材质过渡),title #F5EAD0 浅金色+sub #C9B28A 棕色头部文字(模拟宣纸上淡金题字),text1/text2/text3三级文字色(深木色/中木色/浅木色),win #C0392B 朱红色+lose #2E7D32 竹青色+danger #D9534F 危险红三色功能色(朱红色用于高值高张力高难度/竹青色用于低值低张力低难度/危险红用于删除操作),tabBg #3E2A1A 深木色+tabOn #D9A441 琥珀色导航色。主色体系采用"木色+琥珀+丝色+墨色+宣纸"五色组合,五色各司其职形成"以木色定筝、以琥珀定饰、以丝色定弦、以墨色定底、以宣纸定景"的中国古筝语义着色体系。本应用最大的视觉特色是头部琴弦opacity呼吸与雁柱translate振动动画效果——6根丝色琴弦520ms opacity 1↔0.55往返呼吸模拟琴弦拨动后的震颤余韵,6个琥珀色雁柱520ms translate y:-2↔2往返振动模拟雁柱在琴弦震动时的上下微颤,1个琥珀色共振点520ms scale 1↔1.4往返缩放模拟古筝共鸣箱的脉动呼吸,三种动画同步以520ms节奏播放,形成"弦颤、柱振、箱鸣"的三重动态意境。统一的色彩系统是整个应用的视觉基石,木色琥珀丝色墨色主题配合6个颜色分级函数和多维度动态着色,营造出既木色温润又琥珀明亮的古筝非遗氛围。
在交互层面,应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:addModal新增订单(三字段TextInput表单:客户名称/订单金额/交付日期,保存accent木色底,splice插入数组头部)、editModal编辑琴弦(紧弦+5%win朱红色底/松弦-5%amber琥珀色底,Math.min/max确保张力10-100范围,修改@Observed属性触发UI自动刷新)、delModal删除雅集(确认删除danger危险红色底,splice移除选中项,显示删除目标名称+不可恢复警告)。三个弹窗都是@Builder方法内联在入口组件中,通过if条件渲染控制显隐(showAdd/showEdit/showDel三个@State布尔变量),遮罩用rgba(0,0,0,0.45)(45%黑色),弹窗主体与遮罩用Stack层叠,整体zIndex(999)覆盖主内容。古筝页"订"点击触发addModal,琴弦页"改"点击触发editModal(selString记录目标项),雅集页"删"点击触发delModal(selSalon记录目标项),形成了完整的"订购古筝→调节琴弦→删除雅集"交互闭环。张力标定横条用tensionBarW像素宽度映射+tensionColor三级着色(高用朱红色/中用琥珀色/低用竹青色),工时横条用craftBarW像素宽度映射+stageColor三级着色(精序朱红色/中序琥珀色/其他竹青色),柱状图用orderBarH像素高度映射+amber/accent交替色+breath呼吸opacity,难度标签用levelColor三级着色(高难朱红色/中级琥珀色/入门竹青色),流派标签用schoolColor四级着色(浙派琥珀色/潮州派朱红色/客家派竹青色/其他木色),都是本应用交互设计与数据可视化的精细之处。
技术特性对比表
| 维度 | 古筝坊 | 香囊坊 | 筷子坊 | 折扇坊 |
|---|---|---|---|---|
| 架构模式 | 单组件@Builder(1组件18方法) | 多组件(10组件) | 多组件(11组件) | 单组件@Builder |
| Tab数量 | 8 Tab双行四列 | 8 Tab双行四列 | 5 Tab单排 | 8 Tab双行四列 |
| 数据类 | 8个@Observed独立类 | @Observed NangData | @Observed ChopstickData | @Observed FanShopData |
| 数据规模 | 70条(8数组) | 76条(8数组) | 83条(5数组+4统计) | 76条(8数组) |
| 工具函数 | 9个(定位+张力+工序+金额+难度+流派) | 3个(useBarWidth/useColor/scoreColor) | 5个 | 6个 |
| 弹窗数量 | 3个(新增订单/编辑琴弦/删除雅集) | 3个(新增香方/入库香料/编辑香囊) | 4个 | 3个 |
| 主题色 | 木色#8A5A2B | 玫瑰粉#C08A9E | 乌木黑#2F2A26 | 竹青#7A8B5A |
| 辅助色 | 琥珀#D9A441 | 荷花粉#E8D5DC | 朱红#B03A2E | 金#C9A227 |
| 头部动画 | 琴弦opacity呼吸+雁柱translate振动+共振点scale | 虚线圆环旋转+浮动光点 | 筷子旋转+灯笼位移 | 扇骨辐线开合+流苏 |
| 数据可视化 | 张力横条+工时横条+柱状图 | 用量对比横条 | 柱状图+TOP6+进度条 | 柱状图+难度横条 |
| 文化主题 | 古筝·丝弦雅韵 | 香囊·荷包香事 | 筷子·一箸千年 | 折扇·扇开清凉 |

标签: HarmonyOS · ArkTS · API 24 · HarmonyOS 6.1.1 · 中国古筝 · 木色琥珀丝色 · 单组件@Builder架构 · @Observed · 丝弦雅韵 · 移动端开发
更多推荐

所有评论(0)