引言:布虎工艺的历史文化背景

在这里插入图片描述

布老虎,这一源远流长的民间手工艺瑰宝,扎根于中华大地的黄土之上,承载着数千年的农耕文明与民俗信仰。追溯其源,布虎工艺的萌芽可上溯至远古时期的虎图腾崇拜。在远古先民的认知体系中,虎为百兽之长,是力量、勇猛与威严的化身。《山海经》中记载的西王母形象,便以"豹尾虎齿"的样貌示人,折射出虎在原始信仰中的至高地位。这种对虎的崇拜并非凭空而来——在先民面对的自然环境中,猛虎出没于山林之间,既是威胁也是庇护的象征,由此衍生出以虎驱邪、以虎纳福的深厚民俗传统。

布老虎作为实体工艺品的出现,与古代的端午节俗、婴孩护佑习俗密不可分。古人认为农历五月为"毒月",五毒出没、疫病易发,而虎为阳刚之兽,能吞噬鬼魅、驱除邪祟。于是,母亲们用碎布缝制成小虎,内填艾草、菖蒲或棉花,挂在孩童胸前或枕边,寄托驱邪避害、护佑平安的朴素愿望。这种习俗在中国北方尤为兴盛,山西、陕西、河南、山东、甘肃等地各有独特的布虎流派,形成了蔚为大观的布虎文化版图。

在众多布虎产地中,山西黎城的"黎侯虎"声名最为显赫。黎侯虎以威武雄壮的造型、鲜艳浓烈的色彩和精巧细腻的缝制工艺著称,其历史可追溯至商周时期的黎国。据传,西周时期的黎侯国以虎为图腾,民间便有缝制布虎的习俗,世代相传,绵延不绝。一九九八年,黎侯虎被邮电部选为虎年生肖邮票图案,从此声名远播。二零零八年,黎侯虎被列入国家级非物质文化遗产名录,成为国家认定的民间工艺珍品。黎侯虎的造型特征鲜明:虎头浑圆饱满,双目炯炯有神,额头正中绣一"王"字,身躯敦实厚重,四肢粗壮有力,尾部上翘卷曲,整体给人以稳重而不失灵动的视觉感受。

河南淮阳的布虎则别具一格。淮阳太昊陵为伏羲陵庙所在地,每年农历二月的庙会上,各种泥泥狗、布老虎琳琅满目。淮阳布虎保留着浓厚的原始宗教色彩,造型古朴粗犷,色彩对比强烈,带有明显的远古图腾遗韵。与黎侯虎的写实风格不同,淮阳布虎更具抽象性和象征性,其夸张的头部比例、简化的四肢造型,传达的是一种神秘而庄重的宗教情感。

陕西凤翔的布虎同样名闻遐迩。凤翔自古便是西府重镇,民间工艺发达,泥塑、布艺、刺绣皆有深厚传统。凤翔布虎融入了当地彩绘艺术的精髓,色彩明快热烈,纹样繁复绚丽,常以缠枝纹、卷云纹、如意纹等传统吉祥纹样装饰虎身,使得每一只布虎都成为一件色彩斑斓的艺术品。凤翔布虎的另一个特色是注重实用性,除传统的镇宅虎外,还有虎头枕、虎头帽、虎头鞋等衍生品类,将装饰与实用完美结合。

除了上述三大流派外,山东高密、河北定州、甘肃庆阳、湖南湘西等地的布虎工艺也各有千秋。山东高密的布虎以朴拙浑厚见长,造型简练,色彩以红黄为主调,洋溢着齐鲁大地的豪迈气概。甘肃庆阳的布虎则带有浓郁的西北黄土高原气息,造型粗犷,常以夸张的大头、圆眼和竖耳塑造形象,色彩沉稳,纹样古朴,与当地的剪纸、刺绣艺术一脉相承。湖南湘西的布虎则融入了少数民族的审美趣味,色彩更为艳丽大胆,纹样中常有蝴蝶、石榴、葡萄等多子多福的吉祥元素。

布虎工艺的制作流程本身就是一门精深的学问。从选布、裁样到填絮、缝身,从绣纹、点睛到安耳、挂须,每一道工序都有严格的规范和独到的技巧。老艺人讲究"虎要有神",关键在于眼睛的刺绣——用打籽绣法绣出的虎眼,立体凸起,在光线下微微闪光,仿佛真的有了生命。而额头上的"王"字,不仅是老虎的标识,更象征着百兽之王的权威与力量。虎须用白色绒线粘附,虎耳以暗针缝制,力求不露针脚,浑然天成。最后系上铜铃和红绳,一只活灵活现的布虎才算完成。

布虎的色彩体系也深含寓意。黄色象征土地与丰收,红色代表喜庆与吉祥,蓝色寓意天空与安宁,绿色象征生命与希望。这些色彩的大胆组合,既体现了民间艺术"大红大绿"的审美偏好,又暗合了五行相生相克的哲学理念。布虎身上的每一朵云纹、每一枚铜钱、每一只蝴蝶,都不是随意为之的装饰,而是承载着祈雨、招财、纳福等具体的文化寓意。可以说,一只布虎就是一个浓缩的民俗文化符号系统,是千百年来劳动人民智慧的结晶。

进入现代社会后,布虎工艺面临着传承与发展的双重挑战。一方面,老一辈手艺人年事已高,年轻人多外出务工,传统技艺面临断代危机。另一方面,工业化生产的廉价仿制品冲击着手工作品的市场空间。但与此同时,非物质文化遗产保护运动的兴起、文化创意产业的蓬勃发展,也为布虎工艺注入了新的生机。越来越多的设计者和开发者开始用数字技术记录、展示和传播布虎文化,让这一古老的手艺在数字时代焕发出新的光彩。

正是在这样的文化背景和技术趋势下,本文所分析的 HarmonyOS ArkTS 应用应运而生。这是一款以布虎工艺为主题的文化展示与管理应用,它将布虎的款式、纹样、针法、绣娘、作坊、工序、藏品和订单八大维度整合于一体,通过精美的界面设计、流畅的交互动效和完整的数据管理功能,在方寸屏幕之间呈现了布虎工艺的丰富内涵。从技术角度来看,该应用充分运用了 ArkTS 的状态管理机制(@State、@Observed)、声明式 UI 范式(@Component、@Builder)、以及丰富的布局和动画能力,堪称 HarmonyOS 应用开发的优秀范例。接下来,让我们逐段深入剖析这份代码的技术细节。


一、色彩体系:ColorPalette 接口与 COLORS 常量

在这里插入图片描述

1.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;
  cardBg: string;
  header1: string;
  header2: string;
  tigerY: string;
  tigerR: string;
  clothB: string;
  clothG: string;
  stripe: 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 接口定义了整个应用所需的所有色彩属性。这是一个纯粹的结构化类型定义,不包含任何方法,只声明了二十三个字符串类型的属性。每个属性名都经过精心设计,具有明确的语义指向:bg 表示页面背景色,cardBg 表示卡片背景色,header1 和 header2 表示标题栏的渐变色彩,tigerY 和 tigerR 分别代表虎身的黄色和红色,clothB 和 clothG 代表布料的蓝色和绿色,stripe 代表条纹色,gold 代表金色,title 和 sub 代表标题文字色和副标题色,text1 至 text3 代表三级正文文字色,accent 代表强调色,hot 和 cool 分别代表热烈色和冷色调,danger 代表危险警告色,tabBg 和 tabOn 分别代表标签栏背景色和选中态色彩,mask 代表遮罩层半透明色。

这种将所有色彩集中定义在一个接口中的设计方式,在软件工程实践中被称为"集中式色彩管理"模式。其核心优势在于统一管理、一致引用和便于维护。试想,如果二十多种色彩散落在数百行代码中,一旦需要调整某一种色彩,开发者将不得不在整个代码库中搜索替换,既耗时又容易遗漏。而采用集中式定义后,只需修改 COLORS 常量中的对应字段,所有引用该色彩的位置都会自动更新,大幅降低了维护成本。

从类型安全的角度来看,ColorPalette 接口为色彩值提供了编译期类型检查。虽然每个属性的类型都是 string,但接口本身的约束意味着,任何试图访问 COLORS 中不存在的属性都会在编译时报错。这有效地防止了因属性名拼写错误或记忆偏差导致的运行时色彩缺失问题。在大型项目中,这种类型保障尤为宝贵——它让色彩系统成为一个自描述的、可被 IDE 自动补全的领域模型,而非一组松散的字符串常量。

从设计美学的角度来看,ColorPalette 接口中定义的二十三个色彩属性,实际上构成了一个完整的视觉设计系统。这些色彩涵盖了背景层、卡片层、头部层、虎身层、布料层、装饰层、文字层、状态层和交互层等多个视觉层次,体现了层次分明、分工清晰的色彩规划理念。这种多层次的色彩架构,使得界面在视觉呈现上既有整体统一性,又有细节层次感,避免了色彩单调乏味或杂乱无章两种极端。

1.2 COLORS 常量实例

在这里插入图片描述

const COLORS: ColorPalette = {
  bg: '#FAF3E4',
  cardBg: '#FFFFFF',
  header1: '#7A1F1F',
  header2: '#3A0E0E',
  tigerY: '#F0A830',
  tigerR: '#C0392B',
  clothB: '#2E6DB4',
  clothG: '#3E8E5A',
  stripe: '#7A4A2B',
  gold: '#D9A441',
  title: '#FDEBD0',
  sub: '#E5B87A',
  text1: '#5A2A1E',
  text2: '#7A5A4A',
  text3: '#A08F7A',
  accent: '#C0392B',
  hot: '#C0392B',
  cool: '#2E6DB4',
  danger: '#D9534F',
  tabBg: '#7A1F1F',
  tabOn: '#F0A830',
  mask: 'rgba(0,0,0,0.45)'
};

COLORS 常量是 ColorPalette 接口的唯一实例,它为每一个色彩属性赋予了具体的色值。让我们逐一审视这些色值的选择及其文化内涵。

页面背景色 bg 设定为 ‘#FAF3E4’,这是一种温暖柔和的米黄色调。它不是纯白也不是纯黄,而是类似于宣纸或老棉布的自然色泽,给人以古朴温馨、岁月沉淀之感。这种背景色的选择,巧妙地呼应了布虎工艺作为传统手工艺的朴素气质,让用户一进入应用就感受到传统文化的氛围。从视觉舒适度的角度来说,米黄色背景对人眼更为友好,长时间观看不易疲劳,这在移动应用的色彩设计中是一个值得称道的考量。

卡片背景色 cardBg 使用纯白色 ‘#FFFFFF’,与米黄色的页面背景形成微妙但清晰的对比。白色卡片悬浮于米黄背景之上,犹如展台上的陈列品,既突出了内容区域,又保持了整体的柔和调性。这种"浅底浮白卡"的配色策略在信息密集型应用中非常常见,它能有效区分内容区域,提高信息可读性,同时不会因为色彩过于跳跃而分散用户的注意力。

标题栏的色彩设计尤为精彩。header1 设为 ‘#7A1F1F’(深酒红),header2 设为 ‘#3A0E0E’(近黑红),两者组合成 135 度的线性渐变,从左上到右下由深红渐变为近黑。这种深沉浓烈的暗红渐变,让人联想到传统漆器的质感,又带有庄严厚重的仪式感,非常契合布虎作为镇宅辟邪之物的文化定位。渐变方向选为 135 度,使光感从左上方倾泻而下,符合自然光源的习惯方向,给人以沉稳而不沉闷的视觉效果。

虎身的主色调 tigerY 选取了 ‘#F0A830’——一种明亮的暖橙黄色。这个色彩既不是标准的老虎黄,也不是纯橙色,而是经过调和的暖黄色调,带有一种阳光照耀下的温暖感。在民间布虎的实际制作中,虎身常用的黄色棉布或绸缎往往就是这种偏暖的黄色,所以这一色值的选择具有相当的真实感。与之配搭的 tigerR 为 ‘#C0392B’,这是一种饱和度较高的朱红色,用于虎身的红色部分和"王"字纹样,代表着喜庆与辟邪的双重含义。

