抟泥成器:基于HarmonyOS ArkTS API 24的紫砂壶坊应用紫泥朱泥段泥暖色主题与单组件八Tab架构全记录

抟泥成器,一壶一世界。宜兴紫砂壶,是中国陶瓷艺术中的奇葩,始自北宋、盛于明清,距今已有六百余年历史。紫砂壶不仅是茶事利器,更是泥料陈腐、拍打成型、明针修整、龙窑烧造的综合载体——一泥一性、五色斑斓;一壶一型、方圆各胜。从供春树瘿到时大彬如意,从陈鸣远瓜果到顾景舟线圆,紫砂壶承载着中国人的茶道美学与匠人精神。本文完整记录一个基于鸿蒙HarmonyOS 6.1.1与HarmonyOS ArkTS API 24声明式框架构建的「紫砂壶坊」非遗手作平台,从紫泥朱泥段泥的紫砂暖色色彩主题,到壶形轮廓头部动画(壶身大圆scale呼吸脉冲+壶盖+壶钮+壶把弧+壶嘴+FIRE_IDX火焰光点translate位移),再到8Tab差异化布局(光器经典列表/花货集珍/筋囊之器/泥料行市陈腐年限横条/大师名录移除交互/壶艺工序难度横条/镇馆收藏估值横条/订单台账柱状图)、单组件@Builder架构、@Observed聚合数据类ZishaShopData、8个数据模型类、6个工具函数(agedBarW/valueBarH/craftBarW/clayColor/rankColor/statusColor)、壶形头部Stack层叠构建、三大弹窗(新增订单/编辑泥料/移除大师)、条件渲染模态遮罩、PlayMode.Alternate呼吸动画体系、订单金额柱状图可视化,附完整源码与开发环境搭建。


一、引言:抟泥成器,一壶一世界

宜兴紫砂壶,是中国陶瓷艺术中的奇葩,始自北宋、盛于明清,距今已有六百余年历史。紫砂壶的起源可追溯至北宋,宜兴丁蜀镇黄龙山的紫砂矿泥被发现可制茶器,至明代正德年间,供春(龚春)以银杏树瘿为形抟制树瘿壶,开紫砂壶艺之先河。紫砂壶不仅是茶事利器,更是泥料陈腐、拍打成型、明针修整、龙窑烧造的综合载体——一泥一性、五色斑斓;一壶一型、方圆各胜。从供春树瘿到时大彬如意,从陈鸣远瓜果到顾景舟线圆,从蒋蓉花货到朱可心报春,紫砂壶承载着中国人的茶道美学与匠人精神。明末时大彬创制大彬如意壶,开方圆兼济之先;清初陈鸣远以瓜果花木入壶,开自然写真之派;清嘉庆陈曼生与杨彭年合作曼生十八式,以文人书画入壶,开"字依壶传、壶随字贵"之文人壶风;现代顾景舟以线圆壶、提壁壶名世,被誉为"壶艺泰斗"。一部紫砂史,半部中国茶文化史。

紫砂泥料五色斑斓,是紫砂壶的灵魂。宜兴黄龙山所产紫砂矿泥,经风化、粉碎、陈腐、捶打之后,可呈现出紫、红、黄、绿、黑、青等斑斓色泽——紫泥#6B4A3A质朴沉稳,是紫砂最经典的底色,烧成后呈深紫褐色,透气性极佳,最适合泡乌龙茶与普洱茶;朱泥#B5573F红润娇艳,含铁量高烧成后呈朱红色,壶身致密适泡铁观音与红茶;段泥#C8B08A金黄温润,由紫泥与本山绿泥共生矿炼成,烧成后呈米黄色,适合泡绿茶与白茶;本山绿泥#C8B08A系罕见单矿,烧成后呈梨皮色,泥性娇脆成型不易;底槽清深紫带砂,是紫泥底层矿料砂感最足;降坡泥红黄相间,是修路降坡所出共生矿;天青泥#5A6B7A青灰如雨过天晴,是紫砂中最罕见的珍稀泥料;清水泥、黑泥、白泥、红泥各有千秋。十大泥料陈腐年限从4年到15年不等——天青泥陈腐15年最为珍贵,泥料陈腐越久越好用,陈腐使泥中有机物分解、可塑性增强、烧成后温润如玉。

紫砂制壶名家辈出,是紫砂壶艺的传承脉络。供春开紫砂壶艺之先河,所制树瘿壶被誉为"供春之壶,胜于金玉";时大彬明万历年间大家,创制大彬如意壶,将紫砂壶从大壶改为小壶,适合文人独啜;陈鸣远清初大家,以瓜果花木入壶,所制南瓜壶、梅桩壶开自然写真之派;陈曼生清嘉庆年间县令,与杨彭年合作创制曼生十八式,以文人书画入壶;邵大亨清嘉道年间大家,所制大亨掇球壶气度恢宏;程寿珍民国大家,所制掇球壶简洁圆浑;顾景舟现代壶艺泰斗,所制线圆壶、提壁壶线条流畅;蒋蓉现代花货大家,所制荷花壶、莲藕壶栩栩如生;裴石民民国大家,所制小石瓢古朴雅致;朱可心现代大家,所制报春壶生机盎然。八位制壶名家贯穿明正德至现代五百年,完整呈现紫砂壶艺的传承脉络。

然而,随着现代生活方式变迁,紫砂壶这门需要"泥性、火候、工手"三重匠艺的传统手作正面临传承困境——年轻一代不了解紫砂壶文化,制壶匠师缺乏展示与销售渠道,紫砂壶泥料配方不透明、壶型分类混乱、名家信息分散、收藏估值缺乏参考,正成为紫砂非遗传承的现实痛点。一个优秀的紫砂壶手作平台,不仅需要覆盖从"光器经典"到"泥料行市"再到"壶艺工序"的完整产品展示闭环,还要在花货花器、筋囊之器、大师名录、镇馆收藏、订单台账等维度做到精准而高效。它需要以光器经典为核心入口,横向扩展出泥料、大师、壶艺、收藏、订单八大功能模块,每个模块采用完全不同的布局风格,形成一个完整闭环的中国紫砂壶非遗手作生态。

正是在这样的背景下,紫砂壶坊类非遗手作平台应运而生。它以光器经典(石瓢、西施、仿古、掇球、井栏、汉铎、秦权、德钟、茄段、乳鼎、虚扁、汉瓦十二款)为核心入口,横向扩展出花货集珍(南瓜壶、梅桩壶、松段壶、竹段壶、葡萄壶、荷花壶、柿子壶、石榴壶八款花器)、筋囊之器(菊蕾、合菱、水仙、玉兰、葵花、百合、菱花、海棠八款筋囊器)、泥料行市(紫泥、红泥、段泥、底槽清、朱泥、降坡泥、绿泥、黑泥、清水泥、天青泥十种泥料陈腐年限横条+编辑交互)、大师名录(顾抟泥、蒋拍筒、汪明针、徐琢嘴、吕镶身、曹窑烧、李整口、吴调砂八位匠人+移除交互)、壶艺工序(抟泥、拍身筒、镶身筒、琢嘴把、明针、开孔、窑烧、整口八道工序难度横条)、镇馆收藏(供春树瘿壶、大彬如意壶、曼生石瓢、掇球壶、线圆壶、僧帽壶、小石瓢、报春壶八件传世名作估值横条)、订单台账(十笔定制订单+订单金额柱状图+新增交互)八大功能模块。每个模块采用完全不同的布局风格——光器=在售列表+统计行、花器=在售列表+花形分隔、筋囊=在售列表+横向位移呼吸、泥料=陈腐年限横条+编辑交互、大师=名录列表+移除交互、壶艺=难度指数横条、收藏=估值横条、订单=柱状图+新增交互+台账列表,形成了一个完整闭环的中国紫砂非遗手作生态。它不仅是一个功能完备的演示项目,更是一份典型的ArkUI工程实践范本,涵盖了单组件@Builder架构设计、@Observed聚合数据类、6个颜色与尺寸映射函数、壶形头部Stack层叠动画、条件渲染模态弹窗、layoutWeight弹性进度条、ForEach列表渲染、订单柱状图可视化等关键技术点。

在这里插入图片描述


二、应用整体架构概览

整个应用采用了"单组件@Builder架构 + @Observed聚合数据类 + 配置驱动 + 工具函数抽离"的四层设计模式。入口组件ZishaShopApp作为整个应用的根容器,是唯一一个@Entry @Component装饰的组件,负责管理当前激活的Tab状态、呼吸动画开关amb、三个弹窗的显隐状态、表单数据以及聚合数据对象。与香囊坊多组件架构(1入口+1标签+8内容子组件共10个组件)不同,本应用将所有8个Tab的内容页面全部内联为@Builder contentArea()方法内的if-else if条件分支,由this.activeTab索引驱动条件渲染切换。这种单组件@Builder架构虽然把所有UI逻辑集中在一个struct内,但通过@Builder方法拆分(headerArea/contentArea/bottomBar/bottomItem/addOrderModal/editClayModal/removeMasterModal/modalOverlay八个@Builder方法)保持了代码的可读性,是ArkUI中小型应用的典型实践。

应用一共划分为八个一级页面:光器(Tab 0)、花器(Tab 1)、筋囊(Tab 2)、泥料(Tab 3)、大师(Tab 4)、壶艺(Tab 5)、收藏(Tab 6)、订单(Tab 7)。底部bottomBar采用双行四列布局——ROW1_IDX [0,1,2,3] 为第一行,ROW2_IDX [4,5,6,7] 为第二行,每个Tab项由@Builder bottomItem(i)构建,包含图标(18号Emoji)、标签文字(11号)、选中指示条(16x3圆角2朱泥色小条)三部分。激活态文字zini紫泥色加粗+指示条显示,非激活态textSub灰棕色普通+指示条透明隐藏,点击切换activeTab触发contentArea重新渲染。八个Tab每个布局完全不同——光器采用在售列表+顶部统计三列(12款光器经典/石瓢最热款/¥9.8w本季成交)、花器采用花货列表+花形分隔线、筋囊采用筋纹列表+横向translate位移呼吸、泥料采用陈腐年限横条+编辑交互(10种泥料,agedBarW年限宽度映射,clayColor泥料着色)、大师采用大师名录+移除交互(8位匠人,rankColor等级着色)、壶艺采用难度指数横条(8道工序,craftBarW难度宽度映射)、收藏采用估值横条(8件传世名作,valueBarH估值高度映射)、订单采用柱状图+新增交互+台账列表(10笔订单,valueBarH金额柱高映射,statusColor状态着色),这种"一页一型"的差异化设计最大程度还原了真实中国紫砂壶非遗手作平台的交互体验。

三大弹窗

八大内容页面(contentArea内if分支)

入口组件 ZishaShopApp (@Entry @Component)

工具函数层

agedBarW 陈腐年限→宽度

valueBarH 估值/金额→高度

craftBarW 难度→宽度

clayColor 泥料名→颜色

rankColor 等级→颜色

statusColor 状态→颜色

数据层 @Observed

ZishaShopData
8个数组·74条数据+3方法

plains 光器12款

flowers 花器8款

ribs 筋囊8款

clays 泥料10种

masters 大师8位

crafts 工序8道

collects 收藏8件

orders 订单10笔

@Builder 方法拆分

bottomItem 单个Tab项

modalOverlay 通用遮罩

headerArea 壶形头部

addOrderModal 新增订单弹窗

editClayModal 编辑泥料弹窗

removeMasterModal 移除大师弹窗

contentArea 八页内容

bottomBar 底部导航

头部区域 headerArea
壶形轮廓Stack:壶身大圆scale呼吸+壶盖+壶钮+壶把弧+壶嘴+FIRE_IDX火焰光点

内容区域 contentArea
八Tab if-else if条件渲染切换

底部导航 bottomBar
双行四列布局 ROW1_IDX/ROW2_IDX

光器 Tab0
在售列表+统计三列

花器 Tab1
花货列表+花形分隔

筋囊 Tab2
筋纹列表+横向位移

泥料 Tab3
陈腐横条+编辑

大师 Tab4
大师名录+移除

壶艺 Tab5
难度指数横条

收藏 Tab6
估值横条

订单 Tab7
柱状图+新增+台账

addOrderModal
新增订单

editClayModal
编辑泥料

removeMasterModal
移除大师

在数据层面,应用独创了**@Observed聚合数据类**设计——ZishaShopData一个类包含plains/flowers/ribs/clays/masters/crafts/collects/orders八个数组,每个数组8-12条数据,全部用new构造函数初始化(与香囊坊用对象字面量不同),@Observed装饰整个类。配合8个@Observed装饰的数据模型类(PlainPotItem/FlowerPotItem/RibPotItem/ClayItem/MasterItem/CraftItem/CollectItem/OrderItem),每个数据项都是可观察的实例对象。数据规模达74条(12+8+8+10+8+8+8+10),完整呈现中国紫砂壶非遗手作平台的数据生态。ZishaShopData还定义了3个数据操作方法——addOrder(新增订单,unshift插入数组头部)、editClay(编辑泥料,按索引修改name和price)、removeMaster(移除大师,splice删除指定索引),分别对应三个弹窗的提交动作,形成"弹窗提交→数据操作→列表刷新"的完整响应式数据流。

在颜色逻辑层面,应用定义了6个颜色与尺寸映射函数(agedBarW陈腐年限宽度映射/valueBarH估值金额高度映射/craftBarW难度宽度映射/clayColor泥料名颜色映射/rankColor等级颜色映射/statusColor状态颜色映射),将颜色和尺寸映射逻辑从UI层抽离,每个函数根据不同维度(年限/估值/难度/泥料名/等级/状态)返回对应的颜色值或像素宽度,实现"数据即配色、数据即尺寸"的动态映射模式。应用配色采用"紫泥朱泥段泥暖色"紫砂非遗主题——bg #F3EDE1 米褐色(模拟古绢泥料温润质感),zini #6B4A3A 紫泥(紫砂最经典底色),zhuni #B5573F 朱泥(红润娇艳强调色),duanni #C8B08A 段泥(金黄温润装饰色),deep #3E2A20 深褐(头部壶形深色底),textMain #46332A 深褐(主文字色),五色各司其职形成"以紫泥定壶、以朱泥定强调、以段泥定饰、以深褐定头、以米褐定底"的中国紫砂语义着色体系。

在交互层面,应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环:addOrderModal新增订单(四字段TextInput表单:订单编号/客户姓名/定制壶型/定金金额,提交订单zini紫泥底)、editClayModal编辑泥料(两字段TextInput表单:泥料名称/单价,保存修改zini紫泥底)、removeMasterModal移除大师(确认对话框,确认移除danger朱红底)。三个弹窗都是@Builder方法内联在入口组件中,通过if条件渲染控制显隐(showAddOrder/showEditClay/showRemoveMaster三个@State布尔变量),遮罩用#55000000(55%黑)硬编码,弹窗主体与遮罩用Stack层叠,整体zIndex(999)覆盖主内容。订单页"新增订单"点击触发addOrderModal,泥料页"编辑"点击触发editClayModal(clayIdx记录目标索引,clayName/clayPrice预填当前值),大师页"移除"点击触发removeMasterModal(masterIdx记录目标索引),形成了完整的"新增订单→编辑泥料→移除大师"交互闭环。


三、配色体系:紫泥朱泥段泥暖色主题

3.1 色彩接口定义

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

interface ZishaPalette {
  bg: string;
  cardBg: string;
  zini: string;
  zhuni: string;
  duanni: string;
  deep: string;
  textMain: string;
  textSub: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
}

在这里插入图片描述

ZishaPalette接口共定义15个色彩字段,比香囊坊NangPalette的16字段略少1个(香囊坊多了cream奶油白字段,本应用用cardBg白底兼作表单背景)。接口将整个应用的色彩抽象为结构化契约,包括背景层(bg/cardBg/deep)、主题色层(zini/zhuni/duanni三色对应紫砂三大经典泥料)、文字层(textMain/textSub/textHint三级灰度)、功能色层(success/warning/danger三态)和辅助色(border/white)。这种"接口先行"的设计保证了色彩规范的类型安全,任何使用COLORS常量的地方都有完整的类型提示,是ArkTS强类型语言优势的典型体现。

3.2 色彩常量定义与紫砂泥料文化含义

const COLORS: ZishaPalette = {
  bg: '#F3EDE1',
  cardBg: '#FFFFFF',
  zini: '#6B4A3A',
  zhuni: '#B5573F',
  duanni: '#C8B08A',
  deep: '#3E2A20',
  textMain: '#46332A',
  textSub: '#8A7160',
  textHint: '#B5A08C',
  border: '#E7DCC8',
  success: '#5E7B4A',
  warning: '#B57B2E',
  danger: '#A83A2E',
  white: '#FFFFFF'
};

