鸟笼铺非遗制笼应用基于HarmonyOS API 24深度解析——采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 BirdcageApp 作为整个应用的根容器

本文基于 HarmonyOS NEXT + ArkTS 声明式开发框架,完整实现一个「鸟笼铺」非遗制笼主题 App,涵盖配色系统、状态管理、多 Tab 页面、自定义弹窗、动态颜色映射、深绿色竹架视觉等核心知识点,并附完整源码与运行效果截图。


一、引言:非遗制笼的数字化传承

在移动互联网与智能设备高速发展的今天,非物质文化遗产的传承与传播已经从单纯的线下演示,演变为一种高度数字化、流程化、个性化的线上线下融合体验。鸟笼,作为中国传统文玩技艺的代表,距今已有数百年历史。一只好笼,从选竹、烤直、圈笼、穿丝到雕花、上漆,需经数十道工序,凝聚着匠人的心血与竹丝的温润。绣眼平顶笼的精巧、画眉高笼的雄浑、百灵矮笼的稳当、八哥大圆笼的开阔、鹩哥高塔笼的挺拔、六角宫灯笼的雅致,每一只笼都承载着中国人对鸣鸟与雅趣的追求与向往。然而,这门古老的技艺正面临着传承人老龄化、受众面狭窄、销售渠道单一等现实困境。

正是在这样的背景下,鸟笼铺类数字化应用应运而生,并迅速成为非遗制笼"活态传承"的数字载体。一个优秀的鸟笼铺应用,不仅需要覆盖从"笼架陈列"到"客评口碑"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到细致入微,让用户在指尖滑动间便能感受到竹丝的温润与制笼的雅致。

本文将深入剖析一个基于鸿蒙 ArkUI 声明式开发框架构建的鸟笼铺非遗制笼应用。该应用以"笼架陈列"为核心,横向扩展出栖木捆、食罐格、订单管理、客评口碑五大功能模块,形成了一个完整闭环的制笼管理生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、模态弹窗、列表渲染、图表可视化、动态颜色映射、深绿色竹架视觉等关键技术点。

鸟笼铺应用整体运行效果 - 本周鸟笼销量柱状图与笼架陈列列表


二、应用整体架构概览

整个应用采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 BirdcageApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态,并根据状态切换不同的内容区组件。

应用一共划分为五个一级页面:笼架陈列(CAGE)、栖木捆(PERCH)、食罐格(FEEDER)、订单管理(ORDER)以及客评口碑(REVIEW)。每个页面都是一个独立的 @Component 装饰的自定义组件,内部封装了自己的状态、构建逻辑和子组件。这种"高内聚、低耦合"的组件化设计,使得每个页面可以独立开发、独立测试,同时也便于后续的扩展与维护。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、鸟笼元数据、栖木元数据、食罐元数据、订单元数据、客评元数据、周销量元数据、笼型构成元数据、工艺热度元数据、TOP榜元数据等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类 BirdcageData 为载体,配合五组各 12 条模拟数据,模拟了一个真实的鸟笼铺数据环境。

在交互层面,应用通过条件渲染(if-else)实现了四类弹窗的叠加显示:新制鸟笼、修改订单、撤下栖木、食罐详情。弹窗以全屏半透明遮罩 + 居中卡片的形式呈现,与主内容共享同一根 Column,通过布尔状态控制显隐。

