HarmonyOS 6 @Observed类则提供了具体的状态管理实现,通过构造函数接收interface对象进行初始化
一、HarmonyOS技术背景与ArkTS开发理念
HarmonyOS作为华为面向万物互联时代打造的分布式操作系统,自诞生以来便以"一次开发,多端部署"为核心设计理念。在HarmonyOS 6.1.1版本中,ArkTS语言已经发展成为一种成熟的、类型安全的声明式UI开发语言,它基于TypeScript进行了深度扩展和优化,保留了TypeScript的静态类型检查能力,同时引入了ArkUI框架的声明式语法,使开发者能够以更加简洁、直观的方式构建高性能的跨设备应用界面。ArkTS的核心优势在于其编译时优化机制——通过方舟编译器(ArkCompiler)将ArkTS代码编译为高效的原生机器码,显著提升了应用的启动速度和运行性能,这对于需要处理大量列表数据和复杂动画交互的本地生活服务类应用尤为重要。
ArkUI框架作为HarmonyOS的官方UI开发框架,采用声明式编程范式,开发者只需描述UI在任意时刻的最终状态,框架会自动处理状态变化引起的UI更新。在API 24版本中,ArkUI提供了丰富的内置组件,包括容器组件(Column、Row、Stack、Flex、Scroll等)、基础组件(Text、Image、Button、TextInput等)以及绘制组件(Circle、Rect、Line、Path等),这些组件通过属性方法和事件方法进行链式配置,形成了高度可组合的UI构建体系。本文分析的陶艺DIY体验馆场景,正是充分利用了ArkUI的这些组件能力,通过Column和Row的嵌套组合实现了卷轴画轴式的列表布局,通过Circle和Rect等绘制组件实现了装饰性的画轴端点效果,通过linearGradient实现了头部区域的渐变色彩过渡。
在状态管理方面,ArkTS提供了@State、@Prop、@Link、@Observed、@ObjectLink等装饰器,构成了一个完整的状态管理响应式体系。@State用于组件内部状态管理,当状态值发生变化时,框架会自动重新调用build方法更新依赖该状态的UI部分;@Observed用于标记可观察的类,使其属性变化能够被框架追踪;@ObjectLink用于在子组件中接收@Observed标记的对象引用。这种分层状态管理机制使得陶艺体验馆这类需要频繁更新列表数据(如收藏状态切换、列表刷新轮播)的应用场景能够获得流畅的交互体验,同时保持代码的清晰可维护性。
从业务价值角度来看,本地生活服务类应用是移动互联网生态中不可或缺的一环。陶艺DIY体验馆作为一个典型的"美团类"场景,涵盖了课程预约、匠人展示、作品浏览、窑期管理、会员体系等多个业务模块。这类应用的核心挑战在于:如何在有限的屏幕空间内高效呈现大量结构化信息,如何通过交互设计引导用户完成预约转化,如何通过视觉设计传达品牌调性。本文所分析的代码实现,采用了橙色(#C05A2E)与青绿色(#6A9C89)的互补色搭配方案,前者代表陶土的温暖质感,后者呼应釉色的清冷光泽,通过色彩语言精准传达了陶艺体验的品牌调性。
在数据架构设计上,该场景采用了interface接口定义与@Observed类implements模式。这种设计模式的优势在于:interface提供了纯粹的类型契约,定义了数据模型的字段结构;@Observed类则提供了具体的状态管理实现,通过构造函数接收interface对象进行初始化。这种分离使得数据模型的定义与状态管理逻辑解耦,既保证了类型安全,又实现了响应式更新的能力。同时,通过为可选布尔字段提供默认值(如o.hot ? o.hot : false),增强了数据初始化的健壮性,防止了因外部数据缺失而导致的运行时异常。
二、色彩系统与ColorPalette接口定义
在HarmonyOS ArkTS开发中,色彩管理是UI设计的基础环节。该场景通过interface定义了一个ColorPalette接口,将所有颜色集中管理,确保了视觉风格的一致性。
interface ColorPalette {
bg: string
card: string
white: string
primary: string
primaryDeep: string
accent: string
accentSoft: string
gold: string
textPrimary: string
textSecond: string
textThird: string
line: string
tagBg: string
warn: string
danger: string
ok: string
}

这段代码定义了一个包含16个颜色字段的接口,涵盖了背景色、卡片色、主色、辅助色、文字层级色、分隔线色、标签背景色以及状态色(警告、危险、成功)。通过接口而非具体对象进行类型约束,使得颜色配置可以在编译时进行类型检查,任何遗漏的颜色字段都会在编译阶段报错,有效防止了运行时因颜色未定义而导致的UI渲染异常。
接下来通过const常量COLORS实例化ColorPalette接口,为每个字段赋予了具体的十六进制色值:
const COLORS: ColorPalette = {
bg: '#F7F3EE',
card: '#FFFFFF',
white: '#FFFFFF',
primary: '#C05A2E',
primaryDeep: '#8D3B1A',
accent: '#6A9C89',
accentSoft: '#E4F0EB',
gold: '#B8860B',
textPrimary: '#33261F',
textSecond: '#6F5F55',
textThird: '#A8988E',
line: '#ECE3DA',
tagBg: '#F2EAE2',
warn: '#E65100',
danger: '#C62828',
ok: '#2E7D32'
}
主色#C05A2E是一种偏暖的赤陶色,灵感来源于景德镇陶溪川的老陶土,配合深色变体#8D3B1A用于渐变收尾。辅助色#6A9C89是青绿釉色,呼应了龙泉青瓷的质感,其浅色变体#E4F0EB用于标签和辅助背景。金色#B8860B用于评分和会员等级标识。文字层级通过三档色值(#33261F、#6F5F55、#A8988E)实现了信息层级的视觉区分。背景色#F7F3EE是一种暖灰白色,模拟了素烧陶坯的表面质感,使整个应用界面呈现出温润的陶艺工坊氛围。
三、数据模型设计:interface与@Observed类implements模式
该场景定义了四个核心数据模型,分别对应课程、匠人、作品和窑期四个业务领域。每个模型都采用interface定义字段契约,再通过@Observed修饰的类implements该接口来实现响应式状态管理。
首先是课程模型ClayCourse和ClayCourseItem的定义:
interface ClayCourse {
id: number
name: string
kind: string
price: number
mins: number
score: number
hot: boolean
fav: boolean
}
@Observed
class ClayCourseItem implements ClayCourse {
id: number = 0
name: string = ''
kind: string = ''
price: number = 0
mins: number = 0
score: number = 0
hot: boolean = false
fav: boolean = false
constructor(o: ClayCourse) {
this.id = o.id
this.name = o.name
this.kind = o.kind
this.price = o.price
this.mins = o.mins
this.score = o.score
this.hot = o.hot ? o.hot : false
this.fav = o.fav ? o.fav : false
}
}
这段代码展示了interface与@Observed类配合使用的完整模式。ClayCourse接口定义了课程数据的7个字段:唯一标识id、课程名称name、课程类别kind(如拉坯、手捏、彩绘等)、价格price、时长mins、评分score、热门标记hot和收藏标记fav。@Observed修饰的ClayCourseItem类通过implements关键字实现了该接口,并在构造函数中逐字段赋值。值得注意的是,对于布尔类型的hot和fav字段,构造函数中采用了三元表达式o.hot ? o.hot : false进行安全赋值,当传入的数据对象未提供该字段或值为falsy时,会默认赋为false,从而保证了布尔字段的类型安全性。
匠人模型ClayMaster和ClayMasterItem的设计遵循同样的模式,但字段聚焦于匠人的专业信息:
interface ClayMaster {
id: number
name: string
title: string
years: number
price: number
works: number
fav: boolean
}
@Observed
class ClayMasterItem implements ClayMaster {
id: number = 0
name: string = ''
title: string = ''
years: number = 0
price: number = 0
works: number = 0
fav: boolean = false
constructor(o: ClayMaster) {
this.id = o.id
this.name = o.name
this.title = o.title
this.years = o.years
this.price = o.price
this.works = o.works
this.fav = o.fav ? o.fav : false
}
}
ClayMaster接口包含了匠人的职称title、从业年限years、作品数量works以及指导价格price。这些字段在UI展示时直接呈现在匠人卡片的底部,为用户选择指导匠人提供了直观的参考信息。title字段的设计很巧妙,它不是简单的职称等级,而是包含了"高级陶艺师"“拉坯专项”“柴烧匠人”"釉料工程师"等专业细分标签,帮助用户根据自己感兴趣的方向选择合适的匠人。
作品模型ClayWork和ClayWorkItem的设计则侧重于作品的展示与交易属性:
interface ClayWork {
id: number
title: string
author: string
kind: string
likes: number
price: number
fav: boolean
}
@Observed
class ClayWorkItem implements ClayWork {
id: number = 0
title: string = ''
author: string = ''
kind: string = ''
likes: number = 0
price: number = 0
fav: boolean = false
constructor(o: ClayWork) {
this.id = o.id
this.title = o.title
this.author = o.author
this.kind = o.kind
this.likes = o.likes
this.price = o.price
this.fav = o.fav ? o.fav : false
}
}
ClayWork接口的设计特点是同时包含了创作属性(title标题、author作者、kind工艺类别)和社交属性(likes点赞数)以及交易属性(price价格)。这种设计使得作品列表项既能展示作品的艺术信息,又能呈现其市场热度和价格,为用户提供了完整的决策依据。fav字段的设计将收藏功能直接嵌入到数据模型中,通过@Observed的响应式特性,当用户点击收藏按钮时,UI会立即更新收藏状态的视觉表现。
窑期模型KilnBatch和KilnBatchItem的设计聚焦于烧制流程的进度跟踪:
interface KilnBatch {
id: number
batch: string
date: string
temp: string
stat: string
items: number
}
@Observed
class KilnBatchItem implements KilnBatch {
id: number = 0
batch: string = ''
date: string = ''
temp: string = ''
stat: string = ''
items: number = 0
constructor(o: KilnBatch) {
this.id = o.id
this.batch = o.batch
this.date = o.date
this.temp = o.temp
this.stat = o.stat
this.items = o.items
}
}
KilnBatch接口的字段设计紧密贴合了陶艺烧制的实际流程。batch字段(如"K-0825 素烧窑")编码了窑炉编号、日期和类型信息;temp字段(如"800℃"“1240℃”“1300℃”)反映了不同烧制工艺的温度需求——素烧通常在800℃左右,釉烧在1240℃左右,柴烧则高达1300℃;stat字段则跟踪从"装窑中"到"待装窑"到"开窑"再到"已完成"的状态流转。items字段记录了每批窑次中的作品数量,帮助用户判断窑位剩余情况。值得注意的是,KilnBatchItem的构造函数没有使用布尔字段的安全赋值模式,因为该模型中的所有字段都是必填的字符串和数字类型。
四、静态数据数组与业务常量
在定义了数据模型之后,代码通过静态数组为每个模型提供了丰富的模拟数据。这些数据不仅是UI展示的基础,也体现了对业务场景的深入理解。
课程数据数组COURSES提供了12门不同类型的陶艺课程:
const COURSES: ClayCourseItem[] = [
new ClayCourseItem({ id: 1, name: '拉坯初体验·杯子', kind: '拉坯', price: 89, mins: 60, score: 4.9, hot: true, fav: true }),
new ClayCourseItem({ id: 2, name: '拉坯进阶·花瓶', kind: '拉坯', price: 129, mins: 90, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 3, name: '手捏小碗·亲子场', kind: '手捏', price: 99, mins: 75, score: 4.9, hot: true, fav: false }),
new ClayCourseItem({ id: 4, name: '手捏萌宠·猫猫盘', kind: '手捏', price: 109, mins: 80, score: 4.7, hot: false, fav: false }),
new ClayCourseItem({ id: 5, name: '青花彩绘·碗', kind: '彩绘', price: 119, mins: 70, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 6, name: '雕花镂空·香插', kind: '雕花', price: 139, mins: 95, score: 4.6, hot: false, fav: false }),
new ClayCourseItem({ id: 7, name: '泥板成型·风铃', kind: '手捏', price: 105, mins: 85, score: 4.7, hot: false, fav: false }),
new ClayCourseItem({ id: 8, name: '绞胎纹样·杯垫', kind: '拉坯', price: 115, mins: 80, score: 4.5, hot: false, fav: false }),
new ClayCourseItem({ id: 9, name: '情侣双人对捏', kind: '手捏', price: 199, mins: 100, score: 4.9, hot: true, fav: false }),
new ClayCourseItem({ id: 10, name: '釉下五彩·餐盘', kind: '彩绘', price: 149, mins: 90, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 11, name: '柴烧小件·体验', kind: '雕花', price: 168, mins: 120, score: 5.0, hot: false, fav: false }),
new ClayCourseItem({ id: 12, name: '团建包场·10人', kind: '拉坯', price: 899, mins: 150, score: 4.9, hot: false, fav: false })
]
这组数据的设计颇具业务洞察:课程价格从89元到899元涵盖了入门到团建的不同消费层级;时长从60分钟到150分钟适应了不同的时间安排;课程类型覆盖了拉坯、手捏、彩绘、雕花、绞胎、柴烧等六大陶艺工艺门类。特别值得注意的是"情侣双人对捏"和"团建包场"这类场景化课程的设计,体现了运营层面针对不同用户群体(情侣、亲子、团队)的差异化产品策略。
匠人数据数组MASTERS提供了10位不同专长的驻场匠人信息:
const MASTERS: ClayMasterItem[] = [
new ClayMasterItem({ id: 1, name: '老周', title: '高级陶艺师', years: 15, price: 168, works: 3210, fav: true }),
new ClayMasterItem({ id: 2, name: '青禾', title: '拉坯专项', years: 8, price: 128, works: 1892, fav: false }),
new ClayMasterItem({ id: 3, name: '阿岚', title: '彩绘专项', years: 6, price: 118, works: 1355, fav: false }),
new ClayMasterItem({ id: 4, name: '何师傅', title: '柴烧匠人', years: 22, price: 228, works: 4021, fav: false }),
new ClayMasterItem({ id: 5, name: '小满', title: '亲子导师', years: 4, price: 98, works: 870, fav: false }),
new ClayMasterItem({ id: 6, name: '陶然', title: '手捏专项', years: 9, price: 138, works: 2110, fav: false }),
new ClayMasterItem({ id: 7, name: '素心', title: '釉料工程师', years: 11, price: 158, works: 1780, fav: false }),
new ClayMasterItem({ id: 8, name: '大彬', title: '紫砂专项', years: 18, price: 268, works: 2906, fav: false }),
new ClayMasterItem({ id: 9, name: '可可', title: '陶艺助教', years: 2, price: 78, works: 460, fav: false }),
new ClayMasterItem({ id: 10, name: '沐野', title: '高级陶艺师', years: 13, price: 188, works: 2610, fav: false })
]

匠人数据的设计反映了陶艺行业的人才梯队结构:从业年限从2年到22年,指导价格从78元到268元,作品数量从460件到4021件。这些数据在UI中以匠人卡片的形式呈现,用户可以通过从业年限、作品数量和价格快速判断匠人的资历水平。职称字段的细分设计(拉坯专项、彩绘专项、柴烧匠人、紫砂专项等)使得不同专长方向的用户都能找到匹配的匠人。
此外,代码还定义了多组业务常量用于辅助UI渲染:
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const MONTH_POTS: number[] = [62, 78, 90, 71, 108, 126]
const COURSE_KINDS: string[] = ['拉坯', '手捏', '彩绘', '雕花', '绞胎', '柴烧']
const HAND_TAGS: string[] = ['揉泥', '定中心', '开孔', '提拉', '修坯', '上釉']
const VIP_RIGHTS: string[] = ['每月赠1次免费烧窑', '会员价全场9折', '作品免费寄存30天', '生日当月赠体验课']
function potBar(i: number): number {
return MONTH_POTS[i] * 1.1
}
MONTH_LABELS和MONTH_POTS配合使用,在窑期页面渲染近6个月的拉坯节数柱状图。potBar函数通过将原始数据乘以1.1的系数来放大柱状图高度,使得视觉对比更加明显。COURSE_KINDS和HAND_TAGS分别用于预约弹框中的课程类型选择和陶艺档案弹框中的手法标签选择。VIP_RIGHTS定义了会员权益列表,在会员页面逐条展示。
五、@Entry组件状态管理与方法定义
在ArkTS中,@Entry装饰器标记的struct是应用的入口组件。该场景的入口组件PageClayPlay定义了丰富的状态变量和业务方法。
@Entry
@Component
struct PageClayPlay {
@State curTab: number = 0
@State mainTab: number = 0
@State addOpen: boolean = false
@State editOpen: boolean = false
@State delOpen: boolean = false
@State bizOpen: boolean = false
@State courseList: ClayCourseItem[] = COURSES
@State masterList: ClayMasterItem[] = MASTERS
@State workList: ClayWorkItem[] = WORKS
@State kilnList: KilnBatchItem[] = KILNS
@State rev: number = 0
@State tick: number = 0
@State kindTags: number[] = [0]
@State handTags: number[] = [0, 1]
@State peopleStep: number = 2
@State timesStep: number = 5
@State glazeFlag: boolean = true
@State kilnTags: number[] = [1]
@State rushFlag: boolean = false
private timer: number = -1

状态变量的设计体现了该场景的交互需求。curTab和mainTab分别控制内容区域tab切换和底部导航栏切换。addOpen、editOpen、delOpen、bizOpen四个布尔状态控制四个弹框的显示与隐藏,分别对应预约体验课、陶艺档案、删除警示和加急开窑。courseList、masterList、workList、kilnList四个数组状态绑定了对应的@Observed数据列表。rev是一个版本号计数器,每次列表数据更新时递增,用于ForEach的keyGenerator函数确保列表项正确刷新。tick用于驱动fxLayer动画效果。kindTags和handTags是数组类型的状态变量,用于弹框中的多选标签管理。peopleStep、timesStep等Step类型变量控制步进器的数值。
生命周期方法的管理也很规范:
aboutToAppear(): void {
this.timer = setInterval(() => {
this.tick = this.tick + 1
}, 100)
}
aboutToDisappear(): void {
if (this.timer >= 0) {
clearInterval(this.timer)
}
}
aboutToAppear在组件创建时启动一个100毫秒间隔的定时器,通过递增tick值驱动fxLayer中的动画效果。aboutToDisappear在组件销毁时清理定时器,防止内存泄漏。这种定时器管理模式是ArkTS中实现轻量级动画的常见做法。
列表刷新方法采用了"末尾移至头部"的轮转策略:
refreshCourses(): void {
this.courseList.unshift(this.courseList[this.courseList.length - 1])
this.courseList.splice(this.courseList.length - 1, 1)
this.rev = this.rev + 1
}
toggleCourseFav(i: number): void {
this.courseList[i].fav = !this.courseList[i].fav
this.rev = this.rev + 1
}
toggleTag(list: number[], i: number): void {
if (list.indexOf(i) >= 0) {
list.splice(list.indexOf(i), 1)
} else {
list.push(i)
}
}

refreshCourses方法将列表最后一个元素移到头部并删除尾部,实现了"换一批"的轮播效果。toggleCourseFav方法直接修改列表项的fav属性,由于ClayCourseItem是@Observed类,该属性变化会被框架自动追踪并触发UI更新。toggleTag是一个通用的多选标签切换方法,通过indexOf判断标签是否已选中,实现了灵活的标签增删逻辑。
六、fxLayer动画特效层
fxLayer是该场景中一个富有特色的@Builder,它构建了一个覆盖在主内容之上的装饰性动画层,通过emoji文字和定时器驱动的变换属性实现了轻量级的动态视觉效果。
@Builder
fxLayer() {
Stack() {
Text('🏺')
.fontSize(26)
.rotate({ angle: (this.tick % 24) * 15 })
.position({ x: 44, y: 140 })
.opacity(0.85)
Text('💧')
.fontSize(18)
.translate({ y: this.tick % 50 })
.position({ x: 260, y: 70 })
.opacity(0.7)
Text('🔥')
.fontSize(22)
.scale({ x: 0.85 + (this.tick % 10) / 14, y: 0.85 + (this.tick % 10) / 14 })
.position({ x: 330, y: 240 })
.opacity(0.85)
Text('✨')
.fontSize(16)
.position({ x: 120, y: 330 })
.opacity((this.tick % 8) / 8 + 0.15)
Text('🍃')
.fontSize(18)
.translate({ x: this.tick % 30, y: this.tick % 60 })
.position({ x: 540, y: 120 })
.opacity(0.55)
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
}

fxLayer的设计思路非常巧妙。它使用了五个与陶艺相关的emoji符号:🏺(陶罐)做旋转动画模拟拉坯旋转,💧(水滴)做垂直位移模拟上釉滴水,🔥(火焰)做缩放呼吸模拟窑火跳动,✨(闪光)做透明度闪烁模拟釉面反光,🍃(树叶)做平移飘动模拟自然风。每个动画都通过tick值与取模运算生成周期性的数值变化:tick % 24 * 15使得🏺每24个tick(2.4秒)完成一周360度旋转;tick % 50使💧在50个tick(5秒)内完成一次垂直循环位移。
特别值得注意的是hitTestBehavior(HitTestMode.None)的设置,这使得fxLayer层不拦截任何触摸事件,用户的所有点击操作都能穿透到下方的内容层。这是构建装饰性覆盖层的标准做法,确保了视觉效果不会干扰用户的交互体验。
七、header头部构建器
header构建器定义了应用顶部的品牌区域,包含了品牌名称、定位信息、搜索框和促销标签。
@Builder
header() {
Column() {
Row() {
Text('🏺')
.fontSize(24)
Text('泥好呀')
.fontSize(21)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text('📍景德镇·陶溪川')
.fontSize(12)
.fontColor('#F3DFD0')
Text('🔔')
.fontSize(19)
.margin({ left: 12 })
.onClick(() => {
this.mainTab = 3
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Text('🔍')
.fontSize(15)
Text('搜课程 / 匠人 / 窑期')
.fontSize(13)
.fontColor('#E8C9B4')
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text('搜索')
.fontSize(13)
.fontColor(COLORS.white)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(COLORS.primaryDeep)
.borderRadius(14)
.onClick(() => {
this.curTab = 0
})
}
.width('100%')
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
.backgroundColor('rgba(255,255,255,0.22)')
.borderRadius(20)
Row({ space: 8 }) {
Text('首单39.9')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('开窑盲盒')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('亲子同行第二人半价')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('作品包邮')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 10, bottom: 14 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0], ['#D97742', 1]] })
}
header的视觉设计采用了linearGradient线性渐变背景,从135度方向的#C05A2E渐变到#D97742,营造出陶土由深到浅的层次感。顶行的Row中,品牌名"泥好呀"使用了21号粗体白色文字,配合🏺emoji图标形成品牌标识;右侧的📍定位信息使用了浅色文字#F3DFD0,🔔通知图标绑定了mainTab切换事件。搜索框区域使用了半透明白色背景rgba(255,255,255,0.22),内部文字"搜课程 / 匠人 / 窑期"使用了#E8C9B4的浅色提示文字,右侧的"搜索"按钮使用了primaryDeep深色背景。促销标签行包含了"首单39.9"“开窑盲盒”“亲子同行第二人半价”"作品包邮"四个标签,全部采用半透明白色背景的胶囊形设计,既统一了视觉风格又通过文字内容传达了核心营销信息。
八、卷轴画轴式scrollTab与tabBar
该场景的核心布局特色在于"卷轴画轴式"的tab设计。每个tab项的顶部都绘制了画轴端点的装饰图形,由Circle和Rect组合而成,模拟了传统中国画卷轴的金属端帽造型。
@Builder
scrollTab(name: string, icon: string, idx: number) {
Column() {
Row() {
Circle({ width: 7, height: 7 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
Rect({ width: 1, height: 4 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
.layoutWeight(1)
Circle({ width: 7, height: 7 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
}
.width('100%')
Column() {
Text(icon)
.fontSize(15)
Text(name)
.fontSize(11)
.fontWeight(this.curTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.curTab === idx ? COLORS.white : COLORS.textSecond)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 7, left: 13, right: 13 })
.backgroundColor(this.curTab === idx ? COLORS.primary : COLORS.card)
.borderRadius(8)
.margin({ top: 2 })
}
.width('17%')
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchTab(idx)
})
}
scrollTab构建器的顶部Row中,左右各放置一个7x7的Circle圆形,中间用宽度为1、高度为4的Rect矩形条连接。当tab处于选中状态时,这组装饰元素使用金色COLORS.gold填充,未选中时使用浅灰色COLORS.line填充,形成了卷轴画轴端帽的视觉效果。下方的Column包含了图标和名称文字,选中状态使用primary橙色背景和白色粗体文字,未选中状态使用白色背景和灰色常规文字。宽度设置为17%,确保五个tab项能够均匀排列。
tabBar构建器将五个scrollTab组合在一起:
@Builder
tabBar() {
Row({ space: 6 }) {
this.scrollTab('体验课', '🫖', 0)
this.scrollTab('匠人', '🧑🎨', 1)
this.scrollTab('作品', '🏺', 2)
this.scrollTab('窑期', '🔥', 3)
this.scrollTab('会员', '👑', 4)
}
.width('100%')
.padding({ left: 10, right: 10, top: 10, bottom: 6 })
}

五个tab分别对应"体验课"“匠人”“作品”“窑期”"会员"五个内容板块,每个tab都配有与陶艺场景相关的emoji图标。这种单排5个的布局设计在屏幕宽度利用上做到了极致,每个tab项约17%的宽度加上间距恰好填满屏幕。
九、课程页面pageCourse与列表渲染
pageCourse是默认展示的课程页面,包含了新客优惠横幅和课程列表两个部分。
@Builder
pageCourse() {
Column() {
Row() {
Column() {
Text('新客首单 · 39.9元体验拉坯')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('含泥料+烧制+包邮到家')
.fontSize(11)
.fontColor('#F3E3C8')
.margin({ top: 4 })
Text('限每日前20名')
.fontSize(12)
.fontColor(COLORS.gold)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🫖')
.fontSize(38)
.margin({ right: 12 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
.onClick(() => {
this.addOpen = true
})
this.sectionTitle('全部课程', '换一批', 0)
ForEach(this.courseList, (it: ClayCourseItem, i: number) => {
Column() {
Row() {
Text(it.kind)
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.accent)
.borderRadius(8)
Text(it.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text(it.fav ? '❤️' : '🤍')
.fontSize(16)
.onClick(() => {
this.toggleCourseFav(i)
})
}
.width('100%')
Row() {
Text('⏱ ' + it.mins + '分钟')
.fontSize(11)
.fontColor(COLORS.textThird)
Text('⭐ ' + it.score)
.fontSize(11)
.fontColor(COLORS.gold)
.margin({ left: 10 })
if (it.hot) {
Text('🔥 热门')
.fontSize(10)
.fontColor(COLORS.warn)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.tagBg)
.borderRadius(6)
.margin({ left: 8 })
}
Column()
.layoutWeight(1)
Text('¥' + it.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.addOpen = true
})
}, (it: ClayCourseItem) => 'c' + it.id.toString() + '_' + this.rev.toString())
}
.width('100%')
.padding({ bottom: 12 })
}
新客横幅使用了从primary到accent的120度渐变,视觉上从橙色过渡到青绿色,形成了一种从陶土到釉色的色彩隐喻。横幅文字采用左对齐布局,主标题"新客首单 · 39.9元体验拉坯"使用白色粗体,副标题"含泥料+烧制+包邮到家"使用浅色#F3E3C8,限名额提示使用金色COLORS.gold。点击横幅会打开预约弹框。
ForEach列表渲染是整个页面的核心。每个课程卡片包含两行信息:第一行是课程类别标签(青绿色背景的圆角胶囊)、课程名称(粗体深色文字)、收藏按钮(❤️/🤍切换);第二行是时长、评分、热门标签和价格。ForEach的keyGenerator函数使用了'c' + it.id.toString() + '_' + this.rev.toString()的复合键,其中rev版本号的加入确保了当列表数据通过refreshCourses轮转后,ForEach能够正确识别列表项的变化并重新渲染。条件渲染if (it.hot)只在热门课程上显示火焰标签,避免了非热门课程卡片的视觉冗余。
十、弹框系统设计:预约体验课modalBodyAdd
弹框系统是该场景交互设计的精华所在。四个弹框分别处理预约、编辑、删除和加急四种业务场景,通过modalOverlay统一管理显示逻辑。
预约体验课弹框modalBodyAdd的设计:
@Builder
modalBodyAdd() {
Column() {
Row() {
Text('🫖 预约体验课')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.addOpen = false
})
}
.width('100%')
Text('课程类型')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 14 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(COURSE_KINDS, (k: string, i: number) => {
Text(k)
.fontSize(12)
.fontColor(this.kindTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.kindTags.indexOf(i) >= 0 ? COLORS.primary : COLORS.tagBg)
.borderRadius(14)
.margin({ right: 8, top: 8 })
.onClick(() => {
this.toggleTag(this.kindTags, i)
})
}, (k: string) => 'ck' + k)
}
.width('100%')
.margin({ top: 4 })
Text('体验人数')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 16 })
Row() {
Text('-')
.fontSize(16)
.fontColor(COLORS.textSecond)
.padding(10)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
.onClick(() => {
if (this.peopleStep > 1) {
this.peopleStep = this.peopleStep - 1
}
})
Column() {
Text(this.peopleStep.toString() + ' 人')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Text('+')
.fontSize(16)
.fontColor(COLORS.white)
.padding(10)
.backgroundColor(COLORS.primary)
.borderRadius(10)
.onClick(() => {
if (this.peopleStep < 10) {
this.peopleStep = this.peopleStep + 1
}
})
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('提交预约')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.primary)
.borderRadius(20)
.onClick(() => {
this.addOpen = false
})
Column()
.layoutWeight(1)
Text('合计 ¥' + (this.peopleStep * 89))
.fontSize(13)
.fontColor(COLORS.gold)
}
.width('100%')
.margin({ top: 20, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
该弹框包含了三个核心交互区域:课程类型多选标签、体验人数步进器、提交按钮与合计金额。课程类型标签使用了Flex布局配合FlexWrap.Wrap实现自动换行,通过kindTags数组的indexOf判断选中状态,选中时使用primary橙色背景白色文字,未选中时使用tagBg浅灰色背景灰色文字。体验人数步进器是典型的减号-数值-加号三段式布局,减号按钮使用灰色背景,加号按钮使用橙色背景形成视觉引导,数值显示在中间区域。步进范围限制在1-10人之间,通过if条件判断实现边界控制。底部的合计金额通过this.peopleStep * 89实时计算,当用户调整人数时金额会自动更新。
十一、弹框系统:加急开窑modalBodyBiz
加急开窑弹框是四个弹框中最具业务特色的,它展示了加急烧窑的定价策略和窑位选择逻辑。
@Builder
modalBodyBiz() {
Column() {
Row() {
Text('🔥 加急开窑 KILN RUSH')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.bizOpen = false
})
}
.width('100%')
Row() {
Column() {
Text('快烧窑 1080℃')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('常规7天 → 加急48小时出窑')
.fontSize(11)
.fontColor('#F3E3C8')
.margin({ top: 4 })
Text('加急费 ¥60/件')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔥')
.fontSize(36)
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient({ angle: 120, colors: [[COLORS.primaryDeep, 0], [COLORS.primary, 1]] })
.margin({ top: 14 })
Text('选择窑位')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 14 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.kilnList, (k: KilnBatchItem, i: number) => {
Text(k.batch)
.fontSize(12)
.fontColor(this.kilnTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.kilnTags.indexOf(i) >= 0 ? COLORS.warn : COLORS.tagBg)
.borderRadius(14)
.margin({ right: 8, top: 8 })
.onClick(() => {
this.toggleTag(this.kilnTags, i)
})
}, (k: KilnBatchItem) => 'kb' + k.id.toString())
}
.width('100%')
.margin({ top: 4 })
Row() {
Column() {
Text('短信通知开窑')
.fontSize(14)
.fontColor(COLORS.textPrimary)
Text('出窑第一时间推送取件码')
.fontSize(11)
.fontColor(COLORS.textThird)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(this.rushFlag ? '已开启' : '已关闭')
.fontSize(12)
.fontColor(this.rushFlag ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.rushFlag ? COLORS.warn : COLORS.tagBg)
.borderRadius(14)
.onClick(() => {
this.rushFlag = !this.rushFlag
})
}
.width('100%')
.margin({ top: 16 })
.padding(12)
.backgroundColor(COLORS.tagBg)
.borderRadius(12)
Row() {
Text('确认加急')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.warn)
.borderRadius(20)
.onClick(() => {
this.bizOpen = false
})
Column()
.layoutWeight(1)
Text('今日剩 3 个加急位')
.fontSize(11)
.fontColor(COLORS.warn)
}
.width('100%')
.margin({ top: 20, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}

加急开窑弹框的顶部信息卡片使用了从primaryDeep到primary的深色渐变,与预约弹框形成视觉区分。信息卡片中"常规7天 → 加急48小时出窑"的文案设计直观传达了加急服务的价值主张——将原本需要7天的烧制周期缩短至48小时。窑位选择区域动态遍历kilnList窑期列表,选中状态使用warn橙色背景而非primary橙色,与加急主题保持一致。短信通知开关使用了rushFlag布尔状态,开启时显示橙色背景。底部的"今日剩 3 个加急位"文案营造了稀缺感,是典型的转化率优化设计。
十二、modalOverlay弹框统一管理与build主架构
modalOverlay构建器是弹框系统的统一入口,通过条件判断决定显示哪个弹框:
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Bottom }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(40,22,12,0.45)')
.onClick(() => {
this.closeAll()
})
if (this.addOpen) {
this.modalBodyAdd()
}
if (this.editOpen) {
this.modalBodyEdit()
}
if (this.delOpen) {
this.modalBodyDel()
}
if (this.bizOpen) {
this.modalBodyBiz()
}
}
.width('100%')
.height('100%')
}
modalOverlay使用Stack布局配合Alignment.Bottom对齐方式,使弹框内容从底部弹出。遮罩层使用了半透明深色背景rgba(40,22,12,0.45),色调偏暖棕,与陶艺主题一致。点击遮罩层调用closeAll方法关闭所有弹框。四个if条件判断对应四个弹框状态变量,确保同一时间只显示一个弹框。
build方法是整个组件的入口构建器,定义了页面的整体结构:
build() {
Stack() {
Column() {
this.header()
this.mainContent()
this.bottomBar()
}
.width('100%')
.height('100%')
this.fxLayer()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
整个页面采用Stack堆叠布局,底层是Column垂直排列的header(头部)、mainContent(内容区)和bottomBar(底部导航栏),顶层是fxLayer动画装饰层。mainContent内部通过Scroll组件包裹,配合scrollBar(BarState.Off)隐藏滚动条和edgeEffect(EdgeEffect.Spring)设置弹性边缘效果,为列表浏览提供了流畅的滚动体验。
mainContent的内容切换逻辑:
@Builder
mainContent() {
Scroll() {
Column() {
this.tabBar()
if (this.mainTab === 0) {
if (this.curTab === 0) {
this.pageCourse()
}
if (this.curTab === 1) {
this.pageMaster()
}
if (this.curTab === 2) {
this.pageWork()
}
if (this.curTab === 3) {
this.pageKiln()
}
if (this.curTab === 4) {
this.pageVip()
}
} else {
this.pageMainOther()
}
}
.width('100%')
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
mainContent首先渲染tabBar(卷轴画轴式标签栏),然后根据mainTab判断是显示首页内容还是其他页面。在首页状态下,通过curTab的值选择对应的页面构建器。这种嵌套的条件渲染设计使得页面切换逻辑清晰可读,同时通过@State的响应式特性实现了无刷新的页面切换。
十三、Mermaid架构流程图
以下是该陶艺DIY体验馆场景的组件结构与数据流架构图:
该架构图清晰地展示了从入口组件build方法出发的三层结构:底层Column负责承载主要业务内容(头部、内容区、底部导航),顶层fxLayer负责装饰性动画效果,弹框层modalOverlay通过条件判断覆盖在最上层。数据流方面,四个@Observed数据模型分别驱动对应的页面内容,@State状态变量通过响应式机制连接数据与UI。
十四、数据模型与组件对比表格
| 维度 | ClayCourseItem | ClayMasterItem | ClayWorkItem | KilnBatchItem |
|---|---|---|---|---|
| 接口定义 | ClayCourse | ClayMaster | ClayWork | KilnBatch |
| 字段数量 | 8个 | 7个 | 7个 | 6个 |
| 核心字段 | name/kind/price | name/title/years | title/author/likes | batch/temp/stat |
| 布尔字段 | hot/fav | fav | fav | 无 |
| 布尔安全赋值 | 三元表达式 | 三元表达式 | 三元表达式 | 不需要 |
| 数据数量 | 12条 | 10条 | 10条 | 8条 |
| 关联页面 | pageCourse | pageMaster | pageWork | pageKiln |
| 交互操作 | 收藏/预约/换一批 | 查看/收藏 | 收藏/换一批 | 占位/加急 |
| 弹框触发 | modalBodyAdd | modalBodyEdit | modalBodyBiz | modalBodyBiz/modalBodyAdd |
| 刷新方法 | refreshCourses | refreshMasters | refreshWorks | 无 |
| 维度 | modalBodyAdd | modalBodyEdit | modalBodyDel | modalBodyBiz |
|---|---|---|---|---|
| 功能定位 | 预约体验课 | 陶艺档案编辑 | 删除课程警示 | 加急开窑 |
| 触发方式 | 点击课程/横幅 | 点击匠人卡片 | 点击删除按钮 | 点击加急/占位 |
| 标签选择 | COURSE_KINDS多选 | HAND_TAGS多选 | 无 | kilnList窑位多选 |
| 步进器 | peopleStep 1-10人 | timesStep 0-99节 | 无 | 无 |
| 开关控件 | 无 | glazeFlag上釉服务 | 无 | rushFlag短信通知 |
| 金额计算 | peopleStep*89 | 无 | 固定¥89 | 固定¥60/件 |
| 主色调 | primary橙色 | accent青绿色 | danger红色 | warn橙色 |
| 信息卡片渐变 | 无 | accentSoft浅色 | FDECEA浅红 | primaryDeep深色 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// ============================================================
// 布局风格:卷轴画轴式内容tab(单排5个)+ 静态渐变头部
// 弹框:预约体验课 / 陶艺档案 / 删除警示 / 加急开窑
// ============================================================
interface ColorPalette {
bg: string
card: string
white: string
primary: string
primaryDeep: string
accent: string
accentSoft: string
gold: string
textPrimary: string
textSecond: string
textThird: string
line: string
tagBg: string
warn: string
danger: string
ok: string
}
const COLORS: ColorPalette = {
bg: '#F7F3EE',
card: '#FFFFFF',
white: '#FFFFFF',
primary: '#C05A2E',
primaryDeep: '#8D3B1A',
accent: '#6A9C89',
accentSoft: '#E4F0EB',
gold: '#B8860B',
textPrimary: '#33261F',
textSecond: '#6F5F55',
textThird: '#A8988E',
line: '#ECE3DA',
tagBg: '#F2EAE2',
warn: '#E65100',
danger: '#C62828',
ok: '#2E7D32'
}
interface ClayCourse {
id: number
name: string
kind: string
price: number
mins: number
score: number
hot: boolean
fav: boolean
}
@Observed
class ClayCourseItem implements ClayCourse {
id: number = 0
name: string = ''
kind: string = ''
price: number = 0
mins: number = 0
score: number = 0
hot: boolean = false
fav: boolean = false
constructor(o: ClayCourse) {
this.id = o.id
this.name = o.name
this.kind = o.kind
this.price = o.price
this.mins = o.mins
this.score = o.score
this.hot = o.hot ? o.hot : false
this.fav = o.fav ? o.fav : false
}
}
interface ClayMaster {
id: number
name: string
title: string
years: number
price: number
works: number
fav: boolean
}
@Observed
class ClayMasterItem implements ClayMaster {
id: number = 0
name: string = ''
title: string = ''
years: number = 0
price: number = 0
works: number = 0
fav: boolean = false
constructor(o: ClayMaster) {
this.id = o.id
this.name = o.name
this.title = o.title
this.years = o.years
this.price = o.price
this.works = o.works
this.fav = o.fav ? o.fav : false
}
}
interface ClayWork {
id: number
title: string
author: string
kind: string
likes: number
price: number
fav: boolean
}
@Observed
class ClayWorkItem implements ClayWork {
id: number = 0
title: string = ''
author: string = ''
kind: string = ''
likes: number = 0
price: number = 0
fav: boolean = false
constructor(o: ClayWork) {
this.id = o.id
this.title = o.title
this.author = o.author
this.kind = o.kind
this.likes = o.likes
this.price = o.price
this.fav = o.fav ? o.fav : false
}
}
interface KilnBatch {
id: number
batch: string
date: string
temp: string
stat: string
items: number
}
@Observed
class KilnBatchItem implements KilnBatch {
id: number = 0
batch: string = ''
date: string = ''
temp: string = ''
stat: string = ''
items: number = 0
constructor(o: KilnBatch) {
this.id = o.id
this.batch = o.batch
this.date = o.date
this.temp = o.temp
this.stat = o.stat
this.items = o.items
}
}
const COURSES: ClayCourseItem[] = [
new ClayCourseItem({ id: 1, name: '拉坯初体验·杯子', kind: '拉坯', price: 89, mins: 60, score: 4.9, hot: true, fav: true }),
new ClayCourseItem({ id: 2, name: '拉坯进阶·花瓶', kind: '拉坯', price: 129, mins: 90, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 3, name: '手捏小碗·亲子场', kind: '手捏', price: 99, mins: 75, score: 4.9, hot: true, fav: false }),
new ClayCourseItem({ id: 4, name: '手捏萌宠·猫猫盘', kind: '手捏', price: 109, mins: 80, score: 4.7, hot: false, fav: false }),
new ClayCourseItem({ id: 5, name: '青花彩绘·碗', kind: '彩绘', price: 119, mins: 70, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 6, name: '雕花镂空·香插', kind: '雕花', price: 139, mins: 95, score: 4.6, hot: false, fav: false }),
new ClayCourseItem({ id: 7, name: '泥板成型·风铃', kind: '手捏', price: 105, mins: 85, score: 4.7, hot: false, fav: false }),
new ClayCourseItem({ id: 8, name: '绞胎纹样·杯垫', kind: '拉坯', price: 115, mins: 80, score: 4.5, hot: false, fav: false }),
new ClayCourseItem({ id: 9, name: '情侣双人对捏', kind: '手捏', price: 199, mins: 100, score: 4.9, hot: true, fav: false }),
new ClayCourseItem({ id: 10, name: '釉下五彩·餐盘', kind: '彩绘', price: 149, mins: 90, score: 4.8, hot: false, fav: false }),
new ClayCourseItem({ id: 11, name: '柴烧小件·体验', kind: '雕花', price: 168, mins: 120, score: 5.0, hot: false, fav: false }),
new ClayCourseItem({ id: 12, name: '团建包场·10人', kind: '拉坯', price: 899, mins: 150, score: 4.9, hot: false, fav: false })
]
const MASTERS: ClayMasterItem[] = [
new ClayMasterItem({ id: 1, name: '老周', title: '高级陶艺师', years: 15, price: 168, works: 3210, fav: true }),
new ClayMasterItem({ id: 2, name: '青禾', title: '拉坯专项', years: 8, price: 128, works: 1892, fav: false }),
new ClayMasterItem({ id: 3, name: '阿岚', title: '彩绘专项', years: 6, price: 118, works: 1355, fav: false }),
new ClayMasterItem({ id: 4, name: '何师傅', title: '柴烧匠人', years: 22, price: 228, works: 4021, fav: false }),
new ClayMasterItem({ id: 5, name: '小满', title: '亲子导师', years: 4, price: 98, works: 870, fav: false }),
new ClayMasterItem({ id: 6, name: '陶然', title: '手捏专项', years: 9, price: 138, works: 2110, fav: false }),
new ClayMasterItem({ id: 7, name: '素心', title: '釉料工程师', years: 11, price: 158, works: 1780, fav: false }),
new ClayMasterItem({ id: 8, name: '大彬', title: '紫砂专项', years: 18, price: 268, works: 2906, fav: false }),
new ClayMasterItem({ id: 9, name: '可可', title: '陶艺助教', years: 2, price: 78, works: 460, fav: false }),
new ClayMasterItem({ id: 10, name: '沐野', title: '高级陶艺师', years: 13, price: 188, works: 2610, fav: false })
]
const WORKS: ClayWorkItem[] = [
new ClayWorkItem({ id: 1, title: '雨过天青·茶盏', author: '青禾', kind: '拉坯', likes: 3201, price: 268, fav: true }),
new ClayWorkItem({ id: 2, title: '猫爪印·小碟', author: '小满', kind: '手捏', likes: 2810, price: 128, fav: false }),
new ClayWorkItem({ id: 3, title: '松风·香炉', author: '老周', kind: '雕花', likes: 1902, price: 388, fav: false }),
new ClayWorkItem({ id: 4, title: '缠枝莲·青花碗', author: '阿岚', kind: '彩绘', likes: 2455, price: 218, fav: false }),
new ClayWorkItem({ id: 5, title: '落灰釉·柴烧杯', author: '何师傅', kind: '柴烧', likes: 4102, price: 528, fav: false }),
new ClayWorkItem({ id: 6, title: '月白·注水壶', author: '陶然', kind: '拉坯', likes: 1560, price: 298, fav: false }),
new ClayWorkItem({ id: 7, title: '绞胎木纹·笔筒', author: '沐野', kind: '绞胎', likes: 1220, price: 188, fav: false }),
new ClayWorkItem({ id: 8, title: '冰裂·小口瓶', author: '素心', kind: '釉变', likes: 3310, price: 458, fav: false }),
new ClayWorkItem({ id: 9, title: '童趣·恐龙杯', author: '小满', kind: '手捏', likes: 2088, price: 98, fav: false }),
new ClayWorkItem({ id: 10, title: '紫砂·水平壶', author: '大彬', kind: '紫砂', likes: 5201, price: 1288, fav: false })
]
const KILNS: KilnBatchItem[] = [
new KilnBatchItem({ id: 1, batch: 'K-0825 素烧窑', date: '08-25', temp: '800℃', stat: '装窑中', items: 42 }),
new KilnBatchItem({ id: 2, batch: 'K-0826 釉烧窑', date: '08-26', temp: '1240℃', stat: '待装窑', items: 30 }),
new KilnBatchItem({ id: 3, batch: 'K-0822 柴烧窑', date: '08-22', temp: '1300℃', stat: '开窑', items: 18 }),
new KilnBatchItem({ id: 4, batch: 'K-0827 素烧窑', date: '08-27', temp: '800℃', stat: '待装窑', items: 26 }),
new KilnBatchItem({ id: 5, batch: 'K-0820 釉烧窑', date: '08-20', temp: '1240℃', stat: '已完成', items: 36 }),
new KilnBatchItem({ id: 6, batch: 'K-0828 快烧窑', date: '08-28', temp: '1080℃', stat: '加急位', items: 8 }),
new KilnBatchItem({ id: 7, batch: 'K-0829 柴烧窑', date: '08-29', temp: '1300℃', stat: '预约中', items: 12 }),
new KilnBatchItem({ id: 8, batch: 'K-0830 素烧窑', date: '08-30', temp: '800℃', stat: '待装窑', items: 40 })
]
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const MONTH_POTS: number[] = [62, 78, 90, 71, 108, 126]
const COURSE_KINDS: string[] = ['拉坯', '手捏', '彩绘', '雕花', '绞胎', '柴烧']
const HAND_TAGS: string[] = ['揉泥', '定中心', '开孔', '提拉', '修坯', '上釉']
const VIP_RIGHTS: string[] = ['每月赠1次免费烧窑', '会员价全场9折', '作品免费寄存30天', '生日当月赠体验课']
function potBar(i: number): number {
return MONTH_POTS[i] * 1.1
}
@Entry
@Component
struct PageClayPlay {
@State curTab: number = 0
@State mainTab: number = 0
@State addOpen: boolean = false
@State editOpen: boolean = false
@State delOpen: boolean = false
@State bizOpen: boolean = false
@State courseList: ClayCourseItem[] = COURSES
@State masterList: ClayMasterItem[] = MASTERS
@State workList: ClayWorkItem[] = WORKS
@State kilnList: KilnBatchItem[] = KILNS
@State rev: number = 0
@State tick: number = 0
@State kindTags: number[] = [0]
@State handTags: number[] = [0, 1]
@State peopleStep: number = 2
@State timesStep: number = 5
@State glazeFlag: boolean = true
@State kilnTags: number[] = [1]
@State rushFlag: boolean = false
private timer: number = -1
aboutToAppear(): void {
this.timer = setInterval(() => {
this.tick = this.tick + 1
}, 100)
}
aboutToDisappear(): void {
if (this.timer >= 0) {
clearInterval(this.timer)
}
}
switchTab(i: number): void {
this.curTab = i
}
switchMain(i: number): void {
this.mainTab = i
}
refreshCourses(): void {
this.courseList.unshift(this.courseList[this.courseList.length - 1])
this.courseList.splice(this.courseList.length - 1, 1)
this.rev = this.rev + 1
}
refreshMasters(): void {
this.masterList.unshift(this.masterList[this.masterList.length - 1])
this.masterList.splice(this.masterList.length - 1, 1)
this.rev = this.rev + 1
}
refreshWorks(): void {
this.workList.unshift(this.workList[this.workList.length - 1])
this.workList.splice(this.workList.length - 1, 1)
this.rev = this.rev + 1
}
toggleCourseFav(i: number): void {
this.courseList[i].fav = !this.courseList[i].fav
this.rev = this.rev + 1
}
toggleWorkFav(i: number): void {
this.workList[i].fav = !this.workList[i].fav
this.rev = this.rev + 1
}
toggleTag(list: number[], i: number): void {
if (list.indexOf(i) >= 0) {
list.splice(list.indexOf(i), 1)
} else {
list.push(i)
}
}
closeAll(): void {
this.addOpen = false
this.editOpen = false
this.delOpen = false
this.bizOpen = false
}
kilnStatColor(s: string): string {
if (s === '开窑') {
return COLORS.warn
}
if (s === '已完成') {
return COLORS.ok
}
if (s === '加急位') {
return COLORS.danger
}
return COLORS.accent
}
@Builder
fxLayer() {
Stack() {
Text('🏺')
.fontSize(26)
.rotate({ angle: (this.tick % 24) * 15 })
.position({ x: 44, y: 140 })
.opacity(0.85)
Text('💧')
.fontSize(18)
.translate({ y: this.tick % 50 })
.position({ x: 260, y: 70 })
.opacity(0.7)
Text('🔥')
.fontSize(22)
.scale({ x: 0.85 + (this.tick % 10) / 14, y: 0.85 + (this.tick % 10) / 14 })
.position({ x: 330, y: 240 })
.opacity(0.85)
Text('✨')
.fontSize(16)
.position({ x: 120, y: 330 })
.opacity((this.tick % 8) / 8 + 0.15)
Text('🍃')
.fontSize(18)
.translate({ x: this.tick % 30, y: this.tick % 60 })
.position({ x: 540, y: 120 })
.opacity(0.55)
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
}
@Builder
header() {
Column() {
Row() {
Text('🏺')
.fontSize(24)
Text('泥好呀')
.fontSize(21)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text('📍景德镇·陶溪川')
.fontSize(12)
.fontColor('#F3DFD0')
Text('🔔')
.fontSize(19)
.margin({ left: 12 })
.onClick(() => {
this.mainTab = 3
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Text('🔍')
.fontSize(15)
Text('搜课程 / 匠人 / 窑期')
.fontSize(13)
.fontColor('#E8C9B4')
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text('搜索')
.fontSize(13)
.fontColor(COLORS.white)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(COLORS.primaryDeep)
.borderRadius(14)
.onClick(() => {
this.curTab = 0
})
}
.width('100%')
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
.backgroundColor('rgba(255,255,255,0.22)')
.borderRadius(20)
Row({ space: 8 }) {
Text('首单39.9')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('开窑盲盒')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('亲子同行第二人半价')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
Text('作品包邮')
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('rgba(255,255,255,0.26)')
.borderRadius(12)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 10, bottom: 14 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0], ['#D97742', 1]] })
}
@Builder
scrollTab(name: string, icon: string, idx: number) {
Column() {
Row() {
Circle({ width: 7, height: 7 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
Rect({ width: 1, height: 4 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
.layoutWeight(1)
Circle({ width: 7, height: 7 })
.fill(this.curTab === idx ? COLORS.gold : COLORS.line)
}
.width('100%')
Column() {
Text(icon)
.fontSize(15)
Text(name)
.fontSize(11)
.fontWeight(this.curTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.curTab === idx ? COLORS.white : COLORS.textSecond)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 7, left: 13, right: 13 })
.backgroundColor(this.curTab === idx ? COLORS.primary : COLORS.card)
.borderRadius(8)
.margin({ top: 2 })
}
.width('17%')
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchTab(idx)
})
}
@Builder
tabBar() {
Row({ space: 6 }) {
this.scrollTab('体验课', '🫖', 0)
this.scrollTab('匠人', '🧑🎨', 1)
this.scrollTab('作品', '🏺', 2)
this.scrollTab('窑期', '🔥', 3)
this.scrollTab('会员', '👑', 4)
}
.width('100%')
.padding({ left: 10, right: 10, top: 10, bottom: 6 })
}
@Builder
sectionTitle(title: string, sub: string, act: number) {
Row() {
Column() {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Rect({ width: 22, height: 3 })
.fill(COLORS.accent)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Column()
.layoutWeight(1)
Text(sub)
.fontSize(11)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
.onClick(() => {
if (act === 0) {
this.refreshCourses()
} else if (act === 1) {
this.refreshMasters()
} else if (act === 2) {
this.refreshWorks()
} else {
this.bizOpen = true
}
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 6 })
}
@Builder
pageCourse() {
Column() {
Row() {
Column() {
Text('新客首单 · 39.9元体验拉坯')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('含泥料+烧制+包邮到家')
.fontSize(11)
.fontColor('#F3E3C8')
.margin({ top: 4 })
Text('限每日前20名')
.fontSize(12)
.fontColor(COLORS.gold)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🫖')
.fontSize(38)
.margin({ right: 12 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
.onClick(() => {
this.addOpen = true
})
this.sectionTitle('全部课程', '换一批', 0)
ForEach(this.courseList, (it: ClayCourseItem, i: number) => {
Column() {
Row() {
Text(it.kind)
.fontSize(11)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.accent)
.borderRadius(8)
Text(it.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 8 })
Column()
.layoutWeight(1)
Text(it.fav ? '❤️' : '🤍')
.fontSize(16)
.onClick(() => {
this.toggleCourseFav(i)
})
}
.width('100%')
Row() {
Text('⏱ ' + it.mins + '分钟')
.fontSize(11)
.fontColor(COLORS.textThird)
Text('⭐ ' + it.score)
.fontSize(11)
.fontColor(COLORS.gold)
.margin({ left: 10 })
if (it.hot) {
Text('🔥 热门')
.fontSize(10)
.fontColor(COLORS.warn)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.tagBg)
.borderRadius(6)
.margin({ left: 8 })
}
Column()
.layoutWeight(1)
Text('¥' + it.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.addOpen = true
})
}, (it: ClayCourseItem) => 'c' + it.id.toString() + '_' + this.rev.toString())
}
.width('100%')
.padding({ bottom: 12 })
}
@Builder
pageMaster() {
Column() {
this.sectionTitle('驻场匠人', '换一批', 1)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.masterList, (it: ClayMasterItem, i: number) => {
Column() {
Stack() {
Circle({ width: 50, height: 50 })
.fill(COLORS.tagBg)
Text('🧑🎨')
.fontSize(24)
}
Text(it.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 6 })
Text(it.title)
.fontSize(10)
.fontColor(COLORS.primary)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.tagBg)
.borderRadius(8)
.margin({ top: 4 })
Text('从业' + it.years + '年 · ' + it.works + '件作品')
.fontSize(10)
.fontColor(COLORS.textThird)
.margin({ top: 4 })
Text('指导价 ¥' + it.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
.margin({ top: 6 })
}
.width('48%')
.alignItems(HorizontalAlign.Center)
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ top: 8 })
.onClick(() => {
this.editOpen = true
})
}, (it: ClayMasterItem) => 'ms' + it.id.toString() + '_' + this.rev.toString())
}
.width('94%')
}
.width('100%')
.padding({ bottom: 12 })
}
@Builder
pageWork() {
Column() {
this.sectionTitle('匠人作品', '换一批', 2)
ForEach(this.workList, (it: ClayWorkItem, i: number) => {
Row() {
Stack() {
Column()
.width(64)
.height(64)
.backgroundColor(COLORS.tagBg)
.borderRadius(12)
Text('🏺')
.fontSize(32)
}
Column() {
Text(it.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(it.author + ' · ' + it.kind)
.fontSize(11)
.fontColor(COLORS.textSecond)
.margin({ top: 3 })
Row() {
Text('❤ ' + it.likes)
.fontSize(11)
.fontColor(COLORS.warn)
Text(it.fav ? '已收藏' : '收藏')
.fontSize(11)
.fontColor(it.fav ? COLORS.accent : COLORS.textSecond)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(it.fav ? COLORS.accentSoft : COLORS.tagBg)
.borderRadius(8)
.margin({ left: 8 })
.onClick(() => {
this.toggleWorkFav(i)
})
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Column() {
Text('¥' + it.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
Text('可定制')
.fontSize(10)
.fontColor(COLORS.accent)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.bizOpen = true
})
}, (it: ClayWorkItem) => 'w' + it.id.toString() + '_' + this.rev.toString())
}
.width('100%')
.padding({ bottom: 12 })
}
@Builder
pageKiln() {
Column() {
this.sectionTitle('窑期安排', '加急开窑', 3)
ForEach(this.kilnList, (it: KilnBatchItem, i: number) => {
Row() {
Column() {
Text(it.batch)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(it.date + ' · ' + it.temp)
.fontSize(11)
.fontColor(COLORS.textSecond)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(it.stat)
.fontSize(11)
.fontColor(this.kilnStatColor(it.stat))
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.tagBg)
.borderRadius(8)
Text(it.items + '件')
.fontSize(12)
.fontColor(COLORS.textSecond)
.margin({ left: 10 })
Text('占位')
.fontSize(12)
.fontColor(COLORS.white)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(COLORS.primary)
.borderRadius(14)
.margin({ left: 10 })
.onClick(() => {
if (it.stat === '加急位') {
this.bizOpen = true
} else {
this.addOpen = true
}
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 8 })
}, (it: KilnBatchItem) => 'k' + it.id.toString() + '_' + this.rev.toString())
Column() {
Text('近6月拉坯节数')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row({ space: 12 }) {
ForEach(MONTH_LABELS, (m: string, i: number) => {
Column() {
Text(MONTH_POTS[i].toString())
.fontSize(9)
.fontColor(COLORS.textThird)
Column()
.width(16)
.height(potBar(i))
.backgroundColor(i === 5 ? COLORS.primary : COLORS.accentSoft)
.borderRadius(4)
.margin({ top: 3 })
Text(m)
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Center)
}, (m: string) => 'pm' + m)
}
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ bottom: 12 })
}
@Builder
pageVip() {
Column() {
Row() {
Column() {
Text('陶然卡 · GOLD')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('距升级 PLATINUM 还差 6 节课')
.fontSize(11)
.fontColor('#F3E3C8')
.margin({ top: 4 })
Row() {
Column()
.width(120)
.height(6)
.backgroundColor('rgba(255,255,255,0.3)')
.borderRadius(3)
Column()
.width(78)
.height(6)
.backgroundColor(COLORS.gold)
.borderRadius(3)
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('👑')
.fontSize(38)
}
.width('100%')
.padding(16)
.borderRadius(16)
.linearGradient({ angle: 120, colors: [[COLORS.primaryDeep, 0], [COLORS.primary, 1]] })
.margin({ top: 8 })
Text('会员权益')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 14 })
ForEach(VIP_RIGHTS, (r: string, i: number) => {
Row() {
Text('🎁')
.fontSize(16)
Text(r)
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ left: 10 })
Column()
.layoutWeight(1)
Text('已生效')
.fontSize(10)
.fontColor(COLORS.ok)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ top: 8 })
.onClick(() => {
this.bizOpen = true
})
}, (r: string) => 'vr' + r)
Row({ space: 8 }) {
Column() {
Text('18')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
Text('累计节数')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(10)
.backgroundColor(COLORS.card)
.borderRadius(12)
Column() {
Text('23')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('烧制件数')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(10)
.backgroundColor(COLORS.card)
.borderRadius(12)
Column() {
Text('¥86')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
Text('省下的钱')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(10)
.backgroundColor(COLORS.card)
.borderRadius(12)
}
.width('100%')
.margin({ left: 0, right: 0, top: 12 })
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 12 })
}
@Builder
pageMainOther() {
Column() {
Text('📦')
.fontSize(40)
.margin({ top: 60 })
Text('我的作品')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 10 })
Text('K-0822 柴烧窑 · 猫爪印小碟 · 待取件')
.fontSize(12)
.fontColor(COLORS.textSecond)
.margin({ top: 6 })
Text('预计 08-26 送达')
.fontSize(12)
.fontColor(COLORS.accent)
.margin({ top: 4 })
Text('删除课程')
.fontSize(12)
.fontColor(COLORS.danger)
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor('#FDECEA')
.borderRadius(16)
.margin({ top: 12 })
.onClick(() => {
this.delOpen = true
})
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
@Builder
modalBodyAdd() {
Column() {
Row() {
Text('🫖 预约体验课')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.addOpen = false
})
}
.width('100%')
Text('课程类型')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 14 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(COURSE_KINDS, (k: string, i: number) => {
Text(k)
.fontSize(12)
.fontColor(this.kindTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.kindTags.indexOf(i) >= 0 ? COLORS.primary : COLORS.tagBg)
.borderRadius(14)
.margin({ right: 8, top: 8 })
.onClick(() => {
this.toggleTag(this.kindTags, i)
})
}, (k: string) => 'ck' + k)
}
.width('100%')
.margin({ top: 4 })
Text('体验人数')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 16 })
Row() {
Text('-')
.fontSize(16)
.fontColor(COLORS.textSecond)
.padding(10)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
.onClick(() => {
if (this.peopleStep > 1) {
this.peopleStep = this.peopleStep - 1
}
})
Column() {
Text(this.peopleStep.toString() + ' 人')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Text('+')
.fontSize(16)
.fontColor(COLORS.white)
.padding(10)
.backgroundColor(COLORS.primary)
.borderRadius(10)
.onClick(() => {
if (this.peopleStep < 10) {
this.peopleStep = this.peopleStep + 1
}
})
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('提交预约')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.primary)
.borderRadius(20)
.onClick(() => {
this.addOpen = false
})
Column()
.layoutWeight(1)
Text('合计 ¥' + (this.peopleStep * 89))
.fontSize(13)
.fontColor(COLORS.gold)
}
.width('100%')
.margin({ top: 20, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyEdit() {
Column() {
Row() {
Text('🖐 编辑陶艺档案')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.editOpen = false
})
}
.width('100%')
Text('已解锁手法(可多选)')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 14 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(HAND_TAGS, (h: string, i: number) => {
Text(h)
.fontSize(12)
.fontColor(this.handTags.indexOf(i) >= 0 ? COLORS.white : COLORS.accent)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.handTags.indexOf(i) >= 0 ? COLORS.accent : COLORS.accentSoft)
.borderRadius(14)
.margin({ right: 8, top: 8 })
.onClick(() => {
this.toggleTag(this.handTags, i)
})
}, (h: string) => 'ht' + h)
}
.width('100%')
.margin({ top: 4 })
Text('累计课时(节)')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 16 })
Row() {
Text('-')
.fontSize(16)
.fontColor(COLORS.textSecond)
.padding(10)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
.onClick(() => {
if (this.timesStep > 0) {
this.timesStep = this.timesStep - 1
}
})
Column() {
Text(this.timesStep.toString() + ' 节')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Text('+')
.fontSize(16)
.fontColor(COLORS.white)
.padding(10)
.backgroundColor(COLORS.accent)
.borderRadius(10)
.onClick(() => {
if (this.timesStep < 99) {
this.timesStep = this.timesStep + 1
}
})
}
.width('100%')
.margin({ top: 8 })
Row() {
Column() {
Text('上釉服务')
.fontSize(14)
.fontColor(COLORS.textPrimary)
Text('透明釉默认包含,可换单色釉')
.fontSize(11)
.fontColor(COLORS.textThird)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(this.glazeFlag ? '已开启' : '已关闭')
.fontSize(12)
.fontColor(this.glazeFlag ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.glazeFlag ? COLORS.accent : COLORS.tagBg)
.borderRadius(14)
.onClick(() => {
this.glazeFlag = !this.glazeFlag
})
}
.width('100%')
.margin({ top: 16 })
.padding(12)
.backgroundColor(COLORS.accentSoft)
.borderRadius(12)
Row() {
Text('保存档案')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.accent)
.borderRadius(20)
.onClick(() => {
this.editOpen = false
})
Column()
.layoutWeight(1)
Text('手法 ' + this.handTags.length + ' 项')
.fontSize(11)
.fontColor(COLORS.textThird)
}
.width('100%')
.margin({ top: 20, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyDel() {
Column() {
Row() {
Text('⚠️ 删除课程确认')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.delOpen = false
})
}
.width('100%')
Column() {
Text('🫖')
.fontSize(34)
Text('拉坯初体验·杯子 即将从课程表移除')
.fontSize(13)
.fontColor(COLORS.textPrimary)
.margin({ top: 8 })
Text('开课前2小时外删除可全额退款')
.fontSize(11)
.fontColor(COLORS.warn)
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 18, bottom: 18 })
.backgroundColor('#FDECEA')
.borderRadius(14)
.margin({ top: 14 })
Row({ space: 8 }) {
Column() {
Text('¥89')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
Text('课程金额')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(8)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
Column() {
Text('08-24')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.warn)
Text('开课日期')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(8)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
Column() {
Text('3')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('余下名额')
.fontSize(10)
.fontColor(COLORS.textSecond)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding(8)
.backgroundColor(COLORS.tagBg)
.borderRadius(10)
}
.width('100%')
.margin({ top: 12 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor(COLORS.textSecond)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.tagBg)
.borderRadius(20)
.onClick(() => {
this.delOpen = false
})
Column()
.layoutWeight(1)
Text('确认删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.danger)
.borderRadius(20)
.onClick(() => {
this.delOpen = false
})
}
.width('100%')
.margin({ top: 18, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyBiz() {
Column() {
Row() {
Text('🔥 加急开窑 KILN RUSH')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textThird)
.onClick(() => {
this.bizOpen = false
})
}
.width('100%')
Row() {
Column() {
Text('快烧窑 1080℃')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('常规7天 → 加急48小时出窑')
.fontSize(11)
.fontColor('#F3E3C8')
.margin({ top: 4 })
Text('加急费 ¥60/件')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔥')
.fontSize(36)
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient({ angle: 120, colors: [[COLORS.primaryDeep, 0], [COLORS.primary, 1]] })
.margin({ top: 14 })
Text('选择窑位')
.fontSize(13)
.fontColor(COLORS.textSecond)
.margin({ top: 14 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.kilnList, (k: KilnBatchItem, i: number) => {
Text(k.batch)
.fontSize(12)
.fontColor(this.kilnTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.kilnTags.indexOf(i) >= 0 ? COLORS.warn : COLORS.tagBg)
.borderRadius(14)
.margin({ right: 8, top: 8 })
.onClick(() => {
this.toggleTag(this.kilnTags, i)
})
}, (k: KilnBatchItem) => 'kb' + k.id.toString())
}
.width('100%')
.margin({ top: 4 })
Row() {
Column() {
Text('短信通知开窑')
.fontSize(14)
.fontColor(COLORS.textPrimary)
Text('出窑第一时间推送取件码')
.fontSize(11)
.fontColor(COLORS.textThird)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(this.rushFlag ? '已开启' : '已关闭')
.fontSize(12)
.fontColor(this.rushFlag ? COLORS.white : COLORS.textSecond)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.rushFlag ? COLORS.warn : COLORS.tagBg)
.borderRadius(14)
.onClick(() => {
this.rushFlag = !this.rushFlag
})
}
.width('100%')
.margin({ top: 16 })
.padding(12)
.backgroundColor(COLORS.tagBg)
.borderRadius(12)
Row() {
Text('确认加急')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.backgroundColor(COLORS.warn)
.borderRadius(20)
.onClick(() => {
this.bizOpen = false
})
Column()
.layoutWeight(1)
Text('今日剩 3 个加急位')
.fontSize(11)
.fontColor(COLORS.warn)
}
.width('100%')
.margin({ top: 20, bottom: 20 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16 })
.backgroundColor(COLORS.card)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Bottom }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(40,22,12,0.45)')
.onClick(() => {
this.closeAll()
})
if (this.addOpen) {
this.modalBodyAdd()
}
if (this.editOpen) {
this.modalBodyEdit()
}
if (this.delOpen) {
this.modalBodyDel()
}
if (this.bizOpen) {
this.modalBodyBiz()
}
}
.width('100%')
.height('100%')
}
@Builder
bottomBar() {
Row() {
Column() {
Text('🏠')
.fontSize(20)
Text('首页')
.fontSize(10)
.fontColor(this.mainTab === 0 ? COLORS.primary : COLORS.textThird)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchMain(0)
})
Column() {
Text('📦')
.fontSize(20)
Text('作品')
.fontSize(10)
.fontColor(this.mainTab === 1 ? COLORS.primary : COLORS.textThird)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchMain(1)
})
Column() {
Text('➕')
.fontSize(24)
.fontColor(COLORS.white)
.padding(10)
.backgroundColor(COLORS.primary)
.borderRadius(26)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.addOpen = true
})
Column() {
Text('💬')
.fontSize(20)
Text('消息')
.fontSize(10)
.fontColor(this.mainTab === 3 ? COLORS.primary : COLORS.textThird)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchMain(3)
})
Column() {
Text('👤')
.fontSize(20)
Text('我的')
.fontSize(10)
.fontColor(this.mainTab === 4 ? COLORS.primary : COLORS.textThird)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.switchMain(4)
})
}
.width('100%')
.padding({ top: 6, bottom: 6 })
.backgroundColor(COLORS.card)
}
@Builder
mainContent() {
Scroll() {
Column() {
this.tabBar()
if (this.mainTab === 0) {
if (this.curTab === 0) {
this.pageCourse()
}
if (this.curTab === 1) {
this.pageMaster()
}
if (this.curTab === 2) {
this.pageWork()
}
if (this.curTab === 3) {
this.pageKiln()
}
if (this.curTab === 4) {
this.pageVip()
}
} else {
this.pageMainOther()
}
}
.width('100%')
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
build() {
Stack() {
Column() {
this.header()
this.mainContent()
this.bottomBar()
}
.width('100%')
.height('100%')
this.fxLayer()
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}

十五、技术总结与架构反思
通过对泥好呀陶艺DIY体验馆场景的完整代码分析,我们可以看到HarmonyOS ArkTS在本地生活服务类应用开发中的强大表现力。该场景的代码结构清晰地体现了声明式UI开发的核心思想:通过数据模型定义业务实体,通过@State和@Observed建立状态与UI的响应式绑定,通过@Builder将复杂的UI拆分为可复用的构建器函数,通过build方法组装最终页面。这种分层设计使得代码具有极高的可读性和可维护性,开发者可以快速定位到需要修改的功能模块,而不需要在庞大的构建器函数中搜索。
在数据模型设计方面,interface与@Observed类implements模式的运用展现了ArkTS类型系统的优势。interface提供了编译时的类型检查,确保数据模型的字段完整性;@Observed装饰器赋予了类实例响应式更新的能力,当属性值变化时框架自动触发UI刷新;构造函数中的三元表达式安全赋值模式则保障了布尔字段在数据缺失场景下的默认行为。这种"接口约束+可观察实现"的设计模式,既满足了TypeScript的静态类型检查需求,又实现了ArkUI的响应式更新机制,是HarmonyOS应用开发中处理结构化数据的推荐做法。
在UI架构设计方面,卷轴画轴式tab的设计是该场景最具特色的创新点。通过Circle和Rect绘制组件的组合,在tab项顶部模拟了传统画卷的金属端帽造型,将中国传统文化元素融入了现代移动应用界面设计。这种设计不仅具有视觉辨识度,更重要的是它服务于业务场景——陶艺本身就是中国传统手工艺,卷轴元素与陶艺主题形成了文化层面的呼应。fxLayer动画装饰层的设计同样值得借鉴,它通过emoji文字的transform变换属性实现了轻量级的动态效果,无需引入额外的动画资源,且通过hitTestBehavior设置确保了不干扰用户交互。
在性能优化方面,代码中的几个设计细节值得关注。ForEach的keyGenerator使用了id + rev的复合键模式,rev版本号的引入确保了列表数据轮转后ForEach能够正确识别变化并执行最小化更新。定时器在aboutToDisappear中的清理防止了组件销毁后的内存泄漏。constraintSize的maxHeight限制防止了弹框内容过多时超出屏幕边界。Scroll的scrollBar(BarState.Off)和edgeEffect(EdgeEffect.Spring)配置则在视觉和交互层面优化了滚动体验。
、
更多推荐


所有评论(0)