一、引言:鸿蒙生态中的艺术品押运平台技术全景

鸿蒙操作系统(HarmonyOS)自诞生以来,便以"万物互联"为核心理念,构建了一套跨设备、跨场景的分布式应用生态。在 HarmonyOS 6.1.1 这一重要版本中,ArkTS 声明式开发范式已经高度成熟,开发者可以通过一套语言、一套范式,覆盖手机、平板、折叠屏、车机乃至智慧屏等多种终端形态。本文所剖析的"艺境押运 ART GUARD"平台,正是运行于 HarmonyOS 6.1.1 之上、基于 HarmonyOS ArkTS API 24 开发的专业级艺术品押运与拍卖应用。该平台面向油画、雕塑、古董、装置艺术品等高端艺术品的专业押运场景,融合恒温恒湿舱监控、防震气垫悬浮、安保押运等级管理等专业能力,同时整合艺术品拍卖竞价与展览物流追踪,形成了一个完整的艺术品流通管理闭环。从技术维度看,这个项目充分展现了 ArkTS 语言在类型系统、装饰器元编程、状态驱动渲染等方面的工程实践能力,是研究 HarmonyOS API 24 声明式 UI 架构的优质样本。

在这里插入图片描述

ArkTS 作为鸿蒙生态的首选开发语言,在 TypeScript 的基础上进行了深度定制与扩展。它保留了 TypeScript 的静态类型检查能力,同时引入了 @Entry@Component@State@Observed@Builder 等一系列装饰器,使开发者能够以纯声明式的方式描述 UI 结构与状态绑定关系。在 ArkTS API 24 中,声明式 UI 范式已经涵盖了从基础布局组件(Column、Row、Stack、Scroll)到高级交互组件(TextInput、TextArea、ForEach)的完整能力集。本项目中,@Entry 装饰的主页面 ArtGuardApp 作为应用入口,通过 @State 管理全局活动 Tab 索引、搜索关键词、未读消息数等响应式状态,再通过条件渲染将不同的 @Component 子组件(ShipContentAuctionContentGalleryContentExhibitContentProfileContent)动态切换到内容区域。这种"状态驱动—组件拆分—条件路由"的架构模式,正是 ArkTS 声明式范式的核心精髓所在,也是 HarmonyOS 6.1.1 应用开发的标准范式。

声明式 UI 范式的本质优势在于"数据即视图"——开发者只需描述界面在任何给定状态下的样子,框架自动负责状态变化后的 UI 差异更新。在本项目中,这一理念体现在多个层面:底部 Tab 栏通过 this.activeTab === tab 的条件判断,自动控制图标透明度、字重和金色指示条的显示与隐藏;押运单列表项通过 SHIP_STATUS_CONFIG[s.status] 映射,将订单状态字符串自动转换为对应的标签、文字颜色和背景色;出价弹框中,用户点击加价步进按钮后,@State bidPrice 状态更新,弹框内的出价输入框文本自动同步刷新。这种状态与视图的自动绑定,极大降低了开发者手动操作 DOM 的心智负担,使代码逻辑更加聚焦于业务本身。在 HarmonyOS ArkTS API 24 中,这一机制通过精细的依赖追踪和差分渲染算法实现,确保只有真正受影响的 UI 片段才会被重新构建,性能表现优异。

状态管理模型是任何声明式 UI 框架的核心基础设施。ArkTS API 24 提供了分层的状态管理体系:@State 用于组件内部私有状态,@Prop 用于父到子的单向同步,@Link 用于父子双向同步,@Observed + @ObjectLink 用于跨组件嵌套对象的深度观测。在本项目中,@State 被广泛用于管理各 Tab 页面的表单状态、弹框显隐布尔值、用户选择结果等;@Observed 装饰器则应用于 ArtShipItemAuctionItemCollectionItemExhibitionItem 四个数据类,使它们的属性变化能够被框架追踪。特别值得关注的是,每个 @Observed 类都通过 implements 关键字实现了对应的 interface 接口(如 ArtShipItem implements ArtShipModel),这种"接口定义契约 + 观测类实现数据"的双层设计,既保证了类型安全,又实现了状态可观测,是 ArkTS 工程化实践中的优秀范式。

从艺术品押运平台的业务架构来看,整个应用围绕"艺术品流通全链路"展开,划分为五大功能模块:押运管理(SHIP)负责艺术品运输订单的全生命周期跟踪,包含恒温恒湿环境实时监控、防震气垫舱状态、押运等级分类;拍卖管理(AUCTION)提供在线竞价平台,支持出价步进、撤回拍品、出价热度可视化;藏品管理(GALLERY)以美术馆展签风格展示藏品档案,支持档案编辑与稀有度分级;展览管理(EXHIBIT)以时间线形式追踪展览的布展、展出、撤展全流程;个人中心(PROFILE)汇总用户押运、拍卖、估值等数据统计。这五大模块通过底部 Tab 栏统一调度,各模块内部又通过 @Builder 方法将 UI 片段模块化拆分,形成"入口页面—功能模块—Builder 片段—弹框交互"的四级层次结构。此外,全应用覆盖一层金色光尘粒子特效,在视觉层面呼应了美术馆极简黑白金的设计语言,展现了 ArkTS 动画与渲染能力的工程化运用。

二、应用整体架构流程图

数据层

弹框交互层 @Builder

功能模块层 @Component

UI 构建层 @Builder

状态管理层

@Entry 入口层

activeTab === SHIP

activeTab === AUCTION

activeTab === GALLERY

activeTab === EXHIBIT

activeTab === PROFILE

ArtGuardApp 主页面

@State activeTab
Tab 索引

@State searchKeyword
搜索关键词

@State unreadCount
未读消息

@State dustX/Y/S/O
粒子参数

headerBuilder
头部+品类药丸

contentArea
内容区路由

tabBarBuilder
底部Tab栏

dustLayerBuilder
金色光尘粒子

ShipContent
押运管理

AuctionContent
拍卖竞价

GalleryContent
藏品档案

ExhibitContent
展览时间线

ProfileContent
个人中心

bookModal
预约押运表单

bidModal
出价弹框

withdrawModal
撤回拍品

editModal
编辑藏品档案

@Observed 数据类
ArtShipItem 等

Record 配置映射
SHIP_STATUS_CONFIG 等

静态 Mock 数据
mockShips 等

辅助纯函数
formatWan 等

三、逐段代码深度解析

3.1 颜色常量体系:美术馆极简黑白金设计语言

const COLOR_BG: string = '#F8F7F4'           // 画廊米白背景
const COLOR_CARD: string = '#FFFFFF'         // 卡片白
const COLOR_DARK: string = '#1A1A1A'         // 墨黑(深卡/主色)
const COLOR_GOLD: string = '#B8935A'         // 香槟金
const COLOR_GOLD_LIGHT: string = '#E8D5B5'   // 浅金
const COLOR_PURPLE: string = '#6B5B95'       // 艺术紫
const COLOR_PURPLE_LIGHT: string = '#C5B8E0' // 浅紫
const COLOR_TEXT_MAIN: string = '#1A1A1A'    // 文字主
const COLOR_TEXT_SUB: string = '#6E6A63'     // 文字次
const COLOR_TEXT_HINT: string = '#B5B0A8'    // 文字提示
const COLOR_BORDER: string = '#E5E2DC'       // 边框
const COLOR_SUCCESS: string = '#5A7D5A'      // 成功
const COLOR_WARNING: string = '#C49A3C'      // 警告
const COLOR_DANGER: string = '#A63D40'       // 危险

在这里插入图片描述

本项目在文件最顶部定义了一套完整的颜色常量体系,共计十四个颜色值,构成了整个应用视觉设计的基础语言。这套色彩体系以"美术馆极简黑白金"为核心理念:COLOR_BG 采用 #F8F7F4 画廊米白作为全局背景,模拟美术馆墙面的温润质感;COLOR_DARK 使用 #1A1A1A 墨黑作为深色卡片和主交互色,呼应美术馆中黑色展墙的庄重感;COLOR_GOLD#B8935A 香槟金作为品牌强调色,贯穿于按钮高亮、数值标识、装饰线条等场景,传递高端艺术品行业的奢华气质。辅助色方面,COLOR_PURPLE 艺术紫用于稀有度"珍品"标签和总估值数据,COLOR_SUCCESSCOLOR_WARNINGCOLOR_DANGER 分别用于已交付、即将结拍、撤回拍品等语义化状态标识。在 ArkTS API 24 中,将颜色定义为顶层 const 常量,不仅保证了全应用色彩一致性,还使得主题切换变得可行——只需修改常量值即可全局生效。这种设计模式避免了在数百处 UI 代码中硬编码颜色字符串,大幅提升了可维护性,是鸿蒙应用工程化的基本实践。

3.2 数据模型接口定义:interface 契约与类型安全

interface ArtShipModel {
  id: number;
  orderNo: string;
  artName: string;
  pickup: string;
  destination: string;
  value: number;
  status: string;
  date: string;
}

interface AuctionItemModel {
  id: number;
  lotNo: string;
  name: string;
  artist: string;
  startPrice: number;
  currentPrice: number;
  bidCount: number;
  endTime: string;
  color: string;
}

interface CollectionModel {
  id: number;
  name: string;
  artist: string;
  era: string;
  value: number;
  rarity: string;
  color: string;
}

interface ExhibitionModel {
  id: number;
  name: string;
  venue: string;
  startDate: string;
  endDate: string;
  artworkCount: number;
  status: string;
}

在这里插入图片描述

本项目定义了四个核心数据模型接口:ArtShipModel(押运单模型)、AuctionItemModel(拍卖拍品模型)、CollectionModel(藏品模型)和 ExhibitionModel(展览模型)。在 ArkTS 语言体系中,interface 关键字用于声明纯数据结构的类型契约,它不包含实现逻辑,仅定义属性名与类型。这种设计模式的优势在于"契约先行"——先定义数据应该长什么样,再通过具体类去实现。例如 ArtShipModel 定义了押运单需要的八个字段:订单号 orderNo、艺术品名 artName、出发地 pickup、目的地 destination、估值 value、状态 status、日期 date,以及唯一标识 id。每个字段的类型都被严格声明为 numberstring,编译器会在类型不匹配时报错。AuctionItemModel 额外包含了起拍价 startPrice、当前价 currentPrice、出价次数 bidCount、结拍时间 endTime 以及用于卡片配色的 color 字段。CollectionModel 引入了 era(年代)和 rarity(稀有度)字段,体现藏品管理的专业维度。ExhibitionModel 则包含场馆 venue、展品数量 artworkCount 等展览特有属性。这些接口为后续的 @Observed 数据类提供了类型约束基础,是整个应用类型安全体系的第一道防线。

3.3 @Observed 装饰器与可观测数据类的实现

@Observed
class ArtShipItem implements ArtShipModel {
  id: number = 0;
  orderNo: string = '';
  artName: string = '';
  pickup: string = '';
  destination: string = '';
  value: number = 0;
  status: string = '待包装';
  date: string = '';
  constructor(id: number, orderNo: string, artName: string, pickup: string,
    destination: string, value: number, status: string, date: string) {
    this.id = id; this.orderNo = orderNo; this.artName = artName
    this.pickup = pickup; this.destination = destination
    this.value = value; this.status = status; this.date = date
  }
}

@Observed
class AuctionItem implements AuctionItemModel {
  id: number = 0;
  lotNo: string = '';
  name: string = '';
  artist: string = '';
  startPrice: number = 0;
  currentPrice: number = 0;
  bidCount: number = 0;
  endTime: string = '';
  color: string = '#B8935A';
  constructor(id: number, lotNo: string, name: string, artist: string,
    startPrice: number, currentPrice: number, bidCount: number, endTime: string, color: string) {
    this.id = id; this.lotNo = lotNo; this.name = name; this.artist = artist
    this.startPrice = startPrice; this.currentPrice = currentPrice
    this.bidCount = bidCount; this.endTime = endTime; this.color = color
  }
}

在这里插入图片描述

@Observed 是 ArkTS API 24 中用于创建可观测数据类的装饰器。被 @Observed 装饰的类,其实例的属性变化可以被 ArkTS 框架的状态管理系统追踪到,从而在属性变更时自动触发关联 UI 的重新渲染。在本项目中,四个数据类 ArtShipItemAuctionItemCollectionItemExhibitionItem 都使用了 @Observed 装饰,并同时通过 implements 关键字实现了上一步定义的接口。以 ArtShipItem 为例,它在类内部为每个属性提供了默认初始值(如 status: string = '待包装'),这保证了即使构造函数未完整赋值,对象也处于合法状态。构造函数 constructor 接收所有参数并进行批量赋值,使得创建实例时可以一行完成。这种"接口定义契约 + @Observed 类实现可观测 + 构造函数便捷实例化"的三层设计,是 ArkTS 状态管理的标准工程模式。值得注意的是,AuctionItemcolor 属性默认值为 '#B8935A'(即 COLOR_GOLD),这意味着即使创建拍品时未指定配色,也会自动回退到品牌金色,体现了防御性编程思想。当这些 @Observed 实例在 ForEach@Builder 中被使用时,其属性变化(如拍卖出价更新 currentPrice)将自动驱动 UI 刷新。

3.4 配置接口与 Record 映射:状态元数据的集中管理

interface ShipStatusMeta {
  label: string;
  color: string;
  bg: string;
}

interface PackMeta {
  label: string;
  icon: string;
  desc: string;
}

interface GuardLevelMeta {
  label: string;
  icon: string;
  color: string;
}

const SHIP_STATUS_CONFIG: Record<string, ShipStatusMeta> = {
  '待包装': { label: '待包装', color: COLOR_TEXT_SUB, bg: '#EFEDF7' },
  '押运中': { label: '押运中', color: COLOR_GOLD, bg: '#F4EBDD' },
  '已交付': { label: '已交付', color: COLOR_SUCCESS, bg: '#E9F0E9' }
}

const PACK_CONFIG: Record<string, PackMeta> = {
  '软包': { label: '软包', icon: '📦', desc: '软性无酸材料包裹' },
  '木箱': { label: '木箱', icon: '🪵', desc: '定制实木防潮箱体' },
  '气垫箱': { label: '气垫箱', icon: '🛡️', desc: '悬浮气垫减震系统' },
  '恒温箱': { label: '恒温箱', icon: '🌡️', desc: '主动恒温恒湿舱' }
}

const GUARD_LEVEL_CONFIG: Record<string, GuardLevelMeta> = {
  '标准': { label: '标准', icon: '◇', color: COLOR_TEXT_SUB },
  '安保': { label: '安保', icon: '◈', color: COLOR_GOLD },
  '武装': { label: '武装', icon: '◆', color: COLOR_DANGER }
}

在这里插入图片描述

本段代码展示了 ArkTS 中"配置映射"模式的优雅实现。首先定义了 ShipStatusMetaCategoryMetaPackMetaGuardLevelMetaRarityMetaExhibitStatusMeta 等元数据接口,每个接口描述了对应配置项所需的属性集合。随后,使用 Record<string, XxxMeta> 类型创建了六个配置映射表:SHIP_STATUS_CONFIG 将押运状态字符串映射为标签、文字色、背景色三元组;PACK_CONFIG 将包装方式映射为标签、图标、描述;GUARD_LEVEL_CONFIG 将押运等级映射为标签、图标符号(◇/◈/◆ 递进)和颜色。这种设计模式的核心优势在于"以查表代替分支"——在 UI 渲染时,只需通过 SHIP_STATUS_CONFIG[s.status]?.label 即可获取状态标签,无需编写冗长的 if-elseswitch 分支。Record 是 ArkTS 内置类型,等价于 { [key: string]: ValueType },使用它可以让编译器在访问映射表时进行类型检查。配置中还体现了业务语义的分层:押运等级从"标准"到"安保"再到"武装",图标符号的视觉重量逐级递增,颜色从灰到金再到危险红,完美呼应了安保等级的递进关系。这是将业务领域知识编码进类型系统的优秀实践。

