鸟笼铺非遗制笼应用基于HarmonyOS API 24深度解析——采用了“单入口 + 多内容区 + 底部导航“的经典移动端布局模式。入口组件 BirdcageApp 作为整个应用的根容器
鸟笼铺非遗制笼应用基于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 卡片通过横向进度条展示销量排名前六的鸟笼品种,绣眼平顶笼以 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。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

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

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

16.4 选择项目模板
新建一个空白模板,在模板市场中选择 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 · 华为 · 非遗制笼 · 移动端开发
更多推荐


所有评论(0)