一、HarmonyOS技术生态与医疗健康服务场景的技术交汇

HarmonyOS作为华为面向万物互联时代打造的分布式操作系统,在HarmonyOS 6.1.1版本中已经构建了完整的应用开发技术栈。ArkTS语言作为HarmonyOS应用开发的核心语言,基于TypeScript进行了深度扩展,在保留静态类型检查能力的同时引入了ArkUI声明式框架,使开发者能够以极简的代码描述复杂的UI结构和交互逻辑。在API 24版本中,ArkTS的语言能力已经覆盖了从基础UI组件到高级状态管理、从动画特效到数据绑定的全方位开发需求,为医疗健康服务类应用的开发提供了坚实的技术基础。

就医陪诊服务是近年来快速发展的新兴本地生活服务领域。随着人口老龄化加剧、异地就医需求增长、独居青年增多等社会因素,陪诊服务逐渐成为一种刚需型服务。安心陪诊场景正是基于这一社会需求构建的美团类应用,涵盖了陪诊师预约、服务套餐选择、订单管理、健康档案维护、就医须知科普、用户评价展示等六个核心业务板块。该场景通过病历夹页式的标签栏(单排6个)组织内容,通过青色(#00838F)与橙色(#FF7043)的色彩搭配传达了医疗专业性与服务温暖感的双重品牌调性。

ArkUI声明式框架在医疗健康类应用中的优势尤为突出。这类应用的核心交互需求包括:大量结构化数据的列表展示(陪诊师列表、订单记录、健康档案等)、多维度筛选与过滤(按医院、按关系、按状态)、表单交互(预约信息填写、档案编辑)、以及弹框交互(预约确认、取消警示、加急服务)。ArkTS通过@State装饰器实现了组件内部状态的响应式管理,当用户切换筛选条件或修改表单内容时,框架会自动计算受影响的UI区域并执行精准刷新,而非整页面重建。这种机制在处理大量列表项条件过滤时表现尤为出色——安心陪诊场景中的订单列表通过orderShown方法实现了四种状态的条件过滤渲染,每次筛选切换时只有符合条件的列表项被渲染,未匹配的项被跳过,保证了筛选交互的即时响应。

在色彩心理学层面,该场景的设计选择颇具深意。主色#00838F是Material Design中的Cyan 800色值,一种偏深的青绿色,在医疗领域被广泛使用——它既有蓝色的冷静专业感,又有绿色的生命健康感,是医院导视系统中最常见的色彩之一。辅助色#FF7043是Material Design中的Deep Orange 400色值,一种温暖的珊瑚橙色,用于强调服务的人性化温度和加急服务的紧迫感。这两种颜色的搭配形成了"专业冷色+温暖暖色"的互补色组合,青色主导界面整体氛围传达专业可信赖感,橙色在关键操作按钮和紧急服务标识中点睛,形成视觉焦点的引导。文字三档色值(#1F3033、#5A7276、#93A9AC)都带有微弱的青色基调,与主色保持色彩统一性。

从数据架构角度审视,该场景采用了interface接口定义与@Observed类implements的设计模式。六组数据模型分别定义了陪诊师、服务套餐、订单记录、健康档案、就医须知和用户评价的业务实体。每组模型通过interface声明字段契约,通过@Observed修饰的类实现响应式状态管理,构造函数中通过三元表达式为布尔字段提供安全默认值。这种设计模式在医疗数据场景中尤为重要——医疗相关数据对类型安全和数据完整性的要求极高,任何字段缺失或类型错误都可能导致信息展示错误,interface的编译时类型检查和构造函数的安全赋值模式共同构建了数据可靠性的双重保障。

二、ColorPalette色彩系统与视觉语言

色彩系统的设计是陪诊场景品牌调性传达的基础。通过interface约束确保颜色字段的完整性,通过const实例化统一管理所有色值。

interface ColorPalette {
  bg: string
  card: string
  white: string
  primary: string
  primaryDeep: string
  accent: string
  accentSoft: string
  gold: string
  textPrimary: string
  textSecond: string
  textThird: string
  line: string
  tagBg: string
  warn: string
  danger: string
  ok: string
}

在这里插入图片描述

ColorPalette接口定义了16个颜色字段,这种统一的色彩契约设计确保了应用中所有颜色引用都来自同一个数据源。在医疗应用中,色彩的一致性尤为重要——不同页面使用相同的文字层级色值可以避免视觉跳跃感,状态色(warn/danger/ok)的统一使用可以帮助用户快速识别信息类别。

const COLORS: ColorPalette = {
  bg: '#F0F6F6',
  card: '#FFFFFF',
  white: '#FFFFFF',
  primary: '#00838F',
  primaryDeep: '#005662',
  accent: '#FF7043',
  accentSoft: '#FFEBE3',
  gold: '#C99700',
  textPrimary: '#1F3033',
  textSecond: '#5A7276',
  textThird: '#93A9AC',
  line: '#E2EDEC',
  tagBg: '#E9F2F1',
  warn: '#E65100',
  danger: '#C62828',
  ok: '#2E7D32'
}

在这里插入图片描述

背景色#F0F6F6是一种极浅的青绿色调白色,模拟了医院环境的洁净感。主色#00838F是青绿色,传达了医疗行业的专业性和可信赖感。深色变体#005662用于渐变收尾和按钮的深色态。辅助色#FF7043橙色用于加急服务、价格强调和操作按钮,在青色主背景上形成高对比度的视觉焦点。accentSoft浅橙色#FFEBE3用于标签背景和强调区域。金色#C99700用于评分和统计数据的标识。文字三档色值从#1F3033到#93A9AC,明度递减形成信息层级。分隔线#E2EDEC和标签背景#E9F2F1都带有微弱的青色基调,保持整体色彩和谐。

三、数据模型体系:六组interface与@Observed类实现

安心陪诊场景定义了六组数据模型,每组都采用interface定义字段契约、@Observed类implements实现响应式状态管理的模式。

首先是陪诊师模型Nurse和NurseItem:

interface Nurse {
  id: number
  name: string
  score: number
  orders: number
  hosp: string
  years: number
  fav: boolean
}

@Observed
class NurseItem implements Nurse {
  id: number = 0
  name: string = ''
  score: number = 0
  orders: number = 0
  hosp: string = ''
  years: number = 0
  fav: boolean = false

  constructor(o: Nurse) {
    this.id = o.id
    this.name = o.name
    this.score = o.score
    this.orders = o.orders
    this.hosp = o.hosp
    this.years = o.years
    this.fav = o.fav ? o.fav : false
  }
}

在这里插入图片描述

Nurse接口设计了七个字段:唯一标识id、陪诊师姓名name、评分score、服务单数orders、擅长医院hosp、从业年限years和收藏标记fav。hosp字段是陪诊师模型的核心设计——它标识了陪诊师最擅长的就诊医院(如"协和医院"“同济医院”"儿童医院"等),帮助用户根据就医目标选择熟悉该医院流程的陪诊师。orders字段提供了陪诊师服务经验的量化参考,score字段反映了用户对服务质量的评价。构造函数中fav字段采用三元表达式安全赋值。

服务套餐模型ServicePack和ServicePackItem:

interface ServicePack {
  id: number
  name: string
  desc: string
  price: number
  unit: string
  hot: boolean
}

@Observed
class ServicePackItem implements ServicePack {
  id: number = 0
  name: string = ''
  desc: string = ''
  price: number = 0
  unit: string = ''
  hot: boolean = false

  constructor(o: ServicePack) {
    this.id = o.id
    this.name = o.name
    this.desc = o.desc
    this.price = o.price
    this.unit = o.unit
    this.hot = o.hot ? o.hot : false
  }
}

在这里插入图片描述

ServicePack接口的设计特点是引入了desc描述字段和unit计价单位字段。desc字段(如"挂号候诊缴费取药全程陪同")为用户提供了服务内容的详细说明,弥补了服务名称简洁但信息不足的问题。unit字段(“次”“天”"趟"等)使得价格展示更加准确——半天陪诊按次计价(168元/次),住院陪护按天计价(320元/天),接送就医按趟计价(98元/趟)。hot字段标记热门套餐,在UI中通过火焰emoji标识。这种多维度的服务定价模型充分覆盖了陪诊服务的不同计价场景。

订单记录模型OrderRec和OrderRecItem的设计聚焦于订单的状态追踪:

interface OrderRec {
  id: number
  hosp: string
  date: string
  stat: string
  nurse: string
  fee: number
}

@Observed
class OrderRecItem implements OrderRec {
  id: number = 0
  hosp: string = ''
  date: string = ''
  stat: string = ''
  nurse: string = ''
  fee: number = 0

  constructor(o: OrderRec) {
    this.id = o.id
    this.hosp = o.hosp
    this.date = o.date
    this.stat = o.stat
    this.nurse = o.nurse
    this.fee = o.fee
  }
}

在这里插入图片描述

OrderRec接口的stat字段是该模型的核心,它定义了订单的四种状态:“陪诊中”(正在进行的服务)、“待服务”(已预约未开始)、“已完成”(服务结束)、以及潜在的其他状态。这四种状态在UI中通过statColor函数映射为不同的颜色——陪诊中使用primary青色、待服务使用warn橙色、已完成使用ok绿色,使用户能够通过颜色快速区分订单状态。date字段(如"08-22 09:00")同时包含日期和时间信息,格式紧凑且信息完整。该模型没有布尔字段,因此构造函数中不涉及安全赋值模式。

健康档案模型HealthFile和HealthFileItem的设计体现了医疗数据的特殊性:

interface HealthFile {
  id: number
  name: string
  rel: string
  age: number
  note: string
  remind: boolean
}

@Observed
class HealthFileItem implements HealthFile {
  id: number = 0
  name: string = ''
  rel: string = ''
  age: number = 0
  note: string = ''
  remind: boolean = false

  constructor(o: HealthFile) {
    this.id = o.id
    this.name = o.name
    this.rel = o.rel
    this.age = o.age
    this.note = o.note
    this.remind = o.remind ? o.remind : false
  }
}

在这里插入图片描述

HealthFile接口的设计紧贴家庭健康管理的实际需求。name字段是被管理人的姓名,rel字段是与管理人的关系(如"本人"“母亲”“儿子”“婆婆"等),age字段是年龄,note字段记录了健康备注信息(如"高血压 · 长期用药”“糖尿病 · 每月复诊”"哮喘 · 随身喷雾"等)。remind布尔字段控制复诊智能提醒的开关。这组字段的设计涵盖了家庭健康管理中最核心的信息维度——谁需要管理、与用户什么关系、年龄多大、有什么健康问题、是否需要复诊提醒。在UI中,note字段使用warn橙色显示,使其在卡片中形成视觉焦点,提醒用户关注被管理人的健康状况。

就医须知模型Tip和TipItem以及用户评价模型Review和ReviewItem:

interface Tip {
  id: number
  title: string
  cat: string
  reads: number
}

@Observed
class TipItem implements Tip {
  id: number = 0
  title: string = ''
  cat: string = ''
  reads: number = 0

  constructor(o: Tip) {
    this.id = o.id
    this.title = o.title
    this.cat = o.cat
    this.reads = o.reads
  }
}

interface Review {
  id: number
  user: string
  nurse: string
  stars: number
  txt: string
}

@Observed
class ReviewItem implements Review {
  id: number = 0
  user: string = ''
  nurse: string = ''
  stars: number = 0
  txt: string = ''

  constructor(o: Review) {
    this.id = o.id
    this.user = o.user
    this.nurse = o.nurse
    this.stars = o.stars
    this.txt = o.txt
  }
}

在这里插入图片描述

Tip接口的cat字段(如"新手必读"“医保科普”“流程指南”"挂号攻略"等)为就医须知进行了分类,reads字段记录了阅读量,帮助用户判断文章的热度。Review接口的stars字段是1-5的整数评分,txt字段是用户的文字评价。user字段和nurse字段分别标识了评价者和被评价的陪诊师,形成了一种"用户评价陪诊师"的评价体系。这两个模型都没有布尔字段,构造函数中直接赋值。

四、静态数据数组与业务常量

陪诊场景为六组数据模型分别提供了丰富的模拟数据,这些数据在UI中通过ForEach渲染为列表或网格。

陪诊师数据数组NURSES提供了12位不同专长的陪诊师:

const NURSES: NurseItem[] = [
  new NurseItem({ id: 1, name: '林安', score: 4.9, orders: 2860, hosp: '协和医院', years: 6, fav: true }),
  new NurseItem({ id: 2, name: '周到的周', score: 4.8, orders: 2310, hosp: '同济医院', years: 5, fav: false }),
  new NurseItem({ id: 3, name: '安心姐', score: 5.0, orders: 3120, hosp: '人民医院', years: 8, fav: false }),
  new NurseItem({ id: 4, name: '小鹿陪诊', score: 4.7, orders: 1580, hosp: '儿童医院', years: 3, fav: false }),
  new NurseItem({ id: 5, name: '陈伯通', score: 4.8, orders: 2040, hosp: '肿瘤医院', years: 7, fav: false }),
  new NurseItem({ id: 6, name: '暖阳', score: 4.9, orders: 2670, hosp: '中山医院', years: 6, fav: false }),
  new NurseItem({ id: 7, name: '铁柱哥', score: 4.6, orders: 980, hosp: '骨科医院', years: 2, fav: false }),
  new NurseItem({ id: 8, name: '苏叶', score: 4.8, orders: 1890, hosp: '妇产医院', years: 4, fav: false }),
  new NurseItem({ id: 9, name: '老黄牛', score: 4.7, orders: 1450, hosp: '中医院', years: 5, fav: false }),
  new NurseItem({ id: 10, name: '月半湾', score: 4.9, orders: 2210, hosp: '第一医院', years: 6, fav: false }),
  new NurseItem({ id: 11, name: '青云', score: 4.6, orders: 860, hosp: '口腔医院', years: 2, fav: false }),
  new NurseItem({ id: 12, name: '棠棠', score: 4.8, orders: 1730, hosp: '精神卫生中心', years: 4, fav: false })
]

陪诊师数据的设计反映了陪诊行业的多样化服务能力。每位陪诊师都关联了一家擅长医院,涵盖了综合医院(协和、同济、人民、中山、第一医院)、专科医院(儿童、肿瘤、骨科、妇产、口腔、精神卫生中心)和中医医院等不同类型。评分从4.6到5.0,服务单数从860到3120,从业年限从2年到8年,这些量化指标为用户选择陪诊师提供了多维度的参考依据。陪诊师姓名的设计也颇具特色——“林安”“安心姐”"暖阳"等名字传达了安心温暖的服务调性,“铁柱哥”"老黄牛"等名字则传达了踏实可靠的工薪阶层气质。

业务常量的定义体现了陪诊场景的统计与可视化需求:

const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const MONTH_ORDERS: number[] = [38, 46, 52, 61, 74, 88]
const SVC_LABELS: string[] = ['半天陪诊', '代办问诊', '接送就医', '住院陪护']
const SVC_PCTS: number[] = [42, 26, 20, 12]
const SVC_COLORS: string[] = ['#00838F', '#FF7043', '#C99700', '#78909C']
const HOSP_TAGS: string[] = ['协和医院', '同济医院', '人民医院', '儿童医院', '中医院']
const REL_TAGS: string[] = ['本人', '父母', '子女', '伴侣', '亲友']

function monthOrderBar(i: number): number {
  return MONTH_ORDERS[i] * 1.6
}

function statColor(s: string): string {
  if (s === '陪诊中') {
    return COLORS.primary
  }
  if (s === '待服务') {
    return COLORS.warn
  }
  return COLORS.ok
}

在这里插入图片描述

MONTH_LABELS和MONTH_ORDERS配合使用,在档案页面渲染近6个月的陪诊订单柱状图,数据呈现明显的逐月增长趋势(从38到88),反映了陪诊服务需求的增长态势。monthOrderBar函数通过乘以1.6的系数放大柱状图高度。SVC_LABELS和SVC_PCTS配合SVC_COLORS用于渲染服务类型占比的堆叠条形图,四种服务类型按比例(42%、26%、20%、12%)用不同颜色展示。HOSP_TAGS和REL_TAGS分别用于预约弹框的医院选择和档案弹框的关系选择标签。statColor函数将订单状态字符串映射为颜色值,是状态可视化的核心逻辑。

五、组件状态管理与业务方法

PageCareCompanion组件的状态变量设计覆盖了六个内容板块和四个弹框的全部交互需求。

@Entry
@Component
struct PageCareCompanion {
  @State curTab: number = 0
  @State mainTab: number = 0
  @State addOpen: boolean = false
  @State editOpen: boolean = false
  @State delOpen: boolean = false
  @State bizOpen: boolean = false
  @State nurseList: NurseItem[] = NURSES
  @State orderList: OrderRecItem[] = ORDERS
  @State fileList: HealthFileItem[] = FILES
  @State reviewList: ReviewItem[] = REVIEWS
  @State rev: number = 0
  @State tick: number = 0
  @State hospTags: number[] = [0]
  @State relTags: number[] = [1]
  @State hourStep: number = 4
  @State ageStep: number = 58
  @State remindFlag: boolean = true
  @State carTags: number[] = [0]
  @State rushFlag: boolean = false
  @State orderFilter: number = 0
  private timer: number = -1

状态变量的设计反映了该场景特有的交互需求。orderFilter是订单过滤器的状态变量,取值0-3分别对应"全部"“待服务”“陪诊中”"已完成"四种过滤模式。hospTags用于预约弹框的医院选择,relTags用于档案弹框的关系选择,carTags用于加急弹框的车型选择。hourStep控制预约陪诊的服务时长步进器(2-12小时),ageStep控制档案编辑的年龄步进器(1-110岁),remindFlag控制复诊智能提醒开关,rushFlag控制加急接送的红灯优先调度开关。

生命周期方法采用了105毫秒间隔的定时器:

aboutToAppear(): void {
  this.timer = setInterval(() => {
    this.tick = this.tick + 1
  }, 105)
}

aboutToDisappear(): void {
  if (this.timer >= 0) {
    clearInterval(this.timer)
  }
}

105毫秒的间隔介于陶艺场景的100毫秒和射箭场景的120毫秒之间,这种细微的差异体现了陪诊场景"沉稳但不拖沓"的交互节奏感——医疗场景需要给人以从容感,但又不能过于缓慢影响效率。

订单过滤方法是该场景最具特色的业务逻辑:

orderShown(i: number): boolean {
  if (this.orderFilter === 0) {
    return true
  }
  if (this.orderFilter === 1) {
    return this.orderList[i].stat === '待服务'
  }
  if (this.orderFilter === 2) {
    return this.orderList[i].stat === '陪诊中'
  }
  return this.orderList[i].stat === '已完成'
}

bookTotal(): number {
  return this.hourStep * 42 + 50
}

orderShown方法是订单列表条件渲染的核心。当orderFilter为0时返回true,表示显示全部订单;为1时只显示"待服务"状态的订单;为2时只显示"陪诊中"状态的订单;为3时只显示"已完成"状态的订单。这种通过方法返回布尔值控制ForEach内部条件渲染的设计,是ArkTS中实现列表过滤的推荐模式——它避免了在数据层面进行数组操作(如filter),而是通过UI层面的条件渲染实现过滤效果,保持了原始数据的完整性。bookTotal方法通过hourStep * 42 + 50计算预估费用,其中42是每小时陪诊基准费用,50是基础服务费。

此外,档案提醒的切换方法和收藏方法也值得关注:

toggleNurseFav(i: number): void {
  this.nurseList[i].fav = !this.nurseList[i].fav
  this.rev = this.rev + 1
}

toggleRemind(i: number): void {
  this.fileList[i].remind = !this.fileList[i].remind
  this.rev = this.rev + 1
}

toggleRemind方法是该场景独有的业务方法,它切换健康档案中某位被管理人的复诊提醒状态。由于HealthFileItem是@Observed类,remind属性的变化会立即反映到UI中——提醒开关的背景色和文字会即时更新。这种响应式的状态切换使得用户可以快速为不同家庭成员开启或关闭复诊提醒,操作反馈即时且直观。

六、fxLayer动画特效层

陪诊场景的fxLayer设计使用了与医疗主题相关的emoji符号,通过transform变换实现了轻量级的动态视觉效果。

@Builder
fxLayer() {
  Stack() {
    Text('🚑')
      .fontSize(24)
      .translate({ x: this.tick % 36 })
      .position({ x: 40, y: 130 })
      .opacity(0.8)
    Text('❤️')
      .fontSize(20)
      .scale({ x: 0.85 + (this.tick % 10) / 12, y: 0.85 + (this.tick % 10) / 12 })
      .position({ x: 300, y: 230 })
      .opacity(0.8)
    Text('💊')
      .fontSize(18)
      .rotate({ angle: (this.tick % 24) * 15 })
      .position({ x: 130, y: 330 })
      .opacity(0.6)
    Text('✨')
      .fontSize(16)
      .position({ x: 500, y: 120 })
      .opacity((this.tick % 8) / 8 + 0.15)
    Text('🩺')
      .fontSize(20)
      .translate({ y: this.tick % 40 })
      .position({ x: 540, y: 400 })
      .opacity(0.5)
  }
  .width('100%')
  .height('100%')
  .hitTestBehavior(HitTestMode.None)
}

fxLayer使用了五个与医疗相关的emoji符号:🚑(救护车)做水平位移模拟紧急出车,❤️(心形)做缩放呼吸模拟心跳脉搏,💊(药丸)做旋转动画模拟药物滚动,✨(闪光)做透明度闪烁模拟医疗器械反光,🩺(听诊器)做垂直位移模拟检查动作。每个动画通过tick值与取模运算生成周期性变化:tick % 36使救护车在36个tick(约3.8秒)内完成一次水平循环;tick % 10 / 12使心形的缩放范围控制在0.85到1.68之间,模拟心跳的舒张收缩节律;tick % 24 * 15使药丸每24个tick(约2.5秒)完成一周360度旋转。hitTestBehavior(HitTestMode.None)确保了动画层不拦截触摸事件。

七、header头部与病历夹页式folderTab

header构建器定义了应用顶部的品牌区域:

@Builder
header() {
  Column() {
    Row() {
      Text('🩺')
        .fontSize(24)
      Text('安心陪诊')
        .fontSize(21)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.white)
        .margin({ left: 8 })
      Column()
        .layoutWeight(1)
      Text('📍全国·三甲覆盖')
        .fontSize(12)
        .fontColor('#CBE8EC')
      Text('🔔')
        .fontSize(19)
        .margin({ left: 12 })
        .onClick(() => {
          this.mainTab = 3
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12 })

    Row() {
      Text('🔍')
        .fontSize(15)
      Text('搜陪诊师 / 医院 / 服务')
        .fontSize(13)
        .fontColor('#A9D5DC')
        .margin({ left: 8 })
      Column()
        .layoutWeight(1)
      Text('搜索')
        .fontSize(13)
        .fontColor(COLORS.white)
        .padding({ left: 14, right: 14, top: 6, bottom: 6 })
        .backgroundColor(COLORS.accent)
        .borderRadius(14)
        .onClick(() => {
          this.curTab = 0
        })
    }
    .width('100%')
    .padding({ left: 10, right: 10 })
    .margin({ top: 10 })
    .backgroundColor('rgba(255,255,255,0.2)')
    .borderRadius(20)

    Row({ space: 8 }) {
      Text('持证陪诊师')
        .fontSize(11)
        .fontColor(COLORS.white)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .backgroundColor('rgba(255,255,255,0.24)')
        .borderRadius(12)
      Text('服务上保险')
        .fontSize(11)
        .fontColor(COLORS.white)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .backgroundColor('rgba(255,255,255,0.24)')
        .borderRadius(12)
      Text('不满意全退')
        .fontSize(11)
        .fontColor(COLORS.white)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .backgroundColor('rgba(255,255,255,0.24)')
        .borderRadius(12)
      Text('老人专享价')
        .fontSize(11)
        .fontColor(COLORS.white)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .backgroundColor('rgba(255,255,255,0.24)')
        .borderRadius(12)
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
    .margin({ top: 10, bottom: 14 })
  }
  .width('100%')
  .alignItems(HorizontalAlign.Start)
  .linearGradient({ angle: 135, colors: [[COLORS.primary, 0], ['#0097A7', 1]] })
}

头部渐变从135度方向的#00838F青色渐变到#0097A7浅青色,营造出医院环境的洁净专业感。品牌名"安心陪诊"使用了21号粗体白色文字,配合🩺听诊器emoji图标形成品牌标识。定位信息"📍全国·三甲覆盖"使用了浅色文字#CBE8EC,传达了服务覆盖范围的专业性。搜索按钮使用了accent橙色背景和白色文字,在青色渐变背景上形成高对比度的行动号召点。促销标签行包含了"持证陪诊师"“服务上保险”“不满意全退”"老人专享价"四个标签,前三个强调服务保障,最后一个体现了对老年人群体的特殊关怀——这与陪诊服务的主要受众群体(老年人及其家属)高度契合。

folderTab是病历夹页式标签的设计,是该场景的核心布局特色:

@Builder
folderTab(name: string, icon: string, idx: number) {
  Column() {
    Column() {
      Text(icon)
        .fontSize(15)
      Text(name)
        .fontSize(11)
        .fontWeight(this.curTab === idx ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(this.curTab === idx ? COLORS.white : COLORS.textSecond)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 7, bottom: 7, left: 11, right: 11 })
    .backgroundColor(this.curTab === idx ? COLORS.primary : COLORS.card)
    .borderRadius(8)
    Rect({ width: 18, height: 4 })
      .fill(this.curTab === idx ? COLORS.accent : COLORS.line)
      .margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Center)
  .onClick(() => {
    this.switchTab(idx)
  })
}

folderTab的设计灵感来源于病历夹的标签页。每个标签项由上下两部分组成:上方的Column包含图标和名称文字,选中时使用primary青色背景白色粗体文字,未选中时使用card白色背景灰色常规文字;下方的Rect矩形条宽度为18高度为4,选中时填充accent橙色,未选中时填充line浅灰色。这个矩形条的设计模拟了病历夹中突出于页面上方的彩色标签——当某个标签被选中时,其下方的彩色标记变亮(橙色),未选中的标签标记保持暗淡(浅灰色),形成了一种"翻页标记"的视觉效果。

tabBar将六个folderTab排列在一行中:

@Builder
tabBar() {
  Row({ space: 7 }) {
    this.folderTab('陪诊师', '🧑‍⚕️', 0)
    this.folderTab('服务', '📋', 1)
    this.folderTab('订单', '🗂', 2)
    this.folderTab('档案', '📔', 3)
    this.folderTab('须知', '📖', 4)
    this.folderTab('评价', '💬', 5)
  }
  .width('100%')
  .padding({ left: 12, right: 12, top: 10, bottom: 6 })
}

六个标签分别对应"陪诊师"“服务”“订单”“档案”“须知”"评价"六个内容板块。每个标签配有与医疗相关的emoji图标:🧑‍⚕️医护人员代表陪诊师,📋 clipboard代表服务清单,🗂文件夹代表订单管理,📔笔记本代表健康档案,📖打开的书本代表就医须知,💬对话气泡代表用户评价。单排6个的布局设计通过间距7和padding控制,确保了在标准屏幕宽度下六个标签能够均匀排列且每个标签的可点击区域足够大。

八、陪诊师页面pageNurse与认证标识

pageNurse是默认展示的陪诊师页面,包含了服务横幅和陪诊师列表。

@Builder
pageNurse() {
  Column() {
    Row() {
      Column() {
        Text('家人就医 · 一人陪同全搞定')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Text('平台认证 · 每单投保意外险')
          .fontSize(11)
          .fontColor('#CFEDEF')
          .margin({ top: 4 })
        Text('首单立减 ¥30')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.accent)
          .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text('🩺')
        .fontSize(38)
        .margin({ right: 12 })
    }
    .width('100%')
    .padding(14)
    .borderRadius(14)
    .linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
    .onClick(() => {
      this.addOpen = true
    })

    this.sectionTitle('认证陪诊师', '换一批', 0)
    ForEach(this.nurseList, (it: NurseItem, i: number) => {
      Row() {
        Stack() {
          Circle({ width: 48, height: 48 })
            .fill(COLORS.tagBg)
          Text('🧑‍⚕️')
            .fontSize(23)
        }
        Column() {
          Row() {
            Text(it.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('✓ 认证')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .backgroundColor(COLORS.tagBg)
              .borderRadius(6)
              .margin({ left: 6 })
          }
          Text('擅长 ' + it.hosp + ' · ' + it.years + '年经验')
            .fontSize(11)
            .fontColor(COLORS.textSecond)
            .margin({ top: 3 })
          Text('⭐ ' + it.score + ' · 已服务' + it.orders + '单')
            .fontSize(11)
            .fontColor(COLORS.gold)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 12 })
        Column() {
          Text(it.fav ? '❤️' : '🤍')
            .fontSize(15)
            .onClick(() => {
              this.toggleNurseFav(i)
            })
          Text('约TA')
            .fontSize(12)
            .fontColor(COLORS.white)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(COLORS.primary)
            .borderRadius(14)
            .margin({ top: 6 })
            .onClick(() => {
              this.addOpen = true
            })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
      .margin({ left: 16, right: 16, top: 8 })
    }, (it: NurseItem) => 'n' + it.id.toString() + '_' + this.rev.toString())
  }
  .width('100%')
  .padding({ bottom: 12 })
}

pageNurse的横幅使用了从primary青色到accent橙色的120度渐变,这种冷暖色渐变模拟了从"专业服务"到"温暖关怀"的品牌理念过渡。横幅中的"首单立减 ¥30"使用了accent橙色粗体文字,在渐变背景上形成醒目的价格优惠提示。陪诊师列表卡片的设计中,"✓ 认证"标签是一个重要的信任标识——它使用9号小字号配合primary青色文字和tagBg浅色背景,紧贴在陪诊师姓名右侧,传达了平台认证的专业保障信息。"擅长"前缀和"已服务X单"的文案设计为用户提供了陪诊师能力的关键信息——擅长哪家医院意味着熟悉该医院的就诊流程和科室分布,服务单数反映了实战经验。

九、订单页面pageOrder与多状态过滤

pageOrder是陪诊场景中最具业务复杂度的页面,它实现了订单列表的多状态过滤渲染。

@Builder
pageOrder() {
  Column() {
    this.sectionTitle('订单记录', '筛选', 2)
    Row({ space: 8 }) {
      Text('全部')
        .fontSize(11)
        .fontColor(this.orderFilter === 0 ? COLORS.white : COLORS.textSecond)
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor(this.orderFilter === 0 ? COLORS.primary : COLORS.card)
        .borderRadius(12)
        .onClick(() => {
          this.orderFilter = 0
        })
      Text('待服务')
        .fontSize(11)
        .fontColor(this.orderFilter === 1 ? COLORS.white : COLORS.textSecond)
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor(this.orderFilter === 1 ? COLORS.primary : COLORS.card)
        .borderRadius(12)
        .onClick(() => {
          this.orderFilter = 1
        })
      Text('陪诊中')
        .fontSize(11)
        .fontColor(this.orderFilter === 2 ? COLORS.white : COLORS.textSecond)
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor(this.orderFilter === 2 ? COLORS.primary : COLORS.card)
        .borderRadius(12)
        .onClick(() => {
          this.orderFilter = 2
        })
      Text('已完成')
        .fontSize(11)
        .fontColor(this.orderFilter === 3 ? COLORS.white : COLORS.textSecond)
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor(this.orderFilter === 3 ? COLORS.primary : COLORS.card)
        .borderRadius(12)
        .onClick(() => {
          this.orderFilter = 3
        })
    }
    .width('100%')
    .margin({ left: 16, right: 16 })

    ForEach(this.orderList, (it: OrderRecItem, i: number) => {
      if (this.orderShown(i)) {
        Column() {
          Row() {
            Text(it.hosp)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Column()
              .layoutWeight(1)
            Text(it.stat)
              .fontSize(11)
              .fontColor(statColor(it.stat))
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor(COLORS.tagBg)
              .borderRadius(8)
          }
          .width('100%')
          Row() {
            Text('🧑‍⚕️ ' + it.nurse)
              .fontSize(11)
              .fontColor(COLORS.textSecond)
            Text('🕐 ' + it.date)
              .fontSize(11)
              .fontColor(COLORS.textSecond)
              .margin({ left: 12 })
            Column()
              .layoutWeight(1)
            Text('¥' + it.fee)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.accent)
          }
          .width('100%')
          .margin({ top: 8 })
          Row() {
            Column()
              .layoutWeight(1)
            Text('加急接送')
              .fontSize(11)
              .fontColor(COLORS.accent)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(COLORS.accentSoft)
              .borderRadius(12)
              .onClick(() => {
                this.bizOpen = true
              })
            Text('取消订单')
              .fontSize(11)
              .fontColor(COLORS.danger)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor('#FDECEA')
              .borderRadius(12)
              .margin({ left: 8 })
              .onClick(() => {
                this.delOpen = true
              })
          }
          .width('100%')
          .margin({ top: 8 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 8 })
      }
    }, (it: OrderRecItem) => 'o' + it.id.toString() + '_' + this.rev.toString() + this.orderFilter.toString())
  }
  .width('100%')
  .padding({ bottom: 12 })
}

pageOrder的核心设计在于过滤按钮栏和条件渲染列表。四个过滤按钮通过orderFilter状态变量的值控制选中状态——选中时使用primary青色背景白色文字,未选中时使用card白色背景灰色文字。ForEach遍历orderList时,通过if (this.orderShown(i))条件判断决定是否渲染当前订单项。这种设计的巧妙之处在于:原始数据数组orderList始终保持不变,过滤操作完全在UI层面通过条件渲染实现,避免了数据层面的数组操作开销。ForEach的keyGenerator中加入了this.orderFilter.toString(),确保了过滤状态切换时ForEach能够正确识别并重新渲染列表项。

每个订单卡片的设计包含了三行信息:第一行是医院名称(粗体)和状态标签(通过statColor函数动态着色),第二行是陪诊师姓名、日期时间和费用,第三行是操作按钮——"加急接送"使用accent橙色文字和accentSoft浅橙色背景,"取消订单"使用danger红色文字和浅红色背景。这种通过颜色编码操作按钮的设计,使用户能够通过颜色快速识别操作的后果——橙色代表增值服务(安全),红色代表取消操作(需要谨慎)。

十、弹框系统:预约陪诊modalBodyAdd与加急接送modalBodyBiz

预约陪诊弹框设计用于收集用户的预约信息,包含医院选择、服务时长和费用预估。

@Builder
modalBodyAdd() {
  Column() {
    Row() {
      Text('🩺 预约陪诊 BOOKING')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Column()
        .layoutWeight(1)
      Text('✕')
        .fontSize(16)
        .fontColor(COLORS.textThird)
        .onClick(() => {
          this.addOpen = false
        })
    }
    .width('100%')

    Text('就诊医院')
      .fontSize(13)
      .fontColor(COLORS.textSecond)
      .margin({ top: 14 })
    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(HOSP_TAGS, (h: string, i: number) => {
        Text(h)
          .fontSize(12)
          .fontColor(this.hospTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.hospTags.indexOf(i) >= 0 ? COLORS.primary : COLORS.tagBg)
          .borderRadius(14)
          .margin({ right: 8, top: 8 })
          .onClick(() => {
            this.toggleTag(this.hospTags, i)
          })
      }, (h: string) => 'hb' + h)
    }
    .width('100%')
    .margin({ top: 4 })

    Text('服务时长(小时)')
      .fontSize(13)
      .fontColor(COLORS.textSecond)
      .margin({ top: 16 })
    Row() {
      Text('-')
        .fontSize(16)
        .fontColor(COLORS.textSecond)
        .padding(10)
        .backgroundColor(COLORS.tagBg)
        .borderRadius(10)
        .onClick(() => {
          if (this.hourStep > 2) {
            this.hourStep = this.hourStep - 1
          }
        })
      Column() {
        Text(this.hourStep.toString() + ' 小时')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.primary)
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      Text('+')
        .fontSize(16)
        .fontColor(COLORS.white)
        .padding(10)
        .backgroundColor(COLORS.primary)
        .borderRadius(10)
        .onClick(() => {
          if (this.hourStep < 12) {
            this.hourStep = this.hourStep + 1
          }
        })
    }
    .width('100%')
    .margin({ top: 8 })

    Text('含挂号代取 · 排队代办 · 就医引导')
      .fontSize(11)
      .fontColor(COLORS.textThird)
      .margin({ top: 8 })

    Row() {
      Text('提交预约')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.white)
        .padding({ left: 20, right: 20, top: 10, bottom: 10 })
        .backgroundColor(COLORS.primary)
        .borderRadius(20)
        .onClick(() => {
          this.addOpen = false
        })
      Column()
        .layoutWeight(1)
      Text('预估 ¥' + this.bookTotal())
        .fontSize(13)
        .fontColor(COLORS.accent)
    }
    .width('100%')
    .margin({ top: 20, bottom: 20 })
  }
  .width('100%')
  .padding({ left: 20, right: 20, top: 16 })
  .backgroundColor(COLORS.card)
  .borderRadius(20)
  .constraintSize({ maxHeight: '80%' })
}

预约陪诊弹框的医院选择标签使用了HOSP_TAGS五个医院名称,选中状态使用primary青色背景——这与射箭馆场景使用金色背景的设计形成了有趣的对比:医疗场景用青色传达专业感,射箭场景用金色传达竞技感。服务时长步进器的范围限制在2-12小时,最小2小时的设计保证了陪诊服务的最低时长门槛。"含挂号代取 · 排队代办 · 就医引导"的服务说明文案使用textThird浅色文字,作为辅助信息呈现在步进器下方。预估费用通过this.bookTotal()方法计算,公式为hourStep * 42 + 50,即每小时42元陪诊费加上50元基础服务费。

加急接送弹框modalBodyBiz的设计包含了车型选择和调度开关:

@Builder
modalBodyBiz() {
  Column() {
    Row() {
      Text('🚑 加急接送 RUSH PICKUP')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Column()
        .layoutWeight(1)
      Text('✕')
        .fontSize(16)
        .fontColor(COLORS.textThird)
        .onClick(() => {
          this.bizOpen = false
        })
    }
    .width('100%')

    Row() {
      Column() {
        Text('点到点就医专车')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Text('护士随行 · 轮椅/担架可选')
          .fontSize(11)
          .fontColor('#CFEDEF')
          .margin({ top: 4 })
        Text('加急费 ¥50 · 30分钟内到达')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.accent)
          .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text('🚑')
        .fontSize(36)
    }
    .width('100%')
    .padding(14)
    .borderRadius(14)
    .linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
    .margin({ top: 14 })

    Text('车型选择')
      .fontSize(13)
      .fontColor(COLORS.textSecond)
      .margin({ top: 14 })
    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(['经济轿车', '无障碍车', '商务7座', '担架车'], (c: string, i: number) => {
        Text(c)
          .fontSize(12)
          .fontColor(this.carTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.carTags.indexOf(i) >= 0 ? COLORS.accent : COLORS.tagBg)
          .borderRadius(14)
          .margin({ right: 8, top: 8 })
          .onClick(() => {
            this.toggleTag(this.carTags, i)
          })
      }, (c: string) => 'ca' + c)
    }
    .width('100%')
    .margin({ top: 4 })

    Row() {
      Column() {
        Text('红灯优先调度')
          .fontSize(14)
          .fontColor(COLORS.textPrimary)
        Text('晚高峰不加价,司机实时定位')
          .fontSize(11)
          .fontColor(COLORS.textThird)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text(this.rushFlag ? '已开启' : '已关闭')
        .fontSize(12)
        .fontColor(this.rushFlag ? COLORS.white : COLORS.textSecond)
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .backgroundColor(this.rushFlag ? COLORS.primary : COLORS.tagBg)
        .borderRadius(14)
        .onClick(() => {
          this.rushFlag = !this.rushFlag
        })
    }
    .width('100%')
    .margin({ top: 16 })
    .padding(12)
    .backgroundColor(COLORS.tagBg)
    .borderRadius(12)

    Row() {
      Text('叫车')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.white)
        .padding({ left: 20, right: 20, top: 10, bottom: 10 })
        .backgroundColor(COLORS.accent)
        .borderRadius(20)
        .onClick(() => {
          this.bizOpen = false
        })
      Column()
        .layoutWeight(1)
      Text('附近 7 辆车可用')
        .fontSize(11)
        .fontColor(COLORS.ok)
    }
    .width('100%')
    .margin({ top: 20, bottom: 20 })
  }
  .width('100%')
  .padding({ left: 20, right: 20, top: 16 })
  .backgroundColor(COLORS.card)
  .borderRadius(20)
  .constraintSize({ maxHeight: '80%' })
}

加急接送弹框的信息卡片使用了从primary青色到accent橙色的渐变,与pageNurse横幅的渐变方向一致,形成了品牌视觉的统一性。信息卡片中"护士随行 · 轮椅/担架可选"的文案设计传达了服务的专业医疗属性——不是普通的网约车接送,而是有护士随行的医疗级别接送服务。车型选择标签的设计是该弹框的亮点——四种车型"经济轿车"“无障碍车”“商务7座”"担架车"覆盖了从日常到特殊医疗需求的完整场景,其中"无障碍车"和"担架车"是专门针对行动不便患者设计的特殊车型。红灯优先调度开关使用了rushFlag布尔状态,开启时使用primary青色背景,关闭时使用tagBg浅灰色背景。底部的"附近 7 辆车可用"使用ok绿色文字,传达了车辆充足的可用性信息。

十一、健康档案页面pageFile与提醒切换

pageFile是陪诊场景中医疗属性最强的页面,它管理着用户家庭成员的健康档案。

@Builder
pageFile() {
  Column() {
    this.sectionTitle('健康档案', '新增', 2)
    ForEach(this.fileList, (it: HealthFileItem, i: number) => {
      Column() {
        Row() {
          Stack() {
            Circle({ width: 40, height: 40 })
              .fill(COLORS.tagBg)
            Text('👤')
              .fontSize(19)
          }
          Column() {
            Text(it.name + ' · ' + it.age + '岁')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('关系:' + it.rel)
              .fontSize(11)
              .fontColor(COLORS.textSecond)
              .margin({ top: 3 })
            Text('🩹 ' + it.note)
              .fontSize(11)
              .fontColor(COLORS.warn)
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
          Column() {
            Text(it.remind ? '提醒开' : '提醒关')
              .fontSize(10)
              .fontColor(it.remind ? COLORS.white : COLORS.textSecond)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .backgroundColor(it.remind ? COLORS.primary : COLORS.tagBg)
              .borderRadius(10)
              .onClick(() => {
                this.toggleRemind(i)
              })
            Text('编辑')
              .fontSize(10)
              .fontColor(COLORS.accent)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .backgroundColor(COLORS.accentSoft)
              .borderRadius(10)
              .margin({ top: 6 })
              .onClick(() => {
                this.editOpen = true
              })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
      .margin({ left: 16, right: 16, top: 8 })
    }, (it: HealthFileItem) => 'f' + it.id.toString() + '_' + this.rev.toString())

pageFile的设计中,每个档案卡片左侧使用Circle圆形配合👤emoji作为头像占位符。中间的信息区域包含了三行文字:第一行是姓名和年龄(“王建国 · 58岁”),第二行是关系(“关系:本人”),第三行是健康备注(“🩹 高血压 · 长期用药”)——这里使用了warn橙色字体和🩹创可贴emoji前缀,使健康问题信息在卡片中形成视觉焦点,提醒用户关注。右侧的操作区域包含了"提醒开/关"切换按钮和"编辑"按钮。提醒切换按钮的设计是该页面的核心交互——当remind为true时,按钮显示"提醒开"并使用primary青色背景白色文字;当remind为false时,按钮显示"提醒关"并使用tagBg浅灰色背景灰色文字。用户点击按钮调用toggleRemind方法切换提醒状态,由于HealthFileItem是@Observed类,状态变化会立即反映到UI中。

档案页面下方还包含了月度订单统计柱状图,通过MONTH_LABELS和MONTH_ORDERS数据配合monthOrderBar函数渲染,数据呈现出从3月到8月逐月增长的趋势,反映了陪诊服务需求的增长态势。

十二、build主架构与modalOverlay弹框管理

modalOverlay构建器统一管理四个弹框的显示逻辑:

@Builder
modalOverlay() {
  Stack({ alignContent: Alignment.Bottom }) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,34,0.5)')
      .onClick(() => {
        this.closeAll()
      })
    if (this.addOpen) {
      this.modalBodyAdd()
    }
    if (this.editOpen) {
      this.modalBodyEdit()
    }
    if (this.delOpen) {
      this.modalBodyDel()
    }
    if (this.bizOpen) {
      this.modalBodyBiz()
    }
  }
  .width('100%')
  .height('100%')
}

modalOverlay的遮罩层使用了rgba(6,30,34,0.5)的半透明深青色背景,色调与主色#00838F的RGB值(0,131,143)中的青色基调一致,保持了遮罩层与应用主题色的色彩统一性。这种细节设计避免了遮罩层使用纯黑色而与应用青色主调产生割裂感。

build方法的整体架构:

build() {
  Stack() {
    Column() {
      this.header()
      this.mainContent()
      this.bottomBar()
    }
    .width('100%')
    .height('100%')
    this.fxLayer()
  }
  .width('100%')
  .height('100%')
  .backgroundColor(COLORS.bg)
}

mainContent的内容切换逻辑:

@Builder
mainContent() {
  Scroll() {
    Column() {
      this.tabBar()
      if (this.mainTab === 0) {
        if (this.curTab === 0) {
          this.pageNurse()
        }
        if (this.curTab === 1) {
          this.pageService()
        }
        if (this.curTab === 2) {
          this.pageOrder()
        }
        if (this.curTab === 3) {
          this.pageFile()
        }
        if (this.curTab === 4) {
          this.pageTip()
        }
        if (this.curTab === 5) {
          this.pageReview()
        }
      } else {
        this.pageMainOther()
      }
    }
    .width('100%')
  }
  .width('100%')
  .layoutWeight(1)
  .scrollBar(BarState.Off)
  .edgeEffect(EdgeEffect.Spring)
}

mainContent根据curTab的值在六个页面构建器之间切换,这种多页面切换的if条件链在ArkTS中是推荐的做法——虽然使用了多个if而非if-else,但由于curTab的唯一性,实际上只有一个页面会被渲染。Scroll组件的layoutWeight(1)使其占据header和bottomBar之间的全部剩余空间,scrollBar(BarState.Off)隐藏滚动条,edgeEffect(EdgeEffect.Spring)提供弹性边缘效果。

十三、Mermaid架构流程图

以下是安心陪诊场景的组件结构与数据流架构图:

mainTab=0 首页

其他

curTab=0

curTab=1

curTab=2

curTab=3

curTab=4

curTab=5

0=全部

1=待服务

2=陪诊中

3=已完成

addOpen

editOpen

delOpen

bizOpen

PageCareCompanion 入口组件

build 主构建器

Stack 堆叠层

Column 主内容列

fxLayer 动画装饰层

header 头部区域

mainContent 可滚动内容区

bottomBar 底部导航栏

品牌名称 + 定位

搜索框

保障标签行

tabBar 病历夹页式标签

陪诊师/服务/订单/档案/须知/评价

mainTab 判断

curTab 判断

pageMainOther

pageNurse 陪诊师

pageService 服务套餐

pageOrder 订单记录

pageFile 健康档案

pageTip 就医须知

pageReview 用户评价

NurseItem 数据

OrderRecItem 数据

HealthFileItem 数据

ReviewItem 数据

orderFilter 过滤器

显示全部订单

筛选待服务

筛选陪诊中

筛选已完成

modalOverlay 弹框层

弹框状态判断

modalBodyAdd 预约陪诊

modalBodyEdit 健康档案

modalBodyDel 取消警示

modalBodyBiz 加急接送

tick 定时器 105ms

translate 救护车位移

scale 心形呼吸

rotate 药丸旋转

opacity 闪光闪烁

十四、数据模型与组件对比表格

维度 NurseItem ServicePackItem OrderRecItem HealthFileItem TipItem ReviewItem
接口定义 Nurse ServicePack OrderRec HealthFile Tip Review
字段数量 7个 6个 6个 6个 4个 5个
核心字段 name/hosp/score name/desc/unit hosp/stat/nurse name/rel/note title/cat/reads user/nurse/stars
布尔字段 fav hot remind
布尔安全赋值 三元表达式 三元表达式 不需要 三元表达式 不需要 不需要
数据数量 12条 10条 12条 8条 8条 10条
关联页面 pageNurse pageService pageOrder pageFile pageTip pageReview
列表布局 Row横向列表 Flex双列网格 条件过滤列表 Row横向列表 Row横向列表 Column纵向列表
特殊交互 收藏切换 orderFilter过滤 remind提醒切换 星级渲染
维度 modalBodyAdd modalBodyEdit modalBodyDel modalBodyBiz
功能定位 预约陪诊 健康档案编辑 取消订单警示 加急接送
标签选择 HOSP_TAGS医院 REL_TAGS关系 4种车型
步进器 hourStep 2-12小时 ageStep 1-110岁
步进值 1 1
开关控件 remindFlag复诊提醒 rushFlag红灯优先
金额计算 bookTotal() 固定¥268 固定¥50
信息卡片 tagBg浅色 accentSoft浅橙 青橙渐变
主色调 primary青色 accent橙色 danger红色 accent橙色
服务说明 含挂号代取等 按用药周期推送 服务前4小时退款 30分钟内到达

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 布局风格:病历夹页式内容tab(单排6个)+ 静态渐变头部
// 弹框:预约陪诊 / 健康档案 / 取消警示 / 加急接送
// ============================================================

interface ColorPalette {
  bg: string
  card: string
  white: string
  primary: string
  primaryDeep: string
  accent: string
  accentSoft: string
  gold: string
  textPrimary: string
  textSecond: string
  textThird: string
  line: string
  tagBg: string
  warn: string
  danger: string
  ok: string
}

const COLORS: ColorPalette = {
  bg: '#F0F6F6',
  card: '#FFFFFF',
  white: '#FFFFFF',
  primary: '#00838F',
  primaryDeep: '#005662',
  accent: '#FF7043',
  accentSoft: '#FFEBE3',
  gold: '#C99700',
  textPrimary: '#1F3033',
  textSecond: '#5A7276',
  textThird: '#93A9AC',
  line: '#E2EDEC',
  tagBg: '#E9F2F1',
  warn: '#E65100',
  danger: '#C62828',
  ok: '#2E7D32'
}

interface Nurse {
  id: number
  name: string
  score: number
  orders: number
  hosp: string
  years: number
  fav: boolean
}

@Observed
class NurseItem implements Nurse {
  id: number = 0
  name: string = ''
  score: number = 0
  orders: number = 0
  hosp: string = ''
  years: number = 0
  fav: boolean = false

  constructor(o: Nurse) {
    this.id = o.id
    this.name = o.name
    this.score = o.score
    this.orders = o.orders
    this.hosp = o.hosp
    this.years = o.years
    this.fav = o.fav ? o.fav : false
  }
}

interface ServicePack {
  id: number
  name: string
  desc: string
  price: number
  unit: string
  hot: boolean
}

@Observed
class ServicePackItem implements ServicePack {
  id: number = 0
  name: string = ''
  desc: string = ''
  price: number = 0
  unit: string = ''
  hot: boolean = false

  constructor(o: ServicePack) {
    this.id = o.id
    this.name = o.name
    this.desc = o.desc
    this.price = o.price
    this.unit = o.unit
    this.hot = o.hot ? o.hot : false
  }
}

interface OrderRec {
  id: number
  hosp: string
  date: string
  stat: string
  nurse: string
  fee: number
}

@Observed
class OrderRecItem implements OrderRec {
  id: number = 0
  hosp: string = ''
  date: string = ''
  stat: string = ''
  nurse: string = ''
  fee: number = 0

  constructor(o: OrderRec) {
    this.id = o.id
    this.hosp = o.hosp
    this.date = o.date
    this.stat = o.stat
    this.nurse = o.nurse
    this.fee = o.fee
  }
}

interface HealthFile {
  id: number
  name: string
  rel: string
  age: number
  note: string
  remind: boolean
}

@Observed
class HealthFileItem implements HealthFile {
  id: number = 0
  name: string = ''
  rel: string = ''
  age: number = 0
  note: string = ''
  remind: boolean = false

  constructor(o: HealthFile) {
    this.id = o.id
    this.name = o.name
    this.rel = o.rel
    this.age = o.age
    this.note = o.note
    this.remind = o.remind ? o.remind : false
  }
}

interface Tip {
  id: number
  title: string
  cat: string
  reads: number
}

@Observed
class TipItem implements Tip {
  id: number = 0
  title: string = ''
  cat: string = ''
  reads: number = 0

  constructor(o: Tip) {
    this.id = o.id
    this.title = o.title
    this.cat = o.cat
    this.reads = o.reads
  }
}

interface Review {
  id: number
  user: string
  nurse: string
  stars: number
  txt: string
}

@Observed
class ReviewItem implements Review {
  id: number = 0
  user: string = ''
  nurse: string = ''
  stars: number = 0
  txt: string = ''

  constructor(o: Review) {
    this.id = o.id
    this.user = o.user
    this.nurse = o.nurse
    this.stars = o.stars
    this.txt = o.txt
  }
}

const NURSES: NurseItem[] = [
  new NurseItem({ id: 1, name: '林安', score: 4.9, orders: 2860, hosp: '协和医院', years: 6, fav: true }),
  new NurseItem({ id: 2, name: '周到的周', score: 4.8, orders: 2310, hosp: '同济医院', years: 5, fav: false }),
  new NurseItem({ id: 3, name: '安心姐', score: 5.0, orders: 3120, hosp: '人民医院', years: 8, fav: false }),
  new NurseItem({ id: 4, name: '小鹿陪诊', score: 4.7, orders: 1580, hosp: '儿童医院', years: 3, fav: false }),
  new NurseItem({ id: 5, name: '陈伯通', score: 4.8, orders: 2040, hosp: '肿瘤医院', years: 7, fav: false }),
  new NurseItem({ id: 6, name: '暖阳', score: 4.9, orders: 2670, hosp: '中山医院', years: 6, fav: false }),
  new NurseItem({ id: 7, name: '铁柱哥', score: 4.6, orders: 980, hosp: '骨科医院', years: 2, fav: false }),
  new NurseItem({ id: 8, name: '苏叶', score: 4.8, orders: 1890, hosp: '妇产医院', years: 4, fav: false }),
  new NurseItem({ id: 9, name: '老黄牛', score: 4.7, orders: 1450, hosp: '中医院', years: 5, fav: false }),
  new NurseItem({ id: 10, name: '月半湾', score: 4.9, orders: 2210, hosp: '第一医院', years: 6, fav: false }),
  new NurseItem({ id: 11, name: '青云', score: 4.6, orders: 860, hosp: '口腔医院', years: 2, fav: false }),
  new NurseItem({ id: 12, name: '棠棠', score: 4.8, orders: 1730, hosp: '精神卫生中心', years: 4, fav: false })
]

const SERVICES: ServicePackItem[] = [
  new ServicePackItem({ id: 1, name: '半天陪诊', desc: '挂号候诊缴费取药全程陪同', price: 168, unit: '次', hot: true }),
  new ServicePackItem({ id: 2, name: '全天陪诊', desc: '检查预约+报告解读协助', price: 268, unit: '天', hot: true }),
  new ServicePackItem({ id: 3, name: '代办问诊', desc: '代取号代问诊代取药寄回', price: 128, unit: '次', hot: false }),
  new ServicePackItem({ id: 4, name: '代办取报告', desc: '纸质报告取件顺丰寄回', price: 68, unit: '次', hot: false }),
  new ServicePackItem({ id: 5, name: '住院陪护', desc: '日间照看+三餐协助', price: 320, unit: '天', hot: false }),
  new ServicePackItem({ id: 6, name: '接送就医', desc: '家门口-医院点对点专车', price: 98, unit: '趟', hot: true }),
  new ServicePackItem({ id: 7, name: '异地代诊', desc: '跨城市三甲专家代问诊', price: 388, unit: '次', hot: false }),
  new ServicePackItem({ id: 8, name: '术后接送', desc: '轮椅接送+护士随行', price: 158, unit: '趟', hot: false }),
  new ServicePackItem({ id: 9, name: '儿童陪诊', desc: '儿保有经验陪诊师随行', price: 188, unit: '次', hot: false }),
  new ServicePackItem({ id: 10, name: '体检陪同', desc: '全流程导检+早餐协助', price: 138, unit: '次', hot: false })
]

const ORDERS: OrderRecItem[] = [
  new OrderRecItem({ id: 1, hosp: '协和医院', date: '08-22 09:00', stat: '陪诊中', nurse: '林安', fee: 168 }),
  new OrderRecItem({ id: 2, hosp: '同济医院', date: '08-21 14:00', stat: '已完成', nurse: '周到的周', fee: 268 }),
  new OrderRecItem({ id: 3, hosp: '人民医院', date: '08-20 08:30', stat: '已完成', nurse: '安心姐', fee: 168 }),
  new OrderRecItem({ id: 4, hosp: '儿童医院', date: '08-19 10:00', stat: '已完成', nurse: '小鹿陪诊', fee: 188 }),
  new OrderRecItem({ id: 5, hosp: '中医院', date: '08-18 15:00', stat: '已完成', nurse: '老黄牛', fee: 128 }),
  new OrderRecItem({ id: 6, hosp: '中山医院', date: '08-24 09:30', stat: '待服务', nurse: '暖阳', fee: 268 }),
  new OrderRecItem({ id: 7, hosp: '口腔医院', date: '08-25 11:00', stat: '待服务', nurse: '青云', fee: 98 }),
  new OrderRecItem({ id: 8, hosp: '骨科医院', date: '08-16 09:00', stat: '已完成', nurse: '铁柱哥', fee: 158 }),
  new OrderRecItem({ id: 9, hosp: '妇产医院', date: '08-15 13:30', stat: '已完成', nurse: '苏叶', fee: 320 }),
  new OrderRecItem({ id: 10, hosp: '第一医院', date: '08-14 08:00', stat: '已完成', nurse: '月半湾', fee: 138 }),
  new OrderRecItem({ id: 11, hosp: '肿瘤医院', date: '08-26 10:00', stat: '待服务', nurse: '陈伯通', fee: 388 }),
  new OrderRecItem({ id: 12, hosp: '精神卫生中心', date: '08-12 16:00', stat: '已完成', nurse: '棠棠', fee: 168 })
]

const FILES: HealthFileItem[] = [
  new HealthFileItem({ id: 1, name: '王建国', rel: '本人', age: 58, note: '高血压 · 长期用药', remind: true }),
  new HealthFileItem({ id: 2, name: '李秀兰', rel: '母亲', age: 82, note: '糖尿病 · 每月复诊', remind: true }),
  new HealthFileItem({ id: 3, name: '王小宝', rel: '儿子', age: 6, note: '哮喘 · 随身喷雾', remind: false }),
  new HealthFileItem({ id: 4, name: '赵桂香', rel: '婆婆', age: 76, note: '膝关节术后 · 轮椅', remind: true }),
  new HealthFileItem({ id: 5, name: '王小雨', rel: '女儿', age: 24, note: '过敏体质 · 青霉素', remind: false }),
  new HealthFileItem({ id: 6, name: '老黄', rel: '邻居', age: 69, note: '冠心病 · 支架随访', remind: false }),
  new HealthFileItem({ id: 7, name: '陈芳', rel: '姐姐', age: 45, note: '甲状腺 · 半年复查', remind: false }),
  new HealthFileItem({ id: 8, name: '豆豆', rel: '孙子', age: 3, note: '湿疹 · 皮肤科随访', remind: false })
]

const TIPS: TipItem[] = [
  new TipItem({ id: 1, title: '就诊前需要准备哪些材料?', cat: '新手必读', reads: 12800 }),
  new TipItem({ id: 2, title: '陪诊师会帮忙插队吗?', cat: '服务边界', reads: 9600 }),
  new TipItem({ id: 3, title: '异地医保备案三步搞定', cat: '医保科普', reads: 21000 }),
  new TipItem({ id: 4, title: '带孩子就诊的黄金时间表', cat: '儿童就诊', reads: 8700 }),
  new TipItem({ id: 5, title: '检查报告多久能取?全院汇总', cat: '流程指南', reads: 15400 }),
  new TipItem({ id: 6, title: '老年人就医防跌倒清单', cat: '银发关怀', reads: 11200 }),
  new TipItem({ id: 7, title: '专家号抢号时间表(月度更新)', cat: '挂号攻略', reads: 32400 }),
  new TipItem({ id: 8, title: '术后复查怎么约最省时间', cat: '流程指南', reads: 7800 })
]

const REVIEWS: ReviewItem[] = [
  new ReviewItem({ id: 1, user: '柚**妈', nurse: '林安', stars: 5, txt: '全程搀扶我妈,取药排队全包,太省心了。' }),
  new ReviewItem({ id: 2, user: 'W**g', nurse: '安心姐', stars: 5, txt: '报告解读很耐心,医生说什么都记下来了。' }),
  new ReviewItem({ id: 3, user: '糖**兔', nurse: '小鹿陪诊', stars: 5, txt: '带孩子看病全程哄娃,专业又温柔。' }),
  new ReviewItem({ id: 4, user: '北**风', nurse: '周到的周', stars: 4, txt: '迟到了五分钟,但服务确实到位。' }),
  new ReviewItem({ id: 5, user: 'L**n', nurse: '暖阳', stars: 5, txt: '异地代诊帮我省了一趟高铁,靠谱。' }),
  new ReviewItem({ id: 6, user: '豆**爷', nurse: '老黄牛', stars: 5, txt: '老父亲一个人去医院再也不担心了。' }),
  new ReviewItem({ id: 7, user: 'm**9', nurse: '苏叶', stars: 5, txt: '产后复查全程陪同,情绪价值拉满。' }),
  new ReviewItem({ id: 8, user: '晴**天', nurse: '陈伯通', stars: 5, txt: '肿瘤医院路线门儿清,检查衔接无缝。' }),
  new ReviewItem({ id: 9, user: '啊**哦', nurse: '铁柱哥', stars: 4, txt: '术后轮椅接送很稳,就是话不多。' }),
  new ReviewItem({ id: 10, user: '南**巷', nurse: '月半湾', stars: 5, txt: '体检陪同连早餐都帮忙安排了。' })
]

const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
const MONTH_ORDERS: number[] = [38, 46, 52, 61, 74, 88]
const SVC_LABELS: string[] = ['半天陪诊', '代办问诊', '接送就医', '住院陪护']
const SVC_PCTS: number[] = [42, 26, 20, 12]
const SVC_COLORS: string[] = ['#00838F', '#FF7043', '#C99700', '#78909C']
const HOSP_TAGS: string[] = ['协和医院', '同济医院', '人民医院', '儿童医院', '中医院']
const REL_TAGS: string[] = ['本人', '父母', '子女', '伴侣', '亲友']

function monthOrderBar(i: number): number {
  return MONTH_ORDERS[i] * 1.6
}

function statColor(s: string): string {
  if (s === '陪诊中') {
    return COLORS.primary
  }
  if (s === '待服务') {
    return COLORS.warn
  }
  return COLORS.ok
}

@Entry
@Component
struct PageCareCompanion {
  @State curTab: number = 0
  @State mainTab: number = 0
  @State addOpen: boolean = false
  @State editOpen: boolean = false
  @State delOpen: boolean = false
  @State bizOpen: boolean = false
  @State nurseList: NurseItem[] = NURSES
  @State orderList: OrderRecItem[] = ORDERS
  @State fileList: HealthFileItem[] = FILES
  @State reviewList: ReviewItem[] = REVIEWS
  @State rev: number = 0
  @State tick: number = 0
  @State hospTags: number[] = [0]
  @State relTags: number[] = [1]
  @State hourStep: number = 4
  @State ageStep: number = 58
  @State remindFlag: boolean = true
  @State carTags: number[] = [0]
  @State rushFlag: boolean = false
  @State orderFilter: number = 0
  private timer: number = -1

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.tick = this.tick + 1
    }, 105)
  }

  aboutToDisappear(): void {
    if (this.timer >= 0) {
      clearInterval(this.timer)
    }
  }

  switchTab(i: number): void {
    this.curTab = i
  }

  switchMain(i: number): void {
    this.mainTab = i
  }

  refreshNurses(): void {
    this.nurseList.unshift(this.nurseList[this.nurseList.length - 1])
    this.nurseList.splice(this.nurseList.length - 1, 1)
    this.rev = this.rev + 1
  }

  refreshReviews(): void {
    this.reviewList.unshift(this.reviewList[this.reviewList.length - 1])
    this.reviewList.splice(this.reviewList.length - 1, 1)
    this.rev = this.rev + 1
  }

  toggleNurseFav(i: number): void {
    this.nurseList[i].fav = !this.nurseList[i].fav
    this.rev = this.rev + 1
  }

  toggleRemind(i: number): void {
    this.fileList[i].remind = !this.fileList[i].remind
    this.rev = this.rev + 1
  }

  toggleTag(list: number[], i: number): void {
    if (list.indexOf(i) >= 0) {
      list.splice(list.indexOf(i), 1)
    } else {
      list.push(i)
    }
  }

  closeAll(): void {
    this.addOpen = false
    this.editOpen = false
    this.delOpen = false
    this.bizOpen = false
  }

  orderShown(i: number): boolean {
    if (this.orderFilter === 0) {
      return true
    }
    if (this.orderFilter === 1) {
      return this.orderList[i].stat === '待服务'
    }
    if (this.orderFilter === 2) {
      return this.orderList[i].stat === '陪诊中'
    }
    return this.orderList[i].stat === '已完成'
  }

  bookTotal(): number {
    return this.hourStep * 42 + 50
  }

  @Builder
  fxLayer() {
    Stack() {
      Text('🚑')
        .fontSize(24)
        .translate({ x: this.tick % 36 })
        .position({ x: 40, y: 130 })
        .opacity(0.8)
      Text('❤️')
        .fontSize(20)
        .scale({ x: 0.85 + (this.tick % 10) / 12, y: 0.85 + (this.tick % 10) / 12 })
        .position({ x: 300, y: 230 })
        .opacity(0.8)
      Text('💊')
        .fontSize(18)
        .rotate({ angle: (this.tick % 24) * 15 })
        .position({ x: 130, y: 330 })
        .opacity(0.6)
      Text('✨')
        .fontSize(16)
        .position({ x: 500, y: 120 })
        .opacity((this.tick % 8) / 8 + 0.15)
      Text('🩺')
        .fontSize(20)
        .translate({ y: this.tick % 40 })
        .position({ x: 540, y: 400 })
        .opacity(0.5)
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

  @Builder
  header() {
    Column() {
      Row() {
        Text('🩺')
          .fontSize(24)
        Text('安心陪诊')
          .fontSize(21)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: 8 })
        Column()
          .layoutWeight(1)
        Text('📍全国·三甲覆盖')
          .fontSize(12)
          .fontColor('#CBE8EC')
        Text('🔔')
          .fontSize(19)
          .margin({ left: 12 })
          .onClick(() => {
            this.mainTab = 3
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Row() {
        Text('🔍')
          .fontSize(15)
        Text('搜陪诊师 / 医院 / 服务')
          .fontSize(13)
          .fontColor('#A9D5DC')
          .margin({ left: 8 })
        Column()
          .layoutWeight(1)
        Text('搜索')
          .fontSize(13)
          .fontColor(COLORS.white)
          .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .backgroundColor(COLORS.accent)
          .borderRadius(14)
          .onClick(() => {
            this.curTab = 0
          })
      }
      .width('100%')
      .padding({ left: 10, right: 10 })
      .margin({ top: 10 })
      .backgroundColor('rgba(255,255,255,0.2)')
      .borderRadius(20)

      Row({ space: 8 }) {
        Text('持证陪诊师')
          .fontSize(11)
          .fontColor(COLORS.white)
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('rgba(255,255,255,0.24)')
          .borderRadius(12)
        Text('服务上保险')
          .fontSize(11)
          .fontColor(COLORS.white)
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('rgba(255,255,255,0.24)')
          .borderRadius(12)
        Text('不满意全退')
          .fontSize(11)
          .fontColor(COLORS.white)
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('rgba(255,255,255,0.24)')
          .borderRadius(12)
        Text('老人专享价')
          .fontSize(11)
          .fontColor(COLORS.white)
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('rgba(255,255,255,0.24)')
          .borderRadius(12)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .margin({ top: 10, bottom: 14 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .linearGradient({ angle: 135, colors: [[COLORS.primary, 0], ['#0097A7', 1]] })
  }

  @Builder
  folderTab(name: string, icon: string, idx: number) {
    Column() {
      Column() {
        Text(icon)
          .fontSize(15)
        Text(name)
          .fontSize(11)
          .fontWeight(this.curTab === idx ? FontWeight.Bold : FontWeight.Normal)
          .fontColor(this.curTab === idx ? COLORS.white : COLORS.textSecond)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Center)
      .padding({ top: 7, bottom: 7, left: 11, right: 11 })
      .backgroundColor(this.curTab === idx ? COLORS.primary : COLORS.card)
      .borderRadius(8)
      Rect({ width: 18, height: 4 })
        .fill(this.curTab === idx ? COLORS.accent : COLORS.line)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .onClick(() => {
      this.switchTab(idx)
    })
  }

  @Builder
  tabBar() {
    Row({ space: 7 }) {
      this.folderTab('陪诊师', '🧑‍⚕️', 0)
      this.folderTab('服务', '📋', 1)
      this.folderTab('订单', '🗂', 2)
      this.folderTab('档案', '📔', 3)
      this.folderTab('须知', '📖', 4)
      this.folderTab('评价', '💬', 5)
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 10, bottom: 6 })
  }

  @Builder
  sectionTitle(title: string, sub: string, act: number) {
    Row() {
      Column() {
        Text(title)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Rect({ width: 22, height: 3 })
          .fill(COLORS.accent)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      Column()
        .layoutWeight(1)
      Text(sub)
        .fontSize(11)
        .fontColor(COLORS.accent)
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor(COLORS.accentSoft)
        .borderRadius(10)
        .onClick(() => {
          if (act === 0) {
            this.refreshNurses()
          } else if (act === 1) {
            this.refreshReviews()
          } else {
            this.addOpen = true
          }
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 8, bottom: 6 })
  }

  @Builder
  pageNurse() {
    Column() {
      Row() {
        Column() {
          Text('家人就医 · 一人陪同全搞定')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('平台认证 · 每单投保意外险')
            .fontSize(11)
            .fontColor('#CFEDEF')
            .margin({ top: 4 })
          Text('首单立减 ¥30')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
            .margin({ top: 6 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('🩺')
          .fontSize(38)
          .margin({ right: 12 })
      }
      .width('100%')
      .padding(14)
      .borderRadius(14)
      .linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
      .onClick(() => {
        this.addOpen = true
      })

      this.sectionTitle('认证陪诊师', '换一批', 0)
      ForEach(this.nurseList, (it: NurseItem, i: number) => {
        Row() {
          Stack() {
            Circle({ width: 48, height: 48 })
              .fill(COLORS.tagBg)
            Text('🧑‍⚕️')
              .fontSize(23)
          }
          Column() {
            Row() {
              Text(it.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Text('✓ 认证')
                .fontSize(9)
                .fontColor(COLORS.primary)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor(COLORS.tagBg)
                .borderRadius(6)
                .margin({ left: 6 })
            }
            Text('擅长 ' + it.hosp + ' · ' + it.years + '年经验')
              .fontSize(11)
              .fontColor(COLORS.textSecond)
              .margin({ top: 3 })
            Text('⭐ ' + it.score + ' · 已服务' + it.orders + '单')
              .fontSize(11)
              .fontColor(COLORS.gold)
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Column() {
            Text(it.fav ? '❤️' : '🤍')
              .fontSize(15)
              .onClick(() => {
                this.toggleNurseFav(i)
              })
            Text('约TA')
              .fontSize(12)
              .fontColor(COLORS.white)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .backgroundColor(COLORS.primary)
              .borderRadius(14)
              .margin({ top: 6 })
              .onClick(() => {
                this.addOpen = true
              })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 8 })
      }, (it: NurseItem) => 'n' + it.id.toString() + '_' + this.rev.toString())
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageService() {
    Column() {
      this.sectionTitle('服务套餐', '去预约', 2)
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(SERVICES, (it: ServicePackItem, i: number) => {
          Column() {
            Row() {
              Text(it.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              if (it.hot) {
                Text('🔥')
                  .fontSize(12)
                  .margin({ left: 4 })
              }
            }
            Text(it.desc)
              .fontSize(10)
              .fontColor(COLORS.textSecond)
              .margin({ top: 4 })
            Row() {
              Text('¥' + it.price)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.accent)
              Text('/' + it.unit)
                .fontSize(10)
                .fontColor(COLORS.textThird)
              Column()
                .layoutWeight(1)
              Text('下单')
                .fontSize(11)
                .fontColor(COLORS.white)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .backgroundColor(COLORS.primary)
                .borderRadius(12)
                .onClick(() => {
                  this.addOpen = true
                })
            }
            .margin({ top: 6 })
          }
          .width('48%')
          .alignItems(HorizontalAlign.Start)
          .padding(12)
          .backgroundColor(COLORS.card)
          .borderRadius(12)
          .margin({ top: 8 })
          .onClick(() => {
            this.addOpen = true
          })
        }, (it: ServicePackItem) => 'sv' + it.id.toString())
      }
      .width('94%')

      Row({ space: 8 }) {
        Column() {
          Text('30分钟')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
          Text('平均响应')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(10)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        Column() {
          Text('500+')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
          Text('认证陪诊师')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(10)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        Column() {
          Text('98.6%')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.gold)
          Text('好评率')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(10)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
      }
      .width('100%')
      .margin({ left: 16, right: 16, top: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageOrder() {
    Column() {
      this.sectionTitle('订单记录', '筛选', 2)
      Row({ space: 8 }) {
        Text('全部')
          .fontSize(11)
          .fontColor(this.orderFilter === 0 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .backgroundColor(this.orderFilter === 0 ? COLORS.primary : COLORS.card)
          .borderRadius(12)
          .onClick(() => {
            this.orderFilter = 0
          })
        Text('待服务')
          .fontSize(11)
          .fontColor(this.orderFilter === 1 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .backgroundColor(this.orderFilter === 1 ? COLORS.primary : COLORS.card)
          .borderRadius(12)
          .onClick(() => {
            this.orderFilter = 1
          })
        Text('陪诊中')
          .fontSize(11)
          .fontColor(this.orderFilter === 2 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .backgroundColor(this.orderFilter === 2 ? COLORS.primary : COLORS.card)
          .borderRadius(12)
          .onClick(() => {
            this.orderFilter = 2
          })
        Text('已完成')
          .fontSize(11)
          .fontColor(this.orderFilter === 3 ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .backgroundColor(this.orderFilter === 3 ? COLORS.primary : COLORS.card)
          .borderRadius(12)
          .onClick(() => {
            this.orderFilter = 3
          })
      }
      .width('100%')
      .margin({ left: 16, right: 16 })

      ForEach(this.orderList, (it: OrderRecItem, i: number) => {
        if (this.orderShown(i)) {
          Column() {
            Row() {
              Text(it.hosp)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Column()
                .layoutWeight(1)
              Text(it.stat)
                .fontSize(11)
                .fontColor(statColor(it.stat))
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                .backgroundColor(COLORS.tagBg)
                .borderRadius(8)
            }
            .width('100%')
            Row() {
              Text('🧑‍⚕️ ' + it.nurse)
                .fontSize(11)
                .fontColor(COLORS.textSecond)
              Text('🕐 ' + it.date)
                .fontSize(11)
                .fontColor(COLORS.textSecond)
                .margin({ left: 12 })
              Column()
                .layoutWeight(1)
              Text('¥' + it.fee)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.accent)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Column()
                .layoutWeight(1)
              Text('加急接送')
                .fontSize(11)
                .fontColor(COLORS.accent)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                .backgroundColor(COLORS.accentSoft)
                .borderRadius(12)
                .onClick(() => {
                  this.bizOpen = true
                })
              Text('取消订单')
                .fontSize(11)
                .fontColor(COLORS.danger)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                .backgroundColor('#FDECEA')
                .borderRadius(12)
                .margin({ left: 8 })
                .onClick(() => {
                  this.delOpen = true
                })
            }
            .width('100%')
            .margin({ top: 8 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.card)
          .borderRadius(12)
          .margin({ left: 16, right: 16, top: 8 })
        }
      }, (it: OrderRecItem) => 'o' + it.id.toString() + '_' + this.rev.toString() + this.orderFilter.toString())
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageFile() {
    Column() {
      this.sectionTitle('健康档案', '新增', 2)
      ForEach(this.fileList, (it: HealthFileItem, i: number) => {
        Column() {
          Row() {
            Stack() {
              Circle({ width: 40, height: 40 })
                .fill(COLORS.tagBg)
              Text('👤')
                .fontSize(19)
            }
            Column() {
              Text(it.name + ' · ' + it.age + '岁')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Text('关系:' + it.rel)
                .fontSize(11)
                .fontColor(COLORS.textSecond)
                .margin({ top: 3 })
              Text('🩹 ' + it.note)
                .fontSize(11)
                .fontColor(COLORS.warn)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 10 })
            Column() {
              Text(it.remind ? '提醒开' : '提醒关')
                .fontSize(10)
                .fontColor(it.remind ? COLORS.white : COLORS.textSecond)
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                .backgroundColor(it.remind ? COLORS.primary : COLORS.tagBg)
                .borderRadius(10)
                .onClick(() => {
                  this.toggleRemind(i)
                })
              Text('编辑')
                .fontSize(10)
                .fontColor(COLORS.accent)
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                .backgroundColor(COLORS.accentSoft)
                .borderRadius(10)
                .margin({ top: 6 })
                .onClick(() => {
                  this.editOpen = true
                })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 8 })
      }, (it: HealthFileItem) => 'f' + it.id.toString() + '_' + this.rev.toString())

      Column() {
        Text('近6月陪诊订单')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Row({ space: 12 }) {
          ForEach(MONTH_LABELS, (m: string, i: number) => {
            Column() {
              Text(MONTH_ORDERS[i].toString())
                .fontSize(9)
                .fontColor(COLORS.textThird)
              Column()
                .width(16)
                .height(monthOrderBar(i))
                .backgroundColor(i === 5 ? COLORS.primary : COLORS.tagBg)
                .borderRadius(4)
                .margin({ top: 3 })
              Text(m)
                .fontSize(10)
                .fontColor(COLORS.textSecond)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Center)
          }, (m: string) => 'om' + m)
        }
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
      .margin({ left: 16, right: 16, top: 12 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageTip() {
    Column() {
      this.sectionTitle('就医须知', '全部', 2)
      ForEach(TIPS, (it: TipItem, i: number) => {
        Row() {
          Text('📖')
            .fontSize(18)
          Column() {
            Text(it.title)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text(it.cat + ' · ' + it.reads + '人阅读')
              .fontSize(10)
              .fontColor(COLORS.textThird)
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
          Text('›')
            .fontSize(18)
            .fontColor(COLORS.textThird)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 8 })
        .onClick(() => {
          this.bizOpen = true
        })
      }, (it: TipItem) => 'tp' + it.id.toString())

      Column() {
        Text('服务类型占比')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Row() {
          ForEach(SVC_PCTS, (p: number, i: number) => {
            Column()
              .layoutWeight(p)
              .height(12)
              .backgroundColor(SVC_COLORS[i])
          }, (p: number, i: number) => 'vp' + i.toString())
        }
        .width('100%')
        .borderRadius(6)
        .clip(true)
        .margin({ top: 10 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(SVC_LABELS, (l: string, i: number) => {
            Row() {
              Circle({ width: 7, height: 7 })
                .fill(SVC_COLORS[i])
              Text(l + ' ' + SVC_PCTS[i] + '%')
                .fontSize(10)
                .fontColor(COLORS.textSecond)
                .margin({ left: 4 })
            }
            .margin({ right: 12, top: 8 })
          }, (l: string) => 'vl' + l)
        }
        .margin({ top: 2 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
      .margin({ left: 16, right: 16, top: 12 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageReview() {
    Column() {
      this.sectionTitle('用户评价', '换一批', 1)
      ForEach(this.reviewList, (it: ReviewItem, i: number) => {
        Column() {
          Row() {
            Stack() {
              Circle({ width: 36, height: 36 })
                .fill(COLORS.tagBg)
              Text('🙂')
                .fontSize(17)
            }
            Column() {
              Row() {
                Text(it.user)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text('服务:' + it.nurse)
                  .fontSize(10)
                  .fontColor(COLORS.textSecond)
                  .margin({ left: 8 })
              }
              Row() {
                ForEach([0, 1, 2, 3, 4], (s: number) => {
                  Text(s < it.stars ? '⭐' : '☆')
                    .fontSize(11)
                }, (s: number) => 'st' + s.toString() + it.id.toString())
              }
              .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 10 })
          }
          .width('100%')
          Text('“' + it.txt + '”')
            .fontSize(12)
            .fontColor(COLORS.textSecond)
            .margin({ top: 8 })
            .padding({ left: 10, right: 10 })
            .backgroundColor(COLORS.tagBg)
            .borderRadius(8)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 8 })
      }, (it: ReviewItem) => 'rv' + it.id.toString() + '_' + this.rev.toString())
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder
  pageMainOther() {
    Column() {
      Text('🚑')
        .fontSize(40)
        .margin({ top: 60 })
      Text('进行中的服务')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 10 })
      Text('今日 09:00 · 协和医院 · 林安陪诊中')
        .fontSize(12)
        .fontColor(COLORS.textSecond)
        .margin({ top: 6 })
      Text('全程轨迹可查看 · 已服务1小时42分')
        .fontSize(12)
        .fontColor(COLORS.ok)
        .margin({ top: 4 })
      Text('加急接送')
        .fontSize(12)
        .fontColor(COLORS.white)
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor(COLORS.accent)
        .borderRadius(16)
        .margin({ top: 12 })
        .onClick(() => {
          this.bizOpen = true
        })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  modalBodyAdd() {
    Column() {
      Row() {
        Text('🩺 预约陪诊 BOOKING')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(COLORS.textThird)
          .onClick(() => {
            this.addOpen = false
          })
      }
      .width('100%')

      Text('就诊医院')
        .fontSize(13)
        .fontColor(COLORS.textSecond)
        .margin({ top: 14 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(HOSP_TAGS, (h: string, i: number) => {
          Text(h)
            .fontSize(12)
            .fontColor(this.hospTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.hospTags.indexOf(i) >= 0 ? COLORS.primary : COLORS.tagBg)
            .borderRadius(14)
            .margin({ right: 8, top: 8 })
            .onClick(() => {
              this.toggleTag(this.hospTags, i)
            })
        }, (h: string) => 'hb' + h)
      }
      .width('100%')
      .margin({ top: 4 })

      Text('服务时长(小时)')
        .fontSize(13)
        .fontColor(COLORS.textSecond)
        .margin({ top: 16 })
      Row() {
        Text('-')
          .fontSize(16)
          .fontColor(COLORS.textSecond)
          .padding(10)
          .backgroundColor(COLORS.tagBg)
          .borderRadius(10)
          .onClick(() => {
            if (this.hourStep > 2) {
              this.hourStep = this.hourStep - 1
            }
          })
        Column() {
          Text(this.hourStep.toString() + ' 小时')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        Text('+')
          .fontSize(16)
          .fontColor(COLORS.white)
          .padding(10)
          .backgroundColor(COLORS.primary)
          .borderRadius(10)
          .onClick(() => {
            if (this.hourStep < 12) {
              this.hourStep = this.hourStep + 1
            }
          })
      }
      .width('100%')
      .margin({ top: 8 })

      Text('含挂号代取 · 排队代办 · 就医引导')
        .fontSize(11)
        .fontColor(COLORS.textThird)
        .margin({ top: 8 })

      Row() {
        Text('提交预约')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .backgroundColor(COLORS.primary)
          .borderRadius(20)
          .onClick(() => {
            this.addOpen = false
          })
        Column()
          .layoutWeight(1)
        Text('预估 ¥' + this.bookTotal())
          .fontSize(13)
          .fontColor(COLORS.accent)
      }
      .width('100%')
      .margin({ top: 20, bottom: 20 })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16 })
    .backgroundColor(COLORS.card)
    .borderRadius(20)
    .constraintSize({ maxHeight: '80%' })
  }

  @Builder
  modalBodyEdit() {
    Column() {
      Row() {
        Text('📔 编辑健康档案 HEALTH FILE')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(COLORS.textThird)
          .onClick(() => {
            this.editOpen = false
          })
      }
      .width('100%')

      Text('与我的关系')
        .fontSize(13)
        .fontColor(COLORS.textSecond)
        .margin({ top: 14 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(REL_TAGS, (r: string, i: number) => {
          Text(r)
            .fontSize(12)
            .fontColor(this.relTags.indexOf(i) >= 0 ? COLORS.white : COLORS.accent)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.relTags.indexOf(i) >= 0 ? COLORS.accent : COLORS.accentSoft)
            .borderRadius(14)
            .margin({ right: 8, top: 8 })
            .onClick(() => {
              this.toggleTag(this.relTags, i)
            })
        }, (r: string) => 'rl' + r)
      }
      .width('100%')
      .margin({ top: 4 })

      Text('年龄(岁)')
        .fontSize(13)
        .fontColor(COLORS.textSecond)
        .margin({ top: 16 })
      Row() {
        Text('-')
          .fontSize(16)
          .fontColor(COLORS.textSecond)
          .padding(10)
          .backgroundColor(COLORS.tagBg)
          .borderRadius(10)
          .onClick(() => {
            if (this.ageStep > 1) {
              this.ageStep = this.ageStep - 1
            }
          })
        Column() {
          Text(this.ageStep.toString() + ' 岁')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        Text('+')
          .fontSize(16)
          .fontColor(COLORS.white)
          .padding(10)
          .backgroundColor(COLORS.accent)
          .borderRadius(10)
          .onClick(() => {
            if (this.ageStep < 110) {
              this.ageStep = this.ageStep + 1
            }
          })
      }
      .width('100%')
      .margin({ top: 8 })

      Row() {
        Column() {
          Text('复诊智能提醒')
            .fontSize(14)
            .fontColor(COLORS.textPrimary)
          Text('按用药周期自动推送陪诊建议')
            .fontSize(11)
            .fontColor(COLORS.textThird)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text(this.remindFlag ? '已开启' : '已关闭')
          .fontSize(12)
          .fontColor(this.remindFlag ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.remindFlag ? COLORS.primary : COLORS.tagBg)
          .borderRadius(14)
          .onClick(() => {
            this.remindFlag = !this.remindFlag
          })
      }
      .width('100%')
      .margin({ top: 16 })
      .padding(12)
      .backgroundColor(COLORS.tagBg)
      .borderRadius(12)

      Row() {
        Text('保存档案')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .backgroundColor(COLORS.accent)
          .borderRadius(20)
          .onClick(() => {
            this.editOpen = false
          })
        Column()
          .layoutWeight(1)
        Text('共 ' + this.fileList.length + ' 份档案')
          .fontSize(11)
          .fontColor(COLORS.textThird)
      }
      .width('100%')
      .margin({ top: 20, bottom: 20 })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16 })
    .backgroundColor(COLORS.card)
    .borderRadius(20)
    .constraintSize({ maxHeight: '80%' })
  }

  @Builder
  modalBodyDel() {
    Column() {
      Row() {
        Text('⚠️ 取消订单确认')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.danger)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(COLORS.textThird)
          .onClick(() => {
            this.delOpen = false
          })
      }
      .width('100%')

      Column() {
        Text('🩺')
          .fontSize(34)
        Text('08-24 协和医院陪诊单即将取消')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
          .margin({ top: 8 })
        Text('服务前4小时外取消全额退款')
          .fontSize(11)
          .fontColor(COLORS.warn)
          .margin({ top: 6 })
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
      .padding({ top: 18, bottom: 18 })
      .backgroundColor(COLORS.accentSoft)
      .borderRadius(14)
      .margin({ top: 14 })

      Row({ space: 8 }) {
        Column() {
          Text('¥268')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
          Text('订单金额')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(8)
        .backgroundColor(COLORS.tagBg)
        .borderRadius(10)
        Column() {
          Text('0')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.ok)
          Text('取消费用')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(8)
        .backgroundColor(COLORS.tagBg)
        .borderRadius(10)
        Column() {
          Text('3次')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
          Text('本月已取消')
            .fontSize(10)
            .fontColor(COLORS.textSecond)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding(8)
        .backgroundColor(COLORS.tagBg)
        .borderRadius(10)
      }
      .width('100%')
      .margin({ top: 12 })

      Row() {
        Text('再想想')
          .fontSize(14)
          .fontColor(COLORS.textSecond)
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .backgroundColor(COLORS.tagBg)
          .borderRadius(20)
          .onClick(() => {
            this.delOpen = false
          })
        Column()
          .layoutWeight(1)
        Text('确认取消')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .backgroundColor(COLORS.danger)
          .borderRadius(20)
          .onClick(() => {
            this.delOpen = false
          })
      }
      .width('100%')
      .margin({ top: 18, bottom: 20 })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16 })
    .backgroundColor(COLORS.card)
    .borderRadius(20)
    .constraintSize({ maxHeight: '80%' })
  }

  @Builder
  modalBodyBiz() {
    Column() {
      Row() {
        Text('🚑 加急接送 RUSH PICKUP')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(COLORS.textThird)
          .onClick(() => {
            this.bizOpen = false
          })
      }
      .width('100%')

      Row() {
        Column() {
          Text('点到点就医专车')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('护士随行 · 轮椅/担架可选')
            .fontSize(11)
            .fontColor('#CFEDEF')
            .margin({ top: 4 })
          Text('加急费 ¥50 · 30分钟内到达')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
            .margin({ top: 6 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('🚑')
          .fontSize(36)
      }
      .width('100%')
      .padding(14)
      .borderRadius(14)
      .linearGradient({ angle: 120, colors: [[COLORS.primary, 0], [COLORS.accent, 1]] })
      .margin({ top: 14 })

      Text('车型选择')
        .fontSize(13)
        .fontColor(COLORS.textSecond)
        .margin({ top: 14 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(['经济轿车', '无障碍车', '商务7座', '担架车'], (c: string, i: number) => {
          Text(c)
            .fontSize(12)
            .fontColor(this.carTags.indexOf(i) >= 0 ? COLORS.white : COLORS.textSecond)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.carTags.indexOf(i) >= 0 ? COLORS.accent : COLORS.tagBg)
            .borderRadius(14)
            .margin({ right: 8, top: 8 })
            .onClick(() => {
              this.toggleTag(this.carTags, i)
            })
        }, (c: string) => 'ca' + c)
      }
      .width('100%')
      .margin({ top: 4 })

      Row() {
        Column() {
          Text('红灯优先调度')
            .fontSize(14)
            .fontColor(COLORS.textPrimary)
          Text('晚高峰不加价,司机实时定位')
            .fontSize(11)
            .fontColor(COLORS.textThird)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text(this.rushFlag ? '已开启' : '已关闭')
          .fontSize(12)
          .fontColor(this.rushFlag ? COLORS.white : COLORS.textSecond)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.rushFlag ? COLORS.primary : COLORS.tagBg)
          .borderRadius(14)
          .onClick(() => {
            this.rushFlag = !this.rushFlag
          })
      }
      .width('100%')
      .margin({ top: 16 })
      .padding(12)
      .backgroundColor(COLORS.tagBg)
      .borderRadius(12)

      Row() {
        Text('叫车')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .backgroundColor(COLORS.accent)
          .borderRadius(20)
          .onClick(() => {
            this.bizOpen = false
          })
        Column()
          .layoutWeight(1)
        Text('附近 7 辆车可用')
          .fontSize(11)
          .fontColor(COLORS.ok)
      }
      .width('100%')
      .margin({ top: 20, bottom: 20 })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16 })
    .backgroundColor(COLORS.card)
    .borderRadius(20)
    .constraintSize({ maxHeight: '80%' })
  }

  @Builder
  modalOverlay() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('rgba(6,30,34,0.5)')
        .onClick(() => {
          this.closeAll()
        })
      if (this.addOpen) {
        this.modalBodyAdd()
      }
      if (this.editOpen) {
        this.modalBodyEdit()
      }
      if (this.delOpen) {
        this.modalBodyDel()
      }
      if (this.bizOpen) {
        this.modalBodyBiz()
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  bottomBar() {
    Row() {
      Column() {
        Text('🏠')
          .fontSize(20)
        Text('首页')
          .fontSize(10)
          .fontColor(this.mainTab === 0 ? COLORS.primary : COLORS.textThird)
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.switchMain(0)
      })
      Column() {
        Text('🗂')
          .fontSize(20)
        Text('订单')
          .fontSize(10)
          .fontColor(this.mainTab === 1 ? COLORS.primary : COLORS.textThird)
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.switchMain(1)
      })
      Column() {
        Text('➕')
          .fontSize(24)
          .fontColor(COLORS.white)
          .padding(10)
          .backgroundColor(COLORS.primary)
          .borderRadius(26)
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.addOpen = true
      })
      Column() {
        Text('💬')
          .fontSize(20)
        Text('消息')
          .fontSize(10)
          .fontColor(this.mainTab === 3 ? COLORS.primary : COLORS.textThird)
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.switchMain(3)
      })
      Column() {
        Text('👤')
          .fontSize(20)
        Text('我的')
          .fontSize(10)
          .fontColor(this.mainTab === 4 ? COLORS.primary : COLORS.textThird)
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.switchMain(4)
      })
    }
    .width('100%')
    .padding({ top: 6, bottom: 6 })
    .backgroundColor(COLORS.card)
  }

  @Builder
  mainContent() {
    Scroll() {
      Column() {
        this.tabBar()
        if (this.mainTab === 0) {
          if (this.curTab === 0) {
            this.pageNurse()
          }
          if (this.curTab === 1) {
            this.pageService()
          }
          if (this.curTab === 2) {
            this.pageOrder()
          }
          if (this.curTab === 3) {
            this.pageFile()
          }
          if (this.curTab === 4) {
            this.pageTip()
          }
          if (this.curTab === 5) {
            this.pageReview()
          }
        } else {
          this.pageMainOther()
        }
      }
      .width('100%')
    }
    .width('100%')
    .layoutWeight(1)
    .scrollBar(BarState.Off)
    .edgeEffect(EdgeEffect.Spring)
  }

  build() {
    Stack() {
      Column() {
        this.header()
        this.mainContent()
        this.bottomBar()
      }
      .width('100%')
      .height('100%')
      this.fxLayer()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }
}


在这里插入图片描述

十五、总结

通过对安心陪诊就医陪诊服务场景的完整代码分析,我们能够深刻感受到HarmonyOS ArkTS在医疗健康服务类应用开发中的技术表现力。该场景涵盖了六个内容板块和四个弹框交互,代码结构清晰、逻辑严谨,充分展现了声明式UI开发范式在处理复杂业务场景时的优势。从色彩系统到数据模型,从页面构建器到弹框系统,每一个技术环节都经过了精心设计,使得应用在视觉表现、交互体验和代码可维护性三个维度都达到了高水准。

在数据架构设计方面,六组interface与@Observed类的配合使用展现了ArkTS类型系统在医疗数据场景中的特殊价值。医疗健康数据对准确性和完整性的要求极高——陪诊师的擅长医院、订单的服务状态、档案的健康备注、评价的星级评分,任何一个字段的错误都可能导致信息误导。interface的编译时类型检查确保了字段定义的完整性,@Observed的响应式更新机制保证了数据变化与UI显示的实时同步,构造函数中的三元表达式安全赋值模式则防止了布尔字段在数据缺失时的异常行为。这种多层保障的数据架构设计,使得应用在面对复杂多变的真实数据时依然能够保持稳定可靠的运行。

病历夹页式folderTab的设计是该场景最具创意的UI元素。通过Column与Rect的组合,在每个标签项底部绘制了一个彩色标记条——选中时橙色高亮,未选中时灰色暗淡,模拟了传统病历夹中突出于页面上方的彩色标签。这种设计不仅具有视觉辨识度,更重要的是它服务于业务场景——陪诊服务与医疗场景高度相关,病历夹元素与医疗主题形成了功能层面的呼应,使用户在打开应用的第一时间就能感受到医疗服务的专业氛围。单排6个标签的布局设计在信息密度和可点击区域之间取得了良好的平衡,六个功能板块的命名(陪诊师、服务、订单、档案、须知、评价)覆盖了用户从了解服务到完成评价的完整旅程。

订单页面的多状态过滤设计是该场景在交互逻辑上的亮点。通过orderFilter状态变量和orderShown方法的配合,实现了"全部"“待服务”“陪诊中”"已完成"四种过滤模式的切换。这种设计的核心技术价值在于:过滤操作完全在UI层面通过条件渲染实现,原始数据数组始终保持不变,避免了数据层面的数组操作开销。ForEach的keyGenerator中加入了orderFilter值,确保了过滤状态切换时列表项能够正确识别并重新渲染。这种"数据不变、UI过滤"的设计模式在处理列表筛选时具有广泛的适用性,适用于订单管理、消息过滤、商品筛选等多种业务场景。

在性能优化方面,代码中的多处设计细节值得关注。ForEach的keyGenerator使用了id + rev + orderFilter的复合键模式,确保了列表数据轮转和过滤切换时的正确刷新;定时器在aboutToDisappear中的清理防止了组件销毁后的内存泄漏;constraintSize的maxHeight限制防止了弹框内容过多时超出屏幕边界;hitTestBehavior(HitTestMode.None)确保了fxLayer动画层不干扰用户交互;条件渲染if (this.orderShown(i))避免了未匹配过滤条件的订单项被渲染到DOM中。这些细节优化共同保证了应用在处理大量列表数据和频繁过滤切换时的流畅性能表现。

Logo

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

更多推荐