鸿蒙操作系统(HarmonyOS)是华为面向万物互联时代打造的分布式操作系统,其核心设计理念是"一次开发,多端部署"。在鸿蒙生态中,应用开发框架经历了从Java UI到声明式UI的重大演进。当前主推的ArkUI声明式开发范式,采用ArkTS作为核心开发语言,这是一种在TypeScript基础上扩展而来的语言,专门为鸿蒙的声明式UI量身定制。ArkTS不仅保留了TypeScript的静态类型检查能力,还在此基础上引入了装饰器系统、状态管理模型、构建器函数等特有语法结构,使得开发者能够以极简的代码量构建出功能丰富、交互流畅的跨设备应用界面。

ArkTS语言最显著的特征是其声明式的编程范式。与传统的命令式UI开发不同,声明式UI要求开发者"描述界面应该是什么样子",而不是"一步步地告诉系统如何构建界面"。在ArkTS中,开发者通过链式调用的方式描述组件的属性、事件和子组件层级关系,UI框架会自动根据状态变化来驱动界面更新。这种数据驱动的UI刷新机制,使得开发者可以专注于业务逻辑和状态管理,而不必手动操作DOM节点或调用繁琐的刷新方法。这种范式大大降低了UI代码的复杂度,同时也提高了界面的可维护性和可预测性。

ArkUI组件体系是鸿蒙声明式UI的核心基石。ArkUI提供了丰富的内置组件,涵盖了从基础文本展示(Text)、按钮交互(Button)、输入框(TextInput)到复杂容器布局(Column、Row、Stack、Flex、Grid)的完整组件谱系。每个组件都拥有统一的属性配置方式,开发者可以通过链式调用为组件设置宽度、高度、边距、圆角、背景色、阴影等视觉属性,也可以通过事件绑定方法为组件添加点击、触摸、变化等交互响应。此外,ArkUI还支持自定义组件的封装与复用,通过@Component装饰器标记的结构体可以被当作一个独立组件在其他地方引用,实现了组件级别的模块化开发。

在状态管理方面,ArkTS提供了一套精细化的装饰器体系。@State用于组件内部的状态变量声明,当状态值发生变化时会自动触发关联的UI刷新;@Prop用于父组件向子组件的单向数据传递,子组件接收后会拥有一份独立的拷贝;@Link则实现了父子组件间的双向数据绑定。此外,@Builder装饰器用于定义可复用的UI构建片段,@Entry标记应用的入口组件,@Component声明一个自定义组件。这些装饰器共同构成了ArkTS状态管理与组件复用的完整体系,是理解鸿蒙声明式UI开发的关键所在。

技术要点:ArkTS的声明式UI范式遵循"状态驱动视图"的核心原则——UI是状态的函数映射,状态变化时框架自动计算差异并更新对应的组件节点,开发者无需手动调用刷新方法。这一机制大幅减少了样板代码,是鸿蒙应用开发效率远超传统命令式框架的根本原因。

二、项目整体架构与数据模型设计

本应用是一个完整的厂区职工书屋阅读空间管理系统,涵盖了书架分类管理、图书检索、借阅管理、阅读活动、好书荐读、个人中心六大功能模块。整个应用采用了单文件多组件的架构方式,通过一个入口组件统一管理页面路由和状态分发,六个子组件各自负责一个功能模块的完整界面与交互逻辑。这种架构既保证了模块间的清晰边界,又通过@Prop实现了父组件向子组件的数据传递,形成了自上而下的数据流。

在进入具体代码分析之前,让我们先通过流程图理解应用的整体架构与页面路由机制。

cur === 1

cur === 2

cur === 3

cur === 4

cur === 5

cur === 6

cur === 0

应用入口 Entry 组件

cur 状态值判断

书架分类视图

图书检索视图

借阅管理视图

阅读活动视图

好书荐读视图

我的书屋视图

默认首页仪表盘

书架列表 + 统计图表 + 弹窗

图书列表 + 搜索框 + 热门排行

借阅记录 + 趋势图 + 规则说明

活动列表 + 报名进度 + 活动回顾

推荐书单 + 编辑荐语 + 评分趋势

个人数据 + 阅读画像 + 设置项

2.1 数据接口定义层

应用的数据模型通过多个interface接口定义,每个接口对应一个业务实体的数据结构。这些接口是整个应用数据流的基石,所有的状态变量、列表渲染、弹窗数据传递都依赖于这些结构定义。

interface BookItem {
  code: string
  name: string
  author: string
  type: string
  state: string
  num: number
}

interface ShelfItem {
  code: string
  name: string
  floor: string
  num: number
  capacity: number
}

interface LoanItem {
  code: string
  name: string
  borrow: string
  back: string
  state: string
}

interface ActItem {
  code: string
  name: string
  time: string
  place: string
  state: string
}

interface RecItem {
  code: string
  name: string
  author: string
  reason: string
  score: number
}

interface MyItem {
  icon: string
  title: string
  desc: string
}

在这里插入图片描述

上述代码定义了六个核心数据接口,它们分别描述了图书项、书架项、借阅项、活动项、推荐项和个人数据项的数据结构。我们逐一来看每个接口的设计意图和字段含义。

BookItem接口描述了一本图书的完整信息,其中code是图书的唯一编号(如"B-001"),name是书名,author是作者,type是分类(如"文学经典"),state表示借阅状态(“在架"或"借出”),num表示当前在架的册数。这个接口是图书检索模块的核心数据载体,图书列表的渲染、详情弹窗的数据展示都依赖于这个结构。

ShelfItem接口描述了一个书架分类的信息。与BookItem不同,它多了floor(所在楼层位置)和capacity(书架容量)两个字段。numcapacity的比值可以用来计算书架利用率,当num超过capacity的90%时,界面会显示"较满"状态标签,这是通过状态颜色函数动态判断实现的。

LoanItem接口用于借阅管理模块,包含借出日期borrow和应还日期back两个字段,以及借阅状态state(“在借”、“已还"或"逾期”)。这三态状态设计使得借阅记录可以清晰区分当前借阅、历史归还和逾期未还三种情况。

ActItem接口描述阅读活动信息,包含活动时间time、活动地点place和活动状态state(“报名中"或"已结束”)。RecItem接口额外包含推荐理由reason和评分score(浮点数),用于好书荐读模块的评分展示。MyItem接口则最为简洁,仅包含图标、标题和描述三个字段,用于个人中心的快捷入口卡片。

技术要点:ArkTS中的interface与TypeScript的interface语法一致,用于定义对象的形状契约。在ArkTS的严格类型系统下,所有状态变量、函数参数、组件属性都必须有明确的类型标注,这有助于在编译阶段捕获类型错误,提升代码的健壮性。

2.2 入口组件的状态声明与数据初始化

入口组件Index是整个应用的根节点,由@Entry@Component两个装饰器共同标记。@Entry表示这是一个页面级入口组件,应用启动时首先渲染此组件;@Component则声明这是一个自定义组件,可以被复用和组合。

@Entry
@Component
struct Index {
  @State cur: number = 0
  @State shelfList: ShelfItem[] = [
    { code: 'S-01', name: '文学经典', floor: '一层 · 东区', num: 486, capacity: 600 },
    { code: 'S-02', name: '人文社科', floor: '一层 · 中区', num: 512, capacity: 600 },
    { code: 'S-03', name: '自然科学', floor: '一层 · 西区', num: 398, capacity: 500 },
    { code: 'S-04', name: '经济管理', floor: '二层 · 东区', num: 356, capacity: 450 },
    { code: 'S-05', name: '工程技术', floor: '二层 · 中区', num: 428, capacity: 500 },
    { code: 'S-06', name: '历史传记', floor: '二层 · 西区', num: 312, capacity: 400 }
  ]
  @State bookList: BookItem[] = [
    { code: 'B-001', name: '平凡的世界', author: '路遥', type: '文学经典', state: '在架', num: 6 },
    { code: 'B-002', name: '活着', author: '余华', type: '文学经典', state: '在架', num: 4 },
    { code: 'B-003', name: '三体', author: '刘慈欣', type: '科幻小说', state: '借出', num: 0 },
    // ... 更多图书记录
  ]
  @State loanList: LoanItem[] = [
    { code: 'L-001', name: '三体', borrow: '08-12', back: '09-09', state: '在借' },
    { code: 'L-002', name: '苏东坡传', borrow: '08-15', back: '09-12', state: '在借' },
    // ... 更多借阅记录
  ]
  @State actList: ActItem[] = [
    { code: 'A-01', name: '《平凡的世界》读书分享会', time: '09-05 18:30', place: '书屋沙龙区', state: '报名中' },
    // ... 更多活动记录
  ]
  @State recList: RecItem[] = [
    { code: 'R-01', name: '置身事内', author: '兰小欢', reason: '读懂中国经济运行的底层逻辑', score: 9.2 },
    // ... 更多推荐记录
  ]
  @State myList: MyItem[] = [
    { icon: '📚', title: '我的借阅', desc: '当前在借 3 本 · 应还日 09-09' },
    { icon: '⭐', title: '我的积分', desc: '阅读积分 1 280 · 本月已积 160' },
    { icon: '❤️', title: '收藏书单', desc: '3 个书单 · 共 18 本' },
    { icon: '👣', title: '阅读足迹', desc: '本年累计阅读 12 本 · 2 186 页' }
  ]

在这里插入图片描述

这段代码是入口组件的状态声明区域。@State cur: number = 0是整个应用的核心路由状态变量,它决定了当前显示哪个功能子页面。当cur的值为0时,显示默认的首页仪表盘;当cur为1到6时,分别显示书架分类、图书检索、借阅管理、阅读活动、好书荐读、我的书屋六个子视图。

这里使用了@State装饰器来声明所有状态变量。@State是ArkTS中最基础的状态管理装饰器,它使得被修饰的变量成为"可观察的"——当变量的值发生改变时,框架会自动重新渲染引用了该变量的UI部分。例如,当用户点击某个功能导航卡片时,cur的值从0变为对应的数字,此时build()方法中引用了this.cur的条件渲染分支就会重新执行,显示对应的子组件视图。

shelfListbookListloanListactListrecListmyList这六个数组类型的状态变量,分别承载了六个功能模块的初始数据。这些数据在入口组件中初始化后,会通过@Prop传递给对应的子组件。值得注意的是,这些数据目前是硬编码的静态数据,在实际工程中它们应该来自网络请求或本地数据库,但此处为了演示完整的UI效果,使用了丰富的模拟数据来展示各种状态和场景。

技术要点@State装饰的变量在值变化时会触发UI刷新,但对于数组类型,只有整体赋值(如this.shelfList = newArray)才会触发刷新,直接修改数组元素属性(如this.shelfList[0].name = 'xxx')在当前版本中不会自动触发刷新。如需数组内部变化的响应式更新,需要使用@Observed@ObjectLink配合,或整体替换数组引用。

三、入口组件构建与页面路由分发机制

3.1 功能导航卡片的构建器函数

入口组件中定义了一个@Builder方法tabCard,用于构建可复用的功能导航卡片。@Builder装饰器是ArkTS中定义可复用UI片段的核心机制,它允许开发者将一段UI描述封装为一个函数,在需要的地方通过this.methodName()的方式调用。

@Builder tabCard(t: string, icon: string, desc: string, i: number) {
  Column() {
    Text(icon).fontSize(26).margin({ bottom: 6 })
    Text(t).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
    Text(desc).fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
  }.width('31.5%').padding({ top: 16, bottom: 16 }).backgroundColor('#FFFFFF').borderRadius(14)
  .shadow({ radius: 6, color: '#0000000A', offsetY: 2 })
  .onClick(() => {
    this.cur = i + 1
  })
}

在这里插入图片描述

这个@Builder方法接收四个参数:t是卡片标题(如"书架分类"),icon是表情图标,desc是简短描述,i是索引值用于计算目标页面编号。方法内部构建了一个Column容器组件,其中从上到下排列了图标文本、标题文本和描述文本。

Column是ArkUI中最基础的容器组件之一,它是一个纵向排列的线性容器,子元素按照从上到下的顺序依次排列。Columnwidth('31.5%')设置了宽度占父容器的31.5%,这样配合FlexSpaceBetween对齐方式,可以在一行中恰好放置三张卡片(31.5% * 3 + 间距 = 100%)。padding设置了上下各16的内边距,backgroundColor设置了白色背景,borderRadius(14)设置了14的圆角,shadow添加了带阴影效果的投影。

Column内部,三行文本通过不同的样式配置形成了视觉层次:图标使用26号字体作为最显眼的元素,标题使用13号加粗字体,描述使用9号灰色字体。这种字号递减的设计是卡片式UI的常见模式,能够引导用户的视线从图标到标题再到描述自然流动。

最关键的是.onClick()事件绑定。当用户点击这张导航卡片时,会执行箭头函数this.cur = i + 1,将入口组件的cur状态变量设置为对应的目标页面编号。由于cur@State装饰的变量,赋值后框架会自动重新渲染build()方法中引用了this.cur的部分,从而切换显示的子组件视图。这就是整个应用页面路由的核心机制——通过状态变量驱动条件渲染来实现的视图切换。

技术要点@Builder方法与普通方法的区别在于,@Builder方法内部的UI描述代码会被编译器特殊处理,生成可被ArkUI框架直接调用的构建函数。@Builder方法支持参数传递,但不支持返回值,它本质上是一段"可参数化的UI模板"。通过@Builder可以避免重复编写相同的UI结构,实现UI代码的DRY(Don’t Repeat Yourself)原则。

3.2 入口组件的build方法与Stack层叠布局

build()方法是每个ArkTS组件必须实现的方法,它返回组件的UI描述。入口组件的build方法使用了Stack作为根容器,内部嵌套Column构建了首页仪表盘和条件渲染的子视图。

build() {
  Stack() {
    Column() {
      Column() {
        Text('📖 职工书屋 · 阅读空间').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text('让阅读成为习惯,让书香浸润厂区').fontSize(11).fontColor('#FDE68A').margin({ top: 6 })
        Row() {
          Text('📚 藏书 ' + '12 860').fontSize(10).fontColor('#FEF3C7')
          Text('•').fontSize(10).fontColor('#FDE68A')
          Text('📰 期刊 ' + '86 种').fontSize(10).fontColor('#FEF3C7')
          Text('•').fontSize(10).fontColor('#FDE68A')
          Text('👥 会员 ' + '1 240 人').fontSize(10).fontColor('#FEF3C7')
        }.width('100%').justifyContent(FlexAlign.Center).margin({ top: 12 })
      }.width('100%').padding({ top: 18, bottom: 18 })

      Column() {
        Text('功能导航').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#292524')
          .width('100%').padding({ left: 4, bottom: 8 })
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          this.tabCard('书架分类', '🗄', '6 大类', 0)
          this.tabCard('图书检索', '🔍', '12 860 册', 1)
          this.tabCard('借阅管理', '📤', '在借 5 本', 2)
          this.tabCard('阅读活动', '🎤', '本周 4 场', 3)
          this.tabCard('好书荐读', '💡', '编辑精选', 4)
          this.tabCard('我的书屋', '🧑‍🏫', '个人中心', 5)
        }.width('100%')
      }.width('100%').layoutWeight(1).padding(14).backgroundColor('#FBF7F0')
    }.width('100%').height('100%')

    if (this.cur === 1) {
      ShelfView({ shelfList: this.shelfList })
    } else if (this.cur === 2) {
      BookView({ bookList: this.bookList })
    } else if (this.cur === 3) {
      LoanView({ loanList: this.loanList })
    } else if (this.cur === 4) {
      ActView({ actList: this.actList })
    } else if (this.cur === 5) {
      RecView({ recList: this.recList })
    } else if (this.cur === 6) {
      MyView({ myList: this.myList, loanList: this.loanList })
    }
  }.width('100%').height('100%')
}

