一、技术概述

在 HarmonyOS 6.1.1 的全场景分布式生态中,ArkTS 作为核心声明式开发语言已经演进至 API 24 阶段,为开发者提供了更为强大的状态管理能力与组件化构建体系。本文以一款名为"古着衣橱 VINTAGE VAULT"的复古服饰恒温运输加二手潮流商城平台为例,深入剖析其基于 HarmonyOS ArkTS API 24 的完整实现方案。该应用覆盖了古着服饰恒温恒湿专业运输、二手古着商城交易、穿搭社区互动三大核心业务场景,采用复古波普奶油风的视觉设计语言(奶油底色搭配复古棕紫与芥末砖红点缀),在移动端呈现了一套完整的商业级交互体验。通过对该源码的逐行拆解,读者可以系统掌握 ArkTS 在状态驱动、声明式 UI、组件复用等方面的工程实践。

在这里插入图片描述

从架构层面来看,该应用充分利用了 HarmonyOS API 24 提供的 @Entry@Component@State@Observed@Builder 等装饰器体系,构建了一个包含六大功能模块的完整页面框架。整个应用由一个入口主页面和六个子组件构成,分别对应运输、商城、穿搭、衣橱、故事、个人中心六个 Tab 标签页。每个子组件内部通过 @State 管理局部状态,通过 @Builder 抽取可复用的 UI 构建逻辑,通过条件渲染实现弹窗模态框的显示与隐藏。这种分层架构既保证了模块间的解耦,又通过 aboutToAppearaboutToDisappear 生命周期钩子实现了粒子动画的定时器管理,体现了 ArkTS 声明式编程范式的典型特征。

在数据建模层面,该应用采用了接口定义与 @Observed 类实现相结合的范式。首先通过 interface 声明纯数据结构的契约(如 ShipOrderModelVintageProductModel 等),再用 @Observed 装饰的可观察类去实现这些接口,确保每个数据实例都具备响应式能力。同时,应用引入了大量配置映射表(如 SHIP_STATUS_CONFIGERA_CONFIGCONDITION_CONFIG 等),将业务元数据(标签、颜色、图标)与数据模型分离,实现了视图层与配置层的解耦。辅助纯函数(如 formatPricecreateParticlesstepParticles)则封装了格式化与粒子动画的核心算法,保证了函数的无副作用特性。这种设计模式在 HarmonyOS ArkTS API 24 的工程实践中具有较高的参考价值。

在视觉与交互层面,该应用充分运用了 ArkTS 的布局容器(ColumnRowStackScroll)、样式链式调用(.fontSize().fontColor().borderRadius())以及渐变与定位能力,构建出层次丰富的卡片式界面。粒子动画层通过 Stack 叠加在主内容之上,利用 setInterval 定时驱动粒子位置更新,再借助 hitTestBehavior(HitTestMode.None) 实现穿透式无障碍交互。弹窗模态框则通过条件渲染加 zIndex 层级控制实现,配合半透明遮罩层 modalOverlay 完成点击外部关闭的交互逻辑。整体代码结构清晰、分层明确,是一份值得深入研读的 ArkTS 实战范例。

二、整体架构流程图

SHIP

SHOP

STYLE

CLOSET

STORY

PROFILE

应用入口 @Entry VintageVaultApp

aboutToAppear 生命周期

初始化粒子系统 createParticles

启动定时器 setInterval 300ms

粒子位置更新 stepParticles

页面构建 build

头部区域 Header

Logo + 搜索框 + 消息入口

年代药丸横向滚动

内容区 contentArea Builder

activeTab 判断

ShipContent 运输页

ShopContent 商城页

StyleContent 穿搭页

ClosetContent 衣橱页

StoryContent 故事页

ProfileContent 我的页

底部Tab导航栏

点击切换 activeTab

粒子层 Stack 叠加

hitTestMode.None 穿透交互

恒温环境监控卡

快速预约表单

预约弹窗 bookingModal

月度运输量柱状图

运输记录列表

aboutToDisappear

清除定时器 clearInterval

三、逐段代码深度解析

3.1 颜色常量与设计系统

// ============ 颜色常量 ============
const COLOR_BG: string = '#F5F0E8'
const COLOR_CARD: string = '#FFFFFF'
const COLOR_PRIMARY: string = '#8B4513'
const COLOR_PRIMARY_LIGHT: string = '#D4A76A'
const COLOR_SECONDARY: string = '#6B4E8E'
const COLOR_SECONDARY_LIGHT: string = '#C4A8E0'
const COLOR_BRICK: string = '#B85450'
const COLOR_MUSTARD: string = '#C9A227'
const COLOR_GREEN: string = '#4E6E58'
const COLOR_TEXT_MAIN: string = '#3A2A1E'
const COLOR_TEXT_SUB: string = '#8A7560'
const COLOR_TEXT_HINT: string = '#C0AE98'
const COLOR_BORDER: string = '#E8DFD0'
const COLOR_SUCCESS: string = '#4E6E58'
const COLOR_WARNING: string = '#C9A227'
const COLOR_DANGER: string = '#B85450'

在这里插入图片描述

这段代码定义了整个应用的设计令牌(Design Token)体系,采用全局常量的方式统一管理所有颜色资源。在 HarmonyOS ArkTS API 24 的工程实践中,将颜色值提取为常量是一种推荐做法,它带来了三个显著优势:首先,全局统一的命名规范(如 COLOR_PRIMARY 代表复古棕色、COLOR_BRICK 代表砖红点缀色)使得团队协作时视觉风格保持一致;其次,当需要调整主题色时只需修改一处常量定义即可全局生效,极大降低了维护成本;最后,这种语义化命名让代码可读性大幅提升,开发者一眼就能理解每个颜色的用途。值得注意的是,该设计系统围绕"复古波普奶油风"主题构建,奶油底色 #F5F0E8 作为背景基调,复古棕紫 #8B4513#6B4E8E 作为主辅色,芥末黄 #C9A227 与砖红 #B85450 作为点缀,形成了一套层次分明且风格统一的色彩体系。

3.2 接口定义与数据模型契约

// ============ 数据模型接口 ============
interface ShipOrderModel {
  id: number
  orderNo: string
  itemType: string
  pickup: string
  destination: string
  price: number
  status: string
  date: string
}

interface VintageProductModel {
  id: number
  name: string
  era: string
  price: number
  originPrice: number
  condition: string
  likeCount: number
  color: string
  tag: string
}

interface StyleOutfitModel {
  id: number
  title: string
  styleTags: string
  author: string
  likeCount: number
  favCount: number
  color: string
}

在这里插入图片描述

这里定义了应用核心业务的数据模型接口层。ArkTS 作为 TypeScript 的超集,完整继承了接口(interface)的类型契约能力。通过为运输订单、古着商品、穿搭方案等业务实体分别定义接口,开发者建立了数据结构的类型约束。以 ShipOrderModel 为例,它包含了订单编号、物品类型、出发地、目的地、价格、状态、日期等运输业务的核心字段;VintageProductModel 则涵盖了商品名称、年代、现价、原价、成色、点赞数、主题色、风格标签等电商属性。这种接口先行的方式确保了在后续 @Observed 类实现时,每个字段都有明确的类型约束,编译器能够在开发阶段捕获类型不匹配的错误。同时,接口与实现分离的设计也为未来数据源的替换(如从 Mock 数据切换到网络请求)预留了灵活性,只要新的数据结构符合接口契约,视图层代码无需任何修改。

3.3 @Observed 可观察数据类

// ============ @Observed 数据类 ============
@Observed
class ShipOrderItem implements ShipOrderModel {
  id: number = 0
  orderNo: string = ''
  itemType: string = ''
  pickup: string = ''
  destination: string = ''
  price: number = 0
  status: string = '打包中'
  date: string = ''
  constructor(id: number, orderNo: string, itemType: string, pickup: string,
    destination: string, price: number, status: string, date: string) {
    this.id = id; this.orderNo = orderNo; this.itemType = itemType
    this.pickup = pickup; this.destination = destination; this.price = price
    this.status = status; this.date = date
  }
}

@Observed
class ParticleItem implements ParticleModel {
  x: number = 0
  y: number = 0
  size: number = 10
  opacity: number = 0.5
  color: string = '#C9A227'
  drift: number = 1
  constructor(x: number, y: number, size: number, opacity: number,
    color: string, drift: number) {
    this.x = x; this.y = y; this.size = size; this.opacity = opacity
    this.color = color; this.drift = drift
  }
}

在这里插入图片描述

@Observed 是 HarmonyOS ArkTS API 24 中用于实现二级状态观察的关键装饰器。当被 @Observed 装饰的类的实例属性发生变化时,框架会自动通知所有引用该实例的 @ObjectLink@State 变量进行 UI 刷新。在这段代码中,ShipOrderItem 实现了 ShipOrderModel 接口,所有属性都有默认初始值(如 id 默认为 0,status 默认为"打包中"),同时通过构造函数接收参数完成实例化。ParticleItem 则是粒子动画的核心数据载体,携带了位置坐标(x, y)、尺寸、透明度、颜色、漂移速度等动画参数。这种为每个数据实体定义 @Observed 类的做法,使得当任何一个订单的状态变更或粒子的位置更新时,相关 UI 都能自动响应刷新。尤其值得注意是 ParticleItem 的设计——粒子动画需要高频更新位置数据,@Observed 的响应式机制确保了每次 stepParticles 产生新粒子数组后,界面的星形装饰物能够实时跟随移动,营造出复古金粉漂浮的视觉效果。

3.4 配置映射表设计

// ============ 配置映射 ============
const SHIP_STATUS_CONFIG: Record<string, StatusMeta> = {
  '打包中': { label: '打包中', color: '#C9A227', bg: '#F7EFD8', icon: '📦' },
  '恒温运输': { label: '恒温运输', color: '#8B4513', bg: '#F0E4D4', icon: '🚚' },
  '已签收': { label: '已签收', color: '#4E6E58', bg: '#E4EBE4', icon: '✅' }
}

const ERA_CONFIG: Record<string, EraMeta> = {
  '50s': { label: '50s', color: '#8B4513', icon: '🎩' },
  '60s': { label: '60s', color: '#B85450', icon: '🎹' },
  '70s': { label: '70s', color: '#C9A227', icon: '🌻' },
  '80s': { label: '80s', color: '#6B4E8E', icon: '📼' },
  '90s': { label: '90s', color: '#4E6E58', icon: '🎧' },
  'Y2K': { label: 'Y2K', color: '#D4A76A', icon: '💿' },
  '和风古着': { label: '和风古着', color: '#C4A8E0', icon: '🏯' }
}

const CONDITION_CONFIG: Record<string, ConditionMeta> = {
  'S级': { label: 'S级', color: '#4E6E58', desc: '近乎全新' },
  'A级': { label: 'A级', color: '#8B4513', desc: '轻微使用感' },
  'B级': { label: 'B级', color: '#C9A227', desc: '正常使用痕迹' },
  'C级': { label: 'C级', color: '#B85450', desc: '有明显痕迹' }
}

在这里插入图片描述

这段代码展示了应用中最为精妙的配置层设计。通过 Record<string, XxxMeta> 类型,开发者将运输状态、年代标签、成色等级等业务枚举值与其对应的视觉表现(颜色、背景、图标、描述)进行了映射绑定。以 SHIP_STATUS_CONFIG 为例,"打包中"对应芥末黄配色与包裹图标,"恒温运输"对应复古棕与卡车图标,"已签收"对应森林绿与对勾图标。ERA_CONFIG 更是为从50年代到Y2K乃至和风古着等七个年代分别赋予了专属色彩与代表性 emoji 图标,使得不同年代的古着在视觉上具备即时的辨识度。CONDITION_CONFIG 则将古着成色的S/A/B/C四级评级与颜色和文字描述绑定。这种映射表驱动的做法将业务配置从视图代码中完全剥离,当需要新增一个年代或调整某个状态的配色时,只需修改映射表而不必触碰任何 UI 代码,体现了开闭原则的设计思想。

3.5 静态 Mock 数据初始化

// ============ 静态数据:运输记录 8 条 ============
const mockShipOrders: ShipOrderItem[] = [
  new ShipOrderItem(1, 'VV260801', '衣物', '上海安福路', '东京下北泽', 368, '恒温运输', '08-21'),
  new ShipOrderItem(2, 'VV260802', '鞋帽', '北京鼓楼', '大阪美国村', 420, '打包中', '08-22'),
  new ShipOrderItem(3, 'VV260803', '配饰', '成都玉林', '伦敦砖巷', 680, '恒温运输', '08-20'),
  new ShipOrderItem(4, 'VV260804', '箱包', '广州东山口', '首尔圣水洞', 510, '已签收', '08-15'),
  new ShipOrderItem(5, 'VV260805', '衣物', '杭州天目里', '纽约布鲁克林', 890, '已签收', '08-12'),
  new ShipOrderItem(6, 'VV260806', '大宗', '上海安福路', '代官山茑屋', 1280, '恒温运输', '08-19'),
  new ShipOrderItem(7, 'VV260807', '鞋帽', '南京颐和路', '曼谷恰图恰', 320, '打包中', '08-23'),
  new ShipOrderItem(8, 'VV260808', '配饰', '深圳南头古城', '巴黎玛黑区', 750, '已签收', '08-08')
]

// ============ 静态数据:古着商品 12 件 ============
const mockProducts: VintageProductItem[] = [
  new VintageProductItem(1, '70s 做旧麂皮流苏夹克', '70s', 899, 1499, 'A级', 328, '#B85450', '美式复古'),
  new VintageProductItem(2, '50s 廓形羊毛大衣', '50s', 1280, 1980, 'S级', 512, '#8B4513', '法风'),
  new VintageProductItem(3, '80s 做旧牛仔背带裤', '80s', 459, 720, 'B级', 208, '#4E6E58', '工装'),
  new VintageProductItem(4, '90s 格纹法兰绒衬衫', '90s', 268, 430, 'A级', 466, '#C9A227', '日系')
]

在这里插入图片描述

这段代码展示了应用的 Mock 数据层,为运输、商城、穿搭、衣橱、故事等各业务模块提供了完整的静态数据集。每条运输记录都是一个 ShipOrderItem 实例,包含了真实的跨国古着运输场景(上海安福路到东京下北泽、成都玉林到伦敦砖巷等),订单编号采用"VV+年月日"的格式。古着商品数据则涵盖了从50年代廓形羊毛大衣到Y2K低腰水洗牛仔裤等不同年代的代表性单品,每件商品都标注了现价、原价、成色、点赞数、主题色和风格标签。这种通过构造函数直接实例化数组的方式简洁明了,适合在原型开发阶段快速填充界面。在真实工程中,这些 Mock 数据可以很方便地替换为从网络接口获取的数据,因为它们已经符合接口定义的类型契约。数据的丰富程度也体现了产品的业务深度——12件古着商品横跨7个年代、5种风格,6条故事帖涵盖了不同国家古着玩家的真实体验。

