一、应用概述与设计理念

在当今移动互联网时代,传统服务行业正经历着数字化转型的深刻变革。理发店作为与人们日常生活息息相关的服务场景,其预约管理方式的智能化升级具有重要的现实意义。本文将深入剖析一款基于HarmonyOS ArkTS框架开发的理发预约管理应用,这款应用不仅面向顾客提供便捷的在线预约服务,同时也为店铺管理者提供了完善的订单管理、发型师管理和数据统计功能。从技术架构的角度来看,这款应用采用了经典的声明式UI编程范式,充分利用了ArkTS语言的类型安全特性和HarmonyOS系统的原生组件能力,构建了一个既美观又实用的移动端解决方案。

在这里插入图片描述

这款应用的设计理念源于对传统理发店经营痛点的深度洞察。在日常经营中,理发店往往面临着预约管理混乱、顾客等待时间过长、发型师排班不合理、服务评价难以收集等诸多问题。为了解决这些痛点,本应用以"简约而不简单"为核心设计哲学,在视觉层面采用了复古棕与理发红相结合的色彩方案,营造出一种既专业又温馨的理发店氛围。复古棕色调作为主色,传递出理发店特有的经典与沉稳气质,而理发红色作为点缀色,则在关键操作按钮和重要提示信息处发挥作用,有效引导用户的注意力。这种配色方案不仅符合理发行业的视觉认知习惯,也在用户体验层面实现了功能与美学的有机统一。

在技术架构层面,这款应用遵循了HarmonyOS应用开发的最佳实践,采用了单页面多模块的设计模式。整个应用围绕一个入口组件展开,通过状态管理控制五个主要功能模块的切换,包括首页概览、在线预约、订单管理、顾客评价和个人中心。这种架构设计的优势在于减少了页面跳转带来的性能开销,同时通过组件化的开发方式提高了代码的可维护性和复用性。每一个功能模块都被封装为独立的Builder方法,既可以在主页面中灵活组合,也便于后续的单元测试和功能扩展。此外,应用还引入了设计令牌(Design Token)的概念,将颜色、字体、间距等视觉属性集中管理,确保了整个应用视觉风格的一致性和可配置性。

从应用场景的角度来看,这款应用具有双重用户角色定位。对于普通顾客而言,应用提供了直观的发型师展示、服务项目浏览和在线预约功能,顾客可以查看发型师的专业资质、顾客评价和实时预约状态,从而做出更加明智的选择。对于店铺管理者而言,应用则提供了今日订单统计、收入概览、预约编辑和取消管理等后台功能,帮助管理者实时掌握店铺运营状况。这种双重角色的设计使得应用在实际商业环境中具有更强的适用性,无论是大型连锁理发店还是独立经营的小型工作室,都可以借助这款应用实现数字化管理升级。接下来,本文将从代码层面逐段解析这款应用的实现细节,深入探讨ArkTS语言在构建复杂UI界面时的技术要点和设计模式。

二、数据模型与类型系统的精心设计

在HarmonyOS ArkTS应用开发中,类型系统是保证代码质量和可维护性的基石。这款理发预约应用从一开始就建立了完善的类型定义体系,通过接口(interface)明确了各个业务实体的数据结构,为后续的开发工作奠定了坚实的基础。类型优先的开发方式不仅能够在编译阶段捕获潜在的错误,还能通过IDE的智能提示大幅提升开发效率,这在大型项目或团队协作中尤为重要。

应用首先定义了发型师相关的数据接口StylistMeta,这个接口完整地描述了一个发型师应该具备的所有属性信息。其中包括唯一标识符id、姓名name、职称title、头像颜色avatarColor、评分rating、评价数量reviews、专长领域数组specialties、起步价格price以及忙碌状态booked。这种细粒度的属性设计充分考虑了业务展示的需求,例如avatarColor用于在界面上生成统一的占位头像,specialties数组则支持一个发型师具备多项技能的场景,booked状态则用于实时显示发型师是否正在接受服务。通过将这些属性集中定义在接口中,后续的组件可以直接引用这个类型,避免了魔法字符串和魔术数字的泛滥。

interface StylistMeta {
  id: number
  name: string
  title: string
  avatarColor: string
  rating: number
  reviews: number
  specialties: string[]
  price: number
  booked: boolean
}

在这里插入图片描述

StylistMeta接口中,每个字段的类型都经过仔细推敲。id使用number类型而非string,是因为在应用内部发型师的编号是连续递增的整数,数值类型在比较和排序时具有更好的性能表现。specialties被定义为字符串数组string[],这意味着一个发型师可以同时拥有多项专长,如男士剪发、烫发和染发等,数组结构天然支持这种一对多的关系。booked布尔值则是一个关键的实时状态字段,它决定了在界面上是否显示"忙"的标记,帮助顾客快速判断发型师的可预约状态。这种对接口字段的精心选择体现了开发者对业务逻辑的深刻理解。

紧接着,应用定义了预约订单的数据接口AppointmentMeta。这个接口承载了预约业务的核心信息,包括预约编号id、顾客姓名customerName、联系电话phone、服务项目service、预约发型师stylist、预约日期date、预约时间time、订单状态status和服务价格price。值得注意的是,电话字段采用了脱敏处理格式138****8888,这在保护顾客隐私的同时仍然保留了号码的辨识度。订单状态字段status使用了字符串枚举的形式,包含"已完成"、“进行中”、"待服务"和"已取消"四种状态,这种设计虽然简单但在当前业务场景下完全够用,后续如果需要更复杂的状态机也可以平滑升级。

interface AppointmentMeta {
  id: number
  customerName: string
  phone: string
  service: string
  stylist: string
  date: string
  time: string
  status: string
  price: number
}

在这里插入图片描述

评价数据的接口ReviewMeta同样经过精心设计,它包含了评价编号id、顾客姓名customerName、发型师姓名stylistName、评分星级rating、评价内容content、评价日期date和服务项目service。这个接口的一个巧妙之处在于同时记录了顾客和发型师双方的信息,这意味着在展示评价时可以从顾客视角(我评价了哪位发型师)或发型师视角(我收到了哪些评价)进行筛选和排序。rating字段使用数值类型而非字符串,便于后续计算平均分和进行区间筛选。评价内容content使用长文本字符串,允许顾客输入详细的反馈信息,这对服务质量的持续改进具有重要价值。

interface ReviewMeta {
  id: number
  customerName: string
  stylistName: string
  rating: number
  content: string
  date: string
  service: string
}

在这里插入图片描述

服务项目接口ServiceMeta和时间槽接口TimeSlotMeta则分别描述了可预约的服务类型和可用时间段。ServiceMeta包含了服务编号id、名称name、时长duration、价格price、图标icon和颜色color,其中icon使用了Emoji字符作为服务的视觉标识,这种轻量级的图标方案避免了引入额外的图标字体资源。TimeSlotMeta则采用了简化的结构,仅包含时间字符串time和可用状态available,这种设计使得时间网格的渲染逻辑变得异常简洁。总体而言,这五个接口共同构成了应用的数据骨架,它们之间的关联关系清晰地映射了现实业务中顾客、发型师、服务、预约和评价五个核心实体之间的交互逻辑。

interface ServiceMeta {
  id: number
  name: string
  duration: string
  price: number
  icon: string
  color: string
}

interface TimeSlotMeta {
  time: string
  available: boolean
}

在这里插入图片描述

三、设计令牌与主题配置系统

在现代应用开发中,设计令牌(Design Token)已经成为实现设计系统可维护性和一致性的核心工具。这款理发预约应用虽然体量不大,但同样引入了设计令牌的概念,通过集中管理视觉属性来确保整个应用的色彩、字体和间距风格保持高度统一。这种做法的好处是显而易见的:当需要进行品牌升级或主题切换时,开发者只需修改一处配置,整个应用的视觉风格就会同步更新,极大地降低了维护成本。

应用首先定义了一个完整的颜色调色板接口ColorPalette,这个接口包含了十五个颜色属性,涵盖了主色、强调色、背景色、文字色、边框色和状态色等各个维度的视觉需求。这种细粒度的颜色分类体现了开发者对UI设计规范的深入理解。例如,主色不仅定义了基础色primary,还定义了浅色变体primaryLight和深色变体primaryDark,这为创建渐变背景、悬浮状态和阴影效果提供了必要的颜色层次。同样,强调色也分为了基础色accent和浅色变体accentLight,用于按钮的不同状态和背景高亮场景。

interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  gold: string;
}

在这里插入图片描述

ColorPalette接口的设计充分考虑了实际UI开发中的各种场景。bg属性定义了页面的全局背景色,使用了偏暖的米白色#FAF5F2,这种颜色比纯白色更加柔和,长时间浏览时能够减少视觉疲劳。cardBg则定义了卡片的背景色,采用了纯白色#FFFFFF,通过背景色的微妙差异在视觉上区分页面层和卡片层,营造出层次分明的空间感。文字色被细分为三个层级:textPrimary用于标题和重要信息,textSecondary用于正文和次要信息,textHint则用于占位符和禁用状态的提示文字。这种三层文字色体系是移动端UI设计中的经典模式,它能够有效地建立信息层级,引导用户的阅读顺序。

在颜色令牌的实际赋值中,应用采用了复古棕#5D4037作为主色,这一选择非常契合理发店的行业属性。棕色通常与木材、皮革等天然材质相关联,能够传递出经典、稳重和可信赖的品牌感受。主色的深色变体#3E2723被用于渐变背景的起始色和主要文字色,浅色变体#BCAAA4则用于次要装饰和禁用状态的元素。强调色采用了理发红#E53935,这种鲜艳的红色在棕色基调的衬托下显得格外醒目,非常适合用于预约按钮、价格标签和状态指示等需要吸引用户注意力的场景。

const COLORS: ColorPalette = {
  primary: '#5D4037',
  primaryLight: '#BCAAA4',
  primaryDark: '#3E2723',
  accent: '#E53935',
  accentLight: '#FFCDD2',
  bg: '#FAF5F2',
  cardBg: '#FFFFFF',
  textPrimary: '#3E2723',
  textSecondary: '#795548',
  textHint: '#BCAAA4',
  border: '#EFEBE9',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  white: '#FFFFFF',
  gold: '#D4AF37'
};

在这里插入图片描述

