摘要:本文以一款聚焦中国传统青瓷文化的移动端应用「青瓷坊」为完整案例,深度解析鸿蒙HarmonyOS ArkTS声明式UI开发范式的核心要点。从以「粉青·梅子青·天青」为主线的19色古瓷色彩体系,到底部五页Tab路由、状态驱动的条件渲染、自定义模态弹窗体系,再到@Observed/@ObjectLink响应式数据流在多业务模块中的实践,逐段拆解每一个关键代码块背后的设计思想与ArkTS技术原理。全文覆盖接口约束、枚举安全、数据建模、状态驱动渲染、组件拆分策略、颜色透明度拼接、ForEach键值策略、声明式动画、弹性权重布局等20余个技术知识点,适合已有ArkTS基础、希望深入理解鸿蒙声明式UI架构的开发者进阶阅读。


一、前言:雨过天青云破处的数字再现

青瓷,是中国陶瓷史上最具文人气质与审美高度的品类之一。自商周原始青瓷肇始,历经越窑秘色、龙泉粉青、汝窑天青,至南宋达到审美巅峰。「雨过天青云破处,这般颜色做将来」,柴世宗一句御批,将青瓷釉色定格在中国人审美理想的最高点;而龙泉窑的梅子青、粉青,则以厚釉失透、莹润如玉的质感,成为宋代文人案头最具温度的陪伴。

在数字化浪潮席卷文化消费领域的今天,将这样一门承载着深厚工艺传统的非遗主题以移动应用的形式呈现给现代用户,既是对传统器物传播方式的一次创新探索,也是对鸿蒙ArkTS开发技术的一次完整实践。本文所剖析的「青瓷坊」应用,正是围绕这一主题构建的一个集青瓷陈列、釉彩库、窑口名录、订单台账与客户评价于一体的综合性移动端展示平台。整个应用在视觉层面追求「天青·梅子青·月白」的宋瓷色调,在交互层面覆盖了多Tab页面路由、数据可视化图表、列表卡片、自定义弹窗与微动画等典型场景。

在分析源码的过程中,我们不仅需要关注每一行UI代码如何还原青瓷釉色的温润质感,更需要深入理解ArkTS框架的底层机制:状态管理如何驱动UI刷新、父子组件如何高效通信、@Observed@ObjectLink如何构建可观察的数据流、自定义弹窗如何以条件渲染的方式优雅叠加。这些问题的答案,都隐藏在接下来的每一个代码块之中。本文将从架构总览开始,逐层深入,带你完成一次从「能写」到「写好」的鸿蒙开发进阶之旅。

二、项目架构总览:分层设计与模块化思想

本应用采用经典的单文件声明式架构,所有逻辑集中在一个源码单元中,通过清晰的分层组织实现高内聚、低耦合。整体架构可以划分为以下六个层次:

第一层——类型定义层(第1~132行):包含CeladonPalette色彩接口、CeladonTab枚举、TabMeta标签元数据接口以及全部业务实体接口(CeladonItemGlazeItemKilnItemCeladonOrderItemCeladonReviewItem)和图表数据元接口(WeekCeladonMetaGlazeShareMetaCeladonHotMetaCeladonTopMeta)。这一层是整个应用的「契约」,所有后续的数据结构都必须严格遵循这些接口定义。

第二层——常量实例层(第23~167行):将抽象的类型定义具象化为可用的运行时对象,包括COLORS色彩常量、TAB_LIST标签列表、CELADON_COL装饰序列以及四组静态图表数据(WEEK_SOLD周销量、GLAZE_SHARE釉色构成、CELADON_HOT烧制工艺热度、CELADON_TOP青瓷销量排行)。

第三层——响应式数据类(第169~245行):@Observed装饰的CeladonData类,封装了五组核心业务数据的动态数组,是整个应用的状态中枢。

第四层——工具函数层(第247~300行):五个纯函数式的颜色映射器(getGlazeColorgetKilnColorgetPriceColorgetOrderColorgetReviewTagColor),根据业务规则返回语义化的颜色值。

第五层——主入口与容器组件(第302~520行):@Entry标记的CeladonApp根组件,负责全局状态管理、页面骨架搭建、Tab路由分发和弹窗条件渲染。

第六层——子组件体系(第522行至文件末尾):包括1个标签组件(CeladonTag)、5个内容组件(分别对应5个Tab页)和4个模态弹窗组件,共同构成完整的功能矩阵。

这种分层设计的优势在于:当需要修改某个功能模块时,开发者可以快速定位到对应的层次进行修改,而不会对其他部分产生连锁反应。例如,如果需要调整配色方案,只需修改第一层的接口定义和第二层的常量实例;如果需要新增一个Tab页面,只需在第五层添加路由分支并在第六层新增对应的内容组件即可。正是这种清晰的边界,让单文件应用也能具备不亚于多文件工程的维护性。

三、色彩体系设计:接口约束与常量定义

3.1 调色板接口定义

interface CeladonPalette {
  primary: string;        // 主色——粉青釉色
  primaryLight: string;   // 主色浅变体
  primaryDark: string;    // 主色深变体,用于深色Header
  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;          // 纯白
  gold: string;           // 描金色
  silkWhite: string;      // 绢白/乳白
}

这段代码首先定义了一个名为CeladonPalette的接口,它是整个应用视觉系统的「宪法」。接口中一共定义了19个颜色字段,覆盖了五大语义类别:

  • 主色调系列primary / primaryLight / primaryDark):以粉青#6F9BA4为核心,模拟龙泉窑粉青釉的典雅色调。浅变体#93BCC4用于次级强调与分割线,深变体#4A7A85用于顶部沉浸式Header背景。
  • 强调色系列accent / accentLight):梅子青#5B8A72取自龙泉窑最著名的釉色之一,用于高亮数据条、选中态与成功语义;其浅变体#9CBFA8用于柔和装饰。
  • 背景色系bg / cardBg / cardAlt):#EEF4F2作为页面底色,营造青瓷釉面般的清凉感;纯白#FFFFFF作为卡片底色;#E4EDE9作为斑马纹交替行背景,增加列表的节奏感。
  • 文本色三级灰度textPrimary / textSecondary / textHint):从#243B3F#5E7A80再到#93A8AC,形成清晰的信息层级,分别对应标题、正文和辅助说明。
  • 功能语义色success / warning / danger):钴蓝#4E7BA8对应信息/订单类元素,姜黄#C98A4E用于提醒与评级,朱砂红#9E3B2F用于危险/高价警示。
  • 材质专属色gold / silkWhite):描金色#C9A24B对应汝窑口沿的「紫口铁足」或龙泉「金丝铁线」的视觉意象;绢白色#F7F3E9则用于模拟瓷器胎骨的温润质感。

使用interface而非type来定义调色板结构,是因为interface在ArkTS/TypeScript中具有更好的类型推断能力和扩展性。更重要的是,通过接口约束,任何遗漏字段或类型不匹配都会在编译阶段被TypeScript编译器捕获,实现了「编译期安全」。

3.2 色彩常量实例化

const COLORS: CeladonPalette = {
  primary: '#6F9BA4',
  primaryLight: '#93BCC4',
  primaryDark: '#4A7A85',
  accent: '#5B8A72',
  accentLight: '#9CBFA8',
  bg: '#EEF4F2',
  cardBg: '#FFFFFF',
  cardAlt: '#E4EDE9',
  textPrimary: '#243B3F',
  textSecondary: '#5E7A80',
  textHint: '#93A8AC',
  border: '#CFDFDC',
  line: '#E6EFEC',
  success: '#4E7BA8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#C9A24B',
  silkWhite: '#F7F3E9'
};

COLORS常量被显式标注为CeladonPalette类型,这意味着TypeScript编译器会在编译阶段检查该对象是否完整实现了接口定义的全部19个字段。如果遗漏了任何一个颜色字段,或者字段类型不匹配,编译器都会立即报错。这种「编译期安全」在大型项目中极其重要,能在代码运行之前就消灭掉大量潜在的类型错误。

所有颜色值均采用标准的#RRGGBB六位十六进制格式。在ArkTS生态中这是务实的选择——声明式UI直接支持字符串形式的颜色值,无需额外的运行时解析开销。同时,将所有颜色集中在一个常量对象中管理,使得后续的主题切换(例如实现「暗色模式」)只需要替换这一常量对象即可,体现了良好的架构前瞻性。

四、枚举与标签配置:类型安全的页面路由

4.1 Tab枚举定义

enum CeladonTab {
  CELADON = 0,
  GLAZE = 1,
  KILN = 2,
  ORDER = 3,
  REVIEW = 4
}

CeladonTab枚举定义了应用底部导航栏的五个页面标识,从0到4依次对应:青瓷、釉彩、窑口、订单、客评。使用枚举而非魔术数字(magic number)的好处是多方面的:

第一,类型安全。当我们在代码中编写this.curTab === CeladonTab.CELADON时,编辑器会提供自动补全和拼写检查,有效避免将CELADON误写成CELEDON之类的低级错误。