布料色 clothB 和 clothG 分别对应蓝色和绿色两种常见的布料颜色。蓝色沉稳深邃,常用于虎身底布或装饰条纹;绿色清新自然,常用于虎须或装饰性色块。这两种色彩的加入,丰富了布虎的色彩谱系,使其不再局限于红黄二色,而呈现出更多元的色彩层次。值得注意的是,cool 属性也设为与 clothB 相同的蓝色值,这说明设计者有意将"冷色调"语义与布料蓝色绑定,形成了一致的视觉语义映射。

金色 gold 定为 ‘#D9A441’,这是一种偏暗的哑金色,而非耀眼的亮金色。哑金色在视觉上更为含蓄内敛,与中华传统美学中"温润如玉"的审美追求一脉相承。它用于虎身描金、装饰圆点以及标签选中态等场景,起到了画龙点睛的点缀作用,既增添了华贵感,又不会喧宾夺主。

文字色彩的设计同样层次分明。title 为奶白偏黄色,用于深色背景上的标题文字,确保高对比度可读性;sub 为暖棕黄色,用于副标题,在深色背景上略显柔和但仍清晰可辨。三级正文文字色 text1(深褐红)、text2(暖灰棕)、text3(浅暖灰)形成由深到浅的梯度,分别用于主要文字、次要文字和辅助说明文字,构建了清晰的文字层级体系。这种三级文字色梯度是移动端设计中的标准做法,它使得信息层次一目了然——最重要的信息用最深色,次要信息用中间色,辅助信息用最浅色。

状态色方面,accent 和 hot 都设为与 tigerR 相同的朱红色,强调了红色作为应用主强调色的地位。danger 设为偏亮的警示红,与 accent 红色有所区分,专用于删除等危险操作。tabBg 设为与 header1 相同的深酒红,tabOn 设为与 tigerY 相同的虎黄,确保了标签栏与整体色彩风格的一致性。最后,mask 使用 rgba(0,0,0,0.45) 的半透明黑色,用于模态对话框的遮罩层,45% 的不透明度既能有效遮盖背景内容,又不会完全遮挡,保持了适当的空间层次感。

纵观整个色彩体系,我们可以清晰地看到一条贯穿始终的文化映射主线:以传统民俗色彩为基调,以现代移动端设计规范为骨架。米黄背景对应老棉布,深红标题对应传统漆器与红木家具,暖橙虎黄对应虎身布料,朱红对应喜庆辟邪,哑金对应描金装饰——每一种色彩都不是随机选取的,而是经过深思熟虑的文化编码。同时,三级文字色的梯度设置、状态色的语义区分、遮罩层的透明度控制等,又完全遵循了现代移动应用 UI 设计的最佳实践。这种"传统为体、现代为用"的设计哲学,使得该应用在视觉上既有浓郁的文化韵味,又具备优秀的使用体验,堪称文化类应用色彩设计的典范。


二、标签系统:TabMeta 接口与 TAB_LIST 常量

在这里插入图片描述

2.1 TabMeta 接口定义

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

TabMeta 接口定义了标签元数据的结构,极为简洁明了。它只包含两个属性:label 是标签的文字标签,icon 是标签的图标。这种将文字和图标组合在一个数据结构中的设计方式,是移动端底部导航栏的常见做法。相比将文字和图标分开管理,合并定义的优势在于保证了一一对应的完整性——每一个标签的图标和文字始终绑定在一起,不会出现错配或遗漏的情况。

从类型设计的角度来看,TabMeta 虽然简单,却体现了"数据驱动 UI"的核心思想。标签的内容不硬编码在 UI 组件内部,而是以数据的形式存在,由 UI 组件在运行时读取并渲染。这意味着如果要增删标签,只需修改数据列表,而不需要改动 UI 逻辑,实现了数据与视图的解耦。在需要频繁调整导航结构的迭代开发中,这种设计大大降低了修改成本和出错概率。

2.2 TAB_LIST 常量定义

在这里插入图片描述

const TAB_LIST: TabMeta[] = [
  { label: '布虎', icon: '🐯' },
  { label: '纹样', icon: '🎨' },
  { label: '针线', icon: '🧵' },
  { label: '绣娘', icon: '👩‍🎨' },
  { label: '作坊', icon: '🏠' },
  { label: '工序', icon: '📋' },
  { label: '藏品', icon: '💎' },
  { label: '订单', icon: '📦' }
];

TAB_LIST 是一个包含八个 TabMeta 元素的数组,定义了应用的八大功能模块。让我们逐一分析每个标签的内涵及其在整体架构中的角色。

第一个标签"布虎",对应布虎款式展示模块。这是应用的核心内容模块,展示各种形制的布虎成品,包括镇宅大布虎、端午艾虎、双头虎枕、虎头帽、虎头鞋等。虎头图标直观地传达了模块主题,用户一眼即可识别这是与布虎本体相关的内容。这一模块是整个应用的"门面",用户进入应用后默认看到的就是布虎列表。

第二个标签"纹样",对应吉祥纹样展示模块。布虎身上的纹样是其文化内涵的重要载体,王字额纹、卷云纹、如意纹、铜钱纹等各有寓意。调色板图标恰当地表达了纹样设计与色彩搭配的含义。这一模块从装饰艺术的角度深入布虎文化,展示了纹样的名称、使用位置、吉祥度、地域来源和寓意。

第三个标签"针线",对应针法针线展示模块。平针绣、回针绣、锁边绣、打籽绣、盘金绣等不同针法,是布虎制作工艺的精髓所在。线轴图标形象地代表了缝纫刺绣所需的针线工具。这一模块从制作技法的角度展示布虎工艺,介绍了各种针法的名称、线材、技艺要求和用途。

第四个标签"绣娘",对应绣娘名录展示模块。这一模块展示布虎工艺的传承者——各位绣娘的姓名、等级、从业年限和技艺水平。女艺术家图标体现了以女性为主体的绣娘群体特征,也彰显了对匠人个体的尊重。这一模块将"人"置于与"物"同等重要的位置,体现了对匠人精神的尊重和传承。

第五个标签"作坊",对应布虎作坊展示模块。黎侯虎作坊、淮阳布虎坊、凤翔布艺坊等各地的布虎制作作坊,代表着不同地域的工艺流派。房屋图标象征了作坊这一实体空间。这一模块从产业组织的角度展示布虎工艺的地理分布和规模。

第六个标签"工序",对应制作工序展示模块。从裁样到系铃的八道工序,完整呈现了布虎的制作流程。剪贴板图标暗示了流程清单和工序步骤的含义。这一模块从工艺流程的角度展示布虎从原材料到成品的完整制作过程。

第七个标签"藏品",对应传世藏品展示模块。清代黎侯布虎、明代刺绣虎枕等历史藏品,展现了布虎工艺的悠久传承。钻石图标象征着珍贵稀有和价值连城。这一模块从历史收藏的角度展示布虎工艺的年代传承和文化价值。

第八个标签"订单",对应订单管理模块。这一模块更具商业属性,展示布虎的订单信息,包括买家、金额、数量和日期,体现了布虎工艺在现代社会中的商业价值。包裹图标直观地代表了商品订单和物流配送。这一模块从商业运营的角度展示布虎在当代社会中的市场表现。

这八个标签从布虎本体出发,依次展开纹样装饰、制作技法、匠人传承、地域分布、工艺流程、历史收藏到商业订单的完整链条,构成了一个从产品到工艺、从人到物、从历史到现实的立体化信息体系。这种"以物为核心、以人为纽带、以流程为线索、以历史为根基、以商业为延展"的架构设计,使得应用既有文化深度,又有实用价值,兼顾了展示性、知识性和功能性三重目标。

2.3 辅助索引数组

在这里插入图片描述

const ROW1_IDX: number[] = [0, 1, 2, 3];
const ROW2_IDX: number[] = [4, 5, 6, 7];
const TASSEL_IDX: number[] = [0, 1, 2, 3, 4, 5];
const STIR_IDX: number[] = [0, 1, 2, 3];

这四个常量数组是辅助性的索引工具,虽不起眼但发挥着重要的桥梁作用。

ROW1_IDX 和 ROW2_IDX 将八个标签的索引分为两组,每组四个,用于底部导航栏的两行布局。将一行四个标签改为两行各四个标签,是出于移动端屏幕宽度的考量——在一行中放置八个标签会导致每个标签过窄,文字和图标都难以清晰显示。分成两行后,每个标签拥有足够的展示空间,同时也保留了更多的视觉呼吸感。

TASSEL_IDX 和 STIR_IDX 分别用于头部虎头装饰中的虎须和条纹的循环渲染。TASSEL_IDX 有六个元素,对应六根虎须;STIR_IDX 有四个元素,对应四条条纹。这些索引数组虽然内容简单,但它们将视觉装饰元素的数量和排列以数据化的方式固定下来,体现了"数据驱动渲染"的思维——改变虎须数量只需修改 TASSEL_IDX 的元素个数,而不需要修改循环的内部逻辑。这种将"数量"作为数据而非硬编码在逻辑中的做法,提高了代码的灵活性和可维护性。


三、数据模型:@Observed 类的逐一分析

在这里插入图片描述

3.1 ClothTigerItem 布虎款式数据模型

@Observed
export class ClothTigerItem {
  name: string;
  style: string;
  size: string;
  color: string;
  price: number;

  constructor(name: string, style: string, size: string, color: string, price: number) {
    this.name = name;
    this.style = style;
    this.size = size;
    this.color = color;
    this.price = price;
  }
}

ClothTigerItem 是布虎款式数据的核心模型类,它承载着每一款布虎的基本信息。类上方的 @Observed 装饰器是 ArkTS 框架的关键特性之一——它将一个普通的 TypeScript 类转变为"可观察对象",使其属性的变化能够被框架自动追踪并触发相关 UI 的刷新。这一机制是 ArkTS 响应式编程体系的基础,也是与传统命令式 UI 编程的根本区别所在。

让我们逐一审视 ClothTigerItem 的五个属性。name 属性存储布虎的名称,如"镇宅大布虎"“端午艾虎"等,这些名称融合了功能描述和文化寓意,既是商品的标识,也是文化符号的浓缩。style 属性记录布虎的款式分类,如"坐虎”“艾虎”“虎枕"等,不同的款式对应不同的使用场景和文化功能——坐虎用于镇宅,艾虎用于端午,虎枕兼具实用与装饰功能。size 属性记录布虎的尺寸规格,如"高 45cm”“头围 50cm"等,便于用户了解产品的实际大小。color 属性描述布虎的主色调,如"金黄”“艾绿”"红黄"等,这些色彩描述既具有实用信息价值,又呼应了前文所述的色彩文化内涵。price 属性是数值类型,记录布虎的售价。

构造函数的设计体现了 TypeScript 类的标准范式。五个参数与五个属性一一对应,在构造函数体中通过 this 赋值完成初始化。这种显式的赋值方式虽然看起来较为冗长,但它的优势在于绝对清晰:每个属性如何被赋值、赋值的来源是什么,一目了然,没有任何隐含行为。

@Observed 装饰器的深层意义在于,它使得 ClothTigerItem 实例的属性修改能够被 ArkTS 的响应式系统捕获。当用户在编辑模态框中修改了某个布虎的信息后,对应的列表项会自动刷新显示最新数据,无需开发者手动调用任何刷新方法。这种"数据变则视图变"的自动同步机制,极大地简化了状态管理的复杂度,让开发者可以专注于业务逻辑而非 UI 刷新的细节。在传统的命令式 UI 编程中,修改数据后需要手动找到对应的 UI 元素并更新其内容,代码冗长且容易出错;而 @Observed 机制将这一过程完全自动化,是 ArkTS 开发效率的重要保障。

3.2 PatternItem 吉祥纹样数据模型

@Observed
export class PatternItem {
  name: string;
  use: string;
  charm: number;
  origin: string;
  luck: string;

  constructor(name: string, use: string, charm: number, origin: string, luck: string) {
    this.name = name;
    this.use = use;
    this.charm = charm;
    this.origin = origin;
    this.luck = luck;
  }
}

PatternItem 模型类代表布虎身上的吉祥纹样信息。与 ClothTigerItem 侧重商业信息不同,PatternItem 更侧重文化内涵的表达,其属性设计体现了纹样的多维度文化属性。

