摘要:本文以一个完整的珍珠珠宝展示与交易管理应用"珍珠坊"为案例,深入剖析鸿蒙HarmonyOS ArkTS声明式UI开发范式的核心要点。从以"海蓝·胭脂·丝白"三色为核心的雅致色彩体系的接口化定义,到响应式数据流@Observed/@ObjectLink在五Tab多场景下的深度运用,再到自定义模态弹窗体系、条件渲染路由、layoutWeight弹性布局权重等复杂交互模式的完整实现,逐段拆解每一个关键代码块背后的设计思想与技术原理。全文覆盖接口约束、枚举安全、数据建模、状态驱动渲染、组件拆分策略、颜色透明度拼接、ForEach键值策略、声明式动画等20余个技术知识点,适合有一定ArkTS基础、希望深入理解鸿蒙声明式UI架构的开发者进阶阅读。


一、前言:沧海月明珠有泪

珍珠,被誉为"月亮的眼泪",是人类最早珍视的宝石之一。从《诗经》中"巧笑之瑳,佩玉之傩"的温润意象,到李商隐"沧海月明珠有泪,蓝田日暖玉生烟"的千古绝唱,珍珠始终承载着东方文化对圆满、含蓄与光泽之美的极致追求。一粒珍珠的形成,是母贝以层层珍珠质包裹砂砾的漫长修行;一件珍珠珠宝的呈现,则是设计师、工匠与时光共同谱写的优雅诗篇。

在移动应用高度渗透日常生活的今天,将珍珠这一兼具自然造化与人文审美的高端品类搬上鸿蒙终端,既是对传统珠宝展示方式的一次数字化升级,也是对HarmonyOS ArkTS声明式开发能力的一次深度检验。本文所剖析的"珍珠坊"应用,正是围绕珍珠、项链、耳饰、订单、客评五大业务维度构建的一个集产品陈列、工艺数据可视化、订单台账与客户口碑于一体的综合性移动端展示平台。

与常见的Demo不同,这段代码并未停留在简单的列表堆砌,而是展现了一个成熟前端应用应当具备的多层能力:色彩的语义化治理、数据的类型化建模、状态的可视化驱动、交互的组件化封装、以及视觉反馈的精细化打磨。如何在深海蓝与胭脂粉的交织中保持视觉一致性?如何让@Observed实例在五组业务数组间自动触发UI刷新?如何用layoutWeight一行代码搞定排行榜条形图?如何设计一套既统一又各具功能的自定义弹窗?这些问题的答案,都隐藏在接下来的每一行代码之中。本文将从架构总览开始,逐层深入,带你完成一次从"能写"到"写好"的鸿蒙开发进阶之旅。

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

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

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

第二层——常量实例层(第23~167行):将抽象的类型定义具象化为可用的运行时对象,包括COLORS色彩常量、TAB_LIST标签列表、PEARL_COL珍珠点缀列阵以及四组静态图表数据(WEEK_SOLD周销量、SHAPE_SHARE珠形占比、PEARL_HOT工艺热度、PEARL_TOP名珠排行)。

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

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

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

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

这种分层设计的优势在于:当需要修改某个功能模块时,开发者可以快速定位到对应的层次进行修改,而不会对其他部分产生连锁反应。例如,如果需要调整配色方案,只需修改第一层的接口定义和第二层的常量实例;如果需要新增一个Tab页面,只需在第五层添加路由分支并在第六层新增对应的内容组件即可。珍珠坊的主题虽然偏向珠宝展示,但其架构思想对于电商、内容、工具等各类鸿蒙应用都具有参考价值。

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

3.1 调色板接口定义

interface PearlPalette {
  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;      // 丝白
}

这段代码看似简单,却蕴含着深刻的设计思想。首先,使用interface而不是type来定义调色板结构,是因为interface在ArkTS中具有更好的类型推断能力和扩展性。接口中定义了19个颜色字段,覆盖了以下几大语义类别:

  • 主色调系列(primary / primaryLight / primaryDark):以深海蓝#3E5C76为核心,模拟南洋珠母贝在深海中的幽蓝光泽。三个梯度分别用于常规主色、悬停/次要强调和深色背景区域。
  • 强调色系列(accent / accentLight):胭脂粉#D98E97取自Akoya珍珠表面那层若隐若现的粉绿伴色,用于关键操作按钮和高亮元素;其浅变体#EFC6CB则用于卡片边框和柔和装饰。
  • 背景色系(bg / cardBg / cardAlt):米白#F4F0EC作为页面底色营造温润的珠宝展台质感;纯白#FFFFFF作为卡片底色保证内容可读性;淡粉米色#F1E7E3作为交替行背景,在列表中形成类似珍珠光泽的明暗节奏。
  • 文本色三级灰度(textPrimary / textSecondary / textHint):从深灰蓝#2C3A44到中灰蓝#6B7C88再到浅灰蓝#9CAAB3,形成清晰的信息层级,符合珠宝类应用克制内敛的审美取向。
  • 功能语义色(success / warning / danger):青灰蓝#7BA3A8表示成功/完成,琥珀棕#C98A4E表示提醒/待办,暗红#9E3B2F表示危险/删除,三种语义色与主辅色和谐共处。
  • 主题专属色(gold / silkWhite):珍珠金#F2E3D5模拟南洋金珠的暖金色;丝白#FBF8F4则用于头像边框和高光小圆点,强化珍珠"圆润洁白"的视觉符号。

这种19色的精细划分,使得整个应用的每一个视觉元素都有明确的"色彩归属",避免了随意使用硬编码颜色值带来的视觉混乱。更重要的是,通过接口约束,任何遗漏字段或类型不匹配都会在编译阶段被TypeScript编译器捕获,实现了"编译期安全"。

3.2 色彩常量实例化

const COLORS: PearlPalette = {
  primary: '#3E5C76',
  primaryLight: '#7E9BB0',
  primaryDark: '#2A4056',
  accent: '#D98E97',
  accentLight: '#EFC6CB',
  bg: '#F4F0EC',
  cardBg: '#FFFFFF',
  cardAlt: '#F1E7E3',
  textPrimary: '#2C3A44',
  textSecondary: '#6B7C88',
  textHint: '#9CAAB3',
  border: '#D9E0E6',
  line: '#EAEFEF',
  success: '#7BA3A8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#F2E3D5',
  silkWhite: '#FBF8F4'
};

这里有一个非常值得注意的细节:COLORS常量的类型被显式标注为PearlPalette。这意味着TypeScript编译器会在编译阶段检查COLORS对象是否完整实现了接口定义的所有19个字段。如果遗漏了任何一个颜色字段,或者字段类型不匹配(比如不小心把数字赋给了string类型的字段),编译器会立即报错。这种"编译期安全"是大型项目中极其重要的保障,它能在代码运行之前就消灭掉大量潜在的类型错误。

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

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

4.1 Tab枚举定义

enum PearlTab {
  PEARL = 0,
  NECKLACE = 1,
  EARRING = 2,
  ORDER = 3,
  REVIEW = 4
}

PearlTab枚举定义了应用底部导航栏的五个页面标识,从0到4依次对应:珍珠、项链、耳饰、订单、客评。使用枚举而非魔术数字(magic number)的好处是多方面的:

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

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

第三,重构友好。如果未来需要调整Tab的顺序(比如把"订单"移到第二个位置),只需修改枚举值的赋值顺序,所有引用该枚举的地方都会自动适配。

4.2 标签元数据配置

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

const TAB_LIST: TabMeta[] = [
  { key: 'pearl', icon: '🫧', label: '珍珠', color: '#3E5C76' },
  { key: 'necklace', icon: '📿', label: '项链', color: '#D98E97' },
  { key: 'earring', icon: '💎', label: '耳饰', color: '#7BA3A8' },
  { key: 'order', icon: '📦', label: '订单', color: '#C98A4E' },
  { key: 'review', icon: '⭐', label: '客评', color: '#9CAAB3' }
];

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

每个标签还拥有自己的主题色(color字段),这为选中态的视觉表现提供了丰富的个性化空间。例如,"珍珠"Tab选中时使用深海蓝高亮,"项链"Tab则使用胭脂粉,"耳饰"Tab使用青灰蓝,让用户在不同页面间切换时能获得一致的色彩反馈但又各具辨识度。

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

5.1 业务实体接口定义

interface PearlItem {
  name: string;     // 珍珠名称
  origin: string;  // 产地
  shape: string;   // 形状
  price: number;   // 价格
  emoji: string;   // 表情图标
}

interface NecklaceItem {
  name: string;     // 项链名称
  style: string;   // 风格
  level: number;   // 光泽/等级
  place: string;   // 产地/来源地
  emoji: string;
}

interface EarringItem {
  name: string;     // 耳饰名称
  stone: string;   // 设计关键词
  level: number;   // 品级
  place: string;   // 产地
  emoji: string;
}

interface PearlOrderItem {
  name: string;     // 客户名称
  region: string;   // 区域
  count: number;    // 数量
  amount: number;   // 金额
  emoji: string;
}

interface PearlReviewItem {
  name: string;     // 评价人
  score: number;    // 评分
  date: string;     // 日期
  content: string;  // 内容
  tag: string;      // 标签
  emoji: string;
}

这里展示了五种业务实体的接口定义。每个接口都精心设计了字段,紧密围绕珍珠珠宝行业的实际业务场景:

  • PearlItem(珍珠条目):包含珍珠名称(如"南洋金珠"、"大溪地黑珠")、产地(南海、大溪地、日本海、诸暨等)、形状(圆珠、水滴、异形、半圆、椭圆)、价格和表情图标。其中origin字段不仅用于显示,还是后续getOriginColor()颜色映射函数的核心输入。
  • NecklaceItem(项链条目):记录项链名称、风格(极简、复古、时尚、典雅等)、光泽等级(数值型,用于进度条可视化)和产地。level字段是项链页面的核心数据维度,直接影响每条项链的光泽评级条长度和颜色。
  • EarringItem(耳饰条目):记录耳饰名称、设计关键词(珍珠、简约、绽放、摇曳等)、品级和产地。stone字段虽然字面意义是"宝石",但在这里被创造性地用作设计关键词前缀,例如"珍珠"+"珠"组合成标签文案。
  • PearlOrderItem(订单条目):记录客户名称、所属区域、订购数量和金额。amount字段是getOrderColor()的颜色映射依据——金额越高颜色越"醒目"(胭脂粉/深海蓝),形成直观的业务警示。
  • PearlReviewItem(评价条目):包含评分(1-5星)、日期、评价内容和标签。tag字段是getReviewTagColor()的映射输入,不同类型的评价标签被赋予不同的主题色,帮助用户快速识别评价侧重点。

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