第二,语义明确。枚举成员名称本身就是最好的文档——任何阅读代码的人都能立刻理解CeladonTab.REVIEW代表的是「客评」页面,而数字4则需要查阅上下文才能确定含义。

第三,重构友好。如果未来需要调整Tab的顺序,只需修改枚举值的赋值顺序,所有引用该枚举的地方都会自动适配。

4.2 标签元数据配置

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

const TAB_LIST: TabMeta[] = [
  { key: 'celadon', icon: '🍶', label: '青瓷', color: '#6F9BA4' },
  { key: 'glaze', icon: '🎨', label: '釉彩', color: '#5B8A72' },
  { key: 'kiln', icon: '🔥', label: '窑口', color: '#C98A4E' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#C9A24B' }
];

这是一个非常优雅的设计模式:将标签的配置信息(唯一标识key、图标icon、显示文字label、主题色color)抽象为一个数据数组,而非硬编码在组件的build()方法中。这种「数据驱动UI」的思路,使得后续新增或修改标签时,只需要修改TAB_LIST数组即可,无需触碰任何组件代码。

每个标签还拥有独立的主题色,这为选中态的视觉表现提供了丰富的个性化空间。例如,「青瓷」Tab选中时使用粉青高亮,「窑口」Tab使用姜黄高亮,「客评」Tab使用描金高亮,让用户在不同页面间切换时能获得一致的色彩反馈但又各具辨识度。

五、数据模型设计:接口驱动的类型安全

5.1 业务实体接口定义

interface CeladonItem {
  name: string;    // 青瓷品名
  glaze: string;   // 釉色(粉青/梅子青/天青等)
  kiln: string;    // 窑口(龙泉窑/汝窑/官窑等)
  price: number;   // 价格(元)
  emoji: string;   // 表情图标
}

interface GlazeItem {
  name: string;    // 釉名
  tone: string;    // 釉调描述(淡雅/莹润等)
  level: number;   // 釉层厚度等级(0-100)
  place: string;   // 产地
  emoji: string;
}

interface KilnItem {
  name: string;    // 窑口名
  craft: string;   // 核心工艺(厚釉/天青/开片等)
  level: number;   // 窑火/窑龄评级
  region: string;  // 地区
  emoji: string;
}

interface CeladonOrderItem {
  name: string;    // 客户名称
  region: string;  // 所属区域
  count: number;   // 订购数量
  amount: number;  // 订单金额
  emoji: string;
}

interface CeladonReviewItem {
  name: string;    // 评价人姓名
  score: number;   // 评分(1-5星)
  date: string;    // 评价日期
  content: string; // 评价内容
  tag: string;     // 评价标签
  emoji: string;
}

这里展示了五种业务实体的接口定义。每个接口都围绕青瓷行业的实际业务场景精心设计:

  • CeladonItem(青瓷条目):包含品名(如「梅子青瓶」「粉青贯耳」)、釉色、窑口、价格和表情图标。其中glaze字段不仅是显示文本,也是后续getGlazeColor()颜色映射函数的输入参数;price字段则直接驱动getPriceColor()的颜色分级。
  • GlazeItem(釉彩条目):记录釉名、釉调(如「淡雅」「莹润」「雨过」「温厚」)、釉层厚度等级和产地。level字段是釉彩库页面的核心数据维度,直接影响釉层厚度进度条的长度与颜色。
  • KilnItem(窑口条目):记录窑口名、核心工艺、窑龄/窑火评级和地区。craft字段与level字段共同决定窑口卡片的视觉风格,其中level驱动getKilnColor()的颜色分级。
  • CeladonOrderItem(订单条目):记录客户名称、所属区域、订购数量和金额。amount字段是getOrderColor()的颜色映射依据——金额越高颜色越「危险」(朱砂红),形成直观的业务警示。
  • CeladonReviewItem(评价条目):包含评分、日期、评价内容和标签。tag字段是getReviewTagColor()的映射输入,不同类型的评价标签被赋予不同的主题色。

这五个接口构成了整个应用的数据模型基石。emoji字段虽然不是严格的业务数据,但在ArkTS的Text组件中可以直接渲染表情符号,为每个条目提供了轻量级的视觉标识,增强了列表的可读性和趣味性。

5.2 图表数据元接口

interface WeekCeladonMeta {
  day: string;   // 星期几
  value: number; // 销量
}

interface GlazeShareMeta {
  name: string;   // 釉色名称
  count: number;  // 数量
  color: string;  // 主题色
}

interface CeladonHotMeta {
  name: string;   // 工艺名称
  heat: number;   // 热度值
  color: string;  // 主题色
}

interface CeladonTopMeta {
  name: string;   // 青瓷品名
  sold: number;   // 销量
  color: string;  // 主题色
}

这四个接口专门服务于应用中的四种图表/排行榜可视化组件。它们的设计遵循了一个共同的范式:名称 + 数值 + 主题色的三元组结构。这种统一的数据形状使得四种图表可以共享同一套渲染逻辑(ForEach + layoutWeight条形图),只是数据源不同而已。

WeekCeladonMeta用于垂直柱状图(周销量走势),GlazeShareMeta/CeladonHotMeta/CeladonTopMeta用于水平条形图(占比/热度/排行)。每种接口都自带color字段,使得每个数据条都可以拥有独立的颜色,而不是强制使用统一的系列色。

六、静态数据集:应用的基础数据源

6.1 图表静态数据

const WEEK_SOLD: WeekCeladonMeta[] = [
  { day: '周一', value: 22 },
  { day: '周二', value: 30 },
  { day: '周三', value: 38 },
  { day: '周四', value: 45 },
  { day: '周五', value: 56 },
  { day: '周六', value: 63 },
  { day: '周日', value: 58 }
];

这里展示了周销量数据。在实际项目中,这些数据通常来自后端API接口,但本应用作为展示型Demo采用了静态数据。数据的设计颇具巧思——销量从周一到周六逐步递增(22→30→38→45→56→63),周日略有回落(58),符合文旅消费与手作瓷器零售「工作日平稳、周末高峰、周日略降」的实际规律,让数据看起来真实可信。

const GLAZE_SHARE: GlazeShareMeta[] = [
  { name: '粉青', count: 5, color: '#6F9BA4' },
  { name: '梅子青', count: 4, color: '#5B8A72' },
  { name: '天青', count: 3, color: '#4E7BA8' },
  { name: '月白', count: 2, color: '#C9A24B' }
];

釉色构成数据反映了青瓷陈列中最常见的四种釉色及其数量占比。粉青(5款)居首,是龙泉窑最具代表性的釉色;梅子青(4款)次之,釉色如青梅般莹润;天青(3款)源自汝窑;月白(2款)则如定窑白瓷般素净。每一种釉色都绑定了一个主题色,使得图表条形的颜色与其文化意象保持一致。

6.2 工艺热度与排行榜数据

const CELADON_HOT: CeladonHotMeta[] = [
  { name: '素烧施釉', heat: 96, color: '#6F9BA4' },
  { name: '厚釉多次', heat: 95, color: '#5B8A72' },
  { name: '还原焰烧', heat: 94, color: '#4E7BA8' },
  { name: '釉下刻花', heat: 93, color: '#93BCC4' },
  { name: '跳刀纹饰', heat: 91, color: '#C98A4E' },
  { name: '冰裂开片', heat: 90, color: '#C9A24B' }
];

const CELADON_TOP: CeladonTopMeta[] = [
  { name: '梅子青瓶', sold: 96, color: '#5B8A72' },
  { name: '粉青贯耳', sold: 92, color: '#6F9BA4' },
  { name: '天青葵口', sold: 89, color: '#4E7BA8' },
  { name: '月白盏', sold: 86, color: '#C9A24B' },
  { name: '豆青洗', sold: 82, color: '#93BCC4' },
  { name: '翠青弦纹', sold: 79, color: '#C98A4E' }
];

烧制工艺热度数据涵盖了青瓷制作的六大核心工序,从「素烧施釉」(96分)到「冰裂开片」(90分),热度值梯度平缓,说明每一道工序在制瓷匠人心中都至关重要。青瓷销量TOP6则列出了最受欢迎的六款器物,「梅子青瓶」以96的销量遥遥领先,「粉青贯耳」「天青葵口」紧随其后,完整覆盖了主流釉色与器型。

七、响应式数据类:@Observed装饰器的深度运用

@Observed
export class CeladonData {
  celadons: CeladonItem[] = [
    { name: '梅子青瓶', glaze: '梅子青', kiln: '龙泉窑', price: 980, emoji: '🍶' },
    { name: '粉青贯耳', glaze: '粉青', kiln: '官窑', price: 1280, emoji: '🏺' },
    { name: '天青葵口', glaze: '天青', kiln: '汝窑', price: 1580, emoji: '🫖' },
    // ... 共12件青瓷
  ];

  glazes: GlazeItem[] = [
    { name: '粉青釉', tone: '淡雅', level: 96, place: '龙泉', emoji: '💠' },
    { name: '梅子青釉', tone: '莹润', level: 97, place: '处州', emoji: '🫒' },
    // ... 共12种釉彩
  ];

  kilns: KilnItem[] = [
    { name: '龙泉窑', craft: '厚釉', level: 96, region: '浙江', emoji: '⛰️' },
    { name: '汝窑', craft: '天青', level: 95, region: '河南', emoji: '🏔️' },
    // ... 共12个窑口
  ];

  orders: CeladonOrderItem[] = [
    { name: '故宫文创', region: '华北', count: 800, amount: 720000, emoji: '🏯' },
    // ... 共12条订单
  ];

  reviews: CeladonReviewItem[] = [
    { name: '故宫文创买手', score: 5, date: '09-03',
      content: '梅子青釉色莹润欲滴,摆上展柜衬得满室生辉。',
      tag: '釉色莹润', emoji: '🏯' },
    // ... 共12条评价
  ];
}

这是整个应用最核心的数据层代码@Observed装饰器是ArkTS框架提供的关键装饰器,它的作用是将一个普通的类标记为「可观察的」。当一个类被@Observed标记后,该类实例的属性变化会被ArkTS框架自动追踪,任何引用了该实例属性的@ObjectLink@State变量都会在属性变化时自动触发UI重新渲染。

CeladonData类封装了五组核心业务数据:

  • celadons(12件青瓷):从「梅子青瓶」(¥980,龙泉窑)到「天青盏托」(¥920,汝州窑),覆盖主流釉色、窑口与价位段。
  • glazes(12种釉彩):涵盖粉青、梅子青、天青、豆青、月白、翠青、冬青、龙泉青釉等经典釉色,产地覆盖龙泉、汝州、汴京、临安等地。
  • kilns(12个窑口):从龙泉窑、汝窑、官窑、哥窑到景德镇窑、定窑等,完整覆盖宋代以来最重要的青瓷窑口体系。
  • orders(12条订单):客户涵盖故宫文创、茶空间品牌、博物馆商店、海外瓷器馆、高端民宿等多元渠道,区域分布华北、华东、华中、华南、西南与海外。
  • reviews(12条评价):全部4-5星好评,标签涵盖「釉色莹润」「茶席雅物」「如脂似玉」「窑火纯正」「品控严苛」等多维度评价。

这种将所有业务数据集中在一个@Observed类中的设计,带来了单一数据源原则(Single Source of Truth)。无论哪个子组件需要访问青瓷列表、釉彩数据还是订单信息,它们都通过@ObjectLink引用同一个CeladonData实例。当任何一个数据发生变化时,所有引用该数据的组件都会自动收到通知并刷新UI,无需手动编写繁琐的事件监听和数据同步逻辑。

八、颜色工具函数:纯函数的设计哲学

function getGlazeColor(glaze: string): string {
  if (glaze === '梅子青' || glaze === '翠青') {
    return '#5B8A72';
  } else if (glaze === '天青' || glaze === '粉青') {
    return '#6F9BA4';
  } else if (glaze === '月白' || glaze === '冬青') {
    return '#C9A24B';
  }
  return '#4E7BA8';
}

function getKilnColor(level: number): string {
  if (level >= 95) { return '#6F9BA4'; }
  else if (level >= 93) { return '#5B8A72'; }
  return '#C98A4E';
}

function getPriceColor(price: number): string {
  if (price >= 1200) { return '#9E3B2F'; }
  else if (price >= 800) { return '#6F9BA4'; }
  else if (price >= 500) { return '#5B8A72'; }
  return '#C9A24B';
}

function getOrderColor(amount: number): string {
  if (amount >= 600000) { return '#9E3B2F'; }
  else if (amount >= 450000) { return '#6F9BA4'; }
  else if (amount >= 300000) { return '#5B8A72'; }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '釉色莹润' || tag === '如脂似玉' || tag === '窑火纯正' || tag === '品控严苛') {
    return '#6F9BA4';
  } else if (tag === '茶席雅物' || tag === '雅致上镜' || tag === '成套齐整') {
    return '#5B8A72';
  } else if (tag === '古朴大方' || tag === '物料齐全' || tag === '纹饰利落' || tag === '百看不厌') {
    return '#C9A24B';
  } else if (tag === '走量飞快') {
    return '#C98A4E';
  }
  return '#4E7BA8';
}

