基于HarmonyOS API 24的ArkTS盆景艺术商城应用深度解析——HarmonyOS 6.1.1声明式UI新中式禅意设计实战

在鸿蒙生态不断演进的今天,基于HarmonyOS API 24的声明式UI开发范式正在重塑移动应用的开发体验。本文将深入剖析一款融合东方禅意美学的盆景艺术商城应用,从架构设计到界面实现,全方位展示ArkTS在HarmonyOS 6.1.1平台上的强大表现力。

新中式禅意设计与现代移动应用技术的碰撞,催生了这款以"一木一石,禅意盆栽"为核心理念的盆景艺术平台。通过苔藓绿、陶土棕、水墨灰的精致配色,配合流畅的动画效果与丰富的数据可视化,为用户打造沉浸式的东方美学体验。

本文将从接口定义、主题系统、数据模型、纯函数工具库、主组件架构、六大Tab页面实现等多个维度,逐层剖析这款应用的技术实现细节,为HarmonyOS开发者提供可借鉴的设计模式与最佳实践。

引言

在这里插入图片描述

在HarmonyOS 6.1.1时代,ArkTS声明式UI框架已经趋于成熟,为开发者提供了高效、优雅的应用构建方式。这款盆景艺术商城应用正是基于HarmonyOS ArkTS API 24构建的典型案例,它不仅展示了声明式UI的强大功能,更通过精心的视觉设计与交互体验,将传统东方美学与现代移动技术完美融合。

应用整体采用"BN"前缀的命名规范,以苔藓绿(#5B7C5A)为主色调,陶土棕(#B5651D)为强调色,搭配水墨灰与米白背景,营造出新中式禅意的视觉氛围。应用架构采用单文件组件化模式,在一个ArkTS文件中完成了从数据模型到界面交互的全部实现,这种模式非常适合中小型应用的快速开发与维护。

从业务设计来看,这是一款集盆景鉴赏、在线选购、艺术交流于一体的垂直领域电商应用。六大核心Tab页签——首页、松柏、杂木、山水、盆器、我的——构成了完整的用户体验闭环。每个Tab都有独立的数据展示、筛选、详情查看及CRUD操作能力,同时通过统一的主题系统与动画效果保持了整体的一致性。

技术架构上,应用采用了经典的分层设计:最底层是TypeScript接口定义与常量数据,中间层是纯函数工具库(负责过滤、排序、计算等业务逻辑),上层是ArkTS组件(负责UI渲染与交互)。这种清晰的分层使得代码具有良好的可读性与可维护性,同时也充分利用了ArkTS声明式UI的响应式更新机制。

接口定义与主题系统

在这里插入图片描述

接口定义架构

应用的开篇定义了一系列TypeScript接口,为整个应用的数据模型建立了严格的类型约束。这种接口先行的设计模式是ArkTS开发的最佳实践之一。

interface BnTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  dark: string
}

interface BnTab {
  key: string
  label: string
  icon: Resource
}

interface BnPine {
  name: string
  type: string
  age: number
  height: number
  trunk: number
  price: number
  style: string
  stock: number
  sold: number
  tag: string
}

这段代码展示了三种核心接口类型:主题接口BnTheme定义了9个颜色字段,涵盖了背景、主色、强调色、卡片、文字、次要文字、分割线、辉光和深色等完整的设计令牌体系;Tab接口BnTab用于底部导航栏的配置;而BnPine则是松柏盆景的数据模型,包含名称、树种、树龄、高度、干粗、价格、造型、库存、销量和标签等10个属性。

使用接口定义数据模型是TypeScript/ArkTS的核心优势之一。它不仅提供了编译时的类型检查,还能作为文档帮助开发者理解数据结构,大幅提升团队协作效率。

除了松柏盆景接口外,应用还定义了杂木盆景(BnBroadleaf)、山水盆景(BnLandscape)、盆器(BnPot)、订单(BnOrder)、收藏(BnCollect)和月度消费(BnMonth)等多个数据接口,每个接口都针对特定业务实体进行了精细的属性设计。例如山水盆景包含风格、尺寸、材质、石材数量等特有属性,盆器则包含材质、形状、尺寸、颜色等属性,充分体现了领域驱动设计的思想。

主题常量系统

在这里插入图片描述

const BN: BnTheme = {
  bg: '#F5F2E8',
  primary: '#5B7C5A',
  accent: '#B5651D',
  card: '#FFFFFF',
  text: '#3A3028',
  sub: '#8B7E70',
  line: '#E0D8CC',
  glow: '#8FAB7B',
  dark: '#2C2418'
}

const BN_TABS: BnTab[] = [
  { key: 'home', label: '首页', icon: $r('app.media.layered_image') },
  { key: 'pine', label: '松柏', icon: $r('app.media.layered_image') },
  { key: 'broad', label: '杂木', icon: $r('app.media.layered_image') },
  { key: 'landscape', label: '山水', icon: $r('app.media.layered_image') },
  { key: 'pot', label: '盆器', icon: $r('app.media.layered_image') },
  { key: 'mine', label: '我的', icon: $r('app.media.layered_image') }
]

主题常量BN是整个应用的视觉基石,它将设计令牌集中管理,使得全局样式修改变得极为便捷。这种设计令牌(Design Token)的理念在现代前端开发中被广泛采用,而在ArkTS中通过接口+常量的方式实现得尤为优雅。

Tab配置数组BN_TABS采用数据驱动的方式定义底部导航栏,每个Tab包含key(唯一标识)、label(显示文字)和icon(图标资源)。这种配置化的方式使得增减Tab页签变得非常简单,只需修改数组即可,无需改动组件渲染逻辑。

数据驱动UI是声明式编程的核心思想之一。将导航配置抽离为数据数组,不仅让代码更加清晰,也为后续的动态配置、A/B测试等高级功能奠定了基础。

数据模型与纯函数工具库

在这里插入图片描述

数据模型设计

应用采用常量数组的方式预置了丰富的模拟数据,涵盖松柏盆景10条、杂木盆景8条、山水盆景8条、盆器6条、订单6条、收藏6条和月度消费6条。

const BN_PINES: BnPine[] = [
  { name: '黑松·迎客', type: '黑松', age: 35, height: 45, trunk: 8, price: 12800, style: '斜干', stock: 3, sold: 86, tag: '名木' },
  { name: '五针松·凌云', type: '五针松', age: 28, height: 38, trunk: 6, price: 8600, style: '直干', stock: 5, sold: 124, tag: '经典' },
  { name: '罗汉松·盘龙', type: '罗汉松', age: 42, height: 52, trunk: 10, price: 22000, style: '曲干', stock: 2, sold: 56, tag: '名木' },
  // ... 更多数据
]

每条数据都经过精心设计,名称富有诗意(如"黑松·迎客"、“五针松·凌云”),属性值分布合理,标签体系完整(名木、经典、高端、入门、珍藏等)。这种高质量的模拟数据不仅让界面展示更加真实,也为各种数据可视化图表提供了良好的数据基础。

动画函数库

在这里插入图片描述

function bnInk(wave: number, i: number): number {
  let phase: number = (wave + i * 30) % 100
  return 0.5 + phase / 100 * 0.5
}

function bnSway(wave: number, i: number): number {
  return Math.sin(wave / 50 + i) * 6
}

function bnFloatY(wave: number, i: number): number {
  return Math.sin(wave / 35 + i) * 5
}

function bnBlink(wave: number, i: number): number {
  let v: number = (wave + i * 60) % 200
  return v < 100 ? 1.0 : 0.5
}

这四个动画函数是应用视觉效果的核心引擎。bnInk函数模拟水墨晕染的扩散效果,通过相位偏移计算返回0.5到1.0之间的缩放值;bnSway函数利用正弦函数实现左右摇摆效果,偏移量为±6像素;bnFloatY函数实现上下浮动效果,振幅为5像素;bnBlink函数则产生闪烁效果,在完全不透明和半透明之间切换。

这些动画函数都接收wavei两个参数:wave是由定时器递增的全局波形变量,i是元素索引。通过为不同索引的元素设置不同的相位偏移,实现了错落有致的动画效果,避免了机械感和单调感。

业务工具函数

在这里插入图片描述

function bnFilterPines(tag: string): BnPine[] {
  let r: BnPine[] = []
  for (let i = 0; i < BN_PINES.length; i++) {
    if (tag === '全部' || BN_PINES[i].tag === tag) {
      r.push(BN_PINES[i])
    }
  }
  return r
}

function bnSoldRank(): BnPine[] {
  let r: BnPine[] = BN_PINES.slice()
  r.sort((a: BnPine, b: BnPine) => b.sold - a.sold)
  return r.slice(0, 5)
}

function bnAgeW(age: number): number {
  return Math.min(age / 50 * 100, 100)
}

function bnStatusColor(status: string): string {
  if (status === '已签收') {
    return BN.primary
  }
  if (status === '养护中') {
    return BN.accent
  }
  if (status === '已退换') {
    return '#999999'
  }
  return BN.sub
}

业务工具函数层承担了数据处理的核心职责。bnFilterPines实现按标签筛选功能,支持"全部"和具体标签两种模式;bnSoldRank通过销量排序返回前5名,用于热销榜单展示;bnAgeW将树龄转换为百分比宽度,用于进度条可视化;bnStatusColor则根据订单状态返回对应的颜色值,实现了状态与视觉的映射。

值得注意的是,这些函数全部采用纯函数设计——相同的输入总是产生相同的输出,没有副作用。这种设计不仅使得函数易于测试和复用,也完美契合了ArkTS声明式UI的响应式更新机制。

主组件架构

组件结构概览

应用的主组件BnApp采用@Entry@Component装饰器标记,是整个应用的入口组件。它采用Stack布局包裹Column布局,实现了基础页面与弹框层的叠加效果。