除了颜色令牌,应用还定义了底部导航栏的配置常量TAB_CONFIG。这个常量使用了ArkTS中的Record<string, string>类型,将五个导航标签的英文标识映射为中文显示文本。这种映射关系的设计使得界面文本的修改和维护变得非常方便,同时也为后续的多语言国际化扩展预留了空间。导航标签包括"首页"、“预约”、“订单”、"评价"和"我的"五个模块,覆盖了顾客使用理发店应用的核心业务流程。

const TAB_CONFIG: Record<string, string> = {
  'home': '首页',
  'book': '预约',
  'orders': '订单',
  'reviews': '评价',
  'mine': '我的'
}

在这里插入图片描述

时间槽数据TIME_SLOTS是预约功能的核心配置之一,它定义了从上午九点到晚上八点的十二个可预约时间段。每个时间段都被标记为可用或不可用状态,这种设计模拟了真实业务中某些时段已被预约的场景。时间段的数据结构非常简洁,仅包含timeavailable两个字段,但正是这种简洁性使得后续的时间选择网格渲染变得高效而直观。在实际的业务系统中,这部分数据通常会从后端API动态获取,但在当前的前端原型中,硬编码的时间槽足以展示核心交互逻辑。

const TIME_SLOTS: TimeSlotMeta[] = [
  { time: '09:00', available: true },
  { time: '10:00', available: false },
  { time: '11:00', available: true },
  { time: '12:00', available: false },
  { time: '13:00', available: true },
  { time: '14:00', available: true },
  { time: '15:00', available: false },
  { time: '16:00', available: true },
  { time: '17:00', available: true },
  { time: '18:00', available: false },
  { time: '19:00', available: true },
  { time: '20:00', available: true }
]

在这里插入图片描述

四、工具函数与业务逻辑封装

在任何成熟的应用开发中,将通用的业务逻辑抽象为可复用的工具函数都是一项至关重要的工程实践。这款理发预约应用虽然代码规模不大,但同样遵循了这一原则,定义了四个精心设计的工具函数,分别负责星级评分渲染、订单状态颜色映射、营业额统计和平均评分计算。这些函数的共同特点是纯函数性质——给定相同的输入总是返回相同的输出,且不会产生副作用,这种特性使得它们易于测试、调试和复用。

getRatingStars函数是一个非常典型的字符串格式化工具,它接收一个数值类型的评分参数,返回由五角星字符组成的可视化评分字符串。函数内部通过一个简单的for循环迭代五次,根据评分值决定输出实心星还是空心星。这种基于Unicode字符的星级显示方案具有零依赖、零资源的优点,不需要引入任何图标字体或图片资源即可实现美观的评分展示。虽然这个实现目前只支持整数评分,但在当前的业务场景中已经完全够用,因为发型师的评分通常显示为一位小数,而星级展示往往只需要四舍五入到最近的整数即可。

function getRatingStars(rating: number): string {
  let stars: string = ''
  for (let i = 0; i < 5; i++) {
    if (i < rating) {
      stars += '★'
    } else {
      stars += '☆'
    }
  }
  return stars
}

在这里插入图片描述

getStatusColor函数则承担了订单状态到颜色映射的职责,这是数据可视化中的一个常见需求。函数接收订单状态字符串作为参数,通过一系列if条件判断返回对应的颜色值。已完成的状态映射为成功绿色,进行中的状态映射为强调红色,待服务的状态映射为警告橙色,而取消或其他未知状态则映射为中性灰色。这种颜色编码方案符合用户的直觉认知——绿色代表成功和完成,红色代表正在进行和需要注意,橙色代表待处理和即将开始。值得注意的是,这个函数返回的是来自COLORS令牌的具体色值,而不是硬编码的颜色字符串,这确保了状态颜色与整体主题的一致性。

function getStatusColor(status: string): string {
  if (status === '已完成') {
    return COLORS.success
  }
  if (status === '进行中') {
    return COLORS.accent
  }
  if (status === '待服务') {
    return COLORS.warning
  }
  return COLORS.textHint
}

getTotalRevenue函数实现了营业额的统计计算,它遍历预约订单数组,累加所有状态为"已完成"的订单金额。这个函数虽然简单,但体现了对业务规则的准确理解——只有已完成的订单才能计入实际收入,进行中和待服务的订单存在被取消或变更的风险,因而不应纳入营收统计。函数使用了for...of循环进行数组遍历,这种语法在现代JavaScript和TypeScript中比传统的索引循环更加简洁和可读。返回的总金额是一个整数类型的数值,可以直接用于界面展示或进一步的财务分析。

function getTotalRevenue(): number {
  let total: number = 0
  for (const a of APPOINTMENT_LIST) {
    if (a.status === '已完成') {
      total += a.price
    }
  }
  return total
}

getAvgRating函数计算了所有发型师的平均评分,这个结果在首页和评价页都会被使用到。函数的实现细节值得关注:它首先累加所有发型师的评分,然后除以发型师数量得到平均值,最后通过Math.floor(total / STYLIST_LIST.length * 10) / 10的处理保留一位小数。这种先乘后除的技巧是一种常见的数值精度处理方法,它避免了浮点数运算中可能出现的精度丢失问题。平均评分是店铺综合服务质量的重要指标,在界面上以醒目的金色大号字体展示,能够有效增强潜在顾客对店铺的信任感。

function getAvgRating(): number {
  let total: number = 0
  for (const s of STYLIST_LIST) {
    total += s.rating
  }
  return Math.floor(total / STYLIST_LIST.length * 10) / 10
}

五、入口组件与全局状态管理

在HarmonyOS ArkTS框架中,@Entry装饰器标记的组件是应用的入口点,它负责整个页面的生命周期管理和顶层布局编排。这款理发预约应用的入口组件名为BarberApp,它采用了@Component@Entry双重装饰,意味着它既是一个可复用的UI组件,也是应用的主页面。这种设计模式在单页面应用中非常常见,所有的子模块都在这个入口组件内部通过条件渲染进行切换,而不是通过路由跳转的方式导航。

BarberApp组件的状态管理采用了@State装饰器,这是ArkTS中实现响应式数据绑定的核心机制。组件定义了七个响应式状态变量,分别是当前选中的标签页currentTab、选中的发型师编号selectedStylist、选中的时间段selectedTime、预约弹窗显示状态showBookDialog、编辑弹窗显示状态showEditDialog、取消弹窗显示状态showCancelDialog以及目标预约对象targetAppointment。这些状态变量共同构成了应用的客户端状态树,任何一个状态的变化都会触发相应UI区域的自动重绘,这是声明式编程范式相较于命令式编程的最大优势。

@Entry
@Component
struct BarberApp {
  @State currentTab: string = 'home'
  @State selectedStylist: number = 1
  @State selectedTime: string = ''
  @State showBookDialog: boolean = false
  @State showEditDialog: boolean = false
  @State showCancelDialog: boolean = false
  @State targetAppointment: AppointmentMeta = APPOINTMENT_LIST[0]

currentTab状态的初始值设为'home',这意味着应用启动时默认展示首页内容。selectedStylist初始化为1,对应第一位发型师Tony老师,确保在预约流程中始终有一个默认选中的发型师。selectedTime初始化为空字符串,表示用户在进入预约页面时尚未选择具体时间。三个弹窗状态全部初始化为false,符合界面初始加载时不应显示任何弹窗的UX设计原则。targetAppointment被初始化为预约数组的第一个元素,虽然这个初始值在首次显示编辑或取消弹窗之前会被覆盖,但提供一个合法的初始值可以避免类型系统对空值的警告。

build方法是每个ArkTS组件的核心,它描述了组件的UI结构和布局关系。BarberAppbuild方法采用了Stack容器作为根布局,这种选择非常巧妙——Stack支持子元素的重叠布局,使得弹窗可以自然地覆盖在主内容之上。在Stack内部,主体内容被包裹在Column中,形成了从上到下的垂直布局结构。最顶部是一个自定义的标题栏,中间是动态切换的内容区域,底部是固定的导航标签栏。标题栏使用了Row容器实现水平布局,左侧是应用名称"barber Shop",右侧是一个理发店旋转灯符号"💈",整个标题栏应用了从深棕色到主棕色的135度线性渐变背景,营造出专业且富有层次感的视觉效果。

build() {
  Stack() {
    Column() {
      Row() {
        Text(' barber Shop')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Column().layoutWeight(1)
        Text('💈')
          .fontSize(20)
          .fontColor(COLORS.white)
      }
      .width('100%')
      .height(52)
      .padding({ left: 20, right: 20 })
      .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })

内容区域通过if...else if...else的条件渲染结构实现五个标签页的切换,这种实现方式虽然看起来比路由配置更加冗长,但在单页面场景下具有更好的性能和更简单的状态共享能力。每个条件分支调用对应的Builder方法渲染相应的内容,这些方法都是组件内部定义的私有构建器。内容区域被赋予了layoutWeight(1)属性,这意味着它会占据标题栏和导航栏之外的所有剩余空间,确保布局能够自适应不同高度的屏幕。在Stack的最外层,三个if条件分别控制三个弹窗的显示,当对应的布尔状态为true时,弹窗内容就会以半透明黑色背景的方式覆盖在主界面上。

      Column() {
        if (this.currentTab === 'home') {
          this.homeContent()
        } else if (this.currentTab === 'book') {
          this.bookContent()
        } else if (this.currentTab === 'orders') {
          this.ordersContent()
        } else if (this.currentTab === 'reviews') {
          this.reviewsContent()
        } else {
          this.mineContent()
        }
      }
      .layoutWeight(1)
      .backgroundColor(COLORS.bg)

      this.bottomTabBar()
    }
    .width('100%')
    .height('100%')

