HarmonyOS 6 ArkUI - 弹层体系则借助bindSheet(底部抽屉)与bindContentCover(全屏遮罩)两类原生容器
扎染之美,在于水与色的交融、绿与蓝的转化、人与缸的默契。云染坊将千年草木染工艺搬上屏幕,让每一匝布的浸染过程都可被围观、可被共创、可被收藏。声明式UI并非冷冰冰的布局语法,而是承载匠心温度的容器——靛蓝、茜草红、布白三色之间,折叠着一套完整的状态流转与交互逻辑。
连麦共创的本质,是直播能力与工艺流程的深度耦合。四机位同屏、弹幕互动、步骤跟踪、抽屉表单、不可变数据更新,这些技术要素在染缸边汇聚成一套非遗数字化的完整方案。HarmonyOS ArkTS以@State/@Prop双向数据流、@Builder复用、bindSheet/bindContentCover弹层体系为骨架,撑起了染坊的全部业务场景。
本篇将逐段拆解这套约2400行的ArkTS源码,从数据建模到六Tab架构,从直播工位到花色册增删改查,从染材库图表到坯布架选择,覆盖声明式UI在非遗直播电商场景下的全部关键实践。
引言

扎染(Tie-dye)作为中国非物质文化遗产的重要分支,其核心工艺包含设计捆扎、养缸调液、下缸浸染、氧化显色、拆线漂洗、晾晒定色六大步骤。传统模式下,这些步骤只能在染坊现场完成,学徒与爱好者难以参与其中。云染坊的出现,正是要解决"看得见但染不着"的痛点:通过四机位直播还原染缸浸布的全过程,通过连麦共创让用户与匠人同缸操作,通过花色册沉淀作品资产,通过染材库与坯布架构建选材闭环。
HarmonyOS ArkTS声明式UI范式为这类复杂交互场景提供了天然的建模能力。@State装饰器实现组件内可变状态,当数据变更时框架自动触发UI刷新;@Prop装饰器实现父子组件单向数据传递,保证子组件拿到的是只读快照,避免意外的反向修改;@Builder装饰器将复杂UI结构封装为可复用函数,降低build方法的嵌套深度。三者协同,构成了染坊从数据到视图的完整响应链路。
从架构层面看,整个应用采用单页面多Tab + 多弹层的组织方式。顶部"晾布夹"Tab导航以晾杆、衣夹、错落悬挂布条为视觉隐喻,将六个业务模块串联起来。主入口组件持有全部业务数据(布匹、染材、订单、匠人、弹幕、步骤、坯布等)作为@State,通过回调函数将增删改操作收归到单一数据源,子Tab仅负责展示与事件上报。这种"数据上浮、事件下沉"的设计模式,让状态管理在中等复杂度应用中保持了清晰可维护的边界。弹层体系则借助bindSheet(底部抽屉)与bindContentCover(全屏遮罩)两类原生容器,分别承载表单录入与确认对话两类交互,避免在主滚动区堆积过多条件分支。
一、数据建模:接口驱动的领域对象

扎染业务的全部实体都以interface定义,这是ArkTS类型系统的标准做法,既保证了编译期类型安全,又不会引入类的运行时开销。
interface Cloth208 {
id: number
name: string
dye: string
dips: number
fastness: number
state: string
watchers: number
starred: boolean
}
interface Dye208 {
id: number
name: string
color: string
origin: string
output: number
heat: number
}
interface Order208 {
id: number
name: string
dye: string
dayNum: number
bundles: number
meters: number
state: string
top: boolean
}
interface Artisan208 {
id: number
name: string
city: string
years: number
online: boolean
heat: number
}
interface Barrage208 {
id: number
text: string
}
interface DipStep208 {
id: number
title: string
tip: string
done: boolean
}
interface DyeCount208 {
label: string
count: number
color: string
}
interface FastLog208 {
day: string
score: number
}
interface Fabric208 {
id: number
name: string
icon: string
stock: number
used: boolean
times: number
}
这套接口覆盖了染坊业务的十个核心实体。Cloth208是花色册的主实体,dye字段记录主染材、dips记录复染次数、fastness记录色牢度评分、state记录当前工艺状态(浸染中/氧化中/已晾干)、starred标记是否精选。Dye208描述草木染材本身,包含产地origin、年产缸数output与人气热度heat。Order208是共创订单,dayNum表示第几日开染、bundles记录扎花匝数、meters记录布量。Artisan208是匠人档案,online标记是否在线、heat记录人气值。Barrage208是直播弹幕,DipStep208是染布六步曲,DyeCount208是染材占比统计的聚合视图,FastLog208是色牢度走势日志,Fabric208是坯布库存。
值得注意的是
DyeCount208是一个典型的"投影型"接口——它并非持久化数据,而是由dyeCounts208函数对Cloth208[]实时聚合而成。这种派生数据用同构interface建模,既复用了ForEach的渲染能力,又避免在状态中维护冗余副本。
接口设计的一个细节是所有字段都使用基础类型(number/string/boolean),没有嵌套对象。这并非偷懒,而是深思熟虑后的选择——ArkTS的状态观察基于浅比较,扁平结构能确保字段级变更被精准捕获,而嵌套对象则需要深拷贝对比,性能开销更大。在后续的map/filter更新中可以看到,每次修改都生成全新的对象数组,正是配合扁平接口的最佳实践。
二、配色与状态映射函数集

