琵琶制作工坊:HarmonyOS ArkTS API 24动画系统(animation、PlayMode.Alternate),渐变背景(linearGradient),绝对定位布局position
一、琵琶制作的历史文化背景

琵琶,这件被誉为"弹拨乐器之王"的中国传统民族乐器,其历史可追溯至两千多年前的秦汉时期。琵琶之名,据《释名·释乐器》记载,本为两种弹奏手法的合称——“推手前曰琵,引手却曰琶”,后逐渐演变为这件梨形音箱弹拨乐器的专有名称。从西域传入中原,再到魏晋南北朝时期与本土弦鼗融合定型,琵琶经历了一条漫长而辉煌的演变之路,它不仅是乐器本身的发展史,更是中华文明与丝绸之路多元文化交流互鉴的生动缩影。
早在秦代,中国便已有一种直柄、圆形音箱、皮面弹拨的乐器,时人称之为"弦鼗",这便是琵琶的雏形之一。至汉代,随着丝绸之路的开通,西域波斯风格的曲项琵琶经由龟兹(今新疆库车一带)传入中原。这种曲项琵琶拥有梨形音箱、四弦四相,以拨子弹奏,音色铿锵明亮,迅速在宫廷与民间流传开来。魏晋时期,竹林七贤之一的阮咸善弹此种乐器,故后世将直柄圆箱的琵琶称为"阮"或"阮咸",以纪念这位音乐大家。至此,中国琵琶已初步呈现出直项与曲项并存的多元格局。
到了隋唐盛世,琵琶迎来了其历史上的第一个巅峰。唐代宫廷设有"十部乐",其中燕乐、清乐、龟兹乐、西凉乐等众多乐部均以琵琶为主奏乐器。唐太宗时期,裴神符改良了琵琶的弹奏方式,弃用拨片改以手指直接弹奏,这一革新使琵琶的音色更加细腻丰富,表现力大为增强,轮指、滚奏等复杂技法应运而生。唐代诗人白居易在千古名篇《琵琶行》中写道:"大弦嘈嘈如急雨,小弦切切如私语。嘈嘈切切错杂弹,大珠小珠落玉盘。"这首诗以极其生动传神的笔触描绘了琵琶音色的层次之美,也成为后世理解琵琶艺术最经典的文学注脚。
琵琶的制作工艺同样是一门博大精深的传统手艺。一把上乘琵琶的诞生,需要经过选料、锯坯、刨光、挖膛、蒙面板、上相品、安天牌、配琴弦、上轸子、调音等数十道工序,每一步都凝聚着匠人的智慧与心血。琵琶的音箱(琴腹)多选用质地轻盈、纹理均匀的梧桐木,因其共鸣效果出众;琴头和琴背则常用紫檀、红木、花梨木等硬质木材,既保证结构坚固,又赋予乐器庄重华贵的外观。面板上嵌有的竹质"相"和"品"决定了音高关系,六相二十四品的琵琶可以覆盖近四个八度的宽广音域,这在世界弹拨乐器中堪称罕见。
琵琶的弦材也经历了从蚕丝到钢丝再到钢丝缠尼龙的演变。古代以蚕丝为弦,音色温润古朴但张力不足、易断且受温湿度影响大;近代改用钢丝弦后,音量增大、音准稳定,但音色略嫌尖硬;当代主流的钢丝缠尼龙弦则兼顾了音色与耐用性,成为专业演奏的首选。此外还有银缠弦等特殊材质,以满足不同曲目的音色需求。弦的粗细(线径)、张力大小、材质属性都会直接影响琵琶的音色表现,这些细节在本篇技术博文所分析的 ArkTS 应用中均有精细的数据建模与可视化呈现。
琵琶的流派体系同样蔚为大观。明清以来,琵琶形成了浦东派、平湖派、汪派、崇明派等众多流派,各派在曲目选择、演奏技法、审美追求上各有千秋。浦东派文套细腻,注重左手吟揉技巧;平湖派武套刚健,右手力度雄健有力;汪派刚柔并济,兼具文武之长。近代以来,刘天华等人将琵琶引入高等音乐教育体系,刘德海等大师更在传统基础上大胆创新,使琵琶走向世界舞台。这些流派的历史、风格特点、传承脉络,在本应用的"流派"模块中有完整的数据展示。
在当代,琵琶制作已从纯手工小作坊逐渐发展为半机械化生产的乐器工坊,但高端演奏级琵琶仍坚持全手工制作。浙江平湖、上海浦东、苏州、北京等地聚集了中国最优秀的琵琶制作工坊,每家工坊的匠师人数、年产量、工艺等级都是衡量其实力的重要指标。本篇分析的 ArkTS 应用正是以"琵琶工坊"为核心主题,将琵琶的款式管理、琴弦工艺、曲谱传承、名家档案、流派谱系、工坊分布、名曲赏析、订单管理等八大业务模块融为一体,构建了一个功能完整的琵琶制作与管理系统。
从技术视角来看,这篇博文要分析的源代码运用了 HarmonyOS ArkTS 声明式 UI 开发框架的全部核心特性:@Entry、@Component、@State、@Observed、@Builder 等装饰器的深度运用,声明式布局语法(Column、Row、Stack),动画系统(animation、PlayMode.Alternate),渐变背景(linearGradient),绝对定位布局(position),条件渲染(if-else),循环渲染(ForEach),以及模态弹窗系统。通过对这份代码的逐段深入解析,开发者不仅能掌握 ArkTS 的 API 使用方法,更能理解声明式 UI 的设计哲学与状态驱动渲染的核心思想。
二、色彩体系:ColorPalette 接口与 COLORS 常量