name 属性记录纹样的名称,如"王字额纹"“卷云纹”“如意纹"等。这些名称本身就是中华传统装饰艺术的术语,承载着丰富的文化编码。use 属性记录纹样在布虎身上的使用位置,如"额头”“身侧”“胸前"等,表明不同纹样有特定的装饰部位,这反映了民间工艺中"纹必有位、位必有义"的讲究。charm 属性是一个数值,代表纹样的"吉祥度”——这是一个量化指标,将纹样的文化寓意程度以百分比的形式呈现,使得抽象的文化价值变得可衡量、可比较。origin 属性记录纹样的地域来源,如"中原"“北方”“江南”“水乡"等,揭示了纹样与地域文化的关联。luck 属性用简短的词语概括纹样的吉祥寓意,如"虎威”“祥云”“如意”"招财"等,一目了然地传达了纹样的文化象征。

@Observed 装饰器在此处的应用尤为关键。因为 PatternItem 的 charm 属性是可修改的——用户可以在编辑模态框中对纹样的吉祥度进行增减操作。当 charm 值变化后,不仅纹样列表项的数字显示需要更新,其吉祥度进度条的长度和颜色也需要同步变化。@Observed 机制确保了这一系列联动更新自动完成,无需开发者手动刷新进度条和色彩。

3.3 NeedleItem 针法针线数据模型

@Observed
export class NeedleItem {
  name: string;
  stitch: string;
  thread: string;
  skill: number;
  use: string;

  constructor(name: string, stitch: string, thread: string, skill: number, use: string) {
    this.name = name;
    this.stitch = stitch;
    this.thread = thread;
    this.skill = skill;
    this.use = use;
  }
}

NeedleItem 模型类承载布虎制作中的针法信息。布虎的缝制和刺绣离不开各种针法的运用,每一种针法都有其特定的适用场景和技艺要求,NeedleItem 将这些信息结构化地组织起来。

name 属性是针法的完整名称,如"平针绣"“回针绣”“打籽绣”“盘金绣"等。这些名称本身就是刺绣工艺的专业术语,承载着世代相传的技艺积淀。stitch 属性是针法的简称,如"平针”“回针”“打籽"等,用于在列表项中以简短形式展示。thread 属性记录该针法所使用的线材类型,如"棉线”“丝线”“金线"等——不同的线材赋予绣品不同的质感和光泽,棉线质朴柔和,丝线光泽细腻,金线华贵夺目。skill 属性是数值类型,代表该针法的技艺要求等级,以百分比表示。use 属性用简短词语描述该针法的主要用途,如"铺底”“勾边”“封边”"点睛"等,清晰明了地传达了每种针法在布虎制作中的功能定位。

NeedleItem 同样标注了 @Observed,虽然针法数据在本应用中不涉及用户修改操作,但标注 @Observed 体现了设计者的一致性原则——所有数据模型类统一采用可观察模式,为未来可能的扩展预留了空间。这种前瞻性的设计思维,使得代码在需求变更时更具弹性。

3.4 EmbroidressItem 绣娘数据模型

@Observed
export class EmbroidressItem {
  name: string;
  grade: string;
  years: number;
  tigers: number;
  skill: number;

  constructor(name: string, grade: string, years: number, tigers: number, skill: number) {
    this.name = name;
    this.grade = grade;
    this.years = years;
    this.tigers = tigers;
    this.skill = skill;
  }
}

EmbroidressItem 模型类代表绣娘的个人信息。这一数据模型的引入,使得应用不再只是展示"物",而是将"人"——布虎工艺的传承者和实践者——置于与产品同等重要的位置,体现了对匠人精神的尊重和传承。

name 属性是绣娘的姓名。grade 属性记录绣娘的技艺等级,如"非遗大师"“省级大师”“名师”“匠娘”"学徒"等——这一等级体系从国家级非遗传承人到初学学徒,涵盖了完整的技艺传承梯队。years 属性是从业年限,以数值表示,反映绣娘的经验积累程度。tigers 属性记录累计制作布虎的数量,是对绣娘产出能力的量化衡量。skill 属性是技艺评分,以百分制表示,综合反映绣娘的工艺水平。

这些属性的设计体现了对匠人多维度评价的思考:grade 是定性评价(头衔等级),years 是时间维度(经验积累),tigers 是数量维度(产出能力),skill 是质量维度(技艺水平)。四个维度交叉评估,可以较为全面地刻画一位绣娘的技艺画像。

3.5 AtelierItem 作坊数据模型

@Observed
export class AtelierItem {
  name: string;
  city: string;
  artisans: number;
  output: number;
  level: string;

  constructor(name: string, city: string, artisans: number, output: number, level: string) {
    this.name = name;
    this.city = city;
    this.artisans = artisans;
    this.output = output;
    this.level = level;
  }
}

AtelierItem 模型类代表布虎制作作坊的信息。作坊是传统手工艺的基本生产单元,承载着技艺传承、规模化生产和地域文化传播的多重功能。

name 属性是作坊的名称。city 属性记录作坊所在的城市或地区——这些地名本身就是布虎文化版图上的重要节点,将布虎的地理分布以数据的形式呈现出来。artisans 属性是数值类型,记录作坊中的绣娘人数,反映作坊的规模大小。output 属性记录作坊的年产量,是产能的量化指标。level 属性是作坊的等级评定,如"甲级"“乙级”“丙级”,体现了对作坊综合实力的分级评价。

3.6 CraftStepItem 制作工序数据模型

@Observed
export class CraftStepItem {
  name: string;
  days: number;
  tool: string;
  note: string;
  order: number;

  constructor(name: string, days: number, tool: string, note: string, order: number) {
    this.name = name;
    this.days = days;
    this.tool = tool;
    this.note = note;
    this.order = order;
  }
}

CraftStepItem 模型类代表布虎制作工序中的单一步骤信息。布虎的制作是一个多工序协作的流程,从最初的裁样到最终的系铃,每一步都有特定的工具、工时和操作要点。

name 属性是工序名称,如"裁样"“填絮”“缝身”“绣纹”“点睛”“安耳”“挂须”"系铃"等。这些名称简洁凝练地概括了每道工序的核心动作。days 属性是该工序所需工时,以天为单位。tool 属性记录该工序使用的工具和材料。note 属性是简短的操作说明。order 属性是工序序号,用于确保工序在列表中按正确顺序排列——这一属性的存在,使得即使数据数组的顺序被打乱,也可以通过 order 值重新排序,保证了工序流程的正确性。

3.7 TigerCollectItem 传世藏品数据模型

@Observed
export class TigerCollectItem {
  name: string;
  age: string;
  origin: string;
  value: number;
  rank: string;

  constructor(name: string, age: string, origin: string, value: number, rank: string) {
    this.name = name;
    this.age = age;
    this.origin = origin;
    this.value = value;
    this.rank = rank;
  }
}

TigerCollectItem 模型类代表布虎传世藏品的信息。这一模型将视线从当代产品引向历史收藏,为应用注入了时间维度的文化深度。

name 属性是藏品的名称,如"清代黎侯布虎"“明代刺绣虎枕"等。age 属性记录藏品的时代归属,如"清”“明”“民国”“清末”“当代”“近代"等——一个简短的时代标签,就能将藏品置于历史长河的坐标系中。origin 属性记录藏品的产地或来源地。value 属性是藏品的估价,是一个量化价值指标。rank 属性是藏品的等级评定,如"国宝”"珍品"等,通过 rankColor 函数映射为不同的色彩标签——国宝为红色,珍品为金色,其余为强调色,以视觉差异传达等级差异。

3.8 TigerOrderItem 订单数据模型

@Observed
export class TigerOrderItem {
  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;
  }
}

TigerOrderItem 模型类代表布虎的订单信息。这一模型的引入,使得应用从纯粹的文化展示工具扩展为具备商业管理功能的综合平台。

name 属性是订单对应的布虎产品名称。buyer 属性记录买方信息,如"民俗馆"“节庆公司”"母婴品牌"等——买方的类型多样,反映了布虎在当代社会中多元的应用场景。amount 属性是订单总金额。count 属性是订购数量。date 属性是订单日期,以"年-月"的格式记录,便于时间排序和统计。

TigerOrderItem 的 @Observed 装饰器在此处具有实际的功能意义——因为订单数据支持删除操作。当用户在删除确认模态框中确认删除某笔订单后,orders 数组中的对应元素被移除,@Observed 机制确保列表自动刷新,被删除的订单项从界面中消失。

3.9 数据模型体系的整体架构

纵观八个 @Observed 数据模型类,我们可以提炼出以下架构设计要点。第一,领域驱动设计:每个模型类对应一个清晰的业务领域概念——布虎产品、纹样、针法、绣娘、作坊、工序、藏品、订单,这些概念共同构成了布虎工艺的完整领域模型。第二,响应式一致性:所有模型类统一标注 @Observed,确保了整个数据层的响应式一致性。第三,构造函数规范化:所有模型类都采用"属性声明加构造函数逐个赋值"的标准模式,代码风格高度统一。第四,类型安全:每个属性都有明确的类型标注,将潜在的类型错误消灭在编译期。


四、工具函数:纯函数式的视觉计算逻辑

4.1 虎须位置计算函数

function tasselX(i: number): number {
  return 20 + i * 16;
}

function tasselY(i: number): number {
  return 6 + (i % 3) * 4;
}

function tasselW(i: number): number {
  return 5 + (i % 2) * 2;
}

这三个函数负责计算虎须在头部装饰中的位置和宽度。tasselX 计算第 i 根虎须的横坐标,基础值为 20,每增加一根虎须向右偏移 16 像素,形成均匀的水平排列。tasselY 计算纵坐标,使用 i 对 3 取模的运算,使得虎须在垂直方向上形成三组循环——第 0、3 根在同一高度,第 1、4 根下移 4 像素,第 2、5 根再下移 4 像素,从而产生微微的高低错落效果,避免了六根虎须完全水平排列的呆板感。tasselW 计算虎须宽度,使用 i 对 2 取模,使得奇偶交替的虎须宽度在 5 和 7 之间变化,进一步增加了视觉上的参差感。

这三个函数是典型的纯函数——输入相同必然输出相同,无副作用,不依赖任何外部状态。纯函数的优势在于可测试性、可预测性和可组合性。在复杂的 UI 布局中,将位置计算逻辑提取为独立函数而非内联在循环中,使得代码更加清晰,也便于在需要时调整计算逻辑。

4.2 条纹与进度条计算函数

function stirX(i: number): number {
  return 44 + i * 9;
}

function stirY(i: number): number {
  return 84 + (i % 2) * 7;
}

function charmBarW(c: number): number {
  return 16 + Math.min(c, 100) * 1.35;
}

function charmColor(c: number): string {
  if (c >= 80) {
    return COLORS.hot;
  }
  if (c >= 60) {
    return COLORS.tigerY;
  }
  return COLORS.clothB;
}

stirX 和 stirY 函数负责计算虎头条纹的位置。stirX 以 44 为基础值,每增加一条条纹向右偏移 9 像素。stirY 以 84 为基础值,通过 i 对 2 取模实现奇偶交替的垂直偏移——偶数条纹在 84 高度,奇数条纹在 91 高度,形成上下交错排列的效果,模拟了虎纹自然弯曲、不规则分布的视觉效果。

charmBarW 函数根据吉祥度值计算进度条的宽度。公式为 16 加上 min(c, 100) 乘以 1.35,其中 Math.min(c, 100) 确保即使吉祥度超过 100,进度条也不会无限增长——超过 100 的部分被截断,最大宽度为 151 像素。基础值 16 保证了即使吉祥度为 0,进度条也有最小宽度可见。系数 1.35 是经过调优的视觉参数,使得在 0 到 100 的吉祥度范围内,进度条宽度的变化既明显可辨,又不会超出布局容器的宽度限制。

charmColor 函数根据吉祥度值返回对应的色彩——80 以上为红色(代表极高的吉祥寓意),60 至 79 为虎黄色(代表中等偏上的吉祥度),60 以下为蓝色(代表基础吉祥度)。这种三段式的色彩分级,将数值差异转化为视觉色彩差异,使得用户一眼扫过列表就能感知到不同纹样的吉祥度高低分布,这是一种高效的视觉信息编码方式。

4.3 其他计算与映射函数