    if (this.showBookDialog) {
      this.bookDialog()
    }
    if (this.showEditDialog) {
      this.editDialog()
    }
    if (this.showCancelDialog) {
      this.cancelDialog()
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor(COLORS.bg)
}

六、底部导航栏的实现细节

底部导航栏是移动端应用中不可或缺的导航组件,它为用户提供了在主要功能模块之间快速切换的入口。这款理发预约应用的底部导航栏采用了经典的图标加文字组合形式,五个导航项均匀分布在屏幕底部,每个导航项在选中状态和未选中状态下呈现不同的视觉样式。导航栏的实现使用了@Builder装饰器,这意味着它是一个可复用的UI构建块,虽然在本应用中只在入口组件中调用了一次,但这种封装方式仍然提高了代码的组织性和可读性。

bottomTabBar构建器内部使用Row容器实现水平布局,通过ForEach循环遍历TAB_CONFIG对象的所有键值对来动态生成导航项。Object.keys(TAB_CONFIG)获取到的是导航项的英文标识数组['home', 'book', 'orders', 'reviews', 'mine'],这些标识符在内部使用,而界面上展示的是对应的中文文本。每个导航项都是一个Column容器,内部垂直排列着图标文本和标签文本。layoutWeight(1)属性确保五个导航项平分底部的可用宽度,在任何屏幕尺寸下都能保持均匀的间距分布。

@Builder
bottomTabBar() {
  Row() {
    ForEach(Object.keys(TAB_CONFIG), (key: string) => {
      Column() {
        Text(this.getTabIcon(key))
          .fontSize(20)
        Text(TAB_CONFIG[key])
          .fontSize(11)
          .margin({ top: 2 })
          .fontColor(this.currentTab === key ? COLORS.accent : COLORS.textHint)
      }
      .layoutWeight(1)
      .height(56)
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.currentTab = key
      })
    })
  }
  .width('100%')
  .height(56)
  .backgroundColor(COLORS.white)
  .border({ width: { top: 1 }, color: COLORS.border })
}

导航项的选中状态通过条件表达式this.currentTab === key ? COLORS.accent : COLORS.textHint实现动态颜色切换。当导航项被选中时,标签文字显示为理发红强调色,未选中时则显示为浅灰色的提示色。这种颜色对比虽然不如某些应用中图标变色加文字变色加背景高亮的组合效果那么强烈,但对于一个追求简约风格的理发店应用来说恰到好处,既提供了足够的状态反馈,又不会显得过于花哨。

getTabIcon方法是导航栏的一个有趣细节,它为每个导航项提供了选中态和未选态两套图标。例如,首页在选中时显示实心房子"🏠",未选中时显示空心房子"🏚";预约在选中时显示实心日历"📅",未选中时显示空心日历"📆"。这种成对的图标设计进一步增强了选中状态的视觉辨识度,用户即使不看文字颜色,仅通过图标的填充状态也能快速识别当前所在页面。图标全部使用了Emoji字符,这种轻量级的方案避免了图标字体文件的引入,减小了应用的包体积。

getTabIcon(key: string): string {
  if (key === 'home') {
    return this.currentTab === key ? '🏠' : '🏚'
  }
  if (key === 'book') {
    return this.currentTab === key ? '📅' : '📆'
  }
  if (key === 'orders') {
    return this.currentTab === key ? '📋' : '📑'
  }
  if (key === 'reviews') {
    return this.currentTab === key ? '⭐' : '☆'
  }
  return this.currentTab === key ? '👤' : '🙆'
}

七、首页模块深度解析

首页是用户打开应用后首先看到的界面,它承担着信息概览和功能导航的双重职责。这款理发预约应用的首页设计遵循了"首屏价值最大化"的原则,在有限的屏幕空间内集中展示了店铺的核心运营数据和最受欢迎的服务入口。homeContent构建器使用Scroll容器作为根布局,确保当内容超出屏幕高度时用户可以顺畅地上下滑动浏览。

首页的最上方是一个今日概览卡片,这个卡片采用了三列等分的布局结构,分别展示今日订单数量、今日营业收入和店铺平均评分三个关键指标。每个指标都包含一个醒目的数值和对应的标签文字,数值使用了22像素的加粗白色字体,标签则使用了11像素的半透明白色字体。营业收入的金额特别使用了金色COLORS.gold显示,这种颜色在金融和收入相关的场景中普遍与财富和增值相关联,能够在视觉上突出这一关键业务指标。概览卡片的背景应用了与标题栏相同的135度线性渐变,从深棕色过渡到主棕色,并在底部添加了阴影效果,使其在米白色的页面背景上呈现出明显的悬浮感。

@Builder
homeContent() {
  Scroll() {
    Column() {
      Row() {
        Column() {
          Text(APPOINTMENT_LIST.length.toString())
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('今日订单')
            .fontSize(11)
            .fontColor('#FFFFFFCC')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text('¥' + getTotalRevenue())
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.gold)
          Text('今日收入')
            .fontSize(11)
            .fontColor('#FFFFFFCC')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text(getAvgRating().toString())
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('平均评分')
            .fontSize(11)
            .fontColor('#FFFFFFCC')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }
      .width('92%')
      .padding({ top: 16, bottom: 16 })
      .borderRadius(16)
      .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })
      .margin({ top: 12, bottom: 16 })
      .shadow({ radius: 8, color: '#1A5D4037', offsetY: 4 })

概览卡片下方是推荐发型师区域,这个区域采用了横向滑动的卡片列表形式展示。区域标题使用了Row容器实现左右分布,左侧是"推荐发型师"的加粗标题,右侧是"查看全部 ›"的链接文字。标题文字使用了16像素的深棕色加粗字体,链接文字则使用了12像素的理发红色,这种大小和颜色的差异有效地建立了主次信息的层级关系。发型师卡片列表被包裹在一个水平方向的Scroll容器中,scrollBar(BarState.Off)属性隐藏了滚动条,使得界面更加简洁干净。

      Row() {
        Text('推荐发型师')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Column().layoutWeight(1)
        Text('查看全部 ›')
          .fontSize(12)
          .fontColor(COLORS.accent)
      }
      .width('92%')
      .margin({ bottom: 12 })

      Scroll() {
        Row() {
          ForEach(STYLIST_LIST, (stylist: StylistMeta) => {
            this.stylistCard(stylist)
          })
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(180)
      .margin({ bottom: 16 })

首页的第三个板块是热门服务展示,采用了流式布局(Flex Wrap)的方式排列服务卡片。每个服务卡片占据约30%的宽度,一行可以容纳三个卡片,当服务数量超过三个时自动换行。这种网格布局在移动端的卡片展示中非常高效,既充分利用了水平空间,又避免了卡片过小导致的触控困难。服务卡片的实现细节将在下一节中详细展开。

      Text('热门服务')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ left: 16, bottom: 12 })
        .alignSelf(ItemAlign.Start)

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(SERVICE_LIST, (service: ServiceMeta) => {
          this.serviceCard(service)
        })
      }
      .width('92%')
      .margin({ bottom: 12 })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

八、发型师卡片与服务卡片组件

发型师卡片是首页和预约页面共用的核心UI组件,它的设计直接影响用户对发型师的第一印象和预约意愿。stylistCard构建器接收一个StylistMeta类型的参数,基于发型师的数据动态渲染卡片内容。卡片的整体结构采用垂直Column布局,从上到下依次是头像区域、姓名、职称、评分和起价信息。

头像区域的设计非常有创意,它没有使用真实的照片,而是基于发型师名字的首字母生成一个圆形的纯色头像。这种方案在原型开发或隐私保护场景下非常实用,开发者只需要为每位发型师分配一个唯一的颜色值,就可以生成风格统一但各具特色的头像。头像使用Stack容器实现,内部是一个圆形的Column,背景色来自发型师数据中的avatarColor。在Stack的右上角,通过条件渲染if (stylist.booked)添加了一个"忙"的状态标签,这个标签使用了红色背景和白色文字,以醒目的方式告知顾客该发型师当前正在服务中。

@Builder
stylistCard(stylist: StylistMeta) {
  Column() {
    Stack() {
      Column() {
        Text(stylist.name.charAt(0))
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
      }
      .width(56)
      .height(56)
      .borderRadius(28)
      .backgroundColor(stylist.avatarColor)
      .justifyContent(FlexAlign.Center)

      if (stylist.booked) {
        Text('忙')
          .fontSize(10)
          .fontColor(COLORS.white)
          .padding({ left: 4, right: 4, top: 1, bottom: 1 })
          .backgroundColor(COLORS.danger)
          .borderRadius(4)
          .position({ x: '65%', y: 0 })
      }
    }
    .height(60)

    Text(stylist.name)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
      .margin({ top: 8 })

    Text(stylist.title)
      .fontSize(11)
      .fontColor(COLORS.textSecondary)
      .margin({ top: 2 })

    Row() {
      Text(getRatingStars(stylist.rating))
        .fontSize(12)
        .fontColor(COLORS.gold)
      Text(' ' + stylist.rating)
        .fontSize(11)
        .fontColor(COLORS.textSecondary)
    }
    .margin({ top: 4 })

    Text('¥' + stylist.price + '起')
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.accent)
      .margin({ top: 4 })
  }
  .width(120)
  .padding(12)
  .backgroundColor(COLORS.white)
  .borderRadius(16)
  .margin({ right: 10 })
  .alignItems(HorizontalAlign.Center)
  .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
  .onClick(() => {
    this.selectedStylist = stylist.id
    this.showBookDialog = true
  })
}

评分区域使用了Row容器水平排列星级图标和评分数值,星级通过前面介绍的getRatingStars函数生成,评分数值则直接显示一位小数。这种图文结合的评分展示方式比单纯的数字评分更加直观,用户可以快速扫视星级数量来判断发型师的服务质量。价格信息使用了理发红色和加粗字体,"起"字的添加巧妙地暗示了实际价格可能因服务项目的不同而有所浮动,避免了价格争议。

卡片的交互设计也值得称道,点击整个卡片会触发两个动作:一是将当前发型师的id赋值给selectedStylist状态,二是将showBookDialog设为true打开预约确认弹窗。这种一步到位的交互设计减少了用户的操作步骤,从浏览发型师到发起预约只需要一次点击,符合移动端"少即是多"的交互原则。

serviceCard构建器的结构相对简单,它采用了垂直居中的布局方式展示服务的图标、名称、时长和价格。每个服务都有一个独特的Emoji图标和主题色,例如男士剪发使用橙色剪刀"✂",女士剪发使用粉色理发符号"💇",烫发使用黄色旋风"🌀"。这些图标不仅美化了界面,更重要的是提供了快速识别的视觉线索,用户无需阅读文字就能大致判断服务的类型。服务卡片在流式布局中占据了30%的宽度,配合8像素的右下间距,形成了整齐美观的网格效果。