在这里插入图片描述

这段代码是整个应用的渲染核心。Stack组件是ArkUI中的层叠布局容器,它将所有子元素叠放在同一个位置,后声明的子元素会覆盖在先声明的子元素之上。在这里,Stack的第一层是首页仪表盘(包含标题栏和功能导航区),第二层是根据cur状态值条件渲染的子视图。

Stack的层叠特性在这里发挥了关键作用:当cur为0时,只有首页仪表盘可见,子视图条件分支不执行任何渲染;当cur为1到6时,对应的子视图会被渲染并叠加在首页仪表盘之上,完全覆盖底层内容。这种方式实现了一种"全屏覆盖式"的页面切换效果,子视图从顶部滑入并完全覆盖首页。

首页仪表盘部分使用了两个嵌套的Column。外层Column设置了width('100%')height('100%'),占据整个屏幕。内层第一个Column是顶部标题栏区域,包含应用名称、标语和三组统计数据(藏书量、期刊种类、会员人数)。这三组统计数据使用Row容器横向排列,通过justifyContent(FlexAlign.Center)实现了居中对齐,中间用圆点分隔符隔开。

Row是ArkUI中与Column对应的横向排列容器组件。Row的子元素从左到右依次排列,配合justifyContent属性可以控制子元素的水平对齐方式。FlexAlign.Center表示子元素在主轴(水平方向)上居中对齐,FlexAlign.SpaceBetween则表示子元素均匀分布且首尾两端不留间距。

内层第二个Column是功能导航区,设置了layoutWeight(1)使其占据剩余的屏幕空间。layoutWeight是ArkUI中用于权重分配的关键属性,它类似于CSS中的flex-grow。在一个Column容器中,如果有多个子元素设置了layoutWeight,它们会按照权重比例分配剩余空间;如果只有一个子元素设置了layoutWeight(1),它会占据所有剩余空间。在这里,顶部标题栏没有设置layoutWeight,它占据自身内容高度,功能导航区设置了layoutWeight(1)占据剩余空间,使整个布局能够自适应不同屏幕高度。

功能导航区使用了Flex容器来排列六张导航卡片。Flex是ArkUI中最灵活的弹性布局容器,它通过wrap属性控制是否换行(FlexWrap.Wrap表示允许换行),通过justifyContent属性控制主轴方向的对齐方式(FlexAlign.SpaceBetween表示两端对齐且元素间间距相等)。六张卡片每张宽度31.5%,每行三张,恰好分为两行排列。

条件渲染部分使用了标准的if...else if...else语法。这是ArkTS条件渲染的基础语法——当条件为真时,对应的UI描述会被执行并渲染。由于cur@State变量,当其值改变时,条件渲染分支会重新评估,旧的分支内容被卸载,新的分支内容被挂载,从而实现页面切换效果。

技术要点Stack组件在ArkUI中用于层叠布局,子元素按照声明顺序从底到顶叠放。它常用于实现覆盖层、弹窗蒙层、悬浮按钮等场景。Stack默认将子元素居中对齐,可以通过alignContent属性修改默认对齐方式。在本应用中,Stack巧妙地实现了页面切换——子视图覆盖在首页之上,用户看不到首页但首页仍然存在于组件树中。

四、子组件数据传递与@Prop单向同步机制

4.1 ShelfView组件的状态声明与数据接收

入口组件在条件渲染时通过构造函数参数向子组件传递数据,例如ShelfView({ shelfList: this.shelfList })。子组件通过@Prop装饰器接收这些数据。我们以ShelfView为例来分析子组件的完整结构。