在视觉层面,应用独创了"深绿色竹架"的鸟笼页设计——鸟笼页整个列表卡片都使用深绿色背景(primaryDark #2E402C 与 #3A5038 奇偶交替),白色文字,浅色价格和标签,模仿鸟笼竹架的深色背景,形成了强烈的视觉识别特征。动态颜色函数返回浅色系(浅绿/浅棕/浅金),用于深绿色背景上的高对比度显示。

鸟笼铺客评模块运行效果 - 客评口碑列表(真机运行)

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


三、鸟笼页:深绿色竹架的笼架陈列

鸟笼页是整个应用的默认首页,也是视觉设计最独特的页面。它包含了本周鸟笼销量柱状图、笼架陈列列表,以及新制鸟笼弹窗。

鸟笼页顶部是"本周鸟笼销量"柱状图卡片,采用白色背景配 1px 竹绿半透明边框,圆角 16。通过 ForEach 遍历一周七天的销量数据,柱子高度直接绑定数值,销量超过 42 的日期(周五、周六、周日)使用主色竹绿 #5E7D5A 突出显示,其余日期使用 accent 木棕 #8A5A3B,形成自然的高低起伏。周六以 56 只的销量居首,符合鸟笼作为文玩用品在周末热销的真实节奏。

下方是笼架陈列列表,采用了独创的"深绿色竹架"设计——每个卡片左侧是一个 46×46 的圆形图标(圆角23,背景色 primaryLight 22% 透明度),右侧信息区展示鸟笼名称(白色文字)、价格(颜色按名称动态着色,浅色系)、笼型标签和丝数标签。卡片使用深绿色背景 primaryDark #2E402C 和 #3A5038 奇偶交替(都是深绿色!),白色文字,模仿鸟笼竹架的深色背景。这是本应用最具辨识度的视觉设计特征。

12 款鸟笼覆盖了从 180 元(手提小方笼)到 1280 元(老竹包浆笼)的完整价格梯队,笼型涵盖平顶圆、高桶形、矮桶形、正方形、长方形、大圆形、塔形、屋形、六角、手提方、方形、圆形十二种,丝数从 36 丝到 96 丝不等,每款都配有独特的 emoji 图标(绣眼平顶笼用⚪、画眉高笼用🛢️、百灵矮笼用🪣、八哥大圆笼用⭕、鹩哥高塔笼用🗼、六角宫灯笼用🏮等),让鸟笼不仅是养鸟器具,更是笼型与工艺的双重体验。右上角 ➕ 按钮点击可弹出"新制鸟笼"弹窗。


四、栖木页:销量排行与撤杠管理

栖木页以"栖木"为核心,顶部是鸟笼销量 TOP6 横向条形图,下方是栖木捆列表,支持撤杠操作。

栖木模块 - 鸟笼销量TOP6横向条形图与栖木捆列表

顶部 TOP6 卡片通过横向进度条展示销量排名前六的鸟笼品种,绣眼平顶笼以 96 的销量居首,画眉高笼、百灵矮笼紧随其后。进度条使用 layoutWeight 比例法实现,已售部分与剩余部分按比例分配宽度,颜色直接取数据中的配置色,六种鸟笼各有特色。

下方栖木捆列表展示 12 款栖木,卡片采用"左图标右文字"布局。左侧是一个 42×42 的圆形图标(圆角21),背景色通过 getPerchColor 函数按木材分档着色(枸杞根/毛竹节/桑枝类浅绿、酸枣木/花椒木/梨木/苹果木类浅棕、其余浅金),展示栖木 emoji 图标。右侧展示栖木名称、木材(枸杞根/酸枣木/青藤皮等)、长度与产地(“18cm · 宁夏 料”)和 🗑️ 撤杠按钮。

12 款栖木木材覆盖枸杞根、酸枣木、青藤皮、绿檀、花椒木、梨木、毛竹节、鼠李木、软木、苹果木、桑枝、砂面木十二种,长度从 15cm 到 25cm 不等,产地从宁夏、河北、云南、进口、陕西、山东、安吉、山西、四川、辽宁、江浙到广东,呈现出"栖木天下"的格局。其中藤皮跳杠长达 25cm,竹节跳杠仅 15cm,防滑砂杠来自广东。点击 🗑️ 按钮弹出"撤下栖木"二次确认弹窗,采用警示型设计语言,"确认撤下"按钮使用危险色赭红,防止误操作。


五、食罐页:笼型构成与食罐详情

食罐页以"食罐"为核心,顶部是笼型构成占比图,下方是食罐格列表,支持查看详情。

食罐模块 - 笼型构成占比图与食罐格列表

顶部笼型构成卡片展示三种笼型的分布占比:圆形 5 款、方形 4 款、异形 3 款,合计 12 款,恰好对应笼架陈列的笼型总数。进度条以 12 为满刻度进行比例渲染,比百分比更直观地传达了"几笼型"的语义。

下方食罐格列表展示 12 款食罐,卡片采用"左图标右文字"布局。左侧是一个 42×42 的图标方块,背景色通过 getFeederColor 函数按窑级别分档着色(≥95用浅金、≥91用浅绿、其余用浅棕),圆角 10,展示食罐 emoji 图标。右侧展示食罐名称、纹饰(青花缠枝/粉彩花鸟等)、窑级别(“窑级94”,颜色按级别动态着色)、产地(“景德镇 · 窑口”)和 👁️ 详情按钮。

12 款食罐纹饰覆盖青花缠枝、粉彩花鸟、素面粗陶、釉里红鱼、双联青瓷、草虫手绘、黑釉月纹、酱釉弦纹、白瓷素面、折枝果纹、哥窑开片、老釉皮壳十二种,窑级别从 87(白瓷浅槽)到 97(老窑口虫罐)不等,老窑口虫罐以 97 级居首,开片水罐 96 级次之,釉里红罐 95 级第三,符合"老窑为魁、开片次之"的行业共识。产地从景德镇、宜兴、龙泉、淄博、磁州、佛山、德化、潮州到邯郸,几乎囊括了中国古代制瓷的全部名窑口。点击 👁️ 按钮弹出"食罐详情"弹窗,包含食罐展示区、文艺风格描述文案(“拉坯成型,利坯至薄而不变形。青花以钴料手绘,一笔一画皆要稳。入窑一次烧成,罐口内收防撒食,罐底留孔好穿丝,挂在笼里,鸟啄食如啄花。”)、窑口产地(“江西 · 景德镇”)和匠人评级(“特级 · 画瓷十五年”)。


六、订单页:制笼工艺热度与订单改单

订单页以"订单"为核心,顶部是制笼工艺热度排行,下方是订单列表,支持修改订单。

订单模块 - 制笼工艺热度排行与笼坊订单列表

顶部制笼工艺热度卡片展示六门工序的热度指数:选竹 95、烤直 94、圈笼 96、穿丝 92、雕花 90、上漆 88,恰好对应制笼制作的完整流程(选竹→烤直→圈笼→穿丝→雕花→上漆),圈笼以 96 的热度居首(不是第一道选竹),呈现出"圈笼为核心工序"的合理分布。六根进度条颜色从竹绿到木棕到金棕到蓝色到浅灰到赭红,形成了一条色彩渐变的工艺流水线。

下方订单列表展示 12 条订单,卡片采用"左图标右文字"布局。左侧是一个 40×40 的圆形图标(圆角20),背景色通过 getOrderColor 函数按订单金额分档着色(15万以上浅金、11-15万浅绿、8-11万浅棕、8万以下浅蓝灰),展示客户 emoji 图标。右侧展示客户名称、✏️ 编辑按钮、区域标签和订单详情(“· 260 只 · ¥168000”)。

12 类客户覆盖鸣鸟协会、宠物连锁、园林景区、茶馆鸟市、文创礼品行、影视道具组、非遗展馆、海外宠物店、社区鸟友会、直播文玩行、花鸟批发市、度假民宿群。宠物连锁以 840 只的数量和 196000 元的金额双居首,海外宠物店虽然只有 380 只,但金额高达 178000,单价最高,体现了非遗制笼在海外市场的高溢价。影视道具组以 90 只的数量最少但金额 142000,单价极高,符合影视道具对精品鸟笼的高需求。点击 ✏️ 按钮弹出"修改订单"弹窗,可调整数量和金额,数量行使用箭头符号"→"直观展示修改前后的对比。


七、客评页:口碑展示与标签体系

客评页是应用中最具"人情味"的页面,展示了 12 条用户好评。

客评模块 - 客评口碑列表

页面顶部是标题行"⭐ 客评口碑"和统计摘要"12条好评 · 平均4.9"。下方 12 条评价卡片采用"左头像右信息"的横向布局:最左侧是用户 emoji 头像,中间是用户名称和日期,右侧是星级评分(通过 String.repeat 一行代码生成对应数量的星星)和评价标签(通过动态颜色函数按标签语义分档着色)。评价内容正文在卡片下方,使用次要色文字和 16px 行高。

12 条评价每条都有独特的标签:“丝距匀净”、“膛深安稳”、“跑台稳当”、“颜值能打”、“结实耐用”、“展馆打卡”、“远渡无损”、“檐下添景”、“批价实在”、“晨鸟迎客”、“包浆温润”、“一夜爆单”,这些标签既概括了评价核心,又形成了鸟笼铺的口碑关键词云。

评价内容充满生活气息,如"平顶笼丝距匀净,鸟儿蹦一天不着凉"、“高笼膛深,画眉换羽期住着安稳”、“矮笼跑台稳,凤凰三点头全看见”、“剧组连用三部戏,笼子一点没散”、“海运两月无损,竹色反而更润”、“院子挂两只笼,客人早起听鸟鸣”、“包浆老笼上手温润,竹香还在”,让数据看起来不是冷冰冰的测试桩,而是真实鸟友的心声。卡片背景使用白色与浅绿色奇偶交替,整体视觉雅致而温润。


八、色彩主题系统:竹绿木棕的制笼视觉语言

8.1 色彩接口定义

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

interface BirdcagePalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  cardAlt: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  line: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  bamboo: string;
  cageCream: string;
}
  • primary:主色调,竹绿 #5E7D5A,用于按钮、强调文字、激活状态、鸟笼页柱状图等高优先级视觉元素,是竹丝的标志性色彩。
  • primaryDark:主色深色变体 #2E402C,作为顶部头部背景和鸟笼页列表卡片背景,营造深竹架的沉稳感。
  • accent:强调色,木棕 #8A5A3B,与竹绿形成冷暖对比,用于栖木、烤竹等元素。
  • bg:页面整体背景色 #F2F5EF,浅绿白暖调,营造竹林般的温润质感。
  • bamboo:竹色 #5E7D5A,专用于头部装饰竖条,语义上强调"竹"的质感。
  • cageCream:笼奶白 #FAFBF8,用于装饰性背景,营造出笼内奶白的柔和质感。

鸟笼铺鸟笼模块真机运行效果 - 竹绿主色与深绿色竹架背景的视觉呈现

8.2 色彩实例化

const COLORS: BirdcagePalette = {
  primary: '#5E7D5A',
  primaryLight: '#A8C0A2',
  primaryDark: '#2E402C',
  accent: '#8A5A3B',
  accentLight: '#C9A285',
  bg: '#F2F5EF',
  cardBg: '#FFFFFF',
  cardAlt: '#E6EDE2',
  textPrimary: '#2C382A',
  textSecondary: '#6E7A6A',
  textHint: '#9CABA0',
  border: '#D8E2D2',
  line: '#E4EBDF',
  success: '#5E7D5A',
  warning: '#B0893F',
  danger: '#8E3B2E',
  white: '#FFFFFF',
  bamboo: '#5E7D5A',
  cageCream: '#FAFBF8'
};

