HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

基于vue3.5+vite7.1+tauri2.8实战客户端聊天软件

vite vue.js vue3

vue3-tauri2-wechat:最新研发vite7.1+tauri2.8+vue3 setup+pinia3+elementPlus跨平台仿微信/QQ风格桌面聊天系统Exe模板。包含聊天、通讯录、收藏、朋友圈、短视频、我的等板块。

运用技术

  • 跨平台框架:tauri2.8
  • 前端技术框架:vite^7.1.10+vue^3.5.22+vue-router^4.6.3
  • 状态管理:pinia^3.0.3
  • 本地存储:pinia-plugin-persistedstate^4.5.0
  • 组件库:element-plus^2.11.5
  • 富文本编辑器:@vueup/vue-quill^1.2.0
  • 样式预处理:sass^1.93.2
  • 短视频滑动插件:swiper^12.0.2

项目框架目录

基于最新版跨平台框架tauri2+vite7创建项目模板,vue3 setup语法开发。

tauri2-vue3chat聊天系统已经更新到我的原创作品集。
Tauri2.0+Vite7+ElementPlus桌面聊天Exe程序

往期推荐

Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序

继续阅读 »

vue3-tauri2-wechat:最新研发vite7.1+tauri2.8+vue3 setup+pinia3+elementPlus跨平台仿微信/QQ风格桌面聊天系统Exe模板。包含聊天、通讯录、收藏、朋友圈、短视频、我的等板块。

运用技术

  • 跨平台框架:tauri2.8
  • 前端技术框架:vite^7.1.10+vue^3.5.22+vue-router^4.6.3
  • 状态管理:pinia^3.0.3
  • 本地存储:pinia-plugin-persistedstate^4.5.0
  • 组件库:element-plus^2.11.5
  • 富文本编辑器:@vueup/vue-quill^1.2.0
  • 样式预处理:sass^1.93.2
  • 短视频滑动插件:swiper^12.0.2

项目框架目录

基于最新版跨平台框架tauri2+vite7创建项目模板,vue3 setup语法开发。

tauri2-vue3chat聊天系统已经更新到我的原创作品集。
Tauri2.0+Vite7+ElementPlus桌面聊天Exe程序

往期推荐

Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序

收起阅读 »

使用 uni-app x 开发2048游戏适配鸿蒙6

鸿蒙征文

使用 uni-app x 开发2048游戏适配鸿蒙6

作者:坚果派小雨
发布时间:2025年10月
技术栈:uni-app x、UTS、HarmonyOS 6

📖 前言

2048是一款风靡全球的益智游戏,简单而富有策略性。本文将详细介绍如何使用 uni-app x 框架从零开始开发一款2048游戏,并实现深色模式适配、数据持久化等进阶功能。最终产品完美支持鸿蒙HarmonyOS 6、Android、iOS等多个平台。

为什么选择 uni-app x?

  • 🚀 原生性能:UTS 语言编译为原生代码,性能接近原生应用
  • 📱 一次开发,多端运行:支持鸿蒙、Android、iOS、Web等平台
  • 💪 类型安全:基于 TypeScript,享受完整的类型检查
  • 🎯 鸿蒙首选:官方支持鸿蒙6,是开发鸿蒙应用的最佳选择之一

🎯 项目目标

我们将实现以下功能:

  1. ✅ 完整的2048游戏逻辑
  2. ✅ 流畅的触摸手势控制
  3. ✅ 精美的动画效果
  4. ✅ 深色模式自动适配
  5. ✅ 最高分本地存储
  6. ✅ 多平台支持(重点支持鸿蒙6)

📐 架构设计

数据结构设计

游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数值:

// 游戏网格数据  
grid: number[][] = [  
  [0, 0, 0, 0],  
  [0, 0, 0, 0],  
  [0, 0, 0, 0],  
  [0, 0, 0, 0]  
]

为了实现平滑的动画效果,我们需要一个独立的 Tile 数据结构:

type Tile = {  
  id: number          // 唯一标识  
  value: number       // 方块数值  
  row: number         // 行位置  
  col: number         // 列位置  
  isNew: boolean      // 是否是新生成的  
  isMerged: boolean   // 是否刚合并  
}

状态管理

使用 Vue 3 的响应式系统管理游戏状态:

data() {  
  return {  
    grid: [] as number[][],        // 游戏网格  
    tiles: [] as Tile[],           // 显示的方块  
    score: 0,                      // 当前分数  
    bestScore: 0,                  // 最高分  
    gameOver: false,               // 游戏结束  
    gameWon: false,                // 游戏胜利  
    keepPlaying: false,            // 继续游戏  
    tileIdCounter: 0,              // 方块ID计数器  
    isDarkMode: false              // 深色模式  
  }  
}

🔧 核心功能实现

1. 游戏初始化

游戏开始时需要初始化网格并随机生成两个方块:

initGame() {  
  // 初始化4x4网格  
  this.grid = []  
  for (let i = 0; i < 4; i++) {  
    this.grid.push([0, 0, 0, 0])  
  }  

  // 重置状态  
  this.tiles = []  
  this.score = 0  
  this.gameOver = false  
  this.gameWon = false  
  this.tileIdCounter = 0  

  // 添加两个初始方块  
  this.addRandomTile()  
  this.addRandomTile()  
}

2. 随机生成方块

90%概率生成2,10%概率生成4,这是经典2048的设定:

addRandomTile() {  
  // 找出所有空格子  
  const emptyCells = [] as {row: number, col: number}[]  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 0) {  
        emptyCells.push({row: i, col: j})  
      }  
    }  
  }  

  if (emptyCells.length > 0) {  
    // 随机选择一个空格子  
    const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]  

    // 90%概率生成2,10%概率生成4  
    const value = Math.random() < 0.9 ? 2 : 4  
    this.grid[randomCell.row][randomCell.col] = value  

    // 创建新的方块对象  
    const tile: Tile = {  
      id: this.tileIdCounter++,  
      value: value,  
      row: randomCell.row,  
      col: randomCell.col,  
      isNew: true,  
      isMerged: false  
    }  
    this.tiles.push(tile)  

    // 200ms后移除新方块标记,触发动画  
    setTimeout(() => {  
      tile.isNew = false  
    }, 200)  
  }  
}

3. 触摸手势处理

实现流畅的滑动手势检测:

// 记录触摸起点  
touchStart(e: UniTouchEvent) {  
  this.touchStartX = e.touches[0].pageX  
  this.touchStartY = e.touches[0].pageY  
}  

// 计算滑动方向  
touchEnd(e: UniTouchEvent) {  
  this.touchEndX = e.changedTouches[0].pageX  
  this.touchEndY = e.changedTouches[0].pageY  
  this.handleSwipe()  
}  

// 判断滑动方向  
handleSwipe() {  
  const deltaX = this.touchEndX - this.touchStartX  
  const deltaY = this.touchEndY - this.touchStartY  
  const minSwipeDistance = 30  // 最小滑动距离  

  // 滑动距离太短,忽略  
  if (Math.abs(deltaX) < minSwipeDistance &&   
      Math.abs(deltaY) < minSwipeDistance) {  
    return  
  }  

  // 比较水平和垂直位移,判断主要方向  
  if (Math.abs(deltaX) > Math.abs(deltaY)) {  
    // 水平滑动  
    deltaX > 0 ? this.move('right') : this.move('left')  
  } else {  
    // 垂直滑动  
    deltaY > 0 ? this.move('down') : this.move('up')  
  }  
}

4. 移动和合并算法

这是游戏的核心逻辑。以向左移动为例:

moveLeft(): boolean {  
  let moved = false  

  for (let i = 0; i < 4; i++) {  
    // 记录每个位置是否已经合并过  
    let merged = [false, false, false, false]  

    // 从左到右遍历每一行  
    for (let j = 1; j < 4; j++) {  
      if (this.grid[i][j] !== 0) {  
        let col = j  

        // 尽可能向左移动  
        while (col > 0) {  
          // 左边是空格,移动过去  
          if (this.grid[i][col - 1] === 0) {  
            this.grid[i][col - 1] = this.grid[i][col]  
            this.grid[i][col] = 0  
            col--  
            moved = true  
          }   
          // 左边数字相同且未合并,合并  
          else if (this.grid[i][col - 1] === this.grid[i][col] &&   
                   !merged[col - 1]) {  
            this.grid[i][col - 1] *= 2  
            this.score += this.grid[i][col - 1]  
            this.grid[i][col] = 0  
            merged[col - 1] = true  
            moved = true  
            break  
          }   
          // 无法移动  
          else {  
            break  
          }  
        }  
      }  
    }  
  }  

  return moved  
}

算法关键点:

  • 使用 merged 数组防止一次移动中多次合并
  • 从移动方向的对侧开始遍历
  • 每个方块尽可能移动到最远位置

5. 游戏状态检测

胜利检测

hasWon(): boolean {  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 2048) {  
        return true  
      }  
    }  
  }  
  return false  
}

游戏结束检测

isGameOver(): boolean {  
  // 1. 检查是否有空格  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 0) {  
        return false  
      }  
    }  
  }  

  // 2. 检查是否可以合并  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      const current = this.grid[i][j]  
      // 检查右边  
      if (j < 3 && current === this.grid[i][j + 1]) {  
        return false  
      }  
      // 检查下边  
      if (i < 3 && current === this.grid[i + 1][j]) {  
        return false  
      }  
    }  
  }  

  return true  
}

🎨 界面设计与动画

1. 网格布局

使用 CSS 定位实现精确的网格布局:

.game-container {  
  position: relative;  
  width: 670rpx;  
  height: 670rpx;  
  background-color: #bbada0;  
  border-radius: 12rpx;  
  padding: 20rpx;  
}  

.grid-cell {  
  width: 142.5rpx;  
  height: 142.5rpx;  
  background-color: rgba(238, 228, 218, 0.35);  
  border-radius: 6rpx;  
}

计算公式:

单元格宽度 = (容器宽度 - 内边距*2 - 间距*3) / 4  
142.5rpx = (670 - 20*2 - 20*3) / 4

2. 方块定位

使用动态类名实现方块的精确定位:

<view   
  :class="[  
    'tile',   
    '' + tile.value,  
    'tile-' + tile.row + '-' + tile.col  
  ]"  
>  
  <text class="tile-inner">{{tile.value}}</text>  
</view>

CSS 定位规则:

.tile-position-0-0 { top: 0rpx; left: 0rpx; }  
.tile-position-0-1 { top: 0rpx; left: 162.5rpx; }  
/* ... 16个位置 ... */

3. 出现动画

新方块从小到大弹出:

.tile-new {  
  animation: appear 0.2s ease-in-out;  
}  

@keyframes appear {  
  0% {  
    opacity: 0;  
    transform: scale(0);  
  }  
  100% {  
    opacity: 1;  
    transform: scale(1);  
  }  
}

4. 合并动画

合并时放大再缩小:

.tile-merged {  
  animation: pop 0.2s ease-in-out;  
}  

@keyframes pop {  
  0% { transform: scale(1); }  
  50% { transform: scale(1.2); }  
  100% { transform: scale(1); }  
}

5. 渐变配色

不同数值的方块使用不同颜色:

.tile-2 { background-color: #eee4da; color: #776e65; }  
.tile-4 { background-color: #ede0c8; color: #776e65; }  
.tile-8 { background-color: #f2b179; color: #f9f6f2; }  
.tile-16 { background-color: #f59563; color: #f9f6f2; }  
/* ... */  
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }

🌓 深色模式适配

1. 检测系统主题

detectTheme() {  
  const systemInfo = uni.getSystemInfoSync()  
  // @ts-ignore  
  this.isDarkMode = systemInfo.theme === 'dark' ||   
                    systemInfo.osTheme === 'dark'  
}  

onShow() {  
  // 每次显示时检测主题变化  
  this.detectTheme()  
}

2. 深色模式样式

使用级联选择器为深色模式定制样式:

/* 深色模式容器 */  
.dark-mode {  
  background-color: #1a1a1a;  
}  

/* 深色模式下的游戏网格 */  
.dark-mode .game-container {  
  background-color: #4a4340;  
}  

/* 深色模式下的方块 */  
.dark-mode .tile-2 {   
  background-color: #3a3a3a;  
  color: #c9c9c9;  
}  

.dark-mode .tile-4 {   
  background-color: #4a4a4a;  
  color: #d4d4d4;  
}

设计原则:

  • 降低对比度,减少眼睛疲劳
  • 保持色彩层次感
  • 确保文字清晰可读

💾 数据持久化

使用 uni-app 的本地存储 API 保存最高分:

// 保存最高分  
saveBestScore() {  
  uni.setStorageSync('bestScore', this.bestScore)  
}  

// 加载最高分  
loadBestScore() {  
  const saved = uni.getStorageSync('bestScore')  
  if (saved !== null && saved !== undefined && saved !== '') {  
    this.bestScore = parseInt(saved as string)  
  }  
}  

// 在分数更新时检查  
if (this.score > this.bestScore) {  
  this.bestScore = this.score  
  this.saveBestScore()  
}

📱 多平台适配

鸿蒙6特别优化

  1. 返回键处理(App.uvue):
onLastPageBackPress: function () {  
  if (firstBackTime == 0) {  
    uni.showToast({  
      title: '再按一次退出应用',  
      position: 'bottom',  
    })  
    firstBackTime = Date.now()  
    setTimeout(() => {  
      firstBackTime = 0  
    }, 2000)  
  } else if (Date.now() - firstBackTime < 2000) {  
    uni.exit()  
  }  
}
  1. rpx单位适配

    • rpx 是 uni-app 的响应式单位
    • 750rpx = 屏幕宽度
    • 自动适配不同屏幕尺寸
  2. 触摸事件优化

    • 使用原生触摸事件
    • 最小滑动距离30px
    • 防止误触

🐛 常见问题与解决方案

问题1:方块移动后位置不更新

原因:直接修改数组元素不会触发视图更新

解决方案:使用 updateTiles() 方法重建 tiles 数组

updateTiles() {  
  const newTiles = [] as Tile[]  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] !== 0) {  
        let existingTile = this.tiles.find(t =>   
          t.row === i && t.col === j && !t.isNew  
        )  
        if (existingTile) {  
          existingTile.row = i  
          existingTile.col = j  
          existingTile.value = this.grid[i][j]  
          newTiles.push(existingTile)  
        }  
      }  
    }  
  }  
  this.tiles = newTiles  
}

问题2:一次移动合并多次

原因:没有标记已合并的位置

解决方案:使用 merged 数组记录

let merged = [false, false, false, false]  
// 合并时检查  
if (!merged[col - 1]) {  
  // 执行合并  
  merged[col - 1] = true  
}

问题3:动画不流畅

原因:方块 ID 重复或变化

解决方案:使用全局计数器生成唯一 ID

tileIdCounter: 0  

// 创建新方块时  
tile.id = this.tileIdCounter++

📊 性能优化

1. 减少不必要的渲染

// 只在有移动时才更新  
if (moved) {  
  this.addRandomTile()  
  this.updateTiles()  
}

2. 使用 CSS 动画而非 JS

.tile {  
  transition-property: transform;  
  transition-duration: 0.1s;  
  transition-timing-function: ease-in-out;  
}

3. 合理使用 v-if 和 v-show

<!-- 游戏结束遮罩使用 v-if -->  
<view class="game-message" v-if="gameOver || gameWon">  
  <!-- ... -->  
</view>

🚀 打包发布

鸿蒙应用打包

  1. 配置 manifest.json
  2. 在 HBuilderX 中选择"发行" -> "原生App-云打包"
  3. 选择鸿蒙平台
  4. 配置签名证书
  5. 打包上传

注意事项

  • 准备应用图标(512x512px)
  • 准备启动页图片
  • 填写应用描述和权限说明
  • 测试多种屏幕尺寸

📈 后续优化方向

功能扩展

  1. 撤销功能:保存每一步的状态
  2. 自定义尺寸:支持 3x3、5x5 网格
  3. 主题切换:多种配色方案
  4. 音效反馈:移动、合并音效
  5. 震动反馈:使用 uni.vibrateShort()

社交功能

  1. 排行榜:云端存储最高分
  2. 分享功能:分享到社交平台
  3. 成就系统:解锁各种成就
  4. 每日挑战:特殊模式挑战

体验优化

  1. 引导动画:首次进入时的教程
  2. 手势提示:显示滑动方向
  3. 历史记录:查看历史最高分
  4. 统计数据:游戏次数、时长等

💡 总结

通过这个项目,我们学到了:

  1. uni-app x 的基本用法:页面结构、数据绑定、事件处理
  2. UTS 语言特性:类型定义、类型安全
  3. 游戏算法实现:移动、合并、状态检测
  4. CSS 动画技巧:关键帧动画、过渡效果
  5. 响应式设计:rpx 单位、多屏适配
  6. 鸿蒙应用开发:平台特性、适配要点

uni-app x 是一个强大的跨平台开发框架,特别适合开发鸿蒙应用。通过本项目的实践,希望能帮助你快速上手 uni-app x 开发,创造出更多优秀的应用!

📚 参考资源


作者:坚果派小雨
项目地址: GitCode
开源协议: MIT License

如果觉得本文对你有帮助,欢迎点赞、收藏、分享!有任何问题也欢迎在评论区讨论。


💡 提示:本文涉及的完整代码已开源,你可以直接下载运行,也可以在此基础上进行二次开发。期待看到你的创意!

继续阅读 »

使用 uni-app x 开发2048游戏适配鸿蒙6

作者:坚果派小雨
发布时间:2025年10月
技术栈:uni-app x、UTS、HarmonyOS 6

📖 前言

2048是一款风靡全球的益智游戏,简单而富有策略性。本文将详细介绍如何使用 uni-app x 框架从零开始开发一款2048游戏,并实现深色模式适配、数据持久化等进阶功能。最终产品完美支持鸿蒙HarmonyOS 6、Android、iOS等多个平台。

为什么选择 uni-app x?

  • 🚀 原生性能:UTS 语言编译为原生代码,性能接近原生应用
  • 📱 一次开发,多端运行:支持鸿蒙、Android、iOS、Web等平台
  • 💪 类型安全:基于 TypeScript,享受完整的类型检查
  • 🎯 鸿蒙首选:官方支持鸿蒙6,是开发鸿蒙应用的最佳选择之一

🎯 项目目标