本应用配色最大特色是紫泥 zini #6B4A3A 与朱泥 zhuni #B5573F 与段泥 duanni #C8B08A三色对应紫砂三大经典泥料——紫泥用于按钮/价格/指示条/选中态文字(是紫砂最经典的深紫褐色调,烧成后质朴沉稳透气性佳),朱泥用于最热款标识/估值/收藏价格/选中指示条(是紫砂中含铁量最高的红润色调,烧成后娇艳致密),段泥用于头部标题"紫砂壶坊"/壶钮(是紫泥与本山绿泥共生矿的金黄色调,烧成后温润如玉),与米褐色底bg #F3EDE1形成"紫泥朱泥段泥暖色"的中国紫砂经典配色。下面逐一解析每个色值的紫砂文化含义:

  • bg #F3EDE1 米褐色:整个应用的底色基调,模拟紫砂矿泥风化后的米褐温润质感,呼应紫砂壶"抟泥成器"的泥料本源。米褐色比纯白更柔和、比深褐更明亮,营造出古朴温润的紫砂坊氛围。
  • cardBg #FFFFFF 纯白:卡片背景色,与米褐色底形成层次。纯白卡片在米褐底上浮起,模拟紫砂壶在木案上陈列的视觉效果。
  • zini #6B4A3A 紫泥:紫砂最经典的底色,是本应用的主色调。紫泥是宜兴黄龙山产量最大的紫砂矿料,烧成后呈深紫褐色,透气性极佳,最适合泡乌龙茶与普洱茶。应用中用于价格文字、提交按钮底色、选中态文字色、订单柱状图偶数柱、大师年限数字色,是承载"价值与确认"语义的主色。
  • zhuni #B5573F 朱泥:紫砂中含铁量最高的红润色调,烧成后呈朱红色,壶身致密适泡铁观音与红茶。应用中用于最热款标识"石瓢"、收藏估值价格、选中指示条、花器分隔符号"✿"、订单柱状图奇数柱,是承载"强调与点缀"语义的亮色。
  • duanni #C8B08A 段泥:由紫泥与本山绿泥共生矿炼成的金黄色调,烧成后呈米黄色,适合泡绿茶与白茶。应用中用于头部"紫砂壶坊"标题文字(4号字距letterSpacing)、壶钮小圆,是承载"标题与点缀"语义的暖色,呼应段泥"金黄温润"的视觉气质。
  • deep #3E2A20 深褐:头部壶形区域的深色底色,比紫泥更深沉,模拟紫砂壶在窑火中烧成的深褐釉色。头部用深褐底配紫泥壶身+朱泥壶盖+段泥壶钮,形成紫砂壶在深色窑背景下的立体壶形轮廓。
  • textMain #46332A 深褐:主文字色,非纯黑更有文化感。深褐色比纯黑更柔和,呼应紫砂泥料的褐色基调,用于卡片标题、列表项名称等主要文字。
  • textSub #8A7160 灰棕:副文字色,用于描述信息。灰棕色是深褐色的浅化版本,用于"子冶石瓢 · 紫泥"这类副信息,与主文字形成灰度层次。
  • textHint #B5A08C 浅灰褐:提示文字色,用于辅助信息。浅灰褐色是灰棕色的进一步浅化,用于"光器经典""点击可编辑"等提示文字,是三级灰度中最淡的层次。
  • border #E7DCC8 米褐边框:卡片边框色,比bg更深一档的米褐色,形成柔和的卡片轮廓。米褐边框比纯灰边框更贴合紫砂暖色主题。
  • success #5E7B4A 苔绿:成功状态色,用于"已交付"订单状态徽章。苔绿色模拟紫砂壶烧成后的窑变青苔色,与紫砂暖色主题协调。
  • warning #B57B2E 赭金:警告状态色,用于"窑烧中"订单状态徽章和FIRE_IDX火焰光点。赭金色模拟龙窑窑火的火光,是紫砂"窑烧"工序的视觉隐喻。
  • danger #A83A2E 朱红:危险状态色,用于"确认移除"按钮和"移除"操作文字。朱红色比朱泥更深沉,是承载"危险与删除"语义的功能色。
  • white #FFFFFF 纯白:白色文字色,用于按钮文字、状态徽章文字、统计数字等需要高对比度的场景。

3.3 颜色分级函数体系

应用定义了6个颜色与尺寸映射函数,将颜色与尺寸映射逻辑从UI层抽离:

函数名 输入维度 分级逻辑 输出 用途
agedBarW 陈腐年限(年) min(170, a×10) 像素宽度 泥料陈腐年限横条宽度
valueBarH 估值/金额(元) min(110, v/100000) 像素高度 收藏估值横条高度(注:收藏页实际用v/60000)/订单柱状图柱高
craftBarW 难度指数 min(160, d×1.5) 像素宽度 壶艺工序难度横条宽度
clayColor 泥料名 朱泥/红泥→zhuni;段泥/绿泥→duanni;天青→#5A6B7A;其他→zini 颜色值 泥料名→颜色映射
rankColor 等级 大师→zhuni;名师→success;其他→textSub 颜色值 大师等级/工序难度等级颜色
statusColor 状态 已交付→success;定制中→#8E6FB0紫;窑烧中→warning;其他→zini 颜色值 订单状态徽章颜色

六个函数形成了统一的"动态映射"设计语言——agedBarW/valueBarH/craftBarW将数值映射为像素尺寸,clayColor/rankColor/statusColor将语义映射为颜色值。clayColor是最具紫砂特色的函数,它根据泥料名称中的关键字(朱泥/红泥/段泥/绿泥/天青)返回对应的紫砂色值,实现"泥料名即颜色"的紫砂语义着色;statusColor中"定制中"用#8E6FB0紫色(区别于紫泥的褐色,表示"未开始抟泥"的待定态),"窑烧中"用warning赭金(呼应龙窑窑火的火光),"抟泥中/明针中/镶身中"用zini紫泥(表示"制作中"的紫砂泥料态),状态色与紫砂制作工序语义高度对应。


四、导航Tab设计:八Tab双行四列布局

4.1 Tab元数据接口与TAB_LIST数组

在这里插入图片描述

应用通过TabMeta接口和TAB_LIST数组定义了八个Tab的元数据配置:

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

const TAB_LIST: TabMeta[] = [
  { label: '光器', icon: '🫖' },
  { label: '花器', icon: '🌸' },
  { label: '筋囊', icon: '🌀' },
  { label: '泥料', icon: '🧱' },
  { label: '大师', icon: '🧑‍🎨' },
  { label: '壶艺', icon: '🖐️' },
  { label: '收藏', icon: '🏛️' },
  { label: '订单', icon: '📦' }
];

TabMeta接口定义了label(中文标签)和icon(Emoji图标)两个字段。与香囊坊NangTabMeta的key/icon/label三字段不同,本应用省略了key字段,因为采用单组件@Builder架构时不需要通过key路由到不同子组件,直接用activeTab索引即可驱动条件渲染。八个Tab的命名完整覆盖了紫砂壶艺的分类体系——光器(几何造型壶)、花器(仿生花塑壶)、筋囊(筋纹花瓣壶)是紫砂壶的三大造型门类;泥料是紫砂壶的原料基础;大师是制壶匠师名录;壶艺是制作工序;收藏是传世名作;订单是定制交易。八个Tab从"成品壶型→原料→匠人→工艺→收藏→交易"形成了紫砂壶坊的完整业务闭环,每个Tab的Emoji也精心挑选——🫖茶壶代表光器、🌸花朵代表花器、🌀螺旋代表筋囊、🧱砖块代表泥料、🧑‍🎨匠人代表大师、🖐️手掌代表壶艺工序、🏛️博物馆代表收藏、📦包裹代表订单,图标语义清晰。

4.2 双行四列布局索引

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

ROW1_IDX和ROW2_IDX将八个Tab分为两行四列——第一行[0,1,2,3]对应光器/花器/筋囊/泥料(成品壶型与原料),第二行[4,5,6,7]对应大师/壶艺/收藏/订单(匠人与交易),这种分组体现了"上半行是物(壶型泥料)、下半行是人(匠人工艺交易)"的业务逻辑分层。FIRE_IDX [0,1,2] 是头部壶形动画中火焰光点的索引数组,用于ForEach渲染3个火焰光点,模拟龙窑窑火。TOP_COLLECT_IDX [0,1,2,3,4,5] 是订单页柱状图的索引数组,用于ForEach渲染6根订单金额柱,对应前6笔订单的金额对比。这四个索引常量将布局配置数据化,避免了魔数散落在代码各处,是配置驱动设计的体现。

4.3 底部导航构建

@Builder
bottomItem(i: number) {
  Column() {
    Text(TAB_LIST[i].icon).fontSize(18)
      .opacity(this.activeTab === i ? 1 : 0.55)
    Text(TAB_LIST[i].label).fontSize(11)
      .fontColor(this.activeTab === i ? COLORS.zini : COLORS.textSub)
      .fontWeight(this.activeTab === i ? FontWeight.Bold : FontWeight.Normal)
      .margin({ top: 3 })
    Column()
      .width(16).height(3).borderRadius(2)
      .backgroundColor(this.activeTab === i ? COLORS.zhuni : '#00000000')
      .margin({ top: 4 })
  }
  .layoutWeight(1)
  .padding({ top: 8, bottom: 8 })
  .onClick(() => {
    this.activeTab = i;
  })
}

