精膳通智慧食堂的鸿蒙开发之旅:因 uni-app 而简化,为国产生态而让利
一、困境:想做鸿蒙版,却卡在 “入门关”
作为精膳通智慧食堂的开发负责人,今年随时鸿蒙用户量的暴增,我们接到了大量学校、企业客户的需求 ——“能不能出个鸿蒙版 APP?”。彼时,我们团队主攻的是微信小程序和安卓版,核心技术栈是 Vue,对鸿蒙原生开发几乎一窍不通。
最初,我们试着调研鸿蒙原生开发:要学全新的 ArkTS 语言,得搭建复杂的开发环境,还得单独招懂鸿蒙的工程师,算下来不仅开发周期要延长 2 个月,人力成本还要增加近一倍。更头疼的是,客户还希望后续能同步适配小程序,要是用原生开发,相当于要维护两套完全不同的代码,团队根本扛不住这样的压力。
那段时间,我天天在技术社区逛,就想找个能 “省劲儿” 的方案 —— 既能让我们用熟悉的技术做鸿蒙开发,又能兼顾多端适配,直到朋友推荐了 uni-app。
二、转机:uni-app 让我们 “零门槛” 闯进鸿蒙生态
第一次打开 uni-app 的开发文档,我就眼前一亮:“支持 Vue 语法,一键编译鸿蒙安装包”—— 这不正是我们要找的吗?
1). 不用学新语言,老团队直接上手
团队里的程序员都是做 Vue 出身,之前写小程序的代码稍加修改就能用在 uni-app 里。比如做食堂菜品展示页面,之前小程序里写好的组件,复制到 uni-app 项目里,改改适配逻辑,就能在鸿蒙模拟器上跑起来。没有一个人抱怨 “学不会”,连刚入职半年的新人,跟着官方教程走,3 天就能独立开发简单页面。
对比之前调研的原生开发,这简直是 “降维打击”—— 不用花 1 个月让团队学 ArkTS,不用重新梳理技术逻辑,原有的开发经验完全能复用,相当于 “零成本” 就拥有了开发鸿蒙应用的能力。
2). 跨平台一次搞定,不用做 “重复工”
客户想要的 “鸿蒙 + 小程序” 双端,在 uni-app 里根本不是问题。我们开发 “食堂订餐” 核心功能时,只写了一套代码:在鸿蒙端能适配手机、平板(有些企业食堂用平板点餐),同步编译成小程序后,用户在微信里也能正常下单。
之前做双端开发,得安排两个程序员分别写安卓和小程序,现在一个人就能搞定多端,开发周期直接从预估的 2 个月压缩到 3 周。有次客户临时要加 “菜品评价” 功能,我们改完一套代码,同步发布到鸿蒙和小程序,当天就上线了,客户都惊讶 “怎么这么快”。
三、惊喜:打包发布鸿蒙安装包,原来这么简单
最让我们惊喜的,是用 uni-app 打包鸿蒙安装包的便捷程度,全程在 HBuilderX 里就能搞定,完全不用依赖其他工具。之前打包安卓安装包,要在不同平台间切换配置证书、签名,步骤绕来绕去还总出错,每次打包都得折腾大半天。
但用 uni-app 打包鸿蒙安装包,前期在 HBuilderX 里把证书配置好后,后续生成安装包就是 “一键操作”:打开项目后点击顶部菜单栏的 “发行”,选择 “鸿蒙 APP 打包”,系统会自动调用之前配置好的证书信息,不用再手动输入任何参数,等待几分钟就能直接生成鸿蒙安装包。第一次操作时,我们还捏着把汗担心出问题,结果安装包顺利生成,装到鸿蒙手机上一试,打开速度飞快,滑动点餐、提交订单的操作流畅度,和原生开发的应用没半点差别。
印象特别深的是有次临近客户交付日期,我们突然发现 “食堂套餐优惠标签” 显示有问题,紧急改完代码后,在 HBuilderX 里点一下打包,前后只用了 20 分钟就拿到了新的安装包,准时交付给客户。要是按原生开发的流程,光重新配置打包环境、对接相关工具就得花 1 天时间,肯定赶不上交付进度,那次真的靠 uni-app 救了急。
四、感恩:为国产鸿蒙生态,我们决定免费让利
开发完精膳通智慧食堂鸿蒙版后,我们真切感受到了 uni-app 对中小开发团队的帮助 —— 它让我们不用被技术门槛拦住,不用为多端开发耗费额外成本,轻松跟上了鸿蒙生态发展的步伐。
看着越来越多的客户用上鸿蒙版 APP,反馈 “操作很流畅”“和手机适配得好”,我们也想为国产鸿蒙生态出一份力。经过团队讨论,我们决定:原本收费的精膳通智慧食堂鸿蒙版服务,从现在起免费开放,一直到明年 3 月。
一方面,是感谢 uni-app 降低了我们进入鸿蒙生态的门槛,让我们有能力为客户提供更多选择;另一方面,我们也希望通过免费服务,吸引更多学校、企业使用鸿蒙应用,一起推动国产操作系统生态的发展。
现在,每次有人问我们 “做鸿蒙开发难不难”,我都会说:“不难,只要你会 Vue,用 uni-app 就行 —— 它能帮你省掉 90% 的麻烦,剩下的就是专注做产品。” 未来,我们还会继续用 uni-app 迭代鸿蒙版功能,也期待能和更多开发者一起,在鸿蒙生态里做出更多好用的产品。
五、写在最后:一份来自开发者的感动与回馈
敲完这些文字时,心里满是感慨。回想当初为鸿蒙开发发愁的日子,再看如今借助 uni-app 轻松实现多端落地的成果,真切感受到 uni-app 这些年的成长 —— 它不仅是一个开发工具,更像一位默默助力开发者的伙伴,用越来越强大的功能,一点点降低跨平台开发的门槛,让像我们这样的中小团队也能跟上技术迭代的脚步,在国产生态发展中找到自己的位置。这份便利与支持,让我们在开发路上少走了太多弯路,也让我们对技术赋能产业有了更深的体会。
为了把这份感动与感谢传递下去,也为了回馈同样在使用 uni-app 奋斗的开发者们:如果你的公司有食堂报餐、订餐管理的需求,只要亮出你的 uni-app 开发者身份,联系我们就能免费使用精膳通智慧食堂服务 3 个月。希望能用我们的产品,也能助力国产开发者和创业公司,也期待和大家一起,在 uni-app 搭建的技术桥梁上,共同探索更多国产生态的可能性。
一、困境:想做鸿蒙版,却卡在 “入门关”
作为精膳通智慧食堂的开发负责人,今年随时鸿蒙用户量的暴增,我们接到了大量学校、企业客户的需求 ——“能不能出个鸿蒙版 APP?”。彼时,我们团队主攻的是微信小程序和安卓版,核心技术栈是 Vue,对鸿蒙原生开发几乎一窍不通。
最初,我们试着调研鸿蒙原生开发:要学全新的 ArkTS 语言,得搭建复杂的开发环境,还得单独招懂鸿蒙的工程师,算下来不仅开发周期要延长 2 个月,人力成本还要增加近一倍。更头疼的是,客户还希望后续能同步适配小程序,要是用原生开发,相当于要维护两套完全不同的代码,团队根本扛不住这样的压力。
那段时间,我天天在技术社区逛,就想找个能 “省劲儿” 的方案 —— 既能让我们用熟悉的技术做鸿蒙开发,又能兼顾多端适配,直到朋友推荐了 uni-app。
二、转机:uni-app 让我们 “零门槛” 闯进鸿蒙生态
第一次打开 uni-app 的开发文档,我就眼前一亮:“支持 Vue 语法,一键编译鸿蒙安装包”—— 这不正是我们要找的吗?
1). 不用学新语言,老团队直接上手
团队里的程序员都是做 Vue 出身,之前写小程序的代码稍加修改就能用在 uni-app 里。比如做食堂菜品展示页面,之前小程序里写好的组件,复制到 uni-app 项目里,改改适配逻辑,就能在鸿蒙模拟器上跑起来。没有一个人抱怨 “学不会”,连刚入职半年的新人,跟着官方教程走,3 天就能独立开发简单页面。
对比之前调研的原生开发,这简直是 “降维打击”—— 不用花 1 个月让团队学 ArkTS,不用重新梳理技术逻辑,原有的开发经验完全能复用,相当于 “零成本” 就拥有了开发鸿蒙应用的能力。
2). 跨平台一次搞定,不用做 “重复工”
客户想要的 “鸿蒙 + 小程序” 双端,在 uni-app 里根本不是问题。我们开发 “食堂订餐” 核心功能时,只写了一套代码:在鸿蒙端能适配手机、平板(有些企业食堂用平板点餐),同步编译成小程序后,用户在微信里也能正常下单。
之前做双端开发,得安排两个程序员分别写安卓和小程序,现在一个人就能搞定多端,开发周期直接从预估的 2 个月压缩到 3 周。有次客户临时要加 “菜品评价” 功能,我们改完一套代码,同步发布到鸿蒙和小程序,当天就上线了,客户都惊讶 “怎么这么快”。
三、惊喜:打包发布鸿蒙安装包,原来这么简单
最让我们惊喜的,是用 uni-app 打包鸿蒙安装包的便捷程度,全程在 HBuilderX 里就能搞定,完全不用依赖其他工具。之前打包安卓安装包,要在不同平台间切换配置证书、签名,步骤绕来绕去还总出错,每次打包都得折腾大半天。
但用 uni-app 打包鸿蒙安装包,前期在 HBuilderX 里把证书配置好后,后续生成安装包就是 “一键操作”:打开项目后点击顶部菜单栏的 “发行”,选择 “鸿蒙 APP 打包”,系统会自动调用之前配置好的证书信息,不用再手动输入任何参数,等待几分钟就能直接生成鸿蒙安装包。第一次操作时,我们还捏着把汗担心出问题,结果安装包顺利生成,装到鸿蒙手机上一试,打开速度飞快,滑动点餐、提交订单的操作流畅度,和原生开发的应用没半点差别。
印象特别深的是有次临近客户交付日期,我们突然发现 “食堂套餐优惠标签” 显示有问题,紧急改完代码后,在 HBuilderX 里点一下打包,前后只用了 20 分钟就拿到了新的安装包,准时交付给客户。要是按原生开发的流程,光重新配置打包环境、对接相关工具就得花 1 天时间,肯定赶不上交付进度,那次真的靠 uni-app 救了急。
四、感恩:为国产鸿蒙生态,我们决定免费让利
开发完精膳通智慧食堂鸿蒙版后,我们真切感受到了 uni-app 对中小开发团队的帮助 —— 它让我们不用被技术门槛拦住,不用为多端开发耗费额外成本,轻松跟上了鸿蒙生态发展的步伐。
看着越来越多的客户用上鸿蒙版 APP,反馈 “操作很流畅”“和手机适配得好”,我们也想为国产鸿蒙生态出一份力。经过团队讨论,我们决定:原本收费的精膳通智慧食堂鸿蒙版服务,从现在起免费开放,一直到明年 3 月。
一方面,是感谢 uni-app 降低了我们进入鸿蒙生态的门槛,让我们有能力为客户提供更多选择;另一方面,我们也希望通过免费服务,吸引更多学校、企业使用鸿蒙应用,一起推动国产操作系统生态的发展。
现在,每次有人问我们 “做鸿蒙开发难不难”,我都会说:“不难,只要你会 Vue,用 uni-app 就行 —— 它能帮你省掉 90% 的麻烦,剩下的就是专注做产品。” 未来,我们还会继续用 uni-app 迭代鸿蒙版功能,也期待能和更多开发者一起,在鸿蒙生态里做出更多好用的产品。
五、写在最后:一份来自开发者的感动与回馈
敲完这些文字时,心里满是感慨。回想当初为鸿蒙开发发愁的日子,再看如今借助 uni-app 轻松实现多端落地的成果,真切感受到 uni-app 这些年的成长 —— 它不仅是一个开发工具,更像一位默默助力开发者的伙伴,用越来越强大的功能,一点点降低跨平台开发的门槛,让像我们这样的中小团队也能跟上技术迭代的脚步,在国产生态发展中找到自己的位置。这份便利与支持,让我们在开发路上少走了太多弯路,也让我们对技术赋能产业有了更深的体会。
为了把这份感动与感谢传递下去,也为了回馈同样在使用 uni-app 奋斗的开发者们:如果你的公司有食堂报餐、订餐管理的需求,只要亮出你的 uni-app 开发者身份,联系我们就能免费使用精膳通智慧食堂服务 3 个月。希望能用我们的产品,也能助力国产开发者和创业公司,也期待和大家一起,在 uni-app 搭建的技术桥梁上,共同探索更多国产生态的可能性。
解决uniapp鸿蒙适配深色模式的问题
我们在启动项目的时候就遇到了第一个难题,就是我们真机调试的时候报 no signature file. 没有签名文件怎么办呢?这个时候我们只需要在华为开发者联盟官网https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/harmonyOSDevPlatform/172249065903274453新建一个包名如图所示
这个时候我们再回到Hbuilder X中找到我们的项目中的manifest.json文件填入包名自动获取调试证书就可以启动真机调试啦,如图所示
要点击自动申请调试证书,然后再保存,再重新启动项目就能运行啦
接下来我们来适配深色模式,为什么要适配深色模式呢,是因为最近鸿蒙提交审核意见要求要适配深色模式,所以我们也来适配一个深色模式,那么我们在鸿蒙系统中怎么适配深色模式呢?
1.首先我们先适配底部的tabbar区域每个tabbar要准备两套图标,也就是一个tabbar要准备四张icon,如果你有2个tabbar就要准备8张icon
- 我们在根目录新建一个文件theme.json,并在manifest.json中的源码视图增加"darkmode" : true,"themeLocation" : "theme.json"这两个属性如下图所示
然后配置them.json如图所示
现在深色模式就生效了
注意一定要使用Hbuilder X4.83+版本以上!否则有可能不生效Hbuilder X4.83+! Hbuilder X4.83+! Hbuilder X4.83+!
我们在启动项目的时候就遇到了第一个难题,就是我们真机调试的时候报 no signature file. 没有签名文件怎么办呢?这个时候我们只需要在华为开发者联盟官网https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/harmonyOSDevPlatform/172249065903274453新建一个包名如图所示
这个时候我们再回到Hbuilder X中找到我们的项目中的manifest.json文件填入包名自动获取调试证书就可以启动真机调试啦,如图所示
要点击自动申请调试证书,然后再保存,再重新启动项目就能运行啦
接下来我们来适配深色模式,为什么要适配深色模式呢,是因为最近鸿蒙提交审核意见要求要适配深色模式,所以我们也来适配一个深色模式,那么我们在鸿蒙系统中怎么适配深色模式呢?
1.首先我们先适配底部的tabbar区域每个tabbar要准备两套图标,也就是一个tabbar要准备四张icon,如果你有2个tabbar就要准备8张icon
- 我们在根目录新建一个文件theme.json,并在manifest.json中的源码视图增加"darkmode" : true,"themeLocation" : "theme.json"这两个属性如下图所示
然后配置them.json如图所示
现在深色模式就生效了
注意一定要使用Hbuilder X4.83+版本以上!否则有可能不生效Hbuilder X4.83+! Hbuilder X4.83+! Hbuilder X4.83+!
收起阅读 »从痛点到产品:uni-app x + HarmonyOS打造房产投资管理系统全记录
📖 目录
- 项目背景:从房东的痛点说起
- 为什么选择uni-app x + HarmonyOS
- 系统架构设计:四大模块协同作战
- 核心功能实现:18个页面的血泪史
- HarmonyOS适配的三大挑战与解决方案
- 数据同步机制:让数据流动起来
- 性能优化:从卡顿到丝滑
- UI/UX设计:商务风格的视觉语言
- 踩坑经验:那些让我头秃的Bug
- 开发工具与调试技巧
- 未来规划与展望
- 总结与感悟
项目背景:从房东的痛点说起 {#项目背景}
故事的开始
2024年初,我在苏州购入了第二套投资房产,准备出租。作为一个程序员房东,我自然而然地想用科技手段来管理我的资产。然而现实很骨感:
痛点一:信息管理混乱
- Excel表格记录房产信息,版本管理困难
- 租户资料散落在微信聊天记录里
- 合同文档放在电脑、手机、云盘多个地方
- 想查个租客电话号码都要翻半天
痛点二:财务统计困难
- 每月收租日期记不住,经常漏收
- 水电费、物业费、维修费记录混乱
- 年底算投资回报率要拿出计算器算半天
- 不知道哪套房子最赚钱,哪套在亏损
痛点三:决策缺乏数据支撑
- 出租率是多少?不清楚
- 平均租金水平?凭感觉
- 租客流失率?没统计过
- 要不要继续投资?拍脑袋决定
市场调研
我研究了市面上的房产管理软件:
结论:没有一款软件能完美满足我的需求!
需求分析
作为开发者,我决定自己动手。经过两周的需求梳理,我列出了核心功能:
必备功能(MVP):
- ✅ 房产档案管理(地址、面积、价格)
- ✅ 房间状态管理(空置/已租/维修)
- ✅ 租户信息管理(联系方式、合同期限)
- ✅ 收租记录管理(租金、水电费)
- ✅ 财务收支记录(收入、支出分类)
- ✅ 数据统计分析(出租率、投资回报率)
进阶功能(Nice to have):
- 🔄 收租日提醒
- 📊 数据可视化图表
- 📱 移动端随时查看
- 🔒 数据本地存储(隐私安全)
- 🎨 专业的商务风格UI
目标用户画像
张先生,38岁,IT行业,苏州
- 持有3套房产用于投资
- 工作繁忙,希望高效管理
- 重视数据隐私,不愿上传云端
- 需要专业的数据支撑投资决策
-
🛠️ 为什么选择uni-app x + HarmonyOS {#技术选型}
技术选型的纠结过程
作为一个全栈开发者,我面临着技术选型的"幸福烦恼"。让我列出当时的思考过程:
方案一:原生开发(HarmonyOS + Swift/Kotlin)
优势:
- ✅ 性能最佳,用户体验极致
- ✅ 可以使用平台最新特性
- ✅ 没有框架限制
劣势:
- ❌ 开发成本高(三端分别开发)
- ❌ 维护困难(代码量 x3)
- ❌ 学习成本高(多种语言)
- ❌ 时间成本无法接受(个人项目)
结论: 🚫 虽然性能最好,但对个人开发者不现实
方案二:React Native
优势:
- ✅ 生态成熟,社区活跃
- ✅ 组件库丰富
- ✅ 跨平台能力强
劣势:
- ❌ 对HarmonyOS支持有限
- ❌ 包体积较大(基础包10MB+)
- ❌ 性能相对较差
- ❌ 需要学习React生态
结论: 🤔 可行,但HarmonyOS支持是短板
方案三:Flutter
优势:
- ✅ 性能优秀(接近原生)
- ✅ UI组件精美
- ✅ 热重载提升效率
劣势:
- ❌ Dart语言学习成本
- ❌ 包体积大(基础包15MB+)
- ❌ HarmonyOS支持需要额外适配
- ❌ 对前端开发者不够友好
结论: 🤔 性能好,但学习成本高
方案四:uni-app x(最终选择)✨
优势:
- ✅ 原生性能:基于原生渲染,性能接近原生
- ✅ TypeScript支持:类型安全,开发体验好
- ✅ 一次开发,三端运行:iOS、Android、HarmonyOS
- ✅ Vue生态:前端开发者无缝上手
- ✅ 官方HarmonyOS支持:DCloud官方适配
- ✅ 包体积小:基础包仅5MB
- ✅ 开发效率高:热重载+组件化
劣势:
- ⚠️ 生态相对较新(但在快速完善)
- ⚠️ 部分高级特性需要等待
结论: 🎉 综合考虑,uni-app x是最佳选择!
为什么HarmonyOS是未来趋势?
作为开发者,我深信HarmonyOS代表着移动生态的未来:
- 国产化浪潮:信创政策推动,政企市场潜力巨大
- 技术创新:分布式能力、原子化服务让人眼前一亮
- 生态建设:华为全力推进,开发者红利期
- 市场份额:2024年国内份额已突破20%,增长迅猛
- 开发者友好:完善的文档、活跃的社区
我的判断:提前布局HarmonyOS,就是抓住下一个技术红利!
最终技术栈
经过一周的调研和demo测试,我确定了技术栈:
核心技术明细:
- 前端框架:uni-app x (基于Vue 3)
- 编程语言:TypeScript (主要) + UTS (平台特性)
- 样式方案:SCSS + CSS Variables(主题系统)
- 数据库:HarmonyOS relationalStore (SQLite)
- 状态管理:Composition API + Reactive
- 数据请求:uni.request + Promise
- 本地存储:uni.storage (配置) + relationalStore (业务数据)
-
UI组件:uni-ui + 自定义组件库
🏗️ 系统架构设计:四大模块协同作战 {#架构设计}
整体架构图
系统采用分层架构 + 模块化设计:
核心数据模型(ER图)
数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。
设计亮点:
- property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
- 自动同步机制:收租记录→财务记录的自动创建
- 状态联动:租户状态→房间状态的自动更新
- 数据溯源:通过description字段记录数据来源
项目目录结构
房产投资管理系统/
│
├── pages/ # 页面目录(18个页面)
│ ├── index/ # 首页模块
│ │ └── index.vue # 首页(数据总览)
│ │
│ ├── property/ # 房产管理模块
│ │ ├── list.vue # 房产列表
│ │ ├── add.vue # 添加/编辑房产
│ │ └── detail.vue # 房产详情
│ │
│ ├── rental/ # 租赁管理模块
│ │ ├── room-list.vue # 房间列表
│ │ ├── room-add.vue # 添加/编辑房间
│ │ ├── room-detail.vue # 房间详情
│ │ ├── tenant-list.vue # 租户列表
│ │ ├── tenant-add.vue # 添加/编辑租户
│ │ ├── tenant-detail.vue # 租户详情
│ │ ├── rent-collection.vue # 收租记录列表
│ │ └── rent-add.vue # 添加收租记录
│ │
│ ├── finance/ # 财务管理模块
│ │ ├── list.vue # 财务记录列表
│ │ └── add.vue # 添加财务记录
│ │
│ └── stats/ # 统计分析模块
│ └── index.vue # 数据统计页
│
├── uni_modules/ # 插件模块
│ └── test-relationalStore/ # 数据库封装
│ └── utssdk/
│ └── app-harmony/
│ └── index.uts # HarmonyOS数据库适配
│
├── static/ # 静态资源
│ ├── logo.png # 应用图标
│ ├── 我的资产.png # Tab图标
│ ├── 房产.png
│ ├── 财务.png
│ └── 统计.png
│
├── uni.scss # 全局样式变量
├── App.vue # 应用入口
├── pages.json # 页面配置
├── manifest.json # 应用配置
└── project_analysis.md # 项目开发文档
设计原则:
- 模块化:按功能模块划分目录
- 复用性:list-add-detail的标准页面结构
- 可维护性:清晰的命名和目录层级
💎 核心功能实现:18个页面的血泪史 {#核心功能}
功能模块一:房产管理 - 资产的数字化档案
1.1 房产列表页(pages/property/list.vue)
功能需求:
- 展示所有房产列表
- 显示总资产价值
- 支持房产的增删改查
核心代码:总资产价值计算
// 计算总资产价值
computed: {
totalValue() {
const total = this.propertyList.reduce((sum, item) => {
// 优先使用当前估值,否则使用购买价格
const currentValue = parseFloat(item.current_value) || 0;
const purchasePrice = parseFloat(item.purchase_price) || 0;
const propertyValue = currentValue || purchasePrice;
console.log(`房产 ${item.address}:
current_value=${item.current_value}(${typeof item.current_value}),
purchase_price=${item.purchase_price}(${typeof item.purchase_price}),
使用价值=${propertyValue}`);
return sum + propertyValue;
}, 0);
console.log('总资产价值计算结果:', total);
return total;
}
}
踩坑记录1:资产价值计算错误
问题现象:
总资产显示为"100200300"而不是"600"(三套房各200万)
原因分析:
数据库查询时用getString获取价格字段,导致数值以字符串形式返回。JavaScript的+操作符对字符串执行拼接而非数学加法。
解决方案:
在数据库转换函数中,数值类型字段使用getDouble而非getString
// ❌ 错误写法
function convertResultSetToPropertyArray(resultSet) {
record.purchase_price = resultSet.getString(
resultSet.getColumnIndex('purchase_price')
); // 返回字符串
}
// ✅ 正确写法
function convertResultSetToPropertyArray(resultSet) {
record.purchase_price = resultSet.getDouble(
resultSet.getColumnIndex('purchase_price')
); // 返回数值
}
经验总结:
- ✅ 整数/小数字段:用
getDouble() - ✅ 文本/ID字段:用
getString() - ✅ 在computed中加console.log调试
- ✅ 前端再用
parseFloat兜底确保类型正确
1.2 添加房产页(pages/property/add.vue)
功能需求:
- 录入房产基本信息
- 表单验证
- 数据持久化
核心代码:表单验证
validateForm() {
// 地址验证
if (!this.formData.address || this.formData.address.trim() === '') {
uni.showToast({
title: '请输入房产地址',
icon: 'none'
});
return false;
}
// 面积验证
if (!this.formData.area || parseFloat(this.formData.area) <= 0) {
uni.showToast({
title: '请输入有效的房产面积',
icon: 'none'
});
return false;
}
// 价格验证
if (!this.formData.purchase_price ||
parseFloat(this.formData.purchase_price) <= 0) {
uni.showToast({
title: '请输入有效的购买价格',
icon: 'none'
});
return false;
}
return true;
}
设计亮点:
- 实时验证:输入时即时反馈
- 友好提示:明确告知用户错误原因
- 数据类型转换:字符串→数值的安全转换
功能模块二:租赁管理 - 最复杂的业务逻辑
2.1 房间管理:7种房型 x 4种付款方式 = 28种组合
功能需求:
- 支持7种房型(单间、一室一厅、两室一厅...)
- 支持4种付款方式(押一付一、押一付三...)
- 自定义每月收租日(1-31号)
- 房间状态管理(空置/已租/维修中)
核心代码:房间类型映射
// 房型映射
getRoomTypeText(type) {
const typeMap = {
'single': '单间',
'1b1b': '一室一厅',
'2b1b': '两室一厅',
'2b2b': '两室两厅',
'3b1b': '三室一厅',
'3b2b': '三室两厅',
'4b2b': '四室两厅'
};
return typeMap[type] || type;
}
// 付款方式映射
getPaymentModeText(mode) {
const modeMap = {
'monthly': '押一付一',
'quarterly': '押一付三',
'half_yearly': '半年付',
'yearly': '年付'
};
return modeMap[mode] || mode;
}
踩坑记录2:收租日选择器无法显示全部31天
问题现象:
HarmonyOS的<picker>组件只能显示有限的选项,无法展示完整的31天选择。
解决方案:自定义Grid布局选择器
<template>
<!-- 自定义模态框 -->
<view class="rent-day-modal" v-if="showRentDayModal" @click="cancelRentDay">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">选择每月收租日</text>
</view>
<!-- 7x5 Grid布局显示31天 -->
<view class="day-grid">
<view class="day-item"
v-for="day in 31"
:key="day"
:class="{ selected: selectedRentDay === day }"
@click="selectRentDay(day)">
<text class="day-text">{{ day }}</text>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="cancelRentDay">取消</button>
<button class="confirm-btn" @click="confirmRentDay">确定</button>
</view>
</view>
</view>
</template>
<style lang="scss">
.day-grid {
display: grid;
grid-template-columns: repeat(7, 1fr); // 7列布局,展示31天
gap: 12rpx;
padding: 20rpx;
}
.day-item {
aspect-ratio: 1; // 保持正方形
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
background: #F5F5F5;
transition: all 0.2s;
&.selected {
background: $primary-blue;
color: #FFFFFF;
transform: scale(1.1);
}
&:active {
opacity: 0.7;
}
}
</style>
经验总结:
当原生组件无法满足需求时,果断自定义实现。Grid布局非常适合日历、选择器等场景。
2.2 租户管理:隐私保护与数据联动
功能需求:
- 租户信息录入(姓名、电话、身份证)
- 身份证号脱敏显示
- 租户状态与房间状态联动
- 合同期限管理
核心代码:身份证号脱敏
// 身份证号脱敏处理
maskIdCard(idCard) {
if (!idCard || idCard.length < 8) return idCard;
// 显示前4位和后4位,中间用*替代
const start = idCard.substring(0, 4);
const end = idCard.substring(idCard.length - 4);
const middle = '*'.repeat(idCard.length - 8);
return `${start}${middle}${end}`;
}
// 使用示例
computed: {
maskedIdCard() {
return this.maskIdCard(this.tenant.id_card);
// 输入:510123199001011234
// 输出:5101**********1234
}
}
踩坑记录3:租户状态判断不准确
问题现象:
主页快捷操作"添加租户",明明房间没有租户,却提示"房间已有租户"。
错误逻辑:依赖房间status字段
// ❌ 不准确的判断
if (room.status === 'rented') {
uni.showToast({ title: '房间已有租户' });
}
根本原因:
房间状态字段可能因为各种原因(删除租户时未更新、数据异常等)导致状态不准确。
正确方案:查询实际租户数据
// ✅ 准确的判断:查询数据库
checkRoomTenants(room) {
uni.queryWithConditions('tenants',
`room_id = ${room.id} AND status = 'active'`)
.then(tenants => {
console.log(`房间 ${room.room_number} 的租户查询结果:`,
tenants?.length || 0, '个活跃租户');
if (tenants && tenants.length > 0) {
// 确实有租户
uni.showModal({
title: '房间已有租户',
content: `该房间已有${tenants.length}位租户,是否查看?`,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: `/pages/rental/tenant-list?roomId=${room.id}`
});
}
}
});
} else {
// 房间确实空置,可以添加租户
uni.navigateTo({
url: `/pages/rental/tenant-add?roomId=${room.id}`
});
}
})
.catch(err => {
console.error('查询房间租户失败:', err);
uni.showToast({
title: '查询租户信息失败',
icon: 'error'
});
});
}
设计原则:
关键业务逻辑,永远基于实际数据查询,而非缓存状态!
租户删除后的房间状态联动
// 删除租户时,自动更新房间状态
performDelete() {
const roomId = this.tenant.room_id;
uni.deleteData('tenants', `id = ${this.tenantId}`)
.then(() => {
// 删除成功后,检查房间是否还有其他租户
return this.updateRoomStatus(roomId);
})
.then(() => {
uni.showToast({
title: '删除成功,即将返回',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800);
});
}
// 智能更新房间状态
updateRoomStatus(roomId) {
if (!roomId) return Promise.resolve();
// 查询该房间是否还有活跃租户
return uni.queryWithConditions('tenants', `room_id = ${roomId}`)
.then(tenants => {
const hasActiveTenant = tenants &&
tenants.some(tenant => tenant.status === 'active');
// 根据实际情况更新房间状态
const newStatus = hasActiveTenant ? 'rented' : 'vacant';
return uni.updateData('rooms', {
status: newStatus,
updated_at: new Date().toISOString()
}, `id = ${roomId}`);
})
.then(() => {
console.log('房间状态更新成功');
})
.catch(err => {
console.error('房间状态更新失败:', err);
});
}
2.3 收租记录管理:自动化是关键
功能需求:
- 记录每次收租情况
- 区分租金和水电费
- 支持多种支付方式
- 自动创建财务记录
核心代码:收租记录保存与财务同步
saveRentCollection() {
if (!this.validateForm()) {
return;
}
const collectionData = {
tenant_id: parseInt(this.formData.tenant_id),
collection_date: this.formData.collection_date,
rent_amount: parseFloat(this.formData.rent_amount) || 0,
utility_amount: parseFloat(this.formData.utility_amount) || 0,
total_amount: this.totalAmount,
payment_method: this.formData.payment_method || 'cash',
status: this.formData.status || 'pending',
notes: this.formData.notes || null,
updated_at: new Date().toISOString()
};
// 保存收租记录
uni.insertData('rent_collections', collectionData)
.then(() => {
// 🔥 核心:如果收租状态为已收租,自动创建财务记录
if (collectionData.status === 'collected') {
return this.createFinancialRecord(collectionData);
}
return Promise.resolve();
})
.then(() => {
uni.showToast({
title: '收租记录添加成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800);
})
.catch(err => {
console.error('收租记录保存失败:', err);
uni.showToast({
title: '保存失败,请重试',
icon: 'error'
});
});
}
// 创建对应的财务记录(核心同步逻辑)
createFinancialRecord(collectionData) {
return new Promise((resolve, reject) => {
// 1. 查询租户信息
uni.queryWithConditions('tenants', `id = ${collectionData.tenant_id}`)
.then(tenantResults => {
if (!tenantResults || tenantResults.length === 0) {
console.warn('未找到租户信息,跳过财务记录创建');
return resolve();
}
const tenant = tenantResults[0];
// 2. 查询房间信息(获取property_id)
return uni.queryWithConditions('rooms', `id = ${tenant.room_id}`)
.then(roomResults => {
if (!roomResults || roomResults.length === 0) {
console.warn('未找到房间信息,跳过财务记录创建');
return resolve();
}
const room = roomResults[0];
// 3. 创建财务记录
const financialData = {
property_id: room.property_id,
room_id: room.id, // 🔥 关键:关联房间
type: 'income',
category: 'rent',
amount: collectionData.total_amount,
description: `${tenant.name}租金收入 - ${room.room_number}号房`,
record_date: collectionData.collection_date,
created_at: new Date().toISOString()
};
return uni.insertData('financial_records', financialData);
});
})
.then(() => {
console.log('财务记录创建成功');
resolve();
})
.catch(err => {
console.error('财务记录创建失败:', err);
resolve(); // 不阻断主流程
});
});
}
设计亮点:
- 自动化同步:收租后自动创建财务记录,无需手动二次录入
- 数据溯源:通过description字段清晰标识收入来源
- 三级关联:租户→房间→房产,确保数据完整性
- 容错机制:财务记录失败不影响收租记录保存
功能模块三:财务管理 - 数据的统一中心
3.1 财务记录:收支一目了然
功能需求:
- 记录所有收入支出
- 支持房间级别关联
- 分类管理(租金、押金、维修等)
- 数据统计分析
踩坑记录4:财务记录与租金管理数据不同步
问题现象:
- 在财务页面添加租金收入,租金管理中看不到
- 在租金管理添加收入,有时财务记录中不显示
根本原因:
财务记录表缺少room_id字段,无法建立房间级别的数据关联。
解决方案:增强数据库结构
// 1. 修改financial_records表结构,添加room_id字段
let columnList = [
{ 'name': 'id', 'type': 'integer', 'nullable': false, 'primary': true },
{ 'name': 'property_id', 'type': 'integer', 'nullable': true },
{ 'name': 'room_id', 'type': 'integer', 'nullable': true }, // 🔥 新增
{ 'name': 'type', 'type': 'text', 'nullable': false },
{ 'name': 'category', 'type': 'text', 'nullable': false },
{ 'name': 'amount', 'type': 'real', 'nullable': false },
{ 'name': 'description', 'type': 'text', 'nullable': true },
{ 'name': 'record_date', 'type': 'text', 'nullable': false },
{ 'name': 'created_at', 'type': 'text', 'nullable': true }
];
// 2. 修改数据转换函数
function convertResultSetToFinancialArray(resultSet) {
// ...
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));
record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // 🔥 新增
record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // 🔥 改为getDouble
// ...
}
功能增强:财务记录添加房间选择
<template>
<!-- 房产选择 -->
<view class="field-group">
<text class="field-label">所属房产 *</text>
<view @click="showPropertyPicker">
<text>{{ formData.property_address || '请选择房产' }}</text>
</view>
</view>
<!-- 🔥 新增:房间选择(选择房产后动态显示) -->
<view class="field-group"
v-if="formData.property_id && roomList.length > 0">
<text class="field-label">所属房间</text>
<view @click="showRoomPicker">
<text>{{ formData.room_number ?
`${formData.room_number}号房` : '请选择房间(可选)' }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
property_id: null,
room_id: null, // 🔥 新增
room_number: '', // 🔥 新增
// ...
},
roomList: [] // 🔥 新增
}
},
methods: {
// 房产选择后,自动加载房间列表
handlePropertySelected(property) {
this.formData.property_id = property.id;
this.formData.property_address = property.address;
// 清空之前的房间选择
this.formData.room_id = null;
this.formData.room_number = '';
// 🔥 加载该房产的房间列表
this.loadRoomList(property.id);
},
loadRoomList(propertyId) {
if (!propertyId) {
this.roomList = [];
return;
}
uni.queryWithConditions('rooms', `property_id = ${propertyId}`)
.then(results => {
console.log('房间列表查询成功:', results?.length || 0, '个房间');
this.roomList = results || [];
})
.catch(err => {
console.error('房间列表查询失败:', err);
this.roomList = [];
});
},
showRoomPicker() {
if (this.roomList.length === 0) {
uni.showToast({
title: '该房产暂无房间',
icon: 'none'
});
return;
}
const roomNameList = this.roomList.map(room =>
`${room.room_number}号房 (${room.monthly_rent}元/月)`);
uni.showActionSheet({
itemList: roomNameList,
success: (res) => {
const selectedRoom = this.roomList[res.tapIndex];
this.formData.room_id = selectedRoom.id;
this.formData.room_number = selectedRoom.room_number;
}
});
}
}
}
</script>
功能模块四:数据统计 - 让数字说话
4.1 首页数据总览:一眼看懂经营状况
功能需求:
- 总资产价值展示
- 本月收益分析
- 租赁管理概览
- 快捷操作入口
核心代码:营业收入计算
踩坑记录5:主页营业收入不包含财务记录
问题现象:
在财务页面添加的收入,主页营业收入不显示。
错误逻辑:分离计算
// ❌ 错误:分离计算导致遗漏
monthlyTotalIncome() {
return this.monthlyRentIncome + this.monthlyNonRentIncome;
}
monthlyRentIncome() {
// 只从rent_collections表计算
return this.rentCollections
.filter(r => r.status === 'collected')
.reduce((sum, r) => sum + parseFloat(r.total_amount || 0), 0);
}
monthlyNonRentIncome() {
// 从financial_records表计算,但排除了租金
return this.getMonthlyRecords('income')
.filter(r => r.category !== 'rent')
.reduce((sum, r) => sum + parseFloat(r.amount || 0), 0);
}
问题分析:
- 直接在财务页面添加的租金收入被排除了
- 数据来源不统一,导致统计不完整
正确方案:统一数据源
// ✅ 正确:所有收入统一从financial_records计算
monthlyTotalIncome() {
return this.monthlyIncome; // 统一数据源
}
monthlyIncome() {
// 财务记录中当月所有收入之和
return this.getMonthlyRecords('income').reduce((sum, record) => {
return sum + parseFloat(record.amount || 0);
}, 0);
}
getMonthlyRecords(type) {
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7); // YYYY-MM
return this.financialRecords.filter(record =>
record.type === type &&
record.record_date &&
record.record_date.startsWith(currentMonth)
);
}
架构优化总结:
- ✅ 财务记录表是唯一的收入统计数据源
- ✅ 收租记录表仅用于收租流程管理
- ✅ 收租记录自动创建财务记录,实现数据同步
- ✅ 统计计算全部基于financial_records表,避免遗漏
4.2 统计分析页:深度数据洞察
功能需求:
- 房产收益排行
- 支出分析
- 租赁分析(出租率、租金收入占比)
- 租户分析
核心代码:出租率计算
computed: {
// 总房间数
totalRooms() {
return this.roomList.length;
},
// 已出租房间数
rentedRooms() {
return this.roomList.filter(room => room.status === 'rented').length;
},
// 房间出租率
roomOccupancyRate() {
if (this.totalRooms === 0) return 0;
return ((this.rentedRooms / this.totalRooms) * 100).toFixed(1);
},
// 租金收入占总收入比例
rentIncomePercentage() {
if (this.totalIncomeAmount === 0) return 0;
return ((this.rentIncomeAmount / this.totalIncomeAmount) * 100).toFixed(1);
},
// 平均租金水平
averageMonthlyRent() {
if (this.roomList.length === 0) return 0;
const totalRent = this.roomList.reduce((sum, room) =>
sum + parseFloat(room.monthly_rent || 0), 0);
return (totalRent / this.roomList.length).toFixed(0);
}
}
🔧 HarmonyOS适配的三大挑战与解决方案 {#harmonyos适配}
挑战一:Picker组件的兼容性问题
问题场景:
在租金记录页面,选择租户时使用<picker>组件,当有多个租户时点击无响应。
问题代码:
<!-- ❌ 在HarmonyOS上有问题 -->
<picker :range="tenantList"
range-key="name"
@change="onTenantChange">
<view>{{ selectedTenant?.name || '请选择租户' }}</view>
</picker>
根本原因:
HarmonyOS的picker组件在动态数据绑定和复杂对象数组处理上存在兼容性问题。
解决方案:使用uni.showActionSheet替代
// ✅ 完美兼容的方案
showTenantPicker() {
if (this.tenantList.length === 0) {
uni.showToast({
title: '暂无租户',
icon: 'none'
});
return;
}
const tenantNames = this.tenantList.map(t => t.name);
uni.showActionSheet({
itemList: tenantNames,
success: (res) => {
if (!res.cancel) {
const selectedTenant = this.tenantList[res.tapIndex];
this.formData.tenant_id = selectedTenant.id;
this.selectedTenantText = selectedTenant.name;
this.selectedTenant = selectedTenant;
uni.showToast({
title: `已选择: ${selectedTenant.name}`,
icon: 'success'
});
}
},
fail: (err) => {
console.log('用户取消选择或出错:', err);
}
});
}
HTML改造:
<!-- ✅ 使用点击事件触发 -->
<view class="picker-wrapper" @click="showTenantPicker">
<text class="picker-value">
{{ selectedTenantText || '请选择租户' }}
</text>
<text class="picker-arrow">👤</text>
</view>
| 优势对比: | 特性 | Picker组件 | ActionSheet |
|---|---|---|---|
| 兼容性 | ⚠️ 有问题 | ✅ 完美 | |
| 用户体验 | 一般 | ✅ 原生感强 | |
| 自定义 | 受限 | ✅ 灵活 | |
| 调试难度 | 较难 | 简单 |
经验总结:
在HarmonyOS开发中,涉及复杂数据的选择器,优先考虑
uni.showActionSheet或自定义组件。
挑战二:ResultSet数据类型转换陷阱
问题场景:
总资产计算时,100万+200万+300万 = "100200300"
问题代码:
// ❌ 错误:所有字段都用getString
function convertResultSetToFinancialArray(resultSet) {
const result = [];
resultSet.goToFirstRow();
for (let i = 0; i < count; i++) {
let record = {};
record.id = resultSet.getString(resultSet.getColumnIndex('id'));
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));
record.amount = resultSet.getString(resultSet.getColumnIndex('amount')); // ❌ 金额变成字符串
// ...
result[i] = record;
resultSet.goToNextRow();
}
return result;
}
问题分析:
// JavaScript的+操作符行为
"100" + "200" + "300" = "100200300" // ❌ 字符串拼接
100 + 200 + 300 = 600 // ✅ 数值相加
正确方案:严格区分数据类型
// ✅ 正确:根据字段类型选择方法
function convertResultSetToFinancialArray(resultSet) {
const result = [];
resultSet.goToFirstRow();
for (let i = 0; i < count; i++) {
let record = {};
record.id = resultSet.getString(resultSet.getColumnIndex('id')); // 文本
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id')); // ID
record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // ID
record.type = resultSet.getString(resultSet.getColumnIndex('type')); // 文本
record.category = resultSet.getString(resultSet.getColumnIndex('category')); // 文本
record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // ✅ 数值
record.description = resultSet.getString(resultSet.getColumnIndex('description'));// 文本
record.record_date = resultSet.getString(resultSet.getColumnIndex('record_date'));// 日期
record.created_at = resultSet.getString(resultSet.getColumnIndex('created_at')); // 日期
result[i] = record;
resultSet.goToNextRow();
}
return result;
}
最佳实践规范:
// ResultSet数据类型选择指南
const TYPE_MAPPING = {
// 文本类型
'text': 'getString',
'varchar': 'getString',
// 数值类型
'integer': 'getLong', // 整数
'real': 'getDouble', // 小数
'numeric': 'getDouble', // 数值
// 特殊类型
'blob': 'getBlob', // 二进制
'boolean': 'getLong', // 布尔值(0/1)
// 日期类型(通常存为text)
'datetime': 'getString', // 日期时间
'date': 'getString' // 日期
};
调试技巧:
// 添加类型检查日志
computed: {
totalValue() {
const total = this.propertyList.reduce((sum, item) => {
const value = parseFloat(item.current_value) || 0;
// 🔥 调试日志
console.log(`房产: ${item.address}`);
console.log(` current_value: ${item.current_value} (${typeof item.current_value})`);
console.log(` 转换后: ${value} (${typeof value})`);
return sum + value;
}, 0);
console.log(`总资产: ${total} (${typeof total})`);
return total;
}
}
挑战三:生命周期与数据初始化
问题场景:
收租记录列表页面不停打印"收租记录表创建成功",数据加载重复执行。
问题代码:
// ❌ 错误:onLoad和onShow都初始化表
export default {
onLoad(options) {
this.initTable(); // 创建表
this.loadData(); // 加载数据
},
onShow() {
this.initTable(); // 又创建一次表!
this.loadData(); // 又加载一次数据!
}
}
问题分析:
- 每次页面显示都重新初始化表
- 导致重复的数据库操作
- 日志输出混乱,影响调试
正确方案:明确生命周期职责
// ✅ 正确:职责分离
export default {
data() {
return {
isTableInit: false, // 表初始化标记
propertyId: null,
dataList: []
}
},
onLoad(options) {
// 1. 获取路由参数
this.propertyId = options.propertyId;
// 2. 首次加载:初始化表 + 加载数据
this.initializeOnce();
},
onShow() {
// 3. 后续显示:仅刷新数据
if (this.isTableInit) {
this.refreshData();
}
},
methods: {
// 首次初始化(只执行一次)
async initializeOnce() {
if (this.isTableInit) return;
try {
// 初始化表结构
await this.initTable();
this.isTableInit = true;
console.log('表初始化完成');
// 加载初始数据
await this.loadData();
} catch (err) {
console.error('初始化失败:', err);
}
},
// 刷新数据(不重新初始化表)
async refreshData() {
try {
await this.loadDataList();
console.log('数据刷新完成');
} catch (err) {
console.error('数据刷新失败:', err);
}
},
// 初始化表结构
initTable() {
return uni.createTable('rent_collections', columnList)
.then(() => {
console.log('收租记录表创建成功');
})
.catch(err => {
// createTable会在表已存在时报错,这是正常的
console.log('表已存在或创建失败:', err);
});
},
// 加载完整数据
async loadData() {
await Promise.all([
this.loadTenantMap(), // 先加载依赖数据
this.loadRoomMap()
]);
await this.loadDataList(); // 再加载主数据
},
// 仅加载列表数据
loadDataList() {
return uni.queryWithConditions('rent_collections', '')
.then(results => {
this.dataList = results || [];
})
.catch(err => {
console.error('数据加载失败:', err);
this.dataList = [];
});
}
}
}
生命周期最佳实践:
页面首次加载 → onLoad
├─ 初始化表结构 (once)
├─ 加载依赖数据 (once)
└─ 加载主数据
页面再次显示 → onShow
└─ 仅刷新数据(不重新初始化)
🔄 数据同步机制:让数据流动起来 {#数据同步}
数据同步是整个系统的核心挑战。我设计了三层同步机制:
同步层级一:收租记录 → 财务记录
触发条件: 收租状态为"已收租"
// 自动同步流程
收租记录保存 (status='collected')
↓
查询租户信息 (获取租户名称)
↓
查询房间信息 (获取房间号、房产ID)
↓
创建财务记录 (income, category='rent')
↓
完成同步
同步层级二:租户状态 → 房间状态
触发条件: 添加/删除租户,租户状态变更
// 状态联动逻辑
租户操作完成
↓
查询该房间所有租户
↓
检查是否存在活跃租户 (status='active')
↓
更新房间状态
├─ 有活跃租户 → status='rented'
└─ 无活跃租户 → status='vacant'
同步层级三:房间选择 → 数据联动
触发条件: 财务记录选择房产
// 级联加载流程
选择房产
↓
清空之前的房间选择
↓
查询该房产的所有房间
↓
展示房间选择器(可选)
↓
选择房间后关联room_id
⚡ 性能优化:从卡顿到丝滑 {#性能优化}
优化一:Computed Property 缓存计算
问题: 在模板中直接调用方法导致重复计算
<!-- ❌ 错误:每次渲染都重新计算 -->
<text>{{ getTotalAssets() }}</text>
<text>{{ getTotalAssets() }}</text> <!-- 又计算一次 -->
优化: 使用计算属性缓存结果
// ✅ 优化:使用computed缓存
computed: {
totalAssets() {
return this.propertyList.reduce((sum, p) =>
sum + (parseFloat(p.current_value) || 0), 0);
}
}
<!-- ✅ 多次使用,只计算一次 -->
<text>{{ totalAssets }}</text>
<text>{{ totalAssets }}</text> <!-- 使用缓存值 -->
优化二:数据加载策略
问题: 首页加载慢,等待时间长
原因分析:
// ❌ 串行加载,总耗时 = 每个接口耗时之和
loadData() {
this.loadProperties(); // 100ms
this.loadFinancials(); // 150ms
this.loadRooms(); // 120ms
this.loadTenants(); // 100ms
// 总耗时: 470ms
}
优化方案: 并行加载
// ✅ 并行加载,总耗时 = 最慢的接口耗时
async loadData() {
try {
await Promise.all([
this.loadProperties(), // ┐
this.loadFinancials(), // ├─ 并行执行
this.loadRooms(), // ┤
this.loadTenants() // ┘
]);
// 总耗时: 150ms(最慢的接口)
// 依赖数据的后续处理
this.processData();
} catch (err) {
console.error('数据加载失败:', err);
}
}
性能对比:
- 串行加载:470ms
- 并行加载:150ms
- 性能提升:68% 🚀
优化三:避免不必要的数据库操作
问题: onShow时重复初始化表
优化: 使用标志位控制
data() {
return {
isTableInit: false // 🔥 标志位
}
},
methods: {
initTable() {
if (this.isTableInit) {
console.log('表已初始化,跳过');
return Promise.resolve();
}
return uni.createTable(...)
.then(() => {
this.isTableInit = true;
console.log('表初始化完成');
});
}
}
优化四:列表渲染优化
使用v-for时添加唯一key
<!-- ❌ 没有key,Vue无法高效更新 -->
<view v-for="item in list">
{{ item.name }}
</view>
<!-- ✅ 有key,Vue可以精确复用 -->
<view v-for="item in list" :key="item.id">
{{ item.name }}
</view>
避免在v-for中使用v-if
<!-- ❌ 性能差:先渲染再过滤 -->
<view v-for="item in list" :key="item.id" v-if="item.status === 'active'">
{{ item.name }}
</view>
<!-- ✅ 性能好:先过滤再渲染 -->
<view v-for="item in activeList" :key="item.id">
{{ item.name }}
</view>
<script>
computed: {
activeList() {
return this.list.filter(item => item.status === 'active');
}
}
</script>
🎨 UI/UX设计:商务风格的视觉语言 {#uiux设计}
设计理念
作为一款专业的房产管理工具,UI设计遵循以下原则:
- 商务专业:渐变、阴影、卡片化设计
- 信息清晰:合理的视觉层级和间距
- 操作便捷:大按钮、明确反馈
- 数据可视:数字大、图标辅助
SCSS变量系统
// uni.scss - 全局设计token
// ===== 颜色系统 =====
// 主色
$primary-blue: #3B82F6;
$primary-dark: #2563EB;
// 功能色
$income-text: #10B981; // 收入绿
$income-bg: rgba(16, 185, 129, 0.1);
$expense-text: #EF4444; // 支出红
$expense-bg: rgba(239, 68, 68, 0.1);
// 文本色
$text-primary: #1E293B; // 主要文本
$text-secondary: #64748B; // 次要文本
$text-tertiary: #94A3B8; // 辅助文本
// 背景色
$bg-primary: #FFFFFF;
$bg-secondary: #F8FAFC;
// ===== 间距系统 =====
$spacing-xs: 8rpx;
$spacing-sm: 12rpx;
$spacing-md: 16rpx;
$spacing-lg: 24rpx;
$spacing-xl: 32rpx;
$spacing-xxl: 48rpx;
// ===== 圆角系统 =====
$radius-small: 8rpx;
$radius-medium: 12rpx;
$radius-large: 16rpx;
$radius-pill: 999rpx;
// ===== 阴影系统 =====
$shadow-card: 0 8rpx 32rpx rgba(15, 23, 42, 0.08),
0 4rpx 16rpx rgba(15, 23, 42, 0.04);
// ===== 字体系统 =====
$font-size-caption: 22rpx; // 说明文字
$font-size-body: 28rpx; // 正文
$font-size-subhead: 30rpx; // 副标题
$font-size-headline: 32rpx; // 标题
$font-size-title: 40rpx; // 大标题
卡片设计模式
// 统一的卡片样式
.info-card {
background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%);
border-radius: $radius-large;
padding: $spacing-xl;
box-shadow: $shadow-card;
border: 1rpx solid rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);
}
}
数据展示设计
大数字设计:
<view class="data-showcase">
<text class="data-number">¥{{ formatLargeNumber(2380000) }}</text>
<text class="data-label">总资产价值</text>
</view>
<style lang="scss">
.data-number {
font-size: 56rpx; // 大号字体
font-weight: 700; // 粗体
color: $text-primary;
line-height: 1.2;
letter-spacing: -0.5rpx; // 紧凑间距
}
.data-label {
font-size: $font-size-body;
color: $text-secondary;
margin-top: $spacing-xs;
}
</style>
收支颜色区分:
.income-color {
color: $income-text; // 绿色
}
.expense-color {
color: $expense-text; // 红色
}
用户体验细节
1. 操作反馈优化
// 成功反馈
uni.showToast({
title: '添加成功,即将返回', // 明确告知用户
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800); // 800ms刚好够看到提示
2. 空状态设计
<view class="empty-state" v-if="dataList.length === 0">
<text class="empty-icon">🏠</text>
<text class="empty-title">暂无数据</text>
<text class="empty-subtitle">点击右上角按钮添加</text>
</view>
3. 加载状态
<view class="loading-state" v-if="isLoading">
<text class="loading-icon">⏳</text>
<text class="loading-text">加载中...</text>
</view>
🐛 踩坑经验:那些让我头秃的Bug {#踩坑经验}
Bug 1: 字符串拼接陷阱
现象: 100 + 200 + 300 = "100200300"
原因: 数据库返回字符串类型
解决: getDouble + parseFloat双保险
耗时: 2小时
Bug 2: Picker组件失效
现象: 选择租户时点击无反应
原因: HarmonyOS picker兼容问题
解决: 改用uni.showActionSheet
耗时: 3小时
Bug 3: 数据重复加载
现象: 不停打印"表创建成功"
原因: onLoad和onShow都初始化表
解决: 生命周期职责分离
耗时: 1小时
Bug 4: 房间状态误判
现象: 明明没租户却提示"已有租户"
原因: 依赖status字段而非实际数据
解决: 查询数据库确认
耗时: 1.5小时
Bug 5: 收入统计遗漏
现象: 财务页面添加的收入不计入统计
原因: 分离计算逻辑导致遗漏
解决: 统一数据源(financial_records)
耗时: 2小时
Bug 6: 收租日选择不全
现象: 只能选择部分日期
原因: Picker组件限制
解决: Grid布局自定义选择器
耗时: 4小时
总结:调试时间约占开发时间的30% 😅
🔨 开发工具与调试技巧 {#开发工具}
HBuilder X 实用功能
- 真机调试:实时预览,快速定位问题
- 代码提示:TypeScript类型提示
- 条件编译:处理平台差异
// #ifdef APP-HARMONY
// HarmonyOS专属代码
// #endif
// #ifdef APP-IOS
// iOS专属代码
// #endif
调试技巧总结
1. Console.log大法
// 关键位置添加日志
console.log('=== 数据加载开始 ===');
console.log('查询条件:', condition);
console.log('查询结果:', results);
console.log('=== 数据加载完成 ===');
2. 类型检查
console.log(`金额: ${amount}, 类型: ${typeof amount}`);
3. 生命周期追踪
onLoad() { console.log('⬇️ onLoad'); }
onShow() { console.log('👁️ onShow'); }
onHide() { console.log('🙈 onHide'); }
4. 数据快照
console.log('数据快照:', JSON.stringify(data, null, 2));
🚀 未来规划与展望 {#未来规划}
v1.1.0 - 功能增强(开发中)
- [ ] 合同到期提醒功能
- [ ] 收租日提醒(推送通知)
- [ ] 数据导出(Excel格式)
- [ ] 云同步备份
v1.2.0 - 数据可视化(规划中)
- [ ] 收益趋势图表(折线图)
- [ ] 房产价值变化(柱状图)
- [ ] 租客分布分析(饼图)
- [ ] 年度投资报告
v1.3.0 - 智能化(未来愿景)
- [ ] AI租金定价建议
- [ ] 空置期预测
- [ ] 智能收租提醒
- [ ] 租客画像分析
v2.0.0 - 多平台扩展
- [ ] iOS版本发布
- [ ] Android版本发布
- [ ] Web管理后台
- [ ] 数据云同步
💭 总结与感悟 {#总结}
开发历程回顾
开发阶段:
阶段一:需求分析 + 技术选型
阶段二:数据库设计 + 架构搭建
阶段三:核心功能开发(房产、租赁)
阶段四:财务统计模块
阶段五:HarmonyOS适配调试
阶段六:UI优化 + Bug修复
代码统计:
- 总代码量:10000+ 行
- 页面数量:18个
- 组件数量:30+个
- 数据表:5个核心表
技术收获
1. 深入理解HarmonyOS生态
从零开始学习HarmonyOS的relationalStore数据库,从陌生到熟悉,从踩坑到总结最佳实践。
2. 掌握uni-app x开发
- Composition API的使用
- 生命周期管理
- 数据响应式原理
- 跨平台适配技巧
3. 数据库设计能力
学会设计合理的数据关联关系,理解数据一致性的重要性。
4. 性能优化思维
从用户体验出发,优化加载速度、减少重复计算、提升交互流畅度。
思维成长
1. 用户思维
不仅是开发者,更是用户。每个功能都从实际需求出发,而非为了技术而技术。
2. 架构思维
先设计数据模型,再设计功能模块。好的架构是成功的一半。
3. 工程思维
代码质量、可维护性、可扩展性同样重要。写代码不仅是为了实现功能,更是为了长期维护。
4. 问题解决思维
遇到问题不慌张,系统分析、逐步排查、总结经验。
给开发者的建议
1. 技术选型要慎重
选择适合自己的技术栈,不要盲目追新。uni-app x + HarmonyOS对我来说是最佳选择。
2. 数据库设计要花时间
数据模型设计好了,后续开发事半功倍。不要急于写代码。
3. 用户体验无小事
800ms的延迟优化、身份证脱敏、空状态提示...这些细节决定产品质量。
4. 持续迭代比一次完美更重要
先实现MVP,再逐步完善。不要追求一次性做到完美。
5. 记录踩坑经验
每个Bug都是成长的机会。记录下来,帮助自己也帮助别人。
写在最后
这个项目从构思到完成,经历了无数次的推翻重来、代码重构、问题调试。但当看到一个功能完整、数据准确、体验流畅的应用在HarmonyOS上运行时,所有的努力都值得了。
uni-app x + HarmonyOS Next 的组合,让我既能享受跨平台开发的便利,又能获得原生应用的性能。虽然过程中遇到了很多适配问题,但每个问题的解决都让我对HarmonyOS生态有了更深的理解。
HarmonyOS代表着未来,提前布局就是抓住机遇。感谢DCloud提供的强大框架,感谢HarmonyOS提供的优秀平台。
让我们一起,星光不负,码向未来! 🚀
📖 目录
- 项目背景:从房东的痛点说起
- 为什么选择uni-app x + HarmonyOS
- 系统架构设计:四大模块协同作战
- 核心功能实现:18个页面的血泪史
- HarmonyOS适配的三大挑战与解决方案
- 数据同步机制:让数据流动起来
- 性能优化:从卡顿到丝滑
- UI/UX设计:商务风格的视觉语言
- 踩坑经验:那些让我头秃的Bug
- 开发工具与调试技巧
- 未来规划与展望
- 总结与感悟
项目背景:从房东的痛点说起 {#项目背景}
故事的开始
2024年初,我在苏州购入了第二套投资房产,准备出租。作为一个程序员房东,我自然而然地想用科技手段来管理我的资产。然而现实很骨感:
痛点一:信息管理混乱
- Excel表格记录房产信息,版本管理困难
- 租户资料散落在微信聊天记录里
- 合同文档放在电脑、手机、云盘多个地方
- 想查个租客电话号码都要翻半天
痛点二:财务统计困难
- 每月收租日期记不住,经常漏收
- 水电费、物业费、维修费记录混乱
- 年底算投资回报率要拿出计算器算半天
- 不知道哪套房子最赚钱,哪套在亏损
痛点三:决策缺乏数据支撑
- 出租率是多少?不清楚
- 平均租金水平?凭感觉
- 租客流失率?没统计过
- 要不要继续投资?拍脑袋决定
市场调研
我研究了市面上的房产管理软件:
结论:没有一款软件能完美满足我的需求!
需求分析
作为开发者,我决定自己动手。经过两周的需求梳理,我列出了核心功能:
必备功能(MVP):
- ✅ 房产档案管理(地址、面积、价格)
- ✅ 房间状态管理(空置/已租/维修)
- ✅ 租户信息管理(联系方式、合同期限)
- ✅ 收租记录管理(租金、水电费)
- ✅ 财务收支记录(收入、支出分类)
- ✅ 数据统计分析(出租率、投资回报率)
进阶功能(Nice to have):
- 🔄 收租日提醒
- 📊 数据可视化图表
- 📱 移动端随时查看
- 🔒 数据本地存储(隐私安全)
- 🎨 专业的商务风格UI
目标用户画像
张先生,38岁,IT行业,苏州
- 持有3套房产用于投资
- 工作繁忙,希望高效管理
- 重视数据隐私,不愿上传云端
- 需要专业的数据支撑投资决策
-
🛠️ 为什么选择uni-app x + HarmonyOS {#技术选型}
技术选型的纠结过程
作为一个全栈开发者,我面临着技术选型的"幸福烦恼"。让我列出当时的思考过程:
方案一:原生开发(HarmonyOS + Swift/Kotlin)
优势:
- ✅ 性能最佳,用户体验极致
- ✅ 可以使用平台最新特性
- ✅ 没有框架限制
劣势:
- ❌ 开发成本高(三端分别开发)
- ❌ 维护困难(代码量 x3)
- ❌ 学习成本高(多种语言)
- ❌ 时间成本无法接受(个人项目)
结论: 🚫 虽然性能最好,但对个人开发者不现实
方案二:React Native
优势:
- ✅ 生态成熟,社区活跃
- ✅ 组件库丰富
- ✅ 跨平台能力强
劣势:
- ❌ 对HarmonyOS支持有限
- ❌ 包体积较大(基础包10MB+)
- ❌ 性能相对较差
- ❌ 需要学习React生态
结论: 🤔 可行,但HarmonyOS支持是短板
方案三:Flutter
优势:
- ✅ 性能优秀(接近原生)
- ✅ UI组件精美
- ✅ 热重载提升效率
劣势:
- ❌ Dart语言学习成本
- ❌ 包体积大(基础包15MB+)
- ❌ HarmonyOS支持需要额外适配
- ❌ 对前端开发者不够友好
结论: 🤔 性能好,但学习成本高
方案四:uni-app x(最终选择)✨
优势:
- ✅ 原生性能:基于原生渲染,性能接近原生
- ✅ TypeScript支持:类型安全,开发体验好
- ✅ 一次开发,三端运行:iOS、Android、HarmonyOS
- ✅ Vue生态:前端开发者无缝上手
- ✅ 官方HarmonyOS支持:DCloud官方适配
- ✅ 包体积小:基础包仅5MB
- ✅ 开发效率高:热重载+组件化
劣势:
- ⚠️ 生态相对较新(但在快速完善)
- ⚠️ 部分高级特性需要等待
结论: 🎉 综合考虑,uni-app x是最佳选择!
为什么HarmonyOS是未来趋势?
作为开发者,我深信HarmonyOS代表着移动生态的未来:
- 国产化浪潮:信创政策推动,政企市场潜力巨大
- 技术创新:分布式能力、原子化服务让人眼前一亮
- 生态建设:华为全力推进,开发者红利期
- 市场份额:2024年国内份额已突破20%,增长迅猛
- 开发者友好:完善的文档、活跃的社区
我的判断:提前布局HarmonyOS,就是抓住下一个技术红利!
最终技术栈
经过一周的调研和demo测试,我确定了技术栈:
核心技术明细:
- 前端框架:uni-app x (基于Vue 3)
- 编程语言:TypeScript (主要) + UTS (平台特性)
- 样式方案:SCSS + CSS Variables(主题系统)
- 数据库:HarmonyOS relationalStore (SQLite)
- 状态管理:Composition API + Reactive
- 数据请求:uni.request + Promise
- 本地存储:uni.storage (配置) + relationalStore (业务数据)
-
UI组件:uni-ui + 自定义组件库
🏗️ 系统架构设计:四大模块协同作战 {#架构设计}
整体架构图
系统采用分层架构 + 模块化设计:
核心数据模型(ER图)
数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。
设计亮点:
- property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
- 自动同步机制:收租记录→财务记录的自动创建
- 状态联动:租户状态→房间状态的自动更新
- 数据溯源:通过description字段记录数据来源
项目目录结构
房产投资管理系统/
│
├── pages/ # 页面目录(18个页面)
│ ├── index/ # 首页模块
│ │ └── index.vue # 首页(数据总览)
│ │
│ ├── property/ # 房产管理模块
│ │ ├── list.vue # 房产列表
│ │ ├── add.vue # 添加/编辑房产
│ │ └── detail.vue # 房产详情
│ │
│ ├── rental/ # 租赁管理模块
│ │ ├── room-list.vue # 房间列表
│ │ ├── room-add.vue # 添加/编辑房间
│ │ ├── room-detail.vue # 房间详情
│ │ ├── tenant-list.vue # 租户列表
│ │ ├── tenant-add.vue # 添加/编辑租户
│ │ ├── tenant-detail.vue # 租户详情
│ │ ├── rent-collection.vue # 收租记录列表
│ │ └── rent-add.vue # 添加收租记录
│ │
│ ├── finance/ # 财务管理模块
│ │ ├── list.vue # 财务记录列表
│ │ └── add.vue # 添加财务记录
│ │
│ └── stats/ # 统计分析模块
│ └── index.vue # 数据统计页
│
├── uni_modules/ # 插件模块
│ └── test-relationalStore/ # 数据库封装
│ └── utssdk/
│ └── app-harmony/
│ └── index.uts # HarmonyOS数据库适配
│
├── static/ # 静态资源
│ ├── logo.png # 应用图标
│ ├── 我的资产.png # Tab图标
│ ├── 房产.png
│ ├── 财务.png
│ └── 统计.png
│
├── uni.scss # 全局样式变量
├── App.vue # 应用入口
├── pages.json # 页面配置
├── manifest.json # 应用配置
└── project_analysis.md # 项目开发文档
设计原则:
- 模块化:按功能模块划分目录
- 复用性:list-add-detail的标准页面结构
- 可维护性:清晰的命名和目录层级
💎 核心功能实现:18个页面的血泪史 {#核心功能}
功能模块一:房产管理 - 资产的数字化档案
1.1 房产列表页(pages/property/list.vue)
功能需求:
- 展示所有房产列表
- 显示总资产价值
- 支持房产的增删改查
核心代码:总资产价值计算
// 计算总资产价值
computed: {
totalValue() {
const total = this.propertyList.reduce((sum, item) => {
// 优先使用当前估值,否则使用购买价格
const currentValue = parseFloat(item.current_value) || 0;
const purchasePrice = parseFloat(item.purchase_price) || 0;
const propertyValue = currentValue || purchasePrice;
console.log(`房产 ${item.address}:
current_value=${item.current_value}(${typeof item.current_value}),
purchase_price=${item.purchase_price}(${typeof item.purchase_price}),
使用价值=${propertyValue}`);
return sum + propertyValue;
}, 0);
console.log('总资产价值计算结果:', total);
return total;
}
}
踩坑记录1:资产价值计算错误
问题现象:
总资产显示为"100200300"而不是"600"(三套房各200万)
原因分析:
数据库查询时用getString获取价格字段,导致数值以字符串形式返回。JavaScript的+操作符对字符串执行拼接而非数学加法。
解决方案:
在数据库转换函数中,数值类型字段使用getDouble而非getString
// ❌ 错误写法
function convertResultSetToPropertyArray(resultSet) {
record.purchase_price = resultSet.getString(
resultSet.getColumnIndex('purchase_price')
); // 返回字符串
}
// ✅ 正确写法
function convertResultSetToPropertyArray(resultSet) {
record.purchase_price = resultSet.getDouble(
resultSet.getColumnIndex('purchase_price')
); // 返回数值
}
经验总结:
- ✅ 整数/小数字段:用
getDouble() - ✅ 文本/ID字段:用
getString() - ✅ 在computed中加console.log调试
- ✅ 前端再用
parseFloat兜底确保类型正确
1.2 添加房产页(pages/property/add.vue)
功能需求:
- 录入房产基本信息
- 表单验证
- 数据持久化
核心代码:表单验证
validateForm() {
// 地址验证
if (!this.formData.address || this.formData.address.trim() === '') {
uni.showToast({
title: '请输入房产地址',
icon: 'none'
});
return false;
}
// 面积验证
if (!this.formData.area || parseFloat(this.formData.area) <= 0) {
uni.showToast({
title: '请输入有效的房产面积',
icon: 'none'
});
return false;
}
// 价格验证
if (!this.formData.purchase_price ||
parseFloat(this.formData.purchase_price) <= 0) {
uni.showToast({
title: '请输入有效的购买价格',
icon: 'none'
});
return false;
}
return true;
}
设计亮点:
- 实时验证:输入时即时反馈
- 友好提示:明确告知用户错误原因
- 数据类型转换:字符串→数值的安全转换
功能模块二:租赁管理 - 最复杂的业务逻辑
2.1 房间管理:7种房型 x 4种付款方式 = 28种组合
功能需求:
- 支持7种房型(单间、一室一厅、两室一厅...)
- 支持4种付款方式(押一付一、押一付三...)
- 自定义每月收租日(1-31号)
- 房间状态管理(空置/已租/维修中)
核心代码:房间类型映射
// 房型映射
getRoomTypeText(type) {
const typeMap = {
'single': '单间',
'1b1b': '一室一厅',
'2b1b': '两室一厅',
'2b2b': '两室两厅',
'3b1b': '三室一厅',
'3b2b': '三室两厅',
'4b2b': '四室两厅'
};
return typeMap[type] || type;
}
// 付款方式映射
getPaymentModeText(mode) {
const modeMap = {
'monthly': '押一付一',
'quarterly': '押一付三',
'half_yearly': '半年付',
'yearly': '年付'
};
return modeMap[mode] || mode;
}
踩坑记录2:收租日选择器无法显示全部31天
问题现象:
HarmonyOS的<picker>组件只能显示有限的选项,无法展示完整的31天选择。
解决方案:自定义Grid布局选择器
<template>
<!-- 自定义模态框 -->
<view class="rent-day-modal" v-if="showRentDayModal" @click="cancelRentDay">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">选择每月收租日</text>
</view>
<!-- 7x5 Grid布局显示31天 -->
<view class="day-grid">
<view class="day-item"
v-for="day in 31"
:key="day"
:class="{ selected: selectedRentDay === day }"
@click="selectRentDay(day)">
<text class="day-text">{{ day }}</text>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="cancelRentDay">取消</button>
<button class="confirm-btn" @click="confirmRentDay">确定</button>
</view>
</view>
</view>
</template>
<style lang="scss">
.day-grid {
display: grid;
grid-template-columns: repeat(7, 1fr); // 7列布局,展示31天
gap: 12rpx;
padding: 20rpx;
}
.day-item {
aspect-ratio: 1; // 保持正方形
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
background: #F5F5F5;
transition: all 0.2s;
&.selected {
background: $primary-blue;
color: #FFFFFF;
transform: scale(1.1);
}
&:active {
opacity: 0.7;
}
}
</style>
经验总结:
当原生组件无法满足需求时,果断自定义实现。Grid布局非常适合日历、选择器等场景。
2.2 租户管理:隐私保护与数据联动
功能需求:
- 租户信息录入(姓名、电话、身份证)
- 身份证号脱敏显示
- 租户状态与房间状态联动
- 合同期限管理
核心代码:身份证号脱敏
// 身份证号脱敏处理
maskIdCard(idCard) {
if (!idCard || idCard.length < 8) return idCard;
// 显示前4位和后4位,中间用*替代
const start = idCard.substring(0, 4);
const end = idCard.substring(idCard.length - 4);
const middle = '*'.repeat(idCard.length - 8);
return `${start}${middle}${end}`;
}
// 使用示例
computed: {
maskedIdCard() {
return this.maskIdCard(this.tenant.id_card);
// 输入:510123199001011234
// 输出:5101**********1234
}
}
踩坑记录3:租户状态判断不准确
问题现象:
主页快捷操作"添加租户",明明房间没有租户,却提示"房间已有租户"。
错误逻辑:依赖房间status字段
// ❌ 不准确的判断
if (room.status === 'rented') {
uni.showToast({ title: '房间已有租户' });
}
根本原因:
房间状态字段可能因为各种原因(删除租户时未更新、数据异常等)导致状态不准确。
正确方案:查询实际租户数据
// ✅ 准确的判断:查询数据库
checkRoomTenants(room) {
uni.queryWithConditions('tenants',
`room_id = ${room.id} AND status = 'active'`)
.then(tenants => {
console.log(`房间 ${room.room_number} 的租户查询结果:`,
tenants?.length || 0, '个活跃租户');
if (tenants && tenants.length > 0) {
// 确实有租户
uni.showModal({
title: '房间已有租户',
content: `该房间已有${tenants.length}位租户,是否查看?`,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: `/pages/rental/tenant-list?roomId=${room.id}`
});
}
}
});
} else {
// 房间确实空置,可以添加租户
uni.navigateTo({
url: `/pages/rental/tenant-add?roomId=${room.id}`
});
}
})
.catch(err => {
console.error('查询房间租户失败:', err);
uni.showToast({
title: '查询租户信息失败',
icon: 'error'
});
});
}
设计原则:
关键业务逻辑,永远基于实际数据查询,而非缓存状态!
租户删除后的房间状态联动
// 删除租户时,自动更新房间状态
performDelete() {
const roomId = this.tenant.room_id;
uni.deleteData('tenants', `id = ${this.tenantId}`)
.then(() => {
// 删除成功后,检查房间是否还有其他租户
return this.updateRoomStatus(roomId);
})
.then(() => {
uni.showToast({
title: '删除成功,即将返回',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800);
});
}
// 智能更新房间状态
updateRoomStatus(roomId) {
if (!roomId) return Promise.resolve();
// 查询该房间是否还有活跃租户
return uni.queryWithConditions('tenants', `room_id = ${roomId}`)
.then(tenants => {
const hasActiveTenant = tenants &&
tenants.some(tenant => tenant.status === 'active');
// 根据实际情况更新房间状态
const newStatus = hasActiveTenant ? 'rented' : 'vacant';
return uni.updateData('rooms', {
status: newStatus,
updated_at: new Date().toISOString()
}, `id = ${roomId}`);
})
.then(() => {
console.log('房间状态更新成功');
})
.catch(err => {
console.error('房间状态更新失败:', err);
});
}
2.3 收租记录管理:自动化是关键
功能需求:
- 记录每次收租情况
- 区分租金和水电费
- 支持多种支付方式
- 自动创建财务记录
核心代码:收租记录保存与财务同步
saveRentCollection() {
if (!this.validateForm()) {
return;
}
const collectionData = {
tenant_id: parseInt(this.formData.tenant_id),
collection_date: this.formData.collection_date,
rent_amount: parseFloat(this.formData.rent_amount) || 0,
utility_amount: parseFloat(this.formData.utility_amount) || 0,
total_amount: this.totalAmount,
payment_method: this.formData.payment_method || 'cash',
status: this.formData.status || 'pending',
notes: this.formData.notes || null,
updated_at: new Date().toISOString()
};
// 保存收租记录
uni.insertData('rent_collections', collectionData)
.then(() => {
// 🔥 核心:如果收租状态为已收租,自动创建财务记录
if (collectionData.status === 'collected') {
return this.createFinancialRecord(collectionData);
}
return Promise.resolve();
})
.then(() => {
uni.showToast({
title: '收租记录添加成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800);
})
.catch(err => {
console.error('收租记录保存失败:', err);
uni.showToast({
title: '保存失败,请重试',
icon: 'error'
});
});
}
// 创建对应的财务记录(核心同步逻辑)
createFinancialRecord(collectionData) {
return new Promise((resolve, reject) => {
// 1. 查询租户信息
uni.queryWithConditions('tenants', `id = ${collectionData.tenant_id}`)
.then(tenantResults => {
if (!tenantResults || tenantResults.length === 0) {
console.warn('未找到租户信息,跳过财务记录创建');
return resolve();
}
const tenant = tenantResults[0];
// 2. 查询房间信息(获取property_id)
return uni.queryWithConditions('rooms', `id = ${tenant.room_id}`)
.then(roomResults => {
if (!roomResults || roomResults.length === 0) {
console.warn('未找到房间信息,跳过财务记录创建');
return resolve();
}
const room = roomResults[0];
// 3. 创建财务记录
const financialData = {
property_id: room.property_id,
room_id: room.id, // 🔥 关键:关联房间
type: 'income',
category: 'rent',
amount: collectionData.total_amount,
description: `${tenant.name}租金收入 - ${room.room_number}号房`,
record_date: collectionData.collection_date,
created_at: new Date().toISOString()
};
return uni.insertData('financial_records', financialData);
});
})
.then(() => {
console.log('财务记录创建成功');
resolve();
})
.catch(err => {
console.error('财务记录创建失败:', err);
resolve(); // 不阻断主流程
});
});
}
设计亮点:
- 自动化同步:收租后自动创建财务记录,无需手动二次录入
- 数据溯源:通过description字段清晰标识收入来源
- 三级关联:租户→房间→房产,确保数据完整性
- 容错机制:财务记录失败不影响收租记录保存
功能模块三:财务管理 - 数据的统一中心
3.1 财务记录:收支一目了然
功能需求:
- 记录所有收入支出
- 支持房间级别关联
- 分类管理(租金、押金、维修等)
- 数据统计分析
踩坑记录4:财务记录与租金管理数据不同步
问题现象:
- 在财务页面添加租金收入,租金管理中看不到
- 在租金管理添加收入,有时财务记录中不显示
根本原因:
财务记录表缺少room_id字段,无法建立房间级别的数据关联。
解决方案:增强数据库结构
// 1. 修改financial_records表结构,添加room_id字段
let columnList = [
{ 'name': 'id', 'type': 'integer', 'nullable': false, 'primary': true },
{ 'name': 'property_id', 'type': 'integer', 'nullable': true },
{ 'name': 'room_id', 'type': 'integer', 'nullable': true }, // 🔥 新增
{ 'name': 'type', 'type': 'text', 'nullable': false },
{ 'name': 'category', 'type': 'text', 'nullable': false },
{ 'name': 'amount', 'type': 'real', 'nullable': false },
{ 'name': 'description', 'type': 'text', 'nullable': true },
{ 'name': 'record_date', 'type': 'text', 'nullable': false },
{ 'name': 'created_at', 'type': 'text', 'nullable': true }
];
// 2. 修改数据转换函数
function convertResultSetToFinancialArray(resultSet) {
// ...
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));
record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // 🔥 新增
record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // 🔥 改为getDouble
// ...
}
功能增强:财务记录添加房间选择
<template>
<!-- 房产选择 -->
<view class="field-group">
<text class="field-label">所属房产 *</text>
<view @click="showPropertyPicker">
<text>{{ formData.property_address || '请选择房产' }}</text>
</view>
</view>
<!-- 🔥 新增:房间选择(选择房产后动态显示) -->
<view class="field-group"
v-if="formData.property_id && roomList.length > 0">
<text class="field-label">所属房间</text>
<view @click="showRoomPicker">
<text>{{ formData.room_number ?
`${formData.room_number}号房` : '请选择房间(可选)' }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
property_id: null,
room_id: null, // 🔥 新增
room_number: '', // 🔥 新增
// ...
},
roomList: [] // 🔥 新增
}
},
methods: {
// 房产选择后,自动加载房间列表
handlePropertySelected(property) {
this.formData.property_id = property.id;
this.formData.property_address = property.address;
// 清空之前的房间选择
this.formData.room_id = null;
this.formData.room_number = '';
// 🔥 加载该房产的房间列表
this.loadRoomList(property.id);
},
loadRoomList(propertyId) {
if (!propertyId) {
this.roomList = [];
return;
}
uni.queryWithConditions('rooms', `property_id = ${propertyId}`)
.then(results => {
console.log('房间列表查询成功:', results?.length || 0, '个房间');
this.roomList = results || [];
})
.catch(err => {
console.error('房间列表查询失败:', err);
this.roomList = [];
});
},
showRoomPicker() {
if (this.roomList.length === 0) {
uni.showToast({
title: '该房产暂无房间',
icon: 'none'
});
return;
}
const roomNameList = this.roomList.map(room =>
`${room.room_number}号房 (${room.monthly_rent}元/月)`);
uni.showActionSheet({
itemList: roomNameList,
success: (res) => {
const selectedRoom = this.roomList[res.tapIndex];
this.formData.room_id = selectedRoom.id;
this.formData.room_number = selectedRoom.room_number;
}
});
}
}
}
</script>
功能模块四:数据统计 - 让数字说话
4.1 首页数据总览:一眼看懂经营状况
功能需求:
- 总资产价值展示
- 本月收益分析
- 租赁管理概览
- 快捷操作入口
核心代码:营业收入计算
踩坑记录5:主页营业收入不包含财务记录
问题现象:
在财务页面添加的收入,主页营业收入不显示。
错误逻辑:分离计算
// ❌ 错误:分离计算导致遗漏
monthlyTotalIncome() {
return this.monthlyRentIncome + this.monthlyNonRentIncome;
}
monthlyRentIncome() {
// 只从rent_collections表计算
return this.rentCollections
.filter(r => r.status === 'collected')
.reduce((sum, r) => sum + parseFloat(r.total_amount || 0), 0);
}
monthlyNonRentIncome() {
// 从financial_records表计算,但排除了租金
return this.getMonthlyRecords('income')
.filter(r => r.category !== 'rent')
.reduce((sum, r) => sum + parseFloat(r.amount || 0), 0);
}
问题分析:
- 直接在财务页面添加的租金收入被排除了
- 数据来源不统一,导致统计不完整
正确方案:统一数据源
// ✅ 正确:所有收入统一从financial_records计算
monthlyTotalIncome() {
return this.monthlyIncome; // 统一数据源
}
monthlyIncome() {
// 财务记录中当月所有收入之和
return this.getMonthlyRecords('income').reduce((sum, record) => {
return sum + parseFloat(record.amount || 0);
}, 0);
}
getMonthlyRecords(type) {
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7); // YYYY-MM
return this.financialRecords.filter(record =>
record.type === type &&
record.record_date &&
record.record_date.startsWith(currentMonth)
);
}
架构优化总结:
- ✅ 财务记录表是唯一的收入统计数据源
- ✅ 收租记录表仅用于收租流程管理
- ✅ 收租记录自动创建财务记录,实现数据同步
- ✅ 统计计算全部基于financial_records表,避免遗漏
4.2 统计分析页:深度数据洞察
功能需求:
- 房产收益排行
- 支出分析
- 租赁分析(出租率、租金收入占比)
- 租户分析
核心代码:出租率计算
computed: {
// 总房间数
totalRooms() {
return this.roomList.length;
},
// 已出租房间数
rentedRooms() {
return this.roomList.filter(room => room.status === 'rented').length;
},
// 房间出租率
roomOccupancyRate() {
if (this.totalRooms === 0) return 0;
return ((this.rentedRooms / this.totalRooms) * 100).toFixed(1);
},
// 租金收入占总收入比例
rentIncomePercentage() {
if (this.totalIncomeAmount === 0) return 0;
return ((this.rentIncomeAmount / this.totalIncomeAmount) * 100).toFixed(1);
},
// 平均租金水平
averageMonthlyRent() {
if (this.roomList.length === 0) return 0;
const totalRent = this.roomList.reduce((sum, room) =>
sum + parseFloat(room.monthly_rent || 0), 0);
return (totalRent / this.roomList.length).toFixed(0);
}
}
🔧 HarmonyOS适配的三大挑战与解决方案 {#harmonyos适配}
挑战一:Picker组件的兼容性问题
问题场景:
在租金记录页面,选择租户时使用<picker>组件,当有多个租户时点击无响应。
问题代码:
<!-- ❌ 在HarmonyOS上有问题 -->
<picker :range="tenantList"
range-key="name"
@change="onTenantChange">
<view>{{ selectedTenant?.name || '请选择租户' }}</view>
</picker>
根本原因:
HarmonyOS的picker组件在动态数据绑定和复杂对象数组处理上存在兼容性问题。
解决方案:使用uni.showActionSheet替代
// ✅ 完美兼容的方案
showTenantPicker() {
if (this.tenantList.length === 0) {
uni.showToast({
title: '暂无租户',
icon: 'none'
});
return;
}
const tenantNames = this.tenantList.map(t => t.name);
uni.showActionSheet({
itemList: tenantNames,
success: (res) => {
if (!res.cancel) {
const selectedTenant = this.tenantList[res.tapIndex];
this.formData.tenant_id = selectedTenant.id;
this.selectedTenantText = selectedTenant.name;
this.selectedTenant = selectedTenant;
uni.showToast({
title: `已选择: ${selectedTenant.name}`,
icon: 'success'
});
}
},
fail: (err) => {
console.log('用户取消选择或出错:', err);
}
});
}
HTML改造:
<!-- ✅ 使用点击事件触发 -->
<view class="picker-wrapper" @click="showTenantPicker">
<text class="picker-value">
{{ selectedTenantText || '请选择租户' }}
</text>
<text class="picker-arrow">👤</text>
</view>
| 优势对比: | 特性 | Picker组件 | ActionSheet |
|---|---|---|---|
| 兼容性 | ⚠️ 有问题 | ✅ 完美 | |
| 用户体验 | 一般 | ✅ 原生感强 | |
| 自定义 | 受限 | ✅ 灵活 | |
| 调试难度 | 较难 | 简单 |
经验总结:
在HarmonyOS开发中,涉及复杂数据的选择器,优先考虑
uni.showActionSheet或自定义组件。
挑战二:ResultSet数据类型转换陷阱
问题场景:
总资产计算时,100万+200万+300万 = "100200300"
问题代码:
// ❌ 错误:所有字段都用getString
function convertResultSetToFinancialArray(resultSet) {
const result = [];
resultSet.goToFirstRow();
for (let i = 0; i < count; i++) {
let record = {};
record.id = resultSet.getString(resultSet.getColumnIndex('id'));
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));
record.amount = resultSet.getString(resultSet.getColumnIndex('amount')); // ❌ 金额变成字符串
// ...
result[i] = record;
resultSet.goToNextRow();
}
return result;
}
问题分析:
// JavaScript的+操作符行为
"100" + "200" + "300" = "100200300" // ❌ 字符串拼接
100 + 200 + 300 = 600 // ✅ 数值相加
正确方案:严格区分数据类型
// ✅ 正确:根据字段类型选择方法
function convertResultSetToFinancialArray(resultSet) {
const result = [];
resultSet.goToFirstRow();
for (let i = 0; i < count; i++) {
let record = {};
record.id = resultSet.getString(resultSet.getColumnIndex('id')); // 文本
record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id')); // ID
record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // ID
record.type = resultSet.getString(resultSet.getColumnIndex('type')); // 文本
record.category = resultSet.getString(resultSet.getColumnIndex('category')); // 文本
record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // ✅ 数值
record.description = resultSet.getString(resultSet.getColumnIndex('description'));// 文本
record.record_date = resultSet.getString(resultSet.getColumnIndex('record_date'));// 日期
record.created_at = resultSet.getString(resultSet.getColumnIndex('created_at')); // 日期
result[i] = record;
resultSet.goToNextRow();
}
return result;
}
最佳实践规范:
// ResultSet数据类型选择指南
const TYPE_MAPPING = {
// 文本类型
'text': 'getString',
'varchar': 'getString',
// 数值类型
'integer': 'getLong', // 整数
'real': 'getDouble', // 小数
'numeric': 'getDouble', // 数值
// 特殊类型
'blob': 'getBlob', // 二进制
'boolean': 'getLong', // 布尔值(0/1)
// 日期类型(通常存为text)
'datetime': 'getString', // 日期时间
'date': 'getString' // 日期
};
调试技巧:
// 添加类型检查日志
computed: {
totalValue() {
const total = this.propertyList.reduce((sum, item) => {
const value = parseFloat(item.current_value) || 0;
// 🔥 调试日志
console.log(`房产: ${item.address}`);
console.log(` current_value: ${item.current_value} (${typeof item.current_value})`);
console.log(` 转换后: ${value} (${typeof value})`);
return sum + value;
}, 0);
console.log(`总资产: ${total} (${typeof total})`);
return total;
}
}
挑战三:生命周期与数据初始化
问题场景:
收租记录列表页面不停打印"收租记录表创建成功",数据加载重复执行。
问题代码:
// ❌ 错误:onLoad和onShow都初始化表
export default {
onLoad(options) {
this.initTable(); // 创建表
this.loadData(); // 加载数据
},
onShow() {
this.initTable(); // 又创建一次表!
this.loadData(); // 又加载一次数据!
}
}
问题分析:
- 每次页面显示都重新初始化表
- 导致重复的数据库操作
- 日志输出混乱,影响调试
正确方案:明确生命周期职责
// ✅ 正确:职责分离
export default {
data() {
return {
isTableInit: false, // 表初始化标记
propertyId: null,
dataList: []
}
},
onLoad(options) {
// 1. 获取路由参数
this.propertyId = options.propertyId;
// 2. 首次加载:初始化表 + 加载数据
this.initializeOnce();
},
onShow() {
// 3. 后续显示:仅刷新数据
if (this.isTableInit) {
this.refreshData();
}
},
methods: {
// 首次初始化(只执行一次)
async initializeOnce() {
if (this.isTableInit) return;
try {
// 初始化表结构
await this.initTable();
this.isTableInit = true;
console.log('表初始化完成');
// 加载初始数据
await this.loadData();
} catch (err) {
console.error('初始化失败:', err);
}
},
// 刷新数据(不重新初始化表)
async refreshData() {
try {
await this.loadDataList();
console.log('数据刷新完成');
} catch (err) {
console.error('数据刷新失败:', err);
}
},
// 初始化表结构
initTable() {
return uni.createTable('rent_collections', columnList)
.then(() => {
console.log('收租记录表创建成功');
})
.catch(err => {
// createTable会在表已存在时报错,这是正常的
console.log('表已存在或创建失败:', err);
});
},
// 加载完整数据
async loadData() {
await Promise.all([
this.loadTenantMap(), // 先加载依赖数据
this.loadRoomMap()
]);
await this.loadDataList(); // 再加载主数据
},
// 仅加载列表数据
loadDataList() {
return uni.queryWithConditions('rent_collections', '')
.then(results => {
this.dataList = results || [];
})
.catch(err => {
console.error('数据加载失败:', err);
this.dataList = [];
});
}
}
}
生命周期最佳实践:
页面首次加载 → onLoad
├─ 初始化表结构 (once)
├─ 加载依赖数据 (once)
└─ 加载主数据
页面再次显示 → onShow
└─ 仅刷新数据(不重新初始化)
🔄 数据同步机制:让数据流动起来 {#数据同步}
数据同步是整个系统的核心挑战。我设计了三层同步机制:
同步层级一:收租记录 → 财务记录
触发条件: 收租状态为"已收租"
// 自动同步流程
收租记录保存 (status='collected')
↓
查询租户信息 (获取租户名称)
↓
查询房间信息 (获取房间号、房产ID)
↓
创建财务记录 (income, category='rent')
↓
完成同步
同步层级二:租户状态 → 房间状态
触发条件: 添加/删除租户,租户状态变更
// 状态联动逻辑
租户操作完成
↓
查询该房间所有租户
↓
检查是否存在活跃租户 (status='active')
↓
更新房间状态
├─ 有活跃租户 → status='rented'
└─ 无活跃租户 → status='vacant'
同步层级三:房间选择 → 数据联动
触发条件: 财务记录选择房产
// 级联加载流程
选择房产
↓
清空之前的房间选择
↓
查询该房产的所有房间
↓
展示房间选择器(可选)
↓
选择房间后关联room_id
⚡ 性能优化:从卡顿到丝滑 {#性能优化}
优化一:Computed Property 缓存计算
问题: 在模板中直接调用方法导致重复计算
<!-- ❌ 错误:每次渲染都重新计算 -->
<text>{{ getTotalAssets() }}</text>
<text>{{ getTotalAssets() }}</text> <!-- 又计算一次 -->
优化: 使用计算属性缓存结果
// ✅ 优化:使用computed缓存
computed: {
totalAssets() {
return this.propertyList.reduce((sum, p) =>
sum + (parseFloat(p.current_value) || 0), 0);
}
}
<!-- ✅ 多次使用,只计算一次 -->
<text>{{ totalAssets }}</text>
<text>{{ totalAssets }}</text> <!-- 使用缓存值 -->
优化二:数据加载策略
问题: 首页加载慢,等待时间长
原因分析:
// ❌ 串行加载,总耗时 = 每个接口耗时之和
loadData() {
this.loadProperties(); // 100ms
this.loadFinancials(); // 150ms
this.loadRooms(); // 120ms
this.loadTenants(); // 100ms
// 总耗时: 470ms
}
优化方案: 并行加载
// ✅ 并行加载,总耗时 = 最慢的接口耗时
async loadData() {
try {
await Promise.all([
this.loadProperties(), // ┐
this.loadFinancials(), // ├─ 并行执行
this.loadRooms(), // ┤
this.loadTenants() // ┘
]);
// 总耗时: 150ms(最慢的接口)
// 依赖数据的后续处理
this.processData();
} catch (err) {
console.error('数据加载失败:', err);
}
}
性能对比:
- 串行加载:470ms
- 并行加载:150ms
- 性能提升:68% 🚀
优化三:避免不必要的数据库操作
问题: onShow时重复初始化表
优化: 使用标志位控制
data() {
return {
isTableInit: false // 🔥 标志位
}
},
methods: {
initTable() {
if (this.isTableInit) {
console.log('表已初始化,跳过');
return Promise.resolve();
}
return uni.createTable(...)
.then(() => {
this.isTableInit = true;
console.log('表初始化完成');
});
}
}
优化四:列表渲染优化
使用v-for时添加唯一key
<!-- ❌ 没有key,Vue无法高效更新 -->
<view v-for="item in list">
{{ item.name }}
</view>
<!-- ✅ 有key,Vue可以精确复用 -->
<view v-for="item in list" :key="item.id">
{{ item.name }}
</view>
避免在v-for中使用v-if
<!-- ❌ 性能差:先渲染再过滤 -->
<view v-for="item in list" :key="item.id" v-if="item.status === 'active'">
{{ item.name }}
</view>
<!-- ✅ 性能好:先过滤再渲染 -->
<view v-for="item in activeList" :key="item.id">
{{ item.name }}
</view>
<script>
computed: {
activeList() {
return this.list.filter(item => item.status === 'active');
}
}
</script>
🎨 UI/UX设计:商务风格的视觉语言 {#uiux设计}
设计理念
作为一款专业的房产管理工具,UI设计遵循以下原则:
- 商务专业:渐变、阴影、卡片化设计
- 信息清晰:合理的视觉层级和间距
- 操作便捷:大按钮、明确反馈
- 数据可视:数字大、图标辅助
SCSS变量系统
// uni.scss - 全局设计token
// ===== 颜色系统 =====
// 主色
$primary-blue: #3B82F6;
$primary-dark: #2563EB;
// 功能色
$income-text: #10B981; // 收入绿
$income-bg: rgba(16, 185, 129, 0.1);
$expense-text: #EF4444; // 支出红
$expense-bg: rgba(239, 68, 68, 0.1);
// 文本色
$text-primary: #1E293B; // 主要文本
$text-secondary: #64748B; // 次要文本
$text-tertiary: #94A3B8; // 辅助文本
// 背景色
$bg-primary: #FFFFFF;
$bg-secondary: #F8FAFC;
// ===== 间距系统 =====
$spacing-xs: 8rpx;
$spacing-sm: 12rpx;
$spacing-md: 16rpx;
$spacing-lg: 24rpx;
$spacing-xl: 32rpx;
$spacing-xxl: 48rpx;
// ===== 圆角系统 =====
$radius-small: 8rpx;
$radius-medium: 12rpx;
$radius-large: 16rpx;
$radius-pill: 999rpx;
// ===== 阴影系统 =====
$shadow-card: 0 8rpx 32rpx rgba(15, 23, 42, 0.08),
0 4rpx 16rpx rgba(15, 23, 42, 0.04);
// ===== 字体系统 =====
$font-size-caption: 22rpx; // 说明文字
$font-size-body: 28rpx; // 正文
$font-size-subhead: 30rpx; // 副标题
$font-size-headline: 32rpx; // 标题
$font-size-title: 40rpx; // 大标题
卡片设计模式
// 统一的卡片样式
.info-card {
background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%);
border-radius: $radius-large;
padding: $spacing-xl;
box-shadow: $shadow-card;
border: 1rpx solid rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);
}
}
数据展示设计
大数字设计:
<view class="data-showcase">
<text class="data-number">¥{{ formatLargeNumber(2380000) }}</text>
<text class="data-label">总资产价值</text>
</view>
<style lang="scss">
.data-number {
font-size: 56rpx; // 大号字体
font-weight: 700; // 粗体
color: $text-primary;
line-height: 1.2;
letter-spacing: -0.5rpx; // 紧凑间距
}
.data-label {
font-size: $font-size-body;
color: $text-secondary;
margin-top: $spacing-xs;
}
</style>
收支颜色区分:
.income-color {
color: $income-text; // 绿色
}
.expense-color {
color: $expense-text; // 红色
}
用户体验细节
1. 操作反馈优化
// 成功反馈
uni.showToast({
title: '添加成功,即将返回', // 明确告知用户
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 800); // 800ms刚好够看到提示
2. 空状态设计
<view class="empty-state" v-if="dataList.length === 0">
<text class="empty-icon">🏠</text>
<text class="empty-title">暂无数据</text>
<text class="empty-subtitle">点击右上角按钮添加</text>
</view>
3. 加载状态
<view class="loading-state" v-if="isLoading">
<text class="loading-icon">⏳</text>
<text class="loading-text">加载中...</text>
</view>
🐛 踩坑经验:那些让我头秃的Bug {#踩坑经验}
Bug 1: 字符串拼接陷阱
现象: 100 + 200 + 300 = "100200300"
原因: 数据库返回字符串类型
解决: getDouble + parseFloat双保险
耗时: 2小时
Bug 2: Picker组件失效
现象: 选择租户时点击无反应
原因: HarmonyOS picker兼容问题
解决: 改用uni.showActionSheet
耗时: 3小时
Bug 3: 数据重复加载
现象: 不停打印"表创建成功"
原因: onLoad和onShow都初始化表
解决: 生命周期职责分离
耗时: 1小时
Bug 4: 房间状态误判
现象: 明明没租户却提示"已有租户"
原因: 依赖status字段而非实际数据
解决: 查询数据库确认
耗时: 1.5小时
Bug 5: 收入统计遗漏
现象: 财务页面添加的收入不计入统计
原因: 分离计算逻辑导致遗漏
解决: 统一数据源(financial_records)
耗时: 2小时
Bug 6: 收租日选择不全
现象: 只能选择部分日期
原因: Picker组件限制
解决: Grid布局自定义选择器
耗时: 4小时
总结:调试时间约占开发时间的30% 😅
🔨 开发工具与调试技巧 {#开发工具}
HBuilder X 实用功能
- 真机调试:实时预览,快速定位问题
- 代码提示:TypeScript类型提示
- 条件编译:处理平台差异
// #ifdef APP-HARMONY
// HarmonyOS专属代码
// #endif
// #ifdef APP-IOS
// iOS专属代码
// #endif
调试技巧总结
1. Console.log大法
// 关键位置添加日志
console.log('=== 数据加载开始 ===');
console.log('查询条件:', condition);
console.log('查询结果:', results);
console.log('=== 数据加载完成 ===');
2. 类型检查
console.log(`金额: ${amount}, 类型: ${typeof amount}`);
3. 生命周期追踪
onLoad() { console.log('⬇️ onLoad'); }
onShow() { console.log('👁️ onShow'); }
onHide() { console.log('🙈 onHide'); }
4. 数据快照
console.log('数据快照:', JSON.stringify(data, null, 2));
🚀 未来规划与展望 {#未来规划}
v1.1.0 - 功能增强(开发中)
- [ ] 合同到期提醒功能
- [ ] 收租日提醒(推送通知)
- [ ] 数据导出(Excel格式)
- [ ] 云同步备份
v1.2.0 - 数据可视化(规划中)
- [ ] 收益趋势图表(折线图)
- [ ] 房产价值变化(柱状图)
- [ ] 租客分布分析(饼图)
- [ ] 年度投资报告
v1.3.0 - 智能化(未来愿景)
- [ ] AI租金定价建议
- [ ] 空置期预测
- [ ] 智能收租提醒
- [ ] 租客画像分析
v2.0.0 - 多平台扩展
- [ ] iOS版本发布
- [ ] Android版本发布
- [ ] Web管理后台
- [ ] 数据云同步
💭 总结与感悟 {#总结}
开发历程回顾
开发阶段:
阶段一:需求分析 + 技术选型
阶段二:数据库设计 + 架构搭建
阶段三:核心功能开发(房产、租赁)
阶段四:财务统计模块
阶段五:HarmonyOS适配调试
阶段六:UI优化 + Bug修复
代码统计:
- 总代码量:10000+ 行
- 页面数量:18个
- 组件数量:30+个
- 数据表:5个核心表
技术收获
1. 深入理解HarmonyOS生态
从零开始学习HarmonyOS的relationalStore数据库,从陌生到熟悉,从踩坑到总结最佳实践。
2. 掌握uni-app x开发
- Composition API的使用
- 生命周期管理
- 数据响应式原理
- 跨平台适配技巧
3. 数据库设计能力
学会设计合理的数据关联关系,理解数据一致性的重要性。
4. 性能优化思维
从用户体验出发,优化加载速度、减少重复计算、提升交互流畅度。
思维成长
1. 用户思维
不仅是开发者,更是用户。每个功能都从实际需求出发,而非为了技术而技术。
2. 架构思维
先设计数据模型,再设计功能模块。好的架构是成功的一半。
3. 工程思维
代码质量、可维护性、可扩展性同样重要。写代码不仅是为了实现功能,更是为了长期维护。
4. 问题解决思维
遇到问题不慌张,系统分析、逐步排查、总结经验。
给开发者的建议
1. 技术选型要慎重
选择适合自己的技术栈,不要盲目追新。uni-app x + HarmonyOS对我来说是最佳选择。
2. 数据库设计要花时间
数据模型设计好了,后续开发事半功倍。不要急于写代码。
3. 用户体验无小事
800ms的延迟优化、身份证脱敏、空状态提示...这些细节决定产品质量。
4. 持续迭代比一次完美更重要
先实现MVP,再逐步完善。不要追求一次性做到完美。
5. 记录踩坑经验
每个Bug都是成长的机会。记录下来,帮助自己也帮助别人。
写在最后
这个项目从构思到完成,经历了无数次的推翻重来、代码重构、问题调试。但当看到一个功能完整、数据准确、体验流畅的应用在HarmonyOS上运行时,所有的努力都值得了。
uni-app x + HarmonyOS Next 的组合,让我既能享受跨平台开发的便利,又能获得原生应用的性能。虽然过程中遇到了很多适配问题,但每个问题的解决都让我对HarmonyOS生态有了更深的理解。
HarmonyOS代表着未来,提前布局就是抓住机遇。感谢DCloud提供的强大框架,感谢HarmonyOS提供的优秀平台。
让我们一起,星光不负,码向未来! 🚀
收起阅读 »香港开发者无法完成手机号验证
尊敬的 DCloud 技术支持团队,
你们好!
我是一位来自香港的开发者,正在使用你们的 uni-app 框架进行开发,非常感谢你们提供优秀的工具。
目前,我在进行开发者账户的 手机号验证 时遇到了一个阻碍。在“验证手机号”的页面中,系统只允许输入11位数字的手机号码,并且 没有提供国家/地区代码的下拉选择框。
我的香港手机号码格式为 +852 XXXX XXXX(共8位),无法在目前仅支持+86号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。
作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:
- 是否有针对香港或国际开发者的特殊验证流程?
- 能否在验证页面添加国家代码(例如+852)的选择功能?
- 或者,能否请你们协助手动为我的账户完成手机号验证?
感谢你们的时间和帮助!期待你们的回复。
祝好,
尊敬的 DCloud 技术支持团队,
你们好!
我是一位来自香港的开发者,正在使用你们的 uni-app 框架进行开发,非常感谢你们提供优秀的工具。
目前,我在进行开发者账户的 手机号验证 时遇到了一个阻碍。在“验证手机号”的页面中,系统只允许输入11位数字的手机号码,并且 没有提供国家/地区代码的下拉选择框。
我的香港手机号码格式为 +852 XXXX XXXX(共8位),无法在目前仅支持+86号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。
作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:
- 是否有针对香港或国际开发者的特殊验证流程?
- 能否在验证页面添加国家代码(例如+852)的选择功能?
- 或者,能否请你们协助手动为我的账户完成手机号验证?
感谢你们的时间和帮助!期待你们的回复。
祝好,
1024星光不负,码向未来——以 uni-app 筑梦鸿蒙生态我的uni-app鸿蒙开发之旅
写在前面的话
当 1024 程序员节的代码星光点亮行业夜空,DCloud 发起的 “星光不负,码向未来” 鸿蒙主题征文活动,恰似一座连接开发者与生态未来的桥梁。作为一名深耕跨端开发五年的技术人,我始终感恩 DCloud 为我们搭建了如此优质的交流舞台,更庆幸能借 uni-app 技术栈,深度参与到鸿蒙生态的建设浪潮中。今天,我想分享一款基于 uni-app 开发的鸿蒙元服务项目实战经历,既是对这段开发旅程的复盘,也是对鸿蒙生态璀璨前景的致敬。
一、项目缘起:从痛点出发的技术选型
我们团队今年承接了一款本地生活类鸿蒙元服务的开发需求,核心目标是实现 “即时触达、轻量化交互、跨设备兼容”—— 用户无需安装 App,通过鸿蒙桌面卡片即可快速查看周边商户、领取优惠券并完成预约。在技术选型阶段,我们对比了多种开发方案:原生开发虽能深度适配鸿蒙特性,但跨设备兼容成本高;其他跨端框架对鸿蒙元服务的支持尚不完善。最终,uni-app 凭借其对鸿蒙生态的深度适配、与 Vue 语法的无缝衔接,以及 “一次开发、多端部署” 的核心优势,成为了我们的最优解。
更让我们惊喜的是,DCloud 提供的 uni-app 鸿蒙插件市场(很多鸿蒙插件给我们提供极大便利,开发中也得到鸿蒙插件作者@An_king 帮助)、官方文档与社区支持,为项目启动扫清了初期障碍。从基础配置到进阶功能,这让我们对项目落地充满了信心。
二、核心实践:鸿蒙能力与 uni-app 的深度融合
2.1 鸿蒙云开发的集成与落地
项目初期,我们面临着 “数据实时同步” 与 “服务器部署成本” 的双重挑战。鸿蒙云开发提供的云数据库、云函数等能力,恰好契合我们的需求。通过 uni-app 的扩展 API,我们实现了三大关键突破:
数据双向同步优化:利用鸿蒙云数据库的实时监听能力,结合 uni-app 的响应式数据绑定,实现了商户库存、优惠券余量与用户端的毫秒级同步。此前我们曾遭遇 “本地缓存与云端数据不一致” 的问题,通过自定义云函数,在数据更新时触发云端校验与本地缓存刷新,成功解决了这一痛点。
云函数的轻量化部署:将用户登录验证、优惠券核销等核心业务逻辑部署在鸿蒙云函数中,通过 uni-app 的云对象方法配合调用。这一方案不仅减少了客户端代码体积,更通过云端扩容能力,全面提升了App的性能。
权限申请的优雅处理:鸿蒙系统对用户权限的管控极为严格,我们借助 uni-app 的权限申请封装 API,结合鸿蒙元服务的特性,设计了 “按需申请、分步授权” 的交互流程。例如,在用户首次点击 “获取周边商户” 时才申请定位权限,并通过弹窗清晰说明权限用途,将授权通过率提升了 40%。
2.2 预加载能力的创新应用
元服务的 “秒开体验” 是项目的核心竞争力之一,而uniapp的预加载能力成为了关键突破口。我们通过 uni-app 的app.vue生命周期钩子,结合封装鸿蒙的原生组件,实现了精准化对接预加载策略:
针对高频访问的 “商户列表页”,在元服务启动前预加载核心数据与页面组件,将首屏加载时间从 1.2秒压缩至 0.4秒;
基于用户行为分析,对 “优惠券中心” 等次级页面实现 “智能预加载”—— 当用户浏览商户详情超过 3 秒时,后台异步预加载优惠券数据,既保证了响应速度,又避免了资源浪费。
这一过程中,我们曾遇到 “预加载资源占用过高导致卡顿” 的问题。通过 DCloud 社区的技术交流,我们借鉴了其他开发者分享的 “超级下拉列表“和“资源优先级排序” 方案,对预加载内容按重要性分级,优先加载文本数据,延迟加载图片资源,最终完美解决了性能瓶颈。
2.3 全流程落地:从开发到完成的实践复盘
需求与适配阶段:我们充分利用 uni-app 的多端适配能力,针对安卓、苹果、各家小程序、鸿蒙手机、平板、智慧屏等不同设备,通过uni.getSystemInfoSync()获取设备参数,实现了页面布局的自适应调整。特别是针对鸿蒙元服务的卡片尺寸限制,设计了 “核心信息精简展示、点击展开详情” 的交互模式。
调试与测试阶段:借助鸿蒙云测试平台与 uni-app 的真机调试功能,我们完成了多机型兼容性测试。DCloud 提供的 uni-app 调试工具能够精准定位鸿蒙特有的语法兼容问题,让我们在短时间内修复了 “组件生命周期触发异常”“API 调用权限不足” 等关键 Bug。
三、生态感悟:感恩同行,共赴未来
回望这段开发旅程,我们既惊叹于鸿蒙生态的高速发展,更感激 DCloud 为开发者提供的坚实支撑。从最初对鸿蒙技术的懵懂探索,到如今能熟练运用 uni-app 实现复杂业务场景,每一步成长都离不开 DCloud 官方的技术赋能与社区伙伴的经验分享。uni-app 就像一座桥梁,让我们这些跨端开发者能够低门槛地融入鸿蒙生态,用熟悉的技术栈创造出符合新时代需求的应用产品。
如今,HarmonyOS 6 的到来开启了生态发展的新篇章,流畅的性能、AI 原生能力等创新特性,为开发者提供了更广阔的创新空间。作为生态建设的参与者,我们深刻意识到,每一行代码都是生态的基石,每一次分享都是技术的传承。DCloud 发起的本次征文活动,不仅为我们提供了展示技术成果的舞台,更促进了鸿蒙生态开发者之间的思想碰撞,这种技术共享的氛围,正是行业进步的核心动力。
四、展望未来:以代码为笔,书写生态新篇
星光不负赶路人,时光不负奋斗者。在未来的开发道路上,我们团队将继续深耕 uni-app 与鸿蒙生态的融合创新,计划基于鸿蒙近场能力开发 “设备间优惠券共享” 功能,进一步拓展元服务的应用场景。同时,我们也将积极参与 DCloud 社区的技术分享,把项目中积累的经验转化为帮助他人的力量,就像曾经那些帮助过我们的开发者一样。
我们坚信,随着鸿蒙生态的持续完善与 DCloud 的不断赋能,会有更多开发者加入这场技术革新的浪潮。当无数开发者的代码星光汇聚,必将照亮鸿蒙生态的未来之路。最后,再次感谢 DCloud 举办本次征文活动,感谢每一位为生态建设默默付出的技术人。愿我们以代码为翼,与鸿蒙共成长,在数字时代的浪潮中,书写属于开发者的璀璨篇章!
写在前面的话
当 1024 程序员节的代码星光点亮行业夜空,DCloud 发起的 “星光不负,码向未来” 鸿蒙主题征文活动,恰似一座连接开发者与生态未来的桥梁。作为一名深耕跨端开发五年的技术人,我始终感恩 DCloud 为我们搭建了如此优质的交流舞台,更庆幸能借 uni-app 技术栈,深度参与到鸿蒙生态的建设浪潮中。今天,我想分享一款基于 uni-app 开发的鸿蒙元服务项目实战经历,既是对这段开发旅程的复盘,也是对鸿蒙生态璀璨前景的致敬。
一、项目缘起:从痛点出发的技术选型
我们团队今年承接了一款本地生活类鸿蒙元服务的开发需求,核心目标是实现 “即时触达、轻量化交互、跨设备兼容”—— 用户无需安装 App,通过鸿蒙桌面卡片即可快速查看周边商户、领取优惠券并完成预约。在技术选型阶段,我们对比了多种开发方案:原生开发虽能深度适配鸿蒙特性,但跨设备兼容成本高;其他跨端框架对鸿蒙元服务的支持尚不完善。最终,uni-app 凭借其对鸿蒙生态的深度适配、与 Vue 语法的无缝衔接,以及 “一次开发、多端部署” 的核心优势,成为了我们的最优解。
更让我们惊喜的是,DCloud 提供的 uni-app 鸿蒙插件市场(很多鸿蒙插件给我们提供极大便利,开发中也得到鸿蒙插件作者@An_king 帮助)、官方文档与社区支持,为项目启动扫清了初期障碍。从基础配置到进阶功能,这让我们对项目落地充满了信心。
二、核心实践:鸿蒙能力与 uni-app 的深度融合
2.1 鸿蒙云开发的集成与落地
项目初期,我们面临着 “数据实时同步” 与 “服务器部署成本” 的双重挑战。鸿蒙云开发提供的云数据库、云函数等能力,恰好契合我们的需求。通过 uni-app 的扩展 API,我们实现了三大关键突破:
数据双向同步优化:利用鸿蒙云数据库的实时监听能力,结合 uni-app 的响应式数据绑定,实现了商户库存、优惠券余量与用户端的毫秒级同步。此前我们曾遭遇 “本地缓存与云端数据不一致” 的问题,通过自定义云函数,在数据更新时触发云端校验与本地缓存刷新,成功解决了这一痛点。
云函数的轻量化部署:将用户登录验证、优惠券核销等核心业务逻辑部署在鸿蒙云函数中,通过 uni-app 的云对象方法配合调用。这一方案不仅减少了客户端代码体积,更通过云端扩容能力,全面提升了App的性能。
权限申请的优雅处理:鸿蒙系统对用户权限的管控极为严格,我们借助 uni-app 的权限申请封装 API,结合鸿蒙元服务的特性,设计了 “按需申请、分步授权” 的交互流程。例如,在用户首次点击 “获取周边商户” 时才申请定位权限,并通过弹窗清晰说明权限用途,将授权通过率提升了 40%。
2.2 预加载能力的创新应用
元服务的 “秒开体验” 是项目的核心竞争力之一,而uniapp的预加载能力成为了关键突破口。我们通过 uni-app 的app.vue生命周期钩子,结合封装鸿蒙的原生组件,实现了精准化对接预加载策略:
针对高频访问的 “商户列表页”,在元服务启动前预加载核心数据与页面组件,将首屏加载时间从 1.2秒压缩至 0.4秒;
基于用户行为分析,对 “优惠券中心” 等次级页面实现 “智能预加载”—— 当用户浏览商户详情超过 3 秒时,后台异步预加载优惠券数据,既保证了响应速度,又避免了资源浪费。
这一过程中,我们曾遇到 “预加载资源占用过高导致卡顿” 的问题。通过 DCloud 社区的技术交流,我们借鉴了其他开发者分享的 “超级下拉列表“和“资源优先级排序” 方案,对预加载内容按重要性分级,优先加载文本数据,延迟加载图片资源,最终完美解决了性能瓶颈。
2.3 全流程落地:从开发到完成的实践复盘
需求与适配阶段:我们充分利用 uni-app 的多端适配能力,针对安卓、苹果、各家小程序、鸿蒙手机、平板、智慧屏等不同设备,通过uni.getSystemInfoSync()获取设备参数,实现了页面布局的自适应调整。特别是针对鸿蒙元服务的卡片尺寸限制,设计了 “核心信息精简展示、点击展开详情” 的交互模式。
调试与测试阶段:借助鸿蒙云测试平台与 uni-app 的真机调试功能,我们完成了多机型兼容性测试。DCloud 提供的 uni-app 调试工具能够精准定位鸿蒙特有的语法兼容问题,让我们在短时间内修复了 “组件生命周期触发异常”“API 调用权限不足” 等关键 Bug。
三、生态感悟:感恩同行,共赴未来
回望这段开发旅程,我们既惊叹于鸿蒙生态的高速发展,更感激 DCloud 为开发者提供的坚实支撑。从最初对鸿蒙技术的懵懂探索,到如今能熟练运用 uni-app 实现复杂业务场景,每一步成长都离不开 DCloud 官方的技术赋能与社区伙伴的经验分享。uni-app 就像一座桥梁,让我们这些跨端开发者能够低门槛地融入鸿蒙生态,用熟悉的技术栈创造出符合新时代需求的应用产品。
如今,HarmonyOS 6 的到来开启了生态发展的新篇章,流畅的性能、AI 原生能力等创新特性,为开发者提供了更广阔的创新空间。作为生态建设的参与者,我们深刻意识到,每一行代码都是生态的基石,每一次分享都是技术的传承。DCloud 发起的本次征文活动,不仅为我们提供了展示技术成果的舞台,更促进了鸿蒙生态开发者之间的思想碰撞,这种技术共享的氛围,正是行业进步的核心动力。
四、展望未来:以代码为笔,书写生态新篇
星光不负赶路人,时光不负奋斗者。在未来的开发道路上,我们团队将继续深耕 uni-app 与鸿蒙生态的融合创新,计划基于鸿蒙近场能力开发 “设备间优惠券共享” 功能,进一步拓展元服务的应用场景。同时,我们也将积极参与 DCloud 社区的技术分享,把项目中积累的经验转化为帮助他人的力量,就像曾经那些帮助过我们的开发者一样。
我们坚信,随着鸿蒙生态的持续完善与 DCloud 的不断赋能,会有更多开发者加入这场技术革新的浪潮。当无数开发者的代码星光汇聚,必将照亮鸿蒙生态的未来之路。最后,再次感谢 DCloud 举办本次征文活动,感谢每一位为生态建设默默付出的技术人。愿我们以代码为翼,与鸿蒙共成长,在数字时代的浪潮中,书写属于开发者的璀璨篇章!
Uniapp 的鸿蒙 next 应用中隐藏和显示系统状态栏
本示例至少需要在 HbuilderX 4.61 运行
在 uniapp 开发鸿蒙应用中,通过 UTS 插件,可以调用许多系统原生的 API,这里给出一个小功能:隐藏和显示系统状态栏
原始的界面效果:
隐藏系统状态栏之后的效果:
这个示例的参考文档有:
- 鸿蒙官方文档: https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkui-193
- UTS 插件: https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin.html
- UTSHarmony 的使用: https://doc.dcloud.net.cn/uni-app-x/uts/utsharmony.html
核心页面代码:
<template>
<view>
<button @click="show">显示原生状态栏</button>
<button @click="hide">隐藏原生状态栏</button>
</view>
</template>
<script>
import { showStatusBar, hideStatusBar } from '@/uni_modules/harmony-statusbar';
export default {
data() {
return {
message: 'Hello, World!'
}
},
methods: {
show() {
showStatusBar()
},
hide() {
hideStatusBar()
}
}
}
</script>
<style scoped>
</style>
核心UTS插件代码:
import window from '@ohos.window';
let _window : window.Window;
UTSHarmony.onAppAbilityWindowStageCreate((windowStage : window.WindowStage) => {
_window = windowStage.getMainWindowSync()
})
export const hideStatusBar = () => {
_window.setWindowSystemBarEnable([])
}
export const showStatusBar = () => {
_window.setWindowSystemBarEnable(['status', 'navigation'])
}
示例工程:
本示例至少需要在 HbuilderX 4.61 运行
在 uniapp 开发鸿蒙应用中,通过 UTS 插件,可以调用许多系统原生的 API,这里给出一个小功能:隐藏和显示系统状态栏
原始的界面效果:
隐藏系统状态栏之后的效果:
这个示例的参考文档有:
- 鸿蒙官方文档: https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkui-193
- UTS 插件: https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin.html
- UTSHarmony 的使用: https://doc.dcloud.net.cn/uni-app-x/uts/utsharmony.html
核心页面代码:
<template>
<view>
<button @click="show">显示原生状态栏</button>
<button @click="hide">隐藏原生状态栏</button>
</view>
</template>
<script>
import { showStatusBar, hideStatusBar } from '@/uni_modules/harmony-statusbar';
export default {
data() {
return {
message: 'Hello, World!'
}
},
methods: {
show() {
showStatusBar()
},
hide() {
hideStatusBar()
}
}
}
</script>
<style scoped>
</style>
核心UTS插件代码:
import window from '@ohos.window';
let _window : window.Window;
UTSHarmony.onAppAbilityWindowStageCreate((windowStage : window.WindowStage) => {
_window = windowStage.getMainWindowSync()
})
export const hideStatusBar = () => {
_window.setWindowSystemBarEnable([])
}
export const showStatusBar = () => {
_window.setWindowSystemBarEnable(['status', 'navigation'])
}
示例工程:
收起阅读 »经验分享 鸿蒙里的权限设置,如何获取、查询权限
鸿蒙里的权限
鸿蒙的权限可以分成三类:
开放权限:system_grant, 比如 INTERNET网络权限、VIBRATE 手机震动权限等。无需用户同意。具体可见 开放权限(系统授权)
用户授权:user_grant,弹窗询问用户是否允许位置定位、发送通知等。具体可见 开放权限(用户授权)
敏感权限:需要在华为后台单独填写表格申请获得,比如修改用户公共目录文件、API 读取剪切板等。具体可见 受限开放权限
还有一些针对特定企业管理的权限,场景比较特殊,这里不做进一步描述。
细节可以看文档 《鸿蒙权限配置指南》
如何定义权限
举例定位中用到的模糊定位、精准定位。需要参考文档,在 requestPermissions
如何查询权限是否授权?
const auth = () => {
const res = uni.getAppAuthorizeSetting()
console.log(res)
}
如何主动申请用户授权特定的权限?
先见 uts-api 鸿蒙插件,填写下面代码, uni_modules/harmony-harmony/utssdk/app-harmony/index.uts
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
export const requestSystemPermission = () => {
const permissionList : Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']
UTSHarmony.requestSystemPermission(permissionList, (allRight : boolean, grantedList : Array<string>) => {
console.log('res', allRight, grantedList);
}, (doNotAskAgain : boolean, grantedList : Array<string>) => {
console.log('fail', doNotAskAgain, grantedList);
})
}
在 vue 代码中这样使用
<script setup lang="uts">
import { requestSystemPermission } from '@/uni_modules/harmony-harmony'
const permisson = () => {
requestSystemPermission()
}
</script>
如何打开系统设置?
可引导用户打开设置重新授权。
uni.openAppAuthorizeSetting()
https://uniapp.dcloud.net.cn/api/system/openappauthorizesetting.html
鸿蒙里的权限
鸿蒙的权限可以分成三类:
开放权限:system_grant, 比如 INTERNET网络权限、VIBRATE 手机震动权限等。无需用户同意。具体可见 开放权限(系统授权)
用户授权:user_grant,弹窗询问用户是否允许位置定位、发送通知等。具体可见 开放权限(用户授权)
敏感权限:需要在华为后台单独填写表格申请获得,比如修改用户公共目录文件、API 读取剪切板等。具体可见 受限开放权限
还有一些针对特定企业管理的权限,场景比较特殊,这里不做进一步描述。
细节可以看文档 《鸿蒙权限配置指南》
如何定义权限
举例定位中用到的模糊定位、精准定位。需要参考文档,在 requestPermissions
如何查询权限是否授权?
const auth = () => {
const res = uni.getAppAuthorizeSetting()
console.log(res)
}
如何主动申请用户授权特定的权限?
先见 uts-api 鸿蒙插件,填写下面代码, uni_modules/harmony-harmony/utssdk/app-harmony/index.uts
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
export const requestSystemPermission = () => {
const permissionList : Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']
UTSHarmony.requestSystemPermission(permissionList, (allRight : boolean, grantedList : Array<string>) => {
console.log('res', allRight, grantedList);
}, (doNotAskAgain : boolean, grantedList : Array<string>) => {
console.log('fail', doNotAskAgain, grantedList);
})
}
在 vue 代码中这样使用
<script setup lang="uts">
import { requestSystemPermission } from '@/uni_modules/harmony-harmony'
const permisson = () => {
requestSystemPermission()
}
</script>
如何打开系统设置?
可引导用户打开设置重新授权。
uni.openAppAuthorizeSetting()
https://uniapp.dcloud.net.cn/api/system/openappauthorizesetting.html
收起阅读 »经验分享 鸿蒙中如何隐藏底部触控小白条?
在鸿蒙底部有触控小白条,用来响应系统级用户手势。在应用开发时候,有些业务场景需要隐藏底部触控小白条,鸿蒙提供了响应 API,代码比较简单,使用 UTS 几行代码轻松切换展示。
在 HBuilderX 中新建 uni_modules 文件夹,在 uni_modules 文件夹右键选择创建 UTS-API 插件,创建并编辑 app-harmony/index.uts 文件夹,如果没有就新建该文件。
在文件中填写下面代码:
/**
* 展示底部小白条
*/
export const showNavigationIndicator = () => {
const window = UTSHarmony.getCurrentWindow()
window.setSpecificSystemBarEnabled('navigationIndicator', true)
}
/**
* 隐藏底部小白条
*/
export const hideNavigationIndicator = () => {
const window = UTSHarmony.getCurrentWindow()
window.setSpecificSystemBarEnabled('navigationIndicator', false)
}
在 Vue 页面中导入并使用即可。
<template>
<view>
<button @click="showNavigationIndicator">showNavigationIndicator</button>
<button @click="hideNavigationIndicator">hideNavigationIndicator</button>
</view>
</template>
<script setup lang="uts">
import {
showNavigationIndicator,
hideNavigationIndicator,
} from '@/uni_modules/harmony-toggle-navigation-indicator'
</script>
当用户点击 hideNavigationIndicator 按钮之后,系统大概一秒后会隐藏小白条。点击 showNavigationIndicator 系统会展示小白条。
在鸿蒙底部有触控小白条,用来响应系统级用户手势。在应用开发时候,有些业务场景需要隐藏底部触控小白条,鸿蒙提供了响应 API,代码比较简单,使用 UTS 几行代码轻松切换展示。
在 HBuilderX 中新建 uni_modules 文件夹,在 uni_modules 文件夹右键选择创建 UTS-API 插件,创建并编辑 app-harmony/index.uts 文件夹,如果没有就新建该文件。
在文件中填写下面代码:
/**
* 展示底部小白条
*/
export const showNavigationIndicator = () => {
const window = UTSHarmony.getCurrentWindow()
window.setSpecificSystemBarEnabled('navigationIndicator', true)
}
/**
* 隐藏底部小白条
*/
export const hideNavigationIndicator = () => {
const window = UTSHarmony.getCurrentWindow()
window.setSpecificSystemBarEnabled('navigationIndicator', false)
}
在 Vue 页面中导入并使用即可。
<template>
<view>
<button @click="showNavigationIndicator">showNavigationIndicator</button>
<button @click="hideNavigationIndicator">hideNavigationIndicator</button>
</view>
</template>
<script setup lang="uts">
import {
showNavigationIndicator,
hideNavigationIndicator,
} from '@/uni_modules/harmony-toggle-navigation-indicator'
</script>
当用户点击 hideNavigationIndicator 按钮之后,系统大概一秒后会隐藏小白条。点击 showNavigationIndicator 系统会展示小白条。
收起阅读 »关于如何查看apk 的 so 是否支持16 KB 内存页面大小
命令行执行
sh /Users/xxx/Desktop/check_elf_alignment.sh /Users/xxx/Desktop/2.apk
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/arm64-v8a/libweexjst.so: \e[32mALIGNED\e[0m (214)
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/armeabi-v7a/libweexjsb.so: \e[31mUNALIGNED\e[0m (212)
UNALIGNED - 支持
ALIGNED - 不支持
命令行执行
sh /Users/xxx/Desktop/check_elf_alignment.sh /Users/xxx/Desktop/2.apk
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/arm64-v8a/libweexjst.so: \e[32mALIGNED\e[0m (214)
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/armeabi-v7a/libweexjsb.so: \e[31mUNALIGNED\e[0m (212)
UNALIGNED - 支持
ALIGNED - 不支持
收起阅读 »app端uniapp分片上传文件
思路如下
1.获取大文件的临时目录tempFilePath
- tempFilePath转应用的安全路径safePath
- 根据大文件的size和每片大小进行分片,得到每个分片的base64
4.直接把每片的base64上传到后端,让后端处理
5.删除安全路径的文件
具体方法如下
2.tempFilePath转应用的安全路径safePath
let rootDir = await this.getDbFolder();
let safePath = rootDir.fullPath + filename;
let existFlag = await this.checkFileExists(filename);
if (!existFlag){
//复制到安全路径下
safePath = await this.copyToSafePath(tempFilePath, rootDir,filename);
}
async getDbFolder() { // 返回doc的应用私有目录对象,用于文件copy接口使用
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {
resolve(fs.root)
});
});
},
async checkFileExists(fileName) {
return new Promise((resolve) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getFile(fileName, { create: false },
() => resolve(true), // 文件存在
() => resolve(false) // 文件不存在
);
});
});
},
async copyToSafePath(tempFilePath, rootDir,filename) {
return new Promise((resolve, reject) => {
// 使用plus.io读取文件
plus.io.resolveLocalFileSystemURL(tempFilePath, (entry) => {
entry.copyTo(rootDir,filename,function (res){
console.log("视频复制成功:" + res.fullPath);
resolve(res.fullPath);
},function (e){
console.log("视频复制成功:" ,e);
reject(e);
})
});
});
},
- 根据大文件的size和每片大小进行分片,得到每个分片的base64
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const length = Math.min(size - start, chunkSize); // 实际读取长度
// 读取分片base64
const chunkBase64 = await this.readFileChunk(safePath, start, length);
....
/**
* 获取分片的base64
*/
readFileChunk(safePath, start, length) {
return new Promise((resolve, reject) => {
// 使用plus.io读取文件
plus.io.resolveLocalFileSystemURL(safePath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
// 设置读取成功回调
reader.onloadend = (e)=> {
let base64 = e.target.result;
resolve(base64);
};
// 设置读取错误回调
reader.onerror = function(error) {
console.error('读取文件分片失败:', error);
reject(error);
};
// 截取文件片段并读取 slice是左右都会取到的,所以要减1
const fileSlice = file.slice(start, start + length - 1);
reader.readAsDataURL(fileSlice);
}, (error) => {
console.error('获取文件对象失败:', error);
reject(error);
});
}, (error) => {
console.error('解析文件路径失败:', error);
reject(error);
});
});
},
5.删除安全路径的文件
removeSafePath(safePath) {
uni.removeSavedFile({
filePath: safePath,
success: (res) => {
console.log('文件删除成功', safePath);
},
fail: (err) => {
console.error('文件删除失败', err);
}
});
},
思路如下
1.获取大文件的临时目录tempFilePath
- tempFilePath转应用的安全路径safePath
- 根据大文件的size和每片大小进行分片,得到每个分片的base64
4.直接把每片的base64上传到后端,让后端处理
5.删除安全路径的文件
具体方法如下
2.tempFilePath转应用的安全路径safePath
let rootDir = await this.getDbFolder();
let safePath = rootDir.fullPath + filename;
let existFlag = await this.checkFileExists(filename);
if (!existFlag){
//复制到安全路径下
safePath = await this.copyToSafePath(tempFilePath, rootDir,filename);
}
async getDbFolder() { // 返回doc的应用私有目录对象,用于文件copy接口使用
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {
resolve(fs.root)
});
});
},
async checkFileExists(fileName) {
return new Promise((resolve) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getFile(fileName, { create: false },
() => resolve(true), // 文件存在
() => resolve(false) // 文件不存在
);
});
});
},
async copyToSafePath(tempFilePath, rootDir,filename) {
return new Promise((resolve, reject) => {
// 使用plus.io读取文件
plus.io.resolveLocalFileSystemURL(tempFilePath, (entry) => {
entry.copyTo(rootDir,filename,function (res){
console.log("视频复制成功:" + res.fullPath);
resolve(res.fullPath);
},function (e){
console.log("视频复制成功:" ,e);
reject(e);
})
});
});
},
- 根据大文件的size和每片大小进行分片,得到每个分片的base64
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const length = Math.min(size - start, chunkSize); // 实际读取长度
// 读取分片base64
const chunkBase64 = await this.readFileChunk(safePath, start, length);
....
/**
* 获取分片的base64
*/
readFileChunk(safePath, start, length) {
return new Promise((resolve, reject) => {
// 使用plus.io读取文件
plus.io.resolveLocalFileSystemURL(safePath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
// 设置读取成功回调
reader.onloadend = (e)=> {
let base64 = e.target.result;
resolve(base64);
};
// 设置读取错误回调
reader.onerror = function(error) {
console.error('读取文件分片失败:', error);
reject(error);
};
// 截取文件片段并读取 slice是左右都会取到的,所以要减1
const fileSlice = file.slice(start, start + length - 1);
reader.readAsDataURL(fileSlice);
}, (error) => {
console.error('获取文件对象失败:', error);
reject(error);
});
}, (error) => {
console.error('解析文件路径失败:', error);
reject(error);
});
});
},
5.删除安全路径的文件
removeSafePath(safePath) {
uni.removeSavedFile({
filePath: safePath,
success: (res) => {
console.log('文件删除成功', safePath);
},
fail: (err) => {
console.error('文件删除失败', err);
}
});
},
收起阅读 »
摩尔斯电码转换器
摩尔斯电码转换器 📡
一个基于 uni-app 开发的摩尔斯电码编解码应用,支持文本与摩尔斯电码的双向转换。
✨ 功能特性
核心功能
- 🔤 文本转摩尔斯电码:将英文文本编码为摩尔斯电码
- 🔡 摩尔斯电码转文本:将摩尔斯电码解码为可读文本
- 🔄 双向转换:一键切换编码/解码模式
- 📋 对照表:内置完整的摩尔斯电码对照表,可随时查阅
支持字符
- ✅ 26个英文字母(A-Z)
- ✅ 10个数字(0-9)
- ✅ 常用标点符号:
. , ? ' ! / ( ) & : ; = + - _ " $ @
界面特色
- 🎨 现代化渐变设计
- 📱 响应式布局,适配多种屏幕尺寸
- 💫 流畅的动画过渡效果
- 🌈 直观的视觉反馈
- 📝 可选中复制输出结果
📸 预览
编码模式
将文本转换为摩尔斯电码:
输入:HELLO WORLD
输出:.... . .-.. .-.. --- / .-- --- .-. .-.. -..
解码模式
将摩尔斯电码转换为文本:
输入:.... . .-.. .-.. --- / .-- --- .-. .-.. -..
输出:HELLO WORLD
🚀 快速开始
环境要求
- HBuilderX 3.0+(运行到 HarmonyOS 需要 5.0+ 版本)
- uni-app 框架
- 支持 uni-app 的运行环境:
- 微信小程序:微信开发者工具
- H5:现代浏览器(Chrome、Firefox、Safari 等)
- Android/iOS:Android Studio / Xcode
- HarmonyOS:HarmonyOS 5.0+ 设备或模拟器
安装步骤
- 克隆项目
git clone [your-repository-url]
cd Morse
-
使用 HBuilderX 打开项目
- 启动 HBuilderX
- 文件 → 打开目录 → 选择项目文件夹
-
运行项目
- 运行 → 运行到浏览器 → Chrome(H5)
- 或运行到微信开发者工具(小程序)
- 或运行到手机模拟器(App)
- 或运行到 HarmonyOS(鸿蒙应用)
运行到 HarmonyOS
前置要求
- 安装 HBuilderX 4.0+ 版本
- 配置 HarmonyOS 开发环境
- 安装 DevEco Studio(可选,用于更高级的调试)
运行步骤
- 在 HBuilderX 中打开项目
- 点击菜单栏:运行 → 运行到手机或模拟器 → 运行到 HarmonyOS
- 选择设备:
- 连接 HarmonyOS 真机(需开启开发者模式和 USB 调试)
- 或使用 HarmonyOS 模拟器
- 等待编译:首次运行会自动下载依赖并编译
- 查看效果:应用会自动安装并启动到设备上
注意事项
- 确保设备系统版本为 HarmonyOS5.0 或更高版本
- 真机调试需要在设置中开启"开发者选项"和"USB调试"
- 如遇到编译问题,请检查 HBuilderX 的 HarmonyOS 插件是否已安装
📖 使用说明
编码(文本 → 摩尔斯电码)
- 点击顶部的「文本 → 摩尔斯」按钮切换到编码模式
- 在输入框中输入要编码的文本(支持英文字母、数字和常用符号)
- 点击「编码」按钮
- 编码结果将显示在输出区域
注意事项:
- 字母之间用空格分隔
- 单词之间用
/分隔 - 不支持的字符会被自动忽略
解码(摩尔斯电码 → 文本)
- 点击顶部的「摩尔斯 → 文本」按钮切换到解码模式
- 在输入框中输入摩尔斯电码
- 使用空格分隔不同的字母
- 使用
/分隔不同的单词
- 点击「解码」按钮
- 解码结果将显示在输出区域
示例输入:
.... . .-.. .-.. --- / .-- --- .-. .-.. -..
查看对照表
点击底部的「▶ 摩尔斯电码对照表」可展开完整的字符对照表,方便学习和参考。
🛠️ 技术栈
- 框架:uni-app
- 语言:TypeScript/UTS
- UI:uni-app 组件库
- 样式:CSS3(渐变、阴影、动画)
📂 项目结构
Morse/
├── pages/
│ └── index/
│ └── index.uvue # 主页面(摩尔斯转换器)
├── static/
│ └── logo.png # 应用图标
├── App.uvue # 应用配置
├── main.uts # 入口文件
├── manifest.json # 应用配置清单
├── pages.json # 页面路由配置
├── uni.scss # 全局样式变量
├── LICENSE # MIT 许可证
└── README.md # 项目说明文档
🎯 核心代码说明
摩尔斯电码映射表
项目内置完整的摩尔斯电码映射表,包含:
- 26个字母
- 10个数字
- 24个常用符号
morseCode: {
'A': '.-', 'B': '-...', 'C': '-.-.', // ...
'0': '-----', '1': '.----', // ...
'.': '.-.-.-', ',': '--..--', // ...
}
编码算法
将文本转换为摩尔斯电码的核心逻辑:
- 将输入文本转为大写
- 按空格分割成单词
- 遍历每个单词的字符,查找对应的摩尔斯电码
- 字母间用空格连接,单词间用
/连接
解码算法
将摩尔斯电码转换为文本的核心逻辑:
- 创建反向映射表(摩尔斯 → 字符)
- 按
/分割成单词 - 每个单词按空格分割成字母
- 查找每个摩尔斯码对应的字符
- 无法识别的码用
?表示
🌟 特色亮点
- 智能容错:解码时遇到无法识别的码会用
?标记,不会中断整个转换过程 - 实时反馈:输入为空时会友好提示用户
- 一键清空:快速清除输入和输出内容
- 学习工具:内置对照表,既是工具也是学习资源
- 视觉设计:渐变背景、卡片阴影、动画效果,提供优秀的视觉体验
📱 平台支持
- ✅ HarmonyOS App(鸿蒙原生应用)
- 支持 HarmonyOS 5.0+
- 完整的原生性能体验
- 适配鸿蒙设计规范
- ✅ H5(网页版)
- ✅ 微信小程序
- ✅ Android App
- ✅ iOS App
- ✅ 快应用
- ✅ 其他 uni-app 支持的平台
🤝 贡献指南
欢迎提交 Issue 和 Pull Request!
- Fork 本项目
- 创建特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 开启 Pull Request
📝 更新日志
v1.0.0 (2025-10-22)
- ✨ 初始版本发布
- 🎉 实现文本转摩尔斯电码功能
- 🎉 实现摩尔斯电码转文本功能
- 🎨 设计现代化 UI 界面
- 📋 添加摩尔斯电码对照表
- 🚀 支持 HarmonyOS 平台(鸿蒙原生应用)
- 📱 多平台适配(H5、小程序、App 等)
🔮 未来计划
- [ ] 添加音频播放功能(播放摩尔斯电码声音)
- [ ] 支持闪光灯模式(用手机闪光灯展示摩尔斯电码)
- [ ] 添加振动反馈(鸿蒙设备支持)
- [ ] 添加历史记录功能
- [ ] 支持更多语言(中文电码等)
- [ ] 添加学习模式(摩尔斯电码训练)
- [ ] 支持语音输入
- [ ] 鸿蒙卡片服务(快速转换)
- [ ] 适配鸿蒙折叠屏设备
❓ 常见问题
Q: 为什么有些字符无法转换?
A: 目前只支持英文字母、数字和常用标点符号。中文字符需要另外的电码系统(如中文电码)。
Q: 解码时出现问号是什么意思?
A: 表示该摩尔斯电码无法识别,可能是输入格式错误或不在支持的字符范围内。
Q: 如何正确输入摩尔斯电码?
A: 使用点 . 和横 - 组成字符,字符间用空格分隔,单词间用 / 分隔。
Q: 如何在 HarmonyOS 设备上安装?
A: 使用 HBuilderX 连接 HarmonyOS 设备,选择"运行到 HarmonyOS"即可自动编译并安装。确保设备已开启开发者模式和 USB 调试。
Q: 支持哪些 HarmonyOS 版本?
A: 支持 HarmonyOS 5.0 及以上版本,建议使用 HarmonyOS 5.0+ 以获得最佳体验。
📄 许可证
本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情
👨💻 作者
坚果
🙏 致谢
感谢 uni-app 团队提供的优秀跨平台框架!
摩尔斯电码转换器 📡
一个基于 uni-app 开发的摩尔斯电码编解码应用,支持文本与摩尔斯电码的双向转换。
✨ 功能特性
核心功能
- 🔤 文本转摩尔斯电码:将英文文本编码为摩尔斯电码
- 🔡 摩尔斯电码转文本:将摩尔斯电码解码为可读文本
- 🔄 双向转换:一键切换编码/解码模式
- 📋 对照表:内置完整的摩尔斯电码对照表,可随时查阅
支持字符
- ✅ 26个英文字母(A-Z)
- ✅ 10个数字(0-9)
- ✅ 常用标点符号:
. , ? ' ! / ( ) & : ; = + - _ " $ @
界面特色
- 🎨 现代化渐变设计
- 📱 响应式布局,适配多种屏幕尺寸
- 💫 流畅的动画过渡效果
- 🌈 直观的视觉反馈
- 📝 可选中复制输出结果
📸 预览
编码模式
将文本转换为摩尔斯电码:
输入:HELLO WORLD
输出:.... . .-.. .-.. --- / .-- --- .-. .-.. -..
解码模式
将摩尔斯电码转换为文本:
输入:.... . .-.. .-.. --- / .-- --- .-. .-.. -..
输出:HELLO WORLD
🚀 快速开始
环境要求
- HBuilderX 3.0+(运行到 HarmonyOS 需要 5.0+ 版本)
- uni-app 框架
- 支持 uni-app 的运行环境:
- 微信小程序:微信开发者工具
- H5:现代浏览器(Chrome、Firefox、Safari 等)
- Android/iOS:Android Studio / Xcode
- HarmonyOS:HarmonyOS 5.0+ 设备或模拟器
安装步骤
- 克隆项目
git clone [your-repository-url]
cd Morse
-
使用 HBuilderX 打开项目
- 启动 HBuilderX
- 文件 → 打开目录 → 选择项目文件夹
-
运行项目
- 运行 → 运行到浏览器 → Chrome(H5)
- 或运行到微信开发者工具(小程序)
- 或运行到手机模拟器(App)
- 或运行到 HarmonyOS(鸿蒙应用)
运行到 HarmonyOS
前置要求
- 安装 HBuilderX 4.0+ 版本
- 配置 HarmonyOS 开发环境
- 安装 DevEco Studio(可选,用于更高级的调试)
运行步骤
- 在 HBuilderX 中打开项目
- 点击菜单栏:运行 → 运行到手机或模拟器 → 运行到 HarmonyOS
- 选择设备:
- 连接 HarmonyOS 真机(需开启开发者模式和 USB 调试)
- 或使用 HarmonyOS 模拟器
- 等待编译:首次运行会自动下载依赖并编译
- 查看效果:应用会自动安装并启动到设备上
注意事项
- 确保设备系统版本为 HarmonyOS5.0 或更高版本
- 真机调试需要在设置中开启"开发者选项"和"USB调试"
- 如遇到编译问题,请检查 HBuilderX 的 HarmonyOS 插件是否已安装
📖 使用说明
编码(文本 → 摩尔斯电码)
- 点击顶部的「文本 → 摩尔斯」按钮切换到编码模式
- 在输入框中输入要编码的文本(支持英文字母、数字和常用符号)
- 点击「编码」按钮
- 编码结果将显示在输出区域
注意事项:
- 字母之间用空格分隔
- 单词之间用
/分隔 - 不支持的字符会被自动忽略
解码(摩尔斯电码 → 文本)
- 点击顶部的「摩尔斯 → 文本」按钮切换到解码模式
- 在输入框中输入摩尔斯电码
- 使用空格分隔不同的字母
- 使用
/分隔不同的单词
- 点击「解码」按钮
- 解码结果将显示在输出区域
示例输入:
.... . .-.. .-.. --- / .-- --- .-. .-.. -..
查看对照表
点击底部的「▶ 摩尔斯电码对照表」可展开完整的字符对照表,方便学习和参考。
🛠️ 技术栈
- 框架:uni-app
- 语言:TypeScript/UTS
- UI:uni-app 组件库
- 样式:CSS3(渐变、阴影、动画)
📂 项目结构
Morse/
├── pages/
│ └── index/
│ └── index.uvue # 主页面(摩尔斯转换器)
├── static/
│ └── logo.png # 应用图标
├── App.uvue # 应用配置
├── main.uts # 入口文件
├── manifest.json # 应用配置清单
├── pages.json # 页面路由配置
├── uni.scss # 全局样式变量
├── LICENSE # MIT 许可证
└── README.md # 项目说明文档
🎯 核心代码说明
摩尔斯电码映射表
项目内置完整的摩尔斯电码映射表,包含:
- 26个字母
- 10个数字
- 24个常用符号
morseCode: {
'A': '.-', 'B': '-...', 'C': '-.-.', // ...
'0': '-----', '1': '.----', // ...
'.': '.-.-.-', ',': '--..--', // ...
}
编码算法
将文本转换为摩尔斯电码的核心逻辑:
- 将输入文本转为大写
- 按空格分割成单词
- 遍历每个单词的字符,查找对应的摩尔斯电码
- 字母间用空格连接,单词间用
/连接
解码算法
将摩尔斯电码转换为文本的核心逻辑:
- 创建反向映射表(摩尔斯 → 字符)
- 按
/分割成单词 - 每个单词按空格分割成字母
- 查找每个摩尔斯码对应的字符
- 无法识别的码用
?表示
🌟 特色亮点
- 智能容错:解码时遇到无法识别的码会用
?标记,不会中断整个转换过程 - 实时反馈:输入为空时会友好提示用户
- 一键清空:快速清除输入和输出内容
- 学习工具:内置对照表,既是工具也是学习资源
- 视觉设计:渐变背景、卡片阴影、动画效果,提供优秀的视觉体验
📱 平台支持
- ✅ HarmonyOS App(鸿蒙原生应用)
- 支持 HarmonyOS 5.0+
- 完整的原生性能体验
- 适配鸿蒙设计规范
- ✅ H5(网页版)
- ✅ 微信小程序
- ✅ Android App
- ✅ iOS App
- ✅ 快应用
- ✅ 其他 uni-app 支持的平台
🤝 贡献指南
欢迎提交 Issue 和 Pull Request!
- Fork 本项目
- 创建特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 开启 Pull Request
📝 更新日志
v1.0.0 (2025-10-22)
- ✨ 初始版本发布
- 🎉 实现文本转摩尔斯电码功能
- 🎉 实现摩尔斯电码转文本功能
- 🎨 设计现代化 UI 界面
- 📋 添加摩尔斯电码对照表
- 🚀 支持 HarmonyOS 平台(鸿蒙原生应用)
- 📱 多平台适配(H5、小程序、App 等)
🔮 未来计划
- [ ] 添加音频播放功能(播放摩尔斯电码声音)
- [ ] 支持闪光灯模式(用手机闪光灯展示摩尔斯电码)
- [ ] 添加振动反馈(鸿蒙设备支持)
- [ ] 添加历史记录功能
- [ ] 支持更多语言(中文电码等)
- [ ] 添加学习模式(摩尔斯电码训练)
- [ ] 支持语音输入
- [ ] 鸿蒙卡片服务(快速转换)
- [ ] 适配鸿蒙折叠屏设备
❓ 常见问题
Q: 为什么有些字符无法转换?
A: 目前只支持英文字母、数字和常用标点符号。中文字符需要另外的电码系统(如中文电码)。
Q: 解码时出现问号是什么意思?
A: 表示该摩尔斯电码无法识别,可能是输入格式错误或不在支持的字符范围内。
Q: 如何正确输入摩尔斯电码?
A: 使用点 . 和横 - 组成字符,字符间用空格分隔,单词间用 / 分隔。
Q: 如何在 HarmonyOS 设备上安装?
A: 使用 HBuilderX 连接 HarmonyOS 设备,选择"运行到 HarmonyOS"即可自动编译并安装。确保设备已开启开发者模式和 USB 调试。
Q: 支持哪些 HarmonyOS 版本?
A: 支持 HarmonyOS 5.0 及以上版本,建议使用 HarmonyOS 5.0+ 以获得最佳体验。
📄 许可证
本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情
👨💻 作者
坚果
🙏 致谢
感谢 uni-app 团队提供的优秀跨平台框架!
收起阅读 »
