@Builder
serviceCard(service: ServiceMeta) {
  Column() {
    Text(service.icon)
      .fontSize(28)
    Text(service.name)
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
      .margin({ top: 6 })
    Text(service.duration)
      .fontSize(10)
      .fontColor(COLORS.textHint)
      .margin({ top: 2 })
    Text('¥' + service.price)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.accent)
      .margin({ top: 4 })
  }
  .width('30%')
  .padding({ top: 14, bottom: 14 })
  .backgroundColor(COLORS.white)
  .borderRadius(12)
  .margin({ right: 8, bottom: 8 })
  .alignItems(HorizontalAlign.Center)
  .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
}

九、预约模块深度解析

预约模块是整个应用的业务核心,它直接承载了顾客与理发店之间最核心的交互——服务预约。bookContent构建器实现了完整的在线预约流程,包括发型师选择、时间段选择和确认预约三个主要步骤。整个预约界面采用了垂直Column布局,各个功能区块按照使用流程自上而下依次排列,符合用户从上往下阅读和操作的习惯。

预约页面的顶部是一个简洁的页面标题"在线预约",使用了18像素的加粗深棕色字体,左侧对齐的放置方式与iOS和HarmonyOS的设计规范保持一致。标题下方是发型师选择区域,区域标签"选择发型师"使用了14像素的次要文字色,这种较小的字号和较浅的颜色处理表明它是一个辅助性的提示标签,而非主要内容。发型师选择采用了与首页类似的横向滑动列表,但视觉风格有所不同——选中的发型师头像背景变为理发红色,名字也随之变为红色,未选中的发型师则保持其原始头像色和灰色文字。这种选中态的视觉反馈非常直观,用户一眼就能看出当前选中了哪位发型师。

@Builder
bookContent() {
  Column() {
    Text('在线预约')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
      .margin({ top: 16, left: 16, bottom: 12 })
      .alignSelf(ItemAlign.Start)

    Text('选择发型师')
      .fontSize(14)
      .fontColor(COLORS.textSecondary)
      .margin({ left: 16, bottom: 8 })
      .alignSelf(ItemAlign.Start)

    Scroll() {
      Row() {
        ForEach(STYLIST_LIST, (stylist: StylistMeta) => {
          Column() {
            Column() {
              Text(stylist.name.charAt(0))
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.white)
            }
            .width(44)
            .height(44)
            .borderRadius(22)
            .backgroundColor(this.selectedStylist === stylist.id ? COLORS.accent : stylist.avatarColor)
            .justifyContent(FlexAlign.Center)

            Text(stylist.name)
              .fontSize(11)
              .fontColor(this.selectedStylist === stylist.id ? COLORS.accent : COLORS.textSecondary)
              .margin({ top: 4 })
          }
          .margin({ right: 12 })
          .alignItems(HorizontalAlign.Center)
          .onClick(() => {
            this.selectedStylist = stylist.id
          })
        })
      }
      .padding({ left: 16, right: 16 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .width('100%')
    .height(80)
    .margin({ bottom: 12 })

时间段选择区域是预约模块最具特色的交互设计之一。它采用了网格布局的方式展示一天中的十二个可预约时段,每个时段以药丸形状的按钮形式呈现。这种网格布局相比传统的下拉选择或时间轴选择更加直观,用户可以一次性看到全天的时段分布情况,快速找到符合自己时间安排的空闲时段。每个时段按钮有三种视觉状态:可用且未选中时显示白色背景和深色文字,可用且选中时显示理发红背景和白色文字,不可用时显示灰色背景和浅灰色文字。这三种状态的区分度非常高,用户无需阅读任何说明就能理解各个时段的可预约情况。

    Text('选择时间')
      .fontSize(14)
      .fontColor(COLORS.textSecondary)
      .margin({ left: 16, bottom: 8 })
      .alignSelf(ItemAlign.Start)

    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(TIME_SLOTS, (slot: TimeSlotMeta) => {
        Text(slot.time)
          .fontSize(13)
          .fontColor(slot.available ? (this.selectedTime === slot.time ? COLORS.white : COLORS.textPrimary) : COLORS.textHint)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .margin({ right: 8, bottom: 8 })
          .backgroundColor(slot.available ? (this.selectedTime === slot.time ? COLORS.accent : COLORS.white) : COLORS.border)
          .borderRadius(8)
          .onClick(() => {
            if (slot.available) {
              this.selectedTime = slot.time
            }
          })
      })
    }
    .width('92%')
    .margin({ bottom: 16 })

时段按钮的点击事件处理也非常严谨,只有当slot.availabletrue时才会更新selectedTime状态,这意味着已经被占用的时段不会响应用户的点击操作。这种防错设计避免了用户选择无效时段后的挫败感。页面底部的"确认预约"按钮使用了醒目的理发红色背景和大号白色文字,按钮宽度占据了92%的屏幕宽度,这种宽而明显的按钮设计在移动端表单提交场景中非常常见,它降低了用户的点击难度,同时也通过视觉重量引导用户完成最终的预约操作。

    Button('确认预约')
      .fontSize(16)
      .fontColor(COLORS.white)
      .backgroundColor(COLORS.accent)
      .borderRadius(12)
      .height(46)
      .width('92%')
      .onClick(() => {
        this.showBookDialog = true
      })
  }
  .width('100%')
  .height('100%')
}

十、订单管理模块深度解析

订单管理模块是店铺管理者使用频率最高的功能之一,它需要清晰地展示所有预约订单的信息,并提供便捷的订单操作入口。ordersContent构建器采用了可滚动的垂直列表布局,每个订单以独立的卡片形式呈现,这种设计使得订单信息的浏览和定位变得非常高效。

订单页面的顶部同样是"预约订单"的页面标题,使用了与预约页面一致的标题样式,保持了视觉风格的一致性。标题下方是订单卡片列表,通过ForEach循环遍历APPOINTMENT_LIST数组,为每个订单调用orderCard构建器渲染卡片。这种基于数据驱动的列表渲染方式是声明式UI编程的核心优势之一——开发者只需要描述"对于数组中的每个元素,渲染一个卡片",而无需手动管理列表的增删改查和DOM操作。

@Builder
ordersContent() {
  Scroll() {
    Column() {
      Text('预约订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 16, left: 16, bottom: 12 })
        .alignSelf(ItemAlign.Start)

      ForEach(APPOINTMENT_LIST, (app: AppointmentMeta) => {
        this.orderCard(app)
      })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

orderCard构建器的设计充分体现了信息层级化的设计思想。每张订单卡片被划分为三个主要区域:顶部的顾客与服务信息区、中部的分隔线、底部的发型师时间与操作区。顶部区域采用水平Row布局,左侧是顾客姓名和服务项目的垂直堆叠,右侧是订单状态标签。顾客姓名使用了15像素的加粗字体,服务项目则使用了12像素的次要颜色字体,这种字号差异使得顾客的辨识优先级高于具体的服务内容。订单状态标签是一个圆角矩形色块,背景色通过getStatusColor函数动态获取,文字使用了白色以确保在任何背景色上都有足够的对比度。

@Builder
orderCard(app: AppointmentMeta) {
  Column() {
    Row() {
      Column() {
        Text(app.customerName)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Text(app.service)
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Text(app.status)
        .fontSize(12)
        .fontColor(COLORS.white)
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor(getStatusColor(app.status))
        .borderRadius(10)
    }
    .width('100%')

    Divider().color(COLORS.border).margin({ top: 10, bottom: 10 })

中部使用Divider组件添加了一条浅色的分隔线,这条看似简单的分隔线实际上在视觉上起到了重要的区域划分作用,它帮助用户快速识别出卡片内部的信息分组。底部分为左右两部分,左侧显示了发型师图标、日期时间和价格信息,右侧则根据订单状态动态显示操作按钮。只有当订单状态为"待服务"时,才会显示"编辑"和"取消"两个操作按钮,已完成或已取消的订单则不再提供这些操作,这种条件渲染避免了用户对不可更改的订单进行无效操作。

    Row() {
      Text('💈 ' + app.stylist)
        .fontSize(12)
        .fontColor(COLORS.textSecondary)
      Column().layoutWeight(1)
      Text('📅 ' + app.date + ' ' + app.time)
        .fontSize(12)
        .fontColor(COLORS.textSecondary)
    }
    .width('100%')

    Row() {
      Text('💰 ¥' + app.price)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.accent)
      Column().layoutWeight(1)
      if (app.status === '待服务') {
        Button('编辑')
          .fontSize(12)
          .fontColor(COLORS.accent)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.accent })
          .borderRadius(8)
          .height(28)
          .padding({ left: 12, right: 12 })
          .onClick(() => {
            this.targetAppointment = app
            this.showEditDialog = true
          })
        Button('取消')
          .fontSize(12)
          .fontColor(COLORS.danger)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.danger })
          .borderRadius(8)
          .height(28)
          .padding({ left: 12, right: 12 })
          .margin({ left: 8 })
          .onClick(() => {
            this.targetAppointment = app
            this.showCancelDialog = true
          })
      }
    }
    .width('100%')
    .margin({ top: 8 })
  }
  .width('92%')
  .padding(14)
  .backgroundColor(COLORS.white)
  .borderRadius(14)
  .margin({ bottom: 8 })
  .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
}

操作按钮的设计采用了带边框的幽灵按钮风格,这种按钮在白色卡片背景上显得精致而不突兀。"编辑"按钮使用了理发红色的边框和文字,"取消"按钮则使用了危险红色的边框和文字,两个按钮通过颜色差异传达了不同的操作风险等级——编辑是一个安全的修改操作,而取消则是一个需要谨慎对待的破坏性操作。点击按钮时,会先将当前订单数据赋值给targetAppointment状态,然后打开对应的弹窗,这种数据传递方式确保了弹窗能够准确地显示当前操作对象的信息。

十一、顾客评价模块深度解析

顾客评价模块是建立信任关系和吸引新顾客的重要窗口。reviewsContent构建器在页面顶部设计了一个评分概览区域,然后下方是具体的评价卡片列表。这种"总-分"式的信息架构让用户可以先快速了解店铺的整体口碑,再根据需要深入阅读具体的评价内容。

评分概览区域采用了左右两栏布局,左侧显示综合评分和星级,右侧显示总评价数和好评率。综合评分使用了36像素的超大号金色字体,这种夸张的尺寸处理使其成为整个页面的视觉焦点,第一时间抓住用户的注意力。五星图标紧随其后,进一步强化了高分印象。右侧的总评价数使用了20像素的深色字体,好评率96%则使用了成功绿色,这种色彩编码在视觉上传递了积极的信号——评价数量多且好评率高,说明这是一家值得信赖的理发店。

@Builder
reviewsContent() {
  Scroll() {
    Column() {
      Row() {
        Column() {
          Text(getAvgRating().toString())
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.gold)
          Text(getRatingStars(5))
            .fontSize(14)
            .fontColor(COLORS.gold)
            .margin({ top: 4 })
          Text('综合评分')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text(REVIEW_LIST.length.toString())
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('总评价数')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 2 })
          Text('好评率 96%')
            .fontSize(11)
            .fontColor(COLORS.success)
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }
      .width('92%')
      .padding(16)
      .backgroundColor(COLORS.white)
      .borderRadius(16)
      .margin({ top: 12, bottom: 16 })
      .alignItems(VerticalAlign.Center)
      .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

概览卡片下方是评价列表,每条评价通过reviewCard构建器渲染为独立的卡片。评价卡片的设计借鉴了社交媒体中的评论样式,顶部是用户信息行,包含头像、姓名、服务信息和评分。头像同样采用了首字母加纯色背景的设计方案,但颜色统一使用了COLORS.primaryLight,与发型师卡片的彩色头像形成区分。姓名和服务信息垂直排列,服务信息包含了具体的服务项目名称和发型师姓名,这种详细的上下文信息帮助其他顾客理解评价的背景。评分星级位于信息行的最右侧,使用了金色的实心星显示,直观展示了该顾客给出的具体评分。

@Builder
reviewCard(review: ReviewMeta) {
  Column() {
    Row() {
      Stack() {
        Text(review.customerName.charAt(0))
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
      }
      .width(36)
      .height(36)
      .borderRadius(18)
      .backgroundColor(COLORS.primaryLight)

      Column() {
        Text(review.customerName)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Text(review.service + ' · ' + review.stylistName)
          .fontSize(11)
          .fontColor(COLORS.textSecondary)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 10 })
      .layoutWeight(1)

      Text(getRatingStars(review.rating))
        .fontSize(14)
        .fontColor(COLORS.gold)
    }
    .width('100%')
    .alignItems(VerticalAlign.Center)

评价正文使用了13像素的次要文字色,并设置了20像素的行高,这种稍大的行高使得长文本更易于阅读,不会产生拥挤感。评价日期使用了10像素的最小字号和提示色,放置在卡片的右下角,这种处理方式既保留了时间信息的可追溯性,又避免了对主要内容的视觉干扰。每条评价卡片的底部间距为8像素,配合卡片的白色背景和阴影效果,形成了清晰的列表分隔感。

    Text(review.content)
      .fontSize(13)
      .fontColor(COLORS.textSecondary)
      .lineHeight(20)
      .margin({ top: 10 })

    Text(review.date)
      .fontSize(10)
      .fontColor(COLORS.textHint)
      .margin({ top: 6 })
      .alignSelf(ItemAlign.End)
  }
  .width('92%')
  .padding(14)
  .backgroundColor(COLORS.white)
  .borderRadius(14)
  .margin({ bottom: 8 })
  .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
}

十二、个人中心模块解析

个人中心模块"我的"是应用的最后一个标签页,它为店铺管理者提供了一个账户信息和功能入口的聚合页面。mineContent构建器的设计相对简洁,主要包括店长信息卡片和功能菜单列表两部分。

店长信息卡片采用了水平Row布局,左侧是一个圆形的Emoji头像"👑",背景使用了浅粉色的强调色,这种女性化的颜色选择可能暗示了店长的性别特征,同时也与整体的暖色调主题相协调。右侧是店长的详细信息,包括职位"店长"、店铺名称"barber Shop 旗舰店"和本月营业额"¥12,860"。职位名称使用了18像素的加粗字体,店铺名称使用了12像素的次要颜色,营业额则使用了理发红色,通过颜色的差异建立了信息的主次层级。

@Builder
mineContent() {
  Scroll() {
    Column() {
      Row() {
        Stack() {
          Text('👑')
            .fontSize(32)
        }
        .width(60)
        .height(60)
        .borderRadius(30)
        .backgroundColor(COLORS.accentLight)

        Column() {
          Text('店长')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('barber Shop 旗舰店')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 4 })
          Text('本月营业额 ¥12,860')
            .fontSize(12)
            .fontColor(COLORS.accent)
            .margin({ top: 2 })
        }
        .margin({ left: 16 })
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('92%')
      .padding(16)
      .backgroundColor(COLORS.white)
      .borderRadius(16)
      .margin({ top: 16, bottom: 16 })
      .alignItems(VerticalAlign.Center)
      .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

信息卡片下方是六个功能菜单项,包括"店铺管理"、“发型师管理”、“财务统计”、“会员管理”、“营销活动"和"设置”。这些菜单项使用了统一的Row布局,左侧是功能名称,右侧是一个向右的箭头符号"›",暗示点击后可以进入下一级页面。每个菜单项都有白色的背景、12像素的圆角和8像素的底部间距,整体呈现出典型的列表式导航风格。虽然目前这些菜单项只是占位展示,没有实现具体的页面跳转逻辑,但这种预留的扩展结构为应用后续的功能迭代奠定了良好的基础。

      ForEach(['店铺管理', '发型师管理', '财务统计', '会员管理', '营销活动', '设置'], (item: string) => {
        Row() {
          Text(item)
            .fontSize(14)
            .fontColor(COLORS.textPrimary)
          Column().layoutWeight(1)
          Text('›')
            .fontSize(20)
            .fontColor(COLORS.textHint)
        }
        .width('92%')
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .backgroundColor(COLORS.white)
        .borderRadius(12)
        .margin({ bottom: 8 })
        .alignItems(VerticalAlign.Center)
      })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

十三、弹窗系统实现

弹窗系统是任何完整应用不可或缺的交互组件,它用于在不打断用户当前操作流程的前提下展示重要信息或收集用户输入。这款理发预约应用实现了三种弹窗:预约确认弹窗、编辑预约弹窗和取消预约确认弹窗。这三个弹窗虽然功能不同,但在视觉结构和实现模式上遵循了统一的设计规范。

三个弹窗的共同特点是都使用了Stack配合半透明黑色背景rgba(0,0,0,0.5)来实现遮罩层效果,这种半透明的背景既能突出弹窗内容,又能让用户感知到当前界面仍处于主页面之上。弹窗的内容区域都使用了圆角矩形卡片,宽度分别为80%或76%,这种略小于屏幕宽度的设计在视觉上更加精致,也符合移动端弹窗的设计惯例。每个弹窗内容卡片都绑定了一个空的点击事件onClick(() => {}),这个看似多余的绑定实际上是为了阻止点击事件穿透到遮罩层,避免用户在弹窗内部操作时意外关闭弹窗。

bookDialog构建器实现了预约确认弹窗,它的核心功能是展示用户当前选择的预约信息,并提供确认或取消操作的入口。弹窗顶部是"确认预约"的标题,下方是四个信息行,分别展示了发型师、预约日期、预约时间和服务费用。前三项使用了RowlayoutWeight的布局模式实现左右对齐,左侧是灰色标签,右侧是深色值。服务费用使用了15像素的加粗理发红色,在视觉上突出了本次预约的经济成本。值得注意的是,预约时间在用户尚未选择时会显示"请选择时间"的提示文字,并使用提示色,这种空值处理避免了界面显示空白或undefined的尴尬。

@Builder
bookDialog() {
  Column() {
    Column() {
      Text('确认预约')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)

      Row() {
        Text('发型师')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text(STYLIST_LIST[this.selectedStylist - 1].name)
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('预约日期')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('2024-04-17')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('预约时间')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text(this.selectedTime || '请选择时间')
          .fontSize(13)
          .fontColor(this.selectedTime ? COLORS.textPrimary : COLORS.textHint)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('服务费用')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('¥' + STYLIST_LIST[this.selectedStylist - 1].price)
          .fontSize(15)
          .fontColor(COLORS.accent)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })

      Row() {
        Button('取消')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showBookDialog = false
          })
        Button('确认预约')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.accent)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showBookDialog = false
          })
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('80%')
    .padding(20)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