function skillBarW(s: number): number {
  return 16 + Math.min(s, 100) * 1.35;
}

function daysBarW(d: number): number {
  return 16 + Math.min(d, 10) * 12;
}

function orderBarH(amount: number): number {
  return 24 + Math.min(amount, 30000) / 300;
}

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.clothB;
  }
  return COLORS.clothG;
}

function rankColor(rank: string): string {
  if (rank.indexOf('国宝') >= 0) {
    return COLORS.hot;
  }
  if (rank.indexOf('珍品') >= 0) {
    return COLORS.gold;
  }
  return COLORS.accent;
}

skillBarW 函数与 charmBarW 使用相同的计算公式,因为绣娘技艺评分和纹样吉祥度都采用百分制,所以进度条宽度的计算逻辑一致。这种"同进制同公式"的设计保证了不同模块的进度条在视觉上具有可比性。

daysBarW 函数计算工时进度条的宽度,工时的上限为 10 天,系数为 12,所以最大宽度为 136 像素。工时进度条的系数大于百分制进度条的系数,是因为工时的数值范围远小于百分制的范围,需要更大的系数才能产生足够的视觉差异。

orderBarH 函数计算订单金额柱状图的高度,注意这里计算的是高度而非宽度,因为订单金额在 UI 中以纵向柱状图的形式展示。金额上限为 30000 元,除以 300 得到最大增加值 100,加上基础值 24,最大高度为 124 像素。

gradeColor 函数根据绣娘等级字符串返回对应的色彩。它使用 indexOf 方法检测字符串中是否包含特定关键词:"大师"映射为红色(最高级别),"名师"映射为金色(次高级别),"匠娘"映射为蓝色(中级),其余映射为绿色(基础级)。这种基于字符串包含检测的色彩映射方式,不需要等级与色彩的精确对应表,只需检测等级文本中是否包含特定关键词即可,具有较高的灵活性。

rankColor 函数采用相同的逻辑,将藏品等级映射为色彩:"国宝"为红色,"珍品"为金色,其余为强调色。这两个函数的设计理念一致——通过字符串关键词检测实现等级到色彩的灵活映射,避免了精确匹配的脆弱性。

这些工具函数共同构成了应用的"视觉计算层"——它们是纯函数,不依赖组件状态,可以被任何组件在任何时刻调用,返回确定性的结果。这种将视觉计算逻辑与组件逻辑分离的做法,提高了代码的可维护性和可测试性,是函数式编程思想在 ArkTS 开发中的良好实践。

true

false

应用启动

aboutToAppear 生命周期

启动 460ms 定时器

breath 布尔值循环翻转

breath 状态变化

虎眼放大1.4倍 / 虎须上移 / 进度条全透明 / 序号圆放大

虎眼还原1.0倍 / 虎须下移6px / 进度条半透明0.6 / 序号圆还原

animation 460ms 过渡

PlayMode.Alternate 反向播放


五、主组件 TigerPage:状态管理详解

5.1 组件声明与入口装饰器

@Entry
@Component
struct TigerPage {

TigerPage 是应用的入口页面组件。@Entry 装饰器标记此组件为页面的根组件,HarmonyOS 框架在应用启动时会自动渲染此组件作为初始界面。一个 ArkTS 页面文件中只能有一个 @Entry 标注的组件,它是整个页面组件树的根节点。@Component 装饰器声明 TigerPage 为一个自定义组件,使其可以被框架管理和渲染。

struct 关键字定义了一个结构体——在 ArkTS 中,自定义组件以 struct 而非 class 的形式声明。与 class 相比,struct 不支持继承,这强制了组件之间的组合关系而非继承关系,鼓励开发者通过组合而非继承来构建复杂的 UI,是组合优于继承原则在语言层面的体现。

5.2 交互状态变量

@State curTab: number = 0;
@State breath: boolean = false;
@State showAdd: boolean = false;
@State showEdit: boolean = false;
@State showDel: boolean = false;
@State selPattern: PatternItem | null = null;
@State selOrder: TigerOrderItem | null = null;

这组 @State 变量管理着应用的所有交互状态。curTab 记录当前选中的标签索引(0 到 7),初始值为 0,即默认显示"布虎"标签页。curTab 的变化会触发 tabContent 构建器的条件分支切换,从而显示不同的内容列表——这是整个应用导航体系的核心状态。

breath 是一个布尔值,初始为 false,它以 460 毫秒的间隔在 true 和 false 之间循环切换,驱动着整个应用的"呼吸"动画效果。这个变量的设计极为巧妙——用单一布尔变量驱动全局范围的动画效果,包括虎眼的缩放、虎须的位移和透明度变化、进度条的不透明度变化、工序序号的缩放等。每次 breath 值翻转,所有绑定了 this.breath 的 UI 元素同时切换状态,配合 animation 属性的过渡效果,形成了同步的"呼吸"节奏感。这种以单一状态驱动全局动画的手法,简洁而高效,避免了为每个动画元素单独管理状态变量的复杂性。

showAdd、showEdit、showDel 三个布尔变量分别控制三个模态对话框的显示与隐藏。初始值都为 false(不显示),当用户触发相应操作时设为 true(显示模态框),在模态框中完成操作或取消后设为 false(隐藏模态框)。这种"一个布尔值控制一个模态框"的设计简洁明了。

selPattern 和 selOrder 是两个可空引用变量,分别记录当前选中的纹样对象和当前选中的订单对象。它们的类型是联合类型与 null 的组合,使用联合类型来表达"可能未选择"的语义。当用户点击某个纹样的"改"按钮时,selPattern 被赋值为该纹样对象,然后 showEdit 被设为 true 打开编辑模态框。这种"选中对象加模态框"的配合模式,是 CRUD 应用中常见的交互范式。

5.3 表单状态变量

@State formName: string = '';
@State formStyle: string = '';
@State formColor: string = '';
@State formPrice: string = '';

这四个 @State 变量专门服务于"新增布虎"模态框的表单输入。formName 记录用户输入的布虎名称,formStyle 记录款式,formColor 记录主色,formPrice 记录售价。初始值都为空字符串,当用户在模态框的 TextInput 中输入内容时,通过 onChange 回调实时更新对应的 @State 变量。这种"输入即更新状态"的双向绑定模式,确保了表单数据与状态变量的实时同步。

5.4 数据列表状态变量

以下是八组 @State 数据数组的声明,每组对应一个标签页的内容数据:

@State tigers: ClothTigerItem[] = [
  new ClothTigerItem('镇宅大布虎', '坐虎', '高 45cm', '金黄', 880),
  new ClothTigerItem('端午艾虎', '艾虎', '高 18cm', '艾绿', 128),
  // ... 共12款布虎
];
@State patterns: PatternItem[] = [
  new PatternItem('王字额纹', '额头', 95, '中原', '虎威'),
  // ... 共8种纹样
];
@State needles: NeedleItem[] = [
  new NeedleItem('平针绣', '平针', '棉线', 85, '铺底'),
  // ... 共8种针法
];
@State girls: EmbroidressItem[] = [
  new EmbroidressItem('王秀英', '非遗大师', 52, 3200, 98),
  // ... 共8位绣娘
];
@State ateliers: AtelierItem[] = [
  new AtelierItem('黎侯虎作坊', '山西黎城', 26, 12000, '甲级'),
  // ... 共8家作坊
];
@State steps: CraftStepItem[] = [
  new CraftStepItem('裁样', 1, '剪刀 · 粉线', '定版型', 1),
  // ... 共8道工序
];
@State collects: TigerCollectItem[] = [
  new TigerCollectItem('清代黎侯布虎', '清', '山西黎城', 68000, '国宝'),
  // ... 共8件藏品
];
@State orders: TigerOrderItem[] = [
  new TigerOrderItem('镇宅大布虎', '民俗馆', 17600, 20, '2026-08'),
  // ... 共10笔订单
];

这八组数据数组涵盖了布虎工艺的完整信息维度。tigers 数组包含十二款布虎,从大型的镇宅布虎(880元)到小型的香囊虎(36元),价格和尺寸跨越大范围。patterns 数组包含八种吉祥纹样,吉祥度在 76 至 95 之间。needles 数组包含八种针法,技艺要求从 70 分到 92 分。girls 数组包含八位绣娘,从非遗大师(98分)到学徒(70分),形成了完整的技艺传承梯队。ateliers 数组包含八家作坊,覆盖山西黎城、河南淮阳、陕西凤翔等主要产地。steps 数组包含八道工序,从裁样到系铃,工时合计 10 天。collects 数组包含八件藏品,时代从明代到当代,估价从 8600 元到 96000 元。orders 数组包含十笔订单,买方类型多元,金额从 5360 元到 19800 元。

由于这些数组都标注了 @State,数组内容的增删会触发 UI 的自动刷新。当用户在"新增布虎"模态框中填写信息并保存时,新的 ClothTigerItem 被插入数组头部(通过 splice(0, 0, …)),列表顶部随即出现新增的布虎卡片。当用户删除订单时,orders 数组中的对应元素被移除,列表自动刷新。这种"数据变则视图变"的即时响应,是 ArkTS 响应式编程的核心体验。

5.5 生命周期函数 aboutToAppear

aboutToAppear(): void {
  setInterval(() => {
    this.breath = !this.breath;
  }, 460);
}

aboutToAppear 是 ArkTS 组件的生命周期函数之一,在组件创建后、UI 渲染前被调用。它通常用于执行初始化逻辑——本应用中,它启动了一个 460 毫秒间隔的定时器,反复翻转 breath 布尔值。

这个定时器是整个应用"呼吸"动画的发动机。每 460 毫秒,breath 值在 true 和 false 之间切换一次,所有绑定了 this.breath 的 UI 元素随之切换状态。配合 animation 属性(duration: 460, iterations: -1, playMode: PlayMode.Alternate),每次切换产生 460 毫秒的过渡动画,形成无缝循环的"呼吸"效果——460 毫秒的状态切换间隔恰好等于 460 毫秒的动画持续时间,保证了前一次动画刚结束,下一次状态变化就触发新的动画,形成连续不断的脉动节奏。

选择 460 毫秒而非更常见的 500 毫秒(半秒),可能是经过视觉调优的结果——460 毫秒的频率略快于半秒,给人以更有活力的感觉,但又不会快到令人眼花。这个数值的选择体现了"微调出精品质"的工匠精神——在动画设计中,几十毫秒的差异就能改变整体感觉。


六、UI 组件:@Builder 方法分析

6.1 模态遮罩层 modalOverlay

@Builder
modalOverlay(onClose: () => void) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.mask)
    .onClick(() => {
      onClose();
    })
}

modalOverlay 是一个可复用的 @Builder 方法,用于生成模态对话框的遮罩层。它接收一个 onClose 回调函数作为参数,生成一个覆盖全屏的半透明黑色 Column 组件,点击遮罩区域时调用 onClose 回调关闭模态框。

这个 @Builder 方法的参数化设计非常优雅。onClose 作为一个箭头函数参数传入,使得同一个 modalOverlay 可以被不同的模态框复用——新增布虎模态框传入自己的关闭逻辑(showAdd = false),编辑纹样模态框传入另一种关闭逻辑(showEdit = false),删除订单模态框传入第三种关闭逻辑(showDel = false)。这种"同一构建器、不同行为"的参数化设计,是代码复用的经典范式。

遮罩层使用 COLORS.mask(rgba(0,0,0,0.45))作为背景色,45% 的不透明度既能有效遮盖背景内容,又保留了背景的隐约可见性。点击遮罩层关闭模态框,是移动端模态交互的标准惯例,用户可以通过点击遮罩区域的"空白处"快速取消操作。

@Builder 装饰器是 ArkTS 特有的构建器机制,它允许将一段 UI 声明逻辑封装为可调用的方法。与直接在 build 方法中内联 UI 代码相比,@Builder 方法提供了模块化的 UI 组织方式——将复杂 UI 拆分为多个 @Builder 方法,每个负责一部分 UI,降低了 build 方法的复杂度,提高了代码可读性和可维护性。

6.2 页面头部 pageHeader

pageHeader 是应用中最复杂、最精美的 @Builder 方法,它由头部虎头装饰和统计信息条两部分组成。虎头装饰在一个 Stack 容器中构建,Stack 允许子元素按绝对定位叠放,非常适合构建由多个小元素组合而成的图形。