3.6 辅助纯函数与粒子动画算法

// ============ 辅助纯函数 ============
function formatPrice(v: number): string {
  return '¥' + v.toFixed(0)
}

function getEraLabel(era: string): string {
  return ERA_CONFIG[era]?.label ?? era
}

function getClosetTotalValue(): number {
  let total: number = 0
  for (let i = 0; i < mockClosetItems.length; i++) {
    total += mockClosetItems[i].currentValue
  }
  return total
}

function createParticles(): ParticleItem[] {
  const list: ParticleItem[] = []
  const colors: string[] = ['#C9A227', '#8B4513', '#6B4E8E', '#D4A76A', '#B85450']
  const xs: number[] = [18, 62, 110, 158, 205, 252, 300, 128, 78, 230]
  const ys: number[] = [640, 420, 560, 300, 500, 180, 380, 90, 660, 240]
  const sizes: number[] = [10, 14, 8, 16, 12, 9, 15, 11, 13, 10]
  for (let i = 0; i < 10; i++) {
    list.push(new ParticleItem(xs[i], ys[i], sizes[i], 0.25 + (i % 3) * 0.12,
      colors[i % colors.length], 1 + (i % 3)))
  }
  return list
}

function stepParticles(list: ParticleItem[]): ParticleItem[] {
  const next: ParticleItem[] = []
  for (let i = 0; i < list.length; i++) {
    const p: ParticleItem = list[i]
    let ny: number = p.y - p.drift * 8
    let nx: number = p.x + (i % 2 === 0 ? 3 : -2)
    if (ny < -12) { ny = 700 }
    if (nx > 336) { nx = 6 }
    if (nx < 0) { nx = 330 }
    next.push(new ParticleItem(nx, ny, p.size, p.opacity, p.color, p.drift))
  }
  return next
}

在这里插入图片描述

辅助纯函数层是整个应用中算法逻辑最为集中的部分。formatPrice 将数字格式化为带人民币符号的字符串;getEraLabelgetEraIcon 通过可选链操作符(?.)和空值合并运算符(??)安全地从配置映射中取值,当年代不存在于配置表时回退到原始值,这种防御性编程避免了运行时异常。getClosetTotalValuegetClosetTotalCost 分别遍历衣橱数据计算总估值和总投入成本。最为精彩的是粒子动画的两个函数:createParticles 预设了10个粒子的初始坐标、尺寸、透明度、颜色和漂移速度,粒子颜色从复古金、棕、紫、浅金到砖红中循环取值,透明度则通过 0.25 + (i % 3) * 0.12 公式产生层次差异;stepParticles 则是动画的每一帧推进逻辑,每个粒子的 y 坐标按漂移速度上移,x 坐标根据索引奇偶性左右偏移产生飘动效果,当粒子飞出屏幕边界时从对侧重入,实现了无限循环的漂浮动画。每次调用都返回一个全新的粒子数组而非原地修改,保证了函数的纯度。

3.7 Tab 枚举与主页面入口

// ============ Tab 枚举 ============
enum VintageTab {
  SHIP = 0,
  SHOP = 1,
  STYLE = 2,
  CLOSET = 3,
  STORY = 4,
  PROFILE = 5
}

// ============ 入口主页面 ============
@Entry
@Component
struct VintageVaultApp {
  @State activeTab: VintageTab = VintageTab.SHIP
  @State searchKeyword: string = ''
  @State selectedEra: string = '全部'
  @State particles: ParticleItem[] = []
  private timerId: number = -1

  aboutToAppear() {
    this.particles = createParticles()
    this.timerId = setInterval(() => {
      this.particles = stepParticles(this.particles)
    }, 300)
  }

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

在这里插入图片描述

VintageTab 枚举定义了六个功能页面的索引常量,使用枚举而非魔法数字提升了代码可读性。主页面 VintageVaultApp 通过 @Entry 装饰器标记为应用入口,@Component 声明为自定义组件。页面内部维护了四个核心状态:activeTab 控制当前激活的标签页(默认为运输页),searchKeyword 存储搜索关键词,selectedEra 记录选中的年代筛选,particles 管理粒子动画数据数组。aboutToAppear 是 ArkTS 的页面生命周期回调,在页面即将显示时被调用——此处首先调用 createParticles() 初始化粒子数组,然后通过 setInterval 启动一个300毫秒间隔的定时器,每次触发时调用 stepParticles 更新粒子位置并赋值给 this.particles,由于 particles@State 装饰的状态变量,其变化会自动触发 UI 重渲染,星形粒子便持续漂浮。aboutToDisappear 则在页面销毁时清除定时器,防止内存泄漏,这是 ArkTS 动画资源管理的标准范式。

3.8 @Builder 内容区与底部导航构建

  @Builder contentArea() {
    Column() {
      if (this.activeTab === VintageTab.SHIP) {
        ShipContent()
      } else if (this.activeTab === VintageTab.SHOP) {
        ShopContent()
      } else if (this.activeTab === VintageTab.STYLE) {
        StyleContent()
      } else if (this.activeTab === VintageTab.CLOSET) {
        ClosetContent()
      } else if (this.activeTab === VintageTab.STORY) {
        StoryContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: VintageTab) {
    Column() {
      Text(icon).fontSize(18).opacity(this.activeTab === tab ? 1.0 : 0.45)
      Text(label).fontSize(8)
        .fontColor(this.activeTab === tab ? COLOR_PRIMARY : COLOR_TEXT_HINT)
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 1 })
      if (this.activeTab === tab) {
        Column().width(16).height(3)
          .backgroundColor(COLOR_PRIMARY).borderRadius(2).margin({ top: 2 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tab })
  }

在这里插入图片描述

@Builder 是 ArkTS 中用于抽取可复用 UI 片段的装饰器,类似于其他框架中的渲染函数。contentArea 构建器通过一串条件判断语句,根据 activeTab 的值渲染对应的功能子组件,layoutWeight(1) 使其占据剩余空间。bottomTabItem 则是一个参数化的底部导航项构建器,接收图标、标签文字和目标 Tab 三个参数,内部通过三元表达式动态设置选中态与未选中态的样式差异:选中时图标不透明度为1.0、文字为棕色粗体、并显示一个3像素高的指示条;未选中时图标半透明、文字为浅灰色常规字重。每个 Tab 项的 onClick 回调将 activeTab 设置为对应枚举值,触发 contentArea 重新渲染。这种通过参数化构建器复用 UI 的方式避免了在每个 Tab 项上重复编写样式代码,当底部导航有六个 Tab 时,这种抽象减少了大量冗余代码。

3.9 主页面 build 布局与粒子层叠加

  build() {
    Stack() {
      Column() {
        // ===== 头部:第一行 Logo + 搜索 + 消息 =====
        Row() {
          Column() {
            Text('古着衣橱').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
            Text('VINTAGE VAULT').fontSize(7).fontColor(COLOR_TEXT_HINT).margin({ top: 1 })
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 14 })

          Row() {
            Text('🔍').fontSize(12).margin({ left: 8 })
            TextInput({ placeholder: '搜索Vintage单品' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(11).layoutWeight(1)
              .backgroundColor(COLOR_BG).borderRadius(14).height(30)
              .margin({ left: 4, right: 4 })
              .onChange((v: string) => { this.searchKeyword = v })
          }
          .layoutWeight(1)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .borderRadius(15)
          .margin({ left: 10 })
          .padding({ right: 4 })
          // ... 消息入口与年代药丸省略
        }
        // ===== 内容区 =====
        this.contentArea()
        // ===== 底部Tab =====
        Row() {
          this.bottomTabItem('📦', '运输', VintageTab.SHIP)
          this.bottomTabItem('👕', '商城', VintageTab.SHOP)
          this.bottomTabItem('👗', '穿搭', VintageTab.STYLE)
          this.bottomTabItem('🚪', '衣橱', VintageTab.CLOSET)
          this.bottomTabItem('📖', '故事', VintageTab.STORY)
          this.bottomTabItem('👤', '我的', VintageTab.PROFILE)
        }
        .shadow({ radius: 8, color: '#148B4513', offsetY: -2 })
      }
      .width('100%').height('100%')

      // ===== 粒子层(复古金/棕/紫 漂浮星星)=====
      Stack() {
        ForEach(this.particles, (p: ParticleItem) => {
          Text('✦')
            .fontSize(p.size)
            .fontColor(p.color)
            .opacity(p.opacity)
            .position({ x: p.x, y: p.y })
            .hitTestBehavior(HitTestMode.None)
        })
      }
      .width('100%').height('100%')
      .hitTestBehavior(HitTestMode.None)
    }
    .width('100%').height('100%')
    .backgroundColor(COLOR_BG)
  }

build 方法是每个 @Component 必须实现的核心构建函数。这里采用 Stack 作为最外层容器,将主内容列和粒子动画层进行叠加。主内容列从上到下依次排列:头部行(Logo加搜索框加消息入口)、年代药丸横向滚动区、内容区(通过 contentArea 构建器引入)、底部导航栏。搜索框使用 TextInput 组件并通过 onChange 回调实时更新 searchKeyword 状态。底部导航栏通过六次调用 bottomTabItem 构建器生成六个 Tab 按钮,并添加了向上的阴影投影增强层次感。粒子层是整个布局的点睛之笔——一个透明的 Stack 覆盖在主内容之上,通过 ForEach 遍历 particles 数组渲染星形 Text,每个粒子使用 .position() 绝对定位到其坐标位置。关键的 hitTestBehavior(HitTestMode.None) 设置确保粒子层不拦截任何触摸事件,用户的所有点击都能穿透到下层的主内容,实现了视觉装饰与交互逻辑的完美隔离。

3.10 恒温运输页 - 环境监控与预约表单

@Component
struct ShipContent {
  @State showBookingModal: boolean = false
  @State formItemType: string = '衣物'
  @State formCount: string = '2'
  @State formPack: string = '恒温箱'
  @State formEnv: string = '恒温'
  @State formFrom: string = ''
  @State formTo: string = ''
  @State formNote: string = ''

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

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 恒温运输环境监控卡 =====
            Column() {
              Row() {
                Text('🚚 恒温运输舱环境监控').fontSize(13).fontWeight(FontWeight.Bold)
                  .fontColor(COLOR_TEXT_MAIN)
                Column().layoutWeight(1)
                Text('实时').fontSize(9).fontColor(COLOR_SUCCESS)
                  .backgroundColor('#E4EBE4').padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(8)
              }
              .width('100%')
              Row() {
                Column() {
                  Text('🌡️').fontSize(16)
                  Text('22℃').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                  Text('温度').fontSize(9).fontColor(COLOR_TEXT_SUB)
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
                // ... 湿度、紫外线防护同理
              }
              .width('100%').margin({ top: 12 })
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding(14).margin({ left: 12, right: 12, top: 10 })
          }
        }
      }
      if (this.showBookingModal) { this.bookingModal() }
    }
    .width('100%').height('100%')
  }
}

运输页 ShipContent 是应用中交互最为丰富的功能模块。页面通过七个 @State 变量管理预约表单的各字段状态(物品类型、件数、包装方式、运输环境、出发地、目的地、备注)以及弹窗的显示开关。modalOverlay 构建器是一个高复用的遮罩层组件,接收一个 onClose 回调函数,点击半透明背景时触发关闭——这种将遮罩逻辑抽象为参数化构建器的做法在三个弹窗中被反复复用。页面主体通过 Scroll 包裹可滚动内容,首先呈现恒温运输舱环境监控卡,以三栏布局展示温度(22度)、湿度(45%)和紫外线防护状态,每栏之间用1像素的分割线隔开,大号数字搭配emoji图标形成了仪表盘般的视觉冲击力。弹窗通过 if (this.showBookingModal) 条件渲染实现模态展示,当 Stack 中条件为真时弹窗层覆盖在主内容之上。

3.11 恒温运输页 - 预约弹窗与表单交互

  @Builder bookingModal() {
    Column() {
      this.modalOverlay(() => { this.showBookingModal = false })
      Column() {
        // 棕色头部
        Row() {
          Text('📦 预约恒温运输').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_CARD)
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#F0E4D4')
            .padding(4)
            .onClick(() => { this.showBookingModal = false })
        }
        .width('100%').padding({ left: 18, right: 14, top: 14, bottom: 12 })
        .backgroundColor(COLOR_PRIMARY)
        .borderRadius({ topLeft: 14, topRight: 14 })

        Scroll() {
          Column() {
            Text('物品类型').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            Row() {
              ForEach(ITEM_TYPES, (t: string) => {
                if (this.formItemType === t) {
                  Text((ITEM_TYPE_CONFIG[t]?.icon ?? '') + ' ' + t)
                    .fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text((ITEM_TYPE_CONFIG[t]?.icon ?? '') + ' ' + t)
                    .fontSize(10).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.formItemType = t })
                }
              })
            }
            .margin({ left: 14, right: 14, top: 4 })
            // ... 件数、包装、环境、地址、备注等表单项
          }
          .constraintSize({ maxHeight: '80%' })
        }
        // 底部按钮
        Row() {
          Text('取消').fontSize(13).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG).borderRadius(18)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 22, right: 22, top: 9, bottom: 9 })
            .onClick(() => { this.showBookingModal = false })
          Text('确认预约').fontSize(13).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_PRIMARY).borderRadius(18)
            .padding({ left: 22, right: 22, top: 9, bottom: 9 })
            .margin({ left: 12 })
            .onClick(() => { this.showBookingModal = false })
        }
      }
      .width('90%').height('75%').backgroundColor(COLOR_CARD).borderRadius(14)
      .position({ x: '5%', y: '12%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

预约弹窗 bookingModal 是整个运输页的核心交互组件。弹窗结构分为三层:最外层 Column 占满全屏并设置 zIndex(999) 确保浮于所有内容之上;中间是 modalOverlay 遮罩层;最内层是实际的弹窗卡片,通过 position 绝对定位到屏幕中央偏上位置。弹窗头部使用复古棕色背景搭配白色文字,带有顶部圆角的标题栏设计。表单内容通过 Scroll 包裹以支持内容超出时的滚动,constraintSize({ maxHeight: '80%' }) 限制了内容区最大高度。物品类型选择采用了药丸式标签组,通过 ForEach 遍历 ITEM_TYPES 数组,为每个选项渲染选中态(棕色填充)或未选中态(白底边框),点击时更新 formItemType 状态。底部双按钮(取消和确认预约)分别使用不同配色,确认按钮为复古棕色主色调,取消按钮为浅色辅助风格。整个弹窗代码展示了 ArkTS 中表单交互的完整范式:状态驱动渲染、条件样式切换、回调函数传参。

3.12 商城页 - 商品卡片与详情弹窗

@Component
struct ShopContent {
  @State sortMode: string = '最新'
  @State showDetailModal: boolean = false
  @State selectedProduct: VintageProductItem | null = null
  @State detailSize: string = 'M'
  @State detailCount: number = 1

  @Builder productCardBuilder(p: VintageProductItem) {
    Column() {
      Column() {
        Text((ERA_CONFIG[p.era]?.icon ?? '')).fontSize(24).opacity(0.85)
        Text(getEraLabel(p.era)).fontSize(9).fontColor(COLOR_CARD).opacity(0.9).margin({ top: 3 })
      }
      .width('100%').height(96)
      .backgroundColor(p.color)
      .borderRadius({ topLeft: 10, topRight: 10 })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(p.name).fontSize(11).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Row() {
          Text(p.condition).fontSize(9)
            .fontColor(CONDITION_CONFIG[p.condition]?.color ?? COLOR_PRIMARY)
            .backgroundColor(COLOR_BG)
            .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
          Text('❤️' + p.likeCount.toString()).fontSize(9).fontColor(COLOR_TEXT_HINT)
            .margin({ left: 6 })
        }
        .margin({ top: 4 })
        Row() {
          Text(formatPrice(p.price)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_BRICK)
          Text(formatPrice(p.originPrice)).fontSize(9).fontColor(COLOR_TEXT_HINT)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 4 })
        }
        .margin({ top: 5 })
        Text('入手').fontSize(10).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_PRIMARY).borderRadius(12)
          .padding({ left: 20, right: 20, top: 5, bottom: 5 })
          .alignSelf(ItemAlign.Center).margin({ top: 7 })
          .onClick(() => {
            this.selectedProduct = p
            this.detailCount = 1
            this.showDetailModal = true
          })
      }
      .width('100%').padding(8).alignItems(HorizontalAlign.Start)
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(10).border({ width: 1, color: COLOR_BORDER })
    .margin({ left: 3, right: 3 })
  }
}