bottomItem是单个Tab项的@Builder构建方法,采用Column纵向布局——第一层是18号Emoji图标(激活态opacity 1完全不透明,非激活态opacity 0.55半透明),第二层是11号中文标签(激活态zini紫泥色加粗,非激活态textSub灰棕色普通),第三层是16x3圆角2的选中指示条(激活态zhuni朱泥色显示,非激活态#00000000透明隐藏)。整个Tab项用layoutWeight(1)弹性宽度均分底部空间,padding上下8,点击切换activeTab索引。指示条用朱泥色而非紫泥色,是因为朱泥的红润色调在底部导航中更醒目,能更清晰地标识当前激活的Tab,这是色彩功能分工的精细设计。

@Builder
bottomBar() {
  Column() {
    Row() {
      ForEach(ROW1_IDX, (i: number) => {
        this.bottomItem(i)
      }, (i: number) => i.toString())
    }
    .width('100%')
    Row() {
      ForEach(ROW2_IDX, (i: number) => {
        this.bottomItem(i)
      }, (i: number) => i.toString())
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg)
  .borderRadius({ topLeft: 20, topRight: 20 })
  .border({ width: 1, color: COLORS.border })
}

bottomBar用Column包裹两个Row——第一个Row用ForEach遍历ROW1_IDX渲染前四个Tab,第二个Row用ForEach遍历ROW2_IDX渲染后四个Tab,形成双行四列布局。整个底部导航用cardBg白底+topLeft/topRight 20圆角(顶部圆角模拟紫砂壶托盘的弧形边缘)+border边框,视觉上像一个浮起的紫砂壶陈列托盘。ForEach的key用i.toString()索引字符串,保证Tab项稳定渲染不闪烁。


五、数据接口定义:八大数据模型

应用定义了8个数据接口对应的@Observed可观察类,覆盖紫砂壶坊的全部业务实体。每个类都用@Observed装饰器装饰,确保实例属性变更时能触发响应式刷新。

5.1 光器PlainPotItem

@Observed
export class PlainPotItem {
  name: string = '';
  style: string = '';
  clay: string = '';
  price: number = 0;

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

PlainPotItem是光器(几何造型壶)的数据模型,包含四个字段——name壶名(如"石瓢")、style款式全名(如"子冶石瓢")、clay泥料(如"紫泥")、price价格。每个字段都有默认初始值(空字符串或0),保证类实例化时的类型安全。constructor构造函数接收四个参数赋值给对应字段。@Observed装饰器使PlainPotItem实例的属性变更可被ArkUI框架观察,当price或name变化时,引用该实例的UI会自动刷新。光器是紫砂壶三大造型门类中最经典的——石瓢、西施、仿古、掇球、井栏、汉铎、秦权、德钟、茄段、乳鼎、虚扁、汉瓦十二款光器,覆盖了紫砂光器最经典的壶型谱系。

5.2 花器FlowerPotItem

@Observed
export class FlowerPotItem {
  name: string = '';
  theme: string = '';
  clay: string = '';
  price: number = 0;

  constructor(name: string, theme: string, clay: string, price: number) {
    this.name = name;
    this.theme = theme;
    this.clay = clay;
    this.price = price;
  }
}

FlowerPotItem是花器(仿生花塑壶)的数据模型,与PlainPotItem结构相似但将style字段替换为theme主题字段。theme记录花器的仿生主题(如"瓜藤缠枝"“寒梅报春”),体现花器"以自然物象入壶"的仿生特色。花器是紫砂壶三大造型门类中最具艺术性的——南瓜壶、梅桩壶、松段壶、竹段壶、葡萄壶、荷花壶、柿子壶、石榴壶八款花器,以瓜果花木为形,栩栩如生。

5.3 筋囊RibPotItem

@Observed
export class RibPotItem {
  name: string = '';
  ribs: number = 0;
  clay: string = '';
  price: number = 0;

  constructor(name: string, ribs: number, clay: string, price: number) {
    this.name = name;
    this.ribs = ribs;
    this.clay = clay;
    this.price = price;
  }
}

RibPotItem是筋囊(筋纹花瓣壶)的数据模型,核心特色是ribs筋纹瓣数字段。筋囊器是紫砂壶三大造型门类中最讲究几何对称的——菊蕾壶16瓣、合菱壶18瓣、水仙壶12瓣、玉兰壶14瓣、葵花壶20瓣、百合壶16瓣、菱花壶18瓣、海棠壶14瓣八款筋囊器,筋纹瓣数从12瓣到20瓣不等,瓣数越多制作难度越大。ribs字段是筋囊器区别于光器与花器的关键属性,UI中渲染为"16 瓣筋纹 · 紫泥"的副信息。

5.4 泥料ClayItem

@Observed
export class ClayItem {
  name: string = '';
  aged: number = 0;
  price: number = 0;
  stock: number = 0;

  constructor(name: name: string, aged: number, price: number, stock: number) {
    this.name = name;
    this.aged = aged;
    this.price = price;
    this.stock = stock;
  }
}

ClayItem是泥料的数据模型,包含四个字段——name泥料名(如"紫泥")、aged陈腐年限(年)、price单价(元/斤)、stock库存(斤)。aged陈腐年限是泥料的核心指标——紫砂泥料陈腐越久越好用,陈腐使泥中有机物分解、可塑性增强、烧成后温润如玉。aged字段在UI中通过agedBarW函数映射为陈腐年限横条宽度,是泥料页的核心可视化维度。ten种泥料陈腐年限从4年(清水泥)到15年(天青泥)不等。

5.5 大师MasterItem

@Observed
export class MasterItem {
  name: string = '';
  style: string = '';
  years: number = 0;
  works: number = 0;
  rank: string = '';

  constructor(name: string, style: string, years: number, works: number, rank: string) {
    this.name = name;
    this.style = style;
    this.years = years;
    this.works = works;
    this.rank = rank;
  }
}

MasterItem是制壶大师的数据模型,包含五个字段——name匠师名(如"顾抟泥")、style擅长工艺(如"光器")、years从业年限(年)、works作品数量(件)、rank等级(大师/名师/匠人三级)。rank等级字段是MasterItem的核心特色,通过rankColor函数映射为不同颜色——大师用zhuni朱泥色(最高等级,红润醒目),名师用success苔绿色(中高等级,沉稳健康),匠人用textSub灰棕色(基础等级,低调朴素)。八位匠师名采用"姓+工艺"的命名方式——顾抟泥(抟泥成型)、蒋拍筒(拍身筒)、汪明针(明针修整)、徐琢嘴(嘴把安装)、吕镶身(镶身筒)、曹窑烧(窑温控制)、李整口(整口调平)、吴调砂(调砂配泥),每个名字都点明匠师的专长工艺,是紫砂壶艺"工序分工"的体现。

5.6 工序CraftItem

@Observed
export class CraftItem {
  name: string = '';
  desc: string = '';
  diff: number = 0;
  rank: string = '';

  constructor(name: string, desc: string, diff: number, rank: string) {
    this.name = name;
    this.desc = desc;
    this.diff = diff;
    this.rank = rank;
  }
}

CraftItem是制壶工序的数据模型,包含四个字段——name工序名(如"拍身筒")、desc工序描述(如"拍打成型")、diff难度指数(0-100)、rank难度等级(基础/中等/高难三级)。diff难度指数是CraftItem的核心特色,通过craftBarW函数映射为难度横条宽度,难度越高横条越长。八道工序难度从30(抟泥炼泥陈腐,基础)到90(明针刮压修整,高难)不等——明针90最高(明针是紫砂壶表面光洁度的关键工序,需用牛角明针反复刮压,难度极高)、镶身筒85次之(泥片镶接需精确计算尺寸,稍有偏差即废)、拍身筒80第三(拍打成型是紫砂壶独有的成型工艺,需掌控泥性火候)。

5.7 收藏CollectItem

@Observed
export class CollectItem {
  name: string = '';
  author: string = '';
  era: string = '';
  value: number = 0;

  constructor(name: string, author: string, era: string, value: number) {
    this.name = name;
    this.author = author;
    this.era = value;
    this.value = value;
  }
}

CollectItem是镇馆收藏的数据模型,包含四个字段——name藏品名(如"供春树瘿壶")、author作者(如"供春")、era年代(如"明正德")、value估值(元)。value估值是CollectItem的核心特色,在UI中通过Math.min(170, co.value / 60000)映射为估值横条宽度——估值越高横条越长。八件镇馆收藏估值从180万(裴石民小石瓢)到880万(顾景舟线圆壶)不等,顾景舟线圆壶880万估值最高(顾景舟是现代壶艺泰斗,线圆壶是其代表作),供春树瘿壶680万次之(供春是紫砂壶艺始祖,树瘿壶是传世孤品)。

5.8 订单OrderItem

@Observed
export class OrderItem {
  no: string = '';
  buyer: string = '';
  piece: string = '';
  amount: number = 0;
  status: string = '';

  constructor(no: string, buyer: string, piece: string, amount: number, status: string) {
    this.no = no;
    this.buyer = buyer;
    this.piece = piece;
    this.amount = amount;
    this.status = status;
  }
}

OrderItem是定制订单的数据模型,包含五个字段——no订单编号(如"Z20260801")、buyer客户姓名(如"陈先生")、piece定制壶型(如"石瓢")、amount定金金额(元)、status订单状态(抟泥中/窑烧中/已交付/明针中/镶身中/定制中六态)。status订单状态是OrderItem的核心特色,通过statusColor函数映射为不同颜色——已交付用success苔绿色(完成态,健康良好)、定制中用#8E6FB0紫色(待定态,未开始抟泥)、窑烧中用warning赭金色(关键工序,呼应龙窑窑火)、抟泥中/明针中/镶身中用zini紫泥色(制作中,紫砂泥料态)。十笔订单覆盖了紫砂壶从"定制→抟泥→明针→镶身→窑烧→交付"的完整制作流程。


六、@Observed可观察数据类:ZishaShopData

6.1 聚合数据类定义

应用独创了@Observed聚合数据类设计——ZishaShopData一个类包含八个数组,全部用new构造函数初始化:

@Observed
export class ZishaShopData {
  plains: PlainPotItem[] = [
    new PlainPotItem('石瓢', '子冶石瓢', '紫泥', 6800),
    new PlainPotItem('西施', '倒把西施', '朱泥', 5200),
    new PlainPotItem('仿古', '大仿古', '底槽清', 8800),
    new PlainPotItem('掇球', '大亨掇球', '紫泥', 12800),
    new PlainPotItem('井栏', '曼生井栏', '段泥', 9800),
    new PlainPotItem('汉铎', '汉铎壶', '紫泥', 7600),
    new PlainPotItem('秦权', '秦权壶', '清水泥', 8600),
    new PlainPotItem('德钟', '德钟壶', '紫泥', 7200),
    new PlainPotItem('茄段', '茄段壶', '降坡泥', 5600),
    new PlainPotItem('乳鼎', '乳鼎壶', '底槽清', 10800),
    new PlainPotItem('虚扁', '虚扁壶', '朱泥', 9800),
    new PlainPotItem('汉瓦', '汉瓦壶', '紫泥', 6200)
  ];
  flowers: FlowerPotItem[] = [
    new FlowerPotItem('南瓜壶', '瓜藤缠枝', '段泥', 16800),
    new FlowerPotItem('梅桩壶', '寒梅报春', '紫泥', 19800),
    new FlowerPotItem('松段壶', '苍松劲节', '紫泥', 18800),
    new FlowerPotItem('竹段壶', '高风亮节', '绿泥', 15800),
    new FlowerPotItem('葡萄壶', '硕果累累', '段泥', 13800),
    new FlowerPotItem('荷花壶', '出水芙蓉', '红泥', 12800),
    new FlowerPotItem('柿子壶', '事事如意', '朱泥', 11800),
    new FlowerPotItem('石榴壶', '多子多福', '红泥', 12800)
  ];
  ribs: RibPotItem[] = [
    new RibPotItem('菊蕾壶', 16, '紫泥', 15800),
    new RibPotItem('合菱壶', 18, '段泥', 16800),
    new RibPotItem('水仙壶', 12, '绿泥', 13800),
    new RibPotItem('玉兰壶', 14, '白泥', 14800),
    new RibPotItem('葵花壶', 20, '朱泥', 18800),
    new RibPotItem('百合壶', 16, '段泥', 15800),
    new RibPotItem('菱花壶', 18, '紫泥', 16800),
    new RibPotItem('海棠壶', 14, '红泥', 13800)
  ];
  clays: ClayItem[] = [
    new ClayItem('紫泥', 8, 280, 40),
    new ClayItem('红泥', 6, 240, 50),
    new ClayItem('段泥', 7, 260, 45),
    new ClayItem('底槽清', 12, 420, 20),
    new ClayItem('朱泥', 10, 380, 25),
    new ClayItem('降坡泥', 5, 320, 35),
    new ClayItem('绿泥', 9, 300, 30),
    new ClayItem('黑泥', 11, 360, 22),
    new ClayItem('清水泥', 4, 200, 60),
    new ClayItem('天青泥', 15, 520, 12)
  ];
  masters: MasterItem[] = [
    new MasterItem('顾抟泥', '光器', 45, 260, '大师'),
    new MasterItem('蒋拍筒', '花器', 38, 210, '大师'),
    new MasterItem('汪明针', '明针功夫', 40, 320, '大师'),
    new MasterItem('徐琢嘴', '嘴把流', 30, 150, '名师'),
    new MasterItem('吕镶身', '筋囊器', 32, 170, '名师'),
    new MasterItem('曹窑烧', '窑温控制', 28, 90, '名师'),
    new MasterItem('李整口', '整口调平', 22, 110, '匠人'),
    new MasterItem('吴调砂', '调砂配泥', 26, 130, '匠人')
  ];
  crafts: CraftItem[] = [
    new CraftItem('抟泥', '炼泥陈腐', 30, '基础'),
    new CraftItem('拍身筒', '拍打成型', 80, '高难'),
    new CraftItem('镶身筒', '泥片镶接', 85, '高难'),
    new CraftItem('琢嘴把', '嘴把安装', 75, '高难'),
    new CraftItem('明针', '刮压修整', 90, '高难'),
    new CraftItem('开孔', '气孔壶口', 45, '中等'),
    new CraftItem('窑烧', '龙窑控温', 65, '中等'),
    new CraftItem('整口', '磨口调平', 50, '中等')
  ];
  collects: CollectItem[] = [
    new CollectItem('供春树瘿壶', '供春', '明正德', 6800000),
    new CollectItem('大彬如意壶', '时大彬', '明万历', 5200000),
    new CollectItem('曼生石瓢', '陈曼生', '清嘉庆', 4800000),
    new CollectItem('掇球壶', '程寿珍', '民国', 2600000),
    new CollectItem('线圆壶', '顾景舟', '现代', 8800000),
    new CollectItem('僧帽壶', '蒋蓉', '现代', 3200000),
    new CollectItem('小石瓢', '裴石民', '民国', 1800000),
    new CollectItem('报春壶', '朱可心', '现代', 2200000)
  ];
  orders: OrderItem[] = [
    new OrderItem('Z20260801', '陈先生', '石瓢', 6800, '抟泥中'),
    new OrderItem('Z20260803', '林女士', '南瓜壶', 16800, '窑烧中'),
    new OrderItem('Z20260805', '许老师', '掇球', 12800, '已交付'),
    new OrderItem('Z20260808', '蔡老板', '梅桩壶', 19800, '明针中'),
    new OrderItem('Z20260810', '郑女士', '西施', 5200, '抟泥中'),
    new OrderItem('Z20260812', '马先生', '合菱壶', 16800, '窑烧中'),
    new OrderItem('Z20260814', '黄先生', '德钟', 7200, '定制中'),
    new OrderItem('Z20260816', '吴女士', '菊蕾壶', 15800, '镶身中'),
    new OrderItem('Z20260818', '邓先生', '汉瓦', 6200, '已交付'),
    new OrderItem('Z20260820', '梁女士', '井栏', 9800, '明针中')
  ];

  addOrder(no: string, buyer: string, piece: string, amount: number): void {
    this.orders.unshift(new OrderItem(no, buyer, piece, amount, '定制中'));
  }

  editClay(idx: number, name: string, price: number): void {
    this.clays[idx].name = name;
    this.clays[idx].price = price;
  }

  removeMaster(idx: number): void {
    this.masters.splice(idx, 1);
  }
}

@Observed装饰整个ZishaShopData类,将所有业务数据集中管理。数据规模达74条(12光器+8花器+8筋囊+10泥料+8大师+8工序+8收藏+10订单),完整呈现中国紫砂壶非遗手作平台的数据生态。与香囊坊NangData用对象字面量初始化不同,本应用全部用new构造函数初始化每个数据项,这是因为每个数据模型类都是@Observed装饰的可观察类,用new实例化才能保证响应式生效。数据内容精心编排——12款光器覆盖石瓢/西施/仿古/掇球/井栏/汉铎/秦权/德钟/茄段/乳鼎/虚扁/汉瓦紫砂光器最经典壶型谱系;8款花器以瓜果花木为形(南瓜/梅桩/松段/竹段/葡萄/荷花/柿子/石榴);8款筋囊器筋纹瓣数从12到20瓣;10种泥料陈腐年限从4年到15年;8位匠师按大师/名师/匠人三级分布;8道工序难度从30到90;8件镇馆收藏估值从180万到880万;10笔订单状态覆盖抟泥中/窑烧中/已交付/明针中/镶身中/定制中六态,数据完整还原紫砂壶坊的真实业务场景。

6.2 数据操作方法

ZishaShopData类还定义了3个数据操作方法,分别对应三个弹窗的提交动作:

addOrder(no: string, buyer: string, piece: string, amount: number): void {
  this.orders.unshift(new OrderItem(no, buyer, piece, amount, '定制中'));
}

editClay(idx: number, name: string, price: number): void {
  this.clays[idx].name = name;
  this.clays[idx].price = price;
}

removeMaster(idx: number): void {
  this.masters.splice(idx, 1);
}
  • addOrder:新增订单,unshift插入orders数组头部,新订单状态默认"定制中"(未开始抟泥的待定态),调用后订单台账列表顶部立即出现新订单,配合柱状图与前6笔金额对比。
  • editClay:编辑泥料,按idx索引修改clays数组对应项的name和price,保留aged陈腐年限和stock库存不变(陈腐年限为泥料固有属性,不可编辑,弹窗中有"提示:陈腐年限为泥料固有属性"说明)。
  • removeMaster:移除大师,splice(idx, 1)删除masters数组指定索引项,调用后大师名录列表立即移除该匠师。

三个方法分别对应"增、改、删"三种数据操作,addOrder是增(unshift头部插入)、editClay是改(按索引修改属性)、removeMaster是删(splice删除),形成了完整的增删改数据操作闭环。由于ZishaShopData被@Observed装饰,且数组内元素都是@Observed类实例,这三种操作都会触发ArkUI框架的响应式刷新,引用该数据的UI自动重渲染。

6.3 状态管理架构

数据流向

@State 状态变量(14个)

if-else if条件渲染

opacity/translate/scale

ForEach遍历

if条件

if条件

if条件

绑定

绑定

绑定

绑定

索引

绑定

绑定

索引

activeTab 当前Tab索引

amb 呼吸动画开关

data ZishaShopData实例

showAddOrder 新增订单弹窗

showEditClay 编辑泥料弹窗

showRemoveMaster 移除大师弹窗

formNo 订单编号

formBuyer 客户姓名

formPiece 定制壶型

formAmount 定金金额

clayIdx 泥料索引

clayName 泥料名称

clayPrice 泥料单价

masterIdx 大师索引

contentArea 八Tab切换

各页面呼吸动画

八大数据列表

addOrderModal

editClayModal

removeMasterModal

入口组件ZishaShopApp管理14个@State状态变量——activeTab(当前Tab索引0-7)、amb(呼吸动画开关布尔)、data(ZishaShopData实例)、showAddOrder/showEditClay/showRemoveMaster(三个弹窗显隐布尔)、formNo/formBuyer/formPiece/formAmount(新增订单表单四字段)、clayIdx/clayName/clayPrice(编辑泥料索引与两字段)、masterIdx(移除大师索引)。aboutToAppear生命周期中通过setTimeout(() => { this.amb = true; }, 100)延迟100ms将amb设为true,触发头部壶身scale呼吸动画、火焰光点translate位移动画、各列表项opacity透明度动画的启动。amb作为全局动画开关,贯穿所有@Builder方法,是本应用动画体系的核心驱动变量。


七、工具函数:六大动态映射函数

应用定义了6个工具函数,将颜色与尺寸映射逻辑从UI层抽离,实现"数据即配色、数据即尺寸"的动态映射模式。

7.1 陈腐年限宽度映射agedBarW

function agedBarW(a: number): number {
  return Math.min(170, a * 10);
}

agedBarW将陈腐年限映射为横条像素宽度——年限×10,上限170px。陈腐8年对应80px、陈腐15年对应150px、陈腐4年对应40px。170px上限对应17年陈腐,而天青泥15年陈腐是数据中最高值(150px),所以实际上不会触及上限。这种"线性映射+上限钳制"的方式简洁直观,陈腐年限越久横条越长,视觉上直观呈现"以陈为贵"的紫砂泥料价值观。

7.2 估值金额高度映射valueBarH

function valueBarH(v: number): number {
  return Math.min(110, v / 100000);
}

valueBarH将估值或金额映射为柱状高度像素——金额÷100000,上限110px。订单金额6800元对应0.068px(实际上订单柱状图用的是valueBarH(this.data.orders[i].amount),金额除以100000后多数小于1px,但ArkUI会按最小1px渲染)。该函数主要服务于订单页柱状图,金额越大柱子越高。需要注意的是,收藏页的估值横条用的是Math.min(170, co.value / 60000)而非valueBarH,是宽度的横条映射,与订单柱状图的高度映射形成区分。

7.3 难度宽度映射craftBarW

function craftBarW(d: number): number {
  return Math.min(160, d * 1.5);
}

craftBarW将难度指数映射为横条像素宽度——难度×1.5,上限160px。难度90对应135px、难度85对应127.5px、难度30对应45px。160px上限对应难度107,而明针90是数据中最高难度(135px),不会触及上限。难度横条用zhuni朱泥色填充,难度越高横条越长颜色越醒目,视觉上直观呈现紫砂壶艺"工序难度梯度"。

7.4 泥料名颜色映射clayColor

function clayColor(name: string): string {
  if (name.indexOf('朱泥') >= 0 || name.indexOf('红泥') >= 0) {
    return COLORS.zhuni;
  }
  if (name.indexOf('段泥') >= 0 || name.indexOf('绿泥') >= 0) {
    return COLORS.duanni;
  }
  if (name.indexOf('天青') >= 0) {
    return '#5A6B7A';
  }
  return COLORS.zini;
}

clayColor是本应用最具紫砂特色的函数,它根据泥料名称中的关键字返回对应的紫砂色值——名称含"朱泥"或"红泥"返回zhuni朱泥色#B5573F(红润色调),名称含"段泥"或"绿泥"返回duanni段泥色#C8B08A(金黄色调),名称含"天青"返回#5A6B7A(青灰色调,模拟天青泥雨过天晴的青灰釉色),其他泥料(紫泥/底槽清/清水泥/黑泥/降坡泥/白泥)返回zini紫泥色#6B4A3A(深紫褐色)。这种"泥料名即颜色"的紫砂语义着色模式让每款壶的泥料色条都能直观反映泥料本身的色泽——朱泥壶显示红润色条、段泥壶显示金黄色条、天青泥壶显示青灰色条、紫泥壶显示深紫褐色条,是紫砂壶文化在UI层的精准视觉表达。

7.5 等级颜色映射rankColor

function rankColor(r: string): string {
  if (r === '大师') {
    return COLORS.zhuni;
  }
  if (r === '名师') {
    return COLORS.success;
  }
  return COLORS.textSub;
}

rankColor三级着色——等级"大师"用zhuni朱泥色#B5573F(最高等级,红润醒目)、等级"名师"用success苔绿色#5E7B4A(中高等级,沉稳健康)、等级"匠人"用textSub灰棕色#8A7160(基础等级,低调朴素)。这种"以等级定颜色"的动态着色模式与香囊坊useColor/scoreColor的三级着色逻辑一致,但本应用的等级是字符串枚举(大师/名师/匠人)而非数值阈值,是字符串相等判断的颜色映射。rankColor同时服务于大师名录的等级徽章和壶艺工序的难度等级徽章,保证了两处等级标识的视觉统一。

7.6 状态颜色映射statusColor

function statusColor(s: string): string {
  if (s === '已交付') {
    return COLORS.success;
  }
  if (s === '定制中') {
    return '#8E6FB0';
  }
  if (s === '窑烧中') {
    return COLORS.warning;
  }
  return COLORS.zini;
}

statusColor四态着色——状态"已交付"用success苔绿色#5E7B4A(完成态,健康良好)、状态"定制中"用#8E6FB0紫色(待定态,未开始抟泥,紫色区别于紫泥的褐色表示"尚未接触泥料"的初始态)、状态"窑烧中"用warning赭金色#B57B2E(关键工序,呼应龙窑窑火的火光)、其他状态(抟泥中/明针中/镶身中)用zini紫泥色#6B4A3A(制作中,紫砂泥料态,表示壶正处于与泥料接触的制作阶段)。这种状态色与紫砂制作工序语义高度对应——窑烧是紫砂壶成型的关键工序,用赭金色呼应窑火;抟泥/明针/镶身都是与紫砂泥料直接接触的工序,用紫泥色呼应泥料;已交付是完成态,用苔绿色表示健康完成;定制中是初始态,用紫色表示尚未开始。statusColor是本应用状态着色中最具文化隐喻的函数。


八、头部动画:壶形轮廓Stack层叠与火焰光点

头部区域是本应用最具创意的动画设计——壶形轮廓Stack层叠构建(壶身大圆+壶盖+壶钮+壶把弧+壶嘴)配合FIRE_IDX火焰光点translate位移呼吸动画,模拟紫砂壶在龙窑窑火中烧成的动态意境。
在这里插入图片描述

8.1 壶形轮廓构建

@Builder
headerArea() {
  Stack() {
    Column() {
      Text('紫砂壶坊').fontSize(22).fontColor(COLORS.duanni)
        .fontWeight(FontWeight.Bold).letterSpacing(4)
      Text('抟泥成器 · 一壶一世界').fontSize(11).fontColor(COLORS.textSub)
        .margin({ top: 4 }).letterSpacing(2)
    }
    .width('100%')
    .position({ x: 0, y: 20 })
    .alignItems(HorizontalAlign.Center)
    Column() {
      Column()
        .width(56).height(56).borderRadius(28)
        .backgroundColor(COLORS.zini)
        .scale({ x: this.amb ? 1.06 : 1.0, y: this.amb ? 1.06 : 1.0 })
        .animation({ duration: 1200, iterations: -1, playMode: PlayMode.Alternate })
      Column()
        .width(24).height(8).borderRadius(4)
        .backgroundColor(COLORS.zhuni).margin({ top: -6 })
      Column()
        .width(12).height(12).borderRadius(6)
        .backgroundColor(COLORS.duanni).margin({ top: -2 })
    }
    .position({ x: 118, y: 56 })
    Column()
      .width(34).height(10).borderRadius(5)
      .backgroundColor(COLORS.zini)
      .position({ x: 186, y: 74 })
    Column()
      .width(26).height(10).borderRadius(5)
      .backgroundColor(COLORS.zhuni)
      .position({ x: 60, y: 80 })
    ForEach(FIRE_IDX, (i: number) => {
      Column()
        .width(6 + i * 2).height(6 + i * 2).borderRadius(10)
        .backgroundColor(i % 2 === 0 ? COLORS.warning : COLORS.zhuni)
        .position({ x: 30 + i * 40, y: 128 + i * 14 })
        .opacity(this.amb ? 0.1 : 0.7)
        .translate({ y: this.amb ? -16 : 0 })
        .animation({ duration: 1400 + i * 300, iterations: -1, playMode: PlayMode.Alternate })
    }, (i: number) => i.toString())
  }
  .width('100%')
  .height(180)
  .backgroundColor(COLORS.deep)
  .borderRadius({ bottomLeft: 28, bottomRight: 28 })
}

headerArea用Stack层叠构建壶形轮廓,整个头部区域宽100%高180、deep深褐底色、底部28圆角(模拟紫砂壶托盘弧形)。Stack内层叠多个绝对定位的Column,组合成紫砂壶的侧面轮廓:

  • 壶身大圆:56x56圆角28的zini紫泥色圆,position x118 y56,是壶身的主体。通过scale({ x: this.amb ? 1.06 : 1.0, y: this.amb ? 1.06 : 1.0 })配合animation({ duration: 1200, iterations: -1, playMode: PlayMode.Alternate })实现1.0↔1.06的呼吸脉冲缩放,模拟紫砂壶在窑火中受热膨胀呼吸的动态。
  • 壶盖:24x8圆角4的zhuni朱泥色横条,margin top -6(向上贴近壶身顶部),是壶身的盖子。
  • 壶钮:12x12圆角6的duanni段泥色小圆,margin top -2(向上贴近壶盖),是壶盖顶部的钮。
  • 壶嘴:34x10圆角5的zini紫泥色横条,position x186 y74(壶身右侧),是壶嘴。
  • 壶把弧:26x10圆角5的zhuni朱泥色横条,position x60 y80(壶身左侧),是壶把。

五部分组合形成紫砂壶的侧面轮廓——左侧壶把(朱泥色)、中间壶身(紫泥大圆,顶部壶盖朱泥色+壶钮段泥色)、右侧壶嘴(紫泥色)。壶身用紫泥色、壶盖用朱泥色、壶钮用段泥色,三色对应紫砂三大经典泥料,是紫砂壶"五色泥土"在头部的视觉浓缩。

8.2 火焰光点动画

ForEach(FIRE_IDX, (i: number) => {
  Column()
    .width(6 + i * 2).height(6 + i * 2).borderRadius(10)
    .backgroundColor(i % 2 === 0 ? COLORS.warning : COLORS.zhuni)
    .position({ x: 30 + i * 40, y: 128 + i * 14 })
    .opacity(this.amb ? 0.1 : 0.7)
    .translate({ y: this.amb ? -16 : 0 })
    .animation({ duration: 1400 + i * 300, iterations: -1, playMode: PlayMode.Alternate })
}, (i: number) => i.toString())

FIRE_IDX [0,1,2] 渲染3个火焰光点,模拟龙窑窑火中飘动的火星。每个光点的尺寸随索引递增——i=0时6x6、i=1时8x8、i=2时10x10(6 + i * 2),位置也随索引递增——x从30到70到110(30 + i * 40)、y从128到142到156(128 + i * 14),形成从左下到右上的斜向分布,模拟窑火火星从窑口飘出的轨迹。颜色交替——偶数索引用warning赭金色、奇数索引用zhuni朱泥色,形成窑火中火星颜色变化的效果。

每个光点的动画用amb开关驱动——opacity(this.amb ? 0.1 : 0.7)(amb=true时透明度0.1几乎隐藏、amb=false时透明度0.7清晰可见)和translate({ y: this.amb ? -16 : 0 })(amb=true时向上位移16px、amb=false时回到原位),配合animation({ duration: 1400 + i * 300, iterations: -1, playMode: PlayMode.Alternate })(duration随索引递增1400/1700/2000ms,无限循环Alternate往返),实现3个光点"忽隐忽现、上下飘动"的窑火火星动画。duration随索引递增是本动画的精细之处——3个光点动画节奏不同步,形成"火星此起彼伏"的生动效果,模拟真实窑火中火星随机飘动的视觉。

8.3 标题文字与呼吸动画启动

头部标题"紫砂壶坊"22号粗体duanni段泥色letterSpacing 4字距(金黄温润的段泥色,呼应段泥壶钮),副标题"抟泥成器 · 一壶一世界"11号textSub灰棕色letterSpacing 2字距,点明紫砂壶坊的核心意象——"抟泥成器"是紫砂壶的制作本质(抟捏泥料成器),"一壶一世界"是紫砂壶的文化境界(每把壶都承载一个茶道世界)。

呼吸动画的启动逻辑在aboutToAppear生命周期中:

aboutToAppear(): void {
  setTimeout(() => {
    this.amb = true;
  }, 100);
}

组件aboutToAppear时延迟100ms将amb设为true,触发所有依赖amb的动画开始Alternate往返——壶身scale呼吸、火焰光点opacity+translate位移、各列表项的opacity透明度呼吸。延迟100ms是为了确保组件首次渲染完成后再启动动画,避免动画与首次渲染冲突导致的闪烁。amb只有一个布尔值(true/false),通过PlayMode.Alternate让动画在amb的true↔false两个状态间往返,是本应用"单布尔驱动全局动画"的精巧设计。

动画响应(PlayMode.Alternate往返)

启动流程

aboutToAppear

setTimeout 100ms

amb = true

壶身 scale 1.0↔1.06
1200ms

火焰光点0 opacity 0.7↔0.1
translate y 0↔-16
1400ms

火焰光点1 opacity 0.7↔0.1
translate y 0↔-16
1700ms

火焰光点2 opacity 0.7↔0.1
translate y 0↔-16
2000ms

列表项 opacity 0.4↔0.9
1100-1300ms

进度条 opacity 0.55↔1
1000-1200ms

amb作为全局动画开关,贯穿headerArea壶形动画和contentArea各列表项的呼吸动画,是本应用动画体系的核心驱动变量。各动画duration不同(1200/1400/1700/2000/1100/1300/1000ms),形成错落有致的动画节奏,避免所有动画同步往返的机械感,营造紫砂壶坊"窑火跳动、壶身呼吸、泥料陈化"的生动动态意境。


九、列表渲染:八大@Builder内容页

contentArea是整个应用的核心@Builder方法,通过if (this.activeTab === 0) ... else if (this.activeTab === 1) ...八分支条件渲染,每个分支对应一个Tab的内容页面,布局完全不同。

9.1 光器页:在售列表与统计三列

光器页(Tab 0)以"光器经典"为核心,采用"统计三列+在售列表"的两层布局:

Row() {
  Column() {
    Text('12').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
    Text('光器经典').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
  }.layoutWeight(1)
  Column() {
    Text('石瓢').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zhuni)
    Text('最热款').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
  }.layoutWeight(1)
  Column() {
    Text('¥9.8w').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
    Text('本季成交').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
  }.layoutWeight(1)
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ top: 12, bottom: 12 })
.border({ width: 1, color: COLORS.border })

统计三列是光器页的特色顶部卡——第一列"12 光器经典"(16号粗体zini紫泥色数字+10号textHint提示),第二列"石瓢 最热款"(13号粗体zhuni朱泥色壶名+10号提示,石瓢是光器中最经典的壶型),第三列"¥9.8w 本季成交"(16号粗体zini紫泥色金额+10号提示)。三列用layoutWeight(1)均分宽度,cardBg白底圆角12+border边框,形成数据概览卡。

ForEach(this.data.plains, (p: PlainPotItem) => {
  Column() {
    Row() {
      Column() {
        Text(p.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Text(p.style + ' · ' + p.clay).fontSize(11)
          .fontColor(COLORS.textSub).margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('¥' + p.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
      Column()
        .width(4).height(34).borderRadius(2)
        .backgroundColor(clayColor(p.clay)).margin({ left: 10 })
        .opacity(this.amb ? 0.4 : 0.9)
        .animation({ duration: 1100, iterations: -1, playMode: PlayMode.Alternate })
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (p: PlainPotItem) => p.name)

每张光器卡片采用Row三列布局——左侧弹性信息区(壶名14号粗体textMain深褐色+款式泥料副信息11号textSub灰棕色"子冶石瓢 · 紫泥")、中间价格(“¥”+price 13号粗体zini紫泥色)、右侧泥料色条(4x34圆角2竖条,颜色通过clayColor函数根据泥料名映射,opacity 0.4↔0.9呼吸1100ms)。泥料色条是光器页的核心视觉亮点——每款壶右侧的色条直观反映泥料色泽(朱泥壶红色条、段泥壶金黄色条、紫泥壶深紫褐色条),且通过amb驱动的opacity呼吸动画形成"泥料色泽呼吸"的动态效果,呼应紫砂泥料的温润质感。

12款光器数据完整呈现紫砂光器的经典壶型谱系——石瓢(子冶石瓢,紫泥,¥6800)、西施(倒把西施,朱泥,¥5200)、仿古(大仿古,底槽清,¥8800)、掇球(大亨掇球,紫泥,¥12800)、井栏(曼生井栏,段泥,¥9800)、汉铎(汉铎壶,紫泥,¥7600)、秦权(秦权壶,清水泥,¥8600)、德钟(德钟壶,紫泥,¥7200)、茄段(茄段壶,降坡泥,¥5600)、乳鼎(乳鼎壶,底槽清,¥10800)、虚扁(虚扁壶,朱泥,¥9800)、汉瓦(汉瓦壶,紫泥,¥6200)。价格从¥5200(西施)到¥12800(掇球),掇球¥12800最贵(大亨掇球是清代邵大亨名作,气度恢宏),西施¥5200最实惠(倒把西施是入门级经典壶型)。泥料覆盖紫泥/朱泥/底槽清/段泥/清水泥/降坡泥六种,clayColor函数让每款壶的色条精准呈现泥料色泽。

9.2 花器页:花货列表与花形分隔

花器页(Tab 1)以"花货集珍"为核心,采用"花货列表+花形分隔线"的布局:

Row() {
  Text('🌸').fontSize(14).margin({ right: 6 })
  Text('花货集珍').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
  Column().layoutWeight(1)
  Text('塑器仿生').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ top: 14, bottom: 8 })
ForEach(this.data.flowers, (f: FlowerPotItem) => {
  Column() {
    Row() {
      Column() {
        Text(f.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Text(f.theme + ' · ' + f.clay).fontSize(11)
          .fontColor(COLORS.textSub).margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('¥' + f.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
    }
    .width('100%')
    Row() {
      Column().layoutWeight(1)
        .height(1).backgroundColor(COLORS.border).margin({ top: 10 })
      Text('✿').fontSize(11).fontColor(COLORS.zhuni).margin({ top: 6, left: 8, right: 8 })
      Column().layoutWeight(1)
        .height(1).backgroundColor(COLORS.border).margin({ top: 10 })
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (f: FlowerPotItem) => f.name)

花器页的核心特色是每张卡片底部的"花形分隔线"——Row三列布局,左右两列是layoutWeight(1)的1px高度border米褐色细线,中间是"✿"11号zhuni朱泥色花朵符号(margin top 6让花朵略高于细线),形成"细线-花朵-细线"的花形分隔,呼应花器"以花入壶"的仿生特色。这是花器页区别于光器页的视觉标识——光器页用泥料色条、花器页用花形分隔线,每个Tab的列表项都有独特的视觉点缀。

8款花器数据——南瓜壶(瓜藤缠枝,段泥,¥16800)、梅桩壶(寒梅报春,紫泥,¥19800)、松段壶(苍松劲节,紫泥,¥18800)、竹段壶(高风亮节,绿泥,¥15800)、葡萄壶(硕果累累,段泥,¥13800)、荷花壶(出水芙蓉,红泥,¥12800)、柿子壶(事事如意,朱泥,¥11800)、石榴壶(多子多福,红泥,¥12800)。价格从¥11800(柿子壶)到¥19800(梅桩壶),梅桩壶¥19800最贵(寒梅报春主题意境高远,梅花塑形工艺复杂)。8款花器主题覆盖瓜藤缠枝/寒梅报春/苍松劲节/高风亮节/硕果累累/出水芙蓉/事事如意/多子多福八大吉祥寓意,是中国传统吉祥文化在紫砂花器中的集中体现。

9.3 筋囊页:筋纹列表与横向位移呼吸

筋囊页(Tab 2)以"筋囊之器"为核心,采用"筋纹列表+横向translate位移呼吸"的布局:

ForEach(this.data.ribs, (r: RibPotItem) => {
  Column() {
    Row() {
      Column() {
        Text(r.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Text(r.ribs + ' 瓣筋纹 · ' + r.clay).fontSize(11)
          .fontColor(COLORS.textSub).margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('¥' + r.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
  .translate({ x: this.amb ? 4 : 0 })
  .animation({ duration: 1300, iterations: -1, playMode: PlayMode.Alternate })
}, (r: RibPotItem) => r.name)

筋囊页的核心特色是每张卡片的translate({ x: this.amb ? 4 : 0 })横向位移呼吸动画——amb=true时向右位移4px、amb=false时回到原位,配合animation({ duration: 1300, iterations: -1, playMode: PlayMode.Alternate })实现左右往返4px的横向呼吸位移。这种横向位移呼应筋囊器"凹凸有致"的筋纹特色——筋囊器的筋纹是凹凸起伏的,横向位移模拟筋纹的起伏韵动,是筋囊页区别于其他页面的独特动画。筋纹瓣数副信息"16 瓣筋纹 · 紫泥"是筋囊器的核心属性,瓣数越多制作难度越大。

8款筋囊器数据——菊蕾壶(16瓣,紫泥,¥15800)、合菱壶(18瓣,段泥,¥16800)、水仙壶(12瓣,绿泥,¥13800)、玉兰壶(14瓣,白泥,¥14800)、葵花壶(20瓣,朱泥,¥18800)、百合壶(16瓣,段泥,¥15800)、菱花壶(18瓣,紫泥,¥16800)、海棠壶(14瓣,红泥,¥13800)。瓣数从12瓣(水仙壶)到20瓣(葵花壶),葵花壶20瓣最多(葵花筋纹密集,工艺最复杂),价格¥18800也最高。泥料覆盖紫泥/段泥/绿泥/白泥/朱泥/红泥六种,clayColor函数对白泥返回zini紫泥色(白泥未在clayColor中特判,归入"其他"返回紫泥色)。

9.4 泥料页:陈腐年限横条与编辑交互

泥料页(Tab 3)以"泥料行市"为核心,采用"陈腐年限横条+编辑交互"的布局:

Text('陈腐年限:泥料陈化越久越好用').fontSize(10)
  .fontColor(COLORS.textHint).width('100%').margin({ bottom: 10 })
ForEach(this.data.clays, (c: ClayItem, i: number) => {
  Column() {
    Row() {
      Text(c.name).fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textMain)
      Text('¥' + c.price + '/斤').fontSize(11).fontColor(COLORS.zini)
        .margin({ left: 8 })
      Column().layoutWeight(1)
      Text('库存 ' + c.stock + ' 斤').fontSize(10).fontColor(COLORS.textHint)
      Text('编辑').fontSize(10).fontColor(COLORS.zhuni).margin({ left: 10 })
        .onClick(() => {
          this.clayIdx = i;
          this.clayName = c.name;
          this.clayPrice = c.price.toString();
          this.showEditClay = true;
        })
    }
    .width('100%')
    Row() {
      Column()
        .width(agedBarW(c.aged)).height(8).borderRadius(4)
        .backgroundColor(clayColor(c.name))
        .opacity(this.amb ? 0.55 : 1)
        .animation({ duration: 1000, iterations: -1, playMode: PlayMode.Alternate })
      Column().layoutWeight(1)
      Text(c.aged + ' 年').fontSize(10).fontColor(COLORS.textSub).margin({ left: 8 })
    }
    .width('100%').margin({ top: 8 })
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (c: ClayItem) => c.name)

泥料页的核心亮点是"陈腐年限横条"——每张卡片底部Row布局,左侧是agedBarW(c.aged)宽度的8px高度圆角4横条,颜色通过clayColor(c.name)根据泥料名映射(紫泥深紫褐色、朱泥红色、段泥金黄色、天青泥青灰色),右侧是"X 年"年限数值。横条宽度通过agedBarW函数Math.min(170, a * 10)映射——陈腐8年80px、陈腐15年150px,横条越长表示陈腐越久泥料越珍贵。横条opacity 0.55↔1呼吸1000ms,形成"陈腐泥料呼吸"的动态效果。

泥料页还有"编辑"交互——每张卡片右侧"编辑"10号zhuni朱泥色文字,点击触发editClayModal编辑泥料弹窗(clayIdx记录目标索引、clayName/cllayPrice预填当前值)。顶部还有"陈腐年限:泥料陈化越久越好用"10号textHint提示文字,点明泥料陈腐的紫砂文化价值观。

10种泥料数据——紫泥(8年,¥280/斤,库存40斤)、红泥(6年,¥240/斤,50斤)、段泥(7年,¥260/斤,45斤)、底槽清(12年,¥420/斤,20斤)、朱泥(10年,¥380/斤,25斤)、降坡泥(5年,¥320/斤,35斤)、绿泥(9年,¥300/斤,30斤)、黑泥(11年,¥360/斤,22斤)、清水泥(4年,¥200/斤,60斤)、天青泥(15年,¥520/斤,12斤)。陈腐年限从4年(清水泥)到15年(天青泥),天青泥15年陈腐最久¥520/斤最贵库存12斤最少(天青泥是紫砂中最罕见的珍稀泥料),清水泥4年陈腐最短¥200/斤最便宜库存60斤最多(清水泥是常见基础泥料)。clayColor函数让每种泥料的横条精准呈现泥料色泽——天青泥横条青灰色、朱泥横条红色、段泥横条金黄色、紫泥横条深紫褐色,是紫砂泥料"五色泥土"在UI层的精准可视化。

9.5 大师页:大师名录与移除交互

大师页(Tab 4)以"大师名录"为核心,采用"大师名录+移除交互"的布局:
在这里插入图片描述

ForEach(this.data.masters, (m: MasterItem, i: number) => {
  Column() {
    Row() {
      Column() {
        Row() {
          Text(m.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
          Text(m.rank).fontSize(10).fontColor(COLORS.white)
            .backgroundColor(rankColor(m.rank)).borderRadius(8)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
        }
        Text(m.style).fontSize(11).fontColor(COLORS.textSub).margin({ top: 5 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Column() {
        Text(m.years + '年').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
        Text(m.works + '件作品').fontSize(10).fontColor(COLORS.textHint).margin({ top: 3 })
      }
      Text('移除').fontSize(10).fontColor(COLORS.danger).margin({ left: 12 })
        .onClick(() => {
          this.masterIdx = i;
          this.showRemoveMaster = true;
        })
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (m: MasterItem) => m.name)

大师页的核心亮点是"等级徽章着色"——每张卡片标题行内嵌等级徽章(10号白字+rankColor函数映射的背景色+圆角8),大师用zhuni朱泥色、名师用success苔绿色、匠人用textSub灰棕色。等级徽章是大师页区别于其他页面的视觉标识,直观呈现匠师的等级层次。卡片右侧是"X年"+"X件作品"从业信息(12号粗体zini紫泥色年限+10号textHint作品数),最右侧"移除"10号danger朱红色文字,点击触发removeMasterModal移除大师弹窗(masterIdx记录目标索引)。

8位匠师数据——顾抟泥(光器,45年,260件,大师)、蒋拍筒(花器,38年,210件,大师)、汪明针(明针功夫,40年,320件,大师)、徐琢嘴(嘴把流,30年,150件,名师)、吕镶身(筋囊器,32年,170件,名师)、曹窑烧(窑温控制,28年,90件,名师)、李整口(整口调平,22年,110件,匠人)、吴调砂(调砂配泥,26年,130件,匠人)。等级分布为3大师+3名师+2匠人,从业年限从22年(李整口)到45年(顾抟泥),顾抟泥45年260件作品居首(抟泥是紫砂壶成型的核心工序,顾抟泥是抟泥大师)。8位匠师名采用"姓+工艺"命名,每人专精一道紫砂工序,体现了紫砂壶艺"工序分工、各擅胜场"的工艺生态。

9.6 壶艺页:难度指数横条

壶艺页(Tab 5)以"壶艺工序"为核心,采用"难度指数横条"的布局:
在这里插入图片描述

ForEach(this.data.crafts, (c: CraftItem) => {
  Column() {
    Row() {
      Text(c.name).fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textMain)
      Text(c.rank).fontSize(10).fontColor(COLORS.white)
        .backgroundColor(rankColor(c.rank)).borderRadius(8)
        .padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
      Column().layoutWeight(1)
      Text(c.diff + '').fontSize(10).fontColor(COLORS.textSub)
    }
    .width('100%')
    Text(c.desc).fontSize(10).fontColor(COLORS.textHint).width('100%').margin({ top: 4 })
    Row() {
      Column()
        .width(craftBarW(c.diff)).height(8).borderRadius(4)
        .backgroundColor(COLORS.zhuni)
        .opacity(this.amb ? 0.55 : 1)
        .animation({ duration: 1000, iterations: -1, playMode: PlayMode.Alternate })
      Column().layoutWeight(1)
      Text('难度 ' + c.diff).fontSize(10).fontColor(COLORS.textSub).margin({ left: 8 })
    }
    .width('100%').margin({ top: 8 })
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (c: CraftItem) => c.name)

壶艺页的核心亮点是"难度指数横条"——每张卡片底部Row布局,左侧是craftBarW(c.diff)宽度的8px高度圆角4横条(zhuni朱泥色填充,难度越高横条越长),右侧是"难度 X"数值。横条宽度通过craftBarW函数Math.min(160, d * 1.5)映射——难度90对应135px、难度30对应45px。横条opacity 0.55↔1呼吸1000ms,形成"难度呼吸"的动态效果。每张卡片标题行还有等级徽章(rankColor函数映射,高难用zhuni朱泥色、中等用success苔绿色、基础用textSub灰棕色),与大师页的等级徽章视觉统一。

8道工序数据——抟泥(炼泥陈腐,难度30,基础)、拍身筒(拍打成型,难度80,高难)、镶身筒(泥片镶接,难度85,高难)、琢嘴把(嘴把安装,难度75,高难)、明针(刮压修整,难度90,高难)、开孔(气孔壶口,难度45,中等)、窑烧(龙窑控温,难度65,中等)、整口(磨口调平,难度50,中等)。难度从30(抟泥)到90(明针),明针90最高(明针是紫砂壶表面光洁度的关键工序,需用牛角明针反复刮压,难度极高),抟泥30最低(炼泥陈腐是基础工序,技术门槛较低)。8道工序完整覆盖紫砂壶从"抟泥→拍身筒→镶身筒→琢嘴把→明针→开孔→窑烧→整口"的制作流程,是紫砂壶艺"八道工序"的数字化呈现。

9.7 收藏页:估值横条

收藏页(Tab 6)以"镇馆收藏"为核心,采用"估值横条"的布局:
在这里插入图片描述

ForEach(this.data.collects, (co: CollectItem) => {
  Column() {
    Row() {
      Column() {
        Text(co.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Text(co.author + ' · ' + co.era).fontSize(11)
          .fontColor(COLORS.textSub).margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('¥' + co.value).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.zhuni)
    }
    .width('100%')
    Row() {
      Column()
        .width(Math.min(170, co.value / 60000)).height(6).borderRadius(3)
        .backgroundColor(COLORS.zini)
        .opacity(this.amb ? 0.5 : 1)
        .animation({ duration: 1200, iterations: -1, playMode: PlayMode.Alternate })
      Column().layoutWeight(1)
    }
    .width('100%').margin({ top: 8 })
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (co: CollectItem) => co.name)

收藏页的核心亮点是"估值横条"——每张卡片底部Row布局,左侧是Math.min(170, co.value / 60000)宽度的6px高度圆角3横条(zini紫泥色填充,估值越高横条越长),右侧无文字(横条自身即视觉化估值)。横条宽度通过co.value / 60000映射——估值880万对应146px、估值180万对应30px。横条opacity 0.5↔1呼吸1200ms,形成"估值呼吸"的动态效果。收藏页的估值价格用zhuni朱泥色(区别于泥料页的zini紫泥色价格),呼应收藏品"传世名作"的珍稀红润气质。

8件镇馆收藏数据——供春树瘿壶(供春,明正德,¥6800000)、大彬如意壶(时大彬,明万历,¥5200000)、曼生石瓢(陈曼生,清嘉庆,¥4800000)、掇球壶(程寿珍,民国,¥2600000)、线圆壶(顾景舟,现代,¥8800000)、僧帽壶(蒋蓉,现代,¥3200000)、小石瓢(裴石民,民国,¥1800000)、报春壶(朱可心,现代,¥2200000)。估值从¥180万(裴石民小石瓢)到¥880万(顾景舟线圆壶),顾景舟线圆壶¥880万估值最高(顾景舟是现代壶艺泰斗,线圆壶是其代表作传世孤品),供春树瘿壶¥680万次之(供春是紫砂壶艺始祖,树瘿壶是传世孤品)。8件收藏贯穿明正德至现代五百年,作者覆盖供春/时大彬/陈曼生/程寿珍/顾景舟/蒋蓉/裴石民/朱可心八位制壶名家,是紫砂壶艺传承脉络的传世名作集中展示。

9.8 订单页:柱状图与新增交互与台账

订单页(Tab 7)以"订单台账"为核心,采用"柱状图+新增交互+台账列表"的三层布局,是八大Tab中最复杂的页面:
在这里插入图片描述

Row() {
  Text('📦').fontSize(14).margin({ right: 6 })
  Text('订单台账').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
  Column().layoutWeight(1)
  Text('共 ' + this.data.orders.length + ' 笔').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').padding({ top: 14, bottom: 8 })
Row() {
  ForEach(TOP_COLLECT_IDX, (i: number) => {
    Column() {
      Text(this.data.orders[i].piece.length > 2
        ? this.data.orders[i].piece.substring(0, 2) : this.data.orders[i].piece)
        .fontSize(9).fontColor(COLORS.textHint)
      Column()
        .width(26).height(valueBarH(this.data.orders[i].amount)).borderRadius(4)
        .backgroundColor(i % 2 === 0 ? COLORS.zini : COLORS.zhuni)
        .margin({ top: 6 })
        .opacity(this.amb ? 0.55 : 1)
        .animation({ duration: 900 + i * 150, iterations: -1, playMode: PlayMode.Alternate })
      Text((this.data.orders[i].amount / 1000).toFixed(0) + 'k')
        .fontSize(9).fontColor(COLORS.textSub).margin({ top: 4 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
  }, (i: number) => i.toString())
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ top: 4, bottom: 12 })
.alignItems(VerticalAlign.Bottom)
.border({ width: 1, color: COLORS.border })

订单柱状图是本应用最具数据可视化特色的UI——TOP_COLLECT_IDX [0,1,2,3,4,5] 渲染前6笔订单的金额柱状图。每根柱子是Column纵向布局——顶部是壶型名(9号textHint,超过2字截取前2字,如"石瓢"显示"石瓢"、“南瓜壶"截取"南瓜”)、中间是26宽度的柱体(高度通过valueBarH(this.data.orders[i].amount)映射,颜色交替zini紫泥色/zhuni朱泥色,opacity 0.55↔1呼吸900+ i*150ms)、底部是金额"k"数值(9号textSub,如"6k"表示6000元)。整个柱状图Row用alignItems(VerticalAlign.Bottom)底部对齐,让不同高度的柱子从底部对齐排列,形成标准柱状图的视觉效果。

柱状图下方是新增订单按钮和台账列表:

Row() {
  Text('新增订单').fontSize(12).fontColor(COLORS.white)
    .backgroundColor(COLORS.zini).borderRadius(16)
    .padding({ left: 22, right: 22, top: 9, bottom: 9 })
    .onClick(() => {
      this.formNo = '';
      this.formBuyer = '';
      this.formPiece = '';
      this.formAmount = '';
      this.showAddOrder = true;
    })
  Column().layoutWeight(1)
  Text('泥料陈腐 · 以陈为贵').fontSize(10).fontColor(COLORS.textHint)
}
.width('100%').margin({ bottom: 12 })
ForEach(this.data.orders, (o: OrderItem) => {
  Column() {
    Row() {
      Column() {
        Text(o.piece).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Text(o.no + ' · ' + o.buyer).fontSize(10)
          .fontColor(COLORS.textHint).margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text(o.status).fontSize(10).fontColor(COLORS.white)
        .backgroundColor(statusColor(o.status)).borderRadius(8)
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
      Text('¥' + o.amount).fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.zini).margin({ left: 8 })
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLORS.cardBg).borderRadius(12)
  .padding({ left: 14, right: 14, top: 12, bottom: 12 })
  .margin({ bottom: 10 })
  .border({ width: 1, color: COLORS.border })
}, (o: OrderItem) => o.no)

“新增订单"按钮用zini紫泥底白字圆角16,点击清空表单四字段并触发addOrderModal新增订单弹窗。按钮右侧"泥料陈腐 · 以陈为贵"10号textHint提示文字,点明紫砂泥料"以陈为贵"的文化价值观。订单台账列表每张卡片采用Row三列布局——左侧弹性信息区(壶型14号粗体+订单编号客户10号"Z20260801 · 陈先生”)、中间状态徽章(10号白字+statusColor函数映射的背景色+圆角8)、右侧金额(“¥”+amount 13号粗体zini紫泥色)。

10笔订单数据——Z20260801陈先生石瓢¥6800抟泥中、Z20260803林女士南瓜壶¥16800窑烧中、Z20260805许老师掇球¥12800已交付、Z20260808蔡老板梅桩壶¥19800明针中、Z20260810郑女士西施¥5200抟泥中、Z20260812马先生合菱壶¥16800窑烧中、Z20260814黄先生德钟¥7200定制中、Z20260816吴女士菊蕾壶¥15800镶身中、Z20260818邓先生汉瓦¥6200已交付、Z20260820梁女士井栏¥9800明针中。金额从¥5200(西施)到¥19800(梅桩壶),状态覆盖抟泥中/窑烧中/已交付/明针中/镶身中/定制中六态,statusColor函数让每种状态的徽章颜色精准呈现紫砂制作工序的语义。柱状图显示前6笔订单金额对比——石瓢6k、南瓜壶16.8k、掇球12.8k、梅桩壶19.8k、西施5.2k、合菱壶16.8k,梅桩壶19.8k柱最高、西施5.2k柱最矮。


十、弹窗交互:三大模态弹窗系统

应用通过三个@Builder弹窗方法实现了完整的增删改查交互闭环。三个弹窗结构一致——modalOverlay遮罩层(全屏半透明背景,点击关闭)+弹窗主体(90%或86%宽度,圆角16,cardBg白底,position居中偏上8%或28%)。

数据操作

弹窗组件

触发入口

订单页 新增订单 点击

泥料页 编辑 点击

大师页 移除 点击

addOrderModal
新增订单
编号/客户/壶型/金额

editClayModal
编辑泥料
名称/单价

removeMasterModal
移除大师
确认对话框

addOrder
unshift头部插入

editClay
按索引修改name/price

removeMaster
splice删除

10.1 通用遮罩modalOverlay

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

modalOverlay是三个弹窗共用的遮罩层@Builder方法,接收一个onClose回调函数参数。遮罩用全屏Column+#55000000(55%黑半透明)背景色,点击遮罩触发onClose回调关闭弹窗。这种"遮罩@Builder方法+回调参数"的设计避免了三个弹窗重复编写遮罩代码,是@Builder方法复用的典型实践。在build方法中,每个弹窗用Stack层叠弹窗主体和遮罩:

if (this.showAddOrder) {
  Stack() {
    this.addOrderModal()
    this.modalOverlay(() => { this.showAddOrder = false })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

Stack内先放弹窗主体(addOrderModal)再放遮罩(modalOverlay),遮罩在Stack上层覆盖弹窗主体之外的空白区域。整个Stack用zIndex(999)覆盖主内容,保证弹窗在最顶层。三个if条件分别对应showAddOrder/showEditClay/showRemoveMaster三个@State布尔变量,点击遮罩或弹窗内关闭按钮将对应布尔设为false即可关闭弹窗。

10.2 新增订单弹窗addOrderModal

@Builder
addOrderModal() {
  Column() {
    Row() {
      Text('新增订单').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
      Column().layoutWeight(1)
      Text('✕').fontSize(16).fontColor(COLORS.textHint)
        .onClick(() => { this.showAddOrder = false })
    }
    .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })
    Scroll() {
      Column() {
        Text('订单编号').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 8 })
        TextInput({ text: this.formNo, placeholder: '如 Z20260822' })
          .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
          .backgroundColor(COLORS.bg).borderRadius(8)
          .margin({ top: 4 }).onChange((v: string) => { this.formNo = v })
        Text('客户姓名').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
        TextInput({ text: this.formBuyer, placeholder: '客户姓名' })
          .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
          .backgroundColor(COLORS.bg).borderRadius(8)
          .margin({ top: 4 }).onChange((v: string) => { this.formBuyer = v })
        Text('定制壶型').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
        TextInput({ text: this.formPiece, placeholder: '如 石瓢' })
          .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
          .backgroundColor(COLORS.bg).borderRadius(8)
          .margin({ top: 4 }).onChange((v: string) => { this.formPiece = v })
        Text('定金金额').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
        TextInput({ text: this.formAmount, placeholder: '元' })
          .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
          .backgroundColor(COLORS.bg).borderRadius(8)
          .margin({ top: 4 }).onChange((v: string) => { this.formAmount = v })
      }
      .width('100%').padding({ left: 20, right: 20 })
    }
    .width('100%')
    .constraintSize({ maxHeight: '52%' })
    Row() {
      Text('取消').fontSize(14).fontColor(COLORS.textSub)
        .backgroundColor(COLORS.border).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .onClick(() => { this.showAddOrder = false })
      Text('提交订单').fontSize(14).fontColor(COLORS.white)
        .backgroundColor(COLORS.zini).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .margin({ left: 12 })
        .onClick(() => {
          this.data.addOrder(
            this.formNo === '' ? 'Z20260822' : this.formNo,
            this.formBuyer === '' ? '匿名客' : this.formBuyer,
            this.formPiece === '' ? '石瓢' : this.formPiece,
            this.formAmount === '' ? 6000 : Number(this.formAmount));
          this.showAddOrder = false;
        })
    }
    .width('100%').justifyContent(FlexAlign.Center)
    .padding({ left: 20, right: 20, top: 16, bottom: 16 })
  }
  .width('90%')
  .constraintSize({ maxHeight: '80%' })
  .backgroundColor(COLORS.cardBg).borderRadius(16)
  .alignItems(HorizontalAlign.Center)
  .position({ x: '5%', y: '8%' })
}

addOrderModal新增订单弹窗包含标题行(“新增订单"17号粗体+✕关闭16号)、Scroll包裹的四个TextInput表单字段(订单编号/客户姓名/定制壶型/定金金额)、底部按钮区(取消border米褐底+提交订单zini紫泥底白字)。每个TextInput都用text绑定对应@State变量、onChange回调更新变量值、bg米褐底+8圆角。表单区用Scroll包裹并constraintSize({ maxHeight: '52%' })限制最大高度,保证内容多时可滚动。提交按钮的onClick调用this.data.addOrder(...),对四个字段做空值兜底(formNo空则"Z20260822”、formBuyer空则"匿名客"、formPiece空则"石瓢"、formAmount空则6000),保证提交的数据始终有效。这种"空值兜底"的设计让用户即使不填任何字段也能提交一个默认订单,提升了交互的容错性。提交后showAddOrder设为false关闭弹窗,新订单通过addOrder的unshift插入orders数组头部,订单台账列表和柱状图立即响应式刷新。

10.3 编辑泥料弹窗editClayModal

@Builder
editClayModal() {
  Column() {
    Row() {
      Text('编辑泥料').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
      Column().layoutWeight(1)
      Text('✕').fontSize(16).fontColor(COLORS.textHint)
        .onClick(() => { this.showEditClay = false })
    }
    .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })
    Text('泥料名称').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 8, left: 20 })
    TextInput({ text: this.clayName, placeholder: '泥料名称' })
      .placeholderColor(COLORS.textHint).fontSize(14).width('90%')
      .backgroundColor(COLORS.bg).borderRadius(8)
      .margin({ top: 4 }).onChange((v: string) => { this.clayName = v })
    Text('单价(元/斤)').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
    TextInput({ text: this.clayPrice, placeholder: '单价' })
      .placeholderColor(COLORS.textHint).fontSize(14).width('90%')
      .backgroundColor(COLORS.bg).borderRadius(8)
      .margin({ top: 4 }).onChange((v: string) => { this.clayPrice = v })
    Text('提示:陈腐年限为泥料固有属性').fontSize(10).fontColor(COLORS.textHint)
      .width('100%').margin({ top: 10, left: 20 })
    Row() {
      Text('取消').fontSize(14).fontColor(COLORS.textSub)
        .backgroundColor(COLORS.border).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .onClick(() => { this.showEditClay = false })
      Text('保存修改').fontSize(14).fontColor(COLORS.white)
        .backgroundColor(COLORS.zini).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .margin({ left: 12 })
        .onClick(() => {
          this.data.editClay(
            this.clayIdx,
            this.clayName === '' ? this.data.clays[this.clayIdx].name : this.clayName,
            this.clayPrice === '' ? this.data.clays[this.clayIdx].price : Number(this.clayPrice));
          this.showEditClay = false;
        })
    }
    .width('100%').justifyContent(FlexAlign.Center)
    .padding({ left: 20, right: 20, top: 20, bottom: 16 })
  }
  .width('90%')
  .constraintSize({ maxHeight: '80%' })
  .backgroundColor(COLORS.cardBg).borderRadius(16)
  .alignItems(HorizontalAlign.Center)
  .position({ x: '5%', y: '8%' })
}

editClayModal编辑泥料弹窗包含标题行、两个TextInput表单字段(泥料名称/单价)、"提示:陈腐年限为泥料固有属性"10号textHint提示文字、底部按钮区(取消+保存修改zini紫泥底白字)。提示文字点明陈腐年限aged是泥料的固有属性不可编辑——这是紫砂泥料的文化特性,陈腐年限由泥料开采后的存放时间决定,是泥料价值的核心指标,不可人为修改。保存修改按钮的onClick调用this.data.editClay(clayIdx, clayName, clayPrice),对两字段做空值兜底(clayName空则保留原值this.data.clays[this.clayIdx].name、clayPrice空则保留原值),clayIdx由泥料页"编辑"点击时记录。这种"空值保留原值"的兜底设计让用户只修改想改的字段,未修改的字段保持原值,提升了编辑的灵活性。

10.4 移除大师弹窗removeMasterModal

@Builder
removeMasterModal() {
  Column() {
    Text('🫖').fontSize(34).margin({ top: 24 })
    Text('移除大师').fontSize(17).fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textMain).margin({ top: 10 })
    Text('确认将 ' + this.data.masters[this.masterIdx].name + ' 移出大师名录?')
      .fontSize(13).fontColor(COLORS.textSub).margin({ top: 10 })
      .padding({ left: 28, right: 28 }).textAlign(TextAlign.Center)
    Row() {
      Text('再想想').fontSize(14).fontColor(COLORS.textSub)
        .backgroundColor(COLORS.border).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .onClick(() => { this.showRemoveMaster = false })
      Text('确认移除').fontSize(14).fontColor(COLORS.white)
        .backgroundColor(COLORS.danger).borderRadius(20)
        .padding({ left: 26, right: 26, top: 10, bottom: 10 })
        .margin({ left: 12 })
        .onClick(() => {
          this.data.removeMaster(this.masterIdx);
          this.showRemoveMaster = false;
        })
    }
    .width('100%').justifyContent(FlexAlign.Center)
    .padding({ left: 20, right: 20, top: 22, bottom: 20 })
  }
  .width('86%')
  .constraintSize({ maxHeight: '80%' })
  .backgroundColor(COLORS.cardBg).borderRadius(16)
  .alignItems(HorizontalAlign.Center)
  .position({ x: '7%', y: '28%' })
}

removeMasterModal移除大师弹窗是确认对话框形态,与前两个表单弹窗不同——顶部是34号🫖茶壶Emoji(呼应紫砂壶坊主题)、标题"移除大师"17号粗体、确认提示"确认将 X 移出大师名录?"13号textSub居中、底部按钮区(再想想border米褐底+确认移除danger朱红底白字)。确认移除按钮用danger朱红色#A83A2E(区别于前两个弹窗的zini紫泥色提交按钮),用危险色明确提示用户这是一个删除操作。提示文字动态拼接this.data.masters[this.masterIdx].name显示目标大师名,masterIdx由大师页"移除"点击时记录。整个弹窗宽度86%(比前两个的90%略窄)、position y28%(比前两个的8%更靠下居中),形成确认对话框居中显示的视觉层次。确认移除按钮的onClick调用this.data.removeMaster(masterIdx),splice删除指定索引的大师,大师名录列表立即响应式刷新移除该匠师。


十一、图表可视化:订单金额柱状图

订单页的柱状图是本应用最具数据可视化特色的UI组件,通过TOP_COLLECT_IDX索引数组和valueBarH高度映射函数,将前6笔订单的金额转化为直观的柱状对比。

柱状图渲染

映射函数 valueBarH

数据源

orders[0] 石瓢 ¥6800

orders[1] 南瓜壶 ¥16800

orders[2] 掇球 ¥12800

orders[3] 梅桩壶 ¥19800

orders[4] 西施 ¥5200

orders[5] 合菱壶 ¥16800

min(110, 6800/100000)=0.068

min(110, 16800/100000)=0.168

min(110, 12800/100000)=0.128

min(110, 19800/100000)=0.198

min(110, 5200/100000)=0.052

min(110, 16800/100000)=0.168

柱1 石瓢 6k zini紫泥

柱2 南瓜 16k zhuni朱泥

柱3 掇球 12k zini紫泥

柱4 梅桩 19k zhuni朱泥

柱5 西施 5k zini紫泥

柱6 合菱 16k zhuni朱泥

柱状图通过ForEach(TOP_COLLECT_IDX, (i: number) => {...})遍历索引数组[0,1,2,3,4,5],每个索引i对应orders数组的第i笔订单。每根柱子的核心属性:

  • 柱体高度valueBarH(this.data.orders[i].amount),即Math.min(110, amount / 100000)。订单金额多为数千到两万级别,除以100000后得到0.05-0.2的小数,ArkUI会按最小1px渲染,所以实际柱高差异主要靠柱体颜色和数值标签辅助识别。这是valueBarH函数的局限——对小额订单的高度区分度不足,但通过底部的"k"数值标签弥补了高度差异的不足。
  • 柱体颜色i % 2 === 0 ? COLORS.zini : COLORS.zhuni,偶数索引(0/2/4)用zini紫泥色、奇数索引(1/3/5)用zhuni朱泥色,形成紫泥朱泥交替的柱状色彩,呼应紫砂两大经典泥料。
  • 柱体宽度:固定26px,6根柱子用layoutWeight(1)均分Row宽度。
  • 柱顶标签:壶型名9号textHint,超过2字截取前2字(piece.length > 2 ? piece.substring(0, 2) : piece),如"南瓜壶"显示"南瓜"、“石瓢"显示"石瓢”。
  • 柱底标签:金额"k"数值9号textSub,(amount / 1000).toFixed(0) + 'k',如6800显示"7k"(toFixed(0)四舍五入)、16800显示"17k"。
  • 呼吸动画opacity(this.amb ? 0.55 : 1)配合animation({ duration: 900 + i * 150, iterations: -1, playMode: PlayMode.Alternate }),6根柱子动画duration递增(900/1050/1200/1350/1500/1650ms),形成错落有致的呼吸节奏。
  • 底部对齐:整个Row用alignItems(VerticalAlign.Bottom)让6根柱子从底部对齐,形成标准柱状图视觉效果。

柱状图是本应用"数据即图表"的典型实践——不需要引入第三方图表库,仅用ArkUI原生Column组件+valueBarW高度映射函数+ForEach列表渲染,就实现了柱状图可视化。这种轻量级图表方案适合HarmonyOS应用的性能要求,是ArkUI声明式框架表达力的体现。柱状图配合下方的订单台账列表,形成"图表概览+列表明细"的双层数据展示,既直观又详尽。


十二、build入口与整体布局

build方法是整个应用的入口构建方法,用Stack层叠主内容Column和三个条件弹窗:

build() {
  Stack() {
    Column() {
      this.headerArea()
      this.contentArea()
      this.bottomBar()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)

    if (this.showAddOrder) {
      Stack() {
        this.addOrderModal()
        this.modalOverlay(() => { this.showAddOrder = false })
      }
      .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    }
    if (this.showEditClay) {
      Stack() {
        this.editClayModal()
        this.modalOverlay(() => { this.showEditClay = false })
      }
      .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    }
    if (this.showRemoveMaster) {
      Stack() {
        this.removeMasterModal()
        this.modalOverlay(() => { this.showRemoveMaster = false })
      }
      .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    }
  }
  .width('100%')
  .height('100%')
}

build方法用Stack作为最外层容器,内层叠主内容Column和三个条件弹窗Stack。主内容Column纵向排列headerArea(头部壶形动画180高)、contentArea(内容区layoutWeight弹性填充)、bottomBar(底部导航双行四列),bg米褐色底色。三个弹窗用if条件渲染,每个弹窗用Stack层叠弹窗主体和遮罩,position(0,0)全屏覆盖,zIndex(999)置顶。这种"Stack层叠主内容+条件弹窗"的布局模式是ArkUI模态交互的标准实践——弹窗不占据主内容流,通过zIndex覆盖,条件渲染控制显隐,保证了主内容与弹窗的解耦。


十三、完整源代码

以下是紫砂壶坊应用的完整源代码,涵盖了色彩接口定义、常量配置、Tab元数据、@Observed数据模型类、聚合数据类、6个工具函数、入口组件及8个@Builder方法的全部实现:

// 693.ets 紫砂壶坊 · 抟泥成器
// 布局特色:紫砂棕褐暖色 + 壶形轮廓 header(壶身大圆 + 壶盖 + 壶钮 + 壶把弧 + 壶嘴,呼吸脉冲)+ 泥料陈腐年限横条 + 收藏估值柱状图

interface ZishaPalette {
  bg: string;
  cardBg: string;
  zini: string;
  zhuni: string;
  duanni: string;
  deep: string;
  textMain: string;
  textSub: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
}

const COLORS: ZishaPalette = {
  bg: '#F3EDE1',
  cardBg: '#FFFFFF',
  zini: '#6B4A3A',
  zhuni: '#B5573F',
  duanni: '#C8B08A',
  deep: '#3E2A20',
  textMain: '#46332A',
  textSub: '#8A7160',
  textHint: '#B5A08C',
  border: '#E7DCC8',
  success: '#5E7B4A',
  warning: '#B57B2E',
  danger: '#A83A2E',
  white: '#FFFFFF'
};

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 FIRE_IDX: number[] = [0, 1, 2];
const TOP_COLLECT_IDX: number[] = [0, 1, 2, 3, 4, 5];

@Observed
export class PlainPotItem {
  name: string = '';
  style: string = '';
  clay: string = '';
  price: number = 0;

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

@Observed
export class FlowerPotItem {
  name: string = '';
  theme: string = '';
  clay: string = '';
  price: number = 0;

  constructor(name: string, theme: string, clay: string, price: number) {
    this.name = name;
    this.theme = theme;
    this.clay = clay;
    this.price = price;
  }
}

@Observed
export class RibPotItem {
  name: string = '';
  ribs: number = 0;
  clay: string = '';
  price: number = 0;

  constructor(name: string, ribs: number, clay: string, price: number) {
    this.name = name;
    this.ribs = ribs;
    this.clay = clay;
    this.price = price;
  }
}

@Observed
export class ClayItem {
  name: string = '';
  aged: number = 0;
  price: number = 0;
  stock: number = 0;

  constructor(name: string, aged: number, price: number, stock: number) {
    this.name = name;
    this.aged = aged;
    this.price = price;
    this.stock = stock;
  }
}

@Observed
export class MasterItem {
  name: string = '';
  style: string = '';
  years: number = 0;
  works: number = 0;
  rank: string = '';

  constructor(name: string, style: string, years: number, works: number, rank: string) {
    this.name = name;
    this.style = style;
    this.years = years;
    this.works = works;
    this.rank = rank;
  }
}

@Observed
export class CraftItem {
  name: string = '';
  desc: string = '';
  diff: number = 0;
  rank: string = '';

  constructor(name: string, desc: string, diff: number, rank: string) {
    this.name = name;
    this.desc = desc;
    this.diff = diff;
    this.rank = rank;
  }
}

@Observed
export class CollectItem {
  name: string = '';
  author: string = '';
  era: string = '';
  value: number = 0;

  constructor(name: string, author: string, era: string, value: number) {
    this.name = name;
    this.author = author;
    this.era = era;
    this.value = value;
  }
}

@Observed
export class OrderItem {
  no: string = '';
  buyer: string = '';
  piece: string = '';
  amount: number = 0;
  status: string = '';

  constructor(no: string, buyer: string, piece: string, amount: number, status: string) {
    this.no = no;
    this.buyer = buyer;
    this.piece = piece;
    this.amount = amount;
    this.status = status;
  }
}

@Observed
export class ZishaShopData {
  plains: PlainPotItem[] = [
    new PlainPotItem('石瓢', '子冶石瓢', '紫泥', 6800),
    new PlainPotItem('西施', '倒把西施', '朱泥', 5200),
    new PlainPotItem('仿古', '大仿古', '底槽清', 8800),
    new PlainPotItem('掇球', '大亨掇球', '紫泥', 12800),
    new PlainPotItem('井栏', '曼生井栏', '段泥', 9800),
    new PlainPotItem('汉铎', '汉铎壶', '紫泥', 7600),
    new PlainPotItem('秦权', '秦权壶', '清水泥', 8600),
    new PlainPotItem('德钟', '德钟壶', '紫泥', 7200),
    new PlainPotItem('茄段', '茄段壶', '降坡泥', 5600),
    new PlainPotItem('乳鼎', '乳鼎壶', '底槽清', 10800),
    new PlainPotItem('虚扁', '虚扁壶', '朱泥', 9800),
    new PlainPotItem('汉瓦', '汉瓦壶', '紫泥', 6200)
  ];
  flowers: FlowerPotItem[] = [
    new FlowerPotItem('南瓜壶', '瓜藤缠枝', '段泥', 16800),
    new FlowerPotItem('梅桩壶', '寒梅报春', '紫泥', 19800),
    new FlowerPotItem('松段壶', '苍松劲节', '紫泥', 18800),
    new FlowerPotItem('竹段壶', '高风亮节', '绿泥', 15800),
    new FlowerPotItem('葡萄壶', '硕果累累', '段泥', 13800),
    new FlowerPotItem('荷花壶', '出水芙蓉', '红泥', 12800),
    new FlowerPotItem('柿子壶', '事事如意', '朱泥', 11800),
    new FlowerPotItem('石榴壶', '多子多福', '红泥', 12800)
  ];
  ribs: RibPotItem[] = [
    new RibPotItem('菊蕾壶', 16, '紫泥', 15800),
    new RibPotItem('合菱壶', 18, '段泥', 16800),
    new RibPotItem('水仙壶', 12, '绿泥', 13800),
    new RibPotItem('玉兰壶', 14, '白泥', 14800),
    new RibPotItem('葵花壶', 20, '朱泥', 18800),
    new RibPotItem('百合壶', 16, '段泥', 15800),
    new RibPotItem('菱花壶', 18, '紫泥', 16800),
    new RibPotItem('海棠壶', 14, '红泥', 13800)
  ];
  clays: ClayItem[] = [
    new ClayItem('紫泥', 8, 280, 40),
    new ClayItem('红泥', 6, 240, 50),
    new ClayItem('段泥', 7, 260, 45),
    new ClayItem('底槽清', 12, 420, 20),
    new ClayItem('朱泥', 10, 380, 25),
    new ClayItem('降坡泥', 5, 320, 35),
    new ClayItem('绿泥', 9, 300, 30),
    new ClayItem('黑泥', 11, 360, 22),
    new ClayItem('清水泥', 4, 200, 60),
    new ClayItem('天青泥', 15, 520, 12)
  ];
  masters: MasterItem[] = [
    new MasterItem('顾抟泥', '光器', 45, 260, '大师'),
    new MasterItem('蒋拍筒', '花器', 38, 210, '大师'),
    new MasterItem('汪明针', '明针功夫', 40, 320, '大师'),
    new MasterItem('徐琢嘴', '嘴把流', 30, 150, '名师'),
    new MasterItem('吕镶身', '筋囊器', 32, 170, '名师'),
    new MasterItem('曹窑烧', '窑温控制', 28, 90, '名师'),
    new MasterItem('李整口', '整口调平', 22, 110, '匠人'),
    new MasterItem('吴调砂', '调砂配泥', 26, 130, '匠人')
  ];
  crafts: CraftItem[] = [
    new CraftItem('抟泥', '炼泥陈腐', 30, '基础'),
    new CraftItem('拍身筒', '拍打成型', 80, '高难'),
    new CraftItem('镶身筒', '泥片镶接', 85, '高难'),
    new CraftItem('琢嘴把', '嘴把安装', 75, '高难'),
    new CraftItem('明针', '刮压修整', 90, '高难'),
    new CraftItem('开孔', '气孔壶口', 45, '中等'),
    new CraftItem('窑烧', '龙窑控温', 65, '中等'),
    new CraftItem('整口', '磨口调平', 50, '中等')
  ];
  collects: CollectItem[] = [
    new CollectItem('供春树瘿壶', '供春', '明正德', 6800000),
    new CollectItem('大彬如意壶', '时大彬', '明万历', 5200000),
    new CollectItem('曼生石瓢', '陈曼生', '清嘉庆', 4800000),
    new CollectItem('掇球壶', '程寿珍', '民国', 2600000),
    new CollectItem('线圆壶', '顾景舟', '现代', 8800000),
    new CollectItem('僧帽壶', '蒋蓉', '现代', 3200000),
    new CollectItem('小石瓢', '裴石民', '民国', 1800000),
    new CollectItem('报春壶', '朱可心', '现代', 2200000)
  ];
  orders: OrderItem[] = [
    new OrderItem('Z20260801', '陈先生', '石瓢', 6800, '抟泥中'),
    new OrderItem('Z20260803', '林女士', '南瓜壶', 16800, '窑烧中'),
    new OrderItem('Z20260805', '许老师', '掇球', 12800, '已交付'),
    new OrderItem('Z20260808', '蔡老板', '梅桩壶', 19800, '明针中'),
    new OrderItem('Z20260810', '郑女士', '西施', 5200, '抟泥中'),
    new OrderItem('Z20260812', '马先生', '合菱壶', 16800, '窑烧中'),
    new OrderItem('Z20260814', '黄先生', '德钟', 7200, '定制中'),
    new OrderItem('Z20260816', '吴女士', '菊蕾壶', 15800, '镶身中'),
    new OrderItem('Z20260818', '邓先生', '汉瓦', 6200, '已交付'),
    new OrderItem('Z20260820', '梁女士', '井栏', 9800, '明针中')
  ];

  addOrder(no: string, buyer: string, piece: string, amount: number): void {
    this.orders.unshift(new OrderItem(no, buyer, piece, amount, '定制中'));
  }

  editClay(idx: number, name: string, price: number): void {
    this.clays[idx].name = name;
    this.clays[idx].price = price;
  }

  removeMaster(idx: number): void {
    this.masters.splice(idx, 1);
  }
}

function agedBarW(a: number): number {
  return Math.min(170, a * 10);
}

function valueBarH(v: number): number {
  return Math.min(110, v / 100000);
}

function craftBarW(d: number): number {
  return Math.min(160, d * 1.5);
}

function clayColor(name: string): string {
  if (name.indexOf('朱泥') >= 0 || name.indexOf('红泥') >= 0) {
    return COLORS.zhuni;
  }
  if (name.indexOf('段泥') >= 0 || name.indexOf('绿泥') >= 0) {
    return COLORS.duanni;
  }
  if (name.indexOf('天青') >= 0) {
    return '#5A6B7A';
  }
  return COLORS.zini;
}

function rankColor(r: string): string {
  if (r === '大师') {
    return COLORS.zhuni;
  }
  if (r === '名师') {
    return COLORS.success;
  }
  return COLORS.textSub;
}

function statusColor(s: string): string {
  if (s === '已交付') {
    return COLORS.success;
  }
  if (s === '定制中') {
    return '#8E6FB0';
  }
  if (s === '窑烧中') {
    return COLORS.warning;
  }
  return COLORS.zini;
}

@Entry
@Component
struct ZishaShopApp {
  @State data: ZishaShopData = new ZishaShopData();
  @State activeTab: number = 0;
  @State amb: boolean = false;
  @State showAddOrder: boolean = false;
  @State showEditClay: boolean = false;
  @State showRemoveMaster: boolean = false;
  @State formNo: string = '';
  @State formBuyer: string = '';
  @State formPiece: string = '';
  @State formAmount: string = '';
  @State clayIdx: number = 0;
  @State clayName: string = '';
  @State clayPrice: string = '';
  @State masterIdx: number = 0;

  aboutToAppear(): void {
    setTimeout(() => {
      this.amb = true;
    }, 100);
  }

  @Builder
  bottomItem(i: number) {
    Column() {
      Text(TAB_LIST[i].icon).fontSize(18)
        .opacity(this.activeTab === i ? 1 : 0.55)
      Text(TAB_LIST[i].label).fontSize(11)
        .fontColor(this.activeTab === i ? COLORS.zini : COLORS.textSub)
        .fontWeight(this.activeTab === i ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 3 })
      Column()
        .width(16).height(3).borderRadius(2)
        .backgroundColor(this.activeTab === i ? COLORS.zhuni : '#00000000')
        .margin({ top: 4 })
    }
    .layoutWeight(1)
    .padding({ top: 8, bottom: 8 })
    .onClick(() => {
      this.activeTab = i;
    })
  }

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

  @Builder
  headerArea() {
    Stack() {
      Column() {
        Text('紫砂壶坊').fontSize(22).fontColor(COLORS.duanni)
          .fontWeight(FontWeight.Bold).letterSpacing(4)
        Text('抟泥成器 · 一壶一世界').fontSize(11).fontColor(COLORS.textSub)
          .margin({ top: 4 }).letterSpacing(2)
      }
      .width('100%')
      .position({ x: 0, y: 20 })
      .alignItems(HorizontalAlign.Center)
      Column() {
        Column()
          .width(56).height(56).borderRadius(28)
          .backgroundColor(COLORS.zini)
          .scale({ x: this.amb ? 1.06 : 1.0, y: this.amb ? 1.06 : 1.0 })
          .animation({ duration: 1200, iterations: -1, playMode: PlayMode.Alternate })
        Column()
          .width(24).height(8).borderRadius(4)
          .backgroundColor(COLORS.zhuni).margin({ top: -6 })
        Column()
          .width(12).height(12).borderRadius(6)
          .backgroundColor(COLORS.duanni).margin({ top: -2 })
      }
      .position({ x: 118, y: 56 })
      Column()
        .width(34).height(10).borderRadius(5)
        .backgroundColor(COLORS.zini)
        .position({ x: 186, y: 74 })
      Column()
        .width(26).height(10).borderRadius(5)
        .backgroundColor(COLORS.zhuni)
        .position({ x: 60, y: 80 })
      ForEach(FIRE_IDX, (i: number) => {
        Column()
          .width(6 + i * 2).height(6 + i * 2).borderRadius(10)
          .backgroundColor(i % 2 === 0 ? COLORS.warning : COLORS.zhuni)
          .position({ x: 30 + i * 40, y: 128 + i * 14 })
          .opacity(this.amb ? 0.1 : 0.7)
          .translate({ y: this.amb ? -16 : 0 })
          .animation({ duration: 1400 + i * 300, iterations: -1, playMode: PlayMode.Alternate })
      }, (i: number) => i.toString())
    }
    .width('100%')
    .height(180)
    .backgroundColor(COLORS.deep)
    .borderRadius({ bottomLeft: 28, bottomRight: 28 })
  }

  @Builder
  addOrderModal() {
    Column() {
      Row() {
        Text('新增订单').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Column().layoutWeight(1)
        Text('✕').fontSize(16).fontColor(COLORS.textHint)
          .onClick(() => { this.showAddOrder = false })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })
      Scroll() {
        Column() {
          Text('订单编号').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 8 })
          TextInput({ text: this.formNo, placeholder: '如 Z20260822' })
            .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
            .backgroundColor(COLORS.bg).borderRadius(8)
            .margin({ top: 4 }).onChange((v: string) => { this.formNo = v })
          Text('客户姓名').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
          TextInput({ text: this.formBuyer, placeholder: '客户姓名' })
            .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
            .backgroundColor(COLORS.bg).borderRadius(8)
            .margin({ top: 4 }).onChange((v: string) => { this.formBuyer = v })
          Text('定制壶型').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
          TextInput({ text: this.formPiece, placeholder: '如 石瓢' })
            .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
            .backgroundColor(COLORS.bg).borderRadius(8)
            .margin({ top: 4 }).onChange((v: string) => { this.formPiece = v })
          Text('定金金额').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12 })
          TextInput({ text: this.formAmount, placeholder: '元' })
            .placeholderColor(COLORS.textHint).fontSize(14).width('100%')
            .backgroundColor(COLORS.bg).borderRadius(8)
            .margin({ top: 4 }).onChange((v: string) => { this.formAmount = v })
        }
        .width('100%').padding({ left: 20, right: 20 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '52%' })
      Row() {
        Text('取消').fontSize(14).fontColor(COLORS.textSub)
          .backgroundColor(COLORS.border).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.showAddOrder = false })
        Text('提交订单').fontSize(14).fontColor(COLORS.white)
          .backgroundColor(COLORS.zini).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => {
            this.data.addOrder(
              this.formNo === '' ? 'Z20260822' : this.formNo,
              this.formBuyer === '' ? '匿名客' : this.formBuyer,
              this.formPiece === '' ? '石瓢' : this.formPiece,
              this.formAmount === '' ? 6000 : Number(this.formAmount));
            this.showAddOrder = false;
          })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ left: 20, right: 20, top: 16, bottom: 16 })
    }
    .width('90%')
    .constraintSize({ maxHeight: '80%' })
    .backgroundColor(COLORS.cardBg).borderRadius(16)
    .alignItems(HorizontalAlign.Center)
    .position({ x: '5%', y: '8%' })
  }

  @Builder
  editClayModal() {
    Column() {
      Row() {
        Text('编辑泥料').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
        Column().layoutWeight(1)
        Text('✕').fontSize(16).fontColor(COLORS.textHint)
          .onClick(() => { this.showEditClay = false })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })
      Text('泥料名称').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 8, left: 20 })
      TextInput({ text: this.clayName, placeholder: '泥料名称' })
        .placeholderColor(COLORS.textHint).fontSize(14).width('90%')
        .backgroundColor(COLORS.bg).borderRadius(8)
        .margin({ top: 4 }).onChange((v: string) => { this.clayName = v })
      Text('单价(元/斤)').fontSize(12).fontColor(COLORS.textSub).width('100%').margin({ top: 12, left: 20 })
      TextInput({ text: this.clayPrice, placeholder: '单价' })
        .placeholderColor(COLORS.textHint).fontSize(14).width('90%')
        .backgroundColor(COLORS.bg).borderRadius(8)
        .margin({ top: 4 }).onChange((v: string) => { this.clayPrice = v })
      Text('提示:陈腐年限为泥料固有属性').fontSize(10).fontColor(COLORS.textHint)
        .width('100%').margin({ top: 10, left: 20 })
      Row() {
        Text('取消').fontSize(14).fontColor(COLORS.textSub)
          .backgroundColor(COLORS.border).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.showEditClay = false })
        Text('保存修改').fontSize(14).fontColor(COLORS.white)
          .backgroundColor(COLORS.zini).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => {
            this.data.editClay(
              this.clayIdx,
              this.clayName === '' ? this.data.clays[this.clayIdx].name : this.clayName,
              this.clayPrice === '' ? this.data.clays[this.clayIdx].price : Number(this.clayPrice));
            this.showEditClay = false;
          })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ left: 20, right: 20, top: 20, bottom: 16 })
    }
    .width('90%')
    .constraintSize({ maxHeight: '80%' })
    .backgroundColor(COLORS.cardBg).borderRadius(16)
    .alignItems(HorizontalAlign.Center)
    .position({ x: '5%', y: '8%' })
  }

  @Builder
  removeMasterModal() {
    Column() {
      Text('🫖').fontSize(34).margin({ top: 24 })
      Text('移除大师').fontSize(17).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textMain).margin({ top: 10 })
      Text('确认将 ' + this.data.masters[this.masterIdx].name + ' 移出大师名录?')
        .fontSize(13).fontColor(COLORS.textSub).margin({ top: 10 })
        .padding({ left: 28, right: 28 }).textAlign(TextAlign.Center)
      Row() {
        Text('再想想').fontSize(14).fontColor(COLORS.textSub)
          .backgroundColor(COLORS.border).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.showRemoveMaster = false })
        Text('确认移除').fontSize(14).fontColor(COLORS.white)
          .backgroundColor(COLORS.danger).borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => {
            this.data.removeMaster(this.masterIdx);
            this.showRemoveMaster = false;
          })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ left: 20, right: 20, top: 22, bottom: 20 })
    }
    .width('86%')
    .constraintSize({ maxHeight: '80%' })
    .backgroundColor(COLORS.cardBg).borderRadius(16)
    .alignItems(HorizontalAlign.Center)
    .position({ x: '7%', y: '28%' })
  }

  @Builder
  contentArea() {
    if (this.activeTab === 0) {
      Scroll() {
        Column() {
          Row() {
            Column() {
              Text('12').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
              Text('光器经典').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
            }.layoutWeight(1)
            Column() {
              Text('石瓢').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zhuni)
              Text('最热款').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
            }.layoutWeight(1)
            Column() {
              Text('¥9.8w').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
              Text('本季成交').fontSize(10).fontColor(COLORS.textHint).margin({ top: 2 })
            }.layoutWeight(1)
          }
          .width('100%')
          .padding({ top: 12, bottom: 12 })
          .backgroundColor(COLORS.cardBg).borderRadius(12)
          .margin({ top: 12, bottom: 12 })
          .border({ width: 1, color: COLORS.border })
          ForEach(this.data.plains, (p: PlainPotItem) => {
            Column() {
              Row() {
                Column() {
                  Text(p.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                  Text(p.style + ' · ' + p.clay).fontSize(11)
                    .fontColor(COLORS.textSub).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text('¥' + p.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
                Column()
                  .width(4).height(34).borderRadius(2)
                  .backgroundColor(clayColor(p.clay)).margin({ left: 10 })
                  .opacity(this.amb ? 0.4 : 0.9)
                  .animation({ duration: 1100, iterations: -1, playMode: PlayMode.Alternate })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (p: PlainPotItem) => p.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 1) {
      Scroll() {
        Column() {
          Row() {
            Text('🌸').fontSize(14).margin({ right: 6 })
            Text('花货集珍').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('塑器仿生').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          ForEach(this.data.flowers, (f: FlowerPotItem) => {
            Column() {
              Row() {
                Column() {
                  Text(f.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                  Text(f.theme + ' · ' + f.clay).fontSize(11)
                    .fontColor(COLORS.textSub).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text('¥' + f.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
              }
              .width('100%')
              Row() {
                Column().layoutWeight(1)
                  .height(1).backgroundColor(COLORS.border).margin({ top: 10 })
                Text('✿').fontSize(11).fontColor(COLORS.zhuni).margin({ top: 6, left: 8, right: 8 })
                Column().layoutWeight(1)
                  .height(1).backgroundColor(COLORS.border).margin({ top: 10 })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (f: FlowerPotItem) => f.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 2) {
      Scroll() {
        Column() {
          Row() {
            Text('🌀').fontSize(14).margin({ right: 6 })
            Text('筋囊之器').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('凹凸有致').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          ForEach(this.data.ribs, (r: RibPotItem) => {
            Column() {
              Row() {
                Column() {
                  Text(r.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                  Text(r.ribs + ' 瓣筋纹 · ' + r.clay).fontSize(11)
                    .fontColor(COLORS.textSub).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text('¥' + r.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
            .translate({ x: this.amb ? 4 : 0 })
            .animation({ duration: 1300, iterations: -1, playMode: PlayMode.Alternate })
          }, (r: RibPotItem) => r.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 3) {
      Scroll() {
        Column() {
          Row() {
            Text('🧱').fontSize(14).margin({ right: 6 })
            Text('泥料行市').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('点击可编辑').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          Text('陈腐年限:泥料陈化越久越好用').fontSize(10)
            .fontColor(COLORS.textHint).width('100%').margin({ bottom: 10 })
          ForEach(this.data.clays, (c: ClayItem, i: number) => {
            Column() {
              Row() {
                Text(c.name).fontSize(13).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textMain)
                Text('¥' + c.price + '/斤').fontSize(11).fontColor(COLORS.zini)
                  .margin({ left: 8 })
                Column().layoutWeight(1)
                Text('库存 ' + c.stock + ' 斤').fontSize(10).fontColor(COLORS.textHint)
                Text('编辑').fontSize(10).fontColor(COLORS.zhuni).margin({ left: 10 })
                  .onClick(() => {
                    this.clayIdx = i;
                    this.clayName = c.name;
                    this.clayPrice = c.price.toString();
                    this.showEditClay = true;
                  })
              }
              .width('100%')
              Row() {
                Column()
                  .width(agedBarW(c.aged)).height(8).borderRadius(4)
                  .backgroundColor(clayColor(c.name))
                  .opacity(this.amb ? 0.55 : 1)
                  .animation({ duration: 1000, iterations: -1, playMode: PlayMode.Alternate })
                Column().layoutWeight(1)
                Text(c.aged + ' 年').fontSize(10).fontColor(COLORS.textSub).margin({ left: 8 })
              }
              .width('100%').margin({ top: 8 })
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (c: ClayItem) => c.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 4) {
      Scroll() {
        Column() {
          Row() {
            Text('🧑‍🎨').fontSize(14).margin({ right: 6 })
            Text('大师名录').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('点击可移除').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          ForEach(this.data.masters, (m: MasterItem, i: number) => {
            Column() {
              Row() {
                Column() {
                  Row() {
                    Text(m.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                    Text(m.rank).fontSize(10).fontColor(COLORS.white)
                      .backgroundColor(rankColor(m.rank)).borderRadius(8)
                      .padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
                  }
                  Text(m.style).fontSize(11).fontColor(COLORS.textSub).margin({ top: 5 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Column() {
                  Text(m.years + '年').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.zini)
                  Text(m.works + '件作品').fontSize(10).fontColor(COLORS.textHint).margin({ top: 3 })
                }
                Text('移除').fontSize(10).fontColor(COLORS.danger).margin({ left: 12 })
                  .onClick(() => {
                    this.masterIdx = i;
                    this.showRemoveMaster = true;
                  })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (m: MasterItem) => m.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 5) {
      Scroll() {
        Column() {
          Row() {
            Text('🖐️').fontSize(14).margin({ right: 6 })
            Text('壶艺工序').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('难度指数').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          ForEach(this.data.crafts, (c: CraftItem) => {
            Column() {
              Row() {
                Text(c.name).fontSize(13).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textMain)
                Text(c.rank).fontSize(10).fontColor(COLORS.white)
                  .backgroundColor(rankColor(c.rank)).borderRadius(8)
                  .padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
                Column().layoutWeight(1)
                Text(c.diff + '').fontSize(10).fontColor(COLORS.textSub)
              }
              .width('100%')
              Text(c.desc).fontSize(10).fontColor(COLORS.textHint).width('100%').margin({ top: 4 })
              Row() {
                Column()
                  .width(craftBarW(c.diff)).height(8).borderRadius(4)
                  .backgroundColor(COLORS.zhuni)
                  .opacity(this.amb ? 0.55 : 1)
                  .animation({ duration: 1000, iterations: -1, playMode: PlayMode.Alternate })
                Column().layoutWeight(1)
                Text('难度 ' + c.diff).fontSize(10).fontColor(COLORS.textSub).margin({ left: 8 })
              }
              .width('100%').margin({ top: 8 })
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (c: CraftItem) => c.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else if (this.activeTab === 6) {
      Scroll() {
        Column() {
          Row() {
            Text('🏛️').fontSize(14).margin({ right: 6 })
            Text('镇馆收藏').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('估值横条').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          ForEach(this.data.collects, (co: CollectItem) => {
            Column() {
              Row() {
                Column() {
                  Text(co.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                  Text(co.author + ' · ' + co.era).fontSize(11)
                    .fontColor(COLORS.textSub).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text('¥' + co.value).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.zhuni)
              }
              .width('100%')
              Row() {
                Column()
                  .width(Math.min(170, co.value / 60000)).height(6).borderRadius(3)
                  .backgroundColor(COLORS.zini)
                  .opacity(this.amb ? 0.5 : 1)
                  .animation({ duration: 1200, iterations: -1, playMode: PlayMode.Alternate })
                Column().layoutWeight(1)
              }
              .width('100%').margin({ top: 8 })
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (co: CollectItem) => co.name)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    } else {
      Scroll() {
        Column() {
          Row() {
            Text('📦').fontSize(14).margin({ right: 6 })
            Text('订单台账').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
            Column().layoutWeight(1)
            Text('共 ' + this.data.orders.length + ' 笔').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').padding({ top: 14, bottom: 8 })
          Row() {
            ForEach(TOP_COLLECT_IDX, (i: number) => {
              Column() {
                Text(this.data.orders[i].piece.length > 2
                  ? this.data.orders[i].piece.substring(0, 2) : this.data.orders[i].piece)
                  .fontSize(9).fontColor(COLORS.textHint)
                Column()
                  .width(26).height(valueBarH(this.data.orders[i].amount)).borderRadius(4)
                  .backgroundColor(i % 2 === 0 ? COLORS.zini : COLORS.zhuni)
                  .margin({ top: 6 })
                  .opacity(this.amb ? 0.55 : 1)
                  .animation({ duration: 900 + i * 150, iterations: -1, playMode: PlayMode.Alternate })
                Text((this.data.orders[i].amount / 1000).toFixed(0) + 'k')
                  .fontSize(9).fontColor(COLORS.textSub).margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (i: number) => i.toString())
          }
          .width('100%')
          .padding({ top: 12, bottom: 12 })
          .backgroundColor(COLORS.cardBg).borderRadius(12)
          .margin({ top: 4, bottom: 12 })
          .alignItems(VerticalAlign.Bottom)
          .border({ width: 1, color: COLORS.border })
          Row() {
            Text('新增订单').fontSize(12).fontColor(COLORS.white)
              .backgroundColor(COLORS.zini).borderRadius(16)
              .padding({ left: 22, right: 22, top: 9, bottom: 9 })
              .onClick(() => {
                this.formNo = '';
                this.formBuyer = '';
                this.formPiece = '';
                this.formAmount = '';
                this.showAddOrder = true;
              })
            Column().layoutWeight(1)
            Text('泥料陈腐 · 以陈为贵').fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').margin({ bottom: 12 })
          ForEach(this.data.orders, (o: OrderItem) => {
            Column() {
              Row() {
                Column() {
                  Text(o.piece).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textMain)
                  Text(o.no + ' · ' + o.buyer).fontSize(10)
                    .fontColor(COLORS.textHint).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text(o.status).fontSize(10).fontColor(COLORS.white)
                  .backgroundColor(statusColor(o.status)).borderRadius(8)
                  .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                Text('¥' + o.amount).fontSize(13).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.zini).margin({ left: 8 })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLORS.cardBg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 12, bottom: 12 })
            .margin({ bottom: 10 })
            .border({ width: 1, color: COLORS.border })
          }, (o: OrderItem) => o.no)
        }
        .width('100%').padding({ left: 14, right: 14, bottom: 12 })
      }
      .width('100%')
      .layoutWeight(1)
    }
  }

  @Builder
  bottomBar() {
    Column() {
      Row() {
        ForEach(ROW1_IDX, (i: number) => {
          this.bottomItem(i)
        }, (i: number) => i.toString())
      }
      .width('100%')
      Row() {
        ForEach(ROW2_IDX, (i: number) => {
          this.bottomItem(i)
        }, (i: number) => i.toString())
      }
      .width('100%')
    }
    .width('100%')
    .backgroundColor(COLORS.cardBg)
    .borderRadius({ topLeft: 20, topRight: 20 })
    .border({ width: 1, color: COLORS.border })
  }

  build() {
    Stack() {
      Column() {
        this.headerArea()
        this.contentArea()
        this.bottomBar()
      }
      .width('100%')
      .height('100%')
      .backgroundColor(COLORS.bg)

      if (this.showAddOrder) {
        Stack() {
          this.addOrderModal()
          this.modalOverlay(() => { this.showAddOrder = false })
        }
        .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
      }
      if (this.showEditClay) {
        Stack() {
          this.editClayModal()
          this.modalOverlay(() => { this.showEditClay = false })
        }
        .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
      }
      if (this.showRemoveMaster) {
        Stack() {
          this.removeMasterModal()
          this.modalOverlay(() => { this.showRemoveMaster = false })
        }
        .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
      }
    }
    .width('100%')
    .height('100%')
  }
}

十四、开发环境搭建

要运行紫砂壶坊应用,需要先搭建HarmonyOS开发环境。以下是完整的安装与配置步骤:

安装 DevEco Studio

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

DevEco Studio 安装欢迎界面

选择安装路径

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

选择安装位置

配置安装选项

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

安装选项配置

选择项目模板

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

选择 Empty Ability 模板

配置项目信息

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

配置项目信息

完成上述步骤后,DevEco Studio 会自动初始化项目并下载 HarmonyOS SDK 及相关依赖。将紫砂壶坊的 693.ets 源码文件复制到项目的 entry/src/main/ets/pages/ 目录下,在模块的 Index.ets 中导入该页面即可运行。确保 SDK 版本为 API 24(HarmonyOS 6.1.1),所有 ArkTS 声明式语法(@Entry/@Component/@State/@Observed/@Builder)均可在该版本下正常编译。


十五、总结:紫砂壶坊技术特性与横向对比

15.1 技术特性对比表

紫砂壶坊作为"非遗手作坊"系列应用之一,在架构设计、配色主题、动画体系、数据模型等方面都有独特之处。以下将其与其他同类坊应用进行横向对比:

技术维度 紫砂壶坊 香囊坊 折扇坊 内画鼻烟壶坊
主题文化 宜兴紫砂壶非遗 传统香囊佩饰 苏工折扇书画 内画鼻烟壶工艺
配色主题 紫泥朱泥段泥暖色 香色暖棕系 竹青墨黑系 琉璃翠蓝系
主色调 zini #6B4A3A 紫泥 香色 #8B6F47 竹青 #6B8E5A 琉璃蓝 #4A7A8A
强调色 zhuni #B5573F 朱泥 朱红 #C04A3E 墨黑 #2C2C2C 翠绿 #5E9A6E
色彩字段数 15个(ZishaPalette) 16个(NangPalette) 15个 16个
架构模式 单组件@Builder 多组件@Builder 单组件@Builder 多组件@Builder
@Entry组件数 1个 1个 1个 1个
子组件数 0(全@Builder内联) 8个内容组件 0 8个内容组件
@Builder方法数 8个 6个 8个 7个
Tab数量 8个 8个 8个 8个
Tab布局 双行四列(ROW1/ROW2) 双行四列 双行四列 双行四列
数据模型类 8个@Observed 8个@Observed 8个@Observed 8个@Observed
聚合数据类 ZishaShopData NangShopData ShanShopData BiShopData
数据条目数 74条 70条 72条 76条
数据初始化 new构造函数 对象字面量 new构造函数 对象字面量
数据操作方法 3个(add/edit/remove) 3个 3个 3个
工具函数数 6个 6个 6个 6个
尺寸映射函数 agedBarW/valueBarH/craftBarW 每个Tab不同 每个Tab不同 每个Tab不同
颜色映射函数 clayColor/rankColor/statusColor 统一色系 统一色系 统一色系
头部动画 壶形轮廓(scale呼吸+火焰translate) 香囊摆动(rotate) 扇面展开(scale+rotate) 瓶身旋转(rotate)
FIRE_IDX特效 有(3个火焰光点)
进度条类型 layoutWeight弹性横条+柱状图 layoutWeight弹性横条 固定宽度横条 layoutWeight弹性横条
柱状图 有(订单金额TOP_COLLECT_IDX)
弹窗数量 3个(addOrder/editClay/removeMaster) 3个 2个 3个
弹窗遮罩 #55000000硬编码 统一遮罩组件 #66000000 统一遮罩组件
动画模式 PlayMode.Alternate呼吸 PlayMode.Alternate PlayMode.Normal+Alternate PlayMode.Alternate
动画函数 scale/translate/opacity rotate/scale scale/rotate/translate rotate/opacity
特色互动 泥料编辑+大师移除+订单新增 香囊新增+材料编辑+移除 扇骨调整+扇面编辑 瓶身旋转+内画编辑

15.2 核心技术亮点

紫砂壶坊应用在技术实现上有以下核心亮点:

  1. 壶形轮廓头部动画:独创性地用Stack层叠构建了一个完整的紫砂壶轮廓——壶身大圆(56x56紫泥色scale呼吸脉冲)、壶盖(24x8朱泥色小条)、壶钮(12x12段泥色小圆)、壶嘴(34x10紫泥色条位于右侧)、壶把弧(26x10朱泥色条位于左侧),再叠加FIRE_IDX三个火焰光点(大小递增6/8/10、位置递增、warning赭金与zhuni朱泥交替、translate位移+opacity闪烁),形成"壶身呼吸+火焰升腾"的紫砂窑火意境,是所有坊应用中最复杂的头部动画。

  2. 紫砂泥料语义着色系统:clayColor函数根据泥料名称中的关键字(朱泥/红泥→zhuni朱泥色、段泥/绿泥→duanni段泥色、天青→#5A6B7A青灰色、其他→zini紫泥色)返回对应的紫砂色值,实现"泥料名即颜色"的紫砂语义着色。这种设计将紫砂五色泥料的色彩特性直接编码为函数逻辑,是紫砂文化在代码层面的深度体现。

  3. 八大差异化页面布局:八个Tab每个布局完全不同——光器=在售列表+泥料色条+统计三列、花器=花货列表+花形分隔线(✿符号两侧横线)、筋囊=筋纹列表+横向translate位移呼吸、泥料=陈腐年限横条(agedBarW宽度映射+clayColor泥料着色+编辑交互)、大师=大师名录+等级徽章(rankColor着色)+移除交互、壶艺=难度指数横条(craftBarW宽度映射)、收藏=估值横条(v/60000宽度映射)、订单=柱状图(valueBarH柱高映射+紫泥朱泥交替柱色)+新增交互+台账列表,"一页一型"最大程度还原真实紫砂壶坊的交互体验。

  4. @Observed聚合数据类+new构造函数初始化:ZishaShopData一个@Observed类包含8个数组共74条数据,全部用new构造函数初始化(与香囊坊用对象字面量不同),每个数据项都是@Observed装饰的可观察实例对象,配合3个数据操作方法(addOrder的unshift插入、editClay的索引修改、removeMaster的splice删除),形成完整的响应式数据流。

  5. PlayMode.Alternate呼吸动画体系:全应用共使用了7处PlayMode.Alternate动画——壶身scale呼吸(1200ms)、火焰光点translate+opacity(1400+i300ms递增时长)、光器泥料色条opacity闪烁(1100ms)、筋囊列表translate位移(1300ms)、泥料横条opacity闪烁(1000ms)、壶艺难度横条opacity闪烁(1000ms)、收藏估值横条opacity闪烁(1200ms)、订单柱状图opacity闪烁(900+i150ms递增时长),每个动画的duration和opacity值都根据场景定制,形成"全页呼吸"的紫砂窑火氛围。

  6. 在这里插入图片描述

15.3 标签

HarmonyOS ArkTS API 24 HarmonyOS 6.1.1 紫砂壶坊 ZishaTeapot 非遗手作 @Observed @Builder @Entry @Component @State 紫泥 朱泥 段泥 五色泥料 宜兴紫砂 供春 时大彬 陈鸣远 顾景舟 PlayMode.Alternate Stack层叠 壶形轮廓动画 火焰光点 clayColor rankColor statusColor agedBarW valueBarH craftBarW 柱状图 模态弹窗 条件渲染 ForEach layoutWeight 双行四列Tab 光器花器筋囊 泥料大师壶艺 收藏订单

Logo

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

更多推荐