## 一、鸿蒙生态与 ArkTS 声明式 UI 范式的前沿探索 HarmonyOS 6.1.1 作为华为全场景分布式操作系统的最新演进版本,其应用开发框架在跨设备协同、声明式 UI 渲染引擎以及
基于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函数则产生闪烁效果,在完全不透明和半透明之间切换。
这些动画函数都接收
wave和i两个参数: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存储热销榜单数据,showDetail和selName控制详情弹框。四个回调函数onExhibit、onCourse、onAuction、onCommunity则作为与父组件通信的桥梁。
正确管理定时器等副作用是前端开发中的重要课题。在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年"。右侧是产品的详细信息,包括名称、造型、尺寸和价格,信息层级分明,重点突出。
紧随其后的是四格统计卡,展示松柏、杂木、山水、盆器四大品类的数量统计。使用竖向分割线分隔四个数据项,每项采用上下结构(数字+标签),主色和强调色交替使用增强节奏感。
应用整体流程图
松柏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() {
}
总结

这款基于HarmonyOS API 24的盆景艺术商城应用,充分展示了ArkTS声明式UI框架在构建精美移动应用方面的强大能力。从接口定义到主题系统,从数据模型到纯函数工具库,从主组件架构到各Tab页面的精细实现,每一层都体现了清晰的设计思想和扎实的编码功底。
应用的技术架构具有很强的参考价值。接口先行的类型设计确保了代码的类型安全和可维护性;设计令牌化的主题系统使得全局样式管理变得简单高效;纯函数工具库将业务逻辑与UI渲染分离,提升了代码的可测试性和复用性;组件化的页面结构则实现了关注点分离,每个组件都有清晰的职责边界。
更多推荐



所有评论(0)