3.5 静态 Mock 数据与辅助纯函数

const mockShips: ArtShipItem[] = [
  new ArtShipItem(1, 'AG-2608-1201', '《暮色中的塞纳河》布面油画', '上海西岸艺术中心', '北京798艺术区', 380, '押运中', '08-21'),
  new ArtShipItem(2, 'AG-2608-1202', '青铜饕餮纹爵(西周)', '陕西历史博物馆库房', '香港中环画廊', 1200, '已交付', '08-15'),
  new ArtShipItem(3, 'AG-2608-1203', '《山魂》水墨长卷', '杭州南山路画院', '深圳湾一号美术馆', 260, '待包装', '08-24'),
  new ArtShipItem(4, 'AG-2608-1204', '《凝视》大理石雕塑', '佛罗伦萨工作室', '上海外源美术馆', 890, '押运中', '08-22'),
]

const monthlyShipValues: number[] = [120, 260, 180, 340, 300, 420]
const monthlyLabels: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const monthlyMax: number = 420

function formatWan(v: number): string {
  return '¥' + v.toString() + '万'
}

function getBarHeight(v: number, max: number): number {
  return Math.round(v / max * 96)
}

function getBidHeatWidth(count: number): number {
  return Math.round(count / getMaxBidCount() * 100)
}

在这里插入图片描述

本段定义了应用的静态数据源和辅助纯函数。mockShips 数组通过连续调用 new ArtShipItem(...) 构造函数创建了八条押运单数据,覆盖油画、青铜器、水墨、雕塑、装置、摄影、唐三彩等多种艺术品类型,出发地和目的地涵盖上海、北京、杭州、深圳、佛罗伦萨、东京、新加坡等国内外艺术重镇,估值从 75 万到 1200 万不等,状态覆盖"待包装"“押运中”"已交付"全流程。这种 Mock 数据设计既服务于 UI 开发调试,又隐含了业务领域模型的完整画像。辅助函数方面,formatWan 将数字格式化为"¥380万"形式的估值展示字符串;getBarHeight 接收数值和最大值,计算柱状图高度(百分比映射到 96 像素最大高度);getBidHeatWidth 将出价次数映射为进度条宽度百分比。这些函数均为纯函数——输入相同则输出相同,无副作用,易于测试和推理。在 HarmonyOS ArkTS API 24 中,纯函数配合声明式 UI 使用时,框架会在状态变化时自动重新调用它们刷新计算结果,不会产生不必要的状态污染。将数据格式化逻辑抽取为独立函数而非内联在 UI 代码中,遵循了关注点分离原则。

3.6 Tab 枚举与 @Entry 主页面入口

enum ArtTab {
  SHIP = 0,
  AUCTION = 1,
  GALLERY = 2,
  EXHIBIT = 3,
  PROFILE = 4
}

@Entry
@Component
struct ArtGuardApp {
  @State activeTab: ArtTab = ArtTab.SHIP
  @State selectedCategory: string = '全部'
  @State searchKeyword: string = ''
  @State unreadCount: number = 3
  @State dustX: number[] = [12, 78, 33, 90, 55, 20, 66, 44, 82]
  @State dustY: number[] = [15, 40, 70, 25, 88, 55, 10, 62, 34]
  @State dustS: number[] = DUST_SIZE_INIT
  @State dustO: number[] = DUST_OPACITY_INIT
  private timerId: number = -1

在这里插入图片描述

ArtTab 枚举定义了应用的五个功能模块索引,使用枚举而非魔法数字(0/1/2/3/4)使得代码语义清晰,Tab 切换逻辑可读性强。@Entry 装饰器标记 ArtGuardApp 为应用入口组件,@Component 声明它是一个自定义组件。在 ArtGuardApp 内部,@State 装饰器管理了应用级的响应式状态:activeTab 记录当前激活的 Tab,初始值为 ArtTab.SHIPselectedCategory 管理品类筛选状态,初始为"全部";searchKeyword 绑定搜索框输入;unreadCount 管理未读消息角标数字;dustXdustYdustSdustO 四个数组分别存储九个金色光尘粒子的横坐标、纵坐标、字号和透明度。在 ArkTS API 24 中,@State 装饰的变量一旦被赋值修改,框架会自动检测哪些 UI 片段依赖了这个变量,并只重新构建那些受影响的片段。private timerId 用于保存粒子动画定时器的 ID,它不使用 @State 是因为定时器 ID 不需要驱动 UI 更新。这种对"哪些状态需要响应式、哪些不需要"的精确划分,是 ArkTS 性能优化的关键意识——不必要的 @State 会增加框架的依赖追踪开销。

3.7 生命周期回调与金色光尘粒子动画引擎

aboutToAppear() {
  this.timerId = setInterval(() => {
    const nx: number[] = []
    const ny: number[] = []
    for (let i = 0; i < DUST_COUNT; i++) {
      let y: number = this.dustY[i] - 0.5 - (i % 3) * 0.15
      if (y < -6) {
        y = 104
      }
      nx.push(this.dustX[i] + Math.sin(Date.now() / 2400 + i * 1.7) * 0.35)
      ny.push(y)
    }
    this.dustX = nx
    this.dustY = ny
  }, 320)
}

aboutToDisappear() {
  if (this.timerId >= 0) {
    clearInterval(this.timerId)
    this.timerId = -1
  }
}

@Builder dustLayerBuilder() {
  Column() {
    ForEach(this.dustX, (x: number, i: number) => {
      Text('✦')
        .fontSize(this.dustS[i])
        .fontColor(i % 2 === 0 ? COLOR_GOLD : COLOR_GOLD_LIGHT)
        .opacity(this.dustO[i])
        .position({ x: this.dustX[i] + '%', y: this.dustY[i] + '%' })
    })
  }
  .width('100%')
  .height('100%')
  .hitTestBehavior(HitTestMode.None)
}

在这里插入图片描述

本段代码实现了应用全局的金色光尘粒子特效,是 ArkTS 动画能力的精彩展现。aboutToAppear 是 ArkTS 组件生命周期回调,在组件出现前被调用,此处用于启动粒子动画定时器。setInterval 每 320 毫秒执行一次回调,在回调内部遍历九个粒子,对每个粒子的纵坐标 y 做递减运算(-0.5 - (i % 3) * 0.15),不同粒子以略微不同的速度上升,制造参差感;当粒子飘出顶部(y < -6)时重置到底部(y = 104),形成循环。横坐标则通过 Math.sin(Date.now() / 2400 + i * 1.7) * 0.35 加入正弦波摆动,使粒子在上升过程中有轻微的水平飘移,模拟空气中尘埃的自然运动。每次计算完成后,将新坐标数组整体赋值给 this.dustXthis.dustY,触发 @State 状态更新,驱动 dustLayerBuilder 中的 ForEach 重新渲染。dustLayerBuilder 使用 Text('✦') 星形字符作为粒子,奇偶索引交替使用 COLOR_GOLDCOLOR_GOLD_LIGHT 两种金色,通过 .position() 以百分比定位在屏幕上。关键的是 .hitTestBehavior(HitTestMode.None)——这使粒子层不拦截任何触摸事件,让用户可以穿透粒子层与下方 UI 正常交互。aboutToDisappear 中清理定时器,防止内存泄漏。

3.8 头部构建器:搜索栏与品类药丸的声明式组合

@Builder headerBuilder() {
  Column() {
    Row() {
      Column() {
        Text('艺境押运')
          .fontSize(20)
          .fontColor(COLOR_TEXT_MAIN)
        Text('ART GUARD')
          .fontSize(9)
          .fontColor(COLOR_GOLD)
          .fontWeight(FontWeight.Medium)
          .letterSpacing(3)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)

      Row() {
        Text('🔍').fontSize(12).margin({ left: 10 })
        TextInput({ placeholder: '搜索艺术品 / 拍品 / 展览' })
          .placeholderColor(COLOR_TEXT_HINT)
          .placeholderFont({ size: 11 })
          .fontSize(11)
          .fontColor(COLOR_TEXT_MAIN)
          .backgroundColor(Color.Transparent)
          .padding({ left: 4, right: 4 })
          .layoutWeight(1)
          .onChange((v: string) => { this.searchKeyword = v })
      }
      .layoutWeight(1)
      .height(34)
      .margin({ left: 14 })
      .backgroundColor(COLOR_CARD)
      .borderRadius(17)
      .border({ width: 1, color: COLOR_BORDER })
      .alignItems(VerticalAlign.Center)

      Stack({ alignContent: Alignment.Center }) {
        Text('✉').fontSize(19).fontColor(COLOR_TEXT_MAIN)
        if (this.unreadCount > 0) {
          Text(this.unreadCount.toString())
            .fontSize(8)
            .fontColor(COLOR_CARD)
            .backgroundColor(COLOR_GOLD)
            .borderRadius(7)
            .padding({ left: 3, right: 3, top: 1, bottom: 1 })
            .offset({ x: 8, y: -8 })
        }
      }
      .width(34)
      .height(34)
      .margin({ left: 10 })
      .backgroundColor(COLOR_CARD)
      .borderRadius(17)
      .border({ width: 1, color: COLOR_BORDER })
      .onClick(() => { this.unreadCount = 0 })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 14, bottom: 12 })
    .alignItems(VerticalAlign.Center)

    Scroll() {
      Row() {
        ForEach(HEADER_CATEGORIES, (cat: string) => {
          if (this.selectedCategory === cat) {
            Text(cat === '全部' ? '◈ 全部' : ((ART_CATEGORY_CONFIG[cat]?.icon ?? '') + ' ' + cat))
              .fontSize(11)
              .fontColor(COLOR_CARD)
              .backgroundColor(COLOR_DARK)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(14)
              .margin({ left: 4, right: 4 })
          } else {
            Text(cat === '全部' ? '◇ 全部' : ((ART_CATEGORY_CONFIG[cat]?.icon ?? '') + ' ' + cat))
              .fontSize(11)
              .fontColor(COLOR_TEXT_SUB)
              .backgroundColor(COLOR_CARD)
              .border({ width: 1, color: COLOR_BORDER })
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(14)
              .margin({ left: 4, right: 4 })
              .onClick(() => { this.selectedCategory = cat })
          }
        })
      }
      .padding({ left: 12, right: 12 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .height(36)
    .width('100%')
  }
  .width('100%')
  .backgroundColor(COLOR_BG)
}

在这里插入图片描述

headerBuilder 是应用头部的完整构建器,展示了 ArkTS 声明式 UI 中组件嵌套组合的典型模式。头部分为两层:上层是品牌标识 + 搜索栏 + 消息图标的水平排列 Row,下层是可横向滚动的品类药丸筛选条。品牌标识用中英双语 Text 纵向排列,英文 “ART GUARD” 使用 letterSpacing(3) 增加字间距营造高端品牌感。搜索栏由 Row 包裹搜索图标和 TextInput 组成,TextInput 通过 .layoutWeight(1) 占满剩余宽度,.onChange 回调将输入值同步到 @State searchKeyword。消息图标使用 Stack 叠加信封字符和未读数角标,角标通过 if (this.unreadCount > 0) 条件渲染控制显隐,.offset({ x: 8, y: -8 }) 将角标偏移到右上角,点击后 this.unreadCount = 0 清零。品类药丸条使用 Scroll + Row + ForEach 组合实现横向滚动,每个药丸根据 this.selectedCategory === cat 条件分支渲染选中态(墨黑底白字、 实心符号)和未选中态(白底灰字带边框、 空心符号),点击未选中药丸时更新 selectedCategory。整个头部通过 @Builder 方法封装,可在 build() 中以 this.headerBuilder() 一行调用,实现了 UI 片段的模块化复用。

3.9 底部 Tab 栏与内容区条件路由

@Builder contentArea() {
  Column() {
    if (this.activeTab === ArtTab.SHIP) {
      ShipContent()
    } else if (this.activeTab === ArtTab.AUCTION) {
      AuctionContent()
    } else if (this.activeTab === ArtTab.GALLERY) {
      GalleryContent()
    } else if (this.activeTab === ArtTab.EXHIBIT) {
      ExhibitContent()
    } else {
      ProfileContent()
    }
  }
  .layoutWeight(1)
}

@Builder bottomTabItem(icon: string, label: string, tab: ArtTab) {
  Column() {
    Text(icon)
      .fontSize(19)
      .opacity(this.activeTab === tab ? 1.0 : 0.4)
    Text(label)
      .fontSize(9)
      .fontColor(this.activeTab === tab ? COLOR_TEXT_MAIN : COLOR_TEXT_HINT)
      .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
      .margin({ top: 2 })
    if (this.activeTab === tab) {
      Column()
        .width(16)
        .height(2)
        .backgroundColor(COLOR_GOLD)
        .borderRadius(1)
        .margin({ top: 3 })
    }
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 6, bottom: 6 })
  .onClick(() => { this.activeTab = tab })
}

@Builder tabBarBuilder() {
  Column() {
    Column().width('100%').height(1).backgroundColor(COLOR_BORDER)
    Row() {
      this.bottomTabItem('🚛', '押运', ArtTab.SHIP)
      this.bottomTabItem('🔨', '拍卖', ArtTab.AUCTION)
      this.bottomTabItem('🖼️', '藏品', ArtTab.GALLERY)
      this.bottomTabItem('🏛️', '展览', ArtTab.EXHIBIT)
      this.bottomTabItem('👤', '我的', ArtTab.PROFILE)
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
    .padding({ top: 2, bottom: 4 })
  }
  .width('100%')
  .backgroundColor(COLOR_CARD)
}

在这里插入图片描述

内容区 contentArea 是整个应用的路由核心。它通过 if-else if-else 链式条件判断 this.activeTab 的值,动态渲染对应的 @Component 子组件。在 ArkTS API 24 中,条件渲染不仅控制组件的创建与销毁,还涉及到状态的保存与恢复——当用户从押运页切换到拍卖页再切回时,ShipContent 会被重新创建,其内部 @State 状态会重置为初始值。bottomTabItem 是一个参数化 @Builder,接收图标、标签、Tab 枚举三个参数,通过 this.activeTab === tab 三元表达式动态设置图标透明度(1.0 vs 0.4)、文字颜色(主色 vs 提示色)、字重(Bold vs Normal),选中态额外渲染一条金色指示条。tabBarBuilder 将五个 bottomTabItem 横向排列,顶部加一条分隔线。点击任一 Tab 项时,this.activeTab = tab 触发状态更新,框架自动重新评估 contentArea 的条件链和所有 bottomTabItem 的三元表达式,实现页面切换和 Tab 高亮的同步刷新。这种"单一状态源驱动多组件协同更新"的模式,正是声明式 UI 的核心力量所在。

3.10 Stack 布局与 build 主入口:分层叠加的视图架构

build() {
  Stack() {
    Column() {
      this.headerBuilder()
      this.contentArea()
      this.tabBarBuilder()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLOR_BG)

    this.dustLayerBuilder()
  }
  .width('100%')
  .height('100%')
}

build 方法是 @Entry 组件的渲染入口,它使用 Stack 布局将两层视图叠加在一起。底层是一个 Column,纵向排列头部、内容区和底部 Tab 栏三个 @Builder 片段,占据整个屏幕并设置画廊米白背景。顶层是 dustLayerBuilder 粒子层,通过 Stack 的层叠特性覆盖在所有 UI 之上。Stack 是 ArkTS 中用于实现 Z 轴层叠布局的核心容器,后声明的子元素自动位于先声明的子元素之上。这种设计使得金色光尘粒子可以飘浮在所有功能模块之上,营造美术馆空间中的微尘光影氛围。同时,由于粒子层设置了 hitTestBehavior(HitTestMode.None),它不会拦截触摸事件,用户可以正常操作下方 UI。整个 build 方法仅十余行代码,却通过 @Builder 的组合调用和 Stack 的层叠布局,构建出了"头部—内容—底栏"纵向三段式加"粒子层"全屏叠加的完整应用框架。这种简洁的顶层结构是 ArkTS 声明式范式的典型体现——复杂的应用界面通过 Builder 拆分和组件组合,最终汇聚为清晰的声明式描述。

3.11 押运弹框实现:遮罩层与表单的分层构建

@Builder modalOverlay(onClose: () => void) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(26,26,26,0.55)')
    .onClick(onClose)
}

@Builder bookModal() {
  Column() {
    this.modalOverlay(() => { this.showBookModal = false })
    Column() {
      Column()
        .width('100%')
        .height(3)
        .backgroundColor(COLOR_GOLD)
        .borderRadius({ topLeft: 16, topRight: 16 })

      Row() {
        Text('🖼️ 预约艺术品押运')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
        Column().layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(COLOR_TEXT_HINT)
          .padding(6)
          .onClick(() => { this.showBookModal = false })
      }
      .width('100%')
      .padding({ left: 18, right: 14, top: 14, bottom: 10 })

      Scroll() {
        Column() {
          Text('艺术品名').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 8 })
          TextInput({ placeholder: '如:《暮色中的塞纳河》' })
            .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
            .width('100%').height(38)
            .backgroundColor(COLOR_BG)
            .borderRadius(6)
            .border({ width: 1, color: COLOR_BORDER })
            .margin({ top: 5 })
            .onChange((v: string) => { this.formArtName = v })
          // ... 更多表单字段
        }
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      Row() {
        Text('取消')
          .fontSize(13).fontColor(COLOR_TEXT_SUB)
          .backgroundColor(COLOR_BG)
          .border({ width: 1, color: COLOR_BORDER })
          .borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.showBookModal = false })
        Text('确认押运')
          .fontSize(13).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_DARK)
          .borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => {
            this.bookResult = this.formArtName !== '' ? this.formArtName : '未命名艺术品'
            this.showBookModal = false
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 12, bottom: 16 })
    }
    .width('90%')
    .height('75%')
    .backgroundColor(COLOR_CARD)
    .borderRadius(16)
    .alignItems(HorizontalAlign.Center)
    .position({ x: '5%', y: '11%' })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

