在鸿蒙(HarmonyOS)应用开发生态中,ArkTS 作为声明式 UI 编程范式,为开发者提供了状态驱动、组件化构建、高效渲染的能力。本文将以一个完整的软装布艺窗帘生活馆商城应用为蓝本,逐段剖析其代码实现,深入讲解 ArkTS 的核心语法、组件设计、状态管理、动画系统与弹框架构,帮助开发者全面掌握鸿蒙单文件复杂应用的开发方法。

一、应用整体架构与技术栈概览

在这里插入图片描述

本应用是一个面向软装布艺行业的垂直电商与服务预约平台,业务覆盖窗帘选购、面料小样申请、软装搭配方案、上门量装服务、会员中心等完整链路。整个应用采用鸿蒙原生 ArkTS 语言编写,基于声明式 UI 框架,通过单文件承载全部逻辑,体现了高内聚、低耦合的工程化思想。

应用整体采用"一主多子"的组件架构。主组件 FABRICApp 负责全局状态管理、Tab 路由分发、弹框层调度,六个子内容组件分别承载首页、窗帘、面料、方案、上门、我的六个业务页面。每个子组件通过回调函数向主组件通信,主组件通过属性绑定向子组件下发数据与指令,形成清晰的双向数据流。

在技术栈层面,应用充分利用了 ArkTS 的核心能力。状态管理方面使用 @State 装饰器实现响应式数据绑定。组件化方面使用 @Component@Builder 装饰器分别声明可复用组件与构建器。布局方面综合运用 ColumnRowStackFlexScroll 等容器组件。动画方面通过 animateTo 实现无限循环的呼吸、飘动、摆动等微交互特效。数据可视化方面通过手工绘制进度条、柱状图、配色比例条等实现轻量级图表展示。

FABRICApp 主组件

头部导航区

内容区分发 contentArea

底部 Tab 导航

Tab1 首页

Tab2 窗帘

Tab3 面料

Tab4 方案

Tab5 上门

Tab6 我的

弹框层 7 个 Modal

小样申请

删除确认

编辑量尺

风格选择

上门预约

配色方案

窗帘详情

上图展示了应用的整体组件树结构。主组件作为中枢,向下分发到六个业务 Tab 页面和七个弹框模块。每个 Tab 页面内部又有独立的动画状态和数据展示逻辑,弹框层则通过条件渲染叠加在内容区之上,实现模态交互。

应用的视觉设计采用"紫罗兰亚麻风"配色体系,以亚麻米色为底、紫罗兰紫为主调、雾灰紫为辅助色,营造出温暖而高级的家居布艺氛围。所有颜色通过统一的配色常量集中管理,确保视觉一致性。

二、配色体系设计

在这里插入图片描述

在鸿蒙 ArkTS 应用开发中,配色管理是视觉一致性的基石。本应用通过接口定义与常量声明的方式,构建了一套完整的配色体系。

interface FBColorPalette {
  bg: string
  card: string
  violet: string
  violetDeep: string
  mist: string
  mistLight: string
  linen: string
  linenLight: string
  ink: string
  line: string
  textSub: string
  textHint: string
}

const FB_COLORS: FBColorPalette = {
  bg: '#F6F2EC',
  card: '#FFFFFF',
  violet: '#7B5EA7',
  violetDeep: '#5E4688',
  mist: '#C9BBDD',
  mistLight: '#EDE7F5',
  linen: '#B99B7A',
  linenLight: '#F0E6D8',
  ink: '#3D3352',
  line: '#EAE2D6',
  textSub: '#7A7290',
  textHint: '#B4AAC6'
}

首先,这里使用 interface 关键字定义了 FBColorPalette 接口。在 ArkTS 中,接口用于声明对象的形状,它是一种类型契约,确保实现该接口的对象必须包含所有声明的字段。这个接口定义了十二个颜色字段,涵盖了背景色、卡片色、主色调、深色调、辅助色、文字色、分割线色等全部视觉维度。

然后,通过 const 声明了 FB_COLORS 常量并赋值为符合接口类型的对象。这种集中式颜色管理方案的优势在于:当需要调整品牌色或适配暗色模式时,只需修改一处即可全局生效。#F6F2EC 作为亚麻米色背景,给人温暖柔和的第一印象;#7B5EA7 紫罗兰紫是品牌主色,用于按钮、标签、强调文字等关键元素。

在实际鸿蒙项目开发中,推荐将配色常量单独抽离到资源文件或公共模块中,通过 AppStorage 或静态类进行全局访问。本应用采用单文件内集中声明的方式,便于在代码层面直接引用,也方便开发者快速定位颜色来源。

值得注意的是,配色体系中还定义了 textSubtextHint 两级辅助文字色。#7A7290 用于次要文字信息,#B4AAC6 用于提示性文字,这种三级文字色彩层次使信息层级清晰可辨,是移动端界面设计的基本素养。

三、Tab 枚举与导航定义

在这里插入图片描述

应用的底部导航采用六 Tab 设计,通过枚举类型管理 Tab 标识,配合数据数组驱动渲染。

enum FBTab {
  Home = 0,
  Curtain = 1,
  Fabric = 2,
  Plan = 3,
  Visit = 4,
  Mine = 5
}

interface FBTabItem {
  tab: FBTab
  icon: string
  label: string
}

const FB_TABS: FBTabItem[] = [
  { tab: FBTab.Home, icon: '🏠', label: '首页' },
  { tab: FBTab.Curtain, icon: '🪟', label: '窗帘' },
  { tab: FBTab.Fabric, icon: '🧵', label: '面料' },
  { tab: FBTab.Plan, icon: '🎨', label: '方案' },
  { tab: FBTab.Visit, icon: '📐', label: '上门' },
  { tab: FBTab.Mine, icon: '👤', label: '我的' }
]

这里首先使用 enum 关键字定义了 FBTab 枚举。在 ArkTS 中,枚举是一种用于定义命名常量集合的数据类型,它为有限的离散值提供了类型安全。每个 Tab 都有一个数字值,从 0 到 5 依次递增。使用枚举而非魔术数字(magic number)的好处是代码可读性大幅提升,FBTab.Home 远比 0 更能表达意图。

接着定义了 FBTabItem 接口,描述每个 Tab 项的结构:tab 字段是枚举标识,icon 是 emoji 图标,label 是显示文字。最后声明 FB_TABS 数组,将六个 Tab 项的数据集中存放。这种"数据驱动渲染"的模式是 ArkTS 的核心思想之一,通过 ForEach 遍历数组即可自动生成底部导航项,无需为每个 Tab 手写重复的 UI 代码。

ForEach 是 ArkTS 中的列表渲染组件,它接收数据数组和渲染函数,自动为每个数据项生成对应的 UI 组件。当数据数组发生变化时,ForEach 会自动触发差分更新,只重新渲染变化的部分,保证渲染性能。

四、首页统计数据模型

在这里插入图片描述

首页顶部展示四个关键统计指标,其数据结构定义如下。

interface FBDataStat {
  icon: string
  label: string
  value: string
  sub: string
  color: string
}

const FB_STATS: FBDataStat[] = [
  { icon: '🪟', label: '在售窗帘', value: '16', sub: '款', color: '#7B5EA7' },
  { icon: '🧵', label: '甄选面料', value: '12', sub: '种', color: '#B99B7A' },
  { icon: '📐', label: '累计上门量窗', value: '3,208', sub: '户', color: '#5E4688' },
  { icon: '⭐', label: '搭配好评率', value: '99.2', sub: '%', color: '#9B8AC4' }
]

FBDataStat 接口定义了统计数据的五元组结构。icon 用于表情符号展示,label 是指标名称,value 是核心数值,sub 是单位后缀,color 是该指标的主题色。这种设计使得每个统计卡片都能拥有独立的色彩标识,视觉上不单调。

FB_STATS 数组包含四条数据,分别对应在售窗帘、甄选面料、累计上门量窗、搭配好评率四项指标。数值使用字符串类型存储而非数字,这是因为数值中可能包含千位分隔符(如 3,208)或小数点(如 99.2),使用字符串避免了格式化逻辑的复杂性。

在 ArkTS 中,接口配合常量数组的数据定义模式,相当于传统前端开发中的 Mock 数据层。在实际项目中,这些数据通常来自网络请求,但在单文件应用中,静态数据先行保证了界面开发的独立性,也便于后续将数据源替换为真实 API。

每个统计项的 color 字段使用品牌色系中的不同色值,紫罗兰紫、亚麻棕、深紫、雾灰紫交替使用,四宫格在视觉上形成色彩节奏,避免了同色疲劳。

五、风格与功能宫格数据模型

在这里插入图片描述

首页的横滑风格大卡和功能宫格分别使用以下数据结构。

interface FBDataStyle {
  id: number
  name: string
  icon: string
  desc: string
  colorHex: string
  count: number
}

const FB_STYLES: FBDataStyle[] = [
  { id: 1, name: '雾紫浪漫', icon: '🌙', desc: '紫罗兰纱帘 × 亚麻床品,卧室一夜好眠', colorHex: '#C9BBDD', count: 326 },
  { id: 2, name: '燕麦自然', icon: '🌾', desc: '本色亚麻 × 原木家具,客厅呼吸感满分', colorHex: '#D9CDB8', count: 284 },
  { id: 3, name: '灰紫轻奢', icon: '💜', desc: '雾灰紫提花 × 丝绒抱枕,全屋高级质感', colorHex: '#9B8AC4', count: 198 },
  { id: 4, name: '亚麻日式', icon: '🍵', desc: '苎麻平幔 × 藤编地毯,书房禅意十足', colorHex: '#CDBFA5', count: 162 },
  { id: 5, name: '奶油法式', icon: '🍰', desc: '米白泡泡纱 × 蕾丝纱幔,餐厅温柔滤镜', colorHex: '#EFD9E2', count: 141 },
  { id: 6, name: '星夜静谧', icon: '🌌', desc: '深紫墨全遮光 × 静音轨道,儿童房哄睡神器', colorHex: '#3D3352', count: 118 }
]

FBDataStyle 接口定义了软装风格的数据结构。colorHex 字段存储风格的代表色,这个颜色将直接用作卡片的背景色,使每种风格在视觉上拥有独特的色彩标识。count 字段记录已有多少户家庭采用了该风格方案,作为社交信任的量化指标。

六种风格的色彩从浅到深排列,从雾紫浪漫的 #C9BBDD 到星夜静谧的 #3D3352,形成一条完整的色彩光谱。desc 字段以"×"符号连接两个软装元素,简洁地传达搭配理念,这是软装行业常见的文案范式。

interface FBDataGrid {
  icon: string
  label: string
  bg: string
}

const FB_GRIDS: FBDataGrid[] = [
  { icon: '🧵', label: '面料小样', bg: '#EDE7F5' },
  { icon: '📐', label: '上门量窗', bg: '#F0E6D8' },
  { icon: '🪟', label: '窗帘定制', bg: '#EDE7F5' },
  { icon: '🎨', label: '软装方案', bg: '#F0E6D8' },
  { icon: '🔧', label: '安装服务', bg: '#EDE7F5' },
  { icon: '♻️', label: '旧帘换新', bg: '#F0E6D8' },
  { icon: '❤️', label: '我的收藏', bg: '#EDE7F5' },
  { icon: '💬', label: '搭配顾问', bg: '#F0E6D8' }
]

功能宫格的数据结构更加简洁,仅包含图标、标签、背景色三个字段。bg 字段的值在 #EDE7F5(雾紫浅)和 #F0E6D8(亚麻浅)之间交替,形成棋盘式的视觉韵律。八项功能覆盖了面料小样、上门量窗、窗帘定制、软装方案、安装服务、旧帘换新、我的收藏、搭配顾问等完整业务链路。

六、风格分布与窗帘数据模型

在这里插入图片描述

首页还展示了风格类型分布条和窗帘商品列表,其数据结构如下。

interface FBDataDist {
  style: string
  count: number
  color: string
}

const FB_STYLE_DIST: FBDataDist[] = [
  { style: '自然风', count: 128, color: '#7B5EA7' },
  { style: '轻奢风', count: 96, color: '#9B8AC4' },
  { style: '日式', count: 84, color: '#B99B7A' },
  { style: '法式', count: 62, color: '#C9BBDD' },
  { style: '极简', count: 58, color: '#CDBFA5' },
  { style: '复古', count: 40, color: '#5E4688' }
]

FBDataDist 接口用于风格分布数据,style 是风格名称,count 是订单数量,color 是该风格对应的条形图颜色。六种风格的订单量从 128 递减到 40,配合不同色值的条形图,直观展示客户偏好分布。

interface FBDataCurtain {
  id: number
  name: string
  cat: string
  icon: string
  colorHex: string
  price: number
  sales: number
  dim: number
  fold: number
  spaces: string
  tag: string
}

const FBCURTAINS: FBDataCurtain[] = [
  { id: 1, name: '雾紫轻语 · 雪尼尔遮光帘', cat: '卧室', icon: '🌙', colorHex: '#C9BBDD', price: 268, sales: 4820, dim: 95, fold: 2.0, spaces: '主卧/老人房', tag: '爆款' },
  { id: 2, name: '亚麻晨光 · 棉麻半遮光帘', cat: '客厅', icon: '🌾', colorHex: '#D9CDB8', price: 189, sales: 3960, dim: 60, fold: 1.8, spaces: '客厅/阳台', tag: '热卖' },
  { id: 3, name: '星夜黑金 · 全遮光隔音帘', cat: '卧室', icon: '🌌', colorHex: '#3D3352', price: 359, sales: 3610, dim: 99, fold: 2.5, spaces: '主卧/影音室', tag: '隔音' }
  // ... 共 16 款
]

FBDataCurtain 是窗帘商品的核心数据接口,包含十一个字段。cat 用于分类筛选,dim 是遮光率百分比,fold 是褶皱倍数,spaces 以斜杠分隔两个适用空间,tag 是营销标签。十六款窗帘覆盖了卧室、客厅、书房、儿童房、餐厅等全部空间场景,价格从 129 元到 398 元每平方米,满足不同预算需求。

colorHex 字段存储窗帘的代表色,在 UI 层面直接作为色块背景展示,让用户在浏览时就能直观感受窗帘的色彩风格,这比文字描述更高效。

七、面料、方案与服务数据模型

在这里插入图片描述

interface FBDataFabric {
  id: number
  name: string
  colorHex: string
  comp: string
  price: number
  feature: string
  stock: number
  feel: number
}

const FBFABRICS: FBDataFabric[] = [
  { id: 1, name: '本色亚麻', colorHex: '#D9CDB8', comp: '100% 亚麻', price: 68, feature: '透气亲肤', stock: 420, feel: 96 },
  { id: 2, name: '棉麻混纺', colorHex: '#E4D9C6', comp: '55% 棉 45% 麻', price: 45, feature: '垂感自然', stock: 680, feel: 92 },
  { id: 3, name: '雾灰紫雪尼尔', colorHex: '#C9BBDD', comp: '雪尼尔绒', price: 88, feature: '厚实遮光', stock: 350, feel: 98 }
  // ... 共 12 种
]

面料数据接口 FBDataFabric 包含成分 comp、特色 feature、库存 stock、手感指数 feel 等专业字段。feel 是一个 0 到 100 的数值,代表盲摸手感的评分,用于手感榜单排序。十二种面料从 32 元的雪纺纱到 128 元的紫罗兰丝绒,价格梯度覆盖大众到轻奢市场。

interface FBDataPlan {
  id: number
  name: string
  style: string
  icon: string
  price: number
  rooms: string
  rating: number
  applied: number
  c1: string
  c2: string
  c3: string
  c4: string
  r1: number
  r2: number
  r3: number
  r4: number
}

const FBPLANS: FBDataPlan[] = [
  { id: 1, name: '紫雾晨眠 · 卧室套装', style: '雾紫浪漫', icon: '🌙', price: 1680, rooms: '窗帘+床品+抱枕', rating: 4.9, applied: 326, c1: '#C9BBDD', c2: '#F6F2EC', c3: '#7B5EA7', c4: '#B99B7A', r1: 40, r2: 30, r3: 20, r4: 10 }
  // ... 共 8 套
]

软装方案数据接口 FBDataPlan 是最复杂的数据结构,包含四个配色字段 c1c4 和四个占比字段 r1r4。这种设计将配色方案拆解为主色、底色、点缀色、强调色四个层次,遵循"四色法则"(主色 40% + 底色 30% + 点缀色 20% + 强调色 10%),是室内设计的专业方法论在数据层面的直接映射。

软装方案数据