这五个函数构成了应用的颜色映射层,它们都是纯函数(Pure Function)——相同的输入永远产生相同的输出,没有任何副作用。这种函数式的设计哲学在ArkTS开发中具有多重价值:

第一,可测试性极强。每个函数都可以独立编写单元测试,传入已知输入、断言输出颜色,无需mock任何组件或状态。

第二,逻辑集中且可复用。颜色映射规则分散在多个组件中是维护噩梦。例如,如果要调整「高价青瓷」的阈值,现在只需改getPriceColor一处,所有使用价格颜色的地方都会自动生效。

第三,语义化表达业务规则。函数名本身就是最好的文档——getGlazeColor(glaze)一眼就能看出是根据釉色返回主题色;getOrderColor(amount)则是根据订单金额返回金额颜色。

特别值得分析的是getPriceColor阶梯式阈值设计:价格≥1200返回朱砂红(danger色,暗示「高端珍品」),800-1199返回粉青,500-799返回梅子青,<500返回描金色(暗示「性价比之选」)。这种将业务语义(价格区间)映射到视觉语义(颜色情感)的模式,是数据可视化中的经典手法。

getReviewTagColor则展示了更精细的语义分类:与釉色品质相关的标签使用粉青,与使用场景相关的使用梅子青,与整体体验相关的使用描金,「走量飞快」单独使用姜黄,其余兜底使用钴蓝。这种分类让评价标签在视觉上形成了自然的语义聚类。

九、主入口组件:应用骨架与路由管理

9.1 状态声明

@Entry
@Component
struct CeladonApp {
  @State curTab: number = 0;
  @State data: CeladonData = new CeladonData();
  @State showAddCeladon: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteGlaze: boolean = false;
  @State showDetailKiln: boolean = false;
  @State delGlazeName: string = '';
  @State detailKilnName: string = '';
  @State bottleSpin: boolean = false;
  @State glazePulse: boolean = false;

CeladonApp是整个应用的根组件,由@Entry装饰器标记为应用入口,由@Component标记为可复用组件。它声明了10个@State状态变量,可以分为三类:

路由与数据状态(2个):

  • curTab:当前选中的Tab索引(0-4),控制底部导航栏的高亮和内容区的切换。
  • dataCeladonData实例,作为全局唯一的数据源传递给所有子组件。

弹窗显隐状态(4个布尔值 + 2个字符串):

  • showAddCeladon / showEditOrder / showDeleteGlaze / showDetailKiln:分别控制四种弹窗的显示/隐藏。
  • delGlazeName / detailKilnName:传递给删除确认弹窗和详情弹窗的上下文参数(要删除的釉名、要查看详情的窑口名)。

动画交互状态(2个布尔值):