@Entry
@Component
struct BnApp {
  @State tab: string = 'home'
  @State showHomeExhibit: boolean = false
  @State showHomeCourse: boolean = false
  @State showHomeAuction: boolean = false
  @State showHomeCommunity: boolean = false

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showHomeExhibit = false
      this.showHomeCourse = false
      this.showHomeAuction = false
      this.showHomeCommunity = false
    })
  }

主组件维护5个状态变量:tab控制当前显示的Tab页,4个showHomeXxx变量控制首页各功能弹框的显示与隐藏。@Builder装饰的modalOverlay方法是一个可复用的构建器函数,用于生成弹框的半透明遮罩层,点击遮罩可关闭所有弹框。

@Builder是ArkTS提供的一项重要特性,它允许将UI片段抽离为可复用的构建函数,类似于React中的组件组合,但更加轻量。通过@Builder,开发者可以有效减少代码重复,提升UI代码的可维护性。

页面主体结构

build() {
  Stack() {
    Column() {
      // 头部品牌栏
      Row() {
        Column() {
          Text('盆景').fontSize(22).fontWeight(900).fontColor(BN.dark)
          Text('BONSAI CRAFT').fontSize(10).fontColor(BN.sub).letterSpacing(2)
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Row() {
          Text('一木一石').fontSize(12).fontColor(BN.sub)
          Column().width(2).height(12).backgroundColor(BN.line).margin({ left: 8, right: 8 })
          Text('禅意盆栽').fontSize(10).fontColor(BN.accent).fontWeight(700)
        }
      }
      .width('100%').height(56).padding({ left: 16, right: 16 }).backgroundColor(BN.card)

      // 搜索条
      Row() {
        Row() {
          Text('🔍').fontSize(14).fontColor(BN.sub).margin({ left: 12, right: 8 })
          Text('搜索松柏 / 杂木 / 山水...').fontSize(13).fontColor(BN.sub)
          Column().layoutWeight(1)
          Text().width(28).height(28).borderRadius(14).backgroundColor(BN.primary).onClick(() => {})
        }
        .width('100%').height(40).backgroundColor(BN.bg).borderRadius(20).padding({ left: 4, right: 4 })
      }
      .width('100%').padding({ left: 12, right: 12, top: 8, bottom: 8 })

页面主体采用经典的三段式布局:顶部品牌栏+搜索条、中间内容区、底部Tab栏。品牌栏左侧是应用名称和英文Logo,右侧是品牌Slogan"一木一石 · 禅意盆栽",通过竖向分割线增强视觉层次。搜索条采用胶囊形状设计,内嵌搜索图标和占位文字,右侧有一个圆形按钮。

Tab切换机制

      // tab 内容区
      Column() {
        if (this.tab === 'home') {
          BnHomeTab({
            onExhibit: () => { this.showHomeExhibit = true },
            onCourse: () => { this.showHomeCourse = true },
            onAuction: () => { this.showHomeAuction = true },
            onCommunity: () => { this.showHomeCommunity = true }
          })
        } else if (this.tab === 'pine') {
          BnPineTab()
        } else if (this.tab === 'broad') {
          BnBroadleafTab()
        } else if (this.tab === 'landscape') {
          BnLandscapeTab()
        } else if (this.tab === 'pot') {
          BnPotTab()
        } else {
          BnMineTab()
        }
      }
      .layoutWeight(1)

      // 底部 tab 栏
      Row() {
        ForEach(BN_TABS, (t: BnTab) => {
          Column() {
            Text(t.icon).width(20).height(20).fontColor(this.tab === t.key ? BN.primary : BN.sub)
            Text(t.label).fontSize(10).fontColor(this.tab === t.key ? BN.primary : BN.sub).fontWeight(this.tab === t.key ? 700 : 400)
          }
          .layoutWeight(1)
          .padding({ top: 6, bottom: 6 })
          .onClick(() => {
            this.tab = t.key
          })
        }, (t: BnTab) => t.key)
      }
      .width('100%').height(52).backgroundColor(BN.card)
      .border({ width: { top: 1 }, color: BN.line })

Tab切换机制是整个应用导航的核心。内容区使用if-else条件渲染,根据this.tab的值显示对应的子组件。首页组件接收四个回调函数参数,用于将子组件内部的点击事件冒泡到父组件,从而控制弹框的显示。

底部Tab栏使用ForEach循环渲染,这是ArkTS中列表渲染的标准方式。ForEach接收三个参数:数据源数组、子组件构建函数、键生成函数。选中状态通过this.tab === t.key的条件判断动态改变文字颜色和字重,实现了清晰的视觉反馈。

ForEach的第三个参数(键生成函数)非常重要,它用于标识每个列表项的唯一性。正确设置键值可以大幅提升列表渲染性能,避免不必要的组件重建。

首页Tab实现深度解析

组件状态与生命周期

@Component
struct BnHomeTab {
  @State wave: number = 0
  @State pineList: BnPine[] = []
  @State rankList: BnPine[] = []
  private timer: number = -1
  @State showDetail: boolean = false
  @State selName: string = ''
  onExhibit: () => void = () => {}
  onCourse: () => void = () => {}
  onAuction: () => void = () => {}
  onCommunity: () => void = () => {}

  aboutToAppear() {
    this.pineList = BN_PINES.slice(0, 4)
    this.rankList = bnSoldRank()
    this.timer = setInterval(() => {
      this.wave += 1
    }, 60)
  }

  aboutToDisappear() {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

首页组件BnHomeTab展示了ArkTS组件的完整生命周期管理。aboutToAppear在组件即将显示时调用,用于初始化数据和启动动画定时器;aboutToDisappear在组件即将销毁时调用,用于清除定时器,防止内存泄漏。

组件的状态变量设计体现了关注点分离的原则:wave用于驱动所有动画效果,pineList存储首页展示的盆景列表,rankList存储热销榜单数据,showDetailselName控制详情弹框。四个回调函数onExhibitonCourseonAuctiononCommunity则作为与父组件通信的桥梁。

正确管理定时器等副作用是前端开发中的重要课题。在ArkTS中,务必在aboutToDisappear中清理所有定时器、事件监听器等资源,否则可能导致内存泄漏和性能问题。

水墨晕染Banner

          // 水墨晕染 banner
          Stack() {
            Column().width('100%').height(120)
              .linearGradient({ angle: 135, colors: [[BN.primary, 0], [BN.dark, 1]] })
            Column() {
              Text('一木一石').fontSize(28).fontWeight(900).fontColor('#F5F2E8').letterSpacing(3)
              Text('禅意盆景 · 东方美学').fontSize(12).fontColor('rgba(245,242,232,0.8)').margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Center).margin({ top: 20 })
            // 晕染效果圆
            Row() {
              ForEach([0, 1, 2, 3], (i: number) => {
                Column().width(50).height(50).borderRadius(25).backgroundColor('rgba(245,242,232,0.08)')
                  .scale({ x: bnInk(this.wave, i), y: bnInk(this.wave, i) })
                  .margin({ left: 12, right: 12 })
              }, (i: number) => i.toString())
            }
            .margin({ top: 60 })
          }
          .width('100%').height(120)

首页Banner是应用视觉设计的亮点之一。它使用Stack叠加布局实现多层视觉效果:底层是135度角的线性渐变(从苔藓绿到水墨灰),中层是标题文字,顶层是四个动态缩放的半透明圆形,模拟水墨晕染扩散的效果。

bnInk(this.wave, i)函数为每个圆形计算不同的缩放比例,结合全局wave变量的持续递增,形成了此起彼伏的晕染动画。这种纯CSS/属性动画在ArkTS中性能优异,完全由渲染框架驱动,不会阻塞主线程。

仪表盘与统计卡

          // 盆景仪表盘
          Row() {
            Stack() {
              Column().width(100).height(100).borderRadius(50).border({ width: 4, color: BN.line })
              Column().width(80).height(80).borderRadius(40).backgroundColor(BN.card)
              Column() {
                Text('树龄').fontSize(10).fontColor(BN.sub)
                Text('45').fontSize(24).fontWeight(900).fontColor(BN.primary).margin({ top: 2 })
                Text('年').fontSize(10).fontColor(BN.sub)
              }
              .alignItems(HorizontalAlign.Center)
            }
            .width(110).height(110)
            Column() {
              Text('镇店之宝').fontSize(11).fontColor(BN.sub)
              Text('真柏·翠云').fontSize(20).fontWeight(800).fontColor(BN.dark).margin({ top: 4 })
              Text('曲干式 | 树高42cm').fontSize(11).fontColor(BN.sub).margin({ top: 4 })
              Text('树龄45年 | ¥28,000').fontSize(11).fontColor(BN.accent).fontWeight(600).margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1).padding({ left: 12 })
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

镇店之宝仪表盘采用同心圆设计,外层圆环使用边框实现,内层圆使用背景色填充,中心显示核心数据"45年"。右侧是产品的详细信息,包括名称、造型、尺寸和价格,信息层级分明,重点突出。

紧随其后的是四格统计卡,展示松柏、杂木、山水、盆器四大品类的数量统计。使用竖向分割线分隔四个数据项,每项采用上下结构(数字+标签),主色和强调色交替使用增强节奏感。

应用整体流程图

home

pine

broad

landscape

pot

mine

点击切换

应用入口 BnApp

初始化主题与Tab配置

渲染头部品牌栏

渲染搜索条

当前Tab判断

首页 Tab

松柏 Tab

杂木 Tab

山水 Tab

盆器 Tab

我的 Tab

水墨晕染Banner

镇店之宝仪表盘

四格统计卡

功能宫格

热销榜单

名家横滑

筛选Chips

双列网格展示

树龄对比图

高度柱状图

会员卡

收藏/订单统计

月度消费柱状图

订单列表

收藏列表

设置列表

首页弹框层

展览/课堂/拍卖/社区弹框

详情/新增/编辑/删除弹框

昵称/地址/关于弹框

底部Tab栏

松柏Tab与CRUD弹框

筛选与双列网格

          // 筛选 chips
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '名木', '经典', '高端', '入门', '珍藏'], (t: string) => {
                Text(t).fontSize(12).fontColor(this.filterTag === t ? BN.card : BN.text)
                  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.filterTag === t ? BN.primary : BN.bg)
                  .onClick(() => {
                    this.filterTag = t
                    this.list = bnFilterPines(t)
                  })
              }, (t: string) => t)
            }
            .padding({ left: 12, right: 12 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
          .height(36)

筛选Chips组件是分类页面的标配功能。它使用横向滚动的Scroll容器包裹一排标签按钮,选中状态通过背景色和文字颜色的变化来标识。点击标签时更新filterTag状态并调用bnFilterPines函数重新过滤数据列表,从而驱动界面自动更新。

声明式UI的核心魅力在于"数据驱动视图"。开发者只需关心状态的变化,框架会自动计算差异并更新DOM/UI。这种模式相较于命令式操作(如直接操作DOM)大幅降低了心智负担,减少了Bug产生的可能性。

数据可视化图表

          // 树龄对比图
          Column() {
            Text('树龄对比 (年)').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            ForEach(this.list.slice(0, 5), (p: BnPine, i: number) => {
              Row() {
                Text(p.name.substring(0, 5)).fontSize(10).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(10).borderRadius(5).backgroundColor(BN.bg)
                  Column().width(bnAgeW(p.age).toString() + '%').height(10).borderRadius(5).backgroundColor(BN.primary)
                }
                .layoutWeight(1)
                Text(p.age.toString()).fontSize(10).fontColor(BN.text).fontWeight(600).width(28)
              }
              .width('100%').margin({ bottom: 6 })
            }, (p: BnPine) => p.name)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

树龄对比图是一个横向条形图,使用Stack叠加布局实现进度条效果——底层是灰色背景条,上层是根据数据计算宽度的彩色进度条。bnAgeW函数将树龄值转换为百分比宽度,最大树龄50年对应100%宽度。

高度柱状图则是另一种可视化形式,使用竖向柱形展示各盆景的高度分布。通过ForEach循环生成多个柱形,每个柱形的高度由bnHeightW函数计算得出,底部标注具体数值。

在ArkTS中实现数据可视化无需依赖第三方图表库,通过基础的Column、Row、Stack等布局组件配合动态属性计算,完全可以构建出精美的图表效果。这种方式虽然灵活性不如专业图表库,但对于大多数业务场景已经足够,且性能更优、包体更小。

CRUD弹框系统

      // 松柏详情弹框
      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('松柏详情').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text(bnPineByName(this.selName).name).fontSize(16).fontWeight(600).fontColor(BN.text).margin({ top: 8 })
            // ... 详情内容
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showDetail = false })
              Column().layoutWeight(1)
              Text('编辑').fontSize(12).fontColor(BN.card).padding({ left: 14, right: 14, top: 8, bottom: 8 }).borderRadius(16).backgroundColor(BN.accent)
                .onClick(() => { this.showDetail = false; this.showEdit = true })
              Text('删除').fontSize(12).fontColor('#FFFFFF').padding({ left: 14, right: 14, top: 8, bottom: 8 }).borderRadius(16).backgroundColor('#CC3333')
                .margin({ left: 8 }).onClick(() => { this.showDetail = false; this.showDelete = true })
            }
            .width('100%').margin({ top: 16 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

详情弹框是CRUD操作的入口,展示盆景的完整信息,包括树龄、树高、干粗三个进度条可视化,以及价格、库存、已售三个数据项的横向排列。底部操作区包含关闭按钮、编辑按钮和删除按钮,点击编辑或删除会关闭当前弹框并打开对应弹框。

新增弹框和编辑弹框采用表单式设计,包含名称输入、树种选择、造型选择等字段。删除弹框则采用确认式设计,用红色突出警告信息,提供取消和确认两个操作选项。

弹框间的状态流转是一个值得注意的设计细节:从详情弹框点击编辑,会先关闭详情弹框再打开编辑弹框(this.showDetail = false; this.showEdit = true)。这种串行打开的方式避免了弹框叠加造成的视觉混乱,也简化了状态管理。

"我的"Tab与会员体系

会员卡与消费统计

          // 会员卡
          Stack() {
            Column().width('100%').height(120).borderRadius(16)
              .linearGradient({ angle: 135, colors: [[BN.dark, 0], [BN.primary, 1]] })
            Column() {
              Text('BONSAI VIP').fontSize(22).fontWeight(900).fontColor('#F5F2E8').letterSpacing(2)
              Text('禅意盆景会员').fontSize(12).fontColor('rgba(245,242,232,0.7)').margin({ top: 4 })
              Row() {
                Text('积分').fontSize(11).fontColor('rgba(245,242,232,0.7)')
                Text('12,800').fontSize(20).fontWeight(800).fontColor('#F5F2E8').margin({ left: 8 })
              }
              .margin({ top: 12 })
            }
            .alignItems(HorizontalAlign.Center).margin({ top: 24 })
            Column().width(60).height(60).borderRadius(30).backgroundColor('rgba(245,242,232,0.15)')
              .scale({ x: bnInk(this.wave, 0), y: bnInk(this.wave, 0) })
              .position({ x: 280, y: 16 })
          }
          .width('100%').height(120).padding({ left: 16, right: 16, top: 16 })

会员卡模块采用渐变背景+光晕动画的设计,模拟高端会员卡的视觉质感。右上角的动态光晕使用bnInk函数驱动缩放动画,为静态的卡片增添了生命力。积分数字使用大号字体,强化用户的成就感和归属感。

月度消费柱状图

          // 月度消费柱状图
          Column() {
            Text('月度消费').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            Row({ space: 8 }) {
              ForEach(this.months, (m: BnMonth, i: number) => {
                Column() {
                  Column().width(28).height(bnMonthH(m.amount)).borderRadius(4)
                    .linearGradient({ angle: 0, colors: [[BN.accent, 0], [BN.primary, 1]] })
                  Text(m.month).fontSize(9).fontColor(BN.sub).margin({ top: 4 })
                  Text((m.amount / 1000).toFixed(1) + 'k').fontSize(8).fontColor(BN.text).fontWeight(600)
                }
                .alignItems(HorizontalAlign.Center)
              }, (m: BnMonth) => m.month)
            }
            .width('100%').justifyContent(FlexAlign.SpaceAround)
          }
          .width('94%').padding(16).backgroundColor(BN.card).borderRadius(12).margin({ top: 8 })

月度消费柱状图是"我的"页面的核心数据可视化组件。每个柱形使用竖向渐变(从陶土棕到苔藓绿),底部标注月份和消费金额。bnMonthH函数根据消费金额计算柱形高度,最大金额对应120px高度,最小不低于8px,确保即使是小额消费也能看到柱形。

订单与收藏列表

          // 订单列表
          Column() {
            Text('我的订单').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 8 })
            ForEach(this.orders, (o: BnOrder, i: number) => {
              Row() {
                Column() {
                  Text(o.item).fontSize(13).fontWeight(600).fontColor(BN.text)
                  Text(o.id + ' · ' + o.date).fontSize(10).fontColor(BN.sub).margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Column() {
                  Text('¥' + o.amount.toString()).fontSize(13).fontWeight(700).fontColor(BN.text)
                  Text(o.status).fontSize(10).fontColor(bnStatusColor(o.status)).margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%').padding({ top: 8, bottom: 8 })
              .border({ width: { bottom: 1 }, color: BN.line })
            }, (o: BnOrder) => o.id)
          }

订单列表采用经典的左右布局:左侧显示商品名称和订单信息,右侧显示金额和状态。状态文字的颜色通过bnStatusColor函数动态计算,已签收显示主色、养护中显示强调色、已退换显示灰色,使用户一眼就能识别订单状态。

技术点对比表格

技术维度 盆景应用 (BN) 皮具应用 (LT) 纹身应用 (IW) 玻璃应用 (VG)
主题风格 新中式禅意,浅色暖调 复古手作风,浅色暖调 潮流纹身风,深色系 彩色玻璃风,浅色系
主色调 苔藓绿 #5B7C5A 马鞍棕 #8B4513 荧光青 #00FFFF 靛蓝 #4338CA
强调色 陶土棕 #B5651D 焦糖橙 #D2691E 电粉 #FF1493 琥珀金 #D97706
Tab数量 6个 6个 6个 6个
动画函数数 4个(晕染/摇摆/浮动/闪烁) 6个(光泽/缝线/复古/旋转/浮动/闪烁) 6个(墨水/辉光/旋转/滴落/浮动/闪烁) 4个(光线/闪烁/浮动/眨眼)
数据模型数 8个接口 8个接口 8个接口 7个接口
CRUD弹框 详情+新增+编辑+删除 详情+新增+编辑+删除 详情+新增+编辑+删除 详情+新增+编辑+删除
首页功能 展览/课堂/拍卖/社区 定制/课堂/匠人/社区 定制/课堂/店铺/社区 (多样化功能入口)
图表类型 横向进度条+柱状图 横向进度条+柱状图 横向进度条+柱状图 多种可视化形式
特色亮点 水墨晕染效果 皮革光泽与缝线 荧光与深色主题 彩色玻璃光效

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// BN 前缀 | 苔藓绿×陶土棕×水墨灰 | 新中式禅意 | 浅色暖调 | 6 tab

// ============ 接口定义 ============
interface BnTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  dark: string
}

interface BnTab {
  key: string
  label: string
  icon: Resource
}

interface BnPine {
  name: string
  type: string
  age: number
  height: number
  trunk: number
  price: number
  style: string
  stock: number
  sold: number
  tag: string
}

interface BnBroadleaf {
  name: string
  type: string
  age: number
  height: number
  leaf: string
  price: number
  stock: number
  sold: number
  tag: string
}

interface BnLandscape {
  name: string
  style: string
  size: number
  material: string
  stones: number
  price: number
  stock: number
  sold: number
  tag: string
}

interface BnPot {
  name: string
  material: string
  shape: string
  size: number
  color: string
  price: number
  stock: number
  sold: number
}

interface BnOrder {
  id: string
  item: string
  date: string
  status: string
  amount: number
}

interface BnCollect {
  name: string
  type: string
  price: number
  tag: string
}

interface BnMonth {
  month: string
  amount: number
}

// ============ 主题常量 ============
const BN: BnTheme = {
  bg: '#F5F2E8',
  primary: '#5B7C5A',
  accent: '#B5651D',
  card: '#FFFFFF',
  text: '#3A3028',
  sub: '#8B7E70',
  line: '#E0D8CC',
  glow: '#8FAB7B',
  dark: '#2C2418'
}

const BN_TABS: BnTab[] = [
  { key: 'home', label: '首页', icon: $r('app.media.layered_image') },
  { key: 'pine', label: '松柏', icon: $r('app.media.layered_image') },
  { key: 'broad', label: '杂木', icon: $r('app.media.layered_image') },
  { key: 'landscape', label: '山水', icon: $r('app.media.layered_image') },
  { key: 'pot', label: '盆器', icon: $r('app.media.layered_image') },
  { key: 'mine', label: '我的', icon: $r('app.media.layered_image') }
]

// ============ 数据:松柏盆景 10 条 ============
const BN_PINES: BnPine[] = [
  { name: '黑松·迎客', type: '黑松', age: 35, height: 45, trunk: 8, price: 12800, style: '斜干', stock: 3, sold: 86, tag: '名木' },
  { name: '五针松·凌云', type: '五针松', age: 28, height: 38, trunk: 6, price: 8600, style: '直干', stock: 5, sold: 124, tag: '经典' },
  { name: '罗汉松·盘龙', type: '罗汉松', age: 42, height: 52, trunk: 10, price: 22000, style: '曲干', stock: 2, sold: 56, tag: '名木' },
  { name: '赤松·清风', type: '赤松', age: 22, height: 32, trunk: 5, price: 5800, style: '悬崖', stock: 7, sold: 168, tag: '入门' },
  { name: '锦松·鹤舞', type: '锦松', age: 38, height: 48, trunk: 9, price: 16800, style: '文人', stock: 4, sold: 72, tag: '高端' },
  { name: '真柏·翠云', type: '真柏', age: 45, height: 42, trunk: 11, price: 28000, style: '曲干', stock: 1, sold: 38, tag: '名木' },
  { name: '黑松·听涛', type: '黑松', age: 30, height: 40, trunk: 7, price: 9800, style: '斜干', stock: 6, sold: 112, tag: '经典' },
  { name: '五针松·玉翠', type: '五针松', age: 25, height: 35, trunk: 5, price: 7200, style: '直干', stock: 8, sold: 145, tag: '入门' },
  { name: '罗汉松·福寿', type: '罗汉松', age: 50, height: 58, trunk: 12, price: 35000, style: '曲干', stock: 2, sold: 42, tag: '珍藏' },
  { name: '杜松·云游', type: '杜松', age: 33, height: 44, trunk: 8, price: 11500, style: '悬崖', stock: 4, sold: 78, tag: '经典' }
]

// ============ 数据:杂木盆景 8 条 ============
const BN_BROADLEAF: BnBroadleaf[] = [
  { name: '榉树·秋韵', type: '榉树', age: 20, height: 40, leaf: '落叶', price: 4800, stock: 6, sold: 132, tag: '四季' },
  { name: '枫树·丹霞', type: '枫树', age: 25, height: 45, leaf: '落叶', price: 6800, stock: 4, sold: 98, tag: '赏叶' },
  { name: '榆树·苍古', type: '榆树', age: 30, height: 42, leaf: '落叶', price: 8200, stock: 5, sold: 76, tag: '古朴' },
  { name: '雀梅·叠翠', type: '雀梅', age: 18, height: 32, leaf: '常绿', price: 3600, stock: 8, sold: 156, tag: '入门' },
  { name: '黄杨·玉润', type: '黄杨', age: 35, height: 28, leaf: '常绿', price: 12000, stock: 3, sold: 62, tag: '名木' },
  { name: '六月雪·繁星', type: '六月雪', age: 15, height: 25, leaf: '常绿', price: 2800, stock: 10, sold: 198, tag: '赏花' },
  { name: '石榴·硕果', type: '石榴', age: 22, height: 38, leaf: '落叶', price: 5500, stock: 5, sold: 108, tag: '赏果' },
  { name: '三角枫·迎秋', type: '三角枫', age: 28, height: 44, leaf: '落叶', price: 7200, stock: 4, sold: 84, tag: '赏叶' }
]

// ============ 数据:山水盆景 8 条 ============
const BN_LANDSCAPE: BnLandscape[] = [
  { name: '桂林山水', style: '远山', size: 60, material: '英石', stones: 7, price: 5800, stock: 5, sold: 89, tag: '经典' },
  { name: '太湖晨曦', style: '近山', size: 45, material: '太湖石', stones: 5, price: 4200, stock: 7, sold: 112, tag: '入门' },
  { name: '黄山云海', style: '远山', size: 80, material: '斧劈石', stones: 9, price: 8800, stock: 3, sold: 56, tag: '高端' },
  { name: '长江三峡', style: '峡谷', size: 70, material: '千层石', stones: 6, price: 6800, stock: 4, sold: 72, tag: '经典' },
  { name: '桂林烟雨', style: '远山', size: 55, material: '英石', stones: 8, price: 5200, stock: 6, sold: 98, tag: '四季' },
  { name: '武陵春色', style: '近山', size: 50, material: '钟乳石', stones: 5, price: 4800, stock: 5, sold: 86, tag: '入门' },
  { name: '峨眉金顶', style: '高远', size: 90, material: '斧劈石', stones: 12, price: 12000, stock: 2, sold: 38, tag: '珍藏' },
  { name: '西湖晚照', style: '平远', size: 65, material: '太湖石', stones: 6, price: 6200, stock: 4, sold: 82, tag: '经典' }
]

// ============ 数据:盆器 6 条 ============
const BN_POTS: BnPot[] = [
  { name: '紫砂椭圆盆', material: '紫砂', shape: '椭圆', size: 35, color: '紫红', price: 1800, stock: 8, sold: 156 },
  { name: '青瓷长方盆', material: '青瓷', shape: '长方', size: 40, color: '天青', price: 1200, stock: 10, sold: 198 },
  { name: '粗陶圆盆', material: '粗陶', shape: '圆形', size: 30, color: '土黄', price: 680, stock: 15, sold: 234 },
  { name: '紫砂方盆', material: '紫砂', shape: '正方', size: 32, color: '紫褐', price: 2200, stock: 6, sold: 128 },
  { name: '石湾陶浅盆', material: '石湾陶', shape: '浅口', size: 45, color: '墨绿', price: 980, stock: 12, sold: 178 },
  { name: '白瓷六角盆', material: '白瓷', shape: '六角', size: 28, color: '月白', price: 1500, stock: 9, sold: 145 }
]

// ============ 数据:订单 6 条 ============
const BN_ORDERS: BnOrder[] = [
  { id: 'BN20260810', item: '五针松·凌云', date: '08-10', status: '已签收', amount: 8600 },
  { id: 'BN20260805', item: '紫砂椭圆盆', date: '08-05', status: '已签收', amount: 1800 },
  { id: 'BN20260728', item: '桂林山水盆景', date: '07-28', status: '养护中', amount: 5800 },
  { id: 'BN20260720', item: '枫树·丹霞', date: '07-20', status: '已签收', amount: 6800 },
  { id: 'BN20260715', item: '真柏·翠云', date: '07-15', status: '已签收', amount: 28000 },
  { id: 'BN20260701', item: '青瓷长方盆', date: '07-01', status: '已退换', amount: 1200 }
]

// ============ 数据:收藏 6 条 ============
const BN_COLLECTS: BnCollect[] = [
  { name: '罗汉松·福寿', type: '松柏', price: 35000, tag: '珍藏' },
  { name: '黄山云海', type: '山水', price: 8800, tag: '高端' },
  { name: '黄杨·玉润', type: '杂木', price: 12000, tag: '名木' },
  { name: '紫砂方盆', type: '盆器', price: 2200, tag: '紫砂' },
  { name: '峨眉金顶', type: '山水', price: 12000, tag: '珍藏' },
  { name: '石榴·硕果', type: '杂木', price: 5500, tag: '赏果' }
]

// ============ 数据:月度消费 6 条 ============
const BN_MONTHS: BnMonth[] = [
  { month: '03月', amount: 8600 },
  { month: '04月', amount: 4200 },
  { month: '05月', amount: 12800 },
  { month: '06月', amount: 6800 },
  { month: '07月', amount: 34800 },
  { month: '08月', amount: 10400 }
]

// ============ 全局纯函数 ============
function bnInk(wave: number, i: number): number {
  let phase: number = (wave + i * 30) % 100
  return 0.5 + phase / 100 * 0.5
}

function bnSway(wave: number, i: number): number {
  return Math.sin(wave / 50 + i) * 6
}

function bnFloatY(wave: number, i: number): number {
  return Math.sin(wave / 35 + i) * 5
}

function bnBlink(wave: number, i: number): number {
  let v: number = (wave + i * 60) % 200
  return v < 100 ? 1.0 : 0.5
}

function bnFilterPines(tag: string): BnPine[] {
  let r: BnPine[] = []
  for (let i = 0; i < BN_PINES.length; i++) {
    if (tag === '全部' || BN_PINES[i].tag === tag) {
      r.push(BN_PINES[i])
    }
  }
  return r
}

function bnFilterBroadleaf(tag: string): BnBroadleaf[] {
  let r: BnBroadleaf[] = []
  for (let i = 0; i < BN_BROADLEAF.length; i++) {
    if (tag === '全部' || BN_BROADLEAF[i].tag === tag) {
      r.push(BN_BROADLEAF[i])
    }
  }
  return r
}

function bnSoldRank(): BnPine[] {
  let r: BnPine[] = BN_PINES.slice()
  r.sort((a: BnPine, b: BnPine) => b.sold - a.sold)
  return r.slice(0, 5)
}

function bnAgeW(age: number): number {
  return Math.min(age / 50 * 100, 100)
}

function bnHeightW(h: number): number {
  return Math.min(h / 60 * 100, 100)
}

function bnTrunkW(t: number): number {
  return Math.min(t / 12 * 100, 100)
}

function bnPriceW(p: number): number {
  return Math.min(p / 35000 * 100, 100)
}

function bnSizeW(s: number): number {
  return Math.min(s / 90 * 100, 100)
}

function bnMonthH(amount: number): number {
  let max: number = 34800
  return Math.max(amount / max * 120, 8)
}

function bnTopN(i: number): string {
  return ['1', '2', '3', '4', '5'][i] || ''
}

function bnStatusColor(status: string): string {
  if (status === '已签收') {
    return BN.primary
  }
  if (status === '养护中') {
    return BN.accent
  }
  if (status === '已退换') {
    return '#999999'
  }
  return BN.sub
}

function bnPineByName(name: string): BnPine {
  let idx: number = 0
  for (let i = 0; i < BN_PINES.length; i++) {
    if (BN_PINES[i].name === name) {
      idx = i
    }
  }
  return BN_PINES[idx]
}

function bnLandscapeByName(name: string): BnLandscape {
  let idx: number = 0
  for (let i = 0; i < BN_LANDSCAPE.length; i++) {
    if (BN_LANDSCAPE[i].name === name) {
      idx = i
    }
  }
  return BN_LANDSCAPE[idx]
}

function bnPotByName(name: string): BnPot {
  let idx: number = 0
  for (let i = 0; i < BN_POTS.length; i++) {
    if (BN_POTS[i].name === name) {
      idx = i
    }
  }
  return BN_POTS[idx]
}

function bnBroadleafByName(name: string): BnBroadleaf {
  let idx: number = 0
  for (let i = 0; i < BN_BROADLEAF.length; i++) {
    if (BN_BROADLEAF[i].name === name) {
      idx = i
    }
  }
  return BN_BROADLEAF[idx]
}

// ============ 主组件 ============
@Entry
@Component
struct BnApp {
  @State tab: string = 'home'
  @State showHomeExhibit: boolean = false
  @State showHomeCourse: boolean = false
  @State showHomeAuction: boolean = false
  @State showHomeCommunity: boolean = false

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showHomeExhibit = false
      this.showHomeCourse = false
      this.showHomeAuction = false
      this.showHomeCommunity = false
    })
  }

  build() {
    Stack() {
      Column() {
        // 头部品牌栏
        Row() {
          Column() {
            Text('盆景').fontSize(22).fontWeight(900).fontColor(BN.dark)
            Text('BONSAI CRAFT').fontSize(10).fontColor(BN.sub).letterSpacing(2)
          }
          .alignItems(HorizontalAlign.Start)
          Column().layoutWeight(1)
          Row() {
            Text('一木一石').fontSize(12).fontColor(BN.sub)
            Column().width(2).height(12).backgroundColor(BN.line).margin({ left: 8, right: 8 })
            Text('禅意盆栽').fontSize(10).fontColor(BN.accent).fontWeight(700)
          }
        }
        .width('100%').height(56).padding({ left: 16, right: 16 }).backgroundColor(BN.card)

        // 搜索条
        Row() {
          Row() {
            Text('🔍').fontSize(14).fontColor(BN.sub).margin({ left: 12, right: 8 })
            Text('搜索松柏 / 杂木 / 山水...').fontSize(13).fontColor(BN.sub)
            Column().layoutWeight(1)
            Text().width(28).height(28).borderRadius(14).backgroundColor(BN.primary).onClick(() => {})
          }
          .width('100%').height(40).backgroundColor(BN.bg).borderRadius(20).padding({ left: 4, right: 4 })
        }
        .width('100%').padding({ left: 12, right: 12, top: 8, bottom: 8 })

        // tab 内容区
        Column() {
          if (this.tab === 'home') {
            BnHomeTab({
              onExhibit: () => { this.showHomeExhibit = true },
              onCourse: () => { this.showHomeCourse = true },
              onAuction: () => { this.showHomeAuction = true },
              onCommunity: () => { this.showHomeCommunity = true }
            })
          } else if (this.tab === 'pine') {
            BnPineTab()
          } else if (this.tab === 'broad') {
            BnBroadleafTab()
          } else if (this.tab === 'landscape') {
            BnLandscapeTab()
          } else if (this.tab === 'pot') {
            BnPotTab()
          } else {
            BnMineTab()
          }
        }
        .layoutWeight(1)

        // 底部 tab 栏
        Row() {
          ForEach(BN_TABS, (t: BnTab) => {
            Column() {
              Text(t.icon).width(20).height(20).fontColor(this.tab === t.key ? BN.primary : BN.sub)
              Text(t.label).fontSize(10).fontColor(this.tab === t.key ? BN.primary : BN.sub).fontWeight(this.tab === t.key ? 700 : 400)
            }
            .layoutWeight(1)
            .padding({ top: 6, bottom: 6 })
            .onClick(() => {
              this.tab = t.key
            })
          }, (t: BnTab) => t.key)
        }
        .width('100%').height(52).backgroundColor(BN.card)
        .border({ width: { top: 1 }, color: BN.line })
      }
      .width('100%').height('100%').backgroundColor(BN.bg)

      // 首页弹框
      if (this.showHomeExhibit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('盆景展览').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('精选名木盆景展').fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('展览名称:').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 8 })
              Row() {
                Text('秋季名木展').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.primary)
                Text('文人雅趣展').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('山水意境展').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
              .width('100%')
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('展览地点:').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 8 })
              Row() {
                Text('苏州虎丘').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.accent)
                Text('扬州瘦西湖').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('杭州西溪').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
              .width('100%')
            }
            .width('100%').margin({ top: 12 })
            Column() {
              Text('展期:').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 8 })
              Row() {
                Text('08-25~09-10').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.primary)
                Text('09-15~10-07').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
              .width('100%')
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Text('门票:').fontSize(13).fontColor(BN.text).fontWeight(600)
              Text('¥88').fontSize(18).fontWeight(800).fontColor(BN.accent)
              Column().layoutWeight(1)
              Text('预约参观').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      if (this.showHomeCourse) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('盆景课堂').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('名师传授盆景技艺').fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('课程一:松柏造型基础').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 4 })
              Text('讲师:周师傅 | 3小时 | ¥580').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 16 })
            Column() {
              Text('课程二:山水盆景构图').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 4 })
              Text('讲师:李师傅 | 2.5小时 | ¥480').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 8 })
            Column() {
              Text('课程三:日常养护秘诀').fontSize(13).fontColor(BN.text).fontWeight(600).margin({ bottom: 4 })
              Text('讲师:王师傅 | 1.5小时 | ¥280').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 8 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showHomeCourse = false })
              Column().layoutWeight(1)
              Text('预约全部').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      if (this.showHomeAuction) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('盆景拍卖').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('名木盆景竞价专场').fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Row() {
              ForEach(['#5B7C5A', '#B5651D', '#8FAB7B', '#2C2418'], (c: string) => {
                Column().width(36).height(36).borderRadius(18).backgroundColor(c).margin({ left: 6, right: 6 })
              }, (c: string, i: number) => `${c}_${i}`)
            }
            .margin({ top: 16 })
            Column() {
              Text('当前拍品:罗汉松·福寿').fontSize(14).fontColor(BN.text).fontWeight(600).margin({ bottom: 4 })
              Text('树龄50年 | 曲干式 | 树高58cm').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 16 })
            Row() {
              Text('当前出价:').fontSize(13).fontColor(BN.text).fontWeight(600)
              Text('¥35,000').fontSize(20).fontWeight(800).fontColor(BN.accent)
              Column().layoutWeight(1)
              Text('加价').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.accent)
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      if (this.showHomeCommunity) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('盆景社区').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('与盆景爱好者交流心得').fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('"养了三年的五针松终于成型,分享给大家看看"').fontSize(13).fontColor(BN.text).margin({ bottom: 4 })
              Text('@松韵阁 | 2小时前').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 16 })
            Column() {
              Text('"新入手的紫砂盆配榉树,古韵十足"').fontSize(13).fontColor(BN.text).margin({ bottom: 4 })
              Text('@山水间 | 5小时前').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 8 })
            Column() {
              Text('"山水盆景的石头选择很关键,英石最佳"').fontSize(13).fontColor(BN.text).margin({ bottom: 4 })
              Text('@石痴 | 1天前').fontSize(11).fontColor(BN.sub)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 8 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showHomeCommunity = false })
              Column().layoutWeight(1)
              Text('加入社区').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab 1: 首页 ============