主色 c1/r1 40%

底色 c2/r2 30%

点缀色 c3/r3 20%

强调色 c4/r4 10%

大面积窗帘/墙面

床品/地毯底色

抱枕/装饰品

金属件/小摆件

上图展示了四色法则在软装方案中的角色分工。主色占据 40% 的视觉面积,是空间的主基调;底色占 30% 作为主色的衬托;点缀色占 20% 用于增加视觉层次;强调色仅占 10% 起画龙点睛之效。

八、上门服务与用户数据模型

interface FBDataVisit {
  id: number
  name: string
  icon: string
  duration: string
  price: number
  desc: string
  tag: string
  today: string
  sat: string
  sun: string
}

const FBVISITS: FBDataVisit[] = [
  { id: 1, name: '免费上门量窗', icon: '📐', duration: '约 40 分钟', price: 0, desc: '激光测距 · 出报价单', tag: '免费', today: '余 6', sat: '余 3', sun: '余 5' },
  { id: 2, name: '窗帘安装挂帘', icon: '🔧', duration: '约 90 分钟', price: 128, desc: '打孔挂钩 · 调试平整', tag: '标准化', today: '余 4', sat: '满', sun: '余 2' }
  // ... 共 8 项
]

上门服务数据接口 FBDataVisit 包含 todaysatsun 三个字段,分别表示今日、周六、周日的可约余量。值为"余 N"表示还有名额,"满"表示已约满,"预约"表示需联系客服。这种设计直接将排期信息嵌入数据,无需额外的日历组件即可展示服务可用性。

interface FBDataBooking {
  id: number
  service: string
  master: string
  date: string
  time: string
  status: string
  addr: string
}

const FBBOOKINGS: FBDataBooking[] = [
  { id: 1, service: '免费上门量窗', master: '量尺组 · 阿哲', date: '08-16', time: '10:00-12:00', status: '已完成', addr: '澜山别院 3 栋 802' },
  { id: 2, service: '静音轨道加装', master: '安装组 · 老周', date: '08-17', time: '14:00-16:00', status: '已完成', addr: '澜山别院 3 栋 802' }
  // ... 共 14 条
]

预约记录数据接口 FBDataBooking 记录了服务名称、师傅信息、日期、时段、状态和地址。十四条记录覆盖了已完成、进行中、待上门、已预约四种状态,构成一条完整的时间轴。master 字段采用"组别 · 人名"的格式,既体现了服务团队的专业分工,又保留了人情味。

九、订单、收藏与消费数据模型

interface FBDataOrder {
  id: number
  item: string
  date: string
  status: string
  price: number
}

const FBORDERS: FBDataOrder[] = [
  { id: 1, item: '雾紫轻语 · 雪尼尔遮光帘 ×2', date: '08-18', status: '已完成', price: 536 },
  { id: 2, item: '紫雾晨眠 · 卧室套装', date: '08-18', status: '已完成', price: 1680 },
  { id: 3, item: '本色亚麻面料 6 米', date: '08-20', status: '已完成', price: 408 }
  // ... 共 12 条
]

订单数据接口 FBDataOrder 包含商品名称、日期、状态和金额。十二条订单呈现了已完成、已发货、制作中、待付尾款、待服务等多种状态,真实模拟了电商订单的全生命周期。

interface FBDataSpend {
  month: string
  amount: number
}

const FBSPEND: FBDataSpend[] = [
  { month: '3月', amount: 560 },
  { month: '4月', amount: 890 },
  { month: '5月', amount: 640 },
  { month: '6月', amount: 1280 },
  { month: '7月', amount: 960 },
  { month: '8月', amount: 1180 }
]

月消费数据接口 FBDataSpend 记录了六个月的布艺消费金额。这些数据将用于"我的"页面的柱状图展示,通过计算每月金额占最大值的比例来动态计算柱高,实现轻量级的数据可视化。

在 ArkTS 中,数据可视化的实现并不一定需要引入图表库。对于简单的柱状图、进度条、比例条等可视化需求,利用 Column 的高度属性和 backgroundColor 即可手工绘制,既减少依赖又保持包体积精简。

十、评价数据与全局纯函数

interface FBDataReview {
  id: number
  user: string
  avatar: string
  item: string
  rating: number
  date: string
  content: string
  tag: string
}

const FBREVIEWS: FBDataReview[] = [
  { id: 1, user: '眠眠兔', avatar: '🐰', item: '紫雾晨眠 · 卧室套装', rating: 5.0, date: '08-25', content: '纱帘配床品绝了,卧室像加了一层柔光滤镜,睡眠质量直线上升。', tag: '质感满分' },
  { id: 2, user: '山茶开了', avatar: '🌺', item: '雾紫轻语 · 雪尼尔遮光帘', rating: 4.9, date: '08-24', content: '遮光真的顶,白天拉上像深夜,加厚褶皱垂感特别高级。', tag: '遮光强' }
  // ... 共 10 条
]

评价数据接口 FBDataReview 包含用户昵称、emoji 头像、评分、日期、评价内容和标签。十条评价使用不同的 emoji 作为用户头像,既节省了图片资源加载,又增添了趣味性。tag 字段是评价的提炼标签,如"质感满分""遮光强"等,帮助浏览者快速筛选关注点。

function fbMoney(p: number): string {
  return '¥' + p.toString()
}

function fbDimW(d: number): string {
  return d.toString() + '%'
}

function fbSalesW(s: number): string {
  return Math.round(s / 4820 * 100).toString() + '%'
}

function fbPriceW(p: number): string {
  return Math.round(p / 128 * 100).toString() + '%'
}

function fbFeelW(f: number): string {
  return f.toString() + '%'
}

function fbDistW(c: number): string {
  return Math.round(c / 128 * 100).toString() + '%'
}

function fbSpendH(a: number): number {
  return Math.round(a / 1280 * 92) + 8
}

全局纯函数是应用的工具层,负责数据格式化和比例计算。fbMoney 将数字转为带人民币符号的字符串。fbDimW 将遮光率数字转为百分比字符串。fbSalesW 将销量转为相对最大销量的百分比,以 4820 为基准值进行归一化计算。

这些函数遵循"纯函数"设计原则:相同输入永远产生相同输出,不产生副作用。fbPriceW 以 128 元为基准计算价格占比,fbFeelW 直接将手感指数转为百分比字符串,fbDistW 以 128 单为基准计算分布占比,fbSpendH 将消费金额映射为柱状图高度(8 到 100 像素区间)。

纯函数是函数式编程的核心概念。在 ArkTS 状态驱动的框架中,纯函数特别适合用于计算 UI 展示所需的派生值,因为它们不依赖外部可变状态,在任何时机调用都能得到确定的结果,便于调试和测试。

十一、状态转换与列表过滤函数

function fbFoldColor(f: number): string {
  return f >= 2.5 ? '#5E4688' : (f >= 2 ? '#7B5EA7' : '#B99B7A')
}

function fbStatusColor(s: string): string {
  return s === '已完成' ? '#7B5EA7' : (s === '进行中' ? '#B99B7A' : (s === '待上门' ? '#9B8AC4' : '#3D3352'))
}

function fbStatusBg(s: string): string {
  return s === '已完成' ? '#EDE7F5' : (s === '进行中' ? '#F0E6D8' : '#F6F2EC')
}

function fbOrderColor(s: string): string {
  return s === '已完成' ? '#7B5EA7' : (s === '已发货' ? '#9B8AC4' : (s === '制作中' ? '#B99B7A' : '#3D3352'))
}

function fbSlotColor(q: string): string {
  return q === '满' ? '#B4AAC6' : (q === '预约' ? '#5E4688' : '#7B5EA7')
}

这一组函数负责将业务状态映射为视觉颜色。fbFoldColor 根据褶皱倍数返回不同深浅的紫色:2.5 倍及以上用深紫 #5E4688,2 倍到 2.5 倍用标准紫 #7B5EA7,2 倍以下用亚麻棕 #B99B7A。这种"数值越大颜色越深"的映射逻辑,让用户通过颜色就能感知参数等级。

fbStatusColorfbStatusBg 分别返回预约状态的前景色和背景色。已完成用紫罗兰紫配雾紫浅底,进行中用亚麻棕配亚麻浅底,待上门用雾灰紫,其他用深紫墨。fbOrderColor 则针对订单状态映射颜色,已完成、已发货、制作中、其他分别对应不同色值。

function fbToggleAt(src: boolean[], i: number): boolean[] {
  let r: boolean[] = src.slice()
  r[i] = !r[i]
  return r
}

function fbFilterCurtains(cat: string): FBDataCurtain[] {
  let r: FBDataCurtain[] = []
  for (let i = 0; i < FBCURTAINS.length; i++) {
    if (cat === '全部' || FBCURTAINS[i].cat === cat) {
      r.push(FBCURTAINS[i])
    }
  }
  return r
}

function fbTopSales(): FBDataCurtain[] {
  let r: FBDataCurtain[] = FBCURTAINS.slice()
  r.sort((a: FBDataCurtain, b: FBDataCurtain) => b.sales - a.sales)
  return r.slice(0, 5)
}

function fbTopDim(): FBDataCurtain[] {
  let r: FBDataCurtain[] = FBCURTAINS.slice()
  r.sort((a: FBDataCurtain, b: FBDataCurtain) => b.dim - a.dim)
  return r.slice(0, 6)
}

function fbTopFabrics(): FBDataFabric[] {
  let r: FBDataFabric[] = FBFABRICS.slice()
  r.sort((a: FBDataFabric, b: FBDataFabric) => b.price - a.price)
  return r
}

function fbTopFeel(): FBDataFabric[] {
  let r: FBDataFabric[] = FBFABRICS.slice()
  r.sort((a: FBDataFabric, b: FBDataFabric) => b.feel - a.feel)
  return r.slice(0, 6)
}

function fbTotalSpend(): number {
  let sum: number = 0
  for (let i = 0; i < FBSPEND.length; i++) {
    sum += FBSPEND[i].amount
  }
  return sum
}

fbToggleAt 是一个重要的不可变更新函数。它首先通过 slice() 复制原数组,然后翻转指定索引的布尔值,返回新数组。在 ArkTS 的 @State 状态管理中,直接修改数组元素不会触发重新渲染,必须返回新的数组引用才能让框架检测到变化。这个函数正是为收藏切换、小样多选等场景设计的。

fbFilterCurtains 根据分类名称过滤窗帘列表。当分类为"全部"时返回所有窗帘,否则只返回匹配分类的窗帘。fbTopSalesfbTopDimfbTopFabricsfbTopFeel 分别按销量、遮光率、价格、手感指数排序并截取前几项,用于各类榜单展示。fbTotalSpend 累加六个月消费金额。

十二、首页内容组件(第一部分:品牌横幅与统计)

@Component
struct FABRICHomeContent {
  @State weaveShift: number = 0
  @State weaveOpacity: number = 0.3
  onAddSample: () => void = () => {}
  onBookVisit: () => void = () => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.weaveShift = 10
      this.weaveOpacity = 0.85
    })
  }

@Component 装饰器将 FABRICHomeContent 声明为一个可复用的 UI 组件。在 ArkTS 中,@Component 是组件化的基础,被装饰的 struct 拥有独立的构建函数 build()、生命周期回调 aboutToAppear(),以及自身的状态管理能力。

@State 装饰器声明了两个响应式状态变量:weaveShift 控制横幅布纹的位移量,weaveOpacity 控制透明度。当这些变量的值发生变化时,引用它们的 UI 部分会自动重新渲染。@State 是 ArkTS 状态管理体系的核心装饰器,它实现了观察者模式,状态变更会自动通知依赖该状态的视图更新。

aboutToAppear() 是组件的生命周期回调,在组件创建后、build() 执行前调用。这里通过 getUIContext().animateTo() 启动一个无限循环动画。duration: 2200 设置动画时长 2.2 秒,iterations: -1 表示无限重复,playMode: PlayMode.Alternate 表示交替播放(正向结束后反向播放),curve: Curve.EaseInOut 设置缓动曲线为先快后慢再快。动画闭包内将 weaveShift 从 0 变到 10、weaveOpacity 从 0.3 变到 0.85,由于是交替模式,动画会在两个状态之间持续往复,形成飘动效果。

aboutToAppear 是 ArkTS 组件生命周期的早期钩子,适合用于初始化状态和启动动画。除了 aboutToAppear,还有 aboutToDisappear(组件销毁前)、onPageShow(页面显示时)、onPageHide(页面隐藏时)等生命周期回调,开发者应根据场景选择合适的时机执行逻辑。

三个回调属性 onAddSampleonBookVisitonGoto 是子组件向父组件通信的桥梁。在 ArkTS 中,子组件无法直接修改父组件的状态,但可以通过调用父组件传入的回调函数来间接通信。这种"属性绑定 + 回调通知"的模式是 ArkTS 组件间通信的标准做法。