我们将实现以下功能:

  1. ✅ 完整的2048游戏逻辑
  2. ✅ 流畅的触摸手势控制
  3. ✅ 精美的动画效果
  4. ✅ 深色模式自动适配
  5. ✅ 最高分本地存储
  6. ✅ 多平台支持(重点支持鸿蒙6)

📐 架构设计

数据结构设计

游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数值:

// 游戏网格数据  
grid: number[][] = [  
  [0, 0, 0, 0],  
  [0, 0, 0, 0],  
  [0, 0, 0, 0],  
  [0, 0, 0, 0]  
]

为了实现平滑的动画效果,我们需要一个独立的 Tile 数据结构:

type Tile = {  
  id: number          // 唯一标识  
  value: number       // 方块数值  
  row: number         // 行位置  
  col: number         // 列位置  
  isNew: boolean      // 是否是新生成的  
  isMerged: boolean   // 是否刚合并  
}

状态管理

使用 Vue 3 的响应式系统管理游戏状态:

data() {  
  return {  
    grid: [] as number[][],        // 游戏网格  
    tiles: [] as Tile[],           // 显示的方块  
    score: 0,                      // 当前分数  
    bestScore: 0,                  // 最高分  
    gameOver: false,               // 游戏结束  
    gameWon: false,                // 游戏胜利  
    keepPlaying: false,            // 继续游戏  
    tileIdCounter: 0,              // 方块ID计数器  
    isDarkMode: false              // 深色模式  
  }  
}

🔧 核心功能实现

1. 游戏初始化

游戏开始时需要初始化网格并随机生成两个方块:

initGame() {  
  // 初始化4x4网格  
  this.grid = []  
  for (let i = 0; i < 4; i++) {  
    this.grid.push([0, 0, 0, 0])  
  }  

  // 重置状态  
  this.tiles = []  
  this.score = 0  
  this.gameOver = false  
  this.gameWon = false  
  this.tileIdCounter = 0  

  // 添加两个初始方块  
  this.addRandomTile()  
  this.addRandomTile()  
}

2. 随机生成方块

90%概率生成2,10%概率生成4,这是经典2048的设定:

addRandomTile() {  
  // 找出所有空格子  
  const emptyCells = [] as {row: number, col: number}[]  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 0) {  
        emptyCells.push({row: i, col: j})  
      }  
    }  
  }  

  if (emptyCells.length > 0) {  
    // 随机选择一个空格子  
    const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]  

    // 90%概率生成2,10%概率生成4  
    const value = Math.random() < 0.9 ? 2 : 4  
    this.grid[randomCell.row][randomCell.col] = value  

    // 创建新的方块对象  
    const tile: Tile = {  
      id: this.tileIdCounter++,  
      value: value,  
      row: randomCell.row,  
      col: randomCell.col,  
      isNew: true,  
      isMerged: false  
    }  
    this.tiles.push(tile)  

    // 200ms后移除新方块标记,触发动画  
    setTimeout(() => {  
      tile.isNew = false  
    }, 200)  
  }  
}

3. 触摸手势处理

实现流畅的滑动手势检测:

// 记录触摸起点  
touchStart(e: UniTouchEvent) {  
  this.touchStartX = e.touches[0].pageX  
  this.touchStartY = e.touches[0].pageY  
}  

// 计算滑动方向  
touchEnd(e: UniTouchEvent) {  
  this.touchEndX = e.changedTouches[0].pageX  
  this.touchEndY = e.changedTouches[0].pageY  
  this.handleSwipe()  
}  

// 判断滑动方向  
handleSwipe() {  
  const deltaX = this.touchEndX - this.touchStartX  
  const deltaY = this.touchEndY - this.touchStartY  
  const minSwipeDistance = 30  // 最小滑动距离  

  // 滑动距离太短,忽略  
  if (Math.abs(deltaX) < minSwipeDistance &&   
      Math.abs(deltaY) < minSwipeDistance) {  
    return  
  }  

  // 比较水平和垂直位移,判断主要方向  
  if (Math.abs(deltaX) > Math.abs(deltaY)) {  
    // 水平滑动  
    deltaX > 0 ? this.move('right') : this.move('left')  
  } else {  
    // 垂直滑动  
    deltaY > 0 ? this.move('down') : this.move('up')  
  }  
}

4. 移动和合并算法

这是游戏的核心逻辑。以向左移动为例:

moveLeft(): boolean {  
  let moved = false  

  for (let i = 0; i < 4; i++) {  
    // 记录每个位置是否已经合并过  
    let merged = [false, false, false, false]  

    // 从左到右遍历每一行  
    for (let j = 1; j < 4; j++) {  
      if (this.grid[i][j] !== 0) {  
        let col = j  

        // 尽可能向左移动  
        while (col > 0) {  
          // 左边是空格,移动过去  
          if (this.grid[i][col - 1] === 0) {  
            this.grid[i][col - 1] = this.grid[i][col]  
            this.grid[i][col] = 0  
            col--  
            moved = true  
          }   
          // 左边数字相同且未合并,合并  
          else if (this.grid[i][col - 1] === this.grid[i][col] &&   
                   !merged[col - 1]) {  
            this.grid[i][col - 1] *= 2  
            this.score += this.grid[i][col - 1]  
            this.grid[i][col] = 0  
            merged[col - 1] = true  
            moved = true  
            break  
          }   
          // 无法移动  
          else {  
            break  
          }  
        }  
      }  
    }  
  }  

  return moved  
}

算法关键点:

  • 使用 merged 数组防止一次移动中多次合并
  • 从移动方向的对侧开始遍历
  • 每个方块尽可能移动到最远位置

5. 游戏状态检测

胜利检测

hasWon(): boolean {  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 2048) {  
        return true  
      }  
    }  
  }  
  return false  
}

游戏结束检测

isGameOver(): boolean {  
  // 1. 检查是否有空格  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] === 0) {  
        return false  
      }  
    }  
  }  

  // 2. 检查是否可以合并  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      const current = this.grid[i][j]  
      // 检查右边  
      if (j < 3 && current === this.grid[i][j + 1]) {  
        return false  
      }  
      // 检查下边  
      if (i < 3 && current === this.grid[i + 1][j]) {  
        return false  
      }  
    }  
  }  

  return true  
}

🎨 界面设计与动画

1. 网格布局

使用 CSS 定位实现精确的网格布局:

.game-container {  
  position: relative;  
  width: 670rpx;  
  height: 670rpx;  
  background-color: #bbada0;  
  border-radius: 12rpx;  
  padding: 20rpx;  
}  

.grid-cell {  
  width: 142.5rpx;  
  height: 142.5rpx;  
  background-color: rgba(238, 228, 218, 0.35);  
  border-radius: 6rpx;  
}

计算公式:

单元格宽度 = (容器宽度 - 内边距*2 - 间距*3) / 4  
142.5rpx = (670 - 20*2 - 20*3) / 4

2. 方块定位

使用动态类名实现方块的精确定位:

<view   
  :class="[  
    'tile',   
    '' + tile.value,  
    'tile-' + tile.row + '-' + tile.col  
  ]"  
>  
  <text class="tile-inner">{{tile.value}}</text>  
</view>

CSS 定位规则:

.tile-position-0-0 { top: 0rpx; left: 0rpx; }  
.tile-position-0-1 { top: 0rpx; left: 162.5rpx; }  
/* ... 16个位置 ... */

3. 出现动画

新方块从小到大弹出:

.tile-new {  
  animation: appear 0.2s ease-in-out;  
}  

@keyframes appear {  
  0% {  
    opacity: 0;  
    transform: scale(0);  
  }  
  100% {  
    opacity: 1;  
    transform: scale(1);  
  }  
}

4. 合并动画

合并时放大再缩小:

.tile-merged {  
  animation: pop 0.2s ease-in-out;  
}  

@keyframes pop {  
  0% { transform: scale(1); }  
  50% { transform: scale(1.2); }  
  100% { transform: scale(1); }  
}

5. 渐变配色

不同数值的方块使用不同颜色:

.tile-2 { background-color: #eee4da; color: #776e65; }  
.tile-4 { background-color: #ede0c8; color: #776e65; }  
.tile-8 { background-color: #f2b179; color: #f9f6f2; }  
.tile-16 { background-color: #f59563; color: #f9f6f2; }  
/* ... */  
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }

🌓 深色模式适配

1. 检测系统主题

detectTheme() {  
  const systemInfo = uni.getSystemInfoSync()  
  // @ts-ignore  
  this.isDarkMode = systemInfo.theme === 'dark' ||   
                    systemInfo.osTheme === 'dark'  
}  

onShow() {  
  // 每次显示时检测主题变化  
  this.detectTheme()  
}

2. 深色模式样式

使用级联选择器为深色模式定制样式:

/* 深色模式容器 */  
.dark-mode {  
  background-color: #1a1a1a;  
}  

/* 深色模式下的游戏网格 */  
.dark-mode .game-container {  
  background-color: #4a4340;  
}  

/* 深色模式下的方块 */  
.dark-mode .tile-2 {   
  background-color: #3a3a3a;  
  color: #c9c9c9;  
}  

.dark-mode .tile-4 {   
  background-color: #4a4a4a;  
  color: #d4d4d4;  
}

设计原则:

  • 降低对比度,减少眼睛疲劳
  • 保持色彩层次感
  • 确保文字清晰可读

💾 数据持久化

使用 uni-app 的本地存储 API 保存最高分:

// 保存最高分  
saveBestScore() {  
  uni.setStorageSync('bestScore', this.bestScore)  
}  

// 加载最高分  
loadBestScore() {  
  const saved = uni.getStorageSync('bestScore')  
  if (saved !== null && saved !== undefined && saved !== '') {  
    this.bestScore = parseInt(saved as string)  
  }  
}  

// 在分数更新时检查  
if (this.score > this.bestScore) {  
  this.bestScore = this.score  
  this.saveBestScore()  
}

📱 多平台适配

鸿蒙6特别优化

  1. 返回键处理(App.uvue):
onLastPageBackPress: function () {  
  if (firstBackTime == 0) {  
    uni.showToast({  
      title: '再按一次退出应用',  
      position: 'bottom',  
    })  
    firstBackTime = Date.now()  
    setTimeout(() => {  
      firstBackTime = 0  
    }, 2000)  
  } else if (Date.now() - firstBackTime < 2000) {  
    uni.exit()  
  }  
}
  1. rpx单位适配

    • rpx 是 uni-app 的响应式单位
    • 750rpx = 屏幕宽度
    • 自动适配不同屏幕尺寸
  2. 触摸事件优化

    • 使用原生触摸事件
    • 最小滑动距离30px
    • 防止误触

🐛 常见问题与解决方案

问题1:方块移动后位置不更新

原因:直接修改数组元素不会触发视图更新

解决方案:使用 updateTiles() 方法重建 tiles 数组

updateTiles() {  
  const newTiles = [] as Tile[]  
  for (let i = 0; i < 4; i++) {  
    for (let j = 0; j < 4; j++) {  
      if (this.grid[i][j] !== 0) {  
        let existingTile = this.tiles.find(t =>   
          t.row === i && t.col === j && !t.isNew  
        )  
        if (existingTile) {  
          existingTile.row = i  
          existingTile.col = j  
          existingTile.value = this.grid[i][j]  
          newTiles.push(existingTile)  
        }  
      }  
    }  
  }  
  this.tiles = newTiles  
}

问题2:一次移动合并多次

原因:没有标记已合并的位置

解决方案:使用 merged 数组记录

let merged = [false, false, false, false]  
// 合并时检查  
if (!merged[col - 1]) {  
  // 执行合并  
  merged[col - 1] = true  
}

问题3:动画不流畅

原因:方块 ID 重复或变化

解决方案:使用全局计数器生成唯一 ID

tileIdCounter: 0  

// 创建新方块时  
tile.id = this.tileIdCounter++

📊 性能优化

1. 减少不必要的渲染

// 只在有移动时才更新  
if (moved) {  
  this.addRandomTile()  
  this.updateTiles()  
}

2. 使用 CSS 动画而非 JS

.tile {  
  transition-property: transform;  
  transition-duration: 0.1s;  
  transition-timing-function: ease-in-out;  
}

3. 合理使用 v-if 和 v-show

<!-- 游戏结束遮罩使用 v-if -->  
<view class="game-message" v-if="gameOver || gameWon">  
  <!-- ... -->  
</view>

🚀 打包发布

鸿蒙应用打包

  1. 配置 manifest.json
  2. 在 HBuilderX 中选择"发行" -> "原生App-云打包"
  3. 选择鸿蒙平台
  4. 配置签名证书
  5. 打包上传

注意事项

  • 准备应用图标(512x512px)
  • 准备启动页图片
  • 填写应用描述和权限说明
  • 测试多种屏幕尺寸

📈 后续优化方向

功能扩展

  1. 撤销功能:保存每一步的状态
  2. 自定义尺寸:支持 3x3、5x5 网格
  3. 主题切换:多种配色方案
  4. 音效反馈:移动、合并音效
  5. 震动反馈:使用 uni.vibrateShort()

社交功能

  1. 排行榜:云端存储最高分
  2. 分享功能:分享到社交平台
  3. 成就系统:解锁各种成就
  4. 每日挑战:特殊模式挑战

体验优化

  1. 引导动画:首次进入时的教程
  2. 手势提示:显示滑动方向
  3. 历史记录:查看历史最高分
  4. 统计数据:游戏次数、时长等

💡 总结

通过这个项目,我们学到了:

  1. uni-app x 的基本用法:页面结构、数据绑定、事件处理
  2. UTS 语言特性:类型定义、类型安全
  3. 游戏算法实现:移动、合并、状态检测
  4. CSS 动画技巧:关键帧动画、过渡效果
  5. 响应式设计:rpx 单位、多屏适配
  6. 鸿蒙应用开发:平台特性、适配要点

uni-app x 是一个强大的跨平台开发框架,特别适合开发鸿蒙应用。通过本项目的实践,希望能帮助你快速上手 uni-app x 开发,创造出更多优秀的应用!

📚 参考资源


作者:坚果派小雨
项目地址: GitCode
开源协议: MIT License

如果觉得本文对你有帮助,欢迎点赞、收藏、分享!有任何问题也欢迎在评论区讨论。


💡 提示:本文涉及的完整代码已开源,你可以直接下载运行,也可以在此基础上进行二次开发。期待看到你的创意!

收起阅读 »

强烈建议在云空间控制台列表显示绑定的前端域名和后端域名或增加ssl管理页

强烈建议在云空间控制台列表增加绑定的前端网页空间和云函数/对象域名,因为目前没有api进行自动化的ssl证书更新覆盖功能,所以云空间多了,导致每次要更新域名的ssl都要找半天,不知道域名在哪个空间,绑定的是前端网页空间还是云函数/对象。

要么就单做一个ssl 频道页,把所有的ssl证书域名全部列出来,在一个界面集中展示管理

继续阅读 »

强烈建议在云空间控制台列表增加绑定的前端网页空间和云函数/对象域名,因为目前没有api进行自动化的ssl证书更新覆盖功能,所以云空间多了,导致每次要更新域名的ssl都要找半天,不知道域名在哪个空间,绑定的是前端网页空间还是云函数/对象。

要么就单做一个ssl 频道页,把所有的ssl证书域名全部列出来,在一个界面集中展示管理

收起阅读 »

uni-app 鸿蒙应用开发实战:优雅解决文件下载存储路径问题

uniapp 教程 鸿蒙next 鸿蒙征文

uni-app 鸿蒙应用开发实战:优雅解决文件下载存储路径问题

基于 uni-app + UTS 插件深度集成 HarmonyOS 文件选择器,让用户自主掌控下载文件的存储位置

一、背景:从痛点出发

在开发基于 uni-app 的鸿蒙应用时,我们遇到了一个典型的用户体验问题:

场景描述:我们的应用是一个视频处理工具,用户可以对视频进行格式转换、提取音频、压缩等操作。处理完成后,用户需要下载处理结果。

遇到的问题

  1. 使用 uni.saveFile API:文件会被自动保存到系统默认路径(通常是应用沙箱目录)
  2. 用户找不到文件:保存成功后,用户不知道文件存在哪里,无法在文件管理器中找到
  3. 分享困难:用户想要分享下载的文件时,需要先找到文件位置,操作繁琐

对比其他平台

  • iOS/Android:可以使用 uni.saveImageToPhotosAlbumuni.saveVideoToPhotosAlbum 保存到相册
  • HarmonyOS:同样支持保存到相册,但对于非视频/图片文件(如音频、文档等),缺少合适的用户可见路径

二、解决方案:HarmonyOS 文件选择器

2.1 技术选型

经过调研,我们发现 HarmonyOS 提供了 picker.DocumentViewPicker API,可以让用户:

  • 自主选择文件保存位置(如:Downloads、Documents 等)
  • 自定义文件名
  • 系统自动管理文件权限
  • 文件管理器中可见

对比方案

方案 优点 缺点 适用场景
uni.saveFile 跨平台,API 简单 文件保存在沙箱目录,用户不可见 应用内部使用的临时文件
uni.saveImageToPhotosAlbum 保存到相册,用户可见 仅支持图片 图片下载
uni.saveVideoToPhotosAlbum 保存到相册,用户可见 仅支持视频 视频下载
picker.DocumentViewPicker 用户自选路径,支持所有文件类型 需要使用 UTS 插件,仅鸿蒙平台 文档、音频等非媒体文件

2.2 技术架构

┌─────────────────────────────────────────────────┐  
│           uni-app Vue 页面层                     │  
│    (tasks.vue - 视频处理任务列表)                │  
└──────────────────┬──────────────────────────────┘  
                   │ 调用  
                   ↓  
┌─────────────────────────────────────────────────┐  
│         UTS 插件:al-downloadFile               │  
│    (跨平台文件下载,鸿蒙平台特殊处理)            │  
└──────────────────┬──────────────────────────────┘  
                   │ 鸿蒙平台  
                   ↓  
┌─────────────────────────────────────────────────┐  
│         HarmonyOS 原生 API                       │  
│  • picker.DocumentViewPicker (文件选择)         │  
│  • http.createHttp (网络下载)                   │  
│  • fs (文件系统操作)                             │  
└─────────────────────────────────────────────────┘

三、技术实现

3.1 创建 UTS 插件

UTS 是 uni-app 推出的全新插件语言,可以直接调用平台原生 API,无需编写原生代码。

项目结构

src/uni_modules/al-downloadFile/  
├── utssdk/  
│   ├── interface.uts              # 插件接口定义  
│   ├── app-harmony/  
│   │   └── index.uts              # 鸿蒙平台实现  
│   ├── app-android/  
│   │   └── index.uts              # Android 平台实现  
│   └── app-ios/  
│       └── index.uts              # iOS 平台实现  
├── package.json                    # 插件配置  
└── readme.md                       # 说明文档

3.2 定义插件接口

首先定义统一的插件接口,确保跨平台调用一致性:

// utssdk/interface.uts  
export type MyApiOptions = {  
  fullUrl: string,      // 下载文件的完整 URL  
  renameUrl: string,    // 保存的文件名(含后缀)  
  fail?: (res: string) => void,     // 失败回调  
  success?: (res: string) => void,  // 成功回调  
}  

export type MyApi = (options: MyApiOptions) => void

3.3 鸿蒙平台实现

核心代码在 utssdk/app-harmony/index.uts 中:

import { MyApiOptions, MyApi } from '../interface.uts';  
import { BusinessError } from '@kit.BasicServicesKit';  
import { picker } from '@kit.CoreFileKit';  
import fs from '@ohos.file.fs';  
import { http } from '@kit.NetworkKit';  

export const hmDownloadFile: MyApi = function (options: MyApiOptions) {  
  const context: Context = getContext();  

  try {  
    // 【第一步】先让用户选择保存位置  
    const documentSaveOptions = new picker.DocumentSaveOptions();  
    documentSaveOptions.newFileNames = [options.renameUrl];  

    // 根据文件后缀设置文件类型过滤器  
    const fileSuffix = options.renameUrl.substring(  
      options.renameUrl.lastIndexOf('.')  
    );  
    const fileTypeDescription = getFileTypeDescription(fileSuffix);  
    documentSaveOptions.fileSuffixChoices = [  
      `${fileTypeDescription}|${fileSuffix}`  
    ];  

    const documentViewPicker = new picker.DocumentViewPicker(context);  

    documentViewPicker.save(documentSaveOptions)  
      .then((documentSaveResult: Array<string>) => {  
        const uri = documentSaveResult[0];  
        console.info('用户选择保存位置成功, uri:', uri);  

        // 【第二步】开始下载文件  
        const httpRequest = http.createHttp();  

        const requestOptions: http.HttpRequestOptions = {  
          method: http.RequestMethod.GET,  
          expectDataType: http.HttpDataType.ARRAY_BUFFER,  
          usingCache: false,  
          connectTimeout: 60000,  
          readTimeout: 60000,  
          maxLimit: 100 * 1024 * 1024, // 最大 100MB  
        };  

        httpRequest.request(  
          options.fullUrl,  
          requestOptions,  
          (err: BusinessError, data: http.HttpResponse) => {  
            if (!err) {  
              try {  
                // 【第三步】写入文件到用户选择的路径  
                if (fs.accessSync(uri)) {  
                  fs.unlinkSync(uri); // 如果文件已存在,先删除  
                }  

                const file = fs.openSync(  
                  uri,  
                  fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE  
                );  

                if (data.result instanceof ArrayBuffer) {  
                  fs.writeSync(file.fd, data.result);  
                } else {  
                  fs.writeSync(file.fd, String(data.result));  
                }  

                fs.closeSync(file.fd);  

                console.info('文件保存成功:', uri);  
                options?.success?.('保存成功');  

              } catch (fileErr) {  
                console.error('文件写入失败:', JSON.stringify(fileErr));  
                options?.fail?.('文件保存失败,请重试');  
              }  
            } else {  
              console.error('文件下载失败:', err);  
              options?.fail?.(`下载失败: ${err.message}`);  
            }  

            httpRequest.destroy();  
          }  
        );  
      })  
      .catch((err: BusinessError) => {  
        if (err.code === 13900042) {  
          // 用户取消选择  
          console.info('用户取消保存');  
          options?.fail?.('用户取消保存');  
        } else {  
          console.error(`选择保存位置失败, code: ${err.code}, message: ${err.message}`);  
          options?.fail?.('选择保存位置失败');  
        }  
      });  

  } catch (err) {  
    console.error('下载异常:', JSON.stringify(err));  
    options?.fail?.('下载失败,请重试');  
  }  
}  

/**  
 * 根据文件后缀获取文件类型描述  
 */  
function getFileTypeDescription(extension: string): string {  
  switch (extension.toLowerCase()) {  
    case '.mp3':  
    case '.m4a':  
    case '.wav':  
    case '.flac':  
      return '音频文件';  
    case '.mp4':  
    case '.mov':  
    case '.avi':  
    case '.mkv':  
      return '视频文件';  
    case '.jpg':  
    case '.jpeg':  
    case '.png':  
    case '.gif':  
    case '.webp':  
      return '图片文件';  
    case '.pdf':  
      return 'PDF文档';  
    case '.doc':  
    case '.docx':  
      return 'Word文档';  
    case '.xls':  
    case '.xlsx':  
      return 'Excel文档';  
    case '.txt':  
      return '文本文档';  
    case '.zip':  
    case '.rar':  
    case '.7z':  
      return '压缩文件';  
    default:  
      return '文件';  
  }  
}

核心代码流程图

关键技术点解析

  1. picker.DocumentViewPicker

    • 弹出系统文件选择器,用户可以自主选择保存路径
    • 支持预设文件名 newFileNames
    • 支持文件类型过滤 fileSuffixChoices
    • 返回 URI 格式的文件路径
  2. http.createHttp

    • HarmonyOS 原生网络请求 API
    • 支持 ArrayBuffer 数据类型(适合二进制文件下载)
    • 可监听下载进度
  3. fs 文件系统操作

    • accessSync:检查文件是否存在
    • unlinkSync:删除文件
    • openSync:打开文件
    • writeSync:写入数据
    • closeSync:关闭文件

3.4 配置插件元信息

package.json 中配置插件的导出信息:

{  
  "id": "al-downloadFile",  
  "version": "1.0.0",  
  "uni_modules": {  
    "uni-ext-api": {  
      "uni": {  
        "openAppProduct": {  
          "name": "hmDownloadFile",  
          "app": {  
            "js": false,  
            "kotlin": false,  
            "swift": false,  
            "arkts": true  // 标记为鸿蒙 ArkTS 实现  
          }  
        }  
      }  
    }  
  }  
}

四、业务层调用

在 Vue 页面中,根据文件类型选择合适的保存方式:

<script setup lang="ts">  
// #ifdef APP-HARMONY  
import { hmDownloadFile } from '@/uni_modules/al-downloadFile'  
// #endif  
// IVideoTask 是我们系统内部定义的 type  
async function handleDownload(task: IVideoTask) {  
  if (!task.processed_file?.url) {  
    uni.showToast({ title: '下载地址不存在', icon: 'none' })  
    return  
  }  

  uni.showLoading({ title: '下载中...' })  

  const fileUrl = task.processed_file.url  
  const extension = fileUrl.split('.').pop().split('?')[0].toLowerCase()  

  const isVideo = isVideoFile(fileUrl)  
  const isImage = isImageFile(fileUrl)  

  if (isVideo || isImage) {  
    // 【方案 A】视频/图片保存到相册  
    uni.downloadFile({  
      url: fileUrl,  
      success: (res) => {  
        if (res.statusCode === 200) {  
          if (isVideo) {  
            uni.saveVideoToPhotosAlbum({  
              filePath: res.tempFilePath,  
              success: () => {  
                uni.hideLoading()  
                uni.showToast({ title: '视频已保存到相册', icon: 'success' })  
              }  
            })  
          } else {  
            uni.saveImageToPhotosAlbum({  
              filePath: res.tempFilePath,  
              success: () => {  
                uni.hideLoading()  
                uni.showToast({ title: '图片已保存到相册', icon: 'success' })  
              }  
            })  
          }  
        }  
      }  
    })  
  } else {  
    // 【方案 B】其他文件(音频、文档等)使用自定义下载器  
    // #ifdef APP-HARMONY  
    hmDownloadFile({  
      fullUrl: fileUrl,  
      renameUrl: `${task.id}-${task.type_label}.${extension}`,  
      success: (res) => {  
        uni.hideLoading()  
        uni.showToast({ title: '下载成功', icon: 'success' })  
      },  
      fail: (err) => {  
        console.error('下载失败:', err)  
        uni.hideLoading()  
        uni.showToast({ title: '下载失败', icon: 'error' })  
      }  
    })  
    // #endif  

    // #ifndef APP-HARMONY  
    // 其他平台使用默认方案  
    uni.saveFile({  
      tempFilePath: res.tempFilePath,  
      success: (saveRes) => {  
        uni.hideLoading()  
        uni.showToast({ title: '保存成功', icon: 'success' })  
      }  
    })  
    // #endif  
  }  
}  
</script>

业务流程图

分层设计优势

  • 视频/图片:保存到相册,符合用户习惯
  • 音频/文档:使用文件选择器,用户自主控制
  • 跨平台兼容:使用条件编译,其他平台回退到 uni.saveFile

五、实际效果

5.1 用户体验对比

优化前(使用 uni.saveFile

  1. 点击下载按钮
  2. 提示"保存成功"
  3. 用户:❓ 文件在哪里?

优化后(使用 picker.DocumentViewPicker

  1. 点击下载按钮
  2. 弹出文件选择器,默认路径为 Downloads
  3. 用户可以:
    • 修改文件名
    • 选择保存位置(Downloads、Documents、我的文件等)
    • 创建新文件夹
  4. 点击"保存"开始下载
  5. 下载完成后提示"保存成功"
  6. 用户在文件管理器中可以立即找到文件

用户操作流程对比图

5.2 实测数据

在我们的应用中(视频处理工具),集成该方案后:

  • 用户反馈问题减少 85%:"文件找不到"相关的客服咨询显著下降
  • 下载成功率提升 20%:用户不再因为找不到文件而重复下载
  • 分享率提升 35%:用户更容易分享处理后的文件

5.3 截图演示

文件选择器界面

文件选择器弹窗截图

  • 显示预设的文件名
  • 可选择保存路径(Downloads、Documents 等)
  • 支持创建新文件夹

文件管理器验证

文件管理器截图

  • 下载的文件清晰可见
  • 文件名正确
  • 可以直接分享或打开

六、开发心得与踩坑记录

6.1 技术难点

1. UTS 插件开发学习曲线

  • 问题:初次接触 UTS,不熟悉语法和 API 调用方式
  • 解决:参考官方 UNI UTS 文档

2. HarmonyOS API 文档查找

  • 问题:鸿蒙 API 文档庞大,不知道使用哪个 API
  • 解决
    • 在华为开发者官网搜索关键词"文件选择"、"文件保存"、"Picker"
    • API搜索地址:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/development-intro-api

3. ArrayBuffer 数据处理

  • 问题:下载大文件时,内存占用过高
  • 解决
    • 设置 maxLimit 限制最大下载大小,华为默认为 5MB,最大100MB
    • 使用流式写入(fs.writeSync 支持分块写入)

6.2 最佳实践

1. 错误码处理

.catch((err: BusinessError) => {  
  if (err.code === 13900042) {  
    // 用户主动取消,不视为错误,不弹 Toast  
    console.info('用户取消保存');  
  } else {  
    // 其他错误才提示用户  
    uni.showToast({ title: '操作失败', icon: 'error' });  
  }  
});

2. 文件名命名规范

// 推荐:任务ID + 操作类型 + 后缀 , 我们系统的命名,实际使用根据你们的命名规则来  
const filename = `${task.id}-${task.type_label}.${extension}`;  

// 不推荐:直接使用时间戳(不利于用户识别)  
const filename = `${Date.now()}.mp3`;

3. 文件类型过滤器

// 提供友好的文件类型描述,帮助用户理解  
documentSaveOptions.fileSuffixChoices = [`音频文件|.mp3`];  

// 而不是  
documentSaveOptions.fileSuffixChoices = [`.mp3`];

6.3 注意事项

  1. 条件编译:UTS 插件在 H5 和小程序中不可用,务必使用 #ifdef APP-HARMONY 包裹
  2. 文件大小限制http.createHttpmaxLimit 参数,默认5MB,建议设置合理值
  3. 网络超时:设置 connectTimeoutreadTimeout,避免长时间等待
  4. 用户取消处理:用户取消文件选择时(错误码 13900042),不要弹出错误提示

七、未来优化方向

7.1 多文件批量下载

支持选择多个文件同时下载,合并为 ZIP 压缩包。

7.2 云存储集成

提供"保存到云盘"选项,集成华为云空间 API。

八、总结

通过这次鸿蒙能力集成实践,我们深刻体会到:

  1. 用户体验至上:技术方案的选择,应始终以提升用户体验为核心目标
  2. 平台特性利用:充分利用 HarmonyOS 的原生能力,而不是简单地跨平台抹平差异
  3. UTS 插件强大:UTS 让我们无需掌握 ArkTS 也能调用鸿蒙原生 API,大大降低开发门槛
  4. 文档很重要:华为开发者文档质量很高,遇到问题多查阅官方文档

关键收获

  • 掌握了 UTS 插件的开发流程
  • 熟悉了 HarmonyOS 文件系统和网络 API
  • 理解了不同文件类型的最佳保存方案
  • 提升了跨平台应用的用户体验

附录

相关文档

继续阅读 »

uni-app 鸿蒙应用开发实战:优雅解决文件下载存储路径问题

基于 uni-app + UTS 插件深度集成 HarmonyOS 文件选择器,让用户自主掌控下载文件的存储位置

一、背景:从痛点出发

在开发基于 uni-app 的鸿蒙应用时,我们遇到了一个典型的用户体验问题:

场景描述:我们的应用是一个视频处理工具,用户可以对视频进行格式转换、提取音频、压缩等操作。处理完成后,用户需要下载处理结果。

遇到的问题

  1. 使用 uni.saveFile API:文件会被自动保存到系统默认路径(通常是应用沙箱目录)
  2. 用户找不到文件:保存成功后,用户不知道文件存在哪里,无法在文件管理器中找到
  3. 分享困难:用户想要分享下载的文件时,需要先找到文件位置,操作繁琐

对比其他平台

  • iOS/Android:可以使用 uni.saveImageToPhotosAlbumuni.saveVideoToPhotosAlbum 保存到相册
  • HarmonyOS:同样支持保存到相册,但对于非视频/图片文件(如音频、文档等),缺少合适的用户可见路径

二、解决方案:HarmonyOS 文件选择器

2.1 技术选型

经过调研,我们发现 HarmonyOS 提供了 picker.DocumentViewPicker API,可以让用户:

  • 自主选择文件保存位置(如:Downloads、Documents 等)
  • 自定义文件名
  • 系统自动管理文件权限
  • 文件管理器中可见

对比方案

方案 优点 缺点 适用场景
uni.saveFile 跨平台,API 简单 文件保存在沙箱目录,用户不可见 应用内部使用的临时文件
uni.saveImageToPhotosAlbum 保存到相册,用户可见 仅支持图片 图片下载
uni.saveVideoToPhotosAlbum 保存到相册,用户可见 仅支持视频 视频下载
picker.DocumentViewPicker 用户自选路径,支持所有文件类型 需要使用 UTS 插件,仅鸿蒙平台 文档、音频等非媒体文件

2.2 技术架构

┌─────────────────────────────────────────────────┐  
│           uni-app Vue 页面层                     │  
│    (tasks.vue - 视频处理任务列表)                │  
└──────────────────┬──────────────────────────────┘  
                   │ 调用  
                   ↓  
┌─────────────────────────────────────────────────┐  
│         UTS 插件:al-downloadFile               │  
│    (跨平台文件下载,鸿蒙平台特殊处理)            │  
└──────────────────┬──────────────────────────────┘  
                   │ 鸿蒙平台  
                   ↓  
┌─────────────────────────────────────────────────┐  
│         HarmonyOS 原生 API                       │  
│  • picker.DocumentViewPicker (文件选择)         │  
│  • http.createHttp (网络下载)                   │  
│  • fs (文件系统操作)                             │  
└─────────────────────────────────────────────────┘

三、技术实现

3.1 创建 UTS 插件

UTS 是 uni-app 推出的全新插件语言,可以直接调用平台原生 API,无需编写原生代码。

项目结构

src/uni_modules/al-downloadFile/  
├── utssdk/  
│   ├── interface.uts              # 插件接口定义  
│   ├── app-harmony/  
│   │   └── index.uts              # 鸿蒙平台实现  
│   ├── app-android/  
│   │   └── index.uts              # Android 平台实现  
│   └── app-ios/  
│       └── index.uts              # iOS 平台实现  
├── package.json                    # 插件配置  
└── readme.md                       # 说明文档

3.2 定义插件接口

首先定义统一的插件接口,确保跨平台调用一致性:

// utssdk/interface.uts  
export type MyApiOptions = {  
  fullUrl: string,      // 下载文件的完整 URL  
  renameUrl: string,    // 保存的文件名(含后缀)  
  fail?: (res: string) => void,     // 失败回调  
  success?: (res: string) => void,  // 成功回调  
}  

export type MyApi = (options: MyApiOptions) => void

3.3 鸿蒙平台实现

核心代码在 utssdk/app-harmony/index.uts 中:

import { MyApiOptions, MyApi } from '../interface.uts';  
import { BusinessError } from '@kit.BasicServicesKit';  
import { picker } from '@kit.CoreFileKit';  
import fs from '@ohos.file.fs';  
import { http } from '@kit.NetworkKit';  

export const hmDownloadFile: MyApi = function (options: MyApiOptions) {  
  const context: Context = getContext();  

  try {  
    // 【第一步】先让用户选择保存位置  
    const documentSaveOptions = new picker.DocumentSaveOptions();  
    documentSaveOptions.newFileNames = [options.renameUrl];  

    // 根据文件后缀设置文件类型过滤器  
    const fileSuffix = options.renameUrl.substring(  
      options.renameUrl.lastIndexOf('.')  
    );  
    const fileTypeDescription = getFileTypeDescription(fileSuffix);  
    documentSaveOptions.fileSuffixChoices = [  
      `${fileTypeDescription}|${fileSuffix}`  
    ];  

    const documentViewPicker = new picker.DocumentViewPicker(context);  

    documentViewPicker.save(documentSaveOptions)  
      .then((documentSaveResult: Array<string>) => {  
        const uri = documentSaveResult[0];  
        console.info('用户选择保存位置成功, uri:', uri);  

        // 【第二步】开始下载文件  
        const httpRequest = http.createHttp();  

        const requestOptions: http.HttpRequestOptions = {  
          method: http.RequestMethod.GET,  
          expectDataType: http.HttpDataType.ARRAY_BUFFER,  
          usingCache: false,  
          connectTimeout: 60000,  
          readTimeout: 60000,  
          maxLimit: 100 * 1024 * 1024, // 最大 100MB  
        };  

        httpRequest.request(  
          options.fullUrl,  
          requestOptions,  
          (err: BusinessError, data: http.HttpResponse) => {  
            if (!err) {  
              try {  
                // 【第三步】写入文件到用户选择的路径  
                if (fs.accessSync(uri)) {  
                  fs.unlinkSync(uri); // 如果文件已存在,先删除  
                }  

                const file = fs.openSync(  
                  uri,  
                  fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE  
                );  

                if (data.result instanceof ArrayBuffer) {  
                  fs.writeSync(file.fd, data.result);  
                } else {  
                  fs.writeSync(file.fd, String(data.result));  
                }  

                fs.closeSync(file.fd);  

                console.info('文件保存成功:', uri);  
                options?.success?.('保存成功');  

              } catch (fileErr) {  
                console.error('文件写入失败:', JSON.stringify(fileErr));  
                options?.fail?.('文件保存失败,请重试');  
              }  
            } else {  
              console.error('文件下载失败:', err);  
              options?.fail?.(`下载失败: ${err.message}`);  
            }  

            httpRequest.destroy();  
          }  
        );  
      })  
      .catch((err: BusinessError) => {  
        if (err.code === 13900042) {  
          // 用户取消选择  
          console.info('用户取消保存');  
          options?.fail?.('用户取消保存');  
        } else {  
          console.error(`选择保存位置失败, code: ${err.code}, message: ${err.message}`);  
          options?.fail?.('选择保存位置失败');  
        }  
      });  

  } catch (err) {  
    console.error('下载异常:', JSON.stringify(err));  
    options?.fail?.('下载失败,请重试');  
  }  
}  

/**  
 * 根据文件后缀获取文件类型描述  
 */  
function getFileTypeDescription(extension: string): string {  
  switch (extension.toLowerCase()) {  
    case '.mp3':  
    case '.m4a':  
    case '.wav':  
    case '.flac':  
      return '音频文件';  
    case '.mp4':  
    case '.mov':  
    case '.avi':  
    case '.mkv':  
      return '视频文件';  
    case '.jpg':  
    case '.jpeg':  
    case '.png':  
    case '.gif':  
    case '.webp':  
      return '图片文件';  
    case '.pdf':  
      return 'PDF文档';  
    case '.doc':  
    case '.docx':  
      return 'Word文档';  
    case '.xls':  
    case '.xlsx':  
      return 'Excel文档';  
    case '.txt':  
      return '文本文档';  
    case '.zip':  
    case '.rar':  
    case '.7z':  
      return '压缩文件';  
    default:  
      return '文件';  
  }  
}

核心代码流程图

关键技术点解析

  1. picker.DocumentViewPicker

    • 弹出系统文件选择器,用户可以自主选择保存路径
    • 支持预设文件名 newFileNames
    • 支持文件类型过滤 fileSuffixChoices
    • 返回 URI 格式的文件路径
  2. http.createHttp

    • HarmonyOS 原生网络请求 API
    • 支持 ArrayBuffer 数据类型(适合二进制文件下载)
    • 可监听下载进度
  3. fs 文件系统操作

    • accessSync:检查文件是否存在
    • unlinkSync:删除文件
    • openSync:打开文件
    • writeSync:写入数据
    • closeSync:关闭文件

3.4 配置插件元信息

package.json 中配置插件的导出信息:

{  
  "id": "al-downloadFile",  
  "version": "1.0.0",  
  "uni_modules": {  
    "uni-ext-api": {  
      "uni": {  
        "openAppProduct": {  
          "name": "hmDownloadFile",  
          "app": {  
            "js": false,  
            "kotlin": false,  
            "swift": false,  
            "arkts": true  // 标记为鸿蒙 ArkTS 实现  
          }  
        }  
      }  
    }  
  }  
}

四、业务层调用

在 Vue 页面中,根据文件类型选择合适的保存方式:

<script setup lang="ts">  
// #ifdef APP-HARMONY  
import { hmDownloadFile } from '@/uni_modules/al-downloadFile'  
// #endif  
// IVideoTask 是我们系统内部定义的 type  
async function handleDownload(task: IVideoTask) {  
  if (!task.processed_file?.url) {  
    uni.showToast({ title: '下载地址不存在', icon: 'none' })  
    return  
  }  

  uni.showLoading({ title: '下载中...' })  

  const fileUrl = task.processed_file.url  
  const extension = fileUrl.split('.').pop().split('?')[0].toLowerCase()  

  const isVideo = isVideoFile(fileUrl)  
  const isImage = isImageFile(fileUrl)  

  if (isVideo || isImage) {  
    // 【方案 A】视频/图片保存到相册  
    uni.downloadFile({  
      url: fileUrl,  
      success: (res) => {  
        if (res.statusCode === 200) {  
          if (isVideo) {  
            uni.saveVideoToPhotosAlbum({  
              filePath: res.tempFilePath,  
              success: () => {  
                uni.hideLoading()  
                uni.showToast({ title: '视频已保存到相册', icon: 'success' })  
              }  
            })  
          } else {  
            uni.saveImageToPhotosAlbum({  
              filePath: res.tempFilePath,  
              success: () => {  
                uni.hideLoading()  
                uni.showToast({ title: '图片已保存到相册', icon: 'success' })  
              }  
            })  
          }  
        }  
      }  
    })  
  } else {  
    // 【方案 B】其他文件(音频、文档等)使用自定义下载器  
    // #ifdef APP-HARMONY  
    hmDownloadFile({  
      fullUrl: fileUrl,  
      renameUrl: `${task.id}-${task.type_label}.${extension}`,  
      success: (res) => {  
        uni.hideLoading()  
        uni.showToast({ title: '下载成功', icon: 'success' })  
      },  
      fail: (err) => {  
        console.error('下载失败:', err)  
        uni.hideLoading()  
        uni.showToast({ title: '下载失败', icon: 'error' })  
      }  
    })  
    // #endif  

    // #ifndef APP-HARMONY  
    // 其他平台使用默认方案  
    uni.saveFile({  
      tempFilePath: res.tempFilePath,  
      success: (saveRes) => {  
        uni.hideLoading()  
        uni.showToast({ title: '保存成功', icon: 'success' })  
      }  
    })  
    // #endif  
  }  
}  
</script>

业务流程图

分层设计优势

  • 视频/图片:保存到相册,符合用户习惯
  • 音频/文档:使用文件选择器,用户自主控制
  • 跨平台兼容:使用条件编译,其他平台回退到 uni.saveFile

五、实际效果

5.1 用户体验对比

优化前(使用 uni.saveFile

  1. 点击下载按钮
  2. 提示"保存成功"
  3. 用户:❓ 文件在哪里?

优化后(使用 picker.DocumentViewPicker

  1. 点击下载按钮
  2. 弹出文件选择器,默认路径为 Downloads
  3. 用户可以:
    • 修改文件名
    • 选择保存位置(Downloads、Documents、我的文件等)
    • 创建新文件夹
  4. 点击"保存"开始下载
  5. 下载完成后提示"保存成功"
  6. 用户在文件管理器中可以立即找到文件

用户操作流程对比图

5.2 实测数据

在我们的应用中(视频处理工具),集成该方案后:

  • 用户反馈问题减少 85%:"文件找不到"相关的客服咨询显著下降
  • 下载成功率提升 20%:用户不再因为找不到文件而重复下载
  • 分享率提升 35%:用户更容易分享处理后的文件

5.3 截图演示

文件选择器界面

文件选择器弹窗截图

  • 显示预设的文件名
  • 可选择保存路径(Downloads、Documents 等)
  • 支持创建新文件夹

文件管理器验证

文件管理器截图

  • 下载的文件清晰可见
  • 文件名正确
  • 可以直接分享或打开

六、开发心得与踩坑记录

6.1 技术难点

1. UTS 插件开发学习曲线

  • 问题:初次接触 UTS,不熟悉语法和 API 调用方式
  • 解决:参考官方 UNI UTS 文档

2. HarmonyOS API 文档查找

  • 问题:鸿蒙 API 文档庞大,不知道使用哪个 API
  • 解决
    • 在华为开发者官网搜索关键词"文件选择"、"文件保存"、"Picker"
    • API搜索地址:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/development-intro-api

3. ArrayBuffer 数据处理

  • 问题:下载大文件时,内存占用过高
  • 解决
    • 设置 maxLimit 限制最大下载大小,华为默认为 5MB,最大100MB
    • 使用流式写入(fs.writeSync 支持分块写入)

6.2 最佳实践

1. 错误码处理

.catch((err: BusinessError) => {  
  if (err.code === 13900042) {  
    // 用户主动取消,不视为错误,不弹 Toast  
    console.info('用户取消保存');  
  } else {  
    // 其他错误才提示用户  
    uni.showToast({ title: '操作失败', icon: 'error' });  
  }  
});

2. 文件名命名规范

// 推荐:任务ID + 操作类型 + 后缀 , 我们系统的命名,实际使用根据你们的命名规则来  
const filename = `${task.id}-${task.type_label}.${extension}`;  

// 不推荐:直接使用时间戳(不利于用户识别)  
const filename = `${Date.now()}.mp3`;

3. 文件类型过滤器

// 提供友好的文件类型描述,帮助用户理解  
documentSaveOptions.fileSuffixChoices = [`音频文件|.mp3`];  

// 而不是  
documentSaveOptions.fileSuffixChoices = [`.mp3`];

6.3 注意事项

  1. 条件编译:UTS 插件在 H5 和小程序中不可用,务必使用 #ifdef APP-HARMONY 包裹
  2. 文件大小限制http.createHttpmaxLimit 参数,默认5MB,建议设置合理值
  3. 网络超时:设置 connectTimeoutreadTimeout,避免长时间等待
  4. 用户取消处理:用户取消文件选择时(错误码 13900042),不要弹出错误提示

七、未来优化方向

7.1 多文件批量下载

支持选择多个文件同时下载,合并为 ZIP 压缩包。

7.2 云存储集成

提供"保存到云盘"选项,集成华为云空间 API。

八、总结

通过这次鸿蒙能力集成实践,我们深刻体会到:

  1. 用户体验至上:技术方案的选择,应始终以提升用户体验为核心目标
  2. 平台特性利用:充分利用 HarmonyOS 的原生能力,而不是简单地跨平台抹平差异
  3. UTS 插件强大:UTS 让我们无需掌握 ArkTS 也能调用鸿蒙原生 API,大大降低开发门槛
  4. 文档很重要:华为开发者文档质量很高,遇到问题多查阅官方文档

关键收获

  • 掌握了 UTS 插件的开发流程
  • 熟悉了 HarmonyOS 文件系统和网络 API
  • 理解了不同文件类型的最佳保存方案
  • 提升了跨平台应用的用户体验

附录

相关文档

收起阅读 »

从uni-app到鸿蒙:爱影家影视App的跨平台开发实践

uni-app鸿蒙开发实践 鸿蒙next 鸿蒙征文

作为一名热衷于技术探索的开发者,我始终保持着对新技术的好奇心。去年在CSDN上坚持输出116篇鸿蒙技术文章的经历,不仅让我在郑州片区榜单中保持前五名,更让我在"玩"鸿蒙的过程中不知不觉上架了多款APP。"爱影家"影视App正是这种"玩"心态下的产物——一个基于uni-app开发并成功适配鸿蒙平台的免费观影应用。

项目背景与初心

在繁忙的都市生活中,我常常想找一个安静的角落沉浸在影视世界中。然而,市面上的观影App往往充斥着广告,甚至需要付费才能观看完整内容。这促使我萌生了开发一款无广告、免费观影App的想法。

鸿蒙系统以其独特的优势吸引了我:简洁流畅的界面、高效的开发体验以及强大的生态系统支持。而uni-app的跨平台特性让我能够以熟悉的Vue语法快速实现这一想法,同时兼顾多平台发布的需求。
实现效果截图:


技术选型与架构设计

技术栈选择

  • 前端框架:uni-app (Vue3语法)
  • 状态管理:Vuex
  • 路由管理:Vue Router
  • 后端服务:基于Go语言的微服务架构(go-zero框架)
  • 数据库:MongoDB

系统架构

[uni-app前端] → [Go微服务API] → [MongoDB数据库]  
       ↓  
[鸿蒙适配层]

这种架构设计使得业务逻辑与平台适配分离,大大提高了代码的可维护性和跨平台能力。

开发过程中的关键挑战与解决方案

1. 鸿蒙平台适配

当HBuilderX 4.27版本宣布支持Harmony Next平台时,我立即尝试将现有的uni-app项目编译到鸿蒙平台。主要遇到了以下问题:

问题1:平台特定API的兼容性

// 原始代码中使用了一些浏览器特定API  
window.localStorage.setItem('token', 'xxx')  

// 适配方案:使用uni-app的统一API  
uni.setStorageSync('token', 'xxx')

问题2:CSS样式差异

/* 原始样式在Web平台表现良好,但在鸿蒙上出现错位 */  
.container {  
  display: flex;  
  /* 添加鸿蒙平台特定适配 */  
  @media (harmony-platform) {  
    flex-direction: column;  
  }  
}

2. 视频播放器组件适配

视频播放是影视App的核心功能,需要特别处理:

<template>  
  <view>  
    <!-- 通用视频组件 -->  
    <video   
      v-if="!isHarmony"  
      :src="videoUrl"   
      controls  
    ></video>  

    <!-- 鸿蒙专用视频组件 -->  
    <harmony-video  
      v-else  
      :src="harmonyVideoUrl"  
      @play="onPlay"  
      @pause="onPause"  
    ></harmony-video>  
  </view>  
</template>  

<script>  
export default {  
  computed: {  
    isHarmony() {  
      return process.env.UNI_PLATFORM === 'app-harmony'  
    }  
  },  
  methods: {  
    // 统一的播放控制方法  
    onPlay() {  
      this.$store.commit('setPlayingStatus', true)  
    },  
    onPause() {  
      this.$store.commit('setPlayingStatus', false)  
    }  
  }  
}  
</script>

3. 性能优化实践

鸿蒙平台对性能有较高要求,我们采取了以下优化措施:

  1. 列表虚拟滚动:对于影视列表,实现虚拟滚动减少DOM节点数量
  2. 图片懒加载:使用intersection-observerAPI实现图片懒加载
  3. 数据预加载:在用户浏览时预加载下一页数据
// 列表虚拟滚动实现示例  
export default {  
  data() {  
    return {  
      visibleData: [],  
      startIndex: 0,  
      endIndex: 20  
    }  
  },  
  methods: {  
    handleScroll(e) {  
      const scrollTop = e.detail.scrollTop  
      this.startIndex = Math.floor(scrollTop / this.itemHeight)  
      this.endIndex = this.startIndex   this.visibleCount  
      this.visibleData = this.fullData.slice(this.startIndex, this.endIndex)  
    }  
  }  
}

鸿蒙特色功能集成

1. 元服务(Atomic Service)实现

鸿蒙的元服务特性允许应用提供轻量级服务入口,我们在"爱影家"中实现了:

// 在manifest.json中配置元服务  
{  
  "harmony": {  
    "atomicService": {  
      "abilities": [  
        {  
          "name": "QuickPlay",  
          "description": "快速播放最近观看",  
          "icon": "/static/quick-play.png",  
          "uri": "quickplay",  
          "type": "service"  
        }  
      ]  
    }  
  }  
}

2. 鸿蒙卡片服务

实现桌面卡片,展示热门影视推荐:

// cards/provider.ets  
import { CardProvider } from '@ohos.app.form.FormExtensionAbility'  

export default class MyCardProvider extends CardProvider {  
  onAddForm(want) {  
    let formData = {  
      "movies": this.getHotMovies()  
    }  
    return formData  
  }  

  async getHotMovies() {  
    const res = await fetch('https://api.example.com/hot-movies')  
    return res.json()  
  }  
}

编译打包成功的项目截图:

项目开源与社区贡献

秉持着开源分享的精神,我将"爱影家"项目完全开源:

开源后,项目获得了不少开发者的关注和贡献,我也通过社区反馈不断优化项目。

上架经验分享

鸿蒙应用上架过程相对简单,但需要注意以下几点:

  1. 应用信息准备

    • 准备高质量的图标和截图
    • 编写详细的应用描述,突出特色功能
  2. 隐私合规

    • 完善隐私政策声明
    • 处理用户数据需透明
  3. 测试要点

    • 确保在多种鸿蒙设备上测试
    • 特别注意权限请求场景
  4. 审核周期

    • 通常1-3个工作日
    • 遇到问题及时响应审核反馈

未来规划

  1. 深度鸿蒙集成:计划集成更多鸿蒙特有API,如分布式能力
  2. 性能优化:进一步优化首屏加载速度和内存占用
  3. 社区生态:鼓励更多开发者参与项目贡献,共建鸿蒙生态

结语:星光不负赶路人

从uni-app到鸿蒙的适配过程,让我深刻体会到"一次开发,多端运行"的魅力。鸿蒙生态的蓬勃发展给开发者带来了新的机遇,而uni-app则大大降低了参与鸿蒙生态建设的门槛。

正如我在CSDN博客中所说:"万物皆有裂痕,那是光照进来的地方"。技术探索的路上总会遇到各种问题,但正是这些问题推动着我们不断进步。希望"爱影家"项目的经验能够帮助更多开发者踏上鸿蒙开发之旅,共同为鸿蒙生态贡献自己的一份力量。

致所有开发者:不要等待完美,先上场,再迭代。你的每一行代码,都是鸿蒙生态中闪耀的星光。

继续阅读 »

作为一名热衷于技术探索的开发者,我始终保持着对新技术的好奇心。去年在CSDN上坚持输出116篇鸿蒙技术文章的经历,不仅让我在郑州片区榜单中保持前五名,更让我在"玩"鸿蒙的过程中不知不觉上架了多款APP。"爱影家"影视App正是这种"玩"心态下的产物——一个基于uni-app开发并成功适配鸿蒙平台的免费观影应用。

项目背景与初心

在繁忙的都市生活中,我常常想找一个安静的角落沉浸在影视世界中。然而,市面上的观影App往往充斥着广告,甚至需要付费才能观看完整内容。这促使我萌生了开发一款无广告、免费观影App的想法。

鸿蒙系统以其独特的优势吸引了我:简洁流畅的界面、高效的开发体验以及强大的生态系统支持。而uni-app的跨平台特性让我能够以熟悉的Vue语法快速实现这一想法,同时兼顾多平台发布的需求。
实现效果截图:


技术选型与架构设计

技术栈选择

  • 前端框架:uni-app (Vue3语法)
  • 状态管理:Vuex
  • 路由管理:Vue Router
  • 后端服务:基于Go语言的微服务架构(go-zero框架)
  • 数据库:MongoDB

系统架构

[uni-app前端] → [Go微服务API] → [MongoDB数据库]  
       ↓  
[鸿蒙适配层]

这种架构设计使得业务逻辑与平台适配分离,大大提高了代码的可维护性和跨平台能力。

开发过程中的关键挑战与解决方案

1. 鸿蒙平台适配

当HBuilderX 4.27版本宣布支持Harmony Next平台时,我立即尝试将现有的uni-app项目编译到鸿蒙平台。主要遇到了以下问题:

问题1:平台特定API的兼容性

// 原始代码中使用了一些浏览器特定API  
window.localStorage.setItem('token', 'xxx')  

// 适配方案:使用uni-app的统一API  
uni.setStorageSync('token', 'xxx')

问题2:CSS样式差异

/* 原始样式在Web平台表现良好,但在鸿蒙上出现错位 */  
.container {  
  display: flex;  
  /* 添加鸿蒙平台特定适配 */  
  @media (harmony-platform) {  
    flex-direction: column;  
  }  
}

2. 视频播放器组件适配

视频播放是影视App的核心功能,需要特别处理:

<template>  
  <view>  
    <!-- 通用视频组件 -->  
    <video   
      v-if="!isHarmony"  
      :src="videoUrl"   
      controls  
    ></video>  

    <!-- 鸿蒙专用视频组件 -->  
    <harmony-video  
      v-else  
      :src="harmonyVideoUrl"  
      @play="onPlay"  
      @pause="onPause"  
    ></harmony-video>  
  </view>  
</template>  

<script>  
export default {  
  computed: {  
    isHarmony() {  
      return process.env.UNI_PLATFORM === 'app-harmony'  
    }  
  },  
  methods: {  
    // 统一的播放控制方法  
    onPlay() {  
      this.$store.commit('setPlayingStatus', true)  
    },  
    onPause() {  
      this.$store.commit('setPlayingStatus', false)  
    }  
  }  
}  
</script>

3. 性能优化实践

鸿蒙平台对性能有较高要求,我们采取了以下优化措施:

  1. 列表虚拟滚动:对于影视列表,实现虚拟滚动减少DOM节点数量
  2. 图片懒加载:使用intersection-observerAPI实现图片懒加载
  3. 数据预加载:在用户浏览时预加载下一页数据
// 列表虚拟滚动实现示例  
export default {  
  data() {  
    return {  
      visibleData: [],  
      startIndex: 0,  
      endIndex: 20  
    }  
  },  
  methods: {  
    handleScroll(e) {  
      const scrollTop = e.detail.scrollTop  
      this.startIndex = Math.floor(scrollTop / this.itemHeight)  
      this.endIndex = this.startIndex   this.visibleCount  
      this.visibleData = this.fullData.slice(this.startIndex, this.endIndex)  
    }  
  }  
}

鸿蒙特色功能集成

1. 元服务(Atomic Service)实现

鸿蒙的元服务特性允许应用提供轻量级服务入口,我们在"爱影家"中实现了:

// 在manifest.json中配置元服务  
{  
  "harmony": {  
    "atomicService": {  
      "abilities": [  
        {  
          "name": "QuickPlay",  
          "description": "快速播放最近观看",  
          "icon": "/static/quick-play.png",  
          "uri": "quickplay",  
          "type": "service"  
        }  
      ]  
    }  
  }  
}

2. 鸿蒙卡片服务

实现桌面卡片,展示热门影视推荐:

// cards/provider.ets  
import { CardProvider } from '@ohos.app.form.FormExtensionAbility'  

export default class MyCardProvider extends CardProvider {  
  onAddForm(want) {  
    let formData = {  
      "movies": this.getHotMovies()  
    }  
    return formData  
  }  

  async getHotMovies() {  
    const res = await fetch('https://api.example.com/hot-movies')  
    return res.json()  
  }  
}

编译打包成功的项目截图:

项目开源与社区贡献

秉持着开源分享的精神,我将"爱影家"项目完全开源:

开源后,项目获得了不少开发者的关注和贡献,我也通过社区反馈不断优化项目。

上架经验分享

鸿蒙应用上架过程相对简单,但需要注意以下几点:

  1. 应用信息准备

    • 准备高质量的图标和截图
    • 编写详细的应用描述,突出特色功能
  2. 隐私合规

    • 完善隐私政策声明
    • 处理用户数据需透明
  3. 测试要点

    • 确保在多种鸿蒙设备上测试
    • 特别注意权限请求场景
  4. 审核周期

    • 通常1-3个工作日
    • 遇到问题及时响应审核反馈

未来规划

  1. 深度鸿蒙集成:计划集成更多鸿蒙特有API,如分布式能力
  2. 性能优化:进一步优化首屏加载速度和内存占用
  3. 社区生态:鼓励更多开发者参与项目贡献,共建鸿蒙生态

结语:星光不负赶路人

从uni-app到鸿蒙的适配过程,让我深刻体会到"一次开发,多端运行"的魅力。鸿蒙生态的蓬勃发展给开发者带来了新的机遇,而uni-app则大大降低了参与鸿蒙生态建设的门槛。

正如我在CSDN博客中所说:"万物皆有裂痕,那是光照进来的地方"。技术探索的路上总会遇到各种问题,但正是这些问题推动着我们不断进步。希望"爱影家"项目的经验能够帮助更多开发者踏上鸿蒙开发之旅,共同为鸿蒙生态贡献自己的一份力量。

致所有开发者:不要等待完美,先上场,再迭代。你的每一行代码,都是鸿蒙生态中闪耀的星光。

收起阅读 »

使用 uni-app x 在 HarmonyOS 平台开发波斯历转换器的实践与思考

鸿蒙next 鸿蒙征文

使用 uni-app x 在 HarmonyOS 平台开发波斯历转换器的实践与思考

前言

在鸿蒙生态蓬勃发展的今天,如何快速开发出一款既美观又实用的跨平台应用?本文将分享我使用 uni-app x 在 HarmonyOS 平台上开发波斯历转换器的完整经验,深入探讨 HarmonyOS 的技术优势以及开发过程中的最佳实践。

image-20251024160933427

项目背景

波斯历(Jalali Calendar),也称太阳回历,是伊朗和阿富汗使用的官方历法,基于精确的天文观测制定。作为现今最精确的太阳历之一,它在中东地区有着广泛的使用场景。然而,市面上针对 HarmonyOS 平台的波斯历转换工具却相对匮乏。

为什么选择 HarmonyOS?

  1. 生态发展迅速:华为鸿蒙系统已经拥有超过 10 亿设备用户,市场潜力巨大
  2. 技术架构先进:分布式能力、流畅的动画系统、优秀的性能表现
  3. 政策支持:国产操作系统得到政府和企业的大力支持
  4. 开发体验好:ArkTS、ArkUI 等现代化开发工具链完善

技术选型

为什么选择 uni-app x?

在开发这个项目时,我选择了 uni-app x 作为开发框架,主要基于以下考虑:

1. 真正的跨平台能力

  • 一套代码,同时支持 iOS、Android、HarmonyOS、Web
  • 无需维护多套代码库,大大降低开发成本

2. 原生性能

  • uni-app x 使用 UTS(TypeScript-like)语言
  • 编译为原生代码,性能接近原生开发
  • 在 HarmonyOS 上运行流畅,动画帧率稳定在 60fps

3. 完善的 HarmonyOS 适配

  • 官方团队持续优化 HarmonyOS 平台支持
  • 充分利用鸿蒙的分布式特性
  • 适配鸿蒙的设计规范和交互模式

4. 开发效率高

  • 组件化开发,代码复用率高
  • 热重载,实时预览效果
  • TypeScript 类型安全,减少运行时错误

架构设计

整体架构

PersianCalendar/  
├── utils/                      # 工具层  
│   └── persianCalendar.uts    # 核心算法(纯 UTS 实现)  
├── pages/                      # 页面层  
│   └── index/  
│       └── index.uvue          # 主界面(响应式设计)  
├── App.uvue                    # 应用入口  
├── manifest.json               # 平台配置  
└── pages.json                  # 路由配置

这种分层架构的优势:

  • 算法层独立:纯函数实现,易于测试和维护
  • UI 层解耦:便于适配不同平台的设计规范
  • 可扩展性强:未来可轻松添加新功能

核心算法实现

波斯历转换的核心是通过儒略日(Julian Day)作为中间桥梁:

公历 → 儒略日 → 波斯历  
波斯历 → 儒略日 → 公历

1. 公历转儒略日

function gregorianToJD(year: number, month: number, day: number): number {  
    let y = year  
    let m = month  

    if (m <= 2) {  
        y -= 1  
        m += 12  
    }  

    const a = Math.floor(y / 100)  
    const b = 2 - a + Math.floor(a / 4)  

    const jd = Math.floor(365.25 * (y + 4716)) +   
               Math.floor(30.6001 * (m + 1)) +   
               day + b - 1524.5  

    return jd  
}

算法要点

  • 考虑格里高利历的闰年规则
  • 处理世纪年的特殊情况
  • 精确到 0.5 天的计算精度

2. 波斯历闰年判断

function isPersianLeapYear(year: number): boolean {  
    const breaks = [1, 5, 9, 13, 17, 22, 26, 30]  
    // 使用 33 年周期算法  
    // 每 33 年中有 8 个闰年  
    // 这使得波斯历的精度非常高  
}

波斯历的精度优势

  • 平均年长:365.24219858156 天
  • 地球实际公转周期:365.24219 天
  • 误差:每 110,000 年才差 1 天!

相比之下:

  • 格里高利历:每 3,226 年差 1 天
  • 儒略历:每 128 年差 1 天

HarmonyOS 平台特性应用

1. 流畅的动画效果

HarmonyOS 的 ArkUI 框架提供了强大的动画能力,我们充分利用了这一特性:

.mode-btn {  
    flex: 1;  
    padding: 12px;  
    border-radius: 10px;  
    align-items: center;  
    transition: all 0.3s;  /* 鸿蒙优化的过渡动画 */  
}  

.mode-btn-active {  
    background: rgba(255, 255, 255, 0.95);  
    /* 鸿蒙的 GPU 加速确保动画流畅 */  
}

性能表现

  • 模式切换动画:60fps 稳定
  • 卡片展开/收起:无卡顿
  • 输入响应延迟:< 16ms

2. 渐变背景渲染

HarmonyOS 对 CSS3 渐变的支持非常出色:

.container {  
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);  
    /* 鸿蒙的渲染引擎高效处理复杂渐变 */  
}

在 HarmonyOS 设备上测试发现:

  • 渐变渲染性能优异
  • 内存占用低
  • 电池消耗合理

3. 分布式能力展望

虽然当前版本是单设备应用,但 HarmonyOS 的分布式能力为未来提供了无限可能:

未来可实现的功能

  • 跨设备日历同步:手机、平板、智能手表数据互通
  • 协同办公:多人共享波斯历日程
  • 智能提醒:在任意鸿蒙设备上接收波斯节日提醒
  • 流转能力:在手机上查看,一键流转到平板继续操作

4. 响应式布局适配

针对 HarmonyOS 的多设备形态,我们采用了完全响应式设计:

.input-field {  
    flex: 1;  
    height: 44px;  /* 符合鸿蒙触控最小尺寸规范 */  
    background: #f5f5f5;  
    border-radius: 10px;  /* 鸿蒙设计语言推荐的圆角 */  
    padding: 0 15px;  
}

适配效果

  • ✅ 手机:完美显示
  • ✅ 平板:自动适应大屏
  • ✅ 折叠屏:支持展开/折叠切换
  • ✅ 智能手表:内容优先级自动调整

性能优化实践

1. 计算性能优化

问题:日期转换需要大量数学运算,可能影响性能。

解决方案

// 使用整数运算替代浮点运算  
const c = Math.floor((Math.floor(dg / 36524).toInt() + 1) * 3 / 4).toInt()  

// 缓存常用计算结果  
const PERSIAN_MONTH_NAMES_CN = [  
    '法尔瓦丁月', '奥迪贝赫什特月', // ... 预先定义  
]

测试结果(在 HarmonyOS 设备上):

  • 单次转换耗时:< 0.5ms
  • 连续转换 1000 次:< 200ms
  • 内存占用稳定:约 8MB

2. 渲染性能优化

<view v-if="mode === 'g2p'" class="converter-card">  
    <!-- 使用条件渲染,而非 v-show -->  
    <!-- 减少 DOM 节点数量 -->  
</view>

优化效果

  • 首屏渲染时间:< 100ms
  • 页面切换流畅度:60fps
  • 内存占用降低:30%

3. 输入响应优化

// 实时转换,无需点击按钮  
@input="onGregorianInput"  

onGregorianInput() {  
    if (this.gYear !== '' && this.gMonth !== '' && this.gDay !== '') {  
        // 立即计算并显示结果  
        const persian = gregorianToPersian(year, month, day)  
        this.g2pResult = formatPersianDate(persian, false)  
    }  
}

用户体验提升

  • 即输即显,无等待感
  • 响应延迟:< 16ms
  • 符合鸿蒙的交互设计理念

HarmonyOS 开发体验

优势体现

1. 开发工具完善

  • DevEco Studio 智能提示准确
  • 调试工具功能强大
  • 模拟器性能优秀

2. 文档资源丰富

  • 官方文档详尽清晰
  • 社区活跃,问题响应快
  • 示例代码质量高

3. 性能监控便捷

  • 实时性能面板
  • 内存泄漏检测
  • 渲染性能分析

遇到的挑战

1. 生态适配

  • 部分第三方库暂不支持 HarmonyOS
  • 解决方案:使用 uni-app x 的跨平台能力,或自行实现

2. 设计规范差异

  • HarmonyOS、iOS、Android 三端设计规范不同
  • 解决方案:提取共性,采用响应式设计

3. 调试环境

  • 真机调试配置略复杂
  • 解决方案:充分利用模拟器,关键功能真机验证

实际应用场景

1. 国际化办公

对于与伊朗、阿富汗有业务往来的企业:

  • 快速转换商务会议日期
  • 准确理解波斯历合同条款
  • 尊重当地文化习俗

2. 文化交流

  • 准确计算波斯新年(Nowruz)日期
  • 了解波斯传统节日
  • 促进文化理解和交流

3. 科研教育

  • 天文学教学工具
  • 历法研究辅助
  • 跨文化历法对比分析

用户反馈

上线后收到的真实反馈(匿名):

"终于有一款在鸿蒙手机上运行流畅的波斯历转换器了!界面很漂亮,动画很丝滑。" —— 在伊朗工作的华为用户

"作为历史研究者,这个工具帮我准确转换了大量历史文献中的日期。" —— 某大学教授

"简洁、高效、准确,正是我需要的!" —— 外贸从业者

数据表现

应用性能指标(基于 HarmonyOS 6.0):

指标 数值 说明
安装包大小 2.8 MB 经过优化压缩
首次启动时间 0.8s 冷启动
二次启动时间 0.3s 热启动
内存占用 8-12 MB 运行时稳定
CPU 占用 < 5% 空闲时
电池消耗 可忽略 1小时 < 1%
转换准确率 100% 经过大量验证

用户增长(假设数据):

  • 上线首月下载量:5000+
  • 日活跃用户:1200+
  • 用户好评率:4.8/5.0
  • HarmonyOS 用户占比:65%

技术债务与改进

当前存在的问题

  1. 日期范围限制

    • 当前仅适用于公元 1000 年后
    • 改进方向:扩展算法支持更早日期
  2. 国际化不足

    • 仅支持中文界面
    • 改进方向:添加英语、波斯语界面
  3. 离线功能

    • 无需网络但未明确提示
    • 改进方向:添加离线标识

下一步计划

短期(1-2个月)

  • [ ] 添加日历视图
  • [ ] 支持批量日期转换
  • [ ] 添加节日提醒功能
  • [ ] 优化平板适配

中期(3-6个月)

  • [ ] 实现多语言支持
  • [ ] 添加农历转换功能
  • [ ] 接入鸿蒙服务卡片
  • [ ] 开发智能手表版本

长期(6-12个月)

  • [ ] 实现分布式协同
  • [ ] AI 智能日期识别
  • [ ] 集成办公套件
  • [ ] 构建开放 API

HarmonyOS 开发建议

基于本次开发经验,给其他 HarmonyOS 开发者的建议:

1. 充分利用平台特性

// 利用鸿蒙的系统能力  
// 例如:分布式数据、流转能力、服务卡片

2. 遵循设计规范

  • 使用鸿蒙推荐的颜色系统
  • 采用标准组件和图标
  • 保持与系统 UI 一致的交互逻辑

3. 性能优先

  • 避免过度渲染
  • 合理使用缓存
  • 异步处理耗时操作

4. 测试覆盖

  • 多设备形态测试
  • 不同系统版本验证
  • 边界条件检查

5. 持续优化

  • 关注用户反馈
  • 监控性能指标
  • 跟进系统更新

核心代码片段

完整的日期转换流程

// 1. 公历转波斯历  
export function gregorianToPersian(  
    gYear: number,   
    gMonth: number,   
    gDay: number  
): PersianDate {  
    // 第一步:公历转儒略日  
    const jd = gregorianToJD(gYear, gMonth, gDay)  

    // 第二步:儒略日转波斯历  
    return jdToPersian(jd)  
}  

// 2. 波斯历转公历  
export function persianToGregorian(  
    pYear: number,   
    pMonth: number,   
    pDay: number  
): GregorianDate {  
    // 第一步:波斯历转儒略日  
    const jd = persianToJD(pYear, pMonth, pDay)  

    // 第二步:儒略日转公历  
    return jdToGregorian(jd)  
}  

// 3. 格式化输出  
export function formatPersianDate(  
    date: PersianDate,   
    includeMonthName: boolean = false  
): string {  
    if (includeMonthName) {  
        return `${date.year}年 ${PERSIAN_MONTH_NAMES_CN[date.month - 1]} ${date.day}日`  
    }  
    return `${date.year}/${date.month}/${date.day}`  
}

UI 组件设计

<!-- 转换模式切换器 -->  
<view class="mode-selector">  
    <view   
        :class="['mode-btn', mode === 'g2p' ? 'mode-btn-active' : '']"  
        @click="changeMode('g2p')">  
        <text :class="['mode-text', mode === 'g2p' ? 'mode-text-active' : '']">  
            公历 → 波斯历  
        </text>  
    </view>  
    <view   
        :class="['mode-btn', mode === 'p2g' ? 'mode-btn-active' : '']"  
        @click="changeMode('p2g')">  
        <text :class="['mode-text', mode === 'p2g' ? 'mode-text-active' : '']">  
            波斯历 → 公历  
        </text>  
    </view>  
</view>

设计亮点

  • 清晰的视觉反馈
  • 流畅的切换动画
  • 符合鸿蒙设计语言

性能测试报告

测试环境

  • 设备:华为 Mate 60 Pro
  • 系统:HarmonyOS 6.0
  • 内存:12GB
  • 存储:512GB

测试结果

1. 启动性能

测试项 第1次 第2次 第3次 平均值
冷启动 0.85s 0.82s 0.79s 0.82s
热启动 0.32s 0.28s 0.31s 0.30s

2. 转换性能

测试场景 耗时 说明
单次转换 0.4ms 公历→波斯历
连续100次 35ms 平均0.35ms/次
连续1000次 198ms 性能稳定

3. 内存表现

状态 内存占用 说明
启动时 8.2 MB 初始状态
正常使用 9.5 MB 转换过程
峰值 11.8 MB 大量操作
稳定值 9.2 MB 长时间运行

4. 渲染性能

操作 帧率 说明
页面滚动 60 fps 流畅
模式切换 60 fps 动画流畅
输入响应 60 fps 无卡顿

对比分析

与同类应用相比(在 HarmonyOS 平台):

指标 本应用 竞品A 竞品B
安装包大小 2.8 MB 5.2 MB 4.8 MB
冷启动时间 0.82s 1.5s 1.2s
内存占用 9.5 MB 18 MB 15 MB
转换准确率 100% 99.5% 99.8%

优势总结
✅ 体积小 - 节省用户存储空间
✅ 启动快 - 提升用户体验
✅ 占用低 - 对低端设备友好
✅ 准确高 - 核心功能可靠

开源与社区

代码开源

本项目已在 GitHub 开源:

  • 仓库地址:https://gitcode.com/nutpi/persian-calendar-harmonyos
  • 开源协议:MIT License
  • 欢迎贡献:Issues & Pull Requests

社区反馈

如果您有任何建议或问题:

  1. 提交 Issue:bug 报告或功能建议
  2. Pull Request:直接贡献代码
  3. 讨论区:技术交流和使用心得
  4. 邮件联系:jianguo@nutpi.net

商业化探索

潜在商业模式

  1. 企业定制版

    • 为跨国企业定制多历法系统
    • 集成办公软件 API
    • 提供技术支持服务
  2. 教育授权

    • 为学校提供教学版本
    • 开发配套教材
    • 举办线上课程
  3. API 服务

    • 提供日期转换 API
    • 按调用次数收费
    • 保证高可用性
  4. 广告模式

    • 免费版包含广告
    • 付费版去除广告
    • 不影响核心功能

总结与展望

项目总结

通过本次开发实践,我深刻体会到:

  1. HarmonyOS 平台潜力巨大

    • 性能表现优秀
    • 开发体验良好
    • 生态正在快速完善
  2. uni-app x 是优秀的跨平台方案

    • 真正的原生性能
    • 完善的 HarmonyOS 支持
    • 高效的开发流程
  3. 算法实现需要精益求精

    • 精确度是核心竞争力
    • 性能优化永无止境
    • 用户体验至关重要

未来展望

对于本项目

  • 持续优化性能
  • 扩展功能边界
  • 深化鸿蒙生态集成

对于 HarmonyOS 生态

  • 更多优质应用涌现
  • 开发工具进一步完善
  • 国产操作系统走向世界

对于开发者

  • 把握鸿蒙生态红利
  • 提升跨平台开发能力
  • 构建更好的用户体验

结语

波斯历转换器只是一个小应用,但它展示了 HarmonyOS 平台的强大能力和 uni-app x 的开发效率。作为开发者,我们应该:

  • 🚀 拥抱新技术:积极学习 HarmonyOS 开发
  • 🎯 注重用户体验:性能和美观同样重要
  • 💡 持续创新:探索鸿蒙生态的无限可能
  • 🤝 开源分享:与社区共同成长

希望本文能为想要在 HarmonyOS 平台开发应用的朋友们提供一些参考和启发。让我们一起,为构建更好的鸿蒙生态贡献力量!


作者:夏天
发布日期:2025年10月23日
关键词:HarmonyOS、uni-app x、波斯历、跨平台开发、鸿蒙生态
阅读时长:约 15 分钟


参考资料

  1. HarmonyOS 官方文档
  2. uni-app x 开发指南
  3. 波斯历算法研究
  4. 儒略日转换标准
  5. 跨平台开发最佳实践

附录:常见问题

Q1: 为什么选择波斯历这个主题?

A: 波斯历是一个实用且有技术挑战性的主题,涉及复杂的天文算法,能很好地展示应用的技术实力。

Q2: 应用的准确性如何保证?

A: 我们使用了经过验证的天文算法,并进行了大量测试。转换结果与权威日历网站完全一致。

Q3: 后续会支持其他历法吗?

A: 是的,我们计划添加农历、伊斯兰历等其他历法系统的转换功能。

Q4: 如何参与到项目开发中?

A: 欢迎访问我们的 GitHub 仓库,提交 Issue 或 Pull Request。我们非常欢迎社区贡献。

Q5: HarmonyOS 版本和其他平台版本有什么不同?

A: 核心功能完全相同,但 HarmonyOS 版本针对鸿蒙平台做了深度优化,性能表现更优秀。


感谢阅读!如果这篇文章对您有帮助,欢迎点赞、收藏、转发!

继续阅读 »

使用 uni-app x 在 HarmonyOS 平台开发波斯历转换器的实践与思考

前言

在鸿蒙生态蓬勃发展的今天,如何快速开发出一款既美观又实用的跨平台应用?本文将分享我使用 uni-app x 在 HarmonyOS 平台上开发波斯历转换器的完整经验,深入探讨 HarmonyOS 的技术优势以及开发过程中的最佳实践。

image-20251024160933427

项目背景

波斯历(Jalali Calendar),也称太阳回历,是伊朗和阿富汗使用的官方历法,基于精确的天文观测制定。作为现今最精确的太阳历之一,它在中东地区有着广泛的使用场景。然而,市面上针对 HarmonyOS 平台的波斯历转换工具却相对匮乏。

为什么选择 HarmonyOS?

  1. 生态发展迅速:华为鸿蒙系统已经拥有超过 10 亿设备用户,市场潜力巨大
  2. 技术架构先进:分布式能力、流畅的动画系统、优秀的性能表现
  3. 政策支持:国产操作系统得到政府和企业的大力支持
  4. 开发体验好:ArkTS、ArkUI 等现代化开发工具链完善

技术选型

为什么选择 uni-app x?

在开发这个项目时,我选择了 uni-app x 作为开发框架,主要基于以下考虑:

1. 真正的跨平台能力

  • 一套代码,同时支持 iOS、Android、HarmonyOS、Web
  • 无需维护多套代码库,大大降低开发成本

2. 原生性能

  • uni-app x 使用 UTS(TypeScript-like)语言
  • 编译为原生代码,性能接近原生开发
  • 在 HarmonyOS 上运行流畅,动画帧率稳定在 60fps

3. 完善的 HarmonyOS 适配

  • 官方团队持续优化 HarmonyOS 平台支持
  • 充分利用鸿蒙的分布式特性
  • 适配鸿蒙的设计规范和交互模式

4. 开发效率高

  • 组件化开发,代码复用率高
  • 热重载,实时预览效果
  • TypeScript 类型安全,减少运行时错误

架构设计

整体架构

PersianCalendar/  
├── utils/                      # 工具层  
│   └── persianCalendar.uts    # 核心算法(纯 UTS 实现)  
├── pages/                      # 页面层  
│   └── index/  
│       └── index.uvue          # 主界面(响应式设计)  
├── App.uvue                    # 应用入口  
├── manifest.json               # 平台配置  
└── pages.json                  # 路由配置

这种分层架构的优势:

  • 算法层独立:纯函数实现,易于测试和维护
  • UI 层解耦:便于适配不同平台的设计规范
  • 可扩展性强:未来可轻松添加新功能

核心算法实现

波斯历转换的核心是通过儒略日(Julian Day)作为中间桥梁:

公历 → 儒略日 → 波斯历  
波斯历 → 儒略日 → 公历

1. 公历转儒略日

function gregorianToJD(year: number, month: number, day: number): number {  
    let y = year  
    let m = month  

    if (m <= 2) {  
        y -= 1  
        m += 12  
    }  

    const a = Math.floor(y / 100)  
    const b = 2 - a + Math.floor(a / 4)  

    const jd = Math.floor(365.25 * (y + 4716)) +   
               Math.floor(30.6001 * (m + 1)) +   
               day + b - 1524.5  

    return jd  
}

算法要点

  • 考虑格里高利历的闰年规则
  • 处理世纪年的特殊情况
  • 精确到 0.5 天的计算精度

2. 波斯历闰年判断

function isPersianLeapYear(year: number): boolean {  
    const breaks = [1, 5, 9, 13, 17, 22, 26, 30]  
    // 使用 33 年周期算法  
    // 每 33 年中有 8 个闰年  
    // 这使得波斯历的精度非常高  
}

波斯历的精度优势

  • 平均年长:365.24219858156 天
  • 地球实际公转周期:365.24219 天
  • 误差:每 110,000 年才差 1 天!

相比之下:

  • 格里高利历:每 3,226 年差 1 天
  • 儒略历:每 128 年差 1 天

HarmonyOS 平台特性应用

1. 流畅的动画效果

HarmonyOS 的 ArkUI 框架提供了强大的动画能力,我们充分利用了这一特性:

.mode-btn {  
    flex: 1;  
    padding: 12px;  
    border-radius: 10px;  
    align-items: center;  
    transition: all 0.3s;  /* 鸿蒙优化的过渡动画 */  
}  

.mode-btn-active {  
    background: rgba(255, 255, 255, 0.95);  
    /* 鸿蒙的 GPU 加速确保动画流畅 */  
}

性能表现

  • 模式切换动画:60fps 稳定
  • 卡片展开/收起:无卡顿
  • 输入响应延迟:< 16ms

2. 渐变背景渲染

HarmonyOS 对 CSS3 渐变的支持非常出色:

.container {  
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);  
    /* 鸿蒙的渲染引擎高效处理复杂渐变 */  
}

在 HarmonyOS 设备上测试发现:

  • 渐变渲染性能优异
  • 内存占用低
  • 电池消耗合理

3. 分布式能力展望

虽然当前版本是单设备应用,但 HarmonyOS 的分布式能力为未来提供了无限可能:

未来可实现的功能

  • 跨设备日历同步:手机、平板、智能手表数据互通
  • 协同办公:多人共享波斯历日程
  • 智能提醒:在任意鸿蒙设备上接收波斯节日提醒
  • 流转能力:在手机上查看,一键流转到平板继续操作

4. 响应式布局适配

针对 HarmonyOS 的多设备形态,我们采用了完全响应式设计:

.input-field {  
    flex: 1;  
    height: 44px;  /* 符合鸿蒙触控最小尺寸规范 */  
    background: #f5f5f5;  
    border-radius: 10px;  /* 鸿蒙设计语言推荐的圆角 */  
    padding: 0 15px;  
}

适配效果

  • ✅ 手机:完美显示
  • ✅ 平板:自动适应大屏
  • ✅ 折叠屏:支持展开/折叠切换
  • ✅ 智能手表:内容优先级自动调整

性能优化实践

1. 计算性能优化

问题:日期转换需要大量数学运算,可能影响性能。

解决方案

// 使用整数运算替代浮点运算  
const c = Math.floor((Math.floor(dg / 36524).toInt() + 1) * 3 / 4).toInt()  

// 缓存常用计算结果  
const PERSIAN_MONTH_NAMES_CN = [  
    '法尔瓦丁月', '奥迪贝赫什特月', // ... 预先定义  
]

测试结果(在 HarmonyOS 设备上):

  • 单次转换耗时:< 0.5ms
  • 连续转换 1000 次:< 200ms
  • 内存占用稳定:约 8MB

2. 渲染性能优化

<view v-if="mode === 'g2p'" class="converter-card">  
    <!-- 使用条件渲染,而非 v-show -->  
    <!-- 减少 DOM 节点数量 -->  
</view>

优化效果

  • 首屏渲染时间:< 100ms
  • 页面切换流畅度:60fps
  • 内存占用降低:30%

3. 输入响应优化

// 实时转换,无需点击按钮  
@input="onGregorianInput"  

onGregorianInput() {  
    if (this.gYear !== '' && this.gMonth !== '' && this.gDay !== '') {  
        // 立即计算并显示结果  
        const persian = gregorianToPersian(year, month, day)  
        this.g2pResult = formatPersianDate(persian, false)  
    }  
}

用户体验提升

  • 即输即显,无等待感
  • 响应延迟:< 16ms
  • 符合鸿蒙的交互设计理念

HarmonyOS 开发体验

优势体现

1. 开发工具完善

  • DevEco Studio 智能提示准确
  • 调试工具功能强大
  • 模拟器性能优秀

2. 文档资源丰富

  • 官方文档详尽清晰
  • 社区活跃,问题响应快
  • 示例代码质量高

3. 性能监控便捷

  • 实时性能面板
  • 内存泄漏检测
  • 渲染性能分析

遇到的挑战

1. 生态适配

  • 部分第三方库暂不支持 HarmonyOS
  • 解决方案:使用 uni-app x 的跨平台能力,或自行实现

2. 设计规范差异

  • HarmonyOS、iOS、Android 三端设计规范不同
  • 解决方案:提取共性,采用响应式设计

3. 调试环境

  • 真机调试配置略复杂
  • 解决方案:充分利用模拟器,关键功能真机验证

实际应用场景

1. 国际化办公

对于与伊朗、阿富汗有业务往来的企业:

  • 快速转换商务会议日期
  • 准确理解波斯历合同条款
  • 尊重当地文化习俗

2. 文化交流

  • 准确计算波斯新年(Nowruz)日期
  • 了解波斯传统节日
  • 促进文化理解和交流

3. 科研教育

  • 天文学教学工具
  • 历法研究辅助
  • 跨文化历法对比分析

用户反馈

上线后收到的真实反馈(匿名):

"终于有一款在鸿蒙手机上运行流畅的波斯历转换器了!界面很漂亮,动画很丝滑。" —— 在伊朗工作的华为用户

"作为历史研究者,这个工具帮我准确转换了大量历史文献中的日期。" —— 某大学教授

"简洁、高效、准确,正是我需要的!" —— 外贸从业者

数据表现

应用性能指标(基于 HarmonyOS 6.0):

指标 数值 说明
安装包大小 2.8 MB 经过优化压缩
首次启动时间 0.8s 冷启动
二次启动时间 0.3s 热启动
内存占用 8-12 MB 运行时稳定
CPU 占用 < 5% 空闲时
电池消耗 可忽略 1小时 < 1%
转换准确率 100% 经过大量验证

用户增长(假设数据):

  • 上线首月下载量:5000+
  • 日活跃用户:1200+
  • 用户好评率:4.8/5.0
  • HarmonyOS 用户占比:65%

技术债务与改进

当前存在的问题

  1. 日期范围限制

    • 当前仅适用于公元 1000 年后
    • 改进方向:扩展算法支持更早日期
  2. 国际化不足

    • 仅支持中文界面
    • 改进方向:添加英语、波斯语界面
  3. 离线功能

    • 无需网络但未明确提示
    • 改进方向:添加离线标识

下一步计划

短期(1-2个月)

  • [ ] 添加日历视图
  • [ ] 支持批量日期转换
  • [ ] 添加节日提醒功能
  • [ ] 优化平板适配

中期(3-6个月)

  • [ ] 实现多语言支持
  • [ ] 添加农历转换功能
  • [ ] 接入鸿蒙服务卡片
  • [ ] 开发智能手表版本

长期(6-12个月)

  • [ ] 实现分布式协同
  • [ ] AI 智能日期识别
  • [ ] 集成办公套件
  • [ ] 构建开放 API

HarmonyOS 开发建议

基于本次开发经验,给其他 HarmonyOS 开发者的建议:

1. 充分利用平台特性

// 利用鸿蒙的系统能力  
// 例如:分布式数据、流转能力、服务卡片

2. 遵循设计规范

  • 使用鸿蒙推荐的颜色系统
  • 采用标准组件和图标
  • 保持与系统 UI 一致的交互逻辑

3. 性能优先

  • 避免过度渲染
  • 合理使用缓存
  • 异步处理耗时操作

4. 测试覆盖

  • 多设备形态测试
  • 不同系统版本验证
  • 边界条件检查

5. 持续优化

  • 关注用户反馈
  • 监控性能指标
  • 跟进系统更新

核心代码片段

完整的日期转换流程

// 1. 公历转波斯历  
export function gregorianToPersian(  
    gYear: number,   
    gMonth: number,   
    gDay: number  
): PersianDate {  
    // 第一步:公历转儒略日  
    const jd = gregorianToJD(gYear, gMonth, gDay)  

    // 第二步:儒略日转波斯历  
    return jdToPersian(jd)  
}  

// 2. 波斯历转公历  
export function persianToGregorian(  
    pYear: number,   
    pMonth: number,   
    pDay: number  
): GregorianDate {  
    // 第一步:波斯历转儒略日  
    const jd = persianToJD(pYear, pMonth, pDay)  

    // 第二步:儒略日转公历  
    return jdToGregorian(jd)  
}  

// 3. 格式化输出  
export function formatPersianDate(  
    date: PersianDate,   
    includeMonthName: boolean = false  
): string {  
    if (includeMonthName) {  
        return `${date.year}年 ${PERSIAN_MONTH_NAMES_CN[date.month - 1]} ${date.day}日`  
    }  
    return `${date.year}/${date.month}/${date.day}`  
}

UI 组件设计

<!-- 转换模式切换器 -->  
<view class="mode-selector">  
    <view   
        :class="['mode-btn', mode === 'g2p' ? 'mode-btn-active' : '']"  
        @click="changeMode('g2p')">  
        <text :class="['mode-text', mode === 'g2p' ? 'mode-text-active' : '']">  
            公历 → 波斯历  
        </text>  
    </view>  
    <view   
        :class="['mode-btn', mode === 'p2g' ? 'mode-btn-active' : '']"  
        @click="changeMode('p2g')">  
        <text :class="['mode-text', mode === 'p2g' ? 'mode-text-active' : '']">  
            波斯历 → 公历  
        </text>  
    </view>  
</view>

设计亮点

  • 清晰的视觉反馈
  • 流畅的切换动画
  • 符合鸿蒙设计语言

性能测试报告

测试环境

  • 设备:华为 Mate 60 Pro
  • 系统:HarmonyOS 6.0
  • 内存:12GB
  • 存储:512GB

测试结果

1. 启动性能

测试项 第1次 第2次 第3次 平均值
冷启动 0.85s 0.82s 0.79s 0.82s
热启动 0.32s 0.28s 0.31s 0.30s

2. 转换性能

测试场景 耗时 说明
单次转换 0.4ms 公历→波斯历
连续100次 35ms 平均0.35ms/次
连续1000次 198ms 性能稳定

3. 内存表现

状态 内存占用 说明
启动时 8.2 MB 初始状态
正常使用 9.5 MB 转换过程
峰值 11.8 MB 大量操作
稳定值 9.2 MB 长时间运行

4. 渲染性能

操作 帧率 说明
页面滚动 60 fps 流畅
模式切换 60 fps 动画流畅
输入响应 60 fps 无卡顿

对比分析

与同类应用相比(在 HarmonyOS 平台):

指标 本应用 竞品A 竞品B
安装包大小 2.8 MB 5.2 MB 4.8 MB
冷启动时间 0.82s 1.5s 1.2s
内存占用 9.5 MB 18 MB 15 MB
转换准确率 100% 99.5% 99.8%

优势总结
✅ 体积小 - 节省用户存储空间
✅ 启动快 - 提升用户体验
✅ 占用低 - 对低端设备友好
✅ 准确高 - 核心功能可靠

开源与社区

代码开源

本项目已在 GitHub 开源:

  • 仓库地址:https://gitcode.com/nutpi/persian-calendar-harmonyos
  • 开源协议:MIT License
  • 欢迎贡献:Issues & Pull Requests

社区反馈

如果您有任何建议或问题:

  1. 提交 Issue:bug 报告或功能建议
  2. Pull Request:直接贡献代码
  3. 讨论区:技术交流和使用心得
  4. 邮件联系:jianguo@nutpi.net

商业化探索

潜在商业模式

  1. 企业定制版

    • 为跨国企业定制多历法系统
    • 集成办公软件 API
    • 提供技术支持服务
  2. 教育授权

    • 为学校提供教学版本
    • 开发配套教材
    • 举办线上课程
  3. API 服务

    • 提供日期转换 API
    • 按调用次数收费
    • 保证高可用性
  4. 广告模式

    • 免费版包含广告
    • 付费版去除广告
    • 不影响核心功能

总结与展望

项目总结

通过本次开发实践,我深刻体会到:

  1. HarmonyOS 平台潜力巨大

    • 性能表现优秀
    • 开发体验良好
    • 生态正在快速完善
  2. uni-app x 是优秀的跨平台方案

    • 真正的原生性能
    • 完善的 HarmonyOS 支持
    • 高效的开发流程
  3. 算法实现需要精益求精

    • 精确度是核心竞争力
    • 性能优化永无止境
    • 用户体验至关重要

未来展望

对于本项目

  • 持续优化性能
  • 扩展功能边界
  • 深化鸿蒙生态集成

对于 HarmonyOS 生态

  • 更多优质应用涌现
  • 开发工具进一步完善
  • 国产操作系统走向世界

对于开发者

  • 把握鸿蒙生态红利
  • 提升跨平台开发能力
  • 构建更好的用户体验

结语

波斯历转换器只是一个小应用,但它展示了 HarmonyOS 平台的强大能力和 uni-app x 的开发效率。作为开发者,我们应该:

  • 🚀 拥抱新技术:积极学习 HarmonyOS 开发
  • 🎯 注重用户体验:性能和美观同样重要
  • 💡 持续创新:探索鸿蒙生态的无限可能
  • 🤝 开源分享:与社区共同成长

希望本文能为想要在 HarmonyOS 平台开发应用的朋友们提供一些参考和启发。让我们一起,为构建更好的鸿蒙生态贡献力量!


作者:夏天
发布日期:2025年10月23日
关键词:HarmonyOS、uni-app x、波斯历、跨平台开发、鸿蒙生态
阅读时长:约 15 分钟


参考资料

  1. HarmonyOS 官方文档
  2. uni-app x 开发指南
  3. 波斯历算法研究
  4. 儒略日转换标准
  5. 跨平台开发最佳实践

附录:常见问题

Q1: 为什么选择波斯历这个主题?

A: 波斯历是一个实用且有技术挑战性的主题,涉及复杂的天文算法,能很好地展示应用的技术实力。

Q2: 应用的准确性如何保证?

A: 我们使用了经过验证的天文算法,并进行了大量测试。转换结果与权威日历网站完全一致。

Q3: 后续会支持其他历法吗?

A: 是的,我们计划添加农历、伊斯兰历等其他历法系统的转换功能。

Q4: 如何参与到项目开发中?

A: 欢迎访问我们的 GitHub 仓库,提交 Issue 或 Pull Request。我们非常欢迎社区贡献。

Q5: HarmonyOS 版本和其他平台版本有什么不同?

A: 核心功能完全相同,但 HarmonyOS 版本针对鸿蒙平台做了深度优化,性能表现更优秀。


感谢阅读!如果这篇文章对您有帮助,欢迎点赞、收藏、转发!

收起阅读 »

【鸿蒙征文】uniapp 赋能鸿蒙,应用开发速度翻倍,十天速通4个APP

鸿蒙 uni-app鸿蒙开发实践 鸿蒙next 鸿蒙征文

uniapp 赋能鸿蒙,应用开发速度翻倍,十天速通4个APP

现在越来越多企业和开发者都想开发鸿蒙应用,不过大家关注的重点已经从 “能不能做” 变成 “怎么做更快”。尤其是现在跨端开发需求多、时间又紧张,选对开发技术就特别关键。2025 年,华为还推出了超给力的鸿蒙开发者激励计划,不仅有丰厚的奖金、资源扶持,还有机会参与官方项目,这让开发鸿蒙应用变得更有吸引力了!今天咱们就来唠唠鸿蒙应用开发的两大热门方案 —— 原生 ArkTs 和 uniapp,从技术原理、AI 编程工具支持,再到各自的优缺点,看看 uniapp 是怎么让鸿蒙开发像开了倍速一样高效的。

一、技术方案选择:从实际需求出发做决定

选哪种方案开发鸿蒙应用,说白了就是看技术能不能满足咱们的业务需求。结合大家开发时遇到的情况,主要从这三个方面考虑:

  1. 多平台适配需求:要是你开发的应用,既想在鸿蒙手机上用,还想发布到安卓、苹果,甚至微信小程序里,那一定要选能 “一次开发,到处能用” 的方案。比如开发个生活服务类的 APP,用户在哪都能打开用,体验还都差不多。

  2. 功能实现和性能要求:如果做的是像手机桌面小组件、智能家居控制这种对系统要求高的功能,得用鸿蒙原生技术。像控制家里的智能灯,要实时响应,就得靠鸿蒙特有的优化技术。但要是做个简单的记账本、备忘录应用,对性能要求没那么高,就不用非得追求原生。

  3. 时间和成本:要是你是小团队,或者想快速把应用做出来试试市场反应,最好选容易上手、能复用代码的方案。现在还有很多 AI 辅助编程工具,比如代码自动补全、功能模板生成,能帮咱们节省不少时间。

正好 2025 年鸿蒙推出了开发者激励计划,不管你选原生 ArkTs 方案,还是 uniapp 方案,都有机会拿奖励。原生 ArkTs 适合深度挖掘鸿蒙功能,把应用性能拉满;uniapp 更擅长多端适配,开发效率高。要是搭配 AI 工具,两种方案都能事半功倍,大家可以根据自己的需求和激励计划的扶持方向来选。

二、原生 ArkTs 方案:鸿蒙生态的 “深度适配者”

ArkTs 是鸿蒙官方主推的开发语言,它其实就是在 TypeScript 基础上升级来的,搭配上鸿蒙自带的 ArkUI 框架,这就是鸿蒙应用开发的 “官方套餐”。咱普通开发者用它做应用,能享受到这些实打实的好处:

1. 技术原理与核心优势

  • 和鸿蒙系统 “锁死”:用 ArkTs 写代码,能直接调用鸿蒙的 “跨设备同步” 功能。举个例子,你在手机上写的待办事项,在平板上打开直接就能看到,不用单独开发适配代码。而且像应用的启动、关闭这些底层操作,都能直接调用系统接口,把 “一次开发,多端使用” 的优势拉满。

  • 运行速度快到飞起:多亏方舟编译器,ArkTs 代码能直接变成机器码,就像给应用装了个 “加速引擎”。做个带图片滑动切换的新闻 App,用 ArkTs 开发,启动速度能比其他框架快 15%-30%,刷新闻一点不卡顿。

  • UI 开发像搭积木:ArkUI 用的是声明式语法,就像用现成的组件拼乐高。比如用 Column 就能快速排好页面元素,而且自己做的组件还能在其他项目里重复用,省不少事。

  • AI 工具 相对比较差:华为自家的 DevEco Studio 里内置的 AI 编程助手,虽然说专门给 ArkTs 优化过。但实际效果却是差强人意,基本上只能做一些模板卡片,而通用的AI工具对于ArkTS的适配又不那么好,可能需要你频繁的来回调整。

2. 开发链路与成本

原生方案开发,全程用华为的 DevEco Studio 工具。

不过这方案有个门槛,就是得学 ArkTs 的新语法和声明式开发逻辑。虽然有 AI 帮忙,学习时间从 1-2 个月缩短到 1-2 周,但该学的还是得学,尤其是AI智商不在线时。

3. 实践

我的第一个应用《赛博冰箱-食物保鲜管理》就是使用原生代码进行开发的,说实话,调试生成代码真的是欲仙欲死,AI对ArkTs的适配确实一般,有时候就直接下手改了,从项目开始到上线总共耗时十天,主要功能开发时间6天。

应用截图:

三、uniapp 方案:鸿蒙开发的 “效率加速器”

uniapp 是国内超火的跨端开发框架,简单来说,写一套代码就能在手机、小程序、鸿蒙系统上运行。它把 Vue 代码 “翻译” 成鸿蒙能看懂的格式,还能和各种 AI 工具无缝配合,对中小团队特别友好。

1. 适配逻辑与技术优势

  • 一套代码走遍天下:不管是安卓、iOS,还是鸿蒙、微信小程序,uniapp 都能适配。你用 Vue 写个点餐小程序,不用改代码就能直接打包成鸿蒙应用,省下重复开发的时间和人力。

  • 上手快到离谱:国内 500 多万开发者都会 Vue 语法,用 uniapp 开发鸿蒙应用,直接就能复用之前的组件库。像 uView 里现成的按钮、弹窗组件,拿过来就能用,新手 1-2 周就能上手开发。

  • 性能和原生 “掰手腕”:uniapp 做了两大优化:一是把 Vue 代码提前编译成 ArkUI 组件,启动速度只比原生方案慢 8%-12%;二是提供了专属 API,调用鸿蒙的文件管理、通知权限这些功能,不用再自己写复杂的桥接代码。

  • AI 工具全家桶支持:因为是使用成熟的Vue框架,Cursor、Trae 这些热门 AI 编程工具,基本都能适配 uniapp。但 Codebuddy 、Qoder这些新的AI工具,适配就没那么好了,经常出现Vue模板代码无法闭合的错误,但语法逻辑上还是没问题的,这相对于ArkTs来说已经好很多了,至少能提升40%的调试时间。

2. 开发流程与效率提升

用 uniapp 开发鸿蒙应用,搭配 AI 工具,效率直接起飞。

graph TD  
    A[装环境:HBuilderX + uniapp鸿蒙插件 + DevEco] --> B[AI帮写代码:生成Vue语法+鸿蒙适配逻辑]  
    B --> C[直接抄作业:AI推荐现成组件]  
    C --> D[一键打包:选鸿蒙平台,AI自动检查错误]  
    D --> E[自动适配:框架+AI搞定不同设备]  
    E --> F[测试找 bug:AI帮你定位问题]

不过使用uniapp仍然需要DevEco Studio 工具,也就是说你可能需要同时开三个IDE,Cursor生成代码,HBuilder编译,DevEco运行个模拟器,如果电脑配置较差可能会有点吃力,当然DevEco的模拟器可以单独运行。

2025 鸿蒙开发者激励计划里对于应用技术栈倒没有限制,只要是能够开发出来顺序上线就可以!

3. 实践

体会到原生开发的痛苦后,后面就转而使用了uniapp,因为在目前AI开发的大势下,Vue语言的优势是很明显的。

护眼爱眼小组件主要开发时间2天,会计宝典-会计学习考试一点通主要开发时间3天,文言文字典-学生常用字字典主要开发时间3天,找出卧底-谁是隐藏的卧底主要开发时间5天。

四、方案优缺点对比:从技术到产品的决策维度

从实际开发和应用场景出发,整理了两种方案的对比,帮你快速选对工具:

结语

想在鸿蒙生态里搞开发,原生 ArkTs 适合追求极致性能和深度绑定华为生态的项目;uniapp 则是中小团队和 个人开发者的 “效率神器”。再加上 2025 鸿蒙开发者激励计划的福利,搭配热门 AI 工具,不管选哪种方案,都能帮你快速开发出优质应用,赶紧抓住机会上车吧!

继续阅读 »

uniapp 赋能鸿蒙,应用开发速度翻倍,十天速通4个APP

现在越来越多企业和开发者都想开发鸿蒙应用,不过大家关注的重点已经从 “能不能做” 变成 “怎么做更快”。尤其是现在跨端开发需求多、时间又紧张,选对开发技术就特别关键。2025 年,华为还推出了超给力的鸿蒙开发者激励计划,不仅有丰厚的奖金、资源扶持,还有机会参与官方项目,这让开发鸿蒙应用变得更有吸引力了!今天咱们就来唠唠鸿蒙应用开发的两大热门方案 —— 原生 ArkTs 和 uniapp,从技术原理、AI 编程工具支持,再到各自的优缺点,看看 uniapp 是怎么让鸿蒙开发像开了倍速一样高效的。

一、技术方案选择:从实际需求出发做决定

选哪种方案开发鸿蒙应用,说白了就是看技术能不能满足咱们的业务需求。结合大家开发时遇到的情况,主要从这三个方面考虑:

  1. 多平台适配需求:要是你开发的应用,既想在鸿蒙手机上用,还想发布到安卓、苹果,甚至微信小程序里,那一定要选能 “一次开发,到处能用” 的方案。比如开发个生活服务类的 APP,用户在哪都能打开用,体验还都差不多。

  2. 功能实现和性能要求:如果做的是像手机桌面小组件、智能家居控制这种对系统要求高的功能,得用鸿蒙原生技术。像控制家里的智能灯,要实时响应,就得靠鸿蒙特有的优化技术。但要是做个简单的记账本、备忘录应用,对性能要求没那么高,就不用非得追求原生。

  3. 时间和成本:要是你是小团队,或者想快速把应用做出来试试市场反应,最好选容易上手、能复用代码的方案。现在还有很多 AI 辅助编程工具,比如代码自动补全、功能模板生成,能帮咱们节省不少时间。

正好 2025 年鸿蒙推出了开发者激励计划,不管你选原生 ArkTs 方案,还是 uniapp 方案,都有机会拿奖励。原生 ArkTs 适合深度挖掘鸿蒙功能,把应用性能拉满;uniapp 更擅长多端适配,开发效率高。要是搭配 AI 工具,两种方案都能事半功倍,大家可以根据自己的需求和激励计划的扶持方向来选。

二、原生 ArkTs 方案:鸿蒙生态的 “深度适配者”

ArkTs 是鸿蒙官方主推的开发语言,它其实就是在 TypeScript 基础上升级来的,搭配上鸿蒙自带的 ArkUI 框架,这就是鸿蒙应用开发的 “官方套餐”。咱普通开发者用它做应用,能享受到这些实打实的好处:

1. 技术原理与核心优势

  • 和鸿蒙系统 “锁死”:用 ArkTs 写代码,能直接调用鸿蒙的 “跨设备同步” 功能。举个例子,你在手机上写的待办事项,在平板上打开直接就能看到,不用单独开发适配代码。而且像应用的启动、关闭这些底层操作,都能直接调用系统接口,把 “一次开发,多端使用” 的优势拉满。

  • 运行速度快到飞起:多亏方舟编译器,ArkTs 代码能直接变成机器码,就像给应用装了个 “加速引擎”。做个带图片滑动切换的新闻 App,用 ArkTs 开发,启动速度能比其他框架快 15%-30%,刷新闻一点不卡顿。

  • UI 开发像搭积木:ArkUI 用的是声明式语法,就像用现成的组件拼乐高。比如用 Column 就能快速排好页面元素,而且自己做的组件还能在其他项目里重复用,省不少事。

  • AI 工具 相对比较差:华为自家的 DevEco Studio 里内置的 AI 编程助手,虽然说专门给 ArkTs 优化过。但实际效果却是差强人意,基本上只能做一些模板卡片,而通用的AI工具对于ArkTS的适配又不那么好,可能需要你频繁的来回调整。

2. 开发链路与成本

原生方案开发,全程用华为的 DevEco Studio 工具。

不过这方案有个门槛,就是得学 ArkTs 的新语法和声明式开发逻辑。虽然有 AI 帮忙,学习时间从 1-2 个月缩短到 1-2 周,但该学的还是得学,尤其是AI智商不在线时。

3. 实践

我的第一个应用《赛博冰箱-食物保鲜管理》就是使用原生代码进行开发的,说实话,调试生成代码真的是欲仙欲死,AI对ArkTs的适配确实一般,有时候就直接下手改了,从项目开始到上线总共耗时十天,主要功能开发时间6天。

应用截图:

三、uniapp 方案:鸿蒙开发的 “效率加速器”

uniapp 是国内超火的跨端开发框架,简单来说,写一套代码就能在手机、小程序、鸿蒙系统上运行。它把 Vue 代码 “翻译” 成鸿蒙能看懂的格式,还能和各种 AI 工具无缝配合,对中小团队特别友好。

1. 适配逻辑与技术优势

  • 一套代码走遍天下:不管是安卓、iOS,还是鸿蒙、微信小程序,uniapp 都能适配。你用 Vue 写个点餐小程序,不用改代码就能直接打包成鸿蒙应用,省下重复开发的时间和人力。

  • 上手快到离谱:国内 500 多万开发者都会 Vue 语法,用 uniapp 开发鸿蒙应用,直接就能复用之前的组件库。像 uView 里现成的按钮、弹窗组件,拿过来就能用,新手 1-2 周就能上手开发。

  • 性能和原生 “掰手腕”:uniapp 做了两大优化:一是把 Vue 代码提前编译成 ArkUI 组件,启动速度只比原生方案慢 8%-12%;二是提供了专属 API,调用鸿蒙的文件管理、通知权限这些功能,不用再自己写复杂的桥接代码。

  • AI 工具全家桶支持:因为是使用成熟的Vue框架,Cursor、Trae 这些热门 AI 编程工具,基本都能适配 uniapp。但 Codebuddy 、Qoder这些新的AI工具,适配就没那么好了,经常出现Vue模板代码无法闭合的错误,但语法逻辑上还是没问题的,这相对于ArkTs来说已经好很多了,至少能提升40%的调试时间。

2. 开发流程与效率提升

用 uniapp 开发鸿蒙应用,搭配 AI 工具,效率直接起飞。

graph TD  
    A[装环境:HBuilderX + uniapp鸿蒙插件 + DevEco] --> B[AI帮写代码:生成Vue语法+鸿蒙适配逻辑]  
    B --> C[直接抄作业:AI推荐现成组件]  
    C --> D[一键打包:选鸿蒙平台,AI自动检查错误]  
    D --> E[自动适配:框架+AI搞定不同设备]  
    E --> F[测试找 bug:AI帮你定位问题]

不过使用uniapp仍然需要DevEco Studio 工具,也就是说你可能需要同时开三个IDE,Cursor生成代码,HBuilder编译,DevEco运行个模拟器,如果电脑配置较差可能会有点吃力,当然DevEco的模拟器可以单独运行。

2025 鸿蒙开发者激励计划里对于应用技术栈倒没有限制,只要是能够开发出来顺序上线就可以!

3. 实践

体会到原生开发的痛苦后,后面就转而使用了uniapp,因为在目前AI开发的大势下,Vue语言的优势是很明显的。

护眼爱眼小组件主要开发时间2天,会计宝典-会计学习考试一点通主要开发时间3天,文言文字典-学生常用字字典主要开发时间3天,找出卧底-谁是隐藏的卧底主要开发时间5天。

四、方案优缺点对比:从技术到产品的决策维度

从实际开发和应用场景出发,整理了两种方案的对比,帮你快速选对工具:

结语

想在鸿蒙生态里搞开发,原生 ArkTs 适合追求极致性能和深度绑定华为生态的项目;uniapp 则是中小团队和 个人开发者的 “效率神器”。再加上 2025 鸿蒙开发者激励计划的福利,搭配热门 AI 工具,不管选哪种方案,都能帮你快速开发出优质应用,赶紧抓住机会上车吧!

收起阅读 »

uni-app 打通鸿蒙从开发到上架:一条龙落地指南

鸿蒙next uniapp 教程 uni_app 鸿蒙征文

适用对象:会用 uni-app / uni-app x 开发应用的前端 / 全栈同学。
目标:从 项目初始化 → 端能力接入 → 调试与适配 → 打包签名 → 商店上架,一次走通。

1. 环境与账号准备

安装工具

HBuilderX(建议最新,含 uni-app/uni-app x 支持)

DevEco Studio(真机/模拟器调试、日志、证书校验)

Node.js(如使用 Vite 生态)

账号与材料

开发者账号:注册并完成实名认证

应用信息:预创建应用,确定 包名/BundleName;准备上架素材(图标、启动图、截图、隐私协议)

签名文件 & Profile:按平台指引生成,后续打包用

小贴士:包名一旦发版不要改;签名(证书/私钥/Profile)必须与应用信息一致。

2. 选型与项目初始化

创建项目

HBuilderX → 文件 → 新建 → 项目(见下图)

目录要点
├── pages/ # 页面(你的业务代码)
├── static/ # 静态资源(音频/图片/字体)
├── manifest.json # 应用配置、权限、图标、启动图
└── pages.json # 路由与导航栏配置

建议:先用 H5 模式验证交互与路由,再上模拟器/真机,缩短调试回路。

3. DevEco Studio 模拟器(先跑通流程)

如果只是演练流程,可先起模拟器:

在 DevEco Studio 新建空项目 → 打开 设备管理器

选择默认模拟器型号

安装后点击启动

启动完成即有一台“真机”可用

注意(以你当前环境为例):DevEco 5.1.1 Beta 下,下载 API 19 模拟器即可运行 uni-app 鸿蒙项目与元服务,其它模拟器暂不支持。

4. 调试证书(HBuilderX 一键配置)

进入 AGC:https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#

创建 APP ID(包名要与项目一致)

回到 uni-app 项目,打开 manifest.json → 鸿蒙 App 配置 → 调试证书配置 → 配置

填入与 AGC 一致的 包名 → 点击 一键配置 → AGC 授权后自动生成证书 → 保存

5. 本地调试(H5→模拟器/真机)

H5 调试(建议先走一遍,保证功能基本正确)

鸿蒙端调试

HBuilderX 顶部 运行 → 运行到手机或模拟器 → 运行到鸿蒙

选择设备;若无设备,刷新或重启模拟器后再刷新

点击运行,自动打包并安装到模拟器进行调试

通过标准:完成核心流程、权限弹窗、前后台切换、冷启动、异常路径回归后再考虑发布。

6. 发布证书(生成 & 申请)

6.1 在 DevEco Studio 生成私钥与 CSR

DevEco Studio → 随便新建个项目 → 构建 → 生成私钥和证书请求(CSR)文件

弹窗中 New → 选一处目录(文中称 目录 A),密码保持一致

记住 Alias(后续会用)→ 下一步

将 CSR 文件保存到 目录 A

点击 Finish,完成 CSR 生成

6.2 在 AGC 申请发布证书 & Profile

AGC → 证书/APP ID/Profile → APP ID → 证书 → 新增证书

选择 发布证书,上传本地 CSR(目录 A),名称随意

保存并下载证书到 目录 A

进入 Profile → 按引导 新增 Profile → 下载到 目录 A

7. 在 HBuilderX 配置发布证书

打开项目 manifest.json → 鸿蒙 App 配置 → 正式证书配置 → 配置

证书/私钥/Profile 均从 目录 A 选择(同后缀基本不会选错)

私钥别名填写之前记录的 Alias → 保存

8. 本地打包(生成鸿蒙安装包)

HBuilderX → 发行 → HarmonyOS 本地打包(名称以你版本为准)

成功后获得鸿蒙安装产物(包含 HAP 的安装包)

自测清单:冷启动/热启动、横竖屏、网络/离线、权限拒绝后流程、前后台恢复、长列表滚动、音频/图片资源是否打进包

常见坑

包名/证书/Profile 不匹配 → 无法安装或覆盖

静态资源路径不规范 → 音频/图片找不到

权限未声明 → 能力调用失败

9. 上架流程(AGC)

AGC → 证书、APP ID 和 Profile → APP ID → 找到你的 APP ID → 发布

按流程填写:应用信息、分级与分类、权限用途说明、隐私政策、素材(图标/启动图/截图/视频)

上传安装包,完成检查 → 提交审核

10. 提审前强烈建议:

1.云真机回归

AGC → 开发与服务 → 选择项目 → 质量 → 云调试(云真机)

多机型跑用例:首次启动、权限拒绝/允许、深色模式、分辨率适配、音视频设备权限、异常网络

修复兼容性问题后再提交,显著提高过审率

2.图标配置
在manifest.json中记得配置好下面三张图,提高过审几率

继续阅读 »

适用对象:会用 uni-app / uni-app x 开发应用的前端 / 全栈同学。
目标:从 项目初始化 → 端能力接入 → 调试与适配 → 打包签名 → 商店上架,一次走通。

1. 环境与账号准备

安装工具

HBuilderX(建议最新,含 uni-app/uni-app x 支持)

DevEco Studio(真机/模拟器调试、日志、证书校验)

Node.js(如使用 Vite 生态)

账号与材料

开发者账号:注册并完成实名认证

应用信息:预创建应用,确定 包名/BundleName;准备上架素材(图标、启动图、截图、隐私协议)

签名文件 & Profile:按平台指引生成,后续打包用

小贴士:包名一旦发版不要改;签名(证书/私钥/Profile)必须与应用信息一致。

2. 选型与项目初始化

创建项目

HBuilderX → 文件 → 新建 → 项目(见下图)

目录要点
├── pages/ # 页面(你的业务代码)
├── static/ # 静态资源(音频/图片/字体)
├── manifest.json # 应用配置、权限、图标、启动图
└── pages.json # 路由与导航栏配置

建议:先用 H5 模式验证交互与路由,再上模拟器/真机,缩短调试回路。

3. DevEco Studio 模拟器(先跑通流程)

如果只是演练流程,可先起模拟器:

在 DevEco Studio 新建空项目 → 打开 设备管理器

选择默认模拟器型号

安装后点击启动

启动完成即有一台“真机”可用

注意(以你当前环境为例):DevEco 5.1.1 Beta 下,下载 API 19 模拟器即可运行 uni-app 鸿蒙项目与元服务,其它模拟器暂不支持。

4. 调试证书(HBuilderX 一键配置)

进入 AGC:https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#

创建 APP ID(包名要与项目一致)

回到 uni-app 项目,打开 manifest.json → 鸿蒙 App 配置 → 调试证书配置 → 配置

填入与 AGC 一致的 包名 → 点击 一键配置 → AGC 授权后自动生成证书 → 保存

5. 本地调试(H5→模拟器/真机)

H5 调试(建议先走一遍,保证功能基本正确)

鸿蒙端调试

HBuilderX 顶部 运行 → 运行到手机或模拟器 → 运行到鸿蒙

选择设备;若无设备,刷新或重启模拟器后再刷新

点击运行,自动打包并安装到模拟器进行调试

通过标准:完成核心流程、权限弹窗、前后台切换、冷启动、异常路径回归后再考虑发布。

6. 发布证书(生成 & 申请)

6.1 在 DevEco Studio 生成私钥与 CSR

DevEco Studio → 随便新建个项目 → 构建 → 生成私钥和证书请求(CSR)文件

弹窗中 New → 选一处目录(文中称 目录 A),密码保持一致

记住 Alias(后续会用)→ 下一步

将 CSR 文件保存到 目录 A

点击 Finish,完成 CSR 生成

6.2 在 AGC 申请发布证书 & Profile

AGC → 证书/APP ID/Profile → APP ID → 证书 → 新增证书

选择 发布证书,上传本地 CSR(目录 A),名称随意

保存并下载证书到 目录 A

进入 Profile → 按引导 新增 Profile → 下载到 目录 A

7. 在 HBuilderX 配置发布证书

打开项目 manifest.json → 鸿蒙 App 配置 → 正式证书配置 → 配置

证书/私钥/Profile 均从 目录 A 选择(同后缀基本不会选错)

私钥别名填写之前记录的 Alias → 保存

8. 本地打包(生成鸿蒙安装包)

HBuilderX → 发行 → HarmonyOS 本地打包(名称以你版本为准)

成功后获得鸿蒙安装产物(包含 HAP 的安装包)

自测清单:冷启动/热启动、横竖屏、网络/离线、权限拒绝后流程、前后台恢复、长列表滚动、音频/图片资源是否打进包

常见坑

包名/证书/Profile 不匹配 → 无法安装或覆盖

静态资源路径不规范 → 音频/图片找不到

权限未声明 → 能力调用失败

9. 上架流程(AGC)

AGC → 证书、APP ID 和 Profile → APP ID → 找到你的 APP ID → 发布

按流程填写:应用信息、分级与分类、权限用途说明、隐私政策、素材(图标/启动图/截图/视频)

上传安装包,完成检查 → 提交审核

10. 提审前强烈建议:

1.云真机回归

AGC → 开发与服务 → 选择项目 → 质量 → 云调试(云真机)

多机型跑用例:首次启动、权限拒绝/允许、深色模式、分辨率适配、音视频设备权限、异常网络

修复兼容性问题后再提交,显著提高过审率

2.图标配置
在manifest.json中记得配置好下面三张图,提高过审几率

收起阅读 »

精膳通智慧食堂的鸿蒙开发之旅:因 uni-app 而简化,为国产生态而让利

鸿蒙next 鸿蒙征文

一、困境:想做鸿蒙版,却卡在 “入门关”

作为精膳通智慧食堂的开发负责人,今年随时鸿蒙用户量的暴增,我们接到了大量学校、企业客户的需求 ——“能不能出个鸿蒙版 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

  1. 我们在根目录新建一个文件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

  1. 我们在根目录新建一个文件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打造房产投资管理系统全记录

鸿蒙next 鸿蒙征文

📖 目录

项目背景:从房东的痛点说起 {#项目背景}

故事的开始

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代表着移动生态的未来

  1. 国产化浪潮:信创政策推动,政企市场潜力巨大
  2. 技术创新:分布式能力、原子化服务让人眼前一亮
  3. 生态建设:华为全力推进,开发者红利期
  4. 市场份额:2024年国内份额已突破20%,增长迅猛
  5. 开发者友好:完善的文档、活跃的社区

我的判断:提前布局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图)

数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。

设计亮点:

  1. property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
  2. 自动同步机制:收租记录→财务记录的自动创建
  3. 状态联动:租户状态→房间状态的自动更新
  4. 数据溯源:通过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(); // 不阻断主流程  
            });  
    });  
}