本段代码展示了 ArkTS 中弹框(Modal)的完整实现模式。弹框由两层构成:外层 modalOverlay 是半透明遮罩层,使用 rgba(26,26,26,0.55) 黑色半透明背景覆盖全屏,点击遮罩区域触发 onClose 回调关闭弹框;内层是实际的弹框卡片,通过 .position({ x: '5%', y: '11%' }).width('90%') .height('75%') 精确定位在屏幕中央偏上位置。弹框顶部有一条 3 像素高的金色顶线,是高端表单的视觉签名。标题行包含标题文字、弹性占位 Column().layoutWeight(1) 和关闭按钮,形成左标题右关闭的经典布局。表单主体使用 Scroll 包裹,内部纵向排列艺术品名、品类选择药丸、估值输入、包装方式选择、押运等级选择、出发地、目的地、预约时间、备注等多个表单字段。每个 TextInput 都通过 .onChange 将输入值同步到对应的 @State 变量。底部操作区使用 Row 横向排列"取消"和"确认押运"两个按钮,确认时将表单中填写的艺术品名存入 bookResult 状态并关闭弹框。整个弹框通过 if (this.showBookModal) { this.bookModal() } 条件渲染控制显隐,.zIndex(999) 确保弹框层级最高。

3.12 押运环境监控大卡:反色深黑布局与指标网格

@Builder envMetricBuilder(icon: string, label: string, value: string, unit: string) {
  Column() {
    Text(icon).fontSize(15)
    Text(label).fontSize(9).fontColor('#8A857C').margin({ top: 4 })
    Row() {
      Text(value).fontSize(22).fontColor(COLOR_GOLD)
      Text(unit).fontSize(10).fontColor(COLOR_GOLD_LIGHT).margin({ left: 2, bottom: 3 })
    }
    .alignItems(VerticalAlign.Bottom)
    .margin({ top: 4 })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 12, bottom: 12 })
}

// 调用处
Column() {
  Row() {
    Column()
      .width(7).height(7)
      .borderRadius(4)
      .backgroundColor(COLOR_SUCCESS)
    Text('押运环境实时监控')
      .fontSize(12)
      .fontColor(COLOR_GOLD_LIGHT)
      .fontWeight(FontWeight.Medium)
      .margin({ left: 7 })
    Column().layoutWeight(1)
    Text('AG-2608-1201')
      .fontSize(9)
      .fontColor('#8A857C')
      .letterSpacing(1)
  }

  Row() {
    this.envMetricBuilder('🌡️', '舱内温度', '20.0', '℃')
    Column().width(1).height(40).backgroundColor('#33322E')
    this.envMetricBuilder('💧', '舱内湿度', '55', '%RH')
  }

  Row() {
    this.envMetricBuilder('📳', '震动加速度', '0.02', 'G')
    Column().width(1).height(40).backgroundColor('#33322E')
    this.envMetricBuilder('🛰️', '定位状态', '锁定', '')
  }

  Row() {
    Column().width(10).height(1).backgroundColor(COLOR_GOLD)
    Text('恒温恒湿舱 · 气垫悬浮 · 全程安保在岗')
      .fontSize(9)
      .fontColor('#8A857C')
      .margin({ left: 6 })
  }
}
.width('100%')
.backgroundColor(COLOR_DARK)
.borderRadius(14)

押运环境监控大卡是整个应用中最具专业感的 UI 模块之一。它采用 COLOR_DARK 墨黑背景,与画廊米白主背景形成反色对比,模拟美术馆中深色展示面板的视觉效果。卡片顶部左侧有一个 COLOR_SUCCESS 绿色圆点(模拟实时在线指示灯),旁配"押运环境实时监控"标题和当前追踪的订单号。核心指标区使用 envMetricBuilder 参数化 Builder 构建,每个指标格包含图标、标签、数值和单位四部分。数值使用 22 像素金色大字突出展示,单位用 10 像素浅金小字底部对齐。四项指标两两一行排列——温度 20.0℃、湿度 55%RH、震动加速度 0.02G、定位状态"锁定"——直接反映艺术品押运中对恒温恒湿、防震、定位追踪的专业要求。指标之间用 1 像素宽的 #33322E 深灰竖线分隔,底部用金色短线引出"恒温恒湿舱 · 气垫悬浮 · 全程安保在岗"的说明文案。这个大卡通过参数化 Builder 实现了指标格的复用,仅需传入不同的图标、标签、数值和单位参数即可生成统一的指标展示单元,是 ArkTS @Builder 参数化复用的典型案例。

3.13 月度押运估值柱状图:linearGradient 渐变与 ForEach 渲染

Column() {
  Text('月度押运估值(万元)')
    .fontSize(14)
    .fontWeight(FontWeight.Bold)
    .fontColor(COLOR_TEXT_MAIN)
    .width('100%')
    .margin({ bottom: 12 })

  Row() {
    ForEach(monthlyShipValues, (v: number, i: number) => {
      Column() {
        Text(v.toString())
          .fontSize(9)
          .fontColor(COLOR_GOLD)
          .margin({ bottom: 4 })
        Column()
          .width(24)
          .height(getBarHeight(v, monthlyMax))
          .borderRadius({ topLeft: 4, topRight: 4 })
          .linearGradient({
            angle: 180,
            colors: [[COLOR_GOLD, 0], [COLOR_GOLD_LIGHT, 1]]
          })
        Text(monthlyLabels[i])
          .fontSize(9)
          .fontColor(COLOR_TEXT_HINT)
          .margin({ top: 5 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
    })
  }
  .width('100%')
}
.width('100%')
.backgroundColor(COLOR_CARD)
.borderRadius(14)
.border({ width: 1, color: COLOR_BORDER })
.padding(14)

本段代码实现了一个纯 ArkTS 声明式的柱状图组件,无需引入任何第三方图表库。ForEach 遍历 monthlyShipValues 数组(3月至8月的押运估值数据),为每个数据项生成一个 Column 柱状单元。每个单元从上到下包含三部分:数值标签(金色 9 像素)、柱体(24 像素宽,高度由 getBarHeight(v, monthlyMax) 计算得出)、月份标签(灰色 9 像素)。柱体使用 .linearGradient({ angle: 180, colors: [[COLOR_GOLD, 0], [COLOR_GOLD_LIGHT, 1]] }) 设置从下到上(angle 180)的垂直渐变——底部为 COLOR_GOLD 深金,顶部为 COLOR_GOLD_LIGHT 浅金,模拟金属光泽效果。.borderRadius({ topLeft: 4, topRight: 4 }) 只对上方两个角做圆角处理,使柱顶呈圆头状。getBarHeight 函数将数值按比例映射到 96 像素的最大高度范围内(Math.round(v / max * 96)),确保最高的 420 万对应 96 像素,其他按比例缩减。每个柱状单元通过 .layoutWeight(1)Row 中等宽分布。整个图表包裹在白色卡片中,视觉上干净利落。这种用原生 ArkTS 组件实现数据可视化的方式,展示了声明式 UI 在图表场景下的灵活性。

渲染层

计算层

数据层

monthlyShipValues
[120,260,180,340,300,420]

monthlyLabels
['3月'...'8月']

monthlyMax = 420

getBarHeight(v, max)
→ Math.round(v/max*96)

ForEach 遍历

Column 柱状单元

Text 数值标签

Column 柱体
.linearGradient 金色渐变

Text 月份标签

3.14 拍品出价弹框:深黑底金色高亮的交互设计

@Builder bidModal() {
  Column() {
    this.modalOverlay(() => { this.showBidModal = false })
    Column() {
      Row() {
        Text('🔨 拍品出价')
          .fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_GOLD_LIGHT)
        Column().layoutWeight(1)
        Text('✕').fontSize(15).fontColor('#8A857C')
          .padding(6)
          .onClick(() => { this.showBidModal = false })
      }
      .width('100%').padding({ left: 16, right: 12, top: 14, bottom: 6 })

      Row() {
        Column() {
          Text('🖼️').fontSize(26)
        }
        .width(56).height(56)
        .backgroundColor(this.selectedLot?.color ?? COLOR_GOLD)
        .borderRadius(8)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text(this.selectedLot?.name ?? '')
            .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_CARD).maxLines(1)
          Text('Lot ' + (this.selectedLot?.lotNo ?? '') + ' · ' + (this.selectedLot?.artist ?? ''))
            .fontSize(10).fontColor('#8A857C').margin({ top: 4 })
          Text('结拍 ' + (this.selectedLot?.endTime ?? ''))
            .fontSize(9).fontColor(COLOR_GOLD).margin({ top: 3 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 12 })
      }

      Row() {
        Text('当前价').fontSize(11).fontColor('#8A857C').margin({ bottom: 5 })
        Text(formatWan(this.selectedLot?.currentPrice ?? 0))
          .fontSize(30).fontColor(COLOR_GOLD).margin({ left: 10 })
        Text('(' + (this.selectedLot?.bidCount ?? 0).toString() + ' 次出价)')
          .fontSize(10).fontColor('#8A857C').margin({ left: 8, bottom: 6 })
      }
      .alignItems(VerticalAlign.Bottom)

      TextInput({ placeholder: '输入出价金额', text: this.bidPrice })
        .placeholderColor('#5A574F')
        .fontSize(16)
        .fontColor(COLOR_GOLD)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#262523')
        .borderRadius(8)
        .border({ width: 1, color: '#3B3934' })

      Row() {
        ForEach(BID_STEPS, (step: number) => {
          Text('+' + step.toString())
            .fontSize(11)
            .fontColor(this.bidStep === step ? COLOR_DARK : COLOR_GOLD)
            .backgroundColor(this.bidStep === step ? COLOR_GOLD : '#262523')
            .borderRadius(12)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ left: 4, right: 4 })
            .onClick(() => {
              this.bidStep = step
              this.bidPrice = ((this.selectedLot?.currentPrice ?? 0) + step).toString()
            })
        })
      }

      Text('确认出价')
        .fontSize(14).fontColor(COLOR_DARK)
        .backgroundColor(COLOR_GOLD)
        .borderRadius(22)
        .padding({ left: 48, right: 48, top: 11, bottom: 11 })
        .onClick(() => { this.showBidModal = false })
    }
    .width('88%')
    .height('55%')
    .backgroundColor(COLOR_DARK)
    .borderRadius(16)
    .position({ x: '6%', y: '20%' })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

出价弹框是拍卖模块的核心交互组件,采用深黑底金色高亮的视觉语言,呼应拍卖行的庄重氛围。弹框使用 this.selectedLotAuctionItem | null 类型)作为数据源,通过可选链 ?. 安全访问拍品属性,配合 ?? '' 空值合并运算符提供默认值,防止空指针异常。弹框头部展示拍品缩略图(使用 selectedLot.color 作为背景色)、拍品名称、Lot 编号与艺术家、结拍时间。当前价格使用 30 像素金色大字突出展示,配合出价次数说明。出价输入区域包含一个 TextInput 和四个加价步进按钮(BID_STEPS = [500, 1000, 5000, 10000]),点击步进按钮时同时更新 bidStep(记录选中的步进值)和 bidPrice(计算当前价加步进值),步进按钮的选中态通过 this.bidStep === step 三元表达式控制颜色反转——选中时金底深字,未选中时深底金字。TextInputtext 参数绑定 this.bidPrice,实现了状态到输入框文本的单向同步。确认按钮使用金色背景深色文字,与弹框整体的深黑底形成强对比,引导用户视线。整个弹框体现了 ArkTS 中"状态驱动 + 可选链安全访问 + ForEach 步进渲染 + 条件样式"的综合运用。

3.15 拍品列表与出价热度进度条

@Builder lotItemBuilder(a: AuctionItem) {
  Row() {
    Column() {
      Text(a.lotNo)
        .fontSize(26)
        .fontColor(COLOR_GOLD_LIGHT)
      Text('LOT')
        .fontSize(8)
        .fontColor(COLOR_TEXT_HINT)
        .letterSpacing(2)
        .margin({ top: 2 })
    }
    .width(64)
    .alignItems(HorizontalAlign.Center)

    Column() {
      Text(a.name)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
        .maxLines(1)
        .width('100%')
      Text(a.artist)
        .fontSize(10)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 3 })
        .width('100%')
      Row() {
        Text('起拍 ' + formatWan(a.startPrice)).fontSize(9).fontColor(COLOR_TEXT_HINT)
        Text('当前 ').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ left: 10 })
        Text(formatWan(a.currentPrice)).fontSize(11).fontColor(COLOR_GOLD).fontWeight(FontWeight.Bold)
        Text(a.bidCount.toString() + ' 次出价').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ left: 10 })
      }
      .margin({ top: 6 })
      Row() {
        Text('⏳ 结拍 ' + a.endTime).fontSize(9).fontColor(COLOR_WARNING)
      }
      .margin({ top: 4 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Start)
    .padding({ left: 10 })

    Column() {
      Text('出价')
        .fontSize(11).fontColor(COLOR_CARD)
        .backgroundColor(COLOR_DARK)
        .borderRadius(13)
        .padding({ left: 15, right: 15, top: 6, bottom: 6 })
        .onClick(() => {
          this.selectedLot = a
          this.bidPrice = a.currentPrice.toString()
          this.bidStep = 0
          this.showBidModal = true
        })
      Text('撤回')
        .fontSize(10).fontColor(COLOR_TEXT_HINT)
        .border({ width: 1, color: COLOR_BORDER })
        .borderRadius(11)
        .padding({ left: 12, right: 12, top: 4, bottom: 4 })
        .margin({ top: 6 })
        .onClick(() => {
          this.selectedLot = a
          this.showWithdrawModal = true
        })
    }
    .alignItems(HorizontalAlign.Center)
  }
  .width('100%')
  .backgroundColor(COLOR_CARD)
  .borderRadius(12)
  .border({ width: 1, color: COLOR_BORDER })
  .padding({ left: 12, right: 12, top: 12, bottom: 12 })
  .margin({ top: 8 })
}