  • bottleSpin:控制头部瓷瓶图标的旋转动画(点击切换120°旋转)。
  • glazePulse:控制星星图标的缩放动画(点击切换1.4倍放大)。

这种细粒度的状态拆分,使得每个UI交互都有独立的状态变量驱动,互不干扰。当需要添加新的弹窗或交互效果时,只需新增一个@State变量即可,不会影响现有逻辑。

9.2 Header区域与装饰序列

CeladonAppbuild()方法构建了完整的三层页面骨架。首先映入眼帘的是顶部Header区域,它以primaryDark(深粉青#4A7A85)为背景,包含左侧标题区、右侧操作胶囊区和下方的CELADON_COL装饰序列。

标题区使用「🍶 青瓷坊」作为主标题,「Celadon · 雨过天青云破处」作为英文副标题,分别使用白色与primaryLight。右侧操作胶囊是一个圆角矩形,背景使用COLORS.primaryLight + '2E'(约18%透明度),边框使用COLORS.primaryLight + '80'(约50%透明度),内部放置三个可点击的表情图标。点击瓷瓶图标会触发.rotate({ angle: this.bottleSpin ? 120 : 0 }).animation({ duration: 700, curve: Curve.EaseOut })的旋转动画;点击星星图标会触发.scale({ x: 1.4, y: 1.4 }).animation({ duration: 500, curve: Curve.EaseOut })的脉冲缩放动画。这两个微交互没有实际业务功能,但为静态的文化类应用注入了生动的触感反馈。

Header下方的装饰序列遍历CELADON_COL数组(0~11),通过r % 3的余数决定每一点的形状与颜色:余0时绘制描金边框的绢白圆点(模拟瓷器口沿的「紫口」意象),余1时绘制梅子青竖条(模拟器物的挺拔轮廓),余2时绘制粉青小圆点(模拟釉面上的气泡或开片点)。三者宽度与高度各异,节奏错落,共同构成一道具有宋瓷韵律的视觉装饰带。

9.3 内容区路由与底部Tab栏

内容区通过if-else if-else链条件渲染五个内容组件:

if (this.curTab === CeladonTab.CELADON) {
  CeladonContent({ data: this.data, onAdd: () => { this.showAddCeladon = true; } })
} else if (this.curTab === CeladonTab.GLAZE) {
  GlazeContent({ data: this.data, onDel: (n: string) => {
    this.delGlazeName = n;
    this.showDeleteGlaze = true;
  }})
} else if (this.curTab === CeladonTab.KILN) {
  KilnContent({ data: this.data, onDetail: (n: string) => {
    this.detailKilnName = n;
    this.showDetailKiln = true;
  }})
} else if (this.curTab === CeladonTab.ORDER) {
  CeladonOrderContent({ data: this.data, onEdit: () => { this.showEditOrder = true; } })
} else {
  CeladonReviewContent({ data: this.data })
}

这种基于枚举的条件渲染模式,适合Tab数量固定(5个)的场景,简单直接且无额外依赖。每个内容组件都接收data: this.data作为@ObjectLink引用,并接收一个或多个回调函数用于触发弹窗。

底部Tab栏固定在页面底部,顶部有一条3vp高的accent色装饰线,下方是高度60vp的Tab按钮行。每个Tab按钮使用ForEach(TAB_LIST, ...)渲染,根据是否为当前选中项动态决定:文字颜色(主题色 vs 提示色)、字重(Bold vs Normal)、背景色(半透明主题色 vs 全透明)、边框(有 vs 无)。点击时更新curTab状态,触发内容区重新渲染。这里使用TAB_LIST.indexOf(t)获取索引的做法虽然时间复杂度为O(n),但在只有5个Tab的场景下完全可接受;若Tab数量继续增长,可以考虑将索引直接存入TabMeta接口。

9.4 弹窗的条件渲染

if (this.showAddCeladon) {
  AddCeladonModal({ onClose: () => { this.showAddCeladon = false; } })
}
if (this.showEditOrder) {
  EditCeladonOrderModal({ onClose: () => { this.showEditOrder = false; } })
}
if (this.showDeleteGlaze) {
  DeleteGlazeModal({ title: this.delGlazeName, onClose: () => { this.showDeleteGlaze = false; } })
}
if (this.showDetailKiln) {
  DetailKilnModal({ name: this.detailKilnName, onClose: () => { this.showDetailKiln = false; } })
}

弹窗的渲染采用了条件渲染 + 回调关闭的模式。每个弹窗组件接收一个onClose回调函数,当用户点击弹窗内的「取消」「确认」「知道了」等按钮时,调用onClose()将对应的showXxx状态设为false,从而触发弹窗从DOM中移除。

DeleteGlazeModalDetailKilnModal额外接收了title/name属性,用于在弹窗内显示具体的操作对象。这种「状态提升」(Lifting State Up)的模式——将弹窗所需的上下文数据存储在父组件的状态中、通过props传递给弹窗——是React/ArkTS中处理跨组件数据流的经典方案。

十、内容组件体系:@ObjectLink与数据双向绑定

10.1 标签组件 CeladonTag

@Component
struct CeladonTag {
  @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 + '14')
      .borderRadius(10)
      .border({ width: 1, color: this.color + '40' })
  }
}

CeladonTag是一个极简但精心设计的标签组件,仅接收两个@Prop属性:text(标签文字)和color(主题色)。它的设计精髓在于颜色透明度的十六进制拼接技巧

  • 背景色使用this.color + '14',即在原色hex后面追加14(约8%不透明度的alpha通道),生成8位颜色值。
  • 边框色使用this.color + '40'(约25%不透明度),比背景更明显但不抢眼。

这种「同色系多层透明度叠加」的技法,使得标签在任何背景下都能保持和谐的视觉效果——背景色淡淡的提供区域感,边框稍深勾勒轮廓,文字用原色保证可读性。三者来自同一个color变量,天然协调,永远不会出现「背景是蓝色但文字是红色」的色彩冲突。

10.2 青瓷内容组件 CeladonContent

CeladonContent是「青瓷」Tab页的主体内容组件。它使用@ObjectLink装饰器接收父组件传递的CeladonData实例,建立引用关系而非值拷贝。当CeladonData实例的属性(如celadons数组)发生变化时,引用这些属性的部分会自动刷新。

组件内部由两大部分组成:

第一部分:本周销量图表卡片。顶部有一排装饰性的瓷器点缀,下方是标题「📈 本周青瓷销量」和副标题「周六63件」。柱状图使用ForEach遍历WEEK_SOLD数据,每根柱子的高度直接由value决定(容器高150vp,底部对齐),颜色由阈值判断决定(≥55用accent梅子青,否则用primary粉青)。柱子使用borderRadius(8)实现圆角,底部对齐(alignItems(VerticalAlign.Bottom))模拟真实柱状图的基线对齐效果。

第二部分:青瓷陈列列表。使用ForEach遍历this.data.celadons数组,每张卡片是一个Row横向布局,包含三个区域:

  • 左侧图标区(宽60vp):圆形头像区显示emoji,下方有绢白圆点与釉色小圆点模拟瓷器口沿与釉斑。整体背景色由getGlazeColor(p.glaze)决定并附加极低透明度(0D=5%),边框用33%透明度。
  • 中间信息区layoutWeight(1)占据剩余空间):第一行是品名(粗体)+ 价格(带getPriceColor颜色),第二行是釉色标签(CeladonTag)+ 窑口标签(CeladonTag)+ 「雨过天青」提示文字。
  • 卡片背景色使用i % 2 === 0实现斑马纹交替(白色/浅青灰),增强列表的可读性。

10.3 釉彩内容组件 GlazeContent

GlazeContent的结构与CeladonContent类似,但顶部图表替换为「青瓷销量 TOP6」水平条形图(使用CELADON_TOP数据和layoutWeight实现比例条),列表项改为釉彩卡片。

釉彩卡片的左侧显示emoji、窑龄评级圆点和金徽点缀,中间显示釉名与「釉调+釉」说明,下方显示「釉层厚度」进度条——同样用layoutWeight(g.level)实现比例填充,剩余部分用layoutWeight(100 - g.level)补足灰色背景。右侧有删除图标🗑️,点击触发onDel(g.name)回调,将釉名传回父组件并打开删除确认弹窗。

这里的设计亮点在于:同一套layoutWeight比例填充模式被复用到水平条形图和釉层厚度进度条两种场景中,只是数据源和最大值不同(TOP6以100为满,釉层厚度同样以100为满)。这种复用性体现了声明式布局的表达能力。

10.4 窑口内容组件 KilnContent

KilnContent顶部图表为「烧制工艺热度」水平条形图(CELADON_HOT数据),列表项为窑口卡片。每张卡片显示窑口emoji、窑龄评级圆点、窑名、工艺标签、地区,右侧有详情图标👁️,点击触发onDetail(k.name)回调。

窑口卡片的视觉层级由getKilnColor(k.level)统一驱动:左侧圆形徽章、背景底色、边框色、右侧「窑龄」文字颜色均来自同一颜色,形成高度一致的视觉印象。当窑龄≥95时使用粉青,≥93时使用梅子青,其余使用姜黄,直观传达了窑口的评级高低。

10.5 订单内容组件 CeladonOrderContent

CeladonOrderContent顶部图表为「釉色构成占比」水平条形图(GLAZE_SHARE数据),列表项为订单卡片。每张订单卡片显示客户emoji、名称、区域与数量,右侧显示金额(颜色由getOrderColor(o.amount)决定)和编辑图标✏️。

金额颜色的分级规则极具业务含义:≥60万为朱砂红(超高价订单),45万~60万为粉青,30万~45万为梅子青,<30万为姜黄。用户扫一眼金额颜色,就能快速判断订单的重要性与规模。

10.6 客评内容组件 CeladonReviewContent

CeladonReviewContent没有顶部图表,直接是评价列表。每条评价显示用户emoji、姓名、日期、星级(用'⭐'.repeat(r.score)重复渲染)、标签(CeladonTag)和评价正文。星级使用字符串的repeat()方法根据score值动态生成对应数量的星星字符,简洁而巧妙。评价正文使用lineHeight(16)设置行高,保证多行文本的阅读舒适度。

十一、弹窗组件体系:自定义模态对话框

应用包含四个自定义弹窗组件,它们共享一致的结构模式但各有不同的内容和用途。

11.1 新增青瓷弹窗 AddCeladonModal

弹窗采用全屏遮罩 + 居中卡片的经典模态布局:

  • 外层Column:width('100%') height('100%'),背景色#66000000(40%黑色半透明遮罩),justifyContent(FlexAlign.Center)使内容居中。
  • 内层卡片:宽度86%、最大高度78%、圆角18px、白色背景、16px内边距。
  • 标题栏:「🍶 定制青瓷」 + 右侧「✕」关闭按钮,两端对齐。
  • 内容区:三行信息展示(品名、釉色、预算),每行使用cardAlt背景色+圆角的统一样式。
  • 操作栏:左「取消」(描边按钮)+ 右「确认入窑」(实心主色按钮),右对齐。

11.2 修改订单弹窗 EditCeladonOrderModal

结构与AddCeladonModal完全对称,内容改为客户/数量/金额三行信息,确认按钮文案为「保存修改」。这里展示的数据(故宫文创、800→900件、¥810000)是静态写死的演示数据,实际项目中可以通过props传入真实订单对象。

11.3 删除釉彩弹窗 DeleteGlazeModal