弹窗底部是两个并排的操作按钮,左侧是"取消"按钮,使用了白色背景和灰色边框的中性风格;右侧是"确认预约"按钮,使用了理发红色背景和白色文字。两个按钮平分弹窗的可用宽度,中间留有12像素的间距。这种双按钮的布局是移动端弹窗的经典模式,左侧的取消操作通常使用中性色,右侧的确认操作则使用主题强调色,符合用户的操作习惯和视觉预期。当前两个按钮的点击事件都只是简单地将弹窗状态设为false,在真实的业务场景中,确认按钮还需要调用后端API创建预约记录。

editDialog构建器用于展示和编辑单个预约订单的信息,它的结构与前述的预约确认弹窗非常相似,但内容有所区别。弹窗标题为"编辑预约",信息行展示了客户姓名、服务项目和预约时间三项只读信息。与预约确认弹窗不同的是,编辑弹窗的信息都是已存在订单的确认信息,因此不需要处理空值状态。按钮区域左侧是"取消"按钮,右侧是"保存"按钮,保存按钮使用了主棕色而非理发红色,这种颜色差异在视觉上区分了"创建新预约"和"修改已有预约"两种不同的操作语义。

@Builder
editDialog() {
  Column() {
    Column() {
      Text('编辑预约')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ bottom: 16 })

      Row() {
        Text('客户姓名')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text(this.targetAppointment.customerName)
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('服务项目')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text(this.targetAppointment.service)
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('预约时间')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text(this.targetAppointment.date + ' ' + this.targetAppointment.time)
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })

      Row() {
        Button('取消')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showEditDialog = false
          })
        Button('保存')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.primary)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showEditDialog = false
          })
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('80%')
    .padding(20)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

cancelDialog构建器是三个弹窗中视觉风格最为独特的一个,因为它需要传达"警告"和"二次确认"的心理暗示。弹窗顶部是一个圆形的警告图标,使用了红色的感叹号"⚠"和浅红色的背景,这种强烈的视觉符号立刻让用户意识到这是一个需要谨慎对待的操作。标题"取消预约确认"使用了加粗字体,正文则详细说明了取消操作的后果——“取消后客户将收到通知短信”,这种具体的后果描述能够促使用户更加慎重地做出决定。