虎头主体是一个 112x106 像素的圆角矩形,背景色为虎黄色,外边框为哑金色。虎耳由两个 30x30 像素的圆形组成,位于虎头上方的左右两侧。虎眼由两个 14x14 像素的朱红色正圆形组成。额头正中的"王"字以 26 像素的字号、粗体字重、朱红色字体渲染,是整个虎头的"画眼"。

虎头装饰中最精妙的部分是呼吸动画。虎鼻由两个 9x9 像素的近黑红色正圆形组成,通过 scale 属性绑定 breath 动画,在 1 和 1.4 之间交替缩放,形成了"呼吸"效果。虎须通过 ForEach 遍历 TASSEL_IDX 数组生成六根,每根绑定了 breath 动画——通过 translate(垂直位移)和 opacity(透明度)的双重变化,形成了"飘动"的视觉效果。虎头右下角的金色圆形装饰点通过 rotate 在 -14 度和 14 度之间交替旋转,形成了持续摇摆的效果。

这些动画元素共同使用了 animation 属性的 PlayMode.Alternate 播放模式——它使得动画在正向播放完毕后自动反向播放,形成"放大、缩小、放大、缩小"的无限循环。如果使用 PlayMode.Normal,动画会呈现跳变效果,不够流畅。Alternate 模式保证了过渡的连续性,是"呼吸"动画效果的最佳选择。

标题区域位于虎头右侧,主标题"布老虎坊"以 24 像素字号、粗体字重、奶白色渲染。副标题"虎虎生威 · 驱邪纳福"以 12 像素字号、暖棕黄色渲染。标题下方是流派信息和四个色彩样块,展示了应用使用的主要色彩。

统计信息条位于虎头装饰下方,以白色卡片背景展示四项核心数据指标:“12 布虎款式”“8 吉祥纹样”“8 工序环节”“8 传世藏品”。四个统计项各占 layoutWeight(1),即四等分水平空间。layoutWeight 是 ArkTS Flex 布局的关键属性——它将可用空间按权重分配给子元素。

6.3 行卡片构建器

应用定义了八个行卡片 @Builder 方法,分别对应八个标签页的内容列表项。tigerRow 是布虎列表的卡片构建器,三段式布局:左侧布虎名称和属性信息,中间价格信息,右侧"编辑"按钮。patternRow 是纹样列表的卡片构建器,双层结构:上层信息行(包含吉祥度色块、纹样名称、吉祥度百分比和"改"按钮),下层吉祥度进度条。needleRow 是针法列表的卡片构建器,两段式布局,复用了 charmColor 函数来根据技艺值决定百分比文字的颜色。

girlRow 是绣娘列表的卡片构建器,双层结构:上层信息行(绣娘姓名、等级加年限、累计制虎数量),下层绣工进度条。等级文字的颜色由 gradeColor 函数决定——"大师"为红色,"名师"为金色,"匠娘"为蓝色。atelierRow 是作坊列表的卡片构建器,两段式布局,产量数字使用蓝色而非红色,给人沉稳理性的感觉。stepRow 是工序列表的卡片构建器,左侧的圆形序号标记绑定 breath 动画,在 1 和 1.12 之间交替缩放,形成脉动效果。collectRow 是藏品列表的卡片构建器,等级标签的设计是亮点——带背景色的小型圆角标签,"国宝"为红色,"珍品"为金色。orderRow 是订单列表的卡片构建器,双层结构:上层信息行(产品名称、买方标签、日期),下层金额进度条和"删除"按钮。

所有行卡片都以白色为背景,14 像素圆角,12 像素内边距,底部 8 像素外边距。白色卡片悬浮于米黄色页面背景之上,圆角和间距使得每张卡片之间有明确的视觉分隔,形成了清晰的卡片列表节奏感。

6.4 标签内容切换器 tabContent

@Builder
tabContent() {
  if (this.curTab === 0) {
    Column() {
      this.sectionTitle('🐯', '布虎家族', '共 ' + this.tigers.length + ' 款')
      ForEach(this.tigers, (item: ClothTigerItem, i: number) => {
        this.tigerRow(item, i)
      }, (item: ClothTigerItem) => item.name)
    }
    .width('100%')
  } else if (this.curTab === 1) {
    // ... 纹样列表
  }
  // ... 其他标签页
  else {
    // ... 订单列表
  }
}

tabContent 是标签内容切换的核心构建器,通过 if-else if-else 条件分支,根据 curTab 的值渲染不同的内容列表。每个分支的结构相同:先调用 sectionTitle 生成区块标题,再用 ForEach 遍历对应的数据数组,调用相应的行卡片构建器生成列表项。

这种"条件分支加 ForEach"的模式是 ArkTS 中实现标签页切换的常见方式。当用户点击底部导航栏的某个标签时,curTab 值改变,tabContent 重新执行,对应的条件分支被激活,渲染新的内容列表。这一过程是 ArkTS 框架自动完成的,开发者只需维护 curTab 状态和条件分支逻辑。

ForEach 的键值生成函数使用 item.name 作为唯一标识。键值生成函数的作用是帮助框架在数据变化时进行最小化的 DOM 更新——通过对比新旧数据的键值,框架可以确定哪些元素是新增的、哪些是删除的、哪些是位置变化的,从而只更新变化的部分。

0

1

2

3

4

5

6

7

用户点击标签

curTab 状态更新

tabContent 重新执行

curTab 值判断

渲染布虎列表

渲染纹样列表

渲染针法列表

渲染绣娘列表

渲染作坊列表

渲染工序列表

渲染藏品列表

渲染订单列表

sectionTitle + 行卡片渲染

6.5 底部导航栏

@Builder
bottomItem(i: number) {
  Column() {
    Text(TAB_LIST[i].icon)
      .fontSize(17)
    Text(TAB_LIST[i].label)
      .fontSize(11)
      .fontColor(this.curTab === i ? COLORS.tigerY : COLORS.sub)
      .margin({ top: 2 })
  }
  .width('25%')
  .padding({ top: 7, bottom: 7 })
  .backgroundColor(this.curTab === i ? '#962626' : COLORS.tabBg)
  .borderRadius(10)
  .onClick(() => {
    this.curTab = i;
  })
}

bottomItem 是底部导航栏中单个标签项的构建器。每个标签项是一个 Column,上方是图标(17像素),下方是文字标签(11像素),文字颜色根据是否为当前选中标签而变化——选中时为虎黄色,未选中时为暖棕黄色。标签项的背景色也根据选中状态变化——选中时为更亮的红色,未选中时为深酒红。点击标签项时,curTab 被设为对应的索引值,触发 tabContent 切换内容。

bottomBar 将八个标签分为两行——第一行通过 ForEach 遍历 ROW1_IDX 生成前四个标签,第二行通过 ForEach 遍历 ROW2_IDX 生成后四个标签。整个底部导航栏以深酒红为背景色,上方两个圆角为 18 像素,下方两个圆角为 0——上圆下直的设计使得导航栏顶部与页面内容区域平滑过渡。

6.6 模态对话框

应用定义了三个模态对话框 @Builder 方法:addModal(新增布虎)、editModal(编辑纹样)、delModal(删除订单)。

addModal 以 if (this.showAdd) 条件控制渲染——只有当 showAdd 为 true 时才生成模态框 UI。模态框结构为 Stack 容器内叠放两层:底层是 modalOverlay 遮罩层,上层是白色卡片内容区。卡片宽度 88%,最大高度 80%(通过 constraintSize 约束)。模态框包含四个表单字段和底部操作按钮(取消加保存)。"保存"按钮执行 splice(0, 0, new ClothTigerItem(…)) 在数组头部插入新元素。

editModal 展示当前选中纹样的名称和吉祥度,提供"加纹 +5"和"减纹 -5"两个操作按钮。增减操作后,由于 selPattern 是 @Observed 对象,其 charm 属性的变化会自动触发纹样列表中对应行卡片的刷新——进度条长度、颜色和百分比文字都会即时更新。

delModal 显示"此操作不可恢复,请确认"的警示文字,以及待删除订单的名称和买方信息。"再想想"按钮文案亲切幽默,减轻了用户的决策压力。"确认删除"按钮执行 splice(indexOf(selOrder), 1) 从数组中移除对应元素。

三个模态框都使用 zIndex(999) 确保覆盖在所有内容之上。这种"条件渲染加 Stack 叠放"的模式,是 ArkTS 实现模态交互的典型方式。


七、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 内含 Scroll 和 bottomBar),上层是条件渲染的三个模态框。

主内容区的 Column 采用垂直布局,包含两个子元素:上方的 Scroll 可滚动区域(layoutWeight(1) 占据弹性空间)和下方的 bottomBar 底部导航栏(固定高度)。这种"上方弹性加下方固定"的布局是移动端应用的经典结构——内容区域可滚动以容纳超长内容列表,导航栏固定在底部不随内容滚动。

Scroll 组件内嵌一个 Column,包含 pageHeader(页面头部)和 tabContent(标签内容),两侧各 14 像素内边距,底部 12 像素内边距。scrollable(ScrollDirection.Vertical) 启用垂直滚动——当内容超出可视区域时,用户可以上下滑动查看全部内容。Scroll 的 layoutWeight(1) 使其占据 Column 中除 bottomBar 之外的所有空间。

三个模态框以 if 条件控制渲染,叠放在 Stack 的上层。当 showAdd、showEdit 或 showDel 为 true 时,对应的模态框被渲染并覆盖在整个页面之上(zIndex(999)),形成模态遮罩效果。当条件为 false 时,模态框不渲染,不影响下层内容。这种"条件渲染加 Stack 叠放"的模式,是 ArkTS 实现模态交互的典型方式。

整个页面的背景色设为 COLORS.bg(米黄色),确保所有区域的背景色调一致。Stack 容器的 width 和 height 都设为 100%,占满整个屏幕。


八、状态管理机制深度解析

8.1 @State 的响应式原理

在本应用中,@State 是最核心的状态管理装饰器,被用于 TigerPage 组件内的所有状态变量。@State 的核心原理是:当被标注的变量值发生变化时,框架自动重新执行依赖该变量的 UI 构建逻辑,更新界面显示。这种"数据驱动视图"的机制,使得开发者无需手动调用任何刷新方法,只需修改变量值,界面就会自动更新。

@State 修饰的变量类型包括基本类型(number、boolean、string)、对象类型( ClothTigerItem 等)、数组类型(ClothTigerItem[] 等)和联合类型(PatternItem | null)。对于基本类型,@State 监测的是值本身的变化;对于对象和数组类型,@State 监测的是引用的变化——如果直接修改对象的属性或数组的元素,需要配合 @Observed 装饰器才能实现深层响应。

以 breath 变量为例,它是一个 boolean 类型的 @State 变量。每当 aboutToAppear 中的定时器翻转 breath 值时,所有在 UI 构建逻辑中引用了 this.breath 的地方都会重新执行——虎鼻的 scale 属性重新计算(1.4 或 1)、虎须的 translate 和 opacity 属性重新计算、进度条的 opacity 属性重新计算、工序序号圆的 scale 属性重新计算。配合 animation 属性的过渡效果,这些重新计算不是瞬间跳变,而是通过 460 毫秒的平滑过渡完成,形成了连续的"呼吸"动画。

8.2 @Observed 与深层响应

@Observed 装饰器用于类定义,使得类的实例成为"可观察对象"。当 @Observed 类的实例被 @State 变量引用时,不仅引用本身的变化会触发 UI 更新,实例的属性变化也会被追踪并触发相关 UI 的刷新——这就是"深层响应"。

在本应用中,八个数据模型类都标注了 @Observed。以 PatternItem 为例,当用户在编辑模态框中点击"加纹 +5"按钮时,执行的是 this.selPattern.charm = this.selPattern.charm + 5——直接修改了 selPattern 对象的 charm 属性。由于 PatternItem 标注了 @Observed,且 selPattern 是 @State 变量,这一属性修改会被框架捕获,自动触发 patternRow 卡片中所有依赖 item.charm 的 UI 元素重新渲染:吉祥度百分比文字更新、进度条宽度通过 charmBarW 重新计算、进度条和色块的颜色通过 charmColor 重新计算。整个更新链路完全自动,开发者只需修改数据,无需关心 UI 刷新。