接收title属性显示要删除的釉名,内容为确认提示文字(「确定撤下「xxx」吗?」),并补充说明「撤下的釉彩将从釉彩库移除」。确认按钮使用危险色danger = 朱砂红)以警示操作的不可逆性。这种视觉警示在涉及删除操作的交互中非常重要。

11.4 窑口详情弹窗 DetailKilnModal

接收name属性,内容包含大号火焰emoji、窑名展示区、窑口文化介绍文字(「窑火炽盛昼夜不熄……」)、窑址信息和窑火评级。单按钮「知道了」关闭。该弹窗是四个弹窗中信息密度最高的,体现了弹窗不仅可以用于操作确认,也可以用于内容详情的展示。

四个弹窗的一致性设计(相同的外层遮罩、卡片尺寸、圆角、内边距、标题栏布局)确保了用户在使用不同功能时获得统一的交互体验。差异仅在内容区和按钮文案/颜色上体现各自的功能特性。

十二、关键技术点深度解析

12.1 layoutWeight布局权重的妙用

layoutWeight是ArkTS弹性布局中最强大的属性之一。在本应用中被广泛用于两类场景:

水平条形图/进度条:在CELADON_TOPCELADON_HOTGLAZE_SHARE以及釉层厚度进度条的渲染中,彩色条使用layoutWeight(dataValue)设置宽度权重,后面的灰色占位条使用layoutWeight(maxValue - dataValue)补足剩余空间。两者之和等于最大值(100或20),从而精确实现按比例分配宽度的效果——无需手动计算像素值,框架自动处理。

列表卡片内部布局:青瓷卡片、釉彩卡片、窑口卡片等信息区的中间列使用layoutWeight(1)占据所有剩余空间,左右固定宽度的图标区和操作区保持不变。这种「两头固定、中间弹性」的布局模式在移动端UI中极为常见。

12.2 颜色透明度的十六进制拼接

本应用大量使用了hex颜色 + 两位alpha的8位颜色格式,例如:

  • COLORS.primaryLight + '2E' → 约18%不透明度,用于Header操作胶囊背景
  • COLORS.primaryLight + '80' → 约50%不透明度,用于操作胶囊边框
  • COLORS.primaryLight + '66' → 约40%不透明度,用于Header分割线
  • COLORS.accent + '55' → 约33%不透明度,用于图表卡片边框
  • this.color + '14' → 约8%不透明度,用于标签背景
  • this.color + '40' → 约25%不透明度,用于标签边框
  • getGlazeColor(...) + '0D' → 约5%不透明度,用于列表项左侧徽章背景

这种技法的优势在于:不需要预先定义几十个半透明色变量,而是根据运行时的主题色动态生成任意透明度的衍生色。只要知道基础色和目标透明度,字符串拼接一行搞定。常用参考值:14≈8%,2E≈18%,40≈25%,55≈33%,66≈40%,80≈50%。

12.3 ForEach的键值生成策略

应用中所有的ForEach循环都精心设计了键值生成函数:

// 青瓷列表
(p: CeladonItem, i: number) => 'cd' + p.name + i
// 釉彩列表
(g: GlazeItem, i: number) => 'gl' + g.name + i
// 窑口列表
(k: KilnItem, i: number) => 'kn' + k.name + i
// 订单列表
(o: CeladonOrderItem, i: number) => 'od' + o.name + i
// 评价列表
(r: CeladonReviewItem, i: number) => 'rv' + r.name + i
// Tab栏
(t: TabMeta) => t.key
// 装饰序列
(r: number) => 'celadon' + r
// 图表数据
(w: WeekCeladonMeta) => w.day
(t: CeladonTopMeta) => t.name
(d: CeladonHotMeta) => d.name
(s: GlazeShareMeta) => s.name

每个键值都采用了「前缀 + 唯一标识 + 索引」的三段式命名:

  • 前缀(cd/gl/kn/od/rv)区分不同的ForEach作用域,避免跨列表键值冲突。
  • 唯一标识(p.name/g.name等)保证同一条数据始终映射到同一个键。
  • 索引(i)作为兜底,即使两条数据name相同,索引也能保证唯一性。

正确的键值策略对于ArkTS的ForEach性能至关重要——框架通过键值来判断哪些节点是「新增」、哪些是「移动」、哪些是「删除」,从而执行最小量的DOM操作。错误的键值(如使用Math.random())会导致每次渲染都销毁重建所有节点,造成严重的性能问题。

12.4 @Observed/@ObjectLink响应式机制

本应用的响应式数据流遵循以下路径:

CeladonData (@Observed 类)
    ↓ @ObjectLink 引用传递
CeladonContent / GlazeContent / KilnContent / CeladonOrderContent / CeladonReviewContent
    ↓ 组件内部读取属性
ForEach / Text / Column / Row 等UI节点

CeladonData的某个数组属性发生变化(如celadons.push(newItem)),ArkTS框架的响应式系统会:

  1. 检测到@Observed类实例的属性变更。
  2. 通知所有持有该实例@ObjectLink引用的组件。
  3. 这些组件重新执行build()方法。
  4. 框架diff新旧虚拟DOM树,只更新实际变化的部分。

这种机制比传统的「手动调用setState/refresh」模式更加声明式和自动化,开发者只需描述「数据是什么样子」,框架负责「如何高效地更新UI」。

12.5 条件渲染与状态驱动UI

应用的Tab切换和弹窗显示/隐藏完全由状态变量驱动,没有使用任何命令式的DOM操作:

  • Tab切换:if (this.curTab === CeladonTab.CELADON) 渲染CeladonContent,否则渲染其他组件。curTab的变化由底部Tab栏的onClick事件触发。
  • 弹窗显示:if (this.showAddCeladon) 渲染AddCeladonModalshowAddCeladon由列表的「➕」按钮设为true,由弹窗内部的「取消」「确认」按钮通过onClose回调设为false

这种「状态即UI」(State is UI)的范式是现代前端框架的核心思想——UI是状态的纯函数,给定相同的状态必然产生相同的UI。这使得应用的行为完全可预测、可调试、可测试。

12.6 声明式动画与微交互

应用在Header区域使用了两种声明式动画:

  • 旋转动画.rotate({ angle: this.bottleSpin ? 120 : 0 }).animation({ duration: 700, curve: Curve.EaseOut })。通过切换布尔状态控制旋转角度,动画参数以链式调用的方式声明。
  • 缩放动画.scale({ x: this.glazePulse ? 1.4 : 1, y: this.glazePulse ? 1.4 : 1 }).animation({ duration: 500, curve: Curve.EaseOut })。通过切换布尔状态控制缩放比例。

这种声明式动画的优势在于:开发者不需要手动管理动画生命周期(开始、结束、取消),只需要描述「目标状态是什么」,框架会自动在状态变化时执行平滑过渡。Curve.EaseOut缓动曲线让动画结束时更加柔和自然,符合瓷器釉面温润的文化气质。

十三、技术要点对比表