@Builder
cancelDialog() {
  Column() {
    Column() {
      Stack() {
        Text('⚠')
          .fontSize(40)
          .fontColor(COLORS.danger)
      }
      .width(64)
      .height(64)
      .borderRadius(32)
      .backgroundColor('#FFEBEE')
      .margin({ bottom: 16 })

      Text('取消预约确认')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)

      Text('确定要取消 ' + this.targetAppointment.customerName + ' 的预约吗?取消后客户将收到通知短信。')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
        .textAlign(TextAlign.Center)
        .lineHeight(20)
        .margin({ top: 12 })

      Row() {
        Button('再想想')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showCancelDialog = false
          })
        Button('确认取消')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.danger)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showCancelDialog = false
          })
      }
      .width('100%')
      .margin({ top: 20 })
    }
    .width('76%')
    .padding(24)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

取消弹窗的按钮文案也经过了精心设计,左侧按钮不是简单的"取消"而是"再想想",这种更具人情味的措辞降低了用户点击取消的心理阻力;右侧按钮"确认取消"则使用了危险红色背景,与常规的强调色形成鲜明对比,进一步强化了操作的风险提示。弹窗宽度设为76%,略小于其他两个弹窗,这种微妙的尺寸差异在视觉上增加了一些变化,避免了所有弹窗千篇一律的单调感。

十四、模块对比分析

为了更清晰地展示这款理发预约应用各个模块之间的差异和特点,下面通过一张详细的对比表格进行总结。

模块名称 核心功能 布局方式 主要交互元素 数据展示形式 视觉特色 状态管理复杂度
首页概览 展示店铺核心运营数据和推荐内容 垂直滚动+横向滑动+流式布局 发型师卡片点击、服务卡片浏览 数字指标+卡片列表+网格 渐变背景概览卡片、圆角阴影 低,主要展示静态数据
在线预约 引导用户完成服务预约全流程 垂直线性布局 发型师选择、时段网格选择、确认按钮 横向头像列表+时段网格 选中态红色高亮、药丸形时段按钮 中,涉及发型师和时间两个选择状态
订单管理 展示所有预约订单并提供操作入口 垂直滚动列表 编辑按钮、取消按钮 订单卡片纵向列表 状态颜色编码、条件显示操作按钮 中,涉及目标订单的选择和弹窗控制
顾客评价 展示店铺综合评分和顾客评价详情 垂直滚动列表 评价卡片浏览 评分概览+评价卡片列表 金色高分强调、用户头像首字母 低,主要展示静态评价数据
个人中心 展示店长信息和功能菜单入口 垂直滚动列表 菜单项点击(预留) 信息卡片+菜单列表 皇冠Emoji头像、箭头导航指示 低,以静态展示为主
预约确认弹窗 汇总预约信息并请求用户最终确认 居中弹窗 确认按钮、取消按钮 信息行纵向排列 半透明遮罩、圆角白色卡片 中,依赖外部传入的选择状态
编辑预约弹窗 展示订单详情并提供编辑保存功能 居中弹窗 保存按钮、取消按钮 只读信息行 主棕色确认按钮、分隔线信息排版 中,依赖目标订单对象状态
取消确认弹窗 警告用户取消操作的后果并二次确认 居中弹窗 确认取消按钮、再想想按钮 警告图标+说明文字 红色危险主题、圆形警告标识 低,仅控制弹窗显示状态

从上表可以看出,这款应用的八个主要模块在功能定位上各有侧重,但在设计语言和交互模式上保持了高度的一致性。所有模块都采用了圆角卡片作为信息容器,都使用了阴影效果来增加层次感和悬浮感,文字层级也都遵循了"标题-正文-提示"的三层体系。这种一致性不仅提升了用户的认知效率,也体现了开发者在设计系统层面的深思熟虑。状态管理复杂度方面,首页和顾客评价模块相对较低,因为它们以数据展示为主;而预约和订单模块则涉及更多的用户交互和状态变化,需要更多的@State变量来支撑其功能。弹窗系统虽然视觉风格各异,但在底层实现上遵循了统一的遮罩加卡片模式,这种复用性的设计思想值得在实际开发中借鉴和推广。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 245.ets - 理发预约APP
// 主题色:复古棕 #5D4037 + 理发红 #E53935
// 布局:发型师横滑卡片 + 预约时间网格 + 评价星级列表 + 今日订单

// ==================== 类型定义 ====================
interface StylistMeta {
  id: number
  name: string
  title: string
  avatarColor: string
  rating: number
  reviews: number
  specialties: string[]
  price: number
  booked: boolean
}

interface AppointmentMeta {
  id: number
  customerName: string
  phone: string
  service: string
  stylist: string
  date: string
  time: string
  status: string
  price: number
}

interface ReviewMeta {
  id: number
  customerName: string
  stylistName: string
  rating: number
  content: string
  date: string
  service: string
}

interface ServiceMeta {
  id: number
  name: string
  duration: string
  price: number
  icon: string
  color: string
}

interface TimeSlotMeta {
  time: string
  available: boolean
}

// ==================== 设计令牌 ====================
interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  gold: string;
}

const COLORS: ColorPalette = {
  primary: '#5D4037',
  primaryLight: '#BCAAA4',
  primaryDark: '#3E2723',
  accent: '#E53935',
  accentLight: '#FFCDD2',
  bg: '#FAF5F2',
  cardBg: '#FFFFFF',
  textPrimary: '#3E2723',
  textSecondary: '#795548',
  textHint: '#BCAAA4',
  border: '#EFEBE9',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  white: '#FFFFFF',
  gold: '#D4AF37'
};

const TAB_CONFIG: Record<string, string> = {
  'home': '首页',
  'book': '预约',
  'orders': '订单',
  'reviews': '评价',
  'mine': '我的'
}

const TIME_SLOTS: TimeSlotMeta[] = [
  { time: '09:00', available: true },
  { time: '10:00', available: false },
  { time: '11:00', available: true },
  { time: '12:00', available: false },
  { time: '13:00', available: true },
  { time: '14:00', available: true },
  { time: '15:00', available: false },
  { time: '16:00', available: true },
  { time: '17:00', available: true },
  { time: '18:00', available: false },
  { time: '19:00', available: true },
  { time: '20:00', available: true }
]

// ==================== 硬编码数据 ====================
const STYLIST_LIST: StylistMeta[] = [
  { id: 1, name: 'Tony老师', title: '首席发型师', avatarColor: '#FFAB91', rating: 4.9, reviews: 386, specialties: ['男士剪发', '烫发', '染发'], price: 128, booked: false },
  { id: 2, name: 'Amy老师', title: '高级发型师', avatarColor: '#F48FB1', rating: 4.8, reviews: 254, specialties: ['女士剪发', '造型', '染发'], price: 98, booked: true },
  { id: 3, name: 'Kevin老师', title: '资深发型师', avatarColor: '#FFE082', rating: 4.7, reviews: 198, specialties: ['烫发', '头皮护理'], price: 88, booked: false },
  { id: 4, name: 'Lisa老师', title: '创意总监', avatarColor: '#CE93D8', rating: 5.0, reviews: 512, specialties: ['新娘造型', '时尚染发', '编发'], price: 188, booked: true },
  { id: 5, name: 'Mike老师', title: '高级发型师', avatarColor: '#90CAF9', rating: 4.6, reviews: 167, specialties: ['男士造型', ' beard修整'], price: 78, booked: false }
]

const APPOINTMENT_LIST: AppointmentMeta[] = [
  { id: 1001, customerName: '张先生', phone: '138****8888', service: '男士剪发+造型', stylist: 'Tony老师', date: '2024-04-16', time: '14:00', status: '已完成', price: 168 },
  { id: 1002, customerName: '李女士', phone: '139****6666', service: '染发+护理', stylist: 'Amy老师', date: '2024-04-16', time: '15:00', status: '进行中', price: 288 },
  { id: 1003, customerName: '王先生', phone: '137****5555', service: '烫发', stylist: 'Kevin老师', date: '2024-04-16', time: '16:00', status: '待服务', price: 358 },
  { id: 1004, customerName: '赵女士', phone: '136****4444', service: '新娘造型', stylist: 'Lisa老师', date: '2024-04-17', time: '09:00', status: '待服务', price: 588 },
  { id: 1005, customerName: '陈先生', phone: '135****3333', service: '男士剪发', stylist: 'Mike老师', date: '2024-04-17', time: '10:00', status: '待服务', price: 78 },
  { id: 1006, customerName: '孙女士', phone: '134****2222', service: '剪发+染发', stylist: 'Amy老师', date: '2024-04-17', time: '11:00', status: '待服务', price: 328 },
  { id: 1007, customerName: '周先生', phone: '133****1111', service: '烫发+造型', stylist: 'Tony老师', date: '2024-04-15', time: '16:00', status: '已取消', price: 288 },
  { id: 1008, customerName: '吴女士', phone: '132****0000', service: '头皮护理', stylist: 'Kevin老师', date: '2024-04-15', time: '14:00', status: '已完成', price: 158 }
]

const REVIEW_LIST: ReviewMeta[] = [
  { id: 1, customerName: '张先生', stylistName: 'Tony老师', rating: 5, content: 'Tony老师手艺非常好,剪的发型很满意,下次还会来!', date: '2024-04-15', service: '男士剪发+造型' },
  { id: 2, customerName: '李女士', stylistName: 'Amy老师', rating: 5, content: 'Amy老师染的颜色很自然,服务态度也特别好,推荐!', date: '2024-04-14', service: '染发+护理' },
  { id: 3, customerName: '王先生', stylistName: 'Kevin老师', rating: 4, content: '烫发效果不错,就是等待时间稍微有点长', date: '2024-04-13', service: '烫发' },
  { id: 4, customerName: '赵女士', stylistName: 'Lisa老师', rating: 5, content: 'Lisa老师做的新娘造型太美了,婚礼当天被夸了一整天!', date: '2024-04-12', service: '新娘造型' },
  { id: 5, customerName: '陈先生', stylistName: 'Mike老师', rating: 4, content: '剪发很利落,价格也实惠,性价比很高', date: '2024-04-11', service: '男士剪发' },
  { id: 6, customerName: '孙女士', stylistName: 'Amy老师', rating: 5, content: '每次都找Amy老师,技术稳定,沟通也很顺畅', date: '2024-04-10', service: '剪发+染发' }
]

