HarmonyOS ArkTS API 24 dustLayerBuilder 使用 Text(‘✦‘) 星形字符作为粒子,奇偶索引交替,通过.position() 以百分比定位在屏幕上
一、引言:鸿蒙生态中的艺术品押运平台技术全景
鸿蒙操作系统(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 子组件(ShipContent、AuctionContent、GalleryContent、ExhibitContent、ProfileContent)动态切换到内容区域。这种"状态驱动—组件拆分—条件路由"的架构模式,正是 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 装饰器则应用于 ArtShipItem、AuctionItem、CollectionItem、ExhibitionItem 四个数据类,使它们的属性变化能够被框架追踪。特别值得关注的是,每个 @Observed 类都通过 implements 关键字实现了对应的 interface 接口(如 ArtShipItem implements ArtShipModel),这种"接口定义契约 + 观测类实现数据"的双层设计,既保证了类型安全,又实现了状态可观测,是 ArkTS 工程化实践中的优秀范式。
从艺术品押运平台的业务架构来看,整个应用围绕"艺术品流通全链路"展开,划分为五大功能模块:押运管理(SHIP)负责艺术品运输订单的全生命周期跟踪,包含恒温恒湿环境实时监控、防震气垫舱状态、押运等级分类;拍卖管理(AUCTION)提供在线竞价平台,支持出价步进、撤回拍品、出价热度可视化;藏品管理(GALLERY)以美术馆展签风格展示藏品档案,支持档案编辑与稀有度分级;展览管理(EXHIBIT)以时间线形式追踪展览的布展、展出、撤展全流程;个人中心(PROFILE)汇总用户押运、拍卖、估值等数据统计。这五大模块通过底部 Tab 栏统一调度,各模块内部又通过 @Builder 方法将 UI 片段模块化拆分,形成"入口页面—功能模块—Builder 片段—弹框交互"的四级层次结构。此外,全应用覆盖一层金色光尘粒子特效,在视觉层面呼应了美术馆极简黑白金的设计语言,展现了 ArkTS 动画与渲染能力的工程化运用。
二、应用整体架构流程图
三、逐段代码深度解析
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_SUCCESS、COLOR_WARNING、COLOR_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。每个字段的类型都被严格声明为 number 或 string,编译器会在类型不匹配时报错。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 的重新渲染。在本项目中,四个数据类 ArtShipItem、AuctionItem、CollectionItem、ExhibitionItem 都使用了 @Observed 装饰,并同时通过 implements 关键字实现了上一步定义的接口。以 ArtShipItem 为例,它在类内部为每个属性提供了默认初始值(如 status: string = '待包装'),这保证了即使构造函数未完整赋值,对象也处于合法状态。构造函数 constructor 接收所有参数并进行批量赋值,使得创建实例时可以一行完成。这种"接口定义契约 + @Observed 类实现可观测 + 构造函数便捷实例化"的三层设计,是 ArkTS 状态管理的标准工程模式。值得注意的是,AuctionItem 的 color 属性默认值为 '#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 中"配置映射"模式的优雅实现。首先定义了 ShipStatusMeta、CategoryMeta、PackMeta、GuardLevelMeta、RarityMeta、ExhibitStatusMeta 等元数据接口,每个接口描述了对应配置项所需的属性集合。随后,使用 Record<string, XxxMeta> 类型创建了六个配置映射表:SHIP_STATUS_CONFIG 将押运状态字符串映射为标签、文字色、背景色三元组;PACK_CONFIG 将包装方式映射为标签、图标、描述;GUARD_LEVEL_CONFIG 将押运等级映射为标签、图标符号(◇/◈/◆ 递进)和颜色。这种设计模式的核心优势在于"以查表代替分支"——在 UI 渲染时,只需通过 SHIP_STATUS_CONFIG[s.status]?.label 即可获取状态标签,无需编写冗长的 if-else 或 switch 分支。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.SHIP;selectedCategory 管理品类筛选状态,初始为"全部";searchKeyword 绑定搜索框输入;unreadCount 管理未读消息角标数字;dustX、dustY、dustS、dustO 四个数组分别存储九个金色光尘粒子的横坐标、纵坐标、字号和透明度。在 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.dustX 和 this.dustY,触发 @State 状态更新,驱动 dustLayerBuilder 中的 ForEach 重新渲染。dustLayerBuilder 使用 Text('✦') 星形字符作为粒子,奇偶索引交替使用 COLOR_GOLD 和 COLOR_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 在图表场景下的灵活性。
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.selectedLot(AuctionItem | null 类型)作为数据源,通过可选链 ?. 安全访问拍品属性,配合 ?? '' 空值合并运算符提供默认值,防止空指针异常。弹框头部展示拍品缩略图(使用 selectedLot.color 作为背景色)、拍品名称、Lot 编号与艺术家、结拍时间。当前价格使用 30 像素金色大字突出展示,配合出价次数说明。出价输入区域包含一个 TextInput 和四个加价步进按钮(BID_STEPS = [500, 1000, 5000, 10000]),点击步进按钮时同时更新 bidStep(记录选中的步进值)和 bidPrice(计算当前价加步进值),步进按钮的选中态通过 this.bidStep === step 三元表达式控制颜色反转——选中时金底深字,未选中时深底金字。TextInput 的 text 参数绑定 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 的双层类型设计,为状态管理提供了坚实的类型安全保障。本项目定义了四个数据模型接口(ArtShipModel、AuctionItemModel、CollectionModel、ExhibitionModel)作为类型契约,再通过四个 @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 声明式开发范式的一次深度实践与验证。
更多推荐



所有评论(0)