商城页 ShopContent 管理了排序模式、详情弹窗开关、选中商品、详情尺码和数量五个状态。商品卡片构建器 productCardBuilder 接收一个 VintageProductItem 参数,构建出上下结构的卡片:上半部分是商品图块,使用商品自身的 color 作为背景色,居中显示年代图标和年代标签;下半部分是商品信息区,商品名称通过 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }) 实现单行省略号截断。成色标签从 CONDITION_CONFIG 取色,现价用砖红色粗体大字,原价用删除线小字,形成价格对比的视觉冲击。"入手"按钮的 onClick 回调将选中商品赋值给 selectedProduct,重置数量为1,并打开详情弹窗。selectedProduct 的类型为 VintageProductItem | null,这种联合类型允许在未选中商品时为 null,弹窗内部则通过可选链 this.selectedProduct?.name 安全访问属性。这种空安全设计在 ArkTS 中是处理可空引用的标准模式。

3.13 商城页 - 商品详情弹窗与数量选择

  @Builder productDetailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        // 商品图
        Column() {
          Text(' Vintage ').fontSize(30).fontColor(COLOR_CARD).opacity(0.7)
          Text(this.selectedProduct?.name ?? '').fontSize(12).fontColor(COLOR_CARD)
            .margin({ top: 4 }).opacity(0.9)
        }
        .width('100%').height(140)
        .backgroundColor(this.selectedProduct?.color ?? COLOR_PRIMARY_LIGHT)
        .borderRadius({ topLeft: 14, topRight: 14 })

        Scroll() {
          Column() {
            Row() {
              Text((ERA_CONFIG[this.selectedProduct?.era ?? '90s']?.icon ?? '') + ' '
                + getEraLabel(this.selectedProduct?.era ?? '90s'))
                .fontSize(10).fontColor(COLOR_PRIMARY).backgroundColor('#F0E4D4')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
              // ... 成色、标签
            }
            Row() {
              Text(formatPrice(this.selectedProduct?.price ?? 0))
                .fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLOR_BRICK)
              Text(formatPrice(this.selectedProduct?.originPrice ?? 0))
                .fontSize(11).fontColor(COLOR_TEXT_HINT)
                .decoration({ type: TextDecorationType.LineThrough })
                .margin({ left: 8, top: 6 })
            }
            // 尺码选择
            Row() {
              ForEach(SIZE_OPTIONS, (s: string) => {
                if (this.detailSize === s) {
                  Text(s).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                    .width(38).height(30).borderRadius(15).textAlign(TextAlign.Center)
                } else {
                  Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .width(38).height(30).borderRadius(15).textAlign(TextAlign.Center)
                    .onClick(() => { this.detailSize = s })
                }
              })
            }
            // 数量加减
            Row() {
              Text('-').fontSize(14).fontColor(COLOR_TEXT_SUB)
                .width(28).height(28).borderRadius(14).backgroundColor(COLOR_BG)
                .textAlign(TextAlign.Center)
                .onClick(() => { if (this.detailCount > 1) { this.detailCount = this.detailCount - 1 } })
              Text(this.detailCount.toString()).fontSize(13).fontWeight(FontWeight.Bold)
              Text('+').fontSize(14).fontColor(COLOR_CARD)
                .width(28).height(28).borderRadius(14).backgroundColor(COLOR_PRIMARY)
                .textAlign(TextAlign.Center)
                .onClick(() => { this.detailCount = this.detailCount + 1 })
            }
          }
        }
        Row() {
          Text('☆ 加入收藏').fontSize(12).fontColor(COLOR_SECONDARY)
            .layoutWeight(1).height(38).borderRadius(19)
            .border({ width: 1, color: COLOR_SECONDARY }).textAlign(TextAlign.Center)
          Text('立即入手').fontSize(12).fontColor(COLOR_CARD)
            .layoutWeight(1).height(38).borderRadius(19)
            .backgroundColor(COLOR_BRICK).margin({ left: 10 }).textAlign(TextAlign.Center)
        }
      }
      .width('92%').height('70%').backgroundColor(COLOR_CARD).borderRadius(14)
    }
    .width('100%').height('100%').zIndex(999)
  }

商品详情弹窗是商城页的交互核心,展示了从商品概览到下单的完整信息流。弹窗顶部是140像素高的商品图区,背景色取自 selectedProduct?.color,叠加半透明的"Vintage"水印文字。信息区通过可选链 ?. 和空值合并 ?? 安全访问可能为 null 的商品属性,确保即使 selectedProduct 未初始化也不会崩溃。尺码选择区遍历 SIZE_OPTIONS(XS到XL),选中态为棕色圆角填充,未选中态为白底边框,点击切换 detailSize。数量选择器是一个经典的加减组件:减号按钮在数量大于1时递减(通过 if 条件保护防止减到0),加号按钮直接递增。底部双按钮分为"加入收藏"(紫色描边)和"立即入手"(砖红填充),分别对应不同的交互意图。整个弹窗充分展现了 ArkTS 中可选链空值合并运算符与条件渲染结合使用的安全编程范式,是处理可空状态的经典案例。

3.14 穿搭页 - 渐变卡片与风格筛选

@Component
struct StyleContent {
  @State styleFilter: string = '全部'

  @Builder outfitCardBuilder(o: StyleOutfitItem) {
    Column() {
      // 渐变穿搭大图块
      Column() {
        Text('👗').fontSize(40).opacity(0.9)
        Text('OUTFIT #' + o.id.toString()).fontSize(9)
          .fontColor(COLOR_CARD).opacity(0.85).margin({ top: 6 })
      }
      .width('100%').height(150)
      .linearGradient({
        angle: 135,
        colors: [[o.color, 0.0], ['#8B4513', 1.0]]
      })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(o.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
        Row() {
          ForEach(splitTags(o.styleTags), (t: string) => {
            Text('#' + t).fontSize(9)
              .fontColor(STYLE_CONFIG[t]?.color ?? COLOR_PRIMARY)
              .backgroundColor(COLOR_BG)
              .padding({ left: 7, right: 7, top: 3, bottom: 3 }).borderRadius(10)
              .margin({ right: 5 })
          })
        }
        .margin({ top: 6 })
        Row() {
          Text(o.author).fontSize(10).fontColor(COLOR_SECONDARY).fontWeight(FontWeight.Medium)
          Column().layoutWeight(1)
          Text('❤️ ' + o.likeCount.toString()).fontSize(10).fontColor(COLOR_BRICK)
          Text('⭐ ' + o.favCount.toString()).fontSize(10).fontColor(COLOR_MUSTARD).margin({ left: 10 })
        }
        .width('100%').margin({ top: 10 })
      }
      .width('100%').padding(12).alignItems(HorizontalAlign.Start)
    }
    .width('100%').backgroundColor(COLOR_CARD)
    .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
    .margin({ left: 12, right: 12, top: 10 })
  }

  build() {
    Column() {
      Scroll() {
        Row() {
          ForEach(STYLE_FILTERS, (s: string) => {
            if (this.styleFilter === s) {
              Text(s).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_SECONDARY)
                .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
            } else {
              Text(s).fontSize(10).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
                .border({ width: 1, color: COLOR_BORDER })
                .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                .onClick(() => { this.styleFilter = s })
            }
          })
        }
      }
      // ... 穿搭卡片列表
    }
  }
}

穿搭页 StyleContent 管理风格筛选状态,构建了应用中最具视觉冲击力的穿搭展示卡片。卡片顶部的150像素大图块使用了 linearGradient 线性渐变,从穿搭自身的主题色到复古棕色 #8B4513 的135度对角渐变,营造出复古胶片般的色调过渡。穿搭标题下方,风格标签通过 splitTags 函数将 "美式复古|工装" 格式的字符串拆分为数组,再通过 ForEach 渲染为带 # 前缀的药丸标签,每个标签的颜色从 STYLE_CONFIG 中取对应风格的配色。底部展示作者名、点赞数和收藏数,分别使用紫色、砖红和芥末黄三种颜色。风格筛选区位于页面顶部,遍历 STYLE_FILTERS 数组(全部、美式复古、日系、法风、工装、学院),选中态为紫色填充,这里选中态使用了 COLOR_SECONDARY(复古棕紫)而非主色棕色,通过色彩区分表明穿搭模块与运输模块的功能差异。这种用颜色语义化区分功能模块的做法在多Tab应用中是一种有效的设计策略。

3.15 衣橱页 - 统计面板与编辑/移出双弹窗

@Component
struct ClosetContent {
  @State showEditModal: boolean = false
  @State showRemoveModal: boolean = false
  @State editingItem: ClosetItem | null = null
  @State removeItem: ClosetItem | null = null
  @State editName: string = ''
  @State editEra: string = '80s'
  @State editCondition: string = 'A级'
  @State editBuyPrice: string = ''
  @State editValue: string = ''
  @State editNote: string = ''