@Component
struct BnHomeTab {
  @State wave: number = 0
  @State pineList: BnPine[] = []
  @State rankList: BnPine[] = []
  private timer: number = -1
  @State showDetail: boolean = false
  @State selName: string = ''
  onExhibit: () => void = () => {}
  onCourse: () => void = () => {}
  onAuction: () => void = () => {}
  onCommunity: () => void = () => {}

  aboutToAppear() {
    this.pineList = BN_PINES.slice(0, 4)
    this.rankList = bnSoldRank()
    this.timer = setInterval(() => {
      this.wave += 1
    }, 60)
  }

  aboutToDisappear() {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showDetail = false
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 水墨晕染 banner
          Stack() {
            Column().width('100%').height(120)
              .linearGradient({ angle: 135, colors: [[BN.primary, 0], [BN.dark, 1]] })
            Column() {
              Text('一木一石').fontSize(28).fontWeight(900).fontColor('#F5F2E8').letterSpacing(3)
              Text('禅意盆景 · 东方美学').fontSize(12).fontColor('rgba(245,242,232,0.8)').margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Center).margin({ top: 20 })
            // 晕染效果圆
            Row() {
              ForEach([0, 1, 2, 3], (i: number) => {
                Column().width(50).height(50).borderRadius(25).backgroundColor('rgba(245,242,232,0.08)')
                  .scale({ x: bnInk(this.wave, i), y: bnInk(this.wave, i) })
                  .margin({ left: 12, right: 12 })
              }, (i: number) => i.toString())
            }
            .margin({ top: 60 })
          }
          .width('100%').height(120)

          // 盆景仪表盘
          Row() {
            Stack() {
              Column().width(100).height(100).borderRadius(50).border({ width: 4, color: BN.line })
              Column().width(80).height(80).borderRadius(40).backgroundColor(BN.card)
              Column() {
                Text('树龄').fontSize(10).fontColor(BN.sub)
                Text('45').fontSize(24).fontWeight(900).fontColor(BN.primary).margin({ top: 2 })
                Text('年').fontSize(10).fontColor(BN.sub)
              }
              .alignItems(HorizontalAlign.Center)
            }
            .width(110).height(110)
            Column() {
              Text('镇店之宝').fontSize(11).fontColor(BN.sub)
              Text('真柏·翠云').fontSize(20).fontWeight(800).fontColor(BN.dark).margin({ top: 4 })
              Text('曲干式 | 树高42cm').fontSize(11).fontColor(BN.sub).margin({ top: 4 })
              Text('树龄45年 | ¥28,000').fontSize(11).fontColor(BN.accent).fontWeight(600).margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1).padding({ left: 12 })
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

          // 4 格统计卡
          Row() {
            Column() {
              Text('68').fontSize(22).fontWeight(800).fontColor(BN.primary)
              Text('松柏').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
            Column().width(1).height(36).backgroundColor(BN.line)
            Column() {
              Text('42').fontSize(22).fontWeight(800).fontColor(BN.accent)
              Text('杂木').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
            Column().width(1).height(36).backgroundColor(BN.line)
            Column() {
              Text('35').fontSize(22).fontWeight(800).fontColor(BN.primary)
              Text('山水').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
            Column().width(1).height(36).backgroundColor(BN.line)
            Column() {
              Text('50').fontSize(22).fontWeight(800).fontColor(BN.accent)
              Text('盆器').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
          }
          .width('100%').backgroundColor(BN.card).margin({ top: 8 })

          // 功能宫格
          Column() {
            Text('快捷功能').fontSize(14).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            Row() {
              Column() {
                Column().width(44).height(44).borderRadius(22).backgroundColor(BN.primary)
                Text('展览').fontSize(11).fontColor(BN.text).margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .onClick(() => { this.onExhibit() })
              Column() {
                Column().width(44).height(44).borderRadius(22).backgroundColor(BN.accent)
                Text('课堂').fontSize(11).fontColor(BN.text).margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .onClick(() => { this.onCourse() })
              Column() {
                Column().width(44).height(44).borderRadius(22).backgroundColor(BN.glow)
                Text('拍卖').fontSize(11).fontColor(BN.text).margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .onClick(() => { this.onAuction() })
              Column() {
                Column().width(44).height(44).borderRadius(22).backgroundColor(BN.dark)
                Text('社区').fontSize(11).fontColor(BN.text).margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .onClick(() => { this.onCommunity() })
            }
            .width('100%')
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

          // 热销榜单
          Column() {
            Row() {
              Text('热销榜单').fontSize(14).fontWeight(700).fontColor(BN.dark)
              Column().layoutWeight(1)
              Text('查看全部').fontSize(12).fontColor(BN.sub)
            }
            .width('100%').margin({ bottom: 12 })
            ForEach(this.rankList, (p: BnPine, i: number) => {
              Row() {
                Text(bnTopN(i)).fontSize(18).fontWeight(900).fontColor(i < 3 ? BN.accent : BN.sub).width(28)
                Column() {
                  Text(p.name).fontSize(14).fontWeight(600).fontColor(BN.text)
                  Text(p.type + ' · ' + p.style + ' · ' + p.age + '年').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Column() {
                  Text('已售 ' + p.sold.toString()).fontSize(12).fontColor(BN.accent).fontWeight(700)
                  Text('库存 ' + p.stock.toString()).fontSize(10).fontColor(BN.sub).margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%').padding({ top: 8, bottom: 8 })
              .border({ width: { bottom: 1 }, color: BN.line })
              .onClick(() => {
                this.selName = p.name
                this.showDetail = true
              })
            }, (p: BnPine) => p.name)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

          // 联名限定横滑
          Column() {
            Text('名家之作').fontSize(14).fontWeight(700).fontColor(BN.dark).margin({ bottom: 8 })
            Scroll() {
              Row({ space: 12 }) {
                ForEach(BN_PINES.slice(5, 10), (p: BnPine) => {
                  Column() {
                    Column().width(140).height(80).borderRadius(12)
                      .linearGradient({ angle: 135, colors: [[BN.glow, 0], [BN.primary, 1]] })
                      .padding(12)
                    Text(p.name).fontSize(12).fontWeight(600).fontColor(BN.text).margin({ top: 6 })
                    Text(p.age + '年 · ' + p.style).fontSize(10).fontColor(BN.accent).margin({ top: 2 })
                  }
                  .width(140).alignItems(HorizontalAlign.Start)
                }, (p: BnPine) => p.name)
              }
              .padding({ left: 4, right: 4 })
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8, bottom: 8 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      // 松柏详情弹框
      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('盆景详情').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text(bnPineByName(this.selName).name).fontSize(16).fontWeight(600).fontColor(BN.text).margin({ top: 8 })
            Text(bnPineByName(this.selName).type + ' · ' + bnPineByName(this.selName).style + ' · ' + bnPineByName(this.selName).tag)
              .fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('树龄').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnAgeW(bnPineByName(this.selName).age).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.primary)
              }
              Text(bnPineByName(this.selName).age.toString() + ' 年').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('树高').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnHeightW(bnPineByName(this.selName).height).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.accent)
              }
              Text(bnPineByName(this.selName).height.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 12 })
            Column() {
              Text('干粗').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnTrunkW(bnPineByName(this.selName).trunk).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.glow)
              }
              Text(bnPineByName(this.selName).trunk.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Column() { Text('价格').fontSize(11).fontColor(BN.sub); Text('¥' + bnPineByName(this.selName).price.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('库存').fontSize(11).fontColor(BN.sub); Text(bnPineByName(this.selName).stock.toString()).fontSize(14).fontWeight(700).fontColor(BN.text) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('已售').fontSize(11).fontColor(BN.sub); Text(bnPineByName(this.selName).sold.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showDetail = false })
              Column().layoutWeight(1)
              Text('加入收藏').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
            }
            .width('100%').margin({ top: 16 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab 2: 松柏 ============
@Component
struct BnPineTab {
  @State wave: number = 0
  @State list: BnPine[] = []
  @State filterTag: string = '全部'
  private timer: number = -1
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDelete: boolean = false
  @State selName: string = ''

  aboutToAppear() {
    this.list = bnFilterPines('全部')
    this.timer = setInterval(() => {
      this.wave += 1
    }, 60)
  }

  aboutToDisappear() {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showDetail = false
      this.showAdd = false
      this.showEdit = false
      this.showDelete = false
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 筛选 chips
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '名木', '经典', '高端', '入门', '珍藏'], (t: string) => {
                Text(t).fontSize(12).fontColor(this.filterTag === t ? BN.card : BN.text)
                  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.filterTag === t ? BN.primary : BN.bg)
                  .onClick(() => {
                    this.filterTag = t
                    this.list = bnFilterPines(t)
                  })
              }, (t: string) => t)
            }
            .padding({ left: 12, right: 12 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
          .height(36)

          // 双列网格
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.list, (p: BnPine, i: number) => {
              Column() {
                Stack() {
                  // 圆形树冠造型
                  Column().width('100%').height(80).borderRadius(12)
                    .linearGradient({ angle: 135, colors: [[BN.glow, 0], [BN.primary, 1]] })
                  Column().width(40).height(40).borderRadius(20).backgroundColor('rgba(245,242,232,0.3)')
                    .scale({ x: bnInk(this.wave, i), y: bnInk(this.wave, i) })
                    .translate({ x: bnSway(this.wave, i) })
                }
                .width('100%').height(80)
                Text(p.name).fontSize(13).fontWeight(700).fontColor(BN.text).margin({ top: 8 })
                Text(p.type + ' · ' + p.style).fontSize(10).fontColor(BN.sub).margin({ top: 2 })
                Row() {
                  Text(p.tag).fontSize(10).fontColor(BN.accent).fontWeight(600)
                  Column().layoutWeight(1)
                  Text(p.age + '年').fontSize(10).fontColor(BN.sub)
                }
                .width('100%').margin({ top: 4 })
                Row() {
                  Text('¥').fontSize(10).fontColor(BN.sub)
                  Text(p.price.toString()).fontSize(14).fontColor(BN.accent).fontWeight(700)
                  Column().layoutWeight(1)
                  Text('库存 ' + p.stock.toString()).fontSize(10).fontColor(BN.sub)
                }
                .width('100%').margin({ top: 4 })
              }
              .width('48.5%')
              .padding(10)
              .backgroundColor(BN.card)
              .borderRadius(12)
              .margin({ bottom: 8 })
              .onClick(() => {
                this.selName = p.name
                this.showDetail = true
              })
            }, (p: BnPine) => p.name)
          }
          .width('100%').padding({ left: 12, right: 12 })

          // 树龄对比图
          Column() {
            Text('树龄对比 (年)').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            ForEach(this.list.slice(0, 5), (p: BnPine, i: number) => {
              Row() {
                Text(p.name.substring(0, 5)).fontSize(10).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(10).borderRadius(5).backgroundColor(BN.bg)
                  Column().width(bnAgeW(p.age).toString() + '%').height(10).borderRadius(5).backgroundColor(BN.primary)
                }
                .layoutWeight(1)
                Text(p.age.toString()).fontSize(10).fontColor(BN.text).fontWeight(600).width(28)
              }
              .width('100%').margin({ bottom: 6 })
            }, (p: BnPine) => p.name)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

          // 高度柱状图
          Column() {
            Text('树高分布 (cm)').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            Row({ space: 4 }) {
              ForEach(this.list.slice(0, 6), (p: BnPine, i: number) => {
                Column() {
                  Column().width(24).height(bnHeightW(p.height)).borderRadius(4).backgroundColor(BN.accent)
                  Text(p.height.toString()).fontSize(9).fontColor(BN.sub).margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Center)
              }, (p: BnPine) => p.name)
            }
            .width('100%').justifyContent(FlexAlign.SpaceAround)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8, bottom: 8 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      // 松柏详情弹框
      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('松柏详情').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text(bnPineByName(this.selName).name).fontSize(16).fontWeight(600).fontColor(BN.text).margin({ top: 8 })
            Text(bnPineByName(this.selName).type + ' · ' + bnPineByName(this.selName).style + ' · ' + bnPineByName(this.selName).tag)
              .fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('树龄').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnAgeW(bnPineByName(this.selName).age).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.primary)
              }
              Text(bnPineByName(this.selName).age.toString() + ' 年').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('树高').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnHeightW(bnPineByName(this.selName).height).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.accent)
              }
              Text(bnPineByName(this.selName).height.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 12 })
            Column() {
              Text('干粗').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnTrunkW(bnPineByName(this.selName).trunk).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.glow)
              }
              Text(bnPineByName(this.selName).trunk.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Column() { Text('价格').fontSize(11).fontColor(BN.sub); Text('¥' + bnPineByName(this.selName).price.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('库存').fontSize(11).fontColor(BN.sub); Text(bnPineByName(this.selName).stock.toString()).fontSize(14).fontWeight(700).fontColor(BN.text) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('已售').fontSize(11).fontColor(BN.sub); Text(bnPineByName(this.selName).sold.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showDetail = false })
              Column().layoutWeight(1)
              Text('编辑').fontSize(12).fontColor(BN.card).padding({ left: 14, right: 14, top: 8, bottom: 8 }).borderRadius(16).backgroundColor(BN.accent)
                .onClick(() => { this.showDetail = false; this.showEdit = true })
              Text('删除').fontSize(12).fontColor('#FFFFFF').padding({ left: 14, right: 14, top: 8, bottom: 8 }).borderRadius(16).backgroundColor('#CC3333')
                .margin({ left: 8 }).onClick(() => { this.showDetail = false; this.showDelete = true })
            }
            .width('100%').margin({ top: 16 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      // 新增弹框
      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('新增松柏').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Column() {
              Text('名称').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('请输入盆景名称').fontSize(12).fontColor(BN.sub)
                Column().layoutWeight(1)
              }
              .width('100%').height(36).backgroundColor(BN.bg).borderRadius(8).padding({ left: 12, right: 12 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('树种').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('黑松').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.primary)
                Text('五针松').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('罗汉松').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('真柏').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Column() {
              Text('造型').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('直干').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.accent)
                Text('斜干').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('曲干').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('悬崖').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showAdd = false })
              Column().layoutWeight(1)
              Text('确认新增').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
                .onClick(() => { this.showAdd = false })
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      // 编辑弹框
      if (this.showEdit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('编辑松柏').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('当前:' + this.selName).fontSize(12).fontColor(BN.sub).margin({ top: 8 })
            Column() {
              Text('修改名称').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text(this.selName).fontSize(12).fontColor(BN.text)
                Column().layoutWeight(1)
              }
              .width('100%').height(36).backgroundColor(BN.bg).borderRadius(8).padding({ left: 12, right: 12 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('修改造型').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('斜干').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.accent)
                Text('直干').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('曲干').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('悬崖').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showEdit = false })
              Column().layoutWeight(1)
              Text('保存修改').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
                .onClick(() => { this.showEdit = false })
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      // 删除弹框
      if (this.showDelete) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('确认删除').fontSize(18).fontWeight(700).fontColor('#CC3333').margin({ top: 20 })
            Text('删除后不可恢复').fontSize(12).fontColor(BN.sub).margin({ top: 8 })
            Column() {
              Text('删除目标').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Text(this.selName).fontSize(16).fontWeight(700).fontColor(BN.text)
            }
            .width('100%').padding(16).backgroundColor('#FFF8F0').borderRadius(12)
            .border({ width: 1, color: '#E0C0A0' }).margin({ top: 16 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showDelete = false })
              Column().layoutWeight(1)
              Text('取消').fontSize(13).fontColor(BN.text).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.bg)
                .onClick(() => { this.showDelete = false })
              Text('确认删除').fontSize(13).fontColor('#FFFFFF').padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor('#CC3333')
                .margin({ left: 8 }).onClick(() => { this.showDelete = false })
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab 3: 杂木 ============
@Component
struct BnBroadleafTab {
  @State wave: number = 0
  @State list: BnBroadleaf[] = []
  private timer: number = -1
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State selName: string = ''

  aboutToAppear() {
    this.list = bnFilterBroadleaf('全部')
    this.timer = setInterval(() => {
      this.wave += 1
    }, 60)
  }

  aboutToDisappear() {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showDetail = false
      this.showAdd = false
      this.showEdit = false
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 排行榜
          Column() {
            Text('杂木热销榜').fontSize(14).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            ForEach(this.list, (b: BnBroadleaf, i: number) => {
              Row() {
                Text(bnTopN(i)).fontSize(20).fontWeight(900).fontColor(i < 3 ? BN.accent : BN.sub).width(28)
                Column() {
                  Column().width(56).height(56).borderRadius(28).backgroundColor(BN.bg)
                    .border({ width: 1, color: BN.line })
                  Text(b.leaf).fontSize(10).fontColor(b.leaf === '常绿' ? BN.primary : BN.accent).fontWeight(600).margin({ top: 2 })
                }
                .width(56).alignItems(HorizontalAlign.Center)
                Column() {
                  Text(b.name).fontSize(14).fontWeight(700).fontColor(BN.text)
                  Text(b.type + ' · ' + b.leaf).fontSize(10).fontColor(BN.sub).margin({ top: 2 })
                  Row() {
                    Text(b.tag).fontSize(10).fontColor(BN.primary).padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4).backgroundColor(BN.bg)
                    Column().layoutWeight(1)
                    Text(b.age + '年').fontSize(10).fontColor(BN.sub)
                  }
                  .width('100%').margin({ top: 4 })
                  Row() {
                    Text('¥').fontSize(10).fontColor(BN.sub)
                    Text(b.price.toString()).fontSize(12).fontColor(BN.accent).fontWeight(700)
                    Column().layoutWeight(1)
                    Text(b.height + 'cm').fontSize(10).fontColor(BN.sub)
                  }
                  .width('100%').margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1).padding({ left: 12 })
                Column() {
                  Text('已售').fontSize(10).fontColor(BN.sub)
                  Text(b.sold.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent)
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%').padding(12).backgroundColor(BN.card).borderRadius(12).margin({ bottom: 8 })
              .onClick(() => {
                this.selName = b.name
                this.showDetail = true
              })
            }, (b: BnBroadleaf) => b.name)
          }
          .width('100%').padding(16).backgroundColor(BN.bg)

          // 叶型分布
          Column() {
            Text('叶型分布').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            Column() {
              Row() {
                Text('常绿').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('38%').height(8).borderRadius(4).backgroundColor(BN.primary)
                }
                .layoutWeight(1)
                Text('38%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
              .margin({ bottom: 8 })
              Row() {
                Text('落叶').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('62%').height(8).borderRadius(4).backgroundColor(BN.accent)
                }
                .layoutWeight(1)
                Text('62%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
            }
            .width('100%')
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8 })

          // 价格对比
          Column() {
            Text('价格对比 (¥)').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            ForEach(this.list.slice(0, 5), (b: BnBroadleaf, i: number) => {
              Row() {
                Text(b.name.substring(0, 5)).fontSize(10).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(10).borderRadius(5).backgroundColor(BN.bg)
                  Column().width(bnPriceW(b.price).toString() + '%').height(10).borderRadius(5).backgroundColor(BN.accent)
                }
                .layoutWeight(1)
                Text(b.price.toString()).fontSize(10).fontColor(BN.text).fontWeight(600).width(40)
              }
              .width('100%').margin({ bottom: 6 })
            }, (b: BnBroadleaf) => b.name)
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8, bottom: 8 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      // 杂木详情弹框
      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('杂木详情').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text(bnBroadleafByName(this.selName).name).fontSize(16).fontWeight(600).fontColor(BN.text).margin({ top: 8 })
            Text(bnBroadleafByName(this.selName).type + ' · ' + bnBroadleafByName(this.selName).leaf + ' · ' + bnBroadleafByName(this.selName).tag)
              .fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('树龄').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnAgeW(bnBroadleafByName(this.selName).age).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.primary)
              }
              Text(bnBroadleafByName(this.selName).age.toString() + ' 年').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('树高').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnHeightW(bnBroadleafByName(this.selName).height).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.accent)
              }
              Text(bnBroadleafByName(this.selName).height.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Column() { Text('价格').fontSize(11).fontColor(BN.sub); Text('¥' + bnBroadleafByName(this.selName).price.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('叶型').fontSize(11).fontColor(BN.sub); Text(bnBroadleafByName(this.selName).leaf).fontSize(14).fontWeight(700).fontColor(BN.primary) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('库存').fontSize(11).fontColor(BN.sub); Text(bnBroadleafByName(this.selName).stock.toString()).fontSize(14).fontWeight(700).fontColor(BN.text) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('已售').fontSize(11).fontColor(BN.sub); Text(bnBroadleafByName(this.selName).sold.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showDetail = false })
              Column().layoutWeight(1)
              Text('编辑').fontSize(12).fontColor(BN.card).padding({ left: 14, right: 14, top: 8, bottom: 8 }).borderRadius(16).backgroundColor(BN.accent)
                .onClick(() => { this.showDetail = false; this.showEdit = true })
            }
            .width('100%').margin({ top: 16 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      // 新增弹框
      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('新增杂木').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Column() {
              Text('名称').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('请输入杂木名称').fontSize(12).fontColor(BN.sub)
                Column().layoutWeight(1)
              }
              .width('100%').height(36).backgroundColor(BN.bg).borderRadius(8).padding({ left: 12, right: 12 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('树种').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('榉树').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.primary)
                Text('枫树').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('榆树').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
                Text('黄杨').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Column() {
              Text('叶型').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('常绿').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.accent)
                Text('落叶').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showAdd = false })
              Column().layoutWeight(1)
              Text('确认新增').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
                .onClick(() => { this.showAdd = false })
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }

      // 编辑弹框
      if (this.showEdit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('编辑杂木').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text('当前:' + this.selName).fontSize(12).fontColor(BN.sub).margin({ top: 8 })
            Column() {
              Text('修改名称').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text(this.selName).fontSize(12).fontColor(BN.text)
                Column().layoutWeight(1)
              }
              .width('100%').height(36).backgroundColor(BN.bg).borderRadius(8).padding({ left: 12, right: 12 })
            }
            .width('100%').margin({ top: 16 })
            Column() {
              Text('修改叶型').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Row() {
                Text('常绿').fontSize(12).fontColor(BN.card).padding(8).borderRadius(8).backgroundColor(BN.accent)
                Text('落叶').fontSize(12).fontColor(BN.text).padding(8).borderRadius(8).backgroundColor(BN.bg).margin({ left: 8 })
              }
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Text().width(28).height(28).borderRadius(14).backgroundColor(BN.sub).onClick(() => { this.showEdit = false })
              Column().layoutWeight(1)
              Text('保存修改').fontSize(13).fontColor(BN.card).padding({ left: 20, right: 20, top: 8, bottom: 8 }).borderRadius(20).backgroundColor(BN.primary)
                .onClick(() => { this.showEdit = false })
            }
            .width('100%').margin({ top: 20 })
          }
          .width('86%').backgroundColor(BN.card).borderRadius(16).padding(20)
          .constraintSize({ maxHeight: '80%' }).zIndex(1000)
        }
        .width('100%').height('100%')
      }
    }
    .width('100%').height('100%')
  }
}

// ============ Tab 4: 山水 ============
@Component
struct BnLandscapeTab {
  @State wave: number = 0
  @State list: BnLandscape[] = []
  private timer: number = -1
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showDelete: boolean = false
  @State selName: string = ''

  aboutToAppear() {
    this.list = BN_LANDSCAPE.slice()
    this.timer = setInterval(() => {
      this.wave += 1
    }, 60)
  }

  aboutToDisappear() {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

  @Builder modalOverlay() {
    Column() {
      Column().width('100%').height('100%').backgroundColor('rgba(44,36,24,0.62)')
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
    .onClick(() => {
      this.showDetail = false
      this.showAdd = false
      this.showDelete = false
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          ForEach(this.list, (l: BnLandscape, i: number) => {
            Column() {
              Row() {
                Column() {
                  Text(l.name).fontSize(16).fontWeight(700).fontColor(BN.text)
                  Text(l.style + ' · ' + l.material + ' · ' + l.stones + '石').fontSize(11).fontColor(BN.sub).margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)
                Text(l.tag).fontSize(11).fontColor(BN.card).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .borderRadius(12).backgroundColor(BN.primary)
              }
              .width('100%')

              // 尺寸条
              Column() {
                Row() {
                  Text('尺寸').fontSize(11).fontColor(BN.sub)
                  Column().layoutWeight(1)
                  Text(l.size.toString() + 'cm').fontSize(11).fontColor(BN.text).fontWeight(600)
                }
                .width('100%').margin({ bottom: 4 })
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(6).borderRadius(3).backgroundColor(BN.bg)
                  Column().width(bnSizeW(l.size).toString() + '%').height(6).borderRadius(3).backgroundColor(BN.glow)
                }
                .width('100%')
              }
              .width('100%').margin({ top: 10 })

              // 石材数 + 价格
              Row() {
                Column() {
                  Text('石材数').fontSize(10).fontColor(BN.sub)
                  Text(l.stones.toString() + '块').fontSize(13).fontWeight(700).fontColor(BN.text)
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column().width(1).height(28).backgroundColor(BN.line)
                Column() {
                  Text('价格').fontSize(10).fontColor(BN.sub)
                  Text('¥' + l.price.toString()).fontSize(13).fontWeight(700).fontColor(BN.accent)
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column().width(1).height(28).backgroundColor(BN.line)
                Column() {
                  Text('已售').fontSize(10).fontColor(BN.sub)
                  Text(l.sold.toString()).fontSize(13).fontWeight(700).fontColor(BN.accent)
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
              }
              .width('100%').padding({ top: 10, bottom: 10 }).backgroundColor(BN.bg).borderRadius(10).margin({ top: 10 })

              Row() {
                Text('查看详情 >').fontSize(11).fontColor(BN.primary)
                Column().layoutWeight(1)
                Text('库存 ' + l.stock.toString()).fontSize(11).fontColor(BN.sub)
              }
              .width('100%').margin({ top: 8 })
            }
            .width('100%').padding(14).backgroundColor(BN.card).borderRadius(14).margin({ left: 12, right: 12, bottom: 10 })
            .onClick(() => {
              this.selName = l.name
              this.showDetail = true
            })
          }, (l: BnLandscape) => l.name)

          // 石材类型分布
          Column() {
            Text('石材类型分布').fontSize(13).fontWeight(700).fontColor(BN.dark).margin({ bottom: 12 })
            Column() {
              Row() {
                Text('英石').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('25%').height(8).borderRadius(4).backgroundColor(BN.primary)
                }
                .layoutWeight(1)
                Text('25%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
              .margin({ bottom: 8 })
              Row() {
                Text('太湖石').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('25%').height(8).borderRadius(4).backgroundColor(BN.accent)
                }
                .layoutWeight(1)
                Text('25%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
              .margin({ bottom: 8 })
              Row() {
                Text('斧劈石').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('25%').height(8).borderRadius(4).backgroundColor(BN.glow)
                }
                .layoutWeight(1)
                Text('25%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
              .margin({ bottom: 8 })
              Row() {
                Text('其他').fontSize(11).fontColor(BN.sub).width(56)
                Stack({ alignContent: Alignment.Start }) {
                  Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                  Column().width('25%').height(8).borderRadius(4).backgroundColor(BN.sub)
                }
                .layoutWeight(1)
                Text('25%').fontSize(11).fontColor(BN.text).fontWeight(600).width(32)
              }
            }
            .width('100%')
          }
          .width('100%').padding(16).backgroundColor(BN.card).margin({ top: 8, bottom: 8 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      // 山水详情弹框
      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('山水详情').fontSize(18).fontWeight(700).fontColor(BN.dark).margin({ top: 20 })
            Text(bnLandscapeByName(this.selName).name).fontSize(16).fontWeight(600).fontColor(BN.text).margin({ top: 8 })
            Text(bnLandscapeByName(this.selName).style + ' · ' + bnLandscapeByName(this.selName).material + ' · ' + bnLandscapeByName(this.selName).tag)
              .fontSize(12).fontColor(BN.sub).margin({ top: 4 })
            Column() {
              Text('尺寸').fontSize(12).fontColor(BN.sub).margin({ bottom: 4 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(8).borderRadius(4).backgroundColor(BN.bg)
                Column().width(bnSizeW(bnLandscapeByName(this.selName).size).toString() + '%').height(8).borderRadius(4).backgroundColor(BN.glow)
              }
              Text(bnLandscapeByName(this.selName).size.toString() + ' cm').fontSize(12).fontColor(BN.text).fontWeight(600).margin({ top: 4 })
            }
            .width('100%').margin({ top: 16 })
            Row() {
              Column() { Text('石材数').fontSize(11).fontColor(BN.sub); Text(bnLandscapeByName(this.selName).stones.toString() + '块').fontSize(14).fontWeight(700).fontColor(BN.text) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('价格').fontSize(11).fontColor(BN.sub); Text('¥' + bnLandscapeByName(this.selName).price.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('库存').fontSize(11).fontColor(BN.sub); Text(bnLandscapeByName(this.selName).stock.toString()).fontSize(14).fontWeight(700).fontColor(BN.text) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() { Text('已售').fontSize(11).fontColor(BN.sub); Text(bnLandscapeByName(this.selName).sold.toString()).fontSize(14).fontWeight(700).fontColor(BN.accent) }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').padding(12).backgroundColor(BN.bg).borderRadius(12).margin({ top: 12 })
  }
}


总结

在这里插入图片描述

这款基于HarmonyOS API 24的盆景艺术商城应用,充分展示了ArkTS声明式UI框架在构建精美移动应用方面的强大能力。从接口定义到主题系统,从数据模型到纯函数工具库,从主组件架构到各Tab页面的精细实现,每一层都体现了清晰的设计思想和扎实的编码功底。

应用的技术架构具有很强的参考价值。接口先行的类型设计确保了代码的类型安全和可维护性;设计令牌化的主题系统使得全局样式管理变得简单高效;纯函数工具库将业务逻辑与UI渲染分离,提升了代码的可测试性和复用性;组件化的页面结构则实现了关注点分离,每个组件都有清晰的职责边界。

Logo

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

更多推荐