技术维度 本应用实现方案 设计意图与优势
状态管理 10个@State + @Observed类 + @ObjectLink引用 单一数据源 + 自动响应式更新,避免手动同步
组件通信 Props单向数据流 + 回调函数(onClose/onAdd/onDel/onDetail/onEdit 父→子通过Props,子→父通过回调,数据流清晰可预测
路由方案 if-else if-else条件渲染 + 枚举索引 适合Tab数量固定(5个)的场景,简单直接无额外依赖
弹窗模式 条件渲染 + 全屏遮罩 + 居中卡片 + 回调关闭 统一的模态交互规范,遮罩阻止背景交互
列表渲染 ForEach + 三段式键值(prefix+name+index 高效diff、防冲突、可预测的节点复用
布局策略 layoutWeight弹性权重 + 固定像素结合 两头固定中间弹性的经典移动端布局
颜色系统 19色接口约束 + hex+alpha透明度拼接 编译期类型安全 + 运行时动态透明度
数据建模 5个业务接口 + 4个图表接口 + 纯函数颜色映射 类型安全 + 逻辑集中 + 可独立测试
动画交互 .rotate() + .scale() + .animation() 声明式动画 状态驱动动画,无需手动管理动画生命周期
斑马纹列表 i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt 偶数行白色、奇数行浅青灰,增强列表可读性

十四、总结与展望

14.1 架构设计总结

「青瓷坊」应用虽然是一个单文件Demo,但其内部蕴含的架构思想值得认真总结:

分层清晰,职责分明。从顶部的类型定义层到底部的子组件层,每一层都有明确的单一职责。类型定义层管「契约」,常量实例层管「数据」,响应式类管「状态」,工具函数层管「逻辑」,入口组件管「路由」,子组件管「展示」。这种分层使得无论是添加新功能、修bug,还是做性能优化,都能快速定位到正确的层次。

接口先行,类型安全。所有数据结构都先定义接口,再实现具体数据。TypeScript编译器在编译阶段就能捕获大部分类型错误,大大降低了运行时出错的概率。19色调色板接口、5组业务实体接口、4组图表接口——合计超过10个接口定义,构筑了坚实的类型安全防线。

数据驱动,声明式渲染。UI完全由状态驱动,没有命令式的DOM操作。ForEach遍历数据数组生成列表,if条件根据状态渲染不同组件,@Observed/@ObjectLink自动追踪数据变化并刷新UI。这种范式让代码更接近「描述UI应该是什么样子」而非「如何一步步构建UI」。

14.2 技术亮点回顾

回顾全文,以下几个技术点尤其值得关注和借鉴:

  1. 19色青瓷色彩体系:以「粉青·梅子青·天青」为核心,配合描金与绢白,在视觉上完美还原了宋瓷釉色的清凉与温润。接口约束 + 常量实例化的双层设计,兼顾了类型安全和运行效率。
  2. @Observed + @ObjectLink的深度运用:将全部业务数据封装在一个可观察类中,通过@ObjectLink引用传递给5个子组件,实现了真正的「单一数据源 + 双向绑定」。任何数据变化都会自动级联刷新所有相关UI,无需手动编写事件监听。
  3. 纯函数颜色映射层:5个无副作用的颜色映射函数,将业务规则(釉色分类、窑龄评级、价格区间、订单金额、评价标签)映射到视觉语义(颜色情感)。逻辑集中、可测试、可复用。
  4. layoutWeight驱动的数据可视化:不依赖任何图表库,仅用ArkTS内置的layoutWeight属性就实现了4种精美的条形图/柱状图/进度条。这种「零依赖」的数据可视化方案在轻量级应用中极具实用价值。
  5. 统一的弹窗交互规范:4个弹窗共享相同的结构模板(遮罩+居中卡片+标题栏+内容区+操作栏),仅在内容和按钮细节上差异化。这种一致性设计大幅降低了用户的认知负荷。
  6. 文化与技术的融合:Header的装饰序列、Tab的主题色、评价标签的语义分类,都体现了开发者在UI细节上的文化考量——代码不仅是功能的实现,也是文化表达的一种媒介。

14.3 优化建议与展望

尽管本应用在架构和实现上已经相当成熟,但从工程实践的角度,仍有以下几点可以进一步优化:

第一,状态管理的精细化。当前10个@State变量全部集中在根组件中,随着功能增长可能导致「状态爆炸」。建议引入轻量级状态管理方案(如ArkTS的@Provide/@ConsumeAppStorage),将弹窗状态下沉到各自的父组件中,根组件只保留全局路由状态。

第二,网络数据对接的准备。当前所有数据均为静态硬编码。CeladonData类的数组属性已经为未来对接真实后端API留出了清晰的接口——只需将静态初始化改为异步fetch调用,UI层无需任何修改。这种「函数化的封装方式」体现了良好的架构前瞻性。

第三,组件的进一步拆分。当前每个内容组件(如CeladonContent)内部同时包含了图表卡片和列表卡片两种UI形态。如果未来需要单独复用「本周销量图表」或「青瓷陈列列表」,可以考虑将它们拆分为独立的子组件(WeeklySalesChartCeladonList),提升复用性。

第四,无障碍访问(Accessibility)的支持。当前组件未设置accessibilityText等无障碍属性。对于一款面向文化传播的应用,确保视障用户也能通过屏幕阅读器获取青瓷信息,是产品完善度的重要维度。

第五,国际化(i18n)的预留。虽然「青瓷坊」面向中文用户,但如果未来需要推出多语言版本(如面向海外瓷器爱好者的英文版),建议将所有硬编码的中文字符串提取到资源文件(string.json)中,通过$r()引用。当前的TAB_LIST、静态数据中的中文字符串都需要做相应改造。

总而言之,「青瓷坊」应用以其精巧的宋瓷色彩体系、严谨的类型安全设计、高效的响应式数据流和优雅的组件化架构,为我们提供了一个学习鸿蒙ArkTS声明式UI开发的优秀范本。从「能写出能跑的代码」到「写出架构清晰、可维护、可扩展的代码」,这正是每一位鸿蒙开发者应当追求的成长路径。愿这篇深度解析能为你的进阶之路提供一份有价值的参考。


十五、附录:DevEco Studio 安装与环境搭建

为方便读者将本文分析的「青瓷坊」代码真正跑起来,下面补充一份 DevEco Studio 的安装与环境配置简明教程(配图来源于官方文档)。

安装DevEco Studio程序

在这里插入图片描述

选择目标安装目录:

在这里插入图片描述

设置环境变量,设置完务必重启:

在这里插入图片描述

新建一个空白模板:

在这里插入图片描述

设置API为24的模板项目:

在这里插入图片描述

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

在这里插入图片描述

完整代码

interface CeladonPalette {
  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;
  gold: string;
  silkWhite: string;
}

const COLORS: CeladonPalette = {
  primary: '#6F9BA4',
  primaryLight: '#93BCC4',
  primaryDark: '#4A7A85',
  accent: '#5B8A72',
  accentLight: '#9CBFA8',
  bg: '#EEF4F2',
  cardBg: '#FFFFFF',
  cardAlt: '#E4EDE9',
  textPrimary: '#243B3F',
  textSecondary: '#5E7A80',
  textHint: '#93A8AC',
  border: '#CFDFDC',
  line: '#E6EFEC',
  success: '#4E7BA8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#C9A24B',
  silkWhite: '#F7F3E9'
};

enum CeladonTab {
  CELADON = 0,
  GLAZE = 1,
  KILN = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'celadon', icon: '🍶', label: '青瓷', color: '#6F9BA4' },
  { key: 'glaze', icon: '🎨', label: '釉彩', color: '#5B8A72' },
  { key: 'kiln', icon: '🔥', label: '窑口', color: '#C98A4E' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#C9A24B' }
];

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

interface CeladonItem {
  name: string;
  glaze: string;
  kiln: string;
  price: number;
  emoji: string;
}

interface GlazeItem {
  name: string;
  tone: string;
  level: number;
  place: string;
  emoji: string;
}

interface KilnItem {
  name: string;
  craft: string;
  level: number;
  region: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekCeladonMeta[] = [
  { day: '周一', value: 22 },
  { day: '周二', value: 30 },
  { day: '周三', value: 38 },
  { day: '周四', value: 45 },
  { day: '周五', value: 56 },
  { day: '周六', value: 63 },
  { day: '周日', value: 58 }
];

const GLAZE_SHARE: GlazeShareMeta[] = [
  { name: '粉青', count: 5, color: '#6F9BA4' },
  { name: '梅子青', count: 4, color: '#5B8A72' },
  { name: '天青', count: 3, color: '#4E7BA8' },
  { name: '月白', count: 2, color: '#C9A24B' }
];

const CELADON_HOT: CeladonHotMeta[] = [
  { name: '素烧施釉', heat: 96, color: '#6F9BA4' },
  { name: '厚釉多次', heat: 95, color: '#5B8A72' },
  { name: '还原焰烧', heat: 94, color: '#4E7BA8' },
  { name: '釉下刻花', heat: 93, color: '#93BCC4' },
  { name: '跳刀纹饰', heat: 91, color: '#C98A4E' },
  { name: '冰裂开片', heat: 90, color: '#C9A24B' }
];

const CELADON_TOP: CeladonTopMeta[] = [
  { name: '梅子青瓶', sold: 96, color: '#5B8A72' },
  { name: '粉青贯耳', sold: 92, color: '#6F9BA4' },
  { name: '天青葵口', sold: 89, color: '#4E7BA8' },
  { name: '月白盏', sold: 86, color: '#C9A24B' },
  { name: '豆青洗', sold: 82, color: '#93BCC4' },
  { name: '翠青弦纹', sold: 79, color: '#C98A4E' }
];

@Observed
export class CeladonData {
  celadons: CeladonItem[] = [
    { name: '梅子青瓶', glaze: '梅子青', kiln: '龙泉窑', price: 980, emoji: '🍶' },
    { name: '粉青贯耳', glaze: '粉青', kiln: '官窑', price: 1280, emoji: '🏺' },
    { name: '天青葵口', glaze: '天青', kiln: '汝窑', price: 1580, emoji: '🫖' },
    { name: '月白盏', glaze: '月白', kiln: '定窑', price: 680, emoji: '🥃' },
    { name: '豆青洗', glaze: '豆青', kiln: '龙泉窑', price: 560, emoji: '🪣' },
    { name: '翠青弦纹', glaze: '翠青', kiln: '耀州窑', price: 880, emoji: '🎐' },
    { name: '冬青簋', glaze: '冬青', kiln: '越窑', price: 1180, emoji: '⚱️' },
    { name: '粉青盘', glaze: '粉青', kiln: '景德镇', price: 760, emoji: '🍽️' },
    { name: '梅子青尊', glaze: '梅子青', kiln: '处州窑', price: 1380, emoji: '🗿' },
    { name: '天青盏托', glaze: '天青', kiln: '汝州窑', price: 920, emoji: '🧿' },
    { name: '青瓷笔山', glaze: '粉青', kiln: '景德镇', price: 480, emoji: '✒️' },
    { name: '豆青罐', glaze: '豆青', kiln: '临汝窑', price: 620, emoji: '🫙' }
  ];

  glazes: GlazeItem[] = [
    { name: '粉青釉', tone: '淡雅', level: 96, place: '龙泉', emoji: '💠' },
    { name: '梅子青釉', tone: '莹润', level: 97, place: '处州', emoji: '🫒' },
    { name: '天青釉', tone: '雨过', level: 95, place: '汝州', emoji: '🌧️' },
    { name: '豆青釉', tone: '温厚', level: 94, place: '临汝', emoji: '🫛' },
    { name: '月白釉', tone: '素净', level: 93, place: '定州', emoji: '🌙' },
    { name: '翠青釉', tone: '清脆', level: 94, place: '耀州', emoji: '🌿' },
    { name: '冬青釉', tone: '沉静', level: 92, place: '越州', emoji: '🎋' },
    { name: '龙泉青釉', tone: '厚润', level: 96, place: '龙泉', emoji: '💧' },
    { name: '汝窑天青', tone: '雨霁', level: 95, place: '汝州', emoji: '🌈' },
    { name: '官窑粉青', tone: '灰雅', level: 94, place: '汴京', emoji: '🏯' },
    { name: '哥窑冰裂', tone: '开片', level: 92, place: '临安', emoji: '🧊' },
    { name: '越窑秘色', tone: '翠润', level: 93, place: '上林湖', emoji: '🍀' }
  ];

  kilns: KilnItem[] = [
    { name: '龙泉窑', craft: '厚釉', level: 96, region: '浙江', emoji: '⛰️' },
    { name: '汝窑', craft: '天青', level: 95, region: '河南', emoji: '🏔️' },
    { name: '官窑', craft: '粉青', level: 94, region: '开封', emoji: '🏛️' },
    { name: '哥窑', craft: '开片', level: 93, region: '临安', emoji: '🧊' },
    { name: '钧窑', craft: '窑变', level: 92, region: '禹州', emoji: '🎆' },
    { name: '越窑', craft: '秘色', level: 91, region: '上虞', emoji: '🌊' },
    { name: '耀州窑', craft: '刻花', level: 93, region: '铜川', emoji: '🪓' },
    { name: '景德镇窑', craft: '影青', level: 95, region: '景德镇', emoji: '🏮' },
    { name: '定窑', craft: '白瓷', level: 92, region: '曲阳', emoji: '🥛' },
    { name: '汝州窑', craft: '支烧', level: 91, region: '汝州', emoji: '🪨' },
    { name: '处州窑', craft: '梅子青', level: 94, region: '丽水', emoji: '🍐' },
    { name: '临汝窑', craft: '豆青', level: 92, region: '临汝', emoji: '🌾' }
  ];

  orders: CeladonOrderItem[] = [
    { name: '故宫文创', region: '华北', count: 800, amount: 720000, emoji: '🏯' },
    { name: '茶空间品牌', region: '华东', count: 600, amount: 480000, emoji: '🍵' },
    { name: '博物馆商店', region: '华中', count: 700, amount: 560000, emoji: '🗿' },
    { name: '海外瓷器馆', region: '海外', count: 400, amount: 600000, emoji: '🌏' },
    { name: '高端民宿', region: '西南', count: 500, amount: 300000, emoji: '🏡' },
    { name: '茶器直播间', region: '华东', count: 650, amount: 455000, emoji: '📺' },
    { name: '文化主题餐厅', region: '华南', count: 450, amount: 360000, emoji: '🍲' },
    { name: '非遗研学基地', region: '华东', count: 550, amount: 275000, emoji: '🎓' },
    { name: '礼品定制公司', region: '华北', count: 350, amount: 420000, emoji: '🎁' },
    { name: '茶艺馆连锁', region: '华南', count: 480, amount: 384000, emoji: '🫖' },
    { name: '国宴用瓷处', region: '华北', count: 300, amount: 660000, emoji: '👑' },
    { name: '收藏家私订', region: '海外', count: 200, amount: 700000, emoji: '💎' }
  ];

  reviews: CeladonReviewItem[] = [
    { name: '故宫文创买手', score: 5, date: '09-03', content: '梅子青釉色莹润欲滴,摆上展柜衬得满室生辉。', tag: '釉色莹润', emoji: '🏯' },
    { name: '茶空间主理', score: 5, date: '09-02', content: '天青葵口盏配乌龙,茶汤映盏色,雅不可言。', tag: '茶席雅物', emoji: '🍵' },
    { name: '博物馆店长', score: 5, date: '09-01', content: '粉青贯耳瓶釉面如脂,游客问价不断。', tag: '如脂似玉', emoji: '🗿' },
    { name: '海外藏家', score: 5, date: '08-31', content: '还原焰烧得正宗,青中透润,海外难觅。', tag: '窑火纯正', emoji: '🌏' },
    { name: '民宿主理', score: 4, date: '08-30', content: '豆青罐置茶席古朴大方,若附木架更佳。', tag: '古朴大方', emoji: '🏡' },
    { name: '直播选品', score: 5, date: '08-29', content: '青瓷笔山上播即空,书桌雅器走量极快。', tag: '走量飞快', emoji: '📺' },
    { name: '餐厅采购', score: 5, date: '08-28', content: '月白盏配餐瓷雅致,食客拍照率极高。', tag: '雅致上镜', emoji: '🍲' },
    { name: '研学馆长', score: 4, 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: '👑' },
    { name: '老主顾', score: 5, date: '08-23', content: '收第三件青瓷了,雨过天青云破处,百看不厌。', tag: '百看不厌', emoji: '💎' }
  ];
}

function getGlazeColor(glaze: string): string {
  if (glaze === '梅子青' || glaze === '翠青') {
    return '#5B8A72';
  } else if (glaze === '天青' || glaze === '粉青') {
    return '#6F9BA4';
  } else if (glaze === '月白' || glaze === '冬青') {
    return '#C9A24B';
  }
  return '#4E7BA8';
}

function getKilnColor(level: number): string {
  if (level >= 95) {
    return '#6F9BA4';
  } else if (level >= 93) {
    return '#5B8A72';
  }
  return '#C98A4E';
}

function getPriceColor(price: number): string {
  if (price >= 1200) {
    return '#9E3B2F';
  } else if (price >= 800) {
    return '#6F9BA4';
  } else if (price >= 500) {
    return '#5B8A72';
  }
  return '#C9A24B';
}

function getOrderColor(amount: number): string {
  if (amount >= 600000) {
    return '#9E3B2F';
  } else if (amount >= 450000) {
    return '#6F9BA4';
  } else if (amount >= 300000) {
    return '#5B8A72';
  }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '釉色莹润' || tag === '如脂似玉' || tag === '窑火纯正' || tag === '品控严苛') {
    return '#6F9BA4';
  } else if (tag === '茶席雅物' || tag === '雅致上镜' || tag === '成套齐整') {
    return '#5B8A72';
  } else if (tag === '古朴大方' || tag === '物料齐全' || tag === '纹饰利落' || tag === '百看不厌') {
    return '#C9A24B';
  } else if (tag === '走量飞快') {
    return '#C98A4E';
  }
  return '#4E7BA8';
}

@Entry
@Component
struct CeladonApp {
  @State curTab: number = 0;
  @State data: CeladonData = new CeladonData();
  @State showAddCeladon: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteGlaze: boolean = false;
  @State showDetailKiln: boolean = false;
  @State delGlazeName: string = '';
  @State detailKilnName: string = '';
  @State bottleSpin: boolean = false;
  @State glazePulse: 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('Celadon · 雨过天青云破处')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🍶')
                .fontSize(18)
                .onClick(() => {
                  this.bottleSpin = !this.bottleSpin;
                })
                .rotate({ angle: this.bottleSpin ? 120 : 0 })
                .animation({ duration: 700, curve: Curve.EaseOut })
              Text('✨')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.glazePulse = !this.glazePulse;
                })
                .scale({ x: this.glazePulse ? 1.4 : 1, y: this.glazePulse ? 1.4 : 1 })
                .animation({ duration: 500, curve: Curve.EaseOut })
              Text('🏺')
                .fontSize(14)
                .margin({ left: 6 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(COLORS.primaryLight + '2E')
            .borderRadius(24)
            .border({ width: 1, color: COLORS.primaryLight + '80' })
          }
          .width('100%')

          Row() {
            ForEach(CELADON_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(12)
                    .height(12)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(6)
                    .border({ width: 2, color: COLORS.gold })
                } else if (r % 3 === 1) {
                  Column()
                    .width(6)
                    .height(14)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(3)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'celadon' + 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 === CeladonTab.CELADON) {
          CeladonContent({ data: this.data, onAdd: () => {
            this.showAddCeladon = true;
          } })
        } else if (this.curTab === CeladonTab.GLAZE) {
          GlazeContent({ data: this.data, onDel: (n: string) => {
            this.delGlazeName = n;
            this.showDeleteGlaze = true;
          } })
        } else if (this.curTab === CeladonTab.KILN) {
          KilnContent({ data: this.data, onDetail: (n: string) => {
            this.detailKilnName = n;
            this.showDetailKiln = true;
          } })
        } else if (this.curTab === CeladonTab.ORDER) {
          CeladonOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          CeladonReviewContent({ data: this.data })
        }
      }
      .width('100%')
      .height('100%')

      Column() {
        Column()
          .width('100%')
          .height(3)
          .backgroundColor(COLORS.accent)
        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.showAddCeladon) {
        AddCeladonModal({
          onClose: () => {
            this.showAddCeladon = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditCeladonOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteGlaze) {
        DeleteGlazeModal({
          title: this.delGlazeName, onClose: () => {
            this.showDeleteGlaze = false;
          }
        })
      }
      if (this.showDetailKiln) {
        DetailKilnModal({
          name: this.detailKilnName, onClose: () => {
            this.showDetailKiln = false;
          }
        })
      }
    }
  }
}

@Component
struct CeladonTag {
  @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 + '14')
      .borderRadius(10)
      .border({ width: 1, color: this.color + '40' })
  }
}