  @Builder closetCardBuilder(c: ClosetItem) {
    Column() {
      Column() {
        Text(getEraIcon(c.era)).fontSize(18).opacity(0.9)
      }
      .width('100%').height(58)
      .backgroundColor(c.color)
      .borderRadius({ topLeft: 8, topRight: 8 })

      Column() {
        Text(c.name).fontSize(9).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(getEraLabel(c.era) + ' · ' + c.condition).fontSize(8).fontColor(COLOR_TEXT_HINT)
        Text(formatPrice(c.currentValue)).fontSize(11).fontWeight(FontWeight.Bold)
          .fontColor(COLOR_PRIMARY)
        Text('购入' + formatPrice(c.purchasePrice)).fontSize(8).fontColor(COLOR_TEXT_HINT)
          .decoration({ type: TextDecorationType.LineThrough })
        Row() {
          Text('改').fontSize(8).fontColor(COLOR_SECONDARY)
            .backgroundColor('#F0EAF6').borderRadius(8)
            .padding({ left: 9, right: 9, top: 3, bottom: 3 })
            .onClick(() => {
              this.editingItem = c
              this.editName = c.name
              this.editEra = c.era
              this.editCondition = c.condition
              this.editBuyPrice = c.purchasePrice.toString()
              this.editValue = c.currentValue.toString()
              this.showEditModal = true
            })
          Text('移出').fontSize(8).fontColor(COLOR_DANGER)
            .backgroundColor('#F6E3E2').borderRadius(8)
            .padding({ left: 7, right: 7, top: 3, bottom: 3 })
            .margin({ left: 5 })
            .onClick(() => {
              this.removeItem = c
              this.showRemoveModal = true
            })
        }
      }
      .width('100%').padding(6).alignItems(HorizontalAlign.Center)
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(8).border({ width: 1, color: COLOR_BORDER })
  }
}

衣橱页 ClosetContent 是状态管理最为复杂的模块,维护了九个状态变量:两个弹窗开关、两个当前操作项引用、五个编辑表单字段。衣橱卡片 closetCardBuilder 以三列网格布局呈现12件藏品,每张卡片包含年代图标色块、衣物名称、年代与成色、当前估值(粗体棕色)、购入价(删除线灰字),以及"改"和"移出"两个操作按钮。编辑按钮的 onClick 回调展示了状态预填充的典型模式:将选中衣物的各属性赋值到对应的编辑状态变量(editNameeditEraeditCondition 等),然后打开编辑弹窗,弹窗内的表单控件即可显示当前值。移出按钮则将选中衣物赋值给 removeItem 并打开确认弹窗。页面顶部还有统计面板展示总件数、总估值(通过 getClosetTotalValue() 函数计算)、最爱年代、本月新增,以及四格成色分布统计(S/A/B/C级各多少件及对应估值)。两个弹窗在 build 中通过 if (this.showEditModal)if (this.showRemoveModal) 分别条件渲染,实现了同一页面内管理多个模态弹窗的交互范式。

四、弹窗系统与状态流转

衣橱页

点击改

点击移出

编辑字段

确认移出

closetCardBuilder 衣物卡片

editClothModal 编辑弹窗

removeConfirmModal 确认弹窗

editName/editEra/editCondition

showRemoveModal = false

商城页

点击入手

选择尺码

调整数量

收藏或购买

productCardBuilder 商品卡片

productDetailModal 详情弹窗

detailSize

detailCount

showDetailModal = false

运输页

点击展开完整预约

填充表单字段

确认或取消

快速预约表单

bookingModal 预约弹窗

formItemType/formCount/formPack/formEnv

showBookingModal = false

五、核心特性对比分析

特性维度运输页 ShipContent商城页 ShopContent衣橱页 ClosetContent穿搭页 StyleContent
状态变量数量7个5个9个1个
弹窗数量1个(预约)1个(商品详情)2个(编辑+移出)
数据源mockShipOrders 8条mockProducts 12件mockClosetItems 12件mockOutfits 6套
布局结构Scroll纵向滚动双列网格三列网格单列卡片流
图表组件月度柱状图年代分布进度条成色分布统计格
交互复杂度高(表单+弹窗+列表)高(卡片+详情+尺码数量)最高(双弹窗+预填充)中(筛选+列表)
配置映射SHIP_STATUS_CONFIGERA_CONFIG + CONDITION_CONFIGERA_CONFIG + CONDITION_CONFIGSTYLE_CONFIG
渐变效果linearGradient 135度

六、组件间通信与状态管理

纯函数调用

子组件局部状态

Tab切换

定时器驱动

全局状态 VintageVaultApp

条件渲染

条件渲染

条件渲染

条件渲染

条件渲染

条件渲染

activeTab 当前Tab

selectedEra 年代筛选

searchKeyword 搜索词

particles 粒子数组

setInterval 300ms

stepParticles 计算新位置

ForEach 重渲染星形粒子

ShipContent

ShopContent

StyleContent

ClosetContent

StoryContent

ProfileContent

showBookingModal 弹窗开关

selectedProduct 选中商品

editingItem/removeItem 操作项

likedStoryId 交互反馈

formatPrice 格式化价格

getEraLabel 取年代标签

getClosetTotalValue 计算总估值

splitTags 拆分标签


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 古着衣橱 VINTAGE VAULT — 古着/复古服饰恒温运输 + 二手潮流商城
// 场景:古着服饰恒温恒湿专业运输 · 二手古着商城 · 穿搭社区
// 风格:复古波普奶油风(奶油底 + 复古棕紫 + 芥末砖红点缀)
// ============================================================

// ============ 颜色常量 ============
const COLOR_BG: string = '#F5F0E8'
const COLOR_CARD: string = '#FFFFFF'
const COLOR_PRIMARY: string = '#8B4513'
const COLOR_PRIMARY_LIGHT: string = '#D4A76A'
const COLOR_SECONDARY: string = '#6B4E8E'
const COLOR_SECONDARY_LIGHT: string = '#C4A8E0'
const COLOR_BRICK: string = '#B85450'
const COLOR_MUSTARD: string = '#C9A227'
const COLOR_GREEN: string = '#4E6E58'
const COLOR_TEXT_MAIN: string = '#3A2A1E'
const COLOR_TEXT_SUB: string = '#8A7560'
const COLOR_TEXT_HINT: string = '#C0AE98'
const COLOR_BORDER: string = '#E8DFD0'
const COLOR_SUCCESS: string = '#4E6E58'
const COLOR_WARNING: string = '#C9A227'
const COLOR_DANGER: string = '#B85450'

// ============ 配置元数据接口 ============
interface StatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

interface EraMeta {
  label: string
  color: string
  icon: string
}

interface ConditionMeta {
  label: string
  color: string
  desc: string
}

interface TypeMeta {
  label: string
  icon: string
  color: string
}

interface StyleMeta {
  label: string
  color: string
}

// ============ 数据模型接口 ============
interface ShipOrderModel {
  id: number
  orderNo: string
  itemType: string
  pickup: string
  destination: string
  price: number
  status: string
  date: string
}

interface VintageProductModel {
  id: number
  name: string
  era: string
  price: number
  originPrice: number
  condition: string
  likeCount: number
  color: string
  tag: string
}

interface StyleOutfitModel {
  id: number
  title: string
  styleTags: string
  author: string
  likeCount: number
  favCount: number
  color: string
}

interface ClosetItemModel {
  id: number
  name: string
  era: string
  purchasePrice: number
  currentValue: number
  condition: string
  color: string
}

interface StoryModel {
  id: number
  userName: string
  userAvatar: string
  title: string
  content: string
  likeCount: number
  commentCount: number
  color: string
}

interface ParticleModel {
  x: number
  y: number
  size: number
  opacity: number
  color: string
  drift: number
}

// ============ @Observed 数据类 ============
@Observed
class ShipOrderItem implements ShipOrderModel {
  id: number = 0
  orderNo: string = ''
  itemType: string = ''
  pickup: string = ''
  destination: string = ''
  price: number = 0
  status: string = '打包中'
  date: string = ''
  constructor(id: number, orderNo: string, itemType: string, pickup: string, destination: string, price: number, status: string, date: string) {
    this.id = id; this.orderNo = orderNo; this.itemType = itemType
    this.pickup = pickup; this.destination = destination; this.price = price
    this.status = status; this.date = date
  }
}

@Observed
class VintageProductItem implements VintageProductModel {
  id: number = 0
  name: string = ''
  era: string = ''
  price: number = 0
  originPrice: number = 0
  condition: string = ''
  likeCount: number = 0
  color: string = ''
  tag: string = ''
  constructor(id: number, name: string, era: string, price: number, originPrice: number, condition: string, likeCount: number, color: string, tag: string) {
    this.id = id; this.name = name; this.era = era; this.price = price
    this.originPrice = originPrice; this.condition = condition; this.likeCount = likeCount
    this.color = color; this.tag = tag
  }
}

@Observed
class StyleOutfitItem implements StyleOutfitModel {
  id: number = 0
  title: string = ''
  styleTags: string = ''
  author: string = ''
  likeCount: number = 0
  favCount: number = 0
  color: string = ''
  constructor(id: number, title: string, styleTags: string, author: string, likeCount: number, favCount: number, color: string) {
    this.id = id; this.title = title; this.styleTags = styleTags
    this.author = author; this.likeCount = likeCount; this.favCount = favCount; this.color = color
  }
}

@Observed
class ClosetItem implements ClosetItemModel {
  id: number = 0
  name: string = ''
  era: string = ''
  purchasePrice: number = 0
  currentValue: number = 0
  condition: string = ''
  color: string = ''
  constructor(id: number, name: string, era: string, purchasePrice: number, currentValue: number, condition: string, color: string) {
    this.id = id; this.name = name; this.era = era
    this.purchasePrice = purchasePrice; this.currentValue = currentValue
    this.condition = condition; this.color = color
  }
}

@Observed
class StoryItem implements StoryModel {
  id: number = 0
  userName: string = ''
  userAvatar: string = ''
  title: string = ''
  content: string = ''
  likeCount: number = 0
  commentCount: number = 0
  color: string = ''
  constructor(id: number, userName: string, userAvatar: string, title: string, content: string, likeCount: number, commentCount: number, color: string) {
    this.id = id; this.userName = userName; this.userAvatar = userAvatar
    this.title = title; this.content = content
    this.likeCount = likeCount; this.commentCount = commentCount; this.color = color
  }
}

@Observed
class ParticleItem implements ParticleModel {
  x: number = 0
  y: number = 0
  size: number = 10
  opacity: number = 0.5
  color: string = '#C9A227'
  drift: number = 1
  constructor(x: number, y: number, size: number, opacity: number, color: string, drift: number) {
    this.x = x; this.y = y; this.size = size; this.opacity = opacity
    this.color = color; this.drift = drift
  }
}

// ============ 配置映射 ============
const SHIP_STATUS_CONFIG: Record<string, StatusMeta> = {
  '打包中': { label: '打包中', color: '#C9A227', bg: '#F7EFD8', icon: '📦' },
  '恒温运输': { label: '恒温运输', color: '#8B4513', bg: '#F0E4D4', icon: '🚚' },
  '已签收': { label: '已签收', color: '#4E6E58', bg: '#E4EBE4', icon: '✅' }
}

const ERA_CONFIG: Record<string, EraMeta> = {
  '50s': { label: '50s', color: '#8B4513', icon: '🎩' },
  '60s': { label: '60s', color: '#B85450', icon: '🎹' },
  '70s': { label: '70s', color: '#C9A227', icon: '🌻' },
  '80s': { label: '80s', color: '#6B4E8E', icon: '📼' },
  '90s': { label: '90s', color: '#4E6E58', icon: '🎧' },
  'Y2K': { label: 'Y2K', color: '#D4A76A', icon: '💿' },
  '和风古着': { label: '和风古着', color: '#C4A8E0', icon: '🏯' }
}

const CONDITION_CONFIG: Record<string, ConditionMeta> = {
  'S级': { label: 'S级', color: '#4E6E58', desc: '近乎全新' },
  'A级': { label: 'A级', color: '#8B4513', desc: '轻微使用感' },
  'B级': { label: 'B级', color: '#C9A227', desc: '正常使用痕迹' },
  'C级': { label: 'C级', color: '#B85450', desc: '有明显痕迹' }
}

const ITEM_TYPE_CONFIG: Record<string, TypeMeta> = {
  '衣物': { label: '衣物', icon: '👕', color: '#8B4513' },
  '鞋帽': { label: '鞋帽', icon: '👟', color: '#B85450' },
  '配饰': { label: '配饰', icon: '📿', color: '#6B4E8E' },
  '箱包': { label: '箱包', icon: '🧳', color: '#4E6E58' },
  '大宗': { label: '大宗', icon: '🗃️', color: '#C9A227' }
}

const STYLE_CONFIG: Record<string, StyleMeta> = {
  '美式复古': { label: '美式复古', color: '#8B4513' },
  '日系': { label: '日系', color: '#4E6E58' },
  '法风': { label: '法风', color: '#B85450' },
  '工装': { label: '工装', color: '#C9A227' },
  '学院': { label: '学院', color: '#6B4E8E' }
}

// ============ 药丸选项 ============
const ERA_PILLS: string[] = ['全部', '50s', '60s', '70s', '80s', '90s', 'Y2K', '和风古着']
const ITEM_TYPES: string[] = ['衣物', '鞋帽', '配饰', '箱包', '大宗']
const PACKAGES: string[] = ['防尘袋', '恒温箱', '木箱']
const ENVIRONMENTS: string[] = ['恒温', '防潮', '防紫外线']
const CONDITIONS: string[] = ['S级', 'A级', 'B级', 'C级']
const CLOSET_ERAS: string[] = ['50s', '60s', '70s', '80s', '90s', 'Y2K']
const SIZE_OPTIONS: string[] = ['XS', 'S', 'M', 'L', 'XL']
const SORT_OPTIONS: string[] = ['最新', '价格', '成色', '年代']
const STYLE_FILTERS: string[] = ['全部', '美式复古', '日系', '法风', '工装', '学院']
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const SHIP_MONTH_DATA: number[] = [12, 18, 15, 22, 19, 26]
const SHIP_MONTH_MAX: number = 26

// ============ 静态数据:运输记录 8 条 ============
const mockShipOrders: ShipOrderItem[] = [
  new ShipOrderItem(1, 'VV260801', '衣物', '上海安福路', '东京下北泽', 368, '恒温运输', '08-21'),
  new ShipOrderItem(2, 'VV260802', '鞋帽', '北京鼓楼', '大阪美国村', 420, '打包中', '08-22'),
  new ShipOrderItem(3, 'VV260803', '配饰', '成都玉林', '伦敦砖巷', 680, '恒温运输', '08-20'),
  new ShipOrderItem(4, 'VV260804', '箱包', '广州东山口', '首尔圣水洞', 510, '已签收', '08-15'),
  new ShipOrderItem(5, 'VV260805', '衣物', '杭州天目里', '纽约布鲁克林', 890, '已签收', '08-12'),
  new ShipOrderItem(6, 'VV260806', '大宗', '上海安福路', '代官山茑屋', 1280, '恒温运输', '08-19'),
  new ShipOrderItem(7, 'VV260807', '鞋帽', '南京颐和路', '曼谷恰图恰', 320, '打包中', '08-23'),
  new ShipOrderItem(8, 'VV260808', '配饰', '深圳南头古城', '巴黎玛黑区', 750, '已签收', '08-08')
]

// ============ 静态数据:古着商品 12 件 ============
const mockProducts: VintageProductItem[] = [
  new VintageProductItem(1, '70s 做旧麂皮流苏夹克', '70s', 899, 1499, 'A级', 328, '#B85450', '美式复古'),
  new VintageProductItem(2, '50s 廓形羊毛大衣', '50s', 1280, 1980, 'S级', 512, '#8B4513', '法风'),
  new VintageProductItem(3, '80s 做旧牛仔背带裤', '80s', 459, 720, 'B级', 208, '#4E6E58', '工装'),
  new VintageProductItem(4, '90s 格纹法兰绒衬衫', '90s', 268, 430, 'A级', 466, '#C9A227', '日系'),
  new VintageProductItem(5, 'Y2K 亮片吊带连衣裙', 'Y2K', 388, 590, 'A级', 689, '#C4A8E0', '法风'),
  new VintageProductItem(6, '60s 复古丝绒礼服裙', '60s', 1080, 1660, 'S级', 356, '#6B4E8E', '法风'),
  new VintageProductItem(7, '70s 手工钩针披肩', '70s', 329, 520, 'B级', 178, '#D4A76A', '和风古着'),
  new VintageProductItem(8, '80s 皮衣机车夹克', '80s', 1560, 2380, 'A级', 743, '#3A2A1E', '美式复古'),
  new VintageProductItem(9, '90s 尼龙运动套装', '90s', 356, 560, 'B级', 297, '#4E6E58', '学院'),
  new VintageProductItem(10, '和风古着 刺子绣外套', '和风古着', 920, 1420, 'S级', 421, '#C4A8E0', '日系'),
  new VintageProductItem(11, '50s 珍珠扣真丝衬衫', '50s', 640, 990, 'A级', 245, '#F0E4D4', '学院'),
  new VintageProductItem(12, 'Y2K 低腰水洗牛仔裤', 'Y2K', 299, 460, 'B级', 534, '#6B4E8E', '美式复古')
]

// ============ 静态数据:穿搭 6 套 ============
const mockOutfits: StyleOutfitItem[] = [
  new StyleOutfitItem(1, '下北泽周末淘货Look', '美式复古|工装', '@古着阿茶', 1203, 486, '#D4A76A'),
  new StyleOutfitItem(2, '巴黎玛黑的雨天穿搭', '法风|学院', '@Marion_V', 986, 352, '#B85450'),
  new StyleOutfitItem(3, '70s复古派对全场焦点', '美式复古', '@Vincent老周', 1570, 623, '#C9A227'),
  new StyleOutfitItem(4, '东京日常·刺子绣温柔风', '日系|和风古着', '@小野花子', 864, 401, '#C4A8E0'),
  new StyleOutfitItem(5, '校园里的90年代少年', '学院|日系', '@阿绿Green', 1109, 517, '#4E6E58'),
  new StyleOutfitItem(6, '机车周末·皮衣出街', '美式复古|工装', '@Rockabilly黎', 1392, 588, '#6B4E8E')
]

// ============ 静态数据:衣橱衣物 12 件 ============
const mockClosetItems: ClosetItem[] = [
  new ClosetItem(1, '80s 机车皮衣', '80s', 1200, 2100, 'S级', '#3A2A1E'),
  new ClosetItem(2, '70s 流苏麂皮夹克', '70s', 899, 1350, 'A级', '#B85450'),
  new ClosetItem(3, '90s 格纹衬衫', '90s', 268, 320, 'A级', '#C9A227'),
  new ClosetItem(4, '50s 羊毛大衣', '50s', 1280, 1980, 'S级', '#8B4513'),
  new ClosetItem(5, 'Y2K 亮片吊带裙', 'Y2K', 388, 560, 'A级', '#C4A8E0'),
  new ClosetItem(6, '60s 丝绒礼服', '60s', 1080, 1520, 'S级', '#6B4E8E'),
  new ClosetItem(7, '和风 刺子绣外套', '和风古着', 920, 1380, 'S级', '#C4A8E0'),
  new ClosetItem(8, '90s 运动卫衣', '90s', 210, 260, 'B级', '#4E6E58'),
  new ClosetItem(9, '80s 牛仔背带裤', '80s', 459, 610, 'B级', '#4E6E58'),
  new ClosetItem(10, '50s 真丝衬衫', '50s', 640, 820, 'A级', '#D4A76A'),
  new ClosetItem(11, '70s 钩针披肩', '70s', 329, 470, 'B级', '#D4A76A'),
  new ClosetItem(12, 'Y2K 水洗牛仔裤', 'Y2K', 299, 380, 'B级', '#8A7560')
]

// ============ 静态数据:故事 6 条 ============
const mockStories: StoryItem[] = [
  new StoryItem(1, '古着阿茶', '🍵', '在下北泽用3000日元淘到人生夹克', '翻了一下午的货架,最后在角落发现了这件80s的机车皮衣,皮质柔软得像黄油,老板说前主人是当地乐队贝斯手……', 1203, 156, '#D4A76A'),
  new StoryItem(2, 'Marion_V', '🥐', '玛黑区古着店地图(收藏向)', '整理了巴黎玛黑区7家值得一逛的古着店,从平价集市到高定 Archive,附上营业时间和砍价话术……', 986, 203, '#B85450'),
  new StoryItem(3, 'Vincent老周', '🎸', '修复一件1972年的麂皮夹克', '袖口磨破、内衬脱落、拉链卡顿,花了三个周末慢慢修,过程比结果更治愈,附工具清单……', 1570, 289, '#C9A227'),
  new StoryItem(4, '小野花子', '🌸', '刺子绣外套的日常搭配笔记', '和风古着最迷人的是织物里的时间感,这件刺子绣外套我配了靛蓝阔腿裤和木屐凉鞋……', 864, 132, '#C4A8E0'),
  new StoryItem(5, '阿绿Green', '🎒', '为什么我不再买快时尚', '入坑古着三年,衣橱从37件精简到12件,每件都被认真穿着。这是我的消费观转变记录……', 1109, 347, '#4E6E58'),
  new StoryItem(6, 'Rockabilly黎', '🪩', '第一次参加复古摇摆舞会', '为了这场Y2K主题舞会准备了两个月,从造型到舞步,完整复盘这套亮片吊带裙Look……', 1392, 198, '#6B4E8E')
]

// ============ 辅助纯函数 ============
function formatPrice(v: number): string {
  return '¥' + v.toFixed(0)
}

function getEraLabel(era: string): string {
  return ERA_CONFIG[era]?.label ?? era
}

function getEraIcon(era: string): string {
  return ERA_CONFIG[era]?.icon ?? ' Vintage '
}

function getClosetTotalValue(): number {
  let total: number = 0
  for (let i = 0; i < mockClosetItems.length; i++) {
    total += mockClosetItems[i].currentValue
  }
  return total
}

function getClosetTotalCost(): number {
  let total: number = 0
  for (let i = 0; i < mockClosetItems.length; i++) {
    total += mockClosetItems[i].purchasePrice
  }
  return total
}

function splitTags(tags: string): string[] {
  return tags.split('|')
}

function createParticles(): ParticleItem[] {
  const list: ParticleItem[] = []
  const colors: string[] = ['#C9A227', '#8B4513', '#6B4E8E', '#D4A76A', '#B85450']
  const xs: number[] = [18, 62, 110, 158, 205, 252, 300, 128, 78, 230]
  const ys: number[] = [640, 420, 560, 300, 500, 180, 380, 90, 660, 240]
  const sizes: number[] = [10, 14, 8, 16, 12, 9, 15, 11, 13, 10]
  for (let i = 0; i < 10; i++) {
    list.push(new ParticleItem(xs[i], ys[i], sizes[i], 0.25 + (i % 3) * 0.12, colors[i % colors.length], 1 + (i % 3)))
  }
  return list
}

function stepParticles(list: ParticleItem[]): ParticleItem[] {
  const next: ParticleItem[] = []
  for (let i = 0; i < list.length; i++) {
    const p: ParticleItem = list[i]
    let ny: number = p.y - p.drift * 8
    let nx: number = p.x + (i % 2 === 0 ? 3 : -2)
    if (ny < -12) {
      ny = 700
    }
    if (nx > 336) {
      nx = 6
    }
    if (nx < 0) {
      nx = 330
    }
    next.push(new ParticleItem(nx, ny, p.size, p.opacity, p.color, p.drift))
  }
  return next
}

// ============ Tab 枚举 ============
enum VintageTab {
  SHIP = 0,
  SHOP = 1,
  STYLE = 2,
  CLOSET = 3,
  STORY = 4,
  PROFILE = 5
}

// ============ 入口主页面 ============
@Entry
@Component
struct VintageVaultApp {
  @State activeTab: VintageTab = VintageTab.SHIP
  @State searchKeyword: string = ''
  @State selectedEra: string = '全部'
  @State particles: ParticleItem[] = []
  private timerId: number = -1

