鸿蒙原生应用实战(二):首页与诗词库页面开发——多元布局与交互实现
鸿蒙原生应用实战(二):首页与诗词库页面开发——多元布局与交互实现
前言
在上一章中,我们完成了项目初始化和架构设计。本章将正式进入编码阶段,集中开发应用的两个核心页面:
- 首页(Index.ets)—— 信息聚合入口
- 诗词库(PoemListPage.ets)—— 搜索与筛选
这两个页面涉及了大量 ArkTS 布局技巧、组件复用和数据绑定模式,是鸿蒙开发的核心实战内容。
一、首页开发(Index.ets)
1.1 页面布局总览
首页从上到下分为五个区域:
┌──────────────────────┐
│ 标题栏 + 用户头像 │ ← Row + Column 组合
├──────────────────────┤
│ 每日诗词推荐卡片 │ ← 渐变背景 + 引用样式
├──────────────────────┤
│ 6 大分类入口 (Grid) │ ← 2 行 3 列网格
├──────────────────────┤
│ 热门排行列表 │ ← 带序号和点赞数
├──────────────────────┤
│ 为你推荐列表 │ ← 与排行相同结构
├──────────────────────┤
│ 底部导航栏 │ ← 4 个 Tab
└──────────────────────┘
1.2 数据结构定义
在 ArkTS 的严格模式下,所有对象字面量必须有显式类型声明:
// 诗词条目接口
interface PoemItem {
id: number;
title: string;
author: string;
dynasty: string;
content: string[]; // 诗句数组
type: string; // 五言绝句 / 词 / 乐府 ...
likes: number;
}
// 每日推荐数据
const dailyPoem: DailyPoem = {
title: '定风波',
author: '苏轼',
dynasty: '宋',
excerpt: '竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。'
};
// 热门排行数据
const topPoems: PoemItem[] = [
{ id: 1, title: '静夜思', author: '李白', dynasty: '唐',
content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'],
type: '五言绝句', likes: 9852 },
// ... 更多诗词
];
1.3 渐变背景卡片(每日诗词)
首页最醒目的"每日一首"卡片使用了渐变背景效果。在 ArkTS 中,可以通过 background 属性实现:
Column() {
Text('每日一首')
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
.width('100%')
Text(dailyPoem.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%')
.padding({ top: 8 })
Text('—— ' + dailyPoem.dynasty + '·' + dailyPoem.author)
.fontSize(13)
.fontColor('rgba(255,255,255,0.8)')
.width('100%')
// 居中展示经典名句
Text(dailyPoem.excerpt)
.fontSize(17)
.fontColor(Color.White)
.lineHeight(28)
.textAlign(TextAlign.Center)
.padding({ top: 16, bottom: 8 })
}
.width('100%')
.padding(20)
// 渐变色背景——紫色系渐变
.background('linear-gradient(135deg, #667eea, #764ba2)')
.borderRadius(16)
技巧:
linear-gradient是 ArkTS 支持的背景渐变语法,适合做卡片头部装饰。
1.4 网格布局(6 大分类)
使用 Grid 组件实现 2 行 3 列的诗词分类入口:
Grid() {
ForEach(categories, (cat: string) => {
GridItem() {
this.createCategoryCard(cat)
}
}, (cat: string) => cat)
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽
.rowsTemplate('1fr 1fr') // 2行
.rowsGap(12)
.columnsGap(12)
.width('100%')
每个分类卡片包含 emoji 图标和文字标签,点击后跳转到诗词库页面并自动筛选该分类。
1.5 @Builder 组件复用
在 ArkTS 中,@Builder 是组件复用的核心机制。需要注意一个关键限制:@Builder 内不能声明变量:
// ❌ 错误——@Builder 内不能有 const/interface
@Builder
createCategoryCard(name: string) {
const icons: Record<string, string> = { ... }; // 编译报错!
// ...
}
// ✅ 正确——将数据提取为普通方法
getCatIcon(name: string): string {
const icons: Record<string, string> = {
'唐诗三百': '📜', '宋词精选': '🌸', '元曲': '🎭',
'古诗十九首': '📖', '乐府诗集': '🎵', '诗经': '📗'
};
return icons[name] || '📜';
}
@Builder
createCategoryCard(name: string) {
Column() {
Text(this.getCatIcon(name))
.fontSize(28)
Text(name)
.fontSize(12)
.fontColor($r('app.color.text_primary'))
.margin({ top: 8 })
.fontWeight(FontWeight.Medium)
}
// ...
}
1.6 图片圆形容器
首页右上角的用户头像使用了 Circle 组件 + .overlay() 的组合:
Circle()
.width(40)
.height(40)
.fill($r('app.color.accent_purple'))
.overlay(this.avatarText())
overlay 是一个 @Builder 方法:
@Builder
avatarText() {
Text('诗')
.fontColor(Color.White)
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
注意:在早期版本的 ArkTS 中,
.overlay()不能直接接受Text()组件,必须通过@Builder方法包装。
二、诗词库页面开发(PoemListPage.ets)
2.1 交互功能概览
诗词库页面是用户浏览诗词的核心入口,包含三个维度:
| 交互维度 | 实现方式 | 数据来源 |
|---|---|---|
| 搜索 | TextInput 组件 |
用户输入,实时过滤 |
| 朝代筛选 | 标签按钮 Row | 6 个选项(全部/先秦/唐/五代/宋/元) |
| 类型筛选 | 标签按钮 Row | 5 个选项(全部/五绝/七律/词/乐府) |
2.2 数据过滤逻辑
之前我们使用了 get filteredPoems() 访问器,但在运行时发现其在模板中会返回 undefined:
// ❌ 不可行——get 访问器在模板返回 undefined
get filteredPoems(): PoemItem[] {
// ...过滤逻辑
return result; // 运行时始终 undefined!
}
正确做法:使用 @State + @Watch 组合:
@State @Watch('onFilterChange') searchText: string = '';
@State @Watch('onFilterChange') activeDynasty: string = 'all';
@State @Watch('onFilterChange') activeType: string = 'all';
@State filteredList: PoemItem[] = allPoems; // 存储过滤结果
onFilterChange(): void {
let result: PoemItem[] = allPoems;
if (this.searchText.length > 0) {
const keyword: string = this.searchText.toLowerCase();
result = result.filter((p: PoemItem) =>
p.title.includes(keyword) || p.author.includes(keyword)
);
}
if (this.activeDynasty !== 'all') {
result = result.filter((p: PoemItem) => p.dynasty === this.activeDynasty);
}
if (this.activeType !== 'all') {
result = result.filter((p: PoemItem) => p.type === this.activeType);
}
this.filteredList = result; // 更新状态触发重新渲染
}
工作原理:当 searchText、activeDynasty 或 activeType 任一状态变化时,@Watch('onFilterChange') 自动触发 onFilterChange() 方法,更新 filteredList,UI 随之刷新。
2.3 搜索框实现
TextInput 是鸿蒙中的文本输入组件:
Row() {
Text('🔍')
.fontSize(16)
.margin({ left: 12 })
TextInput({ placeholder: '搜索诗词名称或作者...', text: this.searchText })
.layoutWeight(1)
.backgroundColor(Color.Transparent)
.fontSize(14)
.placeholderColor($r('app.color.text_secondary'))
.onChange((val: string) => { this.searchText = val; })
// 搜索框不为空时显示清除按钮
if (this.searchText.length > 0) {
Text('✕')
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
.margin({ right: 12 })
.onClick(() => { this.searchText = ''; })
}
}
.width('100%')
.height(44)
.backgroundColor($r('app.color.bg_card'))
.borderRadius(22) // 圆角搜索框
2.4 筛选标签
筛选标签的样式逻辑:选中的标签用主题色填充,未选中的用白色:
Text(filter.label)
.fontSize(13)
.fontColor(filter.name === this.activeDynasty ?
Color.White : $r('app.color.text_secondary'))
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(filter.name === this.activeDynasty ?
$r('app.color.accent_purple') : $r('app.color.bg_card'))
.borderRadius(16)
.onClick(() => { this.activeDynasty = filter.name; })
2.5 结果计数与空状态
// 结果计数
Row() {
Text('共 ' + this.filteredList.length + ' 首')
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
Blank()
}
// 空状态展示
if (this.filteredList.length === 0) {
Column() {
Text('📖').fontSize(48)
Text('没有找到相关诗词')
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
.margin({ top: 12 })
}
.width('100%')
.height(200)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
2.6 卡片列表
每条诗词卡片显示:序号、标题、类型标签、朝代·作者、诗文节选、点赞数:
@Builder
createPoemCard(poem: PoemItem) {
Row() {
// 序号
Text(poem.id.toString()).fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.accent_purple'))
.opacity(0.3)
Column() {
Row() {
Text(poem.title).fontSize(17).fontWeight(FontWeight.Bold)
Text(poem.type).fontSize(10)
.fontColor($r('app.color.accent_purple'))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor($r('app.color.accent_purple') + '15')
.borderRadius(4)
}
Text(poem.dynasty + ' · ' + poem.author).fontSize(13)
.fontColor($r('app.color.text_secondary'))
// 诗文节选(最多两行)
Text(poem.content[0] + (poem.content.length > 1 ?
',' + poem.content[1] : ''))
.fontSize(14).fontColor($r('app.color.text_secondary'))
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('❤ ' + poem.likes.toString()).fontSize(12)
.fontColor($r('app.color.accent_red'))
}
}
.width('100%').padding(14)
.backgroundColor($r('app.color.bg_card'))
.borderRadius(12)
.onClick(() => {
router.pushUrl({
url: 'pages/PoemDetailPage',
params: { poemId: poem.id }
});
})
}
三、跨页面参数传递
3.1 从作者页跳转到诗词库并搜索
AuthorPage 点击诗人卡片后,会跳转到诗词库并自动填入作者名进行搜索:
// AuthorPage.ets
.onClick(() => {
router.pushUrl({
url: 'pages/PoemListPage',
params: { searchAuthor: author.name }
});
})
// PoemListPage.ets — 接收参数
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['searchAuthor'] !== undefined) {
this.searchText = params['searchAuthor'] as string;
// @Watch 会自动触发 onFilterChange,更新 filteredList
}
}
四、@Builder 中的 if 条件
在 ArkTS 中,if 条件语句可以直接在 build() 和 @Builder 中使用:
@Builder
createPoemCard(poem: PoemItem) {
Row() {
if (this.editMode) {
Circle() // 编辑模式下的选择框
.width(22).height(22)
.stroke($r('app.color.accent_purple'))
.strokeWidth(2)
.fill(Color.Transparent)
}
// ... 其余内容
}
}
但需要注意:if 条件内部只能包含 UI 组件语法,不能包含变量声明、函数调用赋值等。
小结
本章完成了首页和诗词库两个核心页面的开发,涵盖了:
- 渐变背景卡片的设计
- Grid 网格布局的使用
- @Builder 组件复用技巧
- 搜索 + 双维度筛选的实现
- 跨页面参数传递
- 数据过滤的最佳实践(@State + @Watch)
在下一章中,我们将继续开发诗词详情和作者天地两个页面,深入复杂数据展示和交互设计。
【系列目录】
- (一)项目初始化与架构设计
- (二)首页与诗词库页面开发 ← 本文
- (三)诗词详情与作者天地页面开发
- (四)收藏页面与底部导航实现
- (五)编译调试与问题修复经验
更多推荐




所有评论(0)