在HarmonyOS ArkTS声明式UI范式下,地质科考直播场景的构建需要兼顾实时音视频流、多机位画面切换、弹幕系统、标本档案管理等核心能力的协同运作。岩语·云地质营作为一款融合了科普教育与电商直播双重属性的应用,将地质学家的化石野采过程以连线围观的方式呈现给用户,实现了"科研+直播+电商"三位一体的产品形态。本文将围绕其ArkTS源码架构展开逐段深度剖析,从数据模型设计到UI组件编排,从状态管理到弹窗交互,全面解析HarmonyOS 6.1.1声明式开发的技术内功。

声明式UI的核心优势在于数据驱动视图——当@State修饰的状态变量发生变化时,ArkTS框架会自动触发依赖该状态的UI组件重新渲染,开发者无需手动调用刷新方法。这一机制在本应用中得到了淋漓尽致的运用:用户点击收藏化石标本时,starred字段的翻转立即驱动UI上星标图标的切换;弹幕列表通过ForEach的数据绑定实现自动追加;六步法野采流程的完成状态切换同样由状态变量驱动的条件渲染完成。这种"状态即真相"的设计哲学,使得整个应用的代码逻辑清晰可追踪。

从工程架构层面审视,本应用采用了典型的"主页面+六子Tab+五弹窗"的分层组件树结构。主组件Index207作为状态枢纽,持有所有业务数据并通过@Prop单向数据流将数据分发给各子Tab组件;各子组件通过回调函数将用户操作回传给主组件,由主组件统一执行状态更新。这种单向数据流架构保证了数据变更的可预测性,同时配合bindSheet和bindContentCover的双向绑定语法($$),实现了弹窗的优雅弹出与关闭。

一、引言:地质科考直播应用的技术架构概览

在这里插入图片描述

在HarmonyOS 6.1.1的ArkTS声明式UI开发框架中,构建一款地质科考直播围观应用需要综合考虑多方面的技术挑战。首先是实时性要求——直播画面、弹幕消息、用户连线状态都需要在毫秒级别内完成更新与渲染;其次是数据模型的复杂性——化石标本、地层信息、挖掘日志、向导信息、工具库存等业务实体之间存在着错综复杂的关联关系;最后是交互体验的丰富度——从底部地层剖面式Tab导航到多种弹窗的协同切换,每一个交互细节都需要精心设计。

岩语·云地质营应用以"岩层棕×玛瑙橙×石灰灰"的配色体系为视觉基调,模拟了真实地质勘探场景的色彩氛围。底部Tab导航采用了"地层剖面"的视觉隐喻——每个Tab项由三条宽度递增的色带堆叠而成,从上至下分别对应图标层、文字层和指示层,选中项的色带从棕色渐变为橙色并整体放大1.07倍,形成了一个立体的地层剖面效果。这种将业务领域概念(地层剖面)融入UI视觉设计的做法,正是HarmonyOS ArkTS声明式UI灵活表达能力的体现。

从状态管理的角度,应用采用了@State和@Prop两类状态装饰器的组合策略。主组件Index207通过@State持有所有可变数据(化石列表、弹窗开关、表单状态等),并通过@Prop将数据以只读方式传递给子组件。子组件内部的状态变更通过回调函数回传给父组件执行,确保了数据流的单向性。这种架构模式在HarmonyOS API 24中是被官方推荐的最佳实践,它既保证了组件间的数据隔离性,又避免了多组件间状态同步的复杂性问题。

二、数据接口与类型定义:化石标本的领域模型

在这里插入图片描述

在ArkTS声明式开发中,interface是定义业务数据模型的核心手段。本应用定义了10个接口,覆盖了地质科考直播场景下的全部业务实体。

interface DigDay207 {
  day: string
  finds: number
}

interface Fossil207 {
  id: number
  name: string
  era: string
  integrity: number
  depth: number
  state: string
  watchers: number
  starred: boolean
}

interface Stratum207 {
  id: number
  name: string
  age: number
  color: string
  fossil: string
  heat: number
}

interface DigLog207 {
  id: number
  name: string
  era: string
  dayNum: number
  strikes: number
  weight: number
  state: string
  top: boolean
}

以上代码定义了四个核心业务接口。DigDay207表示每日挖掘发现量,包含星期几和化石发现数量两个字段,用于柱状图渲染。Fossil207是整个应用最核心的数据实体——化石标本,它携带了编号、名称、地质年代(寒武纪/奥陶纪/泥盆纪/侏罗纪/白垩纪)、轮廓完好度百分比、出土深度(米)、当前状态(敲击中/热层待掘/已封装)、围观人次和精选标记等八个字段,几乎涵盖了标本档案卡所需的全部信息。

在ArkTS的类型系统中,interface定义的纯数据结构不允许包含方法实现,这与TypeScript的interface有本质区别。所有对数据的操作逻辑必须通过独立的函数来实现,这种"数据与行为分离"的设计原则强制开发者在架构层面保持数据模型的纯粹性。

Stratum207定义了地层信息,包括地层名称(如"寒武系灰岩")、地质年龄(亿年前)、代表色、代表化石和挖掘热度值。DigLog207则记录了野采日志条目,含敲击次数和岩样重量等操作量化数据。这些接口的字段命名遵循了中文语义化的原则,使得业务含义一目了然。

interface Guide207 {
  id: number
  name: string
  city: string
  years: number
  online: boolean
  heat: number
}

interface Barrage207 {
  id: number
  text: string
}

interface DigStep207 {
  id: number
  title: string
  tip: string
  done: boolean
}

interface EraCount207 {
  label: string
  count: number
  color: string
}

interface ToolUse207 {
  day: string
  uses: number
}

interface Tool207 {
  id: number
  name: string
  icon: string
  stock: number
  borrowed: boolean
  times: number
}

第二组接口定义了辅助业务实体。Guide207描述向导信息,包含在线状态和人气热度,用于向导团Tab的人气榜展示。Barrage207是极简的弹幕模型,仅含编号和文本内容。DigStep207定义了野采六步法的每个步骤——标题、操作提示和完成状态。EraCount207是年代统计数据传输对象,用于堆叠条形图渲染。ToolUse207Tool207分别描述工具使用频次和工具库存信息。

三、工具函数:状态色彩映射与数据聚合

在这里插入图片描述

ArkTS要求所有业务逻辑函数独立于组件之外定义,本应用在文件顶部定义了8个工具函数,承担了状态色彩映射和数据统计聚合两大职能。