  aboutToAppear() {
    this.particles = createParticles()
    this.timerId = setInterval(() => {
      this.particles = stepParticles(this.particles)
    }, 300)
  }

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

  @Builder contentArea() {
    Column() {
      if (this.activeTab === VintageTab.SHIP) {
        ShipContent()
      } else if (this.activeTab === VintageTab.SHOP) {
        ShopContent()
      } else if (this.activeTab === VintageTab.STYLE) {
        StyleContent()
      } else if (this.activeTab === VintageTab.CLOSET) {
        ClosetContent()
      } else if (this.activeTab === VintageTab.STORY) {
        StoryContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: VintageTab) {
    Column() {
      Text(icon).fontSize(18).opacity(this.activeTab === tab ? 1.0 : 0.45)
      Text(label).fontSize(8)
        .fontColor(this.activeTab === tab ? COLOR_PRIMARY : COLOR_TEXT_HINT)
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 1 })
      if (this.activeTab === tab) {
        Column().width(16).height(3)
          .backgroundColor(COLOR_PRIMARY).borderRadius(2).margin({ top: 2 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Stack() {
      Column() {
        // ===== 头部:第一行 Logo + 搜索 + 消息 =====
        Row() {
          Column() {
            Text('古着衣橱').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
            Text('VINTAGE VAULT').fontSize(7).fontColor(COLOR_TEXT_HINT).margin({ top: 1 })
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 14 })

          Row() {
            Text('🔍').fontSize(12).margin({ left: 8 })
            TextInput({ placeholder: '搜索Vintage单品' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(11).layoutWeight(1)
              .backgroundColor(COLOR_BG).borderRadius(14).height(30)
              .margin({ left: 4, right: 4 })
              .onChange((v: string) => { this.searchKeyword = v })
          }
          .layoutWeight(1)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .borderRadius(15)
          .margin({ left: 10 })
          .padding({ right: 4 })

          Column() {
            Text('✉️').fontSize(16)
            Column().width(6).height(6).borderRadius(3).backgroundColor(COLOR_BRICK)
          }
          .width(34).height(34).borderRadius(17)
          .backgroundColor(COLOR_PRIMARY_LIGHT)
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          .margin({ left: 10, right: 12 })
          .onClick(() => {
            this.activeTab = VintageTab.STORY
          })
        }
        .width('100%').height(48)
        .alignItems(VerticalAlign.Center)
        .backgroundColor(COLOR_CARD)

        // ===== 头部:第二行 年代药丸横向滚动 =====
        Scroll() {
          Row() {
            ForEach(ERA_PILLS, (era: string) => {
              if (this.selectedEra === era) {
                Text((era === '全部' ? '🕰️ ' : (ERA_CONFIG[era]?.icon ?? '') + ' ') + era)
                  .fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
              } else {
                Text((era === '全部' ? '🕰️ ' : (ERA_CONFIG[era]?.icon ?? '') + ' ') + era)
                  .fontSize(10).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
                  .border({ width: 1, color: COLOR_BORDER })
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
                  .onClick(() => { this.selectedEra = era })
              }
            })
          }
          .padding({ left: 8, right: 8, top: 6, bottom: 6 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
        .width('100%')
        .backgroundColor(COLOR_BG)

        // ===== 内容区 =====
        this.contentArea()

        // ===== 底部Tab =====
        Row() {
          this.bottomTabItem('📦', '运输', VintageTab.SHIP)
          this.bottomTabItem('👕', '商城', VintageTab.SHOP)
          this.bottomTabItem('👗', '穿搭', VintageTab.STYLE)
          this.bottomTabItem('🚪', '衣橱', VintageTab.CLOSET)
          this.bottomTabItem('📖', '故事', VintageTab.STORY)
          this.bottomTabItem('👤', '我的', VintageTab.PROFILE)
        }
        .width('100%')
        .backgroundColor(COLOR_CARD)
        .border({ width: 1, color: COLOR_BORDER })
        .padding({ top: 3, bottom: 5 })
        .shadow({ radius: 8, color: '#148B4513', offsetY: -2 })
      }
      .width('100%').height('100%')

      // ===== 粒子层(复古金/棕/紫 漂浮星星)=====
      Stack() {
        ForEach(this.particles, (p: ParticleItem) => {
          Text('✦')
            .fontSize(p.size)
            .fontColor(p.color)
            .opacity(p.opacity)
            .position({ x: p.x, y: p.y })
            .hitTestBehavior(HitTestMode.None)
        })
      }
      .width('100%').height('100%')
      .hitTestBehavior(HitTestMode.None)
    }
    .width('100%').height('100%')
    .backgroundColor(COLOR_BG)
  }
}

// ============ Tab1 运输页 ============
@Component
struct ShipContent {
  @State showBookingModal: boolean = false
  @State formItemType: string = '衣物'
  @State formCount: string = '2'
  @State formPack: string = '恒温箱'
  @State formEnv: string = '恒温'
  @State formFrom: string = ''
  @State formTo: string = ''
  @State formNote: string = ''

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

  // ========== 弹框1:预约恒温运输(复古表单式)==========
  @Builder bookingModal() {
    Column() {
      this.modalOverlay(() => { this.showBookingModal = false })
      Column() {
        // 棕色头部
        Row() {
          Text('📦 预约恒温运输').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_CARD)
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#F0E4D4')
            .padding(4)
            .onClick(() => { this.showBookingModal = false })
        }
        .width('100%').padding({ left: 18, right: 14, top: 14, bottom: 12 })
        .backgroundColor(COLOR_PRIMARY)
        .border({ width: 1, color: COLOR_PRIMARY })
        .borderRadius({ topLeft: 14, topRight: 14 })

        Scroll() {
          Column() {
            Text('物品类型').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            Row() {
              ForEach(ITEM_TYPES, (t: string) => {
                if (this.formItemType === t) {
                  Text((ITEM_TYPE_CONFIG[t]?.icon ?? '') + ' ' + t)
                    .fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text((ITEM_TYPE_CONFIG[t]?.icon ?? '') + ' ' + t)
                    .fontSize(10).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.formItemType = t })
                }
              })
            }
            .margin({ left: 14, right: 14, top: 4 })

            Text('件数').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            TextInput({ placeholder: '输入件数,如 2', text: this.formCount })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(12).type(InputType.Number)
              .backgroundColor(COLOR_BG).borderRadius(8).height(36)
              .margin({ left: 18, right: 18, top: 4 })
              .onChange((v: string) => { this.formCount = v })

            Text('包装方式').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            Row() {
              ForEach(PACKAGES, (p: string) => {
                if (this.formPack === p) {
                  Text(p).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_SECONDARY)
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(p).fontSize(10).fontColor(COLOR_SECONDARY).backgroundColor('#F0EAF6')
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formPack = p })
                }
              })
            }
            .margin({ left: 14, right: 14, top: 4 })

            Text('运输环境').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            Row() {
              ForEach(ENVIRONMENTS, (e: string) => {
                if (this.formEnv === e) {
                  Text(e).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_GREEN)
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(e).fontSize(10).fontColor(COLOR_GREEN).backgroundColor('#E4EBE4')
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formEnv = e })
                }
              })
            }
            .margin({ left: 14, right: 14, top: 4 })

            Row() {
              Column() {
                Text('出发地').fontSize(11).fontColor(COLOR_TEXT_SUB)
                TextInput({ placeholder: '如 上海安福路' })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
                  .backgroundColor(COLOR_BG).borderRadius(8).height(36).margin({ top: 4 })
                  .onChange((v: string) => { this.formFrom = v })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start)

              Column() {
                Text('目的地').fontSize(11).fontColor(COLOR_TEXT_SUB)
                TextInput({ placeholder: '如 东京下北泽' })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
                  .backgroundColor(COLOR_BG).borderRadius(8).height(36).margin({ top: 4 })
                  .onChange((v: string) => { this.formTo = v })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
            }
            .width('100%').margin({ left: 18, right: 18, top: 10 })

            Text('备注').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 10, left: 18 })
            TextArea({ placeholder: '特殊要求,如防压、悬挂运输、亮片衣物单独防尘袋…' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(11).height(60)
              .backgroundColor(COLOR_BG).borderRadius(8)
              .margin({ left: 18, right: 18, top: 4 })
              .onChange((v: string) => { this.formNote = v })
          }
          .constraintSize({ maxHeight: '80%' })
          .width('100%')
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1).scrollBar(BarState.Off)

        // 底部按钮
        Row() {
          Text('取消').fontSize(13).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG).borderRadius(18)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 22, right: 22, top: 9, bottom: 9 })
            .onClick(() => { this.showBookingModal = false })
          Text('确认预约').fontSize(13).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_PRIMARY).borderRadius(18)
            .padding({ left: 22, right: 22, top: 9, bottom: 9 })
            .margin({ left: 12 })
            .onClick(() => { this.showBookingModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 12, bottom: 14 })
      }
      .width('90%').height('75%').backgroundColor(COLOR_CARD).borderRadius(14)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '5%', y: '12%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder shipRecordBuilder(o: ShipOrderItem) {
    Column() {
      Row() {
        Column() {
          Text(ITEM_TYPE_CONFIG[o.itemType]?.icon ?? '📦').fontSize(18)
        }
        .width(38).height(38).borderRadius(19)
        .backgroundColor(COLOR_BG)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Text(o.pickup + ' → ' + o.destination)
            .fontSize(12).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
          Row() {
            Text(o.orderNo).fontSize(9).fontColor(COLOR_TEXT_HINT)
            Text(' · ' + o.date).fontSize(9).fontColor(COLOR_TEXT_HINT)
          }
          .margin({ top: 2 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })

        Column() {
          Text(formatPrice(o.price)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
          Text((SHIP_STATUS_CONFIG[o.status]?.icon ?? '') + ' ' + (SHIP_STATUS_CONFIG[o.status]?.label ?? o.status))
            .fontSize(9).fontColor(SHIP_STATUS_CONFIG[o.status]?.color ?? COLOR_TEXT_SUB)
            .backgroundColor(SHIP_STATUS_CONFIG[o.status]?.bg ?? COLOR_BG)
            .padding({ left: 6, right: 6, top: 1, bottom: 1 }).borderRadius(8)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
    }
    .width('100%').backgroundColor(COLOR_CARD)
    .borderRadius(10).border({ width: 1, color: COLOR_BORDER })
    .padding(10).margin({ left: 12, right: 12, top: 6 })
    .onClick(() => { this.showBookingModal = false })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 恒温运输环境监控卡 =====
            Column() {
              Row() {
                Text('🚚 恒温运输舱环境监控').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                Column().layoutWeight(1)
                Text('实时').fontSize(9).fontColor(COLOR_SUCCESS)
                  .backgroundColor('#E4EBE4').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8)
              }
              .width('100%')

              Row() {
                Column() {
                  Text('🌡️').fontSize(16)
                  Text('22℃').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                    .margin({ top: 2 })
                  Text('温度').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)

                Column().width(1).height(44).backgroundColor(COLOR_BORDER)

                Column() {
                  Text('💧').fontSize(16)
                  Text('45%').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_SECONDARY)
                    .margin({ top: 2 })
                  Text('湿度').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)

                Column().width(1).height(44).backgroundColor(COLOR_BORDER)

                Column() {
                  Text('🕶️').fontSize(16)
                  Text('已开启').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_GREEN)
                    .margin({ top: 5 })
                  Text('紫外线防护').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 5 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
              }
              .width('100%').margin({ top: 12 })
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding(14).margin({ left: 12, right: 12, top: 10 })

            // ===== 运输预约表单(内嵌)=====
            Column() {
              Text('📋 快速预约').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .width('100%')

              Text('物品类型').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 10 })
              Row() {
                ForEach(['衣物', '鞋帽', '配饰', '箱包'], (t: string) => {
                  if (this.formItemType === t) {
                    Text(t).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                      .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                      .margin({ left: 3, right: 3 })
                  } else {
                    Text(t).fontSize(10).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.formItemType = t })
                  }
                })
              }
              .margin({ top: 4 })

              Row() {
                Column() {
                  Text('件数').fontSize(10).fontColor(COLOR_TEXT_SUB)
                  TextInput({ placeholder: '件数', text: this.formCount })
                    .placeholderColor(COLOR_TEXT_HINT).fontSize(11).type(InputType.Number)
                    .backgroundColor(COLOR_BG).borderRadius(8).height(32).margin({ top: 3 })
                    .onChange((v: string) => { this.formCount = v })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Start)

                Column() {
                  Text('包装').fontSize(10).fontColor(COLOR_TEXT_SUB)
                  Row() {
                    ForEach(PACKAGES, (p: string) => {
                      if (this.formPack === p) {
                        Text(p).fontSize(9).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
                          .margin({ left: 2, right: 2 })
                      } else {
                        Text(p).fontSize(9).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                          .border({ width: 1, color: COLOR_BORDER })
                          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
                          .margin({ left: 2, right: 2 })
                          .onClick(() => { this.formPack = p })
                      }
                    })
                  }
                  .margin({ top: 3 })
                }
                .layoutWeight(1.4).alignItems(HorizontalAlign.Start).padding({ left: 10 })
              }
              .width('100%').margin({ top: 10 })

              Row() {
                Column() {
                  Text('出发地').fontSize(10).fontColor(COLOR_TEXT_SUB)
                  TextInput({ placeholder: '出发地' })
                    .placeholderColor(COLOR_TEXT_HINT).fontSize(11)
                    .backgroundColor(COLOR_BG).borderRadius(8).height(32).margin({ top: 3 })
                    .onChange((v: string) => { this.formFrom = v })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Start)

                Column() {
                  Text('目的地').fontSize(10).fontColor(COLOR_TEXT_SUB)
                  TextInput({ placeholder: '目的地' })
                    .placeholderColor(COLOR_TEXT_HINT).fontSize(11)
                    .backgroundColor(COLOR_BG).borderRadius(8).height(32).margin({ top: 3 })
                    .onChange((v: string) => { this.formTo = v })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
              }
              .width('100%').margin({ top: 10 })

              Text('展开完整恒温预约单 →').fontSize(11).fontColor(COLOR_CARD)
                .backgroundColor(COLOR_PRIMARY).borderRadius(16)
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .alignSelf(ItemAlign.Center).margin({ top: 14 })
                .onClick(() => { this.showBookingModal = true })
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding(14).margin({ left: 12, right: 12, top: 10 })
            .alignItems(HorizontalAlign.Start)

            // ===== 月度运输量柱状图 =====
            Column() {
              Text('📊 月度恒温运输量').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .width('100%')
              Row() {
                ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
                  Column() {
                    Text(SHIP_MONTH_DATA[i].toString())
                      .fontSize(9).fontColor(COLOR_PRIMARY).margin({ bottom: 3 })
                    Column()
                      .width(24)
                      .height((SHIP_MONTH_DATA[i] / SHIP_MONTH_MAX * 76).toFixed(0) + 'vp')
                      .backgroundColor(i % 2 === 0 ? COLOR_PRIMARY_LIGHT : COLOR_PRIMARY)
                      .borderRadius({ topLeft: 4, topRight: 4 })
                    Text(MONTH_LABELS[i]).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
                  }
                  .layoutWeight(1).alignItems(HorizontalAlign.Center)
                })
              }
              .width('100%').margin({ top: 10 })
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding(14).margin({ left: 12, right: 12, top: 10 })

            // ===== 运输记录 =====
            Row() {
              Text('🧾 运输记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
              Column().layoutWeight(1)
              Text('共8单').fontSize(10).fontColor(COLOR_TEXT_HINT)
            }
            .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 4 })

            this.shipRecordBuilder(mockShipOrders[0])
            this.shipRecordBuilder(mockShipOrders[1])
            this.shipRecordBuilder(mockShipOrders[2])
            this.shipRecordBuilder(mockShipOrders[3])
            this.shipRecordBuilder(mockShipOrders[4])
            this.shipRecordBuilder(mockShipOrders[5])
            this.shipRecordBuilder(mockShipOrders[6])
            this.shipRecordBuilder(mockShipOrders[7])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showBookingModal) { this.bookingModal() }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab2 商城页 ============