设计亮点:

  1. 自动化同步:收租后自动创建财务记录,无需手动二次录入
  2. 数据溯源:通过description字段清晰标识收入来源
  3. 三级关联:租户→房间→房产,确保数据完整性
  4. 容错机制:财务记录失败不影响收租记录保存

功能模块三:财务管理 - 数据的统一中心

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设计遵循以下原则:

  1. 商务专业:渐变、阴影、卡片化设计
  2. 信息清晰:合理的视觉层级和间距
  3. 操作便捷:大按钮、明确反馈
  4. 数据可视:数字大、图标辅助

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 实用功能

  1. 真机调试:实时预览,快速定位问题
  2. 代码提示:TypeScript类型提示
  3. 条件编译:处理平台差异
// #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提供的优秀平台。

让我们一起,星光不负,码向未来! 🚀

继续阅读 »

📖 目录

项目背景:从房东的痛点说起 {#项目背景}

故事的开始

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代表着移动生态的未来

  1. 国产化浪潮:信创政策推动,政企市场潜力巨大
  2. 技术创新:分布式能力、原子化服务让人眼前一亮
  3. 生态建设:华为全力推进,开发者红利期
  4. 市场份额:2024年国内份额已突破20%,增长迅猛
  5. 开发者友好:完善的文档、活跃的社区

我的判断:提前布局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图)