5.2 图表数据元接口

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

interface ShapeShareMeta {
  name: string;   // 珠形名称
  count: number;  // 数量
  color: string;  // 主题色
}

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

interface PearlTopMeta {
  name: string;   // 珠名
  sold: number;   // 销量
  color: string;  // 主题色
}

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

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

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

6.1 周销量与珠形构成数据

const WEEK_SOLD: WeekPearlMeta[] = [
  { day: '周一', value: 18 },
  { day: '周二', value: 26 },
  { day: '周三', value: 34 },
  { day: '周四', value: 42 },
  { day: '周五', value: 55 },
  { day: '周六', value: 62 },
  { day: '周日', value: 57 }
];

这里展示了周销量数据。在实际项目中,这些数据通常来自后端API接口,但本应用作为展示型Demo采用了静态数据的方式。数据的设计颇具巧思——销量从周一到周六逐步递增(18→26→34→42→55→62),周日略有回落(57),这完全符合珠宝零售业"工作日平稳增长、周末达到高峰、周日略降"的实际规律,让数据看起来真实可信。同时,数据中设置了一个明显的阈值分界点:55颗以上(周五、周六、周日)被标记为胭脂粉色,提示经营高峰;55颗以下则使用深海蓝,保持冷静基调。

const SHAPE_SHARE: ShapeShareMeta[] = [
  { name: '圆珠', count: 5, color: '#D98E97' },
  { name: '水滴', count: 4, color: '#3E5C76' },
  { name: '巴洛克', count: 3, color: '#7BA3A8' },
  { name: '异形', count: 2, color: '#C98A4E' }
];

珠形构成数据反映了当前陈列珍珠的四种主流形状及其数量占比。圆珠(5款)居首,这是经典、最被市场认可的正圆珍珠;水滴(4款)次之,常用于耳坠和吊坠设计;巴洛克(3款)是不规则珍珠,因独特光泽深受设计师喜爱;异形(2款)最少,强调个性表达。每一类都绑定了独立主题色,其中圆珠使用胭脂粉突出其"经典"地位,水滴使用深海蓝呼应海水珠意象,巴洛克使用青灰蓝彰显艺术感,异形使用琥珀棕暗示小众独特。

6.2 工艺热度与名珠排行榜数据

const PEARL_HOT: PearlHotMeta[] = [
  { name: '插核植珠', heat: 96, color: '#3E5C76' },
  { name: '海水养殖', heat: 95, color: '#D98E97' },
  { name: '母贝优选', heat: 94, color: '#7BA3A8' },
  { name: '层叠包覆', heat: 93, color: '#7E9BB0' },
  { name: '打孔穿串', heat: 92, color: '#C98A4E' },
  { name: '镜面抛光', heat: 91, color: '#F2E3D5' }
];

const PEARL_TOP: PearlTopMeta[] = [
  { name: '南洋金珠', sold: 96, color: '#D98E97' },
  { name: '大溪地黑珠', sold: 92, color: '#3E5C76' },
  { name: '爱迪生珠', sold: 89, color: '#7BA3A8' },
  { name: 'akoya白珠', sold: 86, color: '#7E9BB0' },
  { name: '淡水无核珠', sold: 83, color: '#C98A4E' },
  { name: '马贝珠', sold: 79, color: '#F2E3D5' }
];

养殖工艺热度数据涵盖了珍珠养殖与加工的六大核心工序,从"插核植珠"(96分)到"镜面抛光"(91分),热度值递减但差距不大,说明每一道工序在从业者心中都至关重要。名珠销量TOP6则列出了最受欢迎的六类珍珠,"南洋金珠"以96的销量遥遥领先——这款金珠产自南海,是高端珍珠市场的宠儿。

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

@Observed
export class PearlData {
  pearls: PearlItem[] = [
    { name: '南洋金珠', origin: '南海', shape: '圆珠', price: 1280, emoji: '🟡' },
    { name: '大溪地黑珠', origin: '大溪地', shape: '圆珠', price: 1580, emoji: '⚫' },
    // ... 共12颗珍珠
  ];

  necklaces: NecklaceItem[] = [
    { name: '单珠链', style: '极简', level: 96, place: '东京', emoji: '📿' },
    // ... 共12条项链
  ];

  earrings: EarringItem[] = [
    { name: '水滴耳坠', stone: '珍珠', level: 96, place: '深圳', emoji: '💧' },
    // ... 共12款耳饰
  ];

  orders: PearlOrderItem[] = [
    { name: '珠宝连锁', region: '华南', count: 600, amount: 1080000, emoji: '💎' },
    // ... 共12条订单
  ];

  reviews: PearlReviewItem[] = [
    { name: '珠宝买手', score: 5, date: '09-03', content: '南洋金珠色泽温润,柜台试戴率极高。', tag: '色泽温润', emoji: '💎' },
    // ... 共12条评价
  ];
}

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

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

  • pearls(12颗珍珠):涵盖从入门级的淡水无核珠(¥320)到收藏级的螺珠(¥2600)的全价位段产品矩阵,产地横跨南海、大溪地、日本海、加勒比、海南、冲绳、诸暨等地。
  • necklaces(12条项链):风格涵盖极简、复古、闪耀、时尚、典雅、传统、腕间、匠心等,等级91-96不等,产地包括东京、大阪、巴黎、米兰、首尔、伦敦、北京、上海等。
  • earrings(12款耳饰):设计关键词涵盖水滴、圆珠、花型、流苏、复古、镶嵌、耳线、耳骨夹、螺钿、竹节、双珠、马蹄等,品级91-96不等。
  • orders(12条订单):客户涵盖珠宝连锁、高定品牌、海外买手、婚庆公司、直播电商、古着店铺、设计师品牌、免税店采购、博物馆文创、银楼定制、影视造型、老牌珠宝行等多元渠道,区域覆盖华南、华东、华北、西南、海外。
  • reviews(12条评价):全部4-5星好评,标签涵盖"色泽温润"、"个性十足"、"品相一流"、"婚礼首选"、"走量飞快"、"细节讲究"、"配色精妙"、"轻盈百搭"、"光泽流转"、"成色上佳"、"上镜极佳"、"珠圆玉润"等多维度评价。

这种将所有业务数据集中在一个@Observed类中的设计,带来了一个重要的架构优势:单一数据源原则(Single Source of Truth)。无论哪个子组件需要访问珍珠列表、项链数据还是订单信息,它们都通过@ObjectLink引用同一个PearlData实例。当任何一个数据发生变化(比如新增了一颗珍珠、删除了一条项链),所有引用该数据的组件都会自动收到通知并刷新UI,无需手动编写繁琐的事件监听和数据同步逻辑。

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