// 出价热度进度条
Column() {
  Text('出价热度')
    .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
    .width('100%').margin({ bottom: 10 })
  ForEach(mockAuctions, (a: AuctionItem) => {
    Column() {
      Row() {
        Text('Lot ' + a.lotNo).fontSize(10).fontColor(COLOR_TEXT_SUB)
        Column().layoutWeight(1)
        Text(a.bidCount.toString() + ' 次').fontSize(10).fontColor(COLOR_GOLD)
      }
      Row() {
        Column()
          .width(getBidHeatWidth(a.bidCount) + '%')
          .height(5)
          .backgroundColor(COLOR_GOLD)
          .borderRadius(3)
        Column().layoutWeight(1)
      }
      .margin({ top: 4 })
    }
    .width('100%')
    .margin({ top: 5 })
  })
}

拍品列表项 lotItemBuilder 采用编号式拍卖行风格布局:左侧是 64 像素宽的 Lot 编号区,26 像素金色浅光大字展示编号,下方配 8 像素灰色 “LOT” 字样;中间是拍品信息区,包含名称、艺术家、起拍价、当前价、出价次数和结拍时间,其中当前价使用金色加粗 11 像素字突出,结拍时间使用 COLOR_WARNING 警告色提示紧迫感;右侧是操作区,包含"出价"和"撤回"两个按钮。点击出价按钮时,将当前拍品赋值给 selectedLot,初始化出价金额为当前价,重置步进为 0,然后显示出价弹框。点击撤回按钮时类似地设置 selectedLot 并显示撤回弹框。出价热度进度条则使用 ForEach 遍历所有拍品,为每个拍品生成一行进度条——左侧是 Lot 编号和出价次数,右侧是宽度按 getBidHeatWidth(a.bidCount) 计算的百分比金色进度条。getBidHeatWidth 函数将出价次数映射为 0-100 的百分比宽度,出价最多的拍品(89 次)对应 100% 宽度。这种用 Column + 百分比宽度实现的进度条,是 ArkTS 中纯声明式数据可视化的又一实例,无需任何图表组件即可完成。

3.16 藏品档案编辑弹框与展签风格网格

@Builder editModal() {
  Column() {
    this.modalOverlay(() => { this.showEditModal = false })
    Column() {
      Column() {
        Text('✏️ 编辑藏品档案')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
          .margin({ top: 14 })
        Text('ARCHIVE · ' + (this.selectedCollection?.name ?? ''))
          .fontSize(9)
          .fontColor(COLOR_TEXT_HINT)
          .letterSpacing(1)
          .margin({ top: 4, bottom: 10 })
        Column().width(36).height(2).backgroundColor(COLOR_GOLD)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)

      Scroll() {
        Column() {
          Text('藏品名').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 10 })
          TextInput({ placeholder: this.selectedCollection?.name ?? '' })
            .onChange((v: string) => { this.editName = v })

          Row() {
            Column() {
              Text('年代').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%')
              TextInput({ placeholder: this.selectedCollection?.era ?? '' })
                .onChange((v: string) => { this.editEra = v })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)

            Column() {
              Text('估值(万元)').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%')
              TextInput({ placeholder: this.selectedCollection?.value.toString() ?? '' })
                .onChange((v: string) => { this.editValue = v })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
          }

          Row() {
            ForEach(RARITY_OPTIONS, (r: string) => {
              if (this.editRarity === r) {
                Text(r).fontSize(11).fontColor(COLOR_CARD)
                  .backgroundColor(RARITY_CONFIG[r]?.color ?? COLOR_TEXT_SUB)
              } else {
                Text(r).fontSize(11).fontColor(RARITY_CONFIG[r]?.color ?? COLOR_TEXT_SUB)
                  .backgroundColor(COLOR_BG)
                  .border({ width: 1, color: COLOR_BORDER })
                  .onClick(() => { this.editRarity = r })
              }
            })
          }

          Row() {
            ForEach(CONDITION_OPTIONS, (c: string) => {
              if (this.editCondition === c) {
                Text(c).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_PURPLE)
              } else {
                Text(c).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                  .border({ width: 1, color: COLOR_BORDER })
                  .onClick(() => { this.editCondition = c })
              }
            })
          }
        }
      }

      Row() {
        Text('取消')
          .onClick(() => { this.showEditModal = false })
        Text('保存')
          .backgroundColor(COLOR_GOLD)
          .onClick(() => {
            this.saveHint = this.editName !== '' ? this.editName : (this.selectedCollection?.name ?? '')
            this.showEditModal = false
          })
      }
    }
    .width('85%')
    .height('60%')
    .backgroundColor(COLOR_CARD)
    .borderRadius(16)
    .position({ x: '7.5%', y: '18%' })
  }
  .zIndex(999)
}

@Builder galleryCardBuilder(c: CollectionItem) {
  Column() {
    Column() {
      Text('🖼️').fontSize(30)
      if (c.rarity === '孤品') {
        Text('孤品')
          .fontSize(8).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_DANGER)
          .position({ x: '72%', y: 6 })
      }
    }
    .width('100%')
    .height(86)
    .backgroundColor(c.color)
    .borderRadius(8)

    Column() {
      Text(c.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).maxLines(1)
      Text(c.artist + ' · ' + c.era).fontSize(9).fontColor(COLOR_TEXT_SUB)
      Row() {
        Text(formatWan(c.value)).fontSize(10).fontColor(COLOR_GOLD).fontWeight(FontWeight.Medium)
        Column().layoutWeight(1)
        Text(RARITY_CONFIG[c.rarity]?.label ?? c.rarity)
          .fontSize(8)
          .fontColor(RARITY_CONFIG[c.rarity]?.color ?? COLOR_TEXT_SUB)
      }
    }
  }
  .onClick(() => {
    this.selectedCollection = c
    this.editName = c.name
    this.editArtist = c.artist
    this.editRarity = c.rarity
    this.showEditModal = true
  })
}

藏品档案编辑弹框采用展签卡片风格设计,头部中央对齐标题"编辑藏品档案",下方配 "ARCHIVE · " 加藏品名的副标题,再加一条 36 像素宽金色短线,模拟美术馆展签的视觉签名。表单包含藏品名、艺术家、年代与估值(两列并排)、稀有度选择和保存状态选择。稀有度选项 RARITY_OPTIONS = ['普品', '佳作', '珍品', '孤品'] 通过 ForEach 渲染,选中态使用 RARITY_CONFIG[r]?.color 作为背景色——这意味着不同稀有度选中时呈现不同颜色(普品灰、佳作金、珍品紫、孤品红),视觉信息密度高。保存状态 CONDITION_OPTIONS 的选中态统一使用 COLOR_PURPLE 紫色背景。藏品卡 galleryCardBuilder 以展签风格展示藏品信息:顶部是 86 像素高的色块区(背景色取自 c.color),孤品额外叠加红色角标;下方是藏品名、艺术家与年代、估值与稀有度标签。点击藏品卡时,将当前藏品赋值给 selectedCollection,预填编辑表单字段,并显示编辑弹框。藏品网格在 build 中通过 ForEach 遍历 mockCollections,每两个为一行用 Row 包裹,实现了两列网格布局。

3.17 展览时间线:Stack 嵌套与状态驱动的节点渲染

@Builder timelineItemBuilder(e: ExhibitionItem, isLast: boolean) {
  Row() {
    Stack({ alignContent: Alignment.Top }) {
      Column()
        .width(1)
        .height(isLast ? 24 : '100%')
        .backgroundColor(COLOR_BORDER)
      Column()
        .width(13)
        .height(13)
        .borderRadius(7)
        .backgroundColor(e.status === '展出中' ? COLOR_GOLD : COLOR_GOLD_LIGHT)
        .border({ width: 3, color: COLOR_BG })
        .margin({ top: 6 })
    }
    .width(24)
    .alignContent(Alignment.Top)

    Column() {
      Row() {
        Text(e.name)
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
          .maxLines(1)
          .layoutWeight(1)
        Text(EXHIBIT_STATUS_CONFIG[e.status]?.label ?? e.status)
          .fontSize(9)
          .fontColor(EXHIBIT_STATUS_CONFIG[e.status]?.color ?? COLOR_TEXT_SUB)
          .backgroundColor(EXHIBIT_STATUS_CONFIG[e.status]?.bg ?? COLOR_BG)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .borderRadius(9)
      }

      Row() {
        Text('🏛 ' + e.venue).fontSize(10).fontColor(COLOR_TEXT_SUB).maxLines(1).layoutWeight(1)
      }

      Row() {
        Text('📅 ' + e.startDate + ' — ' + e.endDate).fontSize(10).fontColor(COLOR_TEXT_HINT)
        Column().layoutWeight(1)
        Text(e.artworkCount.toString() + ' 件展品').fontSize(10).fontColor(COLOR_GOLD)
      }

      Row() {
        Text('展品运输由艺境押运全程承运').fontSize(8).fontColor(COLOR_TEXT_HINT)
        Column().layoutWeight(1)
        Text('物流单 ›')
          .fontSize(10).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_DARK)
          .borderRadius(11)
          .onClick(() => { this.logisticsHint = e.name })
      }
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(12)
    .border({ width: 1, color: COLOR_BORDER })
    .padding({ left: 12, right: 12, top: 12, bottom: 12 })
    .margin({ bottom: 10 })
  }
  .width('100%')
  .alignItems(VerticalAlign.Top)
}

展览时间线是本项目中最精巧的布局实现之一。每个时间线项由 Row 横向排列两部分:左侧 24 像素宽的时间线节点列,右侧的展览信息卡。节点列使用 Stack({ alignContent: Alignment.Top }) 叠加两层元素——底层是一条 1 像素宽的竖线(COLOR_BORDER 灰色),高度根据 isLast 参数判断:最后一项高度仅 24 像素(不延伸到卡片底部),非最后一项高度为 100%(与右侧卡片等高);上层是一个 13 像素的圆形节点,背景色根据展览状态取值——"展出中"用 COLOR_GOLD 金色,其他状态用 COLOR_GOLD_LIGHT 浅金,外层加 3 像素宽的 COLOR_BG 背景色描边,使节点与竖线之间形成视觉间隔。右侧展览卡内包含展览名称与状态标签(通过 EXHIBIT_STATUS_CONFIG 映射获取标签、颜色、背景色)、场馆信息、日期范围与展品数量、以及"展品运输由艺境押运全程承运"的品牌植入和"物流单"按钮。点击物流单按钮时更新 logisticsHint 状态。isLast 参数的传入是时间线渲染的关键——在 build 中,前七项传入 false,最后一项传入 true,控制竖线是否完整延伸。这种用 Stack 嵌套实现时间线视觉的方式,展示了 ArkTS 布局组件的灵活组合能力。

3.18 个人中心:统计网格与设置列表的模块化构建

@Builder statCellBuilder(icon: string, value: string, label: string, color: string) {
  Column() {
    Text(icon).fontSize(18)
    Text(value)
      .fontSize(22)
      .fontColor(color)
      .margin({ top: 5 })
    Text(label)
      .fontSize(10)
      .fontColor(COLOR_TEXT_SUB)
      .margin({ top: 3 })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 16, bottom: 16 })
  .backgroundColor(COLOR_CARD)
  .borderRadius(12)
  .border({ width: 1, color: COLOR_BORDER })
}

@Builder settingRowBuilder(icon: string, label: string, hint: string) {
  Row() {
    Text(icon).fontSize(17)
    Text(label)
      .fontSize(13)
      .fontColor(COLOR_TEXT_MAIN)
      .margin({ left: 12 })
      .layoutWeight(1)
    Text(hint)
      .fontSize(10)
      .fontColor(COLOR_TEXT_HINT)
    Text('›')
      .fontSize(15)
      .fontColor(COLOR_TEXT_HINT)
      .margin({ left: 8 })
  }
  .width('100%')
  .padding({ top: 14, bottom: 14 })
  .onClick(() => { this.profileHint = label })
}

// build 中调用
Row() {
  this.statCellBuilder('🚛', '86', '总押运(单)', COLOR_TEXT_MAIN)
  Column().width(8)
  this.statCellBuilder('🔨', '34', '总拍品(件)', COLOR_GOLD)
}

Row() {
  this.statCellBuilder('💎', formatWan(9680), '总估值', COLOR_PURPLE)
  Column().width(8)
  this.statCellBuilder('✦', '12,860', '积分', COLOR_WARNING)
}

Column() {
  this.settingRowBuilder('🖼️', '我的藏品', '162 件')
  Divider().color(COLOR_BG)
  this.settingRowBuilder('📍', '常用地址', '6 个')
  Divider().color(COLOR_BG)
  this.settingRowBuilder('🛡️', '保额管理', '总额 ¥9,680万')
  Divider().color(COLOR_BG)
  this.settingRowBuilder('☎️', '客服', '7×24h')
  Divider().color(COLOR_BG)
  this.settingRowBuilder('⚙️', '设置', '')
}

个人中心模块通过两个参数化 @Builder 实现了 UI 的高度复用。statCellBuilder 接收图标、数值、标签和颜色四个参数,生成一个统计格——图标 18 像素,数值 22 像素并使用传入的颜色着色(总押运用主色黑、总拍品用金色、总估值用紫色、积分用警告色),标签 10 像素灰色。四个统计格两两一行,行间用 8 像素宽的空白 Column 做间距,形成 2x2 统计网格。settingRowBuilder 接收图标、标签、提示三个参数,生成标准设置行——左侧图标、中间标签、右侧提示文字和 箭头,整行可点击,点击后更新 profileHint 状态。设置行之间用 Divider().color(COLOR_BG) 分隔,分隔线颜色设为背景色使其视觉上存在但不突兀。个人中心顶部还有一个米白资料卡,展示用户头像(64 像素圆形金色描边)、姓名"沈亦藏"、“鉴藏家 · 金"金色徽章、藏家编号和注册天数,底部以深黑卡片收尾,印有"艺境押运 ART GUARD"品牌名和"让每一件艺术品,都被温柔以待"的品牌口号,最后是版本号"v1.0.0 © 2026 艺境押运”。这种从参数化 Builder 到组合调用的设计,使个人中心的代码结构清晰、扩展性强——新增统计格或设置行只需调用 Builder 并传入不同参数即可。

四、核心技术特性对比分析

技术维度实现方式ArkTS API 24 特性运用艺术品押运业务映射
状态管理@State + @Observed组件内私有状态 + 跨组件可观测对象押运单状态、出价金额、表单数据实时同步
条件渲染if-else 链 + 三元表达式框架自动管理组件创建销毁Tab 路由切换、弹框显隐、选中态样式
列表渲染ForEach + 数组遍历声明式数据到视图映射押运单列表、拍品列表、藏品网格、时间线
组件复用@Builder 参数化方法UI 片段模块化封装与参数传入指标格、统计格、设置行、卡片项复用
布局体系Column + Row + Stack + Scroll弹性布局 + 层叠布局 + 滚动容器纵向流式、横向滚动、Z 轴叠加(粒子层)
弹框交互modalOverlay + 条件渲染 + zIndex遮罩层 + 卡片定位 + 事件穿透预约表单、出价弹框、撤回确认、档案编辑
数据可视化linearGradient + 百分比宽度 Column纯声明式图表,无第三方依赖柱状图(月度估值)、进度条(出价热度、品类分布)
动画特效setInterval + @State 数组 + position定时器驱动状态更新触发重渲染金色光尘粒子全局飘浮
配置映射Record<string, Meta> + ?. 可选链类型安全查表代替分支语句状态色、品类图标、包装方式、押运等级
生命周期aboutToAppear / aboutToDisappear组件挂载/卸载回调定时器启动与清理,防止内存泄漏
类型安全interface + implements + @Observed接口契约 + 可观测实现类四大数据模型的双层类型保障
样式系统链式属性方法 + const 颜色常量声明式样式链 + 全局常量美术馆极简黑白金设计语言统一

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