染坊的视觉语言高度依赖色彩语义,靛蓝(#283593)代表浸染、茜草红(#E53935)代表已晾干与警告、青绿(#00897B)代表氧化中、黄(#FBC02D)代表栀子染材。这些映射被封装为纯函数,保证可测试性与可复用。
function clothStateColor208(state: string): string {
if (state === '浸染中') {
return '#283593'
}
if (state === '氧化中') {
return '#00897B'
}
if (state === '已晾干') {
return '#E53935'
}
return '#90A4AE'
}
function orderStateColor208(state: string): string {
if (state === '今日开缸') {
return '#E53935'
}
if (state === '氧化等待') {
return '#00897B'
}
if (state === '已交付') {
return '#7CB342'
}
return '#90A4AE'
}
function dyeColor208(dye: string): string {
if (dye === '板蓝根') {
return '#283593'
}
if (dye === '茜草') {
return '#E53935'
}
if (dye === '栀子') {
return '#FBC02D'
}
if (dye === '苏木') {
return '#8D6E63'
}
return '#C62828'
}
clothStateColor208将布匹的三种工艺状态映射为对应色相,让状态标签在列表中一眼可辨——浸染中的靛蓝、氧化中的青绿、已晾干的茜草红,与真实染缸中布匹的颜色演变完全对应。orderStateColor208为订单状态着色,今日开缸用红色突出紧迫感、已交付用绿色传达完成信号。dyeColor208则是染材到标准色的查表函数,板蓝根出靛蓝、茜草出红、栀子出黄、苏木出褐、薯莨出深红。这些函数都是无副作用的纯函数,输入相同则输出相同,在任何组件中调用都安全可靠。
将颜色语义集中到函数层而非散落在UI内联,是维护视觉一致性的关键。当设计需要全局调色时,只需改一处函数即可,而非在数百处内联值中逐一搜索替换。
三、聚合统计函数:不可变数据处理

列表场景常需要从原始数组派生统计指标,染坊用一组函数封装了这些计算。
function starredClothCount208(cloths: Cloth208[]): number {
let n: number = 0
for (let i = 0; i < cloths.length; i++) {
if (cloths[i].starred) {
n++
}
}
return n
}
function dippingClothCount208(cloths: Cloth208[]): number {
let n: number = 0
for (let i = 0; i < cloths.length; i++) {
if (cloths[i].state === '浸染中' || cloths[i].state === '氧化中') {
n++
}
}
return n
}
function onlineArtisanCount208(artisans: Artisan208[]): number {
let n: number = 0
for (let i = 0; i < artisans.length; i++) {
if (artisans[i].online) {
n++
}
}
return n
}
function maxArtisanHeat208(artisans: Artisan208[]): number {
let m: number = 1
for (let i = 0; i < artisans.length; i++) {
if (artisans[i].heat > m) {
m = artisans[i].heat
}
}
return m
}
function dyeCounts208(cloths: Cloth208[]): DyeCount208[] {
const counts: DyeCount208[] = []
for (let i = 0; i < dyeTags208.length; i++) {
let n: number = 0
for (let j = 0; j < cloths.length; j++) {
if (cloths[j].dye === dyeTags208[i]) {
n++
}
}
counts.push({ label: dyeTags208[i], count: n, color: ['#283593', '#E53935', '#FBC02D', '#8D6E63', '#C62828'][i] })
}
return counts
}
starredClothCount208统计精选标记数,dippingClothCount208统计在缸/氧化中的布匹数,这两个指标在花色册头部和直播工位都有使用。onlineArtisanCount208与maxArtisanHeat208服务于匠人榜:前者计算在线人数,后者取人气最大值作为进度条归一化基准——a.heat / maxArtisanHeat208(this.artisans) * 100就是某位匠人相对最高人气的百分比。dyeCounts208是最复杂的聚合,它遍历染材标签数组,对每个染材统计布匹中使用该染材的数量,返回带颜色的DyeCount208[],直接驱动堆叠条形图的渲染。
这些函数的共性是:纯输入输出、无副作用、可直接在build中调用而不会引发额外刷新。ArkTS在每次状态变更后会重新执行build,纯函数的重算是幂等的,结果稳定。但要注意,如果聚合函数内部依赖了外部可变状态,就会破坏幂等性,染坊通过把染材标签作为模块级常量dyeTags208来规避——常量不参与状态观察,重算结果必然一致。
四、主入口组件:状态中枢与Tab路由

Index208是整个应用的入口组件,持有全部业务数据,并通过顶部"晾布夹"导航在六个Tab之间切换。
@Entry
@Component
struct Index208 {
@State tabIndex1: number = 0
// 弹层开关
@State showJoinSheet: boolean = false
@State showPatternSheet: boolean = false
@State showEditSheet: boolean = false
@State showDelDialog: boolean = false
@State showDetailDialog: boolean = false
// 预约连麦共创表单
@State joinPattern: number = 0
@State joinFabric: number = 0
@State joinDips: number = 3
@State joinLive: boolean = true
@State joinKeep: boolean = false
// 上传花色表单
@State patternName: string = ''
@State patternDye: number = 0
@State patternDips: number = 3
@State patternFast: number = 4
@State patternPublic: boolean = true
// 编辑表单
@State editIndex: number = -1
@State editName: string = ''
@State editDips: number = 3
@State editFast: number = 4
@State editTop: boolean = false
// 删除
@State delIndex: number = -1
@State delKeepAlbum: boolean = true
// 详情
@State detailIndex: number = 0
// 业务数据
@State cloths: Cloth208[] = [ /* 十条花色数据 */ ]
@State dyes: Dye208[] = [ /* 五种染材 */ ]
@State orders: Order208[] = [ /* 八条订单 */ ]
@State artisans: Artisan208[] = [ /* 六位匠人 */ ]
@State dipDays: DipDay208[] = [ /* 周浸染批次 */ ]
@State fastLogs: FastLog208[] = [ /* 色牢度日志 */ ]
@State fabrics: Fabric208[] = [ /* 八种坯布 */ ]
@State barrages: Barrage208[] = [ /* 八条弹幕 */ ]
@State dipSteps: DipStep208[] = [ /* 染布六步 */ ]
tabs208: string[] = ['染坊', '花色册', '染材库', '坯布架', '匠人团', '我的']
tabIcons208: string[] = ['🪣', '🎨', '🌿', '🧵', '🧕', '👤']
tabDrops208: number[] = [0, 6, 2, 8, 4, 10]
...
}
状态设计是这里最值得品味的部分。主入口持有三类状态:第一类是路由与弹层开关(tabIndex1及五个show*布尔),控制页面可见性;第二类是各表单的临时录入值(joinPattern、patternName、editName等),承载用户在弹层中的操作;第三类是业务数据(cloths、dyes、orders等),是应用的持久资产。三类状态泾渭分明,弹层开关与表单值是易变的临时态,业务数据是需被回调修改的核心态。
将表单临时态放在主入口而非弹层组件内部,是为了"打开即回填"的体验——编辑花色时主入口先把目标行数据拷贝到
edit*字段,再打开弹层,弹层读取这些字段渲染初始值,保存时再把edit*写回cloths。这种"数据上浮"避免了子组件维护镜像副本的同步难题。
tabs208、tabIcons208、tabDrops208三个数组没有@State装饰,因为它们是常量,不参与状态观察。其中tabDrops208是晾布夹导航的错落偏移量,每个Tab向下悬挂不同距离,模拟真实晾杆上布条长短不一的效果。这种细节让导航从功能性控件升级为有场景感的视觉元素。
五、头部Builder:电商非遗季风横幅

头部是一个独立的@Builder,承担品牌标识、席位统计与开染入口三个职能。
@Builder
header208() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('织彩 · 云染坊').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('扎染匠人连麦共创 · 染缸浸布直播围观').fontSize(11).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column({ space: 2 }) {
Text('🪣').fontSize(20)
Text('1,526').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('围染席位').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 10 }) {
Column().width(4).height(34).borderRadius(2).backgroundColor('#E53935')
Column({ space: 3 }) {
Text('板蓝根老缸今晚开染').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('连麦共创抽留样布 · 围观送扎染方巾').fontSize(9).fontColor('#E8EAF6')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('去连染 →').fontSize(11).fontColor('#283593').fontWeight(FontWeight.Bold)
}
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#E8EAF6')
.onClick(() => {
this.showJoinSheet = true
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#1A237E')
}
.alignItems(HorizontalAlign.Start)
.padding(14)
.linearGradient({ angle: 140, colors: [['#3949AB', 0], ['#283593', 1]] })
}
@Builder装饰器让这段复杂UI可以被当作方法调用(this.header208()),在build中与其他Builder并列,降低嵌套深度。头部第一行是品牌区,左侧标题副标题纵向排列、右侧围染席位数字纵向堆叠,中间用Text('').layoutWeight(1)做弹性占位实现两端对齐。第二行是开染横幅,左侧红色竖条做视觉锚点,文案之后是浅底胶囊按钮"去连染",点击触发showJoinSheet = true打开预约抽屉。整个头部用linearGradient铺设靛蓝渐变背景,angle: 140让光从左上斜入,营造非遗季风的厚重感。
layoutWeight(1)配合空Text('')是ArkTS中实现"剩余空间分配"的惯用手法。空文本本身不占视觉,但它的layoutWeight会吃掉父容器剩余宽度,把后续元素推到右侧。这种写法比Flex的justifyContent更细粒度,因为可以在任意位置插入多个弹性占位实现复杂的三段布局。
六、晾布夹Tab导航:场景化导航的视觉建模

顶部Tab是整个应用的视觉灵魂,用晾杆、衣夹、错落布条重构了传统Tab的呈现方式。
@Builder
tabBar208() {
Column({ space: 0 }) {
// 晾杆
Row() {
Text('').layoutWeight(1)
}
.width('100%')
.height(2)
.backgroundColor('#B0BEC5')
.margin({ left: 10, right: 10 })
Row({ space: 4 }) {
ForEach(this.tabs208, (t: string, i: number) => {
Column({ space: 0 }) {
// 衣夹
Row() {
Column().width(3).height(7).borderRadius(1)
.backgroundColor(this.tabIndex1 === i ? '#E53935' : '#90A4AE')
}
.height(8)
.justifyContent(FlexAlign.Center)
// 悬挂布条
Column({ space: 2 }) {
Text(this.tabIcons208[i]).fontSize(13)
Text(t).fontSize(9).maxLines(1)
.fontColor(this.tabIndex1 === i ? '#FFFFFF' : '#5C6BC0')
}
.width(46)
.padding({ top: 5, bottom: 5 })
.alignItems(HorizontalAlign.Center)
.borderRadius({ bottomLeft: 8, bottomRight: 8 })
.backgroundColor(this.tabIndex1 === i ? '#283593' : '#FFFFFF')
.shadow(this.tabIndex1 === i
? { radius: 8, color: 'rgba(40,53,147,0.4)', offsetY: 3 }
: { radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
}
.layoutWeight(1)
.padding({ top: 0, bottom: 4 })
.margin({ top: this.tabDrops208[i] })
.scale({ x: this.tabIndex1 === i ? 1.08 : 1, y: this.tabIndex1 === i ? 1.08 : 1 })
.animation({ duration: 180 })
.onClick(() => {
this.tabIndex1 = i
})
}, (t: string) => t)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.padding({ top: 2, left: 6, right: 6 })
}
.width('100%')
.padding({ top: 4, bottom: 4 })
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: 3 })
}
晾杆是一根2像素高的灰色横线,横跨整个宽度。每个Tab由"衣夹+布条"两部分构成:衣夹是顶部一个3×7的小竖条(选中时变红、未选中时灰),布条是下方的圆角胶囊,只对底边做圆角(borderRadius({ bottomLeft: 8, bottomRight: 8 })),模拟布条从夹子下垂的形态。tabDrops208[i]作为margin({ top })让每个布条下沉不同距离,制造错落悬挂感。选中态用1.08倍缩放+靛蓝填充+强阴影三重视觉强化,配合animation({ duration: 180 })实现180毫秒的弹性过渡。
这种场景化导航的精髓在于:视觉元素不仅是装饰,更要与业务语义同构。晾杆对应"挂布"、衣夹对应"固定"、错落布条对应"不同花色"——每个像素都在讲故事,用户在点击Tab时,潜意识里完成的是"取下一匹布查看"的动作映射。
ForEach的第三个参数是键生成器(t: string) => t,用Tab文本本身作为唯一键。键的稳定性决定Diff效率,这里文本不变则键不变,框架能精准识别哪一项被选中,只重渲染变化的项,而非整列重建。
七、build主方法:Tab分发与弹层挂载

build方法是组件的渲染入口,这里通过if分支分发六个Tab,并在根容器挂载五个弹层。
build() {
Column() {
this.header208()
this.tabBar208()
Scroll() {
Column({ space: 12 }) {
if (this.tabIndex1 === 0) {
LiveTab208({
cloths: this.cloths,
artisans: this.artisans,
dipSteps: this.dipSteps,
barrages: this.barrages,
onStep: (i: number) => {
this.dipSteps = this.dipSteps.map((s: DipStep208, si: number) => {
if (si === i) {
return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
}
return s
})
},
onJoin: () => {
this.showJoinSheet = true
}
})
}
if (this.tabIndex1 === 1) {
PatternTab208({
cloths: this.cloths,
onAdd: () => { this.showPatternSheet = true },
onDetail: (i: number) => {
this.detailIndex = i
this.showDetailDialog = true
},
onEdit: (i: number) => {
this.editIndex = i
this.editName = this.cloths[i].name
this.editDips = this.cloths[i].dips
this.editFast = this.cloths[i].fastness
this.editTop = this.cloths[i].starred
this.showEditSheet = true
},
onDel: (i: number) => {
this.delIndex = i
this.showDelDialog = true
},
onStar: (i: number) => {
this.cloths = this.cloths.map((c: Cloth208, ci: number) => {
if (ci === i) {
return { id: c.id, name: c.name, dye: c.dye, dips: c.dips,
fastness: c.fastness, state: c.state, watchers: c.watchers,
starred: !c.starred }
}
return c
})
}
})
}
if (this.tabIndex1 === 2) {
DyeTab208({ dyes: this.dyes, cloths: this.cloths, dipDays: this.dipDays })
}
if (this.tabIndex1 === 3) {
FabricTab208({ fabrics: this.fabrics, fastLogs: this.fastLogs })
}
if (this.tabIndex1 === 4) {
ArtisanTab208({
artisans: this.artisans,
onJoin: () => { this.showJoinSheet = true }
})
}
if (this.tabIndex1 === 5) {
MineTab208({ cloths: this.cloths, orders: this.orders, dipDays: this.dipDays })
}
}
.width('100%')
.padding(14)
}
.layoutWeight(1)
.align(Alignment.Top)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F0E8')
.bindSheet($$this.showJoinSheet, this.joinSheet208(), {
height: 620, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showPatternSheet, this.patternSheet208(), {
height: 600, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheet208(), {
height: 560, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showDelDialog, this.delDialog208(), {})
.bindContentCover($$this.showDetailDialog, this.detailDialog208(), {})
}
这里体现了"数据下传、事件上传"的经典模式。子Tab通过@Prop接收数据快照,通过回调函数(onStep、onJoin、onAdd、onDetail、onEdit、onDel、onStar)将用户意图回传主入口。主入口在回调中执行真正的状态修改,保证数据源唯一。以onStar为例,点击星标时子组件只上报索引i,主入口用map生成全新数组:命中索引的项starred取反,其余项原样返回。这种不可变更新让ArkTS的状态观察能精准识别"引用变更",触发刷新。
onEdit的回填逻辑尤其值得学习。打开编辑弹层前,主入口先把目标行的name、dips、fastness、starred拷贝到edit*临时态,再置showEditSheet = true。这样弹层首次渲染就读到正确初始值,避免"先开空表单再异步填充"的闪烁。onStep则用map翻转指定步骤的done,让染布六步曲可点击切换完成态,进度可视化。
$$this.showJoinSheet是双向绑定语法,bindSheet内部修改该布尔(如下拉关闭)会反向写回@State,无需手动同步。这是HarmonyOS对弹层状态管理的原生支持,比手动监听关闭事件再置false简洁得多。
弹层分两类:bindSheet挂载底部抽屉(预约、上传、编辑三个表单),可设高度、拖拽条、背景色;bindContentCover挂载全屏遮罩(删除确认、花色详情两个对话框),居中展示。两类容器都不占用主滚动区位置,而是浮于其上,开关由布尔状态驱动,ArkTS自动管理显隐动画。
八、预约连麦共创抽屉:表单交互全流程
预约抽屉是业务最重的弹层,包含花色选择、坯布选择、浸染次数、直播开关、留样开关、费用计算六个表单元素。
@Builder
joinSheet208() {
Column({ space: 16 }) {
Row({ space: 10 }) {
Column().width(4).height(30).borderRadius(2).backgroundColor('#283593')
Column({ space: 2 }) {
Text('预约连麦共创').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('和靛缸阿婆同缸染布 · 出布可留样').fontSize(10).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('✕').fontSize(14).fontColor('#5C6BC0')
}
.width(30).height(30).borderRadius(15)
.backgroundColor('#E8EAF6').justifyContent(FlexAlign.Center)
.onClick(() => { this.showJoinSheet = false })
}
.width('100%')
Scroll() {
Column({ space: 16 }) {
// 目标花色 - 标签选择
Column({ space: 8 }) {
Text('目标花色').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(dyeTags208, (tag: string, i: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.joinPattern === i ? '#FFFFFF' : '#5C6BC0')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.joinPattern === i ? dyeColor208(tag) : '#E8EAF6')
.margin(4)
.onClick(() => { this.joinPattern = i })
}, (tag: string) => tag)
}
}
.alignItems(HorizontalAlign.Start).width('100%')
// 坯布选择
Column({ space: 8 }) {
Text('坯布选择').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(fabricTags208, (tag: string, i: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.joinFabric === i ? '#FFFFFF' : '#5C6BC0')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.joinFabric === i ? '#E53935' : '#E8EAF6')
.margin(4)
.onClick(() => { this.joinFabric = i })
}, (tag: string) => tag)
}
}
.alignItems(HorizontalAlign.Start).width('100%')
// 浸染次数步进器
Row({ space: 14 }) {
Column() { Text('-').fontSize(16).fontColor('#283593') }
.width(34).height(34).borderRadius(17)
.backgroundColor('#E8EAF6').justifyContent(FlexAlign.Center)
.onClick(() => { if (this.joinDips > 1) { this.joinDips -= 1 } })
Column({ space: 2 }) {
Text('浸染 ' + this.joinDips + ' 次').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('次数越多颜色越深').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() { Text('+').fontSize(16).fontColor('#283593') }
.width(34).height(34).borderRadius(17)
.backgroundColor('#E8EAF6').justifyContent(FlexAlign.Center)
.onClick(() => { if (this.joinDips < 6) { this.joinDips += 1 } })
Text('').layoutWeight(1)
}
.width('100%')
// 费用计算
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('预计花费').fontSize(13).fontColor('#283593')
Text('含坯布与染材费').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Text('¥ ' + (this.joinDips * 22 + (this.joinKeep ? 15 : 0)))
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C62828')
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#E8EAF6')
Button() {
Text('确认预约连染').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%').height(48).borderRadius(24).backgroundColor('#283593')
.onClick(() => { this.showJoinSheet = false })
}
.width('100%').padding({ left: 18, right: 18, top: 4, bottom: 24 })
}
.constraintSize({ maxHeight: 460 })
}
.width('100%').height('100%').padding({ top: 14 }).backgroundColor('#FFFFFF')
}
标签选择用Flex({ wrap: FlexWrap.Wrap })实现自动换行的胶囊群,选中态用染材对应色填充(dyeColor208(tag)),未选中用浅靛底。这种"选中即着色"的反馈让用户直观感知所选染材的色相。浸染次数是自定义步进器:左右两个圆形按钮加减,中间显示当前值与提示文案,加减都有边界保护(1到6次),防止越界。
费用计算是响应式的:this.joinDips * 22 + (this.joinKeep ? 15 : 0),浸染次数每加一次加22元,勾选留样布再加15元。由于joinDips和joinKeep都是@State,任一变化都触发这段Text重算,实现价格实时联动。整个抽屉用Scroll包裹内容区,constraintSize({ maxHeight: 460 })限制最大高度,超出则内部滚动,避免抽屉过高遮挡主页面。
自定义开关(直播共创、留样布纪念)用
Row + Circle模拟iOS风格拨动开关:外层Row是轨道、内层Circle是滑块,backgroundColor随状态在主题色与灰色间切换。这是HarmonyOS中未用Toggle组件时的标准实现范式。
九、编辑与删除弹层:不可变更新实践
编辑抽屉保存时用map回写,删除对话框确认时用filter移除,二者体现了ArkTS中数组操作的核心范式。
// 编辑保存 - map回写
Button() {
Text('保存修改').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%').height(48).borderRadius(24).backgroundColor('#00897B')
.onClick(() => {
this.cloths = this.cloths.map((c: Cloth208, ci: number) => {
if (ci === this.editIndex) {
return {
id: c.id,
name: this.editName === '' ? c.name : this.editName,
dye: c.dye,
dips: this.editDips,
fastness: this.editFast * 18,
state: c.state,
watchers: c.watchers,
starred: this.editTop
}
}
return c
})
this.showEditSheet = false
})
// 删除确认 - filter移除
Button() {
Text('确认下架').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.layoutWeight(1).height(44).borderRadius(22).backgroundColor('#C62828')
.onClick(() => {
this.cloths = this.cloths.filter((c: Cloth208, ci: number) => ci !== this.delIndex)
this.showDelDialog = false
})
map回写时,命中索引的项返回全新对象(editName为空则保留原名、fastness由editFast * 18换算为百分制),未命中项原样返回。这种"部分替换"模式保证只有被编辑行获得新引用,其余行引用不变,ArkTS的Diff能精准识别最小变更范围。filter移除则生成排除目标索引的新数组,长度减一。
注意编辑保存时
fastness: this.editFast * 18这一行——弹层里色牢度是1到5级的整数,但花色册存储的是百分制,这里做了×18的近似换算。这种"录入态与存储态分离"的设计,让用户用熟悉的1-5级操作,底层用百分制存储精度,是表单设计中的常见妥协。
删除对话框还提供"保留染样册"选项(delKeepAlbum开关),即使下架花色卡,染样册仍可保留历史记录。这种"软删除"语义在非遗场景中尤其重要——作品可以下架但工艺档案不能丢失,体现了对非遗资产的保护态度。
十、花色详情对话框:图表与跳转联动
详情对话框是信息密度最高的弹层,包含指标卡、七日柱图、工艺档案、跳转预约四个区块。
@Builder
detailDialog208() {
Column({ space: 0 }) {
Scroll() {
Column({ space: 0 }) {
// 头部渐变区
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() {
Text('🎨').fontSize(30)
}
.width(56).height(56).borderRadius(28)
.backgroundColor('#FFFFFF').justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(this.cloths[this.detailIndex].name)
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(this.cloths[this.detailIndex].dye + ' · 复染 '
+ this.cloths[this.detailIndex].dips + ' 次')
.fontSize(11).fontColor('#E8EAF6')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() { Text('✕').fontSize(13).fontColor('#FFFFFF') }
.width(28).height(28).borderRadius(14)
.backgroundColor('rgba(255,255,255,0.2)')
.justifyContent(FlexAlign.Center)
.onClick(() => { this.showDetailDialog = false })
}
.width('100%')
}
.width('100%').padding(18)
.linearGradient({ angle: 135, colors: [['#3949AB', 0], ['#C62828', 1]] })
// 三指标卡
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(this.cloths[this.detailIndex].fastness + '%')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('色牢度评分').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('#E8EAF6')
// ... 观看人次、当前状态
}
.width('100%')
// 近7日跟染人数柱图
Column({ space: 8 }) {
Text('近 7 日跟染人数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row({ space: 6 }) {
ForEach(this.dipDays, (d: DipDay208) => {
Column({ space: 4 }) {
Column()
.width(16)
.height(d.dips * 5)
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({ angle: 180, colors: [['#5C6BC0', 0], ['#283593', 1]] })
Text(d.day).fontSize(8).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
}, (d: DipDay208) => ('d' + d.day))
}
.width('100%').alignItems(VerticalAlign.Bottom).height(90)
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#F5F5F5')
// 跳转预约
Button() {
Text('预约连染同款花色').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%').height(44).borderRadius(22).backgroundColor('#C62828')
.onClick(() => {
this.showDetailDialog = false
this.showJoinSheet = true
})
}
.width('100%')
}
.constraintSize({ maxHeight: 480 })
}
.width('86%').borderRadius(20).backgroundColor('#FFFFFF').clip(true)
}
柱图的实现是纯ArkTS声明式:每根柱子是一个Column,高度由d.dips * 5动态计算(浸染批次乘5像素),底色用linearGradient从浅靛到深靛渐变模拟布条下垂。Row({ space: 6 })水平排列七根柱子,alignItems(VerticalAlign.Bottom)让所有柱子底对齐,形成标准柱状图效果。这种"用Column高度做数据可视化"的手法在HarmonyOS中无需引入图表库即可实现轻量图表。
详情对话框最巧妙的是跳转联动:点击"预约连染同款花色"时,先showDetailDialog = false关闭详情,再showJoinSheet = true打开预约抽屉。两个弹层状态的串行切换实现了"看中某个花色→立即预约同款"的业务闭环。.clip(true)配合borderRadius(20)让内部渐变头部被圆角裁剪,避免方角溢出。
十一、Tab1染坊直播:四机位与工艺步骤
LiveTab208是直播Tab,用Grid布局呈现四机位,配合染布六步曲与弹幕互动。
@Component
struct LiveTab208 {
@State localMic: boolean = true
@State localCam: boolean = true
@State localStir: boolean = true
@Prop cloths: Cloth208[] = []
@Prop artisans: Artisan208[] = []
@Prop dipSteps: DipStep208[] = []
@Prop barrages: Barrage208[] = []
onStep: (i: number) => void = () => {}
onJoin: () => void = () => {}
build() {
Column({ space: 12 }) {
// 四机位网格
Grid() {
GridItem() {
Column({ space: 4 }) {
Row({ space: 6 }) {
Column() { Text('🪣').fontSize(18) }
.width(34).height(34).borderRadius(17)
.backgroundColor('rgba(255,255,255,0.25)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text('主镜 · 靛缸浸染').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('靛缸阿婆 · 老缸').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Start)
}
.width('100%').padding(10)
Text('').layoutWeight(1)
Row({ space: 6 }) {
Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text(dippingClothCount208(this.cloths) + ' 匝布在缸').fontSize(9).fontColor('#C5CAE9')
}
.width('100%').padding(8)
}
.width('100%').height('100%').borderRadius(12).padding(6)
.linearGradient({ angle: 150, colors: [['#283593', 0], ['#1A237E', 1]] })
.onClick(() => { this.onJoin() })
}
// 氧化晾晒位、拆线特写位、我的工位...
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.height(240).width('100%')
// 工具条
Row({ space: 10 }) {
Column({ space: 3 }) {
Text(this.localMic ? '🎙️' : '🔇').fontSize(18)
Text('对讲').fontSize(9).fontColor('#283593')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 }).borderRadius(12)
.backgroundColor(this.localMic ? '#C5CAE9' : '#E8EAF6')
.onClick(() => { this.localMic = !this.localMic })
// 镜头、搅缸、连染...
}
.width('100%')
// 染布六步曲
Column({ space: 10 }) {
Text('染布六步曲').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.dipSteps, (s: DipStep208, i: number) => {
Row({ space: 10 }) {
Column() {
Text(s.done ? '✓' : (i + 1) + '').fontSize(12)
.fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width(24).height(24).borderRadius(12)
.backgroundColor(s.done ? '#7CB342' : '#9FA8DA')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(s.title).fontSize(12)
.fontColor(s.done ? '#1A237E' : '#5C6BC0').fontWeight(FontWeight.Bold)
Text(s.tip).fontSize(9).fontColor('#BDBDBD')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(s.done ? '已完成' : '进行中').fontSize(9)
.fontColor(s.done ? '#7CB342' : '#C62828')
}
.width('100%').padding(10).borderRadius(10)
.backgroundColor(s.done ? '#F1F8E9' : '#FFFFFF')
.onClick(() => { this.onStep(i) })
}, (s: DipStep208) => ('s' + s.id))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
// 弹幕
Column({ space: 6 }) {
Text('围染弹幕').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#283593')
ForEach(this.barrages, (b: Barrage208, i: number) => {
Row({ space: 6 }) {
Text('#' + (i + 1)).fontSize(8).fontColor('#C62828')
Text(b.text).fontSize(10).fontColor('#37474F')
}
.width('100%').padding(6).borderRadius(8)
.backgroundColor(i % 2 === 0 ? '#E8EAF6' : '#FBE9E7')
}, (b: Barrage208) => ('b' + b.id))
}
.width('100%').padding(10).borderRadius(12).backgroundColor('#E8EAF6')
}
.width('100%')
}
}
四机位用Grid的columnsTemplate('1fr 1fr')和rowsTemplate('1fr 1fr')切成2×2网格,分别是主镜(靛缸浸染,靛蓝渐变)、氧化晾晒位(青绿渐变)、拆线特写位(红色渐变,“开盲盒时刻”)、我的工位(摄像头开关)。每个机位卡片用linearGradient铺底,左上角圆形图标+机位名称,右下角状态标签(LIVE、氧化中、围观数等)。主镜卡片点击触发onJoin预约连染。
四机位的色彩分配并非随意:主镜靛蓝对应浸染、氧化位青绿对应氧化显色、拆线位红色对应成品的茜草红、我的工位靛紫是用户色。四块卡片的渐变色就是扎染全流程的色相演变图,用户看一眼就理解了工艺脉络。
染布六步曲用ForEach渲染步骤列表,每步左侧圆形序号(完成显示✓绿底、未完成显示序号灰底),中间标题与提示,右侧状态标签。点击步骤触发onStep(i)回调,主入口翻转该步done状态,实现工艺进度可交互标记。弹幕列表用奇偶交替背景色(靛底/红底)增强可读性,每条弹幕前缀红色序号,模拟直播平台的弹幕流。
localMic、localCam、localStir三个@State是LiveTab的本地状态,不涉及主入口数据,因此放在子组件内自治。麦克风、摄像头、搅缸三个工具按钮根据这些状态切换图标与背景色,点击翻转布尔值。这种"本地交互态留在子组件"的分层,避免把所有状态都堆到主入口,保持各组件的职责清晰。
十二、Tab2花色册:增删改查与堆叠条
PatternTab208是花色册的管理中枢,提供统计卡、上传入口、染材占比堆叠条与花色列表的四联操作。
@Component
struct PatternTab208 {
@Prop cloths: Cloth208[] = []
onAdd: () => void = () => {}
onDetail: (i: number) => void = () => {}
onEdit: (i: number) => void = () => {}
onDel: (i: number) => void = () => {}
onStar: (i: number) => void = () => {}
build() {
Column({ space: 12 }) {
// 统计卡
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(this.cloths.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#C62828')
Text('馆藏花色').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('#FBE9E7')
Column({ space: 2 }) {
Text(starredClothCount208(this.cloths) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('精选标记').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('#E8EAF6')
Column({ space: 2 }) {
Text(dippingClothCount208(this.cloths) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#00897B')
Text('在缸/氧化').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('#E0F2F1')
}
.width('100%')
// 染材占比堆叠条
Column({ space: 8 }) {
Text('馆藏染材构成').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row() {
ForEach(dyeCounts208(this.cloths), (c: DyeCount208) => {
Row() {
Text(c.label + ' ' + c.count).fontSize(8).fontColor('#FFFFFF')
}
.width('100%').justifyContent(FlexAlign.Center).backgroundColor(c.color)
}, (c: DyeCount208) => (c.label + c.count))
}
.width('100%').height(22).borderRadius(11).clip(true)
Row({ space: 10 }) {
ForEach(dyeCounts208(this.cloths), (c: DyeCount208) => {
Row({ space: 4 }) {
Column().width(8).height(8).borderRadius(4).backgroundColor(c.color)
Text(c.label).fontSize(9).fontColor('#5C6BC0')
}
}, (c: DyeCount208) => ('lg' + c.label))
}
.width('100%')
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
// 花色列表
ForEach(this.cloths, (c: Cloth208, i: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() { Text('🎨').fontSize(20) }
.width(44).height(44).borderRadius(12)
.backgroundColor('#E8EAF6').justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(c.name).fontSize(13).fontColor('#1A237E').fontWeight(FontWeight.Bold)
Row({ space: 6 }) {
Text(c.dye).fontSize(8).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(dyeColor208(c.dye))
Text('复染 ' + c.dips + ' 次').fontSize(9).fontColor('#5C6BC0')
Text('围观 ' + c.watchers).fontSize(9).fontColor('#BDBDBD')
}
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() { Text(c.starred ? '⭐' : '☆').fontSize(18) }
.width(32).height(32).justifyContent(FlexAlign.Center)
.onClick(() => { this.onStar(i) })
}
.width('100%')
Row() {
Row({ space: 4 }) {
Text('色牢度').fontSize(9).fontColor('#5C6BC0')
Text(c.fastness + '%').fontSize(10).fontColor('#C62828').fontWeight(FontWeight.Bold)
}
Row({ space: 4 }) {
Text('状态').fontSize(9).fontColor('#5C6BC0')
Text(c.state).fontSize(10).fontColor(clothStateColor208(c.state)).fontWeight(FontWeight.Bold)
}
.margin({ left: 14 })
Text('').layoutWeight(1)
Column() { Text('详情').fontSize(10).fontColor('#FFFFFF') }
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10)
.backgroundColor('#283593')
.onClick(() => { this.onDetail(i) })
Column() { Text('编辑').fontSize(10).fontColor('#283593') }
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10)
.backgroundColor('#E8EAF6').margin({ left: 6 })
.onClick(() => { this.onEdit(i) })
Column() { Text('下架').fontSize(10).fontColor('#C62828') }
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10)
.backgroundColor('#FBE9E7').margin({ left: 6 })
.onClick(() => { this.onDel(i) })
}
.width('100%')
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
}, (c: Cloth208) => ('c' + c.id + c.state))
}
.width('100%')
}
}
统计卡用三个layoutWeight(1)等分的色块呈现馆藏总数、精选标记数、在缸/氧化数,数字颜色与背景色语义对应(红/蓝/绿)。堆叠条是亮点:dyeCounts208(this.cloths)返回各染材的计数,外层Row水平排列每个染材的色块,每个色块width('100%')但被Flex按比例分配实际宽度(因为多个子项平分),实现按占比的堆叠效果。.clip(true)配合borderRadius(11)让两端色块被圆角裁剪。下方图例用小圆点+标签对应色块。
花色列表每行包含图标、名称、染材标签(用dyeColor208着色)、复染次数、围观数、星标按钮、色牢度、状态标签,以及详情/编辑/下架三个操作按钮。星标点击触发onStar(i),三个操作按钮分别触发onDetail、onEdit、onDel回调。ForEach的键是'c' + c.id + c.state,包含id与状态,当某条花色状态变更时键也变更,框架识别为新项重新渲染,确保状态色标签即时更新。
三个操作按钮的颜色编码遵循固定语义:详情用靛蓝(信息查看)、编辑用浅靛底深靛字(中性修改)、下架用红底红字(危险操作)。这种"色彩即语义"的按钮体系让用户在密集列表中也能凭色辨操作。
十三、Tab3染材库:热度条与柱图
DyeTab208展示草木染材档案,包含染材卡、人气热度横条、本周浸染批次柱图、染材用量占比堆叠条。
@Component
struct DyeTab208 {
@Prop dyes: Dye208[] = []
@Prop cloths: Cloth208[] = []
@Prop dipDays: DipDay208[] = []
build() {
Column({ space: 12 }) {
// 染材卡
Column({ space: 8 }) {
Text('草木染材架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.dyes, (d: Dye208) => {
Row({ space: 10 }) {
Column() { Text('🌿').fontSize(18) }
.width(40).height(40).borderRadius(10)
.backgroundColor(d.color + '22').justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(d.name).fontSize(12).fontColor('#1A237E').fontWeight(FontWeight.Bold)
Column().width(10).height(10).borderRadius(5).backgroundColor(d.color)
}
Text('主产地 ' + d.origin + ' · 年产 ' + d.output + ' 缸')
.fontSize(9).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(d.heat + '°').fontSize(13).fontColor(d.color).fontWeight(FontWeight.Bold)
}
.width('100%').padding(10).borderRadius(10).backgroundColor('#F5F5F5')
}, (d: Dye208) => ('d' + d.id))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
// 染材热度横条
Column({ space: 10 }) {
Text('染材人气热度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.dyes, (d: Dye208) => {
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(d.name).fontSize(10).fontColor('#283593').width(52)
Row() {
Row()
.width(d.heat + '%')
.height(10).borderRadius(5)
.linearGradient({ angle: 0, colors: [[d.color, 0], ['#C62828', 1]] })
}
.layoutWeight(1).height(10).borderRadius(5).backgroundColor('#E8EAF6')
Text(d.heat + '').fontSize(9).fontColor('#C62828').fontWeight(FontWeight.Bold)
}
.width('100%')
}
.width('100%')
}, (d: Dye208) => ('heat' + d.id))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
// 本周浸染批次柱图
Column({ space: 8 }) {
Text('本周每日浸染批次').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row({ space: 6 }) {
ForEach(this.dipDays, (d: DipDay208) => {
Column({ space: 4 }) {
Text(d.dips + '').fontSize(8).fontColor('#283593')
Column()
.width(18)
.height(d.dips * 5)
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({ angle: 180, colors: [['#5C6BC0', 0], ['#1A237E', 1]] })
Text(d.day).fontSize(8).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
}, (d: DipDay208) => ('dd' + d.day))
}
.width('100%').alignItems(VerticalAlign.Bottom).height(100)
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
}
}
染材卡用d.color + '22'生成带透明度的色块背景(22是十六进制透明度,约13%),让染材图标底色与染材本色对应但更柔和。人气热度横条是进度条变体:外层Row是灰色轨道,内层Row宽度为d.heat + '%',用linearGradient从染材色渐变到红色,表示热度越高越偏红。柱图与详情对话框中的柱图实现一致,d.dips * 5为像素高度,底对齐排列。
d.color + '22'这种字符串拼接透明度的写法是HarmonyOS中16进制色值+Alpha的惯用法。#28359322表示靛蓝带13%透明度,比rgba(40,53,147,0.13)更简洁,且与d.color同源便于复用。
十四、Tab4坯布架与Tab5匠人团
坯布架提供库存列表与色牢度走势柱图,匠人团提供在线统计与人气榜。
@Component
struct FabricTab208 {
@Prop fabrics: Fabric208[] = []
@Prop fastLogs: FastLog208[] = []
@State pickIndex: number = -1
build() {
Column({ space: 12 }) {
// 统计卡:在架坯布、本周耗用、平均色牢度
Row({ space: 8 }) { /* ... */ }
.width('100%')
// 坯布列表
Column({ space: 8 }) {
Text('坯布货架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.fabrics, (f: Fabric208, i: number) => {
Row({ space: 10 }) {
Column() { Text(f.icon).fontSize(18) }
.width(40).height(40).borderRadius(10).backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(f.name).fontSize(12).fontColor('#1A237E').fontWeight(FontWeight.Bold)
Text('库存 ' + f.stock + ' 匹 · 累计用量 ' + f.times + ' 匹')
.fontSize(9).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text(this.pickIndex === i ? '已选定' : (f.used ? '常用' : '备选')).fontSize(10)
.fontColor(this.pickIndex === i ? '#FFFFFF' : (f.used ? '#283593' : '#BDBDBD'))
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10)
.backgroundColor(this.pickIndex === i ? '#C62828' : (f.used ? '#E8EAF6' : '#F5F5F5'))
.onClick(() => { this.pickIndex = i })
}
.width('100%').padding(10).borderRadius(10)
.backgroundColor(this.pickIndex === i ? '#FBE9E7' : '#FAFAFA')
}, (f: Fabric208) => ('t' + f.id + this.pickIndex))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
// 色牢度走势柱图
Column({ space: 8 }) {
Text('近 7 日成品色牢度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row({ space: 6 }) {
ForEach(this.fastLogs, (l: FastLog208) => {
Column({ space: 4 }) {
Text(l.score + '').fontSize(8).fontColor('#283593')
Column()
.width(16)
.height(l.score)
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({ angle: 180, colors: [['#80CBC4', 0], ['#00695C', 1]] })
Text(l.day).fontSize(8).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
}, (l: FastLog208) => ('u' + l.day))
}
.width('100%').alignItems(VerticalAlign.Bottom).height(110)
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
}
}
@Component
struct ArtisanTab208 {
@Prop artisans: Artisan208[] = []
@State followIndex: number = -1
onJoin: () => void = () => {}
build() {
Column({ space: 12 }) {
// 在线匠人统计 + 去连染
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() { Text('🧕').fontSize(24) }
.width(48).height(48).borderRadius(24).backgroundColor('#FBE9E7')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('在线匠人 ' + onlineArtisanCount208(this.artisans) + ' / ' + this.artisans.length)
.fontSize(14).fontColor('#1A237E').fontWeight(FontWeight.Bold)
Text('连麦共创 · 同缸染布').fontSize(10).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() { Text('去连染').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.padding({ left: 12, right: 12, top: 7, bottom: 7 }).borderRadius(14)
.backgroundColor('#C62828')
.onClick(() => { this.onJoin() })
}
.width('100%')
}
.width('100%').padding(14).borderRadius(14).backgroundColor('#FFFFFF')
// 匠人人气榜
Column({ space: 10 }) {
Text('匠人人气榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.artisans, (a: Artisan208, i: number) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text((i + 1) + '').fontSize(13)
.fontColor(i < 3 ? '#C62828' : '#BDBDBD').fontWeight(FontWeight.Bold)
}
.width(22).justifyContent(FlexAlign.Center)
Column() { Text(a.name.slice(0, 1)).fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.width(40).height(40).borderRadius(20)
.backgroundColor(dyeColor208(dyeTags208[i % dyeTags208.length]))
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(a.name).fontSize(12).fontColor('#1A237E').fontWeight(FontWeight.Bold)
if (a.online) {
Column() { Text('在线').fontSize(8).fontColor('#7CB342') }
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor('#F1F8E9')
}
}
Text(a.city + ' · 染龄 ' + a.years + ' 年').fontSize(9).fontColor('#5C6BC0')
Row({ space: 6 }) {
Row() {
Row()
.width((a.heat / maxArtisanHeat208(this.artisans) * 100) + '%')
.height(8).borderRadius(4)
.linearGradient({ angle: 0, colors: [['#EF9A9A', 0], ['#C62828', 1]] })
}
.width(90).height(8).borderRadius(4).backgroundColor('#E8EAF6').clip(true)
Text('人气 ' + a.heat).fontSize(8).fontColor('#C62828')
}
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text(this.followIndex === i ? '已关注' : '关注').fontSize(10)
.fontColor(this.followIndex === i ? '#FFFFFF' : '#C62828')
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
.backgroundColor(this.followIndex === i ? '#C62828' : '#FBE9E7')
.onClick(() => { this.followIndex = i })
}
.width('100%').padding(10).borderRadius(10).backgroundColor('#FAFAFA')
}, (a: Artisan208) => ('a' + a.id + this.followIndex))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
}
}
坯布架的pickIndex是子组件本地状态,点击坯布行切换选中态,选中行背景变红、标签变"已选定"。ForEach的键't' + f.id + this.pickIndex包含pickIndex,确保选中态变化时对应行重新渲染。色牢度走势柱图用青绿色渐变(#80CBC4到#00695C),与染材库的靛蓝柱图色彩区分,让不同Tab的图表有各自的色相身份。
匠人榜的排名前三序号用红色突出,其余灰色。头像用a.name.slice(0, 1)取姓名首字,背景色用dyeColor208(dyeTags208[i % dyeTags208.length])循环取染材色,让六位匠人的头像底色恰好对应六种染材,形成"一位匠人一种染材"的视觉隐喻。人气进度条宽度是a.heat / maxArtisanHeat208(this.artisans) * 100 + '%',相对最高人气归一化,最高者满格。
十五、Tab6我的:个人卡与设置开关
MineTab208是用户中心,包含个人渐变卡、本周浸染柱图、共创单列表、三个设置开关。
@Component
struct MineTab208 {
@Prop cloths: Cloth208[] = []
@Prop orders: Order208[] = []
@Prop dipDays: DipDay208[] = []
@State nightDip: boolean = false
@State notifyOpen: boolean = true
@State autoAlbum: boolean = false
build() {
Column({ space: 12 }) {
// 个人卡 - 渐变
Column({ space: 10 }) {
Row({ space: 12 }) {
Column() { Text('染').fontSize(20).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.width(54).height(54).borderRadius(27)
.backgroundColor('rgba(255,255,255,0.25)').justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('染友 · 靛蓝学徒').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('共创 26 次 · 出作品 18 件 · 上榜 3 次').fontSize(10).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('🪣').fontSize(22)
}
.width('100%')
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(starredClothCount208(this.cloths) + '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E8EAF6')
Text('精选作品').fontSize(9).fontColor('#C5CAE9')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('980').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E8EAF6')
Text('染币余额').fontSize(9).fontColor('#C5CAE9')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('Lv.4').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E8EAF6')
Text('染友等级').fontSize(9).fontColor('#C5CAE9')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 10 })
}
.width('100%').padding(16).borderRadius(16)
.linearGradient({ angle: 135, colors: [['#3949AB', 0], ['#C62828', 1]] })
// 我的共创单
Column({ space: 8 }) {
Text('我的共创单').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
ForEach(this.orders, (o: Order208, i: number) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('D' + o.dayNum).fontSize(11).fontColor('#C62828').fontWeight(FontWeight.Bold)
Text(o.dye).fontSize(8).fontColor('#BDBDBD')
}
.width(38).alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text(o.name).fontSize(11).fontColor('#1A237E').fontWeight(FontWeight.Bold)
Text('扎花 ' + o.bundles + ' 匝 · 布量 ' + o.meters + ' 米').fontSize(9).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (o.top) {
Column() { Text('置顶').fontSize(8).fontColor('#C62828') }
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor('#FBE9E7')
}
Text(o.state).fontSize(9).fontColor(orderStateColor208(o.state)).fontWeight(FontWeight.Bold)
}
.width('100%').padding(8).borderRadius(8)
.backgroundColor(i % 2 === 0 ? '#FAFAFA' : '#E8EAF6')
}, (o: Order208) => ('mo' + o.id))
}
.width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start)
// 设置开关
Column({ space: 0 }) {
Row({ space: 10 }) {
Text('🌙').fontSize(16)
Column({ space: 2 }) {
Text('夜染模式').fontSize(12).fontColor('#1A237E')
Text('夜间连染自动补光').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Row() { Circle().width(16).height(16).fill('#FFFFFF') }
.width(44).height(24).borderRadius(12).justifyContent(FlexAlign.Center)
.backgroundColor(this.nightDip ? '#C62828' : '#BDBDBD')
.onClick(() => { this.nightDip = !this.nightDip })
}
.width('100%').padding(12)
// 开缸开播提醒、自动存染样册...
}
.width('100%').borderRadius(14).backgroundColor('#FFFFFF')
}
.width('100%')
}
}
个人卡用linearGradient从靛蓝渐变到红色(#3949AB到#C62828),与详情对话框头部渐变一致,形成"品牌色"统一识别。卡片内三指标(精选作品、染币余额、染友等级)等分排列,白色文字在渐变底上高对比可读。共创单列表每行左侧D + dayNum表示第几日开染,右侧状态标签用orderStateColor208着色,置顶订单额外显示红色"置顶"标签。三个设置开关(夜染模式、开缸提醒、自动存样册)都是本地@State布尔,点击翻转,轨道色随状态在红色与灰色间切换。
业务流程图
下面用流程图梳理从用户进入应用到完成一次连麦共创的完整链路。
技术点对比表
| 技术点 | 实现方式 | 适用场景 | 优势 | 注意事项 |
|---|---|---|---|---|
| @State | 组件内可变状态 | 主入口数据、本地开关 | 自动触发UI刷新 | 引用变更才触发观察,需不可变更新 |
| @Prop | 父到子单向传递 | 子Tab接收数据快照 | 隔离子组件误改 | 只读,修改需回调上报 |
| @Builder | UI结构复用函数 | 头部、导航、弹层 | 降低build嵌套深度 | 内部this指向宿主组件 |
| bindSheet | 底部抽屉容器 | 表单录入(预约/上传/编辑) | 原生拖拽、高度可设 | 需$$双向绑定开关 |
| bindContentCover | 全屏遮罩容器 | 对话框(删除/详情) | 居中展示、不占位 | 内容需自带圆角裁剪 |
| ForEach键生成 | 第三参数key | 列表渲染 | 精准Diff、最小重渲染 | 键需稳定唯一,含易变字段 |
| map不可变更新 | 生成新数组 | 编辑/星标/步骤切换 | 引用变更触发观察 | 返回新对象,勿原地修改 |
| filter不可变更新 | 排除目标项 | 删除操作 | 长度自动减一 | 索引基准可能偏移,用ci比较 |
| linearGradient | 渐变背景 | 头部、机位、柱图 | 视觉层次丰富 | angle控制光线方向 |
| layoutWeight(1)空Text | 弹性占位 | 两端对齐布局 | 细粒度空间分配 | 空文本不占视觉但占布局 |
| Column高度做图表 | 数据可视化 | 柱图、进度条 | 无需图表库 | 像素值需手动换算 |
| Circle+Row模拟开关 | 自定义Toggle | 设置项、表单开关 | 视觉风格统一 | 非原生Toggle,无障碍支持弱 |
| 纯函数聚合 | 无副作用计算 | 统计指标、占比 | 幂等可复用 | 避免依赖外部可变状态 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// ============================================================
// 场景:扎染匠人连麦共创 / 染缸浸布直播围观
// 配色:靛蓝 #283593 × 茜草红 #E53935 × 布白 #F5F0E8
// Tab 布局:顶部「晾布夹」导航(横向晾杆 + 衣夹 + 错落悬挂布条)
// ============================================================
interface DipDay208 {
day: string
dips: number
}
interface Cloth208 {
id: number
name: string
dye: string
dips: number
fastness: number
state: string
watchers: number
starred: boolean
}
interface Dye208 {
id: number
name: string
color: string
origin: string
output: number
heat: number
}
interface Order208 {
id: number
name: string
dye: string
dayNum: number
bundles: number
meters: number
state: string
top: boolean
}
interface Artisan208 {
id: number
name: string
city: string
years: number
online: boolean
heat: number
}
interface Barrage208 {
id: number
text: string
}
interface DipStep208 {
id: number
title: string
tip: string
done: boolean
}
interface DyeCount208 {
label: string
count: number
color: string
}
interface FastLog208 {
day: string
score: number
}
interface Fabric208 {
id: number
name: string
icon: string
stock: number
used: boolean
times: number
}
const dyeTags208: string[] = ['板蓝根', '茜草', '栀子', '苏木', '薯莨']
const fabricTags208: string[] = ['棉麻方巾', '真丝长巾', '帆布袋']
function clothStateColor208(state: string): string {
if (state === '浸染中') {
return '#283593'
}
if (state === '氧化中') {
return '#00897B'
}
if (state === '已晾干') {
return '#E53935'
}
return '#90A4AE'
}
function orderStateColor208(state: string): string {
if (state === '今日开缸') {
return '#E53935'
}
if (state === '氧化等待') {
return '#00897B'
}
if (state === '已交付') {
return '#7CB342'
}
return '#90A4AE'
}
function dyeColor208(dye: string): string {
if (dye === '板蓝根') {
return '#283593'
}
if (dye === '茜草') {
return '#E53935'
}
if (dye === '栀子') {
return '#FBC02D'
}
if (dye === '苏木') {
return '#8D6E63'
}
return '#C62828'
}
function starredClothCount208(cloths: Cloth208[]): number {
let n: number = 0
for (let i = 0; i < cloths.length; i++) {
if (cloths[i].starred) {
n++
}
}
return n
}
function dippingClothCount208(cloths: Cloth208[]): number {
let n: number = 0
for (let i = 0; i < cloths.length; i++) {
if (cloths[i].state === '浸染中' || cloths[i].state === '氧化中') {
n++
}
}
return n
}
function onlineArtisanCount208(artisans: Artisan208[]): number {
let n: number = 0
for (let i = 0; i < artisans.length; i++) {
if (artisans[i].online) {
n++
}
}
return n
}
function maxArtisanHeat208(artisans: Artisan208[]): number {
let m: number = 1
for (let i = 0; i < artisans.length; i++) {
if (artisans[i].heat > m) {
m = artisans[i].heat
}
}
return m
}
function dyeCounts208(cloths: Cloth208[]): DyeCount208[] {
const counts: DyeCount208[] = []
for (let i = 0; i < dyeTags208.length; i++) {
let n: number = 0
for (let j = 0; j < cloths.length; j++) {
if (cloths[j].dye === dyeTags208[i]) {
n++
}
}
counts.push({ label: dyeTags208[i], count: n, color: ['#283593', '#E53935', '#FBC02D', '#8D6E63', '#C62828'][i] })
}
return counts
}
// ================= 主页面 =================
@Entry
@Component
struct Index208 {
@State tabIndex1: number = 0
// 弹框开关
@State showJoinSheet: boolean = false
@State showPatternSheet: boolean = false
@State showEditSheet: boolean = false
@State showDelDialog: boolean = false
@State showDetailDialog: boolean = false
// 预约连麦共创表单
@State joinPattern: number = 0
@State joinFabric: number = 0
@State joinDips: number = 3
@State joinLive: boolean = true
@State joinKeep: boolean = false
// 上传花色表单
@State patternName: string = ''
@State patternDye: number = 0
@State patternDips: number = 3
@State patternFast: number = 4
@State patternPublic: boolean = true
// 编辑表单
@State editIndex: number = -1
@State editName: string = ''
@State editDips: number = 3
@State editFast: number = 4
@State editTop: boolean = false
// 删除
@State delIndex: number = -1
@State delKeepAlbum: boolean = true
// 详情
@State detailIndex: number = 0
// 数据
@State cloths: Cloth208[] = [
{ id: 1, name: '星空纹方巾', dye: '板蓝根', dips: 5, fastness: 92, state: '浸染中', watchers: 3120, starred: true },
{ id: 2, name: '鱼子缬手帕', dye: '茜草', dips: 3, fastness: 86, state: '氧化中', watchers: 1680, starred: false },
{ id: 3, name: '鹿胎缬桌旗', dye: '苏木', dips: 4, fastness: 78, state: '已晾干', watchers: 2240, starred: true },
{ id: 4, name: '蝴蝶花长巾', dye: '板蓝根', dips: 6, fastness: 90, state: '浸染中', watchers: 3560, starred: true },
{ id: 5, name: '栀子金茶席', dye: '栀子', dips: 2, fastness: 82, state: '氧化中', watchers: 1120, starred: false },
{ id: 6, name: '玛瑙纹帆布袋', dye: '茜草', dips: 4, fastness: 88, state: '已晾干', watchers: 2050, starred: false },
{ id: 7, name: '苍山雪围巾', dye: '板蓝根', dips: 3, fastness: 85, state: '浸染中', watchers: 1890, starred: true },
{ id: 8, name: '薯莨赤披肩', dye: '薯莨', dips: 6, fastness: 94, state: '已晾干', watchers: 4380, starred: true },
{ id: 9, name: '叠染云肩', dye: '苏木', dips: 5, fastness: 80, state: '氧化中', watchers: 1350, starred: false },
{ id: 10, name: '青花瓷纹餐垫', dye: '板蓝根', dips: 4, fastness: 87, state: '已晾干', watchers: 960, starred: false }
]
@State dyes: Dye208[] = [
{ id: 1, name: '板蓝根', color: '#283593', origin: '大理周城', output: 320, heat: 96 },
{ id: 2, name: '茜草', color: '#E53935', origin: '渭南', output: 210, heat: 88 },
{ id: 3, name: '栀子', color: '#FBC02D', origin: '岳阳', output: 160, heat: 82 },
{ id: 4, name: '苏木', color: '#8D6E63', origin: '凭祥', output: 140, heat: 77 },
{ id: 5, name: '薯莨', color: '#C62828', origin: '顺德', output: 95, heat: 90 }
]
@State orders: Order208[] = [
{ id: 1, name: '星空纹方巾共创单', dye: '板蓝根', dayNum: 2, bundles: 6, meters: 12, state: '今日开缸', top: true },
{ id: 2, name: '鱼子缬手帕拼单', dye: '茜草', dayNum: 1, bundles: 4, meters: 6, state: '氧化等待', top: false },
{ id: 3, name: '蝴蝶花长巾跟染', dye: '板蓝根', dayNum: 3, bundles: 8, meters: 16, state: '今日开缸', top: false },
{ id: 4, name: '栀子金茶席团染', dye: '栀子', dayNum: 1, bundles: 5, meters: 9, state: '氧化等待', top: false },
{ id: 5, name: '薯莨赤披肩定制', dye: '薯莨', dayNum: 4, bundles: 3, meters: 15, state: '已交付', top: false },
{ id: 6, name: '鹿胎缬桌旗跟染', dye: '苏木', dayNum: 2, bundles: 4, meters: 8, state: '已交付', top: false },
{ id: 7, name: '叠染云肩共创', dye: '苏木', dayNum: 5, bundles: 7, meters: 14, state: '氧化等待', top: false },
{ id: 8, name: '青花瓷纹餐垫拼单', dye: '板蓝根', dayNum: 1, bundles: 9, meters: 11, state: '已交付', top: false }
]
@State artisans: Artisan208[] = [
{ id: 1, name: '靛缸阿婆', city: '大理', years: 32, online: true, heat: 98 },
{ id: 2, name: '扎花阿姐', city: '自贡', years: 15, online: true, heat: 91 },
{ id: 3, name: '薯莨传人', city: '顺德', years: 21, online: false, heat: 89 },
{ id: 4, name: '茜草姑娘', city: '渭南', years: 8, online: true, heat: 80 },
{ id: 5, name: '缬蓝师傅', city: '南通', years: 18, online: false, heat: 86 },
{ id: 6, name: '栀子小哥', city: '岳阳', years: 6, online: true, heat: 70 }
]
@State dipDays: DipDay208[] = [
{ day: '周一', dips: 6 },
{ day: '周二', dips: 9 },
{ day: '周三', dips: 7 },
{ day: '周四', dips: 12 },
{ day: '周五', dips: 14 },
{ day: '周六', dips: 18 },
{ day: '周日', dips: 15 }
]
@State fastLogs: FastLog208[] = [
{ day: 'D1', score: 78 },
{ day: 'D2', score: 82 },
{ day: 'D3', score: 80 },
{ day: 'D4', score: 86 },
{ day: 'D5', score: 88 },
{ day: 'D6', score: 91 },
{ day: 'D7', score: 90 }
]
@State fabrics: Fabric208[] = [
{ id: 1, name: '棉麻方巾', icon: '🧵', stock: 24, used: true, times: 132 },
{ id: 2, name: '真丝长巾', icon: '丝绸', stock: 12, used: false, times: 76 },
{ id: 3, name: '帆布袋', icon: '👜', stock: 18, used: true, times: 58 },
{ id: 4, name: '棉布茶席', icon: '🍵', stock: 15, used: false, times: 64 },
{ id: 5, name: '亚麻围巾', icon: '🧣', stock: 9, used: false, times: 41 },
{ id: 6, name: '手帕坯', icon: '🪡', stock: 30, used: true, times: 118 },
{ id: 7, name: '束口袋', icon: '🎒', stock: 21, used: false, times: 37 },
{ id: 8, name: 'T 恤坯', icon: '👕', stock: 11, used: false, times: 29 }
]
@State barrages: Barrage208[] = [
{ id: 1, text: '出缸那一瞬间太惊艳了' },
{ id: 2, text: '靛蓝从绿变蓝的过程百看不厌' },
{ id: 3, text: '星空纹的留白好绝' },
{ id: 4, text: '第一次云扎染,拆线像开奖' },
{ id: 5, text: '阿婆的手法行云流水' },
{ id: 6, text: '这缸养了八年了吧' },
{ id: 7, text: '浸染 +1,记第五次' },
{ id: 8, text: '茜草红越晒越亮' }
]
@State dipSteps: DipStep208[] = [
{ id: 1, title: '设计捆扎', tip: '皮筋弹珠防染 · 想好留白', done: true },
{ id: 2, title: '养缸调液', tip: '看靛花 · 试染布条定浓淡', done: true },
{ id: 3, title: '下缸浸染', tip: '浸透 3-5 分钟 · 轻柔翻动', done: true },
{ id: 4, title: '氧化显色', tip: '出缸透氧 · 绿转蓝再复染', done: false },
{ id: 5, title: '拆线漂洗', tip: '剪皮筋 · 清水漂至无浮色', done: false },
{ id: 6, title: '晾晒定色', tip: '阴干固色 · 避暴晒褪色', done: false }
]
tabs208: string[] = ['染坊', '花色册', '染材库', '坯布架', '匠人团', '我的']
tabIcons208: string[] = ['🪣', '🎨', '🌿', '🧵', '🧕', '👤']
tabDrops208: number[] = [0, 6, 2, 8, 4, 10]
// ---------- 头部(电商非遗季风,无动画) ----------
@Builder
header208() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('织彩 · 云染坊').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('扎染匠人连麦共创 · 染缸浸布直播围观').fontSize(11).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column({ space: 2 }) {
Text('🪣').fontSize(20)
Text('1,526').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('围染席位').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 10 }) {
Column().width(4).height(34).borderRadius(2).backgroundColor('#E53935')
Column({ space: 3 }) {
Text('板蓝根老缸今晚开染').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('连麦共创抽留样布 · 围观送扎染方巾').fontSize(10).fontColor('#E8EAF6')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('去连染 →').fontSize(11).fontColor('#283593').fontWeight(FontWeight.Bold)
}
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#E8EAF6')
.onClick(() => {
this.showJoinSheet = true
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#1A237E')
}
.alignItems(HorizontalAlign.Start)
.padding(14)
.linearGradient({ angle: 140, colors: [['#3949AB', 0], ['#283593', 1]] })
}
// ---------- 顶部「晾布夹」tab ----------
@Builder
tabBar208() {
Column({ space: 0 }) {
// 晾杆
Row() {
Text('').layoutWeight(1)
}
.width('100%')
.height(2)
.backgroundColor('#B0BEC5')
.margin({ left: 10, right: 10 })
Row({ space: 4 }) {
ForEach(this.tabs208, (t: string, i: number) => {
Column({ space: 0 }) {
// 衣夹
Row() {
Column().width(3).height(7).borderRadius(1).backgroundColor(this.tabIndex1 === i ? '#E53935' : '#90A4AE')
}
.height(8)
.justifyContent(FlexAlign.Center)
// 悬挂布条
Column({ space: 2 }) {
Text(this.tabIcons208[i]).fontSize(13)
Text(t).fontSize(9).fontColor(this.tabIndex1 === i ? '#FFFFFF' : '#5C6BC0').maxLines(1)
}
.width(46)
.padding({ top: 5, bottom: 5 })
.alignItems(HorizontalAlign.Center)
.borderRadius({ bottomLeft: 8, bottomRight: 8 })
.backgroundColor(this.tabIndex1 === i ? '#283593' : '#FFFFFF')
.shadow(this.tabIndex1 === i ? { radius: 8, color: 'rgba(40,53,147,0.4)', offsetY: 3 } : { radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
}
.layoutWeight(1)
.padding({ top: 0, bottom: 4 })
.margin({ top: this.tabDrops208[i] })
.scale({ x: this.tabIndex1 === i ? 1.08 : 1, y: this.tabIndex1 === i ? 1.08 : 1 })
.animation({ duration: 180 })
.onClick(() => {
this.tabIndex1 = i
})
}, (t: string) => t)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.padding({ top: 2, left: 6, right: 6 })
}
.width('100%')
.padding({ top: 4, bottom: 4 })
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: 3 })
}
build() {
Column() {
this.header208()
this.tabBar208()
Scroll() {
Column({ space: 12 }) {
if (this.tabIndex1 === 0) {
LiveTab208({
cloths: this.cloths,
artisans: this.artisans,
dipSteps: this.dipSteps,
barrages: this.barrages,
onStep: (i: number) => {
this.dipSteps = this.dipSteps.map((s: DipStep208, si: number) => {
if (si === i) {
return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
}
return s
})
},
onJoin: () => {
this.showJoinSheet = true
}
})
}
if (this.tabIndex1 === 1) {
PatternTab208({
cloths: this.cloths,
onAdd: () => {
this.showPatternSheet = true
},
onDetail: (i: number) => {
this.detailIndex = i
this.showDetailDialog = true
},
onEdit: (i: number) => {
this.editIndex = i
this.editName = this.cloths[i].name
this.editDips = this.cloths[i].dips
this.editFast = this.cloths[i].fastness
this.editTop = this.cloths[i].starred
this.showEditSheet = true
},
onDel: (i: number) => {
this.delIndex = i
this.showDelDialog = true
},
onStar: (i: number) => {
this.cloths = this.cloths.map((c: Cloth208, ci: number) => {
if (ci === i) {
return { id: c.id, name: c.name, dye: c.dye, dips: c.dips, fastness: c.fastness, state: c.state, watchers: c.watchers, starred: !c.starred }
}
return c
})
}
})
}
if (this.tabIndex1 === 2) {
DyeTab208({ dyes: this.dyes, cloths: this.cloths, dipDays: this.dipDays })
}
if (this.tabIndex1 === 3) {
FabricTab208({ fabrics: this.fabrics, fastLogs: this.fastLogs })
}
if (this.tabIndex1 === 4) {
ArtisanTab208({
artisans: this.artisans,
onJoin: () => {
this.showJoinSheet = true
}
})
}
if (this.tabIndex1 === 5) {
MineTab208({ cloths: this.cloths, orders: this.orders, dipDays: this.dipDays })
}
}
.width('100%')
.padding(14)
}
.layoutWeight(1)
.align(Alignment.Top)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F0E8')
.bindSheet($$this.showJoinSheet, this.joinSheet208(), {
height: 620,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showPatternSheet, this.patternSheet208(), {
height: 600,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheet208(), {
height: 560,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showDelDialog, this.delDialog208(), {
})
.bindContentCover($$this.showDetailDialog, this.detailDialog208(), {
})
}
// ---------- 弹框1:预约连麦共创(抽屉) ----------
@Builder
joinSheet208() {
Column({ space: 16 }) {
Row({ space: 10 }) {
Column().width(4).height(30).borderRadius(2).backgroundColor('#283593')
Column({ space: 2 }) {
Text('预约连麦共创').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('和靛缸阿婆同缸染布 · 出布可留样').fontSize(10).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('✕').fontSize(14).fontColor('#5C6BC0')
}
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showJoinSheet = false
})
}
.width('100%')
Scroll() {
Column({ space: 16 }) {
Column({ space: 8 }) {
Text('目标花色').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(dyeTags208, (tag: string, i: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.joinPattern === i ? '#FFFFFF' : '#5C6BC0')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.joinPattern === i ? dyeColor208(tag) : '#E8EAF6')
.margin(4)
.onClick(() => {
this.joinPattern = i
})
}, (tag: string) => tag)
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Column({ space: 8 }) {
Text('坯布选择').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(fabricTags208, (tag: string, i: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.joinFabric === i ? '#FFFFFF' : '#5C6BC0')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.joinFabric === i ? '#E53935' : '#E8EAF6')
.margin(4)
.onClick(() => {
this.joinFabric = i
})
}, (tag: string) => tag)
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row({ space: 14 }) {
Column() {
Text('-').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.joinDips > 1) {
this.joinDips -= 1
}
})
Column({ space: 2 }) {
Text('浸染 ' + this.joinDips + ' 次').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('次数越多颜色越深').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() {
Text('+').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.joinDips < 6) {
this.joinDips += 1
}
})
Text('').layoutWeight(1)
}
.width('100%')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('直播共创').fontSize(13).fontColor('#283593')
Text('出缸瞬间同步给你看').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Row() {
Circle().width(18).height(18).fill('#FFFFFF')
}
.width(48)
.height(26)
.borderRadius(13)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.joinLive ? '#283593' : '#BDBDBD')
.onClick(() => {
this.joinLive = !this.joinLive
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#E8EAF6')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('留样布纪念').fontSize(13).fontColor('#283593')
Text('成品裁角样包邮到家').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Row() {
Circle().width(18).height(18).fill('#FFFFFF')
}
.width(48)
.height(26)
.borderRadius(13)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.joinKeep ? '#E53935' : '#BDBDBD')
.onClick(() => {
this.joinKeep = !this.joinKeep
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FBE9E7')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('预计花费').fontSize(13).fontColor('#283593')
Text('含坯布与染材费').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Text('¥ ' + (this.joinDips * 22 + (this.joinKeep ? 15 : 0))).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C62828')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#E8EAF6')
Button() {
Text('确认预约连染').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor('#283593')
.onClick(() => {
this.showJoinSheet = false
})
Text('开染前 1 小时可免费改期 · 成品归共创者所有').fontSize(9).fontColor('#BDBDBD')
}
.width('100%')
.padding({ left: 18, right: 18, top: 4, bottom: 24 })
}
.constraintSize({ maxHeight: 460 })
}
.width('100%')
.height('100%')
.padding({ top: 14 })
.backgroundColor('#FFFFFF')
}
// ---------- 弹框2:上传花色(抽屉,concat 前插) ----------
@Builder
patternSheet208() {
Column({ space: 16 }) {
Row({ space: 10 }) {
Column().width(4).height(30).borderRadius(2).backgroundColor('#E53935')
Column({ space: 2 }) {
Text('上传新花色').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('晒出你的扎染作品 · 收进花色册').fontSize(10).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('✕').fontSize(14).fontColor('#5C6BC0')
}
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showPatternSheet = false
})
}
.width('100%')
Scroll() {
Column({ space: 16 }) {
Column({ space: 8 }) {
Text('花色名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
TextInput({ placeholder: '例如:洱海月纹方巾', text: this.patternName })
.fontSize(13)
.padding(12)
.borderRadius(12)
.backgroundColor('#E8EAF6')
.onChange((v: string) => {
this.patternName = v
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Column({ space: 8 }) {
Text('主染材').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(dyeTags208, (tag: string, i: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.patternDye === i ? '#FFFFFF' : '#5C6BC0')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.patternDye === i ? dyeColor208(tag) : '#E8EAF6')
.margin(4)
.onClick(() => {
this.patternDye = i
})
}, (tag: string) => tag)
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row({ space: 14 }) {
Column() {
Text('-').fontSize(16).fontColor('#E53935')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#FBE9E7')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.patternDips > 1) {
this.patternDips -= 1
}
})
Column({ space: 2 }) {
Text('浸染 ' + this.patternDips + ' 次').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('记下你的复染次数').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() {
Text('+').fontSize(16).fontColor('#E53935')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#FBE9E7')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.patternDips < 6) {
this.patternDips += 1
}
})
Text('').layoutWeight(1)
}
.width('100%')
Row({ space: 14 }) {
Column() {
Text('-').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.patternFast > 1) {
this.patternFast -= 1
}
})
Column({ space: 2 }) {
Text('自评色牢度 ' + this.patternFast + ' 级').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('水洗不掉色 1-5 级').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() {
Text('+').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.patternFast < 5) {
this.patternFast += 1
}
})
Text('').layoutWeight(1)
}
.width('100%')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('公开到花色册').fontSize(13).fontColor('#283593')
Text('其他染友可见你的作品').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Row() {
Circle().width(18).height(18).fill('#FFFFFF')
}
.width(48)
.height(26)
.borderRadius(13)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.patternPublic ? '#283593' : '#BDBDBD')
.onClick(() => {
this.patternPublic = !this.patternPublic
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#E8EAF6')
Button() {
Text('上传花色').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor('#E53935')
.onClick(() => {
this.patternName = ''
})
}
.width('100%')
.padding({ left: 18, right: 18, top: 4, bottom: 24 })
}
.constraintSize({ maxHeight: 440 })
}
.width('100%')
.height('100%')
.padding({ top: 14 })
.backgroundColor('#FFFFFF')
}
// ---------- 弹框3:编辑花色(抽屉,map 回写) ----------
@Builder
editSheet208() {
Column({ space: 16 }) {
Row({ space: 10 }) {
Column().width(4).height(30).borderRadius(2).backgroundColor('#00897B')
Column({ space: 2 }) {
Text('编辑花色档案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('修正复染次数与色牢度').fontSize(10).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('✕').fontSize(14).fontColor('#5C6BC0')
}
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showEditSheet = false
})
}
.width('100%')
Scroll() {
Column({ space: 16 }) {
Column({ space: 8 }) {
Text('花色名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#283593')
TextInput({ placeholder: '输入新名称', text: this.editName })
.fontSize(13)
.padding(12)
.borderRadius(12)
.backgroundColor('#E8EAF6')
.onChange((v: string) => {
this.editName = v
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row({ space: 14 }) {
Column() {
Text('-').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.editDips > 1) {
this.editDips -= 1
}
})
Column({ space: 2 }) {
Text('浸染 ' + this.editDips + ' 次').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('补记复染次数').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() {
Text('+').fontSize(16).fontColor('#283593')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.editDips < 6) {
this.editDips += 1
}
})
Text('').layoutWeight(1)
}
.width('100%')
Row({ space: 14 }) {
Column() {
Text('-').fontSize(16).fontColor('#00897B')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E0F2F1')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.editFast > 1) {
this.editFast -= 1
}
})
Column({ space: 2 }) {
Text('色牢度 ' + this.editFast + ' 级').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#00897B')
Text('复测后可修正').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Column() {
Text('+').fontSize(16).fontColor('#00897B')
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('#E0F2F1')
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (this.editFast < 5) {
this.editFast += 1
}
})
Text('').layoutWeight(1)
}
.width('100%')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('置顶到花色册').fontSize(13).fontColor('#283593')
Text('最满意的作品放最上面').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Row() {
Circle().width(18).height(18).fill('#FFFFFF')
}
.width(48)
.height(26)
.borderRadius(13)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.editTop ? '#E53935' : '#BDBDBD')
.onClick(() => {
this.editTop = !this.editTop
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FBE9E7')
Button() {
Text('保存修改').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%')
.height(48)
.borderRadius(24)
.backgroundColor('#00897B')
.onClick(() => {
this.cloths = this.cloths.map((c: Cloth208, ci: number) => {
if (ci === this.editIndex) {
return {
id: c.id,
name: this.editName === '' ? c.name : this.editName,
dye: c.dye,
dips: this.editDips,
fastness: this.editFast * 18,
state: c.state,
watchers: c.watchers,
starred: this.editTop
}
}
return c
})
this.showEditSheet = false
})
}
.width('100%')
.padding({ left: 18, right: 18, top: 4, bottom: 24 })
}
.constraintSize({ maxHeight: 420 })
}
.width('100%')
.height('100%')
.padding({ top: 14 })
.backgroundColor('#FFFFFF')
}
// ---------- 弹框4:删除花色(居中,filter) ----------
@Builder
delDialog208() {
Column({ space: 14 }) {
Column() {
Text('🧵').fontSize(34)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#E8EAF6')
.justifyContent(FlexAlign.Center)
Text('下架这个花色?').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('「' + (this.delIndex >= 0 && this.delIndex < this.cloths.length ? this.cloths[this.delIndex].name : '') + '」将从花色册下架,跟染入口同步关闭').fontSize(11).fontColor('#5C6BC0').textAlign(TextAlign.Center)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('保留染样册').fontSize(12).fontColor('#283593')
Text('仅下架花色卡').fontSize(9).fontColor('#90A4AE')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Row() {
Circle().width(16).height(16).fill('#FFFFFF')
}
.width(44)
.height(24)
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.delKeepAlbum ? '#283593' : '#BDBDBD')
.onClick(() => {
this.delKeepAlbum = !this.delKeepAlbum
})
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#E8EAF6')
Row({ space: 10 }) {
Button() {
Text('再想想').fontSize(14).fontColor('#283593').fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(44)
.borderRadius(22)
.backgroundColor('#E8EAF6')
.onClick(() => {
this.showDelDialog = false
})
Button() {
Text('确认下架').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(44)
.borderRadius(22)
.backgroundColor('#C62828')
.onClick(() => {
this.cloths = this.cloths.filter((c: Cloth208, ci: number) => ci !== this.delIndex)
this.showDelDialog = false
})
}
.width('100%')
}
.width('82%')
.padding(20)
.borderRadius(20)
.backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Center)
}
// ---------- 弹框5:花色详情(居中,图表+跳转联动) ----------
@Builder
detailDialog208() {
Column({ space: 0 }) {
Scroll() {
Column({ space: 0 }) {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() {
Text('🎨').fontSize(30)
}
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].name : '').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text((this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].dye : '') + ' · 复染 ' + (this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].dips : 0) + ' 次').fontSize(11).fontColor('#E8EAF6')
}
.alignItems(HorizontalAlign.Start)
Text('').layoutWeight(1)
Column() {
Text('✕').fontSize(13).fontColor('#FFFFFF')
}
.width(28)
.height(28)
.borderRadius(14)
.backgroundColor('rgba(255,255,255,0.2)')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showDetailDialog = false
})
}
.width('100%')
}
.width('100%')
.padding(18)
.linearGradient({ angle: 135, colors: [['#3949AB', 0], ['#C62828', 1]] })
Column({ space: 14 }) {
Row({ space: 8 }) {
Column({ space: 2 }) {
Text((this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].fastness + '%' : '')).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#283593')
Text('色牢度评分').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#E8EAF6')
Column({ space: 2 }) {
Text((this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].watchers : 0) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#C62828')
Text('累计跟染人次').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#FBE9E7')
Column({ space: 2 }) {
Text(this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].state : '').fontSize(14).fontWeight(FontWeight.Bold).fontColor(clothStateColor208(this.detailIndex >= 0 && this.detailIndex < this.cloths.length ? this.cloths[this.detailIndex].state : ''))
Text('当前状态').fontSize(9).fontColor('#5C6BC0')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#E8EAF6')
}
.width('100%')
Column({ space: 8 }) {
Text('近 7 日跟染人数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row({ space: 6 }) {
ForEach(this.dipDays, (d: DipDay208) => {
Column({ space: 4 }) {
Column()
.width(16)
.height(d.dips * 5)
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({ angle: 180, colors: [['#5C6BC0', 0], ['#283593', 1]] })
Text(d.day).fontSize(8).fontColor('#5C6BC0')
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (d: DipDay208) => ('d' + d.day))
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
.height(90)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F5F5F5')
Column({ space: 8 }) {
Text('工艺档案').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Row({ space: 8 }) {
Text('捆扎').fontSize(11).fontColor('#5C6BC0')
Text('弹珠 + 梯子捆').fontSize(11).fontColor('#283593').fontWeight(FontWeight.Bold)
Text('').layoutWeight(1)
Text('留白 32%').fontSize(10).fontColor('#C62828')
}
.width('100%')
Row({ space: 8 }) {
Text('养缸').fontSize(11).fontColor('#5C6BC0')
Text('八年老靛缸 · pH 11').fontSize(11).fontColor('#283593').fontWeight(FontWeight.Bold)
Text('').layoutWeight(1)
Text('靛花饱满').fontSize(10).fontColor('#00897B')
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F5F5F5')
.alignItems(HorizontalAlign.Start)
Button() {
Text('预约连染同款花色').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.width('100%')
.height(44)
.borderRadius(22)
.backgroundColor('#C62828')
.onClick(() => {
this.showDetailDialog = false
this.showJoinSheet = true
})
}
.width('100%')
.padding(16)
}
.width('100%')
}
.constraintSize({ maxHeight: 480 })
}
.width('86%')
.borderRadius(20)
.backgroundColor('#FFFFFF')
.clip(true)
}
}
// ================= Tab1:染坊(直播) =================
@Component
struct LiveTab208 {
@State localMic: boolean = true
@State localCam: boolean = true
@State localStir: boolean = true
@Prop cloths: Cloth208[] = []
@Prop artisans: Artisan208[] = []
@Prop dipSteps: DipStep208[] = []
@Prop barrages: Barrage208[] = []
onStep: (i: number) => void = () => {
}
onJoin: () => void = () => {
}
build() {
Column({ space: 12 }) {
// 四机位
Grid() {
GridItem() {
Column({ space: 4 }) {
Row({ space: 6 }) {
Column() {
Text('🪣').fontSize(18)
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('rgba(255,255,255,0.25)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text('主镜 · 靛缸浸染').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('靛缸阿婆 · 老缸').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(10)
Text('').layoutWeight(1)
Row({ space: 6 }) {
Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text(dippingClothCount208(this.cloths) + ' 匝布在缸').fontSize(9).fontColor('#C5CAE9')
}
.width('100%')
.padding(8)
}
.width('100%')
.height('100%')
.borderRadius(12)
.padding(6)
.linearGradient({ angle: 150, colors: [['#283593', 0], ['#1A237E', 1]] })
.onClick(() => {
}
}
总结

云染坊这套源码的完整度,体现在它不是Demo片段的堆砌,而是一个从数据建模到交互闭环的自洽系统。十个interface定义了全部领域实体,八个纯函数封装了配色映射与统计聚合,一个主入口组件统管六个Tab与五个弹层的状态中枢,六个子Tab各司其职地承载直播、花色册、染材库、坯布架、匠人团、我的六类业务场景。每一处状态变更都走"回调上报→主入口不可变更新→@State触发刷新→@Prop快照下传→子Tab重渲染"的完整链路,没有捷径也没有旁路,保证了数据流的单向可追踪。
从工程实践角度,这套代码示范了HarmonyOS ArkTS在中等复杂度应用中的几项关键范式:@Builder拆分复杂UI降低build嵌套、bindSheet/bindContentCover分层弹层管理、map/filter不可变数组更新配合状态观察、ForEach键生成器精准Diff、linearGradient与layoutWeight的视觉建模能力。特别值得注意的是"数据上浮、事件下沉"的架构——主入口持有全部业务数据与表单临时态,子组件只接收快照与回调,这种分层让状态边界清晰,在应用规模增长时仍能保持可维护性。晾布夹Tab导航的错落布条、四机位的色相演变、堆叠条的占比可视化、匠人头像的染材色循环,这些细节则展示了声明式UI在场景化设计中的表达力——每一个像素都在为非遗工艺的数字化体验服务。
扎染的核心是"留白",染坊代码的核心是"分层"。捆扎防染留出布的白,状态分层留出维护的白;浸染复染叠出色的深,不可变更新叠出引用的深。千年工艺与现代框架,在留白与叠加的哲学上殊途同归。
云染坊提供了一个可直接参考的骨架:把Tab导航做成场景化隐喻、把直播工位做成色相演变图、把表单录入做成抽屉式交互、把列表操作做成回调式CRUD、把数据可视化做成纯声明式柱图。这套范式不依赖任何第三方图表库或复杂状态管理框架,纯用ArkTS原生能力即实现了完整的业务闭环,是声明式UI"够用且好用"的最佳注脚。
更多推荐

所有评论(0)