function getOriginColor(origin: string): string {
  if (origin === '南海' || origin === '加勒比') {
    return '#D98E97';
  } else if (origin === '大溪地' || origin === '日本海') {
    return '#3E5C76';
  } else if (origin === '诸暨' || origin === '海南') {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getLevelColor(level: number): string {
  if (level >= 95) {
    return '#D98E97';
  } else if (level >= 93) {
    return '#3E5C76';
  }
  return '#7BA3A8';
}

function getPriceColor(price: number): string {
  if (price >= 2000) {
    return '#D98E97';
  } else if (price >= 1000) {
    return '#3E5C76';
  } else if (price >= 500) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getOrderColor(amount: number): string {
  if (amount >= 900000) {
    return '#D98E97';
  } else if (amount >= 700000) {
    return '#3E5C76';
  } else if (amount >= 500000) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '色泽温润' || tag === '品相一流' || tag === '珠圆玉润' || tag === '细节讲究') {
    return '#3E5C76';
  } else if (tag === '个性十足' || tag === '婚礼首选' || tag === '配色精妙') {
    return '#D98E97';
  } else if (tag === '走量飞快' || tag === '轻盈百搭' || tag === '成色上佳' || tag === '上镜极佳') {
    return '#7BA3A8';
  } else if (tag === '光泽流转') {
    return '#C98A4E';
  }
  return '#9CAAB3';
}

interface PearlPalette {
  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: PearlPalette = {
  primary: '#3E5C76',
  primaryLight: '#7E9BB0',
  primaryDark: '#2A4056',
  accent: '#D98E97',
  accentLight: '#EFC6CB',
  bg: '#F4F0EC',
  cardBg: '#FFFFFF',
  cardAlt: '#F1E7E3',
  textPrimary: '#2C3A44',
  textSecondary: '#6B7C88',
  textHint: '#9CAAB3',
  border: '#D9E0E6',
  line: '#EAEFEF',
  success: '#7BA3A8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#F2E3D5',
  silkWhite: '#FBF8F4'
};

enum PearlTab {
  PEARL = 0,
  NECKLACE = 1,
  EARRING = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'pearl', icon: '🫧', label: '珍珠', color: '#3E5C76' },
  { key: 'necklace', icon: '📿', label: '项链', color: '#D98E97' },
  { key: 'earring', icon: '💎', label: '耳饰', color: '#7BA3A8' },
  { key: 'order', icon: '📦', label: '订单', color: '#C98A4E' },
  { key: 'review', icon: '⭐', label: '客评', color: '#9CAAB3' }
];

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

interface PearlItem {
  name: string;
  origin: string;
  shape: string;
  price: number;
  emoji: string;
}

interface NecklaceItem {
  name: string;
  style: string;
  level: number;
  place: string;
  emoji: string;
}

interface EarringItem {
  name: string;
  stone: string;
  level: number;
  place: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekPearlMeta[] = [
  { day: '周一', value: 18 },
  { day: '周二', value: 26 },
  { day: '周三', value: 34 },
  { day: '周四', value: 42 },
  { day: '周五', value: 55 },
  { day: '周六', value: 62 },
  { day: '周日', value: 57 }
];

const SHAPE_SHARE: ShapeShareMeta[] = [
  { name: '圆珠', count: 5, color: '#D98E97' },
  { name: '水滴', count: 4, color: '#3E5C76' },
  { name: '巴洛克', count: 3, color: '#7BA3A8' },
  { name: '异形', count: 2, color: '#C98A4E' }
];

const PEARL_HOT: PearlHotMeta[] = [
  { name: '插核植珠', heat: 96, color: '#3E5C76' },
  { name: '海水养殖', heat: 95, color: '#D98E97' },
  { name: '母贝优选', heat: 94, color: '#7BA3A8' },
  { name: '层叠包覆', heat: 93, color: '#7E9BB0' },
  { name: '打孔穿串', heat: 92, color: '#C98A4E' },
  { name: '镜面抛光', heat: 91, color: '#F2E3D5' }
];

const PEARL_TOP: PearlTopMeta[] = [
  { name: '南洋金珠', sold: 96, color: '#D98E97' },
  { name: '大溪地黑珠', sold: 92, color: '#3E5C76' },
  { name: '爱迪生珠', sold: 89, color: '#7BA3A8' },
  { name: 'akoya白珠', sold: 86, color: '#7E9BB0' },
  { name: '淡水无核珠', sold: 83, color: '#C98A4E' },
  { name: '马贝珠', sold: 79, color: '#F2E3D5' }
];

@Observed
export class PearlData {
  pearls: PearlItem[] = [
    { name: '南洋金珠', origin: '南海', shape: '圆珠', price: 1280, emoji: '🟡' },
    { name: '大溪地黑珠', origin: '大溪地', shape: '圆珠', price: 1580, emoji: '⚫' },
    { name: 'akoya白珠', origin: '日本海', shape: '圆珠', price: 880, emoji: '⚪' },
    { name: '淡水无核珠', origin: '诸暨', shape: '异形', price: 320, emoji: '🌾' },
    { name: '马贝珠', origin: '冲绳', shape: '半圆', price: 980, emoji: '🌕' },
    { name: '孔克珠', origin: '加勒比', shape: '椭圆', price: 1880, emoji: '🌸' },
    { name: '巴洛克珠', origin: '南海', shape: '异形', price: 720, emoji: '🌀' },
    { name: '爱迪生珠', origin: '诸暨', shape: '圆珠', price: 1080, emoji: '💡' },
    { name: '极光白珠', origin: '大溪地', shape: '圆珠', price: 1480, emoji: '🌟' },
    { name: '粉红珠', origin: '海南', shape: '水滴', price: 690, emoji: '🌷' },
    { name: '螺珠', origin: '南海', shape: '椭圆', price: 2600, emoji: '🐚' },
    { name: '海螺珠', origin: '加勒比', shape: '水滴', price: 2200, emoji: '🎺' }
  ];

  necklaces: NecklaceItem[] = [
    { name: '单珠链', style: '极简', level: 96, place: '东京', emoji: '📿' },
    { name: '三层链', style: '复古', level: 95, place: '大阪', emoji: '💠' },
    { name: '锁骨链', style: '轻盈', level: 94, place: '巴黎', emoji: '✨' },
    { name: '满天星链', style: '闪耀', level: 95, place: '米兰', emoji: '🌌' },
    { name: '珍珠choker', style: '时尚', level: 93, place: '首尔', emoji: '🎀' },
    { name: '吊坠链', style: '典雅', level: 94, place: '伦敦', emoji: '🔮' },
    { name: '串珠长链', style: '传统', level: 92, place: '北京', emoji: '🧵' },
    { name: '珍珠手链', style: '腕间', level: 93, place: '上海', emoji: '⌚' },
    { name: '花式编链', style: '匠心', level: 94, place: '杭州', emoji: '🌺' },
    { name: '双股链', style: '复刻', level: 92, place: '苏州', emoji: '🎗️' },
    { name: '婴儿珠链', style: '细巧', level: 91, place: '诸暨', emoji: '👶' },
    { name: '渐变珠链', style: '层次', level: 96, place: '广州', emoji: '🌈' }
  ];

  earrings: EarringItem[] = [
    { name: '水滴耳坠', stone: '珍珠', level: 96, place: '深圳', emoji: '💧' },
    { name: '圆珠耳钉', stone: '简约', level: 95, place: '广州', emoji: '⚪' },
    { name: '花型耳坠', stone: '绽放', level: 94, place: '昆明', emoji: '🌸' },
    { name: '流苏耳环', stone: '摇曳', level: 93, place: '西安', emoji: '🌾' },
    { name: '复古耳环', stone: '典雅', level: 95, place: '北京', emoji: '🏛️' },
    { name: '镶嵌耳环', stone: '璀璨', level: 94, place: '香港', emoji: '💎' },
    { name: '珍珠耳线', stone: '灵动', level: 92, place: '上海', emoji: '🎐' },
    { name: '耳骨夹珠', stone: '前卫', level: 91, place: '成都', emoji: '🎧' },
    { name: '螺钿耳坠', stone: '光泽', level: 94, place: '福州', emoji: '🐚' },
    { name: '竹节耳环', stone: '清雅', level: 92, place: '苏州', emoji: '🎋' },
    { name: '双珠耳钉', stone: '成对', level: 93, place: '杭州', emoji: '🫧' },
    { name: '马蹄耳环', stone: '异形', level: 91, place: '大理', emoji: '🐎' }
  ];

  orders: PearlOrderItem[] = [
    { name: '珠宝连锁', region: '华南', count: 600, amount: 1080000, emoji: '💎' },
    { name: '高定品牌', region: '华东', count: 400, amount: 960000, emoji: '👑' },
    { name: '海外买手', region: '海外', count: 350, amount: 875000, emoji: '🌏' },
    { name: '婚庆公司', region: '华南', count: 500, amount: 750000, emoji: '💒' },
    { name: '直播电商', region: '华东', count: 800, amount: 720000, emoji: '📺' },
    { name: '古着店铺', region: '华北', count: 450, amount: 675000, emoji: '🏛️' },
    { name: '设计师品牌', region: '华东', count: 300, amount: 780000, emoji: '🎨' },
    { name: '免税店采购', region: '华南', count: 550, amount: 825000, emoji: '✈️' },
    { name: '博物馆文创', region: '华北', count: 380, amount: 570000, emoji: '🗿' },
    { name: '银楼定制', region: '西南', count: 320, amount: 640000, emoji: '🥈' },
    { name: '影视造型', region: '华北', count: 250, amount: 625000, emoji: '🎬' },
    { name: '老牌珠宝行', region: '华东', count: 420, amount: 1008000, emoji: '⏳' }
  ];

  reviews: PearlReviewItem[] = [
    { 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: 5, 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: 4, 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 getOriginColor(origin: string): string {
  if (origin === '南海' || origin === '加勒比') {
    return '#D98E97';
  } else if (origin === '大溪地' || origin === '日本海') {
    return '#3E5C76';
  } else if (origin === '诸暨' || origin === '海南') {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getLevelColor(level: number): string {
  if (level >= 95) {
    return '#D98E97';
  } else if (level >= 93) {
    return '#3E5C76';
  }
  return '#7BA3A8';
}

function getPriceColor(price: number): string {
  if (price >= 2000) {
    return '#D98E97';
  } else if (price >= 1000) {
    return '#3E5C76';
  } else if (price >= 500) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getOrderColor(amount: number): string {
  if (amount >= 900000) {
    return '#D98E97';
  } else if (amount >= 700000) {
    return '#3E5C76';
  } else if (amount >= 500000) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '色泽温润' || tag === '品相一流' || tag === '珠圆玉润' || tag === '细节讲究') {
    return '#3E5C76';
  } else if (tag === '个性十足' || tag === '婚礼首选' || tag === '配色精妙') {
    return '#D98E97';
  } else if (tag === '走量飞快' || tag === '轻盈百搭' || tag === '成色上佳' || tag === '上镜极佳') {
    return '#7BA3A8';
  } else if (tag === '光泽流转') {
    return '#C98A4E';
  }
  return '#9CAAB3';
}

@Entry
@Component
struct PearlApp {
  @State curTab: number = 0;
  @State data: PearlData = new PearlData();
  @State showAddPearl: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteNecklace: boolean = false;
  @State showDetailEarring: boolean = false;
  @State delNecklaceName: string = '';
  @State detailEarringName: string = '';
  @State bubbleFloat: boolean = false;
  @State lusterPulse: 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('Pearl · 沧海月明珠有泪')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🫧')
                .fontSize(18)
                .onClick(() => {
                  this.bubbleFloat = !this.bubbleFloat;
                })
                .translate({ x: 0, y: this.bubbleFloat ? -10 : 4 })
                .animation({ duration: 640, curve: Curve.EaseOut })
              Text('✨')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.lusterPulse = !this.lusterPulse;
                })
                .scale({ x: this.lusterPulse ? 1.4 : 1, y: this.lusterPulse ? 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(PEARL_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(12)
                    .height(12)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(6)
                    .border({ width: 2, color: COLORS.accent })
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'pearl' + 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 === PearlTab.PEARL) {
          PearlContent({ data: this.data, onAdd: () => {
            this.showAddPearl = true;
          } })
        } else if (this.curTab === PearlTab.NECKLACE) {
          NecklaceContent({ data: this.data, onDel: (n: string) => {
            this.delNecklaceName = n;
            this.showDeleteNecklace = true;
          } })
        } else if (this.curTab === PearlTab.EARRING) {
          EarringContent({ data: this.data, onDetail: (n: string) => {
            this.detailEarringName = n;
            this.showDetailEarring = true;
          } })
        } else if (this.curTab === PearlTab.ORDER) {
          PearlOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          PearlReviewContent({ 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(20)
            .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.showAddPearl) {
        AddPearlModal({
          onClose: () => {
            this.showAddPearl = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditPearlOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteNecklace) {
        DeleteNecklaceModal({
          title: this.delNecklaceName, onClose: () => {
            this.showDeleteNecklace = false;
          }
        })
      }
      if (this.showDetailEarring) {
        DetailEarringModal({
          name: this.detailEarringName, onClose: () => {
            this.showDetailEarring = false;
          }
        })
      }
    }
  }
}

@Component
struct PearlTag {
  @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 PearlContent {
  @ObjectLink data: PearlData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.silkWhite)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.accent })
            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.accent })
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 本周珍珠销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六62颗')
              .fontSize(9)
              .fontColor(COLORS.accent)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekPearlMeta) => {
              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: WeekPearlMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.pearls, (p: PearlItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(44)
                .height(44)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getOriginColor(p.origin) + '14')
                .borderRadius(22)
                .border({ width: 1, color: getOriginColor(p.origin) + '66' })
                Row() {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(getOriginColor(p.origin))
                    .borderRadius(4)
                    .margin({ left: 4 })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                    .margin({ left: 4 })
                }
                .width(40)
                .justifyContent(FlexAlign.Center)
              }
              .width(60)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getOriginColor(p.origin) + '0D')
              .borderRadius(30)
              .border({ width: 1, color: getOriginColor(p.origin) + '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() {
                  PearlTag({ text: p.origin, color: getOriginColor(p.origin) })
                  PearlTag({ text: p.shape, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: PearlItem, i: number) => 'pl' + 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 NecklaceContent {
  @ObjectLink data: PearlData;
  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(PEARL_TOP, (t: PearlTopMeta) => {
            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: PearlTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.necklaces, (c: NecklaceItem, i: number) => {
            Row() {
              Column() {
                Text(c.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(getLevelColor(c.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(getLevelColor(c.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(c.level) + '33' })

              Column() {
                Row() {
                  Text(c.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(c.style + '链')
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  Text('光泽评级')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                  Row() {
                    Column()
                      .layoutWeight(c.level)
                      .height(6)
                      .backgroundColor(getLevelColor(c.level))
                      .borderRadius(3)
                    Column()
                      .layoutWeight(100 - c.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(c.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (c: NecklaceItem, i: number) => 'nk' + c.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 EarringContent {
  @ObjectLink data: PearlData;
  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(PEARL_HOT, (d: PearlHotMeta) => {
            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: PearlHotMeta) => d.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.earrings, (e: EarringItem, i: number) => {
            Row() {
              Column() {
                Text(e.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Column()
                  .width(14)
                  .height(14)
                  .backgroundColor(getLevelColor(e.level))
                  .borderRadius(7)
                  .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getLevelColor(e.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(e.level) + '33' })

              Column() {
                Row() {
                  Text(e.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('品级' + e.level)
                    .fontSize(9)
                    .fontColor(getLevelColor(e.level))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  PearlTag({ text: e.stone + '珠', color: COLORS.textSecondary })
                  Text(e.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 })

              Text('👁️')
                .fontSize(14)
                .onClick(() => {
                  this.onDetail(e.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (e: EarringItem, i: number) => 'er' + e.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 PearlOrderContent {
  @ObjectLink data: PearlData;
  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(SHAPE_SHARE, (s: ShapeShareMeta) => {
            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: ShapeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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: PearlOrderItem, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: PearlOrderItem, 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 PearlReviewContent {
  @ObjectLink data: PearlData;

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

@Component
struct AddPearlModal {
  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('¥1280')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditPearlOrderModal {
  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('600 → 700 颗')
            .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('¥1260000')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeleteNecklaceModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailEarringModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

interface PearlPalette {
  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: PearlPalette = {
  primary: '#3E5C76',
  primaryLight: '#7E9BB0',
  primaryDark: '#2A4056',
  accent: '#D98E97',
  accentLight: '#EFC6CB',
  bg: '#F4F0EC',
  cardBg: '#FFFFFF',
  cardAlt: '#F1E7E3',
  textPrimary: '#2C3A44',
  textSecondary: '#6B7C88',
  textHint: '#9CAAB3',
  border: '#D9E0E6',
  line: '#EAEFEF',
  success: '#7BA3A8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#F2E3D5',
  silkWhite: '#FBF8F4'
};

enum PearlTab {
  PEARL = 0,
  NECKLACE = 1,
  EARRING = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'pearl', icon: '🫧', label: '珍珠', color: '#3E5C76' },
  { key: 'necklace', icon: '📿', label: '项链', color: '#D98E97' },
  { key: 'earring', icon: '💎', label: '耳饰', color: '#7BA3A8' },
  { key: 'order', icon: '📦', label: '订单', color: '#C98A4E' },
  { key: 'review', icon: '⭐', label: '客评', color: '#9CAAB3' }
];

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

interface PearlItem {
  name: string;
  origin: string;
  shape: string;
  price: number;
  emoji: string;
}

interface NecklaceItem {
  name: string;
  style: string;
  level: number;
  place: string;
  emoji: string;
}

interface EarringItem {
  name: string;
  stone: string;
  level: number;
  place: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekPearlMeta[] = [
  { day: '周一', value: 18 },
  { day: '周二', value: 26 },
  { day: '周三', value: 34 },
  { day: '周四', value: 42 },
  { day: '周五', value: 55 },
  { day: '周六', value: 62 },
  { day: '周日', value: 57 }
];

const SHAPE_SHARE: ShapeShareMeta[] = [
  { name: '圆珠', count: 5, color: '#D98E97' },
  { name: '水滴', count: 4, color: '#3E5C76' },
  { name: '巴洛克', count: 3, color: '#7BA3A8' },
  { name: '异形', count: 2, color: '#C98A4E' }
];

const PEARL_HOT: PearlHotMeta[] = [
  { name: '插核植珠', heat: 96, color: '#3E5C76' },
  { name: '海水养殖', heat: 95, color: '#D98E97' },
  { name: '母贝优选', heat: 94, color: '#7BA3A8' },
  { name: '层叠包覆', heat: 93, color: '#7E9BB0' },
  { name: '打孔穿串', heat: 92, color: '#C98A4E' },
  { name: '镜面抛光', heat: 91, color: '#F2E3D5' }
];

const PEARL_TOP: PearlTopMeta[] = [
  { name: '南洋金珠', sold: 96, color: '#D98E97' },
  { name: '大溪地黑珠', sold: 92, color: '#3E5C76' },
  { name: '爱迪生珠', sold: 89, color: '#7BA3A8' },
  { name: 'akoya白珠', sold: 86, color: '#7E9BB0' },
  { name: '淡水无核珠', sold: 83, color: '#C98A4E' },
  { name: '马贝珠', sold: 79, color: '#F2E3D5' }
];

@Observed
export class PearlData {
  pearls: PearlItem[] = [
    { name: '南洋金珠', origin: '南海', shape: '圆珠', price: 1280, emoji: '🟡' },
    { name: '大溪地黑珠', origin: '大溪地', shape: '圆珠', price: 1580, emoji: '⚫' },
    { name: 'akoya白珠', origin: '日本海', shape: '圆珠', price: 880, emoji: '⚪' },
    { name: '淡水无核珠', origin: '诸暨', shape: '异形', price: 320, emoji: '🌾' },
    { name: '马贝珠', origin: '冲绳', shape: '半圆', price: 980, emoji: '🌕' },
    { name: '孔克珠', origin: '加勒比', shape: '椭圆', price: 1880, emoji: '🌸' },
    { name: '巴洛克珠', origin: '南海', shape: '异形', price: 720, emoji: '🌀' },
    { name: '爱迪生珠', origin: '诸暨', shape: '圆珠', price: 1080, emoji: '💡' },
    { name: '极光白珠', origin: '大溪地', shape: '圆珠', price: 1480, emoji: '🌟' },
    { name: '粉红珠', origin: '海南', shape: '水滴', price: 690, emoji: '🌷' },
    { name: '螺珠', origin: '南海', shape: '椭圆', price: 2600, emoji: '🐚' },
    { name: '海螺珠', origin: '加勒比', shape: '水滴', price: 2200, emoji: '🎺' }
  ];

  necklaces: NecklaceItem[] = [
    { name: '单珠链', style: '极简', level: 96, place: '东京', emoji: '📿' },
    { name: '三层链', style: '复古', level: 95, place: '大阪', emoji: '💠' },
    { name: '锁骨链', style: '轻盈', level: 94, place: '巴黎', emoji: '✨' },
    { name: '满天星链', style: '闪耀', level: 95, place: '米兰', emoji: '🌌' },
    { name: '珍珠choker', style: '时尚', level: 93, place: '首尔', emoji: '🎀' },
    { name: '吊坠链', style: '典雅', level: 94, place: '伦敦', emoji: '🔮' },
    { name: '串珠长链', style: '传统', level: 92, place: '北京', emoji: '🧵' },
    { name: '珍珠手链', style: '腕间', level: 93, place: '上海', emoji: '⌚' },
    { name: '花式编链', style: '匠心', level: 94, place: '杭州', emoji: '🌺' },
    { name: '双股链', style: '复刻', level: 92, place: '苏州', emoji: '🎗️' },
    { name: '婴儿珠链', style: '细巧', level: 91, place: '诸暨', emoji: '👶' },
    { name: '渐变珠链', style: '层次', level: 96, place: '广州', emoji: '🌈' }
  ];

  earrings: EarringItem[] = [
    { name: '水滴耳坠', stone: '珍珠', level: 96, place: '深圳', emoji: '💧' },
    { name: '圆珠耳钉', stone: '简约', level: 95, place: '广州', emoji: '⚪' },
    { name: '花型耳坠', stone: '绽放', level: 94, place: '昆明', emoji: '🌸' },
    { name: '流苏耳环', stone: '摇曳', level: 93, place: '西安', emoji: '🌾' },
    { name: '复古耳环', stone: '典雅', level: 95, place: '北京', emoji: '🏛️' },
    { name: '镶嵌耳环', stone: '璀璨', level: 94, place: '香港', emoji: '💎' },
    { name: '珍珠耳线', stone: '灵动', level: 92, place: '上海', emoji: '🎐' },
    { name: '耳骨夹珠', stone: '前卫', level: 91, place: '成都', emoji: '🎧' },
    { name: '螺钿耳坠', stone: '光泽', level: 94, place: '福州', emoji: '🐚' },
    { name: '竹节耳环', stone: '清雅', level: 92, place: '苏州', emoji: '🎋' },
    { name: '双珠耳钉', stone: '成对', level: 93, place: '杭州', emoji: '🫧' },
    { name: '马蹄耳环', stone: '异形', level: 91, place: '大理', emoji: '🐎' }
  ];

  orders: PearlOrderItem[] = [
    { name: '珠宝连锁', region: '华南', count: 600, amount: 1080000, emoji: '💎' },
    { name: '高定品牌', region: '华东', count: 400, amount: 960000, emoji: '👑' },
    { name: '海外买手', region: '海外', count: 350, amount: 875000, emoji: '🌏' },
    { name: '婚庆公司', region: '华南', count: 500, amount: 750000, emoji: '💒' },
    { name: '直播电商', region: '华东', count: 800, amount: 720000, emoji: '📺' },
    { name: '古着店铺', region: '华北', count: 450, amount: 675000, emoji: '🏛️' },
    { name: '设计师品牌', region: '华东', count: 300, amount: 780000, emoji: '🎨' },
    { name: '免税店采购', region: '华南', count: 550, amount: 825000, emoji: '✈️' },
    { name: '博物馆文创', region: '华北', count: 380, amount: 570000, emoji: '🗿' },
    { name: '银楼定制', region: '西南', count: 320, amount: 640000, emoji: '🥈' },
    { name: '影视造型', region: '华北', count: 250, amount: 625000, emoji: '🎬' },
    { name: '老牌珠宝行', region: '华东', count: 420, amount: 1008000, emoji: '⏳' }
  ];

  reviews: PearlReviewItem[] = [
    { 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: 5, 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: 4, 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 getOriginColor(origin: string): string {
  if (origin === '南海' || origin === '加勒比') {
    return '#D98E97';
  } else if (origin === '大溪地' || origin === '日本海') {
    return '#3E5C76';
  } else if (origin === '诸暨' || origin === '海南') {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getLevelColor(level: number): string {
  if (level >= 95) {
    return '#D98E97';
  } else if (level >= 93) {
    return '#3E5C76';
  }
  return '#7BA3A8';
}

function getPriceColor(price: number): string {
  if (price >= 2000) {
    return '#D98E97';
  } else if (price >= 1000) {
    return '#3E5C76';
  } else if (price >= 500) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getOrderColor(amount: number): string {
  if (amount >= 900000) {
    return '#D98E97';
  } else if (amount >= 700000) {
    return '#3E5C76';
  } else if (amount >= 500000) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '色泽温润' || tag === '品相一流' || tag === '珠圆玉润' || tag === '细节讲究') {
    return '#3E5C76';
  } else if (tag === '个性十足' || tag === '婚礼首选' || tag === '配色精妙') {
    return '#D98E97';
  } else if (tag === '走量飞快' || tag === '轻盈百搭' || tag === '成色上佳' || tag === '上镜极佳') {
    return '#7BA3A8';
  } else if (tag === '光泽流转') {
    return '#C98A4E';
  }
  return '#9CAAB3';
}

@Entry
@Component
struct PearlApp {
  @State curTab: number = 0;
  @State data: PearlData = new PearlData();
  @State showAddPearl: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteNecklace: boolean = false;
  @State showDetailEarring: boolean = false;
  @State delNecklaceName: string = '';
  @State detailEarringName: string = '';
  @State bubbleFloat: boolean = false;
  @State lusterPulse: 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('Pearl · 沧海月明珠有泪')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🫧')
                .fontSize(18)
                .onClick(() => {
                  this.bubbleFloat = !this.bubbleFloat;
                })
                .translate({ x: 0, y: this.bubbleFloat ? -10 : 4 })
                .animation({ duration: 640, curve: Curve.EaseOut })
              Text('✨')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.lusterPulse = !this.lusterPulse;
                })
                .scale({ x: this.lusterPulse ? 1.4 : 1, y: this.lusterPulse ? 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(PEARL_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(12)
                    .height(12)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(6)
                    .border({ width: 2, color: COLORS.accent })
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'pearl' + 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 === PearlTab.PEARL) {
          PearlContent({ data: this.data, onAdd: () => {
            this.showAddPearl = true;
          } })
        } else if (this.curTab === PearlTab.NECKLACE) {
          NecklaceContent({ data: this.data, onDel: (n: string) => {
            this.delNecklaceName = n;
            this.showDeleteNecklace = true;
          } })
        } else if (this.curTab === PearlTab.EARRING) {
          EarringContent({ data: this.data, onDetail: (n: string) => {
            this.detailEarringName = n;
            this.showDetailEarring = true;
          } })
        } else if (this.curTab === PearlTab.ORDER) {
          PearlOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          PearlReviewContent({ 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(20)
            .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.showAddPearl) {
        AddPearlModal({
          onClose: () => {
            this.showAddPearl = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditPearlOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteNecklace) {
        DeleteNecklaceModal({
          title: this.delNecklaceName, onClose: () => {
            this.showDeleteNecklace = false;
          }
        })
      }
      if (this.showDetailEarring) {
        DetailEarringModal({
          name: this.detailEarringName, onClose: () => {
            this.showDetailEarring = false;
          }
        })
      }
    }
  }
}

@Component
struct PearlTag {
  @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 PearlContent {
  @ObjectLink data: PearlData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.silkWhite)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.accent })
            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.accent })
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 本周珍珠销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六62颗')
              .fontSize(9)
              .fontColor(COLORS.accent)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekPearlMeta) => {
              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: WeekPearlMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.pearls, (p: PearlItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(44)
                .height(44)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getOriginColor(p.origin) + '14')
                .borderRadius(22)
                .border({ width: 1, color: getOriginColor(p.origin) + '66' })
                Row() {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(getOriginColor(p.origin))
                    .borderRadius(4)
                    .margin({ left: 4 })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                    .margin({ left: 4 })
                }
                .width(40)
                .justifyContent(FlexAlign.Center)
              }
              .width(60)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getOriginColor(p.origin) + '0D')
              .borderRadius(30)
              .border({ width: 1, color: getOriginColor(p.origin) + '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() {
                  PearlTag({ text: p.origin, color: getOriginColor(p.origin) })
                  PearlTag({ text: p.shape, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: PearlItem, i: number) => 'pl' + 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 NecklaceContent {
  @ObjectLink data: PearlData;
  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(PEARL_TOP, (t: PearlTopMeta) => {
            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: PearlTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.necklaces, (c: NecklaceItem, i: number) => {
            Row() {
              Column() {
                Text(c.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(getLevelColor(c.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(getLevelColor(c.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(c.level) + '33' })

              Column() {
                Row() {
                  Text(c.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(c.style + '链')
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  Text('光泽评级')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                  Row() {
                    Column()
                      .layoutWeight(c.level)
                      .height(6)
                      .backgroundColor(getLevelColor(c.level))
                      .borderRadius(3)
                    Column()
                      .layoutWeight(100 - c.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(c.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (c: NecklaceItem, i: number) => 'nk' + c.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 EarringContent {
  @ObjectLink data: PearlData;
  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(PEARL_HOT, (d: PearlHotMeta) => {
            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: PearlHotMeta) => d.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.earrings, (e: EarringItem, i: number) => {
            Row() {
              Column() {
                Text(e.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Column()
                  .width(14)
                  .height(14)
                  .backgroundColor(getLevelColor(e.level))
                  .borderRadius(7)
                  .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getLevelColor(e.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(e.level) + '33' })

              Column() {
                Row() {
                  Text(e.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('品级' + e.level)
                    .fontSize(9)
                    .fontColor(getLevelColor(e.level))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  PearlTag({ text: e.stone + '珠', color: COLORS.textSecondary })
                  Text(e.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 })

              Text('👁️')
                .fontSize(14)
                .onClick(() => {
                  this.onDetail(e.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (e: EarringItem, i: number) => 'er' + e.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 PearlOrderContent {
  @ObjectLink data: PearlData;
  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(SHAPE_SHARE, (s: ShapeShareMeta) => {
            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: ShapeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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: PearlOrderItem, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: PearlOrderItem, 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 PearlReviewContent {
  @ObjectLink data: PearlData;

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

@Component
struct AddPearlModal {
  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('¥1280')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditPearlOrderModal {
  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('600 → 700 颗')
            .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('¥1260000')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeleteNecklaceModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailEarringModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

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

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

第二,逻辑集中且可复用。颜色映射规则分散在多个组件中是维护噩梦——如果要调整"高价珍珠"的阈值从2000改为2500,需要在每个使用处逐一修改。而现在只需改getPriceColor一处。

第三,语义化表达业务规则。函数名本身就是最好的文档——getOriginColor(origin)一眼就能看出是根据产地返回珍珠卡片的主题色;getPriceColor(price)则是根据价格区间返回价格颜色;getReviewTagColor(tag)将评价标签归类为四个语义分组并映射颜色。

特别值得分析的是getPriceColor阶梯式阈值设计:价格≥2000返回胭脂粉(暗示"高端收藏"),1000-1999返回深海蓝(暗示"品质之选"),500-999返回青灰蓝(暗示"日常佩戴"),<500返回琥珀棕(暗示"入门体验")。这种将业务语义(价格区间)映射到视觉语义(颜色情感)的模式,是数据可视化中的经典手法。

getReviewTagColor的设计也颇具匠心:它按照标签的语义内涵将12个标签分为四组——"品质传承"类(色泽温润、品相一流、珠圆玉润、细节讲究)用深海蓝,"场景情感"类(个性十足、婚礼首选、配色精妙)用胭脂粉,"市场表现"类(走量飞快、轻盈百搭、成色上佳、上镜极佳)用青灰蓝,"光泽质感"类(光泽流转)用琥珀棕。通过颜色就能快速感知评价的属性维度。

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

9.1 状态声明

@Entry
@Component
struct PearlApp {
  @State curTab: number = 0;
  @State data: PearlData = new PearlData();
  @State showAddPearl: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteNecklace: boolean = false;
  @State showDetailEarring: boolean = false;
  @State delNecklaceName: string = '';
  @State detailEarringName: string = '';
  @State bubbleFloat: boolean = false;
  @State lusterPulse: boolean = false;

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

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

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

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

  • showAddPearl / showEditOrder / showDeleteNecklace / showDetailEarring:分别控制四种弹窗的显示/隐藏。
  • delNecklaceName / detailEarringName:传递给删除确认弹窗和详情弹窗的上下文参数(要删除的项链名、要查看详情的耳饰名)。

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

  • bubbleFloat:控制头部气泡图标的上下漂浮动画(点击切换translateY -10/4)。
  • lusterPulse:控制光泽星星图标的脉冲缩放动画(点击切换1.4倍放大)。

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

9.2 构建方法与页面结构

build()方法是ArkTS声明式UI的核心——它描述了"UI应该长什么样",框架负责在状态变化时高效地 diff 和更新真实DOM。PearlAppbuild()方法构建了以下三层结构:

第一层:主内容区(外层Column)。包含两个子区域:

  • 顶部Header区域:深海蓝(primaryDark)背景,包含应用标题"🫧 珍珠坊"、英文副标题"Pearl · 沧海月明珠有泪"、右侧操作图标区(气泡漂浮、光泽脉冲、项链图标),以及下方的12颗珍珠点缀小圆点(用三种尺寸和颜色模拟散落在海沙上的珍珠)。
  • 内容切换区:根据curTab的值,使用if-else if-else链条件渲染对应的内容组件(PearlContent / NecklaceContent / EarringContent / PearlOrderContent / PearlReviewContent)。

第二层:底部导航栏(固定在底部)。使用ForEach遍历TAB_LIST数组渲染5个Tab项,每个Tab项根据是否为当前选中项来决定:文字颜色(主题色 vs 提示色)、字重(Bold vs Normal)、背景色(半透明主题色 vs 透明)、边框(有 vs 无)。点击时更新curTab状态触发切换。

第三层:弹窗叠加层。四个if条件分别控制四种弹窗的渲染,弹窗以绝对定位的方式覆盖在主内容区上方。

9.3 弹窗的条件渲染

if (this.showAddPearl) {
  AddPearlModal({
    onClose: () => { this.showAddPearl = false; }
  })
}
if (this.showEditOrder) {
  EditPearlOrderModal({
    onClose: () => { this.showEditOrder = false; }
  })
}
if (this.showDeleteNecklace) {
  DeleteNecklaceModal({
    title: this.delNecklaceName, onClose: () => { this.showDeleteNecklace = false; }
  })
}
if (this.showDetailEarring) {
  DetailEarringModal({
    name: this.detailEarringName, onClose: () => { this.showDetailEarring = false; }
  })
}

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

DeleteNecklaceModalDetailEarringModal额外接收了title/name属性,用于在弹窗内显示具体的操作对象(要删除哪条项链、查看哪款耳饰的详情)。这种"状态提升"(Lifting State Up)的模式——将弹窗所需的上下文数据存储在父组件的状态中、通过props传递给弹窗——是React/ArkTS中处理跨组件数据流的经典方案。

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

10.1 标签组件 PearlTag

@Component
struct PearlTag {
  @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' })
  }
}

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

  • 背景色使用this.color + '14',即在原色hex后面追加14(约8%不透明度的alpha通道),生成8位颜色值。ArkTS原生支持这种8位hex颜色格式,前6位是RGB,后2位是Alpha(00=完全透明,FF=完全不透明)。
  • 边框色使用this.color + '40'(约25%不透明度),比背景更明显但不抢眼。

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

10.2 珍珠内容组件 PearlContent

@Component
struct PearlContent {
  @ObjectLink data: PearlData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        // 销量图表卡片
        Column() { /* ... */ }
        // 珍珠陈列列表
        ForEach(this.data.pearls, (p: PearlItem, i: number) => {
          Row() { /* 卡片内容 */ }
        }, (p: PearlItem, i: number) => 'pl' + p.name + i)
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

PearlContent是珍珠Tab页的主体内容组件。它使用了@ObjectLink装饰器来接收父组件传递的PearlData实例。@ObjectLink@Prop的关键区别在于:@ObjectLink建立的是引用关系,而非值的拷贝——当PearlData实例的属性(如pearls数组)发生变化时,PearlContent中引用这些属性的部分会自动刷新。这使得数据在父子组件间实现了"双向绑定"的效果。

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

第一部分:本周销量图表卡片。顶部有一排装饰性的珍珠点缀(用三种样式的小圆点模拟散落在丝绒上的珍珠),下方是标题"📈 本周珍珠销量"和副标题"周六62颗"。柱状图使用ForEach遍历WEEK_SOLD数据,每根柱子的高度value决定,颜色由阈值判断决定(≥55用胭脂粉,否则用深海蓝)。柱子使用borderRadius(8)实现圆角,底部对齐(alignItems(VerticalAlign.Bottom))模拟真实柱状图的基线对齐效果。

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

  • 左侧图标区(宽60):圆形头像区显示emoji,下方有三颗珍珠小圆点模拟项链串珠。整体背景色由getOriginColor(p.origin)决定并附加极低透明度(0D=5%),边框用33%透明度。
  • 中间信息区layoutWeight(1)占据剩余空间):第一行是珍珠名(粗体)+ 价格(带颜色),第二行是产地标签(PearlTag)+ 形状标签(PearlTag)+ "珠圆玉润"提示文字。
  • 卡片背景色使用i % 2 === 0实现斑马纹交替(白色/淡粉米),增强列表的可读性。

10.3 其他内容组件

NecklaceContent(项链坊):结构与PearlContent类似,但顶部图表替换为"珍珠销量TOP6"水平条形图(使用PEARL_TOP数据和layoutWeight实现比例条),列表项改为项链卡片——左侧显示emoji+等级圆点+丝白点缀,中间显示项链名+风格,下方显示"光泽评级"进度条(同样用layoutWeight(c.level)实现比例填充)。右侧有删除图标,点击触发onDel回调。

EarringContent(耳饰坊):顶部图表为"养殖工艺热度"水平条形图(PEARL_HOT数据),列表项为耳饰卡片——显示耳饰名+设计关键词+品级评级,右侧有详情图标,点击触发onDetail回调。

PearlOrderContent(订单台账):顶部图表为"珠形构成占比"水平条形图(SHAPE_SHARE数据),列表项为订单卡片——显示客户emoji+名称+区域+数量+金额(金额颜色由getOrderColor决定),右侧有编辑图标。

PearlReviewContent(客评口碑):无顶部图表,直接是评价列表。每条评价显示用户emoji+姓名+日期+星级(用.repeat(score)重复渲染)+ 标签(PearlTag)+ 评价正文。星级使用repeat()方法根据score值动态生成对应数量的星星字符,简洁而巧妙。

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

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

11.1 新增珍珠弹窗 AddPearlModal

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

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

11.2 其他弹窗组件

  • EditPearlOrderModal(修改订单):结构与AddPearlModal完全对称,内容改为客户/数量/金额三行信息,确认按钮文案为"保存修改"。
  • DeleteNecklaceModal(删除确认):接收title属性显示要删除的项链名,内容为确认提示文字("确定撤下「xxx」吗?"),确认按钮使用危险色danger = 暗红)以警示操作的不可逆性。
  • DetailEarringModal(耳饰详情):接收name属性,内容包含大号emoji+耳饰名展示区、工艺文化介绍文字、"工艺产地"和"匠人评级"信息行,单按钮"知道了"关闭。

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

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

12.1 layoutWeight布局权重的妙用

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

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

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

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

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

  • COLORS.accent + '55' → #D98E9755(约33%不透明度,用于卡片边框)
  • COLORS.primaryLight + '2E' → #7E9BB02E(约18%不透明度,用于头部操作区背景)
  • COLORS.primaryLight + '66' → #7E9BB066(约40%不透明度,用于分割线)
  • t.color + '14' → 如#3E5C7614(约8%不透明度,用于标签背景)

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

12.3 ForEach的键值生成策略

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

// 珍珠列表
(p: PearlItem, i: number) => 'pl' + p.name + i
// 项链列表
(c: NecklaceItem, i: number) => 'nk' + c.name + i
// 耳饰列表
(e: EarringItem, i: number) => 'er' + e.name + i
// 订单列表
(o: PearlOrderItem, i: number) => 'od' + o.name + i
// 评价列表
(r: PearlReviewItem, i: number) => 'rv' + r.name + i
// Tab栏
(t: TabMeta) => t.key
// 图表数据
(w: WeekPearlMeta) => w.day
(t: PearlTopMeta) => t.name
(d: PearlHotMeta) => d.name
(s: ShapeShareMeta) => s.name

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

  • 前缀(pl/nk/er/od/rv)区分不同的ForEach作用域,避免跨列表键值冲突。
  • 唯一标识(p.name/c.name等)保证同一条数据始终映射到同一个键。
  • 索引(i)作为兜底,即使两条数据name相同(理论上不应发生但有防御价值),索引也能保证唯一性。

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

12.4 @Observed/@ObjectLink响应式机制

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

PearlData (@Observed 类)
    ↓ @ObjectLink 引用传递
PearlContent / NecklaceContent / EarringContent / PearlOrderContent / PearlReviewContent
    ↓ 组件内部读取属性
ForEach / Text / 等UI节点

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

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

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

12.5 条件渲染与状态驱动UI

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

  • Tab切换:if (this.curTab === PearlTab.PEARL) 渲染PearlContent,否则渲染其他组件。curTab的变化由底部Tab栏的onClick事件触发。
  • 弹窗显示:if (this.showAddPearl) 渲染AddPearlModalshowAddPearl由"➕入珠"按钮的onClick设为true,由弹窗内部的"取消"/"确认"按钮的onClick通过onClose回调设为false

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

12.6 声明式动画的轻量运用

应用在顶部Header中设置了两个趣味性的交互动画:

Text('🫧')
  .onClick(() => { this.bubbleFloat = !this.bubbleFloat; })
  .translate({ x: 0, y: this.bubbleFloat ? -10 : 4 })
  .animation({ duration: 640, curve: Curve.EaseOut })

Text('✨')
  .onClick(() => { this.lusterPulse = !this.lusterPulse; })
  .scale({ x: this.lusterPulse ? 1.4 : 1, y: this.lusterPulse ? 1.4 : 1 })
  .animation({ duration: 500, curve: Curve.EaseOut })

这两个动画分别通过.translate().scale()实现,并通过.animation()统一配置时长和缓动曲线。它们不是业务核心,却在细节上传达了珍珠"漂浮于海水"与"光泽闪耀"的意象,是"用代码表达主题"的典范。

十三、技术要点对比表

技术维度 本应用实现方案 设计意图与优势
状态管理 @State(11个) + @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个图表接口 + 纯函数颜色映射 类型安全 + 逻辑集中 + 可独立测试
动画交互 .translate() + .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色雅致色彩体系:以"海蓝·胭脂·丝白"为核心的三色系设计,配合精细的透明度梯度(8%/18%/25%/33%/40%),在视觉上完美还原了珍珠珠宝的温润光泽。接口约束 + 常量实例化的双层设计,兼顾了类型安全和运行效率。
  2. @Observed + @ObjectLink的深度运用:将全部业务数据封装在一个可观察类中,通过@ObjectLink引用传递给5个子组件,实现了真正的"单一数据源 + 双向绑定"。任何数据变化都会自动级联刷新所有相关UI,无需手动编写事件监听。
  3. 纯函数颜色映射层:5个无副作用的颜色映射函数,将业务规则(产地、品级、价格区间、订单金额、评价标签等)映射到视觉语义(颜色情感)。逻辑集中、可测试、可复用。
  4. layoutWeight驱动的数据可视化:不依赖任何图表库,仅用ArkTS内置的layoutWeight属性就实现了4种精美的条形图/柱状图。这种"零依赖"的数据可视化方案在轻量级应用中极具实用价值。
  5. 统一的弹窗交互规范:4个弹窗共享相同的结构模板(遮罩+居中卡片+标题栏+内容区+操作栏),仅在内容和按钮细节上差异化。这种一致性设计大幅降低了用户的认知负荷。

14.3 优化建议与展望

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

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

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

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

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

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

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


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

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

安装DevEco Studio程序

在这里插入图片描述

选择目标安装目录:

在这里插入图片描述

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

在这里插入图片描述

新建一个空白模板:

在这里插入图片描述

设置API为24的模板项目:

在这里插入图片描述

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

在这里插入图片描述

完整代码

interface PearlPalette {
  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: PearlPalette = {
  primary: '#3E5C76',
  primaryLight: '#7E9BB0',
  primaryDark: '#2A4056',
  accent: '#D98E97',
  accentLight: '#EFC6CB',
  bg: '#F4F0EC',
  cardBg: '#FFFFFF',
  cardAlt: '#F1E7E3',
  textPrimary: '#2C3A44',
  textSecondary: '#6B7C88',
  textHint: '#9CAAB3',
  border: '#D9E0E6',
  line: '#EAEFEF',
  success: '#7BA3A8',
  warning: '#C98A4E',
  danger: '#9E3B2F',
  white: '#FFFFFF',
  gold: '#F2E3D5',
  silkWhite: '#FBF8F4'
};

enum PearlTab {
  PEARL = 0,
  NECKLACE = 1,
  EARRING = 2,
  ORDER = 3,
  REVIEW = 4
}

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

const TAB_LIST: TabMeta[] = [
  { key: 'pearl', icon: '🫧', label: '珍珠', color: '#3E5C76' },
  { key: 'necklace', icon: '📿', label: '项链', color: '#D98E97' },
  { key: 'earring', icon: '💎', label: '耳饰', color: '#7BA3A8' },
  { key: 'order', icon: '📦', label: '订单', color: '#C98A4E' },
  { key: 'review', icon: '⭐', label: '客评', color: '#9CAAB3' }
];

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

interface PearlItem {
  name: string;
  origin: string;
  shape: string;
  price: number;
  emoji: string;
}

interface NecklaceItem {
  name: string;
  style: string;
  level: number;
  place: string;
  emoji: string;
}

interface EarringItem {
  name: string;
  stone: string;
  level: number;
  place: string;
  emoji: string;
}

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

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

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

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

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

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

const WEEK_SOLD: WeekPearlMeta[] = [
  { day: '周一', value: 18 },
  { day: '周二', value: 26 },
  { day: '周三', value: 34 },
  { day: '周四', value: 42 },
  { day: '周五', value: 55 },
  { day: '周六', value: 62 },
  { day: '周日', value: 57 }
];

const SHAPE_SHARE: ShapeShareMeta[] = [
  { name: '圆珠', count: 5, color: '#D98E97' },
  { name: '水滴', count: 4, color: '#3E5C76' },
  { name: '巴洛克', count: 3, color: '#7BA3A8' },
  { name: '异形', count: 2, color: '#C98A4E' }
];

const PEARL_HOT: PearlHotMeta[] = [
  { name: '插核植珠', heat: 96, color: '#3E5C76' },
  { name: '海水养殖', heat: 95, color: '#D98E97' },
  { name: '母贝优选', heat: 94, color: '#7BA3A8' },
  { name: '层叠包覆', heat: 93, color: '#7E9BB0' },
  { name: '打孔穿串', heat: 92, color: '#C98A4E' },
  { name: '镜面抛光', heat: 91, color: '#F2E3D5' }
];

const PEARL_TOP: PearlTopMeta[] = [
  { name: '南洋金珠', sold: 96, color: '#D98E97' },
  { name: '大溪地黑珠', sold: 92, color: '#3E5C76' },
  { name: '爱迪生珠', sold: 89, color: '#7BA3A8' },
  { name: 'akoya白珠', sold: 86, color: '#7E9BB0' },
  { name: '淡水无核珠', sold: 83, color: '#C98A4E' },
  { name: '马贝珠', sold: 79, color: '#F2E3D5' }
];

@Observed
export class PearlData {
  pearls: PearlItem[] = [
    { name: '南洋金珠', origin: '南海', shape: '圆珠', price: 1280, emoji: '🟡' },
    { name: '大溪地黑珠', origin: '大溪地', shape: '圆珠', price: 1580, emoji: '⚫' },
    { name: 'akoya白珠', origin: '日本海', shape: '圆珠', price: 880, emoji: '⚪' },
    { name: '淡水无核珠', origin: '诸暨', shape: '异形', price: 320, emoji: '🌾' },
    { name: '马贝珠', origin: '冲绳', shape: '半圆', price: 980, emoji: '🌕' },
    { name: '孔克珠', origin: '加勒比', shape: '椭圆', price: 1880, emoji: '🌸' },
    { name: '巴洛克珠', origin: '南海', shape: '异形', price: 720, emoji: '🌀' },
    { name: '爱迪生珠', origin: '诸暨', shape: '圆珠', price: 1080, emoji: '💡' },
    { name: '极光白珠', origin: '大溪地', shape: '圆珠', price: 1480, emoji: '🌟' },
    { name: '粉红珠', origin: '海南', shape: '水滴', price: 690, emoji: '🌷' },
    { name: '螺珠', origin: '南海', shape: '椭圆', price: 2600, emoji: '🐚' },
    { name: '海螺珠', origin: '加勒比', shape: '水滴', price: 2200, emoji: '🎺' }
  ];

  necklaces: NecklaceItem[] = [
    { name: '单珠链', style: '极简', level: 96, place: '东京', emoji: '📿' },
    { name: '三层链', style: '复古', level: 95, place: '大阪', emoji: '💠' },
    { name: '锁骨链', style: '轻盈', level: 94, place: '巴黎', emoji: '✨' },
    { name: '满天星链', style: '闪耀', level: 95, place: '米兰', emoji: '🌌' },
    { name: '珍珠choker', style: '时尚', level: 93, place: '首尔', emoji: '🎀' },
    { name: '吊坠链', style: '典雅', level: 94, place: '伦敦', emoji: '🔮' },
    { name: '串珠长链', style: '传统', level: 92, place: '北京', emoji: '🧵' },
    { name: '珍珠手链', style: '腕间', level: 93, place: '上海', emoji: '⌚' },
    { name: '花式编链', style: '匠心', level: 94, place: '杭州', emoji: '🌺' },
    { name: '双股链', style: '复刻', level: 92, place: '苏州', emoji: '🎗️' },
    { name: '婴儿珠链', style: '细巧', level: 91, place: '诸暨', emoji: '👶' },
    { name: '渐变珠链', style: '层次', level: 96, place: '广州', emoji: '🌈' }
  ];

  earrings: EarringItem[] = [
    { name: '水滴耳坠', stone: '珍珠', level: 96, place: '深圳', emoji: '💧' },
    { name: '圆珠耳钉', stone: '简约', level: 95, place: '广州', emoji: '⚪' },
    { name: '花型耳坠', stone: '绽放', level: 94, place: '昆明', emoji: '🌸' },
    { name: '流苏耳环', stone: '摇曳', level: 93, place: '西安', emoji: '🌾' },
    { name: '复古耳环', stone: '典雅', level: 95, place: '北京', emoji: '🏛️' },
    { name: '镶嵌耳环', stone: '璀璨', level: 94, place: '香港', emoji: '💎' },
    { name: '珍珠耳线', stone: '灵动', level: 92, place: '上海', emoji: '🎐' },
    { name: '耳骨夹珠', stone: '前卫', level: 91, place: '成都', emoji: '🎧' },
    { name: '螺钿耳坠', stone: '光泽', level: 94, place: '福州', emoji: '🐚' },
    { name: '竹节耳环', stone: '清雅', level: 92, place: '苏州', emoji: '🎋' },
    { name: '双珠耳钉', stone: '成对', level: 93, place: '杭州', emoji: '🫧' },
    { name: '马蹄耳环', stone: '异形', level: 91, place: '大理', emoji: '🐎' }
  ];

  orders: PearlOrderItem[] = [
    { name: '珠宝连锁', region: '华南', count: 600, amount: 1080000, emoji: '💎' },
    { name: '高定品牌', region: '华东', count: 400, amount: 960000, emoji: '👑' },
    { name: '海外买手', region: '海外', count: 350, amount: 875000, emoji: '🌏' },
    { name: '婚庆公司', region: '华南', count: 500, amount: 750000, emoji: '💒' },
    { name: '直播电商', region: '华东', count: 800, amount: 720000, emoji: '📺' },
    { name: '古着店铺', region: '华北', count: 450, amount: 675000, emoji: '🏛️' },
    { name: '设计师品牌', region: '华东', count: 300, amount: 780000, emoji: '🎨' },
    { name: '免税店采购', region: '华南', count: 550, amount: 825000, emoji: '✈️' },
    { name: '博物馆文创', region: '华北', count: 380, amount: 570000, emoji: '🗿' },
    { name: '银楼定制', region: '西南', count: 320, amount: 640000, emoji: '🥈' },
    { name: '影视造型', region: '华北', count: 250, amount: 625000, emoji: '🎬' },
    { name: '老牌珠宝行', region: '华东', count: 420, amount: 1008000, emoji: '⏳' }
  ];

  reviews: PearlReviewItem[] = [
    { 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: 5, 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: 4, 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 getOriginColor(origin: string): string {
  if (origin === '南海' || origin === '加勒比') {
    return '#D98E97';
  } else if (origin === '大溪地' || origin === '日本海') {
    return '#3E5C76';
  } else if (origin === '诸暨' || origin === '海南') {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getLevelColor(level: number): string {
  if (level >= 95) {
    return '#D98E97';
  } else if (level >= 93) {
    return '#3E5C76';
  }
  return '#7BA3A8';
}

function getPriceColor(price: number): string {
  if (price >= 2000) {
    return '#D98E97';
  } else if (price >= 1000) {
    return '#3E5C76';
  } else if (price >= 500) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getOrderColor(amount: number): string {
  if (amount >= 900000) {
    return '#D98E97';
  } else if (amount >= 700000) {
    return '#3E5C76';
  } else if (amount >= 500000) {
    return '#7BA3A8';
  }
  return '#C98A4E';
}

function getReviewTagColor(tag: string): string {
  if (tag === '色泽温润' || tag === '品相一流' || tag === '珠圆玉润' || tag === '细节讲究') {
    return '#3E5C76';
  } else if (tag === '个性十足' || tag === '婚礼首选' || tag === '配色精妙') {
    return '#D98E97';
  } else if (tag === '走量飞快' || tag === '轻盈百搭' || tag === '成色上佳' || tag === '上镜极佳') {
    return '#7BA3A8';
  } else if (tag === '光泽流转') {
    return '#C98A4E';
  }
  return '#9CAAB3';
}

@Entry
@Component
struct PearlApp {
  @State curTab: number = 0;
  @State data: PearlData = new PearlData();
  @State showAddPearl: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteNecklace: boolean = false;
  @State showDetailEarring: boolean = false;
  @State delNecklaceName: string = '';
  @State detailEarringName: string = '';
  @State bubbleFloat: boolean = false;
  @State lusterPulse: 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('Pearl · 沧海月明珠有泪')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🫧')
                .fontSize(18)
                .onClick(() => {
                  this.bubbleFloat = !this.bubbleFloat;
                })
                .translate({ x: 0, y: this.bubbleFloat ? -10 : 4 })
                .animation({ duration: 640, curve: Curve.EaseOut })
              Text('✨')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.lusterPulse = !this.lusterPulse;
                })
                .scale({ x: this.lusterPulse ? 1.4 : 1, y: this.lusterPulse ? 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(PEARL_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(12)
                    .height(12)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(6)
                    .border({ width: 2, color: COLORS.accent })
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'pearl' + 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 === PearlTab.PEARL) {
          PearlContent({ data: this.data, onAdd: () => {
            this.showAddPearl = true;
          } })
        } else if (this.curTab === PearlTab.NECKLACE) {
          NecklaceContent({ data: this.data, onDel: (n: string) => {
            this.delNecklaceName = n;
            this.showDeleteNecklace = true;
          } })
        } else if (this.curTab === PearlTab.EARRING) {
          EarringContent({ data: this.data, onDetail: (n: string) => {
            this.detailEarringName = n;
            this.showDetailEarring = true;
          } })
        } else if (this.curTab === PearlTab.ORDER) {
          PearlOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          PearlReviewContent({ 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(20)
            .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.showAddPearl) {
        AddPearlModal({
          onClose: () => {
            this.showAddPearl = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditPearlOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteNecklace) {
        DeleteNecklaceModal({
          title: this.delNecklaceName, onClose: () => {
            this.showDeleteNecklace = false;
          }
        })
      }
      if (this.showDetailEarring) {
        DetailEarringModal({
          name: this.detailEarringName, onClose: () => {
            this.showDetailEarring = false;
          }
        })
      }
    }
  }
}

@Component
struct PearlTag {
  @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 PearlContent {
  @ObjectLink data: PearlData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(10)
              .height(10)
              .backgroundColor(COLORS.silkWhite)
              .borderRadius(5)
              .border({ width: 2, color: COLORS.accent })
            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.accent })
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 本周珍珠销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六62颗')
              .fontSize(9)
              .fontColor(COLORS.accent)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekPearlMeta) => {
              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: WeekPearlMeta) => w.day)
          }
          .width('100%')
          .height(150)
          .alignItems(VerticalAlign.Bottom)
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.pearls, (p: PearlItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(44)
                .height(44)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getOriginColor(p.origin) + '14')
                .borderRadius(22)
                .border({ width: 1, color: getOriginColor(p.origin) + '66' })
                Row() {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(getOriginColor(p.origin))
                    .borderRadius(4)
                    .margin({ left: 4 })
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.silkWhite)
                    .borderRadius(4)
                    .border({ width: 2, color: getOriginColor(p.origin) })
                    .margin({ left: 4 })
                }
                .width(40)
                .justifyContent(FlexAlign.Center)
              }
              .width(60)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getOriginColor(p.origin) + '0D')
              .borderRadius(30)
              .border({ width: 1, color: getOriginColor(p.origin) + '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() {
                  PearlTag({ text: p.origin, color: getOriginColor(p.origin) })
                  PearlTag({ text: p.shape, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: PearlItem, i: number) => 'pl' + 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 NecklaceContent {
  @ObjectLink data: PearlData;
  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(PEARL_TOP, (t: PearlTopMeta) => {
            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: PearlTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.necklaces, (c: NecklaceItem, i: number) => {
            Row() {
              Column() {
                Text(c.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Row() {
                  Column()
                    .width(10)
                    .height(10)
                    .backgroundColor(getLevelColor(c.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(getLevelColor(c.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(c.level) + '33' })

              Column() {
                Row() {
                  Text(c.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(c.style + '链')
                    .fontSize(9)
                    .fontColor(COLORS.textSecondary)
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  Text('光泽评级')
                    .fontSize(9)
                    .fontColor(COLORS.textHint)
                  Row() {
                    Column()
                      .layoutWeight(c.level)
                      .height(6)
                      .backgroundColor(getLevelColor(c.level))
                      .borderRadius(3)
                    Column()
                      .layoutWeight(100 - c.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(c.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (c: NecklaceItem, i: number) => 'nk' + c.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 EarringContent {
  @ObjectLink data: PearlData;
  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(PEARL_HOT, (d: PearlHotMeta) => {
            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: PearlHotMeta) => d.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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.earrings, (e: EarringItem, i: number) => {
            Row() {
              Column() {
                Text(e.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                Column()
                  .width(14)
                  .height(14)
                  .backgroundColor(getLevelColor(e.level))
                  .borderRadius(7)
                  .margin({ top: 5 })
              }
              .width(56)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getLevelColor(e.level) + '0D')
              .borderRadius(28)
              .border({ width: 1, color: getLevelColor(e.level) + '33' })

              Column() {
                Row() {
                  Text(e.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text('品级' + e.level)
                    .fontSize(9)
                    .fontColor(getLevelColor(e.level))
                    .margin({ left: 8 })
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                Row() {
                  PearlTag({ text: e.stone + '珠', color: COLORS.textSecondary })
                  Text(e.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 })

              Text('👁️')
                .fontSize(14)
                .onClick(() => {
                  this.onDetail(e.name);
                })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (e: EarringItem, i: number) => 'er' + e.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 PearlOrderContent {
  @ObjectLink data: PearlData;
  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(SHAPE_SHARE, (s: ShapeShareMeta) => {
            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: ShapeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(20)
        .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: PearlOrderItem, 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(22)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: PearlOrderItem, 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 PearlReviewContent {
  @ObjectLink data: PearlData;

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

@Component
struct AddPearlModal {
  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('¥1280')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct EditPearlOrderModal {
  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('600 → 700 颗')
            .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('¥1260000')
            .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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DeleteNecklaceModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}

@Component
struct DetailEarringModal {
  @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(22)
      .constraintSize({ maxHeight: '78%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
  }
}
Logo

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

更多推荐