// ============================================================================
// 艺境押运 ART GUARD — 艺术品/文物级恒温押运 + 画廊拍卖平台
// 场景:油画、雕塑、古董、装置艺术品的专业押运(恒温恒湿/防震气垫/安保押运)
//       + 艺术品拍卖 + 展览物流。美术馆极简黑白金风格。
// 结构:颜色常量 -> 数据模型 -> 配置映射 -> 静态数据 -> 辅助函数 -> Tab枚举
//       -> @Entry 主页面 -> 各Tab内容struct -> 弹框@Builder
// ============================================================================

// ============ 颜色常量(美术馆极简黑白金风) ============
const COLOR_BG: string = '#F8F7F4'           // 画廊米白背景
const COLOR_CARD: string = '#FFFFFF'         // 卡片白
const COLOR_DARK: string = '#1A1A1A'         // 墨黑(深卡/主色)
const COLOR_GOLD: string = '#B8935A'         // 香槟金
const COLOR_GOLD_LIGHT: string = '#E8D5B5'   // 浅金
const COLOR_PURPLE: string = '#6B5B95'       // 艺术紫
const COLOR_PURPLE_LIGHT: string = '#C5B8E0' // 浅紫
const COLOR_TEXT_MAIN: string = '#1A1A1A'    // 文字主
const COLOR_TEXT_SUB: string = '#6E6A63'     // 文字次
const COLOR_TEXT_HINT: string = '#B5B0A8'    // 文字提示
const COLOR_BORDER: string = '#E5E2DC'       // 边框
const COLOR_SUCCESS: string = '#5A7D5A'      // 成功
const COLOR_WARNING: string = '#C49A3C'      // 警告
const COLOR_DANGER: string = '#A63D40'       // 危险

// ============ 数据模型接口 ============
interface ArtShipModel {
  id: number;
  orderNo: string;
  artName: string;
  pickup: string;
  destination: string;
  value: number;
  status: string;
  date: string;
}

interface AuctionItemModel {
  id: number;
  lotNo: string;
  name: string;
  artist: string;
  startPrice: number;
  currentPrice: number;
  bidCount: number;
  endTime: string;
  color: string;
}

interface CollectionModel {
  id: number;
  name: string;
  artist: string;
  era: string;
  value: number;
  rarity: string;
  color: string;
}

interface ExhibitionModel {
  id: number;
  name: string;
  venue: string;
  startDate: string;
  endDate: string;
  artworkCount: number;
  status: string;
}

// ============ @Observed 数据类 ============
@Observed
class ArtShipItem implements ArtShipModel {
  id: number = 0;
  orderNo: string = '';
  artName: string = '';
  pickup: string = '';
  destination: string = '';
  value: number = 0;
  status: string = '待包装';
  date: string = '';
  constructor(id: number, orderNo: string, artName: string, pickup: string,
    destination: string, value: number, status: string, date: string) {
    this.id = id; this.orderNo = orderNo; this.artName = artName
    this.pickup = pickup; this.destination = destination
    this.value = value; this.status = status; this.date = date
  }
}

@Observed
class AuctionItem implements AuctionItemModel {
  id: number = 0;
  lotNo: string = '';
  name: string = '';
  artist: string = '';
  startPrice: number = 0;
  currentPrice: number = 0;
  bidCount: number = 0;
  endTime: string = '';
  color: string = '#B8935A';
  constructor(id: number, lotNo: string, name: string, artist: string,
    startPrice: number, currentPrice: number, bidCount: number, endTime: string, color: string) {
    this.id = id; this.lotNo = lotNo; this.name = name; this.artist = artist
    this.startPrice = startPrice; this.currentPrice = currentPrice
    this.bidCount = bidCount; this.endTime = endTime; this.color = color
  }
}

@Observed
class CollectionItem implements CollectionModel {
  id: number = 0;
  name: string = '';
  artist: string = '';
  era: string = '';
  value: number = 0;
  rarity: string = '普品';
  color: string = '#E8D5B5';
  constructor(id: number, name: string, artist: string, era: string,
    value: number, rarity: string, color: string) {
    this.id = id; this.name = name; this.artist = artist; this.era = era
    this.value = value; this.rarity = rarity; this.color = color
  }
}

@Observed
class ExhibitionItem implements ExhibitionModel {
  id: number = 0;
  name: string = '';
  venue: string = '';
  startDate: string = '';
  endDate: string = '';
  artworkCount: number = 0;
  status: string = '展出中';
  constructor(id: number, name: string, venue: string, startDate: string,
    endDate: string, artworkCount: number, status: string) {
    this.id = id; this.name = name; this.venue = venue
    this.startDate = startDate; this.endDate = endDate
    this.artworkCount = artworkCount; this.status = status
  }
}

// ============ 配置接口 + Record 映射 ============
interface ShipStatusMeta {
  label: string;
  color: string;
  bg: string;
}

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

interface PackMeta {
  label: string;
  icon: string;
  desc: string;
}

interface GuardLevelMeta {
  label: string;
  icon: string;
  color: string;
}

interface RarityMeta {
  label: string;
  color: string;
}

interface ExhibitStatusMeta {
  label: string;
  color: string;
  bg: string;
}

const SHIP_STATUS_CONFIG: Record<string, ShipStatusMeta> = {
  '待包装': { label: '待包装', color: COLOR_TEXT_SUB, bg: '#EFEDF7' },
  '押运中': { label: '押运中', color: COLOR_GOLD, bg: '#F4EBDD' },
  '已交付': { label: '已交付', color: COLOR_SUCCESS, bg: '#E9F0E9' }
}

const ART_CATEGORY_CONFIG: Record<string, CategoryMeta> = {
  '油画': { label: '油画', icon: '🖌️' },
  '雕塑': { label: '雕塑', icon: '🗿' },
  '水墨': { label: '水墨', icon: '🖋️' },
  '装置': { label: '装置', icon: '💡' },
  '摄影': { label: '摄影', icon: '📷' },
  '古董': { label: '古董', icon: '🏺' }
}

const PACK_CONFIG: Record<string, PackMeta> = {
  '软包': { label: '软包', icon: '📦', desc: '软性无酸材料包裹' },
  '木箱': { label: '木箱', icon: '🪵', desc: '定制实木防潮箱体' },
  '气垫箱': { label: '气垫箱', icon: '🛡️', desc: '悬浮气垫减震系统' },
  '恒温箱': { label: '恒温箱', icon: '🌡️', desc: '主动恒温恒湿舱' }
}

const GUARD_LEVEL_CONFIG: Record<string, GuardLevelMeta> = {
  '标准': { label: '标准', icon: '◇', color: COLOR_TEXT_SUB },
  '安保': { label: '安保', icon: '◈', color: COLOR_GOLD },
  '武装': { label: '武装', icon: '◆', color: COLOR_DANGER }
}

const RARITY_CONFIG: Record<string, RarityMeta> = {
  '普品': { label: '普品', color: COLOR_TEXT_HINT },
  '佳作': { label: '佳作', color: COLOR_GOLD },
  '珍品': { label: '珍品', color: COLOR_PURPLE },
  '孤品': { label: '孤品', color: COLOR_DANGER }
}

const EXHIBIT_STATUS_CONFIG: Record<string, ExhibitStatusMeta> = {
  '布展中': { label: '布展中', color: COLOR_WARNING, bg: '#F6EEDB' },
  '展出中': { label: '展出中', color: COLOR_GOLD, bg: '#F4EBDD' },
  '已撤展': { label: '已撤展', color: COLOR_TEXT_HINT, bg: '#F0EFEA' }
}