8.3 状态管理的设计模式

从设计模式的角度审视本应用的状态管理,可以提炼出以下模式:

单一状态源模式:TigerPage 组件作为唯一的状态持有者,所有状态变量都定义在其内部。子 @Builder 方法通过参数接收数据,不持有自己的状态。这种"状态上移、视图下沉"的模式,使得状态变化的影响范围清晰可控。

选择器模式:selPattern 和 selOrder 两个状态变量充当"选择器"的角色——它们记录当前被选中的数据对象,为模态框提供操作目标。当用户点击"改"按钮时,selPattern 被设为对应的 PatternItem 实例,编辑模态框通过 selPattern 读取和修改数据。这种"先选中、再操作"的模式,是列表交互的标准范式。

命令模式:modalOverlay 的 onClose 参数是一个回调函数,实质上是命令模式的简化形式——将"关闭模态框"这一行为封装为函数对象,由调用方决定具体执行什么操作。不同的模态框传入不同的 onClose 函数,实现了同一 @Builder 方法的不同行为。


九、布局样式分析

9.1 Stack 布局

Stack 是 ArkTS 中的层叠布局容器,允许子元素按绝对定位叠放。在本应用中,Stack 用于两个关键场景:头部虎头装饰的图形组合和 build 方法中的模态框叠放。

在头部虎头装饰中,Stack 容纳了虎头主体、虎耳、虎眼、"王"字、条纹、虎鼻、虎嘴、虎须、装饰圆点、标题文字和色彩样块等十余个元素,每个元素通过 position 绝对定位在精确的坐标位置。这种"Stack 加 position"的组合,是构建复杂图形的标准方式——每个元素的位置独立于其他元素,修改一个元素的位置不影响其他元素。

在 build 方法中,Stack 用于将模态框叠放在主内容区之上。模态框通过 zIndex(999) 确保显示在最上层,遮罩层覆盖整个屏幕形成模态效果。

9.2 Flex 布局与 layoutWeight

Row 和 Column 是 ArkTS 中的 Flex 布局容器,分别用于水平和垂直方向的排列。layoutWeight 是 Flex 布局的核心属性,用于分配弹性空间。

在本应用中,layoutWeight 被广泛使用。统计信息条中四个统计项各设 layoutWeight(1),平分水平空间。行卡片中左侧信息区设 layoutWeight(1),占据弹性空间,将右侧价格和按钮推到行尾。sectionTitle 中间的空 Column 设 layoutWeight(1),实现"两端对齐"布局。build 方法中 Scroll 设 layoutWeight(1),占据 bottomBar 之外的所有垂直空间。

layoutWeight 的工作原理是:当一个 Flex 容器的总尺寸为 W,其中固定尺寸子元素占用了 F 的空间后,剩余空间 W-F 将按 layoutWeight 的比例分配给弹性子元素。如果所有弹性子元素的 layoutWeight 相同,它们平分剩余空间;如果不同,则按比例分配。

9.3 圆角与间距

圆角(borderRadius)是本应用视觉设计的重要元素。头部虎头装饰的圆角为 22 像素,统计信息条的上圆角为 16 像素,行卡片的圆角为 14 像素,底部导航栏的上圆角为 18 像素,标签项的圆角为 10 像素。这些圆角值形成了一个从大到小的梯度——容器越大,圆角越大;元素越小,圆角越小。这种梯度式的圆角设计,使得界面的圆角风格既有统一性,又有层次感。

间距(margin 和 padding)的设计同样精心。行卡片之间的间距为 8 像素(margin bottom),页面内容的左右内边距为 14 像素,行卡片的内边距为 12 像素,统计信息条的上下内边距为 12 像素。这些间距值形成了"外松内紧"的布局节奏——元素之间有足够的呼吸空间,元素内部的信息紧凑而不拥挤。


十、技术对比表格

以下表格对本应用中使用的主要 ArkTS 技术特性进行对比总结:

技术特性用途本应用中的使用场景优势分析注意事项
@Entry标记入口组件TigerPage 组件声明框架自动识别页面根组件,启动渲染每个页面文件仅一个
@Component声明自定义组件TigerPage 组件声明使 struct 可被框架管理和渲染以 struct 而非 class 声明
@State组件内部状态curTab、breath、showAdd 等十余个变量值变化自动触发 UI 更新基本类型监测值变化,对象需配合 @Observed
@Observed可观察对象八个数据模型类对象属性变化被自动追踪需配合 @State 使用才生效
@BuilderUI 构建器modalOverlay、pageHeader、tigerRow 等十余个方法模块化 UI 组织,支持参数化复用不能直接访问 this 之外的上下文
@Builder 参数化传递数据和回调modalOverlay(onClose)、sectionTitle(icon,title,sub)同一构建器不同行为,减少代码重复参数类型需明确声明
ForEach列表渲染八个数据数组的列表渲染、虎须和条纹的循环渲染支持键值生成函数,高效更新需提供键值生成函数以优化性能
Stack层叠布局虎头装饰图形组合、模态框叠放支持绝对定位,适合复杂图形子元素需 position 定位
layoutWeight弹性空间分配统计信息条四等分、行卡片信息区占弹性空间自动分配剩余空间,响应式布局需在 Flex 容器中使用
linearGradient线性渐变头部背景从深酒红到近黑红的渐变丰富的视觉层次感需指定 angle 和 colors 数组
animation属性动画breath 驱动的所有动画效果自动过渡,支持无限循环和交替播放需配合状态变量使用
PlayMode.Alternate交替播放breath 动画的往返式循环平滑的正反向过渡,避免跳变需设置 iterations 为 -1
position绝对定位虎头装饰中所有元素的定位精确控制元素位置,互不影响需在 Stack 容器中使用
translate相对偏移虎须的垂直位移动画不改变基础位置,只做偏移与 position 配合使用
scale缩放变换虎鼻、色块、序号圆的缩放动画统一缩放,保持比例需配合 animation 使用
rotate旋转变换条纹旋转、装饰圆点摇摆增加动态感角度正值顺时针,负值逆时针
opacity透明度虎须和进度条的闪烁效果简单有效的视觉变化0 全透明,1 全不透明
Scroll可滚动区域主内容区的垂直滚动容纳超长内容列表需指定 scrollable 方向
TextInput文本输入新增布虎模态框的四个输入字段支持 placeholder 和 onChangetext 属性绑定状态变量实现双向绑定
constraintSize尺寸约束模态框的 maxHeight 限制防止内容溢出屏幕可设置 max/min 的宽高
if 条件渲染条件控制模态框的显示隐藏、tabContent 的分支切换条件为 false 时不渲染,节省资源与 visibility 控制不同

十一、Mermaid 流程图:数据增删改流程

以下流程图展示了本应用中数据的增删改操作流程:

新增布虎

编辑纹样

加纹

减纹

删除订单

用户操作

操作类型

点击新增按钮

showAdd = true

addModal 渲染

用户填写表单

点击保存?

tigers.splice 0,0,new ClothTigerItem

showAdd = false 关闭模态框

showAdd = false

列表自动刷新 新增项出现在顶部

点击改按钮

selPattern = item, showEdit = true

editModal 渲染

加纹或减纹?

selPattern.charm += 5

selPattern.charm -= 5

showEdit = false

列表自动刷新 进度条和颜色更新

点击删按钮

selOrder = item, showDel = true

delModal 渲染

确认删除?

orders.splice indexOf selOrder, 1

showDel = false

showDel = false

列表自动刷新 被删项消失


安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface ColorPalette {
  bg: string;
  cardBg: string;
  header1: string;
  header2: string;
  tigerY: string;
  tigerR: string;
  clothB: string;
  clothG: string;
  stripe: 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: '#FAF3E4',
  cardBg: '#FFFFFF',
  header1: '#7A1F1F',
  header2: '#3A0E0E',
  tigerY: '#F0A830',
  tigerR: '#C0392B',
  clothB: '#2E6DB4',
  clothG: '#3E8E5A',
  stripe: '#7A4A2B',
  gold: '#D9A441',
  title: '#FDEBD0',
  sub: '#E5B87A',
  text1: '#5A2A1E',
  text2: '#7A5A4A',
  text3: '#A08F7A',
  accent: '#C0392B',
  hot: '#C0392B',
  cool: '#2E6DB4',
  danger: '#D9534F',
  tabBg: '#7A1F1F',
  tabOn: '#F0A830',
  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 TASSEL_IDX: number[] = [0, 1, 2, 3, 4, 5];
const STIR_IDX: number[] = [0, 1, 2, 3];

@Observed
export class ClothTigerItem {
  name: string;
  style: string;
  size: string;
  color: string;
  price: number;

  constructor(name: string, style: string, size: string, color: string, price: number) {
    this.name = name;
    this.style = style;
    this.size = size;
    this.color = color;
    this.price = price;
  }
}

@Observed
export class PatternItem {
  name: string;
  use: string;
  charm: number;
  origin: string;
  luck: string;

  constructor(name: string, use: string, charm: number, origin: string, luck: string) {
    this.name = name;
    this.use = use;
    this.charm = charm;
    this.origin = origin;
    this.luck = luck;
  }
}

@Observed
export class NeedleItem {
  name: string;
  stitch: string;
  thread: string;
  skill: number;
  use: string;

  constructor(name: string, stitch: string, thread: string, skill: number, use: string) {
    this.name = name;
    this.stitch = stitch;
    this.thread = thread;
    this.skill = skill;
    this.use = use;
  }
}

@Observed
export class EmbroidressItem {
  name: string;
  grade: string;
  years: number;
  tigers: number;
  skill: number;

  constructor(name: string, grade: string, years: number, tigers: number, skill: number) {
    this.name = name;
    this.grade = grade;
    this.years = years;
    this.tigers = tigers;
    this.skill = skill;
  }
}

@Observed
export class AtelierItem {
  name: string;
  city: string;
  artisans: number;
  output: number;
  level: string;

  constructor(name: string, city: string, artisans: number, output: number, level: string) {
    this.name = name;
    this.city = city;
    this.artisans = artisans;
    this.output = output;
    this.level = level;
  }
}

@Observed
export class CraftStepItem {
  name: string;
  days: number;
  tool: string;
  note: string;
  order: number;

  constructor(name: string, days: number, tool: string, note: string, order: number) {
    this.name = name;
    this.days = days;
    this.tool = tool;
    this.note = note;
    this.order = order;
  }
}

@Observed
export class TigerCollectItem {
  name: string;
  age: string;
  origin: string;
  value: number;
  rank: string;

  constructor(name: string, age: string, origin: string, value: number, rank: string) {
    this.name = name;
    this.age = age;
    this.origin = origin;
    this.value = value;
    this.rank = rank;
  }
}

@Observed
export class TigerOrderItem {
  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 tasselX(i: number): number {
  return 20 + i * 16;
}

function tasselY(i: number): number {
  return 6 + (i % 3) * 4;
}

function tasselW(i: number): number {
  return 5 + (i % 2) * 2;
}

function stirX(i: number): number {
  return 44 + i * 9;
}

function stirY(i: number): number {
  return 84 + (i % 2) * 7;
}

function charmBarW(c: number): number {
  return 16 + Math.min(c, 100) * 1.35;
}

function charmColor(c: number): string {
  if (c >= 80) {
    return COLORS.hot;
  }
  if (c >= 60) {
    return COLORS.tigerY;
  }
  return COLORS.clothB;
}

function skillBarW(s: number): number {
  return 16 + Math.min(s, 100) * 1.35;
}

function daysBarW(d: number): number {
  return 16 + Math.min(d, 10) * 12;
}

function orderBarH(amount: number): number {
  return 24 + Math.min(amount, 30000) / 300;
}

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.clothB;
  }
  return COLORS.clothG;
}

function rankColor(rank: string): string {
  if (rank.indexOf('国宝') >= 0) {
    return COLORS.hot;
  }
  if (rank.indexOf('珍品') >= 0) {
    return COLORS.gold;
  }
  return COLORS.accent;
}