@Component
struct ShopContent {
  @State sortMode: string = '最新'
  @State showDetailModal: boolean = false
  @State selectedProduct: VintageProductItem | null = null
  @State detailSize: string = 'M'
  @State detailCount: number = 1

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

  // ========== 弹框4:商品详情 + 入手 ==========
  @Builder productDetailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        // 商品图
        Column() {
          Text(' Vintage ').fontSize(30).fontColor(COLOR_CARD).opacity(0.7)
          Text(this.selectedProduct?.name ?? '').fontSize(12).fontColor(COLOR_CARD)
            .margin({ top: 4 }).opacity(0.9)
        }
        .width('100%').height(140)
        .backgroundColor(this.selectedProduct?.color ?? COLOR_PRIMARY_LIGHT)
        .borderRadius({ topLeft: 14, topRight: 14 })
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Scroll() {
          Column() {
            Row() {
              Text((ERA_CONFIG[this.selectedProduct?.era ?? '90s']?.icon ?? '') + ' ' + getEraLabel(this.selectedProduct?.era ?? '90s'))
                .fontSize(10).fontColor(COLOR_PRIMARY).backgroundColor('#F0E4D4')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
              Text(this.selectedProduct?.condition ?? 'A级')
                .fontSize(10).fontColor(CONDITION_CONFIG[this.selectedProduct?.condition ?? 'A级']?.color ?? COLOR_PRIMARY)
                .backgroundColor(COLOR_BG)
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                .margin({ left: 6 })
              Text('#' + (this.selectedProduct?.tag ?? ''))
                .fontSize(10).fontColor(COLOR_SECONDARY).backgroundColor('#F0EAF6')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                .margin({ left: 6 })
              Column().layoutWeight(1)
              Text('✕').fontSize(15).fontColor(COLOR_TEXT_HINT)
                .padding(4)
                .onClick(() => { this.showDetailModal = false })
            }
            .width('100%').margin({ top: 10 })

            Row() {
              Text(formatPrice(this.selectedProduct?.price ?? 0))
                .fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLOR_BRICK)
              Text(formatPrice(this.selectedProduct?.originPrice ?? 0))
                .fontSize(11).fontColor(COLOR_TEXT_HINT)
                .decoration({ type: TextDecorationType.LineThrough })
                .margin({ left: 8, top: 6 })
              Column().layoutWeight(1)
              Text('❤️ ' + (this.selectedProduct?.likeCount ?? 0).toString())
                .fontSize(10).fontColor(COLOR_TEXT_SUB)
            }
            .width('100%').alignItems(VerticalAlign.Center).margin({ top: 6 })

            Text('成色说明:' + (CONDITION_CONFIG[this.selectedProduct?.condition ?? 'A级']?.desc ?? ''))
              .fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 6 })

            Text('古着描述:真古着单品,经过专业清洗与恒温仓储保养,织物纤维状态良好,纽扣拉链均为原装,带自然年代感与使用痕迹,正是古着的灵魂所在。')
              .fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 8 })

            Text('尺码选择').fontSize(11).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
              .margin({ top: 12 })
            Row() {
              ForEach(SIZE_OPTIONS, (s: string) => {
                if (this.detailSize === s) {
                  Text(s).fontSize(11).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                    .width(38).height(30).borderRadius(15)
                    .textAlign(TextAlign.Center).margin({ left: 4, right: 4 })
                } else {
                  Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .width(38).height(30).borderRadius(15)
                    .textAlign(TextAlign.Center).margin({ left: 4, right: 4 })
                    .onClick(() => { this.detailSize = s })
                }
              })
            }
            .margin({ top: 6 })

            Row() {
              Text('数量').fontSize(11).fontColor(COLOR_TEXT_MAIN)
              Column().layoutWeight(1)
              Text('-').fontSize(14).fontColor(COLOR_TEXT_SUB)
                .width(28).height(28).borderRadius(14).backgroundColor(COLOR_BG)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  if (this.detailCount > 1) {
                    this.detailCount = this.detailCount - 1
                  }
                })
              Text(this.detailCount.toString()).fontSize(13).fontWeight(FontWeight.Bold)
                .fontColor(COLOR_TEXT_MAIN).width(36).textAlign(TextAlign.Center)
              Text('+').fontSize(14).fontColor(COLOR_CARD)
                .width(28).height(28).borderRadius(14).backgroundColor(COLOR_PRIMARY)
                .textAlign(TextAlign.Center)
                .onClick(() => { this.detailCount = this.detailCount + 1 })
            }
            .width('100%').alignItems(VerticalAlign.Center).margin({ top: 14 })
          }
          .width('100%').padding({ left: 16, right: 16, bottom: 12 })
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1).scrollBar(BarState.Off)

        // 底部双按钮
        Row() {
          Text('☆ 加入收藏').fontSize(12).fontColor(COLOR_SECONDARY)
            .layoutWeight(1).height(38).borderRadius(19)
            .border({ width: 1, color: COLOR_SECONDARY })
            .textAlign(TextAlign.Center)
            .onClick(() => { this.showDetailModal = false })
          Text('立即入手').fontSize(12).fontColor(COLOR_CARD)
            .layoutWeight(1).height(38).borderRadius(19)
            .backgroundColor(COLOR_BRICK).margin({ left: 10 })
            .textAlign(TextAlign.Center)
            .onClick(() => { this.showDetailModal = false })
        }
        .width('100%').alignItems(VerticalAlign.Center)
        .padding({ left: 16, right: 16, top: 10, bottom: 14 })
      }
      .width('92%').height('70%').backgroundColor(COLOR_CARD).borderRadius(14)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '4%', y: '14%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder productCardBuilder(p: VintageProductItem) {
    Column() {
      Column() {
        Text((ERA_CONFIG[p.era]?.icon ?? '')).fontSize(24).opacity(0.85)
        Text(getEraLabel(p.era)).fontSize(9).fontColor(COLOR_CARD).opacity(0.9).margin({ top: 3 })
      }
      .width('100%').height(96)
      .backgroundColor(p.color)
      .borderRadius({ topLeft: 10, topRight: 10 })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(p.name).fontSize(11).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Row() {
          Text(p.condition).fontSize(9)
            .fontColor(CONDITION_CONFIG[p.condition]?.color ?? COLOR_PRIMARY)
            .backgroundColor(COLOR_BG)
            .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
          Text('❤️' + p.likeCount.toString()).fontSize(9).fontColor(COLOR_TEXT_HINT)
            .margin({ left: 6 })
        }
        .margin({ top: 4 })

        Row() {
          Text(formatPrice(p.price)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_BRICK)
          Text(formatPrice(p.originPrice)).fontSize(9).fontColor(COLOR_TEXT_HINT)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 4 })
        }
        .margin({ top: 5 })

        Text('入手').fontSize(10).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_PRIMARY).borderRadius(12)
          .padding({ left: 20, right: 20, top: 5, bottom: 5 })
          .alignSelf(ItemAlign.Center).margin({ top: 7 })
          .onClick(() => {
            this.selectedProduct = p
            this.detailCount = 1
            this.showDetailModal = true
          })
      }
      .width('100%').padding(8).alignItems(HorizontalAlign.Start)
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(10).border({ width: 1, color: COLOR_BORDER })
    .margin({ left: 3, right: 3 })
    .onClick(() => {
      this.selectedProduct = p
      this.detailCount = 1
      this.showDetailModal = true
    })
  }

  build() {
    Stack() {
      Column() {
        // 排序药丸
        Scroll() {
          Row() {
            ForEach(SORT_OPTIONS, (s: string) => {
              if (this.sortMode === s) {
                Text(s).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
              } else {
                Text(s).fontSize(10).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
                  .border({ width: 1, color: COLOR_BORDER })
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
                  .onClick(() => { this.sortMode = s })
              }
            })
          }
          .padding({ left: 9, right: 9, top: 8, bottom: 4 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
        .width('100%')

        Scroll() {
          Column() {
            // ===== 年代分布横向进度条 =====
            Column() {
              Text('🕰️ 在售年代分布').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                .width('100%')
              Column() {
                Row() {
                  Text('70s').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('25%').height(6).backgroundColor(COLOR_BRICK).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('3件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
                Row() {
                  Text('80s').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('17%').height(6).backgroundColor(COLOR_PRIMARY).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('2件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
                Row() {
                  Text('90s').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('17%').height(6).backgroundColor(COLOR_GREEN).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('2件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
                Row() {
                  Text('Y2K').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('17%').height(6).backgroundColor(COLOR_SECONDARY).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('2件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
                Row() {
                  Text('50s/60s').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('17%').height(6).backgroundColor(COLOR_MUSTARD).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('2件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
                Row() {
                  Text('和风').fontSize(10).fontColor(COLOR_TEXT_SUB).width(46)
                  Column().width('8%').height(6).backgroundColor(COLOR_SECONDARY_LIGHT).borderRadius(3)
                  Column().layoutWeight(1)
                  Text('1件').fontSize(9).fontColor(COLOR_TEXT_HINT)
                }
                .width('100%').margin({ top: 8 })
              }
              .width('100%').margin({ top: 6 })
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding(14).margin({ left: 12, right: 12, top: 6 })

            // ===== 2列商品网格 =====
            Row() {
              this.productCardBuilder(mockProducts[0])
              this.productCardBuilder(mockProducts[1])
            }
            .width('100%').margin({ top: 10, left: 6, right: 6 })

            Row() {
              this.productCardBuilder(mockProducts[2])
              this.productCardBuilder(mockProducts[3])
            }
            .width('100%').margin({ top: 8, left: 6, right: 6 })

            Row() {
              this.productCardBuilder(mockProducts[4])
              this.productCardBuilder(mockProducts[5])
            }
            .width('100%').margin({ top: 8, left: 6, right: 6 })

            Row() {
              this.productCardBuilder(mockProducts[6])
              this.productCardBuilder(mockProducts[7])
            }
            .width('100%').margin({ top: 8, left: 6, right: 6 })

            Row() {
              this.productCardBuilder(mockProducts[8])
              this.productCardBuilder(mockProducts[9])
            }
            .width('100%').margin({ top: 8, left: 6, right: 6 })

            Row() {
              this.productCardBuilder(mockProducts[10])
              this.productCardBuilder(mockProducts[11])
            }
            .width('100%').margin({ top: 8, left: 6, right: 6 })
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showDetailModal) { this.productDetailModal() }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab3 穿搭页 ============
@Component
struct StyleContent {
  @State styleFilter: string = '全部'

  @Builder outfitCardBuilder(o: StyleOutfitItem) {
    Column() {
      // 渐变穿搭大图块
      Column() {
        Text('👗').fontSize(40).opacity(0.9)
        Text('OUTFIT #' + o.id.toString()).fontSize(9)
          .fontColor(COLOR_CARD).opacity(0.85).margin({ top: 6 })
      }
      .width('100%').height(150)
      .linearGradient({
        angle: 135,
        colors: [[o.color, 0.0], ['#8B4513', 1.0]]
      })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(o.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
        Row() {
          ForEach(splitTags(o.styleTags), (t: string) => {
            Text('#' + t).fontSize(9)
              .fontColor(STYLE_CONFIG[t]?.color ?? COLOR_PRIMARY)
              .backgroundColor(COLOR_BG)
              .padding({ left: 7, right: 7, top: 3, bottom: 3 }).borderRadius(10)
              .margin({ right: 5 })
          })
        }
        .margin({ top: 6 })

        Row() {
          Text(o.author).fontSize(10).fontColor(COLOR_SECONDARY).fontWeight(FontWeight.Medium)
          Column().layoutWeight(1)
          Text('❤️ ' + o.likeCount.toString()).fontSize(10).fontColor(COLOR_BRICK)
          Text('⭐ ' + o.favCount.toString()).fontSize(10).fontColor(COLOR_MUSTARD).margin({ left: 10 })
        }
        .width('100%').margin({ top: 10 })
      }
      .width('100%').padding(12).alignItems(HorizontalAlign.Start)
    }
    .width('100%').backgroundColor(COLOR_CARD)
    .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
    .margin({ left: 12, right: 12, top: 10 })
    .onClick(() => { this.styleFilter = this.styleFilter })
  }

  build() {
    Column() {
      // 风格筛选药丸
      Scroll() {
        Row() {
          ForEach(STYLE_FILTERS, (s: string) => {
            if (this.styleFilter === s) {
              Text(s).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_SECONDARY)
                .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                .margin({ left: 3, right: 3 })
            } else {
              Text(s).fontSize(10).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
                .border({ width: 1, color: COLOR_BORDER })
                .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.styleFilter = s })
            }
          })
        }
        .padding({ left: 9, right: 9, top: 10, bottom: 4 })
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
      .width('100%')

      Scroll() {
        Column() {
          Text('✨ 本周热门古着穿搭').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
            .width('100%').padding({ left: 16, top: 6, bottom: 2 })

          this.outfitCardBuilder(mockOutfits[0])
          this.outfitCardBuilder(mockOutfits[1])
          this.outfitCardBuilder(mockOutfits[2])
          this.outfitCardBuilder(mockOutfits[3])
          this.outfitCardBuilder(mockOutfits[4])
          this.outfitCardBuilder(mockOutfits[5])
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab4 衣橱页 ============
@Component
struct ClosetContent {
  @State showEditModal: boolean = false
  @State showRemoveModal: boolean = false
  @State editingItem: ClosetItem | null = null
  @State removeItem: ClosetItem | null = null
  @State editName: string = ''
  @State editEra: string = '80s'
  @State editCondition: string = 'A级'
  @State editBuyPrice: string = ''
  @State editValue: string = ''
  @State editNote: string = ''

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

  // ========== 弹框2:编辑衣物档案(卡片式)==========
  @Builder editClothModal() {
    Column() {
      this.modalOverlay(() => { this.showEditModal = false })
      Column() {
        Row() {
          Column() {
            Text('🧵').fontSize(20)
          }
          .width(36).height(36).borderRadius(18)
          .backgroundColor(this.editingItem?.color ?? COLOR_PRIMARY_LIGHT)
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

          Column() {
            Text('✏️ 编辑衣物档案').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
            Text('每件古着都值得被认真记录').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 2 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })

        Scroll() {
          Column() {
            Text('衣物名称').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 6 })
            TextInput({ placeholder: '如 80s 机车皮衣', text: this.editName })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(12)
              .backgroundColor(COLOR_BG).borderRadius(8).height(34).margin({ top: 3 })
              .onChange((v: string) => { this.editName = v })

            Text('年代').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 10 })
            Scroll() {
              Row() {
                ForEach(CLOSET_ERAS, (e: string) => {
                  if (this.editEra === e) {
                    Text(e).fontSize(10).fontColor(COLOR_CARD).backgroundColor(COLOR_PRIMARY)
                      .padding({ left: 11, right: 11, top: 5, bottom: 5 }).borderRadius(12)
                      .margin({ left: 3, right: 3 })
                  } else {
                    Text(e).fontSize(10).fontColor(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.editEra = e })
                  }
                })
              }
            }
            .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
            .width('100%').margin({ top: 3 })

            Text('成色').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 10 })
            Row() {
              ForEach(CONDITIONS, (c: string) => {
                if (this.editCondition === c) {
                  Text(c).fontSize(10).fontColor(COLOR_CARD)
                    .backgroundColor(CONDITION_CONFIG[c]?.color ?? COLOR_PRIMARY)
                    .padding({ left: 13, right: 13, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(c).fontSize(10)
                    .fontColor(CONDITION_CONFIG[c]?.color ?? COLOR_TEXT_SUB)
                    .backgroundColor(COLOR_BG)
                    .border({ width: 1, color: COLOR_BORDER })
                    .padding({ left: 13, right: 13, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.editCondition = c })
                }
              })
            }
            .margin({ top: 3 })

            Row() {
              Column() {
                Text('购入价').fontSize(10).fontColor(COLOR_TEXT_SUB)
                TextInput({ placeholder: '购入价', text: this.editBuyPrice })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(11).type(InputType.Number)
                  .backgroundColor(COLOR_BG).borderRadius(8).height(32).margin({ top: 3 })
                  .onChange((v: string) => { this.editBuyPrice = v })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start)

              Column() {
                Text('当前估值').fontSize(10).fontColor(COLOR_TEXT_SUB)
                TextInput({ placeholder: '当前估值', text: this.editValue })
                  .placeholderColor(COLOR_TEXT_HINT).fontSize(11).type(InputType.Number)
                  .backgroundColor(COLOR_BG).borderRadius(8).height(32).margin({ top: 3 })
                  .onChange((v: string) => { this.editValue = v })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
            }
            .width('100%').margin({ top: 10 })

            Text('备注').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 10 })
            TextArea({ placeholder: '记录来源、修补历史、搭配心得…' })
              .placeholderColor(COLOR_TEXT_HINT).fontSize(11).height(56)
              .backgroundColor(COLOR_BG).borderRadius(8).margin({ top: 3 })
              .onChange((v: string) => { this.editNote = v })
          }
          .width('100%').padding({ left: 16, right: 16, bottom: 10 })
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1).scrollBar(BarState.Off)

        Row() {
          Text('取消').fontSize(12).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG).borderRadius(16)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .onClick(() => { this.showEditModal = false })
          Text('保存档案').fontSize(12).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_PRIMARY).borderRadius(16)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .margin({ left: 10 })
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 10, bottom: 14 })
      }
      .width('85%').height('60%').backgroundColor(COLOR_CARD).borderRadius(14)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '7.5%', y: '18%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // ========== 弹框3:移出衣橱确认(警示式)==========
  @Builder removeConfirmModal() {
    Column() {
      this.modalOverlay(() => { this.showRemoveModal = false })
      Column() {
        Column() {
          Text('🗑️').fontSize(34)
        }
        .width(64).height(64).borderRadius(32)
        .backgroundColor('#F6E3E2')
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
        .margin({ top: 22 })

        Text('确认移出衣橱?').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
          .margin({ top: 12 })
        Text('移出后将转入「已售出/转让」档案').fontSize(11).fontColor(COLOR_DANGER).margin({ top: 4 })

        Column() {
          Row() {
            Text('衣物').fontSize(11).fontColor(COLOR_TEXT_HINT)
            Column().layoutWeight(1)
            Text(this.removeItem?.name ?? '').fontSize(11).fontWeight(FontWeight.Medium)
              .fontColor(COLOR_TEXT_MAIN)
          }
          .width('100%').margin({ top: 4 })
          Row() {
            Text('年代').fontSize(11).fontColor(COLOR_TEXT_HINT)
            Column().layoutWeight(1)
            Text(getEraLabel(this.removeItem?.era ?? '80s')).fontSize(11).fontColor(COLOR_TEXT_MAIN)
          }
          .width('100%').margin({ top: 6 })
          Row() {
            Text('当前估值').fontSize(11).fontColor(COLOR_TEXT_HINT)
            Column().layoutWeight(1)
            Text(formatPrice(this.removeItem?.currentValue ?? 0)).fontSize(11).fontWeight(FontWeight.Bold)
              .fontColor(COLOR_BRICK)
          }
          .width('100%').margin({ top: 6 })
        }
        .width('100%').backgroundColor(COLOR_BG)
        .borderRadius(10).border({ width: 1, color: COLOR_BORDER })
        .padding({ left: 14, right: 14, top: 10, bottom: 10 })
        .margin({ left: 20, right: 20, top: 16 })

        Row() {
          Text('取消').fontSize(12).fontColor(COLOR_TEXT_SUB)
            .backgroundColor(COLOR_BG).borderRadius(16)
            .border({ width: 1, color: COLOR_BORDER })
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .onClick(() => { this.showRemoveModal = false })
          Text('确认移出').fontSize(12).fontColor(COLOR_CARD)
            .backgroundColor(COLOR_DANGER).borderRadius(16)
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .margin({ left: 10 })
            .onClick(() => { this.showRemoveModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 20, bottom: 22 })
      }
      .width('80%').backgroundColor(COLOR_CARD).borderRadius(14)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '32%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder closetCardBuilder(c: ClosetItem) {
    Column() {
      Column() {
        Text(getEraIcon(c.era)).fontSize(18).opacity(0.9)
      }
      .width('100%').height(58)
      .backgroundColor(c.color)
      .borderRadius({ topLeft: 8, topRight: 8 })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(c.name).fontSize(9).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(getEraLabel(c.era) + ' · ' + c.condition).fontSize(8).fontColor(COLOR_TEXT_HINT).margin({ top: 2 })
        Text(formatPrice(c.currentValue)).fontSize(11).fontWeight(FontWeight.Bold)
          .fontColor(COLOR_PRIMARY).margin({ top: 2 })
        Text('购入' + formatPrice(c.purchasePrice)).fontSize(8).fontColor(COLOR_TEXT_HINT)
          .decoration({ type: TextDecorationType.LineThrough })
        Row() {
          Text('改').fontSize(8).fontColor(COLOR_SECONDARY)
            .backgroundColor('#F0EAF6').borderRadius(8)
            .padding({ left: 9, right: 9, top: 3, bottom: 3 })
            .onClick(() => {
              this.editingItem = c
              this.editName = c.name
              this.editEra = c.era
              this.editCondition = c.condition
              this.editBuyPrice = c.purchasePrice.toString()
              this.editValue = c.currentValue.toString()
              this.showEditModal = true
            })
          Text('移出').fontSize(8).fontColor(COLOR_DANGER)
            .backgroundColor('#F6E3E2').borderRadius(8)
            .padding({ left: 7, right: 7, top: 3, bottom: 3 })
            .margin({ left: 5 })
            .onClick(() => {
              this.removeItem = c
              this.showRemoveModal = true
            })
        }
        .margin({ top: 5 })
      }
      .width('100%').padding(6).alignItems(HorizontalAlign.Center)
    }
    .layoutWeight(1)
    .backgroundColor(COLOR_CARD)
    .borderRadius(8).border({ width: 1, color: COLOR_BORDER })
    .margin({ left: 3, right: 3, top: 6 })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            // ===== 顶部统计条 =====
            Row() {
              Column() {
                Text('12').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                Text('总件数').fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)

              Column().width(1).height(28).backgroundColor(COLOR_BORDER)

              Column() {
                Text(formatPrice(getClosetTotalValue())).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_BRICK)
                Text('总估值').fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)

              Column().width(1).height(28).backgroundColor(COLOR_BORDER)

              Column() {
                Text('80s').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_SECONDARY)
                Text('最爱年代').fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)

              Column().width(1).height(28).backgroundColor(COLOR_BORDER)

              Column() {
                Text('+3').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_GREEN)
                Text('本月新增').fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').backgroundColor(COLOR_CARD)
            .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .padding({ top: 10, bottom: 10 }).margin({ left: 12, right: 12, top: 10 })

            // ===== 估值分布统计(4格)=====
            Row() {
              Column() {
                Text('S级').fontSize(9).fontColor(COLOR_TEXT_HINT)
                Text('4件').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_GREEN)
                Text(formatPrice(6980)).fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 })
              .backgroundColor(COLOR_CARD).borderRadius(10).border({ width: 1, color: COLOR_BORDER })
              .margin({ left: 6, right: 3 })

              Column() {
                Text('A级').fontSize(9).fontColor(COLOR_TEXT_HINT)
                Text('4件').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                Text(formatPrice(3250)).fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 })
              .backgroundColor(COLOR_CARD).borderRadius(10).border({ width: 1, color: COLOR_BORDER })
              .margin({ left: 3, right: 3 })

              Column() {
                Text('B级').fontSize(9).fontColor(COLOR_TEXT_HINT)
                Text('4件').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_MUSTARD)
                Text(formatPrice(2040)).fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 })
              .backgroundColor(COLOR_CARD).borderRadius(10).border({ width: 1, color: COLOR_BORDER })
              .margin({ left: 3, right: 3 })

              Column() {
                Text('C级').fontSize(9).fontColor(COLOR_TEXT_HINT)
                Text('0件').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_DANGER)
                Text(formatPrice(0)).fontSize(8).fontColor(COLOR_TEXT_SUB).margin({ top: 1 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 })
              .backgroundColor(COLOR_CARD).borderRadius(10).border({ width: 1, color: COLOR_BORDER })
              .margin({ left: 3, right: 6 })
            }
            .width('100%').margin({ top: 8 })

            // ===== 3列衣橱网格 =====
            Row() {
              Text('🚪 我的衣橱').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
              Column().layoutWeight(1)
              Text('投入' + formatPrice(getClosetTotalCost())).fontSize(9).fontColor(COLOR_TEXT_HINT)
            }
            .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 2 })

            Row() {
              this.closetCardBuilder(mockClosetItems[0])
              this.closetCardBuilder(mockClosetItems[1])
              this.closetCardBuilder(mockClosetItems[2])
            }
            .width('100%').margin({ left: 6, right: 6 })

            Row() {
              this.closetCardBuilder(mockClosetItems[3])
              this.closetCardBuilder(mockClosetItems[4])
              this.closetCardBuilder(mockClosetItems[5])
            }
            .width('100%').margin({ left: 6, right: 6 })

            Row() {
              this.closetCardBuilder(mockClosetItems[6])
              this.closetCardBuilder(mockClosetItems[7])
              this.closetCardBuilder(mockClosetItems[8])
            }
            .width('100%').margin({ left: 6, right: 6 })

            Row() {
              this.closetCardBuilder(mockClosetItems[9])
              this.closetCardBuilder(mockClosetItems[10])
              this.closetCardBuilder(mockClosetItems[11])
            }
            .width('100%').margin({ left: 6, right: 6 })
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showEditModal) { this.editClothModal() }
      if (this.showRemoveModal) { this.removeConfirmModal() }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab5 故事页 ============
@Component
struct StoryContent {
  @State likedStoryId: number = 0

  @Builder storyCardBuilder(s: StoryItem) {
    Column() {
      // 作者行
      Row() {
        Column() {
          Text(s.userAvatar).fontSize(18)
        }
        .width(36).height(36).borderRadius(18)
        .backgroundColor(COLOR_BG)
        .border({ width: 1, color: COLOR_BORDER })
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Text(s.userName).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
          Text('古着收藏家 · 资深玩家').fontSize(8).fontColor(COLOR_TEXT_HINT).margin({ top: 1 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })

        Text('关注').fontSize(9).fontColor(COLOR_SECONDARY)
          .backgroundColor('#F0EAF6').borderRadius(11)
          .padding({ left: 12, right: 12, top: 4, bottom: 4 })
          .onClick(() => { this.likedStoryId = s.id })
      }
      .width('100%')

      Text(s.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
        .margin({ top: 10 })

      Text(s.content).fontSize(11).fontColor(COLOR_TEXT_SUB)
        .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
        .margin({ top: 6 })

      // 图片色块
      Column() {
        Text('📷').fontSize(24).opacity(0.85)
        Text('Vintage Moment').fontSize(8).fontColor(COLOR_CARD).opacity(0.8).margin({ top: 4 })
      }
      .width('100%').height(110)
      .backgroundColor(s.color)
      .borderRadius(10)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      .margin({ top: 10 })

      Row() {
        Text('❤️ 点赞 ' + s.likeCount.toString()).fontSize(10).fontColor(COLOR_BRICK)
          .onClick(() => { this.likedStoryId = s.id })
        Text('💬 评论 ' + s.commentCount.toString()).fontSize(10).fontColor(COLOR_SECONDARY)
          .margin({ left: 16 })
        Column().layoutWeight(1)
        Text('⭐ 收藏').fontSize(10).fontColor(COLOR_MUSTARD)
          .onClick(() => { this.likedStoryId = s.id })
      }
      .width('100%').margin({ top: 10 })
    }
    .width('100%').backgroundColor(COLOR_CARD)
    .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
    .padding(14).margin({ left: 12, right: 12, top: 10 })
  }

  build() {
    Column() {
      // 顶部分享按钮
      Row() {
        Column() {
          Text('📖 古着故事').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
          Text('听衣物讲述它们的时间旅行').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)

        Column().layoutWeight(1)

        Text('✍️ 分享故事').fontSize(11).fontColor(COLOR_CARD)
          .backgroundColor(COLOR_SECONDARY).borderRadius(15)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .onClick(() => { this.likedStoryId = -1 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 4 })

      Scroll() {
        Column() {
          this.storyCardBuilder(mockStories[0])
          this.storyCardBuilder(mockStories[1])
          this.storyCardBuilder(mockStories[2])
          this.storyCardBuilder(mockStories[3])
          this.storyCardBuilder(mockStories[4])
          this.storyCardBuilder(mockStories[5])
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab6 我的页 ============
@Component
struct ProfileContent {
  @State selectedSetting: string = ''

  @Builder settingRowBuilder(icon: string, label: string, sub: string) {
    Row() {
      Column() {
        Text(icon).fontSize(16)
      }
      .width(34).height(34).borderRadius(17)
      .backgroundColor(COLOR_BG)
      .border({ width: 1, color: COLOR_BORDER })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(label).fontSize(12).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
        Text(sub).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 1 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })

      Text('›').fontSize(16).fontColor(COLOR_TEXT_HINT)
    }
    .width('100%').padding({ top: 10, bottom: 10 })
    .onClick(() => { this.selectedSetting = label })
  }

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

              Column() {
                Row() {
                  Text('古着阿茶').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
                  Text('Lv.6 收藏家').fontSize(9).fontColor(COLOR_CARD)
                    .backgroundColor(COLOR_SECONDARY).borderRadius(9)
                    .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    .margin({ left: 8, top: 2 })
                }
                Text('Vintage是一种生活态度').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 4 })
                Text('🪡 藏品 12 件 · 入坑 1024 天').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
            }
            .width('100%').padding(16)

            Row() {
              Column() {
                Text('12').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                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('1024').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_SECONDARY)
                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('86%').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_GREEN)
                Text('衣橱着用率').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').padding({ top: 4, bottom: 14 })
          }
          .width('100%').backgroundColor(COLOR_CARD)
          .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
          .margin({ left: 12, right: 12, top: 10 })

          // ===== 大数字统计网格 2x2 =====
          Row() {
            Column() {
              Text('🕰️').fontSize(16)
              Text('12').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
                .margin({ top: 3 })
              Text('总藏品(件)').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLOR_CARD).borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .margin({ left: 12, right: 6, top: 10 })

            Column() {
              Text('💎').fontSize(16)
              Text(formatPrice(getClosetTotalValue())).fontSize(24).fontWeight(FontWeight.Bold)
                .fontColor(COLOR_BRICK).margin({ top: 3 })
              Text('衣橱总估值').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLOR_CARD).borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .margin({ left: 6, right: 12, top: 10 })
          }
          .width('100%')

          Row() {
            Column() {
              Text('📦').fontSize(16)
              Text('27').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_SECONDARY)
                .margin({ top: 3 })
              Text('累计售出(件)').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLOR_CARD).borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .margin({ left: 12, right: 6, top: 8 })

            Column() {
              Text('✦').fontSize(16).fontColor(COLOR_MUSTARD)
              Text('8640').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLOR_MUSTARD)
                .margin({ top: 3 })
              Text('古着积分').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLOR_CARD).borderRadius(12).border({ width: 1, color: COLOR_BORDER })
            .margin({ left: 6, right: 12, top: 8 })
          }
          .width('100%')

          // ===== 设置列表 =====
          Column() {
            Text('⚙️ 通用').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
              .width('100%').padding({ left: 16, top: 12, bottom: 4 })

            Column() {
              this.settingRowBuilder('🚪', '衣橱管理', '整理、估值、档案编辑')
              Divider().color(COLOR_BORDER)
              this.settingRowBuilder('📍', '收货地址', '恒温运输签收地址 3 个')
              Divider().color(COLOR_BORDER)
              this.settingRowBuilder('❤️', '我的收藏', '收藏的古着单品 46 件')
              Divider().color(COLOR_BORDER)
              this.settingRowBuilder('🎧', '联系客服', '古着鉴定 · 运输理赔')
              Divider().color(COLOR_BORDER)
              this.settingRowBuilder('⚙️', '设置', '通知、隐私、账号')
            }
            .width('100%').padding({ left: 16, right: 16, bottom: 12 })
          }
          .width('100%').backgroundColor(COLOR_CARD)
          .borderRadius(12).border({ width: 1, color: COLOR_BORDER })
          .margin({ left: 12, right: 12, top: 12 })

          Text('VINTAGE VAULT v1.0 · 让旧时光恒温抵达').fontSize(9).fontColor(COLOR_TEXT_HINT)
            .alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 16 })
        }
        .width('100%')
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}


七、总结

通过对"古着衣橱 VINTAGE VAULT"这份基于 HarmonyOS 6.1.1 与 ArkTS API 24 的完整源码深度解析,我们可以清晰地看到声明式 UI 范式在复杂商业应用中的工程实践价值。该应用以 @Entry 入口组件为根节点,通过 @State 管理全局标签切换与搜索状态,通过 @Builder 抽取可复用的 UI 片段(如 contentAreabottomTabItemmodalOverlay),通过 @Observed 装饰的数据类实现深层对象的响应式追踪,构建了一个涵盖运输、商城、穿搭、衣橱、故事、个人中心六大模块的完整应用框架。每个子组件内部又通过各自的 @State 变量管理局部交互状态(弹窗开关、表单数据、选中项),形成了清晰的两级状态管理体系:全局状态负责跨模块导航,局部状态负责模块内交互。这种分层设计使得每个模块可以独立开发和维护,极大提升了代码的可维护性。

在这里插入图片描述

在数据架构层面,该应用展示了接口契约、配置映射与纯函数三层分离的设计思想。interface 定义了数据模型的类型契约,@Observed 类提供了响应式实现,两者通过 implements 关键字建立约束关系。Record<string, XxxMeta> 类型的配置映射表将业务枚举值与视觉表现绑定,实现了配置与视图的解耦——新增一个年代或调整一个状态的配色只需修改映射表,无需触碰任何 UI 代码。辅助纯函数(formatPricecreateParticlesstepParticles 等)则封装了格式化和动画算法,保证无副作用且可独立测试。这种分层架构在 HarmonyOS ArkTS API 24 的工程实践中是一种高度推荐的模式,它让代码具备了良好的可扩展性和可测试性,为后续接入真实网络数据源打下了坚实基础。

在视觉与交互工程层面,该应用的实现同样值得称道。粒子动画系统通过 setInterval 定时驱动 stepParticles 纯函数计算新位置,赋值给 @State 装饰的 particles 数组触发 UI 重渲染,再配合 Stack 层叠与 hitTestBehavior(HitTestMode.None) 实现了不影响用户交互的装饰性动画层。弹窗模态框系统通过 if 条件渲染加 zIndex 层级控制加 modalOverlay 遮罩层的三重组合,实现了点击外部关闭的标准交互模式,且该遮罩构建器在三个不同弹窗中被复用,体现了 DRY 原则。渐变背景通过 linearGradientanglecolors 参数实现135度对角渐变,删除线通过 TextDecorationType.LineThrough 实现,单行省略通过 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }) 组合实现——这些细节共同构成了复古波普奶油风的完整视觉体系。

综合来看,这份源码不仅是一个古着服饰电商应用的功能实现,更是一份 HarmonyOS ArkTS API 24 的工程实践教学范例。它完整展示了装饰器体系(@Entry@Component@State@Observed@Builder)的协同使用、生命周期管理(aboutToAppear/aboutToDisappear)与资源清理、条件渲染与模态弹窗、可选链与空值合并的安全编程、配置驱动的视图解耦、纯函数封装与粒子动画算法等核心技术点。对于希望深入掌握 HarmonyOS 声明式开发范式的工程师而言,这份代码涵盖了从设计系统到状态管理、从数据建模到交互工程的完整知识链路,是一份值得反复研读和借鉴的参考实现。随着 HarmonyOS 生态的不断演进,这种基于 ArkTS 的组件化、状态驱动、配置分离的工程范式将成为全场景应用开发的主流方向。

Logo

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

更多推荐