const HEADER_CATEGORIES: string[] = ['全部', '油画', '雕塑', '水墨', '装置', '摄影', '古董']
const PACK_OPTIONS: string[] = ['软包', '木箱', '气垫箱', '恒温箱']
const GUARD_LEVELS: string[] = ['标准', '安保', '武装']
const RARITY_OPTIONS: string[] = ['普品', '佳作', '珍品', '孤品']
const CONDITION_OPTIONS: string[] = ['恒温库房', '展柜陈列', '借展中', '修复中']
const BID_STEPS: number[] = [500, 1000, 5000, 10000]
const SORT_OPTIONS: string[] = ['即将结拍', '最新', '价格']


  }

  // ---------- 内容区 ----------
  @Builder contentArea() {
    Column() {
      if (this.activeTab === ArtTab.SHIP) {
        ShipContent()
      } else if (this.activeTab === ArtTab.AUCTION) {
        AuctionContent()
      } else if (this.activeTab === ArtTab.GALLERY) {
        GalleryContent()
      } else if (this.activeTab === ArtTab.EXHIBIT) {
        ExhibitContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  // ---------- 底部 Tab ----------
  @Builder bottomTabItem(icon: string, label: string, tab: ArtTab) {
    Column() {
      Text(icon)
        .fontSize(19)
        .opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label)
        .fontSize(9)
        .fontColor(this.activeTab === tab ? COLOR_TEXT_MAIN : COLOR_TEXT_HINT)
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
      if (this.activeTab === tab) {
        Column()
          .width(16)
          .height(2)
          .backgroundColor(COLOR_GOLD)
          .borderRadius(1)
          .margin({ top: 3 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 6, bottom: 6 })
    .onClick(() => { this.activeTab = tab })
  }

  @Builder tabBarBuilder() {
    Column() {
      Column().width('100%').height(1).backgroundColor(COLOR_BORDER)
      Row() {
        this.bottomTabItem('🚛', '押运', ArtTab.SHIP)
        this.bottomTabItem('🔨', '拍卖', ArtTab.AUCTION)
        this.bottomTabItem('🖼️', '藏品', ArtTab.GALLERY)
        this.bottomTabItem('🏛️', '展览', ArtTab.EXHIBIT)
        this.bottomTabItem('👤', '我的', ArtTab.PROFILE)
      }
      .width('100%')
      .backgroundColor(COLOR_CARD)
      .padding({ top: 2, bottom: 4 })
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
  }

  // ---------- 金色光尘粒子层 ----------
  @Builder dustLayerBuilder() {
    Column() {
      ForEach(this.dustX, (x: number, i: number) => {
        Text('✦')
          .fontSize(this.dustS[i])
          .fontColor(i % 2 === 0 ? COLOR_GOLD : COLOR_GOLD_LIGHT)
          .opacity(this.dustO[i])
          .position({ x: this.dustX[i] + '%', y: this.dustY[i] + '%' })
      })
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

  build() {
    Stack() {
      Column() {
        this.headerBuilder()
        this.contentArea()
        this.tabBarBuilder()
      }
      .width('100%')
      .height('100%')
      .backgroundColor(COLOR_BG)

      this.dustLayerBuilder()
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab1 押运 SHIP ============
@Component
struct ShipContent {
  @State showBookModal: boolean = false
  @State formArtName: string = ''
  @State formCategory: string = '油画'
  @State formValue: string = ''
  @State formPack: string = '恒温箱'
  @State formFrom: string = ''
  @State formTo: string = ''
  @State formLevel: string = '安保'
  @State formTime: string = ''
  @State formNotes: string = ''
  @State bookResult: string = ''
  @State detailHint: string = ''

  // ========== 弹框遮罩 ==========
  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(26,26,26,0.55)')
      .onClick(onClose)
  }

  // ========== 弹框1:预约艺术品押运(画廊白卡表单式) ==========
  @Builder bookModal() {
    Column() {
      this.modalOverlay(() => { this.showBookModal = false })
      Column() {
        // 金色顶线
        Column()
          .width('100%')
          .height(3)
          .backgroundColor(COLOR_GOLD)
          .borderRadius({ topLeft: 16, topRight: 16 })

        Row() {
          Text('🖼️ 预约艺术品押运')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_TEXT_MAIN)
          Column().layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor(COLOR_TEXT_HINT)
            .padding(6)
            .onClick(() => { this.showBookModal = false })
        }
        .width('100%')
        .padding({ left: 18, right: 14, top: 14, bottom: 10 })

        Scroll() {
          Column() {
            Text('艺术品名').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 8 })
            TextInput({ placeholder: '如:《暮色中的塞纳河》' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.formArtName = v })

            Text('品类').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            Row() {
              ForEach(['油画', '雕塑', '水墨', '装置', '古董'], (c: string) => {
                if (this.formCategory === c) {
                  Text(c).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_DARK)
                    .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(c).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formCategory = c })
                }
              })
            }
            .margin({ top: 5 })

            Text('估值(万元)').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextInput({ placeholder: '如:380' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.formValue = v })

            Text('包装方式').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            Row() {
              ForEach(PACK_OPTIONS, (p: string) => {
                if (this.formPack === p) {
                  Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p).fontSize(11)
                    .fontColor(COLOR_CARD).backgroundColor(COLOR_GOLD)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p).fontSize(11)
                    .fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formPack = p })
                }
              })
            }
            .margin({ top: 5 })

            Text('押运等级').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            Row() {
              ForEach(GUARD_LEVELS, (l: string) => {
                if (this.formLevel === l) {
                  Text((GUARD_LEVEL_CONFIG[l]?.icon ?? '') + ' ' + l).fontSize(11)
                    .fontColor(COLOR_CARD).backgroundColor(COLOR_DARK)
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text((GUARD_LEVEL_CONFIG[l]?.icon ?? '') + ' ' + l).fontSize(11)
                    .fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formLevel = l })
                }
              })
            }
            .margin({ top: 5 })

            Text('出发地').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextInput({ placeholder: '如:上海西岸艺术中心' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.formFrom = v })

            Text('目的地').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextInput({ placeholder: '如:北京798艺术区' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.formTo = v })

            Text('预约时间').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextInput({ placeholder: '如:2026-08-26 09:00' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.formTime = v })

            Text('备注').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextArea({ placeholder: '艺术品特殊要求(悬挂方式 / 温区 / 保险等)...' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
              .width('100%').height(64)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5, bottom: 14 })
              .onChange((v: string) => { this.formNotes = v })
          }
          .padding({ left: 18, right: 18 })
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)

        Row() {
          Text('取消')
            .fontSize(13).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG)
            .border({ width: 1, color: COLOR_BORDER })
            .borderRadius(20)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .onClick(() => { this.showBookModal = false })
          Text('确认押运')
            .fontSize(13).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_DARK)
            .borderRadius(20)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => {
              this.bookResult = this.formArtName !== '' ? this.formArtName : '未命名艺术品'
              this.showBookModal = false
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding({ top: 12, bottom: 16 })
      }
      .width('90%')
      .height('75%')
      .backgroundColor(COLOR_CARD)
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '5%', y: '11%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // ========== 押运环境指标格 ==========
  @Builder envMetricBuilder(icon: string, label: string, value: string, unit: string) {
    Column() {
      Text(icon).fontSize(15)
      Text(label).fontSize(9).fontColor('#8A857C').margin({ top: 4 })
      Row() {
        Text(value).fontSize(22).fontColor(COLOR_GOLD)
        Text(unit).fontSize(10).fontColor(COLOR_GOLD_LIGHT).margin({ left: 2, bottom: 3 })
      }
      .alignItems(VerticalAlign.Bottom)
      .margin({ top: 4 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 12, bottom: 12 })
  }

  // ========== 押运单列表项 ==========
  @Builder shipItemBuilder(s: ArtShipItem) {
    Column() {
      Row() {
        Text(s.orderNo)
          .fontSize(10)
          .fontColor(COLOR_TEXT_HINT)
          .letterSpacing(1)
        Text(s.date)
          .fontSize(10)
          .fontColor(COLOR_TEXT_HINT)
          .margin({ left: 8 })
        Column().layoutWeight(1)
        Text(SHIP_STATUS_CONFIG[s.status]?.label ?? s.status)
          .fontSize(10)
          .fontColor(SHIP_STATUS_CONFIG[s.status]?.color ?? COLOR_TEXT_SUB)
          .backgroundColor(SHIP_STATUS_CONFIG[s.status]?.bg ?? COLOR_BG)
          .padding({ left: 9, right: 9, top: 3, bottom: 3 })
          .borderRadius(10)
      }
      .width('100%')

      Text(s.artName)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
        .margin({ top: 7 })
        .width('100%')

      Row() {
        Text(s.pickup).fontSize(10).fontColor(COLOR_TEXT_SUB).maxLines(1)
        Text(' → ').fontSize(10).fontColor(COLOR_GOLD)
        Text(s.destination).fontSize(10).fontColor(COLOR_TEXT_SUB).maxLines(1)
      }
      .width('100%')
      .margin({ top: 5 })

      Row() {
        Column().width(14).height(1).backgroundColor(COLOR_GOLD_LIGHT)
        Text('估值 ' + formatWan(s.value))
          .fontSize(11)
          .fontColor(COLOR_GOLD)
          .fontWeight(FontWeight.Medium)
          .margin({ left: 6 })
        Column().layoutWeight(1)
        Text('详情 ›')
          .fontSize(10)
          .fontColor(COLOR_TEXT_SUB)
          .padding({ left: 8, top: 3, bottom: 3 })
          .onClick(() => { this.detailHint = s.orderNo })
      }
      .width('100%')
      .margin({ top: 7 })
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
    .borderRadius(12)
    .border({ width: 1, color: COLOR_BORDER })
    .padding({ left: 14, right: 14, top: 12, bottom: 12 })
    .margin({ top: 8 })
  }

  // ========== 快捷预约表单内联块 ==========
  @Builder quickFormBuilder() {
    Column() {
      Text('快速预约')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
        .width('100%')
        .margin({ bottom: 10 })

      TextInput({ placeholder: '艺术品名' })
        .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
        .width('100%').height(38)
        .backgroundColor(COLOR_CARD)
        .borderRadius(8)
        .border({ width: 1, color: COLOR_BORDER })
        .onChange((v: string) => { this.formArtName = v })

      Row() {
        ForEach(HEADER_CATEGORIES.slice(1), (c: string) => {
          if (this.formCategory === c) {
            Text(c).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_DARK)
              .padding({ left: 9, right: 9, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
          } else {
            Text(c).fontSize(10).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
              .border({ width: 1, color: COLOR_BORDER })
              .padding({ left: 9, right: 9, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
              .onClick(() => { this.formCategory = c })
          }
        })
      }
      .margin({ top: 8 })

      TextInput({ placeholder: '估值(万元)' })
        .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
        .width('100%').height(38)
        .backgroundColor(COLOR_CARD)
        .borderRadius(8)
        .border({ width: 1, color: COLOR_BORDER })
        .margin({ top: 8 })
        .onChange((v: string) => { this.formValue = v })

      Row() {
        ForEach(PACK_OPTIONS, (p: string) => {
          if (this.formPack === p) {
            Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p).fontSize(10)
              .fontColor(COLOR_CARD).backgroundColor(COLOR_GOLD)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
          } else {
            Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p).fontSize(10)
              .fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
              .border({ width: 1, color: COLOR_BORDER })
              .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
              .onClick(() => { this.formPack = p })
          }
        })
      }
      .margin({ top: 8 })

      Row() {
        TextInput({ placeholder: '出发地' })
          .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
          .layoutWeight(1).height(38)
          .backgroundColor(COLOR_CARD)
          .borderRadius(8)
          .border({ width: 1, color: COLOR_BORDER })
          .onChange((v: string) => { this.formFrom = v })
        Text('→').fontSize(14).fontColor(COLOR_GOLD).margin({ left: 6, right: 6 })
        TextInput({ placeholder: '目的地' })
          .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
          .layoutWeight(1).height(38)
          .backgroundColor(COLOR_CARD)
          .borderRadius(8)
          .border({ width: 1, color: COLOR_BORDER })
          .onChange((v: string) => { this.formTo = v })
      }
      .margin({ top: 8 })

      Row() {
        ForEach(GUARD_LEVELS, (l: string) => {
          if (this.formLevel === l) {
            Text((GUARD_LEVEL_CONFIG[l]?.icon ?? '') + ' ' + l + '押运').fontSize(10)
              .fontColor(COLOR_CARD).backgroundColor(COLOR_DARK)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
          } else {
            Text((GUARD_LEVEL_CONFIG[l]?.icon ?? '') + ' ' + l + '押运').fontSize(10)
              .fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
              .border({ width: 1, color: COLOR_BORDER })
              .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(11)
              .margin({ left: 2, right: 2 })
              .onClick(() => { this.formLevel = l })
          }
        })
      }
      .margin({ top: 8 })

      Text('预约押运 · 恒温舱位')
        .fontSize(13).fontColor(COLOR_CARD)
        .backgroundColor(COLOR_DARK)
        .borderRadius(20)
        .padding({ left: 32, right: 32, top: 10, bottom: 10 })
        .margin({ top: 12 })
        .onClick(() => { this.showBookModal = true })

      if (this.bookResult !== '') {
        Text('已受理:' + this.bookResult + '(' + this.formLevel + '级 · ' + this.formPack + ')')
          .fontSize(10).fontColor(COLOR_SUCCESS).margin({ top: 8 })
      }
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
    .borderRadius(14)
    .border({ width: 1, color: COLOR_BORDER })
    .padding(14)
    .margin({ top: 10 })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 顶部统计条 =====
            Row() {
              Column() {
                Text(getShipMonthCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                Text('本月押运').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column().width(1).height(24).backgroundColor(COLOR_BORDER)
              Column() {
                Text(getShipOnWayCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_GOLD)
                Text('在途').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column().width(1).height(24).backgroundColor(COLOR_BORDER)
              Column() {
                Text(getShipDeliveredCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
                Text('已交付').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column().width(1).height(24).backgroundColor(COLOR_BORDER)
              Column() {
                Text(formatWan(getShipTotalValue())).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_PURPLE)
                Text('总估值').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(12)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ top: 12, bottom: 12 })
            .margin({ top: 4 })

            // ===== 押运环境监控大卡(反色深黑) =====
            Column() {
              Row() {
                Column()
                  .width(7).height(7)
                  .borderRadius(4)
                  .backgroundColor(COLOR_SUCCESS)
                Text('押运环境实时监控')
                  .fontSize(12)
                  .fontColor(COLOR_GOLD_LIGHT)
                  .fontWeight(FontWeight.Medium)
                  .margin({ left: 7 })
                Column().layoutWeight(1)
                Text('AG-2608-1201')
                  .fontSize(9)
                  .fontColor('#8A857C')
                  .letterSpacing(1)
              }
              .width('100%')

              Row() {
                this.envMetricBuilder('🌡️', '舱内温度', '20.0', '℃')
                Column().width(1).height(40).backgroundColor('#33322E')
                this.envMetricBuilder('💧', '舱内湿度', '55', '%RH')
                Column().width(1).height(40).backgroundColor('#33322E')
              }
              .width('100%')
              .margin({ top: 4 })

              Row() {
                this.envMetricBuilder('📳', '震动加速度', '0.02', 'G')
                Column().width(1).height(40).backgroundColor('#33322E')
                this.envMetricBuilder('🛰️', '定位状态', '锁定', '')
              }
              .width('100%')

              Row() {
                Column().width(10).height(1).backgroundColor(COLOR_GOLD)
                Text('恒温恒湿舱 · 气垫悬浮 · 全程安保在岗')
                  .fontSize(9)
                  .fontColor('#8A857C')
                  .margin({ left: 6 })
              }
              .width('100%')
              .margin({ top: 10, bottom: 12 })
              .padding({ left: 14, right: 14 })
            }
            .width('100%')
            .backgroundColor(COLOR_DARK)
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 14 })
            .margin({ top: 10 })

            // ===== 快捷预约表单 =====
            this.quickFormBuilder()

            // ===== 月度押运估值柱状图 =====
            Column() {
              Text('月度押运估值(万元)')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_TEXT_MAIN)
                .width('100%')
                .margin({ bottom: 12 })

              Row() {
                ForEach(monthlyShipValues, (v: number, i: number) => {
                  Column() {
                    Text(v.toString())
                      .fontSize(9)
                      .fontColor(COLOR_GOLD)
                      .margin({ bottom: 4 })
                    Column()
                      .width(24)
                      .height(getBarHeight(v, monthlyMax))
                      .borderRadius({ topLeft: 4, topRight: 4 })
                      .linearGradient({
                        angle: 180,
                        colors: [[COLOR_GOLD, 0], [COLOR_GOLD_LIGHT, 1]]
                      })
                    Text(monthlyLabels[i])
                      .fontSize(9)
                      .fontColor(COLOR_TEXT_HINT)
                      .margin({ top: 5 })
                  }
                  .layoutWeight(1)
                  .alignItems(HorizontalAlign.Center)
                })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(14)
            .border({ width: 1, color: COLOR_BORDER })
            .padding(14)
            .margin({ top: 10 })

            // ===== 押运单列表 =====
            Row() {
              Column().width(16).height(2).backgroundColor(COLOR_GOLD)
              Text('押运单')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_TEXT_MAIN)
                .margin({ left: 8 })
              Column().layoutWeight(1)
              Text('8 单')
                .fontSize(10)
                .fontColor(COLOR_TEXT_HINT)
            }
            .width('100%')
            .padding({ left: 4, right: 4, top: 16, bottom: 2 })

            if (this.detailHint !== '') {
              Text('正在追踪押运单:' + this.detailHint + '(恒温舱 温度20℃ 湿度55%)')
                .fontSize(10).fontColor(COLOR_GOLD)
                .margin({ top: 6 })
            }

            this.shipItemBuilder(mockShips[0])
            this.shipItemBuilder(mockShips[1])
            this.shipItemBuilder(mockShips[2])
            this.shipItemBuilder(mockShips[3])
            this.shipItemBuilder(mockShips[4])
            this.shipItemBuilder(mockShips[5])
            this.shipItemBuilder(mockShips[6])
            this.shipItemBuilder(mockShips[7])
          }
          .padding({ left: 12, right: 12, bottom: 24 })
          .alignItems(HorizontalAlign.Center)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .height('100%')

      if (this.showBookModal) { this.bookModal() }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab2 拍卖 AUCTION ============
@Component
struct AuctionContent {
  @State auctionSort: string = '即将结拍'
  @State showBidModal: boolean = false
  @State showWithdrawModal: boolean = false
  @State selectedLot: AuctionItem | null = null
  @State bidPrice: string = ''
  @State bidStep: number = 0
  @State withdrawHint: string = ''

  // ========== 弹框遮罩 ==========
  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(26,26,26,0.6)')
      .onClick(onClose)
  }

  // ========== 弹框3:撤回拍品确认(警示式,深黑底金字) ==========
  @Builder withdrawModal() {
    Column() {
      this.modalOverlay(() => { this.showWithdrawModal = false })
      Column() {
        Text('⚠').fontSize(40).margin({ top: 22 }).fontColor(COLOR_GOLD)
        Text('确认撤回此拍品?')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_GOLD_LIGHT)
          .margin({ top: 8 })
        Text('撤回后将立即终止竞价,保证金 15 个工作日内退还')
          .fontSize(11)
          .fontColor('#8A857C')
          .margin({ top: 6 })

        Column() {
          Row() {
            Text('拍品').fontSize(11).fontColor('#8A857C')
            Column().layoutWeight(1)
            Text(this.selectedLot?.name ?? '').fontSize(12).fontColor(COLOR_CARD).fontWeight(FontWeight.Medium)
          }
          .width('100%')
          Row() {
            Text('艺术家').fontSize(11).fontColor('#8A857C')
            Column().layoutWeight(1)
            Text(this.selectedLot?.artist ?? '').fontSize(11).fontColor(COLOR_GOLD_LIGHT)
          }
          .width('100%').margin({ top: 6 })
          Row() {
            Text('当前价').fontSize(11).fontColor('#8A857C')
            Column().layoutWeight(1)
            Text(formatWan(this.selectedLot?.currentPrice ?? 0))
              .fontSize(13).fontColor(COLOR_GOLD).fontWeight(FontWeight.Bold)
          }
          .width('100%').margin({ top: 6 })
        }
        .width('100%')
        .backgroundColor('#262523')
        .borderRadius(10)
        .padding(12)
        .margin({ top: 16 })

        Row() {
          Text('取消')
            .fontSize(13).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG)
            .borderRadius(18)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .onClick(() => { this.showWithdrawModal = false })
          Text('确认撤回')
            .fontSize(13).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_DANGER)
            .borderRadius(18)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => {
              this.withdrawHint = this.selectedLot?.name ?? ''
              this.showWithdrawModal = false
            })
        }
        .justifyContent(FlexAlign.Center)
        .margin({ top: 20, bottom: 20 })
      }
      .width('80%')
      .backgroundColor(COLOR_DARK)
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '36%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // ========== 弹框4:拍品出价(深黑底金色高亮) ==========
  @Builder bidModal() {
    Column() {
      this.modalOverlay(() => { this.showBidModal = false })
      Column() {
        Row() {
          Text('🔨 拍品出价')
            .fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_GOLD_LIGHT)
          Column().layoutWeight(1)
          Text('✕').fontSize(15).fontColor('#8A857C')
            .padding(6)
            .onClick(() => { this.showBidModal = false })
        }
        .width('100%').padding({ left: 16, right: 12, top: 14, bottom: 6 })

        Row() {
          Column() {
            Text('🖼️').fontSize(26)
          }
          .width(56).height(56)
          .backgroundColor(this.selectedLot?.color ?? COLOR_GOLD)
          .borderRadius(8)
          .alignItems(HorizontalAlign.Center)
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(this.selectedLot?.name ?? '')
              .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_CARD).maxLines(1)
            Text('Lot ' + (this.selectedLot?.lotNo ?? '') + ' · ' + (this.selectedLot?.artist ?? ''))
              .fontSize(10).fontColor('#8A857C').margin({ top: 4 })
            Text('结拍 ' + (this.selectedLot?.endTime ?? ''))
              .fontSize(9).fontColor(COLOR_GOLD).margin({ top: 3 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 12 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 8 })

        Row() {
          Text('当前价')
            .fontSize(11).fontColor('#8A857C')
            .margin({ bottom: 5 })
          Text(formatWan(this.selectedLot?.currentPrice ?? 0))
            .fontSize(30).fontColor(COLOR_GOLD)
            .margin({ left: 10 })
          Text('(' + (this.selectedLot?.bidCount ?? 0).toString() + ' 次出价)')
            .fontSize(10).fontColor('#8A857C')
            .margin({ left: 8, bottom: 6 })
        }
        .width('100%')
        .padding({ left: 16, top: 16 })
        .alignItems(VerticalAlign.Bottom)

        Text('我的出价(万元)')
          .fontSize(11).fontColor('#8A857C')
          .width('100%')
          .margin({ top: 14, left: 16 })

        TextInput({ placeholder: '输入出价金额', text: this.bidPrice })
          .placeholderColor('#5A574F')
          .fontSize(16)
          .fontColor(COLOR_GOLD)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .backgroundColor('#262523')
          .borderRadius(8)
          .border({ width: 1, color: '#3B3934' })
          .margin({ top: 6 })
          .onChange((v: string) => { this.bidPrice = v })

        Row() {
          ForEach(BID_STEPS, (step: number) => {
            Text('+' + step.toString())
              .fontSize(11)
              .fontColor(this.bidStep === step ? COLOR_DARK : COLOR_GOLD)
              .backgroundColor(this.bidStep === step ? COLOR_GOLD : '#262523')
              .borderRadius(12)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .margin({ left: 4, right: 4 })
              .onClick(() => {
                this.bidStep = step
                this.bidPrice = ((this.selectedLot?.currentPrice ?? 0) + step).toString()
              })
          })
        }
        .margin({ top: 10 })

        Row() {
          Column().width(10).height(1).backgroundColor(COLOR_GOLD)
          Text('参拍需缴纳拍品当前价 10% 保证金,未成交自动退还;成交后 7 日内完成交割,本平台提供恒温押运服务。')
            .fontSize(9)
            .fontColor('#8A857C')
            .lineHeight(14)
            .margin({ left: 6 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12 })
        .alignItems(VerticalAlign.Top)

        Text('确认出价')
          .fontSize(14).fontColor(COLOR_DARK)
          .backgroundColor(COLOR_GOLD)
          .borderRadius(22)
          .padding({ left: 48, right: 48, top: 11, bottom: 11 })
          .margin({ top: 14, bottom: 18 })
          .onClick(() => { this.showBidModal = false })
      }
      .width('88%')
      .height('55%')
      .backgroundColor(COLOR_DARK)
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '20%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // ========== 精选拍品大卡 ==========
  @Builder featuredCardBuilder(a: AuctionItem) {
    Column() {
      Column() {
        Text('🖼️').fontSize(34)
      }
      .width('100%')
      .height(90)
      .backgroundColor(a.color)
      .borderRadius(10)
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.Center)

      Text('Lot ' + a.lotNo)
        .fontSize(9)
        .fontColor(COLOR_GOLD)
        .letterSpacing(2)
        .margin({ top: 10 })
      Text(a.name)
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_CARD)
        .maxLines(1)
        .margin({ top: 3 })
      Text(a.artist)
        .fontSize(10)
        .fontColor('#8A857C')
        .margin({ top: 3 })

      Row() {
        Column() {
          Text('当前价').fontSize(8).fontColor('#8A857C')
          Text(formatWan(a.currentPrice))
            .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_GOLD)
            .margin({ top: 2 })
        }.alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Text('出价')
          .fontSize(11).fontColor(COLOR_DARK)
          .backgroundColor(COLOR_GOLD)
          .borderRadius(13)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
          .onClick(() => {
            this.selectedLot = a
            this.bidPrice = a.currentPrice.toString()
            this.bidStep = 0
            this.showBidModal = true
          })
      }
      .width('100%')
      .margin({ top: 10 })
      .alignItems(VerticalAlign.Bottom)
    }
    .width(190)
    .backgroundColor(COLOR_DARK)
    .borderRadius(14)
    .padding(12)
    .margin({ left: 6, right: 6 })
  }

  // ========== 拍品列表项(编号式拍卖行风格) ==========
  @Builder lotItemBuilder(a: AuctionItem) {
    Row() {
      Column() {
        Text(a.lotNo)
          .fontSize(26)
          .fontColor(COLOR_GOLD_LIGHT)
        Text('LOT')
          .fontSize(8)
          .fontColor(COLOR_TEXT_HINT)
          .letterSpacing(2)
          .margin({ top: 2 })
      }
      .width(64)
      .alignItems(HorizontalAlign.Center)

      Column() {
        Text(a.name)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
          .maxLines(1)
          .width('100%')
        Text(a.artist)
          .fontSize(10)
          .fontColor(COLOR_TEXT_SUB)
          .margin({ top: 3 })
          .width('100%')
        Row() {
          Text('起拍 ' + formatWan(a.startPrice)).fontSize(9).fontColor(COLOR_TEXT_HINT)
          Text('当前 ').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ left: 10 })
          Text(formatWan(a.currentPrice)).fontSize(11).fontColor(COLOR_GOLD).fontWeight(FontWeight.Bold)
          Text(a.bidCount.toString() + ' 次出价').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ left: 10 })
        }
        .margin({ top: 6 })
        Row() {
          Text('⏳ 结拍 ' + a.endTime).fontSize(9).fontColor(COLOR_WARNING)
        }
        .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10 })

      Column() {
        Text('出价')
          .fontSize(11).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_DARK)
          .borderRadius(13)
          .padding({ left: 15, right: 15, top: 6, bottom: 6 })
          .onClick(() => {
            this.selectedLot = a
            this.bidPrice = a.currentPrice.toString()
            this.bidStep = 0
            this.showBidModal = true
          })
        Text('撤回')
          .fontSize(10).fontColor(COLOR_TEXT_HINT)
          .border({ width: 1, color: COLOR_BORDER })
          .borderRadius(11)
          .padding({ left: 12, right: 12, top: 4, bottom: 4 })
          .margin({ top: 6 })
          .onClick(() => {
            this.selectedLot = a
            this.showWithdrawModal = true
          })
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
    .borderRadius(12)
    .border({ width: 1, color: COLOR_BORDER })
    .padding({ left: 12, right: 12, top: 12, bottom: 12 })
    .margin({ top: 8 })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 精选拍品大卡横滑区 =====
            Row() {
              Column().width(16).height(2).backgroundColor(COLOR_GOLD)
              Text('本季精选')
                .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .margin({ left: 8 })
              Column().layoutWeight(1)
              Text('秋季拍卖会 · 08.28')
                .fontSize(9).fontColor(COLOR_TEXT_HINT)
            }
            .width('100%')
            .padding({ left: 4, right: 4, top: 4, bottom: 6 })

            Scroll() {
              Row() {
                this.featuredCardBuilder(featuredLots[0])
                this.featuredCardBuilder(featuredLots[1])
                this.featuredCardBuilder(featuredLots[2])
              }
              .padding({ left: 6, right: 6 })
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
            .width('100%')

            // ===== 排序药丸 =====
            Row() {
              ForEach(SORT_OPTIONS, (s: string) => {
                if (this.auctionSort === s) {
                  Text(s).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_DARK)
                    .padding({ left: 13, right: 13, top: 6, bottom: 6 }).borderRadius(13)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 13, right: 13, top: 6, bottom: 6 }).borderRadius(13)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.auctionSort = s })
                }
              })
            }
            .margin({ top: 10, bottom: 4 })

            if (this.withdrawHint !== '') {
              Text('已撤回拍品:' + this.withdrawHint)
                .fontSize(10).fontColor(COLOR_DANGER)
                .margin({ top: 6 })
            }

            // ===== 出价热度进度条 =====
            Column() {
              Text('出价热度')
                .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .width('100%').margin({ bottom: 10 })
              Column() {
                ForEach(mockAuctions, (a: AuctionItem) => {
                  Column() {
                    Row() {
                      Text('Lot ' + a.lotNo).fontSize(10).fontColor(COLOR_TEXT_SUB)
                      Column().layoutWeight(1)
                      Text(a.bidCount.toString() + ' 次').fontSize(10).fontColor(COLOR_GOLD)
                    }
                    .width('100%')
                    Row() {
                      Column()
                        .width(getBidHeatWidth(a.bidCount) + '%')
                        .height(5)
                        .backgroundColor(COLOR_GOLD)
                        .borderRadius(3)
                      Column().layoutWeight(1)
                    }
                    .width('100%')
                    .margin({ top: 4 })
                  }
                  .width('100%')
                  .margin({ top: 5 })
                })
              }
              .width('100%')
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(14)
            .border({ width: 1, color: COLOR_BORDER })
            .padding(14)
            .margin({ top: 10 })

            // ===== 拍品列表 =====
            Row() {
              Column().width(16).height(2).backgroundColor(COLOR_GOLD)
              Text('全部拍品')
                .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .margin({ left: 8 })
              Column().layoutWeight(1)
              Text('8 件')
                .fontSize(10).fontColor(COLOR_TEXT_HINT)
            }
            .width('100%')
            .padding({ left: 4, right: 4, top: 16, bottom: 2 })

            this.lotItemBuilder(mockAuctions[0])
            this.lotItemBuilder(mockAuctions[1])
            this.lotItemBuilder(mockAuctions[2])
            this.lotItemBuilder(mockAuctions[3])
            this.lotItemBuilder(mockAuctions[4])
            this.lotItemBuilder(mockAuctions[5])
            this.lotItemBuilder(mockAuctions[6])
            this.lotItemBuilder(mockAuctions[7])
          }
          .padding({ left: 12, right: 12, bottom: 24 })
          .alignItems(HorizontalAlign.Center)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .height('100%')

      if (this.showWithdrawModal) { this.withdrawModal() }
      if (this.showBidModal) { this.bidModal() }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab3 藏品 GALLERY ============