interface ColorPalette {
bg: string;
cardBg: string;
header1: string;
header2: string;
wood: string;
line: string;
stringC: string;
cinnabar: string;
ivory: string;
gold: string;
title: string;
sub: string;
text1: string;
text2: string;
text3: string;
accent: string;
hot: string;
cool: string;
danger: string;
tabBg: string;
tabOn: string;
mask: string;
}
这是整个应用的色彩基础定义。ColorPalette 作为一个 TypeScript 接口,声明了二十二个颜色字段,每一个字段名都经过精心设计,既语义明确又与琵琶文化主题紧密关联。接口的定义确保了类型安全——任何实现此接口的对象必须包含全部二十二个字段,编译器会在编码阶段即进行校验,杜绝因字段缺失或拼写错误导致的运行时异常。
这种以接口统一定义色彩体系的做法在工程实践中具有极高的价值。首先,它实现了"单一数据源"原则,所有组件引用同一套色彩常量,当需要适配深色模式或品牌换肤时,只需修改一处即可全局生效。其次,语义化的字段命名(如 cinnabar 表示朱砂红、ivory 表示象牙白、wood 表示木质棕)让代码本身成为最好的设计文档,开发者无需查阅额外的色值表就能理解每种颜色的用途。最后,TypeScript 的静态类型检查会在编译阶段捕获所有类型不匹配的错误,大幅提升了代码的健壮性。
从接口设计的角度来看,ColorPalette 并非简单的字符串集合,而是一个经过深思熟虑的色彩语义系统。其中 bg 和 cardBg 分别表示页面背景和卡片背景,形成基础的两级背景层次;header1 和 header2 是渐变色对,专门用于标题栏的线性渐变;wood、line、stringC、cinnabar、ivory 这五个字段直接对应琵琶的物理材质——木材、金线、琴弦、朱砂、象牙,使色彩配置本身就成为琵琶构造的映射。text1、text2、text3 三级文字色和 title、sub 两个标题色构成了完整的文字层次体系。hot、cool、danger 是语义化的状态色,分别用于表示热烈(武曲)、冷静(文曲)和危险(删除)三种语义。tabBg、tabOn 和 mask 则是交互组件专用色。这种按用途分组的色彩架构,使开发者在查找和使用颜色时有章可循,不会随意使用未经定义的颜色值。
const COLORS: ColorPalette = {
bg: '#F5F0E8',
cardBg: '#FFFFFF',
header1: '#3A1F14',
header2: '#1D0E08',
wood: '#7A4A2B',
line: '#C9A86A',
stringC: '#E8DFD2',
cinnabar: '#B33A2B',
ivory: '#F2EADB',
gold: '#D9A441',
title: '#F7E9D6',
sub: '#D0B392',
text1: '#3A1F14',
text2: '#6E5B4B',
text3: '#9C8F7A',
accent: '#7A4A2B',
hot: '#C0392B',
cool: '#1F5FA8',
danger: '#D9534F',
tabBg: '#3A1F14',
tabOn: '#D9A441',
mask: 'rgba(0,0,0,0.45)'
};
COLORS 常量是 ColorPalette 接口的具体实现,以 const 关键字声明,确保在应用整个生命周期中不可被重新赋值,保障了色彩数据的不可变性。这份色彩配置经过精心调校,呈现出浓郁的中国传统美学风格。
背景色 bg 采用 #F5F0E8,这是一种接近宣纸的暖白色调,给人以古朴温润的视觉感受,非常适合作为承载传统文化主题应用的基础底色。卡片背景 cardBg 使用纯白 #FFFFFF,与暖色背景形成微妙的层次差异,使信息卡片在视觉上自然浮起。页眉的两种深色 header1(#3A1F14)和 header2(#1D0E08)是极深的暗棕色系,用于构建渐变标题栏,模拟紫檀木的深沉质感。这两组背景色的搭配在视觉上构建了从浅到深的层次递进——暖白底色上浮动着白色卡片,卡片上方又顶着深棕色标题栏,形成了"浅-白-深"的三级视觉层次。
wood(#7A4A2B)是标准的木质棕色,用于绘制琵琶琴身等木质部件;line(#C9A86A)是金色线条色,用于装饰边框和描线;stringC(#E8DFD2)是琴弦色,呈现出丝弦特有的米白色调;cinnabar(#B33A2B)是朱砂红,用于琵琶琴身上的装饰点,也用于强调色和进度条;ivory(#F2EADB)是象牙白,用于品相和装饰细节。这组色彩搭配在视觉上完美还原了传统琵琶的材质特征——木质棕的琴身、金色的镶嵌线条、米白色的丝弦、朱砂红的装饰、象牙白的品相,每一种颜色都指向琵琶的具体物理构件。
文字色彩分为三个层级:text1(#3A1F14)用于主标题文字,是最深的棕色,确保最大对比度;text2(#6E5B4B)用于副标题和说明文字,是中等灰棕;text3(#9C8F7A)用于辅助说明和时间戳等元信息,是最浅的灰褐色。这种三级文字层级体系遵循了 Material Design 的排版原则,但在色彩选择上完全融入了中式古雅风格。三级文字色在明度上递减,使信息的重要性通过文字明暗自然传达——最重要的信息最深最醒目,辅助信息最浅最不引人注目。
hot(#C0392B)和 cool(#1F5FA8)分别代表暖色与冷色,在应用中用于区分武曲(热烈激昂)和文曲(清雅委婉)的标签色彩,这一设计直接呼应了琵琶音乐中"文曲"与"武曲"的二元分类体系。danger(#D9534F)是标准的警示红色,用于删除按钮等危险操作。标签栏的 tabBg(#3A1F14)和 tabOn(#D9A441)分别表示未选中和选中状态的色彩,深棕色背景上以金色高亮选中项,视觉对比鲜明。最后,mask(rgba(0,0,0,0.45))使用半透明黑色作为模态遮罩,既保留了背景可见度又突出了弹窗内容。
三、标签系统:TabMeta 接口与 TAB_LIST 常量

interface TabMeta {
label: string;
icon: string;
}
TabMeta 接口定义了底部导航标签的元数据结构,包含两个字段:label 是标签的文字名称,icon 是标签的图标字符。这里使用 Emoji 作为图标载体,这是一种轻量而高效的图标方案。相比传统的图片资源或 SVG 矢量图标,Emoji 字符不需要额外的资源文件加载,渲染效率极高,且天然支持跨平台显示。接口的定义同样确保了类型安全——每个标签项都必须同时提供名称和图标,防止数据不完整。
使用 Emoji 作为图标的另一个优势在于其丰富的语义表达能力。🎻 直接传达"乐器"的含义,🎼 暗示"曲谱"和"乐理",📜 象征"古代文献"和"传承",🎭 关联"表演"和"人物",🧩 暗示"拼图"和"流派分支",🏭 明确指向"工厂"和"制作",🎵 表达"音乐"和"名曲",📦 表示"包裹"和"订单"。这些 Emoji 图标与标签文字形成了双重语义编码,用户即使不阅读文字,仅凭图标也能大致理解每个标签的功能含义。
const TAB_LIST: TabMeta[] = [
{ label: '琵琶', icon: '🎻' },
{ label: '琴弦', icon: '🎼' },
{ label: '曲谱', icon: '📜' },
{ label: '乐师', icon: '🎭' },
{ label: '流派', icon: '🧩' },
{ label: '工坊', icon: '🏭' },
{ label: '名曲', icon: '🎵' },
{ label: '订单', icon: '📦' }
];
TAB_LIST 是一个包含八个标签项的数组常量,定义了应用的八大功能模块。这八个模块涵盖了琵琶制作工坊的核心业务全流程:从乐器本身(琵琶款式)、到配件(琴弦)、到曲谱传承(曲谱)、到演奏者(乐师)、到学术体系(流派)、到生产基地(工坊)、到艺术作品(名曲)、最终到商业运营(订单)。这一模块划分逻辑清晰,从"物"到"人"到"艺"到"商",构成了一个完整的产业生态。
数组以 TabMeta[] 类型声明,配合 TypeScript 的结构化类型系统,编译器会检查每个元素是否符合 TabMeta 接口的结构。使用数组而非对象的理由在于:标签需要按索引顺序排列和访问,数组天然支持索引操作,底部导航栏的渲染逻辑可以直接遍历数组元素,代码简洁直观。数组索引与标签的对应关系在后续的 ROW1_IDX 和 ROW2_IDX 常量中得到进一步利用——索引 0-3 对应第一行的前四个标签,索引 4-7 对应第二行的后四个标签。
四、索引常量与数组分组

const ROW1_IDX: number[] = [0, 1, 2, 3];
const ROW2_IDX: number[] = [4, 5, 6, 7];
const STRING_IDX: number[] = [0, 1, 2, 3];
const FRET_IDX: number[] = [0, 1, 2, 3, 4, 5];
const NOTE_IDX: number[] = [0, 1, 2];
这组索引常量虽然简单,但在实际使用中扮演着关键角色。ROW1_IDX 和 ROW2_IDX 将八个标签索引分为两组,每组四个,用于底部导航栏的两行布局。这种分组方式使底部导航栏可以在宽屏设备上以两行四列的网格形式展示所有标签,避免了一行排列导致的拥挤或溢出问题。
STRING_IDX 包含四个索引,对应琵琶的四根琴弦(缠弦、老弦、中弦、子弦),在页面头部装饰区域用于循环渲染四根弦的线条。FRET_IDX 包含六个索引,对应琵琶的"六相"——即六个相位的品相标记。NOTE_IDX 包含三个索引,用于渲染飘动的音符装饰(♪和♫),为页面增添灵动的音乐气息。
使用具名常量而非内联字面量数组(如直接写 [0, 1, 2, 3]),是软件工程中"命名优于字面"原则的体现。当未来需要调整琴弦数量或品相数量时,只需修改常量定义即可,所有引用该常量的地方自动更新,避免了分散在代码各处的魔法数字带来的维护噩梦。这种做法虽然增加了少量代码行数,但大幅提升了代码的可读性和可维护性——当其他开发者看到 STRING_IDX 时,立即知道这是"琴弦索引"的语义,而看到 [0, 1, 2, 3] 时则完全不知道这组数字代表什么。
五、数据模型:@Observed 类体系

5.1 PipaItem——琵琶款式模型
@Observed
export class PipaItem {
name: string;
wood: string;
frets: number;
size: string;
price: number;
constructor(name: string, wood: string, frets: number, size: string, price: number) {
this.name = name;
this.wood = wood;
this.frets = frets;
this.size = size;
this.price = price;
}
}
@Observed 装饰器是 ArkTS 响应式系统的核心之一。被 @Observed 修饰的类,其实例的属性变化可以被 ArkUI 框架自动追踪。当这些属性被修改时,与之绑定的 UI 组件会自动重新渲染,无需开发者手动调用刷新方法。这就是"数据驱动视图"理念的具体实现。
PipaItem 模型描述了一把琵琶的核心属性。name 是款式名称,如"紫檀琵琶"、“竹节琵琶"等,这些名称往往直接反映了琵琶的材质特征或造型特点。wood 字段记录木料种类,如"小叶紫檀”、“红木”、“桐木"等,不同木料直接决定了琵琶的音色品质和市场价值。frets 是品相数量,传统琵琶为六相二十四品或六相二十六品,品数越多音域越宽。size 描述规格,如"四弦”、“六相”、“曲项”、"直项"等,反映了琵琶的形制差异。price 是售价,从数千元的练习琵琶到数万元的高端演奏琵琶,价格跨度极大。
构造函数接收全部五个参数并赋值给实例属性,这是 TypeScript 类的标准写法。使用 export 关键字导出该类,使其可以在其他模块中被引入使用,体现了模块化设计的思想。在 ArkTS 中,@Observed 类通常与 @State 数组配合使用——数组本身用 @State 修饰以追踪数组增删,数组元素是 @Observed 实例以追踪属性修改,两者协同实现完整的响应式数据流。
5.2 PipaStringItem—— 琴弦工艺模型

@Observed
export class PipaStringItem {
name: string;
gauge: string;
tension: number;
material: string;
tone: string;
constructor(name: string, gauge: string, tension: number, material: string, tone: string) {
this.name = name;
this.gauge = gauge;
this.tension = tension;
this.material = material;
this.tone = tone;
}
}
PipaStringItem 模型描述了琵琶琴弦的工艺参数。name 是弦名,如"缠弦·一弦"、“子弦·四弦”,命名方式遵循中国传统琵琶弦序的称呼习惯——从最粗的缠弦到最细的子弦,依次为一至四弦。gauge 是弦径规格,以毫米为单位精确到小数点后两位,如"0.32mm"、“0.20mm”,弦径直接影响音高和音色。tension 是张力百分比,数值越高表示弦越紧,音色越明亮但弹奏难度也越大。material 是材质,包括"钢丝缠丝"、“钢丝”、“蚕丝”、“尼龙”、“银丝"等多种选择。tone 是音色描述,如"低沉浑厚”、"明亮清越"等,以感性的文字传达音色特征。
这个模型的设计体现了"数据即体验"的理念。每一根弦的工艺参数都配有文字化的音色描述,使非专业的用户也能通过文字理解不同琴弦的音色差异。张力数值会被用于可视化进度条渲染,让用户直观感知每根弦的张力大小。tension 属性是整个模型中唯一会被运行时修改的字段——用户通过编辑弹窗调整张力后,@Observed 装饰器确保这一修改被框架追踪并即时反映到 UI 上。
5.3 PipaScoreItem—— 曲谱模型

@Observed
export class PipaScoreItem {
title: string;
genre: string;
measures: number;
era: string;
level: string;
constructor(title: string, genre: string, measures: number, era: string, level: string) {
this.title = title;
this.genre = genre;
this.measures = measures;
this.era = era;
this.level = level;
}
}
PipaScoreItem 模型描述了琵琶曲谱的元信息。title 是曲名,如"十面埋伏"、“春江花月夜"等传世名曲。genre 是曲类,分为"武曲”、“文曲”、“新作"三大类,这是琵琶曲最核心的分类体系。measures 是乐段数量,反映了乐曲的规模和复杂度。era 是创作时代,从元、明、清到近代、当代,跨越数百年。level 是谱本级别,如"古谱”、“改编”、“创作”,反映了曲谱的来源与传承方式。
武曲以"十面埋伏"为代表,气势磅礴、金戈铁马,用强烈的力度对比和密集的节奏模拟战争场景;文曲以"春江花月夜"为代表,委婉细腻、情景交融,以吟揉推挽等左手技巧描绘自然意境。这一文武二分的传统,贯穿了中国琵琶数百年的发展史,在应用中通过 genre 字段的颜色映射得以视觉化呈现。genre 字段通过 levelColor 函数进行颜色映射——"武曲"映射为红色,"文曲"映射为蓝色,"大曲"映射为金色,使每首曲谱的类型在列表中一目了然。
5.4 PipaMasterItem—— 乐师模型

@Observed
export class PipaMasterItem {
name: string;
grade: string;
years: number;
pieces: number;
skill: number;
constructor(name: string, grade: string, years: number, pieces: number, skill: number) {
this.name = name;
this.grade = grade;
this.years = years;
this.pieces = pieces;
this.skill = skill;
}
}
PipaMasterItem 模型描述了琵琶演奏家的职业档案。name 是乐师姓名,如"刘德海"、“林石城"等当代琵琶泰斗。grade 是职称级别,如"国宝级大师”、“国家级大师”、“名家”、"名师"等,级别越高表示在业界的地位越显赫。years 是从业年限,反映了演奏经验的深厚程度。pieces 是代表曲目数量,衡量其艺术造诣的广度。skill 是演奏技艺评分,以百分制量化评估,数值越高技艺越精湛。
职称级别的颜色映射通过 gradeColor 函数实现:大师级用红色标识,名家用金色,名师用棕色,其余用蓝色,形成清晰的视觉层级。技艺评分则通过 skillBarW 函数转换为进度条宽度,配合 breath 动画效果呈现出呼吸般的律动感。这种"职称颜色编码+技艺进度条"的双重可视化设计,使用户在浏览乐师列表时能够快速识别每位乐师的级别和实力。
5.5 PipaSchoolItem—— 流派模型
@Observed
export class PipaSchoolItem {
name: string;
style: string;
members: number;
age: number;
hot: number;
constructor(name: string, style: string, members: number, age: number, hot: number) {
this.name = name;
this.style = style;
this.members = members;
this.age = age;
this.hot = hot;
}
}
PipaSchoolItem 模型描述了琵琶演奏流派的信息。name 是流派名称,如"浦东派"、“平湖派”、“汪派"等。style 是风格特征,如"文套细腻”、“武套刚健”、"刚柔并济"等简短描述。members 是流派传承人数,反映其影响力大小。age 是流派传承年数,历史越悠久文化底蕴越深。hot 是传承热度评分,衡量流派在当代的活跃程度与受欢迎程度。
这组数据涵盖了中国琵琶最主要的八大流派。浦东派源自上海浦东,以文套琵琶曲见长,讲究吟揉技巧的细腻变化;平湖派发源于浙江平湖,武套气势雄健,右手功夫深厚;汪派由汪昱庭创立,融合文武之长,刚柔并济;崇明派清新雅致,自成一体。每个流派的热度评分通过 hotBarW 函数转化为金色进度条,直观展示其在当代乐坛的影响力。
5.6 PipaShopItem—— 工坊模型
@Observed
export class PipaShopItem {
name: string;
city: string;
crafts: number;
output: number;
level: string;
constructor(name: string, city: string, crafts: number, output: number, level: string) {
this.name = name;
this.city = city;
this.crafts = crafts;
this.output = output;
this.level = level;
}
}
PipaShopItem 模型描述了琵琶制作工坊的基本信息。name 是工坊名称,如"平湖派琵琶坊"、“北京民族乐器厂"等。city 是所在城市,工坊的地理分布反映了中国琵琶制作产业的区域集聚特征——以江浙沪为核心,辐射至北京、广州、西安、成都等地。crafts 是匠师人数,体现工坊的人力资源规模。output 是年产量,衡量工坊的生产能力。level 是工艺等级,分为"甲级”、“乙级”、"丙级"三档,甲级代表最高水准。
这组数据中,甲级工坊主要分布在琵琶制作的传统核心区域——平湖、浦东、苏州、北京,而乙级和丙级工坊则分布在更广的区域,形成了从高端手工到普及型生产的完整产业链。工坊信息中的产量用蓝色字体突出显示,匠师人数作为辅助信息,让用户一眼就能评估各工坊的综合实力。
5.7 PipaSongItem—— 名曲模型
@Observed
export class PipaSongItem {
title: string;
style: string;
beauty: number;
era: string;
note: string;
constructor(title: string, style: string, beauty: number, era: string, note: string) {
this.title = title;
this.style = style;
this.beauty = beauty;
this.era = era;
this.note = note;
}
}
PipaSongItem 模型描述了琵琶传世名曲的艺术信息。与 PipaScoreItem 不同,这个模型侧重于名曲的艺术鉴赏维度。title 是曲名,与曲谱模块的数据一一对应。style 是风格分类,同样使用"武曲"、“文曲”、“新作"的分类体系。beauty 是传世之美评分,量化评估这首乐曲的艺术价值和审美境界。era 是创作时代。note 是意境注解,如"金戈铁马”、“江月清辉”、"垓下悲歌"等四字短语,以诗意化的文字传达乐曲的审美意象。
传世之美评分通过 beautyBarW 函数转化为红色进度条宽度,配合呼吸动画效果,让每首名曲的艺术魅力以可视化的方式跃然屏上。意境注解则通过文字直接传达乐曲的情感内核——"十面埋伏"的"金戈铁马"让人联想到楚汉相争的恢弘战场,"春江花月夜"的"江月清辉"让人沉醉于月下江景的恬静意境。这种将量化评分与诗意注解相结合的设计,使数据展示既有理性的精确又有感性的温度。
5.8 PipaOrderItem—— 订单模型
@Observed
export class PipaOrderItem {
name: string;
buyer: string;
amount: number;
count: number;
date: string;
constructor(name: string, buyer: string, amount: number, count: number, date: string) {
this.name = name;
this.buyer = buyer;
this.amount = amount;
this.count = count;
this.date = date;
}
}
PipaOrderItem 模型描述了琵琶销售订单的交易信息。name 是订购的琵琶款式名称,与 PipaItem 中的款式对应。buyer 是买方名称,如"民族乐团"、“音乐学院”、"收藏家"等,反映了琵琶的多元化购买群体。amount 是订单总金额。count 是订购数量。date 是下单日期,以"年-月"格式记录。
订单金额通过 orderBarH 函数转化为柱状高度,金额越高柱状条越长,视觉上直观展示各订单的交易规模。订单模块还支持删除操作——用户可以通过点击订单卡片上的"删"按钮触发删除确认弹窗,在确认后从订单列表中移除该订单。这一交互流程充分运用了 @State 与 @Observed 的响应式特性,删除操作即时反映到 UI 上。买方信息以蓝色胶囊标签形式展示,与工坊模块中的产量颜色形成呼应,暗示了工坊产能与订单需求之间的业务关联。
六、辅助函数与工具方法
6.1 位置计算函数
function stringX(i: number): number {
return 44 + i * 8;
}
function fretY(i: number): number {
return 44 + i * 9;
}
function noteX(i: number): number {
return 168 + i * 34;
}
function noteY(i: number): number {
return 20 + (i % 2) * 26;
}
这组函数用于计算页面头部装饰区域中各元素的位置坐标。stringX 计算第 i 根琴弦的横坐标,起始位置为 44,每根弦间距 8 个单位。fretY 计算第 i 个品相的纵坐标,起始位置为 44,每个品相间距 9 个单位。noteX 和 noteY 分别计算音符装饰的横纵坐标——音符横向以 34 为间距排列,纵向则根据奇偶索引交替偏移,营造出音符高低跳跃的视觉效果。
将位置计算抽取为独立函数,是函数式编程思想在 UI 布局中的应用。相比在布局代码中直接书写算术表达式,具名函数提供了更好的可读性和可维护性。当需要调整布局间距时,只需修改函数实现,所有调用处自动更新。这种"计算与渲染分离"的设计模式,使布局逻辑更加清晰可追溯。noteY 函数中 i % 2 的取模运算特别值得注意——它使奇数索引和偶数索引的音符分别位于不同的纵坐标,形成参差错落的排列效果,模拟了真实乐谱中音符高低交替的视觉特征。
6.2 进度条宽度计算函数
function tensionBarW(t: number): number {
return 16 + Math.min(t, 100) * 1.35;
}
function skillBarW(s: number): number {
return 16 + Math.min(s, 100) * 1.35;
}
function hotBarW(hot: number): number {
return 18 + Math.min(hot, 100) * 1.35;
}
function beautyBarW(b: number): number {
return 16 + Math.min(b, 100) * 1.35;
}
function orderBarH(amount: number): number {
return 24 + Math.min(amount, 50000) / 500;
}
这组函数将业务数值映射为可视化进度条的尺寸。它们共享相同的数学模型:基础值加上缩放后的数值。以 tensionBarW 为例,张力值为 0 时进度条宽度为 16(基础值),张力值为 100 时宽度为 16 + 100 * 1.35 = 151,形成从 16 到 151 的线性映射区间。Math.min(t, 100) 的作用是上限保护——当张力值超过 100 时,进度条不再继续增长,防止溢出布局边界。
每个函数的参数名和返回值语义明确:tensionBarW 计算张力进度条宽度,skillBarW 计算技艺进度条宽度,hotBarW 计算热度进度条宽度,beautyBarW 计算美感进度条宽度,orderBarH 计算订单金额柱状高度。orderBarH 的映射逻辑略有不同,以 50000 为上限、除以 500 得到偏移量,最大高度为 24 + 100 = 124,适合纵向柱状图的渲染需求。这种将数据映射逻辑独立为纯函数的做法,使业务逻辑与 UI 渲染彻底解耦。未来如果需要更换映射算法(如改用对数映射以增强低值区间的分辨率),只需修改函数实现而无需触碰任何 UI 代码。
注意到 hotBarW 的基础值是 18 而非其他函数的 16,这是一个细微但有意的设计差异——热度进度条比其他进度条略宽 2 个单位,使流派热度在视觉上略微突出,因为热度是衡量流派当代影响力最核心的指标。这种在统一模板中通过参数微调实现差异化强调的手法,体现了精细的视觉设计考量。
6.3 颜色映射函数
function tensionColor(t: number): string {
if (t >= 70) {
return COLORS.hot;
}
if (t >= 45) {
return COLORS.gold;
}
return COLORS.cool;
}
tensionColor 函数根据张力值返回对应颜色:高张力(70 以上)返回红色 hot,表示弦绷得很紧;中等张力(45 以上)返回金色 gold,表示张力适中;低张力返回蓝色 cool,表示弦较松。这种颜色分级设计使用户能够通过颜色直观判断每根弦的张力状态,无需细读数字。
三段式阈值设计(70/45/0)在数据可视化领域是常见的分箱策略。它将连续的数值域离散化为三个语义区间——高、中、低,每个区间映射一种颜色,形成直观的色彩编码。这种设计在仪表盘、热力图等场景中广泛应用,其核心优势在于利用人类对色彩的快速感知能力,降低信息获取的认知负担。阈值 70 和 45 的选择也经过考量——70 代表"偏高"的张力,45 代表"适中"的张力,低于 45 则为"偏松",这三个区间覆盖了琵琶琴弦张力的实际分布范围。
function gradeColor(grade: string): string {
if (grade.indexOf('大师') >= 0) {
return COLORS.hot;
}
if (grade.indexOf('名家') >= 0) {
return COLORS.gold;
}
if (grade.indexOf('名师') >= 0) {
return COLORS.accent;
}
return COLORS.cool;
}
function levelColor(level: string): string {
if (level.indexOf('武曲') >= 0) {
return COLORS.hot;
}
if (level.indexOf('文曲') >= 0) {
return COLORS.cool;
}
if (level.indexOf('大曲') >= 0) {
return COLORS.gold;
}
return COLORS.accent;
}
gradeColor 和 levelColor 是基于字符串匹配的颜色映射函数。gradeColor 根据乐师职称中的关键词返回颜色——"大师"返回红色,"名家"返回金色,"名师"返回棕色,其余返回蓝色。levelColor 根据曲类关键词返回颜色——"武曲"返回红色,"文曲"返回蓝色,"大曲"返回金色,其余返回棕色。
使用 indexOf 进行子串匹配而非全等比较,是一种灵活的设计——它允许职称字符串包含额外的修饰词(如"国宝级大师"、“国家级大师"都能匹配到"大师”),增强了函数的适用性。这种基于关键词的语义着色策略,使不同级别的信息在视觉上自然分层,用户扫一眼就能分辨出信息的重要程度和类别归属。两个函数的匹配顺序也有讲究——gradeColor 按"大师>名家>名师"的优先级排列,levelColor 按"武曲>文曲>大曲"的优先级排列,确保最具体的关键词优先匹配,避免误匹配到更宽泛的类别。
七、核心组件:PipaPage 的状态管理
7.1 组件声明与入口标记
@Entry
@Component
struct PipaPage {
@Entry 装饰器标记 PipaPage 为应用的入口组件——即页面加载时首先渲染的根组件。每个 ArkTS 页面有且仅有一个 @Entry 组件,它是整个组件树的根节点。@Component 装饰器声明 PipaPage 为一个自定义组件,使其可以被 ArkUI 框架管理和调度。struct 关键字(而非 class)是 ArkTS 的特殊语法要求——在 ArkTS 中,自定义组件必须以 struct 而非 class 声明。
@Entry 与 @Component 的组合构成了一个完整的页面定义。@Entry 赋予该组件页面级生命周期(如 aboutToAppear、aboutToDisappear)和路由能力,而 @Component 提供组件级的状态管理和渲染能力。两者的协同使 PipaPage 成为既可独立运行、又具备完整状态驱动渲染能力的页面单元。struct 与 class 的关键区别在于——struct 是值类型,在 ArkTS 的编译器处理中被特殊对待,框架会为 struct 组件生成额外的状态管理代码和渲染调度逻辑,这些是 class 不具备的。
7.2 @State 状态变量声明
@State curTab: number = 0;
@State breath: boolean = false;
@State showAdd: boolean = false;
@State showEdit: boolean = false;
@State showDel: boolean = false;
@State selString: PipaStringItem | null = null;
@State selOrder: PipaOrderItem | null = null;
@State formName: string = '';
@State formWood: string = '';
@State formFrets: string = '';
@State formPrice: string = '';
@State 装饰器是 ArkTS 状态管理体系中最基础的一环。被 @State 修饰的变量,其值变化会自动触发所属组件的重新渲染。这十一组状态变量分别承担不同的职责。
curTab 是当前选中的标签索引,初始值为 0,对应"琵琶"标签。用户点击底部导航栏切换标签时,curTab 被更新,tabContent Builder 中的条件渲染逻辑据此切换显示对应的列表内容。这是整个应用的核心导航状态。
breath 是呼吸动画的开关布尔值,初始为 false。在 aboutToAppear 生命周期中通过 setInterval 每 500 毫秒翻转一次,驱动页面中所有依赖 breath 状态的动画效果——琴弦的透明度脉动、琵琶琴身的缩放呼吸、音符的上下飘移、进度条的透明度变化等。这个单一布尔变量以极简的方式驱动了整个页面的"活气",是状态驱动动画的经典案例。
showAdd、showEdit、showDel 三个布尔值分别控制新增、编辑、删除三个模态弹窗的显示与隐藏。这种"一状态一弹窗"的设计模式简洁明了,避免了多弹窗状态混叠的风险。当任何一个状态为 true 时,对应的模态弹窗通过条件渲染浮现在页面上层。
selString 和 selOrder 用于存储当前选中的琴弦和订单对象,类型为联合类型 PipaStringItem | null 和 PipaOrderItem | null。初始值为 null 表示未选中任何项。当用户点击琴弦卡片的"改"按钮时,selString 被赋值为对应的琴弦对象,随后 showEdit 置为 true 弹出编辑窗口。同理,点击订单卡片的"删"按钮时,selOrder 被赋值并弹出删除确认窗口。这种"先选中后操作"的模式是 CRUD 应用的经典交互流程。
formName、formWood、formFrets、formPrice 是新增琵琶弹窗中的表单字段,均以字符串类型存储。在 ArkTS 中,TextInput 组件的 onChange 回调返回的值始终为字符串类型,因此即使品数和价格在逻辑上是数字,表单状态也以字符串存储,在最终保存时再进行类型转换。这一设计简化了双向绑定的类型处理。
7.3 数据列表状态
@State pipas: PipaItem[] = [
new PipaItem('梨花木琵琶', '梨花木', 24, '四弦', 18800),
new PipaItem('紫檀琵琶', '小叶紫檀', 26, '四弦', 56000),
new PipaItem('红木演奏琵琶', '红木', 24, '四弦', 9800),
new PipaItem('白牛角轸琵琶', '老红木', 26, '四弦', 32000),
new PipaItem('六相十八品琵琶', '酸枝木', 24, '六相', 22000),
new PipaItem('少儿练习琵琶', '香樟木', 20, '四弦', 2600),
new PipaItem('檀木曲项琵琶', '檀木', 26, '曲项', 42000),
new PipaItem('竹节琵琶', '楠竹', 22, '四弦', 4800),
new PipaItem('螺钿镶嵌琵琶', '黄花梨', 26, '四弦', 68000),
new PipaItem('金漆描龙琵琶', '桐木', 24, '四弦', 15000),
new PipaItem('乌木直项琵琶', '乌木', 24, '直项', 36000),
new PipaItem('桐木古式琵琶', '桐木', 22, '四弦', 6800)
];
pipas 数组存储了十二款琵琶的详细数据。这组数据覆盖了从入门级练习琵琶到高端收藏级琵琶的全价格区间,木料从常见的红木、桐木到珍贵的小叶紫檀、黄花梨,品数从 20 到 26,规格涵盖四弦、六相、曲项、直项等多种形制。每一行数据通过 new PipaItem(…) 构造函数创建实例。由于 PipaItem 被 @Observed 修饰,数组中每个实例的属性变化都能被框架追踪。当通过 splice 向数组中插入新元素或删除元素时,@State 的监听机制会检测到数组引用的变化,触发 ForEach 的重新渲染,UI 列表随之更新。
这十二款琵琶的数据设置体现了丰富的行业知识。价格最高的是"螺钿镶嵌琵琶"(68000 元),其特点是黄花梨木料配合螺钿镶嵌工艺,属于艺术收藏级;价格最低的是"少儿练习琵琶"(2600 元),采用香樟木制作,面向初学儿童。"紫檀琵琶"以小叶紫檀为材、26 品高规格,售价 56000 元,是专业演奏级的高端选择。这些数据的多样性使应用在展示时信息丰富、层次分明。
@State strings: PipaStringItem[] = [
new PipaStringItem('缠弦 · 一弦', '0.32mm', 82, '钢丝缠丝', '低沉浑厚'),
new PipaStringItem('老弦 · 二弦', '0.28mm', 74, '钢丝缠丝', '圆润厚重'),
new PipaStringItem('中弦 · 三弦', '0.24mm', 62, '钢丝', '明亮清越'),
new PipaStringItem('子弦 · 四弦', '0.20mm', 55, '钢丝', '清脆高亢'),
new PipaStringItem('丝弦 · 一弦', '0.30mm', 60, '蚕丝', '温润古朴'),
new PipaStringItem('丝弦 · 二弦', '0.26mm', 52, '蚕丝', '柔和高雅'),
new PipaStringItem('尼龙缠弦', '0.28mm', 58, '尼龙', '均衡耐用'),
new PipaStringItem('银缠弦', '0.30mm', 66, '银丝', '音色华丽')
];
strings 数组存储了八种琴弦的数据。前四项是标准的钢丝缠丝四弦组——从最粗的缠弦(0.32mm,82% 张力)到最细的子弦(0.20mm,55% 张力),张力递减,音色从低沉浑厚到清脆高亢。后四项是不同材质的替换选择——蚕丝弦的温润古朴、尼龙弦的均衡耐用、银缠弦的华丽音色。张力的颜色映射在这组数据中得到充分体现:缠弦的 82% 张力对应红色,老弦的 74% 同样是红色,中弦的 62% 对应金色,子弦的 55% 也是金色。这种颜色编码让用户一眼就能判断每根弦的张力状态,无需逐个阅读数字。
@State scores: PipaScoreItem[] = [
new PipaScoreItem('十面埋伏', '武曲', 138, '明', '古谱'),
new PipaScoreItem('霸王卸甲', '武曲', 112, '明', '古谱'),
new PipaScoreItem('春江花月夜', '文曲', 96, '近代', '改编'),
new PipaScoreItem('阳春白雪', '文曲', 84, '清', '古谱'),
new PipaScoreItem('昭君出塞', '文曲', 76, '元', '古谱'),
new PipaScoreItem('海青拿天鹅', '武曲', 128, '元', '古谱'),
new PipaScoreItem('月儿高', '文曲', 88, '清', '古谱'),
new PipaScoreItem('彝族舞曲', '新作', 92, '当代', '创作')
];
scores 数组包含八首经典琵琶曲谱。武曲四首——“十面埋伏”(138 段,明代古谱)、“霸王卸甲”(112 段)、“海青拿天鹅”(128 段)气势恢宏,以战争和狩猎为题材;文曲三首——“春江花月夜”(96 段)、“阳春白雪”(84 段)、“月儿高”(88 段)意境优美,以自然景色和情感抒发为主题;新作一首——“彝族舞曲”(92 段,当代创作)展现了琵琶音乐在新时代的创新活力。这组数据的时间跨度从元代到当代,涵盖了琵琶曲谱发展的关键节点。
@State masters: PipaMasterItem[] = [
new PipaMasterItem('刘德海', '国宝级大师', 60, 45, 99),
new PipaMasterItem('林石城', '国宝级大师', 58, 38, 97),
new PipaMasterItem('王范地', '国家级大师', 52, 30, 95),
new PipaMasterItem('吴玉霞', '国家级大师', 44, 26, 93),
new PipaMasterItem('章红艳', '名家', 36, 20, 90),
new PipaMasterItem('赵聪', '名家', 32, 18, 88),
new PipaMasterItem('方锦龙', '名家', 30, 16, 92),
new PipaMasterItem('杨靖', '名师', 28, 12, 86)
];
masters 数组记录了八位琵琶演奏家的档案。刘德海和林石城是国宝级大师,技艺评分高达 99 和 97,从业均近六十年,是中国琵琶界的泰斗级人物。王范地和吴玉霞为国家级大师,技艺评分 95 和 93。章红艳、赵聪、方锦龙为名家级,杨靖为名师级。方锦龙以 92 分的技艺评分在"名家"级别中尤为突出,他精通多种民族乐器的跨界演奏风格使其在当代乐坛具有独特的影响力。技艺进度条的颜色统一使用 cinnabar(朱砂红),配合 breath 动画的透明度脉动效果,呈现出技艺流动的视觉感受。
@State schools: PipaSchoolItem[] = [
new PipaSchoolItem('浦东派', '文套细腻', 60, 280, 94),
new PipaSchoolItem('平湖派', '武套刚健', 45, 260, 90),
new PipaSchoolItem('汪派', '刚柔并济', 80, 300, 96),
new PipaSchoolItem('崇明派', '清新雅致', 30, 220, 82),
new PipaSchoolItem('无锡派', '朴实无华', 25, 190, 76),
new PipaSchoolItem('大套曲派', '气势磅礴', 20, 240, 84),
new PipaSchoolItem('浏阳派', '乡土灵秀', 18, 170, 72),
new PipaSchoolItem('现代演奏派', '中西融合', 120, 150, 92)
];
schools 数组记录了八大琵琶流派的信息。汪派以 96 的热度评分位居榜首,传承 300 年、80 名传人,刚柔并济的风格使其影响力最为广泛。浦东派 94 分、现代演奏派 92 分紧随其后。传统流派中,浏阳派的热度最低(72 分),仅有 18 名传人和 170 年传承历史,属于较为小众的地方流派。现代演奏派虽然传承年限最短(150 年),但拥有 120 名传人——远超其他流派,反映了当代琵琶艺术普及推广的蓬勃态势。热度进度条使用 gold 金色渲染,不参与 breath 动画,保持了稳定的视觉呈现。
@State shops: PipaShopItem[] = [
new PipaShopItem('平湖派琵琶坊', '浙江平湖', 12, 800, '甲级'),
new PipaShopItem('浦东派琵琶坊', '上海浦东', 10, 650, '甲级'),
new PipaShopItem('苏州琵琶社', '苏州', 14, 720, '甲级'),
new PipaShopItem('北京民族乐器厂', '北京', 16, 980, '甲级'),
new PipaShopItem('广州琵琶坊', '广州', 8, 520, '乙级'),
new PipaShopItem('西安琵琶坊', '西安', 6, 380, '乙级'),
new PipaShopItem('成都琵琶坊', '成都', 5, 300, '乙级'),
new PipaShopItem('扬州琵琶坊', '扬州', 4, 240, '丙级')
];
shops 数组记录了八家琵琶制作工坊的分布与规模。甲级工坊四家——北京民族乐器厂以 16 位匠师、980 把年产量居首,苏州琵琶社以 14 位匠师、720 把产量次之,平湖派琵琶坊和浦东派琵琶坊则分别代表了两大流派发源地的制作传统。乙级工坊分布在广州、西安、成都,丙级工坊在扬州,产量和匠师规模递减。工坊信息卡片的设计注重"量"的展示——年产量以蓝色字体突出,匠师人数作为辅助信息。
@State songs: PipaSongItem[] = [
new PipaSongItem('十面埋伏', '武曲', 98, '明', '金戈铁马'),
new PipaSongItem('春江花月夜', '文曲', 96, '近代', '江月清辉'),
new PipaSongItem('霸王卸甲', '武曲', 94, '明', '垓下悲歌'),
new PipaSongItem('阳春白雪', '文曲', 90, '清', '阳春和煦'),
new PipaSongItem('昭君出塞', '文曲', 88, '元', '塞外乡愁'),
new PipaSongItem('海青拿天鹅', '武曲', 92, '元', '海东青搏'),
new PipaSongItem('月儿高', '文曲', 86, '清', '月朗风清'),
new PipaSongItem('彝族舞曲', '新作', 91, '当代', '火把欢歌')
];
songs 数组是名曲鉴赏模块的数据源,与 scores 数组形成互补。scores 侧重曲谱的技术参数(乐段数、谱本级别),而 songs 侧重艺术鉴赏维度(美感评分、意境注解)。"十面埋伏"以 98 的美感评分居首,"金戈铁马"的四字意境精炼概括了这首武曲巅峰之作的恢弘气象。"春江花月夜"以 96 分紧随其后,"江月清辉"的注解让人联想到张若虚"春江潮水连海平,海上明月共潮生"的千古意境。美感进度条使用 hot 红色渲染,配合 breath 动画的透明度脉动,每首名曲的"传世之美"以流动的红色光带呈现。
@State orders: PipaOrderItem[] = [
new PipaOrderItem('紫檀琵琶', '民族乐团', 56000, 2, '2026-08'),
new PipaOrderItem('红木演奏琵琶', '音乐学院', 88000, 9, '2026-07'),
new PipaOrderItem('少儿练习琵琶', '琴行连锁', 26000, 10, '2026-08'),
new PipaOrderItem('螺钿镶嵌琵琶', '收藏家', 68000, 1, '2026-06'),
new PipaOrderItem('金漆描龙琵琶', '文创公司', 30000, 2, '2026-07'),
new PipaOrderItem('梨花木琵琶', '私塾', 37600, 2, '2026-05'),
new PipaOrderItem('竹节琵琶', '民乐培训', 14400, 3, '2026-08'),
new PipaOrderItem('桐木古式琵琶', '剧场', 13600, 2, '2026-06'),
new PipaOrderItem('檀木曲项琵琶', '海外华侨', 42000, 1, '2026-04'),
new PipaOrderItem('乌木直项琵琶', '美术馆', 36000, 1, '2026-05')
];
orders 数组记录了十笔琵琶订单的交易信息。金额最高的是"红木演奏琵琶"订单——音乐学院采购 9 把共计 88000 元,反映了教育机构对练习用琵琶的批量需求。单笔金额最高的是"螺钿镶嵌琵琶"——收藏家以 68000 元购入一把,体现了高端琵琶的收藏价值。买方类型多元化——从民族乐团、音乐学院到琴行连锁、收藏家、文创公司、私塾、民乐培训、剧场、海外华侨、美术馆,涵盖了琵琶销售的各个渠道。订单卡片的设计独树一帜——金额以横向柱状条可视化呈现,买方名称以蓝色胶囊标签展示,每张卡片底部还配有红色"删"字按钮。
八、生命周期:aboutToAppear
aboutToAppear(): void {
setInterval(() => {
this.breath = !this.breath;
}, 500);
}
aboutToAppear 是 ArkTS 组件生命周期的核心回调之一,在组件实例创建后、build 方法执行前被调用。它通常用于初始化数据、启动定时器、发起网络请求等准备工作。在这里,aboutToAppear 启动了一个 500 毫秒间隔的定时器,持续翻转 breath 布尔值。
这个看似简单的定时器是整个应用动画系统的"心脏"。breath 状态的周期性翻转驱动了页面中所有依赖它的动画效果:琴弦的透明度在 0.5 到 1 之间脉动,琵琶琴身在 1.0 到 1.03 之间缩放,音符在上下位置间跳跃,进度条的透明度在 0.6 到 1 之间变化。配合 .animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate }) 的动画属性设置,这些元素呈现出持续 500 毫秒的交替往返动画,形成了"呼吸"般的视觉效果。
PlayMode.Alternate 是动画播放模式的关键设置——它使动画在正向播放完毕后自动反向播放,形成往返循环。配合 iterations: -1(无限循环),动画将持续运行直到组件销毁。这种以单一布尔状态驱动多元素同步动画的设计,是 ArkTS 响应式系统强大能力的体现——开发者只需修改一个状态变量,框架自动处理所有关联 UI 的更新和动画过渡。
从生命周期管理的角度来看,aboutToAppear 中启动的定时器在组件销毁时理应被清除以防止内存泄漏。虽然在当前代码中未显式调用 clearInterval(因为 setInterval 的返回值未被保存),但在 ArkTS 的实际运行环境中,当组件被销毁时,其内部的所有定时器和事件监听会被框架自动清理。不过,作为最佳实践,开发者应保存定时器 ID 并在 aboutToDisappear 中清除,以确保资源的确定性释放。
九、模态遮罩:modalOverlay Builder
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => {
onClose();
})
}
@Builder 装饰器用于定义可复用的 UI 构建片段。与 @Component 不同,@Builder 不创建独立的组件实例,而是将其内容内联到调用处,因此性能开销更小,适合用于简单的、无独立状态的 UI 片段。
modalOverlay 是一个通用的模态遮罩 Builder,接收一个 onClose 回调函数作为参数。它渲染一个全屏的半透明遮罩层——width(‘100%’) 和 height(‘100%’) 确保覆盖整个屏幕,backgroundColor(COLORS.mask) 设置为 rgba(0,0,0,0.45) 的半透明黑色。当用户点击遮罩区域时,触发 onClose 回调关闭弹窗。
这个 Builder 的设计体现了"参数化复用"的思想。三个模态弹窗(新增、编辑、删除)都需要遮罩层,但各自的关闭逻辑不同。通过将 onClose 作为参数传入,modalOverlay 实现了一次定义、三处复用,避免了重复代码。在 addModal、editModal 和 delModal 中,分别传入不同的关闭回调:在 addModal 中传入 () => { this.showAdd = false; },在 editModal 中传入 () => { this.showEdit = false; },在 delModal 中传入 () => { this.showDel = false; }。这种设计模式在 ArkTS 开发中极为常见——将公共 UI 抽取为 @Builder,通过参数传入差异化逻辑,既保证了视觉一致性又实现了行为灵活性。
半透明遮罩的 rgba(0,0,0,0.45) 透明度选择也经过考量——0.45 的透明度使背景内容隐约可见但不分散注意力,既保留了上下文感知(用户知道弹窗下方有内容),又充分突出了弹窗本身。如果透明度过低(如 0.2),弹窗与背景的视觉分离不足;如果过高(如 0.8),背景完全不可见,用户失去了上下文感知。0.45 是经过实践验证的平衡值。
十、页面头部:pageHeader Builder
@Builder
pageHeader() {
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.borderRadius(22)
.linearGradient({
angle: 135,
colors: [[COLORS.header1, 0], [COLORS.header2, 1]]
})
pageHeader 是整个应用中最复杂、最精美的 Builder,它用纯代码绘制了一个微型的琵琶图案,配合文字信息构成视觉冲击力极强的页面头部。整个头部使用 Stack 作为根容器——Stack 允许子元素层叠排列,通过 position 绝对定位精确控制每个元素的位置,这是实现复杂图形绘制的关键布局技术。
第一层是一个全屏的渐变背景列。linearGradient 接收一个配置对象,angle: 135 指定渐变方向为 135 度(从左上到右下),colors 数组定义渐变断点——COLORS.header1(#3A1F14,深棕)在起始位置(0%),COLORS.header2(#1D0E08,极深棕)在结束位置(100%)。这个渐变模拟了紫檀木表面的光影质感,为头部区域奠定了深沉古朴的基调。borderRadius(22) 为四角添加圆角,使头部区域与下方内容区域的过渡更加柔和。
linearGradient 的参数结构值得详细说明。angle 以度为单位,0 度表示从下到上,90 度表示从左到右,135 度表示从左上到右下——这种斜向渐变比水平或垂直渐变更自然,因为现实中光线照射物体时很少是纯水平或纯垂直的。colors 数组中的每个元素是一个 [颜色值, 位置] 的二元组,位置值范围从 0 到 1,表示渐变进程中的百分比位置。通过设置多个断点可以创建复杂的多色渐变,但这里只用了两个断点形成简洁的双色渐变,效果干净利落。
Column()
.width(116)
.height(104)
.borderRadius(58)
.backgroundColor('#2B150D')
.border({ width: 2, color: COLORS.line })
.position({ x: 16, y: 16 })
第二层是琵琶琴背的主体形状。宽 116、高 104 的矩形,配合 borderRadius(58) 形成圆角矩形——半径 58 接近宽度的一半,使矩形的左右两侧呈现半圆形,近似模拟琵琶梨形音箱的轮廓。背景色 #2B150D 是比 header2 更深的暗棕色,代表琴背的木质纹理。金色边框(COLORS.line,#C9A86A)以 2 像素宽度环绕琴身,模拟琵琶琴背的镶嵌线条装饰。position({ x: 16, y: 16 }) 将琴身定位在 Stack 内的 (16, 16) 坐标处。
这里使用绝对定位而非相对布局的原因在于——Stack 内的多个元素需要精确叠加在特定位置上,形成"拼接"的视觉效果。如果使用 Flex 布局,元素只能按顺序排列,无法实现层叠叠加。position 属性使每个元素脱离正常文档流,通过指定的坐标精确定位,这是绘制复杂图形的必要手段。
Column()
.width(30)
.height(34)
.borderRadius({ topLeft: 6, topRight: 6, bottomLeft: 15, bottomRight: 15 })
.backgroundColor(COLORS.wood)
.position({ x: 58, y: 16 })
第三层是琵琶的琴头部分。宽 30、高 34 的矩形,使用非对称圆角——上方两角半径 6(较小的圆角),下方两角半径 15(较大的圆角),这种上窄下宽的圆角配置模拟了琵琶琴头(轸槽部位)的造型特征。backgroundColor(COLORS.wood) 使用木质棕色填充,与琴身的深色形成层次对比。position({ x: 58, y: 16 }) 将琴头定位在琴身正上方居中位置。borderRadius 的对象式语法 { topLeft, topRight, bottomLeft, bottomRight } 允许为四个角分别设置不同的圆角半径。这种灵活性在绘制不对称图形时极为重要——真实乐器的各部位形状很少是完全对称的圆角矩形,通过差异化圆角设置可以更精确地还原乐器的造型特征。
Column()
.width(10)
.height(26)
.borderRadius(5)
.backgroundColor(COLORS.ivory)
.position({ x: 68, y: 42 })
ForEach(FRET_IDX, (i: number) => {
Column()
.width(44 - i * 4)
.height(3)
.borderRadius(1.5)
.backgroundColor(COLORS.ivory)
.opacity(0.8)
.position({ x: 52 + i * 2, y: fretY(i) })
}, (i: number) => 'fr' + i)
第四层是琴头的轸子(调弦轴)和六相品相。轸子用一个 10x26 的象牙白圆角矩形表示,位于琴头正中央。随后的 ForEach 循环渲染六个品相——即琵琶的"六相"。每个品相是一条水平线,宽度从 44 递减到 24(44 - i * 4),模拟从琴身上端到下端品相逐渐变窄的真实形态。高度统一为 3,圆角 1.5,使用象牙白色填充并设置 0.8 的透明度,呈现出品相竹片的质感。位置通过 fretY(i) 函数计算纵坐标,横坐标则根据索引偏移 52 + i * 2,使品相在水平方向上微微错位,更加贴合真实琵琶品相的排列方式。ForEach 的第三个参数 (i: number) => ‘fr’ + i 为每个元素生成唯一键值 ‘fr0’ 到 ‘fr5’,确保框架在 diff 更新时能正确识别和复用元素。品相宽度随索引递减的设计(44 - i * 4)是模拟琵琶真实形态的关键——从琴颈到琴腹,品相的长度逐渐缩短,因为琴身在下端变宽,品相不需要那么长就能横跨琴面。
ForEach(STRING_IDX, (i: number) => {
Column()
.width(1.5)
.height(78)
.backgroundColor(COLORS.stringC)
.opacity(this.breath ? 1 : 0.5)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: stringX(i), y: 46 })
}, (i: number) => 'st' + i)
第五层是四根琴弦。ForEach 遍历 STRING_IDX 数组,为每根弦渲染一个宽度 1.5、高度 78 的细长矩形,使用 stringC 米白色填充。这里是 breath 动画的核心应用点之一——透明度在 this.breath ? 1 : 0.5 之间切换,配合 500 毫秒的交替往返动画,琴弦呈现出持续脉动的视觉效果,仿佛琴弦在轻微振动发声。stringX(i) 函数计算每根弦的横坐标,四根弦分别位于 x=44、52、60、68 的位置,间距 8 单位,模拟琵琶四弦从粗到细的排列。纵坐标统一为 46,使四根弦从琴身中部贯穿到底部,符合真实琵琶弦的走向。琴弦宽度仅 1.5 个单位,极为纤细,配合米白色调,在视觉上还原了丝弦的质感。透明度在 0.5 和 1 之间交替——0.5 时琴弦若隐若现,1 时清晰可见,这种脉动效果暗示了琴弦正在振动发声,赋予静态图形以动态的音乐感。
Column()
.width(74)
.height(52)
.borderRadius({ topLeft: 26, topRight: 26, bottomLeft: 14, bottomRight: 14 })
.backgroundColor(COLORS.wood)
.border({ width: 1.5, color: COLORS.line })
.scale({ x: this.breath ? 1.03 : 1, y: this.breath ? 1.03 : 1 })
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 34, y: 66 })
第六层是琵琶的面板(梧桐木面板)。宽 74、高 52 的矩形,使用非对称圆角——上方两角半径 26(形成半圆形顶部),下方两角半径 14(较小的圆角底部),这一圆角配置使面板呈现出上圆下窄的水滴形轮廓,精确模拟了琵琶面板的经典造型。木质棕色填充配合金色边框,与琴背形成统一的木质色调。scale 属性是这里的动画核心——this.breath ? 1.03 : 1 使面板在 1.0 和 1.03 之间缩放,形成轻微的"呼吸"效果。配合 PlayMode.Alternate 的 500 毫秒动画,面板仿佛随着呼吸轻微膨胀和收缩,赋予静态图形以生命力。这种微动效设计是现代 UI 设计中"活气"理念的经典体现——通过极其细微的持续动画,让界面显得有生命、有温度。缩放幅度仅 3%(1.0 到 1.03),既不会过于显眼影响阅读,又足以被用户的余光感知,营造出"活的"视觉体验。
Column()
.width(14)
.height(14)
.borderRadius(7)
.backgroundColor(COLORS.cinnabar)
.position({ x: 88, y: 80 })
第七层是面板上的音孔装饰。一个 14x14 的圆形(borderRadius(7) 为半径),填充朱砂红色(COLORS.cinnabar,#B33A2B),位于面板右下方。这个红色圆点模拟了琵琶面板上的装饰物或音孔位置,为以棕金色为主的头部增添了一抹亮色点缀,打破了色彩的单调感。在以深棕、木质棕、金色、米白色为主色调的头部区域中,这个朱砂红的圆点形成了强烈的色彩对比——暖色系中的冷色点缀,暗色系中的亮色突出。这种"画龙点睛"式的色彩处理在视觉设计中极为重要,它为整体画面注入了活力和焦点,避免了大面积同色调带来的沉闷感。圆点不参与动画,保持了稳定的视觉锚点,与周围脉动的琴弦和呼吸的面板形成动静对比。
ForEach(NOTE_IDX, (i: number) => {
Text(i % 2 === 0 ? '♪' : '♫')
.fontSize(13)
.fontColor(COLORS.gold)
.translate({ y: this.breath ? noteY(i) : noteY(i) - 6 })
.opacity(this.breath ? 1 : 0.4)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: noteX(i), y: 24 })
}, (i: number) => 'nt' + i)
第八层是飘动的音符装饰。ForEach 遍历 NOTE_IDX 数组,渲染三个音符——偶数索引为单音符 ♪,奇数索引为双音符 ♫,使用金色(COLORS.gold)和 13 号字体。这是 breath 动画的又一个应用场景:通过 translate 的 y 值在 noteY(i) 和 noteY(i) - 6 之间切换,音符在垂直方向上下飘移 6 个单位;同时透明度在 0.4 到 1 之间脉动。两个动画维度叠加,音符呈现出"飘忽不定"的灵动感,仿佛从琵琶中飘出的乐音。noteX(i) 计算横坐标(168、202、236),三个音符在头部右侧水平排列;noteY(i) 计算纵坐标,根据 i % 2 使音符高低交替(20 和 46),形成参差错落的排列效果。translate 属性与 position 的区别在于——position 设置元素的绝对位置,而 translate 是在当前位置基础上的偏移量。这里先用 position 确定音符的基础位置,再用 translate 的 y 值变化实现上下飘移的动画效果,两种定位方式配合使用,既保证了布局的精确性又实现了动画的流畅性。
Column() {
Text('琵琶坊')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Text('大弦嘈嘈 · 小弦切切')
.fontSize(12)
.fontColor(COLORS.sub)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 158, y: 22 })
第九层是应用标题文字。"琵琶坊"三个字以 24 号加粗字体、暖白色(COLORS.title,#F7E9D6)显示,是整个头部最醒目的文字元素。下方副标题"大弦嘈嘈·小弦切切"以 12 号字体、棕色(COLORS.sub,#D0B392)显示,这句副标题直接化用了白居易《琵琶行》的名句,以极简的方式点明了应用的文化主题。Column 容器设置 alignItems(HorizontalAlign.Start) 使文字左对齐,position({ x: 158, y: 22 }) 将文字块定位在头部右侧——与左侧的琵琶图形形成左右呼应的布局结构。margin({ top: 6 }) 在主标题和副标题之间留出适当的间距,保证文字的呼吸感。24 号字体配合加粗处理使"琵琶坊"三个字成为头部的视觉焦点,而 12 号的副标题则作为辅助信息存在,字号差异形成了清晰的信息层次。
Column() {
Text('四弦 · 六相 · 二十四品')
.fontSize(12)
.fontColor(COLORS.gold)
Row() {
Text('武曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.hot)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text('文曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.cool)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.margin({ left: 8 })
Text('大曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.gold)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 158, y: 80 })
}
.width('100%')
.height(136)
第十层是琵琶规格说明和曲类标签。"四弦·六相·二十四品"以金色小字描述了标准琵琶的形制参数。下方的 Row 容器中排列三个曲类标签——"武曲"红色背景、"文曲"蓝色背景、"大曲"金色背景,白色文字配合 8 像素圆角和内边距,形成三个色彩鲜明的胶囊标签。这三个标签的色彩编码与后续列表中的 levelColor 函数保持一致,确保了整个应用中"武曲-红、文曲-蓝、大曲-金"的色彩语义统一。用户在头部看到这组标签后,在浏览列表时会自然将颜色与曲类关联,降低了认知成本。padding 的对称设置(左右各 8,上下各 3)使标签内部留白均匀,margin({ left: 8 }) 使标签之间保持 8 单位的间距,三个标签紧密排列又互不粘连。
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('琵琶款式')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('经典曲谱')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('名家乐师')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
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%')
在 Stack 头部图形下方,是一个四列统计数据栏。四个 Column 分别显示"12 琵琶款式"、“8 经典曲谱”、“8 名家乐师”、"8 传世名曲"的统计数字。每个 Column 使用 layoutWeight(1) 均分水平空间,金色加粗数字在上,棕色小字说明在下,形成了简洁的仪表盘式信息呈现。这个统计栏的设计体现了"信息密度与可读性平衡"的原则。四个关键数字让用户在进入页面时立即获取应用的核心数据概览,无需翻阅列表。borderRadius 只设置了上方两角的圆角(16),下方两角为 0,使统计栏与下方内容区域无缝衔接。margin({ top: 10 }) 在头部图形和统计栏之间留出 10 单位的间距,避免视觉拥挤。数字使用 20 号字体加粗金色显示,标签使用 10 号字体棕色显示,字号差异(20:10 = 2:1)形成了强烈的视觉对比,使数字成为信息焦点。
十一、章节标题:sectionTitle Builder
@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 })
}
sectionTitle 是一个接收三个参数的通用标题 Builder:icon 是 Emoji 图标字符,title 是主标题文字,sub 是右侧辅助说明。它渲染为一个水平排列的 Row——左侧是图标和标题,中间用 Column().layoutWeight(1) 撑开空间,右侧是辅助说明。这个 Builder 在 tabContent 中被八个标签页分别调用,每次传入不同的参数。例如,"琵琶"标签页调用 this.sectionTitle(‘🎻’, ‘传世琵琶’, ‘共 ’ + this.pipas.length + ’ 款’),“琴弦"标签页调用 this.sectionTitle(‘🎼’, ‘琴弦工艺’, ‘四弦一柱’)。这种参数化复用使八个标签页的标题风格完全统一,同时通过不同参数内容体现各页特色。Column().layoutWeight(1) 是一个常见布局技巧——一个空白的、权重为 1 的 Column 充当"弹簧”,将左右两侧的内容推开。这种"弹性占位"模式在水平布局中极为实用,无需复杂的对齐属性即可实现"左右分布"效果。padding 只设置了左右和底部内边距,使标题与下方列表内容之间有 10 单位的呼吸空间。三个参数的字体大小递减——图标 15 号、标题 16 号、副标题 11 号——但标题因加粗处理在视觉上最为突出,图标作为视觉引导,副标题作为补充说明,三者形成了清晰的信息层次。
十二至十九、列表行 Builder 体系
应用定义了八个列表行 Builder,分别对应八个标签页的内容渲染。每个 Builder 接收对应的数据模型实例和索引作为参数,渲染一张信息卡片。这些 Builder 共享统一的视觉规范——白色背景(cardBg)、14 像素圆角、12 像素内边距、8 像素底部外边距——但在内部布局和信息呈现上各有特色。
pipaRow 渲染琵琶列表行,采用单行布局——左侧名称和木料规格,右侧价格和品数,最右侧"编"字按钮。stringRow 渲染琴弦列表行,采用上下两行布局——上行包含颤动的弦条、名称材质、张力和"改"按钮,下行是张力进度条。scoreRow 渲染曲谱列表行,单行布局,左侧曲名和三合一描述,右侧乐段数。masterRow 渲染乐师列表行,上下两行布局,上行姓名职称和代表曲目,下行技艺进度条。schoolRow 渲染流派列表行,上下两行布局,上行流派名和风格标签,下行热度进度条和人数。shopRow 渲染工坊列表行,单行布局,左侧工坊名和城市等级,右侧产量和匠师数。songRow 渲染名曲列表行,上下两行布局,上行曲名和风格标签,下行美感进度条。orderRow 渲染订单列表行,上下两行布局,上行琵琶名和买方标签,下行金额柱条和删除按钮。
这些 Builder 的设计遵循了"数据决定布局"的原则——数据字段越多、需要可视化的维度越多,布局就越复杂。简单的列表行(如曲谱、工坊)采用单行布局,复杂的列表行(如琴弦、乐师、名曲、订单)采用上下两行布局以容纳进度条等可视化元素。所有进度条都使用相同的结构——标签+动态宽度彩色条+弹性填充+数值——但颜色和宽度计算函数各不相同,体现了"统一模板、差异化参数"的设计哲学。
每个 Builder 中动画的应用也经过精心设计。stringRow 中的弦条使用 scale 缩放动画模拟弦振,进度条使用 opacity 透明度脉动。masterRow 的技艺进度条使用 opacity 脉动。songRow 的美感进度条使用 opacity 脉动。orderRow 的金额柱条使用 opacity 脉动。而 schoolRow 的热度进度条不参与动画,保持稳定——这是因为热度是相对静态的指标,不需要动态强调。这种"选择性动画"的设计避免了页面中所有元素同时脉动导致的视觉混乱,使动画效果有重点有节奏。
二十、标签内容切换:tabContent Builder
@Builder
tabContent() {
if (this.curTab === 0) {
Column() {
this.sectionTitle('🎻', '传世琵琶', '共 ' + this.pipas.length + ' 款')
ForEach(this.pipas, (item: PipaItem, i: number) => {
this.pipaRow(item, i)
}, (item: PipaItem) => item.name)
}
.width('100%')
} else if (this.curTab === 1) {
Column() {
this.sectionTitle('🎼', '琴弦工艺', '四弦一柱')
ForEach(this.strings, (item: PipaStringItem, i: number) => {
this.stringRow(item, i)
}, (item: PipaStringItem) => item.name)
}
.width('100%')
} else if (this.curTab === 2) {
Column() {
this.sectionTitle('📜', '传世曲谱', '减字谱 · 工尺谱')
ForEach(this.scores, (item: PipaScoreItem, i: number) => {
this.scoreRow(item, i)
}, (item: PipaScoreItem) => item.title)
}
.width('100%')
} else if (this.curTab === 3) {
Column() {
this.sectionTitle('🎭', '琵琶名家', '技艺传承')
ForEach(this.masters, (item: PipaMasterItem, i: number) => {
this.masterRow(item, i)
}, (item: PipaMasterItem) => item.name)
}
.width('100%')
} else if (this.curTab === 4) {
Column() {
this.sectionTitle('🧩', '演奏流派', '派别林立')
ForEach(this.schools, (item: PipaSchoolItem, i: number) => {
this.schoolRow(item, i)
}, (item: PipaSchoolItem) => item.name)
}
.width('100%')
} else if (this.curTab === 5) {
Column() {
this.sectionTitle('🏭', '制琴工坊', '选料 · 挖膛 · 蒙皮')
ForEach(this.shops, (item: PipaShopItem, i: number) => {
this.shopRow(item, i)
}, (item: PipaShopItem) => item.name)
}
.width('100%')
} else if (this.curTab === 6) {
Column() {
this.sectionTitle('🎵', '千古名曲', '文曲武曲')
ForEach(this.songs, (item: PipaSongItem, i: number) => {
this.songRow(item, i)
}, (item: PipaSongItem) => item.title)
}
.width('100%')
} else {
Column() {
this.sectionTitle('📦', '最新订单', '共 ' + this.orders.length + ' 单')
ForEach(this.orders, (item: PipaOrderItem, i: number) => {
this.orderRow(item, i)
}, (item: PipaOrderItem) => item.name)
}
.width('100%')
}
}
tabContent 是整个应用内容区的核心 Builder,通过 if-else if-else 条件分支根据 curTab 的值渲染对应的标签页内容。这是 ArkTS 中条件渲染的典型应用——当 curTab 变化时,框架自动卸载旧分支的 UI,挂载新分支的 UI。每个分支的结构高度统一:先调用 sectionTitle 渲染章节标题,再使用 ForEach 遍历对应的数据数组渲染列表行。
每个 ForEach 的第三个参数是一个键值生成函数,为每个列表项生成唯一标识。这些键值的命名各不相同——琵琶用 item.name,曲谱用 item.title,名曲用 item.title,其余用 item.name。使用数据的唯一属性作为键值,确保了当数组发生变化(增删改)时,框架能高效地 diff 出变化项,仅更新受影响的 UI 元素,而非全量重新渲染。ForEach 的键值函数是 ArkTS 列表渲染性能优化的关键。如果列表项没有唯一键值,框架在数据变化时可能需要重新渲染整个列表,导致性能下降。通过为每项提供稳定的唯一键值,框架可以精确识别新增、删除和修改的项,实现最小化更新。这种设计在大数据量列表中尤为重要。
八个标签页的章节副标题各有特色——“共 12 款”、“四弦一柱”、“减字谱·工尺谱”、“技艺传承”、“派别林立”、“选料·挖膛·蒙皮”、“文曲武曲”、“共 10 单”。这些副标题既有数据统计型(如"共 12 款"),也有知识科普型(如"减字谱·工尺谱"),还有工艺描述型(如"选料·挖膛·蒙皮"),在统一格式中体现了各模块的内容特色。if-else if-else 链的最后一个分支使用 else 而非 else if (this.curTab === 7),这是一种防御性编程的写法——即使 curTab 的值因某种原因超出预期范围,else 分支也能兜底渲染订单列表,不会出现空白页面。
二十一至二十二、底部导航:bottomItem 与 bottomBar Builder
@Builder
bottomItem(i: number) {
Column() {
Text(TAB_LIST[i].icon)
.fontSize(17)
Text(TAB_LIST[i].label)
.fontSize(11)
.fontColor(this.curTab === i ? COLORS.gold : COLORS.sub)
.margin({ top: 2 })
}
.width('25%')
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === i ? '#4A2A18' : COLORS.tabBg)
.borderRadius(10)
.onClick(() => {
this.curTab = i;
})
}
bottomItem 渲染底部导航栏的单个标签项。接收一个索引参数 i,从 TAB_LIST 数组中读取对应的图标和标签文字。图标以 17 号字体显示,标签文字以 11 号字体显示在图标下方。这里的核心设计是选中态的视觉反馈——通过 this.curTab === i 三元表达式实现两处动态样式:文字颜色在选中时为金色(COLORS.gold),未选中为棕色(COLORS.sub);背景色在选中时为 #4A2A18(比默认 tabBg 稍亮的棕色),未选中为 COLORS.tabBg。这种"颜色变化+背景高亮"的双重反馈机制,使用户能清晰识别当前所在的标签页。width(‘25%’) 使每个标签项占据四分之一宽度,配合 borderRadius(10) 的圆角,四个标签在一行内等距排列,视觉间距均匀。点击事件 this.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 将八个标签项组织为两行四列的网格布局。两个 Row 分别渲染 ROW1_IDX(索引 0-3)和 ROW2_IDX(索引 4-7)对应的标签项。第二个 Row 通过 margin({ top: 6 }) 与第一行保持 6 单位的间距。整个导航栏以深棕色(COLORS.tabBg,#3A1F14)为背景,上方两角设置 18 像素圆角,下方两角为直角,使导航栏与上方内容区域的衔接呈现"上圆下直"的过渡效果。这种设计模拟了从内容区"浮出"底部的视觉效果,是移动端应用中常见的底部导航栏样式。ForEach 的键值生成函数分别为 ‘r1’ + i 和 ‘r2’ + i,确保两行标签项的键值不会冲突。这种"行前缀+索引"的键值命名策略,在多维列表渲染中是防止键值冲突的有效手段。
二十三至二十五、模态弹窗:addModal、editModal、delModal Builder
三个模态弹窗共享相同的架构模式:if 条件控制显示、Stack 层叠遮罩与主体、modalOverlay 传入关闭回调、白色圆角弹窗主体、底部操作按钮。这种统一的架构确保了三个弹窗在视觉和行为上的一致性。
addModal 是新增琵琶的表单弹窗,包含四个 TextInput 输入框(名称、木料、品数、售价)和取消/保存两个按钮。保存逻辑通过 splice(0, 0, newItem) 在数组头部插入新元素。editModal 是琴弦张力调整弹窗,展示选中琴弦的名称和当前张力,提供"紧弦+5%"和"松弦-5%"两个操作按钮。张力修改直接作用于 @Observed 对象的属性,触发关联 UI 的即时更新。delModal 是删除订单的确认弹窗,展示被删订单信息,提供"再想想"和"确认删除"两个按钮。删除逻辑通过 splice(indexOf(item), 1) 从数组中移除选中项。
三个弹窗的按钮颜色编码各不相同:addModal 的保存按钮使用棕色(accent),editModal 的紧弦按钮使用红色(hot)、松弦按钮使用蓝色(cool),delModal 的确认删除按钮使用警示红色(danger)。这些颜色选择不仅是为了视觉区分,更与整个应用的色彩语义体系保持一致——红色代表热烈/危险,蓝色代表冷静/安全,棕色代表确认/操作。三个弹窗都使用 zIndex(999) 确保浮于所有内容之上,constraintSize({ maxHeight: ‘80%’ }) 限制最大高度防止溢出屏幕。
二十六、页面构建: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 方法是 ArkTS 组件的心脏——它定义了组件的完整 UI 结构。PipaPage 的 build 方法以 Stack 为根容器,实现了"主内容层"和"模态弹窗层"的层叠架构。Stack 内最底层是一个全屏 Column,包含两个部分:上方是可滚动的 Scroll 容器,下方是固定的 bottomBar 底部导航栏。Scroll 容器内嵌一个 Column,依次渲染 pageHeader(页面头部)和 tabContent(标签内容)。padding({ left: 14, right: 14, bottom: 12 }) 为内容区域提供左右各 14 单位、底部 12 单位的内边距,确保内容不贴边。
Scroll 设置 .scrollable(ScrollDirection.Vertical) 启用垂直滚动,.layoutWeight(1) 使其占据底部导航栏之外的全部剩余高度。当列表内容超过屏幕高度时,用户可以通过垂直滑动浏览所有列表项。bottomBar 作为 Column 的第二个子元素,固定在页面底部,不随 Scroll 滚动。这种"可滚动内容区+固定底部导航"的布局是移动端应用最经典的页面结构。在 Stack 的上层,三个 if 条件分别控制三个模态弹窗的渲染。由于三个状态默认都是 false,页面初始加载时不会显示任何弹窗,用户看到的是干净的主内容界面。Stack 的层叠架构使弹窗的显示和隐藏不影响主内容的布局和状态,用户关闭弹窗后能无缝返回之前的操作位置。
二十七、系统架构流程图
上图展示了应用从启动到渲染的完整流程。aboutToAppear 生命周期中启动的 breath 定时器是整个动画系统的驱动源——它以 500 毫秒为周期翻转布尔值,触发所有依赖 breath 的动画效果同步更新。build 方法构建的三层架构(主内容层、底部导航层、模态弹窗层)各司其职,通过 curTab 状态变量实现内容切换,通过 showAdd/showEdit/showDel 状态变量实现弹窗控制。八个标签页的内容通过 if-else if-else 条件分支按需渲染,每次只有一个标签页的内容存在于 UI 树中,保证了渲染效率。
二十八、数据流与状态驱动流程图
上图展示了应用的数据流与状态驱动机制。用户交互触发状态变更,状态变更驱动 UI 更新,形成了"交互-状态-渲染"的单向数据流。@State 负责组件级状态(如 curTab、showEdit),@Observed 负责对象级状态(如 PipaStringItem.tension 的属性变更),两者协同实现了全场景的响应式更新。breath 定时器作为独立的状态驱动源,不依赖用户交互,以固定周期驱动所有动画元素的持续运动。
二十九、模态弹窗交互流程图
上图详细展示了三个模态弹窗的完整交互流程。新增流程通过 splice(0, 0, newItem) 在数组头部插入新项;编辑流程通过直接修改 @Observed 对象的 tension 属性触发响应式更新;删除流程通过 splice(index, 1) 从数组中移除选中项。三种操作虽然机制不同(数组插入、属性修改、数组删除),但都通过 @State 和 @Observed 的响应式系统实现了 UI 的自动刷新,无需手动调用任何刷新方法。
三十、状态管理与响应式机制深度分析
ArkTS 的状态管理体系是整个应用架构的核心支撑。在本应用中,状态管理分为三个层次:组件级状态(@State)、可观察对象状态(@Observed)和 Builder 参数传递。
@State 装饰器用于管理组件内部的可变状态。当 @State 变量的值发生变化时,ArkUI 框架会自动重新调用 build 方法,更新与该状态绑定的 UI 部分。在本应用中,curTab 控制标签页切换是最典型的 @State 应用——用户点击底部导航项后,curTab 更新,tabContent 中的条件分支重新求值,新标签页的列表内容被渲染到屏幕上。这种"状态即真理"的设计理念,使开发者无需关心 UI 更新的时机和方式,只需维护状态数据的正确性。
@Observed 装饰器用于使自定义类的实例成为可观察对象。当被 @Observed 修饰的类的属性被修改时,框架能够追踪到这一变化并通知所有引用该对象的 UI 组件重新渲染。在本应用中,editModal 修改 selString.tension 后,stringRow 中引用同一 PipaStringItem 对象的张力进度条和颜色会自动更新。这种"对象级响应式"的能力,使得深度修改对象属性也能触发精确的 UI 更新,无需替换整个对象引用。
Builder 参数传递是 ArkTS 中实现组件间数据传递的机制。@Builder 修饰的方法可以接收参数,在调用时将外部数据传入 Builder 内部使用。本应用中,sectionTitle 接收 icon、title、sub 三个参数,pipaRow 接收 item 和 i 两个参数,modalOverlay 接收 onClose 回调函数。这种参数化设计使 Builder 具备了复用性和灵活性,同一个 Builder 可以在不同上下文中以不同参数被调用。
breath 状态是本应用中最特殊的状态变量——它不是响应用户交互,而是由定时器自动驱动的"动画心跳"。每 500 毫秒翻转一次的布尔值,驱动了页面中所有动画元素的同步运动。这种以单一状态驱动多元素动画的设计,展示了 ArkTS 状态管理在动画场景中的强大能力。开发者只需维护一个布尔变量,框架自动处理所有关联 UI 的属性更新和动画过渡,大大简化了动画实现的复杂度。
@State 与 @Observed 的协同工作模式值得深入理解。@State 修饰数组变量(如 pipas: PipaItem[]),框架追踪数组本身的变更(如 splice 操作导致的增删);@Observed 修饰数组元素的类(如 PipaItem),框架追踪实例属性的变更(如 tension 的修改)。两者分别管理"数组级"和"属性级"两个粒度的响应式更新,形成了完整的响应式覆盖。当数组被 splice 修改时,@State 触发 ForEach 重新渲染;当数组元素的属性被修改时,@Observed 触发引用该属性的 UI 组件更新。这种分层设计确保了无论数据变化发生在哪个层级,UI 都能精确地响应。
三十一、布局系统深度分析
ArkTS 的布局系统以声明式语法为核心,通过 Column(纵向排列)、Row(横向排列)、Stack(层叠排列)三种基本容器构建复杂的 UI 结构。本应用的布局运用了这三种容器的全部特性,并配合 layoutWeight、position、margin、padding 等属性实现精确的布局控制。
Column 是最常用的纵向容器。在本应用中,列表行卡片(如 stringRow、masterRow、schoolRow、songRow、orderRow)都使用了 Column 作为根容器,内部包含两个 Row 子行——上行显示基本信息,下行显示进度条或附加信息。alignItems(HorizontalAlign.Start) 属性控制子元素的水平对齐方式,Start 表示左对齐,使文字内容从左侧起始排列。alignItems(HorizontalAlign.End) 则使内容右对齐,常用于价格、数值等右对齐信息。
Row 是横向容器,在列表行中用于将左侧信息区、右侧数值区和操作按钮水平排列。layoutWeight 是 Row 中最重要的属性——它为子元素分配弹性权重。layoutWeight(1) 意味着该元素占据所有剩余空间,在信息区和数值区之间充当"弹性填充"。这种弹性布局模式使列表行能够自适应不同屏幕宽度,左侧信息区和右侧数值区始终保持合理的间距。
Stack 是层叠容器,允许子元素在 Z 轴方向叠加。本应用中,pageHeader 使用 Stack 将琵琶图形的各层组件(背景渐变、琴身、琴头、品相、琴弦、面板、装饰点、音符、文字、标签)层叠组合,通过 position 绝对定位精确控制每个元素的位置。build 方法也使用 Stack 实现主内容层和模态弹窗层的叠加。
position 属性是绝对定位布局的核心。在 pageHeader 中,琵琶图形的每个组成部分都通过 position({ x: 横坐标, y: 纵坐标 }) 精确定位。这种绝对定位方式适合绘制复杂的图形和装饰元素——开发者可以像在画布上作画一样,精确控制每个视觉元素的位置。配合 stringX、fretY、noteX、noteY 等位置计算函数,头部图形的布局既精确又可维护。
borderRadius 的灵活运用是本应用布局的一大特色。简单圆角使用数值参数(如 borderRadius(14)),非对称圆角使用对象参数(如 borderRadius({ topLeft: 26, topRight: 26, bottomLeft: 14, bottomRight: 14 }))。非对称圆角在绘制琵琶琴身、琴头和面板的形状时发挥了关键作用——通过为四个角设置不同的圆角半径,可以模拟出梨形、水滴形等复杂的有机轮廓,使纯代码绘制的图形具有手绘般的自然感。
layoutWeight 在本应用中还有"弹性占位"的用法——sectionTitle 中的 Column().layoutWeight(1) 是一个空白弹性元素,它在 Row 中占据所有剩余空间,将左侧标题和右侧副标题推向两端。这种技巧无需复杂的 justifyContent 设置即可实现"两端对齐"效果,代码简洁而高效。在列表行中,Column().layoutWeight(1) 也常用于在进度条后填充空间,使进度条不会延伸到行尾。
linearGradient 渐变背景的使用为页面增添了深度感。pageHeader 中的深棕色渐变模拟了紫檀木的光影质感,这种通过 CSS 渐变模拟材质表面的技巧在前端开发中广泛使用。渐变方向 135 度(左上到右下)是最自然的光照角度,因为它模拟了来自左上方的光源照射效果。渐变的两个断点 COLORS.header1 和 COLORS.header2 都是极深的棕色,色差不大但足以产生微妙的深度感。
三十二、动画系统深度分析
本应用的动画系统以 breath 布尔状态为核心驱动源,通过 .animation() 属性配置实现多种动画效果的同步播放。整个动画体系可以概括为"一个状态源、多种动画类型、统一时间参数"的设计模式。
一个状态源指的是 breath 布尔变量。它在 aboutToAppear 中通过 setInterval 每 500 毫秒翻转一次,是所有动画的唯一触发源。当 breath 从 false 变为 true(或从 true 变为 false)时,所有引用 this.breath 的属性值都会发生变化,框架检测到这些变化后自动启动对应的动画过渡。
多种动画类型包括:透明度动画(opacity 在不同值之间切换)、缩放动画(scale 的 x/y 在不同比例之间切换)、位移动画(translate 的 y 值在不同位置之间切换)。在 pageHeader 中,琴弦使用透明度动画(0.5 到 1),面板使用缩放动画(1.0 到 1.03),音符同时使用位移和透明度动画。在列表行中,弦条使用缩放动画(1.0 到 1.4),各种进度条使用透明度动画(0.6 到 1)。每种动画类型都针对元素的物理特征进行了选择——琴弦细长适合透明度脉动(模拟振动时的视觉残影),面板面积大适合微小缩放(模拟呼吸膨胀),音符轻灵适合位移飘移(模拟飘动)。
统一时间参数指的是所有动画都使用相同的配置:duration: 500(500 毫秒持续时间)、iterations: -1(无限循环)、playMode: PlayMode.Alternate(交替往返)。500 毫秒的持续时间与 breath 定时器的 500 毫秒间隔完美匹配——当 breath 翻转时,动画恰好完成一个往返周期,然后开始下一个周期。这种"定时器频率与动画时长同步"的设计确保了动画的连续性和平滑性,不会出现跳帧或卡顿。
PlayMode.Alternate 的作用机制值得深入理解。普通动画模式(Normal)在播放完毕后会回到起始状态,形成"去-回-去-回"的跳变效果。而 Alternate 模式在正向播放完毕后自动反向播放,形成"去-回-去-回"的平滑往返效果。这种往返效果恰好与 breath 布尔值的 true-false-true-false 翻转周期同步——breath 为 true 时动画正向播放到终点,breath 为 false 时动画反向播放回起点,两者完美契合。
动画幅度的差异化设计也是本应用的一大亮点。不同元素的动画幅度根据其视觉特征进行了调整:面板缩放幅度仅 3%(1.0 到 1.03),因为面板面积大,微小变化即可被感知;弦条缩放幅度达 40%(1.0 到 1.4),因为弦条体积小,需要大幅度变化才能被注意到;音符位移幅度 6 个单位,因为音符是装饰性元素,较大的位移更能传达"飘动"的意象;进度条透明度在 0.6 到 1 之间变化(40% 的变化幅度),足以产生明显的脉动感但又不至于完全消失。这种"因元素制宜"的动画幅度设计,确保了所有动画在视觉感知上具有一致的强度。
三十三、技术对比表格
| 技术维度 | ArkTS 声明式 UI | 传统命令式 UI | React/Vue 前端框架 |
|---|---|---|---|
| 状态管理 | @State + @Observed 双层响应式 | 手动调用刷新方法 | useState/useState + Context |
| 组件声明 | struct + @Component | class/struct 继承 | function/class 组件 |
| UI 构建 | @Builder 声明式链式调用 | 命令式创建/布局/添加 | JSX/template 模板语法 |
| 布局系统 | Column/Row/Stack + layoutWeight | XML 布局 + LayoutParams | Flexbox/Grid + CSS |
| 动画系统 | .animation() 属性 + PlayMode | 属性动画/帧动画 | CSS Transition/Animation |
| 列表渲染 | ForEach + 键值生成函数 | RecyclerView/Adapter | map/filter + key 属性 |
| 条件渲染 | if-else 条件分支 | View.GONE/VISIBLE | 条件渲染 v-if/&& |
| 模态弹窗 | Stack 层叠 + zIndex + 条件渲染 | Dialog/BottomSheet | Portal/Modal 组件 |
| 类型安全 | TypeScript 静态类型 + 接口 | Java/Kotlin 强类型 | TypeScript/PropTypes |
| 渲染性能 | 编译时优化 + 虚拟 DOM diff | 直接操作原生视图 | 虚拟 DOM diff |
| 事件处理 | .onClick() 链式绑定 | setOnClickListener | onClick/on@事件 |
| 样式系统 | 链式属性设置 | XML 样式 + 代码修改 | CSS/内联样式 |
| 数据绑定 | @State 双向绑定 + @Observed | DataBinding/手动更新 | useState/useReducer |
| 组件复用 | @Builder 参数化复用 | 继承/组合 | HOC/Hooks/组合 |
上表从十四个技术维度对比了 ArkTS 声明式 UI 与传统命令式 UI(如 Android View 体系)和现代前端框架(React/Vue)的差异。ArkTS 的独特优势在于:它将 TypeScript 的类型安全性与声明式 UI 的开发效率相结合,通过 @State/@Observed 双层响应式系统实现了从组件状态到对象属性的全面追踪,通过 @Builder 实现了比组件继承更轻量的 UI 复用,通过链式属性设置提供了比 CSS 更直接的样式控制。与传统命令式 UI 相比,ArkTS 消除了大量的样板代码(如 findViewById、setOnClickListener);与 React/Vue 相比,ArkTS 的编译时优化带来了更好的运行时性能,无需虚拟 DOM 的运行时开销。
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
interface ColorPalette {
bg: string;
cardBg: string;
header1: string;
header2: string;
wood: string;
line: string;
stringC: string;
cinnabar: string;
ivory: string;
gold: string;
title: string;
sub: string;
text1: string;
text2: string;
text3: string;
accent: string;
hot: string;
cool: string;
danger: string;
tabBg: string;
tabOn: string;
mask: string;
}
const COLORS: ColorPalette = {
bg: '#F5F0E8',
cardBg: '#FFFFFF',
header1: '#3A1F14',
header2: '#1D0E08',
wood: '#7A4A2B',
line: '#C9A86A',
stringC: '#E8DFD2',
cinnabar: '#B33A2B',
ivory: '#F2EADB',
gold: '#D9A441',
title: '#F7E9D6',
sub: '#D0B392',
text1: '#3A1F14',
text2: '#6E5B4B',
text3: '#9C8F7A',
accent: '#7A4A2B',
hot: '#C0392B',
cool: '#1F5FA8',
danger: '#D9534F',
tabBg: '#3A1F14',
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 STRING_IDX: number[] = [0, 1, 2, 3];
const FRET_IDX: number[] = [0, 1, 2, 3, 4, 5];
const NOTE_IDX: number[] = [0, 1, 2];
@Observed
export class PipaItem {
name: string;
wood: string;
frets: number;
size: string;
price: number;
constructor(name: string, wood: string, frets: number, size: string, price: number) {
this.name = name;
this.wood = wood;
this.frets = frets;
this.size = size;
this.price = price;
}
}
@Observed
export class PipaStringItem {
name: string;
gauge: string;
tension: number;
material: string;
tone: string;
constructor(name: string, gauge: string, tension: number, material: string, tone: string) {
this.name = name;
this.gauge = gauge;
this.tension = tension;
this.material = material;
this.tone = tone;
}
}
@Observed
export class PipaScoreItem {
title: string;
genre: string;
measures: number;
era: string;
level: string;
constructor(title: string, genre: string, measures: number, era: string, level: string) {
this.title = title;
this.genre = genre;
this.measures = measures;
this.era = era;
this.level = level;
}
}
@Observed
export class PipaMasterItem {
name: string;
grade: string;
years: number;
pieces: number;
skill: number;
constructor(name: string, grade: string, years: number, pieces: number, skill: number) {
this.name = name;
this.grade = grade;
this.years = years;
this.pieces = pieces;
this.skill = skill;
}
}
@Observed
export class PipaSchoolItem {
name: string;
style: string;
members: number;
age: number;
hot: number;
constructor(name: string, style: string, members: number, age: number, hot: number) {
this.name = name;
this.style = style;
this.members = members;
this.age = age;
this.hot = hot;
}
}
@Observed
export class PipaShopItem {
name: string;
city: string;
crafts: number;
output: number;
level: string;
constructor(name: string, city: string, crafts: number, output: number, level: string) {
this.name = name;
this.city = city;
this.crafts = crafts;
this.output = output;
this.level = level;
}
}
@Observed
export class PipaSongItem {
title: string;
style: string;
beauty: number;
era: string;
note: string;
constructor(title: string, style: string, beauty: number, era: string, note: string) {
this.title = title;
this.style = style;
this.beauty = beauty;
this.era = era;
this.note = note;
}
}
@Observed
export class PipaOrderItem {
name: string;
buyer: string;
amount: number;
count: number;
date: string;
constructor(name: string, buyer: string, amount: number, count: number, date: string) {
this.name = name;
this.buyer = buyer;
this.amount = amount;
this.count = count;
this.date = date;
}
}
function stringX(i: number): number {
return 44 + i * 8;
}
function fretY(i: number): number {
return 44 + i * 9;
}
function noteX(i: number): number {
return 168 + i * 34;
}
function noteY(i: number): number {
return 20 + (i % 2) * 26;
}
function tensionBarW(t: number): number {
return 16 + Math.min(t, 100) * 1.35;
}
function tensionColor(t: number): string {
if (t >= 70) {
return COLORS.hot;
}
if (t >= 45) {
return COLORS.gold;
}
return COLORS.cool;
}
function skillBarW(s: number): number {
return 16 + Math.min(s, 100) * 1.35;
}
function hotBarW(hot: number): number {
return 18 + Math.min(hot, 100) * 1.35;
}
function beautyBarW(b: number): number {
return 16 + Math.min(b, 100) * 1.35;
}
function orderBarH(amount: number): number {
return 24 + Math.min(amount, 50000) / 500;
}
function gradeColor(grade: string): string {
if (grade.indexOf('大师') >= 0) {
return COLORS.hot;
}
if (grade.indexOf('名家') >= 0) {
return COLORS.gold;
}
if (grade.indexOf('名师') >= 0) {
return COLORS.accent;
}
return COLORS.cool;
}
function levelColor(level: string): string {
if (level.indexOf('武曲') >= 0) {
return COLORS.hot;
}
if (level.indexOf('文曲') >= 0) {
return COLORS.cool;
}
if (level.indexOf('大曲') >= 0) {
return COLORS.gold;
}
return COLORS.accent;
}
@Entry
@Component
struct PipaPage {
@State curTab: number = 0;
@State breath: boolean = false;
@State showAdd: boolean = false;
@State showEdit: boolean = false;
@State showDel: boolean = false;
@State selString: PipaStringItem | null = null;
@State selOrder: PipaOrderItem | null = null;
@State formName: string = '';
@State formWood: string = '';
@State formFrets: string = '';
@State formPrice: string = '';
@State pipas: PipaItem[] = [
new PipaItem('梨花木琵琶', '梨花木', 24, '四弦', 18800),
new PipaItem('紫檀琵琶', '小叶紫檀', 26, '四弦', 56000),
new PipaItem('红木演奏琵琶', '红木', 24, '四弦', 9800),
new PipaItem('白牛角轸琵琶', '老红木', 26, '四弦', 32000),
new PipaItem('六相十八品琵琶', '酸枝木', 24, '六相', 22000),
new PipaItem('少儿练习琵琶', '香樟木', 20, '四弦', 2600),
new PipaItem('檀木曲项琵琶', '檀木', 26, '曲项', 42000),
new PipaItem('竹节琵琶', '楠竹', 22, '四弦', 4800),
new PipaItem('螺钿镶嵌琵琶', '黄花梨', 26, '四弦', 68000),
new PipaItem('金漆描龙琵琶', '桐木', 24, '四弦', 15000),
new PipaItem('乌木直项琵琶', '乌木', 24, '直项', 36000),
new PipaItem('桐木古式琵琶', '桐木', 22, '四弦', 6800)
];
@State strings: PipaStringItem[] = [
new PipaStringItem('缠弦 · 一弦', '0.32mm', 82, '钢丝缠丝', '低沉浑厚'),
new PipaStringItem('老弦 · 二弦', '0.28mm', 74, '钢丝缠丝', '圆润厚重'),
new PipaStringItem('中弦 · 三弦', '0.24mm', 62, '钢丝', '明亮清越'),
new PipaStringItem('子弦 · 四弦', '0.20mm', 55, '钢丝', '清脆高亢'),
new PipaStringItem('丝弦 · 一弦', '0.30mm', 60, '蚕丝', '温润古朴'),
new PipaStringItem('丝弦 · 二弦', '0.26mm', 52, '蚕丝', '柔和高雅'),
new PipaStringItem('尼龙缠弦', '0.28mm', 58, '尼龙', '均衡耐用'),
new PipaStringItem('银缠弦', '0.30mm', 66, '银丝', '音色华丽')
];
@State scores: PipaScoreItem[] = [
new PipaScoreItem('十面埋伏', '武曲', 138, '明', '古谱'),
new PipaScoreItem('霸王卸甲', '武曲', 112, '明', '古谱'),
new PipaScoreItem('春江花月夜', '文曲', 96, '近代', '改编'),
new PipaScoreItem('阳春白雪', '文曲', 84, '清', '古谱'),
new PipaScoreItem('昭君出塞', '文曲', 76, '元', '古谱'),
new PipaScoreItem('海青拿天鹅', '武曲', 128, '元', '古谱'),
new PipaScoreItem('月儿高', '文曲', 88, '清', '古谱'),
new PipaScoreItem('彝族舞曲', '新作', 92, '当代', '创作')
];
@State masters: PipaMasterItem[] = [
new PipaMasterItem('刘德海', '国宝级大师', 60, 45, 99),
new PipaMasterItem('林石城', '国宝级大师', 58, 38, 97),
new PipaMasterItem('王范地', '国家级大师', 52, 30, 95),
new PipaMasterItem('吴玉霞', '国家级大师', 44, 26, 93),
new PipaMasterItem('章红艳', '名家', 36, 20, 90),
new PipaMasterItem('赵聪', '名家', 32, 18, 88),
new PipaMasterItem('方锦龙', '名家', 30, 16, 92),
new PipaMasterItem('杨靖', '名师', 28, 12, 86)
];
@State schools: PipaSchoolItem[] = [
new PipaSchoolItem('浦东派', '文套细腻', 60, 280, 94),
new PipaSchoolItem('平湖派', '武套刚健', 45, 260, 90),
new PipaSchoolItem('汪派', '刚柔并济', 80, 300, 96),
new PipaSchoolItem('崇明派', '清新雅致', 30, 220, 82),
new PipaSchoolItem('无锡派', '朴实无华', 25, 190, 76),
new PipaSchoolItem('大套曲派', '气势磅礴', 20, 240, 84),
new PipaSchoolItem('浏阳派', '乡土灵秀', 18, 170, 72),
new PipaSchoolItem('现代演奏派', '中西融合', 120, 150, 92)
];
@State shops: PipaShopItem[] = [
new PipaShopItem('平湖派琵琶坊', '浙江平湖', 12, 800, '甲级'),
new PipaShopItem('浦东派琵琶坊', '上海浦东', 10, 650, '甲级'),
new PipaShopItem('苏州琵琶社', '苏州', 14, 720, '甲级'),
new PipaShopItem('北京民族乐器厂', '北京', 16, 980, '甲级'),
new PipaShopItem('广州琵琶坊', '广州', 8, 520, '乙级'),
new PipaShopItem('西安琵琶坊', '西安', 6, 380, '乙级'),
new PipaShopItem('成都琵琶坊', '成都', 5, 300, '乙级'),
new PipaShopItem('扬州琵琶坊', '扬州', 4, 240, '丙级')
];
@State songs: PipaSongItem[] = [
new PipaSongItem('十面埋伏', '武曲', 98, '明', '金戈铁马'),
new PipaSongItem('春江花月夜', '文曲', 96, '近代', '江月清辉'),
new PipaSongItem('霸王卸甲', '武曲', 94, '明', '垓下悲歌'),
new PipaSongItem('阳春白雪', '文曲', 90, '清', '阳春和煦'),
new PipaSongItem('昭君出塞', '文曲', 88, '元', '塞外乡愁'),
new PipaSongItem('海青拿天鹅', '武曲', 92, '元', '海东青搏'),
new PipaSongItem('月儿高', '文曲', 86, '清', '月朗风清'),
new PipaSongItem('彝族舞曲', '新作', 91, '当代', '火把欢歌')
];
@State orders: PipaOrderItem[] = [
new PipaOrderItem('紫檀琵琶', '民族乐团', 56000, 2, '2026-08'),
new PipaOrderItem('红木演奏琵琶', '音乐学院', 88000, 9, '2026-07'),
new PipaOrderItem('少儿练习琵琶', '琴行连锁', 26000, 10, '2026-08'),
new PipaOrderItem('螺钿镶嵌琵琶', '收藏家', 68000, 1, '2026-06'),
new PipaOrderItem('金漆描龙琵琶', '文创公司', 30000, 2, '2026-07'),
new PipaOrderItem('梨花木琵琶', '私塾', 37600, 2, '2026-05'),
new PipaOrderItem('竹节琵琶', '民乐培训', 14400, 3, '2026-08'),
new PipaOrderItem('桐木古式琵琶', '剧场', 13600, 2, '2026-06'),
new PipaOrderItem('檀木曲项琵琶', '海外华侨', 42000, 1, '2026-04'),
new PipaOrderItem('乌木直项琵琶', '美术馆', 36000, 1, '2026-05')
];
aboutToAppear(): void {
setInterval(() => {
this.breath = !this.breath;
}, 500);
}
@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: 135,
colors: [[COLORS.header1, 0], [COLORS.header2, 1]]
})
Column()
.width(116)
.height(104)
.borderRadius(58)
.backgroundColor('#2B150D')
.border({ width: 2, color: COLORS.line })
.position({ x: 16, y: 16 })
Column()
.width(30)
.height(34)
.borderRadius({ topLeft: 6, topRight: 6, bottomLeft: 15, bottomRight: 15 })
.backgroundColor(COLORS.wood)
.position({ x: 58, y: 16 })
Column()
.width(10)
.height(26)
.borderRadius(5)
.backgroundColor(COLORS.ivory)
.position({ x: 68, y: 42 })
ForEach(FRET_IDX, (i: number) => {
Column()
.width(44 - i * 4)
.height(3)
.borderRadius(1.5)
.backgroundColor(COLORS.ivory)
.opacity(0.8)
.position({ x: 52 + i * 2, y: fretY(i) })
}, (i: number) => 'fr' + i)
ForEach(STRING_IDX, (i: number) => {
Column()
.width(1.5)
.height(78)
.backgroundColor(COLORS.stringC)
.opacity(this.breath ? 1 : 0.5)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: stringX(i), y: 46 })
}, (i: number) => 'st' + i)
Column()
.width(74)
.height(52)
.borderRadius({ topLeft: 26, topRight: 26, bottomLeft: 14, bottomRight: 14 })
.backgroundColor(COLORS.wood)
.border({ width: 1.5, color: COLORS.line })
.scale({ x: this.breath ? 1.03 : 1, y: this.breath ? 1.03 : 1 })
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: 34, y: 66 })
Column()
.width(14)
.height(14)
.borderRadius(7)
.backgroundColor(COLORS.cinnabar)
.position({ x: 88, y: 80 })
ForEach(NOTE_IDX, (i: number) => {
Text(i % 2 === 0 ? '♪' : '♫')
.fontSize(13)
.fontColor(COLORS.gold)
.translate({ y: this.breath ? noteY(i) : noteY(i) - 6 })
.opacity(this.breath ? 1 : 0.4)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
.position({ x: noteX(i), y: 24 })
}, (i: number) => 'nt' + i)
Column() {
Text('琵琶坊')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Text('大弦嘈嘈 · 小弦切切')
.fontSize(12)
.fontColor(COLORS.sub)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 158, y: 22 })
Column() {
Text('四弦 · 六相 · 二十四品')
.fontSize(12)
.fontColor(COLORS.gold)
Row() {
Text('武曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.hot)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text('文曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.cool)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.margin({ left: 8 })
Text('大曲')
.fontSize(11)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.gold)
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.position({ x: 158, y: 80 })
}
.width('100%')
.height(136)
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('琵琶款式')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('经典曲谱')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('名家乐师')
.fontSize(10)
.fontColor(COLORS.sub)
}
.layoutWeight(1)
Column() {
Text('8')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
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
pipaRow(item: PipaItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.wood + ' · ' + item.size)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('¥' + item.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(item.frets + ' 品')
.fontSize(10)
.fontColor(COLORS.gold)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('编')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.wood)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.showEdit = true;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
stringRow(item: PipaStringItem, i: number) {
Column() {
Row() {
Column()
.width(20)
.height(4)
.borderRadius(2)
.backgroundColor(COLORS.stringC)
.scale({ x: this.breath ? 1.4 : 1, y: this.breath ? 1.4 : 1 })
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.material + ' · ' + item.tone)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(item.tension + '%')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(tensionColor(item.tension))
Text(item.gauge)
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
.margin({ right: 10 })
Text('改')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.gold)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selString = item;
this.showEdit = true;
})
}
.width('100%')
Row() {
Text('张力')
.fontSize(10)
.fontColor(COLORS.text2)
Column()
.width(tensionBarW(item.tension))
.height(8)
.borderRadius(4)
.backgroundColor(tensionColor(item.tension))
.margin({ left: 8 })
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
scoreRow(item: PipaScoreItem, i: number) {
Row() {
Column() {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.text1)
Text(item.genre + ' · ' + item.era + ' · ' + item.level)
.fontSize(11)
.fontColor(levelColor(item.genre))
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.measures + ' 段')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('乐段')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
masterRow(item: PipaMasterItem, i: number) {
Column() {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.grade + ' · ' + item.years + ' 年')
.fontSize(11)
.fontColor(gradeColor(item.grade))
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.pieces + ' 曲')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('代表曲目')
.fontSize(10)
.fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('演奏技艺')
.fontSize(10)
.fontColor(COLORS.text2)
Column()
.width(skillBarW(item.skill))
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.cinnabar)
.margin({ left: 8 })
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text(item.skill + ' 分')
.fontSize(10)
.fontColor(COLORS.hot)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
schoolRow(item: PipaSchoolItem, i: number) {
Column() {
Row() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.style)
.fontSize(10)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.accent)
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.age + ' 年')
.fontSize(11)
.fontColor(COLORS.text3)
}
.width('100%')
Row() {
Text('传承热度')
.fontSize(10)
.fontColor(COLORS.text2)
Column()
.width(hotBarW(item.hot))
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.gold)
.margin({ left: 8 })
Text(item.members + ' 人')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.hot + ' 分')
.fontSize(10)
.fontColor(COLORS.hot)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
shopRow(item: PipaShopItem, i: number) {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.city + ' · ' + item.level)
.fontSize(11)
.fontColor(COLORS.text2)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(item.output + ' 把')
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(COLORS.cool)
Text(item.crafts + ' 位匠师')
.fontSize(11)
.fontColor(COLORS.text3)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
songRow(item: PipaSongItem, i: number) {
Column() {
Row() {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.style)
.fontSize(10)
.fontColor(COLORS.cardBg)
.backgroundColor(levelColor(item.style))
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.note)
.fontSize(11)
.fontColor(COLORS.text3)
}
.width('100%')
Row() {
Text('传世之美')
.fontSize(10)
.fontColor(COLORS.text2)
Column()
.width(beautyBarW(item.beauty))
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.hot)
.margin({ left: 8 })
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text(item.beauty + ' 分')
.fontSize(10)
.fontColor(COLORS.hot)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
orderRow(item: PipaOrderItem, i: number) {
Column() {
Row() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.text1)
Text(item.buyer)
.fontSize(10)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.cool)
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.margin({ left: 8 })
Column().layoutWeight(1)
Text(item.date)
.fontSize(11)
.fontColor(COLORS.text3)
}
.width('100%')
Row() {
Text('金额 ¥' + item.amount)
.fontSize(11)
.fontColor(COLORS.text2)
Column()
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.hot)
.margin({ left: 8 })
.opacity(this.breath ? 1 : 0.6)
.animation({ duration: 500, iterations: -1, playMode: PlayMode.Alternate })
Column().layoutWeight(1)
Text(item.count + ' 把')
.fontSize(10)
.fontColor(COLORS.text3)
.margin({ right: 8 })
Text('删')
.fontSize(12)
.fontColor(COLORS.cardBg)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.selOrder = item;
this.showDel = true;
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ bottom: 8 })
}
@Builder
tabContent() {
if (this.curTab === 0) {
Column() {
this.sectionTitle('🎻', '传世琵琶', '共 ' + this.pipas.length + ' 款')
ForEach(this.pipas, (item: PipaItem, i: number) => {
this.pipaRow(item, i)
}, (item: PipaItem) => item.name)
}
.width('100%')
} else if (this.curTab === 1) {
Column() {
this.sectionTitle('🎼', '琴弦工艺', '四弦一柱')
ForEach(this.strings, (item: PipaStringItem, i: number) => {
this.stringRow(item, i)
}, (item: PipaStringItem) => item.name)
}
.width('100%')
} else if (this.curTab === 2) {
Column() {
this.sectionTitle('📜', '传世曲谱', '减字谱 · 工尺谱')
ForEach(this.scores, (item: PipaScoreItem, i: number) => {
this.scoreRow(item, i)
}, (item: PipaScoreItem) => item.title)
}
.width('100%')
} else if (this.curTab === 3) {
Column() {
this.sectionTitle('🎭', '琵琶名家', '技艺传承')
ForEach(this.masters, (item: PipaMasterItem, i: number) => {
this.masterRow(item, i)
}, (item: PipaMasterItem) => item.name)
}
.width('100%')
} else if (this.curTab === 4) {
Column() {
this.sectionTitle('🧩', '演奏流派', '派别林立')
ForEach(this.schools, (item: PipaSchoolItem, i: number) => {
this.schoolRow(item, i)
}, (item: PipaSchoolItem) => item.name)
}
.width('100%')
} else if (this.curTab === 5) {
Column() {
this.sectionTitle('🏭', '制琴工坊', '选料 · 挖膛 · 蒙皮')
ForEach(this.shops, (item: PipaShopItem, i: number) => {
this.shopRow(item, i)
}, (item: PipaShopItem) => item.name)
}
.width('100%')
} else if (this.curTab === 6) {
Column() {
this.sectionTitle('🎵', '千古名曲', '文曲武曲')
ForEach(this.songs, (item: PipaSongItem, i: number) => {
this.songRow(item, i)
}, (item: PipaSongItem) => item.title)
}
.width('100%')
} else {
Column() {
this.sectionTitle('📦', '最新订单', '共 ' + this.orders.length + ' 单')
ForEach(this.orders, (item: PipaOrderItem, i: number) => {
this.orderRow(item, i)
}, (item: PipaOrderItem) => item.name)
}
.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.gold : COLORS.sub)
.margin({ top: 2 })
}
.width('25%')
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === i ? '#4A2A18' : 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.formName, placeholder: '如:桐木练习琵琶' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formName = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 14 })
Column() {
Text('木料')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formWood, placeholder: '如:红木' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formWood = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Column() {
Text('品数')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formFrets, placeholder: '如:24' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formFrets = v;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.margin({ top: 12 })
Column() {
Text('售价')
.fontSize(12)
.fontColor(COLORS.text2)
TextInput({ text: this.formPrice, placeholder: '如:6800' })
.height(38)
.fontSize(13)
.margin({ top: 5 })
.onChange((v: string) => {
this.formPrice = 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('#EFE9DD')
.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.pipas.splice(0, 0, new PipaItem(this.formName, this.formWood, 24, '四弦', 6800));
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 ? '—' : (this.selString.tension + '%'))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(tensionColor(this.selString === null ? 50 : 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.hot)
.borderRadius(10)
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = this.selString.tension + 5;
}
this.showEdit = false;
})
Text('松弦 -5%')
.fontSize(13)
.fontColor(COLORS.cardBg)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.cool)
.borderRadius(10)
.margin({ left: 10 })
.onClick(() => {
if (this.selString !== null) {
this.selString.tension = this.selString.tension - 5;
}
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('#EFE9DD')
.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.selOrder === null ? '—' : (this.selOrder.name + ' · ' + this.selOrder.buyer))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.backgroundColor('#FAEDEC')
.borderRadius(12)
.margin({ top: 14 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor(COLORS.text2)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor('#EFE9DD')
.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.selOrder !== null) {
this.orders.splice(this.orders.indexOf(this.selOrder), 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)
}
}
三十四、结尾总结

通过对这份琵琶制作工坊应用的完整源码逐段解析,我们可以提炼出以下核心技术要点和工程实践启示。
第一,状态驱动渲染是 ArkTS 的核心理念。整个应用的 UI 更新完全由状态变量驱动——curTab 驱动标签页切换,showAdd/showEdit/showDel 驱动弹窗显隐,breath 驱动全局动画,selString/selOrder 驱动选中项操作。开发者只需维护状态数据的正确性,框架自动处理所有 UI 更新。这种"状态即真理"的设计哲学大幅降低了 UI 同步的复杂度,使开发者能够专注于业务逻辑而非视图刷新。在实际项目中,应始终遵循"先修改状态、后由框架驱动 UI"的模式,避免直接操作 UI 元素。
第二,@State 与 @Observed 的双层响应式覆盖了从数组到属性的全粒度变更。@State 修饰数组变量追踪数组增删(splice 操作),@Observed 修饰数组元素的类追踪实例属性修改(如 tension 的加减)。两者分别管理"数组级"和"属性级"两个粒度的响应式更新,形成了完整的响应式覆盖。这种分层设计确保了无论数据变化发生在哪个层级,UI 都能精确响应,无需全量重新渲染。在实际开发中,应根据数据的变更粒度选择合适的装饰器——数组增删用 @State,对象属性修改用 @Observed。
第三,@Builder 的参数化复用是 UI 代码组织的最佳实践。本应用中,modalOverlay 通过 onClose 参数实现三处复用,sectionTitle 通过 icon/title/sub 参数实现八处复用,八个列表行 Builder 通过 item/i 参数实现 ForEach 内的循环复用。这种"定义一次、多处调用、参数差异化"的模式,既保证了视觉一致性又实现了行为灵活性,比传统的组件继承更轻量、更灵活。在实际项目中,应将重复的 UI 片段抽取为 @Builder,通过参数传入差异化数据和回调函数。
第四,纯代码绘制复杂图形是 ArkTS 的独特能力。pageHeader 中的琵琶图案完全通过 Column、Text 等基本组件配合 position 绝对定位和 borderRadius 非对称圆角绘制而成,无需任何图片资源。这种"代码即图形"的方式不仅减少了资源文件依赖,还使图形元素可以参与动画和状态驱动——琴弦可以脉动、面板可以呼吸、音符可以飘移。位置计算函数(stringX、fretY 等)的抽取使布局逻辑清晰可维护。在实际项目中,对于需要动态效果的装饰性图形,应优先考虑纯代码绘制而非静态图片。
第五,单一状态驱动多元素同步动画是高效的动画设计模式。breath 布尔变量以 500 毫秒为周期翻转,同时驱动了透明度、缩放、位移三种类型的动画效果,覆盖了琴弦、面板、音符、弦条、进度条等多种 UI 元素。所有动画使用统一的时间参数(duration: 500, iterations: -1, playMode: Alternate),确保了动画的同步性和连续性。这种设计模式的精髓在于"一因多果"——一个状态变化同时影响多个 UI 元素,框架自动处理每个元素的动画过渡。在实际项目中,对于需要多元素同步动画的场景,应优先考虑单一状态驱动模式。
第六,防御性编程在联合类型处理中至关重要。selString 和 selOrder 的类型为 PipaStringItem | null 和 PipaOrderItem | null,所有访问都通过三元表达式进行空值保护。这种"先判空、后访问"的模式确保了在未选中任何项时不会抛出空指针异常。在实际项目中,对于可能为 null 的状态变量,应始终进行空值检查,避免运行时崩溃。
第七,色彩体系的接口化管理确保了视觉一致性。ColorPalette 接口定义了二十二个语义化颜色字段,COLORS 常量提供了具体的色值实现。所有组件统一引用 COLORS 常量,不存在散落在代码各处的魔法色值。这种"单一数据源"的色彩管理方式使换肤、主题适配等需求只需修改一处即可全局生效。在实际项目中,应始终通过接口或常量对象统一定义色彩体系,禁止在组件中直接使用字面色值。
第八,进度条的颜色编码体系提升了信息传递效率。tensionColor、gradeColor、levelColor 三个颜色映射函数将业务数值和关键词映射为语义化颜色——高张力红色、低张力蓝色、大师红色、武曲红色等。用户通过颜色即可快速判断信息类别和级别,无需逐个阅读文字。这种"颜色即信息"的设计在数据密集型列表中尤为重要。在实际项目中,对于需要分类或分级展示的数据,应建立统一的颜色编码体系并贯穿整个应用。
第九,模态弹窗的统一架构保证了交互一致性。三个弹窗(addModal、editModal、delModal)共享相同的架构模式——Stack 层叠、modalOverlay 遮罩、白色圆角主体、底部操作按钮。差异仅在于内容区域和按钮逻辑。这种"统一架构+差异化内容"的设计使弹窗的视觉风格完全一致,用户在不同弹窗中的操作体验无缝衔接。在实际项目中,应建立统一的弹窗架构模板,所有弹窗遵循相同的视觉规范和交互模式。
第十,ForEach 的键值生成函数是列表渲染性能的关键。每个 ForEach 都提供了基于数据唯一属性的键值生成函数(如 item.name、item.title),确保框架在数据变化时能高效地 diff 出变化项,仅更新受影响的元素。在实际项目中,应始终为 ForEach 提供稳定且唯一的键值,避免使用数组索引作为键值(因为索引在增删操作时会变化,导致框架无法正确识别变化项)。
更多推荐




所有评论(0)