function fossilStateColor207(state: string): string {
  if (state === '敲击中') {
    return '#FF7043'
  }
  if (state === '热层待掘') {
    return '#FBC02D'
  }
  if (state === '已封装') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function digStateColor207(state: string): string {
  if (state === '重点层') {
    return '#E53935'
  }
  if (state === '出化石') {
    return '#FF7043'
  }
  if (state === '已完成') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function eraColor207(era: string): string {
  if (era === '寒武纪') {
    return '#8D6E63'
  }
  if (era === '奥陶纪') {
    return '#A1887F'
  }
  if (era === '泥盆纪') {
    return '#FF7043'
  }
  if (era === '侏罗纪') {
    return '#43A047'
  }
  return '#1E88E5'
}

三个色彩映射函数分别服务于化石状态、挖掘日志状态和地质年代的视觉呈现。fossilStateColor207将三种化石状态映射为橙色(敲击中,表示活跃进行)、黄色(热层待掘,表示预警等待)和绿色(已封装,表示完成归档)。digStateColor207为挖掘日志的状态标记提供了红色(重点层)、橙色(出化石)和绿色(已完成)的色彩区分。eraColor207则将五个地质年代映射为不同的棕/橙/绿/蓝色系,使得年代标签在UI上一眼可辨。

色彩映射函数采用纯函数设计,无副作用——相同的输入永远返回相同的输出。这种设计使得函数可以在任意组件中被安全调用,无需担心状态污染问题,也是ArkTS声明式范式中推荐的最佳实践之一。

function starredFossilCount207(fossils: Fossil207[]): number {
  let n: number = 0
  for (let i = 0; i < fossils.length; i++) {
    if (fossils[i].starred) {
      n++
    }
  }
  return n
}

function strikingFossilCount207(fossils: Fossil207[]): number {
  let n: number = 0
  for (let i = 0; i < fossils.length; i++) {
    if (fossils[i].state === '敲击中') {
      n++
    }
  }
  return n
}

function onlineGuideCount207(guides: Guide207[]): number {
  let m: number = 1
  for (let i = 0; i < guides.length; i++) {
    if (guides[i].heat > m) {
      m = guides[i].heat
    }
  }
  return m
}

function eraCounts207(fossils: Fossil207[]): EraCount207[] {
  const counts: EraCount207[] = []
  for (let i = 0; i < eraTags207.length; i++) {
    let n: number = 0
    for (let j = 0; j < fossils.length; j++) {
      if (fossils[j].era === eraTags207[i]) {
        n++
      }
    }
    counts.push({ label: eraTags207[i], count: n, color: ['#8D6E63', '#A1887F', '#FF7043', '#43A047', '#1E88E5'][i] })
  }
  return counts
}

四个统计聚合函数负责在数据集合上执行遍历计算。starredFossilCount207统计精选标记的化石数量,strikingFossilCount207统计当前正在敲击中的化石数量——这两个函数的结果直接驱动了标本墙Tab顶部统计卡的数字显示。maxGuideHeat207获取向导列表中的最高人气值,用于人气榜进度条的归一化计算。eraCounts207是最复杂的聚合函数,它遍历所有化石标本,按地质年代分组计数,并生成带有色彩标签的统计结果数组,供堆叠条形图渲染使用。

四、主组件状态定义:弹窗开关与表单状态

在这里插入图片描述

主组件Index207是整个应用的状态中枢,它通过@State装饰器持有了应用运行时的全部可变数据。

@Entry
@Component
struct Index207 {
  @State tabIndex1: number = 0

  // 弹框开关
  @State showJoinSheet: boolean = false
  @State showSampleSheet: boolean = false
  @State showEditSheet: boolean = false
  @State showDelDialog: boolean = false
  @State showDetailDialog: boolean = false

  // 预约连线敲化石表单
  @State joinStrata: number = 3
  @State joinTool: number = 0
  @State joinMins: number = 45
  @State joinLamp: boolean = true
  @State joinGoggle: boolean = false

  // 新增标本记录表单
  @State sampleName: string = ''
  @State sampleEra: number = 3
  @State sampleIntegrity: number = 80
  @State sampleDepth: number = 12
  @State samplePublic: boolean = true

  // 编辑表单
  @State editIndex: number = -1
  @State editName: string = ''
  @State editIntegrity: number = 80
  @State editDepth: number = 12
  @State editTop: boolean = false

  // 删除
  @State delIndex: number = -1
  @State delKeepLog: boolean = true

  // 详情
  @State detailIndex: number = 0

状态变量可以分为四大类。第一类是Tab索引tabIndex1,控制当前激活的Tab页面。第二类是五个弹窗布尔开关——showJoinSheet控制预约连线抽屉、showSampleSheet控制上传标本抽屉、showEditSheet控制编辑标本抽屉、showDelDialog控制删除确认全屏遮罩、showDetailDialog控制标本详情全屏遮罩。这些布尔值通过$$双向绑定语法绑定到bindSheet和bindContentCover的显示状态上。

第三类是各弹窗内部的表单状态。预约表单包含目标层位索引、自带工具索引、连线时长(分钟)、夜采头灯开关和护目镜保障开关。标本上传表单包含名称、年代、完好度、深度和公开开关。编辑表单与上传表单字段类似但额外携带编辑索引。第四类是删除和详情弹窗的上下文索引——delIndexdetailIndex记录了用户当前操作的标本在数组中的位置。

在HarmonyOS ArkTS中,@State修饰的状态变量变更会自动触发引用该变量的UI组件的重新渲染。这一机制是实现声明式UI数据驱动的基础,开发者只需要关注"状态如何变化",而无需关心"视图如何更新",框架会自动完成Diff计算并执行最小化DOM操作。

五、化石标本数据初始化:业务种子数据

在这里插入图片描述

应用在组件初始化时通过@State的默认值注入了完整的种子数据,覆盖了化石标本、地层信息、挖掘日志、向导信息、每日出土量、工具使用频次、工具库存、弹幕消息和野采步骤等全部业务实体。

@State fossils: Fossil207[] = [
  { id: 1, name: '震旦角石', era: '奥陶纪', integrity: 92, depth: 8, state: '敲击中', watchers: 3210, starred: true },
  { id: 2, name: '莱得利基虫', era: '寒武纪', integrity: 78, depth: 15, state: '热层待掘', watchers: 1580, starred: false },
  { id: 3, name: '狼鳍鱼', era: '白垩纪', integrity: 88, depth: 6, state: '已封装', watchers: 2450, starred: true },
  { id: 4, name: '拟蜉蝣', era: '侏罗纪', integrity: 65, depth: 11, state: '热层待掘', watchers: 990, starred: false },
  { id: 5, name: '菊石切片', era: '泥盆纪', integrity: 95, depth: 4, state: '敲击中', watchers: 3860, starred: true },
  { id: 6, name: '恐龙蛋皮', era: '白垩纪', integrity: 60, depth: 18, state: '热层待掘', watchers: 4120, starred: false },
  { id: 7, name: '海百合冠', era: '泥盆纪', integrity: 85, depth: 13, state: '已封装', watchers: 2760, starred: true },
  { id: 8, name: '王冠虫', era: '志留纪', integrity: 72, depth: 9, state: '热层待掘', watchers: 1340, starred: false },
  { id: 9, name: '琥珀蚊', era: '白垩纪', integrity: 98, depth: 2, state: '已封装', watchers: 5230, starred: true },
  { id: 10, name: '枝脉蕨叶', era: '侏罗纪', integrity: 70, depth: 7, state: '热层待掘', watchers: 1120, starred: false }
]

化石标本数据是整个应用最核心的业务数据集,包含10条化石记录,涵盖了震旦角石、莱得利基虫、狼鳍鱼、菊石切片、恐龙蛋皮、海百合冠、琥珀蚊等古生物化石类型。每条记录都携带了完整的业务字段——年代跨越寒武纪到白垩纪,完好度从60%到98%不等,出土深度从2米到18米,围观人次最高达5230。这些真实感极强的种子数据使得应用在启动时即可呈现出丰富的内容生态。

@State strata: Stratum207[] = [
  { id: 1, name: '寒武系灰岩', age: 5.4, color: '#8D6E63', fossil: '三叶虫', heat: 92 },
  { id: 2, name: '奥陶系白云岩', age: 4.8, color: '#A1887F', fossil: '角石', heat: 88 },
  { id: 3, name: '泥盆系砂岩', age: 3.9, color: '#FF7043', fossil: '菊石', heat: 95 },
  { id: 4, name: '侏罗系页岩', age: 1.9, color: '#43A047', fossil: '狼鳍鱼', heat: 86 },
  { id: 5, name: '白垩系凝灰岩', age: 1.0, color: '#1E88E5', fossil: '琥珀', heat: 90 }
]

@State digLogs: DigLog207[] = [
  { id: 1, name: '震旦角石围岩剥离', era: '奥陶纪', dayNum: 3, strikes: 42, weight: 260, state: '重点层', top: true },
  { id: 2, name: '莱得利基虫层定位', era: '寒武纪', dayNum: 1, strikes: 18, weight: 120, state: '出化石', top: false },
  { id: 3, name: '狼鳍鱼页岩劈开', era: '白垩纪', dayNum: 5, strikes: 65, weight: 180, state: '出化石', top: false },
  { id: 4, name: '菊石结核敲取', era: '泥盆纪', dayNum: 2, strikes: 38, weight: 420, state: '重点层', top: false },
  { id: 5, name: '琥珀砂层淘洗', era: '白垩纪', dayNum: 6, strikes: 8, weight: 12, state: '已完成', top: false },
  { id: 6, name: '海百合冠修样', era: '泥盆纪', dayNum: 4, strikes: 25, weight: 310, state: '已完成', top: false },
  { id: 7, name: '恐龙蛋皮碎片收集', era: '白垩纪', dayNum: 7, strikes: 30, weight: 95, state: '重点层', top: false },
  { id: 8, name: '枝脉蕨板劈理观察', era: '侏罗纪', dayNum: 1, strikes: 12, weight: 140, state: '已完成', top: false }
]

地层信息数据描述了5个地质年代的地层——从5.4亿年前的寒武系灰岩到1.0亿年前的白垩系凝灰岩,每个地层携带代表色和挖掘热度值。挖掘日志数据记录了8条野采操作记录,包含敲击次数和岩样重量等量化指标,状态分为"重点层"、"出化石"和"已完成"三类,第一条记录被标记为置顶(top: true)。

六、头部Builder与地层剖面Tab导航

在这里插入图片描述

头部区域和底部Tab导航是应用视觉设计的精华所在,通过@Builder装饰器封装为可复用的UI构建方法。

@Builder
header207() {
  Column({ space: 12 }) {
    Row({ space: 10 }) {
      Column({ space: 4 }) {
        Text('岩语 · 云地质营').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        Text('地质学家连线敲化石 · 岩层野采直播围观').fontSize(11).fontColor('#D7CCC8')
      }
      .alignItems(HorizontalAlign.Start)
      Text('').layoutWeight(1)
      Column({ space: 2 }) {
        Text('🪨').fontSize(20)
        Text('1,862').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        Text('围挖掘位').fontSize(9).fontColor('#D7CCC8')
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')

    Row({ space: 10 }) {
      Column().width(4).height(34).borderRadius(2).backgroundColor('#FF7043')
      Column({ space: 3 }) {
        Text('泥盆纪菊石热层已开凿').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        Text('连线敲击抽标本命名权 · 围观送淘洗砂样').fontSize(10).fontColor('#EFEBE9')
      }
      .alignItems(HorizontalAlign.Start)
      Text('').layoutWeight(1)
      Column() {
        Text('去连线 →').fontSize(11).fontColor('#5D4037').fontWeight(FontWeight.Bold)
      }
      .padding({ left: 12, right: 12, top: 7, bottom: 7 })
      .borderRadius(14)
      .backgroundColor('#FBE9E7')
      .onClick(() => {
        this.showJoinSheet = true
      })
    }
    .width('100%')
    .padding(12)
    .borderRadius(12)
    .backgroundColor('#4E342E')
  }
  .alignItems(HorizontalAlign.Start)
  .padding(14)
  .linearGradient({ angle: 140, colors: [['#6D4C41', 0], ['#3E2723', 1]] })
}

头部Builder构建了一个深棕色渐变背景的科考季风头部区域。上层Row左侧显示应用标题"岩语·云地质营"和副标题,右侧展示围挖掘位数量统计。下层Row是一个促销活动条——左侧4像素宽的橙色竖条作为视觉锚点,中间展示"泥盆纪菊石热层已开凿"的活动文案,右侧"去连线→"按钮点击后设置showJoinSheet = true弹出预约抽屉。

linearGradient是ArkTS中实现线性渐变背景的核心API,通过angle参数控制渐变方向,colors数组定义渐变断点。本例中140度角从#6D4C41到#3E2723的渐变模拟了岩层断面的光影效果。

@Builder
tabBar207() {
  Column({ space: 0 }) {
    Row({ space: 0 }) {
      ForEach(this.tabs207, (t: string, i: number) => {
        Column({ space: 2 }) {
          // 地层剖面:三条宽度递增色带(上窄下宽)
          Row() {
            Text(this.tabIcons207[i]).fontSize(12)
          }
          .width(this.tabIndex1 === i ? '100%' : '56%')
          .height(18)
          .justifyContent(FlexAlign.Center)
          .borderRadius({ topLeft: 6, topRight: 6 })
          .backgroundColor(this.tabIndex1 === i ? '#FF8A65' : '#BCAAA4')

          Row() {
            Text(t).fontSize(9).fontColor('#FFFFFF').maxLines(1)
          }
          .width('76%')
          .height(16)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(this.tabIndex1 === i ? '#FF7043' : '#A1887F')

          Row() {
            Text(this.tabIndex1 === i ? '●' : '·').fontSize(8)
          }
          .width('100%')
          .height(10)
          .justifyContent(FlexAlign.Center)
          .borderRadius({ bottomLeft: 4, bottomRight: 4 })
          .backgroundColor(this.tabIndex1 === i ? '#E64A19' : '#8D6E63')
        }
        .layoutWeight(1)
        .scale({ x: this.tabIndex1 === i ? 1.07 : 1, y: this.tabIndex1 === i ? 1.07 : 1 })
        .animation({ duration: 180 })
        .onClick(() => {
          this.tabIndex1 = i
        })
      }, (t: string) => t)
    }
    .width('100%')
    .padding({ left: 8, right: 8 })
  }
  .width('100%')
  .padding({ top: 6, bottom: 6 })
  .backgroundColor('#3E2723')
  .shadow({ radius: 10, color: 'rgba(0,0,0,0.25)', offsetY: -3 })
}

底部Tab导航是本应用最具创意的UI组件。每个Tab项由三条宽度递增的Row堆叠而成:最上层是图标层(选中时宽度100%,未选中56%),中间是文字层(固定76%宽度),最底层是指示层(100%宽度,选中显示●,未选中显示·)。选中Tab的三条色带从灰色系切换为橙色系(#FF8A65→#FF7043→#E64A19),并通过scale属性放大1.07倍,配合180毫秒的animation动画实现平滑过渡效果。

七、build方法:主页面编排与弹窗绑定

在这里插入图片描述

build方法是每个ArkTS组件的核心入口,它描述了组件的UI结构树。主组件的build方法通过条件渲染编排六个Tab子组件,并通过bindSheet和bindContentCover挂载五个弹窗。

build() {
  Column() {
    this.header207()
    Scroll() {
      Column({ space: 12 }) {
        if (this.tabIndex1 === 0) {
          LiveTab207({
            micOn: true,
            camOn: true,
            lampOn: true,
            fossils: this.fossils,
            guides: this.guides,
            digSteps: this.digSteps,
            barrages: this.barrages,
            onStep: (i: number) => {
              this.digSteps = this.digSteps.map((s: DigStep207, si: number) => {
                if (si === i) {
                  return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
                }
                return s
              })
            },
            onJoin: () => {
              this.showJoinSheet = true
            }
          })
        }
        if (this.tabIndex1 === 1) {
          SampleTab207({
            fossils: this.fossils,
            onAdd: () => {
              this.showSampleSheet = true
            },
            onDetail: (i: number) => {
              this.detailIndex = i
              this.showDetailDialog = true
            },
            onEdit: (i: number) => {
              this.editIndex = i
              this.editName = this.fossils[i].name
              this.editIntegrity = this.fossils[i].integrity
              this.editDepth = this.fossils[i].depth
              this.editTop = this.fossils[i].starred
              this.showEditSheet = true
            },
            onDel: (i: number) => {
              this.delIndex = i
              this.showDelDialog = true
            },
            onStar: (i: number) => {
              this.fossils = this.fossils.map((f: Fossil207, fi: number) => {
                if (fi === i) {
                  return { id: f.id, name: f.name, era: f.era, integrity: f.integrity, depth: f.depth, state: f.state, watchers: f.watchers, starred: !f.starred }
                }
                return f
              })
            }
          })
        }
        // ... 其他Tab
      }
      .width('100%')
      .padding(14)
    }
    .layoutWeight(1)
    .align(Alignment.Top)
    this.tabBar207()
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#EFEBE9')
  .bindSheet($$this.showJoinSheet, this.joinSheet207(), {
    height: 620,
    dragBar: true,
    showClose: false,
    backgroundColor: '#FFFFFF'
  })
  .bindSheet($$this.showSampleSheet, this.sampleSheet207(), {
    height: 600,
    dragBar: true,
    showClose: false,
    backgroundColor: '#FFFFFF'
  })
  .bindSheet($$this.showEditSheet, this.editSheet207(), {
    height: 560,
    dragBar: true,
    showClose: false,
    backgroundColor: '#FFFFFF'
  })
  .bindContentCover($$this.showDelDialog, this.delDialog207(), {
  })
  .bindContentCover($$this.showDetailDialog, this.detailDialog207(), {
  })
}

build方法的结构清晰分为三层:最外层Column包裹头部、Scroll内容区和底部Tab;Scroll内部通过if (this.tabIndex1 === N)条件渲染六个子Tab组件;最外层Column链式调用bindSheet和bindContentCover挂载弹窗。

值得深入分析的是回调函数的设计模式。以onStep回调为例,当用户在LiveTab207中点击某个野采步骤时,回调被触发,主组件通过this.digSteps.map(...)创建一个新数组——将对应索引的步骤的done字段取反,其余步骤保持不变。onStar回调同理,通过map方法创建新数组来翻转指定化石的starred字段。onEdit回调在打开编辑弹窗前,先将目标化石的当前数据预填充到编辑表单状态中,实现"编辑回填"功能。

$$是ArkTS中的双向绑定语法糖,它将@State布尔变量与bindSheet/bindContentCover的显示状态双向关联。当布尔变量变为true时弹窗弹出,当用户拖拽关闭弹窗时布尔变量自动变为false,无需手动编写关闭逻辑。这一语法极大简化了弹窗管理的代码量。

八、预约连线弹窗:抽屉式表单交互

预约连线敲化石弹窗通过bindSheet以底部抽屉的形式弹出,高度620像素,支持拖拽关闭。弹窗内部包含目标层位选择、自带工具选择、连线时长调节、夜采头灯开关、护目镜保障开关和费用计算等完整表单元素。

@Builder
joinSheet207() {
  Column({ space: 16 }) {
    Row({ space: 10 }) {
      Column().width(4).height(30).borderRadius(2).backgroundColor('#FF7043')
      Column({ space: 2 }) {
        Text('预约连线敲化石').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Text('和锤王老周同层位开凿 · 出化石可留名').fontSize(10).fontColor('#8D6E63')
      }
      .alignItems(HorizontalAlign.Start)
      Text('').layoutWeight(1)
      Column() {
        Text('✕').fontSize(14).fontColor('#8D6E63')
      }
      .width(30)
      .height(30)
      .borderRadius(15)
      .backgroundColor('#EFEBE9')
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.showJoinSheet = false
      })
    }
    .width('100%')

    Scroll() {
      Column({ space: 16 }) {
        Column({ space: 8 }) {
          Text('目标层位').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.strata, (s: Stratum207, i: number) => {
              Text(s.name + ' · ' + s.fossil)
                .fontSize(11)
                .fontColor(this.joinStrata === i ? '#FFFFFF' : '#8D6E63')
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .borderRadius(16)
                .backgroundColor(this.joinStrata === i ? '#FF7043' : '#EFEBE9')
                .margin(4)
                .onClick(() => {
                  this.joinStrata = i
                })
            }, (s: Stratum207) => ('j' + s.id))
          }
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')

弹窗头部使用了橙色竖条+标题+副标题的标准布局,右上角圆形关闭按钮通过设置showJoinSheet = false关闭弹窗。目标层位选择区使用Flex容器配合FlexWrap.Wrap实现自动换行的标签组布局,每个标签显示地层名称和代表化石,选中时背景色变为橙色#FF7043、文字变白,未选中时背景为浅棕色#EFEBE9。点击标签设置joinStrata索引值,由声明式框架自动驱动UI更新。

        Row({ space: 14 }) {
          Column() {
            Text('-').fontSize(16).fontColor('#E64A19')
          }
          .width(34)
          .height(34)
          .borderRadius(17)
          .backgroundColor('#FBE9E7')
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            if (this.joinMins > 15) {
              this.joinMins -= 15
            }
          })
          Column({ space: 2 }) {
            Text('连线时长 ' + this.joinMins + ' 分钟').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E64A19')
            Text('超时自动转围观模式').fontSize(9).fontColor('#90A4AE')
          }
          .alignItems(HorizontalAlign.Start)
          Column() {
            Text('+').fontSize(16).fontColor('#E64A19')
          }
          .width(34)
          .height(34)
          .borderRadius(17)
          .backgroundColor('#FBE9E7')
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            if (this.joinMins < 120) {
              this.joinMins += 15
            }
          })
          Text('').layoutWeight(1)
        }
        .width('100%')

连线时长调节器采用减号按钮+显示区+加号按钮的三段式布局,每次步进15分钟,范围限制在15到120分钟之间。减号按钮的onClick中通过if (this.joinMins > 15)做下界保护,加号按钮通过if (this.joinMins < 120)做上界保护。这种数值步进器的交互模式在HarmonyOS ArkTS中非常常见,通过条件判断实现安全的状态变更。

费用计算区根据连线时长和附加选项动态计算价格:¥ ' + (this.joinMins * 3 + (this.joinGoggle ? 20 : 0)),连线每分钟3元,护目镜保障加收20元。这个表达式直接内联在Text的内容中,当joinMins或joinGoggle状态变化时,框架自动重新计算并更新显示。

九、标本上传弹窗与编辑弹窗

标本上传弹窗和编辑弹窗在结构上高度相似,但编辑弹窗额外携带了"map回写"逻辑——保存修改时通过map方法将编辑后的数据写回原数组。

@Builder
editSheet207() {
  Column({ space: 16 }) {
    Row({ space: 10 }) {
      Column().width(4).height(30).borderRadius(2).backgroundColor('#A1887F')
      Column({ space: 2 }) {
        Text('编辑标本档案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Text('修正完好度与层位深度').fontSize(10).fontColor('#8D6E63')
      }
      .alignItems(HorizontalAlign.Start)
      // ... 关闭按钮
    }
    .width('100%')

    Scroll() {
      Column({ space: 16 }) {
        Column({ space: 8 }) {
          Text('标本名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          TextInput({ placeholder: '输入新名称', text: this.editName })
            .fontSize(13)
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#EFEBE9')
            .onChange((v: string) => {
              this.editName = v
            })
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')

        // ... 完好度步进器、深度步进器、置顶开关

        Button() {
          Text('保存修改').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .height(48)
        .borderRadius(24)
        .backgroundColor('#A1887F')
        .onClick(() => {
          this.fossils = this.fossils.map((f: Fossil207, fi: number) => {
            if (fi === this.editIndex) {
              return {
                id: f.id,
                name: this.editName === '' ? f.name : this.editName,
                era: f.era,
                integrity: this.editIntegrity,
                depth: this.editDepth,
                state: f.state,
                watchers: f.watchers,
                starred: this.editTop
              }
            }
            return f
          })
          this.showEditSheet = false
        })
      }
    }
  }
}

编辑弹窗的保存逻辑是整个应用状态管理的核心体现。点击"保存修改"按钮后,执行this.fossils = this.fossils.map(...)——map方法遍历化石数组,当索引等于editIndex时,用编辑表单中的值(editName、editIntegrity、editDepth、editTop)替换原始字段,同时保留未编辑的字段(id、era、state、watchers)。如果editName为空字符串,则保留原始name值。保存完成后关闭弹窗this.showEditSheet = false

map方法返回的是新数组而非修改原数组,这是ArkTS状态管理的关键要求——@State变量的赋值必须产生新的引用才能触发UI更新。直接修改数组元素的属性(如this.fossils[0].name = ‘新名称’)不会触发重新渲染,因为数组引用未变。

十、删除弹窗与详情弹窗

删除弹窗和详情弹窗都通过bindContentCover以全屏遮罩形式呈现。删除弹窗是一个居中的确认对话框,详情弹窗则是一个信息丰富的标本档案卡。

@Builder
delDialog207() {
  Column({ space: 14 }) {
    Column() {
      Text('🪨').fontSize(34)
    }
    .width(64)
    .height(64)
    .borderRadius(32)
    .backgroundColor('#EFEBE9')
    .justifyContent(FlexAlign.Center)

    Text('移除这块标本?').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
    Text('「' + (this.delIndex >= 0 && this.delIndex < this.fossils.length ? this.fossils[this.delIndex].name : '') + '」将从标本墙下架,出土信息不再展示').fontSize(11).fontColor('#8D6E63').textAlign(TextAlign.Center)

    Row({ space: 10 }) {
      Column({ space: 2 }) {
        Text('保留野采日志').fontSize(12).fontColor('#5D4037')
        Text('仅删除标本卡').fontSize(9).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      Text('').layoutWeight(1)
      Row() {
        Circle().width(16).height(16).fill('#FFFFFF')
      }
      .width(44)
      .height(24)
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)
      .backgroundColor(this.delKeepLog ? '#5D4037' : '#BDBDBD')
      .onClick(() => {
        this.delKeepLog = !this.delKeepLog
      })
    }
    .width('100%')
    .padding(10)
    .borderRadius(10)
    .backgroundColor('#EFEBE9')

    Row({ space: 10 }) {
      Button() {
        Text('再想想').fontSize(14).fontColor('#5D4037').fontWeight(FontWeight.Bold)
      }
      .layoutWeight(1)
      .height(44)
      .borderRadius(22)
      .backgroundColor('#EFEBE9')
      .onClick(() => {
        this.showDelDialog = false
      })

      Button() {
        Text('确认移除').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .layoutWeight(1)
      .height(44)
      .borderRadius(22)
      .backgroundColor('#E64A19')
      .onClick(() => {
        this.fossils = this.fossils.filter((f: Fossil207, fi: number) => fi !== this.delIndex)
        this.showDelDialog = false
      })
    }
    .width('100%')
  }
  .width('82%')
  .padding(20)
  .borderRadius(20)
  .backgroundColor('#FFFFFF')
  .alignItems(HorizontalAlign.Center)
}

删除弹窗的确认按钮通过this.fossils.filter(...)创建新数组——过滤掉索引等于delIndex的元素,实现删除操作。filter方法同样返回新数组引用,触发@State的变更检测。弹窗中还包含一个"保留野采日志"的开关,允许用户选择仅删除标本卡但保留挖掘日志记录。

@Builder
detailDialog207() {
  Column({ space: 0 }) {
    Scroll() {
      Column({ space: 0 }) {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Column() {
              Text('🪨').fontSize(30)
            }
            .width(56)
            .height(56)
            .borderRadius(28)
            .backgroundColor('#FFFFFF')
            .justifyContent(FlexAlign.Center)
            Column({ space: 3 }) {
              Text(this.fossils[this.detailIndex].name).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text(this.fossils[this.detailIndex].era + ' · 深度 ' + this.fossils[this.detailIndex].depth + ' 米').fontSize(11).fontColor('#EFEBE9')
            }
            .alignItems(HorizontalAlign.Start)
            // ... 关闭按钮
          }
          .width('100%')
        }
        .width('100%')
        .padding(18)
        .linearGradient({ angle: 135, colors: [['#FF7043', 0], ['#5D4037', 1]] })

        Column({ space: 14 }) {
          Row({ space: 8 }) {
            Column({ space: 2 }) {
              Text(this.fossils[this.detailIndex].integrity + '%').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
              Text('轮廓完好度').fontSize(9).fontColor('#8D6E63')
            }
            .layoutWeight(1)
            // ... 围观人次、当前状态
          }
          .width('100%')

          Column({ space: 8 }) {
            Text('近 7 日围挖掘位走势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
            Row({ space: 6 }) {
              ForEach(this.digDays, (d: DigDay207, i: number) => {
                Column({ space: 4 }) {
                  Column()
                    .width(16)
                    .height(d.finds * 5)
                    .borderRadius({ topLeft: 4, topRight: 4 })
                    .linearGradient({ angle: 180, colors: [['#FF8A65', 0], ['#E64A19', 1]] })
                  Text(d.day).fontSize(8).fontColor('#8D6E63')
                }
                .alignItems(HorizontalAlign.Center)
                .layoutWeight(1)
              }, (d: DigDay207) => ('d' + d.day))
            }
            .width('100%')
            .alignItems(VerticalAlign.Bottom)
            .height(90)
          }

详情弹窗的头部使用橙棕渐变背景展示化石名称和年代信息。主体区域包含三个统计卡(完好度、围观人次、状态)和一个柱状图——通过ForEach遍历digDays数据,每个柱子的高度由d.finds * 5计算得出,使用linearGradient实现从浅橙到深橙的纵向渐变。这种通过layoutWeight和height映射实现柱状图的方式是HarmonyOS ArkTS中轻量级数据可视化的典型手法,无需引入第三方图表库即可实现丰富的图表效果。

十一、直播Tab:四机位网格与弹幕系统

LiveTab207是直播Tab的核心子组件,通过@Prop接收化石、向导、步骤和弹幕数据,通过回调函数将用户操作回传给父组件。

@Component
struct LiveTab207 {
  @Prop micOn: boolean = true
  @Prop camOn: boolean = true
  @Prop lampOn: boolean = true
  @State localMic: boolean = true
  @State localCam: boolean = true
  @State localLamp: boolean = true
  @Prop fossils: Fossil207[] = []
  @Prop guides: Guide207[] = []
  @Prop digSteps: DigStep207[] = []
  @Prop barrages: Barrage207[] = []
  onStep: (i: number) => void = () => {
  }
  onJoin: () => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Grid() {
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('⛏️').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('主镜 · 岩壁敲击').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('锤王老周 · 泥盆系').fontSize(9).fontColor('#FFCCBC')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text(strikingFossilCount207(this.fossils) + ' 块敲击中').fontSize(9).fontColor('#FFCCBC')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#5D4037', 0], ['#3E2723', 1]] })
          .onClick(() => {
            this.onJoin()
          })
        }
        // ... 显微特写位、淘洗盘位、我的野采位
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .height(240)
      .width('100%')

四机位网格通过Grid组件的columnsTemplate('1fr 1fr')rowsTemplate('1fr 1fr')配置为2×2的等分布局,高度240像素。四个GridItem分别对应主镜(岩壁敲击,棕褐色渐变)、显微特写位(琥珀内含物40x,橙红色渐变)、淘洗盘位(琥珀浮选,绿色渐变)和我的野采位(用户自己的机位,灰色背景)。主镜机位点击触发onJoin()回调弹出预约弹窗。

工具条行包含对讲、镜头、头灯和连线四个功能按钮,前三个按钮通过localMiclocalCamlocalLamp三个@State布尔变量控制开关状态,点击时翻转对应状态值,背景色随之变化。连线按钮始终为橙色#FF7043,点击触发onJoin()回调。

      // 野采六步
      Column({ space: 10 }) {
        Text('野采六步法').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.digSteps, (s: DigStep207, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text(s.done ? '✓' : (i + 1) + '').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width(24)
            .height(24)
            .borderRadius(12)
            .backgroundColor(s.done ? '#7CB342' : '#BCAAA4')
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Text(s.title).fontSize(12).fontColor(s.done ? '#3E2723' : '#8D6E63').fontWeight(FontWeight.Bold)
              Text(s.tip).fontSize(9).fontColor('#BDBDBD')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(s.done ? '已完成' : '进行中').fontSize(9).fontColor(s.done ? '#7CB342' : '#FF7043')
          }
          .width('100%')
          .padding(10)
          .borderRadius(10)
          .backgroundColor(s.done ? '#F1F8E9' : '#FFFFFF')
          .onClick(() => {
            this.onStep(i)
          })
        }, (s: DigStep207) => ('s' + s.id))
      }

野采六步法区域通过ForEach渲染六个步骤项,每个步骤左侧的圆形指示器在完成时显示绿色对勾✓和绿色背景#7CB342,未完成时显示序号和灰色背景#BCAAA4。整行背景在完成时为浅绿色#F1F8E9。点击任意步骤触发onStep(i)回调,父组件通过map方法翻转该步骤的done字段。

弹幕区域通过ForEach渲染barrages数组,每条弹幕显示序号和文本内容,背景色根据奇偶索引交替使用浅棕色#EFEBE9和浅橙色#FBE9E7,实现斑马条纹效果。

十二、标本墙Tab:卡片列表与堆叠条形图

SampleTab207负责展示化石标本的完整卡片列表,包含统计卡、上传入口、年代占比堆叠条和标本卡片。

@Component
struct SampleTab207 {
  @Prop fossils: Fossil207[] = []
  onAdd: () => void = () => {
  }
  onDetail: (i: number) => void = () => {
  }
  onEdit: (i: number) => void = () => {
  }
  onDel: (i: number) => void = () => {
  }
  onStar: (i: number) => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.fossils.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          Text('馆藏标本').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#FBE9E7')
        Column({ space: 2 }) {
          Text(starredFossilCount207(this.fossils) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          Text('精选标记').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text(strikingFossilCount207(this.fossils) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF7043')
          Text('敲击中').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
      }
      .width('100%')

三个统计卡分别显示馆藏标本总数、精选标记数和敲击中数量,通过调用工具函数starredFossilCount207strikingFossilCount207实时计算。当 fossils数组发生变化(增删改)时,@Prop传递的数据自动更新,统计数字同步刷新。

      // 年代占比堆叠条
      Column({ space: 8 }) {
        Text('馆藏年代构成').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row() {
          ForEach(eraCounts207(this.fossils), (c: EraCount207) => {
            Row() {
              Text(c.label + ' ' + c.count).fontSize(8).fontColor('#FFFFFF')
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .backgroundColor(c.color)
          }, (c: EraCount207) => (c.label + c.count))
        }
        .width('100%')
        .height(22)
        .borderRadius(11)
        .clip(true)
        Row({ space: 10 }) {
          ForEach(eraCounts207(this.fossils), (c: EraCount207) => {
            Row({ space: 4 }) {
              Column().width(8).height(8).borderRadius(4).backgroundColor(c.color)
              Text(c.label).fontSize(9).fontColor('#8D6E63')
            }
          }, (c: EraCount207) => ('lg' + c.label))
        }
        .width('100%')
      }

年代占比堆叠条是一个创新的轻量级图表组件。通过eraCounts207(this.fossils)获取年代统计数据数组,每个年代对应的Row在Flex容器中按比例占据宽度,背景色为年代代表色,内部显示年代名称和数量。整条高度22像素,圆角11像素,配合clip(true)裁剪溢出部分。下方还有图例行,显示每个年代的色块和标签。

十三、地层库Tab与工具箱Tab

地层库Tab展示了地层年代尺、挖掘热度横条和出土量柱状图,工具箱Tab展示了工具列表和使用频次柱状图。

@Component
struct StrataTab207 {
  @Prop strata: Stratum207[] = []
  @Prop fossils: Fossil207[] = []
  @Prop digDays: DigDay207[] = []

  build() {
    Column({ space: 12 }) {
      // 地层年代尺
      Column({ space: 6 }) {
        Text('地层年代尺(单位:亿年前)').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.strata, (s: Stratum207) => {
          Row({ space: 8 }) {
            Column().width(10).height(26).borderRadius(3).backgroundColor(s.color)
            Column({ space: 2 }) {
              Text(s.name).fontSize(11).fontColor('#3E2723').fontWeight(FontWeight.Bold)
              Text('代表化石:' + s.fossil).fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(s.age + ' 亿年前').fontSize(9).fontColor('#BDBDBD')
          }
          .width('100%')
          .padding(8)
          .borderRadius(8)
          .backgroundColor('#FAFAFA')
        }, (s: Stratum207) => ('st' + s.id))
      }

      // 挖掘热度横条
      Column({ space: 10 }) {
        Text('各地层挖掘热度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.strata, (s: Stratum207) => {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(s.name).fontSize(10).fontColor('#5D4037').width(88)
              Row() {
                Row()
                  .width(s.heat + '%')
                  .height(10)
                  .borderRadius(5)
                  .linearGradient({ angle: 0, colors: [[s.color, 0], ['#FF7043', 1]] })
              }
              .layoutWeight(1)
              .height(10)
              .borderRadius(5)
              .backgroundColor('#EFEBE9')
              Text(s.heat + '').fontSize(9).fontColor('#E64A19').fontWeight(FontWeight.Bold)
            }
            .width('100%')
          }
        })
      }

地层年代尺通过ForEach遍历strata数组,每行显示地层色条、名称、代表化石和年龄。挖掘热度横条则通过Row嵌套实现进度条效果——外层Row为灰色轨道,内层Row的宽度为s.heat + '%',背景使用从地层色到橙色的横向渐变。这种进度条的实现在HarmonyOS ArkTS中非常简洁,不需要ProgressBar组件即可自定义实现。

工具箱Tab的ToolTab207组件包含工具架列表和近7日使用频次柱状图。工具列表每项显示工具图标、名称、库存和累计借用次数,右侧的状态标签根据borrowed字段和borrowIndex状态显示"已借出"、“占用中"或"可借”。使用频次柱状图与详情弹窗中的柱状图实现方式一致——通过ForEach遍历toolUses数组,每个柱子高度为u.uses * 2,使用棕色系纵向渐变。

十四、向导团Tab与个人中心Tab

向导团Tab展示在线向导列表和人气排行榜,个人中心Tab展示用户信息卡、出土量柱状图、野采日志和设置项。

@Component
struct GuideTab207 {
  @Prop guides: Guide207[] = []
  @State followIndex: number = -1
  onJoin: () => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Row({ space: 10 }) {
          Column() {
            Text('🧭').fontSize(24)
          }
          .width(48)
          .height(48)
          .borderRadius(24)
          .backgroundColor('#FBE9E7')
          .justifyContent(FlexAlign.Center)
          Column({ space: 3 }) {
            Text('在线向导 ' + onlineGuideCount207(this.guides) + ' / ' + this.guides.length).fontSize(14).fontColor('#3E2723').fontWeight(FontWeight.Bold)
            Text('连线敲化石 · 同层位带掘').fontSize(10).fontColor('#8D6E63')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Column() {
            Text('去连线').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .padding({ left: 12, right: 12, top: 7, bottom: 7 })
          .borderRadius(14)
          .backgroundColor('#FF7043')
          .onClick(() => {
            this.onJoin()
          })
        }
        .width('100%')
      }

向导团Tab的头部显示在线向导数量——通过onlineGuideCount207(this.guides)实时计算在线人数,分母为this.guides.length总数。人气榜列表中每个向导的进度条宽度通过(g.heat / maxGuideHeat207(this.guides) * 100) + '%'计算,以最高人气值为基准做归一化。前三名排名序号显示为橙色#E64A19,之后为灰色。头像背景色通过eraColor207(eraTags207[i % eraTags207.length])循环取年代色,增加了视觉多样性。

@Component
struct MineTab207 {
  @Prop fossils: Fossil207[] = []
  @Prop digLogs: DigLog207[] = []
  @Prop digDays: DigDay207[] = []
  @State nightMode: boolean = false
  @State notifyHit: boolean = true
  @State autoLog: boolean = false

  build() {
    Column({ space: 12 }) {
      // 个人卡
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Column() {
            Text('掘').fontSize(20).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width(54)
          .height(54)
          .borderRadius(27)
          .backgroundColor('rgba(255,255,255,0.25)')
          .justifyContent(FlexAlign.Center)
          Column({ space: 3 }) {
            Text('掘友 · 岩芯少年').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('野采 38 次 · 出化石 21 块 · 命中率 55%').fontSize(10).fontColor('#FFCCBC')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('⛏️').fontSize(22)
        }
        .width('100%')

        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text(starredFossilCount207(this.fossils) + '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFF3E0')
            Text('精选标本').fontSize(9).fontColor('#FFCCBC')
          }
          .layoutWeight(1)
          // ... 贡献积分、掘友等级
        }
      }
      .width('100%')
      .padding(16)
      .borderRadius(16)
      .linearGradient({ angle: 135, colors: [['#FF7043', 0], ['#3E2723', 1]] })

个人中心Tab的个人卡使用橙棕渐变背景,头像为一个半透明白色圆形显示"掘"字,下方展示精选标本数(实时调用starredFossilCount207)、贡献积分和掘友等级。设置区域包含夜采模式、热层开播提醒和自动记野采日志三个开关项,每个开关通过@State布尔变量控制Circle圆形滑块的位置和背景色。

十五、应用数据流架构总览

渲染错误: Mermaid 渲染失败: Parse error on line 2: ... A[主组件 Index207] -->|@Prop 只读数据| B[LiveT -----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

十六、技术点对比总结

技术维度实现方式核心API/装饰器应用场景
状态管理@State修饰主组件可变数据@Statefossils数组、弹窗开关、表单状态
数据传递@Prop只读传递给子组件@PropLiveTab207接收fossils/guides/barrages
双向绑定$$语法糖绑定弹窗显示状态$$this.showSheetbindSheet/bindContentCover开关
组件通信回调函数从子到父回传箭头函数onStep/onStar/onEdit/onDel/onDetail
不可变更新map/filter创建新数组引用Array.map/filter编辑回写、删除过滤、状态翻转
条件渲染if判断tabIndex渲染对应Tabif语句六个Tab页面切换
列表渲染ForEach遍历数组生成UIForEach化石卡片、弹幕、步骤、向导列表
弹窗管理bindSheet底部抽屉bindSheet预约/上传/编辑弹窗(高度可配)
全屏遮罩bindContentCover居中弹窗bindContentCover删除确认/标本详情弹窗
背景渐变linearGradient线性渐变linearGradient头部、个人卡、柱状图渐变
阴影效果shadow投影shadowTab栏上方阴影
缩放动画scale+animationscale/animationTab选中放大1.07倍
进度条Row嵌套+width百分比width(‘XX%’)挖掘热度、向导人气进度条
柱状图Column高度映射+ForEachheight(d.finds*5)出土量/使用频次/围挖掘位走势
堆叠条形图Row容器内ForEach按比例FlexWrap布局年代占比/染材占比堆叠条
色彩映射纯函数返回颜色字符串独立functionfossilStateColor/eraColor/digStateColor
统计聚合遍历数组计数for循环starredFossilCount/strikingFossilCount
图标渲染Text显示emojiText(‘⛏️’)工具/Tab/机位图标
圆角裁剪borderRadius+clipborderRadius/clip堆叠条圆角裁剪
表单输入TextInput+onChangeTextInput标本名称编辑/花色名称输入
步进器减号按钮+显示+加号按钮onClick条件判断时长/完好度/深度/浸染次数

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 场景:地质学家连线敲化石 / 岩层野采直播围观
// 配色:岩层棕 #5D4037 × 玛瑙橙 #FF7043 × 化石灰 #ECEFF1
// Tab 布局:底部「地层剖面」导航(每项堆叠 3 条地层色带,选中橙棕渐变)
// ============================================================

interface DigDay207 {
  day: string
  finds: number
}

interface Fossil207 {
  id: number
  name: string
  era: string
  integrity: number
  depth: number
  state: string
  watchers: number
  starred: boolean
}

interface Stratum207 {
  id: number
  name: string
  age: number
  color: string
  fossil: string
  heat: number
}

interface DigLog207 {
  id: number
  name: string
  era: string
  dayNum: number
  strikes: number
  weight: number
  state: string
  top: boolean
}

interface Guide207 {
  id: number
  name: string
  city: string
  years: number
  online: boolean
  heat: number
}

interface Barrage207 {
  id: number
  text: string
}

interface DigStep207 {
  id: number
  title: string
  tip: string
  done: boolean
}

interface EraCount207 {
  label: string
  count: number
  color: string
}

interface ToolUse207 {
  day: string
  uses: number
}

interface Tool207 {
  id: number
  name: string
  icon: string
  stock: number
  borrowed: boolean
  times: number
}

const eraTags207: string[] = ['寒武纪', '奥陶纪', '泥盆纪', '侏罗纪', '白垩纪']
const toolTags207: string[] = ['地质锤', '尖头凿', '软毛刷', '放大镜']

function fossilStateColor207(state: string): string {
  if (state === '敲击中') {
    return '#FF7043'
  }
  if (state === '热层待掘') {
    return '#FBC02D'
  }
  if (state === '已封装') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function digStateColor207(state: string): string {
  if (state === '重点层') {
    return '#E53935'
  }
  if (state === '出化石') {
    return '#FF7043'
  }
  if (state === '已完成') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function eraColor207(era: string): string {
  if (era === '寒武纪') {
    return '#8D6E63'
  }
  if (era === '奥陶纪') {
    return '#A1887F'
  }
  if (era === '泥盆纪') {
    return '#FF7043'
  }
  if (era === '侏罗纪') {
    return '#43A047'
  }
  return '#1E88E5'
}

function starredFossilCount207(fossils: Fossil207[]): number {
  let n: number = 0
  for (let i = 0; i < fossils.length; i++) {
    if (fossils[i].starred) {
      n++
    }
  }
  return n
}

function strikingFossilCount207(fossils: Fossil207[]): number {
  let n: number = 0
  for (let i = 0; i < fossils.length; i++) {
    if (fossils[i].state === '敲击中') {
      n++
    }
  }
  return n
}

function onlineGuideCount207(guides: Guide207[]): number {
  let n: number = 0
  for (let i = 0; i < guides.length; i++) {
    if (guides[i].online) {
      n++
    }
  }
  return n
}

function maxGuideHeat207(guides: Guide207[]): number {
  let m: number = 1
  for (let i = 0; i < guides.length; i++) {
    if (guides[i].heat > m) {
      m = guides[i].heat
    }
  }
  return m
}

function eraCounts207(fossils: Fossil207[]): EraCount207[] {
  const counts: EraCount207[] = []
  for (let i = 0; i < eraTags207.length; i++) {
    let n: number = 0
    for (let j = 0; j < fossils.length; j++) {
      if (fossils[j].era === eraTags207[i]) {
        n++
      }
    }
    counts.push({ label: eraTags207[i], count: n, color: ['#8D6E63', '#A1887F', '#FF7043', '#43A047', '#1E88E5'][i] })
  }
  return counts
}

// ================= 主页面 =================

@Entry
@Component
struct Index207 {
  @State tabIndex1: number = 0

  // 弹框开关
  @State showJoinSheet: boolean = false
  @State showSampleSheet: boolean = false
  @State showEditSheet: boolean = false
  @State showDelDialog: boolean = false
  @State showDetailDialog: boolean = false

  // 预约连线敲化石表单
  @State joinStrata: number = 3
  @State joinTool: number = 0
  @State joinMins: number = 45
  @State joinLamp: boolean = true
  @State joinGoggle: boolean = false

  // 新增标本记录表单
  @State sampleName: string = ''
  @State sampleEra: number = 3
  @State sampleIntegrity: number = 80
  @State sampleDepth: number = 12
  @State samplePublic: boolean = true

  // 编辑表单
  @State editIndex: number = -1
  @State editName: string = ''
  @State editIntegrity: number = 80
  @State editDepth: number = 12
  @State editTop: boolean = false

  // 删除
  @State delIndex: number = -1
  @State delKeepLog: boolean = true

  // 详情
  @State detailIndex: number = 0

  // 数据
  @State fossils: Fossil207[] = [
    { id: 1, name: '震旦角石', era: '奥陶纪', integrity: 92, depth: 8, state: '敲击中', watchers: 3210, starred: true },
    { id: 2, name: '莱得利基虫', era: '寒武纪', integrity: 78, depth: 15, state: '热层待掘', watchers: 1580, starred: false },
    { id: 3, name: '狼鳍鱼', era: '白垩纪', integrity: 88, depth: 6, state: '已封装', watchers: 2450, starred: true },
    { id: 4, name: '拟蜉蝣', era: '侏罗纪', integrity: 65, depth: 11, state: '热层待掘', watchers: 990, starred: false },
    { id: 5, name: '菊石切片', era: '泥盆纪', integrity: 95, depth: 4, state: '敲击中', watchers: 3860, starred: true },
    { id: 6, name: '恐龙蛋皮', era: '白垩纪', integrity: 60, depth: 18, state: '热层待掘', watchers: 4120, starred: false },
    { id: 7, name: '海百合冠', era: '泥盆纪', integrity: 85, depth: 13, state: '已封装', watchers: 2760, starred: true },
    { id: 8, name: '王冠虫', era: '志留纪', integrity: 72, depth: 9, state: '热层待掘', watchers: 1340, starred: false },
    { id: 9, name: '琥珀蚊', era: '白垩纪', integrity: 98, depth: 2, state: '已封装', watchers: 5230, starred: true },
    { id: 10, name: '枝脉蕨叶', era: '侏罗纪', integrity: 70, depth: 7, state: '热层待掘', watchers: 1120, starred: false }
  ]

  @State strata: Stratum207[] = [
    { id: 1, name: '寒武系灰岩', age: 5.4, color: '#8D6E63', fossil: '三叶虫', heat: 92 },
    { id: 2, name: '奥陶系白云岩', age: 4.8, color: '#A1887F', fossil: '角石', heat: 88 },
    { id: 3, name: '泥盆系砂岩', age: 3.9, color: '#FF7043', fossil: '菊石', heat: 95 },
    { id: 4, name: '侏罗系页岩', age: 1.9, color: '#43A047', fossil: '狼鳍鱼', heat: 86 },
    { id: 5, name: '白垩系凝灰岩', age: 1.0, color: '#1E88E5', fossil: '琥珀', heat: 90 }
  ]

  @State digLogs: DigLog207[] = [
    { id: 1, name: '震旦角石围岩剥离', era: '奥陶纪', dayNum: 3, strikes: 42, weight: 260, state: '重点层', top: true },
    { id: 2, name: '莱得利基虫层定位', era: '寒武纪', dayNum: 1, strikes: 18, weight: 120, state: '出化石', top: false },
    { id: 3, name: '狼鳍鱼页岩劈开', era: '白垩纪', dayNum: 5, strikes: 65, weight: 180, state: '出化石', top: false },
    { id: 4, name: '菊石结核敲取', era: '泥盆纪', dayNum: 2, strikes: 38, weight: 420, state: '重点层', top: false },
    { id: 5, name: '琥珀砂层淘洗', era: '白垩纪', dayNum: 6, strikes: 8, weight: 12, state: '已完成', top: false },
    { id: 6, name: '海百合冠修样', era: '泥盆纪', dayNum: 4, strikes: 25, weight: 310, state: '已完成', top: false },
    { id: 7, name: '恐龙蛋皮碎片收集', era: '白垩纪', dayNum: 7, strikes: 30, weight: 95, state: '重点层', top: false },
    { id: 8, name: '枝脉蕨板劈理观察', era: '侏罗纪', dayNum: 1, strikes: 12, weight: 140, state: '已完成', top: false }
  ]

  @State guides: Guide207[] = [
    { id: 1, name: '锤王老周', city: '桂林', years: 18, online: true, heat: 97 },
    { id: 2, name: '岩层阿姐', city: '昆明', years: 11, online: true, heat: 89 },
    { id: 3, name: '化石猎人K', city: '赤峰', years: 9, online: false, heat: 84 },
    { id: 4, name: '菊石小哥', city: '六盘水', years: 7, online: true, heat: 78 },
    { id: 5, name: '三叶虫博士', city: '莱芜', years: 15, online: false, heat: 92 },
    { id: 6, name: '淘砂阿妹', city: '抚顺', years: 5, online: true, heat: 66 }
  ]

  @State digDays: DigDay207[] = [
    { day: '周一', finds: 4 },
    { day: '周二', finds: 7 },
    { day: '周三', finds: 5 },
    { day: '周四', finds: 9 },
    { day: '周五', finds: 11 },
    { day: '周六', finds: 14 },
    { day: '周日', finds: 12 }
  ]

  @State toolUses: ToolUse207[] = [
    { day: 'D1', uses: 12 },
    { day: 'D2', uses: 18 },
    { day: 'D3', uses: 15 },
    { day: 'D4', uses: 22 },
    { day: 'D5', uses: 26 },
    { day: 'D6', uses: 31 },
    { day: 'D7', uses: 28 }
  ]

  @State tools: Tool207[] = [
    { id: 1, name: '地质锤', icon: '🔨', stock: 3, borrowed: false, times: 128 },
    { id: 2, name: '尖头凿', icon: '⛏️', stock: 2, borrowed: true, times: 96 },
    { id: 3, name: '软毛刷', icon: '🧹', stock: 6, borrowed: false, times: 152 },
    { id: 4, name: '放大镜', icon: '🔍', stock: 4, borrowed: false, times: 88 },
    { id: 5, name: '淘洗盘', icon: '🥘', stock: 2, borrowed: true, times: 64 },
    { id: 6, name: '护目镜', icon: '🥽', stock: 5, borrowed: false, times: 110 },
    { id: 7, name: '标本袋', icon: '袋', stock: 9, borrowed: false, times: 140 },
    { id: 8, name: '记号笔', icon: '🖊️', stock: 7, borrowed: false, times: 75 }
  ]

  @State barrages: Barrage207[] = [
    { id: 1, text: '这一锤下去直接出轮廓了!' },
    { id: 2, text: '角石的围岩剥离看得好解压' },
    { id: 3, text: '第一次云敲化石,比开盲盒刺激' },
    { id: 4, text: '琥珀那格显微镜位太清楚了' },
    { id: 5, text: '锤王老周的手稳得像机器' },
    { id: 6, text: '敲击 +1,我来记 strikes' },
    { id: 7, text: '这层页岩劈理漂亮' },
    { id: 8, text: '狼鳍鱼的尾巴都完整' }
  ]

  @State digSteps: DigStep207[] = [
    { id: 1, title: '踏勘选点', tip: '看岩层走向 · 找化石富集带', done: true },
    { id: 2, title: '清表开槽', tip: '清掉浮土 · 沿层理开凿口', done: true },
    { id: 3, title: '顺层敲击', tip: '顺劈理落锤 · 切忌垂直硬砸', done: true },
    { id: 4, title: '修样取整', tip: '软刷清土 · 凿子修边', done: false },
    { id: 5, title: '淘洗分选', tip: '砂样过盘 · 琥珀浮选法', done: false },
    { id: 6, title: '封装编号', tip: '标本袋写号 · 拍照入档', done: false }
  ]

  tabs207: string[] = ['野采房', '标本墙', '地层库', '工具箱', '向导团', '我的']
  tabIcons207: string[] = ['⛏️', '🪨', '📚', '🧰', '🧭', '👤']

  // ---------- 头部(电商科考季风,无动画) ----------
  @Builder
  header207() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('岩语 · 云地质营').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('地质学家连线敲化石 · 岩层野采直播围观').fontSize(11).fontColor('#D7CCC8')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column({ space: 2 }) {
          Text('🪨').fontSize(20)
          Text('1,862').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('围挖掘位').fontSize(9).fontColor('#D7CCC8')
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

      Row({ space: 10 }) {
        Column().width(4).height(34).borderRadius(2).backgroundColor('#FF7043')
        Column({ space: 3 }) {
          Text('泥盆纪菊石热层已开凿').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('连线敲击抽标本命名权 · 围观送淘洗砂样').fontSize(10).fontColor('#EFEBE9')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('去连线 →').fontSize(11).fontColor('#5D4037').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 12, right: 12, top: 7, bottom: 7 })
        .borderRadius(14)
        .backgroundColor('#FBE9E7')
        .onClick(() => {
          this.showJoinSheet = true
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#4E342E')
    }
    .alignItems(HorizontalAlign.Start)
    .padding(14)
    .linearGradient({ angle: 140, colors: [['#6D4C41', 0], ['#3E2723', 1]] })
  }

  // ---------- 底部「地层剖面」tab ----------
  @Builder
  tabBar207() {
    Column({ space: 0 }) {
      Row({ space: 0 }) {
        ForEach(this.tabs207, (t: string, i: number) => {
          Column({ space: 2 }) {
            // 地层剖面:三条宽度递增色带(上窄下宽)
            Row() {
              Text(this.tabIcons207[i]).fontSize(12)
            }
            .width(this.tabIndex1 === i ? '100%' : '56%')
            .height(18)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ topLeft: 6, topRight: 6 })
            .backgroundColor(this.tabIndex1 === i ? '#FF8A65' : '#BCAAA4')

            Row() {
              Text(t).fontSize(9).fontColor(this.tabIndex1 === i ? '#FFFFFF' : '#FFFFFF').maxLines(1)
            }
            .width('76%')
            .height(16)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.tabIndex1 === i ? '#FF7043' : '#A1887F')

            Row() {
              Text(this.tabIndex1 === i ? '●' : '·').fontSize(8).fontColor(this.tabIndex1 === i ? '#FFF3E0' : '#D7CCC8')
            }
            .width('100%')
            .height(10)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ bottomLeft: 4, bottomRight: 4 })
            .backgroundColor(this.tabIndex1 === i ? '#E64A19' : '#8D6E63')
          }
          .layoutWeight(1)
          .padding({ top: 4, bottom: 4 })
          .scale({ x: this.tabIndex1 === i ? 1.07 : 1, y: this.tabIndex1 === i ? 1.07 : 1 })
          .animation({ duration: 180 })
          .onClick(() => {
            this.tabIndex1 = i
          })
        }, (t: string) => t)
      }
      .width('100%')
      .padding({ left: 8, right: 8 })
    }
    .width('100%')
    .padding({ top: 6, bottom: 6 })
    .backgroundColor('#3E2723')
    .shadow({ radius: 10, color: 'rgba(0,0,0,0.25)', offsetY: -3 })
  }

  build() {
    Column() {
      this.header207()
      Scroll() {
        Column({ space: 12 }) {
          if (this.tabIndex1 === 0) {
            LiveTab207({
              micOn: true,
              camOn: true,
              lampOn: true,
              fossils: this.fossils,
              guides: this.guides,
              digSteps: this.digSteps,
              barrages: this.barrages,
              onStep: (i: number) => {
                this.digSteps = this.digSteps.map((s: DigStep207, si: number) => {
                  if (si === i) {
                    return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
                  }
                  return s
                })
              },
              onJoin: () => {
                this.showJoinSheet = true
              }
            })
          }
          if (this.tabIndex1 === 1) {
            SampleTab207({
              fossils: this.fossils,
              onAdd: () => {
                this.showSampleSheet = true
              },
              onDetail: (i: number) => {
                this.detailIndex = i
                this.showDetailDialog = true
              },
              onEdit: (i: number) => {
                this.editIndex = i
                this.editName = this.fossils[i].name
                this.editIntegrity = this.fossils[i].integrity
                this.editDepth = this.fossils[i].depth
                this.editTop = this.fossils[i].starred
                this.showEditSheet = true
              },
              onDel: (i: number) => {
                this.delIndex = i
                this.showDelDialog = true
              },
              onStar: (i: number) => {
                this.fossils = this.fossils.map((f: Fossil207, fi: number) => {
                  if (fi === i) {
                    return { id: f.id, name: f.name, era: f.era, integrity: f.integrity, depth: f.depth, state: f.state, watchers: f.watchers, starred: !f.starred }
                  }
                  return f
                })
              }
            })
          }
          if (this.tabIndex1 === 2) {
            StrataTab207({ strata: this.strata, fossils: this.fossils, digDays: this.digDays })
          }
          if (this.tabIndex1 === 3) {
            ToolTab207({ tools: this.tools, toolUses: this.toolUses })
          }
          if (this.tabIndex1 === 4) {
            GuideTab207({
              guides: this.guides,
              onJoin: () => {
                this.showJoinSheet = true
              }
            })
          }
          if (this.tabIndex1 === 5) {
            MineTab207({ fossils: this.fossils, digLogs: this.digLogs, digDays: this.digDays })
          }
        }
        .width('100%')
        .padding(14)
      }
      .layoutWeight(1)
      .align(Alignment.Top)
      this.tabBar207()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#EFEBE9')
    .bindSheet($$this.showJoinSheet, this.joinSheet207(), {
      height: 620,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showSampleSheet, this.sampleSheet207(), {
      height: 600,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showEditSheet, this.editSheet207(), {
      height: 560,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindContentCover($$this.showDelDialog, this.delDialog207(), {
    })
    .bindContentCover($$this.showDetailDialog, this.detailDialog207(), {
    })
  }

  // ---------- 弹框1:预约连线敲化石(抽屉) ----------
  @Builder
  joinSheet207() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#FF7043')
        Column({ space: 2 }) {
          Text('预约连线敲化石').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Text('和锤王老周同层位开凿 · 出化石可留名').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#EFEBE9')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showJoinSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('目标层位').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(this.strata, (s: Stratum207, i: number) => {
                Text(s.name + ' · ' + s.fossil)
                  .fontSize(11)
                  .fontColor(this.joinStrata === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.joinStrata === i ? '#FF7043' : '#EFEBE9')
                  .margin(4)
                  .onClick(() => {
                    this.joinStrata = i
                  })
              }, (s: Stratum207) => ('j' + s.id))
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('自带工具').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(toolTags207, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.joinTool === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.joinTool === i ? '#5D4037' : '#EFEBE9')
                  .margin(4)
                  .onClick(() => {
                    this.joinTool = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#E64A19')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.joinMins > 15) {
                this.joinMins -= 15
              }
            })
            Column({ space: 2 }) {
              Text('连线时长 ' + this.joinMins + ' 分钟').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E64A19')
              Text('超时自动转围观模式').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#E64A19')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.joinMins < 120) {
                this.joinMins += 15
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('夜采头灯').fontSize(13).fontColor('#5D4037')
              Text('夜间层位连线必备').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.joinLamp ? '#FF7043' : '#BDBDBD')
            .onClick(() => {
              this.joinLamp = !this.joinLamp
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FBE9E7')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('护目镜保障').fontSize(13).fontColor('#5D4037')
              Text('飞溅岩屑全额保险').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.joinGoggle ? '#FF7043' : '#BDBDBD')
            .onClick(() => {
              this.joinGoggle = !this.joinGoggle
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FBE9E7')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('预计花费').fontSize(13).fontColor('#5D4037')
              Text('含工具租借与场地费').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Text('¥ ' + (this.joinMins * 3 + (this.joinGoggle ? 20 : 0))).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#EFEBE9')

          Button() {
            Text('确认预约连线').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#FF7043')
          .onClick(() => {
            this.showJoinSheet = false
          })

          Text('预约后 10 分钟内可免费改期 · 出化石归地层所有方').fontSize(9).fontColor('#BDBDBD')
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 460 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框2:上传标本记录(抽屉,concat 前插) ----------
  @Builder
  sampleSheet207() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#5D4037')
        Column({ space: 2 }) {
          Text('上传标本记录').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Text('晒出你的野采成果 · 自动生成档案卡').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#EFEBE9')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showSampleSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('标本名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
            TextInput({ placeholder: '例如:圆形菊石化石', text: this.sampleName })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#EFEBE9')
              .onChange((v: string) => {
                this.sampleName = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('所属年代').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(eraTags207, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.sampleEra === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.sampleEra === i ? eraColor207(tag) : '#EFEBE9')
                  .margin(4)
                  .onClick(() => {
                    this.sampleEra = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#E64A19')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.sampleIntegrity > 10) {
                this.sampleIntegrity -= 5
              }
            })
            Column({ space: 2 }) {
              Text('完好度 ' + this.sampleIntegrity + '%').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E64A19')
              Text('按轮廓完整程度估').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#E64A19')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.sampleIntegrity < 100) {
                this.sampleIntegrity += 5
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.sampleDepth > 1) {
                this.sampleDepth -= 1
              }
            })
            Column({ space: 2 }) {
              Text('出土深度 ' + this.sampleDepth + ' 米').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
              Text('记层位距地表距离').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.sampleDepth < 30) {
                this.sampleDepth += 1
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('公开到标本墙').fontSize(13).fontColor('#5D4037')
              Text('其他掘友可见你的战绩').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.samplePublic ? '#5D4037' : '#BDBDBD')
            .onClick(() => {
              this.samplePublic = !this.samplePublic
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#EFEBE9')

          Button() {
            Text('上传记录').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#5D4037')
          .onClick(() => {
            this.showSampleSheet = false
            this.sampleName = ''
          })
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 440 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框3:编辑标本(抽屉,map 回写) ----------
  @Builder
  editSheet207() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#A1887F')
        Column({ space: 2 }) {
          Text('编辑标本档案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Text('修正完好度与层位深度').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#EFEBE9')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showEditSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('标本名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#5D4037')
            TextInput({ placeholder: '输入新名称', text: this.editName })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#EFEBE9')
              .onChange((v: string) => {
                this.editName = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#FF7043')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editIntegrity > 10) {
                this.editIntegrity -= 5
              }
            })
            Column({ space: 2 }) {
              Text('完好度 ' + this.editIntegrity + '%').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF7043')
              Text('受潮或磕碰后请下调').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#FF7043')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FBE9E7')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editIntegrity < 100) {
                this.editIntegrity += 5
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editDepth > 1) {
                this.editDepth -= 1
              }
            })
            Column({ space: 2 }) {
              Text('出土深度 ' + this.editDepth + ' 米').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
              Text('补测后可修正').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editDepth < 30) {
                this.editDepth += 1
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('置顶到标本墙').fontSize(13).fontColor('#5D4037')
              Text('最得意的标本放最上面').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.editTop ? '#FF7043' : '#BDBDBD')
            .onClick(() => {
              this.editTop = !this.editTop
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#EFEBE9')

          Button() {
            Text('保存修改').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#A1887F')
          .onClick(() => {
            this.fossils = this.fossils.map((f: Fossil207, fi: number) => {
              if (fi === this.editIndex) {
                return {
                  id: f.id,
                  name: this.editName === '' ? f.name : this.editName,
                  era: f.era,
                  integrity: this.editIntegrity,
                  depth: this.editDepth,
                  state: f.state,
                  watchers: f.watchers,
                  starred: this.editTop
                }
              }
              return f
            })
            this.showEditSheet = false
          })
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 420 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框4:删除标本(居中,filter) ----------
  @Builder
  delDialog207() {
    Column({ space: 14 }) {
      Column() {
        Text('🪨').fontSize(34)
      }
      .width(64)
      .height(64)
      .borderRadius(32)
      .backgroundColor('#EFEBE9')
      .justifyContent(FlexAlign.Center)

      Text('移除这块标本?').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
      Text('「' + (this.delIndex >= 0 && this.delIndex < this.fossils.length ? this.fossils[this.delIndex].name : '') + '」将从标本墙下架,出土信息不再展示').fontSize(11).fontColor('#8D6E63').textAlign(TextAlign.Center)

      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text('保留野采日志').fontSize(12).fontColor('#5D4037')
          Text('仅删除标本卡').fontSize(9).fontColor('#90A4AE')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Row() {
          Circle().width(16).height(16).fill('#FFFFFF')
        }
        .width(44)
        .height(24)
        .borderRadius(12)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(this.delKeepLog ? '#5D4037' : '#BDBDBD')
        .onClick(() => {
          this.delKeepLog = !this.delKeepLog
        })
      }
      .width('100%')
      .padding(10)
      .borderRadius(10)
      .backgroundColor('#EFEBE9')

      Row({ space: 10 }) {
        Button() {
          Text('再想想').fontSize(14).fontColor('#5D4037').fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)
        .height(44)
        .borderRadius(22)
        .backgroundColor('#EFEBE9')
        .onClick(() => {
          this.showDelDialog = false
        })

        Button() {
          Text('确认移除').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)
        .height(44)
        .borderRadius(22)
        .backgroundColor('#E64A19')
        .onClick(() => {
          this.fossils = this.fossils.filter((f: Fossil207, fi: number) => fi !== this.delIndex)
          this.showDelDialog = false
        })
      }
      .width('100%')
    }
    .width('82%')
    .padding(20)
    .borderRadius(20)
    .backgroundColor('#FFFFFF')
    .alignItems(HorizontalAlign.Center)
  }

  // ---------- 弹框5:标本详情(居中,图表+跳转联动) ----------
  @Builder
  detailDialog207() {
    Column({ space: 0 }) {
      Scroll() {
        Column({ space: 0 }) {
          Column({ space: 8 }) {
            Row({ space: 10 }) {
              Column() {
                Text('🪨').fontSize(30)
              }
              .width(56)
              .height(56)
              .borderRadius(28)
              .backgroundColor('#FFFFFF')
              .justifyContent(FlexAlign.Center)
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].name : '').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text((this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].era : '') + ' · 深度 ' + (this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].depth : 0) + ' 米').fontSize(11).fontColor('#EFEBE9')
              }
              .alignItems(HorizontalAlign.Start)
              Text('').layoutWeight(1)
              Column() {
                Text('✕').fontSize(13).fontColor('#FFFFFF')
              }
              .width(28)
              .height(28)
              .borderRadius(14)
              .backgroundColor('rgba(255,255,255,0.2)')
              .justifyContent(FlexAlign.Center)
              .onClick(() => {
                this.showDetailDialog = false
              })
            }
            .width('100%')
          }
          .width('100%')
          .padding(18)
          .linearGradient({ angle: 135, colors: [['#FF7043', 0], ['#5D4037', 1]] })

          Column({ space: 14 }) {
            Row({ space: 8 }) {
              Column({ space: 2 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].integrity + '%' : '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
                Text('轮廓完好度').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#FBE9E7')
              Column({ space: 2 }) {
                Text((this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].watchers : 0) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#5D4037')
                Text('累计围观人次').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#EFEBE9')
              Column({ space: 2 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].state : '').fontSize(14).fontWeight(FontWeight.Bold).fontColor(fossilStateColor207(this.detailIndex >= 0 && this.detailIndex < this.fossils.length ? this.fossils[this.detailIndex].state : ''))
                Text('当前状态').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#EFEBE9')
            }
            .width('100%')

            Column({ space: 8 }) {
              Text('近 7 日围挖掘位走势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
              Row({ space: 6 }) {
                ForEach(this.digDays, (d: DigDay207, i: number) => {
                  Column({ space: 4 }) {
                    Column()
                      .width(16)
                      .height(d.finds * 5)
                      .borderRadius({ topLeft: 4, topRight: 4 })
                      .linearGradient({ angle: 180, colors: [['#FF8A65', 0], ['#E64A19', 1]] })
                    Text(d.day).fontSize(8).fontColor('#8D6E63')
                  }
                  .alignItems(HorizontalAlign.Center)
                  .layoutWeight(1)
                }, (d: DigDay207) => ('d' + d.day))
              }
              .width('100%')
              .alignItems(VerticalAlign.Bottom)
              .height(90)
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#FAFAFA')

            Column({ space: 8 }) {
              Text('层位档案').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
              Row({ space: 8 }) {
                Text('围岩').fontSize(11).fontColor('#8D6E63')
                Text('灰黑色薄层灰岩').fontSize(11).fontColor('#5D4037').fontWeight(FontWeight.Bold)
                Text('').layoutWeight(1)
                Text('劈理发育').fontSize(10).fontColor('#E64A19')
              }
              .width('100%')
              Row({ space: 8 }) {
                Text('修理').fontSize(11).fontColor('#8D6E63')
                Text('气动笔精修 + 软刷除尘').fontSize(11).fontColor('#5D4037').fontWeight(FontWeight.Bold)
                Text('').layoutWeight(1)
                Text('耗时 3.5h').fontSize(10).fontColor('#8D6E63')
              }
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#FAFAFA')
            .alignItems(HorizontalAlign.Start)

            Button() {
              Text('预约连线敲同层位').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .height(44)
            .borderRadius(22)
            .backgroundColor('#FF7043')
            .onClick(() => {
              this.showDetailDialog = false
              this.showJoinSheet = true
            })
          }
          .width('100%')
          .padding(16)
        }
        .width('100%')
      }
      .constraintSize({ maxHeight: 480 })
    }
    .width('86%')
    .borderRadius(20)
    .backgroundColor('#FFFFFF')
    .clip(true)
  }
}

// ================= Tab1:野采房(直播) =================

@Component
struct LiveTab207 {
  @Prop micOn: boolean = true
  @Prop camOn: boolean = true
  @Prop lampOn: boolean = true
  @State localMic: boolean = true
  @State localCam: boolean = true
  @State localLamp: boolean = true
  @Prop fossils: Fossil207[] = []
  @Prop guides: Guide207[] = []
  @Prop digSteps: DigStep207[] = []
  @Prop barrages: Barrage207[] = []
  onStep: (i: number) => void = () => {
  }
  onJoin: () => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      // 四机位
      Grid() {
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('⛏️').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('主镜 · 岩壁敲击').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('锤王老周 · 泥盆系').fontSize(9).fontColor('#FFCCBC')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text(strikingFossilCount207(this.fossils) + ' 块敲击中').fontSize(9).fontColor('#FFCCBC')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#5D4037', 0], ['#3E2723', 1]] })
          .onClick(() => {
            this.onJoin()
          })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🔬').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('显微特写位').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('琥珀内含物 40x').fontSize(9).fontColor('#FFCCBC')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('●').fontSize(9).fontColor('#FF1744')
              Text('蚊翅脉清晰').fontSize(9).fontColor('#FFCCBC')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#F57C00', 0], ['#E65100', 1]] })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🥘').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('淘洗盘位').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('琥珀浮选进行中').fontSize(9).fontColor('#DCEDC8')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('●').fontSize(9).fontColor('#76FF03')
              Text('已出 3 颗').fontSize(9).fontColor('#DCEDC8')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#558B2F', 0], ['#33691E', 1]] })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🧑‍🌾').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('我的野采位').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text(this.localCam ? '镜头开启中' : '镜头已关闭').fontSize(9).fontColor('#E0E0E0')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text(this.localMic ? '🎙 开麦' : '🔇 静音').fontSize(9).fontColor(this.localMic ? '#FFFFFF' : '#BDBDBD')
              Text(this.localLamp ? '💡 头灯亮' : '💡 头灯灭').fontSize(9).fontColor(this.localLamp ? '#FFF59D' : '#BDBDBD')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .backgroundColor(this.localCam ? '#616161' : '#263238')
          .onClick(() => {
            this.localCam = !this.localCam
          })
        }
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .height(240)
      .width('100%')

      // 工具条
      Row({ space: 10 }) {
        Column({ space: 3 }) {
          Text(this.localMic ? '🎙️' : '🔇').fontSize(18)
          Text('对讲').fontSize(9).fontColor('#5D4037')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localMic ? '#FFCCBC' : '#EFEBE9')
        .onClick(() => {
          this.localMic = !this.localMic
        })
        Column({ space: 3 }) {
          Text(this.localCam ? '📹' : '📷').fontSize(18)
          Text('镜头').fontSize(9).fontColor('#5D4037')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localCam ? '#FFCCBC' : '#EFEBE9')
        .onClick(() => {
          this.localCam = !this.localCam
        })
        Column({ space: 3 }) {
          Text(this.localLamp ? '💡' : '🔅').fontSize(18)
          Text('头灯').fontSize(9).fontColor('#5D4037')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localLamp ? '#FFF59D' : '#EFEBE9')
        .onClick(() => {
          this.localLamp = !this.localLamp
        })
        Column({ space: 3 }) {
          Text('⛏️').fontSize(18)
          Text('连线').fontSize(9).fontColor('#FFFFFF')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor('#FF7043')
        .onClick(() => {
          this.onJoin()
        })
      }
      .width('100%')

      // 野采六步
      Column({ space: 10 }) {
        Text('野采六步法').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.digSteps, (s: DigStep207, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text(s.done ? '✓' : (i + 1) + '').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width(24)
            .height(24)
            .borderRadius(12)
            .backgroundColor(s.done ? '#7CB342' : '#BCAAA4')
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Text(s.title).fontSize(12).fontColor(s.done ? '#3E2723' : '#8D6E63').fontWeight(FontWeight.Bold)
              Text(s.tip).fontSize(9).fontColor('#BDBDBD')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(s.done ? '已完成' : '进行中').fontSize(9).fontColor(s.done ? '#7CB342' : '#FF7043')
          }
          .width('100%')
          .padding(10)
          .borderRadius(10)
          .backgroundColor(s.done ? '#F1F8E9' : '#FFFFFF')
          .onClick(() => {
            this.onStep(i)
          })
        }, (s: DigStep207) => ('s' + s.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 今晚看点
      Column({ space: 10 }) {
        Text('今晚围掘看点').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.fossils, (f: Fossil207) => {
          if (f.state === '敲击中' || f.state === '热层待掘') {
            Row({ space: 10 }) {
              Column() {
                Text('🪨').fontSize(14)
              }
              .width(30)
              .height(30)
              .borderRadius(8)
              .backgroundColor('#EFEBE9')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text(f.name).fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Bold)
                Text(f.era + ' · 完好度 ' + f.integrity + '%').fontSize(9).fontColor('#8D6E63')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Column() {
                Text(f.state).fontSize(9).fontColor('#FFFFFF')
              }
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .borderRadius(8)
              .backgroundColor(fossilStateColor207(f.state))
            }
            .width('100%')
            .padding(8)
            .borderRadius(10)
            .backgroundColor('#FAFAFA')
          }
        }, (f: Fossil207) => ('h' + f.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 弹幕
      Column({ space: 6 }) {
        Text('围掘弹幕').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#5D4037')
        ForEach(this.barrages, (b: Barrage207, i: number) => {
          Row({ space: 6 }) {
            Text('#' + (i + 1)).fontSize(8).fontColor('#FF7043')
            Text(b.text).fontSize(10).fontColor('#6D4C41')
          }
          .width('100%')
          .padding(6)
          .borderRadius(8)
          .backgroundColor(i % 2 === 0 ? '#EFEBE9' : '#FBE9E7')
        }, (b: Barrage207) => ('b' + b.id))
      }
      .width('100%')
      .padding(10)
      .borderRadius(12)
      .backgroundColor('#FFF3E0')
    }
    .width('100%')
  }
}

// ================= Tab2:标本墙 =================

@Component
struct SampleTab207 {
  @Prop fossils: Fossil207[] = []
  onAdd: () => void = () => {
  }
  onDetail: (i: number) => void = () => {
  }
  onEdit: (i: number) => void = () => {
  }
  onDel: (i: number) => void = () => {
  }
  onStar: (i: number) => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.fossils.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          Text('馆藏标本').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#FBE9E7')
        Column({ space: 2 }) {
          Text(starredFossilCount207(this.fossils) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          Text('精选标记').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text(strikingFossilCount207(this.fossils) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF7043')
          Text('敲击中').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
      }
      .width('100%')

      // 上传入口
      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text('🪨').fontSize(14)
        }
        .width(36)
        .height(36)
        .borderRadius(18)
        .backgroundColor('#FF7043')
        .justifyContent(FlexAlign.Center)
        Column({ space: 2 }) {
          Text('上传新的野采标本').fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Bold)
          Text('晒战绩 · 自动生成档案卡').fontSize(9).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Column() {
          Text('+ 上传').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 14, right: 14, top: 8, bottom: 8 })
        .borderRadius(16)
        .backgroundColor('#FF7043')
        .onClick(() => {
          this.onAdd()
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 年代占比堆叠条
      Column({ space: 8 }) {
        Text('馆藏年代构成').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row() {
          ForEach(eraCounts207(this.fossils), (c: EraCount207) => {
            Row() {
              Text(c.label + ' ' + c.count).fontSize(8).fontColor('#FFFFFF')
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .backgroundColor(c.color)
          }, (c: EraCount207) => (c.label + c.count))
        }
        .width('100%')
        .height(22)
        .borderRadius(11)
        .clip(true)
        Row({ space: 10 }) {
          ForEach(eraCounts207(this.fossils), (c: EraCount207) => {
            Row({ space: 4 }) {
              Column().width(8).height(8).borderRadius(4).backgroundColor(c.color)
              Text(c.label).fontSize(9).fontColor('#8D6E63')
            }
          }, (c: EraCount207) => ('lg' + c.label))
        }
        .width('100%')
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      ForEach(this.fossils, (f: Fossil207, i: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Column() {
              Text('🪨').fontSize(20)
            }
            .width(44)
            .height(44)
            .borderRadius(12)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            Column({ space: 3 }) {
              Text(f.name).fontSize(13).fontColor('#3E2723').fontWeight(FontWeight.Bold)
              Row({ space: 6 }) {
                Column() {
                  Text(f.era).fontSize(8).fontColor('#FFFFFF')
                }
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .borderRadius(6)
                .backgroundColor(eraColor207(f.era))
                Text('深度 ' + f.depth + 'm').fontSize(9).fontColor('#8D6E63')
                Text('围观 ' + f.watchers).fontSize(9).fontColor('#BDBDBD')
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column() {
              Text(f.starred ? '⭐' : '☆').fontSize(18)
            }
            .width(32)
            .height(32)
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              this.onStar(i)
            })
          }
          .width('100%')

          Row() {
            Row({ space: 4 }) {
              Text('完好度').fontSize(9).fontColor('#8D6E63')
              Text(f.integrity + '%').fontSize(10).fontColor('#E64A19').fontWeight(FontWeight.Bold)
            }
            Row({ space: 4 }) {
              Text('状态').fontSize(9).fontColor('#8D6E63')
              Text(f.state).fontSize(10).fontColor(fossilStateColor207(f.state)).fontWeight(FontWeight.Bold)
            }
            .margin({ left: 14 })
            Text('').layoutWeight(1)
            Column() {
              Text('详情').fontSize(10).fontColor('#FFFFFF')
            }
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(10)
            .backgroundColor('#FF7043')
            .onClick(() => {
              this.onDetail(i)
            })
            Column() {
              Text('编辑').fontSize(10).fontColor('#5D4037')
            }
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(10)
            .backgroundColor('#EFEBE9')
            .margin({ left: 6 })
            .onClick(() => {
              this.onEdit(i)
            })
            Column() {
              Text('移除').fontSize(10).fontColor('#E64A19')
            }
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(10)
            .backgroundColor('#FBE9E7')
            .margin({ left: 6 })
            .onClick(() => {
              this.onDel(i)
            })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .borderRadius(14)
        .backgroundColor('#FFFFFF')
      }, (f: Fossil207) => ('f' + f.id + f.state))
    }
    .width('100%')
  }
}

// ================= Tab3:地层库 =================

@Component
struct StrataTab207 {
  @Prop strata: Stratum207[] = []
  @Prop fossils: Fossil207[] = []
  @Prop digDays: DigDay207[] = []

  build() {
    Column({ space: 12 }) {
      // 地层年代尺
      Column({ space: 6 }) {
        Text('地层年代尺(单位:亿年前)').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.strata, (s: Stratum207) => {
          Row({ space: 8 }) {
            Column().width(10).height(26).borderRadius(3).backgroundColor(s.color)
            Column({ space: 2 }) {
              Text(s.name).fontSize(11).fontColor('#3E2723').fontWeight(FontWeight.Bold)
              Text('代表化石:' + s.fossil).fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(s.age + ' 亿年前').fontSize(9).fontColor('#BDBDBD')
          }
          .width('100%')
          .padding(8)
          .borderRadius(8)
          .backgroundColor('#FAFAFA')
        }, (s: Stratum207) => ('st' + s.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 挖掘热度横条
      Column({ space: 10 }) {
        Text('各地层挖掘热度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.strata, (s: Stratum207) => {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(s.name).fontSize(10).fontColor('#5D4037').width(88)
              Row() {
                Row()
                  .width(s.heat + '%')
                  .height(10)
                  .borderRadius(5)
                  .linearGradient({ angle: 0, colors: [[s.color, 0], ['#FF7043', 1]] })
              }
              .layoutWeight(1)
              .height(10)
              .borderRadius(5)
              .backgroundColor('#EFEBE9')
              Text(s.heat + '').fontSize(9).fontColor('#E64A19').fontWeight(FontWeight.Bold)
            }
            .width('100%')
          }
          .width('100%')
        }, (s: Stratum207) => ('heat' + s.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 本周出土量柱图
      Column({ space: 8 }) {
        Text('本周每日出土量').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row({ space: 6 }) {
          ForEach(this.digDays, (d: DigDay207) => {
            Column({ space: 4 }) {
              Text(d.finds + '').fontSize(8).fontColor('#E64A19')
              Column()
                .width(18)
                .height(d.finds * 5)
                .borderRadius({ topLeft: 4, topRight: 4 })
                .linearGradient({ angle: 180, colors: [['#FF8A65', 0], ['#5D4037', 1]] })
              Text(d.day).fontSize(8).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Center)
            .layoutWeight(1)
          }, (d: DigDay207) => ('dd' + d.day))
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(100)
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 各地层馆藏数
      Column({ space: 8 }) {
        Text('各地层馆藏标本数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.strata, (s: Stratum207, i: number) => {
          Row({ space: 8 }) {
            Column().width(10).height(10).borderRadius(5).backgroundColor(s.color)
            Text(s.name).fontSize(10).fontColor('#5D4037').width(96)
            Row() {
              Row()
                .width(eraCounts207(this.fossils)[i].count * 10)
                .height(10)
                .borderRadius(5)
                .backgroundColor(s.color)
            }
            .layoutWeight(1)
            .height(10)
            .borderRadius(5)
            .backgroundColor('#EFEBE9')
            .clip(true)
            .justifyContent(FlexAlign.Start)
            Text(eraCounts207(this.fossils)[i].count + ' 块').fontSize(9).fontColor('#E64A19').fontWeight(FontWeight.Bold)
          }
          .width('100%')
        }, (s: Stratum207) => ('cnt' + s.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFF3E0')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
  }
}

// ================= Tab4:工具箱 =================

@Component
struct ToolTab207 {
  @Prop tools: Tool207[] = []
  @Prop toolUses: ToolUse207[] = []
  @State borrowIndex: number = -1

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.tools.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          Text('在库工具').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#FBE9E7')
        Column({ space: 2 }) {
          Text('26').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          Text('今日借出').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text('98%').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#7CB342')
          Text('完好率').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#F1F8E9')
      }
      .width('100%')

      // 工具列表
      Column({ space: 8 }) {
        Text('工具架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.tools, (t: Tool207, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text(t.icon).fontSize(18)
            }
            .width(40)
            .height(40)
            .borderRadius(10)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Text(t.name).fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Bold)
              Text('库存 ' + t.stock + ' · 累计借用 ' + t.times + ' 次').fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column() {
              Text(this.borrowIndex === i ? '已借出' : (t.borrowed ? '占用中' : '可借')).fontSize(10)
                .fontColor(this.borrowIndex === i ? '#FFFFFF' : (t.borrowed ? '#BDBDBD' : '#7CB342'))
            }
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(10)
            .backgroundColor(this.borrowIndex === i ? '#FF7043' : (t.borrowed ? '#EFEBE9' : '#F1F8E9'))
            .onClick(() => {
              this.borrowIndex = i
            })
          }
          .width('100%')
          .padding(10)
          .borderRadius(10)
          .backgroundColor(this.borrowIndex === i ? '#FFF3E0' : '#FAFAFA')
        }, (t: Tool207) => ('t' + t.id + this.borrowIndex))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 使用频次柱图
      Column({ space: 8 }) {
        Text('近 7 日工具使用频次').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row({ space: 6 }) {
          ForEach(this.toolUses, (u: ToolUse207) => {
            Column({ space: 4 }) {
              Text(u.uses + '').fontSize(8).fontColor('#E64A19')
              Column()
                .width(16)
                .height(u.uses * 2)
                .borderRadius({ topLeft: 4, topRight: 4 })
                .linearGradient({ angle: 180, colors: [['#A1887F', 0], ['#3E2723', 1]] })
              Text(u.day).fontSize(8).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Center)
            .layoutWeight(1)
          }, (u: ToolUse207) => ('u' + u.day))
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(90)
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
  }
}

// ================= Tab5:向导团 =================

@Component
struct GuideTab207 {
  @Prop guides: Guide207[] = []
  @State followIndex: number = -1
  onJoin: () => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Row({ space: 10 }) {
          Column() {
            Text('🧭').fontSize(24)
          }
          .width(48)
          .height(48)
          .borderRadius(24)
          .backgroundColor('#FBE9E7')
          .justifyContent(FlexAlign.Center)
          Column({ space: 3 }) {
            Text('在线向导 ' + onlineGuideCount207(this.guides) + ' / ' + this.guides.length).fontSize(14).fontColor('#3E2723').fontWeight(FontWeight.Bold)
            Text('连线敲化石 · 同层位带掘').fontSize(10).fontColor('#8D6E63')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Column() {
            Text('去连线').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .padding({ left: 12, right: 12, top: 7, bottom: 7 })
          .borderRadius(14)
          .backgroundColor('#FF7043')
          .onClick(() => {
            this.onJoin()
          })
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      Column({ space: 10 }) {
        Text('向导人气榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.guides, (g: Guide207, i: number) => {
          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text((i + 1) + '').fontSize(13).fontColor(i < 3 ? '#E64A19' : '#BDBDBD').fontWeight(FontWeight.Bold)
            }
            .width(22)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text(g.name.slice(0, 1)).fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width(40)
            .height(40)
            .borderRadius(20)
            .backgroundColor(eraColor207(eraTags207[i % eraTags207.length]))
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Row({ space: 6 }) {
                Text(g.name).fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Bold)
                if (g.online) {
                  Column() {
                    Text('在线').fontSize(8).fontColor('#7CB342')
                  }
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(6)
                  .backgroundColor('#F1F8E9')
                }
              }
              Text(g.city + ' · ' + g.years + ' 年野采经验').fontSize(9).fontColor('#8D6E63')
              Row({ space: 6 }) {
                Row() {
                  Row()
                    .width((g.heat / maxGuideHeat207(this.guides) * 100) + '%')
                    .height(8)
                    .borderRadius(4)
                    .linearGradient({ angle: 0, colors: [['#FF8A65', 0], ['#E64A19', 1]] })
                }
                .width(90)
                .height(8)
                .borderRadius(4)
                .backgroundColor('#EFEBE9')
                .clip(true)
                Text('人气 ' + g.heat).fontSize(8).fontColor('#E64A19')
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column() {
              Text(this.followIndex === i ? '已关注' : '关注').fontSize(10)
                .fontColor(this.followIndex === i ? '#FFFFFF' : '#FF7043')
            }
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(12)
            .backgroundColor(this.followIndex === i ? '#FF7043' : '#FBE9E7')
            .onClick(() => {
              this.followIndex = i
            })
          }
          .width('100%')
          .padding(10)
          .borderRadius(10)
          .backgroundColor('#FAFAFA')
        }, (g: Guide207) => ('g' + g.id + this.followIndex))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
  }
}

// ================= Tab6:我的 =================

@Component
struct MineTab207 {
  @Prop fossils: Fossil207[] = []
  @Prop digLogs: DigLog207[] = []
  @Prop digDays: DigDay207[] = []
  @State nightMode: boolean = false
  @State notifyHit: boolean = true
  @State autoLog: boolean = false

  build() {
    Column({ space: 12 }) {
      // 个人卡
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Column() {
            Text('掘').fontSize(20).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width(54)
          .height(54)
          .borderRadius(27)
          .backgroundColor('rgba(255,255,255,0.25)')
          .justifyContent(FlexAlign.Center)
          Column({ space: 3 }) {
            Text('掘友 · 岩芯少年').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('野采 38 次 · 出化石 21 块 · 命中率 55%').fontSize(10).fontColor('#FFCCBC')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('⛏️').fontSize(22)
        }
        .width('100%')

        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text(starredFossilCount207(this.fossils) + '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFF3E0')
            Text('精选标本').fontSize(9).fontColor('#FFCCBC')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column({ space: 2 }) {
            Text('1,240').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFF3E0')
            Text('贡献积分').fontSize(9).fontColor('#FFCCBC')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column({ space: 2 }) {
            Text('Lv.5').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFF3E0')
            Text('掘友等级').fontSize(9).fontColor('#FFCCBC')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 10 })
      }
      .width('100%')
      .padding(16)
      .borderRadius(16)
      .linearGradient({ angle: 135, colors: [['#FF7043', 0], ['#3E2723', 1]] })

      // 我的出土量
      Column({ space: 8 }) {
        Text('我的本周出土量').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row({ space: 6 }) {
          ForEach(this.digDays, (d: DigDay207) => {
            Column({ space: 4 }) {
              Column()
                .width(16)
                .height(d.finds * 4)
                .borderRadius({ topLeft: 4, topRight: 4 })
                .linearGradient({ angle: 180, colors: [['#FF8A65', 0], ['#E64A19', 1]] })
              Text(d.day).fontSize(8).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Center)
            .layoutWeight(1)
          }, (d: DigDay207) => ('md' + d.day))
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(80)
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 我的野采日志
      Column({ space: 8 }) {
        Text('我的野采日志').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        ForEach(this.digLogs, (l: DigLog207, i: number) => {
          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('D' + l.dayNum).fontSize(11).fontColor('#E64A19').fontWeight(FontWeight.Bold)
              Text(l.era).fontSize(8).fontColor('#BDBDBD')
            }
            .width(38)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text(l.name).fontSize(11).fontColor('#3E2723').fontWeight(FontWeight.Bold)
              Text('敲击 ' + l.strikes + ' 次 · 岩样 ' + l.weight + 'g').fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            if (l.top) {
              Column() {
                Text('置顶').fontSize(8).fontColor('#FF7043')
              }
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(6)
              .backgroundColor('#FFF3E0')
            }
            Text(l.state).fontSize(9).fontColor(digStateColor207(l.state)).fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .padding(8)
          .borderRadius(8)
          .backgroundColor(i % 2 === 0 ? '#FAFAFA' : '#FBE9E7')
        }, (l: DigLog207) => ('ml' + l.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      // 设置
      Column({ space: 0 }) {
        Row({ space: 10 }) {
          Text('🌙').fontSize(16)
          Column({ space: 2 }) {
            Text('夜采模式').fontSize(12).fontColor('#3E2723')
            Text('夜间连线自动开补光').fontSize(9).fontColor('#90A4AE')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Row() {
            Circle().width(16).height(16).fill('#FFFFFF')
          }
          .width(44)
          .height(24)
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(this.nightMode ? '#FF7043' : '#BDBDBD')
          .onClick(() => {
            this.nightMode = !this.nightMode
          })
        }
        .width('100%')
        .padding(12)
        Row({ space: 10 }) {
          Text('🔔').fontSize(16)
          Column({ space: 2 }) {
            Text('热层开播提醒').fontSize(12).fontColor('#3E2723')
            Text('目标层位开凿时推送').fontSize(9).fontColor('#90A4AE')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Row() {
            Circle().width(16).height(16).fill('#FFFFFF')
          }
          .width(44)
          .height(24)
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(this.notifyHit ? '#FF7043' : '#BDBDBD')
          .onClick(() => {
            this.notifyHit = !this.notifyHit
          })
        }
        .width('100%')
        .padding(12)
        Row({ space: 10 }) {
          Text('📒').fontSize(16)
          Column({ space: 2 }) {
            Text('自动记野采日志').fontSize(12).fontColor('#3E2723')
            Text('连线结束后一键生成').fontSize(9).fontColor('#90A4AE')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Row() {
            Circle().width(16).height(16).fill('#FFFFFF')
          }
          .width(44)
          .height(24)
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(this.autoLog ? '#FF7043' : '#BDBDBD')
          .onClick(() => {
            this.autoLog = !this.autoLog
          })
        }
        .width('100%')
        .padding(12)
      }
      .width('100%')
      .borderRadius(14)
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
  }
}


十七、总结

在这里插入图片描述

岩语·云地质营应用作为一款基于HarmonyOS 6.1.1 ArkTS API 24声明式UI框架构建的地质科考直播围观平台,其架构设计充分体现了声明式范式的核心优势。从数据模型层面看,应用定义了10个interface接口覆盖了全部业务实体——化石标本、地层信息、挖掘日志、向导、弹幕、步骤、工具等,每个接口的字段命名遵循中文语义化原则,使得代码可读性极高。8个工具函数以纯函数的方式实现了状态色彩映射和数据统计聚合两大职能,严格遵循"数据与行为分离"的设计原则,确保了函数的可测试性和可复用性。

从状态管理层面看,应用采用了@State+@Prop+回调函数的经典单向数据流架构。主组件Index207通过@State持有全部可变数据,子组件通过@Prop以只读方式接收数据,用户操作通过回调函数回传给主组件执行状态更新。所有状态变更都采用不可变更新模式——通过map方法创建新数组实现编辑回写,通过filter方法创建新数组实现删除,通过map翻转布尔字段实现收藏和步骤切换。配合bindSheet和bindContentCover的$$双向绑定语法,弹窗的弹出与关闭无需编写额外的命令式逻辑。这种架构模式保证了数据流的单向可追踪性,是HarmonyOS官方推荐的最佳实践。

从UI视觉层面看,应用在配色、布局、动画和数据可视化四个维度都展现了ArkTS声明式UI的强大表达能力。配色采用岩层棕×玛瑙橙的地质主题色系,通过linearGradient实现从头部到个人卡的多角度渐变背景。底部Tab导航创新性地使用三条宽度递增色带堆叠模拟"地层剖面"效果,配合scale放大和animation动画实现选中态的平滑过渡。数据可视化方面,通过layoutWeight+height映射实现了柱状图,通过Row嵌套+width百分比实现了进度条,通过ForEach+Flex布局实现了堆叠条形图——所有图表均为纯ArkTS组件实现,无需引入任何第三方图表库。这种"原生即图表"的设计哲学,正是HarmonyOS ArkTS声明式UI在数据可视化领域的独特魅力所在。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