@Component
struct GalleryContent {
  @State showEditModal: boolean = false
  @State selectedCollection: CollectionItem | null = null
  @State editName: string = ''
  @State editArtist: string = ''
  @State editEra: string = ''
  @State editValue: string = ''
  @State editRarity: string = '佳作'
  @State editCondition: string = '恒温库房'
  @State editNotes: string = ''
  @State saveHint: string = ''

  // ========== 弹框遮罩 ==========
  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(26,26,26,0.55)')
      .onClick(onClose)
  }

  // ========== 弹框2:编辑藏品档案(展签卡片式) ==========
  @Builder editModal() {
    Column() {
      this.modalOverlay(() => { this.showEditModal = false })
      Column() {
        // 展签风格头部
        Column() {
          Text('✏️ 编辑藏品档案')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_TEXT_MAIN)
            .margin({ top: 14 })
          Text('ARCHIVE · ' + (this.selectedCollection?.name ?? ''))
            .fontSize(9)
            .fontColor(COLOR_TEXT_HINT)
            .letterSpacing(1)
            .margin({ top: 4, bottom: 10 })
          Column().width(36).height(2).backgroundColor(COLOR_GOLD)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Center)

        Scroll() {
          Column() {
            Text('藏品名').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 10 })
            TextInput({ placeholder: this.selectedCollection?.name ?? '' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.editName = v })

            Text('艺术家').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextInput({ placeholder: this.selectedCollection?.artist ?? '' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
              .width('100%').height(38)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5 })
              .onChange((v: string) => { this.editArtist = v })

            Row() {
              Column() {
                Text('年代').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%')
                TextInput({ placeholder: this.selectedCollection?.era ?? '' })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
                  .width('100%').height(38)
                  .backgroundColor(COLOR_BG)
                  .borderRadius(6)
                  .border({ width: 1, color: COLOR_BORDER })
                  .margin({ top: 5 })
                  .onChange((v: string) => { this.editEra = v })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)

              Column() {
                Text('估值(万元)').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%')
                TextInput({ placeholder: this.selectedCollection?.value.toString() ?? '' })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(13)
                  .width('100%').height(38)
                  .backgroundColor(COLOR_BG)
                  .borderRadius(6)
                  .border({ width: 1, color: COLOR_BORDER })
                  .margin({ top: 5 })
                  .onChange((v: string) => { this.editValue = v })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
            }
            .width('100%')
            .margin({ top: 12 })

            Text('稀有度').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            Row() {
              ForEach(RARITY_OPTIONS, (r: string) => {
                if (this.editRarity === r) {
                  Text(r).fontSize(11).fontColor(COLOR_CARD)
                    .backgroundColor(RARITY_CONFIG[r]?.color ?? COLOR_TEXT_SUB)
                    .padding({ left: 11, right: 11, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(r).fontSize(11).fontColor(RARITY_CONFIG[r]?.color ?? COLOR_TEXT_SUB)
                    .backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 11, right: 11, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.editRarity = r })
                }
              })
            }
            .margin({ top: 5 })

            Text('保存状态').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            Row() {
              ForEach(CONDITION_OPTIONS, (c: string) => {
                if (this.editCondition === c) {
                  Text(c).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_PURPLE)
                    .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(c).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.editCondition = c })
                }
              })
            }
            .margin({ top: 5 })

            Text('备注').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
            TextArea({ placeholder: '保存环境 / 修复记录 / 展览历史...' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
              .width('100%').height(60)
              .backgroundColor(COLOR_BG)
              .borderRadius(6)
              .border({ width: 1, color: COLOR_BORDER })
              .margin({ top: 5, bottom: 12 })
              .onChange((v: string) => { this.editNotes = v })
          }
          .padding({ left: 18, right: 18 })
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)

        Row() {
          Text('取消')
            .fontSize(13).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG)
            .border({ width: 1, color: COLOR_BORDER })
            .borderRadius(20)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .onClick(() => { this.showEditModal = false })
          Text('保存')
            .fontSize(13).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_GOLD)
            .borderRadius(20)
            .padding({ left: 30, right: 30, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => {
              this.saveHint = this.editName !== '' ? this.editName : (this.selectedCollection?.name ?? '')
              this.showEditModal = false
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding({ top: 10, bottom: 16 })
      }
      .width('85%')
      .height('60%')
      .backgroundColor(COLOR_CARD)
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '7.5%', y: '18%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // ========== 藏品卡(美术馆展签风格) ==========
  @Builder galleryCardBuilder(c: CollectionItem) {
    Column() {
      Column() {
        Text('🖼️').fontSize(30)
        if (c.rarity === '孤品') {
          Text('孤品')
            .fontSize(8).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_DANGER)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(7)
            .position({ x: '72%', y: 6 })
        }
      }
      .width('100%')
      .height(86)
      .backgroundColor(c.color)
      .borderRadius(8)
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.Center)

      // 展签
      Column() {
        Text(c.name)
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
          .maxLines(1)
          .width('100%')
        Text(c.artist + ' · ' + c.era)
          .fontSize(9)
          .fontColor(COLOR_TEXT_SUB)
          .maxLines(1)
          .width('100%')
          .margin({ top: 3 })
        Row() {
          Text(formatWan(c.value))
            .fontSize(10)
            .fontColor(COLOR_GOLD)
            .fontWeight(FontWeight.Medium)
          Column().layoutWeight(1)
          Text(RARITY_CONFIG[c.rarity]?.label ?? c.rarity)
            .fontSize(8)
            .fontColor(RARITY_CONFIG[c.rarity]?.color ?? COLOR_TEXT_SUB)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 6, right: 6, top: 1, bottom: 1 })
            .borderRadius(7)
        }
        .width('100%')
        .margin({ top: 6 })
      }
      .width('100%')
      .padding({ top: 8 })
      .alignItems(HorizontalAlign.Start)
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(10)
    .border({ width: 1, color: COLOR_BORDER })
    .padding(8)
    .margin({ left: 3, right: 3, top: 6 })
    .onClick(() => {
      this.selectedCollection = c
      this.editName = c.name
      this.editArtist = c.artist
      this.editRarity = c.rarity
      this.showEditModal = true
    })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 收藏统计条 =====
            Row() {
              Column() {
                Text(getCollectionTotal().toString()).fontSize(20).fontColor(COLOR_TEXT_MAIN)
                Text('总藏品').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column().width(1).height(26).backgroundColor(COLOR_BORDER)
              Column() {
                Text(formatWan(getCollectionValue())).fontSize(20).fontColor(COLOR_GOLD)
                Text('总估值').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column().width(1).height(26).backgroundColor(COLOR_BORDER)
              Column() {
                Text('+' + getCollectionMonthNew().toString()).fontSize(20).fontColor(COLOR_PURPLE)
                Text('本月新增').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(12)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ top: 14, bottom: 14 })
            .margin({ top: 4 })

            // ===== 品类分布横向进度条 =====
            Column() {
              Text('品类分布')
                .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .width('100%').margin({ bottom: 10 })
              ForEach(categoryDistLabels, (label: string, i: number) => {
                Column() {
                  Row() {
                    Text((ART_CATEGORY_CONFIG[label]?.icon ?? '') + ' ' + label)
                      .fontSize(10).fontColor(COLOR_TEXT_SUB)
                    Column().layoutWeight(1)
                    Text(categoryDistCounts[i].toString() + ' 件')
                      .fontSize(10).fontColor(COLOR_GOLD)
                  }
                  .width('100%')
                  Row() {
                    Column()
                      .width(getDistWidth(categoryDistCounts[i]) + '%')
                      .height(5)
                      .backgroundColor(i % 2 === 0 ? COLOR_GOLD : COLOR_PURPLE)
                      .borderRadius(3)
                    Column().layoutWeight(1)
                  }
                  .width('100%')
                  .margin({ top: 4 })
                }
                .width('100%')
                .margin({ top: 5 })
              })
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(14)
            .border({ width: 1, color: COLOR_BORDER })
            .padding(14)
            .margin({ top: 10 })

            if (this.saveHint !== '') {
              Text('档案已更新:' + this.saveHint)
                .fontSize(10).fontColor(COLOR_SUCCESS)
                .margin({ top: 10 })
            }

            // ===== 2列藏品网格 =====
            Row() {
              Column().width(16).height(2).backgroundColor(COLOR_GOLD)
              Text('藏品陈列')
                .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .margin({ left: 8 })
              Column().layoutWeight(1)
              Text('点击展签编辑档案')
                .fontSize(9).fontColor(COLOR_TEXT_HINT)
            }
            .width('100%')
            .padding({ left: 4, right: 4, top: 14, bottom: 2 })

            ForEach(mockCollections, (c: CollectionItem, idx: number) => {
              if (idx % 2 === 0) {
                Row() {
                  this.galleryCardBuilder(mockCollections[idx])
                  this.galleryCardBuilder(mockCollections[idx + 1])
                }
                .width('100%')
              }
            })
          }
          .padding({ left: 10, right: 10, bottom: 24 })
          .alignItems(HorizontalAlign.Center)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .height('100%')

      if (this.showEditModal) { this.editModal() }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab4 展览 EXHIBIT ============
@Component
struct ExhibitContent {
  @State logisticsHint: string = ''

  // ========== 时间线展览项 ==========
  @Builder timelineItemBuilder(e: ExhibitionItem, isLast: boolean) {
    Row() {
      // 时间线节点列
      Stack({ alignContent: Alignment.Top }) {
        Column()
          .width(1)
          .height(isLast ? 24 : '100%')
          .backgroundColor(COLOR_BORDER)
        Column()
          .width(13)
          .height(13)
          .borderRadius(7)
          .backgroundColor(e.status === '展出中' ? COLOR_GOLD : COLOR_GOLD_LIGHT)
          .border({ width: 3, color: COLOR_BG })
          .margin({ top: 6 })
      }
      .width(24)
      .alignContent(Alignment.Top)

      // 展览卡
      Column() {
        Row() {
          Text(e.name)
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_TEXT_MAIN)
            .maxLines(1)
            .layoutWeight(1)
          Text(EXHIBIT_STATUS_CONFIG[e.status]?.label ?? e.status)
            .fontSize(9)
            .fontColor(EXHIBIT_STATUS_CONFIG[e.status]?.color ?? COLOR_TEXT_SUB)
            .backgroundColor(EXHIBIT_STATUS_CONFIG[e.status]?.bg ?? COLOR_BG)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .borderRadius(9)
        }
        .width('100%')

        Row() {
          Text('🏛 ' + e.venue).fontSize(10).fontColor(COLOR_TEXT_SUB).maxLines(1).layoutWeight(1)
        }
        .width('100%')
        .margin({ top: 5 })

        Row() {
          Text('📅 ' + e.startDate + ' — ' + e.endDate)
            .fontSize(10).fontColor(COLOR_TEXT_HINT)
          Column().layoutWeight(1)
          Text(e.artworkCount.toString() + ' 件展品')
            .fontSize(10).fontColor(COLOR_GOLD)
            .fontWeight(FontWeight.Medium)
        }
        .width('100%')
        .margin({ top: 5 })

        Row() {
          Column().width(10).height(1).backgroundColor(COLOR_GOLD_LIGHT)
          Text('展品运输由艺境押运全程承运')
            .fontSize(8)
            .fontColor(COLOR_TEXT_HINT)
            .margin({ left: 5 })
          Column().layoutWeight(1)
          Text('物流单 ›')
            .fontSize(10)
            .fontColor(COLOR_CARD)
            .backgroundColor(COLOR_DARK)
            .borderRadius(11)
            .padding({ left: 12, right: 12, top: 5, bottom: 5 })
            .onClick(() => { this.logisticsHint = e.name })
        }
        .width('100%')
        .margin({ top: 9 })
        .alignItems(VerticalAlign.Center)
      }
      .layoutWeight(1)
      .backgroundColor(COLOR_CARD)
      .borderRadius(12)
      .border({ width: 1, color: COLOR_BORDER })
      .padding({ left: 12, right: 12, top: 12, bottom: 12 })
      .margin({ bottom: 10 })
    }
    .width('100%')
    .alignItems(VerticalAlign.Top)
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 进行中展览大数字 =====
          Column() {
            Row() {
              Column() {
                Text(getOngoingExhibitCount().toString())
                  .fontSize(44)
                  .fontColor(COLOR_GOLD)
                Text('场展览正在进行')
                  .fontSize(12)
                  .fontColor(COLOR_TEXT_SUB)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Center)

              Column() {
                Text('EXHIBITION')
                  .fontSize(9)
                  .fontColor(COLOR_TEXT_HINT)
                  .letterSpacing(3)
                Column().width(24).height(1).backgroundColor(COLOR_GOLD).margin({ top: 5 })
                Text('2026 秋季展览季')
                  .fontSize(10)
                  .fontColor(COLOR_TEXT_SUB)
                  .margin({ top: 6 })
                Text('布展 2 场 · 撤展 2 场')
                  .fontSize(9)
                  .fontColor(COLOR_TEXT_HINT)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.End)
              .layoutWeight(1)
            }
            .width('100%')
            .padding({ left: 20, right: 20 })
            .alignItems(VerticalAlign.Center)
          }
          .width('100%')
          .backgroundColor(COLOR_DARK)
          .borderRadius(14)
          .padding({ top: 20, bottom: 20 })
          .margin({ top: 4 })

          if (this.logisticsHint !== '') {
            Text('已打开物流单:' + this.logisticsHint)
              .fontSize(10).fontColor(COLOR_SUCCESS)
              .margin({ top: 10 })
          }

          // ===== 展览时间线 =====
          Row() {
            Column().width(16).height(2).backgroundColor(COLOR_GOLD)
            Text('展览时间线')
              .fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
              .margin({ left: 8 })
            Column().layoutWeight(1)
            Text('8 场')
              .fontSize(10).fontColor(COLOR_TEXT_HINT)
          }
          .width('100%')
          .padding({ left: 4, right: 4, top: 14, bottom: 8 })

          this.timelineItemBuilder(mockExhibitions[0], false)
          this.timelineItemBuilder(mockExhibitions[1], false)
          this.timelineItemBuilder(mockExhibitions[2], false)
          this.timelineItemBuilder(mockExhibitions[3], false)
          this.timelineItemBuilder(mockExhibitions[4], false)
          this.timelineItemBuilder(mockExhibitions[5], false)
          this.timelineItemBuilder(mockExhibitions[6], false)
          this.timelineItemBuilder(mockExhibitions[7], true)
        }
        .padding({ left: 12, right: 12, bottom: 24 })
        .alignItems(HorizontalAlign.Center)
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab5 我的 PROFILE ============
@Component
struct ProfileContent {
  @State profileHint: string = ''

  @Builder statCellBuilder(icon: string, value: string, label: string, color: string) {
    Column() {
      Text(icon).fontSize(18)
      Text(value)
        .fontSize(22)
        .fontColor(color)
        .margin({ top: 5 })
      Text(label)
        .fontSize(10)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 3 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 16, bottom: 16 })
    .backgroundColor(COLOR_CARD)
    .borderRadius(12)
    .border({ width: 1, color: COLOR_BORDER })
  }

  @Builder settingRowBuilder(icon: string, label: string, hint: string) {
    Row() {
      Text(icon).fontSize(17)
      Text(label)
        .fontSize(13)
        .fontColor(COLOR_TEXT_MAIN)
        .margin({ left: 12 })
        .layoutWeight(1)
      Text(hint)
        .fontSize(10)
        .fontColor(COLOR_TEXT_HINT)
      Text('›')
        .fontSize(15)
        .fontColor(COLOR_TEXT_HINT)
        .margin({ left: 8 })
    }
    .width('100%')
    .padding({ top: 14, bottom: 14 })
    .onClick(() => { this.profileHint = label })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 米白资料卡 =====
          Column() {
            Row() {
              Column() {
                Text('🎩').fontSize(34)
              }
              .width(64)
              .height(64)
              .borderRadius(32)
              .backgroundColor(COLOR_GOLD_LIGHT)
              .border({ width: 2, color: COLOR_GOLD })
              .alignItems(HorizontalAlign.Center)
              .justifyContent(FlexAlign.Center)

              Column() {
                Row() {
                  Text('沈亦藏')
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLOR_TEXT_MAIN)
                  Text('鉴藏家 · 金')
                    .fontSize(9)
                    .fontColor(COLOR_GOLD)
                    .backgroundColor('#F4EBDD')
                    .border({ width: 1, color: COLOR_GOLD_LIGHT })
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                    .borderRadius(9)
                    .margin({ left: 8 })
                }
                .alignItems(VerticalAlign.Center)
                Text('艺术品收藏 9 年 · 藏家编号 AG-000318')
                  .fontSize(10)
                  .fontColor(COLOR_TEXT_SUB)
                  .margin({ top: 5 })
                Text('注册 1247 天')
                  .fontSize(10)
                  .fontColor(COLOR_TEXT_HINT)
                  .margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .padding({ left: 14 })
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Row() {
              Column().width(22).height(1).backgroundColor(COLOR_GOLD)
              Text('艺境押运黑金会员 · 专属恒温舱位优先权')
                .fontSize(9)
                .fontColor(COLOR_GOLD)
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 14 })
            .padding({ left: 4 })
          }
          .width('100%')
          .backgroundColor(COLOR_CARD)
          .borderRadius(14)
          .border({ width: 1, color: COLOR_BORDER })
          .padding(16)
          .margin({ top: 4 })

          // ===== 2x2 统计网格 =====
          Row() {
            this.statCellBuilder('🚛', '86', '总押运(单)', COLOR_TEXT_MAIN)
            Column().width(8)
            this.statCellBuilder('🔨', '34', '总拍品(件)', COLOR_GOLD)
          }
          .width('100%')
          .margin({ top: 10 })

          Row() {
            this.statCellBuilder('💎', formatWan(9680), '总估值', COLOR_PURPLE)
            Column().width(8)
            this.statCellBuilder('✦', '12,860', '积分', COLOR_WARNING)
          }
          .width('100%')
          .margin({ top: 8 })

          if (this.profileHint !== '') {
            Text('已进入:' + this.profileHint)
              .fontSize(10)
              .fontColor(COLOR_SUCCESS)
              .margin({ top: 10 })
          }

          // ===== 设置列表 =====
          Column() {
            Text('藏品与账户')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_TEXT_MAIN)
              .width('100%')
              .margin({ bottom: 4 })

            Column() {
              this.settingRowBuilder('🖼️', '我的藏品', '162 件')
              Divider().color(COLOR_BG)
              this.settingRowBuilder('📍', '常用地址', '6 个')
              Divider().color(COLOR_BG)
              this.settingRowBuilder('🛡️', '保额管理', '总额 ¥9,680万')
              Divider().color(COLOR_BG)
              this.settingRowBuilder('☎️', '客服', '7×24h')
              Divider().color(COLOR_BG)
              this.settingRowBuilder('⚙️', '设置', '')
            }
            .width('100%')
            .backgroundColor(COLOR_CARD)
            .borderRadius(14)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 14, right: 14, top: 4, bottom: 4 })
            .margin({ top: 8 })
          }
          .width('100%')
          .margin({ top: 16 })

          // ===== 深色结尾卡 =====
          Column() {
            Text('艺境押运 ART GUARD')
              .fontSize(11)
              .fontColor(COLOR_GOLD_LIGHT)
              .letterSpacing(2)
            Text('让每一件艺术品,都被温柔以待')
              .fontSize(9)
              .fontColor('#8A857C')
              .margin({ top: 6 })
          }
          .width('100%')
          .backgroundColor(COLOR_DARK)
          .borderRadius(12)
          .padding({ top: 18, bottom: 18 })
          .margin({ top: 16 })
          .alignItems(HorizontalAlign.Center)

          Text('v1.0.0 · © 2026 艺境押运')
            .fontSize(9)
            .fontColor(COLOR_TEXT_HINT)
            .margin({ top: 14, bottom: 8 })
        }
        .padding({ left: 12, right: 12, bottom: 24 })
        .alignItems(HorizontalAlign.Center)
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
  }
}