主色 #5E7D5A 是一种醇厚的竹绿色,既契合"制笼"主题(竹丝的标志性色彩),又具有传统、沉稳的气质。强调色 #8A5A3B 木棕与竹绿形成冷暖对比,呼应栖木、烤竹的色彩。bamboo 竹色 #5E7D5A 用于头部装饰竖条,营造出竹丝排列的视觉感。背景色 #F2F5EF 浅绿白暖调与主色同暖色系,营造出竹林般的温润氛围。值得注意的是,本应用的动态颜色函数返回的是浅色系(#A8C0A2浅绿/#C9A285浅棕/#E3C878浅金),专门用于深绿色背景上的价格和标签,形成高对比度显示。这种将所有色彩集中管理的做法保证了全局一致性,支持主题切换,降低了维护成本。

鸟笼铺栖木模块真机运行效果 - 浅绿背景与浅绿色交替卡片的层次效果


九、类型定义:构建严谨的数据契约

应用通过一组接口定义,为每一类业务对象建立了明确的数据契约。

9.1 鸟笼、栖木、食罐元数据

interface CageItem {
  name: string; shape: string; bars: number; price: number; emoji: string;
}
interface PerchItem {
  name: string; wood: string; length: string; origin: string; emoji: string;
}
interface FeederItem {
  name: string; pattern: string; level: number; place: string; emoji: string;
}

CageItem 描述鸟笼的名称、笼型、丝数、价格和图标。PerchItem 描述栖木的名称、木材、长度、产地和图标。FeederItem 描述食罐的名称、纹饰、窑级别、产地和图标,用 level 字段体现"以级定品"。

9.2 订单、客评与图表元数据

interface CageOrderItem {
  name: string; region: string; count: number; amount: number; emoji: string;
}
interface CageReviewItem {
  name: string; score: number; date: string; content: string; tag: string; emoji: string;
}
interface WeekCageMeta { day: string; value: number; }
interface ShapeShareMeta { name: string; count: number; color: string; }
interface CageHotMeta { name: string; heat: number; color: string; }
interface CageTopMeta { name: string; sold: number; color: string; }

四组图表元数据(WeekCageMeta/ShapeShareMeta/CageHotMeta/CageTopMeta)都自带 color 字段,实现了"数据即配色"的图表渲染模式,组件渲染时无需额外的颜色映射逻辑。


十、配置驱动:常量数据与可视化配置

10.1 Tab 导航配置

enum BirdcageTab { CAGE = 0, PERCH = 1, FEEDER = 2, ORDER = 3, REVIEW = 4 }