数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。

设计亮点:

  1. property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
  2. 自动同步机制:收租记录→财务记录的自动创建
  3. 状态联动:租户状态→房间状态的自动更新
  4. 数据溯源:通过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(); // 不阻断主流程  
            });  
    });  
}

设计亮点:

  1. 自动化同步:收租后自动创建财务记录,无需手动二次录入
  2. 数据溯源:通过description字段清晰标识收入来源
  3. 三级关联:租户→房间→房产,确保数据完整性
  4. 容错机制:财务记录失败不影响收租记录保存

功能模块三:财务管理 - 数据的统一中心

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设计遵循以下原则:

  1. 商务专业:渐变、阴影、卡片化设计
  2. 信息清晰:合理的视觉层级和间距
  3. 操作便捷:大按钮、明确反馈
  4. 数据可视:数字大、图标辅助

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 实用功能

  1. 真机调试:实时预览,快速定位问题
  2. 代码提示:TypeScript类型提示
  3. 条件编译:处理平台差异
// #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号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。

作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:

  1. 是否有针对香港或国际开发者的特殊验证流程?
  2. 能否在验证页面添加国家代码(例如+852)的选择功能?
  3. 或者,能否请你们协助手动为我的账户完成手机号验证?
    感谢你们的时间和帮助!期待你们的回复。
    祝好,
继续阅读 »

尊敬的 DCloud 技术支持团队,

你们好!

我是一位来自香港的开发者,正在使用你们的 uni-app 框架进行开发,非常感谢你们提供优秀的工具。
目前,我在进行开发者账户的 手机号验证 时遇到了一个阻碍。在“验证手机号”的页面中,系统只允许输入11位数字的手机号码,并且 没有提供国家/地区代码的下拉选择框。
我的香港手机号码格式为 +852 XXXX XXXX(共8位),无法在目前仅支持+86号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。

作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:

  1. 是否有针对香港或国际开发者的特殊验证流程?
  2. 能否在验证页面添加国家代码(例如+852)的选择功能?
  3. 或者,能否请你们协助手动为我的账户完成手机号验证?
    感谢你们的时间和帮助!期待你们的回复。
    祝好,
收起阅读 »