const SERVICE_LIST: ServiceMeta[] = [
  { id: 1, name: '男士剪发', duration: '30分钟', price: 58, icon: '✂', color: '#FFAB91' },
  { id: 2, name: '女士剪发', duration: '45分钟', price: 78, icon: '💇', color: '#F48FB1' },
  { id: 3, name: '烫发', duration: '2小时', price: 258, icon: '🌀', color: '#FFE082' },
  { id: 4, name: '染发', duration: '1.5小时', price: 198, icon: '🎨', color: '#CE93D8' },
  { id: 5, name: '头皮护理', duration: '1小时', price: 158, icon: '💆', color: '#90CAF9' },
  { id: 6, name: '造型设计', duration: '40分钟', price: 88, icon: '✨', color: '#A5D6A7' }
]

function getRatingStars(rating: number): string {
  let stars: string = ''
  for (let i = 0; i < 5; i++) {
    if (i < rating) {
      stars += '★'
    } else {
      stars += '☆'
    }
  }
  return stars
}

function getStatusColor(status: string): string {
  if (status === '已完成') {
    return COLORS.success
  }
  if (status === '进行中') {
    return COLORS.accent
  }
  if (status === '待服务') {
    return COLORS.warning
  }
  return COLORS.textHint
}

function getTotalRevenue(): number {
  let total: number = 0
  for (const a of APPOINTMENT_LIST) {
    if (a.status === '已完成') {
      total += a.price
    }
  }
  return total
}

function getAvgRating(): number {
  let total: number = 0
  for (const s of STYLIST_LIST) {
    total += s.rating
  }
  return Math.floor(total / STYLIST_LIST.length * 10) / 10
}

// ==================== 入口组件 ====================
@Entry
@Component
struct BarberApp {
  @State currentTab: string = 'home'
  @State selectedStylist: number = 1
  @State selectedTime: string = ''
  @State showBookDialog: boolean = false
  @State showEditDialog: boolean = false
  @State showCancelDialog: boolean = false
  @State targetAppointment: AppointmentMeta = APPOINTMENT_LIST[0]

  build() {
    Stack() {
      Column() {
        Row() {
          Text(' barber Shop')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Column().layoutWeight(1)
          Text('💈')
            .fontSize(20)
            .fontColor(COLORS.white)
        }
        .width('100%')
        .height(52)
        .padding({ left: 20, right: 20 })
        .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })

        Column() {
          if (this.currentTab === 'home') {
            this.homeContent()
          } else if (this.currentTab === 'book') {
            this.bookContent()
          } else if (this.currentTab === 'orders') {
            this.ordersContent()
          } else if (this.currentTab === 'reviews') {
            this.reviewsContent()
          } else {
            this.mineContent()
          }
        }
        .layoutWeight(1)
        .backgroundColor(COLORS.bg)

        this.bottomTabBar()
      }
      .width('100%')
      .height('100%')