@Component
struct ShelfView {
  @Prop shelfList: ShelfItem[]
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State showDetail: boolean = false
  @State curName: string = ''
  @State curState: string = ''
  @State curNum: number = 0
  @State newName: string = ''
  @State newFloor: string = ''

在这里插入图片描述

ShelfView组件由@Component装饰器声明为一个自定义组件。它接收一个@Prop属性shelfList,类型为ShelfItem[],这是从父组件Index传递过来的书架列表数据。@Prop实现了父到子的单向数据同步——当父组件中的shelfList发生变化时,子组件中的shelfList会自动更新;但子组件内部对shelfList的修改不会反向同步给父组件。

除了@Prop属性外,ShelfView还声明了多个@State状态变量用于管理自身的交互状态。showAddshowEditshowDelshowDetail是四个布尔型状态变量,分别控制新增、编辑、删除、详情四个弹窗的显示与隐藏。curNamecurStatecurNum用于存储当前选中书架的信息,在详情弹窗中展示。newNamenewFloor用于新增书架弹窗中的表单输入绑定。

这种"布尔状态变量控制弹窗显隐"的模式在ArkTS中非常常见。由于ArkTS没有传统Web开发中的DOM操作API,不能通过document.createElement来动态创建弹窗,所有的弹窗都必须预先声明在build()方法中,通过条件渲染来控制其可见性。布尔状态变量充当了弹窗的"开关",点击触发时设为true,关闭时设为false

技术要点@Prop@State的核心区别在于数据流向。@State是组件内部管理的状态,可读可写且变化会触发UI刷新;@Prop是从父组件接收的属性,子组件内部可以读取但修改不会回传给父组件。当需要双向绑定时,应使用@Link装饰器替代@Prop。选择正确的装饰器是ArkTS状态管理的核心决策点。

4.2 @Prop数据传递的底层机制

让我们深入理解@Prop的工作原理。当父组件执行ShelfView({ shelfList: this.shelfList })时,框架会将父组件中shelfList的当前值做一次深拷贝,然后赋值给子组件的shelfList属性。此后,每当父组件的shelfList发生变化时,框架会重新执行深拷贝并更新子组件的值。

这种"拷贝传递"的设计保证了父子组件之间的数据隔离——子组件即使修改了接收到的数据,也不会影响到父组件中的原始数据。这在大多数场景下是合理的,因为子组件通常只是展示数据,不需要反向修改。但在需要双向同步的场景下(如购物车数量增减),就应该使用@Link来建立双向绑定关系。

在本应用中,所有子组件接收的都是只读展示数据,因此使用@Prop是恰当的选择。例如ShelfView只需要展示书架列表,不需要修改父组件的数据;BookView只需要展示图书列表;LoanView只需要展示借阅记录。这些场景下,@Prop的单向同步既满足了数据更新的需求,又保证了数据流的清晰可控。

五、书架分类视图的深度剖析

5.1 头部标题栏与统计卡片

ShelfViewbuild()方法由多个@Builder方法组合而成,每个@Builder负责构建一个独立的信息区块。我们先来看头部标题栏headBar和统计卡片statCard

@Builder headBar() {
  Column() {
    Row() {
      Text('🗄').fontSize(20)
      Column() {
        Text('书架分类').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('按类别浏览馆藏图书').fontSize(10).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('6 大类').fontSize(10).fontColor('#B45309').padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor('#FEF3C7').borderRadius(12)
    }.width('100%')
  }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}

headBar构建了一个标题栏区块,使用了Row横向布局。左侧是表情图标,中间是标题和副标题的Column纵向容器,右侧是"6 大类"的标签徽章。中间的Column设置了layoutWeight(1),使它占据图标和标签之间的所有剩余空间,从而将右侧标签推到最右边。这种"左图标-中标题-右标签"的三段式布局是移动端标题栏的经典模式。

右侧的标签使用了琥珀色背景#FEF3C7和棕橙色文字#B45309,配合borderRadius(12)的圆角和padding内边距,形成了一个药丸形状的标签徽章。这种设计既美观又能快速传达数量信息。

@Builder statCard() {
  Row() {
    Column() {
      Text('2 492').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#B45309')
      Text('在架图书').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1)
    Column() {
      Text('86%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#D97706')
      Text('书架利用率').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1)
    Column() {
      Text('6').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#A16207')
      Text('分类书架').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1)
    Column() {
      Text('128').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C2410C')
      Text('本月上新').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1)
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

在这里插入图片描述

statCard构建了一个四列统计数据卡片。四个Column各自设置了layoutWeight(1),在Row容器中均分宽度。每列包含一个大号数字和一个小号标签,数字使用不同的棕橙色调来区分,形成视觉层次。这种四等分的统计卡片在企业级应用中极为常见,能够在一行内展示四个关键指标。

技术要点layoutWeightRow容器中用于水平方向的空间分配,在Column容器中用于垂直方向的空间分配。多个子元素设置相同的layoutWeight(1)时,它们会均分剩余空间。如果其中一个设置为layoutWeight(2),它将占据其他元素两倍的空间。这与CSS Flexbox中的flex-grow概念一致。

5.2 自定义柱状图进度条的纯UI实现

shelfEffect方法构建了一个藏书占比柱状图,这是本应用中最值得关注的纯UI可视化实现之一。在没有引入任何图表库的情况下,开发者通过组合Text空文本组件和颜色背景,手工绘制出了柱状进度条效果。

@Builder shelfEffect() {
  Column() {
    Row() {
      Text('📊 各书架藏书占比').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('实时').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('文学经典').fontSize(9).fontColor('#57534E')
        Text('486 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('92%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 8 })
      Column() {
        Text('人文社科').fontSize(9).fontColor('#57534E')
        Text('512 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('96%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 8 })
    }.width('100%').margin({ top: 12 })
    // ... 更多书架的柱状图
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

在这里插入图片描述

这段代码实现了一个柱状进度条效果。核心思路是:在一个Column容器中先放一个Text('')空文本作为背景条(宽度100%,高度8,浅色背景#FED7AA),再叠放一个Text('')空文本作为前景条(宽度为百分比数值如92%,高度8,深色背景#D97706)。

这里巧妙利用了Column的层叠效果——Column默认将子元素从上到下排列,但由于两个Text('')的高度相同(都是8),第二个会紧接在第一个下方。等等,仔细看代码结构,两个Text是放在同一个Column中的,它们会纵向排列而非层叠。但实际上这里利用了一个视觉技巧:由于两个空文本都没有内容,它们在Column中会以极小的空间排列,但由于设置了固定height(8),它们各占8的高度。不过从视觉效果上看,由于使用了相同的borderRadius和渐变的颜色,两个色条上下排列形成了一个双色进度条的视觉效果——浅色条在下作为"轨道",深色条在上作为"已填充部分"。

每个柱状图项的标签和数值使用不同的字号和颜色区分层次:分类名使用9号灰色字体,册数使用11号加粗棕色字体。整个柱状图区域分为两行排列,每行两个书架,通过layoutWeight(1)均分宽度,配合左右margin形成间距。

技术要点:在ArkTS中,没有HTML中的div元素,所有的可视化效果都需要通过现有组件的组合来实现。空内容的Text('')组件常被用作"色块"或"间隔条"来使用,配合widthheightbackgroundColorborderRadius等属性,可以模拟出进度条、分隔线、色块装饰等各种视觉效果。这种"用文本组件当色块"的技巧在ArkTS开发中非常实用。

5.3 Progress进度组件与分类排行榜

typeBoard方法使用了ArkUI内置的Progress组件来展示分类借阅排行榜。与上面手工绘制的进度条不同,Progress是ArkUI提供的标准进度展示组件,支持线性、环形等多种展示形态。

@Builder typeBoard() {
  Column() {
    Row() {
      Text('🏷 分类使用榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('近 30 日').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('1').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
      Column() {
        Text('文学经典').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('借出 386 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Progress({ value: 96, total: 100, type: ProgressType.Linear })
        .width(80).height(6).color('#D97706').backgroundColor('#FED7AA')
    }.width('100%').margin({ top: 10 })
    Row() {
      Text('2').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
      Column() {
        Text('工程技术').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('借出 312 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Progress({ value: 80, total: 100, type: ProgressType.Linear })
        .width(80).height(6).color('#B45309').backgroundColor('#FED7AA')
    }.width('100%').margin({ top: 10 })
    // ... 更多排行项
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

Progress组件的构造函数接收三个核心参数:value是当前进度值,total是总量,type是进度条类型。ProgressType.Linear表示线性进度条,此外还支持ProgressType.Ring(环形)和ProgressType.ElasticRing(弹性环形)。通过color属性设置进度填充色,backgroundColor设置轨道背景色。

排行榜中排名前三的分类使用了不同的颜色深浅来区分等级:第一名使用最亮的#D97706,第二名使用中等深度的#B45309,第三名使用更深的#A16207。同时排名数字的颜色也做了区分——第一名数字使用棕色,第二、三名使用灰色,通过色彩对比强化了排名层次感。

技术要点Progress组件是ArkUI内置的进度展示组件,支持线性(Linear)、环形(Ring)、弹性环形(ElasticRing)、刻度仪表(ScaleRing)等多种形态。valuetotal的比值决定了进度的填充比例。在需要动态进度展示的场景(如文件下载、任务完成度)中,只需将value绑定到一个@State变量,即可实现进度条的实时更新。

5.4 书架列表卡片与ForEach列表渲染

shelfCard方法定义了单个书架卡片的UI结构,配合ForEach实现了书架列表的动态渲染。这是ArkTS列表渲染的标准模式。

@Builder shelfCard(it: ShelfItem, i: number) {
  Row() {
    Column() {
      Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
    }
    Column() {
      Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(it.floor + ' · 藏书 ' + it.num + ' / ' + it.capacity + ' 册').fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Column() {
      Text(it.num >= it.capacity * 0.9 ? '较满' : '正常').fontSize(9)
        .fontColor(this.getStateColor(it)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor(this.getStateBg(it)).borderRadius(10)
      Text('查看 ›').fontSize(9).fontColor('#B45309').margin({ top: 6 })
    }
  }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(() => {
    this.curName = it.name
    this.curState = it.floor + ' · 藏书 ' + it.num + ' / ' + it.capacity
    this.curNum = it.num
    this.showDetail = true
  })
}

每张书架卡片使用Row横向布局,分为三部分:左侧是编号色块,中间是书架名称和位置信息,右侧是状态标签和操作提示。

左侧编号色块通过it.code.substring(2)提取编号的后两位(如"S-01"提取出"01"),使用getColor(i)方法返回的颜色作为背景色,白色文字居中显示。textAlign(TextAlign.Center)设置了文本在指定宽高内的居中对齐。getColor方法内部维护了一个颜色数组,通过i % arr.length取模运算实现循环配色——当列表项超过颜色数组长度时,颜色会从头开始循环使用。

中间区域使用layoutWeight(1)占据剩余空间,展示书架名称(加粗)和位置藏书信息(灰色小字)。右侧的状态标签通过三元表达式动态判断:当藏书量达到容量的90%以上时显示"较满"(红色),否则显示"正常"(绿色)。这种基于业务逻辑的动态状态展示,使得每张卡片都能直观反映书架的利用情况。

卡片的.onClick()事件绑定了点击处理逻辑:将当前书架的信息保存到组件状态变量中,然后打开详情弹窗。这种"先记录选中项数据,再打开弹窗"的模式在本应用的所有列表项中反复使用,是处理列表项点击交互的标准范式。

// 在build方法中的ForEach调用
ForEach(this.shelfList, (it: ShelfItem, i: number) => {
  this.shelfCard(it, i)
}, (it: ShelfItem, i: number) => it.code + i)

在这里插入图片描述

ForEach是ArkUI中用于列表渲染的核心组件。它接收三个参数:第一个是数据源数组,第二个是项渲染函数(为每个数据项生成UI),第三个是键值生成函数(为每个项生成唯一标识)。键值生成函数it.code + i将编号和索引拼接作为唯一键,这在数据更新时帮助框架识别哪些项是新增的、哪些是删除的、哪些是位置变化的,从而实现最小化的DOM更新。

技术要点ForEach的第三个参数(键值生成函数)虽然可选,但强烈建议始终提供。缺少键值时框架默认使用数组索引作为键,当列表项顺序变化时会导致不必要的重新渲染,甚至出现状态错乱。通过业务唯一标识(如code+index)作为键,可以让框架精准识别每个列表项,实现高效的差量更新。

5.5 弹窗组件的条件渲染实现

ShelfView中定义了四个弹窗@Builder方法:addModal(新增书架)、editModal(编辑书架)、delModal(删除书架)、detailModal(书架详情)。它们通过布尔状态变量的条件渲染来控制显隐。

@Builder addModal() {
  Column() {
    Row() {
      Text('新增书架').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
        this.showAdd = false
      })
    }.width('100%')
    Text('书架名称').fontSize(12).fontColor('#57534E').margin({ top: 14 }).width('100%')
    TextInput({ placeholder: '如:艺术设计', text: this.newName })
      .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
      this.newName = v
    })
    Text('所在楼层').fontSize(12).fontColor('#57534E').margin({ top: 12 }).width('100%')
    TextInput({ placeholder: '如:一层 · 东区', text: this.newFloor })
      .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
      this.newFloor = v
    })
    Button({ type: ButtonType.Normal }) {
      Text('确认新增').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
    }.width('100%').height(40).backgroundColor('#D97706').borderRadius(20).margin({ top: 16 })
    .onClick(() => {
      this.showAdd = false
      this.newName = ''
      this.newFloor = ''
    })
  }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
}

新增书架弹窗包含标题栏(标题+关闭按钮)、两个表单输入字段(书架名称、所在楼层)和一个确认按钮。标题栏使用Row布局,Text('').layoutWeight(1)占据中间空间将关闭按钮推到右侧。

TextInput是ArkUI中的文本输入框组件,构造函数接收一个配置对象,其中placeholder设置占位提示文本,text设置当前输入值。onChange事件在用户输入时触发,回调参数v是当前输入框的值。在回调中通过this.newName = v将输入值同步到状态变量,实现了双向数据绑定——输入框的值变化更新状态变量,状态变量的变化也会反映回输入框的text属性。

Button组件使用ButtonType.Normal类型,这是一种普通按钮类型(区别于ButtonType.Circle圆形按钮和ButtonType.Capsule胶囊按钮)。按钮内部嵌套了一个Text组件来设置按钮文本的样式。确认按钮的点击事件中先将showAdd设为false关闭弹窗,然后清空表单状态变量,为下次打开做准备。

build()方法中,弹窗通过条件渲染来控制显示:

if (this.showAdd) {
  Column() {
    this.addModal()
  }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
  .onClick(() => {
    this.showAdd = false
  })
}

showAddtrue时,渲染一个全屏的蒙版Column,设置半透明黑色背景#00000040(25%不透明度),通过justifyContent(FlexAlign.Center)将弹窗内容居中显示。蒙版的onClick事件关闭弹窗——点击弹窗外部区域时关闭弹窗,这是移动端弹窗交互的标准行为。需要注意的是,弹窗内部的addModal内容也需要阻止事件冒泡,否则点击弹窗内部区域也会触发蒙版的关闭事件。

技术要点:ArkTS没有传统Web开发中的模态对话框API(如dialog.show()),所有弹窗都是通过条件渲染实现的。这种设计使得弹窗的状态完全由组件状态变量控制,状态变化与UI更新之间的因果关系清晰可追溯。蒙版+居中内容+半透明背景是ArkTS实现弹窗的经典三件套,开发者需要手动处理事件冒泡来避免内部点击触发外部关闭。

六、图书检索视图的搜索与列表交互

6.1 搜索输入框与统计面板

BookView组件是图书检索模块的视图组件。它的结构与ShelfView高度相似——同样由头部标题栏、统计卡片、信息看板、列表渲染和弹窗系统组成。我们重点关注其中独有的技术点。

@Builder headBar() {
  Column() {
    Row() {
      Text('🔍').fontSize(20)
      Column() {
        Text('图书检索').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('按书名 / 作者 / 分类快速查找').fontSize(10).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('12 860 册').fontSize(10).fontColor('#B45309').padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor('#FEF3C7').borderRadius(12)
    }.width('100%')
    TextInput({ placeholder: '🔎 输入书名、作者或分类关键字' })
      .fontSize(12).height(38).margin({ top: 12 }).backgroundColor('#FAFAF9')
  }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}

ShelfViewheadBar相比,BookView的头部多了一个搜索输入框。这个TextInput设置了浅灰色背景#FAFAF9,通过margin({ top: 12 })与上方的标题行保持间距。placeholder中使用了放大镜emoji作为视觉提示,增强了搜索框的可识别性。

值得注意的是,这个搜索框目前没有绑定onChange事件和过滤逻辑。在实际工程中,开发者需要在onChange回调中根据输入的关键字过滤bookList数据源,并通过一个新的@State变量存储过滤结果,再用ForEach渲染过滤后的列表。当前代码将搜索框作为UI展示的一部分,展示了完整的搜索交互界面框架。

6.2 热门借阅排行榜与状态颜色映射

hotBoard方法构建了一个本周热门借阅TOP5排行榜。排行榜的视觉设计很有层次感:第一名使用火焰emoji和棕色排名数字,第二到五名使用灰色排名数字,通过颜色对比突出冠军地位。

@Builder hotBoard() {
  Column() {
    Row() {
      Text('🔥 本周热门借阅 TOP5').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('更新于今日').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('1').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
      Column() {
        Text('平凡的世界').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('路遥 · 借出 58 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('🔥').fontSize(14)
    }.width('100%').margin({ top: 10 })
    Row() {
      Text('2').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
      Column() {
        Text('三体').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('刘慈欣 · 借出 52 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('🔥').fontSize(14)
    }.width('100%').margin({ top: 8 })
    // ... 更多排行项
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

排行榜每项使用Row横向布局:排名数字在左,书名和借阅次数在中间(layoutWeight(1)占据剩余空间),火焰emoji在右。第一名到第五名的排名数字颜色从棕色#D97706渐变为灰色#A8A29E,前三名还附加了火焰emoji作为视觉强化,后两名则没有。这种"前三名突出、后两名弱化"的设计是排行榜UI的通用范式。

技术要点:在ArkTS中,Text组件的fontColor属性可以使用任意合法的十六进制颜色值。开发者通常会定义一组语义化的颜色常量来统一管理配色方案。在本应用中,棕色系(#B45309、#D97706、#A16207、#C2410C、#92400E、#EA580C)作为主题色系反复使用,灰色系(#292524、#57534E、#A8A29E)用于文字层次,绿色系(#15803D、#DCFCE7)表示正常状态,红色系(#DC2626、#FFE4E6)表示警示状态。

6.3 图书列表卡片与状态判断函数

bookCard方法构建了单个图书卡片的UI。与书架卡片类似,图书卡片也使用了getColorgetStateColor等辅助函数来动态设置颜色,但判断逻辑不同——书架卡片基于容量利用率判断状态,图书卡片基于借阅状态字段判断。

@Builder bookCard(it: BookItem, i: number) {
  Row() {
    Column() {
      Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
    }
    Column() {
      Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(it.author + ' · ' + it.type).fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Column() {
      Text(it.state).fontSize(9).fontColor(this.getStateColor(it)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor(this.getStateBg(it)).borderRadius(10)
      Text('余 ' + it.num + ' 册').fontSize(9).fontColor('#A8A29E').margin({ top: 6 })
    }
  }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(() => {
    this.curName = it.name
    this.curState = it.author + ' · ' + it.type
    this.curNum = it.num
    this.showDetail = true
  })
}

图书卡片右侧的状态标签直接显示it.state的值(“在架"或"借出”),颜色通过getStateColor(it)函数动态判断。这个函数的逻辑很简单:当状态为"在架"时返回绿色,否则返回红色。getStateBg函数则返回对应的浅色背景色。

getStateColor(it: BookItem): string {
  if (it.state === '在架') {
    return '#15803D'
  }
  return '#DC2626'
}

getStateBg(it: BookItem): string {
  if (it.state === '在架') {
    return '#DCFCE7'
  }
  return '#FFE4E6'
}

这两个方法体现了ArkTS中"状态驱动的动态样式"模式。组件的方法可以在UI描述中被调用,返回值直接用于设置组件属性。当数据源中的状态值不同时,同一个卡片模板会呈现出不同的视觉样式。这种将业务逻辑(状态判断)与样式配置(颜色值)分离到独立方法中的做法,使得代码更加清晰可维护——如果将来需要新增状态类型(如"预约中"),只需修改这两个方法即可。

七、借阅管理视图的图表与规则展示

7.1 借还趋势柱状图的实现

LoanView中的monthChart方法构建了一个近7日借还趋势柱状图。这个柱状图使用纯UI方式实现,与ShelfView中的藏书占比图类似,但更为复杂——每个柱子由两层色块组成,分别代表借出量和归还量。

@Builder monthChart() {
  Column() {
    Row() {
      Text('📊 近 7 日借还趋势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('借 / 还').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('周一').fontSize(9).fontColor('#A8A29E')
        Text('28').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(46).borderRadius(6).backgroundColor('#FED7AA')
          Text('').width('100%').height(30).borderRadius(6).backgroundColor('#FDE68A')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 6 })
      Column() {
        Text('周二').fontSize(9).fontColor('#A8A29E')
        Text('35').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(52).borderRadius(6).backgroundColor('#FED7AA')
          Text('').width('100%').height(38).borderRadius(6).backgroundColor('#FDE68A')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 6, right: 6 })
      // ... 周三到周日的柱状图
    }.width('100%').margin({ top: 10 })
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

每个柱子是一个Column容器,从上到下排列:星期标签、总数数字、双色柱体。柱体本身也是一个Column,包含两个Text('')色块:上层是深橙色#FED7AA(代表借出量),下层是浅黄色#FDE68A(代表归还量)。两个色块的高度按照数据比例设置,如周一借出46(height(46))、归还30(height(30)),叠加后柱子的总高度就代表了当日的总借还量。

七个柱子通过Row横向排列,每个使用layoutWeight(1)均分宽度,配合左右margin形成间距。柱子的高度差异直观地反映了每日借还量的变化趋势——周三最高(42次),周日最低(12次),工作日明显高于周末,这符合厂区职工的阅读习惯。

技术要点:纯UI图表实现的关键在于将数据值映射为UI属性值(如将借还次数映射为色块高度)。这种方式的优点是无需引入第三方图表库,代码量少且可控;缺点是无法实现动画过渡效果和交互(如点击柱子查看详情)。在需要复杂交互和动画的场景下,建议使用ArkUI的Canvas组件或引入专门的图表库。

7.2 应还提醒与借阅规则展示

dueBoard方法构建了应还提醒看板,ruleBoard方法构建了借阅规则说明区。这两个看板展示了不同类型的信息呈现方式。

@Builder dueBoard() {
  Column() {
    Row() {
      Text('⏰ 应还提醒').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('3 日内 2 本').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('📚').fontSize(14)
      Column() {
        Text('三体 · 借阅人 王工').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('应还 09-09 · 剩余 12 天').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 8 })
      Text('正常').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor('#DCFCE7').borderRadius(10)
    }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
    Row() {
      Text('⚠️').fontSize(14)
      Column() {
        Text('原则 · 借阅人 陈工').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('应还 08-25 · 已逾期 3 天').fontSize(9).fontColor('#DC2626').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 8 })
      Text('已逾期').fontSize(9).fontColor('#DC2626').padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor('#FFE4E6').borderRadius(10)
    }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFF1F2').borderRadius(12)
    // ... 更多提醒项
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

应还提醒看板中,每条提醒项使用Row布局:左侧emoji图标、中间书名和借阅人信息(layoutWeight(1))、右侧状态标签。状态标签的颜色根据借阅状态动态设置——正常状态使用绿色系(文字#15803D、背景#DCFCE7、卡片背景#F0FDF4),逾期状态使用红色系(文字#DC2626、背景#FFE4E6、卡片背景#FFF1F2)。

这种"同结构不同颜色"的设计通过统一的卡片模板配合不同的颜色配置,实现了状态信息的快速识别。用户一扫而过就能通过颜色判断哪些图书即将到期、哪些已经逾期,无需仔细阅读文字内容。

@Builder ruleBoard() {
  Column() {
    Row() {
      Text('📋 借阅规则').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('全员适用').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('📅').fontSize(14)
      Text('借阅周期 28 天 · 可续借 1 次(14 天)').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
    }.width('100%').padding({ top: 8 })
    Row() {
      Text('📚').fontSize(14)
      Text('每人同时可借 5 本 · 预约 3 本').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
    }.width('100%').padding({ top: 6 })
    // ... 更多规则项
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

借阅规则看板使用了简洁的"图标+文字说明"布局,每条规则一行,通过emoji图标增加视觉辨识度。文字使用统一的11号深灰色字体,保持信息展示的清晰和一致性。这种规则说明区在企业应用中非常常见,用于向用户传达使用规范和注意事项。

技术要点:在ArkTS中,emoji表情可以直接作为Text组件的内容使用。由于emoji是Unicode字符,在所有设备上都能正常显示,因此常被用作轻量级的图标替代方案。相比使用图片资源或图标字体,emoji不需要额外的资源加载,能够有效减少应用的包体积。

7.3 借阅列表卡片与三态状态管理

loanCard方法构建了借阅记录卡片,LoanView中的状态判断函数支持三种状态:已还、在借、逾期,比BookView的两种状态更为复杂。

@Builder loanCard(it: LoanItem, i: number) {
  Row() {
    Column() {
      Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
    }
    Column() {
      Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('借 ' + it.borrow + ' · 应还 ' + it.back).fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Column() {
      Text(it.state).fontSize(9).fontColor(this.getStateColor(it)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor(this.getStateBg(it)).borderRadius(10)
      Text('详情 ›').fontSize(9).fontColor('#B45309').margin({ top: 6 })
    }
  }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(() => {
    this.curName = it.name
    this.curState = '借出 ' + it.borrow + ' · 应还 ' + it.back
    this.curBack = it.back
    this.showDetail = true
  })
}

getStateColor(it: LoanItem): string {
  if (it.state === '已还') {
    return '#15803D'
  }
  if (it.state === '在借') {
    return '#B45309'
  }
  return '#DC2626'
}

getStateBg(it: LoanItem): string {
  if (it.state === '已还') {
    return '#DCFCE7'
  }
  if (it.state === '在借') {
    return '#FEF3C7'
  }
  return '#FFE4E6'
}

借阅列表卡片的结构与书架卡片、图书卡片保持一致——三段式布局:编号色块+信息区+状态区。但状态判断逻辑支持三种状态:已还(绿色)、在借(琥珀色)、逾期(红色)。getStateColorgetStateBg函数使用连续的if判断来匹配状态值,返回对应的颜色对。

这种三态状态管理使得借阅记录的信息展示更加丰富:用户可以一眼区分哪些书已经归还(绿色)、哪些正在借阅中(琥珀色)、哪些已经逾期需要处理(红色)。颜色编码是信息可视化中最基本的手段之一,在状态管理类应用中尤为重要。

八、阅读活动视图的报名与回顾管理

8.1 活动参与率进度条与报名进度看板

ActView组件负责阅读活动模块的展示。其中actEffect方法构建了活动参与率进度条,signBoard方法构建了报名进度看板。

@Builder actEffect() {
  Column() {
    Row() {
      Text('📊 本月活动参与率').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('月度').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('读书分享').fontSize(9).fontColor('#57534E')
        Text('92%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('92%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 8 })
      Column() {
        Text('专题讲座').fontSize(9).fontColor('#57534E')
        Text('78%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('78%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 8 })
    }.width('100%').margin({ top: 12 })
    // ... 更多活动类型
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

活动参与率进度条使用了与ShelfView中藏书占比图相同的双色柱状条技术。每个进度条项包含活动类型名称、百分比数值和双色进度条。四个活动类型(读书分享92%、专题讲座78%、亲子共读85%、经典诵读68%)分为两行排列,通过百分比数值的差异直观反映了不同类型活动的受欢迎程度。

@Builder signBoard() {
  Column() {
    Row() {
      Text('🎫 报名进度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('本周').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('📖').fontSize(14)
      Column() {
        Text('《平凡的世界》读书分享会').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('报名 46 / 60 人 · 09-05').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 8 })
      Text('77%').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#D97706')
    }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    // ... 更多报名项
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

报名进度看板展示了三个正在报名中的活动。每项使用浅黄色背景卡片#FFFBF1,右侧显示报名百分比。百分比的文字颜色根据报名进度动态设置——高进度(77%)使用亮棕色#D97706,低进度(20%)使用深棕色#A16207,通过颜色深浅暗示报名热度。

8.2 活动列表卡片与条件状态渲染

@Builder actCard(it: ActItem, i: number) {
  Row() {
    Column() {
      Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
    }
    Column() {
      Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(it.time + ' · ' + it.place).fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Column() {
      Text(it.state).fontSize(9).fontColor(this.getStateColor(it)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor(this.getStateBg(it)).borderRadius(10)
      Text('详情 ›').fontSize(9).fontColor('#B45309').margin({ top: 6 })
    }
  }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(() => {
    this.curName = it.name
    this.curState = it.time + ' · ' + it.place
    this.curTime = it.time
    this.showDetail = true
  })
}

getStateColor(it: ActItem): string {
  if (it.state === '报名中') {
    return '#15803D'
  }
  return '#A8A29E'
}

getStateBg(it: ActItem): string {
  if (it.state === '报名中') {
    return '#DCFCE7'
  }
  return '#F5F5F4'
}

活动卡片的状态判断逻辑较为简单——"报名中"显示绿色,"已结束"显示灰色。这种二态设计使得用户可以快速识别哪些活动还可以参加。已结束的活动使用灰色标签弱化处理,避免用户误以为还可以报名。

技术要点:状态颜色映射是ArkTS中"数据驱动UI"理念的典型应用。通过将状态值传入条件判断函数,返回对应的颜色值,实现了UI样式的动态化。这种模式的扩展性很好——新增状态类型只需在判断函数中添加分支,不需要修改UI模板代码。

九、好书荐读视图的评分展示与趋势分析

9.1 推荐书单卡片与评分格式化

RecView组件负责好书荐读模块。推荐卡片中增加了评分展示,使用了toFixed(1)方法来格式化浮点数评分。

@Builder recCard(it: RecItem, i: number) {
  Row() {
    Column() {
      Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
    }
    Column() {
      Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(it.author).fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
      Text(it.reason).fontSize(10).fontColor('#57534E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Column() {
      Text(it.score.toFixed(1)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#D97706')
      Text('评分').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
    }
  }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(() => {
    this.curName = it.name
    this.curState = it.author + ' · ' + it.reason
    this.curScore = it.score
    this.showDetail = true
  })
}

推荐卡片与前面几种卡片的主要区别在于右侧区域——它展示的是评分数字而非状态标签。it.score.toFixed(1)将浮点数评分格式化为一位小数的字符串(如9.2、8.8),确保评分的显示格式统一。评分数字使用13号加粗棕色字体,下方标注"评分"二字,形成"数字+标签"的层次结构。

中间区域比其他卡片多了一行推荐理由it.reason,使用10号深灰色字体。这使得推荐卡片的信息量更丰富,用户在浏览书单时不仅可以看到书名和作者,还能直接看到推荐理由,帮助快速判断是否感兴趣。

9.2 评分趋势柱状图与编辑荐语

trendBoard方法构建了一个近4期书单评分趋势柱状图,editorBoard方法展示了编辑荐语。评分趋势图使用了与借还趋势图类似的双色柱体技术。

@Builder trendBoard() {
  Column() {
    Row() {
      Text('📈 书单评分趋势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('近 4 期').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('6 月').fontSize(9).fontColor('#A8A29E')
        Text('8.7').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(34).borderRadius(6).backgroundColor('#FED7AA')
          Text('').width('100%').height(28).borderRadius(6).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 6 })
      Column() {
        Text('7 月').fontSize(9).fontColor('#A8A29E')
        Text('8.9').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(40).borderRadius(6).backgroundColor('#FED7AA')
          Text('').width('100%').height(34).borderRadius(6).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 6, right: 6 })
      // ... 8月、9月数据
    }.width('100%').margin({ top: 10 })
    Row() {
      Text('🏅').fontSize(14)
      Text('书单评分连续 4 期稳步上升,选书质量获职工认可。').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
    }.width('100%').padding({ top: 10 })
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

评分趋势图展示了6月到9月的平均评分变化:8.7、8.9、9.0、9.2,呈稳步上升趋势。柱体高度也相应递增,视觉上直观反映了选书质量的持续提升。图下方还有一条文字总结,使用奖牌emoji引导视线,传达正向评价信息。

@Builder editorBoard() {
  Column() {
    Row() {
      Text('✍️ 本期编辑荐语').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('书评组').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('📕').fontSize(14)
      Column() {
        Text('《置身事内》').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('从政府与土地切入,把中国经济讲得透彻又亲切。').fontSize(10).fontColor('#57534E').margin({ top: 3 })
      }.layoutWeight(1).margin({ left: 8 })
    }.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    // ... 更多编辑荐语
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

编辑荐语看板展示书评组对推荐图书的简短评价。每条荐语使用书封emoji+书名+荐语的布局,浅黄色背景卡片#FFFBF1与整体暖调书香主题保持一致。荐语使用10号深灰色字体,保持文字的可读性同时不喧宾夺主。

技术要点toFixed(digits)是JavaScript/TypeScript标准的数字格式化方法,将数字转换为指定位数小数的字符串。在ArkTS中,由于严格类型检查,toFixed的返回值类型为string,可以直接用于Text组件的内容。注意toFixed会进行四舍五入,9.05.toFixed(1)返回"9.1"而非"9.0"。

十、个人中心视图的用户画像与设置系统

10.1 用户卡片与个人数据统计

MyView组件是个人中心模块的视图组件。它接收两个@Prop属性:myList(个人快捷入口列表)和loanList(借阅记录列表,用于统计借阅数据)。

@Component
struct MyView {
  @Prop myList: MyItem[]
  @Prop loanList: LoanItem[]
  @State showAccount: boolean = false
  @State showNotify: boolean = false
  @State showPrivacy: boolean = false
  @State showAbout: boolean = false

MyView是唯一一个接收两个@Prop属性的子组件。myList用于渲染"我的数据"快捷入口列表,loanList用于在详情弹窗中展示借阅统计信息。四个@State布尔变量分别控制账号安全、消息通知、隐私权限、关于书屋四个设置弹窗的显隐。

@Builder userCard() {
  Row() {
    Text('👨‍🔧').fontSize(30).width(52).height(52).textAlign(TextAlign.Center)
      .backgroundColor('#FEF3C7').borderRadius(26)
    Column() {
      Text('李工 · 机械车间').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('借阅证号 WZ-2026-0188 · 会员 2 年').fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
      Row() {
        Text('🏅 金牌读者').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10)
        Text('📚 年借 42 本').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10).margin({ left: 6 })
      }.width('100%').margin({ top: 8 })
    }.layoutWeight(1).margin({ left: 12 })
  }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

用户卡片展示用户头像(emoji)、姓名部门、借阅证号和会员年限,以及两个成就标签(金牌读者、年借42本)。头像使用52x52的圆角方块(borderRadius(26)使其成为圆形),浅黄色背景。成就标签使用药丸形状(borderRadius(10)配合padding),两个标签通过margin({ left: 6 })保持间距。

10.2 年度阅读画像与阅读时长周报

myEffect方法构建了年度阅读画像,包含四个维度的数据展示和进度条;readBoard方法构建了阅读时长周报柱状图。

@Builder myEffect() {
  Column() {
    Row() {
      Text('📊 年度阅读画像').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('2026').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('42').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#92400E')
        Text('阅读本数').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('86%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 8 })
      Column() {
        Text('2 186').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#92400E')
        Text('阅读页数').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('72%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 8 })
    }.width('100%').margin({ top: 12 })
    // ... 按时归还率、读书笔记
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

年度阅读画像展示了四个维度的数据:阅读本数(42本,86%进度)、阅读页数(2186页,72%进度)、按时归还率(98%,98%进度)、读书笔记(16篇,64%进度)。每个维度使用大号数字+标签+双色进度条的布局,进度条的填充比例反映了相对于年度目标的完成程度。

@Builder readBoard() {
  Column() {
    Row() {
      Text('📈 阅读时长周报').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('本周 6.5h').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('周一').fontSize(9).fontColor('#A8A29E')
        Text('45').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(34).borderRadius(6).backgroundColor('#FED7AA')
          Text('').width('100%').height(24).borderRadius(6).backgroundColor('#FDE68A')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 5 })
      // ... 周二到周日
    }.width('100%').margin({ top: 10 })
    Row() {
      Text('⏱').fontSize(14)
      Text('本月累计阅读 26.5 小时 · 超上月 4 小时').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
    }.width('100%').padding({ top: 10 })
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

阅读时长周报展示了周一到周日的阅读分钟数柱状图。与借还趋势图的双色柱体设计一致,深橙色条代表总时长,浅黄色条代表实际阅读时长。周五的阅读时长最高(90分钟),周日最低(30分钟),反映了用户在工作日午休时间阅读较多的习惯。

10.3 阅读目标追踪与成就徽章

goalBoard方法构建了阅读目标追踪看板,certWall方法构建了阅读成就徽章墙。

@Builder goalBoard() {
  Column() {
    Row() {
      Text('🎯 我的阅读目标').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('年度').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Column() {
        Text('42 / 100').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#92400E')
        Text('年阅读本数').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('42%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ right: 8 })
      Column() {
        Text('16 / 30').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#92400E')
        Text('读书笔记').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
        Column() {
          Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
          Text('').width('53%').height(8).borderRadius(4).backgroundColor('#D97706')
        }.width('100%').margin({ top: 6 })
      }.layoutWeight(1).margin({ left: 8 })
    }.width('100%').margin({ top: 12 })
    // ... 活动参与、荐书上榜
    Row() {
      Text('🎉').fontSize(14)
      Text('完成年度目标可解锁「书香职工」徽章。').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
    }.width('100%').padding({ top: 10 })
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

阅读目标看板展示了四个年度目标及其完成进度:年阅读本数(42/100,42%)、读书笔记(16/30,53%)、活动参与(30/40,75%)、荐书上榜(6/10,60%)。每个目标使用"当前值/目标值"的格式展示,配合进度条的填充比例直观反映完成度。四个目标分为两行排列,通过进度条的视觉对比,用户可以快速判断哪些目标完成度较高、哪些需要加把劲。

@Builder certWall() {
  Column() {
    Row() {
      Text('🏅 阅读成就').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('4 枚').fontSize(10).fontColor('#A8A29E')
    }.width('100%')
    Row() {
      Text('🏆').fontSize(16)
      Column() {
        Text('季度阅读之星 · Q2').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('连续 90 天阅读打卡').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('已获得').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor('#DCFCE7').borderRadius(10)
    }.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    Row() {
      Text('📚').fontSize(16)
      Column() {
        Text('年度百本挑战 · 进度 42%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('已读 42 / 100 本').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1).margin({ left: 10 })
      Text('进行中').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor('#FEF3C7').borderRadius(10)
    }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    // ... 书评达人
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

成就徽章墙展示了三个成就:季度阅读之星(已获得)、年度百本挑战(进行中)、书评达人(已获得)。每个成就项使用奖杯emoji作为视觉标识,右侧的状态标签区分"已获得"(绿色)和"进行中"(琥珀色),与借阅管理中的状态颜色体系保持一致。

技术要点:成就系统是游戏化设计(Gamification)在应用中的典型应用。通过将用户行为量化为目标和徽章,可以显著提升用户的参与度和留存率。在ArkTS中,成就的展示本质上也是数据驱动的——成就数据存储在状态变量中,UI根据数据的状态值动态渲染对应的标签颜色和文字内容。

10.4 设置列表与带回调函数的@Builder

MyView中的设置项使用了带回调函数参数的@Builder方法setCard,这是本应用中技术含量较高的一个实现。

@Builder setCard(title: string, desc: string, action: string, onClick: () => void = () => {}) {
  Row() {
    Column() {
      Text(title).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(desc).fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
    }.layoutWeight(1).margin({ left: 12 })
    Text(action).fontSize(10).fontColor('#B45309')
  }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
  .onClick(onClick)
}

setCard方法接收四个参数:title(设置项标题)、desc(描述)、action(右侧操作文字)、onClick(点击回调函数)。第四个参数onClick的类型是() => void(无参数无返回值的箭头函数),默认值为一个空函数() => {}。这种设计使得setCard成为一个高度可配置的设置项模板,不同的调用可以传入不同的回调函数来实现不同的点击行为。

build()方法中的调用方式:

this.setCard('账号与安全', '手机号 · 工号 · 借阅证绑定', '›', () => {
  this.showAccount = true
})
this.setCard('消息通知', '应还提醒 · 活动通知 · 新书上架', '›', () => {
  this.showNotify = true
})
this.setCard('隐私与权限', '定位 · 相机 · 数据说明', '›', () => {
  this.showPrivacy = true
})
this.setCard('关于书屋', '版本信息 · 意见反馈', '›', () => {
  this.showAbout = true
})

四次调用分别传入不同的标题、描述和回调函数,实现了四个设置项的渲染。每次点击会将对应的布尔状态变量设为true,从而打开对应的设置弹窗。这种"模板方法+回调函数"的设计模式极大地减少了重复代码——如果没有setCard的参数化封装,需要为四个设置项分别编写几乎相同的Row布局结构。

技术要点:ArkTS的@Builder方法支持函数类型的参数,这为UI组件的复用提供了强大的灵活性。通过将点击行为作为参数传入,同一个@Builder模板可以适配不同的交互逻辑。这是ArkTS中"高阶组件"思想的体现——组件不仅接收数据参数,还可以接收行为参数。

10.5 设置弹窗与隐私权限说明

四个设置弹窗(accountModalnotifyModalprivacyModalaboutModal)的结构高度一致,都是标题栏+内容行+确认按钮的标准弹窗布局。我们以隐私权限弹窗为例进行分析。

@Builder privacyModal() {
  Column() {
    Row() {
      Text('隐私与权限').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text('').layoutWeight(1)
      Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
        this.showPrivacy = false
      })
    }.width('100%')
    Text('定位服务:仅在到馆签到打卡时使用。').fontSize(12).fontColor('#57534E').margin({ top: 14 }).width('100%')
    Text('相机权限:用于扫描图书条码。').fontSize(12).fontColor('#57534E').margin({ top: 8 }).width('100%')
    Text('数据说明:阅读记录仅用于个人统计。').fontSize(12).fontColor('#57534E').margin({ top: 8 }).width('100%')
    Button({ type: ButtonType.Normal }) {
      Text('知道了').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
    }.width('100%').height(40).backgroundColor('#A16207').borderRadius(20).margin({ top: 16 })
    .onClick(() => {
      this.showPrivacy = false
    })
  }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
}

隐私权限弹窗展示了三条权限使用说明,使用统一的12号深灰色字体。确认按钮使用了与前几个弹窗不同的颜色#A16207(更深的棕色),这是应用中有意设计的颜色层次区分——不同弹窗的确认按钮使用不同深浅的棕色,在视觉上形成微妙的变化,避免所有按钮看起来完全相同。

关于书屋弹窗展示了应用版本信息(V1.3.0)、藏书统计和版权声明,底部有一个"检查更新"按钮,颜色为#C2410C(橙红色),与其他弹窗的按钮颜色再次区分。这种通过按钮颜色变化来区分功能重要性的设计思路,体现了应用在视觉设计层面的精细考量。

十一、组件构建流程与数据流全景图

通过以上分析,我们可以绘制出完整的应用构建流程与数据流图,帮助理解各组件之间的协作关系。

渲染错误: Mermaid 渲染失败: Parse error on line 33: ... end D1 -->|@Prop| S1 D2 -- --------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

这张流程图清晰地展示了数据从入口组件的@State状态变量,通过@Prop单向传递到六个子视图组件的过程。入口组件的cur状态变量作为路由控制中枢,通过条件分支决定渲染哪个子视图。每个子视图内部通过ForEach渲染列表、通过条件渲染控制弹窗、通过onClickonChange处理用户交互。

十二、核心装饰器与组件技术总结对比

下面通过表格对本应用中涉及的核心装饰器、组件、状态变量、布局技术等进行系统对比:

技术要素类别作用说明本应用中的使用场景关键注意事项
@Entry装饰器标记应用入口组件,每个页面仅一个Index组件标记为入口仅能用于页面根组件
@Component装饰器声明自定义组件,可被复用和组合七个组件均使用此装饰器自定义组件必须有build()方法
@State状态装饰器组件内部可观察状态,变化触发UI刷新curshowAddcurName等数十处数组内部属性变更不自动触发刷新
@Prop状态装饰器父到子单向数据传递,子组件拥有独立拷贝各子视图接收入口组件数据修改子组件@Prop不回传父组件
@Builder构建装饰器定义可复用UI片段,支持参数传递tabCardshelfCardsetCard无返回值,本质是UI模板函数
Stack布局容器层叠布局,子元素从底到顶叠放入口组件根容器、各子视图根容器默认居中对齐,可用alignContent修改
Column布局容器纵向线性排列,子元素从上到下几乎所有卡片和区块的容器justifyContent控制纵向对齐
Row布局容器横向线性排列,子元素从左到右标题栏、列表项、统计行justifyContent控制横向对齐
Flex布局容器弹性布局,支持换行和对齐方式功能导航区六张卡片排列wrap控制换行,justifyContent控制对齐
Scroll滚动容器可滚动区域,内容超出可视范围时滚动各子视图的内容容器scrollBar(BarState.Off)隐藏滚动条
Text基础组件文本展示,支持字号、颜色、粗细等全应用最常用的组件空内容Text('')可作色块使用
Button基础组件按钮交互,支持多种类型弹窗确认按钮、操作按钮ButtonType.Normal/Circle/Capsule
TextInput基础组件文本输入框,支持placeholder和onChange弹窗表单、搜索框onChange回调参数为当前输入值
Progress基础组件进度展示,支持线性和环形分类使用榜排行value/total比值决定填充比例
ForEach渲染组件列表循环渲染,根据数据源生成UI六个子视图的列表渲染建议提供键值生成函数优化性能
layoutWeight布局属性权重分配,子元素按比例占据剩余空间统计卡片均分、标题栏中间区域Row中控制水平,Column中控制垂直
borderRadius样式属性圆角设置,影响视觉柔和度几乎所有卡片和按钮值越大圆角越明显
backgroundColor样式属性背景色设置全应用的颜色配置支持十六进制和RGBA格式
shadow样式属性阴影投影效果导航卡片投影效果radius/color/offsetY三参数
margin间距属性外边距,控制元素外部间距全应用的间距控制支持单值和对象格式
padding间距属性内边距,控制元素内部留白全应用的内部留白支持单值和对象格式
onClick事件绑定点击事件回调卡片点击、按钮点击、弹窗关闭箭头函数形式绑定
onChange事件绑定输入变化回调TextInput的输入监听回调参数为当前值字符串
if/else条件渲染根据条件动态渲染UI分支入口路由切换、弹窗显隐条件变化时自动重新评估
justifyContent对齐属性主轴方向对齐方式控制FlexAlign.Center/SpaceBetweenRow主轴为水平,Column主轴为垂直
textAlign对齐属性文本在容器内的对齐方式编号色块中的数字居中TextAlign.Center/Start/End
fontWeight文字样式字体粗细控制标题和数值的加粗FontWeight.Bold/Normal

十三、全维度技术总结

13.1 架构设计层面的总结

从整体架构来看,本应用采用了典型的"单入口+多子视图"的组件化架构模式。入口组件Index作为应用的路由中枢和数据源中心,通过一个@State状态变量cur实现了六个功能模块的条件渲染切换。六个子视图组件各自独立封装了对应模块的完整UI和交互逻辑,通过@Prop从入口组件接收所需的数据。这种架构的优点在于模块边界清晰、数据流单向可控、组件可独立维护。每个子视图组件都可以看作一个独立的"微型应用",拥有自己的状态管理、UI构建方法和事件处理逻辑。

在数据传递方面,入口组件充当了"数据仓库"的角色,所有初始数据在入口组件中集中声明为@State变量,然后通过@Prop分发给子组件。这种集中式数据管理的好处是数据来源单一、可追溯性强——当某个子视图的数据出现问题时,开发者可以直接定位到入口组件中的数据声明处进行排查。但缺点是当数据量增大时,入口组件会变得臃肿,所有数据都耦合在一个组件中。在实际的大型工程中,通常会引入全局状态管理方案(如AppStorageLocalStorage)来解耦数据层和视图层。

13.2 状态管理层面的总结

本应用的状态管理主要使用了@State@Prop两个装饰器,形成了一个清晰的两级状态体系。入口组件的@State变量是"全局状态"(相对于整个应用而言),子组件的@Prop属性是"传入状态"(从父组件同步而来),子组件自身的@State变量是"局部状态"(仅组件内部使用)。这种三级状态层次覆盖了应用中所有的数据管理需求:路由切换由全局状态cur驱动,数据展示由传入状态驱动,弹窗显隐和表单输入由局部状态驱动。

特别值得关注的是弹窗管理的状态模式。每个子视图组件都声明了多个布尔型@State变量(如showAddshowEditshowDelshowDetail)来控制弹窗的显隐。这种"一弹窗一状态变量"的模式虽然简单直接,但在弹窗数量增多时会导致状态变量的膨胀。在更复杂的场景下,可以考虑使用一个枚举型变量(如currentModal: ModalType)来统一管理弹窗状态,减少布尔变量的数量。

13.3 UI构建层面的总结

在UI构建方面,本应用大量使用了@Builder装饰器来封装可复用的UI片段。每个子视图组件内部定义了十多个@Builder方法,分别负责构建标题栏、统计卡片、信息看板、列表项、弹窗等独立UI区块。这种"分而治之"的UI构建策略使得build()方法非常简洁——只需按顺序调用各个@Builder方法即可组装出完整的页面。如果需要修改某个区块的UI,只需修改对应的@Builder方法,不会影响其他部分。

纯UI图表实现是本应用在UI构建方面的一大亮点。在没有引入任何图表库的情况下,开发者通过Text('')空文本组件配合widthheightbackgroundColorborderRadius等属性,手工绘制出了双色进度条、柱状图、趋势图等多种可视化效果。这种"零依赖图表"的实现方式虽然不如专业图表库功能丰富,但在简单数据可视化场景下具有体积小、可控性强、无额外依赖的优势。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 功能:书架分类 / 图书检索 / 借阅管理 / 阅读活动 / 好书荐读 / 我的书屋
// 颜色主题:暖调书香(米白底 + 琥珀棕橙)

interface BookItem {
  code: string
  name: string
  author: string
  type: string
  state: string
  num: number
}

interface ShelfItem {
  code: string
  name: string
  floor: string
  num: number
  capacity: number
}

interface LoanItem {
  code: string
  name: string
  borrow: string
  back: string
  state: string
}

interface ActItem {
  code: string
  name: string
  time: string
  place: string
  state: string
}

interface RecItem {
  code: string
  name: string
  author: string
  reason: string
  score: number
}

interface MyItem {
  icon: string
  title: string
  desc: string
}

@Entry
@Component
struct Index {
  @State cur: number = 0
  @State shelfList: ShelfItem[] = [
    { code: 'S-01', name: '文学经典', floor: '一层 · 东区', num: 486, capacity: 600 },
    { code: 'S-02', name: '人文社科', floor: '一层 · 中区', num: 512, capacity: 600 },
    { code: 'S-03', name: '自然科学', floor: '一层 · 西区', num: 398, capacity: 500 },
    { code: 'S-04', name: '经济管理', floor: '二层 · 东区', num: 356, capacity: 450 },
    { code: 'S-05', name: '工程技术', floor: '二层 · 中区', num: 428, capacity: 500 },
    { code: 'S-06', name: '历史传记', floor: '二层 · 西区', num: 312, capacity: 400 }
  ]
  @State bookList: BookItem[] = [
    { code: 'B-001', name: '平凡的世界', author: '路遥', type: '文学经典', state: '在架', num: 6 },
    { code: 'B-002', name: '活着', author: '余华', type: '文学经典', state: '在架', num: 4 },
    { code: 'B-003', name: '三体', author: '刘慈欣', type: '科幻小说', state: '借出', num: 0 },
    { code: 'B-004', name: '围城', author: '钱钟书', type: '文学经典', state: '在架', num: 3 },
    { code: 'B-005', name: '人类简史', author: '尤瓦尔·赫拉利', type: '人文社科', state: '在架', num: 2 },
    { code: 'B-006', name: '苏东坡传', author: '林语堂', type: '历史传记', state: '借出', num: 0 },
    { code: 'B-007', name: '明朝那些事儿', author: '当年明月', type: '历史传记', state: '在架', num: 5 },
    { code: 'B-008', name: '刻意练习', author: '安德斯·艾利克森', type: '经济管理', state: '在架', num: 3 },
    { code: 'B-009', name: '原则', author: '瑞·达利欧', type: '经济管理', state: '在架', num: 2 },
    { code: 'B-010', name: '乡土中国', author: '费孝通', type: '人文社科', state: '在架', num: 4 },
    { code: 'B-011', name: '百年孤独', author: '加西亚·马尔克斯', type: '文学经典', state: '借出', num: 0 },
    { code: 'B-012', name: '你当像鸟飞往你的山', author: '塔拉·韦斯特弗', type: '人文社科', state: '在架', num: 3 }
  ]
  @State loanList: LoanItem[] = [
    { code: 'L-001', name: '三体', borrow: '08-12', back: '09-09', state: '在借' },
    { code: 'L-002', name: '苏东坡传', borrow: '08-15', back: '09-12', state: '在借' },
    { code: 'L-003', name: '百年孤独', borrow: '08-18', back: '09-15', state: '在借' },
    { code: 'L-004', name: '平凡的世界', borrow: '08-20', back: '09-17', state: '在借' },
    { code: 'L-005', name: '活着', borrow: '08-25', back: '09-22', state: '在借' },
    { code: 'L-006', name: '人类简史', borrow: '07-10', back: '08-07', state: '已还' },
    { code: 'L-007', name: '围城', borrow: '07-18', back: '08-15', state: '已还' },
    { code: 'L-008', name: '刻意练习', borrow: '07-22', back: '08-19', state: '已还' },
    { code: 'L-009', name: '乡土中国', borrow: '08-01', back: '08-29', state: '已还' },
    { code: 'L-010', name: '明朝那些事儿', borrow: '08-05', back: '09-02', state: '已还' },
    { code: 'L-011', name: '原则', borrow: '07-28', back: '08-25', state: '逾期' },
    { code: 'L-012', name: '你当像鸟飞往你的山', borrow: '08-08', back: '09-05', state: '在借' }
  ]
  @State actList: ActItem[] = [
    { code: 'A-01', name: '《平凡的世界》读书分享会', time: '09-05 18:30', place: '书屋沙龙区', state: '报名中' },
    { code: 'A-02', name: '经典诵读 · 唐诗宋词专场', time: '09-12 19:00', place: '职工礼堂', state: '报名中' },
    { code: 'A-03', name: '作家进厂区 · 主题讲座', time: '09-19 15:00', place: '书屋主厅', state: '报名中' },
    { code: 'A-04', name: '亲子共读一小时', time: '09-26 10:00', place: '少儿阅读角', state: '报名中' },
    { code: 'A-05', name: '技术书籍共读营', time: '08-28 18:30', place: '书屋研讨室', state: '已结束' },
    { code: 'A-06', name: '党史读书月启动仪式', time: '08-20 14:00', place: '职工礼堂', state: '已结束' },
    { code: 'A-07', name: '好书漂流 · 换书集市', time: '08-15 12:00', place: '书屋外广场', state: '已结束' },
    { code: 'A-08', name: '新员工入职书单导读', time: '08-10 15:30', place: '书屋主厅', state: '已结束' },
    { code: 'A-09', name: '亲子绘本剧展演', time: '08-03 10:00', place: '少儿阅读角', state: '已结束' },
    { code: 'A-10', name: '机电技术专题书展', time: '07-28 09:00', place: '二层西区', state: '已结束' },
    { code: 'A-11', name: '朗读者 · 职工赛', time: '07-20 19:00', place: '职工礼堂', state: '已结束' },
    { code: 'A-12', name: '暑期阅读打卡挑战', time: '07-01 起', place: '线上共读', state: '已结束' }
  ]
  @State recList: RecItem[] = [
    { code: 'R-01', name: '置身事内', author: '兰小欢', reason: '读懂中国经济运行的底层逻辑', score: 9.2 },
    { code: 'R-02', name: '芯片战争', author: '克里斯·米勒', reason: '一部半导体产业百年博弈史', score: 9.4 },
    { code: 'R-03', name: '长安的荔枝', author: '马伯庸', reason: '小吏视角看大唐职场的生存智慧', score: 9.1 },
    { code: 'R-04', name: '认知觉醒', author: '周岭', reason: '普通人可执行的成长方法论', score: 8.9 },
    { code: 'R-05', name: '翦商', author: '李硕', reason: '殷周之变与华夏新生', score: 9.0 },
    { code: 'R-06', name: '也许你该找个人聊聊', author: '洛莉·戈特利布', reason: '心理咨询室里的温暖故事', score: 8.8 },
    { code: 'R-07', name: '置身烟火人间', author: '汪曾祺', reason: '人间草木皆有趣味', score: 9.3 },
    { code: 'R-08', name: '纳瓦尔宝典', author: '埃里克·乔根森', reason: '关于财富与幸福的智慧箴言', score: 8.7 },
    { code: 'R-09', name: '显微镜下的大明', author: '马伯庸', reason: '六个尘封案件还原大明基层', score: 9.0 },
    { code: 'R-10', name: '写作脑科学', author: '杨滢', reason: '从大脑机制理解好文章从何而来', score: 8.6 },
    { code: 'R-11', name: '毫无意义的工作', author: '大卫·格雷伯', reason: '对现代劳动异化的犀利观察', score: 8.5 },
    { code: 'R-12', name: '山茶文具店', author: '小川糸', reason: '代笔人笔下的温柔人间', score: 8.8 }
  ]
  @State myList: MyItem[] = [
    { icon: '📚', title: '我的借阅', desc: '当前在借 3 本 · 应还日 09-09' },
    { icon: '⭐', title: '我的积分', desc: '阅读积分 1 280 · 本月已积 160' },
    { icon: '❤️', title: '收藏书单', desc: '3 个书单 · 共 18 本' },
    { icon: '👣', title: '阅读足迹', desc: '本年累计阅读 12 本 · 2 186 页' }
  ]

  @Builder tabCard(t: string, icon: string, desc: string, i: number) {
    Column() {
      Text(icon).fontSize(26).margin({ bottom: 6 })
      Text(t).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
      Text(desc).fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
    }.width('31.5%').padding({ top: 16, bottom: 16 }).backgroundColor('#FFFFFF').borderRadius(14)
    .shadow({ radius: 6, color: '#0000000A', offsetY: 2 })
    .onClick(() => {
      this.cur = i + 1
    })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Text('📖 职工书屋 · 阅读空间').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('让阅读成为习惯,让书香浸润厂区').fontSize(11).fontColor('#FDE68A').margin({ top: 6 })
          Row() {
            Text('📚 藏书 ' + '12 860').fontSize(10).fontColor('#FEF3C7')
            Text('•').fontSize(10).fontColor('#FDE68A')
            Text('📰 期刊 ' + '86 种').fontSize(10).fontColor('#FEF3C7')
            Text('•').fontSize(10).fontColor('#FDE68A')
            Text('👥 会员 ' + '1 240 人').fontSize(10).fontColor('#FEF3C7')
          }.width('100%').justifyContent(FlexAlign.Center).margin({ top: 12 })
        }.width('100%').padding({ top: 18, bottom: 18 })
        Column() {
          Text('功能导航').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#292524')
            .width('100%').padding({ left: 4, bottom: 8 })
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            this.tabCard('书架分类', '🗄', '6 大类', 0)
            this.tabCard('图书检索', '🔍', '12 860 册', 1)
            this.tabCard('借阅管理', '📤', '在借 5 本', 2)
            this.tabCard('阅读活动', '🎤', '本周 4 场', 3)
            this.tabCard('好书荐读', '💡', '编辑精选', 4)
            this.tabCard('我的书屋', '🧑‍🏫', '个人中心', 5)
          }.width('100%')
        }.width('100%').layoutWeight(1).padding(14)
        .backgroundColor('#FBF7F0')
      }.width('100%').height('100%')

      if (this.cur === 1) {
        ShelfView({ shelfList: this.shelfList })
      } else if (this.cur === 2) {
        BookView({ bookList: this.bookList })
      } else if (this.cur === 3) {
        LoanView({ loanList: this.loanList })
      } else if (this.cur === 4) {
        ActView({ actList: this.actList })
      } else if (this.cur === 5) {
        RecView({ recList: this.recList })
      } else if (this.cur === 6) {
        MyView({ myList: this.myList, loanList: this.loanList })
      }
    }.width('100%').height('100%')
  }
}

@Component
struct ShelfView {
  @Prop shelfList: ShelfItem[]
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State showDetail: boolean = false
  @State curName: string = ''
  @State curState: string = ''
  @State curNum: number = 0
  @State newName: string = ''
  @State newFloor: string = ''

  @Builder headBar() {
    Column() {
      Row() {
        Text('🗄').fontSize(20)
        Column() {
          Text('书架分类').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('按类别浏览馆藏图书').fontSize(10).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('6 大类').fontSize(10).fontColor('#B45309').padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .backgroundColor('#FEF3C7').borderRadius(12)
      }.width('100%')
    }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
  }

  @Builder statCard() {
    Row() {
      Column() {
        Text('2 492').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#B45309')
        Text('在架图书').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('86%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#D97706')
        Text('书架利用率').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('6').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#A16207')
        Text('分类书架').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('128').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C2410C')
        Text('本月上新').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder shelfEffect() {
    Column() {
      Row() {
        Text('📊 各书架藏书占比').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('实时').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Column() {
          Text('文学经典').fontSize(9).fontColor('#57534E')
          Text('486 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('92%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ right: 8 })
        Column() {
          Text('人文社科').fontSize(9).fontColor('#57534E')
          Text('512 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('96%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ left: 8 })
      }.width('100%').margin({ top: 12 })
      Row() {
        Column() {
          Text('自然科学').fontSize(9).fontColor('#57534E')
          Text('398 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('82%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ right: 8 })
        Column() {
          Text('经济管理').fontSize(9).fontColor('#57534E')
          Text('356 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('74%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ left: 8 })
      }.width('100%').margin({ top: 10 })
      Row() {
        Column() {
          Text('工程技术').fontSize(9).fontColor('#57534E')
          Text('428 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('88%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ right: 8 })
        Column() {
          Text('历史传记').fontSize(9).fontColor('#57534E')
          Text('312 册').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#92400E').margin({ top: 4 })
          Column() {
            Text('').width('100%').height(8).borderRadius(4).backgroundColor('#FED7AA')
            Text('').width('68%').height(8).borderRadius(4).backgroundColor('#D97706')
          }.width('100%').margin({ top: 6 })
        }.layoutWeight(1).margin({ left: 8 })
      }.width('100%').margin({ top: 10 })
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder noticeBoard() {
    Column() {
      Row() {
        Text('📢 书架动态').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('今日 3 条').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('📦').fontSize(14)
        Text('二层东区新增「智能制造」专题书架').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
        Text('10:20').fontSize(10).fontColor('#A8A29E')
      }.width('100%').padding({ top: 8 })
      Row() {
        Text('♻️').fontSize(14)
        Text('本月下架破损图书 36 册 · 已修复 28 册').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
        Text('09:40').fontSize(10).fontColor('#A8A29E')
      }.width('100%').padding({ top: 6 })
      Row() {
        Text('🧹').fontSize(14)
        Text('周五闭馆整理日 · 借还服务暂停半天').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
        Text('公告').fontSize(10).fontColor('#B45309')
      }.width('100%').padding({ top: 6 })
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder typeBoard() {
    Column() {
      Row() {
        Text('🏷 分类使用榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('近 30 日').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('1').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
        Column() {
          Text('文学经典').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('借出 386 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Progress({ value: 96, total: 100, type: ProgressType.Linear })
          .width(80).height(6).color('#D97706').backgroundColor('#FED7AA')
      }.width('100%').margin({ top: 10 })
      Row() {
        Text('2').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('工程技术').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('借出 312 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Progress({ value: 80, total: 100, type: ProgressType.Linear })
          .width(80).height(6).color('#B45309').backgroundColor('#FED7AA')
      }.width('100%').margin({ top: 10 })
      Row() {
        Text('3').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('人文社科').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('借出 268 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Progress({ value: 70, total: 100, type: ProgressType.Linear })
          .width(80).height(6).color('#A16207').backgroundColor('#FED7AA')
      }.width('100%').margin({ top: 10 })
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder newShelfBoard() {
    Column() {
      Row() {
        Text('🆕 本月上新书架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('共 128 册').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('📗').fontSize(14)
        Column() {
          Text('智能制造 · 新技术专题').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('新增 42 册 · 二层东区').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 8 })
        Text('8/26').fontSize(10).fontColor('#A8A29E')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📘').fontSize(14)
        Column() {
          Text('质量管理 · QC 工具书').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('新增 36 册 · 二层中区').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 8 })
        Text('8/24').fontSize(10).fontColor('#A8A29E')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📙').fontSize(14)
        Column() {
          Text('文学新刊 · 当月热门').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('新增 28 册 · 一层东区').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 8 })
        Text('8/22').fontSize(10).fontColor('#A8A29E')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📕').fontSize(14)
        Column() {
          Text('外语学习 · 词汇专项').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('新增 22 册 · 二层西区').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 8 })
        Text('8/20').fontSize(10).fontColor('#A8A29E')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder shelfCard(it: ShelfItem, i: number) {
    Row() {
      Column() {
        Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
      }
      Column() {
        Text(it.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text(it.floor + ' · 藏书 ' + it.num + ' / ' + it.capacity + ' 册').fontSize(10).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1).margin({ left: 12 })
      Column() {
        Text(it.num >= it.capacity * 0.9 ? '较满' : '正常').fontSize(9)
          .fontColor(this.getStateColor(it)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor(this.getStateBg(it)).borderRadius(10)
        Text('查看 ›').fontSize(9).fontColor('#B45309').margin({ top: 6 })
      }
    }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
    .onClick(() => {
      this.curName = it.name
      this.curState = it.floor + ' · 藏书 ' + it.num + ' / ' + it.capacity
      this.curNum = it.num
      this.showDetail = true
    })
  }

  @Builder addModal() {
    Column() {
      Row() {
        Text('新增书架').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
          this.showAdd = false
        })
      }.width('100%')
      Text('书架名称').fontSize(12).fontColor('#57534E').margin({ top: 14 }).width('100%')
      TextInput({ placeholder: '如:艺术设计', text: this.newName })
        .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
        this.newName = v
      })
      Text('所在楼层').fontSize(12).fontColor('#57534E').margin({ top: 12 }).width('100%')
      TextInput({ placeholder: '如:一层 · 东区', text: this.newFloor })
        .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
        this.newFloor = v
      })
      Button({ type: ButtonType.Normal }) {
        Text('确认新增').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      }.width('100%').height(40).backgroundColor('#D97706').borderRadius(20).margin({ top: 16 })
      .onClick(() => {
        this.showAdd = false
        this.newName = ''
        this.newFloor = ''
      })
    }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
  }

  @Builder editModal() {
    Column() {
      Row() {
        Text('编辑书架').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
          this.showEdit = false
        })
      }.width('100%')
      Text('书架名称').fontSize(12).fontColor('#57534E').margin({ top: 14 }).width('100%')
      TextInput({ placeholder: '书架名称', text: this.curName })
        .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
        this.curName = v
      })
      Text('所在楼层').fontSize(12).fontColor('#57534E').margin({ top: 12 }).width('100%')
      TextInput({ placeholder: '所在楼层', text: this.curState })
        .fontSize(12).height(40).margin({ top: 6 }).onChange((v: string) => {
        this.curState = v
      })
      Button({ type: ButtonType.Normal }) {
        Text('保存修改').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      }.width('100%').height(40).backgroundColor('#B45309').borderRadius(20).margin({ top: 16 })
      .onClick(() => {
        this.showEdit = false
      })
    }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
  }

  @Builder delModal() {
    Column() {
      Row() {
        Text('删除书架').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
          this.showDel = false
        })
      }.width('100%')
      Text('确认删除书架「' + this.curName + '」?').fontSize(13).fontColor('#57534E').margin({ top: 16 }).width('100%')
      Text('删除后该分类下 ' + this.curNum + ' 册图书将转为未分类状态。').fontSize(11).fontColor('#A8A29E').margin({ top: 8 }).width('100%')
      Row() {
        Button({ type: ButtonType.Normal }) {
          Text('取消').fontSize(12).fontColor('#57534E')
        }.layoutWeight(1).height(40).backgroundColor('#F5F5F4').borderRadius(20)
        .onClick(() => {
          this.showDel = false
        })
        Button({ type: ButtonType.Normal }) {
          Text('确认删除').fontSize(12).fontColor('#FFFFFF')
        }.layoutWeight(1).height(40).backgroundColor('#DC2626').borderRadius(20).margin({ left: 12 })
        .onClick(() => {
          this.showDel = false
        })
      }.width('100%').margin({ top: 16 })
    }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
  }

  @Builder detailModal() {
    Column() {
      Row() {
        Text('书架详情').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#A8A29E').onClick(() => {
          this.showDetail = false
        })
      }.width('100%')
      Text('🗄 ' + this.curName).fontSize(22).margin({ top: 14 }).width('100%')
      Text('位置:' + this.curState).fontSize(12).fontColor('#57534E').margin({ top: 8 }).width('100%')
      Text('馆藏:' + this.curNum + ' 册').fontSize(12).fontColor('#57534E').margin({ top: 8 }).width('100%')
      Text('管理员:张老师 · 值班时间 周二/周四').fontSize(12).fontColor('#57534E').margin({ top: 8 }).width('100%')
      Row() {
        Text('支持线上预约借书。').fontSize(10).fontColor('#B45309')
      }.width('100%').margin({ top: 12 }).padding(10).backgroundColor('#FFFBEB').borderRadius(10)
      Button({ type: ButtonType.Normal }) {
        Text('知道了').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      }.width('100%').height(40).backgroundColor('#D97706').borderRadius(20).margin({ top: 16 })
      .onClick(() => {
        this.showDetail = false
      })
    }.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
  }

  getColor(i: number): string {
    let arr: string[] = ['#D97706', '#B45309', '#A16207', '#C2410C', '#92400E', '#EA580C']
    return arr[i % arr.length]
  }

  getStateColor(it: ShelfItem): string {
    if (it.num >= it.capacity * 0.9) {
      return '#DC2626'
    }
    return '#15803D'
  }

  getStateBg(it: ShelfItem): string {
    if (it.num >= it.capacity * 0.9) {
      return '#FFE4E6'
    }
    return '#DCFCE7'
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            this.headBar()
            this.shelfEffect()
            this.statCard()
            this.noticeBoard()
            this.typeBoard()
            this.newShelfBoard()
            Row() {
              Text('书架列表').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
              Text('').layoutWeight(1)
              Text('共 ' + this.shelfList.length + ' 类').fontSize(10).fontColor('#A8A29E')
            }.width('100%').padding({ left: 4, right: 4, top: 10, bottom: 8 })
            ForEach(this.shelfList, (it: ShelfItem, i: number) => {
              this.shelfCard(it, i)
            }, (it: ShelfItem, i: number) => it.code + i)
            Row() {
              Button({ type: ButtonType.Normal }) {
                Text('➕ 新增书架').fontSize(12).fontColor('#B45309')
              }.layoutWeight(1).height(38).backgroundColor('#FEF3C7').borderRadius(19)
              .onClick(() => {
                this.showAdd = true
              })
              Button({ type: ButtonType.Normal }) {
                Text('🗑 删除书架').fontSize(12).fontColor('#DC2626')
              }.layoutWeight(1).height(38).backgroundColor('#FFE4E6').borderRadius(19).margin({ left: 12 })
              .onClick(() => {
                this.curName = this.shelfList[0].name
                this.curNum = this.shelfList[0].num
                this.showDel = true
              })
            }.width('100%').margin({ top: 4, bottom: 20 })
          }.width('100%')
        }.scrollBar(BarState.Off)
      }.width('100%').layoutWeight(1)

      if (this.showAdd) {
        Column() {
          this.addModal()
        }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
        .onClick(() => {
          this.showAdd = false
        })
      }
      if (this.showEdit) {
        Column() {
          this.editModal()
        }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
        .onClick(() => {
          this.showEdit = false
        })
      }
      if (this.showDel) {
        Column() {
          this.delModal()
        }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
        .onClick(() => {
          this.showDel = false
        })
      }
      if (this.showDetail) {
        Column() {
          this.detailModal()
        }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
        .onClick(() => {
          this.showDetail = false
        })
      }
    }.width('100%').height('100%')
  }
}

@Component
struct BookView {
  @Prop bookList: BookItem[]
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State showDetail: boolean = false
  @State curName: string = ''
  @State curState: string = ''
  @State curNum: number = 0
  @State newName: string = ''
  @State newAuthor: string = ''
  @State newType: string = ''

  @Builder headBar() {
    Column() {
      Row() {
        Text('🔍').fontSize(20)
        Column() {
          Text('图书检索').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('按书名 / 作者 / 分类快速查找').fontSize(10).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('12 860 册').fontSize(10).fontColor('#B45309').padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .backgroundColor('#FEF3C7').borderRadius(12)
      }.width('100%')
      TextInput({ placeholder: '🔎 输入书名、作者或分类关键字' })
        .fontSize(12).height(38).margin({ top: 12 }).backgroundColor('#FAFAF9')
    }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
  }

  @Builder statCard() {
    Row() {
      Column() {
        Text('12 860').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#B45309')
        Text('馆藏总数').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('2 492').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#D97706')
        Text('在架可借').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('316').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#A16207')
        Text('本周上新').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
      Column() {
        Text('18').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C2410C')
        Text('热门排行').fontSize(9).fontColor('#A8A29E').margin({ top: 4 })
      }.layoutWeight(1)
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder hotBoard() {
    Column() {
      Row() {
        Text('🔥 本周热门借阅 TOP5').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('更新于今日').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('1').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
        Column() {
          Text('平凡的世界').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('路遥 · 借出 58 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('🔥').fontSize(14)
      }.width('100%').margin({ top: 10 })
      Row() {
        Text('2').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('三体').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('刘慈欣 · 借出 52 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('🔥').fontSize(14)
      }.width('100%').margin({ top: 8 })
      Row() {
        Text('3').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('活着').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('余华 · 借出 47 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('🔥').fontSize(14)
      }.width('100%').margin({ top: 8 })
      Row() {
        Text('4').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('苏东坡传').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('林语堂 · 借出 41 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
      }.width('100%').margin({ top: 8 })
      Row() {
        Text('5').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#A8A29E')
        Column() {
          Text('刻意练习').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('安德斯 · 借出 38 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
      }.width('100%').margin({ top: 8 })
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder newBoard() {
    Column() {
      Row() {
        Text('🆕 本周新书上架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('共 316 册').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('📗').fontSize(16)
        Column() {
          Text('《置身事内》 兰小欢').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('经济管理 · 8 册上架 · 08-26').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('新').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10)
      }.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📕').fontSize(16)
        Column() {
          Text('《芯片战争》 克里斯·米勒').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('工程技术 · 6 册上架 · 08-24').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('新').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10)
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📘').fontSize(16)
        Column() {
          Text('《长安的荔枝》 马伯庸').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('文学经典 · 5 册上架 · 08-22').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('新').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10)
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📙').fontSize(16)
        Column() {
          Text('《认知觉醒》 周岭').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('人文社科 · 5 册上架 · 08-20').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('新').fontSize(9).fontColor('#B45309').padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor('#FEF3C7').borderRadius(10)
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder authorBoard() {
    Column() {
      Row() {
        Text('👑 人气作者榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#292524')
        Text('').layoutWeight(1)
        Text('年度').fontSize(10).fontColor('#A8A29E')
      }.width('100%')
      Row() {
        Text('👑').fontSize(16)
        Column() {
          Text('余华').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('作品被借 126 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('NO.1').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#D97706')
      }.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('🥈').fontSize(16)
        Column() {
          Text('刘慈欣').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('作品被借 118 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('NO.2').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#B45309')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('🥉').fontSize(16)
        Column() {
          Text('马伯庸').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#292524')
          Text('作品被借 96 次').fontSize(9).fontColor('#A8A29E').margin({ top: 3 })
        }.layoutWeight(1).margin({ left: 10 })
        Text('NO.3').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#A16207')
      }.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFBF1').borderRadius(12)
      Row() {
        Text('📈').fontSize(14)
        Text('上榜作家作品均支持线上预约。').fontSize(11).fontColor('#57534E').margin({ left: 8 }).layoutWeight(1)
      }.width('100%').padding({ top: 8 })
    }.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
  }

  @Builder bookCard(it: BookItem, i: number) {
    Row() {
      Column() {
        Text(it.code.substring(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          .width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
      }
        .onClick(() => {
          this.showAbout = false
        })
      }
    }.width('100%').height('100%')
  }
}


在这里插入图片描述

13.4 交互设计层面的总结

在交互设计方面,本应用实现了完整的"查看-新增-编辑-删除"CRUD交互闭环。每个列表项卡片都绑定了onClick事件,点击后弹出详情弹窗展示完整信息。底部操作按钮区域提供了新增和删除(或下架/归还/取消)两个操作入口,分别打开对应的表单弹窗。弹窗内部使用TextInput收集用户输入,通过onChange事件实时同步到状态变量,确认按钮的onClick事件执行提交逻辑后关闭弹窗。

事件处理方面,本应用统一使用了箭头函数() => { ... }的形式绑定事件回调。箭头函数不会创建自己的this,因此回调内部可以直接访问组件的this来操作状态变量。这种约定使得事件处理代码简洁且不易出错。需要注意的是,在ArkTS中事件回调不能是异步函数(async/await),如果需要在事件处理中执行异步操作,需要在回调内部定义并调用一个异步函数。

13.5 视觉设计层面的总结

在视觉设计方面,本应用采用了"暖调书香"的主题色彩方案,以米白色(#FBF7F0)为背景底色,琥珀棕橙色系(#B45309#D97706#A16207#C2410C#92400E#EA580C)为主色调,配合灰色文字层次(#292524深灰、#57534E中灰、#A8A29E浅灰)和状态色系(绿色#15803D正常、红色#DC2626警示),形成了一套完整的色彩体系。所有卡片使用白色背景(#FFFFFF)配合14的圆角(borderRadius(14))和8-12的外边距,形成了统一且柔和的卡片式视觉风格。

颜色循环机制是本应用视觉设计的一个技术亮点。getColor(i)方法通过i % arr.length取模运算,为列表项的编号色块循环分配六种棕色系颜色。这种设计使得列表项在保持主题色彩统一的同时,又通过颜色变化避免了视觉单调。配合getStateColorgetStateBg两个状态颜色函数,实现了"统一中有变化、变化中有逻辑"的色彩管理体系。

13.6 工程实践层面的总结

从工程实践角度来看,本应用展示了ArkTS声明式UI开发的完整范式。从数据接口定义、状态变量声明、组件结构搭建、UI片段封装、事件处理绑定到条件渲染和列表渲染,覆盖了ArkTS开发的所有核心技术环节。代码组织清晰——接口定义在最前,入口组件居中,子视图组件按功能顺序排列在后。每个组件的内部结构也遵循统一规范:先声明状态变量,再定义@Builder方法,最后实现build()方法。这种一致的代码结构使得维护者能够快速定位到需要修改的代码位置。

本应用的代码也体现了一些值得改进的方面。例如,多个子视图组件中存在大量结构相似的@Builder方法(如每个组件都有headBarstatCard、列表卡片、四个弹窗),这些相似结构可以通过更高级的抽象(如泛型组件或高阶组件)来减少重复代码。此外,颜色常量散落在各组件方法中,没有统一提取为常量文件,在需要调整主题色时需要逐个修改。这些改进方向在实际工程化中值得考虑,但对于演示和学习ArkTS声明式UI开发而言,当前的代码结构已经足够清晰和完整。

技术要点:ArkTS声明式UI开发的核心理念可以概括为三个"驱动"——状态驱动视图(UI是状态的函数映射)、数据驱动列表(ForEach根据数据源渲染列表)、条件驱动分支(if/else根据条件渲染不同UI)。掌握这三个"驱动"机制,就掌握了ArkTS声明式UI开发的精髓。本应用完整地展示了这三个机制在实际工程中的应用方式,是学习ArkTS开发的优秀参考实例。

Logo

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

更多推荐