五、结语:

纵观"艺境押运 ART GUARD"平台的完整实现,我们可以清晰地看到 HarmonyOS 6.1.1 上 ArkTS API 24 声明式 UI 范式在复杂业务应用中的工程价值。整个项目以一套统一的装饰器体系(@Entry@Component@State@Observed@Builder)驱动了五大功能模块、十余个弹框与卡片组件、四种数据可视化图表和一套全局粒子动画系统,代码结构从颜色常量到数据模型、从配置映射到静态数据、从辅助函数到主页面入口、从各 Tab 内容到弹框 Builder,层次分明、职责清晰。这种以"状态驱动渲染"为核心的开发范式,使开发者无需手动操作 DOM 节点,只需声明界面在任何状态下的样子,框架自动完成差异更新。对于艺术品押运这类需要频繁状态切换(Tab 路由、表单输入、弹框交互、出价竞拍)的应用场景,声明式范式的开发效率和代码可维护性优势尤为突出。

在这里插入图片描述

从类型系统的角度看,ArkTS 在 TypeScript 基础上引入的 @Observed 装饰器和 interface + implements 的双层类型设计,为状态管理提供了坚实的类型安全保障。本项目定义了四个数据模型接口(ArtShipModelAuctionItemModelCollectionModelExhibitionModel)作为类型契约,再通过四个 @Observed 类实现可观测的数据实例,编译器在构建阶段即可捕获类型不匹配错误。同时,Record<string, Meta> 配置映射表配合 ?. 可选链和 ?? 空值合并运算符,使状态元数据(标签、颜色、图标)的查找既类型安全又防御性完备。这种"编译时类型检查 + 运行时空值防护"的双重保障,是 ArkTS 区别于普通 JavaScript 框架的重要工程优势,特别适合艺术品行业这类对数据准确性要求极高的领域。

在数据可视化与动画方面,本项目展示了 ArkTS 原生组件的强大表现力。月度押运估值柱状图通过 ForEach + Column + linearGradient 实现,无需引入任何图表库;出价热度和品类分布进度条通过百分比宽度 Column 实现;展览时间线通过 Stack 嵌套叠加竖线和节点实现;金色光尘粒子通过 setInterval 定时器驱动 @State 数组更新,配合 position 百分比定位和 hitTestBehavior(HitTestMode.None) 事件穿透,实现了全局覆盖又不影响交互的视觉特效。这些实现证明,在 HarmonyOS ArkTS API 24 中,纯声明式组件已经能够覆盖绝大多数常见的 UI 可视化需求,减少了对第三方依赖的引入,有利于应用体积控制和性能优化。对于艺术品押运平台这类需要同时呈现数据报表、实时监控和品牌视觉的应用,这种"原生即足够"的能力尤为珍贵。

展望未来,随着 HarmonyOS 生态的持续演进和 ArkTS 语言能力的不断扩展,声明式 UI 范式将在更多垂直行业应用中发挥价值。本项目所展现的"颜色常量体系—接口契约—@Observed 可观测类—Record 配置映射—参数化 @Builder—条件路由—弹框分层"这一完整工程模式,可以作为任何行业级 ArkTS 应用的架构参考模板。对于艺术品押运这一细分领域,后续可进一步扩展的方向包括:接入 HarmonyOS 分布式能力实现跨设备押运监控、利用 ArkTS 的 @Consume / @Provide 实现跨层级状态共享、引入 @Watch 装饰器监听押运状态变化的副作用逻辑、整合 HarmonyOS 传感器 API 获取真实的温湿度和震动数据等。"艺境押运 ART GUARD"不仅是一个功能完备的艺术品流通管理平台,更是 HarmonyOS 6.1.1 ArkTS API 24 声明式开发范式的一次深度实践与验证。

Logo

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

更多推荐