@Entry
@Component
struct TigerPage {
  @State curTab: number = 0;
  @State breath: boolean = false;
  @State showAdd: boolean = false;
  @State showEdit: boolean = false;
  @State showDel: boolean = false;
  @State selPattern: PatternItem | null = null;
  @State selOrder: TigerOrderItem | null = null;
  @State formName: string = '';
  @State formStyle: string = '';
  @State formColor: string = '';
  @State formPrice: string = '';
  @State tigers: ClothTigerItem[] = [
    new ClothTigerItem('镇宅大布虎', '坐虎', '高 45cm', '金黄', 880),
    new ClothTigerItem('端午艾虎', '艾虎', '高 18cm', '艾绿', 128),
    new ClothTigerItem('双头虎枕', '虎枕', '长 30cm', '红黄', 168),
    new ClothTigerItem('虎头帽', '虎帽', '头围 50cm', '红底', 98),
    new ClothTigerItem('虎头鞋', '虎鞋', '12-16码', '花布', 66),
    new ClothTigerItem('布偶老虎', '玩偶虎', '高 28cm', '虎黄', 76),
    new ClothTigerItem('骑虎娃娃', '骑虎', '高 25cm', '彩布', 188),
    new ClothTigerItem('挂帘小虎', '挂虎', '宽 15cm', '红黄', 46),
    new ClothTigerItem('百衲布虎', '百衲虎', '高 22cm', '拼布', 158),
    new ClothTigerItem('金线绣虎', '绣虎', '高 20cm', '金线', 268),
    new ClothTigerItem('香囊虎', '香虎', '高 8cm', '红布', 36),
    new ClothTigerItem('对虎摆件', '对虎', '高 16cm', '花布', 218)
  ];
  @State patterns: PatternItem[] = [
    new PatternItem('王字额纹', '额头', 95, '中原', '虎威'),
    new PatternItem('卷云纹', '身侧', 88, '北方', '祥云'),
    new PatternItem('如意纹', '胸前', 90, '江南', '如意'),
    new PatternItem('铜钱纹', '腹下', 82, '中原', '招财'),
    new PatternItem('双鱼纹', '尾巴', 76, '水乡', '有余'),
    new PatternItem('缠枝纹', '四肢', 84, '江南', '绵长'),
    new PatternItem('八卦纹', '背脊', 80, '道教地', '镇邪'),
    new PatternItem('蝴蝶纹', '耳边', 86, '民间', '福叠')
  ];
  @State needles: NeedleItem[] = [
    new NeedleItem('平针绣', '平针', '棉线', 85, '铺底'),
    new NeedleItem('回针绣', '回针', '丝线', 90, '勾边'),
    new NeedleItem('锁边绣', '锁边', '棉线', 82, '封边'),
    new NeedleItem('打籽绣', '打籽', '丝线', 78, '点睛'),
    new NeedleItem('盘金绣', '盘金', '金线', 92, '贵饰'),
    new NeedleItem('十字绣', '十字', '棉线', 70, '纹样'),
    new NeedleItem('平金绣', '平金', '金线', 88, '王字'),
    new NeedleItem('贴布绣', '贴布', '花布', 74, '拼色')
  ];
  @State girls: EmbroidressItem[] = [
    new EmbroidressItem('王秀英', '非遗大师', 52, 3200, 98),
    new EmbroidressItem('刘桂英', '省级大师', 46, 2800, 95),
    new EmbroidressItem('张巧云', '名师', 38, 2100, 92),
    new EmbroidressItem('李彩凤', '名师', 32, 1800, 89),
    new EmbroidressItem('赵绣娘', '匠娘', 26, 1200, 84),
    new EmbroidressItem('孙兰香', '匠娘', 22, 960, 80),
    new EmbroidressItem('周金花', '匠娘', 18, 720, 76),
    new EmbroidressItem('吴巧儿', '学徒', 12, 480, 70)
  ];
  @State ateliers: AtelierItem[] = [
    new AtelierItem('黎侯虎作坊', '山西黎城', 26, 12000, '甲级'),
    new AtelierItem('淮阳布虎坊', '河南淮阳', 18, 8600, '甲级'),
    new AtelierItem('凤翔布艺坊', '陕西凤翔', 14, 7200, '甲级'),
    new AtelierItem('北京胡同布虎社', '北京', 10, 5200, '乙级'),
    new AtelierItem('河北定州布虎坊', '河北定州', 8, 4600, '乙级'),
    new AtelierItem('山东高密布虎坊', '山东高密', 9, 4800, '乙级'),
    new AtelierItem('湖南湘西布虎坊', '湖南湘西', 6, 3200, '丙级'),
    new AtelierItem('甘肃庆阳布虎坊', '甘肃庆阳', 5, 2800, '丙级')
  ];
  @State steps: CraftStepItem[] = [
    new CraftStepItem('裁样', 1, '剪刀 · 粉线', '定版型', 1),
    new CraftStepItem('填絮', 1, '棉花 · 木棍', '塞紧实', 2),
    new CraftStepItem('缝身', 2, '平针 · 顶针', '缝虎身', 3),
    new CraftStepItem('绣纹', 2, '绣针 · 彩线', '绣王字', 4),
    new CraftStepItem('点睛', 1, '黑线 · 打籽', '绣眼睛', 5),
    new CraftStepItem('安耳', 1, '暗针', '缝虎耳', 6),
    new CraftStepItem('挂须', 1, '绒线', '粘虎须', 7),
    new CraftStepItem('系铃', 1, '铜铃 · 红绳', '挂铃铛', 8)
  ];
  @State collects: TigerCollectItem[] = [
    new TigerCollectItem('清代黎侯布虎', '清', '山西黎城', 68000, '国宝'),
    new TigerCollectItem('民国虎头帽', '民国', '河北', 28000, '珍品'),
    new TigerCollectItem('明代刺绣虎枕', '明', '江南', 96000, '国宝'),
    new TigerCollectItem('清末双头虎枕', '清末', '山东', 32000, '珍品'),
    new TigerCollectItem('民国布虎香囊', '民国', '陕西', 12000, '珍品'),
    new TigerCollectItem('清代金线绣虎', '清', '北京', 76000, '国宝'),
    new TigerCollectItem('建国初布虎', '近代', '河南', 8600, '珍品'),
    new TigerCollectItem('黎侯虎原型', '当代', '山西', 52000, '珍品')
  ];
  @State orders: TigerOrderItem[] = [
    new TigerOrderItem('镇宅大布虎', '民俗馆', 17600, 20, '2026-08'),
    new TigerOrderItem('端午艾虎', '节庆公司', 12800, 100, '2026-07'),
    new TigerOrderItem('虎头鞋', '母婴品牌', 19800, 300, '2026-08'),
    new TigerOrderItem('双头虎枕', '文创店', 10080, 60, '2026-06'),
    new TigerOrderItem('百衲布虎', '手工集市', 9480, 60, '2026-07'),
    new TigerOrderItem('虎头帽', '儿童节定制', 9800, 100, '2026-05'),
    new TigerOrderItem('金线绣虎', '拍卖行', 5360, 20, '2026-08'),
    new TigerOrderItem('骑虎娃娃', '旅店', 7520, 40, '2026-06'),
    new TigerOrderItem('香囊虎', '药店', 7200, 200, '2026-04'),
    new TigerOrderItem('对虎摆件', '婚庆公司', 13080, 60, '2026-05')
  ];

  aboutToAppear(): void {
    setInterval(() => {
      this.breath = !this.breath;
    }, 460);
  }

