当前位置: 首页 > 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.rkmt.cn/news/1504910.html

相关文章:

  • 火绒安全软件
  • 【收藏级·2026版】AI Agent记忆技术演进全解析
  • Three.js 实战:用 Vue3 打造一个可交互的3D人体解剖查看器(含完整源码)
  • 在AI的帮助下理解spring的启动过程
  • 小米穿戴设备表盘设计:从零到一的视觉创作指南
  • htdemucs_6s音乐源分离:6秒完成六音轨精准分离的革命性工具
  • 沈阳高口碑黄金铂金回收白银回收实体老店排行 5 家靠谱门店电话地址全收录 - 诚金汇钻回收公司
  • COMSOL仿真揭秘:母线板温升下的电阻动态响应
  • 企业微信模板卡片消息实战:一个PHP代码示例搞定合同审批提醒(含版本兼容说明)
  • 从[特殊字符]到[特殊字符]:手把手教你用Python爬虫批量下载并分类所有Emoji图片(附代码)
  • OpenCore Simplify:重构黑苹果配置的技术哲学与工程实践
  • Windows下用FFmpeg sws_scale做RGB图像缩放+多图定位叠加的完整工程包
  • 2026深圳GEO优化公司推荐:昊客网络助力企业AI搜索时代抢占先机 - 猫头鹰AI推广
  • 用Python+Matplotlib可视化旋转曲面:从抛物线到双曲面的3D建模实战
  • 2026晋中贵金属回收黄金回收白银回收铂金回收店铺怎么挑?5 家不压价线下实体店完整测评清单 + 商家联络方式 - 信誉隆金银铂奢回收
  • Codesys ST语言实战:手把手教你封装一个可复用的循环队列功能块(附完整代码)
  • string类的模拟实现
  • MPC755嵌入式处理器电源与时序设计:硬件稳定性的关键解析
  • 2026攀枝花贵金属回收黄金回收白银回收铂金回收店铺怎么挑?5 家不压价线下实体店完整测评清单 + 商家联络方式 - 信誉隆金银铂奢回收
  • Python-Pandas从入门到实战:数据分析的“瑞士军刀”全指南
  • ExtractorSharp终极指南:零基础掌握游戏资源编辑的完整教程
  • S32K SPI实战:从时序图到代码实现的配置指南
  • 2026年华为云OpenClaw/Hermes Agent配置Token Plan安装步骤全公开
  • 声音的万花筒:在数字音乐迷宫中寻找属于自己的旋律
  • 如何利用SMUDebugTool深度调优AMD Ryzen处理器性能
  • 智谱与MiniMax港股股价分化,MiniMax调价风波下如何平衡C端与B端业务?
  • 2026年国产清洁度显微镜哪家好?苏州品恩VS进口品牌大测评 - 品牌推荐大师1
  • MC9S12NE64以太网硬件设计:从电气特性到PCB布局的实战指南
  • 武汉南华光电职业技术学校2026年招生简章(最新版) - 善良的阿良
  • 四川芥酸生产厂家实力排行及应用适配指南 - 奔跑123