当前位置: 首页 > news >正文

鸿蒙原生应用实战(三):UI构建 — 首页与写日记页面开发全流程

鸿蒙原生应用实战(三):UI构建 — 首页与写日记页面开发全流程

本文是系列第三篇,聚焦「心情日记」应用的两个核心页面:首页(Index)和写日记页(WritePage)。我们将深入讲解 ArkTS 的声明式 UI 语法、@Builder 装饰器复用、组件化思维和交互设计细节。


一、首页(Index.ets)全面拆解

首页是用户打开应用看到的第一屏,它的设计直接决定了用户的第一印象。

1.1 首页功能需求

┌──────────────────────────────────────┐ │ 📖 心情日记 📊 👤 │ ← 顶部标题栏 ├──────────────────────────────────────┤ │ ┌──────────────────────────────┐ │ │ │ 1月20日 连续 3 天 🔥 │ │ │ │ │ │ │ │ 😊 │ │ ← 今日心情卡片 │ │ 开心 │ │ │ │ 发年终奖了 │ │ │ │ 点击查看详情 > │ │ │ └──────────────────────────────┘ │ │ │ │ ✏️写日记 📅日历 📊统计 👤我的 │ ← 快捷操作 │ │ │ 最近记录 全部 > │ │ ┌─────────────────────────────┐ │ │ │ 😊 发年终奖了 2025-01-20 >│ │ │ │ 😌 周末看书 2025-01-21 >│ │ ← 日记列表 │ │ 😢 告别老朋友 2025-01-22 >│ │ │ │ ... │ │ │ └─────────────────────────────┘ │ └──────────────────────────────────────┘

1.2 状态变量定义