      if (this.showBookDialog) {
        this.bookDialog()
      }
      if (this.showEditDialog) {
        this.editDialog()
      }
      if (this.showCancelDialog) {
        this.cancelDialog()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

  @Builder
  bottomTabBar() {
    Row() {
      ForEach(Object.keys(TAB_CONFIG), (key: string) => {
        Column() {
          Text(this.getTabIcon(key))
            .fontSize(20)
          Text(TAB_CONFIG[key])
            .fontSize(11)
            .margin({ top: 2 })
            .fontColor(this.currentTab === key ? COLORS.accent : COLORS.textHint)
        }
        .layoutWeight(1)
        .height(56)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = key
        })
      })
    }
    .width('100%')
    .height(56)
    .backgroundColor(COLORS.white)
    .border({ width: { top: 1 }, color: COLORS.border })
  }

  getTabIcon(key: string): string {
    if (key === 'home') {
      return this.currentTab === key ? '🏠' : '🏚'
    }
    if (key === 'book') {
      return this.currentTab === key ? '📅' : '📆'
    }
    if (key === 'orders') {
      return this.currentTab === key ? '📋' : '📑'
    }
    if (key === 'reviews') {
      return this.currentTab === key ? '⭐' : '☆'
    }
    return this.currentTab === key ? '👤' : '🙆'
  }

  // ========== 首页Tab ==========
  @Builder
  homeContent() {
    Scroll() {
      Column() {
        // 今日概览
        Row() {
          Column() {
            Text(APPOINTMENT_LIST.length.toString())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.white)
            Text('今日订单')
              .fontSize(11)
              .fontColor('#FFFFFFCC')
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('¥' + getTotalRevenue())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.gold)
            Text('今日收入')
              .fontSize(11)
              .fontColor('#FFFFFFCC')
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text(getAvgRating().toString())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.white)
            Text('平均评分')
              .fontSize(11)
              .fontColor('#FFFFFFCC')
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('92%')
        .padding({ top: 16, bottom: 16 })
        .borderRadius(16)
        .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })
        .margin({ top: 12, bottom: 16 })
        .shadow({ radius: 8, color: '#1A5D4037', offsetY: 4 })

        // 发型师横滑
        Row() {
          Text('推荐发型师')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Column().layoutWeight(1)
          Text('查看全部 ›')
            .fontSize(12)
            .fontColor(COLORS.accent)
        }
        .width('92%')
        .margin({ bottom: 12 })

        Scroll() {
          Row() {
            ForEach(STYLIST_LIST, (stylist: StylistMeta) => {
              this.stylistCard(stylist)
            })
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .height(180)
        .margin({ bottom: 16 })

        // 热门服务
        Text('热门服务')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ left: 16, bottom: 12 })
          .alignSelf(ItemAlign.Start)

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(SERVICE_LIST, (service: ServiceMeta) => {
            this.serviceCard(service)
          })
        }
        .width('92%')
        .margin({ bottom: 12 })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  @Builder
  stylistCard(stylist: StylistMeta) {
    Column() {
      Stack() {
        Column() {
          Text(stylist.name.charAt(0))
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
        }
        .width(56)
        .height(56)
        .borderRadius(28)
        .backgroundColor(stylist.avatarColor)
        .justifyContent(FlexAlign.Center)

        if (stylist.booked) {
          Text('忙')
            .fontSize(10)
            .fontColor(COLORS.white)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .backgroundColor(COLORS.danger)
            .borderRadius(4)
            .position({ x: '65%', y: 0 })
        }
      }
      .height(60)

      Text(stylist.name)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 8 })

      Text(stylist.title)
        .fontSize(11)
        .fontColor(COLORS.textSecondary)
        .margin({ top: 2 })

      Row() {
        Text(getRatingStars(stylist.rating))
          .fontSize(12)
          .fontColor(COLORS.gold)
        Text(' ' + stylist.rating)
          .fontSize(11)
          .fontColor(COLORS.textSecondary)
      }
      .margin({ top: 4 })

      Text('¥' + stylist.price + '起')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.accent)
        .margin({ top: 4 })
    }
    .width(120)
    .padding(12)
    .backgroundColor(COLORS.white)
    .borderRadius(16)
    .margin({ right: 10 })
    .alignItems(HorizontalAlign.Center)
    .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
    .onClick(() => {
      this.selectedStylist = stylist.id
      this.showBookDialog = true
    })
  }

  @Builder
  serviceCard(service: ServiceMeta) {
    Column() {
      Text(service.icon)
        .fontSize(28)
      Text(service.name)
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 6 })
      Text(service.duration)
        .fontSize(10)
        .fontColor(COLORS.textHint)
        .margin({ top: 2 })
      Text('¥' + service.price)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.accent)
        .margin({ top: 4 })
    }
    .width('30%')
    .padding({ top: 14, bottom: 14 })
    .backgroundColor(COLORS.white)
    .borderRadius(12)
    .margin({ right: 8, bottom: 8 })
    .alignItems(HorizontalAlign.Center)
    .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
  }

  // ========== 预约Tab ==========
  @Builder
  bookContent() {
    Column() {
      Text('在线预约')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 16, left: 16, bottom: 12 })
        .alignSelf(ItemAlign.Start)

      // 选择发型师
      Text('选择发型师')
        .fontSize(14)
        .fontColor(COLORS.textSecondary)
        .margin({ left: 16, bottom: 8 })
        .alignSelf(ItemAlign.Start)

      Scroll() {
        Row() {
          ForEach(STYLIST_LIST, (stylist: StylistMeta) => {
            Column() {
              Column() {
                Text(stylist.name.charAt(0))
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.white)
              }
              .width(44)
              .height(44)
              .borderRadius(22)
              .backgroundColor(this.selectedStylist === stylist.id ? COLORS.accent : stylist.avatarColor)
              .justifyContent(FlexAlign.Center)

              Text(stylist.name)
                .fontSize(11)
                .fontColor(this.selectedStylist === stylist.id ? COLORS.accent : COLORS.textSecondary)
                .margin({ top: 4 })
            }
            .margin({ right: 12 })
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.selectedStylist = stylist.id
            })
          })
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(80)
      .margin({ bottom: 12 })

      // 选择时间
      Text('选择时间')
        .fontSize(14)
        .fontColor(COLORS.textSecondary)
        .margin({ left: 16, bottom: 8 })
        .alignSelf(ItemAlign.Start)

      // 时间网格
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(TIME_SLOTS, (slot: TimeSlotMeta) => {
          Text(slot.time)
            .fontSize(13)
            .fontColor(slot.available ? (this.selectedTime === slot.time ? COLORS.white : COLORS.textPrimary) : COLORS.textHint)
            .padding({ left: 16, right: 16, top: 8, bottom: 8 })
            .margin({ right: 8, bottom: 8 })
            .backgroundColor(slot.available ? (this.selectedTime === slot.time ? COLORS.accent : COLORS.white) : COLORS.border)
            .borderRadius(8)
            .onClick(() => {
              if (slot.available) {
                this.selectedTime = slot.time
              }
            })
        })
      }
      .width('92%')
      .margin({ bottom: 16 })

      // 确认按钮
      Button('确认预约')
        .fontSize(16)
        .fontColor(COLORS.white)
        .backgroundColor(COLORS.accent)
        .borderRadius(12)
        .height(46)
        .width('92%')
        .onClick(() => {
          this.showBookDialog = true
        })
    }
    .width('100%')
    .height('100%')
  }

  // ========== 订单Tab ==========
  @Builder
  ordersContent() {
    Scroll() {
      Column() {
        Text('预约订单')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ top: 16, left: 16, bottom: 12 })
          .alignSelf(ItemAlign.Start)

        ForEach(APPOINTMENT_LIST, (app: AppointmentMeta) => {
          this.orderCard(app)
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  @Builder
  orderCard(app: AppointmentMeta) {
    Column() {
      Row() {
        Column() {
          Text(app.customerName)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text(app.service)
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text(app.status)
          .fontSize(12)
          .fontColor(COLORS.white)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .backgroundColor(getStatusColor(app.status))
          .borderRadius(10)
      }
      .width('100%')

      Divider().color(COLORS.border).margin({ top: 10, bottom: 10 })

      Row() {
        Text('💈 ' + app.stylist)
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('📅 ' + app.date + ' ' + app.time)
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
      }
      .width('100%')

      Row() {
        Text('💰 ¥' + app.price)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.accent)
        Column().layoutWeight(1)
        if (app.status === '待服务') {
          Button('编辑')
            .fontSize(12)
            .fontColor(COLORS.accent)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.accent })
            .borderRadius(8)
            .height(28)
            .padding({ left: 12, right: 12 })
            .onClick(() => {
              this.targetAppointment = app
              this.showEditDialog = true
            })
          Button('取消')
            .fontSize(12)
            .fontColor(COLORS.danger)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.danger })
            .borderRadius(8)
            .height(28)
            .padding({ left: 12, right: 12 })
            .margin({ left: 8 })
            .onClick(() => {
              this.targetAppointment = app
              this.showCancelDialog = true
            })
        }
      }
      .width('100%')
      .margin({ top: 8 })
    }
    .width('92%')
    .padding(14)
    .backgroundColor(COLORS.white)
    .borderRadius(14)
    .margin({ bottom: 8 })
    .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
  }

  // ========== 评价Tab ==========
  @Builder
  reviewsContent() {
    Scroll() {
      Column() {
        // 评分概览
        Row() {
          Column() {
            Text(getAvgRating().toString())
              .fontSize(36)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.gold)
            Text(getRatingStars(5))
              .fontSize(14)
              .fontColor(COLORS.gold)
              .margin({ top: 4 })
            Text('综合评分')
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text(REVIEW_LIST.length.toString())
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('总评价数')
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .margin({ top: 2 })
            Text('好评率 96%')
              .fontSize(11)
              .fontColor(COLORS.success)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS.white)
        .borderRadius(16)
        .margin({ top: 12, bottom: 16 })
        .alignItems(VerticalAlign.Center)
        .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

        ForEach(REVIEW_LIST, (review: ReviewMeta) => {
          this.reviewCard(review)
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  @Builder
  reviewCard(review: ReviewMeta) {
    Column() {
      Row() {
        Stack() {
          Text(review.customerName.charAt(0))
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
        }
        .width(36)
        .height(36)
        .borderRadius(18)
        .backgroundColor(COLORS.primaryLight)

        Column() {
          Text(review.customerName)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text(review.service + ' · ' + review.stylistName)
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 10 })
        .layoutWeight(1)

        Text(getRatingStars(review.rating))
          .fontSize(14)
          .fontColor(COLORS.gold)
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)

      Text(review.content)
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
        .lineHeight(20)
        .margin({ top: 10 })

      Text(review.date)
        .fontSize(10)
        .fontColor(COLORS.textHint)
        .margin({ top: 6 })
        .alignSelf(ItemAlign.End)
    }
    .width('92%')
    .padding(14)
    .backgroundColor(COLORS.white)
    .borderRadius(14)
    .margin({ bottom: 8 })
    .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
  }

  // ========== 我的Tab ==========
  @Builder
  mineContent() {
    Scroll() {
      Column() {
        Row() {
          Stack() {
            Text('👑')
              .fontSize(32)
          }
          .width(60)
          .height(60)
          .borderRadius(30)
          .backgroundColor(COLORS.accentLight)

          Column() {
            Text('店长')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('barber Shop 旗舰店')
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .margin({ top: 4 })
            Text('本月营业额 ¥12,860')
              .fontSize(12)
              .fontColor(COLORS.accent)
              .margin({ top: 2 })
          }
          .margin({ left: 16 })
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS.white)
        .borderRadius(16)
        .margin({ top: 16, bottom: 16 })
        .alignItems(VerticalAlign.Center)
        .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

        ForEach(['店铺管理', '发型师管理', '财务统计', '会员管理', '营销活动', '设置'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(14)
              .fontColor(COLORS.textPrimary)
            Column().layoutWeight(1)
            Text('›')
              .fontSize(20)
              .fontColor(COLORS.textHint)
          }
          .width('92%')
          .padding({ left: 16, right: 16, top: 14, bottom: 14 })
          .backgroundColor(COLORS.white)
          .borderRadius(12)
          .margin({ bottom: 8 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  // ==================== 弹窗 ====================
  @Builder
  bookDialog() {
    Column() {
      Column() {
        Text('确认预约')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)

        Row() {
          Text('发型师')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text(STYLIST_LIST[this.selectedStylist - 1].name)
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('预约日期')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('2024-04-17')
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('预约时间')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text(this.selectedTime || '请选择时间')
            .fontSize(13)
            .fontColor(this.selectedTime ? COLORS.textPrimary : COLORS.textHint)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('服务费用')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('¥' + STYLIST_LIST[this.selectedStylist - 1].price)
            .fontSize(15)
            .fontColor(COLORS.accent)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })

        Row() {
          Button('取消')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showBookDialog = false
            })
          Button('确认预约')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.accent)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showBookDialog = false
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('80%')
      .padding(20)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  editDialog() {
    Column() {
      Column() {
        Text('编辑预约')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ bottom: 16 })

        Row() {
          Text('客户姓名')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text(this.targetAppointment.customerName)
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('服务项目')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text(this.targetAppointment.service)
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('预约时间')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text(this.targetAppointment.date + ' ' + this.targetAppointment.time)
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })

        Row() {
          Button('取消')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showEditDialog = false
            })
          Button('保存')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.primary)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showEditDialog = false
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('80%')
      .padding(20)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  cancelDialog() {
    Column() {
      Column() {
        Stack() {
          Text('⚠')
            .fontSize(40)
            .fontColor(COLORS.danger)
        }
        .width(64)
        .height(64)
        .borderRadius(32)
        .backgroundColor('#FFEBEE')
        .margin({ bottom: 16 })

        Text('取消预约确认')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)

        Text('确定要取消 ' + this.targetAppointment.customerName + ' 的预约吗?取消后客户将收到通知短信。')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .textAlign(TextAlign.Center)
          .lineHeight(20)
          .margin({ top: 12 })

        Row() {
          Button('再想想')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showCancelDialog = false
            })
          Button('确认取消')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.danger)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showCancelDialog = false
            })
        }
        .width('100%')
        .margin({ top: 20 })
      }
      .width('76%')
      .padding(24)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}


十五、技术要点总结

通过对这款理发预约应用的逐段代码分析,我们可以提炼出多个值得深入理解和学习的HarmonyOS ArkTS开发技术要点。首先在架构设计层面,这款应用采用了单页面多模块的经典模式,通过一个入口组件和多个@Builder构建器实现了五个功能模块的组织和切换。这种模式的优势在于状态管理简单直接,不同模块之间可以方便地共享状态和方法,避免了多页面架构中常见的状态同步和参数传递问题。当然,这种架构也有其局限性,当应用规模进一步扩大时,入口组件的代码量会变得难以维护,此时就需要考虑引入状态管理框架(如AppStorage或自定义Store)以及路由分层的优化策略。

在这里插入图片描述

其次在类型系统的使用上,这款应用展示了ArkTS作为TypeScript超集在移动端开发中的强大能力。通过为每个业务实体定义精确的接口类型,开发者不仅获得了编译时的类型检查保障,还能享受到IDE提供的智能补全和重构支持。 especially值得注意的是,应用中的类型设计并非简单的字段罗列,而是充分考虑了业务场景的合理性,例如StylistMeta接口中的specialties字符串数组、AppointmentMeta接口中的脱敏手机号格式、以及ServiceMeta接口中的Emoji图标字段,这些细节都体现了类型驱动开发(Type-Driven Development)的思想。

在UI构建方面,这款应用综合运用了ArkTS声明式UI框架中的多种容器和布局方式。ColumnRow作为最基础的线性布局容器,承担了大部分的界面结构搭建工作;Stack容器则在头像状态标记和弹窗遮罩层等场景下发挥了重叠布局的优势;Scroll容器配合ScrollDirection.Horizontal实现了发型师列表的横向滑动效果;Flex容器配合FlexWrap.Wrap则实现了服务卡片的自动换行网格布局。这些布局方式的灵活组合,使得开发者能够以简洁的代码描述出复杂的界面结构,这是声明式编程相对于传统命令式UI编程的核心优势所在。

视觉设计方面,这款应用虽然是一个技术示例,但在设计细节上不乏亮点。复古棕与理发红的配色方案既符合行业属性又具有视觉冲击力;设计令牌的引入确保了全局色彩的一致性;渐变背景、阴影效果、圆角边框等现代UI元素的运用提升了界面的精致感;而Emoji字符作为轻量级图标的方案则在功能性和包体积之间取得了良好的平衡。这些设计决策对于开发者来说具有很好的参考价值,尤其是在资源受限或快速原型开发的场景下。

最后,从可改进的方向来看,这款应用还有多个可以优化和扩展的切入点。在数据层,当前所有的业务数据都是硬编码的常量数组,在实际部署中需要替换为网络请求和本地存储的结合;在交互层,弹窗的确认按钮目前只是简单关闭弹窗,缺少实际的后端调用和 loading 状态处理;在功能层,个人中心的六个菜单项还只是占位符,店铺管理、财务统计等核心后台功能有待实现;在用户体验层,可以添加页面转场动画、列表下拉刷新、上拉加载更多等常见的移动端交互模式。此外,随着HarmonyOS系统的持续演进,开发者还可以考虑引入更多的系统能力,如服务卡片(Widget)、分布式流转、语音助手集成等,进一步提升应用的竞争力和用户粘性。

更多推荐