const TAB_LIST: TabMeta[] = [
  { key: 'cage', icon: '🐦', label: '鸟笼', color: '#5E7D5A' },
  { key: 'perch', icon: '🌿', label: '栖木', color: '#8A5A3B' },
  { key: 'feeder', icon: '🍚', label: '食罐', color: '#B0893F' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#5E7D5A' }
];

枚举提供类型安全的 Tab 标识,TAB_LIST 为每个 Tab 配置独立的图标、标签和主题色:鸟笼用竹绿、栖木用木棕、食罐用金棕、订单用蓝色、客评用竹绿,"一 Tab 一色"让每个页面有鲜明的视觉身份。

10.2 周销量、笼型构成、工艺热度与 TOP 榜

const WEEK_SOLD = [
  { day: '周一', value: 12 }, { day: '周二', value: 18 },
  { day: '周三', value: 25 }, { day: '周四', value: 31 },
  { day: '周五', value: 44 }, { day: '周六', value: 56 }, { day: '周日', value: 49 }
];
const SHAPE_SHARE = [
  { name: '圆形', count: 5, color: '#5E7D5A' },
  { name: '方形', count: 4, color: '#8A5A3B' },
  { name: '异形', count: 3, color: '#B0893F' }
];
const CRAFT_HOT = [
  { name: '选竹', heat: 95 }, { name: '烤直', heat: 94 }, { name: '圈笼', heat: 96 },
  { name: '穿丝', heat: 92 }, { name: '雕花', heat: 90 }, { name: '上漆', heat: 88 }
];
const CAGE_TOP = [
  { name: '绣眼平顶笼', sold: 96 }, { name: '画眉高笼', sold: 93 },
  { name: '百灵矮笼', sold: 90 }, { name: '黄雀方笼', sold: 87 },
  { name: '点颏板笼', sold: 84 }, { name: '八哥大圆笼', sold: 81 }
];

CRAFT_HOT 的六门工艺恰好对应制笼制作的完整流程(选竹→烤直→圈笼→穿丝→雕花→上漆),圈笼以 96 的热度居首(不是第一道选竹),呈现出"圈笼为核心工序"的合理分布。CAGE_COL 常量 [0…11] 用于顶部头部的装饰带渲染,按索引模 3 分别渲染:竹色细竖条 3×16 圆角2(模0)、木棕圆点 8×8(模1)、浅绿圆点 8×8(模2),形成一条竹丝排列的装饰线,呼应鸟笼的"竹丝"视觉元素。


十一、数据模型:可观察的鸟笼实体

@Observed
export class BirdcageData {
  cages: CageItem[] = [ /* 12 款鸟笼 */ ];
  perches: PerchItem[] = [ /* 12 款栖木 */ ];
  feeders: FeederItem[] = [ /* 12 款食罐 */ ];
  orders: CageOrderItem[] = [ /* 12 条订单 */ ];
  reviews: CageReviewItem[] = [ /* 12 条客评 */ ];
}

BirdcageData 是核心数据模型,以五个数组承载鸟笼铺全部业务信息。@Observed 装饰器使得该类实例在被 @State 或 @ObjectLink 引用时,属性变化能自动触发 UI 刷新。每个数组精确包含 12 条数据,"五五对称、各十二条"的设计保证了每个页面都有足够的内容密度。所有数据在类定义时直接初始化,"字段即默认值"的写法避免了构造函数中重复赋值。


十二、模拟数据:构建真实的鸟笼铺数据环境

12.1 鸟笼模拟数据

12 款鸟笼覆盖了从 180 元(手提小方笼)到 1280 元(老竹包浆笼)的完整价格梯队,笼型涵盖平顶圆、高桶形、矮桶形、正方形、长方形、大圆形、塔形、屋形、六角、手提方、方形、圆形十二种,丝数从 36 丝到 96 丝不等。绣眼平顶笼¥380、画眉高笼¥520、百灵矮笼¥320、黄雀方笼¥290、点颏板笼¥260、八哥大圆笼¥680、鹩哥高塔笼¥880、鹦鹉异形笼¥460、六角宫灯笼¥560、手提小方笼¥180、竹木混制笼¥340、老竹包浆笼¥1280,emoji 图标与鸟笼名称呼应(绣眼平顶笼用⚪、画眉高笼用🛢️、八哥大圆笼用⭕、鹩哥高塔笼用🗼等)。

12.2 栖木与食罐模拟数据

栖木木材覆盖枸杞根、酸枣木、青藤皮、绿檀、花椒木、梨木、毛竹节、鼠李木、软木、苹果木、桑枝、砂面木十二种,长度从 15cm 到 25cm 不等,产地从宁夏到广东呈现出"栖木天下"的格局。食罐纹饰覆盖青花缠枝、粉彩花鸟、素面粗陶、釉里红鱼、双联青瓷、草虫手绘、黑釉月纹、酱釉弦纹、白瓷素面、折枝果纹、哥窑开片、老釉皮壳十二种,窑级别从 87 到 97 不等,老窑口虫罐以 97 级居首。

12.3 订单与客评模拟数据

订单覆盖 12 类客户,宠物连锁以 840 只和 196000 元双居首,海外宠物店单价最高,影视道具组数量最少但单价极高。客评 12 条各有独特标签(丝距匀净、膛深安稳、跑台稳当、颜值能打、结实耐用、展馆打卡、远渡无损、檐下添景、批价实在、晨鸟迎客、包浆温润、一夜爆单),评价内容充满生活气息,如"平顶笼丝距匀净,鸟儿蹦一天不着凉"、“海运两月无损,竹色反而更润”。


十三、动态颜色函数:数据驱动的视觉映射

应用通过五个动态颜色函数将业务数据映射为视觉色彩,实现"数据即配色"。值得注意的是,本应用的动态颜色函数返回的是浅色系(#A8C0A2浅绿/#C9A285浅棕/#E3C878浅金),专门用于深绿色背景上的高对比度显示。

function getCageColor(name: string): string {
  if (name === '绣眼平顶笼' || name === '百灵矮笼' || name === '竹木混制笼' || name === '老竹包浆笼') return '#A8C0A2';
  else if (name === '画眉高笼' || name === '黄雀方笼' || name === '手提小方笼') return '#C9A285';
  return '#E3C878';
}
function getPerchColor(wood: string): string {
  if (wood === '枸杞根' || wood === '毛竹节' || wood === '桑枝') return '#A8C0A2';
  else if (wood === '酸枣木' || wood === '花椒木' || wood === '梨木' || wood === '苹果木') return '#C9A285';
  return '#E3C878';
}
function getFeederColor(level: number): string {
  if (level >= 95) return '#E3C878';
  else if (level >= 91) return '#A8C0A2';
  return '#C9A285';
}
function getOrderColor(amount: number): string {
  if (amount >= 150000) return '#E3C878';
  else if (amount >= 110000) return '#A8C0A2';
  else if (amount >= 80000) return '#C9A285';
  return '#8494A6';
}
function getReviewTagColor(tag: string): string { /* 按标签语义分四档着色 */ }
  • getCageColor:按鸟笼名称分三组色彩(绣眼平顶笼/百灵矮笼/竹木混制笼/老竹包浆笼类浅绿、画眉高笼/黄雀方笼/手提小方笼类浅棕、其余浅金)。
  • getPerchColor:按栖木木材分三组(枸杞根/毛竹节/桑枝类浅绿、酸枣木/花椒木/梨木/苹果木类浅棕、其余浅金)。
  • getFeederColor:按窑级别分三档(95以上浅金、91-94浅绿、其余浅棕),“以色定级”。
  • getOrderColor:按订单金额分四档(15万以上浅金、11-15万浅绿、8-11万浅棕、8万以下浅蓝灰)。
  • getReviewTagColor:按评价标签语义分四色系。

值得注意的是,鸟笼页的圆形图标背景色使用 primaryLight 22% 透明度,而价格和标签颜色调用 getCageColor 返回浅色系,卡片整体背景则固定使用深绿色 primaryDark 和 #3A5038 奇偶交替,形成了"图标浅绿、价格浅色、卡片深绿"的视觉层次。这种"深绿色竹架 + 浅色动态颜色 + 圆形图标"的组合是本应用最具辨识度的视觉设计特征。


十四、入口组件:应用的骨架与导航中枢

14.1 组件声明与状态

@Entry
@Component
struct BirdcageApp {
  @State curTab: number = 0;
  @State data: BirdcageData = new BirdcageData();
  @State showAddCage: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeletePerch: boolean = false;
  @State showDetailFeeder: boolean = false;
  @State delPerchName: string = '';
  @State detailFeederName: string = '';
  @State birdFly: boolean = false;
  @State notePulse: boolean = false;
}

10 个 @State 状态分四类:导航状态(curTab)、数据状态(data)、弹窗控制状态(4个布尔+2个参数字符串)、交互动效状态(birdFly、notePulse)。

14.2 顶部头部构建

顶部头部采用 primaryDark #2E402C 深竹绿色背景。第一层标题栏左侧是标题"鸟笼铺"和副标题"Birdcage · 竹丝如弦 笼纳清音",右侧是动效区:鸟🐦点击触发斜向飞行动画(translate x 从 -8 到 8,y 从 2 到 -10,duration 700ms)、音符🎵点击触发缩放脉冲动画(scale 从 0.9 到 1.3,duration 600ms)、竹叶🌿静态装饰。第二层是"装饰带",通过 ForEach 遍历 CAGE_COL,按模 3 渲染竹色细竖条 3×16 圆角2、木棕圆点 8×8、浅绿圆点 8×8,形成一条竹丝排列的装饰线,呼应鸟笼的"竹丝"视觉元素。第三层是标语分割线"🌿 选竹圈笼 · 檐下清音 🐦"。

14.3 底部 Tab 导航

底部导航顶部有一条 3px 高的竹绿色装饰条,与深竹绿色头部形成上下呼应。每个 Tab 激活态有三重视觉反馈:标签文字主题色加粗、背景填充主题色 8% 透明度、主题色实线边框,圆角 14(竹架造型风格)。非激活态用灰色文字、透明背景、无边框。导航栏高度 60。

14.4 条件渲染弹窗

应用采用条件渲染(if-else)实现四个弹窗:新制鸟笼(AddCageModal)、修改订单(EditCageOrderModal)、撤下栖木(DeletePerchModal)、食罐详情(DetailFeederModal)。弹窗直接叠加在根 Column 末尾,通过布尔状态控制显隐,每个弹窗内部是全屏半透明遮罩(#66000000)+ 居中白色卡片。弹窗卡片统一使用圆角 18,信息行圆角 12。


十五、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 BirdcagePalette 接口 + COLORS 常量 集中定义 20 种语义色,含 bamboo/cageCream 装饰色 保证视觉一致性,支持主题切换
数据建模 @Observed 装饰的 BirdcageData 类 5 个数组各 12 条数据,覆盖制笼全业务维度 数据与 UI 双向联动,响应式更新
配置驱动 常量数组 + 元数据接口 WEEK_SOLD/SHAPE_SHARE/CRAFT_HOT/CAGE_TOP 自带 color 数据与表现解耦,扩展零成本
动态颜色 5 个全局映射函数 按名称/木材/级别/金额/标签分档着色,返回浅色系用于深绿背景 数据即配色,高对比度显示
深绿色竹架 primaryDark/#3A5038 交替背景 鸟笼页整个列表深绿色背景,白色文字,浅色价格标签 强烈视觉识别,竹架氛围浓
圆形图标 46x46/42x42/40x40 圆角23/21/20 鸟笼/栖木/订单页图标均为圆形,背景色浅绿半透明 视觉统一,竹架圆润感
状态管理 @State + @ObjectLink 10 个状态分四类(导航/数据/弹窗/动效) 细粒度状态控制,自动触发渲染
组件化 @Component + @Builder 5 个页面组件 + 4 个弹窗组件 + CageTag 通用组件 高内聚低耦合,代码复用率高
模态交互 条件渲染弹窗 + 全屏遮罩 4 个弹窗 if-else 叠加,#66000000 半透明遮罩 灵活可控,完全自定义布局
列表渲染 ForEach + 奇偶交替背景 12 条数据 × 5 页面,cardBg/cardAlt 或深绿交替 数据驱动渲染,长列表可读性强
数据可视化 柱状图 + 横向条形图 height 绑定数据、layoutWeight 比例法、阈值着色 多维度呈现,直观易读
星级渲染 String.repeat 技巧 ‘⭐’.repeat(score) 一行代码生成星级 简洁高效,无需循环
防御编程 字段默认值 + 类型安全 所有数据字段声明即初始化,枚举替代魔法数字 健壮性强,无运行时异常
布局系统 Column/Row + layoutWeight 弹性权重分配、条件渲染弹窗、Scroll 滚动容器 自适应布局,层次分明
交互动效 translate + scale + animation 鸟斜向飞行 700ms、音符缩放脉冲 600ms、EaseOut 曲线 静态头部灵动化,趣味性强
视觉层次 装饰带/圆角14/深绿竹架 CAGE_COL 模3渲染竹丝竖条装饰、全局圆角14、鸟笼页深绿竹架 精致细腻,制笼氛围感强

完整代码

interface BirdcagePalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  cardAlt: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  line: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  bamboo: string;
  cageCream: string;
}

const COLORS: BirdcagePalette = {
  primary: '#5E7D5A',
  primaryLight: '#A8C0A2',
  primaryDark: '#2E402C',
  accent: '#8A5A3B',
  accentLight: '#C9A285',
  bg: '#F2F5EF',
  cardBg: '#FFFFFF',
  cardAlt: '#E6EDE2',
  textPrimary: '#2C382A',
  textSecondary: '#6E7A6A',
  textHint: '#9CABA0',
  border: '#D8E2D2',
  line: '#E4EBDF',
  success: '#5E7D5A',
  warning: '#B0893F',
  danger: '#8E3B2E',
  white: '#FFFFFF',
  bamboo: '#5E7D5A',
  cageCream: '#FAFBF8'
};

enum BirdcageTab {
  CAGE = 0,
  PERCH = 1,
  FEEDER = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'cage', icon: '🐦', label: '鸟笼', color: '#5E7D5A' },
  { key: 'perch', icon: '🌿', label: '栖木', color: '#8A5A3B' },
  { key: 'feeder', icon: '🍚', label: '食罐', color: '#B0893F' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#5E7D5A' }
];

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

interface CageItem {
  name: string;
  shape: string;
  bars: number;
  price: number;
  emoji: string;
}

interface PerchItem {
  name: string;
  wood: string;
  length: string;
  origin: string;
  emoji: string;
}

interface FeederItem {
  name: string;
  pattern: string;
  level: number;
  place: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekCageMeta[] = [
  { day: '周一', value: 12 },
  { day: '周二', value: 18 },
  { day: '周三', value: 25 },
  { day: '周四', value: 31 },
  { day: '周五', value: 44 },
  { day: '周六', value: 56 },
  { day: '周日', value: 49 }
];

const SHAPE_SHARE: ShapeShareMeta[] = [
  { name: '圆形', count: 5, color: '#5E7D5A' },
  { name: '方形', count: 4, color: '#8A5A3B' },
  { name: '异形', count: 3, color: '#B0893F' }
];

const CRAFT_HOT: CageHotMeta[] = [
  { name: '选竹', heat: 95, color: '#5E7D5A' },
  { name: '烤直', heat: 94, color: '#8A5A3B' },
  { name: '圈笼', heat: 96, color: '#B0893F' },
  { name: '穿丝', heat: 92, color: '#4E7BA8' },
  { name: '雕花', heat: 90, color: '#7C8AA0' },
  { name: '上漆', heat: 88, color: '#8E3B2E' }
];

const CAGE_TOP: CageTopMeta[] = [
  { name: '绣眼平顶笼', sold: 96, color: '#5E7D5A' },
  { name: '画眉高笼', sold: 93, color: '#8A5A3B' },
  { name: '百灵矮笼', sold: 90, color: '#B0893F' },
  { name: '黄雀方笼', sold: 87, color: '#7C8AA0' },
  { name: '点颏板笼', sold: 84, color: '#4E7BA8' },
  { name: '八哥大圆笼', sold: 81, color: '#8E3B2E' }
];

@Observed
export class BirdcageData {
  cages: CageItem[] = [
    { name: '绣眼平顶笼', shape: '平顶圆', bars: 64, price: 380, emoji: '⚪' },
    { name: '画眉高笼', shape: '高桶形', bars: 72, price: 520, emoji: '🛢️' },
    { name: '百灵矮笼', shape: '矮桶形', bars: 48, price: 320, emoji: '🪣' },
    { name: '黄雀方笼', shape: '正方形', bars: 56, price: 290, emoji: '⬜' },
    { name: '点颏板笼', shape: '长方形', bars: 40, price: 260, emoji: '▭' },
    { name: '八哥大圆笼', shape: '大圆形', bars: 88, price: 680, emoji: '⭕' },
    { name: '鹩哥高塔笼', shape: '塔形', bars: 96, price: 880, emoji: '🗼' },
    { name: '鹦鹉异形笼', shape: '屋形', bars: 52, price: 460, emoji: '🏠' },
    { name: '六角宫灯笼', shape: '六角', bars: 60, price: 560, emoji: '🏮' },
    { name: '手提小方笼', shape: '手提方', bars: 36, price: 180, emoji: '🧺' },
    { name: '竹木混制笼', shape: '方形', bars: 44, price: 340, emoji: '🪵' },
    { name: '老竹包浆笼', shape: '圆形', bars: 68, price: 1280, emoji: '📜' }
  ];

  perches: PerchItem[] = [
    { name: '枸杞栖木', wood: '枸杞根', length: '18cm', origin: '宁夏', emoji: '🌿' },
    { name: '酸枣栖杠', wood: '酸枣木', length: '20cm', origin: '河北', emoji: '🪵' },
    { name: '藤皮跳杠', wood: '青藤皮', length: '25cm', origin: '云南', emoji: '🪢' },
    { name: '檀木站杠', wood: '绿檀', length: '16cm', origin: '进口', emoji: '🟢' },
    { name: '花椒木杠', wood: '花椒木', length: '22cm', origin: '陕西', emoji: '🌶️' },
    { name: '梨木磨爪杠', wood: '梨木', length: '24cm', origin: '山东', emoji: '🍐' },
    { name: '竹节跳杠', wood: '毛竹节', length: '15cm', origin: '安吉', emoji: '🎋' },
    { name: '麻梨疙瘩杠', wood: '鼠李木', length: '19cm', origin: '山西', emoji: '🟤' },
    { name: '软木粗纹杠', wood: '软木', length: '21cm', origin: '四川', emoji: '🟫' },
    { name: '果木弯杠', wood: '苹果木', length: '23cm', origin: '辽宁', emoji: '🍎' },
    { name: '桑枝天然杠', wood: '桑枝', length: '17cm', origin: '江浙', emoji: '🌱' },
    { name: '防滑砂杠', wood: '砂面木', length: '20cm', origin: '广东', emoji: '🧱' }
  ];

  feeders: FeederItem[] = [
    { name: '青花食罐', pattern: '青花缠枝', level: 94, place: '景德镇', emoji: '🔹' },
    { name: '粉彩水罐', pattern: '粉彩花鸟', level: 92, place: '景德镇', emoji: '🌸' },
    { name: '粗陶食槽', pattern: '素面粗陶', level: 88, place: '宜兴', emoji: '🟤' },
    { name: '釉里红罐', pattern: '釉里红鱼', level: 95, place: '景德镇', emoji: '🔴' },
    { name: '青瓷双联罐', pattern: '双联青瓷', level: 91, place: '龙泉', emoji: '🟩' },
    { name: '手绘虫罐', pattern: '草虫手绘', level: 93, place: '淄博', emoji: '🦗' },
    { name: '黑釉水盂', pattern: '黑釉月纹', level: 89, place: '磁州', emoji: '⚫' },
    { name: '酱釉食罐', pattern: '酱釉弦纹', level: 90, place: '佛山', emoji: '🟫' },
    { name: '白瓷浅槽', pattern: '白瓷素面', level: 87, place: '德化', emoji: '🤍' },
    { name: '彩绘果盘', pattern: '折枝果纹', level: 92, place: '潮州', emoji: '🍑' },
    { name: '开片水罐', pattern: '哥窑开片', level: 96, place: '龙泉', emoji: '🕸️' },
    { name: '老窑口虫罐', pattern: '老釉皮壳', level: 97, place: '邯郸', emoji: '🏺' }
  ];

  orders: CageOrderItem[] = [
    { name: '鸣鸟协会', region: '华北', count: 260, amount: 168000, emoji: '🐦' },
    { name: '宠物连锁', region: '华东', count: 840, amount: 196000, emoji: '🐾' },
    { name: '园林景区', region: '西南', count: 180, amount: 96000, emoji: '🏛️' },
    { name: '茶馆鸟市', region: '华中', count: 320, amount: 88000, emoji: '🍵' },
    { name: '文创礼品行', region: '华南', count: 560, amount: 124000, emoji: '🎁' },
    { name: '影视道具组', region: '华北', count: 90, amount: 142000, emoji: '🎬' },
    { name: '非遗展馆', region: '华东', count: 140, amount: 156000, emoji: '🏢' },
    { name: '海外宠物店', region: '海外', count: 380, amount: 178000, emoji: '🌏' },
    { name: '社区鸟友会', region: '西北', count: 210, amount: 68000, emoji: '👴' },
    { name: '直播文玩行', region: '华东', count: 460, amount: 112000, emoji: '🎥' },
    { name: '花鸟批发市', region: '华中', count: 1200, amount: 138000, emoji: '🏬' },
    { name: '度假民宿群', region: '西南', count: 150, amount: 76000, emoji: '🏡' }
  ];

  reviews: CageReviewItem[] = [
    { name: '绣眼玩家', score: 5, date: '09-04', content: '平顶笼丝距匀净,鸟儿蹦一天不着凉。', tag: '丝距匀净', emoji: '🐦' },
    { name: '画眉老手', score: 5, date: '09-03', content: '高笼膛深,画眉换羽期住着安稳。', tag: '膛深安稳', emoji: '🪶' },
    { name: '百灵饲主', score: 5, date: '09-02', content: '矮笼跑台稳,凤凰三点头全看见。', tag: '跑台稳当', emoji: '🎪' },
    { name: '鹦鹉妈妈', score: 4, date: '09-01', content: '屋形笼可爱,就是盼出大一号。', tag: '颜值能打', emoji: '🏠' },
    { name: '道具师傅', score: 5, date: '08-31', content: '剧组连用三部戏,笼子一点没散。', tag: '结实耐用', emoji: '🎬' },
    { name: '非遗讲解员', score: 5, date: '08-30', content: '雕花六角笼成展馆打卡点。', tag: '展馆打卡', emoji: '🏢' },
    { name: '海外鸟友', score: 5, date: '08-29', content: '海运两月无损,竹色反而更润。', tag: '远渡无损', emoji: '🌏' },
    { name: '茶馆掌柜', score: 4, date: '08-28', content: '笼挂檐下客人爱看,盼配防雨罩。', tag: '檐下添景', emoji: '🍵' },
    { name: '鸟市摊主', score: 5, date: '08-27', content: '批发价实在,周末鸟市全用它。', tag: '批价实在', emoji: '🏬' },
    { name: '民宿主理', score: 5, date: '08-26', content: '院子挂两只笼,客人早起听鸟鸣。', tag: '晨鸟迎客', emoji: '🏡' },
    { name: '老玩家', score: 5, date: '08-25', content: '包浆老笼上手温润,竹香还在。', tag: '包浆温润', emoji: '📜' },
    { name: '主播', score: 5, date: '08-24', content: '开箱视频一夜爆了,问价不断。', tag: '一夜爆单', emoji: '🎥' }
  ];
}

function getCageColor(name: string): string {
  if (name === '绣眼平顶笼' || name === '百灵矮笼' || name === '竹木混制笼' || name === '老竹包浆笼') {
    return '#A8C0A2';
  } else if (name === '画眉高笼' || name === '黄雀方笼' || name === '手提小方笼') {
    return '#C9A285';
  }
  return '#E3C878';
}

function getPerchColor(wood: string): string {
  if (wood === '枸杞根' || wood === '毛竹节' || wood === '桑枝') {
    return '#A8C0A2';
  } else if (wood === '酸枣木' || wood === '花椒木' || wood === '梨木' || wood === '苹果木') {
    return '#C9A285';
  }
  return '#E3C878';
}

function getFeederColor(level: number): string {
  if (level >= 95) {
    return '#E3C878';
  } else if (level >= 91) {
    return '#A8C0A2';
  }
  return '#C9A285';
}

function getOrderColor(amount: number): string {
  if (amount >= 150000) {
    return '#E3C878';
  } else if (amount >= 110000) {
    return '#A8C0A2';
  } else if (amount >= 80000) {
    return '#C9A285';
  }
  return '#8494A6';
}

function getReviewTagColor(tag: string): string {
  if (tag === '丝距匀净' || tag === '远渡无损' || tag === '批价实在' || tag === '包浆温润') {
    return '#A8C0A2';
  } else if (tag === '膛深安稳' || tag === '结实耐用' || tag === '晨鸟迎客') {
    return '#C9A285';
  } else if (tag === '跑台稳当' || tag === '颜值能打' || tag === '展馆打卡' || tag === '檐下添景' || tag === '一夜爆单') {
    return '#E3C878';
  }
  return '#8494A6';
}

@Entry
@Component
struct BirdcageApp {
  @State curTab: number = 0;
  @State data: BirdcageData = new BirdcageData();
  @State showAddCage: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeletePerch: boolean = false;
  @State showDetailFeeder: boolean = false;
  @State delPerchName: string = '';
  @State detailFeederName: string = '';
  @State birdFly: boolean = false;
  @State notePulse: boolean = false;

  @Builder modalOverlay(onClose: () => void) {
    Column() {
      Text('')
        .width(0)
        .height(0)
        .opacity(0)
      Button('')
        .width(1)
        .height(1)
        .opacity(0)
        .onClick(() => {
          onClose();
        })
    }
    .width(1)
    .height(1)
  }

  build() {
    Column() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('🐦 鸟笼铺')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.white)
              Text('Birdcage · 竹丝如弦 笼纳清音')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🐦')
                .fontSize(18)
                .onClick(() => {
                  this.birdFly = !this.birdFly;
                })
                .translate({ x: this.birdFly ? 8 : -8, y: this.birdFly ? -10 : 2 })
                .animation({ duration: 700, curve: Curve.EaseOut })
              Text('🎵')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.notePulse = !this.notePulse;
                })
                .scale({ x: this.notePulse ? 1.3 : 0.9, y: this.notePulse ? 1.3 : 0.9 })
                .animation({ duration: 600, curve: Curve.EaseOut })
              Text('🌿')
                .fontSize(14)
                .margin({ left: 6 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(COLORS.primaryLight + '2E')
            .borderRadius(16)
            .border({ width: 1, color: COLORS.primaryLight + '80' })
          }
          .width('100%')

          Row() {
            ForEach(CAGE_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(3)
                    .height(16)
                    .backgroundColor(COLORS.bamboo)
                    .borderRadius(2)
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primaryLight)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'cage' + r)
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 12 })

          Row() {
            Column()
              .layoutWeight(1)
              .height(1)
              .backgroundColor(COLORS.primaryLight + '66')
            Text('🌿 选竹圈笼 · 檐下清音 🐦')
              .fontSize(9)
              .fontColor(COLORS.primaryLight)
              .margin({ left: 8, right: 8 })
            Column()
              .layoutWeight(1)
              .height(1)
              .backgroundColor(COLORS.primaryLight + '66')
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(COLORS.primaryDark)

        if (this.curTab === BirdcageTab.CAGE) {
          CageContent({ data: this.data, onAdd: () => {
            this.showAddCage = true;
          } })
        } else if (this.curTab === BirdcageTab.PERCH) {
          PerchContent({ data: this.data, onDel: (n: string) => {
            this.delPerchName = n;
            this.showDeletePerch = true;
          } })
        } else if (this.curTab === BirdcageTab.FEEDER) {
          FeederContent({ data: this.data, onDetail: (n: string) => {
            this.detailFeederName = n;
            this.showDetailFeeder = true;
          } })
        } else if (this.curTab === BirdcageTab.ORDER) {
          BirdcageOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          BirdcageReviewContent({ data: this.data })
        }
      }
      .width('100%')
      .height('100%')

      Column() {
        Column()
          .width('100%')
          .height(3)
          .backgroundColor(COLORS.primary)
        Row() {
          ForEach(TAB_LIST, (t: TabMeta) => {
            Column() {
              Text(t.icon)
                .fontSize(19)
              Text(t.label)
                .fontSize(10)
                .fontColor(this.curTab === TAB_LIST.indexOf(t) ? t.color : COLORS.textHint)
                .fontWeight(this.curTab === TAB_LIST.indexOf(t) ? FontWeight.Bold : FontWeight.Normal)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .justifyContent(FlexAlign.Center)
            .padding({ top: 7, bottom: 7 })
            .backgroundColor(this.curTab === TAB_LIST.indexOf(t) ? t.color + '14' : '#00000000')
            .borderRadius(14)
            .border(this.curTab === TAB_LIST.indexOf(t) ? { width: 1, color: t.color } : { width: 0 })
            .onClick(() => {
              this.curTab = TAB_LIST.indexOf(t);
            })
          }, (t: TabMeta) => t.key)
        }
        .width('100%')
        .height(60)
        .padding({ left: 8, right: 8 })
        .backgroundColor(COLORS.cardBg)
      }
      .width('100%')

      if (this.showAddCage) {
        AddCageModal({
          onClose: () => {
            this.showAddCage = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditCageOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeletePerch) {
        DeletePerchModal({
          title: this.delPerchName, onClose: () => {
            this.showDeletePerch = false;
          }
        })
      }
      if (this.showDetailFeeder) {
        DetailFeederModal({
          name: this.detailFeederName, onClose: () => {
            this.showDetailFeeder = false;
          }
        })
      }
    }
  }
}

@Component
struct CageTag {
  @Prop text: string;
  @Prop color: string;

  build() {
    Text(this.text)
      .fontSize(9)
      .fontColor(this.color)
      .padding({ left: 7, right: 7, top: 2, bottom: 2 })
      .backgroundColor(this.color + '1F')
      .borderRadius(9)
      .border({ width: 1, color: this.color + '40' })
  }
}

@Component
struct CageContent {
  @ObjectLink data: BirdcageData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('📈 本周鸟笼销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六56只')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekCageMeta) => {
              Column() {
                Text(w.value + '')
                  .fontSize(8)
                  .fontColor(w.value >= 42 ? COLORS.primary : COLORS.accent)
                Column()
                  .width(16)
                  .height(w.value)
                  .backgroundColor(w.value >= 42 ? COLORS.primary : COLORS.accent)
                  .borderRadius(8)
                  .margin({ top: 4 })
                Text(w.day)
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (w: WeekCageMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '55' })

        Column() {
          Row() {
            Text('🐦 笼架陈列')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击➕制笼')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
            Text('➕')
              .fontSize(14)
              .margin({ left: 8 })
              .onClick(() => {
                this.onAdd();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.cages, (p: CageItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(19)
                  .textAlign(TextAlign.Center)
              }
              .width(46)
              .height(46)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(COLORS.primaryLight + '22')
              .borderRadius(23)
              .border({ width: 1, color: COLORS.primaryLight + '55' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.white)
                  Text('¥' + p.price)
                    .fontSize(10)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(getCageColor(p.name))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  CageTag({ text: p.shape, color: getCageColor(p.name) })
                  CageTag({ text: p.bars + '丝', color: COLORS.primaryLight })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.primaryDark : '#3A5038')
            .borderRadius(14)
            .border({ width: 1, color: COLORS.primaryLight + '26' })
            .margin({ top: 8 })
          }, (p: CageItem, i: number) => 'cg' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct PerchContent {
  @ObjectLink data: BirdcageData;
  onDel: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🏆 鸟笼销量 TOP6')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('绣眼笼居首')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(CAGE_TOP, (t: CageTopMeta) => {
            Row() {
              Text(t.name)
                .fontSize(10)
                .width(76)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(t.sold)
                  .height(12)
                  .backgroundColor(t.color)
                  .borderRadius(6)
                Row()
                  .layoutWeight(100 - t.sold)
                  .height(12)
                  .backgroundColor(COLORS.line)
                  .borderRadius(6)
              }
              .layoutWeight(1)
              .height(12)
              .margin({ left: 6 })
              Text(t.sold + '')
                .fontSize(10)
                .width(28)
                .textAlign(TextAlign.End)
                .fontColor(t.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (t: CageTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '55' })

        Column() {
          Row() {
            Text('🌿 栖木捆')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击🗑️撤杠')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.perches, (p: PerchItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getPerchColor(p.wood) + '22')
              .borderRadius(21)
              .border({ width: 1, color: getPerchColor(p.wood) + '55' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.wood)
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                  Text('🗑️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onDel(p.name);
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text(p.length + ' · ' + p.origin + ' 料')
                    .fontSize(9)
                    .fontColor(COLORS.accent)
                  Text('栖杠')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(14)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: PerchItem, i: number) => 'pc' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct FeederContent {
  @ObjectLink data: BirdcageData;
  onDetail: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('📐 笼型构成')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('圆笼为多')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(SHAPE_SHARE, (s: ShapeShareMeta) => {
            Row() {
              Text(s.name)
                .fontSize(10)
                .width(40)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(s.count)
                  .height(14)
                  .backgroundColor(s.color)
                  .borderRadius(7)
                Row()
                  .layoutWeight(12 - s.count)
                  .height(14)
                  .backgroundColor(COLORS.line)
                  .borderRadius(7)
              }
              .layoutWeight(1)
              .height(14)
              .margin({ left: 6 })
              Text(s.count + ' 款')
                .fontSize(10)
                .width(36)
                .textAlign(TextAlign.End)
                .fontColor(s.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (s: ShapeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '55' })

        Column() {
          Row() {
            Text('🍚 食罐格')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击👁️赏罐')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.feeders, (p: FeederItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getFeederColor(p.level) + '22')
              .borderRadius(10)
              .border({ width: 1, color: getFeederColor(p.level) + '55' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.pattern)
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                  Text('👁️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onDetail(p.name);
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text('窑级' + p.level)
                    .fontSize(9)
                    .fontColor(getFeederColor(p.level))
                  Text(p.place + ' · 窑口')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(14)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: FeederItem, i: number) => 'fd' + p.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct BirdcageOrderContent {
  @ObjectLink data: BirdcageData;
  onEdit: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🔥 制笼工艺热度')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('圈笼最要紧')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(CRAFT_HOT, (h: CageHotMeta) => {
            Row() {
              Text(h.name)
                .fontSize(10)
                .width(52)
                .fontColor(COLORS.textPrimary)
              Row() {
                Row()
                  .layoutWeight(h.heat)
                  .height(10)
                  .backgroundColor(h.color)
                  .borderRadius(5)
                Row()
                  .layoutWeight(100 - h.heat)
                  .height(10)
                  .backgroundColor(COLORS.line)
                  .borderRadius(5)
              }
              .layoutWeight(1)
              .height(10)
              .margin({ left: 6 })
              Text(h.heat + '')
                .fontSize(10)
                .width(28)
                .textAlign(TextAlign.End)
                .fontColor(h.color)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (h: CageHotMeta) => h.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '55' })

        Column() {
          Row() {
            Text('📦 笼坊订单')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('点击✏️改单')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.orders, (o: CageOrderItem, i: number) => {
            Row() {
              Column() {
                Text(o.emoji)
                  .fontSize(16)
                  .textAlign(TextAlign.Center)
              }
              .width(40)
              .height(40)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getOrderColor(o.amount) + '22')
              .borderRadius(20)
              .border({ width: 1, color: getOrderColor(o.amount) + '55' })

              Column() {
                Row() {
                  Text(o.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('✏️')
                    .fontSize(12)
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.onEdit();
                    })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                Row() {
                  Text(o.region)
                    .fontSize(9)
                    .fontColor(getOrderColor(o.amount))
                  Text('· ' + o.count + ' 只 · ¥' + o.amount)
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 6 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(14)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: CageOrderItem, i: number) => 'od' + o.name + i)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct BirdcageReviewContent {
  @ObjectLink data: BirdcageData;

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('⭐ 客评口碑')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('12条好评 · 平均4.9')
            .fontSize(9)
            .fontColor(COLORS.textHint)
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ bottom: 6 })
        ForEach(this.data.reviews, (r: CageReviewItem, i: number) => {
          Column() {
            Row() {
              Text(r.emoji)
                .fontSize(15)
              Column() {
                Text(r.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(r.date)
                  .fontSize(9)
                  .fontColor(COLORS.textHint)
                  .margin({ top: 1 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              .margin({ left: 8 })
              Text('⭐'.repeat(r.score))
                .fontSize(10)
                .fontColor(COLORS.warning)
              CageTag({ text: r.tag, color: getReviewTagColor(r.tag) })
            }
            .width('100%')
            Text(r.content)
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .lineHeight(16)
              .margin({ top: 6 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
          .borderRadius(14)
          .border({ width: 1, color: COLORS.line })
          .margin({ top: 8 })
        }, (r: CageReviewItem, i: number) => 'rv' + r.name + i)
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct AddCageModal {
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('🐦 新制鸟笼')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Row() {
          Text('品名')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('竹丝梅花笼')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 14 })
        Row() {
          Text('工艺')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('三年毛竹 · 烤直圈笼 · 雕梅花窗')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Row() {
          Text('定价')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥660')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(14)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('确认制笼')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.primary)
            .borderRadius(14)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditCageOrderModal {
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('✏️ 修改订单')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Row() {
          Text('客户')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('宠物连锁')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 14 })
        Row() {
          Text('数量')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('840 → 1080 只')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Row() {
          Text('金额')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥252000')
            .fontSize(11)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(14)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('保存修改')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.primary)
            .borderRadius(14)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 14 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeletePerchModal {
  @Prop title: string;
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Text('🗑️ 撤下栖木')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Text('确定撤下「' + this.title + '」吗?')
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
          .margin({ top: 10 })
        Text('撤下的栖木将从栖木捆移除。')
          .fontSize(10)
          .fontColor(COLORS.textHint)
          .margin({ top: 4 })
        Row() {
          Text('再想想')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(14)
            .border({ width: 1, color: COLORS.border })
            .onClick(() => {
              this.onClose();
            })
          Text('确认撤下')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .backgroundColor(COLORS.danger)
            .borderRadius(14)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailFeederModal {
  @Prop name: string;
  onClose: () => void = () => {};

  build() {
    Column() {
      Column() {
        Row() {
          Text('👁️ 食罐详情')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('✕')
            .fontSize(16)
            .fontColor(COLORS.textHint)
            .onClick(() => {
              this.onClose();
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Column() {
          Text('🍚')
            .fontSize(30)
          Text(this.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .margin({ top: 6 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(14)
        .margin({ top: 12 })
        Text('拉坯成型,利坯至薄而不变形。青花以钴料手绘,一笔一画皆要稳。入窑一次烧成,罐口内收防撒食,罐底留孔好穿丝,挂在笼里,鸟啄食如啄花。')
          .fontSize(11)
          .fontColor(COLORS.textSecondary)
          .lineHeight(17)
          .margin({ top: 10 })
        Row() {
          Text('窑口产地')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('江西 · 景德镇')
            .fontSize(11)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Row() {
          Text('匠人评级')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('特级 · 画瓷十五年')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(12)
        .margin({ top: 8 })
        Text('知道了')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 18, right: 18, top: 7, bottom: 7 })
          .backgroundColor(COLORS.primary)
          .borderRadius(14)
          .margin({ top: 12 })
          .onClick(() => {
            this.onClose();
          })
      }
      .width('86%')
      .padding(16)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(18)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}


十六、开发环境搭建

16.1 安装 DevEco Studio

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

DevEco Studio 安装欢迎界面

16.2 选择安装路径

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

选择安装位置

16.3 配置安装选项

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

安装选项配置

16.4 选择项目模板

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

选择 Empty Ability 模板

16.5 配置项目信息

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

配置项目信息


十七、总结

通过对这款鸟笼铺非遗制笼应用的深度剖析,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、防御性编程等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Component,每个组件内部通过 @Builder 进一步复用 UI 片段;状态层利用 ArkUI 的 @State、@Observed 和 @ObjectLink 机制,实现了数据变化到 UI 刷新的自动响应。

在数据层面,BirdcageData 类作为核心数据模型,以 5 个数组各 12 条数据完整描述了鸟笼铺的全部业务信息。5 个动态颜色映射函数将业务数据转化为视觉色彩,实现了"数据即配色"的高级渲染模式,这是该应用最具特色的技术亮点之一。

在视觉层面,本应用独创了"深绿色竹架"的鸟笼页设计——鸟笼页整个列表卡片都使用深绿色背景(primaryDark #2E402C 与 #3A5038 奇偶交替),白色文字,浅色价格和标签,模仿鸟笼竹架的深色背景,形成了强烈的视觉识别特征。动态颜色函数返回浅色系(浅绿/浅棕/浅金),专门用于深绿色背景上的高对比度显示。全局圆形图标设计,模仿竹架的圆润造型。统一的色彩系统是整个应用的视觉基石,竹绿主色搭配木棕强调色,配合金棕语义色和蓝色装饰色,营造出既传统又雅致的制笼主题氛围。顶部头部的竹丝竖条装饰带、鸟斜向飞行+音符缩放脉冲微动效、深绿色竹架与圆形图标等细节处理,使得界面在信息密度较高的同时依然保持精致与舒适,处处透露出"鸟笼铺"的主题气质。

在交互层面,应用实现了完整的 CRUD 闭环——新制鸟笼、修改订单、撤下栖木、食罐详情四类弹窗各司其职,配合统一的条件渲染机制和全屏半透明遮罩,交互流畅且一致。撤杠操作的二次确认、订单金额的动态着色、销量阈值的高亮显示等细节,都体现了对用户体验的深入考量。

鸟笼铺应用整体运行效果 - 本周鸟笼销量柱状图与笼架陈列列表


标签: HarmonyOS · ArkTS · 华为 · 非遗制笼 · 移动端开发

Logo

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

更多推荐