@Entry@Componentstruct Index{@Stateentries:DiaryEntry[]=[];// 所有日记@StatetodayEntry:DiaryEntry|undefined;// 今天的日记@StaterecentEntries:DiaryEntry[]=[];// 最近5条@Statestreak:number=0;// 连续签到天数@StatehasTodayEntry:boolean=false;// 今天是否已写}

@State 的作用:被 @State 装饰的变量是响应式的,当变量值变化时,自动触发 UI 重新渲染。

1.3 数据加载与页面生命周期

// 页面初始化时调用(仅首次)aboutToAppear():void{this.loadData();}// 每次页面显示时调用(包括从其他页面返回)onPageShow():void{this.loadData();}

为什么需要两个生命周期?

  • aboutToAppear:仅在组件首次创建时调用
  • onPageShow:每次页面出现在前台时都调用

当用户在写日记页保存后返回首页,onPageShow负责重新加载数据,确保首页显示最新内容。

1.4 连续签到算法详解

calcStats():void{// ... 计算今日日记、最近列表等 ...// 连续签到天数计算letstreakCount=0;letcheckDate=newDate();while(true){lety=checkDate.getFullYear();letm=(checkDate.getMonth()+1).toString().padStart(2,'0');letd=checkDate.getDate().toString().padStart(2,'0');letds=`${y}-${m}-${d}`;// 查找这一天是否有日记letfound=false;for(leti=0;i<this.entries.length;i++){if(this.entries[i].date===ds){found=true;break;}}if(found){streakCount++;checkDate.setDate(checkDate.getDate()-1);// 往前推一天}else{break;// 断签了就停止}}this.streak=streakCount;}

算法思路:从今天开始,逐天往前检查是否有日记记录,直到某一天没有记录为止。这个算法简单直观,时间复杂度 O(n×m)。

1.5 UI 构建

顶部标题栏
Row(){Text('📖 心情日记').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#333333')Blank()Text('📊').fontSize(22).onClick(()=>{router.pushUrl({url:'pages/StatsPage'});})Text(' 👤').fontSize(22).onClick(()=>{router.pushUrl({url:'pages/ProfilePage'});})}.width('94%').padding({top:16,bottom:8})

设计要点

  • 使用Blank()实现左右对齐
  • 图标直接使用 Emoji,省去图标库依赖
  • 标题左对齐,功能图标右对齐
今日心情卡片
Column(){Row(){Text(getTodayShort()).fontSize(14).fontColor('rgba(255,255,255,0.8)')Blank()Text('连续 '+this.streak+' 天 🔥').fontSize(12).backgroundColor('rgba(255,255,255,0.2)').padding({left:8,right:8,top:2,bottom:2}).borderRadius(10)}.width('100%')if(this.hasTodayEntry&&this.todayEntry){// 已写日记:展示心情图标+标题Text(getMoodInfo(this.todayEntry.mood).icon).fontSize(48)Text(getMoodInfo(this.todayEntry.mood).label).fontSize(18).fontColor('#FFFFFF')Text(this.todayEntry.title).fontSize(14)Text('点击查看详情 >').fontSize(12).fontColor('rgba(255,255,255,0.6)')}else{// 未写日记:展示写日记入口Text('🤔').fontSize(48)Text('今天还没记录心情').fontSize(16).fontColor('#FFFFFF')Button('写一篇日记').backgroundColor('#FFFFFF').fontColor('#6C63FF').borderRadius(18).onClick(()=>{router.pushUrl({url:'pages/WritePage'});})}}.padding(20).backgroundColor('#6C63FF').borderRadius(16)

关键技术点

技术说明
条件渲染if/else根据hasTodayEntry展示不同内容
半透明颜色rgba(255,255,255,0.8)在深色背景上显示浅色文字
内联圆角徽章连续签到天数用胶囊样式展示
按钮白色背景+主题色文字反白设计,突出按钮
@Builder 装饰器复用
@BuilderquickBtn(icon:string,label:string,onClick:()=>void){Column(){Text(icon).fontSize(26).width(48).height(48).textAlign(TextAlign.Center).backgroundColor('#FFFFFF').borderRadius(24)Text(label).fontSize(12).fontColor('#666666').margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(onClick)}// 使用Row(){this.quickBtn('✏️','写日记',()=>{router.pushUrl({url:'pages/WritePage'});})this.quickBtn('📅','日历',()=>{router.pushUrl({url:'pages/CalendarPage'});})this.quickBtn('📊','统计',()=>{router.pushUrl({url:'pages/StatsPage'});})this.quickBtn('👤','我的',()=>{router.pushUrl({url:'pages/ProfilePage'});})}

@Builder 的优势

  • 避免重复代码,一处定义多处使用
  • 支持参数传递,灵活配置
  • 函数式风格,逻辑清晰

1.6 日记列表项

@BuilderdiaryRow(item:DiaryEntry){Row(){Text(getMoodInfo(item.mood).icon).fontSize(28).width(44).height(44).backgroundColor('#F5F5F5').borderRadius(22)Column(){Text(item.title).fontSize(15).fontWeight(FontWeight.Medium)Text(item.date.slice(5)+' · '+getMoodInfo(item.mood).label).fontSize(12).fontColor('#999999').margin({top:2})}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({left:10})Text('>').fontSize(16).fontColor('#CCCCCC')}.padding({left:14,right:14,top:8,bottom:8}).height(60).onClick(()=>{router.pushUrl({url:'pages/CalendarPage'});})}

二、写日记页面(WritePage.ets)全面拆解

2.1 页面功能

┌──────────────────────────────────────┐ │ < 返回 写日记 │ ← 顶部导航栏 ├──────────────────────────────────────┤ │ 📅 2025-01-20 │ │ │ │ 今天的心情 │ │ ┌────┬────┬────┐ │ │ │ 😊 │ 😌 │ 😢 │ │ │ │开心│平静│难过│ │ ← 心情选择器 │ ├────┼────┼────┤ │ │ │ 😠 │ 🤩 │ 😴 │ │ │ │生气│兴奋│疲惫│ │ │ ├────┼────┼────┤ │ │ │ 😰 │ 🙏 │ 😐 │ │ │ │焦虑│感恩│一般│ │ │ └────┴────┴────┘ │ │ │ │ 标题 * │ │ ┌────────────────────────────┐ │ │ │ 给今天的日记取个标题 │ │ ← TextInput │ └────────────────────────────┘ │ │ │ │ 正文 │ │ ┌────────────────────────────┐ │ │ │ │ │ │ │ 写下今天的感受和故事... │ │ ← TextArea │ │ │ │ │ └────────────────────────────┘ │ │ │ │ 标签(用逗号分隔) │ │ ┌────────────────────────────┐ │ │ │ 如: 工作,生活,旅行 │ │ ← TextInput │ └────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ 保存日记 │ │ ← 主题色按钮 │ └──────────────────────────────┘ │ └──────────────────────────────────────┘

2.2 状态变量

@Statemoods:MoodInfo[]=[];// 所有心情选项@StateselectedMood:MoodLevel=MoodLevel.HAPPY;// 选中的心情@Statetitle:string='';// 标题@Statecontent:string='';// 正文@Statetags:string='';// 标签@StatetodayDate:string='';// 今天的日期

2.3 心情选择器:Grid 网格布局

Text('今天的心情').fontSize(14).fontColor('#999999')Grid(){ForEach(this.moods,(m:MoodInfo)=>{GridItem(){Column(){Text(m.icon).fontSize(32).margin({bottom:2})Text(m.label).fontSize(11).fontColor(this.selectedMood===m.level?'#6C63FF':'#999999')}.width('100%').padding({top:10,bottom:10}).backgroundColor(this.selectedMood===m.level?'#EEEAFF':'#F8F8F8').borderRadius(12).alignItems(HorizontalAlign.Center)}.onClick(()=>{this.onMoodClick(m.level);})},(m:MoodInfo)=>m.level)}.columnsTemplate('1fr 1fr 1fr')// 3列等宽.columnsGap(8).rowsGap(8).width('90%')

Grid 布局要点

  • columnsTemplate('1fr 1fr 1fr'):3 列等分
  • 选中态:紫色背景 (#EEEAFF) + 紫色文字 (#6C63FF)
  • 未选态:灰色背景 (#F8F8F8) + 灰色文字 (#999999)
  • 点击后更新selectedMood,通过===判断高亮

2.4 文本输入组件

// 标题输入Text('标题 *')TextInput({placeholder:'给今天的日记取个标题',text:this.title}).fontSize(16).height(44).placeholderColor('#CCCCCC').onChange((v:string)=>{this.title=v;})// 正文输入(多行)Text('正文')TextArea({placeholder:'写下今天的感受和故事...',text:this.content}).fontSize(15).height(180)// 固定高度.backgroundColor('#F9F9F9').borderRadius(8).onChange((v:string)=>{this.content=v;})// 标签输入Text('标签(用逗号分隔)')TextInput({placeholder:'如: 工作,生活,旅行',text:this.tags}).onChange((v:string)=>{this.tags=v;})

TextInput vs TextArea

组件用途行数高度行为
TextInput单行文本(标题、标签)1固定
TextArea多行文本(正文)多行可设置固定高度

2.5 保存逻辑

saveEntry():void{// 标题不能为空if(this.title.trim()===''){return;}// 构造日记条目letentry:DiaryEntry={id:generateId(),date:this.todayDate,mood:this.selectedMood,title:this.title.trim(),content:this.content.trim(),tags:this.tags.trim()};// 存入全局状态letstored=AppStorage.get<DiaryEntry[]>('entries');letlist:DiaryEntry[]=stored?stored:[];list.unshift(entry);// 新日记插到最前面AppStorage.set<DiaryEntry[]>('entries',list);// 返回上一页router.back();}

代码细节

  • list.unshift(entry):新日记插入数组头部,实现时间倒序
  • title.trim():去除首尾空格
  • router.back():保存后自动返回首页,首页onPageShow触发刷新

三、交互设计细节

3.1 导航交互

操作实现方式反馈
返回router.back()返回上一页
跳转统计页router.pushUrl({ url: 'pages/StatsPage' })推入新页面
保存日记saveEntry() + router.back()保存后返回

3.2 状态反馈

// 心情选中反馈:颜色+背景同时变化.backgroundColor(this.selectedMood===m.level?'#EEEAFF':'#F8F8F8').fontColor(this.selectedMood===m.level?'#6C63FF':'#999999')

双重反馈(背景色 + 文字颜色)让选中状态一目了然。

3.3 空状态处理

if(this.recentEntries.length===0){Column(){Text('还没有日记,开始记录今天的心情吧!').fontSize(15).fontColor('#CCCCCC')}.width('100%').height(120).justifyContent(FlexAlign.Center)}

空状态展示友好的提示文字,而不是直接显示空白页面。

四、页面间数据一致性

4.1 数据流

WritePage (保存) │ ├─ AppStorage.set('entries', newList) │ └─ router.back() │ Index.onPageShow() │ ├─ AppStorage.get('entries') └─ 重新渲染 UI

4.2 关键保证

所有页面在onPageShow中重新加载数据:

onPageShow():void{this.loadData();// 确保每次显示都同步最新数据}

这个设计确保无论用户在哪个页面修改了数据(新增、删除),其他页面回到前台时都能看到最新状态。

五、样式系统与主题设计

5.1 主题色定义

用途颜色值使用场景
主色#6C63FF按钮、标题、选态
主色浅色#EEEAFF选中背景
背景色#F8F9FA页面底色
卡片色#FFFFFF卡片、列表项
主文字#333333标题、正文
辅助文字#999999日期、标签
浅色文字#CCCCCC占位符

5.2 圆角系统

// 大圆角卡片.borderRadius(16)// 首页今日心情卡片// 中圆角组件.borderRadius(12)// 快捷按钮、卡片// 小圆角元素.borderRadius(8)// TextArea// 胶囊圆角.borderRadius(24)// 按钮

六、下篇预告

本篇我们完成了首页和写日记页面的开发。下一篇将进入更复杂的交互实现:

  • 日历视图:月份导航、日期网格、心情标记
  • 数据统计:统计卡片、心情分布柱状图、7天心情趋势
  • 你会学到 Grid 网格的高级用法、柱状图的实现思路

敬请期待!


如果你在 UI 开发中遇到问题,欢迎留言交流!

http://www.jsqmd.com/news/993153/

相关文章:

  • 【收藏级·2026版】AI Agent记忆技术演进全解析
  • AI 泡沫走到哪一步了?
  • Three.js 实战:用 Vue3 打造一个可交互的3D人体解剖查看器(含完整源码)
  • 2026年实测10款降AIGC平台推荐:免费与付费全对比,毕业论文淡化AIGC痕迹必看
  • 在AI的帮助下理解spring的启动过程
  • 退役的旧手机千万别去小区门口换不锈钢盆!实测爱回收靠谱吗 - 新闻快传
  • DNF容器化部署实战:从零构建阿拉德大陆的容器化秘籍
  • MC9S08SH8定时器与串口配置详解:从寄存器到代码实战
  • 43k Stars 的 CV 神器:supervision 让你 5 行代码搞定目标检测可视化
  • 富阳家长放心之选:华浙培训联合浙经院下沙高复,助力富阳学子圆梦理想高职 - 弱书讲升学
  • DataIn.cs 完整解析 — 跨模块数据入队引擎
  • 163MusicLyrics:3分钟掌握免费歌词下载,从此告别音乐播放器无字幕烦恼
  • MSC8103网络DSP硬件设计:时序规范与FC-PBGA引脚规划实战
  • 小米穿戴设备表盘设计:从零到一的视觉创作指南
  • 戴尔笔记本风扇控制实战:DellFanManagement深度配置与性能调优指南
  • 《新闻资讯》五、直播模块实现指南
  • htdemucs_6s音乐源分离:6秒完成六音轨精准分离的革命性工具
  • 【Godot4.2】2D导航实战 - 基于AStar2D构建动态障碍寻路系统
  • 从‘火车调度’到‘栈’的应用:一个PTA真题带你玩转数据结构核心概念
  • 2026黔东贵金属回收黄金回收白银回收铂金回收店铺怎么挑?5 家不压价线下实体店完整测评清单 + 商家联络方式 - 信誉隆金银铂奢回收
  • 终极Mac菜单栏整理方案:用Ice告别杂乱,重获桌面控制权
  • 5个专业技巧:让DS4Windows成为你的PlayStation手柄终极PC伴侣
  • 用MonkCode做全栈开发:前端后端数据库一条龙
  • freeCodeCamp认证项目:纯HTML5+CSS3响应式调查表(含全平台预览与官方测试通过)
  • 中望3D 2021 坯料/包容体:从基础概念到高效应用的实战指南
  • NewTab-Redirect:免费定制Chrome新标签页的终极指南
  • 沈阳高口碑黄金铂金回收白银回收实体老店排行 5 家靠谱门店电话地址全收录 - 诚金汇钻回收公司
  • 别再死记硬背P波S波了!用Python模拟地震波传播,直观理解勘探原理
  • 港科大EMBA中英双语校友质量解析:圈层实力、成长价值与行业影响力
  • 2026重庆LV包包回收段位榜单,收的顶王者段位独占榜首 - 奢侈品回收测评