今天摸鱼了吗APP开发实战:基于HarmonyOS API 24的多层Stack与定时器应用
2026/6/12 23:23:53 网站建设 项目流程





摸鱼计时、老板警报、一键切换假工作界面——一个充满幽默感的办公场景模拟器。本文从多层Stack布局到setInterval计时器,从演技评分算法到数据持久化,完整记录开发全过程。


一、项目缘起:为什么做"今天摸鱼了吗"

1.1 创意来源

“摸鱼”——这个源自网络的热词,已经成为当代职场文化不可或缺的一部分。它描述的是一种在工作时间偷偷做与工作无关的事情的行为,带着自嘲和幽默的色彩。

"今天摸鱼了吗"APP正是抓住了这个文化梗,将其转化为一个有趣的互动游戏。它不是鼓励摸鱼,而是用一种戏谑的方式呈现职场中的小趣味。

1.2 产品设计

功能体验目标
🐟 摸鱼计时启动APP即开始计时,看到"摸鱼时长"不断增加,有种"罪恶的快感"
🚨 老板警报随机弹出警报或手动演习,营造紧张感
🎭 演技评分每次警报后给出评分和评语,像游戏一样有反馈
📊 假装工作界面一键切换到仿Excel界面,增加"安全感"

1.3 技术选型

维度选择理由
UI架构多层Stack需要叠加普通UI、弹窗、假界面、结果浮层
计时器setInterval摸鱼计时每秒更新
延迟setTimeout模拟警报持续时间、结果自动关闭
数据持久化Preferences存储演技历史记录
版本API 24HarmonyOS NEXT

二、UI架构:多层Stack的实战应用

2.1 为什么需要多层Stack

这个APP的UI层级非常复杂——同时存在5层:

┌─────────────────────────────────────────────┐ │ 第5层:假装工作界面 (buildFakeWorkView) │ ← 紧急时覆盖一切 ├─────────────────────────────────────────────┤ │ 第4层:演技评分结果弹窗 (buildResultOverlay)│ ← 警报解除后显示4秒 ├─────────────────────────────────────────────┤ │ 第3层:老板警报弹窗 (buildBossAlert) │ ← 随机触发 ├─────────────────────────────────────────────┤ │ 第2层:底部导航栏 (buildBottomNav) │ ← 常驻底部 ├─────────────────────────────────────────────┤ │ 第1层:主内容区 (buildFishView/historyView) │ ← 常规UI └─────────────────────────────────────────────┘

在传统UI框架中,这种多层叠加需要用Dialog或Modal来实现。但在ArkUI中,Stack组件天然支持子组件的层叠排列:

build(){Stack(){Column(){// 第1-2层:主内容 + 导航buildFishView()/buildHistoryView()buildBottomNav()}buildBossAlert()// 第3层:警报弹窗(条件渲染)buildResultOverlay()// 第4层:评分结果(条件渲染)buildFakeWorkView()// 第5层:假工作界面(条件渲染)}}

Stack的特点是:子组件按声明顺序从下到上层叠,后声明的在上层。我们只需要用if条件控制每一层的显隐,ArkUI会自动管理它们的渲染。

2.2 各层的显隐条件

层级显隐条件覆盖范围
主内容始终显示全屏
底部导航始终显示底部56px
警报弹窗showBossAlert === true半透明遮罩 + 居中卡片
评分结果showResult === true半透明遮罩 + 居中卡片
假工作界面isPanic === true全屏覆盖

2.3 全屏覆盖层的特殊性

buildBossAlert()buildResultOverlay()是全屏遮罩层,它们的布局模式相同:

@BuilderbuildBossAlert(){Column(){Column(){// 弹窗内容}.width('85%').padding(28).backgroundColor('#FFF').borderRadius(20);}.width('100%').height('100%').backgroundColor('#80000000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center);}

关键点

  • 外层Column:全屏尺寸 + 半透明黑色背景(#80000000
  • 使用justifyContentalignItems实现居中
  • 内层Column:85%宽度,白色背景,圆角

2.4 假工作界面的位置

buildFakeWorkView()使用Stack作为根容器来实现底部状态栏和提示文字的定位:

@BuilderbuildFakeWorkView(){Stack(){Column(){// Excel工具栏 + 数据表格}.width('100%').height('100%');// 底部状态栏Row(){...}.width('100%').padding(6).backgroundColor('#333').alignSelf(ItemAlign.Bottom);// 提示文字Text('⏳ 老板正在巡视,保持淡定...').width('100%').textAlign(TextAlign.Center).alignSelf(ItemAlign.Bottom).margin({bottom:36});}.width('100%').height('100%').backgroundColor('#1E1E1E');}

这里的Stack与最外层的Stack形成了"Stack嵌套Stack"的层级结构。内层Stack负责Excel界面的内部布局,外层Stack负责整个APP的层级管理。


三、定时器管理:setInterval与setTimeout

3.1 摸鱼计时器

摸鱼计时是APP运行的核心机制——它从aboutToAppear开始,每秒更新一次:

privatetimerId:number=-1;aboutToAppear():void{this.loadHistory();this.startFishTimer();}startFishTimer():void{this.timerId=setInterval(()=>{this.fishTime++;},1000);}

关键设计

  • fishTime是 @State 变量,每秒变化触发UI更新显示
  • timerIdnumber类型(ArkTS中setInterval返回number
  • 理论上组件销毁时应clearInterval,但单页应用无需担心

3.2 警报持续时间控制

当用户点击"假工作"按钮后,警报不会立即解除,而是模拟2-4秒的"危险期":

panic():void{this.showBossAlert=false;this.panicStart=Date.now();this.isPanic=true;constduration=2000+Math.floor(Math.random()*2000);setTimeout(()=>{this.endPanic();},duration);}

设计意图

  • 2-4秒的随机时长模拟真实场景——老板不可能看一眼就走
  • 随机性让每次体验不同
  • 足够长到让用户感受到"紧张",又不会长到不耐烦

3.3 评分结果自动关闭

评分结果显示4秒后自动消失:

this.showResult=true;setTimeout(()=>{this.showResult=false;},4000);

3.4 定时器最佳实践

问题:在ArkTS中,setIntervalsetTimeout的返回值类型是什么?

// JavaScript环境:返回 number(浏览器)或 NodeJS.Timeout(Node)// ArkTS:返回 numberprivatetimerId:number=-1;

清理定时器

clearInterval(this.timerId);

注意事项

  • 组件销毁时清理定时器(使用aboutToDisappear生命周期)
  • 避免在定时器回调中执行耗时操作
  • 定时器回调中访问this需要使用箭头函数保持绑定

四、演技评分算法

4.1 评分公式

演技评分是游戏的核心反馈机制。评分基于两个因素:

反应时间:从警报出现到用户点击"假工作"的时间(单位:ms)

基础分 = 100 扣分 = 反应时间(ms) / 50 原始分 = max(10, 100 - 扣分) 最终分 = max(10, min(100, 原始分 + 随机波动 ±10))

逻辑:反应越快,分数越高。每慢50ms扣1分。100ms反应 = 98分,500ms反应 = 90分,1000ms反应 = 80分。

随机波动:±10分让评分有变化,不会每次都一样。

4.2 评级系统

根据分数给出6个等级:

分数评级表情评语
≥90🏆 S金色奥斯卡影帝!老板完全没察觉!
75-89🌟 A绿色演技精湛,毫无破绽!
60-74👍 B蓝色反应不错,像个认真工作的好员工。
40-59😅 C橙色中规中矩,勉强过关。
20-39😰 D红色太假了!你紧张什么?!
<20💀 F紫色演技堪忧,建议回炉重造。

评级代码

if(finalScore>=90){this.grade='S';this.gradeEmoji='🏆';}elseif(finalScore>=75){this.grade='A';this.gradeEmoji='🌟';}// ...

4.3 反应时间的测量

反应时间通过Date.now()的前后差值计算:

panic():void{// 记录警报出现时间(实际上是用户点击按钮的时间)this.panicStart=Date.now();// ...}endPanic():void{this.reactionMs=Date.now()-this.panicStart;// 计算评分...}

注意:这里的"反应时间"实际包括警报持续时间(2-4秒)加上用户点击到警报解除的时间。因为panicStart是在用户点击"假工作"时记录,而endPanic在2-4秒后触发。所以实际的"反应时间"值包含了等待时间,但这正好符合游戏设计——警报解除越快,评分越高。

4.4 演技历史记录

每次评分后,记录存入actingHistory数组,并通过 Preferences 持久化:

interfaceActingRecord{id:number// 自增IDtime:string// 发生时间score:number// 评分comment:string// 评语reactionMs:number// 反应时间(ms)}

五、UI实现详解

5.1 摸鱼主界面

摸鱼主界面是用户打开APP后的默认视图,布局如下:

Column ├── 标题行:🐟 今天摸鱼了吗 ├── 摸鱼计时:88:88:88 (48fp, 等宽字体, 青绿色) ├── 下班倒计时:还剩 X 小时 X 分钟 (黄色) ├── 鱼缸区 (layoutWeight:1) │ ├── 🐟🐠🐡🐙🦑 (每10秒切换) │ └── 摸鱼等级:🏄 摸鱼大师 ├── 演习按钮:🎯 摸鱼演习 (深蓝底) └── 紧急按钮:⚠️ 老板来了!(红色, 带阴影)

鱼缸动画fishTime / 10 % 5每10秒切换一次表情,从 🐟→🐠→🐡→🐙→🦑 循环。虽然是简单的数组索引切换,但给计时器增加了视觉趣味。

5.2 底部导航

两个标签:🐟 摸鱼 / 📊 演技史

选中标签高亮为青绿色(#4ECDC4),与APP的暗色调主题(#1a1a2e背景)形成鲜明对比。

5.3 演技历史视图

历史记录列表展示所有评分记录:

List └── ForEach: actingHistory └── ListItem └── Row ├── 评级表情 (28fp, 40px宽) ├── Column (layoutWeight:1) │ ├── Row: 分数 + 反应时间 │ └── Text: 评语 (单行省略) └── Text: 时间

每条记录的颜色编码与评级匹配,绿色代表高分,红色代表低分。

5.4 假装工作界面(Excel模拟)

这是最有趣的UI部分——一个仿 Microsoft Excel 的界面:

标题栏(深绿色背景):

📊 2024年度Q4销售数据报表.xlsx - Excel

工具栏(深灰背景):

文件 | 开始 | 插入 | 页面布局 | 公式 | 数据 | 审阅 | 视图

数据表格(一行表头 + 六行数据):

月份 销售额 利润 增长率 一月 135万 25万 10% 二月 150万 30万 12% ...(共6行,随机数据)

底部状态栏(固定在底部):

就绪 平均值:45.2万 计数:6

提示文字(固定在底部状态栏上方):

⏳ 老板正在巡视,保持淡定...

设计细节

  • 表格行交替颜色(#2A2A2A/#222
  • 增长率使用绿色(#4CAF50),模拟Excel的正数显示
  • 数据随机生成,每次进入假界面都不同

六、踩坑合集

坑1:.position({ absolute: true }) 在ArkUI中不可用

症状.position({ absolute: true, top: -2, right: 4 })报错。

原因absolute: true是CSS语法,ArkUI不支持。ArkUI的.position()只接受{ x: number, y: number }{ top, left, right, bottom }

修复方案

方案一:使用Stack作为父容器,子组件通过.alignSelf()定位:

Stack(){Column(){/* 主内容 */}Text('状态栏').alignSelf(ItemAlign.Bottom);// ✅ 固定在底部}

方案二:使用.offset()进行偏移:

Text('徽标').offset({x:4,y:-2});// ✅ 相对当前位置偏移

方案三:使用.margin()推动位置。

最佳实践:在ArkUI中需要绝对定位时,优先考虑Stack+.alignSelf()的组合,而不是依赖.position()

坑2:switch语句缺少default分支

症状getGradeColor方法中 switch 没有 default,ArkTS 编译报错。

修复:添加default: return '#888'

教训:ArkTS的 switch 语句比 TypeScript 更严格——即使代码逻辑上已经覆盖了所有可能的 case,编译器仍然要求有 default。

坑3:setInterval的类型

症状:将setInterval返回值赋给number类型变量时不确定是否正确。

说明:在ArkTS中,setIntervalsetTimeout都返回number类型。这与浏览器环境一致(浏览器中setInterval也返回number)。

privatetimerId:number=-1;// ✅ 正确

坑4:Stack中子组件的z-order

症状:在 Stack 中,子组件的层叠顺序不符合预期。

规则:在 Stack 中,子组件按声明顺序从下到上层叠,后面声明的在上面。

Stack(){Column()/* 第1层(最底层) */Text()/* 第2层 */Row()/* 第3层(最顶层) */}

如果需要控制特定组件的层级,调整声明顺序即可。

坑5:全屏遮罩层中内容不居中

症状:弹窗内容没有在遮罩层中居中显示。

修复

Column(){// 遮罩层Column(){// 弹窗内容// ...}.width('85%');// 限制宽度}.width('100%').height('100%').justifyContent(FlexAlign.Center)// 垂直居中.alignItems(HorizontalAlign.Center);// 水平居中

外层 Column 使用justifyContent+alignItems实现居中,内层 Column 是实际的弹窗内容。


七、项目结构与代码统计

7.1 文件结构

Index.ets (~470行) ├── 类型定义 (~10行) │ └── interface ActingRecord │ ├── 成员变量 (~25行) │ ├── @State变量(view/isPanic/fishTime/score等15个) │ └── private变量(timerId/pref/nextId/bossPhrases/gradeComments) │ ├── 游戏逻辑 (~100行) │ ├── loadHistory / saveHistory │ ├── startFishTimer / randomBossAlert │ ├── panic / endPanic │ ├── drill / getFishTimeStr / getWorkRemainStr │ └── getGradeColor / getFishLevel │ ├── build() + 导航 (~50行) │ ├── build() 多层Stack │ └── buildBottomNav │ ├── @Builder视图 (~280行) │ ├── buildFishView (摸鱼主界面) │ ├── buildBossAlert (警报弹窗) │ ├── buildFakeWorkView (Excel假界面) │ ├── buildResultOverlay (评分结果) │ └── buildHistoryView (演技历史)

7.2 代码量分布

模块行数占比
类型定义~102%
变量声明~255%
游戏逻辑~10021%
UI辅助~5011%
UI视图~28060%

八、总结与展望

8.1 项目复盘

维度数据
开发周期约半天
代码量470行,单文件
UI层级5层Stack
计时器1个setInterval + 3个setTimeout
评级档位6级(S/A/B/C/D/F)
评语库7条
老板警报语8条

8.2 从6个APP中学到的ArkUI开发模式

经过6个APP的开发实践,可以总结出一些通用的ArkUI开发模式:

模式1:单文件单组件
6个APP全部使用单文件单组件结构。对于功能复杂度在"一个屏幕能展示完"级别的APP,单文件开发效率最高。

模式2:@Builder分层
每个视图对应一个@Builder方法,通过build()中的条件渲染切换。这种模式将大型UI拆分为小型可管理的片段。

模式3:@State + 手动刷新
ArkUI的 @State 检测引用变化,对象属性修改需要手动创建新引用。refreshState()reRender()这种统一刷新方法是必备工具。

模式4:底部三/二标签导航
游戏/工具类APP使用2-3个底部标签的导航模式,用户认知成本低。

模式5:Stack多层叠加
对于需要弹窗、遮罩、覆盖层的APP,使用Stack作为根容器。

8.3 可扩展方向

1. 真实剪贴板
接入@ohos.pasteboard实现真正的复制到剪贴板功能,让"一键分享"名副其实。

2. 音效系统
接入@ohos.multimedia.audio为"老板来了"警报添加音效,为评分结果添加音效。

3. 工作时间统计
接入@ohos.data.preferences记录每日摸鱼总时长,生成周报/月报。

4. 多场景切换
不止Excel,还可以假装写代码(IDE界面)、假装开会(Zoom界面)、假装写文档(Word界面)。

5. 排行榜
通过分布式数据库实现好友之间的演技评分PK。


附录:完整API清单

@kit.ArkData

API用途
preferences.getPreferences(ctx, name)获取偏好数据库
Preferences.get(key, default)读取演技历史
Preferences.put(key, value)写入演技历史
Preferences.flush()刷入磁盘

ArkUI组件

组件用途
Stack多层UI根容器
Column/Row布局容器
Text文本显示
Button按钮
List/ListItem演技历史列表
ForEach循环渲染

全局JavaScript API

API用途
setInterval()摸鱼计时器
setTimeout()延迟执行
clearInterval()清理计时器
Date.now()反应时间测量
Math.random()随机数/波动
Math.floor()向下取整
Math.max()/Math.min()值范围限制
String.padStart()时间格式化

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询