今天摸鱼了吗APP开发实战:基于HarmonyOS API 24的多层Stack与定时器应用
摸鱼计时、老板警报、一键切换假工作界面——一个充满幽默感的办公场景模拟器。本文从多层Stack布局到setInterval计时器,从演技评分算法到数据持久化,完整记录开发全过程。
一、项目缘起:为什么做"今天摸鱼了吗"
1.1 创意来源
“摸鱼”——这个源自网络的热词,已经成为当代职场文化不可或缺的一部分。它描述的是一种在工作时间偷偷做与工作无关的事情的行为,带着自嘲和幽默的色彩。
"今天摸鱼了吗"APP正是抓住了这个文化梗,将其转化为一个有趣的互动游戏。它不是鼓励摸鱼,而是用一种戏谑的方式呈现职场中的小趣味。
1.2 产品设计
| 功能 | 体验目标 |
|---|---|
| 🐟 摸鱼计时 | 启动APP即开始计时,看到"摸鱼时长"不断增加,有种"罪恶的快感" |
| 🚨 老板警报 | 随机弹出警报或手动演习,营造紧张感 |
| 🎭 演技评分 | 每次警报后给出评分和评语,像游戏一样有反馈 |
| 📊 假装工作界面 | 一键切换到仿Excel界面,增加"安全感" |
1.3 技术选型
| 维度 | 选择 | 理由 |
|---|---|---|
| UI架构 | 多层Stack | 需要叠加普通UI、弹窗、假界面、结果浮层 |
| 计时器 | setInterval | 摸鱼计时每秒更新 |
| 延迟 | setTimeout | 模拟警报持续时间、结果自动关闭 |
| 数据持久化 | Preferences | 存储演技历史记录 |
| 版本 | API 24 | HarmonyOS 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) - 使用
justifyContent和alignItems实现居中 - 内层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更新显示timerId是number类型(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中,setInterval和setTimeout的返回值类型是什么?
// 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中,setInterval和setTimeout都返回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 代码量分布
| 模块 | 行数 | 占比 |
|---|---|---|
| 类型定义 | ~10 | 2% |
| 变量声明 | ~25 | 5% |
| 游戏逻辑 | ~100 | 21% |
| UI辅助 | ~50 | 11% |
| UI视图 | ~280 | 60% |
八、总结与展望
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() | 时间格式化 |