@Component
struct CeladonContent {
  @ObjectLink data: CeladonData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.silkWhite)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.gold })
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.accent + '55')
              .margin({ left: 4, right: 4 })
            Column()
              .width(6)
              .height(6)
              .backgroundColor(COLORS.primary)
              .borderRadius(3)
              .margin({ right: 4 })
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.accent)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.silkWhite })
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.accent + '55')
              .margin({ left: 4, right: 4 })
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.silkWhite)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.gold })
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 本周青瓷销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六63件')
              .fontSize(9)
              .fontColor(COLORS.accent)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekCeladonMeta) => {
              Column() {
                Text(w.value + '')
                  .fontSize(8)
                  .fontColor(w.value >= 55 ? COLORS.accent : COLORS.primary)
                Column()
                  .width(16)
                  .height(w.value)
                  .backgroundColor(w.value >= 55 ? COLORS.accent : COLORS.primary)
                  .borderRadius(8)
                  .margin({ top: 4 })
                Text(w.day)
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (w: WeekCeladonMeta) => 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.accent + '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.celadons, (p: CeladonItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(44)
                .height(44)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getGlazeColor(p.glaze) + '14')
                .borderRadius(22)
                .border({ width: 1, color: getGlazeColor(p.glaze) + '66' })
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(5)
                    .border({ width: 2, color: getGlazeColor(p.glaze) })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(getGlazeColor(p.glaze))
                    .borderRadius(4)
                    .margin({ left: 4 })
                }
                .width(30)
                .justifyContent(FlexAlign.Center)
              }
              .width(60)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getGlazeColor(p.glaze) + '0D')
              .borderRadius(24)
              .border({ width: 1, color: getGlazeColor(p.glaze) + '33' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('¥' + p.price)
                    .fontSize(10)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(getPriceColor(p.price))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  CeladonTag({ text: p.glaze, color: getGlazeColor(p.glaze) })
                  CeladonTag({ text: p.kiln, color: COLORS.success })
                  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(24)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: CeladonItem, i: number) => 'cd' + 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 GlazeContent {
  @ObjectLink data: CeladonData;
  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(CELADON_TOP, (t: CeladonTopMeta) => {
            Row() {
              Text(t.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .width(90)
              Row() {
                Column()
                  .layoutWeight(t.sold)
                  .height(12)
                  .backgroundColor(t.color)
                  .borderRadius(6)
                Column()
                  .layoutWeight(100 - t.sold)
                  .height(12)
                  .backgroundColor(COLORS.line)
                  .borderRadius(6)
              }
              .layoutWeight(1)
              Text(t.sold + '件')
                .fontSize(9)
                .fontColor(t.color)
                .width(40)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (t: CeladonTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.accent + '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.glazes, (g: GlazeItem, i: number) => {
            Row() {
              Column() {
                Text(g.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(getKilnColor(g.level))
                    .borderRadius(5)
                    .border({ width: 2, color: COLORS.silkWhite })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .margin({ left: 4 })
                }
                .width(28)
                .justifyContent(FlexAlign.Center)
                .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getKilnColor(g.level) + '0D')
              .borderRadius(22)
              .border({ width: 1, color: getKilnColor(g.level) + '33' })

              Column() {
                Row() {
                  Text(g.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(g.tone + '釉')
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  Text('釉层厚度')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                  Row() {
                    Column()
                      .layoutWeight(g.level)
                      .height(6)
                      .backgroundColor(getKilnColor(g.level))
                      .borderRadius(3)
                    Column()
                      .layoutWeight(100 - g.level)
                      .height(6)
                      .backgroundColor(COLORS.line)
                      .borderRadius(3)
                  }
                  .layoutWeight(1)
                  .margin({ left: 6 })
                }
                .width('100%')
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })

              Text('🗑️')
                .fontSize(14)
                .onClick(() => {
                  this.onDel(g.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (g: GlazeItem, i: number) => 'gl' + g.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 KilnContent {
  @ObjectLink data: CeladonData;
  onDetail: (n: string) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🔥 烧制工艺热度')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('素烧施釉96')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(CELADON_HOT, (d: CeladonHotMeta) => {
            Row() {
              Text(d.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .width(80)
              Row() {
                Column()
                  .layoutWeight(d.heat)
                  .height(14)
                  .backgroundColor(d.color)
                  .borderRadius(7)
                Column()
                  .layoutWeight(100 - d.heat)
                  .height(14)
                  .backgroundColor(COLORS.line)
                  .borderRadius(7)
              }
              .layoutWeight(1)
              Text(d.heat + '')
                .fontSize(9)
                .fontColor(d.color)
                .width(32)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 6 })
          }, (d: CeladonHotMeta) => d.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.accent + '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.kilns, (k: KilnItem, i: number) => {
            Row() {
              Column() {
                Text(k.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Column()
                  .width(14)
                  .height(14)
                  .backgroundColor(getKilnColor(k.level))
                  .borderRadius(7)
                  .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getKilnColor(k.level) + '0D')
              .borderRadius(20)
              .border({ width: 1, color: getKilnColor(k.level) + '33' })

              Column() {
                Row() {
                  Text(k.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('窑龄' + k.level)
                    .fontSize(9)
                    .fontColor(getKilnColor(k.level))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  CeladonTag({ text: k.craft + '窑', color: COLORS.textSecondary })
                  Text(k.region)
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .margin({ top: 6 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })

              Text('👁️')
                .fontSize(14)
                .onClick(() => {
                  this.onDetail(k.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(20)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (k: KilnItem, i: number) => 'kn' + k.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 CeladonOrderContent {
  @ObjectLink data: CeladonData;
  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(GLAZE_SHARE, (s: GlazeShareMeta) => {
            Row() {
              Column()
                .width(8)
                .height(8)
                .backgroundColor(s.color)
                .borderRadius(4)
              Text(s.name)
                .fontSize(10)
                .fontColor(COLORS.textPrimary)
                .margin({ left: 6 })
              Text('×' + s.count)
                .fontSize(9)
                .fontColor(COLORS.textSecondary)
                .margin({ left: 4 })
              Row() {
                Column()
                  .layoutWeight(s.count)
                  .height(10)
                  .backgroundColor(s.color)
                  .borderRadius(5)
                Column()
                  .layoutWeight(20 - s.count)
                  .height(10)
                  .backgroundColor(COLORS.line)
                  .borderRadius(5)
              }
              .layoutWeight(1)
              .margin({ left: 10 })
            }
            .width('100%')
            .margin({ top: 6 })
          }, (s: GlazeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.accent + '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.onEdit();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ bottom: 10 })
          ForEach(this.data.orders, (o: CeladonOrderItem, i: number) => {
            Row() {
              Text(o.emoji)
                .fontSize(16)
              Column() {
                Text(o.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(o.region + ' · ' + o.count + '件')
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              .margin({ left: 10 })
              Text('¥' + o.amount)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(getOrderColor(o.amount))
              Text('✏️')
                .fontSize(12)
                .margin({ left: 10 })
                .onClick(() => {
                  this.onEdit();
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(20)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: CeladonOrderItem, 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 CeladonReviewContent {
  @ObjectLink data: CeladonData;

  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: CeladonReviewItem, 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)
              CeladonTag({ 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(20)
          .border({ width: 1, color: COLORS.line })
          .margin({ top: 8 })
        }, (r: CeladonReviewItem, i: number) => 'rv' + r.name + i)
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct AddCeladonModal {
  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(10)
        .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(10)
        .margin({ top: 8 })
        Row() {
          Text('预算')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥980')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(12)
            .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(12)
            .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 EditCeladonOrderModal {
  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(10)
        .margin({ top: 14 })
        Row() {
          Text('数量')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('800 → 900 件')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('金额')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .width(56)
          Text('¥810000')
            .fontSize(11)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardAlt)
        .borderRadius(10)
        .margin({ top: 8 })
        Row() {
          Text('取消')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .padding({ left: 16, right: 16, top: 7, bottom: 7 })
            .borderRadius(12)
            .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(12)
            .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 DeleteGlazeModal {
  @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(12)
            .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(12)
            .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 DetailKilnModal {
  @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(12)
        .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(10)
        .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(10)
        .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(12)
          .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')
  }
}
Logo

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

更多推荐