十三、首页内容组件(第二部分:品牌横幅构建)

  build() {
    Scroll() {
      Column() {
        Stack() {
          Column()
            .width('100%').height(116)
            .linearGradient({
              direction: GradientDirection.RightBottom,
              colors: [['#7B5EA7', 0], ['#5E4688', 1]]
            })
            .borderRadius(16)
          Text('🧵').fontSize(26)
            .opacity(this.weaveOpacity)
            .translate({ x: this.weaveShift, y: 0 })
            .position({ x: '76%', y: 16 })
          Text('🪟').fontSize(18)
            .opacity(this.weaveOpacity * 0.7)
            .translate({ x: -this.weaveShift, y: 0 })
            .position({ x: '88%', y: 64 })
          Row() {
            Column() {
              Text('FABRIC 織語').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('一帘一世界 · 让布艺替家说话').fontSize(10).fontColor('#EDE7F5')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            Column().layoutWeight(1)
            Column() {
              Text('🧵 免费小样').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#5E4688')
            }
            .width(78).height(34)
            .backgroundColor('#FFFFFF')
            .borderRadius(17)
            .justifyContent(FlexAlign.Center)
            .onClick(() => { this.onAddSample() })
          }
          .width('100%').padding(16).alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .margin({ top: 10 })

build() 是组件的构建函数,所有 UI 描述都在此函数内完成。最外层是 Scroll 滚动容器,内部包裹 Column 垂直布局。Scroll 是 ArkTS 的滚动容器组件,当内容超出可视区域时自动支持上下滑动,scrollable(ScrollDirection.Vertical) 指定垂直滚动方向。

Stack 是层叠布局容器,它将子元素按声明顺序从底层到顶层堆叠。这里在 Stack 中放置了四层元素:最底层是带渐变背景的 Column,中间两层是飘动的 emoji 装饰,最顶层是品牌文字和按钮。

linearGradient 方法为 Column 设置线性渐变背景。direction: GradientDirection.RightBottom 表示渐变方向从左上到右下,colors 数组定义渐变断点:['#7B5EA7', 0] 表示起点为紫罗兰紫,['#5E4688', 1] 表示终点为深紫。这种渐变效果比纯色更有层次感。

两个 Text 组件通过 translate 属性实现位移动画。🧵 向右移动 weaveShift 像素,🪟 向左移动 weaveShift 像素(负值),两者方向相反,营造出交错飘动的效果。opacity 绑定到动画状态,使 emoji 的透明度也在 0.3 到 0.85 之间呼吸变化。position 属性使用百分比和像素值混合定位,精确控制装饰元素的位置。

Row 容器内部使用三栏布局:左侧是品牌文字 Column,中间用 Column().layoutWeight(1) 占据剩余空间形成弹性间距,右侧是"免费小样"按钮。layoutWeight(1) 是 ArkTS 的弹性权重属性,它让组件占据父容器中剩余的全部空间。justifyContent(FlexAlign.Center) 让按钮内部文字水平居中。

FlexAlign 是 ArkTS 中的弹性对齐枚举,定义了主轴方向上的对齐方式。FlexAlign.Center 表示居中对齐,FlexAlign.Start 表示主轴起点对齐,FlexAlign.End 表示主轴终点对齐,FlexAlign.SpaceBetween 表示两端对齐且元素间距相等,FlexAlign.SpaceAround 表示每个元素两侧间距相等。

十四、首页内容组件(第三部分:统计四格与风格横滑)

        Row() {
          ForEach(FB_STATS, (s: FBDataStat) => {
            Column() {
              Text(s.icon).fontSize(14)
              Row() {
                Text(s.value).fontSize(16).fontWeight(FontWeight.Bold).fontColor(s.color)
                Text(s.sub).fontSize(9).fontColor('#7A7290').margin({ left: 1 })
              }
              .margin({ top: 3 }).alignItems(VerticalAlign.Bottom)
              Text(s.label).fontSize(9).fontColor('#7A7290').margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .margin({ right: 6 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%').margin({ top: 10 })

统计四格使用 Row 横向布局包裹 ForEach 渲染的四个 ColumnForEach 接收 FB_STATS 数组和渲染函数,函数参数 s 是当前数据项。每个 Column 使用 layoutWeight(1) 等分宽度,形成四等分网格。

每个统计卡片内部从上到下依次是:emoji 图标、数值行(数值 + 单位)、标签文字。数值行的 alignItems(VerticalAlign.Bottom) 让数值和单位底部对齐,因为数值的字号大于单位,底部对齐能保证两者的基线一致,视觉上更协调。fontWeight(FontWeight.Bold) 让数值加粗突出。

        Row() {
          Text('🖼️ 本季灵感风格').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          Column().layoutWeight(1)
          Text('左滑浏览 6 系列').fontSize(9).fontColor('#B99B7A')
        }
        .width('100%').margin({ top: 16 })
        Scroll() {
          Row() {
            ForEach(FB_STYLES, (st: FBDataStyle) => {
              Stack() {
                Column()
                  .width(190).height(150)
                  .backgroundColor(st.colorHex)
                  .borderRadius(16)
                Column() {
                  Text(st.icon).fontSize(30).opacity(0.9)
                  Text(st.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                    .margin({ top: 8 })
                  Text(st.desc).fontSize(9).fontColor('#3D3352').opacity(0.75)
                    .maxLines(2).margin({ top: 4 })
                  Row() {
                    Text(st.count.toString() + ' 户已搭配').fontSize(8).fontColor('#5E4688')
                      .backgroundColor('rgba(255,255,255,0.75)')
                      .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                      .borderRadius(9)
                  }
                  .margin({ top: 7 })
                }
                .width(190).height(150)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .padding({ left: 10, right: 10 })
              }
              .width(190).height(150)
              .margin({ right: 10 })
            })
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .width('100%').margin({ top: 8 }).scrollBar(BarState.Off)

风格横滑区域是首页的核心视觉模块。外层 Scroll 设置 scrollable(ScrollDirection.Horizontal) 启用水平滚动,scrollBar(BarState.Off) 隐藏滚动条,保持界面干净。每个风格卡片使用 Stack 层叠:底层是风格代表色的色块,上层是文字信息。

maxLines(2) 限制描述文字最多显示两行,超出部分自动截断。opacity(0.75) 让描述文字半透明,降低视觉权重,使风格名称更突出。"户已搭配"标签使用 rgba(255,255,255,0.75) 的半透明白色背景,在彩色色块上既能保证文字可读性,又不会过于突兀。

Scroll 组件在 ArkTS 中有水平滚动和垂直滚动两种模式,通过 scrollable 方法配置。横滑卡片流是电商应用首页的常见模式,配合 scrollBar(BarState.Off) 隐藏滚动条,可以实现流畅的轮播浏览体验。

十五、首页内容组件(第四部分:功能宫格与分布条)

        Text('🧩 织语服务').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          .width('100%').margin({ top: 16 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(FB_GRIDS, (g: FBDataGrid) => {
            Column() {
              Column() {
                Text(g.icon).fontSize(18)
              }
              .width(40).height(40)
              .backgroundColor(g.bg)
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)
              Text(g.label).fontSize(9).fontColor('#3D3352').margin({ top: 6 })
            }
            .width('24%')
            .padding({ top: 10, bottom: 10 })
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              if (g.label === '面料小样') {
                this.onAddSample()
              } else if (g.label === '上门量窗' || g.label === '安装服务') {
                this.onBookVisit()
              } else if (g.label === '窗帘定制') {
                this.onGoto(FBTab.Curtain)
              } else if (g.label === '软装方案') {
                this.onGoto(FBTab.Plan)
              } else if (g.label === '旧帘换新') {
                this.onGoto(FBTab.Visit)
              } else if (g.label === '我的收藏') {
                this.onGoto(FBTab.Mine)
              }
            })
          })
        }
        .width('100%')
        .padding({ top: 6, bottom: 10, left: 4, right: 4 })
        .backgroundColor('#FFFFFF')
        .borderRadius(14)
        .margin({ top: 8 })

功能宫格使用 Flex 组件配合 wrap: FlexWrap.Wrap 实现自动换行的网格布局。Flex 是 ArkTS 的弹性布局容器,FlexWrap.Wrap 表示当一行排满后自动换行到下一行。每个宫格项宽度设为 24%,一行正好排列四个,两行排列完八个。

FlexRow 的区别在于:Row 不会自动换行,当子元素总宽度超出容器宽度时会溢出或被压缩;Flex 通过 wrap 属性可以自动换行,更适合网格类布局。每个宫格项的 onClick 根据标签名称路由到不同的弹框或 Tab 页面,通过条件判断实现功能分发。

        Column() {
          ForEach(FB_STYLE_DIST, (d: FBDataDist) => {
            Row() {
              Text(d.style).fontSize(10).fontColor('#3D3352').width(46)
              Row() {
                Column()
                  .width(fbDistW(d.count))
                  .height(9)
                  .backgroundColor(d.color)
                  .borderRadius(4)
              }
              .width('100%').height(9)
              .backgroundColor('#F0E6D8')
              .borderRadius(4)
              .justifyContent(FlexAlign.Start)
              .layoutWeight(1)
              Text(d.count.toString() + ' 单').fontSize(9).fontColor('#7A7290').width(36)
            }
            .width('100%')
            .padding({ top: 6, bottom: 6 })
            .alignItems(VerticalAlign.Center)
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(14)
        .margin({ top: 8 })

风格分布条通过嵌套 RowColumn 手工绘制水平进度条。外层 Row 是一行三列布局:风格名称(固定宽度 46)、进度条(弹性宽度)、数值(固定宽度 36)。进度条本身是一个外层 Row(灰色背景)包裹内层 Column(彩色填充),内层 Column 的宽度通过 fbDistW(d.count) 计算得出。

这种"外层灰色轨道 + 内层彩色填充"的双层结构是手工进度条的经典实现。justifyContent(FlexAlign.Start) 确保彩色填充从左侧起点开始。通过纯函数计算百分比宽度,避免了引入图表库的开销,代码简洁且可维护性高。

十六、首页内容组件(第五部分:热销榜与底部小样横幅)

        ForEach(fbTopSales(), (c: FBDataCurtain, i: number) => {
          Row() {
            Text(i === 0 ? '🥇' : (i === 1 ? '🥈' : (i === 2 ? '🥉' : (i + 1).toString())))
              .fontSize(13).fontWeight(FontWeight.Bold).fontColor(i < 3 ? '#B99B7A' : '#7A7290')
              .width(22)
            Column()
              .width(34).height(34)
              .backgroundColor(c.colorHex)
              .borderRadius(10)
              .justifyContent(FlexAlign.Center)
            Text(c.icon).fontSize(15).width(26).textAlign(TextAlign.Center)
            Column() {
              Text(c.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                .maxLines(1)
              Row() {
                Column()
                  .width(fbSalesW(c.sales))
                  .height(5)
                  .backgroundColor(i === 0 ? '#B99B7A' : '#7B5EA7')
                  .borderRadius(3)
              }
              .width('100%').height(5)
              .backgroundColor('#F0E6D8')
              .borderRadius(3)
              .margin({ top: 4 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 4 })
            Column() {
              Text(fbMoney(c.price)).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
              Text('售 ' + c.sales.toString()).fontSize(8).fontColor('#B4AAC6').margin({ top: 2 })
            }
            .alignItems(VerticalAlign.End)
          }
          .width('100%')
          .padding({ top: 9, bottom: 9, left: 10, right: 10 })
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 6 })
          .alignItems(VerticalAlign.Center)
        })

热销窗帘榜通过 fbTopSales() 函数获取销量前五的窗帘,使用 ForEach 渲染榜单卡片。每行包含排名徽章、色块、商品信息(名称 + 销量进度条)、价格和销量。前三名分别使用金银铜奖牌 emoji,第四名及之后显示数字排名。

textAlign(TextAlign.Center) 让文字在固定宽度的区域内居中对齐。TextAlign 枚举有 Start(左对齐)、Center(居中)、End(右对齐)三种值。销量进度条使用 fbSalesW 函数将销量转为相对最大销量的百分比宽度,第一名用亚麻棕、其余用紫罗兰紫,形成色彩区分。

        Row() {
          Text('📮').fontSize(20)
          Column() {
            Text('摸得到才敢下单').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3D3352')
            Text('12 种面料免费寄小样 · 顺丰到付回寄免邮').fontSize(9).fontColor('#7A7290')
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start).padding({ left: 10 }).layoutWeight(1)
          Text('申请小样').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .backgroundColor('#7B5EA7')
            .padding({ left: 12, right: 12, top: 7, bottom: 7 })
            .borderRadius(14)
            .onClick(() => { this.onAddSample() })
        }
        .width('100%')
        .padding(14)
        .backgroundColor('#EDE7F5')
        .borderRadius(14)
        .margin({ top: 14 })
        .alignItems(VerticalAlign.Center)

底部小样横幅是一个 Row 容器,三栏布局:左侧 emoji 图标、中间文案、右侧按钮。背景色使用雾紫浅 #EDE7F5,与统计卡片的白色背景形成层次对比。onClick 调用 onAddSample 回调打开小样申请弹框。

这个横幅的设计体现了软装行业的用户心理:"摸得到才敢下单"精准抓住了面料购买的核心痛点。免费小样寄送服务降低了决策门槛,是提高转化率的有效手段。

十七、窗帘内容组件(第一部分:遮光实验室横幅与分类)

@Component
struct FABRICCurtainContent {
  @State activeCat: string = '全部'
  @State veilSwing: number = 0
  @State veilOpacity: number = 0.45
  onEditCurtain: (idx: number) => void = (idx: number) => {}
  onCurtainDetail: (idx: number) => void = (idx: number) => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.veilSwing = 6
      this.veilOpacity = 0.9
    })
  }

窗帘内容组件 FABRICCurtainContent 拥有三个状态变量。activeCat 记录当前选中的分类,初始值为"全部"。veilSwingveilOpacity 是纱帘摆动动画的状态变量。与首页横幅的平移动画不同,这里使用 rotate 旋转变换来模拟纱帘随风摆动的效果。

三个回调函数分别处理编辑量尺、查看详情和页面跳转。onEditCurtainonCurtainDetail 都接收窗帘索引参数 idx,但索引的计算方式是 c.id - 1,即从商品 ID 映射到数组索引。

  build() {
    Scroll() {
      Column() {
        Stack() {
          Column()
            .width('100%').height(100)
            .linearGradient({
              direction: GradientDirection.RightBottom,
              colors: [['#C9BBDD', 0], ['#9B8AC4', 1]]
            })
            .borderRadius(16)
          Text('🪞').fontSize(26)
            .opacity(this.veilOpacity)
            .rotate({ x: 0, y: 0, z: 1, angle: this.veilSwing })
            .position({ x: '80%', y: 14 })
          Text('🌫️').fontSize(18)
            .opacity(this.veilOpacity * 0.8)
            .rotate({ x: 0, y: 0, z: 1, angle: -this.veilSwing })
            .position({ x: '88%', y: 60 })
          Column() {
            Text('遮光实验室').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
            Text('每款帘子都有实测遮光率与褶皱倍数报告').fontSize(9).fontColor('#5E4688')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .position({ x: 16, y: 24 })
        }
        .width('100%')
        .margin({ top: 10 })

"遮光实验室"横幅使用雾灰紫渐变背景。两个装饰 emoji 分别用正负角度旋转,营造镜子与雾气交错摆动的效果。rotate 方法接收一个对象参数,xyz 定义旋转轴向量,angle 定义旋转角度。z: 1 表示绕 Z 轴旋转(即平面内旋转),这是 2D 旋转的标准写法。

position 属性使用绝对定位,将元素放置在 Stack 内的指定坐标。与 CSS 的 position: absolute 类似,ArkTS 的 position 让元素脱离文档流,精确定位到容器内的指定位置。这里品牌文字定位在 (16, 24),两个装饰 emoji 分别定位在右上角区域。

        Scroll() {
          Row() {
            ForEach(FB_CURTAIN_CATS, (c: FBDataCat) => {
              if (this.activeCat === c.name) {
                Text(c.icon + ' ' + c.name).fontSize(11).fontColor('#FFFFFF')
                  .backgroundColor('#7B5EA7')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .margin({ right: 8 })
              } else {
                Text(c.icon + ' ' + c.name).fontSize(11).fontColor('#7A7290')
                  .backgroundColor('#FFFFFF')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .margin({ right: 8 })
                  .onClick(() => { this.activeCat = c.name })
              }
            })
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .width('100%').margin({ top: 10 }).scrollBar(BarState.Off)

分类筛选条使用横向 Scroll 包裹 Row,通过 ForEach 渲染六个分类标签。这里使用了 ArkTS 的条件渲染语法:if (this.activeCat === c.name) 判断当前分类是否被选中,选中时显示白字紫底样式,未选中时显示灰字白底样式,点击未选中项时更新 activeCat 状态。

条件渲染是 ArkTS 的核心特性之一。在 build() 函数内使用 if/else 可以根据条件动态渲染不同的 UI 树分支。当条件变化时,框架会自动增删对应的 DOM 节点。这里每次点击分类标签,activeCat 变化会触发 ForEach 重新渲染,选中项的样式立即更新。

十八、窗帘内容组件(第二部分:窗帘大卡)

        ForEach(fbFilterCurtains(this.activeCat), (c: FBDataCurtain) => {
          Column() {
            Stack() {
              Column()
                .width('100%').height(64)
                .backgroundColor(c.colorHex)
                .borderRadius({ topLeft: 14, topRight: 14 })
              Text(c.icon).fontSize(26)
              Text('褶皱 ×' + c.fold.toString())
                .fontSize(9).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                .backgroundColor(fbFoldColor(c.fold))
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                .borderRadius(10)
                .position({ x: '72%', y: 8 })
              Text(c.tag)
                .fontSize(8).fontColor('#5E4688')
                .backgroundColor('rgba(255,255,255,0.8)')
                .padding({ left: 7, right: 7, top: 2, bottom: 2 })
                .borderRadius(8)
                .position({ x: 10, y: 8 })
            }
            .width('100%').height(64)

每款窗帘以大卡片形式展示,卡片由色块头部和信息体两部分组成。色块头部使用 Stack 层叠:底层是窗帘代表色的色块(仅上方圆角),上层是窗帘 emoji、褶皱倍数徽章和营销标签。borderRadius({ topLeft: 14, topRight: 14 }) 分别设置左上和右上圆角,左下和右下保持直角,形成"上半圆角"效果。

褶皱徽章使用 fbFoldColor 函数根据倍数返回不同颜色,倍数越高颜色越深。position({ x: '72%', y: 8 }) 将徽章定位到色块右上角。营销标签使用半透明白色背景,定位到左上角。

            Column() {
              Row() {
                Text(c.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                  .layoutWeight(1).maxLines(1)
                Text(fbMoney(c.price)).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
                Text('/㎡').fontSize(9).fontColor('#B4AAC6')
              }
              .width('100%').alignItems(VerticalAlign.Bottom)

              Row() {
                Text('遮光率').fontSize(9).fontColor('#7A7290').width(34)
                Row() {
                  Column()
                    .width(fbDimW(c.dim))
                    .height(7)
                    .backgroundColor(c.dim >= 90 ? '#5E4688' : (c.dim >= 70 ? '#7B5EA7' : '#C9BBDD'))
                    .borderRadius(4)
                }
                .width('100%').height(7)
                .backgroundColor('#F0E6D8')
                .borderRadius(4)
                .justifyContent(FlexAlign.Start)
                .layoutWeight(1)
                Text(c.dim.toString() + '%').fontSize(9).fontWeight(FontWeight.Bold)
                  .fontColor(c.dim >= 90 ? '#5E4688' : '#7B5EA7').width(30).textAlign(TextAlign.End)
              }
              .width('100%').margin({ top: 9 }).alignItems(VerticalAlign.Center)

信息体从上到下依次是商品名称行、遮光率进度条行、适用空间标签行和操作按钮行。商品名称行使用 layoutWeight(1) 让名称占据弹性空间,价格右对齐。遮光率进度条与前述风格分布条结构一致,但颜色根据遮光率分段:90% 以上用深紫,70% 到 90% 用标准紫,70% 以下用雾灰紫。这种分段着色让用户通过颜色深度直观判断遮光等级。

适用空间标签行将 spaces 字段以斜杠分隔为两部分,分别用不同背景色的标签展示。操作按钮行提供"编辑尺寸"和"查看详情"两个按钮,分别触发 onEditCurtainonCurtainDetail 回调,索引均传入 c.id - 1

十九、窗帘内容组件(第三部分:遮光率对比条)

        Column() {
          ForEach(fbTopDim(), (c: FBDataCurtain, i: number) => {
            Row() {
              Text((i + 1).toString()).fontSize(10).fontWeight(FontWeight.Bold).fontColor('#B99B7A').width(16)
              Column()
                .width(18).height(18)
                .backgroundColor(c.colorHex)
                .borderRadius(5)
              Text(c.name).fontSize(9).fontColor('#3D3352').width(96).maxLines(1)
                .margin({ left: 6 })
              Row() {
                Column()
                  .width(fbDimW(c.dim))
                  .height(8)
                  .backgroundColor(c.dim >= 95 ? '#5E4688' : '#7B5EA7')
                  .borderRadius(4)
              }
              .width('100%').height(8)
              .backgroundColor('#F0E6D8')
              .borderRadius(4)
              .justifyContent(FlexAlign.Start)
              .layoutWeight(1)
              Text(c.dim.toString() + '%').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#5E4688')
                .width(30).textAlign(TextAlign.End)
            }
            .width('100%')
            .padding({ top: 7, bottom: 7 })
            .alignItems(VerticalAlign.Center)
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(14)
        .margin({ top: 8 })

遮光率对比条通过 fbTopDim() 获取遮光率前六的窗帘,以进度条形式对比展示。每行包含排名序号、色块、商品名称、遮光率进度条和百分比数值。95% 以上的进度条用深紫,其余用标准紫。这个对比模块帮助用户快速找到遮光性能最强的窗帘,是窗帘选购的核心决策维度之一。

maxLines(1) 配合固定宽度 96,确保商品名称超出宽度时自动截断而非换行,保持行高一致性。margin({ left: 6 }) 在色块和名称之间添加 6 的间距。

二十、面料内容组件(第一部分:促销横幅与色卡装饰)

@Component
struct FABRICFabricContent {
  @State chipFloat: number = 0
  @State chipOpacity: number = 0.4
  onAddSample: () => void = () => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.chipFloat = -5
      this.chipOpacity = 0.95
    })
  }

面料内容组件 FABRICFabricContent 使用 chipFloatchipOpacity 两个状态变量驱动色卡浮沉动画。与前两个组件的飘动和摆动不同,这里使用 translate 的 Y 轴位移来模拟色卡上下浮动的效果,动画状态从 0 变到 -5(向上移动 5 像素)。

        Row() {
          Column() {
            Text('🧵 亚麻织造节').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('面料满 5 米减 30 · 小样免费申领').fontSize(9).fontColor('#EDE7F5').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
          Text('包邮').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#5E4688')
            .backgroundColor('#FFFFFF')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(14)
        }
        .width('100%')
        .padding(14)
        .linearGradient({
          direction: GradientDirection.RightBottom,
          colors: [['#B99B7A', 0], ['#5E4688', 1]]
        })
        .borderRadius(14)
        .margin({ top: 10 })
        .alignItems(VerticalAlign.Center)

亚麻织造节促销横幅使用从亚麻棕到深紫的渐变背景,左侧是促销文案,右侧是"包邮"标签。这个渐变方向从左上到右下,亚麻棕在左上区域,深紫在右下区域,形成色彩过渡感。layoutWeight(1) 让左侧文案占据弹性空间,"包邮"标签紧贴右侧。

        Row() {
          Text('🟣').fontSize(13)
            .opacity(this.chipOpacity)
            .translate({ x: 0, y: this.chipFloat })
          Text('🟤').fontSize(13)
            .opacity(this.chipOpacity * 0.8)
            .translate({ x: 0, y: -this.chipFloat })
            .margin({ left: 8 })
          Text('◻️').fontSize(13)
            .opacity(this.chipOpacity * 0.6)
            .translate({ x: 0, y: this.chipFloat })
            .margin({ left: 8 })
          Column().layoutWeight(1)
          Text('织物小知识:褶皱倍数越高,垂感越优雅').fontSize(8).fontColor('#B4AAC6')
        }
        .width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)

悬浮色卡装饰行展示三个色块 emoji,分别以不同透明度和位移方向浮动。紫色块向上移动,棕色块向下移动(-this.chipFloat 取反值),白色块向上移动。通过调整透明度倍数(1.0、0.8、0.6)形成景深感,前景色块更清晰,后景色块更模糊。右侧是一句织物小知识文案,增加页面的专业感和信息密度。

二十一、面料内容组件(第二部分:色彩 Grid 与价格榜)

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(FBFABRICS, (f: FBDataFabric) => {
            Column() {
              Stack() {
                Column()
                  .width('100%').height(58)
                  .backgroundColor(f.colorHex)
                  .borderRadius({ topLeft: 10, topRight: 10 })
                Text(f.name.slice(0, 1)).fontSize(20).fontColor('#3D3352').opacity(0.8)
                if (f.price >= 100) {
                  Text('💎').fontSize(11)
                    .opacity(this.chipOpacity)
                    .translate({ x: 0, y: this.chipFloat })
                    .position({ x: '78%', y: 4 })
                }
              }
              .width('100%').height(58)
              Column() {
                Text(f.name).fontSize(9).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                  .maxLines(1)
                Text(f.comp).fontSize(7).fontColor('#9B8AC4')
                  .backgroundColor('#EDE7F5')
                  .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                  .borderRadius(6)
                  .margin({ top: 3 })
                Row() {
                  Text(fbMoney(f.price)).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
                  Text('/米').fontSize(7).fontColor('#B4AAC6')
                }
                .margin({ top: 3 }).alignItems(VerticalAlign.Bottom)
                Text(f.stock.toString() + ' 米现货').fontSize(7).fontColor('#B99B7A')
                  .margin({ top: 2 })
              }
              .width('100%').padding(6).alignItems(HorizontalAlign.Start)
            }
            .width('31%')
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ top: 8, right: 8 })
            .onClick(() => { this.onAddSample() })
          })
        }
        .width('100%').margin({ top: 4 })

十二色面料墙使用 Flex 换行布局,每个色卡宽度为 31%,一行排列三个。每个色卡由色块头部和信息体组成。色块头部使用 Stack 层叠面料代表色背景和面料名称首字。当价格大于等于 100 元时,额外显示钻石 emoji 作为高价标识,并应用浮动动画。

信息体展示面料名称、成分标签、价格行和库存信息。成分标签使用雾紫浅背景的小胶囊样式,与商品名称形成视觉层次。slice(0, 1) 提取面料名称的第一个字符作为色块上的大字展示,这是中文面料命名的常见简化方式。onClick 绑定到 onAddSample 回调,点击任何色卡都可打开小样申请弹框。

        Column() {
          ForEach(fbTopFabrics(), (f: FBDataFabric, i: number) => {
            Row() {
              Text((i + 1).toString()).fontSize(9).fontWeight(FontWeight.Bold)
                .fontColor(i < 3 ? '#B99B7A' : '#B4AAC6').width(14)
              Column()
                .width(16).height(16)
                .backgroundColor(f.colorHex)
                .borderRadius(4)
              Text(f.name).fontSize(9).fontColor('#3D3352').width(78).maxLines(1)
                .margin({ left: 6 })
              Row() {
                Column()
                  .width(fbPriceW(f.price))
                  .height(8)
                  .backgroundColor(f.price >= 100 ? '#5E4688' : (f.price >= 60 ? '#7B5EA7' : '#C9BBDD'))
                  .borderRadius(4)
              }
              .width('100%').height(8)
              .backgroundColor('#F0E6D8')
              .borderRadius(4)
              .justifyContent(FlexAlign.Start)
              .layoutWeight(1)
              Text(fbMoney(f.price)).fontSize(9).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
                .width(34).textAlign(TextAlign.End)
            }
            .width('100%')
            .padding({ top: 6, bottom: 6 })
            .alignItems(VerticalAlign.Center)
          })
        }

价格榜单通过 fbTopFabrics() 获取按价格降序排列的全部面料。进度条颜色根据价格分段:100 元以上用深紫,60 到 100 元用标准紫,60 元以下用雾灰紫。前三名的排名序号用亚麻棕,其余用浅紫,形成金银铜的视觉暗示。

二十二、方案内容组件

@Component
struct FABRICPlanContent {
  @State breathScale: number = 1
  @State breathOpacity: number = 0.3
  onPalette: (idx: number) => void = (idx: number) => {}
  onDeletePlan: (idx: number) => void = (idx: number) => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.breathScale = 1.15
      this.breathOpacity = 0.8
    })
  }

方案内容组件 FABRICPlanContent 使用 breathScalebreathOpacity 驱动呼吸动画。与前面的位移和旋转不同,这里使用 scale 缩放变换来模拟色块呼吸效果,缩放比例从 1.0 到 1.15,配合透明度从 0.3 到 0.8 的变化,形成"忽大忽明"的呼吸节奏。

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(FBPLANS, (p: FBDataPlan, idx: number) => {
            Column() {
              Stack() {
                Row() {
                  Column().width(p.r1 + '%').height(44).backgroundColor(p.c1)
                  Column().width(p.r2 + '%').height(44).backgroundColor(p.c2)
                  Column().width(p.r3 + '%').height(44).backgroundColor(p.c3)
                  Column().width(p.r4 + '%').height(44).backgroundColor(p.c4)
                }
                .width('100%').height(44)
                .borderRadius({ topLeft: 14, topRight: 14 })
                .clip(true)
                Text(p.icon).fontSize(20)
                  .opacity(0.9)
                  .scale({ x: this.breathScale * 0.96, y: this.breathScale * 0.96 })
                Text('✕').fontSize(10).fontColor('#FFFFFF')
                  .padding(4)
                  .position({ x: '86%', y: 2 })
                  .onClick(() => { this.onDeletePlan(idx) })
              }
              .width('100%').height(44)

方案卡片的核心创新是配色比例条。在 Stack 内,底层是一个 Row 包含四个 Column,宽度分别为 r1r4 的百分比,背景色为 c1c4。这四个色块紧密排列,形成一条按比例分段的彩色条带,直观展示方案的配色构成。

clip(true) 是 ArkTS 的裁剪属性,它将子元素超出容器边界的部分裁剪掉。这里配色比例条设置了上方圆角 borderRadius({ topLeft: 14, topRight: 14 }),但内部的四个色块是直角的,clip(true) 确保色块不会溢出圆角区域。删除按钮 定位在右上角,点击触发 onDeletePlan 回调。

配色比例条 Row

主色 Column 40%

底色 Column 30%

点缀色 Column 20%

强调色 Column 10%

backgroundColor: c1

backgroundColor: c2

backgroundColor: c3

backgroundColor: c4

上图展示了配色比例条的内部结构。四个 ColumnRow 内紧密排列,各自占据百分比宽度,背景色对应方案的四色配置。clip(true) 保证整体圆角效果不被内部直角色块破坏。

二十三、上门服务内容组件(第一部分:排期表)

@Component
struct FABRICVisitContent {
  @State dotShift: number = 0
  @State dotOpacity: number = 0.3
  onBookVisit: () => void = () => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.dotShift = 8
      this.dotOpacity = 0.95
    })
  }

上门服务内容组件 FABRICVisitContent 使用 dotShiftdotOpacity 驱动时间轴光点流动动画。这个动画将应用在预约记录时间轴的连接线上,模拟光点沿时间轴流动的效果。

        Column() {
          Row() {
            Text('服务项目').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#7A7290')
              .layoutWeight(1)
            Text('今日').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#7A7290').width(36)
              .textAlign(TextAlign.Center)
            Text('周六').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#7A7290').width(36)
              .textAlign(TextAlign.Center)
            Text('周日').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#7A7290').width(36)
              .textAlign(TextAlign.Center)
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 })
          .backgroundColor('#EDE7F5')
          .borderRadius({ topLeft: 10, topRight: 10 })
          ForEach(FBVISITS, (v: FBDataVisit, i: number) => {
            Row() {
              Row() {
                Text(v.icon).fontSize(12)
                Column() {
                  Text(v.name).fontSize(10).fontWeight(FontWeight.Bold).fontColor('#3D3352').maxLines(1)
                  Text(v.duration + ' · ' + v.desc).fontSize(7).fontColor('#B4AAC6').maxLines(1)
                    .margin({ top: 1 })
                }
                .alignItems(HorizontalAlign.Start).padding({ left: 5 })
              }
              .layoutWeight(1).alignItems(VerticalAlign.Center)
              Text(v.today).fontSize(9).fontWeight(FontWeight.Bold)
                .fontColor(fbSlotColor(v.today)).width(36).textAlign(TextAlign.Center)
              Text(v.sat).fontSize(9).fontWeight(FontWeight.Bold)
                .fontColor(fbSlotColor(v.sat)).width(36).textAlign(TextAlign.Center)
              Text(v.sun).fontSize(9).fontWeight(FontWeight.Bold)
                .fontColor(fbSlotColor(v.sun)).width(36).textAlign(TextAlign.Center)
            }
            .width('100%')
            .padding({ top: 8, bottom: 8, left: 8, right: 8 })
            .backgroundColor(i % 2 === 0 ? '#FFFFFF' : '#FBF8F4')
            .alignItems(VerticalAlign.Center)
          })
        }
        .width('100%')
        .borderRadius(10)
        .margin({ top: 8 })

服务排期表是一个四列表格:服务项目、今日、周六、周日。表头行使用雾紫浅背景,数据行通过 i % 2 === 0 判断奇偶行,交替使用白色和极浅米色背景,实现斑马条纹效果。可约余量的文字颜色通过 fbSlotColor 函数计算:"余 N"用紫罗兰紫,"满"用浅紫灰,"预约"用深紫。

这个排期表的设计简洁实用,用户一眼就能看出每项服务在三个时间段的可约情况。奇偶行交替背景色降低了长表格的视觉疲劳,是数据表格设计的基本原则。maxLines(1) 确保服务名称和描述超出宽度时截断,不会破坏表格的行高一致性。

二十四、上门服务内容组件(第二部分:时间轴)

        ForEach(FBBOOKINGS, (b: FBDataBooking) => {
          Row() {
            Column() {
              Column()
                .width(10).height(10)
                .backgroundColor(fbStatusColor(b.status))
                .borderRadius(5)
              Column()
                .width(2).height(30)
                .backgroundColor('#C9BBDD')
                .opacity(this.dotOpacity)
                .translate({ x: 0, y: this.dotShift * 0.3 })
                .margin({ top: 3 })
            }
            .width(20)
            .alignItems(HorizontalAlign.Center)

            Column() {
              Row() {
                Text(b.service).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                  .layoutWeight(1).maxLines(1)
                Text(b.status).fontSize(8).fontColor(fbStatusColor(b.status))
                  .backgroundColor(fbStatusBg(b.status))
                  .padding({ left: 7, right: 7, top: 3, bottom: 3 })
                  .borderRadius(8)
              }
              .width('100%').alignItems(VerticalAlign.Center)
              Row() {
                Text('👷 ' + b.master).fontSize(8).fontColor('#7A7290')
                Text('📅 ' + b.date + ' ' + b.time).fontSize(8).fontColor('#7A7290')
                  .margin({ left: 8 })
              }
              .width('100%').margin({ top: 4 })
              Text('📍 ' + b.addr).fontSize(8).fontColor('#B4AAC6')
                .margin({ top: 3 })
            }
            .layoutWeight(1)
            .padding({ left: 6, top: 2, bottom: 8 })
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .alignItems(VerticalAlign.Top)
          .padding({ left: 4 })
        })

预约时间轴通过手工绘制实现。每条记录由左右两列组成:左列是时间轴轨道(圆点 + 连接线),右列是记录信息。圆点的颜色通过 fbStatusColor 根据状态映射:已完成为紫、进行中为棕、待上门为雾灰紫、其他为深紫墨。连接线使用雾灰紫色,配合 dotOpacitydotShift * 0.3 实现光点流动效果。

时间轴是 UX 设计中展示历史和未来事件流的经典模式。圆点表示事件节点,连接线表示时间流逝。这里将动画绑定到连接线的 opacitytranslate 属性,使连接线呈现闪烁和微位移效果,暗示时间轴是"活的",增强了动态感。

在 ArkTS 中,时间轴这种复杂结构并非内置组件,而是通过 ColumnRowborderRadius 等基础组件的组合手工构建。这体现了声明式 UI 的灵活性:任何视觉需求都可以通过基础组件的嵌套和属性设置来实现。

二十五、我的内容组件(第一部分:会员卡与消费柱状图)

@Component
struct FABRICMineContent {
  @State glow: number = 0.25
  @State glowScale: number = 1
  @State favOn: boolean[] = [true, true, true, true, true, true]
  onStylePick: () => void = () => {}
  onEditCurtain: () => void = () => {}
  onAddSample: () => void = () => {}
  onGoto: (tab: FBTab) => void = (tab: FBTab) => {}

  aboutToAppear() {
    this.getUIContext().animateTo({
      duration: 2200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.glow = 0.6
      this.glowScale = 1.12
    })
  }

我的内容组件 FABRICMineContent 拥有三个状态变量。glowglowScale 驱动会员卡光晕呼吸动画,favOn 是一个布尔数组,记录六个收藏项的心形状态。这个布尔数组是 @State 管理数组状态的典型用法:初始值全部为 true(已收藏),点击切换时通过 fbToggleAt 函数返回新数组触发更新。

        Stack() {
          Column()
            .width('100%').height(120)
            .linearGradient({
              direction: GradientDirection.RightBottom,
              colors: [['#3D3352', 0], ['#7B5EA7', 1]]
            })
            .borderRadius(16)
          Column()
            .width(98).height(98)
            .borderRadius(49)
            .backgroundColor('#FFFFFF')
            .opacity(this.glow)
            .scale({ x: this.glowScale, y: this.glowScale })
            .position({ x: '76%', y: 10 })
          Text('🧵').fontSize(20)
            .opacity(this.glow)
            .position({ x: '86%', y: 78 })
          Row() {
            Column() {
              Row() {
                Text('亚麻生活家').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text('SVIP').fontSize(8).fontWeight(FontWeight.Bold).fontColor('#5E4688')
                  .backgroundColor('#F0E6D8')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(8)
                  .margin({ left: 8 })
              }
              Text('会员编号 FB-0827 · 8 月布艺节已省 ¥236').fontSize(9).fontColor('#EDE7F5')
                .margin({ top: 5 })
              Row() {
                Text('🎟️').fontSize(10)
                Text('积分 5,260').fontSize(9).fontColor('#EDE7F5').margin({ left: 3 })
                Text('·').fontSize(9).fontColor('#EDE7F5').margin({ left: 6 })
                Text('小样券 4 张').fontSize(9).fontColor('#EDE7F5').margin({ left: 6 })
              }
              .margin({ top: 8 })
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
          }
          .width('100%').padding(16)
        }
        .width('100%')
        .margin({ top: 10 })

会员卡使用 Stack 四层堆叠:最底层是深紫到紫罗兰的渐变背景,第二层是一个白色圆形光晕(通过 opacityscale 实现呼吸效果),第三层是品牌 emoji,最上层是会员信息文字。光晕的 borderRadius(49) 是直径 98 的一半,形成完美圆形。

会员卡信息包含会员等级名称、SVIP 徽章、会员编号、省钱记录、积分和小样券。这些信息让用户感受到会员价值,增强平台粘性。position({ x: '76%', y: 10 }) 将光晕定位到卡片右侧,形成"光从右上方照来"的视觉效果。

        Row() {
          ForEach(FBSPEND, (s: FBDataSpend) => {
            Column() {
              Text(fbMoney(s.amount)).fontSize(8).fontWeight(FontWeight.Bold)
                .fontColor(s.amount === 1280 ? '#B99B7A' : '#7B5EA7')
              Column()
                .width(20)
                .height(fbSpendH(s.amount))
                .backgroundColor(s.amount === 1280 ? '#B99B7A' : '#7B5EA7')
                .borderRadius({ topLeft: 5, topRight: 5 })
                .margin({ top: 4 })
              Text(s.month).fontSize(9).fontColor('#7A7290').margin({ top: 4 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .padding({ top: 14, bottom: 12 })
        .backgroundColor('#FFFFFF')
        .borderRadius(14)
        .margin({ top: 8 })
        .alignItems(VerticalAlign.Bottom)

月消费柱状图通过 ForEach 渲染六根柱子。每根柱子是一个 Column,包含金额标签、柱体和月份标签。柱体高度通过 fbSpendH 函数计算,该函数将金额映射到 8 到 100 像素的高度区间。最高月份(1280 元)用亚麻棕色,其他月份用紫罗兰紫,使峰值一目了然。

borderRadius({ topLeft: 5, topRight: 5 }) 只设置上方圆角,模拟柱状图常见的圆顶效果。alignItems(VerticalAlign.Bottom) 让所有柱子在底部对齐,这是柱状图的标准对齐方式。整个柱状图完全由基础组件手工绘制,未引入任何图表库,体现了 ArkTS 声明式 UI 的灵活性。

二十六、我的内容组件(第二部分:订单与收藏列表)

        ForEach(FBORDERS, (o: FBDataOrder) => {
          Row() {
            Text('🛍️').fontSize(13)
            Column() {
              Text(o.item).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                .maxLines(1)
              Row() {
                Text(o.date).fontSize(8).fontColor('#B4AAC6')
                Text(o.status).fontSize(8).fontColor(fbOrderColor(o.status))
                  .backgroundColor('#F6F2EC')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(7)
                  .margin({ left: 8 })
              }
              .margin({ top: 3 }).alignItems(VerticalAlign.Center)
            }
            .alignItems(HorizontalAlign.Start).padding({ left: 10 }).layoutWeight(1)
            Text(fbMoney(o.price)).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
          }
          .width('100%')
          .padding({ top: 10, bottom: 10, left: 12, right: 12 })
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 5 })
          .alignItems(VerticalAlign.Center)
        })

订单列表通过 ForEach 渲染十二条订单。每条订单是一个 Row,包含购物袋 emoji、商品信息(名称 + 日期 + 状态标签)和金额。状态标签的颜色通过 fbOrderColor 函数映射,背景统一使用米色底,形成统一的胶囊样式。

        ForEach(FBFAVS, (f: FBDataFav, i: number) => {
          Row() {
            Stack() {
              Column()
                .width(34).height(34)
                .backgroundColor('#EDE7F5')
                .borderRadius(10)
              Text(f.icon).fontSize(16)
            }
            .width(34).height(34)
            Column() {
              Text(f.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                .maxLines(1)
              Text(f.type + ' · 织语甄选').fontSize(8).fontColor('#B4AAC6').margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start).padding({ left: 10 }).layoutWeight(1)
            Text(this.favOn[i] ? '❤️' : '🤍').fontSize(15)
              .onClick(() => { this.favOn = fbToggleAt(this.favOn, i) })
          }
          .width('100%')
          .padding({ top: 10, bottom: 10, left: 12, right: 12 })
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ top: 5 })
          .alignItems(VerticalAlign.Center)
        })

收藏列表的每项右侧有一个心形切换按钮。this.favOn[i]true 时显示红心 emoji,为 false 时显示白心 emoji。点击时调用 fbToggleAt(this.favOn, i) 返回新数组并赋值给 this.favOn,触发该收藏项的重新渲染。

这里体现了 ArkTS 数组状态管理的核心原则:不能直接修改数组元素(如 this.favOn[i] = !this.favOn[i]),因为这不会改变数组引用,框架无法检测到变化。必须通过返回新数组的方式(fbToggleAt 内部使用 slice() 复制后修改),让 @State 检测到引用变化,从而触发重新渲染。

二十七、主应用状态管理

@Entry
@Component
struct FABRICApp {
  @State activeTab: FBTab = FBTab.Home
  @State showAddSample: boolean = false
  @State showDeletePlan: boolean = false
  @State showEditCurtain: boolean = false
  @State showStylePick: boolean = false
  @State showBookVisit: boolean = false
  @State showPalette: boolean = false
  @State showCurtainDetail: boolean = false

  @State selPlanIdx: number = 0
  @State selCurtainIdx: number = 0
  @State selStyleIdx: number = 0

  @State sampleSel: boolean[] = [true, false, true, false, false, false, false, false, false, false, true, false]
  @State sampleAddr: string = ''
  @State samplePhone: string = ''

  @State editWidth: number = 3.6
  @State editDim: boolean = true
  @State editFold: number = 2

  @State visitRooms: number = 3
  @State visitDate: string = '周六 08-29'
  @State visitSlot: string = '上午 09-12'
  @State visitAddr: string = ''
  @State visitSvc: string = '免费上门量窗'

@Entry 装饰器标记 FABRICApp 为应用的入口组件。在 ArkTS 中,@Entry 告知框架这是页面级组件,会作为渲染树的根节点。一个页面只能有一个 @Entry 组件。

主组件的状态分为四组。第一组是 Tab 路由状态 activeTab 和七个弹框开关布尔值。第二组是三个选中项索引(方案、窗帘、风格)。第三组是小样申请表单字段(多选数组、地址、电话)。第四组是窗帘量尺编辑字段(宽度、遮光开关、褶皱倍数)和上门预约字段(房间数、日期、时段、地址、服务项目)。

这种分组管理状态的方式使代码结构清晰。每个弹框都有一组对应的状态变量,弹框的打开和关闭通过布尔开关控制,弹框内的表单数据通过独立的状态变量管理。当用户关闭再重新打开弹框时,状态保留上次的输入值,提供连续的用户体验。

sampleSel 的初始值是一个包含十二个布尔值的数组,其中第 0、2、10 项为 true(默认选中本色亚麻、雾灰紫雪尼尔、原色苎麻三种面料),其余为 false。这个数组的不可变更新同样使用 fbToggleAt 函数。

二十八、弹框遮罩构建器

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

@Builder 装饰器声明了一个构建器函数 modalOverlay@Builder 是 ArkTS 的方法级 UI 构建器,它允许将可复用的 UI 片段封装为函数,在 build() 中通过 this.xxx() 调用。与 @Component 不同,@Builder 不创建独立组件,而是在调用处内联展开。

modalOverlay 是所有弹框共享的遮罩层。它是一个全屏 Column,背景色为半透明深紫 rgba(61,51,82,0.55),点击时调用传入的 onClose 回调关闭弹框。通过参数化的方式,这个构建器可以被所有弹框复用,每个弹框只需传入自己的关闭函数。

rgba 颜色格式的最后一个参数 0.55 是透明度,55% 的不透明度使背景内容若隐若现,同时保证弹框内容足够清晰。这是模态弹框遮罩的标准透明度设置。

二十九、小样申请弹框

  @Builder addSampleModal() {
    Column() {
      this.modalOverlay(() => { this.showAddSample = false })
      Column() {
        Scroll() {
          Column() {
            Column().width(38).height(4).backgroundColor('#EAE2D6').borderRadius(2)
            Row() {
              Text('🧵 新增小样申请').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
              Column().layoutWeight(1)
              Text('✕').fontSize(16).fontColor('#B4AAC6')
                .padding(4)
                .onClick(() => { this.showAddSample = false })
            }
            .width('100%').margin({ top: 10 }).alignItems(VerticalAlign.Center)

            Text('勾选想摸的面料(可多选),小样 8cm × 8cm 免费寄送。').fontSize(9).fontColor('#7A7290')
              .width('100%').margin({ top: 6 })

            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(FBFABRICS, (f: FBDataFabric, i: number) => {
                Stack() {
                  Column()
                    .width(22).height(22)
                    .backgroundColor(f.colorHex)
                    .borderRadius(6)
                  if (this.sampleSel[i]) {
                    Text('✓').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                  }
                }
                .width(22).height(22)
                .margin({ right: 8, top: 8 })
                .onClick(() => { this.sampleSel = fbToggleAt(this.sampleSel, i) })
              })
            }
            .width('100%')

小样申请弹框是底部抽屉式设计。外层 Column 包含遮罩层和抽屉主体。抽屉主体使用 position({ x: 0, y: '38%' }) 定位在屏幕下方 38% 处,高度通过 constraintSize({ maxHeight: '62%' }) 限制,形成从底部弹出的抽屉效果。

抽屉顶部有一个 38x4 的灰色把手(borderRadius(2)),这是移动端抽屉设计的标准元素,暗示用户可以下拉关闭。面料多选区使用 Flex 换行布局,每个色块 22x22,点击切换选中状态。选中时在色块上层叠加白色对勾 emoji。

TextInput 组件用于地址和电话输入。placeholder 设置占位提示文字,placeholderColor 设置占位文字颜色,onChange 回调在输入变化时更新状态变量。TextInput 是 ArkTS 的文本输入组件,支持单行文本输入,通过 onChange 事件实时同步输入值到状态。

            Column() {
              Text('免费寄送小样').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            }
            .width('100%').height(42)
            .linearGradient({
              direction: GradientDirection.RightBottom,
              colors: [['#7B5EA7', 0], ['#5E4688', 1]]
            })
            .borderRadius(21)
            .justifyContent(FlexAlign.Center)
            .margin({ top: 16 })
            .onClick(() => { this.showAddSample = false })

提交按钮使用渐变背景和胶囊圆角(borderRadius(21),高度 42 的一半)。点击后关闭弹框。按钮文案明确告知"免费寄送",消除用户的价格顾虑。底部还有一行补充说明"小样寄出后 14 天内寄回可全额抵扣面料款",这是软装行业的特色政策。

三十、删除确认弹框

  @Builder deletePlanModal() {
    Column() {
      this.modalOverlay(() => { this.showDeletePlan = false })
      Column() {
        Text('🗑️').fontSize(34)
        Text('确认删除方案?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          .margin({ top: 8 })
        Column() {
          Text(FBPLANS[this.selPlanIdx].name).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#5E4688')
          Text('删除后配色卡与报价单将一并清除,无法恢复。').fontSize(9).fontColor('#7A7290')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#EDE7F5')
        .borderRadius(12)
        .margin({ top: 12 })
        .alignItems(HorizontalAlign.Center)
        Row() {
          Column() {
            Text('再想想').fontSize(12).fontColor('#3D3352')
          }
          .layoutWeight(1).height(38)
          .backgroundColor('#F0E6D8')
          .borderRadius(19)
          .justifyContent(FlexAlign.Center)
          .margin({ right: 10 })
          .onClick(() => { this.showDeletePlan = false })
          Column() {
            Text('确认删除').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          }
          .layoutWeight(1).height(38)
          .backgroundColor('#7B5EA7')
          .borderRadius(19)
          .justifyContent(FlexAlign.Center)
          .onClick(() => { this.showDeletePlan = false })
        }
        .width('100%').margin({ top: 16 })
      }
      .width('78%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding(20)
      .position({ x: '11%', y: '32%' })
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

删除确认弹框是居中圆角卡设计,宽度 78%,定位在屏幕中央偏上。弹框包含删除图标、标题、方案名称提示区和双按钮区。"再想想"按钮用亚麻浅色背景,"确认删除"按钮用紫罗兰紫背景,两者等宽(layoutWeight(1))。

zIndex(999) 设置弹框的层级为 999,确保它显示在所有内容之上。在 ArkTS 中,zIndex 控制同一父容器内子元素的堆叠顺序,值越大越靠上。弹框层需要高于内容层和 Tab 导航层,所以使用 999 这样的高值。

这个弹框的设计遵循了"危险操作需要二次确认"的 UX 原则。删除是不可逆操作,提示文案明确告知"无法恢复",防止用户误操作。取消按钮用较浅的颜色(亚麻浅),确认按钮用较深的颜色(紫罗兰紫),形成视觉引导。

三十一、编辑量尺弹框

  @Builder editCurtainModal() {
    Column() {
      this.modalOverlay(() => { this.showEditCurtain = false })
      Column() {
        Row() {
          Text('📏 编辑窗帘参数').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#B4AAC6')
            .padding(4)
            .onClick(() => { this.showEditCurtain = false })
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Row() {
          Text('当前帘款:').fontSize(10).fontColor('#7A7290')
          Text(FBCURTAINS[this.selCurtainIdx].name).fontSize(10).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
            .maxLines(1).layoutWeight(1)
        }
        .width('100%').margin({ top: 10 }).alignItems(VerticalAlign.Center)

        Text('窗帘宽度(米)').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          .width('100%').margin({ top: 14 })
        Row() {
          Text('🪟').fontSize(16)
          Text(fbWidthText(this.editWidth)).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
            .margin({ left: 8 })
          Text('/ 轨道总宽').fontSize(10).fontColor('#7A7290').margin({ left: 4 })
        }
        .width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Bottom)

        Slider({ value: this.editWidth, min: 1, max: 6, step: 0.1 })
          .width('100%')
          .margin({ top: 12 })
          .onChange((v: number) => { this.editWidth = v })

编辑量尺弹框使用居中卡片设计,宽度 86%。弹框内展示了当前帘款名称,然后提供窗帘宽度滑块、褶皱倍数选择和遮光衬布开关三个参数编辑器。

Slider 是 ArkTS 的滑块组件,value 绑定当前值,minmax 设置范围,step 设置步长。onChange 回调在滑块拖动时实时更新 editWidth 状态。宽度的文字展示通过 fbWidthText 函数格式化为"X.X 米"的形式。

        Row() {
          Text('经济 ×1.5').fontSize(10)
            .fontColor(this.editFold === 1.5 ? '#FFFFFF' : '#7A7290')
            .backgroundColor(this.editFold === 1.5 ? '#7B5EA7' : '#F6F2EC')
            .padding({ left: 11, right: 11, top: 6, bottom: 6 })
            .borderRadius(13)
            .margin({ right: 8 })
            .onClick(() => { this.editFold = 1.5 })
          Text('标准 ×2.0').fontSize(10)
            .fontColor(this.editFold === 2 ? '#FFFFFF' : '#7A7290')
            .backgroundColor(this.editFold === 2 ? '#7B5EA7' : '#F6F2EC')
            .padding({ left: 11, right: 11, top: 6, bottom: 6 })
            .borderRadius(13)
            .margin({ right: 8 })
            .onClick(() => { this.editFold = 2 })
          Text('华丽 ×2.5').fontSize(10)
            .fontColor(this.editFold === 2.5 ? '#FFFFFF' : '#7A7290')
            .backgroundColor(this.editFold === 2.5 ? '#7B5EA7' : '#F6F2EC')
            .padding({ left: 11, right: 11, top: 6, bottom: 6 })
            .borderRadius(13)
            .onClick(() => { this.editFold = 2.5 })
        }
        .width('100%').margin({ top: 8 })

褶皱倍数选择器使用三个胶囊按钮,分别对应经济(1.5 倍)、标准(2.0 倍)、华丽(2.5 倍)三档。当前选中档位用紫底白字,未选中用米底灰字。这种"三选一"的胶囊选择器是移动端参数选择的常见模式,比下拉菜单更直观,比单选按钮更紧凑。

        Row() {
          Column() {
            Text('遮光衬布加厚').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#3D3352')
            Text('开启后遮光率 +15%,适合临街卧室').fontSize(8).fontColor('#B4AAC6').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
          Toggle({ type: ToggleType.Switch, isOn: this.editDim })
            .selectedColor('#7B5EA7')
            .onChange((v: boolean) => { this.editDim = v })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F6F2EC')
        .borderRadius(12)
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Center)

遮光衬布开关使用 Toggle 组件。ToggleType.Switch 设置为拨动开关样式,isOn 绑定布尔状态,selectedColor 设置开启状态的主题色,onChange 在切换时更新状态。Toggle 是 ArkTS 的开关组件,支持开关(Switch)、复选框(Checkbox)和单选按钮(RadioButton)三种类型。

三十二、风格选择弹框

  @Builder stylePickModal() {
    Column() {
      this.modalOverlay(() => { this.showStylePick = false })
      Column() {
        Row() {
          Text('🎨 我的风格档案').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#B4AAC6')
            .padding(4)
            .onClick(() => { this.showStylePick = false })
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Text('选择偏好的家居风格,方案库将按此优先推荐。').fontSize(9).fontColor('#7A7290')
          .width('100%').margin({ top: 6 })
        Text('当前选择:' + FB_STYLES[this.selStyleIdx].name)
          .fontSize(11).fontColor('#7B5EA7')
          .width('100%').margin({ top: 10 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(FB_STYLES, (st: FBDataStyle, i: number) => {
            if (this.selStyleIdx === i) {
              Column() {
                Text(st.icon).fontSize(20)
                Text(st.name).fontSize(9).fontColor('#7B5EA7').margin({ top: 4 })
                Text(st.count.toString() + ' 户').fontSize(7).fontColor('#5E4688').margin({ top: 2 })
              }
              .width('28%')
              .padding({ top: 12, bottom: 12 })
              .border({ width: 1.5, color: '#7B5EA7' })
              .borderRadius(12)
              .backgroundColor('#EDE7F5')
              .margin({ right: 8, bottom: 8 })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => { this.selStyleIdx = i })
            } else {
              Column() {
                Text(st.icon).fontSize(20)
                Text(st.name).fontSize(9).fontColor('#7A7290').margin({ top: 4 })
                Text(st.count.toString() + ' 户').fontSize(7).fontColor('#B4AAC6').margin({ top: 2 })
              }
              .width('28%')
              .padding({ top: 12, bottom: 12 })
              .border({ width: 1, color: '#EAE2D6' })
              .borderRadius(12)
              .backgroundColor('#FFFFFF')
              .margin({ right: 8, bottom: 8 })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => { this.selStyleIdx = i })
            }
          })
        }
        .width('100%').margin({ top: 8 })

风格选择弹框使用宫格选择式设计,宽度 88%。六种风格以三列网格排列,每项宽度 28%。选中项有 1.5 像素粗的紫色边框和雾紫浅背景,未选中项有 1 像素细的灰色边框和白色背景。选中与未选中的视觉差异通过边框粗细、颜色和背景色三重对比来强调。

这个弹框的特色是每个风格项除了图标和名称,还显示了"N 户"的采用数量。这种社交证明数据增加了用户的选择信心。border 方法接收一个对象参数,width 设置边框粗细,color 设置边框颜色。

三十三、上门预约弹框

  @Builder bookVisitModal() {
    Column() {
      this.modalOverlay(() => { this.showBookVisit = false })
      Column() {
        Scroll() {
          Column() {
            Row() {
              Text('📐 上门测量预约').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
              Column().layoutWeight(1)
              Text('✕').fontSize(16).fontColor('#B4AAC6')
                .padding(4)
                .onClick(() => { this.showBookVisit = false })
            }
            .width('100%').alignItems(VerticalAlign.Center)

            Text('🔧 服务项目').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#3D3352')
              .width('100%').margin({ top: 12 })
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(FBVISITS, (v: FBDataVisit) => {
                if (this.visitSvc === v.name) {
                  Text(v.icon + ' ' + v.name).fontSize(9).fontColor('#FFFFFF')
                    .backgroundColor('#7B5EA7')
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 })
                    .borderRadius(12)
                    .margin({ right: 6, top: 6 })
                    .onClick(() => { this.visitSvc = v.name })
                } else {
                  Text(v.icon + ' ' + v.name).fontSize(9).fontColor('#7A7290')
                    .backgroundColor('#F6F2EC')
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 })
                    .borderRadius(12)
                    .margin({ right: 6, top: 6 })
                    .onClick(() => { this.visitSvc = v.name })
                }
              })
            }
            .width('100%')

上门预约弹框是底部抽屉式设计,包含服务项目选择、房间数步进器、日期选择、时段选择和地址输入五个部分。服务项目使用 Flex 换行布局的胶囊标签,选中项紫底白字,未选中米底灰字。

            Row() {
              Column() {
                Text('−').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
              }
              .width(36).height(32)
              .backgroundColor('#F0E6D8')
              .borderRadius(10)
              .justifyContent(FlexAlign.Center)
              .onClick(() => {
                if (this.visitRooms > 1) {
                  this.visitRooms -= 1
                }
              })
              Row() {
                Text(this.visitRooms.toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3D3352')
                Text('间').fontSize(9).fontColor('#7A7290').margin({ left: 3 })
              }
              .justifyContent(FlexAlign.Center)
              .alignItems(VerticalAlign.Bottom)
              .layoutWeight(1)
              Column() {
                Text('+').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              }
              .width(36).height(32)
              .backgroundColor('#7B5EA7')
              .borderRadius(10)
              .justifyContent(FlexAlign.Center)
              .onClick(() => {
                if (this.visitRooms < 12) {
                  this.visitRooms += 1
                }
              })
            }
            .width('100%').margin({ top: 8 })

房间数步进器是手工实现的减加按钮组件。左侧减号按钮用亚麻浅色背景,右侧加号按钮用紫罗兰紫背景,中间显示当前数值和单位。减号按钮有下限保护(不少于 1 间),加号按钮有上限保护(不多于 12 间)。这种边界检查防止用户输入超出合理范围的数值。

日期和时段选择使用三选一的胶囊按钮组,与褶皱倍数选择器的设计模式一致。日期用紫罗兰紫作为选中色,时段用亚麻棕作为选中色,通过色彩区分两组选择器的类别。地址输入使用 TextInput 组件。

三十四、配色方案弹框与窗帘详情弹框

  @Builder paletteModal() {
    Column() {
      this.modalOverlay(() => { this.showPalette = false })
      Column() {
        Row() {
          Text('🎨 配色方案').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#B4AAC6')
            .padding(4)
            .onClick(() => { this.showPalette = false })
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Text(FBPLANS[this.selPlanIdx].name).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          .width('100%').margin({ top: 10 })
        Text(FBPLANS[this.selPlanIdx].style + ' · ' + FBPLANS[this.selPlanIdx].rooms)
          .fontSize(10).fontColor('#7B5EA7')
          .width('100%').margin({ top: 4 })

        Row() {
          ForEach(FBPLANS, (p: FBDataPlan, i: number) => {
            if (i === this.selPlanIdx) {
              Row() {
                Column().width(52).height(52).backgroundColor(p.c1).borderRadius(10)
                Column().width(52).height(52).backgroundColor(p.c2).borderRadius(10).margin({ left: 6 })
                Column().width(52).height(52).backgroundColor(p.c3).borderRadius(10).margin({ left: 6 })
                Column().width(52).height(52).backgroundColor(p.c4).borderRadius(10).margin({ left: 6 })
              }
              .alignItems(VerticalAlign.Top)
            }
          })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .margin({ top: 12 })

配色方案弹框展示了选中方案的详细配色信息。四色色卡以四个 52x52 的圆角方块横向排列,颜色分别对应 c1c4。下方是配色比例条,结构与前述方案卡片中的比例条一致,但增加了独立的占比说明区域,分别列出主色、底色、点缀色、强调色的百分比。

这个弹框通过 ForEach 遍历 FBPLANS 数组,使用 if (i === this.selPlanIdx) 条件渲染只展示当前选中方案的信息。这种写法虽然略显冗长(因为只需展示一个方案却遍历整个数组),但保持了数据驱动渲染的一致性。

  @Builder curtainDetailModal() {
    Column() {
      this.modalOverlay(() => { this.showCurtainDetail = false })
      Column() {
        Row() {
          Text('🪟 帘款详情').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3D3352')
          Column().layoutWeight(1)
          Text('✕').fontSize(16).fontColor('#B4AAC6')
            .padding(4)
            .onClick(() => { this.showCurtainDetail = false })
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Stack() {
          Column()
            .width('100%').height(84)
            .backgroundColor(FBCURTAINS[this.selCurtainIdx].colorHex)
            .borderRadius(12)
          Text(FBCURTAINS[this.selCurtainIdx].icon).fontSize(32)
          Text(FBCURTAINS[this.selCurtainIdx].tag).fontSize(8).fontColor('#FFFFFF')
            .backgroundColor('#5E4688')
            .padding({ left: 7, right: 7, top: 2, bottom: 2 })
            .borderRadius(8)
            .position({ x: 10, y: 8 })
        }
        .width('100%').height(84).margin({ top: 12 })

窗帘详情弹框是居中大卡设计,宽度 88%。色块头部高度 84,展示窗帘代表色和 emoji。下方是商品名称、分类标签和适用空间标签。然后是三栏参数区:遮光率、褶皱倍数、已售数量,三栏之间用 1 像素宽的分隔线(Column().width(1).height(34).backgroundColor('#EAE2D6'))分隔。

底部是双按钮区:"编辑尺寸"按钮(雾紫浅背景)和"加入定制"按钮(紫罗兰紫背景),等宽排列。点击"编辑尺寸"会先关闭详情弹框(showCurtainDetail = false)再打开编辑弹框(showEditCurtain = true),实现弹框间的跳转。

弹框间的跳转是 ArkTS 状态驱动的自然体现。通过修改两个布尔状态变量,一个弹框消失、另一个弹框出现,整个过程由框架的响应式系统自动完成,无需手动操作 DOM 或管理动画。

三十五、内容区分发与主构建

  @Builder contentArea() {
    Stack() {
      if (this.activeTab === FBTab.Home) {
        FABRICHomeContent({
          onAddSample: () => { this.showAddSample = true },
          onBookVisit: () => { this.showBookVisit = true },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      } else if (this.activeTab === FBTab.Curtain) {
        FABRICCurtainContent({
          onEditCurtain: (idx: number) => {
            this.selCurtainIdx = idx
            this.showEditCurtain = true
          },
          onCurtainDetail: (idx: number) => {
            this.selCurtainIdx = idx
            this.showCurtainDetail = true
          },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      } else if (this.activeTab === FBTab.Fabric) {
        FABRICFabricContent({
          onAddSample: () => { this.showAddSample = true },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      } else if (this.activeTab === FBTab.Plan) {
        FABRICPlanContent({
          onPalette: (idx: number) => {
            this.selPlanIdx = idx
            this.showPalette = true
          },
          onDeletePlan: (idx: number) => {
            this.selPlanIdx = idx
            this.showDeletePlan = true
          },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      } else if (this.activeTab === FBTab.Visit) {
        FABRICVisitContent({
          onBookVisit: () => { this.showBookVisit = true },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      } else {
        FABRICMineContent({
          onStylePick: () => { this.showStylePick = true },
          onEditCurtain: () => {
            this.selCurtainIdx = 0
            this.showEditCurtain = true
          },
          onAddSample: () => { this.showAddSample = true },
          onGoto: (tab: FBTab) => { this.activeTab = tab }
        })
      }

      if (this.showAddSample) { this.addSampleModal() }
      if (this.showDeletePlan) { this.deletePlanModal() }
      if (this.showEditCurtain) { this.editCurtainModal() }
      if (this.showStylePick) { this.stylePickModal() }
      if (this.showBookVisit) { this.bookVisitModal() }
      if (this.showPalette) { this.paletteModal() }
      if (this.showCurtainDetail) { this.curtainDetailModal() }
    }
    .width('100%').height('100%')
  }

contentArea 构建器是整个应用的路由中枢。它使用 Stack 作为容器,内层通过 if/else if/else 条件渲染根据 activeTab 的值选择展示对应的 Tab 内容组件。外层是七个弹框的条件渲染,每个弹框通过对应的布尔开关控制。

这种"Stack + 条件渲染"的设计实现了两个关键功能。第一,Tab 切换时,旧 Tab 内容被销毁、新 Tab 内容被创建,保证了各 Tab 的状态独立。第二,弹框叠加在内容之上,当任何弹框打开时,它在 Stack 的最上层渲染,自然覆盖内容区。

子组件的回调属性在初始化时通过对象字面量传入。例如 FABRICHomeContent 接收 onAddSampleonBookVisitonGoto 三个回调,这些回调在父组件中定义,修改父组件的状态变量。这就是子组件向父组件通信的"回调注入"模式。

Home

Curtain

Fabric

Plan

Visit

Mine

activeTab 状态

Tab 值判断

首页组件

窗帘组件

面料组件

方案组件

上门组件

我的组件

回调注入

修改父组件状态

触发弹框渲染

触发 Tab 切换

上图展示了内容区分发的数据流。activeTab 状态驱动条件渲染选择对应 Tab 组件,子组件通过回调函数通知父组件修改状态,状态变化又触发新一轮的条件渲染,形成响应式闭环。

三十六、头部导航与底部 Tab 栏

  build() {
    Column() {
      Column() {
        Row() {
          Stack() {
            Column()
              .width(36).height(36)
              .backgroundColor('#7B5EA7')
              .borderRadius(18)
            Text('🧵').fontSize(17)
          }
          .width(36).height(36)
          Column() {
            Text('FABRIC').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#7B5EA7')
            Text('織語 · 软装布艺窗帘生活馆').fontSize(8).fontColor('#B99B7A')
              .margin({ top: 1 })
          }
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 8 })

          Row() {
            Text('🔍').fontSize(11).margin({ left: 10 })
            TextInput({ placeholder: '搜窗帘 / 面料 / 方案' })
              .placeholderColor('#B4AAC6').fontSize(11)
              .backgroundColor('#F6F2EC')
              .borderRadius(16)
              .layoutWeight(1).height(32)
              .margin({ left: 6, right: 6 })
              .padding({ left: 4, right: 4 })
          }
          .layoutWeight(1)
          .height(32)
          .backgroundColor('#F6F2EC')
          .borderRadius(16)
          .margin({ left: 10, right: 8 })

          Stack() {
            Text('🔔').fontSize(18)
            Text('2').fontSize(8).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              .backgroundColor('#B99B7A')
              .width(14).height(14).borderRadius(7)
              .textAlign(TextAlign.Center)
              .position({ x: 20, y: -3 })
          }
          .width(30).height(24)

          Text('小样').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .backgroundColor('#7B5EA7')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(14)
            .margin({ left: 8 })
            .onClick(() => { this.showAddSample = true })
        }
        .width('100%').height(54)
        .padding({ left: 12, right: 12 })
        .alignItems(VerticalAlign.Center)
        Column().width('100%').height(1).backgroundColor('#EAE2D6')
      }
      .width('100%')
      .backgroundColor('#FFFFFF')

头部导航区高度 54,包含品牌 Logo(圆形紫底 + emoji)、品牌名称、搜索框、通知铃铛(带未读数角标)和小样入口按钮。搜索框使用 TextInput 组件,外层 Row 用米色背景和圆角包裹搜索图标和输入框,形成一体化的搜索栏。

通知角标是一个 14x14 的圆形亚麻棕底色背景上显示数字"2",使用 position({ x: 20, y: -3 }) 定位到铃铛右上角偏移位置。y: -3 使用负值让角标略微超出容器上边界,这是移动端角标的常见定位方式。头部底部有一条 1 像素高的分隔线 #EAE2D6,增加视觉层次。

      Row() {
        ForEach(FB_TABS, (item: FBTabItem) => {
          Column() {
            Text(item.icon).fontSize(17)
              .opacity(this.activeTab === item.tab ? 1.0 : 0.45)
            Text(item.label).fontSize(8)
              .fontColor(this.activeTab === item.tab ? '#7B5EA7' : '#7A7290')
              .fontWeight(this.activeTab === item.tab ? FontWeight.Bold : FontWeight.Normal)
              .margin({ top: 1 })
            if (this.activeTab === item.tab) {
              Column().width(16).height(3)
                .backgroundColor('#7B5EA7').borderRadius(2)
                .margin({ top: 2 })
            } else {
              Column().width(16).height(3)
                .backgroundColor('#FFFFFF').borderRadius(2)
                .margin({ top: 2 })
            }
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 5, bottom: 5 })
          .onClick(() => { this.activeTab = item.tab })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 3, bottom: 4 })
      .border({ width: 1, color: '#EAE2D6' })
    }
    .width('100%').height('100%')
    .backgroundColor('#F6F2EC')
  }

底部 Tab 栏通过 ForEach 渲染六个 Tab 项,每个项使用 layoutWeight(1) 等分宽度。选中 Tab 的图标不透明(opacity: 1.0),文字用紫罗兰紫且加粗;未选中 Tab 的图标半透明(opacity: 0.45),文字用灰色且不加粗。选中 Tab 下方有一条 16x3 的紫色指示条,未选中位置是白色指示条(保持高度一致,避免布局抖动)。

border({ width: 1, color: '#EAE2D6' }) 为底部 Tab 栏设置 1 像素的灰色边框,使其与内容区有明确的视觉分隔。整个主组件的 build() 函数从上到下依次构建头部导航、内容区和底部 Tab 栏三部分,形成完整的页面骨架。

三十七、对比总结

下表对本应用中各 Tab 页面的核心组件、动画类型、数据可视化和交互模式进行对比总结。

Tab 页面核心容器组件动画类型动画状态变量数据可视化形式核心交互
首页Scroll + Column + Stack + Flex布纹飘动(translate + opacity)weaveShift, weaveOpacity统计四格、风格横滑卡、功能宫格、分布条、热销榜点击宫格路由分发、小样申请
窗帘Scroll + Column + Stack纱帘摆动(rotate + opacity)veilSwing, veilOpacity窗帘大卡、遮光率进度条、褶皱徽章、遮光对比条分类筛选、编辑量尺、查看详情
面料Scroll + Column + Flex色卡浮沉(translate + opacity)chipFloat, chipOpacity色彩 Grid、价格对比条、手感指数榜点击色块申请小样
方案Scroll + Column + Flex + Stack色块呼吸(scale + opacity)breathScale, breathOpacity配色比例条、双列方案卡、四色法则提示点击卡片查看配色、删除方案
上门Scroll + Column + Stack光点流动(translate + opacity)dotShift, dotOpacity排期表、价目列表、预约时间轴预约服务、时间轴浏览
我的Scroll + Column + Stack光晕呼吸(scale + opacity)glow, glowScale, favOn会员卡、消费柱状图、订单列表、收藏列表、评价列表风格选择、编辑量尺、收藏切换
弹框名称展示形式宽度核心组件交互模式
小样申请底部抽屉100%(高度 62%)Flex + TextInput + 多选色块多选切换、地址输入
删除确认居中圆角卡78%Column + 双按钮二次确认
编辑量尺居中圆角卡86%Slider + 胶囊按钮组 + Toggle滑块调节、三选一、开关
风格选择居中圆角卡88%Flex + 条件渲染边框单选切换
上门预约底部抽屉100%(高度 62%)Flex + 步进器 + 胶囊按钮组 + TextInput多步表单填写
配色方案居中圆角卡88%Row + 四色色卡 + 比例条信息展示
窗帘详情居中圆角卡88%Stack + 三栏参数区 + 双按钮详情浏览、弹框跳转

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// FABRIC 織語 · 软装布艺窗帘生活馆
// 窗帘/抱枕/床品/地毯布艺商城 + 面料小样申请 + 上门测量安装
// 软装搭配方案 + 我的 · 紫罗兰亚麻风浅色 · ArkTS 单文件
// ============================================================

// ============ 配色(亚麻米 / 紫罗兰 / 雾灰紫 / 亚麻棕 / 深紫墨) ============
interface FBColorPalette {
  bg: string
  card: string
  violet: string
  violetDeep: string
  mist: string
  mistLight: string
  linen: string
  linenLight: string
  ink: string
  line: string
  textSub: string
  textHint: string
}

const FB_COLORS: FBColorPalette = {
  bg: '#F6F2EC',
  card: '#FFFFFF',
  violet: '#7B5EA7',
  violetDeep: '#5E4688',
  mist: '#C9BBDD',
  mistLight: '#EDE7F5',
  linen: '#B99B7A',
  linenLight: '#F0E6D8',
  ink: '#3D3352',
  line: '#EAE2D6',
  textSub: '#7A7290',
  textHint: '#B4AAC6'
}

// ============ Tab 定义 ============
enum FBTab {
  Home = 0,
  Curtain = 1,
  Fabric = 2,
  Plan = 3,
  Visit = 4,
  Mine = 5
}

interface FBTabItem {
  tab: FBTab
  icon: string
  label: string
}

const FB_TABS: FBTabItem[] = [
  { tab: FBTab.Home, icon: '🏠', label: '首页' },
  { tab: FBTab.Curtain, icon: '🪟', label: '窗帘' },
  { tab: FBTab.Fabric, icon: '🧵', label: '面料' },
  { tab: FBTab.Plan, icon: '🎨', label: '方案' },
  { tab: FBTab.Visit, icon: '📐', label: '上门' },
  { tab: FBTab.Mine, icon: '👤', label: '我的' }
]

// ============ 首页统计四格 ============
interface FBDataStat {
  icon: string
  label: string
  value: string
  sub: string
  color: string
}

const FB_STATS: FBDataStat[] = [
  { icon: '🪟', label: '在售窗帘', value: '16', sub: '款', color: '#7B5EA7' },
  { icon: '🧵', label: '甄选面料', value: '12', sub: '种', color: '#B99B7A' },
  { icon: '📐', label: '累计上门量窗', value: '3,208', sub: '户', color: '#5E4688' },
  { icon: '⭐', label: '搭配好评率', value: '99.2', sub: '%', color: '#9B8AC4' }
]

// ============ 首页风格横滑大卡(6 种) ============
interface FBDataStyle {
  id: number
  name: string
  icon: string
  desc: string
  colorHex: string
  count: number
}

const FB_STYLES: FBDataStyle[] = [
  { id: 1, name: '雾紫浪漫', icon: '🌙', desc: '紫罗兰纱帘 × 亚麻床品,卧室一夜好眠', colorHex: '#C9BBDD', count: 326 },
  { id: 2, name: '燕麦自然', icon: '🌾', desc: '本色亚麻 × 原木家具,客厅呼吸感满分', colorHex: '#D9CDB8', count: 284 },
  { id: 3, name: '灰紫轻奢', icon: '💜', desc: '雾灰紫提花 × 丝绒抱枕,全屋高级质感', colorHex: '#9B8AC4', count: 198 },
  { id: 4, name: '亚麻日式', icon: '🍵', desc: '苎麻平幔 × 藤编地毯,书房禅意十足', colorHex: '#CDBFA5', count: 162 },
  { id: 5, name: '奶油法式', icon: '🍰', desc: '米白泡泡纱 × 蕾丝纱幔,餐厅温柔滤镜', colorHex: '#EFD9E2', count: 141 },
  { id: 6, name: '星夜静谧', icon: '🌌', desc: '深紫墨全遮光 × 静音轨道,儿童房哄睡神器', colorHex: '#3D3352', count: 118 }
]

// ============ 首页功能宫格(8 项) ============
interface FBDataGrid {
  icon: string
  label: string
  bg: string
}

const FB_GRIDS: FBDataGrid[] = [
  { icon: '🧵', label: '面料小样', bg: '#EDE7F5' },
  { icon: '📐', label: '上门量窗', bg: '#F0E6D8' },
  { icon: '🪟', label: '窗帘定制', bg: '#EDE7F5' },
  { icon: '🎨', label: '软装方案', bg: '#F0E6D8' },
  { icon: '🔧', label: '安装服务', bg: '#EDE7F5' },
  { icon: '♻️', label: '旧帘换新', bg: '#F0E6D8' },
  { icon: '❤️', label: '我的收藏', bg: '#EDE7F5' },
  { icon: '💬', label: '搭配顾问', bg: '#F0E6D8' }
]

// ============ 风格类型分布条(首页图表) ============
interface FBDataDist {
  style: string
  count: number
  color: string
}

const FB_STYLE_DIST: FBDataDist[] = [
  { style: '自然风', count: 128, color: '#7B5EA7' },
  { style: '轻奢风', count: 96, color: '#9B8AC4' },
  { style: '日式', count: 84, color: '#B99B7A' },
  { style: '法式', count: 62, color: '#C9BBDD' },
  { style: '极简', count: 58, color: '#CDBFA5' },
  { style: '复古', count: 40, color: '#5E4688' }
]

// ============ 窗帘分类 ============
interface FBDataCat {
  name: string
  icon: string
}

const FB_CURTAIN_CATS: FBDataCat[] = [
  { name: '全部', icon: '🪟' },
  { name: '卧室', icon: '🌙' },
  { name: '客厅', icon: '🛋️' },
  { name: '儿童房', icon: '🧸' },
  { name: '书房', icon: '📚' },
  { name: '餐厅', icon: '🍽️' }
]

// ============ 窗帘(16 款) ============
interface FBDataCurtain {
  id: number
  name: string
  cat: string
  icon: string
  colorHex: string
  price: number
  sales: number
  dim: number
  fold: number
  spaces: string
  tag: string
}

const FBCURTAINS: FBDataCurtain[] = [
  { id: 1, name: '雾紫轻语 · 雪尼尔遮光帘', cat: '卧室', icon: '🌙', colorHex: '#C9BBDD', price: 268, sales: 4820, dim: 95, fold: 2.0, spaces: '主卧/老人房', tag: '爆款' },
  { id: 2, name: '亚麻晨光 · 棉麻半遮光帘', cat: '客厅', icon: '🌾', colorHex: '#D9CDB8', price: 189, sales: 3960, dim: 60, fold: 1.8, spaces: '客厅/阳台', tag: '热卖' },
  { id: 3, name: '星夜黑金 · 全遮光隔音帘', cat: '卧室', icon: '🌌', colorHex: '#3D3352', price: 359, sales: 3610, dim: 99, fold: 2.5, spaces: '主卧/影音室', tag: '隔音' },
  { id: 4, name: '薄雾紫纱 · 双层纱帘组', cat: '客厅', icon: '纱', colorHex: '#EDE7F5', price: 129, sales: 3180, dim: 45, fold: 3.0, spaces: '客厅/茶室', tag: '透光' },
  { id: 5, name: '燕麦米 · 日式平幔帘', cat: '书房', icon: '🍵', colorHex: '#E4D9C6', price: 219, sales: 2450, dim: 80, fold: 2.0, spaces: '书房/和室', tag: '日式' },
  { id: 6, name: '紫罗兰之约 · 绒布提花帘', cat: '卧室', icon: '💜', colorHex: '#7B5EA7', price: 328, sales: 2980, dim: 92, fold: 2.2, spaces: '主卧/衣帽间', tag: '提花' },
  { id: 7, name: '云朵白 · 防水百叶帘', cat: '餐厅', icon: '☁️', colorHex: '#F7F3FA', price: 159, sales: 2120, dim: 70, fold: 1.0, spaces: '厨房/卫生间', tag: '防水' },
  { id: 8, name: '莫兰迪灰绿 · 棉麻混纺帘', cat: '客厅', icon: '🌿', colorHex: '#C2CBB8', price: 239, sales: 1860, dim: 75, fold: 2.0, spaces: '客厅/玄关', tag: '莫兰迪' },
  { id: 9, name: '深紫墨 · 复古丝绒帘', cat: '卧室', icon: '🍷', colorHex: '#5E4688', price: 398, sales: 1540, dim: 97, fold: 2.5, spaces: '主卧/酒店风', tag: '复古' },
  { id: 10, name: '裸粉杏 · 泡泡纱公主帘', cat: '儿童房', icon: '🎀', colorHex: '#EFD9E2', price: 168, sales: 2680, dim: 50, fold: 2.8, spaces: '女孩房/亲子房', tag: '亲子' },
  { id: 11, name: '藏蓝星河 · 卡通遮光帘', cat: '儿童房', icon: '⭐', colorHex: '#4A5A8A', price: 208, sales: 3320, dim: 88, fold: 2.0, spaces: '儿童房/游戏间', tag: '哄睡' },
  { id: 12, name: '焦糖棕 · 美式乡村帘', cat: '客厅', icon: '🍂', colorHex: '#B99B7A', price: 288, sales: 1240, dim: 85, fold: 2.2, spaces: '客厅/别墅', tag: '美式' },
  { id: 13, name: '香草米白 · 法式垂坠帘', cat: '餐厅', icon: '🍰', colorHex: '#F0E6D8', price: 278, sales: 1420, dim: 78, fold: 3.0, spaces: '餐厅/法式客厅', tag: '法式' },
  { id: 14, name: '石墨灰 · 北欧极简帘', cat: '书房', icon: '🪵', colorHex: '#8A8592', price: 198, sales: 2260, dim: 90, fold: 1.8, spaces: '书房/工作室', tag: '极简' },
  { id: 15, name: '暮山紫 · 高精密提花帘', cat: '卧室', icon: '🏔️', colorHex: '#9B8AC4', price: 318, sales: 1760, dim: 96, fold: 2.5, spaces: '主卧/轻奢风', tag: '高精密' },
  { id: 16, name: '亚麻本色 · 抗菌防尘帘', cat: '儿童房', icon: '🌱', colorHex: '#CDBFA5', price: 229, sales: 1580, dim: 82, fold: 2.0, spaces: '儿童房/母婴房', tag: '抗菌' }
]

// ============ 面料(12 种) ============
interface FBDataFabric {
  id: number
  name: string
  colorHex: string
  comp: string
  price: number
  feature: string
  stock: number
  feel: number
}

const FBFABRICS: FBDataFabric[] = [
  { id: 1, name: '本色亚麻', colorHex: '#D9CDB8', comp: '100% 亚麻', price: 68, feature: '透气亲肤', stock: 420, feel: 96 },
  { id: 2, name: '棉麻混纺', colorHex: '#E4D9C6', comp: '55% 棉 45% 麻', price: 45, feature: '垂感自然', stock: 680, feel: 92 },
  { id: 3, name: '雾灰紫雪尼尔', colorHex: '#C9BBDD', comp: '雪尼尔绒', price: 88, feature: '厚实遮光', stock: 350, feel: 98 },
  { id: 4, name: '紫罗兰丝绒', colorHex: '#7B5EA7', comp: '真丝绒', price: 128, feature: '轻奢光泽', stock: 180, feel: 97 },
  { id: 5, name: '提花工艺布', colorHex: '#9B8AC4', comp: '涤纶提花', price: 98, feature: '立体花纹', stock: 260, feel: 90 },
  { id: 6, name: '香云雪纺纱', colorHex: '#F2ECF7', comp: '雪纺纱纤维', price: 32, feature: '仙气透光', stock: 900, feel: 85 },
  { id: 7, name: '涤麻遮光布', colorHex: '#8A7BA6', comp: '三层遮光', price: 76, feature: '遮光 95%', stock: 540, feel: 88 },
  { id: 8, name: '奶油泡泡纱', colorHex: '#EFD9E2', comp: '全棉泡泡纱', price: 56, feature: '少女感强', stock: 470, feel: 93 },
  { id: 9, name: '法式蕾丝纱', colorHex: '#F7F3FA', comp: '刺绣蕾丝', price: 108, feature: '精致花边', stock: 150, feel: 89 },
  { id: 10, name: '亚麻棕植绒', colorHex: '#B99B7A', comp: '植绒复合', price: 82, feature: '复古肌理', stock: 310, feel: 94 },
  { id: 11, name: '原色苎麻', colorHex: '#CDBFA5', comp: '100% 苎麻', price: 62, feature: '日式素雅', stock: 380, feel: 91 },
  { id: 12, name: '深紫墨天鹅绒', colorHex: '#3D3352', comp: '天鹅绒', price: 118, feature: '酒店质感', stock: 210, feel: 99 }
]

// ============ 软装方案(8 套) ============
interface FBDataPlan {
  id: number
  name: string
  style: string
  icon: string
  price: number
  rooms: string
  rating: number
  applied: number
  c1: string
  c2: string
  c3: string
  c4: string
  r1: number
  r2: number
  r3: number
  r4: number
}

const FBPLANS: FBDataPlan[] = [
  { id: 1, name: '紫雾晨眠 · 卧室套装', style: '雾紫浪漫', icon: '🌙', price: 1680, rooms: '窗帘+床品+抱枕', rating: 4.9, applied: 326, c1: '#C9BBDD', c2: '#F6F2EC', c3: '#7B5EA7', c4: '#B99B7A', r1: 40, r2: 30, r3: 20, r4: 10 },
  { id: 2, name: '燕麦暖阳 · 客厅套装', style: '燕麦自然', icon: '🌾', price: 1980, rooms: '窗帘+抱枕+地毯', rating: 4.8, applied: 284, c1: '#D9CDB8', c2: '#F0E6D8', c3: '#B99B7A', c4: '#3D3352', r1: 45, r2: 25, r3: 20, r4: 10 },
  { id: 3, name: '灰紫轻奢 · 全屋套装', style: '灰紫轻奢', icon: '💜', price: 4680, rooms: '窗帘+床品+地毯', rating: 5.0, applied: 198, c1: '#9B8AC4', c2: '#3D3352', c3: '#C9BBDD', c4: '#B99B7A', r1: 35, r2: 30, r3: 25, r4: 10 },
  { id: 4, name: '苎麻禅意 · 书房套装', style: '亚麻日式', icon: '🍵', price: 1280, rooms: '窗帘+坐垫+桌旗', rating: 4.7, applied: 162, c1: '#CDBFA5', c2: '#F6F2EC', c3: '#8A8592', c4: '#7B5EA7', r1: 50, r2: 30, r3: 12, r4: 8 },
  { id: 5, name: '奶油法式 · 餐厅套装', style: '奶油法式', icon: '🍰', price: 1480, rooms: '纱帘+桌布+椅套', rating: 4.8, applied: 141, c1: '#EFD9E2', c2: '#F7F3FA', c3: '#C9BBDD', c4: '#B99B7A', r1: 38, r2: 32, r3: 18, r4: 12 },
  { id: 6, name: '星夜安睡 · 儿童房套装', style: '星夜静谧', icon: '🌌', price: 1080, rooms: '遮光帘+床品', rating: 4.9, applied: 118, c1: '#3D3352', c2: '#4A5A8A', c3: '#C9BBDD', c4: '#F0E6D8', r1: 42, r2: 28, r3: 18, r4: 12 },
  { id: 7, name: '暮山紫语 · 主卧套装', style: '灰紫轻奢', icon: '🏔️', price: 2180, rooms: '提花帘+床品+纱', rating: 4.9, applied: 136, c1: '#9B8AC4', c2: '#EDE7F5', c3: '#5E4688', c4: '#B99B7A', r1: 36, r2: 30, r3: 22, r4: 12 },
  { id: 8, name: '石墨静读 · 工作室套装', style: '极简', icon: '🪵', price: 1380, rooms: '窗帘+抱枕', rating: 4.6, applied: 96, c1: '#8A8592', c2: '#F6F2EC', c3: '#3D3352', c4: '#CDBFA5', r1: 40, r2: 28, r3: 20, r4: 12 }
]

// ============ 上门服务(8 项) ============
interface FBDataVisit {
  id: number
  name: string
  icon: string
  duration: string
  price: number
  desc: string
  tag: string
  today: string
  sat: string
  sun: string
}

const FBVISITS: FBDataVisit[] = [
  { id: 1, name: '免费上门量窗', icon: '📐', duration: '约 40 分钟', price: 0, desc: '激光测距 · 出报价单', tag: '免费', today: '余 6', sat: '余 3', sun: '余 5' },
  { id: 2, name: '窗帘安装挂帘', icon: '🔧', duration: '约 90 分钟', price: 128, desc: '打孔挂钩 · 调试平整', tag: '标准化', today: '余 4', sat: '满', sun: '余 2' },
  { id: 3, name: '窗帘拆洗养护', icon: '🧺', duration: '约 60 分钟', price: 98, desc: '上门取送 · 高温除螨', tag: '季末热', today: '余 8', sat: '余 6', sun: '余 4' },
  { id: 4, name: '静音轨道加装', icon: '🛤️', duration: '约 70 分钟', price: 158, desc: '超静音滑轮 · 顺滑不卡', tag: '推荐', today: '余 3', sat: '余 2', sun: '满' },
  { id: 5, name: '帘头幔子定制安装', icon: '🪡', duration: '约 120 分钟', price: 188, desc: '法式平幔 · 工字褶幔', tag: '工艺', today: '余 2', sat: '满', sun: '余 1' },
  { id: 6, name: '全屋软装摆场', icon: '🛋️', duration: '约 240 分钟', price: 588, desc: '窗帘+抱枕+地毯整体陈设', tag: '整屋', today: '预约', sat: '预约', sun: '预约' },
  { id: 7, name: '旧窗帘回收', icon: '♻️', duration: '约 30 分钟', price: 0, desc: '免费拆旧 · 抵扣新帘', tag: '环保', today: '余 9', sat: '余 7', sun: '余 8' },
  { id: 8, name: '布艺除螨喷雾养护', icon: '✨', duration: '约 50 分钟', price: 88, desc: '沙发帘布深层除螨', tag: '健康', today: '余 5', sat: '余 4', sun: '余 6' }
]

// ============ 预约记录(14 条) ============
interface FBDataBooking {
  id: number
  service: string
  master: string
  date: string
  time: string
  status: string
  addr: string
}

const FBBOOKINGS: FBDataBooking[] = [
  { id: 1, service: '免费上门量窗', master: '量尺组 · 阿哲', date: '08-16', time: '10:00-12:00', status: '已完成', addr: '澜山别院 3 栋 802' },
  { id: 2, service: '静音轨道加装', master: '安装组 · 老周', date: '08-17', time: '14:00-16:00', status: '已完成', addr: '澜山别院 3 栋 802' },
  { id: 3, service: '窗帘安装挂帘', master: '安装组 · 老周', date: '08-19', time: '09:00-11:00', status: '已完成', addr: '澜山别院 3 栋 802' },
  { id: 4, service: '免费上门量窗', master: '量尺组 · 小蔓', date: '08-22', time: '15:00-17:00', status: '已完成', addr: '云锦东方 12 栋 1501' },
  { id: 5, service: '布艺除螨喷雾养护', master: '养护组 · 阿玲', date: '08-24', time: '10:00-11:00', status: '已完成', addr: '云锦东方 12 栋 1501' },
  { id: 6, service: '帘头幔子定制安装', master: '工艺组 · 陈师傅', date: '08-26', time: '13:00-15:00', status: '进行中', addr: '云锦东方 12 栋 1501' },
  { id: 7, service: '免费上门量窗', master: '量尺组 · 阿哲', date: '08-28', time: '09:00-10:00', status: '待上门', addr: '翡翠半岛 6 号楼 903' },
  { id: 8, service: '窗帘安装挂帘', master: '安装组 · 大刘', date: '08-29', time: '16:00-18:00', status: '待上门', addr: '翡翠半岛 6 号楼 903' },
  { id: 9, service: '窗帘拆洗养护', master: '养护组 · 阿玲', date: '08-30', time: '10:00-11:00', status: '已预约', addr: '澜山别院 3 栋 802' },
  { id: 10, service: '全屋软装摆场', master: '陈设组 · 苏总监', date: '09-02', time: '09:00-13:00', status: '已预约', addr: '翡翠半岛 6 号楼 903' },
  { id: 11, service: '静音轨道加装', master: '安装组 · 老周', date: '09-05', time: '14:00-16:00', status: '已预约', addr: '云锦东方 12 栋 1501' },
  { id: 12, service: '旧窗帘回收', master: '环保组 · 小柯', date: '09-08', time: '11:00-12:00', status: '已预约', addr: '澜山别院 3 栋 802' },
  { id: 13, service: '布艺除螨喷雾养护', master: '养护组 · 阿玲', date: '09-11', time: '15:00-16:00', status: '已预约', addr: '翡翠半岛 6 号楼 903' },
  { id: 14, service: '窗帘安装挂帘', master: '安装组 · 大刘', date: '09-14', time: '10:00-12:00', status: '已预约', addr: '云锦东方 12 栋 1501' }
]

// ============ 我的订单(12 条) ============
interface FBDataOrder {
  id: number
  item: string
  date: string
  status: string
  price: number
}

               
}


三十八、总结

在这里插入图片描述

本应用是一个功能完整的软装布艺商城与服务预约平台,采用鸿蒙 ArkTS 声明式 UI 框架,在单文件内承载了六个业务 Tab 页面、七个交互弹框、十余组数据模型和十几个全局纯函数,总代码量约 2800 行。应用的架构设计体现了高度的内聚性和模块化特征。

在配色体系层面,应用通过 FBColorPalette 接口和 FB_COLORS 常量集中管理十二种品牌色,以亚麻米为底色、紫罗兰紫为主色、雾灰紫为辅助色、深紫墨为强调色,构建了统一而富有层次的视觉语言。所有 UI 组件的颜色都引用自这一套配色常量,保证了品牌视觉的一致性。

在数据模型层面,应用定义了十一个数据接口和十一个常量数组,覆盖了统计、风格、功能宫格、分布、窗帘分类、窗帘商品、面料、方案、服务、预约、订单、收藏、消费、评价等全部业务实体。每个接口的字段设计贴合业务需求,如窗帘的遮光率和褶皱倍数、方案的四色配色比例、服务的排期余量等,都是软装行业的专业参数在数据层面的直接映射。

在纯函数工具层层面,应用定义了十七个全局纯函数,分为格式化函数(fbMoneyfbDimW 等)、状态映射函数(fbFoldColorfbStatusColor 等)、不可变更新函数(fbToggleAt)和列表操作函数(fbFilterCurtainsfbTopSales 等)四类。这些函数严格遵循纯函数原则,无副作用、可组合、易测试,是 UI 渲染的重要支撑。

在组件架构层面,应用采用"一主六子"的组件树结构。主组件 FABRICApp 作为入口组件,承担 Tab 路由、弹框调度、状态管理三大职责。六个子内容组件各自独立管理本页面的 UI 构建和动画状态,通过回调函数向主组件通信。这种分层设计使各组件职责清晰,互不干扰。

在状态管理层面,主组件使用 @State 管理了四大类共十八个状态变量。Tab 路由状态控制页面切换,弹框开关控制七个弹框的显示与隐藏,选中项索引记录用户在列表中的选择位置,表单字段管理小样申请、窗帘编辑和上门预约的输入数据。每个 @State 变量的变化都会自动触发引用该状态的 UI 部分重新渲染,实现了完整的响应式数据流。

在动画系统层面,六个子组件各自拥有独立的动画状态对,通过 aboutToAppear 生命周期中的 animateTo 启动无限循环动画。首页的布纹飘动使用 translate 位移,窗帘页的纱帘摆动使用 rotate 旋转,面料页的色卡浮沉使用 translate 位移,方案页的色块呼吸使用 scale 缩放,上门页的光点流动使用 translate 位移,我的页的光晕呼吸使用 scale 缩放配合 opacity 透明度。六种动画都采用 PlayMode.Alternate 交替播放模式和 Curve.EaseInOut 缓动曲线,时长统一为 2200 毫秒,形成了各页面独特而又统一的动态语言。

在弹框架构层面,应用通过 @Builder 装饰器定义了七个弹框构建器和一个共享的遮罩层构建器。弹框分为底部抽屉式和居中圆角卡式两种形态,遮罩层统一使用半透明深紫背景。弹框的显示通过布尔状态变量控制,弹框间的跳转通过修改两个布尔变量实现(先关闭当前弹框再打开目标弹框)。每个弹框都通过 zIndex(999) 确保层级最高。

在布局技术层面,应用综合运用了 ArkTS 的五大容器组件。Column 用于垂直排列子元素,是每个页面的骨架容器。Row 用于水平排列子元素,配合 layoutWeight 实现弹性宽度分配。Stack 用于层叠子元素,在品牌横幅、色块头部、会员卡等场景中实现多层视觉效果。Flex 配合 FlexWrap.Wrap 用于自动换行的网格布局,在功能宫格、面料色彩墙、方案网格等场景中使用。Scroll 用于滚动容器,分为垂直滚动(页面内容)和水平滚动(风格横滑、分类筛选)两种模式。

在数据可视化层面,应用完全通过基础组件手工绘制了多种图表。进度条使用"外层灰色轨道 + 内层彩色填充"的双层结构,宽度通过纯函数计算百分比。柱状图使用 Column 的高度属性和 fbSpendH 函数映射金额到像素高度。配色比例条使用 Row 包裹多个 Column,各自占据百分比宽度,配合 clip(true) 实现圆角裁剪。这些手工图表避免了引入第三方图表库的开销,保持了应用的轻量性。

在交互模式层面,应用实现了多选切换(小样面料)、单选切换(风格选择、褶皱倍数、日期时段)、滑块调节(窗帘宽度)、开关切换(遮光衬布)、步进器(房间数量)、条件筛选(窗帘分类)、弹框跳转(详情到编辑)等多种交互模式。每种交互都通过修改 @State 状态变量来驱动 UI 更新,充分体现了 ArkTS"状态驱动 UI"的核心思想。

在组件间通信层面,应用采用"属性绑定 + 回调通知"的通信模式。父组件通过属性绑定向子组件传递回调函数,子组件在用户交互时调用这些回调函数,回调函数内部修改父组件的状态变量。这种模式确保了数据流的单向性:状态从父到子,事件从子到父,便于追踪和调试。

ForEach 渲染层面,应用大量使用 ForEach 进行列表渲染,包括统计四格、风格横滑卡、功能宫格、分布条、窗帘列表、面料 Grid、方案网格、服务排期表、预约时间轴、订单列表、收藏列表、评价列表、底部 Tab 栏等十余处。ForEach 配合条件渲染(if/else)实现了选中态、过滤态、斑马条纹等动态展示效果。

在鸿蒙技术点的运用层面,应用充分展示了 ArkTS 的核心能力。@Component 装饰器声明可复用组件,@Entry 标记入口组件,@State 管理响应式状态,@Builder 定义可复用 UI 片段。aboutToAppear 生命周期用于初始化动画。animateTo 驱动属性动画。FlexAlign 枚举定义弹性对齐方式。GradientDirection 定义渐变方向。PlayMode 定义动画播放模式。Curve 定义缓动曲线。ScrollDirection 定义滚动方向。BarState 控制滚动条显隐。ToggleType 定义开关类型。TextAlign 定义文字对齐方式。FontWeight 定义字体粗细。这些枚举和装饰器共同构成了 ArkTS 的类型安全体系。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