  @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(112)
          .height(106)
          .borderRadius(56)
          .backgroundColor(COLORS.tigerY)
          .border({ width: 2.5, color: COLORS.gold })
          .position({ x: 16, y: 14 })
        Column()
          .width(30)
          .height(30)
          .borderRadius(15)
          .backgroundColor(COLORS.tigerY)
          .border({ width: 2, color: COLORS.gold })
          .position({ x: 20, y: 16 })
        Column()
          .width(30)
          .height(30)
          .borderRadius(15)
          .backgroundColor(COLORS.tigerY)
          .border({ width: 2, color: COLORS.gold })
          .position({ x: 94, y: 16 })
        Column()
          .width(14)
          .height(14)
          .borderRadius(7)
          .backgroundColor(COLORS.tigerR)
          .position({ x: 28, y: 24 })
        Column()
          .width(14)
          .height(14)
          .borderRadius(7)
          .backgroundColor(COLORS.tigerR)
          .position({ x: 102, y: 24 })
        Text('王')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.tigerR)
          .position({ x: 56, y: 42 })
        ForEach(STIR_IDX, (i: number) => {
          Column()
            .width(2)
            .height(16)
            .borderRadius(1)
            .backgroundColor(COLORS.stripe)
            .rotate({ angle: i % 2 === 0 ? -20 : 20 })
            .position({ x: stirX(i), y: stirY(i) })
        }, (i: number) => 'st' + i)
        Column()
          .width(9)
          .height(9)
          .borderRadius(4.5)
          .backgroundColor(COLORS.header2)
          .scale({ x: this.breath ? 1.4 : 1, y: this.breath ? 1.4 : 1 })
          .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
          .position({ x: 56, y: 74 })
        Column()
          .width(9)
          .height(9)
          .borderRadius(4.5)
          .backgroundColor(COLORS.header2)
          .scale({ x: this.breath ? 1.4 : 1, y: this.breath ? 1.4 : 1 })
          .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
          .position({ x: 78, y: 74 })
        Column()
          .width(6)
          .height(5)
          .borderRadius(3)
          .backgroundColor(COLORS.tigerR)
          .position({ x: 66, y: 82 })
        ForEach(TASSEL_IDX, (i: number) => {
          Column()
            .width(tasselW(i))
            .height(26)
            .borderRadius(3)
            .backgroundColor(i % 2 === 0 ? COLORS.clothB : COLORS.clothG)
            .translate({ y: this.breath ? tasselY(i) : tasselY(i) + 6 })
            .opacity(this.breath ? 1 : 0.6)
            .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
            .position({ x: tasselX(i), y: 90 })
        }, (i: number) => 't' + i)
        Column()
          .width(10)
          .height(10)
          .borderRadius(5)
          .backgroundColor(COLORS.gold)
          .border({ width: 1, color: COLORS.title })
          .rotate({ angle: this.breath ? -14 : 14 })
          .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
          .position({ x: 88, y: 98 })
        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: 24 })
        Column() {
          Text('黎侯虎 · 淮阳布虎 · 凤翔虎')
            .fontSize(11)
            .fontColor(COLORS.gold)
          Row() {
            Column()
              .width(24)
              .height(12)
              .borderRadius(6)
              .backgroundColor(COLORS.tigerY)
            Column()
              .width(24)
              .height(12)
              .borderRadius(6)
              .backgroundColor(COLORS.tigerR)
              .margin({ left: 4 })
            Column()
              .width(24)
              .height(12)
              .borderRadius(6)
              .backgroundColor(COLORS.clothB)
              .margin({ left: 4 })
            Column()
              .width(24)
              .height(12)
              .borderRadius(6)
              .backgroundColor(COLORS.clothG)
              .margin({ left: 4 })
          }
          .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
  tigerRow(item: ClothTigerItem, i: number) {
    Row() {
      Column() {
        Text(item.name)
          .fontSize(15)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLORS.text1)
        Text(item.style + ' · ' + item.size + ' · ' + item.color)
          .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.style)
          .fontSize(10)
          .fontColor(COLORS.tigerY)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.End)
      .margin({ right: 10 })
      Text('编')
        .fontSize(12)
        .fontColor(COLORS.cardBg)
        .backgroundColor(COLORS.tigerR)
        .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
  patternRow(item: PatternItem, i: number) {
    Column() {
      Row() {
        Column()
          .width(22)
          .height(22)
          .borderRadius(11)
          .backgroundColor(charmColor(item.charm))
          .scale({ x: this.breath ? 1.18 : 1, y: this.breath ? 1.18 : 1 })
          .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
        Column() {
          Text(item.name)
            .fontSize(15)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLORS.text1)
          Text(item.origin + ' · ' + item.luck)
            .fontSize(11)
            .fontColor(COLORS.text2)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 10 })
        Column() {
          Text(item.charm + '%')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(charmColor(item.charm))
          Text(item.use)
            .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.selPattern = item;
            this.showEdit = true;
          })
      }
      .width('100%')
      Row() {
        Text('吉祥度')
          .fontSize(10)
          .fontColor(COLORS.text2)
        Column()
          .width(charmBarW(item.charm))
          .height(8)
          .borderRadius(4)
          .backgroundColor(charmColor(item.charm))
          .margin({ left: 8 })
          .opacity(this.breath ? 1 : 0.6)
          .animation({ duration: 460, 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
  needleRow(item: NeedleItem, i: number) {
    Row() {
      Column() {
        Text(item.name)
          .fontSize(15)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLORS.text1)
        Text(item.thread + ' · ' + item.use)
          .fontSize(11)
          .fontColor(COLORS.text2)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Column() {
        Text(item.skill + '%')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(charmColor(item.skill))
        Text(item.stitch)
          .fontSize(10)
          .fontColor(COLORS.text3)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)
    .margin({ bottom: 8 })
  }

  @Builder
  girlRow(item: EmbroidressItem, 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.tigers + ' 只')
            .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.tigerR)
          .margin({ left: 8 })
          .opacity(this.breath ? 1 : 0.6)
          .animation({ duration: 460, 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
  atelierRow(item: AtelierItem, 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.artisans + ' 位绣娘')
          .fontSize(11)
          .fontColor(COLORS.text3)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)
    .margin({ bottom: 8 })
  }

  @Builder
  stepRow(item: CraftStepItem, i: number) {
    Row() {
      Column() {
        Text(item.order + '')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.cardBg)
          .width(28)
          .height(28)
          .textAlign(TextAlign.Center)
          .backgroundColor(COLORS.tigerR)
          .borderRadius(14)
          .scale({ x: this.breath ? 1.12 : 1, y: this.breath ? 1.12 : 1 })
          .animation({ duration: 460, iterations: -1, playMode: PlayMode.Alternate })
      }
      Column() {
        Text(item.name)
          .fontSize(15)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLORS.text1)
        Text(item.tool + ' · ' + item.note)
          .fontSize(11)
          .fontColor(COLORS.text2)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12 })
      Column() {
        Text(item.days + ' 天')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.cool)
        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
  collectRow(item: TigerCollectItem, i: number) {
    Row() {
      Column() {
        Text(item.name)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text1)
        Text(item.age + ' · ' + item.origin)
          .fontSize(11)
          .fontColor(COLORS.text2)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Column() {
        Text('¥' + item.value)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.hot)
        Text(item.rank)
          .fontSize(10)
          .fontColor(COLORS.cardBg)
          .backgroundColor(rankColor(item.rank))
          .borderRadius(8)
          .padding({ left: 7, right: 7, top: 2, bottom: 2 })
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)
    .margin({ bottom: 8 })
  }

  @Builder
  orderRow(item: TigerOrderItem, 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: 460, 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.tigers.length + ' 款')
        ForEach(this.tigers, (item: ClothTigerItem, i: number) => {
          this.tigerRow(item, i)
        }, (item: ClothTigerItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 1) {
      Column() {
        this.sectionTitle('🎨', '吉祥纹样', '王字 · 云纹 · 铜钱')
        ForEach(this.patterns, (item: PatternItem, i: number) => {
          this.patternRow(item, i)
        }, (item: PatternItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 2) {
      Column() {
        this.sectionTitle('🧵', '针法针线', '平针 · 打籽 · 盘金')
        ForEach(this.needles, (item: NeedleItem, i: number) => {
          this.needleRow(item, i)
        }, (item: NeedleItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 3) {
      Column() {
        this.sectionTitle('👩‍🎨', '绣娘名录', '一针一线')
        ForEach(this.girls, (item: EmbroidressItem, i: number) => {
          this.girlRow(item, i)
        }, (item: EmbroidressItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 4) {
      Column() {
        this.sectionTitle('🏠', '布虎作坊', '黎侯 · 淮阳 · 凤翔')
        ForEach(this.ateliers, (item: AtelierItem, i: number) => {
          this.atelierRow(item, i)
        }, (item: AtelierItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 5) {
      Column() {
        this.sectionTitle('📋', '制作工序', '裁样到系铃')
        ForEach(this.steps, (item: CraftStepItem, i: number) => {
          this.stepRow(item, i)
        }, (item: CraftStepItem) => item.name)
      }
      .width('100%')
    } else if (this.curTab === 6) {
      Column() {
        this.sectionTitle('💎', '传世藏品', '估价榜')
        ForEach(this.collects, (item: TigerCollectItem, i: number) => {
          this.collectRow(item, i)
        }, (item: TigerCollectItem) => item.name)
      }
      .width('100%')
    } else {
      Column() {
        this.sectionTitle('📦', '最新订单', '共 ' + this.orders.length + ' 单')
        ForEach(this.orders, (item: TigerOrderItem, i: number) => {
          this.orderRow(item, i)
        }, (item: TigerOrderItem) => 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.tigerY : COLORS.sub)
        .margin({ top: 2 })
    }
    .width('25%')
    .padding({ top: 7, bottom: 7 })
    .backgroundColor(this.curTab === i ? '#962626' : 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.formStyle, placeholder: '如:坐虎' })
              .height(38)
              .fontSize(13)
              .margin({ top: 5 })
              .onChange((v: string) => {
                this.formStyle = v;
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .margin({ top: 12 })
          Column() {
            Text('主色')
              .fontSize(12)
              .fontColor(COLORS.text2)
            TextInput({ text: this.formColor, placeholder: '如:虎黄' })
              .height(38)
              .fontSize(13)
              .margin({ top: 5 })
              .onChange((v: string) => {
                this.formColor = v;
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .margin({ top: 12 })
          Column() {
            Text('售价')
              .fontSize(12)
              .fontColor(COLORS.text2)
            TextInput({ text: this.formPrice, placeholder: '如:168' })
              .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.tigers.splice(0, 0, new ClothTigerItem(this.formName, this.formStyle, '高 20cm', this.formColor, 168));
                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.selPattern === null ? '—' : this.selPattern.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.selPattern === null ? '—' : (this.selPattern.charm + '%'))
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(charmColor(this.selPattern === null ? 60 : this.selPattern.charm))
              .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.selPattern !== null) {
                  this.selPattern.charm = this.selPattern.charm + 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.selPattern !== null) {
                  this.selPattern.charm = this.selPattern.charm - 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 响应式编程体系的核心能力。@State 装饰器管理了十余个状态变量,涵盖导航索引(curTab)、动画驱动(breath)、模态控制(showAdd、showEdit、showDel)、选中对象(selPattern、selOrder)和表单数据(formName 等)等多种状态类型。@Observed 装饰器标注了八个数据模型类,使得对象属性的深层变化能够被自动追踪和响应。这种"@State 管引用、@Observed 管属性"的双层响应机制,构成了 ArkTS 响应式编程的基石。当用户修改纹样的吉祥度时,selPattern.charm 的变化通过 @Observed 机制被捕获,自动触发进度条宽度(charmBarW 重新计算)、进度条颜色(charmColor 重新计算)、百分比文字和色块颜色等一系列 UI 元素的更新,整个过程无需开发者编写任何刷新代码,充分体现了响应式编程"数据驱动视图"的核心理念。

UI 架构维度,本应用采用了"@Builder 方法模块化"的组织策略。十余个 @Builder 方法各司其职:modalOverlay 负责遮罩层、pageHeader 负责头部装饰、sectionTitle 负责区块标题、八个行卡片构建器负责各自的数据展示、tabContent 负责内容切换、bottomBar 负责底部导航、三个模态框构建器负责交互操作。这种将复杂 UI 拆分为多个 @Builder 方法的做法,使得每个方法的职责单一、逻辑清晰、易于维护。特别是参数化的 @Builder 方法(如 modalOverlay 接收 onClose 回调、sectionTitle 接收 icon/title/sub 参数),通过参数传递实现了同一构建器的不同行为,是代码复用的优秀实践。

动画设计维度,本应用的"呼吸"动画体系堪称亮点。单一 breath 布尔变量通过 460 毫秒间隔的定时器循环翻转,驱动了虎鼻的缩放、虎须的位移和透明度变化、进度条的闪烁、序号圆的脉动、装饰圆点的摇摆等多重动画效果。所有动画元素使用相同的 animation 参数(duration: 460, iterations: -1, playMode: PlayMode.Alternate),形成了全局同步的动画节奏。PlayMode.Alternate 播放模式的选择尤为关键——它使得动画在正向播放完毕后自动反向播放,形成连续平滑的往返运动,避免了跳变效果。这种以单一状态驱动全局动画的设计,简洁而高效,是动画工程化的优秀范例。

布局设计维度,本应用综合运用了 Stack 层叠布局、Flex 弹性布局(Row/Column 加 layoutWeight)、Scroll 滚动布局等多种布局方式。Stack 用于虎头装饰的复杂图形组合和模态框叠放,Flex 用于行卡片的信息排列和统计信息条的等分布局,Scroll 用于主内容区的垂直滚动。position 绝对定位使得虎头装饰中十余个元素各自精确定位、互不干扰;layoutWeight 弹性分配使得行卡片中左侧信息区占据弹性空间、右侧价格和按钮固定在行尾。圆角和间距的梯度式设计(大容器大圆角、小元素小圆角;外松内紧的间距节奏)使得界面既有统一性又有层次感。

文化融合维度,本应用最令人称道的是将传统布虎工艺的文化内涵与现代移动应用技术完美融合。色彩体系以传统民俗色彩为基调(米黄对应老棉布、深红对应漆器、暖橙对应虎身布料、哑金对应描金),以现代设计规范为骨架(三级文字色梯度、状态色语义区分、遮罩层透明度控制)。数据模型覆盖了布虎款式、吉祥纹样、针法针线、绣娘名录、布虎作坊、制作工序、传世藏品和商业订单八个维度,构成了从产品到工艺、从人到物、从历史到现实的立体化信息体系。头部虎头装饰以纯代码绘制,通过多个圆角矩形和圆形元素的组合,构建了一个包含虎头、虎耳、虎眼、"王"字、条纹、虎鼻、虎嘴、虎须和装饰圆点的完整虎头图形,辅以呼吸动画赋予生命力,堪称代码艺术的精品。

工程实践维度,本应用体现了多项软件工程最佳实践。集中式色彩管理(ColorPalette 接口加 COLORS 常量)确保了色彩的一致性和可维护性。数据驱动 UI(TabList 数组定义导航标签、For 遍历索引数组驱动装饰元素渲染)实现了数据与视图的解耦。纯函数式视觉计算(tasselX、charmBarW、charmColor 等工具函数)将视觉计算逻辑与组件逻辑分离,提高了可测试性和可组合性。防御性编程(selPattern 和 selOrder 的 null 检查)增强了代码的健壮性。统一的代码风格(所有模型类采用相同的构造函数范式、所有行卡片采用相同的布局结构)降低了阅读和维护成本。

展望未来,本应用的技术架构为进一步扩展奠定了坚实基础。在数据持久化方面,可以将当前的内存数据数组扩展为分布式数据存储或云数据库,实现数据的跨设备同步和持久保存。在网络通信方面,可以引入 Retrofit 或 Axios 等网络框架,实现与服务端 API 的交互,获取实时的布虎产品、订单和绣娘信息。在组件化方面,可以将行卡片构建器提取为独立的自定义组件,实现更细粒度的复用。在动画方面,可以引入更复杂的 Lottie 动画或属性动画,丰富视觉效果。在无障碍方面,可以增加 Accessibility 支持,为视障用户提供屏幕朗读功能。这些扩展方向都建立在当前架构的良好基础之上,得益于 @Observed/@State 响应式体系、@Builder 模块化 UI 和数据驱动渲染的设计理念,使得扩展能够以最小的代码改动实现最大的功能提升。

总而言之,本应用以布虎工艺为文化主题,以 ArkTS 为技术基石,通过精心设计的色彩体系、数据模型、状态管理、UI 组件和动画效果,在方寸屏幕间呈现了传统手工艺的丰富内涵。它不仅是一个功能完整的文化展示与管理应用,更是一份 HarmonyOS ArkTS 开发的优秀技术范例,值得开发者深入研读和借鉴。布虎工艺作为国家级非物质文化遗产,通过数字技术的赋能,必将在新时代焕发出更加璀璨的光彩。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