HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

【鸿蒙征文】关于鸿蒙折叠屏(宽屏设备)适配的一些分享

鸿蒙折叠屏适配 宽屏适配 鸿蒙征文

随着鸿蒙系统的不断发展以及uni-app x 对鸿蒙的全面支持,相信使用 uni-app X 进行鸿蒙应用及跨平台应用开发,是前端开发的最优选择。近期完成了对公司的已有应用进行鸿蒙平台适配的适配工作,有了 uni-app X 为基础,整个过程变得“轻松、高效”!

在折叠屏的适配过程中获得了一些经验,分享给大家(仅是自己的一些想法,欢迎大家指正~)。

适配目标效果


如上图所示:屏幕展开时(宽屏),卡片为一行两列布局,屏幕折叠时(窄屏)变成一行一个卡片。

原理分享

1 在页面初始化时获取屏幕宽度,识别运行屏幕环境是否为宽屏;
2 在屏幕宽度变化时,重新判处屏幕运行环境;
3 创建对应的 flex 布局样式,使用 :class="" 进行动态样式切换,达到宽屏及折叠屏幕折叠、展开的适配;

源码分享

<template>  
    <view :class="['flex', isTable?'rows':'column', isTable?'flex-wrap':'']">  
        <view   
        v-for="(item, idx) in 10" :key="idx"  
        :class="['item', isTable ? 'w-half':'w-full']" >  
            <text class="text">{{idx}}</text>  
        </view>  
    </view>  
</template>  
<script>  
export default {  
    data() {  
        return {  
            title: 'Hello',  
            windowWidth : 350, // 保留变量,可以记录屏幕宽度,单位 px  
            isTable : false,  
        }  
    },  
    onReady() {  
        //  
        let info = uni.getWindowInfo();  
        this.isTable = info.windowWidth > 400;  
        console.log(this.isTable);  
    },  
    methods: {  

    },  
    onResize : function(e){  
        this.isTable = e.size.windowWidth > 400;  
        console.log(this.isTable);  
    }  
}  
</script>  
<style>  
.flex{display:flex;}  
.rows{flex-direction:row;}  
.column{flex-direction:column;}  
.flex-wrap{flex-wrap:wrap;}  

.item{background-color:#FF0036;}  
.w-full{width:700rpx; height:300rpx; margin:25rpx;}  
.w-half{width:48%; height:200rpx; margin:20rpx 1%;}  
.text{font-size:20px;}  
</style>

总结

1 演示代码通过监听屏幕尺寸变化,使用 isTable 变量记录了屏幕是否为宽屏,然后通过 flex 布局切换,实现了布局目标;
2 演示代码虽然简单,经过自己的布局设计,完全可以适配 宽屏、折叠屏(动态监听折叠事件);
3 如果您在宽屏应用中遇到字体尺寸问题,可以以 text 组件为基础封装一个自己的文本组件,针对不同屏幕尺寸设置不同的字体大小(可以将 isTable 作为属性传递到组件内,通过 watch 观察屏幕变化,动态切换字体尺寸);
4 如果担心 onResize 监听多次触发引起的效率问题,可以使用 setTimeout 函数实现防抖,仅执行短时间内的一次变化即可;

左右功能不同的布局

如果您开发的应用在宽屏模式下,左侧为列表,右侧为详情:
您可以以 isTable 为核心,使用 if 进行右侧功能的条件渲染,如 : 宽屏模式

<view v-if="isTable">右侧功能区</view>

当我们点击列表项目时调用对应函数,同样判断是否宽屏模式,如果是再右侧详情区域展示详情,反之,打开一个新的页面展示详情即可。

一个很基础的原理分享,希望对大家的鸿蒙之旅有所帮助,谢谢阅读。

继续阅读 »

随着鸿蒙系统的不断发展以及uni-app x 对鸿蒙的全面支持,相信使用 uni-app X 进行鸿蒙应用及跨平台应用开发,是前端开发的最优选择。近期完成了对公司的已有应用进行鸿蒙平台适配的适配工作,有了 uni-app X 为基础,整个过程变得“轻松、高效”!

在折叠屏的适配过程中获得了一些经验,分享给大家(仅是自己的一些想法,欢迎大家指正~)。

适配目标效果


如上图所示:屏幕展开时(宽屏),卡片为一行两列布局,屏幕折叠时(窄屏)变成一行一个卡片。

原理分享

1 在页面初始化时获取屏幕宽度,识别运行屏幕环境是否为宽屏;
2 在屏幕宽度变化时,重新判处屏幕运行环境;
3 创建对应的 flex 布局样式,使用 :class="" 进行动态样式切换,达到宽屏及折叠屏幕折叠、展开的适配;

源码分享

<template>  
    <view :class="['flex', isTable?'rows':'column', isTable?'flex-wrap':'']">  
        <view   
        v-for="(item, idx) in 10" :key="idx"  
        :class="['item', isTable ? 'w-half':'w-full']" >  
            <text class="text">{{idx}}</text>  
        </view>  
    </view>  
</template>  
<script>  
export default {  
    data() {  
        return {  
            title: 'Hello',  
            windowWidth : 350, // 保留变量,可以记录屏幕宽度,单位 px  
            isTable : false,  
        }  
    },  
    onReady() {  
        //  
        let info = uni.getWindowInfo();  
        this.isTable = info.windowWidth > 400;  
        console.log(this.isTable);  
    },  
    methods: {  

    },  
    onResize : function(e){  
        this.isTable = e.size.windowWidth > 400;  
        console.log(this.isTable);  
    }  
}  
</script>  
<style>  
.flex{display:flex;}  
.rows{flex-direction:row;}  
.column{flex-direction:column;}  
.flex-wrap{flex-wrap:wrap;}  

.item{background-color:#FF0036;}  
.w-full{width:700rpx; height:300rpx; margin:25rpx;}  
.w-half{width:48%; height:200rpx; margin:20rpx 1%;}  
.text{font-size:20px;}  
</style>

总结

1 演示代码通过监听屏幕尺寸变化,使用 isTable 变量记录了屏幕是否为宽屏,然后通过 flex 布局切换,实现了布局目标;
2 演示代码虽然简单,经过自己的布局设计,完全可以适配 宽屏、折叠屏(动态监听折叠事件);
3 如果您在宽屏应用中遇到字体尺寸问题,可以以 text 组件为基础封装一个自己的文本组件,针对不同屏幕尺寸设置不同的字体大小(可以将 isTable 作为属性传递到组件内,通过 watch 观察屏幕变化,动态切换字体尺寸);
4 如果担心 onResize 监听多次触发引起的效率问题,可以使用 setTimeout 函数实现防抖,仅执行短时间内的一次变化即可;

左右功能不同的布局

如果您开发的应用在宽屏模式下,左侧为列表,右侧为详情:
您可以以 isTable 为核心,使用 if 进行右侧功能的条件渲染,如 : 宽屏模式

<view v-if="isTable">右侧功能区</view>

当我们点击列表项目时调用对应函数,同样判断是否宽屏模式,如果是再右侧详情区域展示详情,反之,打开一个新的页面展示详情即可。

一个很基础的原理分享,希望对大家的鸿蒙之旅有所帮助,谢谢阅读。

收起阅读 »

uni-app 也能开发纯血鸿蒙 App?使用 wot-starter 这样快速上手!

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

大家好,我是不如摸鱼去,wot-ui 的“主理人”,欢迎来到我的 uni-app 分享专栏。

wot-ui 是当下最流行的 uni-app vue3 组件库之一。

2024年,华为原生鸿蒙 HarmonyOS NEXT(5.0)正式发布,随后 uni-app 宣布了对原生鸿蒙的支持。如今 HarmonyOS 6 都发布了,uni-app 对纯血鸿蒙的支持如何了?我们今天来探索使用 wot-starter 构建支持 HarmonyOS NEXT 的 App。

项目环境

我选择使用我们团队维护的 wot-starter 作为起手项目,VSCode 作为主要开发工具,HbuilderX 和 DevEco Studio 作为调试和打包工具。

技术栈:

  • wot-starter: 基于 vitesse-uni-app 深度整合 Wot UI 组件库的快速启动模板 https://starter.wot-ui.cn/

开发工具:

  • HbuilderX: uni-app 专属开发工具 https://www.dcloud.io/hbuilderx.html
  • DevEco Studio: 开发 HarmonyOS 应用及元服务的集成开发环境(IDE)https://developer.huawei.com/consumer/cn/deveco-studio/
  • VSCode: 大家的最爱 https://code.visualstudio.com/

环境信息:

  • node.js v22.17.1

所有的开发工具我们可以安装最新的版本。

运行

如果手上有 HarmonyOS NEXT 系统的手机可以将手机的开发者模式打开连接到电脑上,如果没有则可以安装鸿蒙模拟器,参考文档 https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html#connectvirtually 进行安装启动

配置 DevEco-Studio 路径

配置调试证书

运行到手机

选择运行设备 or 模拟器后点击运行,在编译代码构建运行包的时候,有三个缓存使用策略可供选择:

  • 根据变化差量更新缓存:正常使用缓存来避免重复操作,提高构建效率。
  • 强制使用缓存,跳过编译:如果没有修改代码,只想重新运行起来,则可以使用这种方式,此时若 HBuilderX 检查到已经有构建好的运行包存在,则直接安装运行,否则按正常方式构建再运行。
  • 清空缓存:每次升级 HBuilderX 之后,新旧版本的鸿蒙工程目录可能不完全兼容,为避免旧版本的干扰,首次运行的时候可以选择这个选项。 另外,如果运行时出现结果不符合预期的奇怪情况,可以尝试使用这种方式重新构建运行,以消除缓存错乱带来的干扰。

打包

创建App

我们访问https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/harmonyOSDevPlatform/172249065903274453,注册账号并新建一个 APP ID,这一步我们在申请调试证书时应该已经做过。

申请发布证书

参考教程 https://developer.huawei.com/consumer/cn/doc/app/agc-help-add-releasecert-0000001946273961 申请发布证书并保存下来。

生成证书请求文件

申请发布证书时需要证书请求文件,我们参考教程 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-signing#section462703710326 在 DevEco Studio 中生成证书请求文件并保存。

注意填写的内容和生成的证书文件都需要妥善保管

配置打包证书

我们在 hbuilderx 中配置打包证书,参考教程:https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html#signing-configs

打包

按照下图操作即可打包出 app 文件。

运行效果

运行效果如下图,与 iOS、安卓应用基本一致。

注意点

在运行到原生鸿蒙的过程中,有一些点需要注意下:

  • harmony os 条件编译不可以使用APP-PLUS,可以使用APP
  • plus API 大部分不可用
  • manifest.json 中配置的证书信息,主要是 app-harmony 对应的内容要手动同步到manifest.config.ts下,不然下次运行就没了。
  • 自行探索...

CI/CD

目前是依赖 HbuilderX 和 DevEco Studio 做 APP 构建的,HbuilderX CLI 也可以打包鸿蒙应用,我期望可以脱离 HbuilderX,使用 app-harmony 产物加 DevEco Studio 做 CI/CD,类似原来安卓离线打包一样的流程,目前还在探索中。

总结

我们今天验证了 通过 wot-starter 模板开发纯血鸿蒙(HarmonyOS NEXT)应用。技术栈基于 wot-ui 组件库与 vitesse-uni-app,结合VSCode、HBuilderX及DevEco Studio实现开发调试。流程涵盖环境配置、证书申请、真机/模拟器运行及打包发布,运行效果与安卓/iOS一致。需注意:条件编译禁用APP-PLUS(改用APP),plus API大部分不可用。当前构建依赖HBuilderX,未来探索通过app-harmony产物结合DevEco Studio实现CI/CD,为跨平台开发提供新路径。

参考资源

  • wot-ui: https://wot-ui.cn/
  • DevEco Studio: https://developer.huawei.com/consumer/cn/deveco-studio/
  • wot-starter: https://starter.wot-ui.cn/
  • uni-app 鸿蒙教程: https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html

欢迎评论区沟通、讨论👇👇

继续阅读 »

大家好,我是不如摸鱼去,wot-ui 的“主理人”,欢迎来到我的 uni-app 分享专栏。

wot-ui 是当下最流行的 uni-app vue3 组件库之一。

2024年,华为原生鸿蒙 HarmonyOS NEXT(5.0)正式发布,随后 uni-app 宣布了对原生鸿蒙的支持。如今 HarmonyOS 6 都发布了,uni-app 对纯血鸿蒙的支持如何了?我们今天来探索使用 wot-starter 构建支持 HarmonyOS NEXT 的 App。

项目环境

我选择使用我们团队维护的 wot-starter 作为起手项目,VSCode 作为主要开发工具,HbuilderX 和 DevEco Studio 作为调试和打包工具。

技术栈:

  • wot-starter: 基于 vitesse-uni-app 深度整合 Wot UI 组件库的快速启动模板 https://starter.wot-ui.cn/

开发工具:

  • HbuilderX: uni-app 专属开发工具 https://www.dcloud.io/hbuilderx.html
  • DevEco Studio: 开发 HarmonyOS 应用及元服务的集成开发环境(IDE)https://developer.huawei.com/consumer/cn/deveco-studio/
  • VSCode: 大家的最爱 https://code.visualstudio.com/

环境信息:

  • node.js v22.17.1

所有的开发工具我们可以安装最新的版本。

运行

如果手上有 HarmonyOS NEXT 系统的手机可以将手机的开发者模式打开连接到电脑上,如果没有则可以安装鸿蒙模拟器,参考文档 https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html#connectvirtually 进行安装启动

配置 DevEco-Studio 路径

配置调试证书

运行到手机

选择运行设备 or 模拟器后点击运行,在编译代码构建运行包的时候,有三个缓存使用策略可供选择:

  • 根据变化差量更新缓存:正常使用缓存来避免重复操作,提高构建效率。
  • 强制使用缓存,跳过编译:如果没有修改代码,只想重新运行起来,则可以使用这种方式,此时若 HBuilderX 检查到已经有构建好的运行包存在,则直接安装运行,否则按正常方式构建再运行。
  • 清空缓存:每次升级 HBuilderX 之后,新旧版本的鸿蒙工程目录可能不完全兼容,为避免旧版本的干扰,首次运行的时候可以选择这个选项。 另外,如果运行时出现结果不符合预期的奇怪情况,可以尝试使用这种方式重新构建运行,以消除缓存错乱带来的干扰。

打包

创建App

我们访问https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/harmonyOSDevPlatform/172249065903274453,注册账号并新建一个 APP ID,这一步我们在申请调试证书时应该已经做过。

申请发布证书

参考教程 https://developer.huawei.com/consumer/cn/doc/app/agc-help-add-releasecert-0000001946273961 申请发布证书并保存下来。

生成证书请求文件

申请发布证书时需要证书请求文件,我们参考教程 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-signing#section462703710326 在 DevEco Studio 中生成证书请求文件并保存。

注意填写的内容和生成的证书文件都需要妥善保管

配置打包证书

我们在 hbuilderx 中配置打包证书,参考教程:https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html#signing-configs

打包

按照下图操作即可打包出 app 文件。

运行效果

运行效果如下图,与 iOS、安卓应用基本一致。

注意点

在运行到原生鸿蒙的过程中,有一些点需要注意下:

  • harmony os 条件编译不可以使用APP-PLUS,可以使用APP
  • plus API 大部分不可用
  • manifest.json 中配置的证书信息,主要是 app-harmony 对应的内容要手动同步到manifest.config.ts下,不然下次运行就没了。
  • 自行探索...

CI/CD

目前是依赖 HbuilderX 和 DevEco Studio 做 APP 构建的,HbuilderX CLI 也可以打包鸿蒙应用,我期望可以脱离 HbuilderX,使用 app-harmony 产物加 DevEco Studio 做 CI/CD,类似原来安卓离线打包一样的流程,目前还在探索中。

总结

我们今天验证了 通过 wot-starter 模板开发纯血鸿蒙(HarmonyOS NEXT)应用。技术栈基于 wot-ui 组件库与 vitesse-uni-app,结合VSCode、HBuilderX及DevEco Studio实现开发调试。流程涵盖环境配置、证书申请、真机/模拟器运行及打包发布,运行效果与安卓/iOS一致。需注意:条件编译禁用APP-PLUS(改用APP),plus API大部分不可用。当前构建依赖HBuilderX,未来探索通过app-harmony产物结合DevEco Studio实现CI/CD,为跨平台开发提供新路径。

参考资源

  • wot-ui: https://wot-ui.cn/
  • DevEco Studio: https://developer.huawei.com/consumer/cn/deveco-studio/
  • wot-starter: https://starter.wot-ui.cn/
  • uni-app 鸿蒙教程: https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html

欢迎评论区沟通、讨论👇👇

收起阅读 »

修复vue2 composition-api 中 computed,app环境无法二次更新的问题

composition_api vue2

vue2项目中使用 @vue/composition-api,h5发现没有任何问题,打包成app就出现了computed无法更新的问题

import { computed, ref } from '@vue/composition-api'  
import { onPageScroll } from '@dcloudio/uni-app'  

export function usePageScroll() {  
  const top = ref(0)  
  onPageScroll(e => {  
    top.value = e.scrollTop  
  })  
  return top  
}  

const navigationBarHeight = uni.getSystemInfoSync().statusBarHeight || 44  
export function useNavbarOpacity(navHeight = navigationBarHeight) {  
  const scrollTop = usePageScroll()  
  return computed(() => Math.min(navHeight, scrollTop.value) / navHeight)  
}

在app里面就发现导航栏的背景颜色无法随着滚动变透明
后面改成 watch+ref就解决了(不知道什么原因)

最终解决方案

vue-composition-plugin.js,在main.ts最开头一行引用,覆盖默认的computed方法


import Vue from 'vue'  
import * as VueCompositionAPI from '@vue/composition-api'  
const watch = VueCompositionAPI.watch  
const ref = VueCompositionAPI.ref  
// app-plus 有bug 直接使用计算属性,计算属性不会自动更新  
// 修复 app 端使用 computed时,第二次修改相关 getter里面的值的时候  
// computed 的值不会更新,去掉原先计算属性的懒加载效果  
// 强制使用watch加ref实现类似 computed  
VueCompositionAPI.computed = function(options){  
let getter = options  
let setter  
if (typeof options === 'object') {  
getter = options.get  
setter = options.set  
}  
let initVal  
const refVal = ref(initVal)  
watch(()=> {  
initVal = getter()  
return initVal  
}, (newVal,oldVal)=>{  
setter && setter(newVal,oldVal)  
refVal.value = newVal  
}, {deep:true,immediate:true})  

return refVal
}
Vue.use(VueCompositionAPI.default)

继续阅读 »

vue2项目中使用 @vue/composition-api,h5发现没有任何问题,打包成app就出现了computed无法更新的问题

import { computed, ref } from '@vue/composition-api'  
import { onPageScroll } from '@dcloudio/uni-app'  

export function usePageScroll() {  
  const top = ref(0)  
  onPageScroll(e => {  
    top.value = e.scrollTop  
  })  
  return top  
}  

const navigationBarHeight = uni.getSystemInfoSync().statusBarHeight || 44  
export function useNavbarOpacity(navHeight = navigationBarHeight) {  
  const scrollTop = usePageScroll()  
  return computed(() => Math.min(navHeight, scrollTop.value) / navHeight)  
}

在app里面就发现导航栏的背景颜色无法随着滚动变透明
后面改成 watch+ref就解决了(不知道什么原因)

最终解决方案

vue-composition-plugin.js,在main.ts最开头一行引用,覆盖默认的computed方法


import Vue from 'vue'  
import * as VueCompositionAPI from '@vue/composition-api'  
const watch = VueCompositionAPI.watch  
const ref = VueCompositionAPI.ref  
// app-plus 有bug 直接使用计算属性,计算属性不会自动更新  
// 修复 app 端使用 computed时,第二次修改相关 getter里面的值的时候  
// computed 的值不会更新,去掉原先计算属性的懒加载效果  
// 强制使用watch加ref实现类似 computed  
VueCompositionAPI.computed = function(options){  
let getter = options  
let setter  
if (typeof options === 'object') {  
getter = options.get  
setter = options.set  
}  
let initVal  
const refVal = ref(initVal)  
watch(()=> {  
initVal = getter()  
return initVal  
}, (newVal,oldVal)=>{  
setter && setter(newVal,oldVal)  
refVal.value = newVal  
}, {deep:true,immediate:true})  

return refVal
}
Vue.use(VueCompositionAPI.default)

收起阅读 »

从零到一开发鸿蒙6原生时钟应用:uni-app x 完全实战指南

鸿蒙征文

🚀 从零到一开发鸿蒙6原生时钟应用:uni-app x 完全实战指南

前言

鸿蒙6时代已来,纯血鸿蒙应用开发正当时!

在移动应用开发的新纪元,鸿蒙6(HarmonyOS 6)作为华为全新一代操作系统,已经完全摆脱了安卓内核,实现了真正的"纯血鸿蒙"。本文将详细介绍如何使用 uni-app x 框架,从零开发一款功能完整、界面精美的鸿蒙6原生时钟应用,集成时钟、闹钟、秒表、计时器四大核心功能。

为什么选择鸿蒙6?

  • 🎯 纯血鸿蒙系统,性能提升30%+
  • 🎯 全新的分布式架构,跨设备协同
  • 🎯 更强大的安全机制
  • 🎯 完善的开发者生态

image-20251028080111473

🌟 项目亮点

  • 🎨 Material Design 深色主题:符合鸿蒙6设计规范,夜间使用友好
  • 📱 鸿蒙6原生应用:使用 uni-app x 编译为纯血鸿蒙原生应用
  • 🚀 完全离线可用:无需网络连接,数据本地存储
  • 🎯 代码结构清晰:模块化设计,易于维护和扩展
  • 极致性能:鸿蒙6平台原生性能,启动速度提升50%
  • 🔥 一次开发,多端部署:支持 HarmonyOS 6、Android、iOS、H5、小程序
  • 🌐 分布式能力:充分利用鸿蒙6的跨设备协同特性
  • 🔐 安全可靠:符合鸿蒙6的安全标准

🛠️ 技术栈

- 核心框架:uni-app x(专为鸿蒙6优化)  
- 开发语言:UTS(TypeScript 超集,编译为鸿蒙原生代码)  
- 前端框架:Vue 3 Composition API  
- 样式方案:SCSS + Material Design  
- 数据存储:uni.storage → 鸿蒙6 Preferences API  
- 目标平台:HarmonyOS 6(纯血鸿蒙系统)  
- API版本:HarmonyOS API 18+

🤔 为什么选择 uni-app x 开发鸿蒙6应用?

uni-app x 是专为鸿蒙6量身打造的跨平台开发框架!

对比维度 原生开发 uni-app x 其他跨平台框架
开发语言 ArkTS UTS(类TypeScript) JavaScript/Dart
性能表现 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
开发效率 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
跨平台能力 ✅ 5端通用 ✅ 有限支持
生态支持 🟢 官方 🟢 官方+社区 🟡 社区
鸿蒙6适配 ✅ 完美 ✅ 完美 🟡 部分

5大核心优势

  1. 🚀 真正的原生性能

    • uni-app x 编译为鸿蒙6原生代码(非WebView)
    • 启动速度快50%,运行流畅度媲美原生
    • 内存占用降低30%
  2. 💡 开发效率提升200%

    • 使用熟悉的 Vue 3 语法
    • 热重载、调试工具完善
    • 一套代码,5端运行(HarmonyOS、Android、iOS、H5、小程序)
  3. 🌐 无缝对接鸿蒙6能力

    • 原生 API 直接调用(通知、振动、后台任务)
    • 支持鸿蒙6分布式特性
    • 完美适配折叠屏、穿戴设备
  4. 🎯 生态丰富

    • 10000+ 插件市场
    • 活跃的开发者社区
    • 完善的文档和示例
  5. 🔥 DCloud 官方支持

    • 全力支持鸿蒙生态建设
    • 及时更新适配鸿蒙最新版本
    • 提供技术支持和培训

一、鸿蒙6应用开发环境搭建

1.1 开发环境要求

要使用 uni-app x 开发鸿蒙6应用,需要准备以下环境:

硬件要求

  • CPU:Intel i5 / AMD Ryzen 5 及以上
  • 内存:16GB 及以上(推荐32GB)
  • 硬盘:至少50GB可用空间(SSD推荐)

软件环境

✅ 操作系统:  
   - Windows 10/11(64位)  
   - macOS 12.0+  

✅ 开发工具:  
   - HBuilderX 4.0+(内置 uni-app x + 鸿蒙6支持)  
   - 下载地址:https://www.dcloud.io/hbuilderx.html  

✅ 鸿蒙开发套件:  
   - HarmonyOS 6 SDK(API Level 18+)  
   - DevEco Studio(可选,用于查看API文档)  
   - 下载地址:https://developer.huawei.com  

✅ 开发语言:  
   - UTS(uni-app TypeScript,编译为鸿蒙原生代码)  

✅ 运行时:  
   - Node.js 18.0+(LTS版本)  
   - npm 9.0+ 或 pnpm 8.0+

获取开发者账号

  1. 注册华为开发者账号:https://developer.huawei.com
  2. 实名认证(个人或企业)
  3. 申请应用签名证书

1.2 鸿蒙6应用配置

manifest.json 中配置鸿蒙6应用信息(关键配置):

{  
  "name": "simpleclock",  
  "appid": "your_app_id",  
  "description": "多功能时钟应用",  
  "versionName": "1.0.0",  
  "versionCode": "100",  
  "uni-app-x": {  
    "harmony": {  
      "minAPIVersion": "12",  
      "targetAPIVersion": "12",  
      "compatibleAPIVersion": "12",  
      "abilities": [  
        {  
          "name": "MainAbility",  
          "description": "时钟应用主界面"  
        }  
      ],  
      "permissions": [  
        "ohos.permission.KEEP_BACKGROUND_RUNNING",  
        "ohos.permission.NOTIFICATION_CONTROLLER",  
        "ohos.permission.VIBRATE"  
      ]  
    }  
  }  
}

1.3 项目架构设计

应用采用经典的 Tab Bar 导航结构,完美适配鸿蒙设计规范:

simpleclock/  
├── pages/  
│   ├── index/         # 时钟页面(世界时钟)  
│   ├── alarm/         # 闹钟页面  
│   ├── stopwatch/     # 秒表页面  
│   ├── timer/         # 计时器页面  
│   └── settings/      # 设置页面  
├── App.uvue           # 应用入口(.uvue 是 uni-app x 专用格式)  
├── pages.json         # 页面配置  
├── uni.scss           # 全局样式变量  
└── manifest.json      # 应用配置(含鸿蒙配置)

1.4 uni-app x 与传统 uni-app 的核心区别

特性 传统 uni-app uni-app x(鸿蒙6) 优势
编译方式 JavaScript 运行时 编译为鸿蒙原生代码 性能提升50%
渲染引擎 WebView 鸿蒙原生渲染 流畅度大幅提升
性能表现 接近原生(80%) 100%原生性能 与原生开发一致
文件格式 .vue .uvue 支持鸿蒙特性
开发语言 JavaScript/TypeScript UTS 类型安全,性能更好
鸿蒙6支持 支持纯血鸿蒙 ✅ 完美支持,未来演进方向 无缝对接鸿蒙API
包体积 较大(含运行时) 更小(30%↓) 启动更快
热更新 ✅ 支持 ⚠️ 受限 鸿蒙安全限制

重要说明

  • 🔥 鸿蒙6应用必须使用 uni-app x
  • 🔥 传统 uni-app 只能运行在 HarmonyOS 兼容模式(非纯血)
  • 🔥 新项目强烈推荐直接使用 uni-app x

1.5 配置 pages.json(鸿蒙适配)

首先配置底部导航栏,这里特别注意鸿蒙平台的样式适配:

{  
  "tabBar": {  
    "color": "#999999",  
    "selectedColor": "#FF6B00",  
    "backgroundColor": "#2C2C2C",  
    "borderStyle": "black",  
    "list": [  
      {  
        "pagePath": "pages/index/index",  
        "text": "时钟"  
      },  
      {  
        "pagePath": "pages/alarm/alarm",  
        "text": "闹钟"  
      },  
      {  
        "pagePath": "pages/stopwatch/stopwatch",  
        "text": "秒表"  
      },  
      {  
        "pagePath": "pages/timer/timer",  
        "text": "计时器"  
      }  
    ]  
  }  
}

1.6 鸿蒙平台条件编译

uni-app x 提供了强大的条件编译能力,可以针对鸿蒙平台做特殊处理:

// #ifdef APP-HARMONY  
// 鸿蒙平台特有代码  
import { vibrator } from '@kit.SensorServiceKit'  
import { notificationManager } from '@kit.NotificationKit'  
// #endif  

// #ifdef APP-ANDROID  
// Android 平台代码  
// #endif  

// #ifndef APP-HARMONY  
// 非鸿蒙平台代码  
// #endif

1.7 Material Design 深色主题配置(鸿蒙适配)

uni.scss 中定义全局颜色变量,完美适配鸿蒙深色模式:

/* Material Design 深色主题颜色 */  
$primary-color: #FF6B00;        // 主色调(橙色)  
$background-dark: #212121;      // 深色背景  
$background-card: #2C2C2C;      // 卡片背景  
$background-elevated: #3A3A3A;  // 高亮背景  

$text-primary: #FFFFFF;         // 主要文本  
$text-secondary: #CCCCCC;       // 次要文本  
$text-disabled: #999999;        // 禁用文本

二、核心功能实现(鸿蒙原生 API 集成)

2.1 时钟页面 - 实时时间与多时区显示

2.1.1 实时时间更新(鸿蒙优化版)

在鸿蒙平台上,我们可以使用更高效的定时器机制。uni-app x 的 setInterval 会被编译为鸿蒙原生定时器:

export default {  
  data() {  
    return {  
      currentTime: '9:23',  
      period: 'a.m.',  
      currentDate: 'Mon, 18 January',  
      timeFormat: 12,  
      timer: null  
    }  
  },  
  onLoad() {  
    this.updateTime()  
    this.startTimer()  
  },  
  methods: {  
    updateTime() {  
      const now = new Date()  
      const hours = now.getHours()  
      const minutes = now.getMinutes()  

      if (this.timeFormat === 12) {  
        const displayHours = hours % 12 || 12  
        this.currentTime = displayHours + ':' +   
          (minutes < 10 ? '0' : '') + minutes  
        this.period = hours >= 12 ? 'p.m.' : 'a.m.'  
      } else {  
        this.currentTime = (hours < 10 ? '0' : '') + hours + ':' +   
          (minutes < 10 ? '0' : '') + minutes  
        this.period = ''  
      }  
    },  
    startTimer() {  
      this.timer = setInterval(() => {  
        this.updateTime()  
      }, 1000)  
    }  
  }  
}

2.1.2 时间格式切换与鸿蒙数据持久化

使用 uni.storage API,在鸿蒙平台上会自动映射到鸿蒙的 Preferences 数据存储:

setTimeFormat(format) {  
  this.timeFormat = format  
  this.updateTime()  
  // 在鸿蒙平台会自动使用 Preferences API  
  uni.setStorageSync('timeFormat', format)  
}  

// 鸿蒙平台的存储路径  
// /data/storage/el2/base/preferences/时钟应用/

2.1.3 鸿蒙系统时间获取优化

// #ifdef APP-HARMONY  
import { systemDateTime } from '@kit.BasicServicesKit'  

// 获取鸿蒙系统时间(更精确)  
const harmonyTime = systemDateTime.getCurrentTime()  
// #endif

2.2 闹钟页面 - 鸿蒙通知与后台任务

2.2.1 鸿蒙权限申请

在开发闹钟功能前,需要申请鸿蒙相关权限:

// manifest.json - 鸿蒙权限配置  
{  
  "uni-app-x": {  
    "harmony": {  
      "permissions": [  
        "ohos.permission.NOTIFICATION_CONTROLLER",  // 通知权限  
        "ohos.permission.VIBRATE",                  // 振动权限  
        "ohos.permission.KEEP_BACKGROUND_RUNNING"   // 后台运行  
      ],  
      "backgroundModes": ["dataTransfer", "audioPlayback"]  
    }  
  }  
}
// 运行时请求权限(鸿蒙平台)  
// #ifdef APP-HARMONY  
import { abilityAccessCtrl } from '@kit.AbilityKit'  

async function requestPermissions() {  
  const atManager = abilityAccessCtrl.createAtManager()  
  try {  
    await atManager.requestPermissionsFromUser(getContext(), [  
      'ohos.permission.NOTIFICATION_CONTROLLER',  
      'ohos.permission.VIBRATE'  
    ])  
  } catch (err) {  
    console.error('权限申请失败', err)  
  }  
}  
// #endif

2.2.2 数据结构设计

闹钟数据结构设计如下:

{  
  time: '08:00',              // 时间  
  label: '起床',              // 标签  
  repeat: [false, true, ...], // 重复(周日到周六)  
  ringtone: 0,                // 铃声索引  
  vibrate: true,              // 振动  
  gradualVolume: true,        // 音量渐增  
  snooze: 10,                 // 贪睡时长(分钟)  
  enabled: true               // 是否启用  
}

2.2.3 鸿蒙本地存储实现

uni.storage 在鸿蒙平台会自动使用 Preferences API,提供高性能的键值对存储:

methods: {  
  // 加载闹钟  
  loadAlarms() {  
    const saved = uni.getStorageSync('alarms')  
    if (saved) {  
      this.alarms = JSON.parse(saved)  
    }  
  },  

  // 保存闹钟  
  saveAlarms() {  
    uni.setStorageSync('alarms', JSON.stringify(this.alarms))  
  },  

  // 添加/编辑闹钟  
  saveAlarm() {  
    if (this.editIndex >= 0) {  
      // 编辑现有闹钟  
      this.alarms[this.editIndex] = this.editAlarmData  
    } else {  
      // 添加新闹钟  
      this.alarms.push(this.editAlarmData)  
    }  
    this.saveAlarms()  
    this.closeDialog()  
  },  

  // 删除闹钟  
  deleteAlarm() {  
    uni.showModal({  
      title: '确认删除',  
      content: '确定要删除这个闹钟吗?',  
      success: (res) => {  
        if (res.confirm) {  
          this.alarms.splice(this.editIndex, 1)  
          this.saveAlarms()  
        }  
      }  
    })  
  }  
}

2.2.4 鸿蒙通知发送

使用鸿蒙原生通知 API 发送闹钟提醒:

// #ifdef APP-HARMONY  
import notificationManager from '@ohos.notificationManager'  

// 发送鸿蒙通知  
async function sendAlarmNotification(alarmData) {  
  const notificationRequest = {  
    id: alarmData.id,  
    content: {  
      contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,  
      normal: {  
        title: '闹钟提醒',  
        text: alarmData.label || '该起床了!',  
        additionalText: new Date().toLocaleTimeString()  
      }  
    },  
    actionButtons: [  
      {  
        title: '贪睡',  
        wantAgent: createSnoozeWantAgent()  
      },  
      {  
        title: '关闭',  
        wantAgent: createCloseWantAgent()  
      }  
    ]  
  }  

  try {  
    await notificationManager.publish(notificationRequest)  
  } catch (err) {  
    console.error('通知发送失败', err)  
  }  
}  
// #endif  

// 跨平台兼容写法  
function sendNotification(alarmData) {  
  // #ifdef APP-HARMONY  
  sendAlarmNotification(alarmData)  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.showModal({  
    title: '闹钟提醒',  
    content: alarmData.label || '该起床了!'  
  })  
  // #endif  
}

2.2.5 鸿蒙振动反馈

// #ifdef APP-HARMONY  
import vibrator from '@ohos.vibrator'  

// 鸿蒙振动效果  
function vibrateHarmony() {  
  try {  
    // 预定义振动效果  
    vibrator.startVibration({  
      type: 'preset',  
      effectId: 'haptic.clock.timer',  
      count: 3  
    })  

    // 或自定义振动  
    vibrator.startVibration({  
      type: 'time',  
      duration: 1000  
    })  
  } catch (err) {  
    console.error('振动失败', err)  
  }  
}  
// #endif  

// 跨平台振动封装  
function triggerVibration() {  
  // #ifdef APP-HARMONY  
  vibrateHarmony()  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.vibrateShort()  
  // #endif  
}

2.2.6 弹窗表单设计

使用自定义弹窗组件实现闹钟编辑界面:

<view v-if="showDialog" class="dialog-mask" @click="closeDialog">  
  <view class="dialog" @click.stop>  
    <view class="dialog-header">  
      <text class="dialog-title">  
        {{editIndex >= 0 ? '编辑闹钟' : '添加闹钟'}}  
      </text>  
    </view>  

    <view class="dialog-content">  
      <!-- 时间选择器 -->  
      <picker mode="time" :value="editAlarmData.time"   
              @change="onTimeChange">  
        <text>{{editAlarmData.time}}</text>  
      </picker>  

      <!-- 周重复选择 -->  
      <view class="week-selector">  
        <text v-for="(day, i) in weekDays" :key="i"   
              :class="{'active': editAlarmData.repeat[i]}"  
              @click="toggleDay(i)">  
          {{day}}  
        </text>  
      </view>  
    </view>  
  </view>  
</view>

2.3 秒表页面 - 高精度计时与圈数记录

2.3.1 高精度计时实现

使用 10ms 间隔实现百分之一秒精度:

export default {  
  data() {  
    return {  
      isRunning: false,  
      totalTime: 0,      // 总时间(毫秒)  
      startTime: 0,  
      timer: null,  
      laps: []           // 圈数记录  
    }  
  },  
  computed: {  
    formattedTime() {  
      const totalSeconds = Math.floor(this.totalTime / 1000)  
      const minutes = Math.floor(totalSeconds / 60)  
      const seconds = totalSeconds % 60  
      return (minutes < 10 ? '0' : '') + minutes + ':' +   
             (seconds < 10 ? '0' : '') + seconds  
    },  
    milliseconds() {  
      return String(Math.floor((this.totalTime % 1000) / 10))  
        .padStart(2, '0')  
    }  
  },  
  methods: {  
    start() {  
      this.isRunning = true  
      this.startTime = Date.now() - this.totalTime  
      this.timer = setInterval(() => {  
        this.totalTime = Date.now() - this.startTime  
      }, 10)  // 10ms 更新一次  
    }  
  }  
}

2.3.2 圈数记录与排序

实现圈数记录和多种排序方式:

methods: {  
  recordLap() {  
    const lapTime = this.totalTime - this.lastLapTime  
    this.lapCounter++  

    this.laps.unshift({  
      id: Date.now(),  
      index: this.lapCounter,  
      time: this.formatTime(lapTime),  
      rawTime: lapTime  
    })  

    this.lastLapTime = this.totalTime  
  }  
},  
computed: {  
  sortedLaps() {  
    let sorted = [...this.laps]  

    // 根据排序模式排序  
    if (this.sortMode === 'fastest') {  
      sorted.sort((a, b) => a.rawTime - b.rawTime)  
    } else if (this.sortMode === 'slowest') {  
      sorted.sort((a, b) => b.rawTime - a.rawTime)  
    }  

    // 标记最快和最慢  
    if (sorted.length > 1) {  
      const times = this.laps.map(l => l.rawTime)  
      const fastest = Math.min(...times)  
      const slowest = Math.max(...times)  

      sorted = sorted.map(lap => ({  
        ...lap,  
        isFastest: lap.rawTime === fastest,  
        isSlowest: lap.rawTime === slowest && fastest !== slowest  
      }))  
    }  

    return sorted  
  }  
}

2.4 计时器页面 - 倒计时与进度显示

2.4.1 时间选择器实现

使用 picker-view 实现时间选择:

<view class="picker-row">  
  <view class="picker-group">  
    <picker-view class="picker" :value="[hours]"   
                 @change="onHoursChange">  
      <picker-view-column>  
        <view v-for="n in 24" :key="n" class="picker-item">  
          <text>{{n - 1}}</text>  
        </view>  
      </picker-view-column>  
    </picker-view>  
    <text class="picker-label">小时</text>  
  </view>  

  <!-- 分钟和秒的选择器类似 -->  
</view>

2.4.2 进度条实现

实时计算并显示倒计时进度:

computed: {  
  progressPercent() {  
    if (this.totalSeconds === 0) return 0  
    return Math.floor(  
      (this.totalSeconds - this.remainingTime) /   
      this.totalSeconds * 100  
    )  
  }  
}
<view class="progress-bar">  
  <view class="progress-fill"   
        :style="{width: progressPercent + '%'}">  
  </view>  
</view>

2.4.3 倒计时完成处理

onTimerComplete() {  
  this.stopTimer()  
  this.isRunning = false  

  // 振动提醒  
  if (this.vibrate) {  
    // uni.vibrateLong()  
  }  

  // 弹窗通知  
  if (this.notification) {  
    uni.showModal({  
      title: '计时完成',  
      content: '计时器时间已到!',  
      showCancel: false  
    })  
  }  
}

2.5 设置页面 - 丰富的个性化选项

2.5.1 屏幕常亮功能

onKeepScreenOnChange(e) {  
  this.keepScreenOn = e.detail.value  
  uni.setStorageSync('keepScreenOn', this.keepScreenOn)  

  uni.setKeepScreenOn({  
    keepScreenOn: this.keepScreenOn  
  })  

  uni.showToast({  
    title: this.keepScreenOn ?   
      '屏幕常亮已开启' : '屏幕常亮已关闭',  
    icon: 'none'  
  })  
}

2.5.2 数据清除功能

clearData() {  
  uni.showModal({  
    title: '确认清除',  
    content: '此操作将清除所有闹钟和设置数据,无法恢复。确定要继续吗?',  
    confirmText: '清除',  
    confirmColor: '#F44336',  
    success: (res) => {  
      if (res.confirm) {  
        uni.clearStorageSync()  
        uni.showToast({  
          title: '数据已清除',  
          icon: 'success'  
        })  
        // 重启应用  
        setTimeout(() => {  
          uni.reLaunch({  
            url: '/pages/index/index'  
          })  
        }, 1500)  
      }  
    }  
  })  
}

三、UI/UX 设计与优化

3.1 Material Design 样式实现

3.1.1 卡片阴影效果

.card {  
  background-color: #2C2C2C;  
  border-radius: 8px;  
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);  
}

3.1.2 悬浮按钮(FAB)

.add-btn {  
  position: fixed;  
  right: 20px;  
  bottom: 100px;  
  width: 60px;  
  height: 60px;  
  background-color: #FF6B00;  
  border-radius: 30px;  
  display: flex;  
  justify-content: center;  
  align-items: center;  
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);  
}

3.2 颜色系统

深色主题配色方案:

元素 颜色值 用途
主色 #FF6B00 按钮、选中状态、导航栏
背景 #212121 页面背景
卡片 #2C2C2C 列表项、卡片背景
分隔 #3A3A3A 边框、分隔线
文本主 #FFFFFF 主要文本
文本次 #CCCCCC 次要文本
文本禁 #999999 禁用文本

3.3 交互优化

3.3.1 空状态设计

<view v-if="alarms.length === 0" class="empty-state">  
  <text class="empty-text">暂无闹钟</text>  
  <text class="empty-hint">点击下方 "+" 按钮添加闹钟</text>  
</view>

3.3.2 加载状态与反馈

// 操作成功提示  
uni.showToast({  
  title: '保存成功',  
  icon: 'success'  
})  

// 确认对话框  
uni.showModal({  
  title: '确认删除',  
  content: '确定要删除这个闹钟吗?',  
  success: (res) => {  
    if (res.confirm) {  
      // 执行删除  
    }  
  }  
})

四、性能优化

4.1 计时器管理

确保在页面卸载时清除定时器,避免内存泄漏:

export default {  
  onUnload() {  
    this.stopTimer()  
  },  
  methods: {  
    stopTimer() {  
      if (this.timer) {  
        clearInterval(this.timer)  
        this.timer = null  
      }  
    }  
  }  
}

4.2 数据缓存策略

  • 使用 uni.setStorageSync 同步存储关键数据
  • 页面加载时从本地读取数据,减少计算
  • 数据变更时立即保存,确保数据一致性

4.3 列表渲染优化

使用 :key 提升列表渲染性能:

<view v-for="(alarm, index) in alarms"   
      :key="alarm.id || index"   
      class="alarm-item">  
  <!-- 内容 -->  
</view>

五、鸿蒙平台深度适配

5.1 鸿蒙返回键处理

鸿蒙系统的返回键需要特殊处理,实现双击退出:

// #ifdef APP-HARMONY  
let firstBackTime = 0  

export default {  
  onLastPageBackPress: function () {  
    console.log('HarmonyOS 返回键按下')  
    if (firstBackTime == 0) {  
      uni.showToast({  
        title: '再按一次退出应用',  
        position: 'bottom',  
      })  
      firstBackTime = Date.now()  
      setTimeout(() => {  
        firstBackTime = 0  
      }, 2000)  
    } else if (Date.now() - firstBackTime < 2000) {  
      uni.exit() // 退出应用  
    }  
  }  
}  
// #endif

5.2 鸿蒙屏幕常亮

利用鸿蒙的电源管理 API 实现屏幕常亮:

// #ifdef APP-HARMONY  
import { runningLock } from '@kit.BasicServicesKit'  

let screenLock = null  

// 保持屏幕常亮  
function keepScreenOn() {  
  try {  
    screenLock = runningLock.createRunningLock('screen',   
      runningLock.RunningLockType.RUNNINGLOCK_SCREEN)  
    screenLock.lock(0) // 0 表示永久锁定  
  } catch (err) {  
    console.error('屏幕常亮失败', err)  
  }  
}  

// 释放屏幕锁  
function releaseScreenLock() {  
  if (screenLock) {  
    screenLock.unlock()  
    screenLock = null  
  }  
}  
// #endif  

// 跨平台封装  
function setKeepScreenOn(keep) {  
  // #ifdef APP-HARMONY  
  if (keep) {  
    keepScreenOn()  
  } else {  
    releaseScreenLock()  
  }  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.setKeepScreenOn({ keepScreenOn: keep })  
  // #endif  
}

5.3 鸿蒙后台任务

实现闹钟的后台运行:

// #ifdef APP-HARMONY  
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager'  

// 申请长时任务  
async function requestBackgroundTask() {  
  try {  
    await backgroundTaskManager.requestSuspendDelay('时钟后台任务', () => {  
      console.log('后台任务即将被挂起')  
      // 保存状态  
    })  
  } catch (err) {  
    console.error('后台任务申请失败', err)  
  }  
}  
// #endif

5.4 鸿蒙安全区域适配

考虑鸿蒙设备的刘海屏、水滴屏和折叠屏:

.container {  
  // 鸿蒙安全区域  
  padding-top: env(safe-area-inset-top);  
  padding-bottom: env(safe-area-inset-bottom);  
  padding-left: env(safe-area-inset-left);  
  padding-right: env(safe-area-inset-right);  
}  

// 鸿蒙折叠屏适配  
@media screen and (min-width: 600px) {  
  .container {  
    max-width: 600px;  
    margin: 0 auto;  
  }  
}

5.5 鸿蒙生命周期适配

export default {  
  // 应用前台  
  onShow() {  
    // #ifdef APP-HARMONY  
    console.log('HarmonyOS 应用进入前台')  
    this.resumeTimers() // 恢复计时器  
    // #endif  
  },  

  // 应用后台  
  onHide() {  
    // #ifdef APP-HARMONY  
    console.log('HarmonyOS 应用进入后台')  
    this.pauseTimers() // 暂停计时器  
    this.saveState() // 保存状态  
    // #endif  
  }  
}

5.6 鸿蒙深色模式适配

鸿蒙系统的深色模式可以自动切换:

// #ifdef APP-HARMONY  
import { configuration } from '@kit.AbilityKit'  

// 监听系统主题变化  
function watchThemeChange() {  
  const config = configuration.getConfiguration()  
  const isDark = config.colorMode === configuration.ColorMode.COLOR_MODE_DARK  

  // 应用深色主题  
  if (isDark) {  
    this.applyDarkTheme()  
  } else {  
    this.applyLightTheme()  
  }  
}  
// #endif

六、鸿蒙应用测试与调试

6.1 鸿蒙开发者模式

在鸿蒙设备上启用开发者模式:

  1. 进入"设置" → "关于手机"
  2. 连续点击"版本号"7次
  3. 返回"设置" → "系统和更新" → "开发者选项"
  4. 开启"USB 调试"和"USB 安装"

6.2 HBuilderX 真机调试

# 1. 连接鸿蒙设备  
# 2. 在 HBuilderX 中选择运行设备  
# 3. 点击"运行" → "运行到手机或模拟器" → "运行到 HarmonyOS"  

# 查看日志  
hdc shell hilog | grep "simpleclock"

6.3 鸿蒙特有功能测试清单

  • [ ] 通知权限:闹钟通知是否正常显示
  • [ ] 振动权限:振动反馈是否生效
  • [ ] 后台任务:应用后台时闹钟是否正常工作
  • [ ] 屏幕常亮:前台运行时屏幕是否保持常亮
  • [ ] 数据持久化:应用重启后数据是否保留
  • [ ] 深色模式:系统切换主题时应用是否自动适配
  • [ ] 折叠屏适配:在折叠屏设备上布局是否正常
  • [ ] 返回键处理:双击返回是否正常退出

6.4 性能测试(鸿蒙平台)

// 使用鸿蒙性能追踪  
// #ifdef APP-HARMONY  
import hiTraceMeter from '@ohos.hiTraceMeter'  

// 开始性能追踪  
hiTraceMeter.startTrace('stopwatch_timing', 1001)  

// 执行计时逻辑  
this.updateStopwatchTime()  

// 结束追踪  
hiTraceMeter.finishTrace('stopwatch_timing', 1001)  
// #endif

6.2 边界情况处理

// 防止重复点击  
if (this.totalSeconds === 0) {  
  uni.showToast({  
    title: '请设置计时时间',  
    icon: 'none'  
  })  
  return  
}  

// 数据验证  
if (!editAlarmData.time) {  
  uni.showToast({  
    title: '请选择时间',  
    icon: 'none'  
  })  
  return  
}

七、项目总结

7.1 技术亮点

  1. 完整的功能闭环:从时间显示到提醒管理,覆盖用户全部需求
  2. 优雅的代码结构:模块化设计,职责清晰
  3. 良好的用户体验:Material Design + 深色主题
  4. 高性能实现:合理使用定时器,优化渲染性能
  5. 跨平台兼容:一套代码,多端运行

7.2 可扩展方向

  1. 云同步功能:支持多设备数据同步
  2. 更多铃声:支持自定义铃声上传
  3. 统计分析:记录使用习惯,提供数据分析
  4. 智能提醒:根据用户习惯智能推荐闹钟时间
  5. 主题定制:支持更多颜色主题

7.3 开发心得

  1. 用户体验至上:每一个交互细节都要仔细打磨
  2. 代码质量:保持代码简洁,注重可维护性
  3. 性能优化:及时清理资源,避免内存泄漏
  4. 测试驱动:充分测试各种边界情况
  5. 持续迭代:根据用户反馈不断优化改进

八、源码与资源

8.1 项目结构

simpleclock/  
├── pages/  
│   ├── index/index.uvue      # 264 行  
│   ├── alarm/alarm.uvue      # 430 行  
│   ├── stopwatch/stopwatch.uvue  # 280 行  
│   ├── timer/timer.uvue      # 300 行  
│   └── settings/settings.uvue    # 350 行  
├── App.uvue                  # 124 行  
├── pages.json                # 76 行  
├── uni.scss                  # 77 行  
├── README.md                 # 项目说明  
└── 使用指南.md               # 用户手册

8.2 代码统计

  • 总代码量:约 1900+ 行
  • 页面数:5 个主要页面
  • 组件数:多个自定义组件
  • 功能模块:4 个核心功能

8.3 运行环境

  • 开发工具:HBuilderX 4.0+
  • uni-app x 版本:最新版
  • 鸿蒙系统:HarmonyOS 6
  • 鸿蒙 SDK:API Level 10+
  • 测试设备:
    • 华为 Mate 60 系列(HarmonyOS 6)
    • 华为 MatePad Pro(折叠屏测试)
    • 鸿蒙模拟器

8.4 鸿蒙应用发布

8.4.1 应用签名

# 在华为开发者联盟申请应用签名  
# 下载签名文件并配置到 manifest.json  

{  
  "uni-app-x": {  
    "harmony": {  
      "signing": {  
        "profile": "path/to/profile.p7b",  
        "certFile": "path/to/cert.pem",  
        "keyFile": "path/to/key.p12",  
        "keyPassword": "your_password"  
      }  
    }  
  }  
}

8.4.2 打包发布

# HBuilderX 中操作:  
# 1. 发行 → 原生App-云打包  
# 2. 选择 HarmonyOS 平台  
# 3. 填写应用信息和签名配置  
# 4. 点击打包,等待云端编译  
# 5. 下载 .hap 安装包  

# 命令行打包(可选)  
npm run build:app-harmony

8.4.3 应用上架

  1. 登录华为开发者联盟
  2. 进入"应用服务" → "AppGallery Connect"
  3. 创建应用并上传 .hap 包
  4. 填写应用信息(截图、描述、分类)
  5. 提交审核
  6. 审核通过后上架

九、鸿蒙开发常见问题

9.1 权限被拒绝

问题:应用无法发送通知或振动

解决

// 在应用启动时主动请求权限  
// #ifdef APP-HARMONY  
async function checkAndRequestPermissions() {  
  const permissions = [  
    'ohos.permission.NOTIFICATION_CONTROLLER',  
    'ohos.permission.VIBRATE'  
  ]  

  for (let permission of permissions) {  
    const result = await checkPermission(permission)  
    if (!result) {  
      await requestPermission(permission)  
    }  
  }  
}  
// #endif

9.2 后台任务被终止

问题:闹钟在后台不响

解决:申请长时任务授权,确保应用在后台持续运行

9.3 数据丢失

问题:应用卸载后数据丢失

解决:使用鸿蒙云空间同步数据(需要用户登录华为账号)

9.4 性能问题

问题:页面切换卡顿

解决

  • 使用虚拟列表优化长列表渲染
  • 避免在主线程执行耗时操作
  • 使用鸿蒙的 Worker 线程处理复杂计算

结语

本文详细介绍了如何使用 uni-app x 开发一款功能完整的鸿蒙6原生时钟应用,从环境搭建、核心功能实现、鸿蒙6平台深度适配到应用发布,涵盖了鸿蒙6应用开发的各个方面。

🎉 这是一个真实的鸿蒙6原生应用开发案例!

核心要点回顾

  1. uni-app x 的优势

    • ✅ 编译为鸿蒙原生代码,性能卓越
    • ✅ 一套代码多端运行,大幅降低开发成本
    • ✅ 丰富的 API 支持,无缝对接鸿蒙能力
  2. 鸿蒙平台特性

    • ✅ 强大的通知系统
    • ✅ 完善的权限管理
    • ✅ 优秀的后台任务机制
    • ✅ 深度的系统集成
  3. 开发建议

    • 💡 充分利用条件编译,针对鸿蒙做优化
    • 💡 注意权限申请和用户体验
    • 💡 重视性能优化和内存管理
    • 💡 遵循鸿蒙设计规范

希望这篇文章能帮助你:

  • 🎯 零基础入门:从环境搭建到应用发布的完整流程
  • 🎨 深度理解鸿蒙6:掌握鸿蒙6的核心特性和设计理念
  • 💡 实战经验:学习时间管理类应用的最佳实践
  • 🚀 快速上手:30分钟开发出第一个鸿蒙6应用
  • 🔥 抓住机遇:在鸿蒙生态爆发期占据先机

下一步计划

  • [ ] 适配鸿蒙折叠屏设备
  • [ ] 接入华为账号系统
  • [ ] 实现华为云空间数据同步
  • [ ] 添加鸿蒙小部件(Widget)
  • [ ] 接入鸿蒙分布式能力

如果你有任何问题或建议,欢迎交流讨论!让我们一起为鸿蒙生态贡献力量!🚀


关键词:#鸿蒙6 #HarmonyOS6 #uni-app-x #纯血鸿蒙 #原生应用开发 #Vue3 #跨平台开发 #时钟应用

作者:坚果

日期:2025年10月28日
项目地址GitCode - simpleclock
开源协议:MIT License


📢 开源说明

本项目完全开源,欢迎:

  • ⭐ Star 项目,关注后续更新
  • 🍴 Fork 项目,二次开发
  • 🐛 提交 Issue,反馈问题
  • 🤝 提交 PR,贡献代码

当前状态

  • ✅ 核心功能已完成
  • ⚠️ 部分细节待优化(如时区同步、铃声播放等)
  • 🎯 持续迭代中,欢迎贡献代码!

🔗 相关资源

💬 交流讨论

如果你在开发过程中遇到问题,或有好的想法和建议:

让我们一起推动鸿蒙生态发展! 🚀🇨🇳

继续阅读 »

🚀 从零到一开发鸿蒙6原生时钟应用:uni-app x 完全实战指南

前言

鸿蒙6时代已来,纯血鸿蒙应用开发正当时!

在移动应用开发的新纪元,鸿蒙6(HarmonyOS 6)作为华为全新一代操作系统,已经完全摆脱了安卓内核,实现了真正的"纯血鸿蒙"。本文将详细介绍如何使用 uni-app x 框架,从零开发一款功能完整、界面精美的鸿蒙6原生时钟应用,集成时钟、闹钟、秒表、计时器四大核心功能。

为什么选择鸿蒙6?

  • 🎯 纯血鸿蒙系统,性能提升30%+
  • 🎯 全新的分布式架构,跨设备协同
  • 🎯 更强大的安全机制
  • 🎯 完善的开发者生态

image-20251028080111473

🌟 项目亮点

  • 🎨 Material Design 深色主题:符合鸿蒙6设计规范,夜间使用友好
  • 📱 鸿蒙6原生应用:使用 uni-app x 编译为纯血鸿蒙原生应用
  • 🚀 完全离线可用:无需网络连接,数据本地存储
  • 🎯 代码结构清晰:模块化设计,易于维护和扩展
  • 极致性能:鸿蒙6平台原生性能,启动速度提升50%
  • 🔥 一次开发,多端部署:支持 HarmonyOS 6、Android、iOS、H5、小程序
  • 🌐 分布式能力:充分利用鸿蒙6的跨设备协同特性
  • 🔐 安全可靠:符合鸿蒙6的安全标准

🛠️ 技术栈

- 核心框架:uni-app x(专为鸿蒙6优化)  
- 开发语言:UTS(TypeScript 超集,编译为鸿蒙原生代码)  
- 前端框架:Vue 3 Composition API  
- 样式方案:SCSS + Material Design  
- 数据存储:uni.storage → 鸿蒙6 Preferences API  
- 目标平台:HarmonyOS 6(纯血鸿蒙系统)  
- API版本:HarmonyOS API 18+

🤔 为什么选择 uni-app x 开发鸿蒙6应用?

uni-app x 是专为鸿蒙6量身打造的跨平台开发框架!

对比维度 原生开发 uni-app x 其他跨平台框架
开发语言 ArkTS UTS(类TypeScript) JavaScript/Dart
性能表现 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
开发效率 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
跨平台能力 ✅ 5端通用 ✅ 有限支持
生态支持 🟢 官方 🟢 官方+社区 🟡 社区
鸿蒙6适配 ✅ 完美 ✅ 完美 🟡 部分

5大核心优势

  1. 🚀 真正的原生性能

    • uni-app x 编译为鸿蒙6原生代码(非WebView)
    • 启动速度快50%,运行流畅度媲美原生
    • 内存占用降低30%
  2. 💡 开发效率提升200%

    • 使用熟悉的 Vue 3 语法
    • 热重载、调试工具完善
    • 一套代码,5端运行(HarmonyOS、Android、iOS、H5、小程序)
  3. 🌐 无缝对接鸿蒙6能力

    • 原生 API 直接调用(通知、振动、后台任务)
    • 支持鸿蒙6分布式特性
    • 完美适配折叠屏、穿戴设备
  4. 🎯 生态丰富

    • 10000+ 插件市场
    • 活跃的开发者社区
    • 完善的文档和示例
  5. 🔥 DCloud 官方支持

    • 全力支持鸿蒙生态建设
    • 及时更新适配鸿蒙最新版本
    • 提供技术支持和培训

一、鸿蒙6应用开发环境搭建

1.1 开发环境要求

要使用 uni-app x 开发鸿蒙6应用,需要准备以下环境:

硬件要求

  • CPU:Intel i5 / AMD Ryzen 5 及以上
  • 内存:16GB 及以上(推荐32GB)
  • 硬盘:至少50GB可用空间(SSD推荐)

软件环境

✅ 操作系统:  
   - Windows 10/11(64位)  
   - macOS 12.0+  

✅ 开发工具:  
   - HBuilderX 4.0+(内置 uni-app x + 鸿蒙6支持)  
   - 下载地址:https://www.dcloud.io/hbuilderx.html  

✅ 鸿蒙开发套件:  
   - HarmonyOS 6 SDK(API Level 18+)  
   - DevEco Studio(可选,用于查看API文档)  
   - 下载地址:https://developer.huawei.com  

✅ 开发语言:  
   - UTS(uni-app TypeScript,编译为鸿蒙原生代码)  

✅ 运行时:  
   - Node.js 18.0+(LTS版本)  
   - npm 9.0+ 或 pnpm 8.0+

获取开发者账号

  1. 注册华为开发者账号:https://developer.huawei.com
  2. 实名认证(个人或企业)
  3. 申请应用签名证书

1.2 鸿蒙6应用配置

manifest.json 中配置鸿蒙6应用信息(关键配置):

{  
  "name": "simpleclock",  
  "appid": "your_app_id",  
  "description": "多功能时钟应用",  
  "versionName": "1.0.0",  
  "versionCode": "100",  
  "uni-app-x": {  
    "harmony": {  
      "minAPIVersion": "12",  
      "targetAPIVersion": "12",  
      "compatibleAPIVersion": "12",  
      "abilities": [  
        {  
          "name": "MainAbility",  
          "description": "时钟应用主界面"  
        }  
      ],  
      "permissions": [  
        "ohos.permission.KEEP_BACKGROUND_RUNNING",  
        "ohos.permission.NOTIFICATION_CONTROLLER",  
        "ohos.permission.VIBRATE"  
      ]  
    }  
  }  
}

1.3 项目架构设计

应用采用经典的 Tab Bar 导航结构,完美适配鸿蒙设计规范:

simpleclock/  
├── pages/  
│   ├── index/         # 时钟页面(世界时钟)  
│   ├── alarm/         # 闹钟页面  
│   ├── stopwatch/     # 秒表页面  
│   ├── timer/         # 计时器页面  
│   └── settings/      # 设置页面  
├── App.uvue           # 应用入口(.uvue 是 uni-app x 专用格式)  
├── pages.json         # 页面配置  
├── uni.scss           # 全局样式变量  
└── manifest.json      # 应用配置(含鸿蒙配置)

1.4 uni-app x 与传统 uni-app 的核心区别

特性 传统 uni-app uni-app x(鸿蒙6) 优势
编译方式 JavaScript 运行时 编译为鸿蒙原生代码 性能提升50%
渲染引擎 WebView 鸿蒙原生渲染 流畅度大幅提升
性能表现 接近原生(80%) 100%原生性能 与原生开发一致
文件格式 .vue .uvue 支持鸿蒙特性
开发语言 JavaScript/TypeScript UTS 类型安全,性能更好
鸿蒙6支持 支持纯血鸿蒙 ✅ 完美支持,未来演进方向 无缝对接鸿蒙API
包体积 较大(含运行时) 更小(30%↓) 启动更快
热更新 ✅ 支持 ⚠️ 受限 鸿蒙安全限制

重要说明

  • 🔥 鸿蒙6应用必须使用 uni-app x
  • 🔥 传统 uni-app 只能运行在 HarmonyOS 兼容模式(非纯血)
  • 🔥 新项目强烈推荐直接使用 uni-app x

1.5 配置 pages.json(鸿蒙适配)

首先配置底部导航栏,这里特别注意鸿蒙平台的样式适配:

{  
  "tabBar": {  
    "color": "#999999",  
    "selectedColor": "#FF6B00",  
    "backgroundColor": "#2C2C2C",  
    "borderStyle": "black",  
    "list": [  
      {  
        "pagePath": "pages/index/index",  
        "text": "时钟"  
      },  
      {  
        "pagePath": "pages/alarm/alarm",  
        "text": "闹钟"  
      },  
      {  
        "pagePath": "pages/stopwatch/stopwatch",  
        "text": "秒表"  
      },  
      {  
        "pagePath": "pages/timer/timer",  
        "text": "计时器"  
      }  
    ]  
  }  
}

1.6 鸿蒙平台条件编译

uni-app x 提供了强大的条件编译能力,可以针对鸿蒙平台做特殊处理:

// #ifdef APP-HARMONY  
// 鸿蒙平台特有代码  
import { vibrator } from '@kit.SensorServiceKit'  
import { notificationManager } from '@kit.NotificationKit'  
// #endif  

// #ifdef APP-ANDROID  
// Android 平台代码  
// #endif  

// #ifndef APP-HARMONY  
// 非鸿蒙平台代码  
// #endif

1.7 Material Design 深色主题配置(鸿蒙适配)

uni.scss 中定义全局颜色变量,完美适配鸿蒙深色模式:

/* Material Design 深色主题颜色 */  
$primary-color: #FF6B00;        // 主色调(橙色)  
$background-dark: #212121;      // 深色背景  
$background-card: #2C2C2C;      // 卡片背景  
$background-elevated: #3A3A3A;  // 高亮背景  

$text-primary: #FFFFFF;         // 主要文本  
$text-secondary: #CCCCCC;       // 次要文本  
$text-disabled: #999999;        // 禁用文本

二、核心功能实现(鸿蒙原生 API 集成)

2.1 时钟页面 - 实时时间与多时区显示

2.1.1 实时时间更新(鸿蒙优化版)

在鸿蒙平台上,我们可以使用更高效的定时器机制。uni-app x 的 setInterval 会被编译为鸿蒙原生定时器:

export default {  
  data() {  
    return {  
      currentTime: '9:23',  
      period: 'a.m.',  
      currentDate: 'Mon, 18 January',  
      timeFormat: 12,  
      timer: null  
    }  
  },  
  onLoad() {  
    this.updateTime()  
    this.startTimer()  
  },  
  methods: {  
    updateTime() {  
      const now = new Date()  
      const hours = now.getHours()  
      const minutes = now.getMinutes()  

      if (this.timeFormat === 12) {  
        const displayHours = hours % 12 || 12  
        this.currentTime = displayHours + ':' +   
          (minutes < 10 ? '0' : '') + minutes  
        this.period = hours >= 12 ? 'p.m.' : 'a.m.'  
      } else {  
        this.currentTime = (hours < 10 ? '0' : '') + hours + ':' +   
          (minutes < 10 ? '0' : '') + minutes  
        this.period = ''  
      }  
    },  
    startTimer() {  
      this.timer = setInterval(() => {  
        this.updateTime()  
      }, 1000)  
    }  
  }  
}

2.1.2 时间格式切换与鸿蒙数据持久化

使用 uni.storage API,在鸿蒙平台上会自动映射到鸿蒙的 Preferences 数据存储:

setTimeFormat(format) {  
  this.timeFormat = format  
  this.updateTime()  
  // 在鸿蒙平台会自动使用 Preferences API  
  uni.setStorageSync('timeFormat', format)  
}  

// 鸿蒙平台的存储路径  
// /data/storage/el2/base/preferences/时钟应用/

2.1.3 鸿蒙系统时间获取优化

// #ifdef APP-HARMONY  
import { systemDateTime } from '@kit.BasicServicesKit'  

// 获取鸿蒙系统时间(更精确)  
const harmonyTime = systemDateTime.getCurrentTime()  
// #endif

2.2 闹钟页面 - 鸿蒙通知与后台任务

2.2.1 鸿蒙权限申请

在开发闹钟功能前,需要申请鸿蒙相关权限:

// manifest.json - 鸿蒙权限配置  
{  
  "uni-app-x": {  
    "harmony": {  
      "permissions": [  
        "ohos.permission.NOTIFICATION_CONTROLLER",  // 通知权限  
        "ohos.permission.VIBRATE",                  // 振动权限  
        "ohos.permission.KEEP_BACKGROUND_RUNNING"   // 后台运行  
      ],  
      "backgroundModes": ["dataTransfer", "audioPlayback"]  
    }  
  }  
}
// 运行时请求权限(鸿蒙平台)  
// #ifdef APP-HARMONY  
import { abilityAccessCtrl } from '@kit.AbilityKit'  

async function requestPermissions() {  
  const atManager = abilityAccessCtrl.createAtManager()  
  try {  
    await atManager.requestPermissionsFromUser(getContext(), [  
      'ohos.permission.NOTIFICATION_CONTROLLER',  
      'ohos.permission.VIBRATE'  
    ])  
  } catch (err) {  
    console.error('权限申请失败', err)  
  }  
}  
// #endif

2.2.2 数据结构设计

闹钟数据结构设计如下:

{  
  time: '08:00',              // 时间  
  label: '起床',              // 标签  
  repeat: [false, true, ...], // 重复(周日到周六)  
  ringtone: 0,                // 铃声索引  
  vibrate: true,              // 振动  
  gradualVolume: true,        // 音量渐增  
  snooze: 10,                 // 贪睡时长(分钟)  
  enabled: true               // 是否启用  
}

2.2.3 鸿蒙本地存储实现

uni.storage 在鸿蒙平台会自动使用 Preferences API,提供高性能的键值对存储:

methods: {  
  // 加载闹钟  
  loadAlarms() {  
    const saved = uni.getStorageSync('alarms')  
    if (saved) {  
      this.alarms = JSON.parse(saved)  
    }  
  },  

  // 保存闹钟  
  saveAlarms() {  
    uni.setStorageSync('alarms', JSON.stringify(this.alarms))  
  },  

  // 添加/编辑闹钟  
  saveAlarm() {  
    if (this.editIndex >= 0) {  
      // 编辑现有闹钟  
      this.alarms[this.editIndex] = this.editAlarmData  
    } else {  
      // 添加新闹钟  
      this.alarms.push(this.editAlarmData)  
    }  
    this.saveAlarms()  
    this.closeDialog()  
  },  

  // 删除闹钟  
  deleteAlarm() {  
    uni.showModal({  
      title: '确认删除',  
      content: '确定要删除这个闹钟吗?',  
      success: (res) => {  
        if (res.confirm) {  
          this.alarms.splice(this.editIndex, 1)  
          this.saveAlarms()  
        }  
      }  
    })  
  }  
}

2.2.4 鸿蒙通知发送

使用鸿蒙原生通知 API 发送闹钟提醒:

// #ifdef APP-HARMONY  
import notificationManager from '@ohos.notificationManager'  

// 发送鸿蒙通知  
async function sendAlarmNotification(alarmData) {  
  const notificationRequest = {  
    id: alarmData.id,  
    content: {  
      contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,  
      normal: {  
        title: '闹钟提醒',  
        text: alarmData.label || '该起床了!',  
        additionalText: new Date().toLocaleTimeString()  
      }  
    },  
    actionButtons: [  
      {  
        title: '贪睡',  
        wantAgent: createSnoozeWantAgent()  
      },  
      {  
        title: '关闭',  
        wantAgent: createCloseWantAgent()  
      }  
    ]  
  }  

  try {  
    await notificationManager.publish(notificationRequest)  
  } catch (err) {  
    console.error('通知发送失败', err)  
  }  
}  
// #endif  

// 跨平台兼容写法  
function sendNotification(alarmData) {  
  // #ifdef APP-HARMONY  
  sendAlarmNotification(alarmData)  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.showModal({  
    title: '闹钟提醒',  
    content: alarmData.label || '该起床了!'  
  })  
  // #endif  
}

2.2.5 鸿蒙振动反馈

// #ifdef APP-HARMONY  
import vibrator from '@ohos.vibrator'  

// 鸿蒙振动效果  
function vibrateHarmony() {  
  try {  
    // 预定义振动效果  
    vibrator.startVibration({  
      type: 'preset',  
      effectId: 'haptic.clock.timer',  
      count: 3  
    })  

    // 或自定义振动  
    vibrator.startVibration({  
      type: 'time',  
      duration: 1000  
    })  
  } catch (err) {  
    console.error('振动失败', err)  
  }  
}  
// #endif  

// 跨平台振动封装  
function triggerVibration() {  
  // #ifdef APP-HARMONY  
  vibrateHarmony()  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.vibrateShort()  
  // #endif  
}

2.2.6 弹窗表单设计

使用自定义弹窗组件实现闹钟编辑界面:

<view v-if="showDialog" class="dialog-mask" @click="closeDialog">  
  <view class="dialog" @click.stop>  
    <view class="dialog-header">  
      <text class="dialog-title">  
        {{editIndex >= 0 ? '编辑闹钟' : '添加闹钟'}}  
      </text>  
    </view>  

    <view class="dialog-content">  
      <!-- 时间选择器 -->  
      <picker mode="time" :value="editAlarmData.time"   
              @change="onTimeChange">  
        <text>{{editAlarmData.time}}</text>  
      </picker>  

      <!-- 周重复选择 -->  
      <view class="week-selector">  
        <text v-for="(day, i) in weekDays" :key="i"   
              :class="{'active': editAlarmData.repeat[i]}"  
              @click="toggleDay(i)">  
          {{day}}  
        </text>  
      </view>  
    </view>  
  </view>  
</view>

2.3 秒表页面 - 高精度计时与圈数记录

2.3.1 高精度计时实现

使用 10ms 间隔实现百分之一秒精度:

export default {  
  data() {  
    return {  
      isRunning: false,  
      totalTime: 0,      // 总时间(毫秒)  
      startTime: 0,  
      timer: null,  
      laps: []           // 圈数记录  
    }  
  },  
  computed: {  
    formattedTime() {  
      const totalSeconds = Math.floor(this.totalTime / 1000)  
      const minutes = Math.floor(totalSeconds / 60)  
      const seconds = totalSeconds % 60  
      return (minutes < 10 ? '0' : '') + minutes + ':' +   
             (seconds < 10 ? '0' : '') + seconds  
    },  
    milliseconds() {  
      return String(Math.floor((this.totalTime % 1000) / 10))  
        .padStart(2, '0')  
    }  
  },  
  methods: {  
    start() {  
      this.isRunning = true  
      this.startTime = Date.now() - this.totalTime  
      this.timer = setInterval(() => {  
        this.totalTime = Date.now() - this.startTime  
      }, 10)  // 10ms 更新一次  
    }  
  }  
}

2.3.2 圈数记录与排序

实现圈数记录和多种排序方式:

methods: {  
  recordLap() {  
    const lapTime = this.totalTime - this.lastLapTime  
    this.lapCounter++  

    this.laps.unshift({  
      id: Date.now(),  
      index: this.lapCounter,  
      time: this.formatTime(lapTime),  
      rawTime: lapTime  
    })  

    this.lastLapTime = this.totalTime  
  }  
},  
computed: {  
  sortedLaps() {  
    let sorted = [...this.laps]  

    // 根据排序模式排序  
    if (this.sortMode === 'fastest') {  
      sorted.sort((a, b) => a.rawTime - b.rawTime)  
    } else if (this.sortMode === 'slowest') {  
      sorted.sort((a, b) => b.rawTime - a.rawTime)  
    }  

    // 标记最快和最慢  
    if (sorted.length > 1) {  
      const times = this.laps.map(l => l.rawTime)  
      const fastest = Math.min(...times)  
      const slowest = Math.max(...times)  

      sorted = sorted.map(lap => ({  
        ...lap,  
        isFastest: lap.rawTime === fastest,  
        isSlowest: lap.rawTime === slowest && fastest !== slowest  
      }))  
    }  

    return sorted  
  }  
}

2.4 计时器页面 - 倒计时与进度显示

2.4.1 时间选择器实现

使用 picker-view 实现时间选择:

<view class="picker-row">  
  <view class="picker-group">  
    <picker-view class="picker" :value="[hours]"   
                 @change="onHoursChange">  
      <picker-view-column>  
        <view v-for="n in 24" :key="n" class="picker-item">  
          <text>{{n - 1}}</text>  
        </view>  
      </picker-view-column>  
    </picker-view>  
    <text class="picker-label">小时</text>  
  </view>  

  <!-- 分钟和秒的选择器类似 -->  
</view>

2.4.2 进度条实现

实时计算并显示倒计时进度:

computed: {  
  progressPercent() {  
    if (this.totalSeconds === 0) return 0  
    return Math.floor(  
      (this.totalSeconds - this.remainingTime) /   
      this.totalSeconds * 100  
    )  
  }  
}
<view class="progress-bar">  
  <view class="progress-fill"   
        :style="{width: progressPercent + '%'}">  
  </view>  
</view>

2.4.3 倒计时完成处理

onTimerComplete() {  
  this.stopTimer()  
  this.isRunning = false  

  // 振动提醒  
  if (this.vibrate) {  
    // uni.vibrateLong()  
  }  

  // 弹窗通知  
  if (this.notification) {  
    uni.showModal({  
      title: '计时完成',  
      content: '计时器时间已到!',  
      showCancel: false  
    })  
  }  
}

2.5 设置页面 - 丰富的个性化选项

2.5.1 屏幕常亮功能

onKeepScreenOnChange(e) {  
  this.keepScreenOn = e.detail.value  
  uni.setStorageSync('keepScreenOn', this.keepScreenOn)  

  uni.setKeepScreenOn({  
    keepScreenOn: this.keepScreenOn  
  })  

  uni.showToast({  
    title: this.keepScreenOn ?   
      '屏幕常亮已开启' : '屏幕常亮已关闭',  
    icon: 'none'  
  })  
}

2.5.2 数据清除功能

clearData() {  
  uni.showModal({  
    title: '确认清除',  
    content: '此操作将清除所有闹钟和设置数据,无法恢复。确定要继续吗?',  
    confirmText: '清除',  
    confirmColor: '#F44336',  
    success: (res) => {  
      if (res.confirm) {  
        uni.clearStorageSync()  
        uni.showToast({  
          title: '数据已清除',  
          icon: 'success'  
        })  
        // 重启应用  
        setTimeout(() => {  
          uni.reLaunch({  
            url: '/pages/index/index'  
          })  
        }, 1500)  
      }  
    }  
  })  
}

三、UI/UX 设计与优化

3.1 Material Design 样式实现

3.1.1 卡片阴影效果

.card {  
  background-color: #2C2C2C;  
  border-radius: 8px;  
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);  
}

3.1.2 悬浮按钮(FAB)

.add-btn {  
  position: fixed;  
  right: 20px;  
  bottom: 100px;  
  width: 60px;  
  height: 60px;  
  background-color: #FF6B00;  
  border-radius: 30px;  
  display: flex;  
  justify-content: center;  
  align-items: center;  
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);  
}

3.2 颜色系统

深色主题配色方案:

元素 颜色值 用途
主色 #FF6B00 按钮、选中状态、导航栏
背景 #212121 页面背景
卡片 #2C2C2C 列表项、卡片背景
分隔 #3A3A3A 边框、分隔线
文本主 #FFFFFF 主要文本
文本次 #CCCCCC 次要文本
文本禁 #999999 禁用文本

3.3 交互优化

3.3.1 空状态设计

<view v-if="alarms.length === 0" class="empty-state">  
  <text class="empty-text">暂无闹钟</text>  
  <text class="empty-hint">点击下方 "+" 按钮添加闹钟</text>  
</view>

3.3.2 加载状态与反馈

// 操作成功提示  
uni.showToast({  
  title: '保存成功',  
  icon: 'success'  
})  

// 确认对话框  
uni.showModal({  
  title: '确认删除',  
  content: '确定要删除这个闹钟吗?',  
  success: (res) => {  
    if (res.confirm) {  
      // 执行删除  
    }  
  }  
})

四、性能优化

4.1 计时器管理

确保在页面卸载时清除定时器,避免内存泄漏:

export default {  
  onUnload() {  
    this.stopTimer()  
  },  
  methods: {  
    stopTimer() {  
      if (this.timer) {  
        clearInterval(this.timer)  
        this.timer = null  
      }  
    }  
  }  
}

4.2 数据缓存策略

  • 使用 uni.setStorageSync 同步存储关键数据
  • 页面加载时从本地读取数据,减少计算
  • 数据变更时立即保存,确保数据一致性

4.3 列表渲染优化

使用 :key 提升列表渲染性能:

<view v-for="(alarm, index) in alarms"   
      :key="alarm.id || index"   
      class="alarm-item">  
  <!-- 内容 -->  
</view>

五、鸿蒙平台深度适配

5.1 鸿蒙返回键处理

鸿蒙系统的返回键需要特殊处理,实现双击退出:

// #ifdef APP-HARMONY  
let firstBackTime = 0  

export default {  
  onLastPageBackPress: function () {  
    console.log('HarmonyOS 返回键按下')  
    if (firstBackTime == 0) {  
      uni.showToast({  
        title: '再按一次退出应用',  
        position: 'bottom',  
      })  
      firstBackTime = Date.now()  
      setTimeout(() => {  
        firstBackTime = 0  
      }, 2000)  
    } else if (Date.now() - firstBackTime < 2000) {  
      uni.exit() // 退出应用  
    }  
  }  
}  
// #endif

5.2 鸿蒙屏幕常亮

利用鸿蒙的电源管理 API 实现屏幕常亮:

// #ifdef APP-HARMONY  
import { runningLock } from '@kit.BasicServicesKit'  

let screenLock = null  

// 保持屏幕常亮  
function keepScreenOn() {  
  try {  
    screenLock = runningLock.createRunningLock('screen',   
      runningLock.RunningLockType.RUNNINGLOCK_SCREEN)  
    screenLock.lock(0) // 0 表示永久锁定  
  } catch (err) {  
    console.error('屏幕常亮失败', err)  
  }  
}  

// 释放屏幕锁  
function releaseScreenLock() {  
  if (screenLock) {  
    screenLock.unlock()  
    screenLock = null  
  }  
}  
// #endif  

// 跨平台封装  
function setKeepScreenOn(keep) {  
  // #ifdef APP-HARMONY  
  if (keep) {  
    keepScreenOn()  
  } else {  
    releaseScreenLock()  
  }  
  // #endif  

  // #ifndef APP-HARMONY  
  uni.setKeepScreenOn({ keepScreenOn: keep })  
  // #endif  
}

5.3 鸿蒙后台任务

实现闹钟的后台运行:

// #ifdef APP-HARMONY  
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager'  

// 申请长时任务  
async function requestBackgroundTask() {  
  try {  
    await backgroundTaskManager.requestSuspendDelay('时钟后台任务', () => {  
      console.log('后台任务即将被挂起')  
      // 保存状态  
    })  
  } catch (err) {  
    console.error('后台任务申请失败', err)  
  }  
}  
// #endif

5.4 鸿蒙安全区域适配

考虑鸿蒙设备的刘海屏、水滴屏和折叠屏:

.container {  
  // 鸿蒙安全区域  
  padding-top: env(safe-area-inset-top);  
  padding-bottom: env(safe-area-inset-bottom);  
  padding-left: env(safe-area-inset-left);  
  padding-right: env(safe-area-inset-right);  
}  

// 鸿蒙折叠屏适配  
@media screen and (min-width: 600px) {  
  .container {  
    max-width: 600px;  
    margin: 0 auto;  
  }  
}

5.5 鸿蒙生命周期适配

export default {  
  // 应用前台  
  onShow() {  
    // #ifdef APP-HARMONY  
    console.log('HarmonyOS 应用进入前台')  
    this.resumeTimers() // 恢复计时器  
    // #endif  
  },  

  // 应用后台  
  onHide() {  
    // #ifdef APP-HARMONY  
    console.log('HarmonyOS 应用进入后台')  
    this.pauseTimers() // 暂停计时器  
    this.saveState() // 保存状态  
    // #endif  
  }  
}

5.6 鸿蒙深色模式适配

鸿蒙系统的深色模式可以自动切换:

// #ifdef APP-HARMONY  
import { configuration } from '@kit.AbilityKit'  

// 监听系统主题变化  
function watchThemeChange() {  
  const config = configuration.getConfiguration()  
  const isDark = config.colorMode === configuration.ColorMode.COLOR_MODE_DARK  

  // 应用深色主题  
  if (isDark) {  
    this.applyDarkTheme()  
  } else {  
    this.applyLightTheme()  
  }  
}  
// #endif

六、鸿蒙应用测试与调试

6.1 鸿蒙开发者模式

在鸿蒙设备上启用开发者模式:

  1. 进入"设置" → "关于手机"
  2. 连续点击"版本号"7次
  3. 返回"设置" → "系统和更新" → "开发者选项"
  4. 开启"USB 调试"和"USB 安装"

6.2 HBuilderX 真机调试

# 1. 连接鸿蒙设备  
# 2. 在 HBuilderX 中选择运行设备  
# 3. 点击"运行" → "运行到手机或模拟器" → "运行到 HarmonyOS"  

# 查看日志  
hdc shell hilog | grep "simpleclock"

6.3 鸿蒙特有功能测试清单

  • [ ] 通知权限:闹钟通知是否正常显示
  • [ ] 振动权限:振动反馈是否生效
  • [ ] 后台任务:应用后台时闹钟是否正常工作
  • [ ] 屏幕常亮:前台运行时屏幕是否保持常亮
  • [ ] 数据持久化:应用重启后数据是否保留
  • [ ] 深色模式:系统切换主题时应用是否自动适配
  • [ ] 折叠屏适配:在折叠屏设备上布局是否正常
  • [ ] 返回键处理:双击返回是否正常退出

6.4 性能测试(鸿蒙平台)

// 使用鸿蒙性能追踪  
// #ifdef APP-HARMONY  
import hiTraceMeter from '@ohos.hiTraceMeter'  

// 开始性能追踪  
hiTraceMeter.startTrace('stopwatch_timing', 1001)  

// 执行计时逻辑  
this.updateStopwatchTime()  

// 结束追踪  
hiTraceMeter.finishTrace('stopwatch_timing', 1001)  
// #endif

6.2 边界情况处理

// 防止重复点击  
if (this.totalSeconds === 0) {  
  uni.showToast({  
    title: '请设置计时时间',  
    icon: 'none'  
  })  
  return  
}  

// 数据验证  
if (!editAlarmData.time) {  
  uni.showToast({  
    title: '请选择时间',  
    icon: 'none'  
  })  
  return  
}

七、项目总结

7.1 技术亮点

  1. 完整的功能闭环:从时间显示到提醒管理,覆盖用户全部需求
  2. 优雅的代码结构:模块化设计,职责清晰
  3. 良好的用户体验:Material Design + 深色主题
  4. 高性能实现:合理使用定时器,优化渲染性能
  5. 跨平台兼容:一套代码,多端运行

7.2 可扩展方向

  1. 云同步功能:支持多设备数据同步
  2. 更多铃声:支持自定义铃声上传
  3. 统计分析:记录使用习惯,提供数据分析
  4. 智能提醒:根据用户习惯智能推荐闹钟时间
  5. 主题定制:支持更多颜色主题

7.3 开发心得

  1. 用户体验至上:每一个交互细节都要仔细打磨
  2. 代码质量:保持代码简洁,注重可维护性
  3. 性能优化:及时清理资源,避免内存泄漏
  4. 测试驱动:充分测试各种边界情况
  5. 持续迭代:根据用户反馈不断优化改进

八、源码与资源

8.1 项目结构

simpleclock/  
├── pages/  
│   ├── index/index.uvue      # 264 行  
│   ├── alarm/alarm.uvue      # 430 行  
│   ├── stopwatch/stopwatch.uvue  # 280 行  
│   ├── timer/timer.uvue      # 300 行  
│   └── settings/settings.uvue    # 350 行  
├── App.uvue                  # 124 行  
├── pages.json                # 76 行  
├── uni.scss                  # 77 行  
├── README.md                 # 项目说明  
└── 使用指南.md               # 用户手册

8.2 代码统计

  • 总代码量:约 1900+ 行
  • 页面数:5 个主要页面
  • 组件数:多个自定义组件
  • 功能模块:4 个核心功能

8.3 运行环境

  • 开发工具:HBuilderX 4.0+
  • uni-app x 版本:最新版
  • 鸿蒙系统:HarmonyOS 6
  • 鸿蒙 SDK:API Level 10+
  • 测试设备:
    • 华为 Mate 60 系列(HarmonyOS 6)
    • 华为 MatePad Pro(折叠屏测试)
    • 鸿蒙模拟器

8.4 鸿蒙应用发布

8.4.1 应用签名

# 在华为开发者联盟申请应用签名  
# 下载签名文件并配置到 manifest.json  

{  
  "uni-app-x": {  
    "harmony": {  
      "signing": {  
        "profile": "path/to/profile.p7b",  
        "certFile": "path/to/cert.pem",  
        "keyFile": "path/to/key.p12",  
        "keyPassword": "your_password"  
      }  
    }  
  }  
}

8.4.2 打包发布

# HBuilderX 中操作:  
# 1. 发行 → 原生App-云打包  
# 2. 选择 HarmonyOS 平台  
# 3. 填写应用信息和签名配置  
# 4. 点击打包,等待云端编译  
# 5. 下载 .hap 安装包  

# 命令行打包(可选)  
npm run build:app-harmony

8.4.3 应用上架

  1. 登录华为开发者联盟
  2. 进入"应用服务" → "AppGallery Connect"
  3. 创建应用并上传 .hap 包
  4. 填写应用信息(截图、描述、分类)
  5. 提交审核
  6. 审核通过后上架

九、鸿蒙开发常见问题

9.1 权限被拒绝

问题:应用无法发送通知或振动

解决

// 在应用启动时主动请求权限  
// #ifdef APP-HARMONY  
async function checkAndRequestPermissions() {  
  const permissions = [  
    'ohos.permission.NOTIFICATION_CONTROLLER',  
    'ohos.permission.VIBRATE'  
  ]  

  for (let permission of permissions) {  
    const result = await checkPermission(permission)  
    if (!result) {  
      await requestPermission(permission)  
    }  
  }  
}  
// #endif

9.2 后台任务被终止

问题:闹钟在后台不响

解决:申请长时任务授权,确保应用在后台持续运行

9.3 数据丢失

问题:应用卸载后数据丢失

解决:使用鸿蒙云空间同步数据(需要用户登录华为账号)

9.4 性能问题

问题:页面切换卡顿

解决

  • 使用虚拟列表优化长列表渲染
  • 避免在主线程执行耗时操作
  • 使用鸿蒙的 Worker 线程处理复杂计算

结语

本文详细介绍了如何使用 uni-app x 开发一款功能完整的鸿蒙6原生时钟应用,从环境搭建、核心功能实现、鸿蒙6平台深度适配到应用发布,涵盖了鸿蒙6应用开发的各个方面。

🎉 这是一个真实的鸿蒙6原生应用开发案例!

核心要点回顾

  1. uni-app x 的优势

    • ✅ 编译为鸿蒙原生代码,性能卓越
    • ✅ 一套代码多端运行,大幅降低开发成本
    • ✅ 丰富的 API 支持,无缝对接鸿蒙能力
  2. 鸿蒙平台特性

    • ✅ 强大的通知系统
    • ✅ 完善的权限管理
    • ✅ 优秀的后台任务机制
    • ✅ 深度的系统集成
  3. 开发建议

    • 💡 充分利用条件编译,针对鸿蒙做优化
    • 💡 注意权限申请和用户体验
    • 💡 重视性能优化和内存管理
    • 💡 遵循鸿蒙设计规范

希望这篇文章能帮助你:

  • 🎯 零基础入门:从环境搭建到应用发布的完整流程
  • 🎨 深度理解鸿蒙6:掌握鸿蒙6的核心特性和设计理念
  • 💡 实战经验:学习时间管理类应用的最佳实践
  • 🚀 快速上手:30分钟开发出第一个鸿蒙6应用
  • 🔥 抓住机遇:在鸿蒙生态爆发期占据先机

下一步计划

  • [ ] 适配鸿蒙折叠屏设备
  • [ ] 接入华为账号系统
  • [ ] 实现华为云空间数据同步
  • [ ] 添加鸿蒙小部件(Widget)
  • [ ] 接入鸿蒙分布式能力

如果你有任何问题或建议,欢迎交流讨论!让我们一起为鸿蒙生态贡献力量!🚀


关键词:#鸿蒙6 #HarmonyOS6 #uni-app-x #纯血鸿蒙 #原生应用开发 #Vue3 #跨平台开发 #时钟应用

作者:坚果

日期:2025年10月28日
项目地址GitCode - simpleclock
开源协议:MIT License


📢 开源说明

本项目完全开源,欢迎:

  • ⭐ Star 项目,关注后续更新
  • 🍴 Fork 项目,二次开发
  • 🐛 提交 Issue,反馈问题
  • 🤝 提交 PR,贡献代码

当前状态

  • ✅ 核心功能已完成
  • ⚠️ 部分细节待优化(如时区同步、铃声播放等)
  • 🎯 持续迭代中,欢迎贡献代码!

🔗 相关资源

💬 交流讨论

如果你在开发过程中遇到问题,或有好的想法和建议:

让我们一起推动鸿蒙生态发展! 🚀🇨🇳

收起阅读 »

【鸿蒙征文】使用 uni-app 开发鸿蒙 App,如何实现 H5 网页和 App 互通讯?

鸿蒙征文

导语:打通 Web 与 Native 的“任督二脉”

在使用 uni-app 跨端开发时,我们经常需要在 App 内嵌一个 H5 页面(比如活动页、复杂表单页等)。

在鸿蒙(HarmonyOS)应用中,要让这个内嵌的 Web 页面App 的原生能力(Native)进行顺畅的沟通,就需要一个桥梁

本文将基于 UTS 插件,在鸿蒙平台上搭建起 H5 和 App 之间的双向通信机制,实现数据的传递和方法的互调。

🎯 核心目标:实现双向通信

我们最终要实现的效果是:

  1. App -> H5: App 侧可以调用 H5 页面中的 JavaScript 方法,比如向 H5 传递用户 ID 或登录 Token。
  2. H5 -> App: H5 页面可以调用 App 侧提供的原生方法,比如调用相册、获取地理位置,或本例中的“App 弹出提示”。

一、 App 侧:创建 UTS 插件并封装原生能力

在鸿蒙平台上,我们需要通过 UTS 插件来提供 App 侧供 H5 调用的原生 API。

1. 创建 UTS 插件结构

  • 在 uni-app 项目的 uni_modules 目录下,新建一个 UTS 插件,命名为 web-bridge-ohos
  • 确认插件目录结构如下:
    uni_modules/web-bridge-ohos/  
    ├── app-harmony/  
    │   └── index.uts   
    ├── package.json  
    └── interface.uts  

2. 编写 UTS 原生代码 (index.uts)

我们要实现一个核心类,这个类将作为 H5 和 App 通信的桥梁。在鸿蒙中,内嵌网页控件是 WebComponent。我们需要为它设置一个 WebMessagePort 监听器。

在本例中,我们创建一个名为 WebInteractionModule 的类,它负责处理 H5 发送过来的消息。

// 文件路径:uni_modules/web-bridge-ohos/app-harmony/index.uts  

import web_webview from '@ohos.web.webview';  
import hilog from '@ohos.hilog';  
import UIAbility from '@ohos.app.ability.UIAbility';  

const LOG_TAG: string = 'WebBridge';  

/**  
 * 【核心类】Web 交互模块  
 * 负责在鸿蒙平台上注入 Web 消息处理器  
 */  
export class WebInteractionModule {  
  private webViewComponent: web_webview.WebComponent | null = null;  

  /**  
   * 构造函数:初始化时可传入 WebComponent 实例  
   * @param webComp WebComponent 实例  
   */  
  constructor(webComp: web_webview.WebComponent) {  
    this.webViewComponent = webComp;  
    hilog.info(0x0001, LOG_TAG, `WebInteractionModule 初始化成功.`);  
  }  

  /**  
   * 步骤 1: 启用 H5 与 App 互通能力,设置消息监听器。  
   * @param bridgeName 桥接对象名称,H5 侧将使用此名称调用 App 方法。  
   * @param appHandler App 侧处理 H5 消息的回调函数。  
   */  
  public setupBridge(bridgeName: string, appHandler: (message: string) => void): void {  
    if (!this.webViewComponent) return;  

    // 1. 设置 Web 消息处理回调  
    this.webViewComponent.onWebMessage = (data: string) => {  
      hilog.info(0x0001, LOG_TAG, `收到 H5 消息: ${data}`);  
      // 2. 将收到的消息交给 App 业务层处理  
      appHandler(data);  
    };  

    // 3. 注入鸿蒙内置的消息通道对象(关键步骤)  
    // H5 侧将通过 window.[bridgeName].postMessage('data') 来调用  
    this.webViewComponent.injectWebMessagePort(bridgeName);  

    hilog.info(0x0001, LOG_TAG, `已注入 Web 桥接对象: ${bridgeName}`);  
  }  

  /**  
   * 步骤 2: 从 App 侧调用 H5 页面内的 JavaScript 方法。  
   * @param jsCode 要执行的 JavaScript 代码(如:`window.myH5Method('Hello from App')`)。  
   */  
  public callH5Function(jsCode: string): void {  
    if (this.webViewComponent) {  
      this.webViewComponent.executeJs(jsCode, (result) => {  
        hilog.info(0x0001, LOG_TAG, `执行 H5 JS 代码结果: ${result}`);  
      });  
    }  
  }  
}  

// =======================================================  
// 为了方便 uni-app 调用,我们提供一个 UTS 导出的工厂函数  
// =======================================================  

/**  
 * 导出工厂函数:创建并返回 WebInteractionModule 实例  
 * @param webComp H5 视图的 WebComponent 实例  
 * @returns WebInteractionModule 实例  
 */  
export function createWebBridge(webComp: web_webview.WebComponent): WebInteractionModule {  
  return new WebInteractionModule(webComp);  
}

3. UTS 接口定义 (interface.uts)

为了让上层 Vue/JS 代码能顺利使用这个类,我们需要定义接口:

// 文件路径:uni_modules/web-bridge-ohos/interface.uts  

import web_webview from '@ohos.web.webview';  

/**  
 * WebInteractionModule 类的接口定义  
 * 供 uni-app TypeScript/JavaScript 代码使用  
 */  
export interface WebInteractionModule {  
  /**  
   * 启动 App 和 H5 之间的通信桥接。  
   * @param bridgeName H5 侧调用的对象名 (如:'HarmonyAppBridge')  
   * @param appHandler App 侧接收 H5 消息的回调函数。  
   */  
  setupBridge(bridgeName: string, appHandler: (message: string) => void): void;  

  /**  
   * 从 App 侧调用 H5 页面中的 JavaScript 代码。  
   * @param jsCode 要在 H5 中执行的 JS 字符串。  
   */  
  callH5Function(jsCode: string): void;  
}  

/**  
 * UTS 导出函数接口:创建 Web 桥接模块实例  
 * @param webComp WebComponent 实例  
 * @returns WebInteractionModule 实例  
 */  
export type CreateWebBridge = (webComp: web_webview.WebComponent) => WebInteractionModule;

二、 App 侧:在 uni-app 页面中集成 WebComponent

在 uni-app 页面中,我们需要使用自定义组件的方式,引入鸿蒙平台专有的 web-view 组件,并调用 UTS 插件。

1. 页面逻辑 (pages/index/index.vue)

我们创建一个页面,其中包含一个内嵌的 web-view 组件,并实现 H5 消息处理逻辑。

<template>  
  <view class="container">  
    <text class="title">鸿蒙 Web-Native 互通演示</text>  

    <button @click="sendDataToH5">App 发送数据给 H5</button>  

    <view class="message-box">  
      <text>收到 H5 消息: {{ lastH5Message }}</text>  
    </view>  

    <web-view   
      ref="h5WebView"   
      class="h5-content"   
      :src="h5Url"   
      @onHarmonyWebComponentCreated="onWebCreated"   
    ></web-view>  
  </view>  
</template>  

<script lang="uts">  
  import { WebInteractionModule, createWebBridge } from '@/uni_modules/web-bridge-ohos';  
  import web_webview from '@ohos.web.webview';  

  // 【注意】请替换成你实际 H5 文件的网络地址或本地路径  
  const H5_PAGE_URL: string = 'https://yourdomain.com/path/to/h5/index.html';   
  const BRIDGE_OBJECT_NAME: string = 'HarmonyAppBridge'; // 供 H5 调用的对象名  

  export default {  
    data() {  
      return {  
        h5Url: H5_PAGE_URL,  
        lastH5Message: '暂无消息',  
        webBridge: null as WebInteractionModule | null, // 存储 UTS 桥接模块实例  
        sendCount: 0,  
      }  
    },  
    methods: {  
      /**  
       * 鸿蒙平台 WebComponent 创建成功后触发的事件  
       * @param e 事件对象,包含了 WebComponent 的实例  
       */  
      onWebCreated(e: { component: web_webview.WebComponent }) {  
        console.log('WebComponent 实例已创建');  
        const webComponent = e.component;  

        // 1. 创建 UTS 桥接模块实例  
        this.webBridge = createWebBridge(webComponent);  

        // 2. 设置桥接并注册 App 侧的消息处理器  
        this.webBridge.setupBridge(BRIDGE_OBJECT_NAME, this.handleMessageFromH5);  
      },  

      /**  
       * App 侧处理 H5 页面发送过来的消息  
       * @param message H5 传递过来的 JSON 字符串  
       */  
      handleMessageFromH5(message: string): void {  
        console.log(`App 收到 H5 消息: ${message}`);  
        this.lastH5Message = message;  

        // 示例:App 收到 H5 消息后,可以调用 App 的原生能力,比如 uni.showToast  
        uni.showToast({  
          title: `H5 说: ${message}`,  
          icon: 'none',  
        });  
      },  

      /**  
       * App 侧主动调用 H5 页面中的 JavaScript 方法  
       */  
      sendDataToH5(): void {  
        if (!this.webBridge) {  
          console.error('Web 桥接尚未初始化!');  
          uni.showToast({ title: '桥接未就绪', icon: 'error' });  
          return;  
        }  

        this.sendCount++;  
        const dataToSend = `{"action":"updateUser","userId":1000${this.sendCount}}`;  

        // 构造要执行的 H5 JS 代码  
        // 假设 H5 页面中有一个全局函数名为 `receiveAppMessage`  
        const jsCode: string = `window.receiveAppMessage('${dataToSend}')`;  

        // 调用 UTS 插件方法执行 JS  
        this.webBridge.callH5Function(jsCode);  
      }  
    }  
  }  
</script>  

<style>  
  .container {  
    padding: 20px;  
  }  
  .title {  
    font-size: 18px;  
    font-weight: bold;  
    margin-bottom: 20px;  
    display: block;  
  }  
  .message-box {  
    margin: 15px 0;  
    padding: 10px;  
    border: 1px solid #ddd;  
    background-color: #f0f0f0;  
  }  
  .h5-content {  
    /* 确保 web-view 有足够的高度显示 */  
    width: 100%;  
    height: 300px;   
    margin-top: 20px;  
    border: 1px solid #ccc;  
  }  
</style>

三、 H5 侧:实现双向通信逻辑

H5 页面需要实现两套机制:一套用于接收 App 消息,一套用于发送消息给 App。

1. H5 接收 App 消息的函数

App 侧会调用我们在 H5 中定义的全局函数 window.receiveAppMessage

<script>  
  // 【接收】App -> H5  
  window.receiveAppMessage = function(dataStr) {  
    console.log('H5 收到 App 消息:', dataStr);  
    try {  
      const data = JSON.parse(dataStr);  
      document.getElementById('app-msg').innerText = '最新 App 消息: ' + data.action + ',用户ID:' + data.userId;  
    } catch (e) {  
      document.getElementById('app-msg').innerText = '收到非法消息格式: ' + dataStr;  
    }  
  };  
</script>

2. H5 发送消息给 App 的方法

H5 通过调用 App 侧注入的桥接对象(我们命名为 HarmonyAppBridge)上的 postMessage 方法来发送消息。

<script>  
  // 【发送】H5 -> App  
  function sendToApp(message) {  
    // 检查 App 侧注入的桥接对象是否存在  
    if (window.HarmonyAppBridge && typeof window.HarmonyAppBridge.postMessage === 'function') {  
      window.HarmonyAppBridge.postMessage(message);  
      document.getElementById('h5-status').innerText = '成功发送消息给 App: ' + message;  
    } else {  
      document.getElementById('h5-status').innerText = 'App 桥接对象 (HarmonyAppBridge) 不存在或未就绪!';  
      console.error('App 桥接对象不存在,无法发送消息。');  
    }  
  }  

  // 按钮点击事件示例  
  document.getElementById('send-btn').onclick = function() {  
    const time = new Date().toLocaleTimeString();  
    const message = `{"event":"h5Ready","time":"${time}"}`;  
    sendToApp(message);  
  };  
</script>  

<div>  
    <h1>H5 页面内容</h1>  
    <p id="app-msg">最新 App 消息: 暂无</p>  
    <button id="send-btn">H5 调用 App 弹窗</button>  
    <p id="h5-status"></p>  
</div>

总结

通过上述三个部分的协作,我们成功地在 uni-app 鸿蒙应用中搭建了 H5 页面和 App 之间的通信桥梁:

  1. App 侧 (UTS): 使用 WebInteractionModule 类和 WebComponent 的原生能力,通过 injectWebMessagePort 注入发送通道,通过 onWebMessage 接收 H5 消息。
  2. App 侧 (Vue): 使用 <web-view> 组件,并通过 onHarmonyWebComponentCreated 事件获取 WebComponent 实例,将其实例传递给 UTS 插件进行初始化和监听。
  3. H5 侧 (JS): 通过调用全局注入的 window.HarmonyAppBridge.postMessage() 发送消息给 App,并通过全局函数 window.receiveAppMessage() 接收 App 的指令。

全文完,欢迎点赞、收藏、转发!

继续阅读 »

导语:打通 Web 与 Native 的“任督二脉”

在使用 uni-app 跨端开发时,我们经常需要在 App 内嵌一个 H5 页面(比如活动页、复杂表单页等)。

在鸿蒙(HarmonyOS)应用中,要让这个内嵌的 Web 页面App 的原生能力(Native)进行顺畅的沟通,就需要一个桥梁

本文将基于 UTS 插件,在鸿蒙平台上搭建起 H5 和 App 之间的双向通信机制,实现数据的传递和方法的互调。

🎯 核心目标:实现双向通信

我们最终要实现的效果是:

  1. App -> H5: App 侧可以调用 H5 页面中的 JavaScript 方法,比如向 H5 传递用户 ID 或登录 Token。
  2. H5 -> App: H5 页面可以调用 App 侧提供的原生方法,比如调用相册、获取地理位置,或本例中的“App 弹出提示”。

一、 App 侧:创建 UTS 插件并封装原生能力

在鸿蒙平台上,我们需要通过 UTS 插件来提供 App 侧供 H5 调用的原生 API。

1. 创建 UTS 插件结构

  • 在 uni-app 项目的 uni_modules 目录下,新建一个 UTS 插件,命名为 web-bridge-ohos
  • 确认插件目录结构如下:
    uni_modules/web-bridge-ohos/  
    ├── app-harmony/  
    │   └── index.uts   
    ├── package.json  
    └── interface.uts  

2. 编写 UTS 原生代码 (index.uts)

我们要实现一个核心类,这个类将作为 H5 和 App 通信的桥梁。在鸿蒙中,内嵌网页控件是 WebComponent。我们需要为它设置一个 WebMessagePort 监听器。

在本例中,我们创建一个名为 WebInteractionModule 的类,它负责处理 H5 发送过来的消息。

// 文件路径:uni_modules/web-bridge-ohos/app-harmony/index.uts  

import web_webview from '@ohos.web.webview';  
import hilog from '@ohos.hilog';  
import UIAbility from '@ohos.app.ability.UIAbility';  

const LOG_TAG: string = 'WebBridge';  

/**  
 * 【核心类】Web 交互模块  
 * 负责在鸿蒙平台上注入 Web 消息处理器  
 */  
export class WebInteractionModule {  
  private webViewComponent: web_webview.WebComponent | null = null;  

  /**  
   * 构造函数:初始化时可传入 WebComponent 实例  
   * @param webComp WebComponent 实例  
   */  
  constructor(webComp: web_webview.WebComponent) {  
    this.webViewComponent = webComp;  
    hilog.info(0x0001, LOG_TAG, `WebInteractionModule 初始化成功.`);  
  }  

  /**  
   * 步骤 1: 启用 H5 与 App 互通能力,设置消息监听器。  
   * @param bridgeName 桥接对象名称,H5 侧将使用此名称调用 App 方法。  
   * @param appHandler App 侧处理 H5 消息的回调函数。  
   */  
  public setupBridge(bridgeName: string, appHandler: (message: string) => void): void {  
    if (!this.webViewComponent) return;  

    // 1. 设置 Web 消息处理回调  
    this.webViewComponent.onWebMessage = (data: string) => {  
      hilog.info(0x0001, LOG_TAG, `收到 H5 消息: ${data}`);  
      // 2. 将收到的消息交给 App 业务层处理  
      appHandler(data);  
    };  

    // 3. 注入鸿蒙内置的消息通道对象(关键步骤)  
    // H5 侧将通过 window.[bridgeName].postMessage('data') 来调用  
    this.webViewComponent.injectWebMessagePort(bridgeName);  

    hilog.info(0x0001, LOG_TAG, `已注入 Web 桥接对象: ${bridgeName}`);  
  }  

  /**  
   * 步骤 2: 从 App 侧调用 H5 页面内的 JavaScript 方法。  
   * @param jsCode 要执行的 JavaScript 代码(如:`window.myH5Method('Hello from App')`)。  
   */  
  public callH5Function(jsCode: string): void {  
    if (this.webViewComponent) {  
      this.webViewComponent.executeJs(jsCode, (result) => {  
        hilog.info(0x0001, LOG_TAG, `执行 H5 JS 代码结果: ${result}`);  
      });  
    }  
  }  
}  

// =======================================================  
// 为了方便 uni-app 调用,我们提供一个 UTS 导出的工厂函数  
// =======================================================  

/**  
 * 导出工厂函数:创建并返回 WebInteractionModule 实例  
 * @param webComp H5 视图的 WebComponent 实例  
 * @returns WebInteractionModule 实例  
 */  
export function createWebBridge(webComp: web_webview.WebComponent): WebInteractionModule {  
  return new WebInteractionModule(webComp);  
}

3. UTS 接口定义 (interface.uts)

为了让上层 Vue/JS 代码能顺利使用这个类,我们需要定义接口:

// 文件路径:uni_modules/web-bridge-ohos/interface.uts  

import web_webview from '@ohos.web.webview';  

/**  
 * WebInteractionModule 类的接口定义  
 * 供 uni-app TypeScript/JavaScript 代码使用  
 */  
export interface WebInteractionModule {  
  /**  
   * 启动 App 和 H5 之间的通信桥接。  
   * @param bridgeName H5 侧调用的对象名 (如:'HarmonyAppBridge')  
   * @param appHandler App 侧接收 H5 消息的回调函数。  
   */  
  setupBridge(bridgeName: string, appHandler: (message: string) => void): void;  

  /**  
   * 从 App 侧调用 H5 页面中的 JavaScript 代码。  
   * @param jsCode 要在 H5 中执行的 JS 字符串。  
   */  
  callH5Function(jsCode: string): void;  
}  

/**  
 * UTS 导出函数接口:创建 Web 桥接模块实例  
 * @param webComp WebComponent 实例  
 * @returns WebInteractionModule 实例  
 */  
export type CreateWebBridge = (webComp: web_webview.WebComponent) => WebInteractionModule;

二、 App 侧:在 uni-app 页面中集成 WebComponent

在 uni-app 页面中,我们需要使用自定义组件的方式,引入鸿蒙平台专有的 web-view 组件,并调用 UTS 插件。

1. 页面逻辑 (pages/index/index.vue)

我们创建一个页面,其中包含一个内嵌的 web-view 组件,并实现 H5 消息处理逻辑。

<template>  
  <view class="container">  
    <text class="title">鸿蒙 Web-Native 互通演示</text>  

    <button @click="sendDataToH5">App 发送数据给 H5</button>  

    <view class="message-box">  
      <text>收到 H5 消息: {{ lastH5Message }}</text>  
    </view>  

    <web-view   
      ref="h5WebView"   
      class="h5-content"   
      :src="h5Url"   
      @onHarmonyWebComponentCreated="onWebCreated"   
    ></web-view>  
  </view>  
</template>  

<script lang="uts">  
  import { WebInteractionModule, createWebBridge } from '@/uni_modules/web-bridge-ohos';  
  import web_webview from '@ohos.web.webview';  

  // 【注意】请替换成你实际 H5 文件的网络地址或本地路径  
  const H5_PAGE_URL: string = 'https://yourdomain.com/path/to/h5/index.html';   
  const BRIDGE_OBJECT_NAME: string = 'HarmonyAppBridge'; // 供 H5 调用的对象名  

  export default {  
    data() {  
      return {  
        h5Url: H5_PAGE_URL,  
        lastH5Message: '暂无消息',  
        webBridge: null as WebInteractionModule | null, // 存储 UTS 桥接模块实例  
        sendCount: 0,  
      }  
    },  
    methods: {  
      /**  
       * 鸿蒙平台 WebComponent 创建成功后触发的事件  
       * @param e 事件对象,包含了 WebComponent 的实例  
       */  
      onWebCreated(e: { component: web_webview.WebComponent }) {  
        console.log('WebComponent 实例已创建');  
        const webComponent = e.component;  

        // 1. 创建 UTS 桥接模块实例  
        this.webBridge = createWebBridge(webComponent);  

        // 2. 设置桥接并注册 App 侧的消息处理器  
        this.webBridge.setupBridge(BRIDGE_OBJECT_NAME, this.handleMessageFromH5);  
      },  

      /**  
       * App 侧处理 H5 页面发送过来的消息  
       * @param message H5 传递过来的 JSON 字符串  
       */  
      handleMessageFromH5(message: string): void {  
        console.log(`App 收到 H5 消息: ${message}`);  
        this.lastH5Message = message;  

        // 示例:App 收到 H5 消息后,可以调用 App 的原生能力,比如 uni.showToast  
        uni.showToast({  
          title: `H5 说: ${message}`,  
          icon: 'none',  
        });  
      },  

      /**  
       * App 侧主动调用 H5 页面中的 JavaScript 方法  
       */  
      sendDataToH5(): void {  
        if (!this.webBridge) {  
          console.error('Web 桥接尚未初始化!');  
          uni.showToast({ title: '桥接未就绪', icon: 'error' });  
          return;  
        }  

        this.sendCount++;  
        const dataToSend = `{"action":"updateUser","userId":1000${this.sendCount}}`;  

        // 构造要执行的 H5 JS 代码  
        // 假设 H5 页面中有一个全局函数名为 `receiveAppMessage`  
        const jsCode: string = `window.receiveAppMessage('${dataToSend}')`;  

        // 调用 UTS 插件方法执行 JS  
        this.webBridge.callH5Function(jsCode);  
      }  
    }  
  }  
</script>  

<style>  
  .container {  
    padding: 20px;  
  }  
  .title {  
    font-size: 18px;  
    font-weight: bold;  
    margin-bottom: 20px;  
    display: block;  
  }  
  .message-box {  
    margin: 15px 0;  
    padding: 10px;  
    border: 1px solid #ddd;  
    background-color: #f0f0f0;  
  }  
  .h5-content {  
    /* 确保 web-view 有足够的高度显示 */  
    width: 100%;  
    height: 300px;   
    margin-top: 20px;  
    border: 1px solid #ccc;  
  }  
</style>

三、 H5 侧:实现双向通信逻辑

H5 页面需要实现两套机制:一套用于接收 App 消息,一套用于发送消息给 App。

1. H5 接收 App 消息的函数

App 侧会调用我们在 H5 中定义的全局函数 window.receiveAppMessage

<script>  
  // 【接收】App -> H5  
  window.receiveAppMessage = function(dataStr) {  
    console.log('H5 收到 App 消息:', dataStr);  
    try {  
      const data = JSON.parse(dataStr);  
      document.getElementById('app-msg').innerText = '最新 App 消息: ' + data.action + ',用户ID:' + data.userId;  
    } catch (e) {  
      document.getElementById('app-msg').innerText = '收到非法消息格式: ' + dataStr;  
    }  
  };  
</script>

2. H5 发送消息给 App 的方法

H5 通过调用 App 侧注入的桥接对象(我们命名为 HarmonyAppBridge)上的 postMessage 方法来发送消息。

<script>  
  // 【发送】H5 -> App  
  function sendToApp(message) {  
    // 检查 App 侧注入的桥接对象是否存在  
    if (window.HarmonyAppBridge && typeof window.HarmonyAppBridge.postMessage === 'function') {  
      window.HarmonyAppBridge.postMessage(message);  
      document.getElementById('h5-status').innerText = '成功发送消息给 App: ' + message;  
    } else {  
      document.getElementById('h5-status').innerText = 'App 桥接对象 (HarmonyAppBridge) 不存在或未就绪!';  
      console.error('App 桥接对象不存在,无法发送消息。');  
    }  
  }  

  // 按钮点击事件示例  
  document.getElementById('send-btn').onclick = function() {  
    const time = new Date().toLocaleTimeString();  
    const message = `{"event":"h5Ready","time":"${time}"}`;  
    sendToApp(message);  
  };  
</script>  

<div>  
    <h1>H5 页面内容</h1>  
    <p id="app-msg">最新 App 消息: 暂无</p>  
    <button id="send-btn">H5 调用 App 弹窗</button>  
    <p id="h5-status"></p>  
</div>

总结

通过上述三个部分的协作,我们成功地在 uni-app 鸿蒙应用中搭建了 H5 页面和 App 之间的通信桥梁:

  1. App 侧 (UTS): 使用 WebInteractionModule 类和 WebComponent 的原生能力,通过 injectWebMessagePort 注入发送通道,通过 onWebMessage 接收 H5 消息。
  2. App 侧 (Vue): 使用 <web-view> 组件,并通过 onHarmonyWebComponentCreated 事件获取 WebComponent 实例,将其实例传递给 UTS 插件进行初始化和监听。
  3. H5 侧 (JS): 通过调用全局注入的 window.HarmonyAppBridge.postMessage() 发送消息给 App,并通过全局函数 window.receiveAppMessage() 接收 App 的指令。

全文完,欢迎点赞、收藏、转发!

收起阅读 »

【鸿蒙征文】使用 UTS 插件优雅实现“一键退出应用”功能

鸿蒙征文

随着 uni-app 对鸿蒙开发的支持日益完善,我们现在可以轻松地使用一套代码同时发布到多个平台。不过,在开发鸿蒙(HarmonyOS)应用时,有些平台特有的功能需要我们通过 UTS 插件来调用原生能力。

本文将手把手教你如何创建一个 UTS 插件,并利用鸿蒙系统提供的原生接口,为你的 uni-app 应用添加一个“一键安全退出”的功能。

一、 UTS 插件的创建与命名

首先,我们需要在 uni-app 项目中创建一个专用于封装鸿蒙原生 API 的 UTS 插件。

  1. 创建插件: 在 HBuilderX 中,找到你的项目目录下的 uni_modules 文件夹。
  2. 右键点击 uni_modules -> 新建插件
  3. 命名插件: 为了与退出应用功能相关联,我们将插件命名为 ohos-exit-helper
  4. 选择类型: 选择插件类型为 uts

创建完成后,你的项目结构会多出一个 uni_modules/ohos-exit-helper 目录。

二、 插件配置:在 uni 对象上注册新方法

接下来,我们需要修改插件的配置文件,告诉 uni-app 框架:我们要在全局的 uni 对象上注册一个名为 exitCurrentApp 的新方法。

打开 uni_modules/ohos-exit-helper/package.json 文件,找到 uni_modules 字段,并添加如下配置:

// 文件路径:uni_modules/ohos-exit-helper/package.json  

{  
  // ... 其他配置项保持不变 ...  
  "uni_modules": {  
    "uni-ext-api": {  
      "uni": {  
        "exitCurrentApp": "exitCurrentApp"   
      }  
    }  
  }  
}

三、 接口定义:确保类型安全(interface.uts)

为了在开发时获得更好的代码提示和类型检查,我们需要在 UTS 接口文件中定义新方法的签名。

打开 uni_modules/ohos-exit-helper/interface.uts 文件,扩展 Uni 接口,并声明我们的同步方法 exitCurrentApp()

// 文件路径:uni_modules/ohos-exit-helper/interface.uts  

// ... (省略文件开头其他类型定义) ...  

/**  
 * 扩展全局 uni 接口,添加鸿蒙原生方法  
 */  
interface Uni {  
  /**  
   * 【鸿蒙专属】安全退出当前应用。  
   * * @example  
   * ```typescript  
   * uni.exitCurrentApp()  
   * ```  
   * @remark  
   * - 该接口需同步调用  
   * @uniPlatform { "harmony": { "osVer": "3.0" } }  
   */  
  exitCurrentApp(): void, // 【关键重构】方法签名定义  
}

四、 鸿蒙原生实现:UTS 代码编写(index.uts)

这是实现退出功能的核心步骤。我们需要在插件目录下创建鸿蒙平台专用的实现文件,并利用鸿蒙的 Ability Context 来执行退出操作。

1. 确认路径: 确保你在插件目录下创建了 app-harmony 文件夹和 index.uts 文件:
uni_modules/ohos-exit-helper/app-harmony/index.uts

2. 编写代码: 写入以下代码。我们引入了 @ohos.app.ability.common 模块,并使用 context.terminateSelf() 这一原生方法来实现退出。


// 文件路径:uni_modules/ohos-exit-helper/app-harmony/index.uts  

// 引入鸿蒙 Ability 相关的公共模块  
import AbilityCommon from '@ohos.app.ability.common';   
import hiLog from '@ohos.hilog'; // 引入日志模块,便于调试  

/**  
 * 导出【鸿蒙实现】退出当前应用的功能  
 * * @returns {void}  
 */  
export function exitCurrentApp(): void { // 【关键重构】导出函数名  
  // 1. 获取当前 UIAbility 的上下文对象  
  // getContext() 是 uni-app UTS 提供的全局函数,用于获取原生上下文  
  const appAbilityContext = getContext() as AbilityCommon.UIAbilityContext;  

  // 2. 调用 terminateSelf() 方法来终止 Ability  
  appAbilityContext.terminateSelf();  

  // 3. 打印日志(重构日志变量名和内容)  
  const exitLogTag: string = "APP_CONTROL_LOG";  
  const statusMessage: string = "Application is shutting down gracefully via UTS plugin.";  

  hiLog.info(0x0001, exitLogTag, statusMessage);  
}

五、 在 uni-app 页面中调用插件

至此,我们的鸿蒙退出插件已经完成。最后一步,是在你的 uni-app 页面中引入并使用它。

你可以在任何 .vue.uvue`` 文件的<script lang="uts">` 块中调用此功能。

<template>  
  <view class="page-container">  
    <image class="app-logo" src="/static/logo.png"></image>  
    <text class="instruction-text">点击下方按钮,使用 UTS 实现鸿蒙应用退出。</text>  
    <view class="button-area">  
      <button class="exit-button" @click="handleAppExit">安全退出应用</button>  
    </view>  
  </view>  
</template>  

<script lang="uts">  

  import { exitCurrentApp } from "@/uni_modules/ohos-exit-helper"   

  export default {  
    data() {  
      return {  
        pageTitle: 'UTS退出功能演示',   
      }  
    },  
    methods: {  
      /**  
       * 处理点击退出按钮的逻辑方法  
       */  
      handleAppExit() { //   
        console.log("准备调用鸿蒙原生退出功能...");  
        // 调用我们插件中导出的退出函数  
        exitCurrentApp();   
      }  
    }  
  }  
</script>  

<style>  
  .page-container {  
    display: flex;  
    flex-direction: column;  
    align-items: center;  
    justify-content: center;  
    padding-top: 150px;  
    background-color: #f7f7f7;  
  }  
  .app-logo {  
    height: 180rpx;  
    width: 180rpx;  
    margin-bottom: 60rpx;  
  }  
  .instruction-text {  
    font-size: 34rpx;  
    color: #333333;  
    margin-bottom: 40rpx;  
  }  
  .exit-button {  
    width: 70%;  
    height: 90rpx;  
    line-height: 90rpx;  
    background-color: #007aff; /* 鸿蒙常用主色调 */  
    color: white;  
    font-size: 36rpx;  
    border-radius: 12rpx;  
    border: none;  
  }  
</style>

六、 运行与测试

现在,你可以运行你的 uni-app 项目到鸿蒙模拟器或真机上。

点击页面上的“安全退出应用”按钮,如果应用顺利关闭,那么恭喜你,你的第一个 UTS 鸿蒙插件就开发成功了!

通过这个示例,我们不仅学会了如何实现退出功能,更掌握了 uni-app 中 UTS 插件的基本开发流程,这是你深度学习鸿蒙原生能力的关键一步!

继续阅读 »

随着 uni-app 对鸿蒙开发的支持日益完善,我们现在可以轻松地使用一套代码同时发布到多个平台。不过,在开发鸿蒙(HarmonyOS)应用时,有些平台特有的功能需要我们通过 UTS 插件来调用原生能力。

本文将手把手教你如何创建一个 UTS 插件,并利用鸿蒙系统提供的原生接口,为你的 uni-app 应用添加一个“一键安全退出”的功能。

一、 UTS 插件的创建与命名

首先,我们需要在 uni-app 项目中创建一个专用于封装鸿蒙原生 API 的 UTS 插件。

  1. 创建插件: 在 HBuilderX 中,找到你的项目目录下的 uni_modules 文件夹。
  2. 右键点击 uni_modules -> 新建插件
  3. 命名插件: 为了与退出应用功能相关联,我们将插件命名为 ohos-exit-helper
  4. 选择类型: 选择插件类型为 uts

创建完成后,你的项目结构会多出一个 uni_modules/ohos-exit-helper 目录。

二、 插件配置:在 uni 对象上注册新方法

接下来,我们需要修改插件的配置文件,告诉 uni-app 框架:我们要在全局的 uni 对象上注册一个名为 exitCurrentApp 的新方法。

打开 uni_modules/ohos-exit-helper/package.json 文件,找到 uni_modules 字段,并添加如下配置:

// 文件路径:uni_modules/ohos-exit-helper/package.json  

{  
  // ... 其他配置项保持不变 ...  
  "uni_modules": {  
    "uni-ext-api": {  
      "uni": {  
        "exitCurrentApp": "exitCurrentApp"   
      }  
    }  
  }  
}

三、 接口定义:确保类型安全(interface.uts)

为了在开发时获得更好的代码提示和类型检查,我们需要在 UTS 接口文件中定义新方法的签名。

打开 uni_modules/ohos-exit-helper/interface.uts 文件,扩展 Uni 接口,并声明我们的同步方法 exitCurrentApp()

// 文件路径:uni_modules/ohos-exit-helper/interface.uts  

// ... (省略文件开头其他类型定义) ...  

/**  
 * 扩展全局 uni 接口,添加鸿蒙原生方法  
 */  
interface Uni {  
  /**  
   * 【鸿蒙专属】安全退出当前应用。  
   * * @example  
   * ```typescript  
   * uni.exitCurrentApp()  
   * ```  
   * @remark  
   * - 该接口需同步调用  
   * @uniPlatform { "harmony": { "osVer": "3.0" } }  
   */  
  exitCurrentApp(): void, // 【关键重构】方法签名定义  
}

四、 鸿蒙原生实现:UTS 代码编写(index.uts)

这是实现退出功能的核心步骤。我们需要在插件目录下创建鸿蒙平台专用的实现文件,并利用鸿蒙的 Ability Context 来执行退出操作。

1. 确认路径: 确保你在插件目录下创建了 app-harmony 文件夹和 index.uts 文件:
uni_modules/ohos-exit-helper/app-harmony/index.uts

2. 编写代码: 写入以下代码。我们引入了 @ohos.app.ability.common 模块,并使用 context.terminateSelf() 这一原生方法来实现退出。


// 文件路径:uni_modules/ohos-exit-helper/app-harmony/index.uts  

// 引入鸿蒙 Ability 相关的公共模块  
import AbilityCommon from '@ohos.app.ability.common';   
import hiLog from '@ohos.hilog'; // 引入日志模块,便于调试  

/**  
 * 导出【鸿蒙实现】退出当前应用的功能  
 * * @returns {void}  
 */  
export function exitCurrentApp(): void { // 【关键重构】导出函数名  
  // 1. 获取当前 UIAbility 的上下文对象  
  // getContext() 是 uni-app UTS 提供的全局函数,用于获取原生上下文  
  const appAbilityContext = getContext() as AbilityCommon.UIAbilityContext;  

  // 2. 调用 terminateSelf() 方法来终止 Ability  
  appAbilityContext.terminateSelf();  

  // 3. 打印日志(重构日志变量名和内容)  
  const exitLogTag: string = "APP_CONTROL_LOG";  
  const statusMessage: string = "Application is shutting down gracefully via UTS plugin.";  

  hiLog.info(0x0001, exitLogTag, statusMessage);  
}

五、 在 uni-app 页面中调用插件

至此,我们的鸿蒙退出插件已经完成。最后一步,是在你的 uni-app 页面中引入并使用它。

你可以在任何 .vue.uvue`` 文件的<script lang="uts">` 块中调用此功能。

<template>  
  <view class="page-container">  
    <image class="app-logo" src="/static/logo.png"></image>  
    <text class="instruction-text">点击下方按钮,使用 UTS 实现鸿蒙应用退出。</text>  
    <view class="button-area">  
      <button class="exit-button" @click="handleAppExit">安全退出应用</button>  
    </view>  
  </view>  
</template>  

<script lang="uts">  

  import { exitCurrentApp } from "@/uni_modules/ohos-exit-helper"   

  export default {  
    data() {  
      return {  
        pageTitle: 'UTS退出功能演示',   
      }  
    },  
    methods: {  
      /**  
       * 处理点击退出按钮的逻辑方法  
       */  
      handleAppExit() { //   
        console.log("准备调用鸿蒙原生退出功能...");  
        // 调用我们插件中导出的退出函数  
        exitCurrentApp();   
      }  
    }  
  }  
</script>  

<style>  
  .page-container {  
    display: flex;  
    flex-direction: column;  
    align-items: center;  
    justify-content: center;  
    padding-top: 150px;  
    background-color: #f7f7f7;  
  }  
  .app-logo {  
    height: 180rpx;  
    width: 180rpx;  
    margin-bottom: 60rpx;  
  }  
  .instruction-text {  
    font-size: 34rpx;  
    color: #333333;  
    margin-bottom: 40rpx;  
  }  
  .exit-button {  
    width: 70%;  
    height: 90rpx;  
    line-height: 90rpx;  
    background-color: #007aff; /* 鸿蒙常用主色调 */  
    color: white;  
    font-size: 36rpx;  
    border-radius: 12rpx;  
    border: none;  
  }  
</style>

六、 运行与测试

现在,你可以运行你的 uni-app 项目到鸿蒙模拟器或真机上。

点击页面上的“安全退出应用”按钮,如果应用顺利关闭,那么恭喜你,你的第一个 UTS 鸿蒙插件就开发成功了!

通过这个示例,我们不仅学会了如何实现退出功能,更掌握了 uni-app 中 UTS 插件的基本开发流程,这是你深度学习鸿蒙原生能力的关键一步!

收起阅读 »

用uni-app搞了个足球战术板,踩了不少canvas坑,分享一下经验

鸿蒙征文

前言

最近开发了一款【苏超排名助手】,里面有一个功能页:足球战术板,主要就是让用户能在球场上画箭头、线条、曲线啥的,方便教练布置战术。本来以为挺简单的,结果各种坑,尤其是要兼容鸿蒙,差点没给我整崩溃。不过最后还是搞定了,记录一下,给后面的兄弟们少踩点坑。

需求是啥

说白了就是这几个功能:

  • 在球场上画箭头(传球路线)
  • 画直线和曲线(跑位路线)
  • 橡皮擦,画错了能擦掉
  • 能保存成图片分享
  • 还得支持鸿蒙(这个最坑)

Canvas初始化 - 第一个大坑

鸿蒙和其他平台的API不一样

uni-app有两种Canvas API:

  • 旧的:uni.createCanvasContext('canvasId')
  • 新的:Canvas 2D(type="2d")

鸿蒙现在只支持旧API,所以得这样写:

<!-- #ifdef APP-HARMONY -->  
<canvas  
  canvas-id="tacticsCanvas"  
  id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
  disable-scroll="true"  
></canvas>  
<!-- #endif -->  

<!-- #ifndef APP-HARMONY -->  
<canvas  
  type="2d"  
  id="tacticsCanvas"  
  canvas-id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove.prevent="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
></canvas>  
<!-- #endif -->

注意几个细节:

  1. 鸿蒙不用写type="2d"
  2. 鸿蒙用disable-scroll="true",其他平台用@touchmove.prevent
  3. 两个平台都得有canvas-idid

初始化时机很关键

不能在onMounted里直接初始化,得等页面真正渲染完了再搞。我试了好几次,最后发现延迟个200ms比较靠谱:

onMounted(() => {  
  // #ifdef APP-HARMONY  
  setTimeout(() => {  
    const systemInfo = uni.getSystemInfoSync();  
    canvasWidth.value = systemInfo.windowWidth;  
    canvasHeight.value = systemInfo.windowWidth * 1.25;  

    ctx.value = uni.createCanvasContext("tacticsCanvas");  
    ctx.value.isCanvas2d = false; // 标记是旧API  

    // 再延迟获取真实边界  
    setTimeout(() => {  
      const query = uni.createSelectorQuery();  
      query.select(".canvas-board").boundingClientRect((rect) => {  
        if (rect) {  
          boardRect.value = rect;  
          canvasReady.value = true; // 就绪标记  
        }  
      }).exec();  
    }, 100);  
  }, 200);  
  // #endif  
});

为啥要搞个isCanvas2d标记?因为后面很多API调用方式不一样,得区分开。

触摸绘制 - 坐标转换是重点

坐标系问题

这个坑我踩了好久。触摸事件返回的坐标,在不同平台上格式不一样:

  • 鸿蒙:touch.x / touch.y(可能是相对坐标)
  • 其他平台:touch.clientX / touch.clientY(需要减去canvas的偏移)

我写了个通用函数处理:

const getCanvasCoords = (touch) => {  
  let x, y;  

  // #ifdef APP-HARMONY  
  if (touch.x !== undefined && touch.y !== undefined) {  
    const rawX = touch.x;  
    const rawY = touch.y;  

    // 如果在合理范围内,直接用  
    if (rawX >= 0 && rawX <= canvasWidth.value &&   
        rawY >= 0 && rawY <= canvasHeight.value) {  
      x = rawX;  
      y = rawY;  
    }  
    // 超出范围就减去偏移  
    else if (boardRect.value && rawX > canvasWidth.value) {  
      x = rawX - boardRect.value.left;  
      y = rawY - boardRect.value.top;  
    }  
    else {  
      x = rawX;  
      y = rawY;  
    }  
  }  
  // #endif  

  // #ifndef APP-HARMONY  
  if (touch.clientX !== undefined && touch.clientY !== undefined && boardRect.value) {  
    x = touch.clientX - boardRect.value.left;  
    y = touch.clientY - boardRect.value.top;  
  }  
  // #endif  

  // 限制在画布范围内  
  x = Math.max(0, Math.min(x, canvasWidth.value));  
  y = Math.max(0, Math.min(y, canvasHeight.value));  

  return { x, y };  
};

绘制流程

整个绘制流程是这样的:

  1. touchstart:记录起点,初始化当前绘制对象

    const handleCanvasTouchStart = (e) => {  
    if (!canvasReady.value) {  
    uni.showToast({ title: "画布初始化中,请稍候", icon: "none" });  
    return;  
    }  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    isDrawing.value = true;  
    currentDrawing.value = {  
    type: currentTool.value, // arrow/line/curve  
    startX: coords.x,  
    startY: coords.y,  
    endX: coords.x,  
    endY: coords.y,  
    points: [{ x: coords.x, y: coords.y }], // 曲线用  
    color: currentTool.value === 'arrow' ? '#ff6b35' : '#1a3b6e'  
    };  
    };
  2. touchmove:更新终点或添加曲线点

    const handleCanvasTouchMove = (e) => {  
    if (!isDrawing.value || !currentDrawing.value) return;  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    if (currentTool.value === 'curve') {  
    // 曲线:不断添加点  
    currentDrawing.value.points.push({ x: coords.x, y: coords.y });  
    } else {  
    // 箭头/直线:更新终点  
    currentDrawing.value.endX = coords.x;  
    currentDrawing.value.endY = coords.y;  
    }  
    
    redrawCanvas(); // 实时重绘  
    };
  3. touchend:保存到数组

    const handleCanvasTouchEnd = () => {  
    if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  
    isDrawing.value = false;  
    currentDrawing.value = null;  
    redrawCanvas();  
    }  
    };

绘制各种图形

兼容两种API的方法

旧API和新API的方法名不一样,得封装一下:

const drawShape = (shape, context = null) => {  
  const drawCtx = context || ctx.value;  
  if (!drawCtx) return;  

  // 兼容函数  
  const setStrokeStyle = (color) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.strokeStyle = color;  
    } else {  
      drawCtx.setStrokeStyle(color); // 旧API  
    }  
  };  

  const setLineWidth = (width) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineWidth = width;  
    } else {  
      drawCtx.setLineWidth(width);  
    }  
  };  

  const setLineCap = (cap) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineCap = cap;  
    } else {  
      drawCtx.setLineCap(cap);  
    }  
  };  

  // ... 其他方法类似  
};

绘制箭头

箭头是最常用的,分两部分:线段 + 箭头头部

if (shape.type === 'arrow') {  
  const endX = shape.endX || shape.startX;  
  const endY = shape.endY || shape.startY;  

  // 1. 画线  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.startX, shape.startY);  
  drawCtx.lineTo(endX, endY);  
  drawCtx.stroke();  

  // 2. 画箭头头部(两条边)  
  const angle = Math.atan2(endY - shape.startY, endX - shape.startX);  
  const arrowLength = 25;  

  setLineWidth(5);  
  drawCtx.beginPath();  
  drawCtx.moveTo(endX, endY);  
  // 左边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle - Math.PI / 6),  
    endY - arrowLength * Math.sin(angle - Math.PI / 6)  
  );  
  drawCtx.moveTo(endX, endY);  
  // 右边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle + Math.PI / 6),  
    endY - arrowLength * Math.sin(angle + Math.PI / 6)  
  );  
  drawCtx.stroke();  
}

箭头的原理:

  1. 先算出线的角度:Math.atan2(dy, dx)
  2. 箭头两边各偏离30度(Math.PI / 6)
  3. 用三角函数算出两条边的终点坐标

绘制曲线(虚线)

曲线用来表示跑位,所以用虚线:

if (shape.type === 'curve' && shape.points) {  
  setLineDash([10, 5]); // 虚线:10px实线,5px空白  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.points[0].x, shape.points[0].y);  
  for (let i = 1; i < shape.points.length; i++) {  
    drawCtx.lineTo(shape.points[i].x, shape.points[i].y);  
  }  
  drawCtx.stroke();  
  setLineDash([]); // 恢复实线  
}

橡皮擦功能 - 点到线段距离算法

橡皮擦要判断点击的位置是不是靠近某条线,这个用到点到线段距离公式:

const pointToLineDistance = (px, py, x1, y1, x2, y2) => {  
  const A = px - x1;  
  const B = py - y1;  
  const C = x2 - x1;  
  const D = y2 - y1;  

  const dot = A * C + B * D;  
  const lenSq = C * C + D * D;  
  let param = -1;  

  if (lenSq !== 0) param = dot / lenSq;  

  let xx, yy;  

  if (param < 0) {  
    // 点在线段外侧,靠近起点  
    xx = x1;  
    yy = y1;  
  } else if (param > 1) {  
    // 点在线段外侧,靠近终点  
    xx = x2;  
    yy = y2;  
  } else {  
    // 点在线段范围内  
    xx = x1 + param * C;  
    yy = y1 + param * D;  
  }  

  const dx = px - xx;  
  const dy = py - yy;  
  return Math.sqrt(dx * dx + dy * dy);  
};

这个算法的核心思路:

  1. 把点投影到直线上
  2. 判断投影点是不是在线段范围内
  3. 算出点到投影点的距离

然后在touchstart判断:

if (currentTool.value === 'eraser') {  
  const clickX = coords.x;  
  const clickY = coords.y;  
  const eraseRadius = 30; // 橡皮擦范围  

  for (let i = drawings.value.length - 1; i >= 0; i--) {  
    const drawing = drawings.value[i];  
    let isNear = false;  

    if (drawing.type === 'line' || drawing.type === 'arrow') {  
      const dist = pointToLineDistance(  
        clickX, clickY,  
        drawing.startX, drawing.startY,  
        drawing.endX, drawing.endY  
      );  
      isNear = dist < eraseRadius;  
    } else if (drawing.type === 'curve') {  
      // 曲线:检查是否靠近任意点  
      for (let point of drawing.points) {  
        const dist = Math.sqrt(  
          Math.pow(clickX - point.x, 2) + Math.pow(clickY - point.y, 2)  
        );  
        if (dist < eraseRadius) {  
          isNear = true;  
          break;  
        }  
      }  
    }  

    if (isNear) {  
      drawings.value.splice(i, 1);  
      redrawCanvas();  
      break;  
    }  
  }  
}

重绘画布 - 性能优化

每次操作都要重绘整个画布,顺序很重要:

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  // 1. 清空画布(但不渲染)  
  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 2. 绘制所有已保存的图形  
  drawings.value.forEach((drawing) => {  
    drawShape(drawing);  
  });  

  // 3. 绘制当前正在画的(实时反馈)  
  if (currentDrawing.value) {  
    drawShape(currentDrawing.value);  
  }  

  // 4. 旧API必须调用draw()才能渲染到屏幕  
  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false); // false表示不清空之前的内容  
  }  
};

注意:

  • 新API(Canvas 2D):每次绘制都是实时的
  • 旧API:必须调用draw()才会显示
  • 鸿蒙用的是旧API,别忘了draw()
  • draw(false)的false很关键,true会清空画布

导出图片 - 生成分享海报

这个功能挺实用的,步骤是这样的:

  1. 先截取战术板:把球场、球员、战术线都画到临时canvas上
  2. 再画海报:在另一个canvas上画标题、战术板图片、底部信息
  3. 导出为图片:用uni.canvasToTempFilePath

绘制战术板完整内容

const captureBoard = () => {  
  return new Promise((resolve, reject) => {  
    const tempCtx = uni.createCanvasContext("tacticsCanvas");  
    tempCtx.isCanvas2d = false;  

    // 1. 画背景(球场)  
    tempCtx.setFillStyle("#2d8659");  
    tempCtx.fillRect(0, 0, boardWidth, boardHeight);  

    // 2. 画场地线(中线、禁区等)  
    tempCtx.setStrokeStyle("rgba(255, 255, 255, 0.6)");  
    tempCtx.setLineWidth(2);  
    tempCtx.beginPath();  
    tempCtx.moveTo(0, boardHeight * 0.5);  
    tempCtx.lineTo(boardWidth, boardHeight * 0.5);  
    tempCtx.stroke();  

    // 中圈  
    const centerCircleRadius = (100 / 750) * boardWidth;  
    tempCtx.beginPath();  
    tempCtx.arc(boardWidth * 0.5, boardHeight * 0.5, centerCircleRadius, 0, 2 * Math.PI);  
    tempCtx.stroke();  

    // ... 更多场地线  

    // 3. 画战术线路  
    drawings.value.forEach((drawing) => {  
      drawShape(drawing, tempCtx);  
    });  

    // 4. 画球员  
    playersOnField.value.forEach((player) => {  
      const markerRadius = (60 / 2) * (screenWidth / 750);  
      const centerX = player.x + markerRadius;  
      const centerY = player.y + markerRadius;  

      // 圆圈  
      tempCtx.setFillStyle(player.team === 'home' ? '#1a3b6e' : '#ff6b35');  
      tempCtx.beginPath();  
      tempCtx.arc(centerX, centerY, markerRadius, 0, 2 * Math.PI);  
      tempCtx.fill();  

      // 号码  
      tempCtx.setFillStyle('#ffffff');  
      tempCtx.setFontSize(markerRadius * 0.8);  
      tempCtx.setTextAlign('center');  
      tempCtx.setTextBaseline('middle');  
      tempCtx.fillText(player.number, centerX, centerY);  
    });  

    // 5. 导出  
    tempCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "tacticsCanvas",  
          success: (res) => {  
            resolve(res.tempFilePath);  
          },  
          fail: reject  
        });  
      }, 500); // 等渲染完成  
    });  
  });  
};

绘制海报

const drawSharePoster = (boardImagePath) => {  
  return new Promise((resolve, reject) => {  
    const pWidth = 375;  
    const pHeight = 550;  
    const pCtx = uni.createCanvasContext("sharePosterCanvas");  

    // 1. 背景渐变  
    const gradient = pCtx.createLinearGradient(0, 0, 0, pHeight);  
    gradient.addColorStop(0, "#1a3b6e");  
    gradient.addColorStop(1, "#2c5282");  
    pCtx.setFillStyle(gradient);  
    pCtx.fillRect(0, 0, pWidth, pHeight);  

    // 2. 标题  
    pCtx.setFillStyle("#ffffff");  
    pCtx.setFontSize(24);  
    pCtx.setTextAlign("center");  
    pCtx.fillText("⚽ 足球战术板", pWidth / 2, 40);  

    // 3. 副标题  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.8)");  
    pCtx.setFontSize(14);  
    pCtx.fillText("Football Tactics Board", pWidth / 2, 65);  

    // 4. 战术板图片(重点!)  
    const margin = 20;  
    const maxBoardWidth = pWidth - margin * 2;  
    const boardAspect = boardRect.value.width / boardRect.value.height;  
    let drawWidth = maxBoardWidth;  
    let drawHeight = drawWidth / boardAspect;  

    const boardX = (pWidth - drawWidth) / 2;  
    const boardY = 90;  

    // 白色卡片背景  
    pCtx.setFillStyle("#ffffff");  
    pCtx.fillRect(boardX - 8, boardY - 8, drawWidth + 16, drawHeight + 16);  

    // 画战术板图片  
    pCtx.drawImage(boardImagePath, boardX, boardY, drawWidth, drawHeight);  

    // 5. 底部信息  
    const footerY = pHeight - 50;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.9)");  
    pCtx.setFontSize(16);  
    pCtx.fillText("苏超排名助手", pWidth / 2, footerY);  

    const now = new Date();  
    const timeStr = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.5)");  
    pCtx.setFontSize(10);  
    pCtx.fillText(`生成时间: ${timeStr}`, pWidth / 2, footerY + 25);  

    // 6. 导出高清图(2倍分辨率)  
    pCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "sharePosterCanvas",  
          width: pWidth,  
          height: pHeight,  
          destWidth: pWidth * 2,  // 2倍分辨率  
          destHeight: pHeight * 2,  
          fileType: "png",  
          quality: 1,  
          success: (res) => resolve(res.tempFilePath),  
          fail: reject  
        });  
      }, 800);  
    });  
  });  
};

注意destWidthdestHeight,设置成2倍能提高导出图片的清晰度。

踩过的坑总结

1. 初始化时机问题

症状:获取不到canvas尺寸,或者坐标转换不准

原因:页面还没渲染完就初始化了

解决:延迟200ms,再用createSelectorQuery获取真实位置

setTimeout(() => {  
  const query = uni.createSelectorQuery();  
  query.select(".canvas-board").boundingClientRect((rect) => {  
    boardRect.value = rect;  
    canvasReady.value = true;  
  }).exec();  
}, 200);

2. 坐标转换不准确

症状:画的位置跟手指点的位置对不上

原因:没考虑canvas的偏移量(padding、margin等)

解决:必须用boundingClientRect获取真实位置,然后减去偏移

x = touch.clientX - boardRect.value.left;  
y = touch.clientY - boardRect.value.top;

3. 鸿蒙draw()必须调用

症状:画了但是屏幕上看不到

原因:旧API必须调用draw()才会渲染

解决:每次绘制完都调用

if (!ctx.value.isCanvas2d) {  
  ctx.value.draw(false);  
}

4. 导出图片时机问题

症状:导出的图片是空白的或者不完整

原因draw()是异步的,还没渲染完就导出了

解决:在draw()的回调里,再延迟500ms

ctx.draw(false, () => {  
  setTimeout(() => {  
    uni.canvasToTempFilePath({...});  
  }, 500);  
});

5. 橡皮擦范围不好控制

症状:点了删不掉,或者不小心删了别的线

原因:距离阈值设置不合理

解决:30-40px比较合适,太小不好点,太大容易误删

const eraseRadius = 30; // 橡皮擦范围  
if (dist < eraseRadius) {  
  // 删除  
}

6. touchmove和页面滚动冲突

症状:画线的时候页面跟着滚动

原因:touchmove事件冒泡到了页面

解决:加.stop.prevent修饰符

<canvas @touchmove.stop.prevent="handleCanvasTouchMove"></canvas>

但是鸿蒙不支持.prevent,要用disable-scroll="true"

<!-- #ifdef APP-HARMONY -->  
<canvas disable-scroll="true"></canvas>  
<!-- #endif -->

7. 重绘性能问题

症状:线条多了之后很卡

原因:每次touchmove都要重绘所有线条

解决

  • 限制绘制对象数量(最多50个)
  • touchmove节流(每10ms更新一次)
  • 复杂背景用离屏canvas缓存
let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 10) return; // 节流  
  lastTime = now;  
  // ...  
};

8. 图片清晰度问题

症状:导出的图片很模糊

原因:没设置destWidthdestHeight

解决:设置成实际尺寸的2倍

uni.canvasToTempFilePath({  
  canvasId: "myCanvas",  
  destWidth: width * 2,  
  destHeight: height * 2,  
  fileType: "png",  
  quality: 1  
});

性能优化建议

1. 节流touchmove

绘制曲线时,不用每次touchmove都添加点:

let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 16) return; // 约60fps  
  lastTime = now;  

  const coords = getCanvasCoords(e.touches[0]);  
  currentDrawing.value.points.push(coords);  
  redrawCanvas();  
};

2. 限制绘制数量

太多绘制对象会导致重绘变慢:

const MAX_DRAWINGS = 50;  

const handleCanvasTouchEnd = () => {  
  if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  

    // 限制数量  
    if (drawings.value.length > MAX_DRAWINGS) {  
      drawings.value.shift(); // 删除最早的  
    }  

    redrawCanvas();  
  }  
};

3. 离屏canvas缓存背景

球场背景每次都画很费性能,可以缓存起来:

let backgroundCache = null;  

const cacheBackground = () => {  
  const offscreenCanvas = uni.createOffscreenCanvas({  
    type: '2d',  
    width: canvasWidth.value,  
    height: canvasHeight.value  
  });  

  const offCtx = offscreenCanvas.getContext('2d');  

  // 画球场背景  
  offCtx.fillStyle = '#2d8659';  
  offCtx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);  
  // ... 画其他场地线  

  backgroundCache = offscreenCanvas;  
};  

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 用缓存的背景  
  if (backgroundCache) {  
    ctx.value.drawImage(backgroundCache, 0, 0);  
  }  

  // 画战术线  
  drawings.value.forEach(drawing => drawShape(drawing));  

  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false);  
  }  
};

4. 按需重绘

如果只是改变某个元素,可以不重绘整个画布(但Canvas API不支持局部重绘,这个比较难实现)。

替代方案:把不同层分开

  • 背景层:静态的球场(很少变化)
  • 战术层:箭头、线条(经常变化)
  • 球员层:用普通DOM而不是Canvas
<!-- 背景canvas -->  
<canvas id="bgCanvas" class="bg-layer"></canvas>  

<!-- 战术canvas -->  
<canvas id="tacticsCanvas" class="tactics-layer"></canvas>  

<!-- 球员用普通DOM -->  
<view class="players-layer">  
  <view v-for="player in players" :key="player.id"   
        :style="{left: player.x, top: player.y}">  
    {{ player.number }}  
  </view>  
</view>

这样球员拖动就不需要重绘canvas了。

实用技巧

1. 调试坐标

在开发时可以实时显示坐标,方便调试:

const handleCanvasTouchMove = (e) => {  
  const coords = getCanvasCoords(e.touches[0]);  
  console.log(`x: ${coords.x}, y: ${coords.y}`);  
  // 或者显示在页面上  
  debugInfo.value = `x: ${coords.x}, y: ${coords.y}`;  
};

2. 撤销功能

保存历史记录数组:

const history = ref([]);  
const historyIndex = ref(-1);  

const saveHistory = () => {  
  // 删除当前索引之后的历史  
  history.value = history.value.slice(0, historyIndex.value + 1);  
  // 添加新状态  
  history.value.push(JSON.parse(JSON.stringify(drawings.value)));  
  historyIndex.value++;  

  // 限制历史记录数量  
  if (history.value.length > 20) {  
    history.value.shift();  
    historyIndex.value--;  
  }  
};  

const undo = () => {  
  if (historyIndex.value > 0) {  
    historyIndex.value--;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};  

const redo = () => {  
  if (historyIndex.value < history.value.length - 1) {  
    historyIndex.value++;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};

3. 颜色选择器

让用户自定义线条颜色:

const colors = ['#ff6b35', '#1a3b6e', '#4caf50', '#f44336', '#9e9e9e'];  
const currentColor = ref('#ff6b35');  

const handleCanvasTouchStart = (e) => {  
  // ...  
  currentDrawing.value = {  
    type: currentTool.value,  
    startX: coords.x,  
    startY: coords.y,  
    color: currentColor.value // 使用选中的颜色  
  };  
};

4. 线条粗细调整

const lineWidths = [2, 4, 6, 8];  
const currentWidth = ref(4);  

const drawShape = (shape) => {  
  // ...  
  const lineWidth = shape.width || 4;  
  setLineWidth(lineWidth);  
  // ...  
};

最后

整个功能做下来,最大的感受就是:Canvas在uni-app编译到鸿蒙还是有不少坑的。不过搞定之后还挺有成就感的。

开发建议

  1. 先在H5上调试,等逻辑都对了再适配鸿蒙
  2. 鸿蒙的真机调试很慢,多用console.loguni.showToast
  3. 坐标问题一定要用实际设备测试,模拟器不准
  4. 多保存代码,Canvas相关的代码很容易改崩

适用场景

  • 战术板(足球、篮球等)
  • 签名功能
  • 涂鸦板
  • 路径规划
  • 图片标注

代码在附件,有问题可以留言交流。希望能帮到要做类似功能的兄弟们!

继续阅读 »

前言

最近开发了一款【苏超排名助手】,里面有一个功能页:足球战术板,主要就是让用户能在球场上画箭头、线条、曲线啥的,方便教练布置战术。本来以为挺简单的,结果各种坑,尤其是要兼容鸿蒙,差点没给我整崩溃。不过最后还是搞定了,记录一下,给后面的兄弟们少踩点坑。

需求是啥

说白了就是这几个功能:

  • 在球场上画箭头(传球路线)
  • 画直线和曲线(跑位路线)
  • 橡皮擦,画错了能擦掉
  • 能保存成图片分享
  • 还得支持鸿蒙(这个最坑)

Canvas初始化 - 第一个大坑

鸿蒙和其他平台的API不一样

uni-app有两种Canvas API:

  • 旧的:uni.createCanvasContext('canvasId')
  • 新的:Canvas 2D(type="2d")

鸿蒙现在只支持旧API,所以得这样写:

<!-- #ifdef APP-HARMONY -->  
<canvas  
  canvas-id="tacticsCanvas"  
  id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
  disable-scroll="true"  
></canvas>  
<!-- #endif -->  

<!-- #ifndef APP-HARMONY -->  
<canvas  
  type="2d"  
  id="tacticsCanvas"  
  canvas-id="tacticsCanvas"  
  class="canvas-board"  
  @touchstart="handleCanvasTouchStart"  
  @touchmove.prevent="handleCanvasTouchMove"  
  @touchend="handleCanvasTouchEnd"  
></canvas>  
<!-- #endif -->

注意几个细节:

  1. 鸿蒙不用写type="2d"
  2. 鸿蒙用disable-scroll="true",其他平台用@touchmove.prevent
  3. 两个平台都得有canvas-idid

初始化时机很关键

不能在onMounted里直接初始化,得等页面真正渲染完了再搞。我试了好几次,最后发现延迟个200ms比较靠谱:

onMounted(() => {  
  // #ifdef APP-HARMONY  
  setTimeout(() => {  
    const systemInfo = uni.getSystemInfoSync();  
    canvasWidth.value = systemInfo.windowWidth;  
    canvasHeight.value = systemInfo.windowWidth * 1.25;  

    ctx.value = uni.createCanvasContext("tacticsCanvas");  
    ctx.value.isCanvas2d = false; // 标记是旧API  

    // 再延迟获取真实边界  
    setTimeout(() => {  
      const query = uni.createSelectorQuery();  
      query.select(".canvas-board").boundingClientRect((rect) => {  
        if (rect) {  
          boardRect.value = rect;  
          canvasReady.value = true; // 就绪标记  
        }  
      }).exec();  
    }, 100);  
  }, 200);  
  // #endif  
});

为啥要搞个isCanvas2d标记?因为后面很多API调用方式不一样,得区分开。

触摸绘制 - 坐标转换是重点

坐标系问题

这个坑我踩了好久。触摸事件返回的坐标,在不同平台上格式不一样:

  • 鸿蒙:touch.x / touch.y(可能是相对坐标)
  • 其他平台:touch.clientX / touch.clientY(需要减去canvas的偏移)

我写了个通用函数处理:

const getCanvasCoords = (touch) => {  
  let x, y;  

  // #ifdef APP-HARMONY  
  if (touch.x !== undefined && touch.y !== undefined) {  
    const rawX = touch.x;  
    const rawY = touch.y;  

    // 如果在合理范围内,直接用  
    if (rawX >= 0 && rawX <= canvasWidth.value &&   
        rawY >= 0 && rawY <= canvasHeight.value) {  
      x = rawX;  
      y = rawY;  
    }  
    // 超出范围就减去偏移  
    else if (boardRect.value && rawX > canvasWidth.value) {  
      x = rawX - boardRect.value.left;  
      y = rawY - boardRect.value.top;  
    }  
    else {  
      x = rawX;  
      y = rawY;  
    }  
  }  
  // #endif  

  // #ifndef APP-HARMONY  
  if (touch.clientX !== undefined && touch.clientY !== undefined && boardRect.value) {  
    x = touch.clientX - boardRect.value.left;  
    y = touch.clientY - boardRect.value.top;  
  }  
  // #endif  

  // 限制在画布范围内  
  x = Math.max(0, Math.min(x, canvasWidth.value));  
  y = Math.max(0, Math.min(y, canvasHeight.value));  

  return { x, y };  
};

绘制流程

整个绘制流程是这样的:

  1. touchstart:记录起点,初始化当前绘制对象

    const handleCanvasTouchStart = (e) => {  
    if (!canvasReady.value) {  
    uni.showToast({ title: "画布初始化中,请稍候", icon: "none" });  
    return;  
    }  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    isDrawing.value = true;  
    currentDrawing.value = {  
    type: currentTool.value, // arrow/line/curve  
    startX: coords.x,  
    startY: coords.y,  
    endX: coords.x,  
    endY: coords.y,  
    points: [{ x: coords.x, y: coords.y }], // 曲线用  
    color: currentTool.value === 'arrow' ? '#ff6b35' : '#1a3b6e'  
    };  
    };
  2. touchmove:更新终点或添加曲线点

    const handleCanvasTouchMove = (e) => {  
    if (!isDrawing.value || !currentDrawing.value) return;  
    
    const touch = e.touches[0];  
    const coords = getCanvasCoords(touch);  
    
    if (currentTool.value === 'curve') {  
    // 曲线:不断添加点  
    currentDrawing.value.points.push({ x: coords.x, y: coords.y });  
    } else {  
    // 箭头/直线:更新终点  
    currentDrawing.value.endX = coords.x;  
    currentDrawing.value.endY = coords.y;  
    }  
    
    redrawCanvas(); // 实时重绘  
    };
  3. touchend:保存到数组

    const handleCanvasTouchEnd = () => {  
    if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  
    isDrawing.value = false;  
    currentDrawing.value = null;  
    redrawCanvas();  
    }  
    };

绘制各种图形

兼容两种API的方法

旧API和新API的方法名不一样,得封装一下:

const drawShape = (shape, context = null) => {  
  const drawCtx = context || ctx.value;  
  if (!drawCtx) return;  

  // 兼容函数  
  const setStrokeStyle = (color) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.strokeStyle = color;  
    } else {  
      drawCtx.setStrokeStyle(color); // 旧API  
    }  
  };  

  const setLineWidth = (width) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineWidth = width;  
    } else {  
      drawCtx.setLineWidth(width);  
    }  
  };  

  const setLineCap = (cap) => {  
    if (drawCtx.isCanvas2d) {  
      drawCtx.lineCap = cap;  
    } else {  
      drawCtx.setLineCap(cap);  
    }  
  };  

  // ... 其他方法类似  
};

绘制箭头

箭头是最常用的,分两部分:线段 + 箭头头部

if (shape.type === 'arrow') {  
  const endX = shape.endX || shape.startX;  
  const endY = shape.endY || shape.startY;  

  // 1. 画线  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.startX, shape.startY);  
  drawCtx.lineTo(endX, endY);  
  drawCtx.stroke();  

  // 2. 画箭头头部(两条边)  
  const angle = Math.atan2(endY - shape.startY, endX - shape.startX);  
  const arrowLength = 25;  

  setLineWidth(5);  
  drawCtx.beginPath();  
  drawCtx.moveTo(endX, endY);  
  // 左边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle - Math.PI / 6),  
    endY - arrowLength * Math.sin(angle - Math.PI / 6)  
  );  
  drawCtx.moveTo(endX, endY);  
  // 右边  
  drawCtx.lineTo(  
    endX - arrowLength * Math.cos(angle + Math.PI / 6),  
    endY - arrowLength * Math.sin(angle + Math.PI / 6)  
  );  
  drawCtx.stroke();  
}

箭头的原理:

  1. 先算出线的角度:Math.atan2(dy, dx)
  2. 箭头两边各偏离30度(Math.PI / 6)
  3. 用三角函数算出两条边的终点坐标

绘制曲线(虚线)

曲线用来表示跑位,所以用虚线:

if (shape.type === 'curve' && shape.points) {  
  setLineDash([10, 5]); // 虚线:10px实线,5px空白  
  drawCtx.beginPath();  
  drawCtx.moveTo(shape.points[0].x, shape.points[0].y);  
  for (let i = 1; i < shape.points.length; i++) {  
    drawCtx.lineTo(shape.points[i].x, shape.points[i].y);  
  }  
  drawCtx.stroke();  
  setLineDash([]); // 恢复实线  
}

橡皮擦功能 - 点到线段距离算法

橡皮擦要判断点击的位置是不是靠近某条线,这个用到点到线段距离公式:

const pointToLineDistance = (px, py, x1, y1, x2, y2) => {  
  const A = px - x1;  
  const B = py - y1;  
  const C = x2 - x1;  
  const D = y2 - y1;  

  const dot = A * C + B * D;  
  const lenSq = C * C + D * D;  
  let param = -1;  

  if (lenSq !== 0) param = dot / lenSq;  

  let xx, yy;  

  if (param < 0) {  
    // 点在线段外侧,靠近起点  
    xx = x1;  
    yy = y1;  
  } else if (param > 1) {  
    // 点在线段外侧,靠近终点  
    xx = x2;  
    yy = y2;  
  } else {  
    // 点在线段范围内  
    xx = x1 + param * C;  
    yy = y1 + param * D;  
  }  

  const dx = px - xx;  
  const dy = py - yy;  
  return Math.sqrt(dx * dx + dy * dy);  
};

这个算法的核心思路:

  1. 把点投影到直线上
  2. 判断投影点是不是在线段范围内
  3. 算出点到投影点的距离

然后在touchstart判断:

if (currentTool.value === 'eraser') {  
  const clickX = coords.x;  
  const clickY = coords.y;  
  const eraseRadius = 30; // 橡皮擦范围  

  for (let i = drawings.value.length - 1; i >= 0; i--) {  
    const drawing = drawings.value[i];  
    let isNear = false;  

    if (drawing.type === 'line' || drawing.type === 'arrow') {  
      const dist = pointToLineDistance(  
        clickX, clickY,  
        drawing.startX, drawing.startY,  
        drawing.endX, drawing.endY  
      );  
      isNear = dist < eraseRadius;  
    } else if (drawing.type === 'curve') {  
      // 曲线:检查是否靠近任意点  
      for (let point of drawing.points) {  
        const dist = Math.sqrt(  
          Math.pow(clickX - point.x, 2) + Math.pow(clickY - point.y, 2)  
        );  
        if (dist < eraseRadius) {  
          isNear = true;  
          break;  
        }  
      }  
    }  

    if (isNear) {  
      drawings.value.splice(i, 1);  
      redrawCanvas();  
      break;  
    }  
  }  
}

重绘画布 - 性能优化

每次操作都要重绘整个画布,顺序很重要:

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  // 1. 清空画布(但不渲染)  
  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 2. 绘制所有已保存的图形  
  drawings.value.forEach((drawing) => {  
    drawShape(drawing);  
  });  

  // 3. 绘制当前正在画的(实时反馈)  
  if (currentDrawing.value) {  
    drawShape(currentDrawing.value);  
  }  

  // 4. 旧API必须调用draw()才能渲染到屏幕  
  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false); // false表示不清空之前的内容  
  }  
};

注意:

  • 新API(Canvas 2D):每次绘制都是实时的
  • 旧API:必须调用draw()才会显示
  • 鸿蒙用的是旧API,别忘了draw()
  • draw(false)的false很关键,true会清空画布

导出图片 - 生成分享海报

这个功能挺实用的,步骤是这样的:

  1. 先截取战术板:把球场、球员、战术线都画到临时canvas上
  2. 再画海报:在另一个canvas上画标题、战术板图片、底部信息
  3. 导出为图片:用uni.canvasToTempFilePath

绘制战术板完整内容

const captureBoard = () => {  
  return new Promise((resolve, reject) => {  
    const tempCtx = uni.createCanvasContext("tacticsCanvas");  
    tempCtx.isCanvas2d = false;  

    // 1. 画背景(球场)  
    tempCtx.setFillStyle("#2d8659");  
    tempCtx.fillRect(0, 0, boardWidth, boardHeight);  

    // 2. 画场地线(中线、禁区等)  
    tempCtx.setStrokeStyle("rgba(255, 255, 255, 0.6)");  
    tempCtx.setLineWidth(2);  
    tempCtx.beginPath();  
    tempCtx.moveTo(0, boardHeight * 0.5);  
    tempCtx.lineTo(boardWidth, boardHeight * 0.5);  
    tempCtx.stroke();  

    // 中圈  
    const centerCircleRadius = (100 / 750) * boardWidth;  
    tempCtx.beginPath();  
    tempCtx.arc(boardWidth * 0.5, boardHeight * 0.5, centerCircleRadius, 0, 2 * Math.PI);  
    tempCtx.stroke();  

    // ... 更多场地线  

    // 3. 画战术线路  
    drawings.value.forEach((drawing) => {  
      drawShape(drawing, tempCtx);  
    });  

    // 4. 画球员  
    playersOnField.value.forEach((player) => {  
      const markerRadius = (60 / 2) * (screenWidth / 750);  
      const centerX = player.x + markerRadius;  
      const centerY = player.y + markerRadius;  

      // 圆圈  
      tempCtx.setFillStyle(player.team === 'home' ? '#1a3b6e' : '#ff6b35');  
      tempCtx.beginPath();  
      tempCtx.arc(centerX, centerY, markerRadius, 0, 2 * Math.PI);  
      tempCtx.fill();  

      // 号码  
      tempCtx.setFillStyle('#ffffff');  
      tempCtx.setFontSize(markerRadius * 0.8);  
      tempCtx.setTextAlign('center');  
      tempCtx.setTextBaseline('middle');  
      tempCtx.fillText(player.number, centerX, centerY);  
    });  

    // 5. 导出  
    tempCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "tacticsCanvas",  
          success: (res) => {  
            resolve(res.tempFilePath);  
          },  
          fail: reject  
        });  
      }, 500); // 等渲染完成  
    });  
  });  
};

绘制海报

const drawSharePoster = (boardImagePath) => {  
  return new Promise((resolve, reject) => {  
    const pWidth = 375;  
    const pHeight = 550;  
    const pCtx = uni.createCanvasContext("sharePosterCanvas");  

    // 1. 背景渐变  
    const gradient = pCtx.createLinearGradient(0, 0, 0, pHeight);  
    gradient.addColorStop(0, "#1a3b6e");  
    gradient.addColorStop(1, "#2c5282");  
    pCtx.setFillStyle(gradient);  
    pCtx.fillRect(0, 0, pWidth, pHeight);  

    // 2. 标题  
    pCtx.setFillStyle("#ffffff");  
    pCtx.setFontSize(24);  
    pCtx.setTextAlign("center");  
    pCtx.fillText("⚽ 足球战术板", pWidth / 2, 40);  

    // 3. 副标题  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.8)");  
    pCtx.setFontSize(14);  
    pCtx.fillText("Football Tactics Board", pWidth / 2, 65);  

    // 4. 战术板图片(重点!)  
    const margin = 20;  
    const maxBoardWidth = pWidth - margin * 2;  
    const boardAspect = boardRect.value.width / boardRect.value.height;  
    let drawWidth = maxBoardWidth;  
    let drawHeight = drawWidth / boardAspect;  

    const boardX = (pWidth - drawWidth) / 2;  
    const boardY = 90;  

    // 白色卡片背景  
    pCtx.setFillStyle("#ffffff");  
    pCtx.fillRect(boardX - 8, boardY - 8, drawWidth + 16, drawHeight + 16);  

    // 画战术板图片  
    pCtx.drawImage(boardImagePath, boardX, boardY, drawWidth, drawHeight);  

    // 5. 底部信息  
    const footerY = pHeight - 50;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.9)");  
    pCtx.setFontSize(16);  
    pCtx.fillText("苏超排名助手", pWidth / 2, footerY);  

    const now = new Date();  
    const timeStr = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`;  
    pCtx.setFillStyle("rgba(255, 255, 255, 0.5)");  
    pCtx.setFontSize(10);  
    pCtx.fillText(`生成时间: ${timeStr}`, pWidth / 2, footerY + 25);  

    // 6. 导出高清图(2倍分辨率)  
    pCtx.draw(false, () => {  
      setTimeout(() => {  
        uni.canvasToTempFilePath({  
          canvasId: "sharePosterCanvas",  
          width: pWidth,  
          height: pHeight,  
          destWidth: pWidth * 2,  // 2倍分辨率  
          destHeight: pHeight * 2,  
          fileType: "png",  
          quality: 1,  
          success: (res) => resolve(res.tempFilePath),  
          fail: reject  
        });  
      }, 800);  
    });  
  });  
};

注意destWidthdestHeight,设置成2倍能提高导出图片的清晰度。

踩过的坑总结

1. 初始化时机问题

症状:获取不到canvas尺寸,或者坐标转换不准

原因:页面还没渲染完就初始化了

解决:延迟200ms,再用createSelectorQuery获取真实位置

setTimeout(() => {  
  const query = uni.createSelectorQuery();  
  query.select(".canvas-board").boundingClientRect((rect) => {  
    boardRect.value = rect;  
    canvasReady.value = true;  
  }).exec();  
}, 200);

2. 坐标转换不准确

症状:画的位置跟手指点的位置对不上

原因:没考虑canvas的偏移量(padding、margin等)

解决:必须用boundingClientRect获取真实位置,然后减去偏移

x = touch.clientX - boardRect.value.left;  
y = touch.clientY - boardRect.value.top;

3. 鸿蒙draw()必须调用

症状:画了但是屏幕上看不到

原因:旧API必须调用draw()才会渲染

解决:每次绘制完都调用

if (!ctx.value.isCanvas2d) {  
  ctx.value.draw(false);  
}

4. 导出图片时机问题

症状:导出的图片是空白的或者不完整

原因draw()是异步的,还没渲染完就导出了

解决:在draw()的回调里,再延迟500ms

ctx.draw(false, () => {  
  setTimeout(() => {  
    uni.canvasToTempFilePath({...});  
  }, 500);  
});

5. 橡皮擦范围不好控制

症状:点了删不掉,或者不小心删了别的线

原因:距离阈值设置不合理

解决:30-40px比较合适,太小不好点,太大容易误删

const eraseRadius = 30; // 橡皮擦范围  
if (dist < eraseRadius) {  
  // 删除  
}

6. touchmove和页面滚动冲突

症状:画线的时候页面跟着滚动

原因:touchmove事件冒泡到了页面

解决:加.stop.prevent修饰符

<canvas @touchmove.stop.prevent="handleCanvasTouchMove"></canvas>

但是鸿蒙不支持.prevent,要用disable-scroll="true"

<!-- #ifdef APP-HARMONY -->  
<canvas disable-scroll="true"></canvas>  
<!-- #endif -->

7. 重绘性能问题

症状:线条多了之后很卡

原因:每次touchmove都要重绘所有线条

解决

  • 限制绘制对象数量(最多50个)
  • touchmove节流(每10ms更新一次)
  • 复杂背景用离屏canvas缓存
let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 10) return; // 节流  
  lastTime = now;  
  // ...  
};

8. 图片清晰度问题

症状:导出的图片很模糊

原因:没设置destWidthdestHeight

解决:设置成实际尺寸的2倍

uni.canvasToTempFilePath({  
  canvasId: "myCanvas",  
  destWidth: width * 2,  
  destHeight: height * 2,  
  fileType: "png",  
  quality: 1  
});

性能优化建议

1. 节流touchmove

绘制曲线时,不用每次touchmove都添加点:

let lastTime = 0;  
const handleCanvasTouchMove = (e) => {  
  const now = Date.now();  
  if (now - lastTime < 16) return; // 约60fps  
  lastTime = now;  

  const coords = getCanvasCoords(e.touches[0]);  
  currentDrawing.value.points.push(coords);  
  redrawCanvas();  
};

2. 限制绘制数量

太多绘制对象会导致重绘变慢:

const MAX_DRAWINGS = 50;  

const handleCanvasTouchEnd = () => {  
  if (isDrawing.value && currentDrawing.value) {  
    drawings.value.push({ ...currentDrawing.value });  

    // 限制数量  
    if (drawings.value.length > MAX_DRAWINGS) {  
      drawings.value.shift(); // 删除最早的  
    }  

    redrawCanvas();  
  }  
};

3. 离屏canvas缓存背景

球场背景每次都画很费性能,可以缓存起来:

let backgroundCache = null;  

const cacheBackground = () => {  
  const offscreenCanvas = uni.createOffscreenCanvas({  
    type: '2d',  
    width: canvasWidth.value,  
    height: canvasHeight.value  
  });  

  const offCtx = offscreenCanvas.getContext('2d');  

  // 画球场背景  
  offCtx.fillStyle = '#2d8659';  
  offCtx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);  
  // ... 画其他场地线  

  backgroundCache = offscreenCanvas;  
};  

const redrawCanvas = () => {  
  if (!ctx.value) return;  

  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);  

  // 用缓存的背景  
  if (backgroundCache) {  
    ctx.value.drawImage(backgroundCache, 0, 0);  
  }  

  // 画战术线  
  drawings.value.forEach(drawing => drawShape(drawing));  

  if (!ctx.value.isCanvas2d) {  
    ctx.value.draw(false);  
  }  
};

4. 按需重绘

如果只是改变某个元素,可以不重绘整个画布(但Canvas API不支持局部重绘,这个比较难实现)。

替代方案:把不同层分开

  • 背景层:静态的球场(很少变化)
  • 战术层:箭头、线条(经常变化)
  • 球员层:用普通DOM而不是Canvas
<!-- 背景canvas -->  
<canvas id="bgCanvas" class="bg-layer"></canvas>  

<!-- 战术canvas -->  
<canvas id="tacticsCanvas" class="tactics-layer"></canvas>  

<!-- 球员用普通DOM -->  
<view class="players-layer">  
  <view v-for="player in players" :key="player.id"   
        :style="{left: player.x, top: player.y}">  
    {{ player.number }}  
  </view>  
</view>

这样球员拖动就不需要重绘canvas了。

实用技巧

1. 调试坐标

在开发时可以实时显示坐标,方便调试:

const handleCanvasTouchMove = (e) => {  
  const coords = getCanvasCoords(e.touches[0]);  
  console.log(`x: ${coords.x}, y: ${coords.y}`);  
  // 或者显示在页面上  
  debugInfo.value = `x: ${coords.x}, y: ${coords.y}`;  
};

2. 撤销功能

保存历史记录数组:

const history = ref([]);  
const historyIndex = ref(-1);  

const saveHistory = () => {  
  // 删除当前索引之后的历史  
  history.value = history.value.slice(0, historyIndex.value + 1);  
  // 添加新状态  
  history.value.push(JSON.parse(JSON.stringify(drawings.value)));  
  historyIndex.value++;  

  // 限制历史记录数量  
  if (history.value.length > 20) {  
    history.value.shift();  
    historyIndex.value--;  
  }  
};  

const undo = () => {  
  if (historyIndex.value > 0) {  
    historyIndex.value--;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};  

const redo = () => {  
  if (historyIndex.value < history.value.length - 1) {  
    historyIndex.value++;  
    drawings.value = JSON.parse(JSON.stringify(history.value[historyIndex.value]));  
    redrawCanvas();  
  }  
};

3. 颜色选择器

让用户自定义线条颜色:

const colors = ['#ff6b35', '#1a3b6e', '#4caf50', '#f44336', '#9e9e9e'];  
const currentColor = ref('#ff6b35');  

const handleCanvasTouchStart = (e) => {  
  // ...  
  currentDrawing.value = {  
    type: currentTool.value,  
    startX: coords.x,  
    startY: coords.y,  
    color: currentColor.value // 使用选中的颜色  
  };  
};

4. 线条粗细调整

const lineWidths = [2, 4, 6, 8];  
const currentWidth = ref(4);  

const drawShape = (shape) => {  
  // ...  
  const lineWidth = shape.width || 4;  
  setLineWidth(lineWidth);  
  // ...  
};

最后

整个功能做下来,最大的感受就是:Canvas在uni-app编译到鸿蒙还是有不少坑的。不过搞定之后还挺有成就感的。

开发建议

  1. 先在H5上调试,等逻辑都对了再适配鸿蒙
  2. 鸿蒙的真机调试很慢,多用console.loguni.showToast
  3. 坐标问题一定要用实际设备测试,模拟器不准
  4. 多保存代码,Canvas相关的代码很容易改崩

适用场景

  • 战术板(足球、篮球等)
  • 签名功能
  • 涂鸦板
  • 路径规划
  • 图片标注

代码在附件,有问题可以留言交流。希望能帮到要做类似功能的兄弟们!

收起阅读 »

表情包搜索助手:uni-app 鸿蒙应用开发全流程解析

鸿蒙征文

写在前面

这是一个用uni-app开发的表情包搜索应用,最大的亮点就是能编译运行到华为鸿蒙系统。说白了,就是用一套代码,可以在iOS、Android、小程序、鸿蒙等多个平台上跑起来。

这个教程是关于这个项目是怎么组织的,用了哪些技术,以及如何编译到鸿蒙系统。


一、整体架构概览

1.1 项目是干啥的?

这个APP就是一个表情包搜索工具,用户可以:

  • 浏览热门表情包
  • 搜索想要的表情
  • 查看专辑分类
  • 下载表情包保存到相册

很简单,就是这四大功能。

1.2 技术栈选型

咱们用的技术栈如下:

前端框架

  • Vue 3 - 这是uni-app采用的最新版Vue框架
  • uni-app - DCloud家的跨平台框架,可以把代码编译到各个平台
  • Pinia - Vue 3推荐的状态管理工具(相当于Vuex的升级版)

开发语言

  • JavaScript/ES6+ - 主要业务逻辑
  • UTS (uni TypeScript) - 用来写鸿蒙原生插件的语言

UI组件

  • 原生组件 - 直接用uni-app的内置组件
  • 自定义组件 - 比如协议弹窗组件

网络请求

  • 基于uni.request封装的HTTP请求库
  • Promise风格,支持拦截器

二、项目目录结构

先看看整个项目的目录结构,这样心里有个底:

biaoqingbaosousuogit/  
├── common/                    # 公共模块  
│   ├── api/                  # 接口管理  
│   │   ├── http.js          # 封装的HTTP请求库  
│   │   └── base.js          # 业务接口定义  
│   └── util/                # 工具函数  
│       ├── datetime.js      # 日期时间处理  
│       └── util.js          # 通用工具方法  
│  
├── components/               # 组件库  
│   └── agreement-popup/     # 用户协议弹窗组件  
│  
├── pages/                    # 页面目录  
│   ├── index/               # 首页(热门表情)  
│   ├── album/               # 专辑页  
│   ├── search/              # 搜索页  
│   ├── detail/              # 详情页  
│   └── webview/             # H5页面容器  
│  
├── stores/                   # 状态管理(Pinia)  
│   └── main.js              # 主store  
│  
├── static/                   # 静态资源  
│   ├── icon/                # 图标  
│   ├── image/               # 图片  
│   └── tabbar/              # 底部导航图标  
│  
├── uni_modules/             # uni插件模块  
│   └── ha-downloadToSystemAlbum/  # 鸿蒙下载插件  
│       └── utssdk/  
│           └── app-harmony/ # 鸿蒙原生实现  
│  
├── harmony-configs/         # 鸿蒙配置(重点!)  
│   ├── build-profile.json5 # 构建配置、签名证书  
│   ├── AppScope/           # 应用级配置  
│   └── entry/              # 入口模块配置  
│  
├── App.vue                  # 应用入口  
├── main.js                  # 主入口文件  
├── pages.json              # 页面路由配置  
├── manifest.json           # 应用配置清单  
└── env.js                  # 环境配置

目录功能说明

common/ - 公共模块,存放各种通用的东西

  • api/ - 网络请求相关,包括封装好的请求库和所有API接口
  • util/ - 工具函数,比如日期格式化之类的

pages/ - 存放所有的页面,每个文件夹就是一个页面模块

stores/ - Pinia状态管理,用来存放全局状态数据(比如token、版本号等)

harmony-configs/ - 这个很重要!专门为鸿蒙系统准备的配置文件,包含签名证书、权限声明等

uni_modules/ - uni-app的插件系统,这里有个自定义的鸿蒙下载插件


三、核心模块详解

3.1 网络请求封装(common/api/http.js)

这个文件封装了一个通用的HTTP请求库,解决几个问题:

  1. 统一的请求配置

    • baseUrl配置
    • 默认请求头
    • 超时时间
  2. 请求拦截

    • 自动添加token
    • 自动添加版本号
    • 添加平台标识
  3. 响应处理

    • 统一的错误处理
    • 日志记录

代码结构大概这样:

export default {  
  config: {  
    baseUrl: baseUrl,  
    timeout: 10000,  
    // ...其他配置  
  },  

  request(options) {  
    // 从store获取token、版本号等信息  
    const mainStore = useMainStore();  

    // 组装请求头  
    options.header = Object.assign({}, options.header, {  
      Version: mainStore.version,  
      Authorization: "Bearer " + mainStore.token,  
      UniPlatform: mainStore.uniPlatform  
    });  

    // 返回Promise  
    return new Promise((resolve, reject) => {  
      uni.request({  
        ...options,  
        complete: (response) => {  
          if (response.statusCode === 200) {  
            resolve(response.data);  
          } else {  
            reject(response.data);  
          }  
        }  
      });  
    });  
  },  

  get(url, data, options) { /* ... */ },  
  post(url, data, options) { /* ... */ },  
  // ...其他方法  
}

使用起来很方便:

import api from '@/common/api/base.js'  

// 发起请求  
const res = await api.homeRandom({ page: 1 })

3.2 业务接口管理(common/api/base.js)

把所有的API接口集中管理,方便维护:

const homeAlbum = (data) => {  
  return http.request({  
    url: "/addon/face/home/album",  
    method: "GET",  
    data: data,  
  });  
};  

const homeRandom = (data) => {  
  return http.request({  
    url: "/addon/face/home/random",  
    method: "GET",  
    data: data,  
  });  
};  

// 导出所有接口  
export default {  
  homeAlbum,  
  homeRandom,  
  homeSearch,  
  homeRelate,  
  homeAlbumList,  
  homeDownload,  
};

这样做的好处:

  • 接口集中管理,一目了然
  • 修改接口时只需改这一个地方
  • 其他地方import进来直接用

3.3 状态管理(stores/main.js)

用Pinia来管理全局状态,代码很简单:

import { defineStore } from 'pinia';  

export const useMainStore = defineStore('main', {  
  state: () => ({  
    count: 0,  
    token: '',          // 用户token  
    version: '',        // 应用版本  
    uniPlatform: '',    // 运行平台  
  }),  
  actions: {  
    increment() {  
      this.count++;  
    },  
  },  
});

使用方式:

import { useMainStore } from '@/stores/main'  

const mainStore = useMainStore()  
console.log(mainStore.token)  // 读取  
mainStore.token = 'xxx'       // 写入

比Vuex简单多了,不需要写那么多模板代码。


四、页面实现详解

4.1 首页(pages/index/index.vue)

首页是个瀑布流展示热门表情的页面,核心功能:

  1. 瀑布流加载

    • 首次加载热门表情
    • 滚动到底部自动加载更多
  2. 返回顶部

    • 滚动超过500px显示返回顶部按钮
  3. 用户协议

    • 首次打开显示用户协议弹窗

关键代码片段:

// 获取热门表情  
const getRandomEmojis = async (isLoadMore = false) => {  
  if (loading.value || !hasMore.value) return;  

  loading.value = true;  
  try {  
    const res = await api.homeRandom({ page: page.value });  

    if (res.code === 200) {  
      if (isLoadMore) {  
        emojis.value = [...emojis.value, ...res.data];  
      } else {  
        emojis.value = res.data;  
      }  

      hasMore.value = res.data.length > 0;  
      if (hasMore.value) {  
        page.value++;  
      }  
    }  
  } catch (e) {  
    console.error('获取热门表情失败:', e);  
  } finally {  
    loading.value = false;  
  }  
};  

// 页面触底加载更多  
onReachBottom(() => {  
  getRandomEmojis(true);  
});

技巧点:

  • loadinghasMore标记来避免重复请求
  • isLoadMore参数区分首次加载和追加加载
  • onReachBottom是uni-app提供的触底钩子

4.2 详情页(pages/detail/detail.vue)

详情页展示单个表情包,可以下载保存,还会推荐相关的表情。

关键功能:

  1. 接收参数

    onLoad((options) => {  
     if (options.item) {  
       emojiData.value = JSON.parse(decodeURIComponent(options.item))  
     }  
    })  
  2. 下载保存

    const saveImage = () => {  
     uni.downloadFile({  
       url: emojiData.value.imgurl,  
       success: (res) => {  
         if (res.statusCode === 200) {  
           uni.saveImageToPhotosAlbum({  
             filePath: res.tempFilePath,  
             success: () => {  
               uni.showToast({ title: '保存成功', icon: 'success' })  
             }  
           })  
         }  
       }  
     })  
    }  
  3. 相关推荐

    • 调用api.homeRelate获取相关表情
    • 点击推荐项时切换当前表情并刷新推荐列表

五、鸿蒙适配核心技术

这部分是重点中的重点!如何让uni-app编译到鸿蒙系统。

5.1 鸿蒙配置目录(harmony-configs/)

这个目录是专门为鸿蒙准备的,uni-app在编译鸿蒙版本时会读取这些配置。

目录结构:

harmony-configs/  
├── build-profile.json5      # 构建配置  
├── AppScope/  
│   ├── app.json5           # 应用信息  
│   └── resources/          # 应用资源(图标等)  
└── entry/  
    └── src/main/  
        └── module.json5    # 模块配置(权限等)

5.2 构建配置(build-profile.json5)

这个文件配置签名证书和SDK版本,非常重要:

{  
  "app": {  
    "signingConfigs": [  
      {  
        "name": "default",  
        "type": "HarmonyOS",  
        "material": {  
          "storePassword": "你的证书库密码",  
          "certpath": "你的证书文件路径",  
          "keyAlias": "密钥别名",  
          "keyPassword": "密钥密码",  
          "profile": "配置文件路径",  
          "signAlg": "SHA256withECDSA",  
          "storeFile": "证书库文件路径"  
        }  
      }  
    ],  
    "products": [  
      {  
        "name": "default",  
        "signingConfig": "default",  
        "compatibleSdkVersion": "5.0.1(13)",  // SDK版本  
        "runtimeOS": "HarmonyOS"  
      }  
    ]  
  }  
}

重点:

  • signingConfigs - 配置开发和发布证书
  • compatibleSdkVersion - 兼容的鸿蒙SDK版本
  • 证书文件需要从华为AGC控制台申请

5.3 应用配置(AppScope/app.json5)

配置应用的基本信息:

{  
  "app": {  
    "bundleName": "com.letwind.biaoqingbaozhushou",  // 包名  
    "vendor": "letwind",  
    "versionCode": 101,  
    "versionName": "1.0.1",  
    "icon": "$media:app_icon",  
    "label": "$string:app_name"  
  }  
}

5.4 模块配置(entry/src/main/module.json5)

配置应用权限和能力:

{  
  "module": {  
    "name": "entry",  
    "type": "entry",  
    "deviceTypes": ["phone"],  
    "requestPermissions": [  
      {  
        "name": "ohos.permission.INTERNET"  // 网络权限  
      }  
    ]  
  }  
}

5.5 manifest.json中的鸿蒙配置

在uni-app的manifest.json里也要配置鸿蒙相关信息:

{  
  "vueVersion": "3",  
  "app-harmony": {  
    "distribute": {  
      "bundleName": "com.letwind.biaoqingbaozhushou",  
      "icons": {  
        "foreground": "static/icon/logo1024.png",  
        "background": "static/icon/logo1024.png"  
      }  
    }  
  }  
}

这里的bundleName要和app.json5里的保持一致!


六、编译运行到鸿蒙

6.1 环境准备

  1. 安装HBuilderX

    • 去DCloud官网下载最新版HBuilderX
    • 必须是支持鸿蒙的版本
  2. 准备证书

    • 去华为AGC控制台申请证书

6.2 配置证书

编辑harmony-configs/build-profile.json5

{  
  "app": {  
    "signingConfigs": [  
      {  
        "name": "default",  
        "material": {  
          "storePassword": "实际的证书库密码",  
          "certpath": "证书文件绝对路径/cert.cer",  
          "keyAlias": "实际的密钥别名",  
          "keyPassword": "实际的密钥密码",  
          "profile": "配置文件绝对路径/profile.p7b",  
          "signAlg": "SHA256withECDSA",  
          "storeFile": "证书库文件绝对路径/cert.p12"  
        }  
      }  
    ]  
  }  
}

6.3 运行项目

  1. 在HBuilderX中打开项目
  2. 点击工具栏的"运行"
  3. 选择"运行到鸿蒙"
  4. 选择设备
  5. 等待编译完成,APP会自动安装到设备上

6.4 打包发布

  1. 配置发布证书

    • signingConfigs中配置release证书
    • 发布证书要在AGC控制台单独申请
  2. 本地打包

    • 点击HBuilderX的"发行"菜单
    • 选择"鸿蒙本地打包"
    • 生成.app文件
  3. 上架应用市场

    • 登录华为应用市场控制台
    • 上传.app文件
    • 填写应用信息
    • 提交审核

七、常见问题和解决方案

7.1 证书配置错误

问题: 编译时报错"签名失败"

解决:

  • 确认证书文件路径正确(建议用绝对路径)
  • 检查证书密码是否正确
  • 确认证书和profile文件是匹配的
  • 检查bundleName是否和证书中的包名一致

7.2 设备识别不了

问题: HBuilderX检测不到鸿蒙设备

解决:

  • 确认手机已开启开发者模式和USB调试
  • 更换数据线(有些数据线只能充电不能传输数据)
  • 重启HBuilderX和手机
  • 检查是否安装了华为手机驱动

7.3 页面跳转失败

问题: 在鸿蒙上页面跳转不成功

解决:

  • 检查pages.json中是否已注册该页面
  • 检查跳转路径是否正确(不要带.vue后缀)
  • 如果是tabBar页面,要用uni.switchTab而不是uni.navigateTo

八、项目亮点总结

8.1 跨平台能力

一套代码,多端运行:

  • iOS APP
  • Android APP
  • 鸿蒙APP(HarmonyOS NEXT)
  • 微信小程序
  • H5网页

这就是uni-app的最大价值,开发效率极高。

8.2 鸿蒙适配方案

完整的鸿蒙适配解决方案:

  • 标准的配置文件结构
  • 签名证书管理
  • 系统API调用(相册、下载等)

可以作为其他uni-app项目适配鸿蒙的参考模板。

8.3 工程化实践

规范的项目结构:

  • 清晰的目录划分
  • API集中管理
  • 组件化开发
  • 状态统一管理

代码可维护性强,方便团队协作。


写在最后

这个项目虽然功能不算复杂,但麻雀虽小五脏俱全,把uni-app开发鸿蒙应用的整套流程都走通了。

关键点就这几个:

  1. uni-app框架 - 跨平台的基础
  2. harmony-configs - 鸿蒙配置的核心
  3. 证书签名 - 打包发布的关键

只要搞懂这几个点,你也能把自己的uni-app项目跑在鸿蒙上。

如果遇到问题,多看看官方文档,或者加入开发者社群交流。鸿蒙生态还在快速发展,遇到坑是正常的,解决问题的过程也是成长的过程。

加油!🚀


项目开源地址

GitHub: https://github.com/zwpro/uniapptohongmeng

欢迎 Star、Fork 和提交 Issue!

继续阅读 »

写在前面

这是一个用uni-app开发的表情包搜索应用,最大的亮点就是能编译运行到华为鸿蒙系统。说白了,就是用一套代码,可以在iOS、Android、小程序、鸿蒙等多个平台上跑起来。

这个教程是关于这个项目是怎么组织的,用了哪些技术,以及如何编译到鸿蒙系统。


一、整体架构概览

1.1 项目是干啥的?

这个APP就是一个表情包搜索工具,用户可以:

  • 浏览热门表情包
  • 搜索想要的表情
  • 查看专辑分类
  • 下载表情包保存到相册

很简单,就是这四大功能。

1.2 技术栈选型

咱们用的技术栈如下:

前端框架

  • Vue 3 - 这是uni-app采用的最新版Vue框架
  • uni-app - DCloud家的跨平台框架,可以把代码编译到各个平台
  • Pinia - Vue 3推荐的状态管理工具(相当于Vuex的升级版)

开发语言

  • JavaScript/ES6+ - 主要业务逻辑
  • UTS (uni TypeScript) - 用来写鸿蒙原生插件的语言

UI组件

  • 原生组件 - 直接用uni-app的内置组件
  • 自定义组件 - 比如协议弹窗组件

网络请求

  • 基于uni.request封装的HTTP请求库
  • Promise风格,支持拦截器

二、项目目录结构

先看看整个项目的目录结构,这样心里有个底:

biaoqingbaosousuogit/  
├── common/                    # 公共模块  
│   ├── api/                  # 接口管理  
│   │   ├── http.js          # 封装的HTTP请求库  
│   │   └── base.js          # 业务接口定义  
│   └── util/                # 工具函数  
│       ├── datetime.js      # 日期时间处理  
│       └── util.js          # 通用工具方法  
│  
├── components/               # 组件库  
│   └── agreement-popup/     # 用户协议弹窗组件  
│  
├── pages/                    # 页面目录  
│   ├── index/               # 首页(热门表情)  
│   ├── album/               # 专辑页  
│   ├── search/              # 搜索页  
│   ├── detail/              # 详情页  
│   └── webview/             # H5页面容器  
│  
├── stores/                   # 状态管理(Pinia)  
│   └── main.js              # 主store  
│  
├── static/                   # 静态资源  
│   ├── icon/                # 图标  
│   ├── image/               # 图片  
│   └── tabbar/              # 底部导航图标  
│  
├── uni_modules/             # uni插件模块  
│   └── ha-downloadToSystemAlbum/  # 鸿蒙下载插件  
│       └── utssdk/  
│           └── app-harmony/ # 鸿蒙原生实现  
│  
├── harmony-configs/         # 鸿蒙配置(重点!)  
│   ├── build-profile.json5 # 构建配置、签名证书  
│   ├── AppScope/           # 应用级配置  
│   └── entry/              # 入口模块配置  
│  
├── App.vue                  # 应用入口  
├── main.js                  # 主入口文件  
├── pages.json              # 页面路由配置  
├── manifest.json           # 应用配置清单  
└── env.js                  # 环境配置

目录功能说明

common/ - 公共模块,存放各种通用的东西

  • api/ - 网络请求相关,包括封装好的请求库和所有API接口
  • util/ - 工具函数,比如日期格式化之类的

pages/ - 存放所有的页面,每个文件夹就是一个页面模块

stores/ - Pinia状态管理,用来存放全局状态数据(比如token、版本号等)

harmony-configs/ - 这个很重要!专门为鸿蒙系统准备的配置文件,包含签名证书、权限声明等

uni_modules/ - uni-app的插件系统,这里有个自定义的鸿蒙下载插件


三、核心模块详解

3.1 网络请求封装(common/api/http.js)

这个文件封装了一个通用的HTTP请求库,解决几个问题:

  1. 统一的请求配置

    • baseUrl配置
    • 默认请求头
    • 超时时间
  2. 请求拦截

    • 自动添加token
    • 自动添加版本号
    • 添加平台标识
  3. 响应处理

    • 统一的错误处理
    • 日志记录

代码结构大概这样:

export default {  
  config: {  
    baseUrl: baseUrl,  
    timeout: 10000,  
    // ...其他配置  
  },  

  request(options) {  
    // 从store获取token、版本号等信息  
    const mainStore = useMainStore();  

    // 组装请求头  
    options.header = Object.assign({}, options.header, {  
      Version: mainStore.version,  
      Authorization: "Bearer " + mainStore.token,  
      UniPlatform: mainStore.uniPlatform  
    });  

    // 返回Promise  
    return new Promise((resolve, reject) => {  
      uni.request({  
        ...options,  
        complete: (response) => {  
          if (response.statusCode === 200) {  
            resolve(response.data);  
          } else {  
            reject(response.data);  
          }  
        }  
      });  
    });  
  },  

  get(url, data, options) { /* ... */ },  
  post(url, data, options) { /* ... */ },  
  // ...其他方法  
}

使用起来很方便:

import api from '@/common/api/base.js'  

// 发起请求  
const res = await api.homeRandom({ page: 1 })

3.2 业务接口管理(common/api/base.js)

把所有的API接口集中管理,方便维护:

const homeAlbum = (data) => {  
  return http.request({  
    url: "/addon/face/home/album",  
    method: "GET",  
    data: data,  
  });  
};  

const homeRandom = (data) => {  
  return http.request({  
    url: "/addon/face/home/random",  
    method: "GET",  
    data: data,  
  });  
};  

// 导出所有接口  
export default {  
  homeAlbum,  
  homeRandom,  
  homeSearch,  
  homeRelate,  
  homeAlbumList,  
  homeDownload,  
};

这样做的好处:

  • 接口集中管理,一目了然
  • 修改接口时只需改这一个地方
  • 其他地方import进来直接用

3.3 状态管理(stores/main.js)

用Pinia来管理全局状态,代码很简单:

import { defineStore } from 'pinia';  

export const useMainStore = defineStore('main', {  
  state: () => ({  
    count: 0,  
    token: '',          // 用户token  
    version: '',        // 应用版本  
    uniPlatform: '',    // 运行平台  
  }),  
  actions: {  
    increment() {  
      this.count++;  
    },  
  },  
});

使用方式:

import { useMainStore } from '@/stores/main'  

const mainStore = useMainStore()  
console.log(mainStore.token)  // 读取  
mainStore.token = 'xxx'       // 写入

比Vuex简单多了,不需要写那么多模板代码。


四、页面实现详解

4.1 首页(pages/index/index.vue)

首页是个瀑布流展示热门表情的页面,核心功能:

  1. 瀑布流加载

    • 首次加载热门表情
    • 滚动到底部自动加载更多
  2. 返回顶部

    • 滚动超过500px显示返回顶部按钮
  3. 用户协议

    • 首次打开显示用户协议弹窗

关键代码片段:

// 获取热门表情  
const getRandomEmojis = async (isLoadMore = false) => {  
  if (loading.value || !hasMore.value) return;  

  loading.value = true;  
  try {  
    const res = await api.homeRandom({ page: page.value });  

    if (res.code === 200) {  
      if (isLoadMore) {  
        emojis.value = [...emojis.value, ...res.data];  
      } else {  
        emojis.value = res.data;  
      }  

      hasMore.value = res.data.length > 0;  
      if (hasMore.value) {  
        page.value++;  
      }  
    }  
  } catch (e) {  
    console.error('获取热门表情失败:', e);  
  } finally {  
    loading.value = false;  
  }  
};  

// 页面触底加载更多  
onReachBottom(() => {  
  getRandomEmojis(true);  
});

技巧点:

  • loadinghasMore标记来避免重复请求
  • isLoadMore参数区分首次加载和追加加载
  • onReachBottom是uni-app提供的触底钩子

4.2 详情页(pages/detail/detail.vue)

详情页展示单个表情包,可以下载保存,还会推荐相关的表情。

关键功能:

  1. 接收参数

    onLoad((options) => {  
     if (options.item) {  
       emojiData.value = JSON.parse(decodeURIComponent(options.item))  
     }  
    })  
  2. 下载保存

    const saveImage = () => {  
     uni.downloadFile({  
       url: emojiData.value.imgurl,  
       success: (res) => {  
         if (res.statusCode === 200) {  
           uni.saveImageToPhotosAlbum({  
             filePath: res.tempFilePath,  
             success: () => {  
               uni.showToast({ title: '保存成功', icon: 'success' })  
             }  
           })  
         }  
       }  
     })  
    }  
  3. 相关推荐

    • 调用api.homeRelate获取相关表情
    • 点击推荐项时切换当前表情并刷新推荐列表

五、鸿蒙适配核心技术

这部分是重点中的重点!如何让uni-app编译到鸿蒙系统。

5.1 鸿蒙配置目录(harmony-configs/)

这个目录是专门为鸿蒙准备的,uni-app在编译鸿蒙版本时会读取这些配置。

目录结构:

harmony-configs/  
├── build-profile.json5      # 构建配置  
├── AppScope/  
│   ├── app.json5           # 应用信息  
│   └── resources/          # 应用资源(图标等)  
└── entry/  
    └── src/main/  
        └── module.json5    # 模块配置(权限等)

5.2 构建配置(build-profile.json5)

这个文件配置签名证书和SDK版本,非常重要:

{  
  "app": {  
    "signingConfigs": [  
      {  
        "name": "default",  
        "type": "HarmonyOS",  
        "material": {  
          "storePassword": "你的证书库密码",  
          "certpath": "你的证书文件路径",  
          "keyAlias": "密钥别名",  
          "keyPassword": "密钥密码",  
          "profile": "配置文件路径",  
          "signAlg": "SHA256withECDSA",  
          "storeFile": "证书库文件路径"  
        }  
      }  
    ],  
    "products": [  
      {  
        "name": "default",  
        "signingConfig": "default",  
        "compatibleSdkVersion": "5.0.1(13)",  // SDK版本  
        "runtimeOS": "HarmonyOS"  
      }  
    ]  
  }  
}

重点:

  • signingConfigs - 配置开发和发布证书
  • compatibleSdkVersion - 兼容的鸿蒙SDK版本
  • 证书文件需要从华为AGC控制台申请

5.3 应用配置(AppScope/app.json5)

配置应用的基本信息:

{  
  "app": {  
    "bundleName": "com.letwind.biaoqingbaozhushou",  // 包名  
    "vendor": "letwind",  
    "versionCode": 101,  
    "versionName": "1.0.1",  
    "icon": "$media:app_icon",  
    "label": "$string:app_name"  
  }  
}

5.4 模块配置(entry/src/main/module.json5)

配置应用权限和能力:

{  
  "module": {  
    "name": "entry",  
    "type": "entry",  
    "deviceTypes": ["phone"],  
    "requestPermissions": [  
      {  
        "name": "ohos.permission.INTERNET"  // 网络权限  
      }  
    ]  
  }  
}

5.5 manifest.json中的鸿蒙配置

在uni-app的manifest.json里也要配置鸿蒙相关信息:

{  
  "vueVersion": "3",  
  "app-harmony": {  
    "distribute": {  
      "bundleName": "com.letwind.biaoqingbaozhushou",  
      "icons": {  
        "foreground": "static/icon/logo1024.png",  
        "background": "static/icon/logo1024.png"  
      }  
    }  
  }  
}

这里的bundleName要和app.json5里的保持一致!


六、编译运行到鸿蒙

6.1 环境准备

  1. 安装HBuilderX

    • 去DCloud官网下载最新版HBuilderX
    • 必须是支持鸿蒙的版本
  2. 准备证书

    • 去华为AGC控制台申请证书

6.2 配置证书

编辑harmony-configs/build-profile.json5

{  
  "app": {  
    "signingConfigs": [  
      {  
        "name": "default",  
        "material": {  
          "storePassword": "实际的证书库密码",  
          "certpath": "证书文件绝对路径/cert.cer",  
          "keyAlias": "实际的密钥别名",  
          "keyPassword": "实际的密钥密码",  
          "profile": "配置文件绝对路径/profile.p7b",  
          "signAlg": "SHA256withECDSA",  
          "storeFile": "证书库文件绝对路径/cert.p12"  
        }  
      }  
    ]  
  }  
}

6.3 运行项目

  1. 在HBuilderX中打开项目
  2. 点击工具栏的"运行"
  3. 选择"运行到鸿蒙"
  4. 选择设备
  5. 等待编译完成,APP会自动安装到设备上

6.4 打包发布

  1. 配置发布证书

    • signingConfigs中配置release证书
    • 发布证书要在AGC控制台单独申请
  2. 本地打包

    • 点击HBuilderX的"发行"菜单
    • 选择"鸿蒙本地打包"
    • 生成.app文件
  3. 上架应用市场

    • 登录华为应用市场控制台
    • 上传.app文件
    • 填写应用信息
    • 提交审核

七、常见问题和解决方案

7.1 证书配置错误

问题: 编译时报错"签名失败"

解决:

  • 确认证书文件路径正确(建议用绝对路径)
  • 检查证书密码是否正确
  • 确认证书和profile文件是匹配的
  • 检查bundleName是否和证书中的包名一致

7.2 设备识别不了

问题: HBuilderX检测不到鸿蒙设备

解决:

  • 确认手机已开启开发者模式和USB调试
  • 更换数据线(有些数据线只能充电不能传输数据)
  • 重启HBuilderX和手机
  • 检查是否安装了华为手机驱动

7.3 页面跳转失败

问题: 在鸿蒙上页面跳转不成功

解决:

  • 检查pages.json中是否已注册该页面
  • 检查跳转路径是否正确(不要带.vue后缀)
  • 如果是tabBar页面,要用uni.switchTab而不是uni.navigateTo

八、项目亮点总结

8.1 跨平台能力

一套代码,多端运行:

  • iOS APP
  • Android APP
  • 鸿蒙APP(HarmonyOS NEXT)
  • 微信小程序
  • H5网页

这就是uni-app的最大价值,开发效率极高。

8.2 鸿蒙适配方案

完整的鸿蒙适配解决方案:

  • 标准的配置文件结构
  • 签名证书管理
  • 系统API调用(相册、下载等)

可以作为其他uni-app项目适配鸿蒙的参考模板。

8.3 工程化实践

规范的项目结构:

  • 清晰的目录划分
  • API集中管理
  • 组件化开发
  • 状态统一管理

代码可维护性强,方便团队协作。


写在最后

这个项目虽然功能不算复杂,但麻雀虽小五脏俱全,把uni-app开发鸿蒙应用的整套流程都走通了。

关键点就这几个:

  1. uni-app框架 - 跨平台的基础
  2. harmony-configs - 鸿蒙配置的核心
  3. 证书签名 - 打包发布的关键

只要搞懂这几个点,你也能把自己的uni-app项目跑在鸿蒙上。

如果遇到问题,多看看官方文档,或者加入开发者社群交流。鸿蒙生态还在快速发展,遇到坑是正常的,解决问题的过程也是成长的过程。

加油!🚀


项目开源地址

GitHub: https://github.com/zwpro/uniapptohongmeng

欢迎 Star、Fork 和提交 Issue!

收起阅读 »

【鸿蒙征文】UniApp(X) 让鸿蒙开发触手可及 —— LimeUI 组件库开发简录

鸿蒙征文 鸿蒙

引言:鸿蒙开发,真的有那么难吗?🤔

当鸿蒙系统(HarmonyOS)带着"分布式"、"原生智能"等前沿概念亮相时,许多开发者,尤其是前端开发者,心中难免产生畏惧:学习全新的 ArkTS 语言、掌握 DevEco Studio IDE、理解复杂的系统架构……这些障碍,似乎构筑了一道难以逾越的高墙。

然而,这种复杂的印象可能只是一种误解。如果你已经熟悉 Vue.js,那么惊喜来了——你与鸿蒙应用开发之间的距离,其实只隔了一个 UniApp(X)。本文将结合我开发 LimeUI 组件库的实战经历,向你证明:借助 UniApp(X),鸿蒙应用开发真的可以"有手就行",让前端开发者轻松拥抱鸿蒙生态。

第一章:UniApp(X) —— 鸿蒙开发的高效解决方案 🚀

在着手为鸿蒙生态贡献组件库时,作为一名熟悉UniApp的开发者,我自然选择了UniApp(X)。正是因为它会编译为ArkTS,能保持与原生相同的性能,同时极大地降低了开发门槛:

1. 零门槛的语法亲和性 ✨

对于Vue开发者而言,UniApp(X)的语法几乎是零学习成本的。你可以继续使用熟悉的template-script-style结构,继续运用v-model、v-if等指令。这种无缝衔接的体验,让鸿蒙开发的"陌生感"瞬间烟消云散。

2. 智能的平台差异抹平机制 🔄

UniApp(X) 的核心魅力在于其强大的条件编译系统。在开发 LimeUI 组件库时,我的代码结构通常是这样的:

<template>  
    <view class="l-button" @click="handleClick">  
      <text>{{ text }}</text>  
    </view>  
</template>  
<script setup lang="uts">  
  import { ButtonProps } from './type';  
  const emit = defineEmits(['click'])  
  const props = withDefaults(defineProps<ButtonProps>(), {  
    disabled: false,  
    ghost: false,  
    loading: false,  
    shape: 'rectangle',  
    size: 'medium',  
    type: 'default',  
})   

const handleClick = () => {  
    emit('click')  
    // #ifdef APP-HARMONY  
    // 鸿蒙平台特有的逻辑  
    console.log('Running on HarmonyOS!');  
    // #endif  
    // #ifdef MP-WEIXIN  
    // 微信小程序特有的逻辑  
    console.log('Running on WeChat!');  
    // #endif  
}  
</script>

通过简洁的#ifdef预处理指令,我能够轻松为不同平台(鸿蒙、微信小程序、iOS、Android等)编写差异化代码,同时保持核心业务逻辑的一致性。这种"一次开发,多端部署"的能力,让LimeUI组件库的开发效率得到了质的飞跃。

第二章:LimeUI 开发实战 —— 鸿蒙组件库的"简易模式" 🛠️

空谈理论不如实战演练。下面,我将以 LimeUI 中一个简单按钮组件的开发流程为例,带你体验这份"有手就行"的简单与高效。

步骤 1:环境搭建 —— 极简配置,快速上手 ⚡

环境配置非常简单直观,无需担心复杂的鸿蒙原生开发环境问题。按照官方教程完成几个基本步骤即可快速上手:运行和发行教程

步骤 2:组件开发 —— 用 Vue 的方式写鸿蒙组件 🎨

只需在创建 uni_modules 组件时选择相应的组件类型:

生成的uni_modules 组件目录结构如下:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-button  
│     │─components  
│     │  └─lime-button  
│     │      └─lime-button.uvue  // 组件实现  
│     │      └─type.ts       // 类型定义

接下来,让我们开始开发 lime-button 组件:

<!-- lime-button.uvue -->  
<template>  
    <view class="l-button" @click="handleClick">  
      <text>{{ text }}</text>  
    </view>  
</template>  
<script setup lang="uts">  
  import { ButtonProps } from './type';  
  const emit = defineEmits(['click'])  
  const props = withDefaults(defineProps<ButtonProps>(), {  
    block: false,  
    disabled: false,  
    ghost: false,  
    loading: false,  
    shape: 'rectangle',  
    size: 'medium',  
    type: 'default',  
    hoverStopPropagation: false,  
    hoverStartTime: 20,  
    hoverStayTime: 70,  
    lang: 'en',  
    sessionFrom: '',  
    sendMessageTitle: '',  
    sendMessagePath: '',  
    sendMessageImg: '',  
    appParameter: '',  
    showMessageCard: false  
})   

const handleClick = () => {  
    emit('click')  
}  
</script>

看到了吗?这完全就是标准的Vue单文件组件!没有任何鸿蒙原生的特定语法。你已掌握的Vue知识,就是开发鸿蒙组件的全部技能储备。这种熟悉感,让开发者能够立即进入高效开发状态。

步骤 3:编译与预览 —— 所见即所得 👀

在HBuilderX中,只需在manifest.json中配置好鸿蒙应用信息,然后点击菜单栏的"运行 > 运行到手机或模拟器 > 运行到鸿蒙"。

接下来,UniApp(X) 编译器会自动将你编写的 .(u)vue 文件,编译转换为标准的鸿蒙原生工程和 ArkTS 代码。你无需关心底层复杂的转换过程,只需静待编译完成,就能在模拟器上看到组件完美运行。

这正是"有手就行"的最佳诠释。你只需用熟悉的语法表达业务逻辑,UniApp(X)则默默处理好所有平台适配的复杂工作。在将整个UI组件库适配到鸿蒙平台的过程中,我几乎没有遇到实质性的技术障碍,这也是UniApp(X)最大的魅力所在。

第三章:进阶挑战与解决方案 —— 当需要调用原生能力时 💪

当然,"有手就行"并不意味着毫无挑战。在开发LimeUI组件库的过程中,我也遇到过需要调用鸿蒙平台特有能力的场景。

挑战场景:我需要开发一个功能强大的lime-svg组件,它不仅要支持颜色修改,还要兼容多种加载方式(路径、base64、XML)。🎨

解决方案:UniApp(X)贴心地提供了native-view机制,让我们能够轻松调用鸿蒙原生能力。只需在创建uni_modules组件时选择"创建uts插件-标准组件":🔌

生成的uni_modules 组件目录结构如下:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-svg  
│     │─components  
│     │  └─lime-svg  
│     │      └─lime-svg.uvue // 组件调用层  
│     └─utssdk  
│         └─app-harmony  
│             └─builder.ets  // 原生组件实现  
│             └─index.uts    // 桥接类导出

在组件调用层 lime-svg.uvue 中,我们这样编写:

<template>  
    <native-view class="l-svg" v-bind="$attrs" @init="onviewinit"></native-view>  
</template>  
<script setup lang="uts">  

import { SvpProps } from './type'  
import { NativeSvg } from "@/uni_modules/lime-svg"; // 导入桥接类  

let nativeSvg : NativeSvg | null = null  
const props = withDefaults(defineProps<SvpProps>(), {  
    src: '',  
    color: ''  
})  

const onviewinit = (e : UniNativeViewInitEvent) => {  
    nativeSvg = new NativeSvg(e.detail.element); // 传入native-view元素  
    nativeSvg?.updateSrc(props.src) // 调用实例方法更新资源  
    nativeSvg?.updateColor(props.color) // 调用实例方法更新颜色  
}  
</script>

在桥接类 index.uts 中,我们实现与原生能力的对接:

import { BuilderNode } from "@kit.ArkUI"  
import buffer from '@ohos.buffer';  
import { fileIo } from '@kit.CoreFileKit';  
// 导入混编实现的声明式UI构建函数  
import { buildSvg } from "./builder.ets"  
import { getEnv } from '@dcloudio/uni-runtime';  

export class NativeSvg {  
    private $element : UniNativeViewElement;  
    private builder : BuilderNode<[NativeSvgOptions]> | null = null  
    private svgMap : Map<string, string> = new Map<string, string>()  
    // 初始化 buildSvg 默认参数  
    private params : NativeSvgOptions = {  
        src: '',  
        onError: (message) => {  
            this.$element.dispatchEvent(new UniNativeViewEvent("error", { message }))  
        },  
        onComplete: (event : ESObject) => {  
            this.$element.dispatchEvent(new UniNativeViewEvent("load", {  
                width: event.width,  
                height: event.height  
            }))  
        },  
    }  

    constructor(element : UniNativeViewElement) {  
        // 绑定 wrapBuilder 函数  
        this.builder = element.bindHarmonyWrappedBuilder(wrapBuilder<[NativeSvgOptions]>(buildSvg), this.params)  
        this.$element = element  
        // 绑定当前实例为自定义的controller,方便其他地方通过 element 获取使用  
        this.$element.bindHarmonyController(this)  
    }  

    updateSrc(src : string) {  
        if (src.startsWith('data:image') || src.startsWith('<svg')) {  
            if (this.svgMap.has(src)) {  
                this.params.src = this.svgMap.get(src)!  
            } else {  
                // 处理临时文件路径  
                const tempFileName = `${Date.now()}.svg`  
                const tempDirPath = `${getEnv().TEMP_PATH}/svg`  
                const tempFilePath : string = `${tempDirPath}/${tempFileName}`  

                // 确保目录存在  
                if (!fileIo.accessSync(tempDirPath)) {  
                    fileIo.mkdirSync(tempDirPath, true)  
                }  

                // 创建并写入文件  
                const file = fileIo.openSync(tempFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);  
                // 根据不同格式保存SVG内容  
                if (src.startsWith('<svg')) {  
                    fileIo.writeSync(file.fd, src); // 直接写入XML文本  
                }  
                // 获取资源文件的原生路径  
                const path = UTSHarmony.getResourcePath(tempFilePath)  
                this.svgMap.set(src, path) // 缓存已处理的资源  
                this.params.src = path  
            }  
        }  
        else {  
            // 处理普通资源路径  
            this.params.src = UTSHarmony.getResourcePath(src)  
        }  
        this.builder?.update(this.params) // 更新渲染  
    }  

    updateColor(color : string) {  
        this.params.color = color  
        this.builder?.update(this.params) // 更新渲染  
    }  
}

看到代码中我导入了一些原生库,有小伙伴可能会好奇这些库是如何知道的。实际上,通过查阅华为开发者文档,搜索"如何创建临时文件"等相关问题,华为的智能小助手就能直接提供相关代码参考。我们可以基于这些参考代码,根据实际需求进行适当修改和调整,轻松实现临时文件创建等功能。


(cv大师就是我)

最后,在原生渲染层 builder.ets 中,我们定义实际的渲染逻辑:

@Builder  
export function buildSvg(params: ESObject) {  
    Image(params.src)  
        .width('100%')  
        .height('100%')  
        .objectFit(ImageFit.Contain)  
        .fillColor(params.color) // 支持动态修改颜色  
        .onComplete((event)=>{  
            params.onComplete(event)  
        })  
        .onError((error) =>{  
            params.onError(error.message)  
        })  
}

通过这种方式,即使是需要调用鸿蒙特定功能,也能通过UniAppX的UTS标准组件机制轻松实现。这种优雅的桥接设计,让我们既能享受Vue开发的便捷,又能在需要原生能力的关键地方获得与原生开发完全一致的能力。而这只是UniApp(X)调用鸿蒙生态能力的一种方式,在下一章中,我们将探索如何通过UTS API灵活调用OpenHarmony三方库中心仓的第三方库和鸿蒙系统自带的原生库,实现更灵活的功能扩展。

第四章:进阶实战 —— 用UTS API调用OpenHarmony三方库中心仓的第三方库 🚀

如果说组件开发是"有手就行",那么直接调用OpenHarmony生态库就是"如虎添翼"。UniApp(X)提供的UTS(Uni TypeScript)能力,不但能调用鸿蒙系统自带的原生能力,还能加载(ohpm,类似npm的包管理平台)的第三方库,让我们可以在页面中轻松访问各种系统API和三方库功能,为LimeUI组件库的功能扩展提供了无限可能。

案例背景:lime-crypto加密库开发 🔐
在开发过程中,我需要一个强大的加密功能库。虽然在传统UniApp中可以使用crypto-js库,但在UniAppX中并不支持。因此,我决定通过UTS机制直接调用发布到OpenHarmony三方库中心仓的@ohos/crypto-js第三方库来实现加密功能。

实现步骤:简单三步走 📋
只需在创建uni_modules组件时选择"创建uts插件-API插件":

生成的uni_modules组件目录结构如下,与lime-svg类似,它也遵循了UniApp(X)的插件规范:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-crypto  
│     └─utssdk  
│         └─app-harmony  
│             └─config.json // 原生依赖配置  
│             └─index.uts   // UTS桥接层实现

第一步:配置原生依赖 📦
在lime-crypto/utssdk/app-harmony/config.json中声明依赖:

{  
    "dependencies": {  
        "@ohos/crypto-js": "2.0.4"  
    }  
}

就是这么简单!UniApp(X)会自动处理依赖管理。
第二步:编写 UTS 桥接层 🌉
在lime-crypto/utssdk/app-harmony/index.uts中:

import { CryptoJS } from '@ohos/crypto-js'  
export class CryptoImpl {  
    constructor() {  
        // 初始化逻辑  
    }  

    // 获取编码器  
    get enc() {  
        return {  
            Utf8: CryptoJS.enc.Utf8,  
            Hex: CryptoJS.enc.Hex,  
            Base64: CryptoJS.enc.Base64,  
            // ... 其他编码器  
        }   
    }  

    // 加密算法  
    get AES() : CryptoJS.CipherHelper {  
        return CryptoJS.AES  
    }  

    get DES(): CryptoJS.CipherHelper {  
        return CryptoJS.DES  
    }  

    // 哈希函数  
    MD5(message: string) {  
        return CryptoJS.MD5(message)  
    }  

    SHA256(message: string) {  
        return CryptoJS.SHA256(message)  
    }  

    // HMAC 签名  
    HmacSHA256(message: string, secretKey: string) {  
        return CryptoJS.HmacSHA256(message, secretKey)  
    }  
}  

export function useCrypto() {  
    return new CryptoImpl()  
}

有细心的小伙伴可能会发现,为何我要封装一个CryptoImpl类,直接导出CryptoJS不香吗?实际上,虽然直接导出CryptoJS是可行的,但封装一个专门的实现类可以更好地控制API暴露范围,提供更符合业务需求的接口,并为后续可能的功能扩展和维护提供便利(借口,凑字数而已)。

export function useCrypto() {  
    return CryptoJS //全网最简单的加密库实现(搬运工的日常)  
}

第三步:在Vue组件中使用 🎯
现在,我们可以在普通的 Vue 组件中直接使用这个原生加密库了:

<template>  
    <view class="demo-container">  
        <lime-button @click="encryptData">加密测试</lime-button>  
        <text>{{ encryptedText }}</text>  
    </view>  
</template>  

<script setup lang="uts">  
import { useCrypto } from '@/uni_modules/lime-crypto'  

const crypto = useCrypto()  
const encryptedText = ref('')  

const encryptData = () => {  
    // 使用鸿蒙原生加密库进行 AES 加密  
    const encrypted = crypto.AES.encrypt(  
        'Hello HarmonyOS',   
        'secret-key-12345',   
        {   
            mode: crypto.mode.CBC,  
            padding: crypto.pad.Pkcs7   
        }  
    )  

    encryptedText.value = encrypted.toString()  
    console.log('加密结果:', encryptedText.value)  

    // 使用 SHA256 哈希  
    const hash = crypto.SHA256('需要哈希的数据')  
    console.log('SHA256 结果:', hash.toString())  
}  
</script>

通过lime-crypto的实现,我们可以看到UniApp(X)的UTS能力真正实现了:"用前端熟悉的语法,调用原生API的便利"。就这?就这么简单!我上我也行了!成为鸿蒙开发大佬,从此走向人生的鼎峰!

开发者既能利用Vue框架的开发便捷性快速构建UI界面,又能在需要原生能力的关键地方直接调用鸿蒙原生API。这种灵活的技术架构,为鸿蒙应用开发提供了高效且强大的解决方案。

不过,当前的开发体验仍有一些可以改进的空间:ArkTS引擎在代码修改后需要重新构建、签名和安装,这增加了开发过程中的等待时间;另外,首次编译所需的时间相对较长。希望未来的版本能够优化这些方面,进一步提升开发效率。

结语:鸿蒙开发,触手可及 🌈

通过LimeUI组件库的开发实践,我深刻体会到:UniApp(X)极大地降低了鸿蒙应用开发的技术门槛。作为开发者,你完全可以利用已掌握的Vue技术栈,以熟悉的开发方式快速进入鸿蒙开发领域。

鸿蒙生态正处于高速发展阶段,对于开发者而言,这是一片充满机遇的蓝海。UniApp(X)作为连接Vue技术栈与鸿蒙生态的桥梁,为开发者提供了一条低门槛、高效率的技术路径。

对于正在观望鸿蒙开发的开发者来说,现在正是借助UniApp(X)进入鸿蒙生态的理想时机。带着你熟悉的技术积累,你会发现鸿蒙开发并非想象中那样困难,而是可以通过现有技能平稳过渡的技术领域。

毕竟,能用熟悉的技术拥抱未来,这本身就是一件超酷的事,不是吗?💻✨

正是借助UniApp(X)的强大生态与开发便利性,我开发的LimeUI组件库中每个组件都作为独立插件上传到UniApp市场,目前已成功开源发布超过100款组件插件,为开发者社区贡献自己的一份力量!🚀

LimeUI组件库还同时支持UniApp和UniAppX双框架,让开发者可以在两个技术栈中无缝使用同一套组件,极大地提升了开发效率和代码复用性!

如果您在使用过程中发现组件库有任何不完善或缺少的组件,请随时在插件市场留言您的需求和建议。每一条反馈都是促使LimeUI不断完善的宝贵动力,我会认真对待并持续优化组件库,为开发者提供更好的使用体验!

如果您觉得我写的内容对您有所帮助,欢迎点赞加关注,一键三连支持!我第一次写文章,如果有不足之处,还请各位轻喷,我会不断学习和进步!

欢迎访问我的插件市场主页:https://ext.dcloud.net.cn/publisher?id=242774

继续阅读 »

引言:鸿蒙开发,真的有那么难吗?🤔

当鸿蒙系统(HarmonyOS)带着"分布式"、"原生智能"等前沿概念亮相时,许多开发者,尤其是前端开发者,心中难免产生畏惧:学习全新的 ArkTS 语言、掌握 DevEco Studio IDE、理解复杂的系统架构……这些障碍,似乎构筑了一道难以逾越的高墙。

然而,这种复杂的印象可能只是一种误解。如果你已经熟悉 Vue.js,那么惊喜来了——你与鸿蒙应用开发之间的距离,其实只隔了一个 UniApp(X)。本文将结合我开发 LimeUI 组件库的实战经历,向你证明:借助 UniApp(X),鸿蒙应用开发真的可以"有手就行",让前端开发者轻松拥抱鸿蒙生态。

第一章:UniApp(X) —— 鸿蒙开发的高效解决方案 🚀

在着手为鸿蒙生态贡献组件库时,作为一名熟悉UniApp的开发者,我自然选择了UniApp(X)。正是因为它会编译为ArkTS,能保持与原生相同的性能,同时极大地降低了开发门槛:

1. 零门槛的语法亲和性 ✨

对于Vue开发者而言,UniApp(X)的语法几乎是零学习成本的。你可以继续使用熟悉的template-script-style结构,继续运用v-model、v-if等指令。这种无缝衔接的体验,让鸿蒙开发的"陌生感"瞬间烟消云散。

2. 智能的平台差异抹平机制 🔄

UniApp(X) 的核心魅力在于其强大的条件编译系统。在开发 LimeUI 组件库时,我的代码结构通常是这样的:

<template>  
    <view class="l-button" @click="handleClick">  
      <text>{{ text }}</text>  
    </view>  
</template>  
<script setup lang="uts">  
  import { ButtonProps } from './type';  
  const emit = defineEmits(['click'])  
  const props = withDefaults(defineProps<ButtonProps>(), {  
    disabled: false,  
    ghost: false,  
    loading: false,  
    shape: 'rectangle',  
    size: 'medium',  
    type: 'default',  
})   

const handleClick = () => {  
    emit('click')  
    // #ifdef APP-HARMONY  
    // 鸿蒙平台特有的逻辑  
    console.log('Running on HarmonyOS!');  
    // #endif  
    // #ifdef MP-WEIXIN  
    // 微信小程序特有的逻辑  
    console.log('Running on WeChat!');  
    // #endif  
}  
</script>

通过简洁的#ifdef预处理指令,我能够轻松为不同平台(鸿蒙、微信小程序、iOS、Android等)编写差异化代码,同时保持核心业务逻辑的一致性。这种"一次开发,多端部署"的能力,让LimeUI组件库的开发效率得到了质的飞跃。

第二章:LimeUI 开发实战 —— 鸿蒙组件库的"简易模式" 🛠️

空谈理论不如实战演练。下面,我将以 LimeUI 中一个简单按钮组件的开发流程为例,带你体验这份"有手就行"的简单与高效。

步骤 1:环境搭建 —— 极简配置,快速上手 ⚡

环境配置非常简单直观,无需担心复杂的鸿蒙原生开发环境问题。按照官方教程完成几个基本步骤即可快速上手:运行和发行教程

步骤 2:组件开发 —— 用 Vue 的方式写鸿蒙组件 🎨

只需在创建 uni_modules 组件时选择相应的组件类型:

生成的uni_modules 组件目录结构如下:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-button  
│     │─components  
│     │  └─lime-button  
│     │      └─lime-button.uvue  // 组件实现  
│     │      └─type.ts       // 类型定义

接下来,让我们开始开发 lime-button 组件:

<!-- lime-button.uvue -->  
<template>  
    <view class="l-button" @click="handleClick">  
      <text>{{ text }}</text>  
    </view>  
</template>  
<script setup lang="uts">  
  import { ButtonProps } from './type';  
  const emit = defineEmits(['click'])  
  const props = withDefaults(defineProps<ButtonProps>(), {  
    block: false,  
    disabled: false,  
    ghost: false,  
    loading: false,  
    shape: 'rectangle',  
    size: 'medium',  
    type: 'default',  
    hoverStopPropagation: false,  
    hoverStartTime: 20,  
    hoverStayTime: 70,  
    lang: 'en',  
    sessionFrom: '',  
    sendMessageTitle: '',  
    sendMessagePath: '',  
    sendMessageImg: '',  
    appParameter: '',  
    showMessageCard: false  
})   

const handleClick = () => {  
    emit('click')  
}  
</script>

看到了吗?这完全就是标准的Vue单文件组件!没有任何鸿蒙原生的特定语法。你已掌握的Vue知识,就是开发鸿蒙组件的全部技能储备。这种熟悉感,让开发者能够立即进入高效开发状态。

步骤 3:编译与预览 —— 所见即所得 👀

在HBuilderX中,只需在manifest.json中配置好鸿蒙应用信息,然后点击菜单栏的"运行 > 运行到手机或模拟器 > 运行到鸿蒙"。

接下来,UniApp(X) 编译器会自动将你编写的 .(u)vue 文件,编译转换为标准的鸿蒙原生工程和 ArkTS 代码。你无需关心底层复杂的转换过程,只需静待编译完成,就能在模拟器上看到组件完美运行。

这正是"有手就行"的最佳诠释。你只需用熟悉的语法表达业务逻辑,UniApp(X)则默默处理好所有平台适配的复杂工作。在将整个UI组件库适配到鸿蒙平台的过程中,我几乎没有遇到实质性的技术障碍,这也是UniApp(X)最大的魅力所在。

第三章:进阶挑战与解决方案 —— 当需要调用原生能力时 💪

当然,"有手就行"并不意味着毫无挑战。在开发LimeUI组件库的过程中,我也遇到过需要调用鸿蒙平台特有能力的场景。

挑战场景:我需要开发一个功能强大的lime-svg组件,它不仅要支持颜色修改,还要兼容多种加载方式(路径、base64、XML)。🎨

解决方案:UniApp(X)贴心地提供了native-view机制,让我们能够轻松调用鸿蒙原生能力。只需在创建uni_modules组件时选择"创建uts插件-标准组件":🔌

生成的uni_modules 组件目录结构如下:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-svg  
│     │─components  
│     │  └─lime-svg  
│     │      └─lime-svg.uvue // 组件调用层  
│     └─utssdk  
│         └─app-harmony  
│             └─builder.ets  // 原生组件实现  
│             └─index.uts    // 桥接类导出

在组件调用层 lime-svg.uvue 中,我们这样编写:

<template>  
    <native-view class="l-svg" v-bind="$attrs" @init="onviewinit"></native-view>  
</template>  
<script setup lang="uts">  

import { SvpProps } from './type'  
import { NativeSvg } from "@/uni_modules/lime-svg"; // 导入桥接类  

let nativeSvg : NativeSvg | null = null  
const props = withDefaults(defineProps<SvpProps>(), {  
    src: '',  
    color: ''  
})  

const onviewinit = (e : UniNativeViewInitEvent) => {  
    nativeSvg = new NativeSvg(e.detail.element); // 传入native-view元素  
    nativeSvg?.updateSrc(props.src) // 调用实例方法更新资源  
    nativeSvg?.updateColor(props.color) // 调用实例方法更新颜色  
}  
</script>

在桥接类 index.uts 中,我们实现与原生能力的对接:

import { BuilderNode } from "@kit.ArkUI"  
import buffer from '@ohos.buffer';  
import { fileIo } from '@kit.CoreFileKit';  
// 导入混编实现的声明式UI构建函数  
import { buildSvg } from "./builder.ets"  
import { getEnv } from '@dcloudio/uni-runtime';  

export class NativeSvg {  
    private $element : UniNativeViewElement;  
    private builder : BuilderNode<[NativeSvgOptions]> | null = null  
    private svgMap : Map<string, string> = new Map<string, string>()  
    // 初始化 buildSvg 默认参数  
    private params : NativeSvgOptions = {  
        src: '',  
        onError: (message) => {  
            this.$element.dispatchEvent(new UniNativeViewEvent("error", { message }))  
        },  
        onComplete: (event : ESObject) => {  
            this.$element.dispatchEvent(new UniNativeViewEvent("load", {  
                width: event.width,  
                height: event.height  
            }))  
        },  
    }  

    constructor(element : UniNativeViewElement) {  
        // 绑定 wrapBuilder 函数  
        this.builder = element.bindHarmonyWrappedBuilder(wrapBuilder<[NativeSvgOptions]>(buildSvg), this.params)  
        this.$element = element  
        // 绑定当前实例为自定义的controller,方便其他地方通过 element 获取使用  
        this.$element.bindHarmonyController(this)  
    }  

    updateSrc(src : string) {  
        if (src.startsWith('data:image') || src.startsWith('<svg')) {  
            if (this.svgMap.has(src)) {  
                this.params.src = this.svgMap.get(src)!  
            } else {  
                // 处理临时文件路径  
                const tempFileName = `${Date.now()}.svg`  
                const tempDirPath = `${getEnv().TEMP_PATH}/svg`  
                const tempFilePath : string = `${tempDirPath}/${tempFileName}`  

                // 确保目录存在  
                if (!fileIo.accessSync(tempDirPath)) {  
                    fileIo.mkdirSync(tempDirPath, true)  
                }  

                // 创建并写入文件  
                const file = fileIo.openSync(tempFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);  
                // 根据不同格式保存SVG内容  
                if (src.startsWith('<svg')) {  
                    fileIo.writeSync(file.fd, src); // 直接写入XML文本  
                }  
                // 获取资源文件的原生路径  
                const path = UTSHarmony.getResourcePath(tempFilePath)  
                this.svgMap.set(src, path) // 缓存已处理的资源  
                this.params.src = path  
            }  
        }  
        else {  
            // 处理普通资源路径  
            this.params.src = UTSHarmony.getResourcePath(src)  
        }  
        this.builder?.update(this.params) // 更新渲染  
    }  

    updateColor(color : string) {  
        this.params.color = color  
        this.builder?.update(this.params) // 更新渲染  
    }  
}

看到代码中我导入了一些原生库,有小伙伴可能会好奇这些库是如何知道的。实际上,通过查阅华为开发者文档,搜索"如何创建临时文件"等相关问题,华为的智能小助手就能直接提供相关代码参考。我们可以基于这些参考代码,根据实际需求进行适当修改和调整,轻松实现临时文件创建等功能。


(cv大师就是我)

最后,在原生渲染层 builder.ets 中,我们定义实际的渲染逻辑:

@Builder  
export function buildSvg(params: ESObject) {  
    Image(params.src)  
        .width('100%')  
        .height('100%')  
        .objectFit(ImageFit.Contain)  
        .fillColor(params.color) // 支持动态修改颜色  
        .onComplete((event)=>{  
            params.onComplete(event)  
        })  
        .onError((error) =>{  
            params.onError(error.message)  
        })  
}

通过这种方式,即使是需要调用鸿蒙特定功能,也能通过UniAppX的UTS标准组件机制轻松实现。这种优雅的桥接设计,让我们既能享受Vue开发的便捷,又能在需要原生能力的关键地方获得与原生开发完全一致的能力。而这只是UniApp(X)调用鸿蒙生态能力的一种方式,在下一章中,我们将探索如何通过UTS API灵活调用OpenHarmony三方库中心仓的第三方库和鸿蒙系统自带的原生库,实现更灵活的功能扩展。

第四章:进阶实战 —— 用UTS API调用OpenHarmony三方库中心仓的第三方库 🚀

如果说组件开发是"有手就行",那么直接调用OpenHarmony生态库就是"如虎添翼"。UniApp(X)提供的UTS(Uni TypeScript)能力,不但能调用鸿蒙系统自带的原生能力,还能加载(ohpm,类似npm的包管理平台)的第三方库,让我们可以在页面中轻松访问各种系统API和三方库功能,为LimeUI组件库的功能扩展提供了无限可能。

案例背景:lime-crypto加密库开发 🔐
在开发过程中,我需要一个强大的加密功能库。虽然在传统UniApp中可以使用crypto-js库,但在UniAppX中并不支持。因此,我决定通过UTS机制直接调用发布到OpenHarmony三方库中心仓的@ohos/crypto-js第三方库来实现加密功能。

实现步骤:简单三步走 📋
只需在创建uni_modules组件时选择"创建uts插件-API插件":

生成的uni_modules组件目录结构如下,与lime-svg类似,它也遵循了UniApp(X)的插件规范:

├─pages  
│  └─index  
│     └─index.uvue  
└─uni_modules  
│  └─lime-crypto  
│     └─utssdk  
│         └─app-harmony  
│             └─config.json // 原生依赖配置  
│             └─index.uts   // UTS桥接层实现

第一步:配置原生依赖 📦
在lime-crypto/utssdk/app-harmony/config.json中声明依赖:

{  
    "dependencies": {  
        "@ohos/crypto-js": "2.0.4"  
    }  
}

就是这么简单!UniApp(X)会自动处理依赖管理。
第二步:编写 UTS 桥接层 🌉
在lime-crypto/utssdk/app-harmony/index.uts中:

import { CryptoJS } from '@ohos/crypto-js'  
export class CryptoImpl {  
    constructor() {  
        // 初始化逻辑  
    }  

    // 获取编码器  
    get enc() {  
        return {  
            Utf8: CryptoJS.enc.Utf8,  
            Hex: CryptoJS.enc.Hex,  
            Base64: CryptoJS.enc.Base64,  
            // ... 其他编码器  
        }   
    }  

    // 加密算法  
    get AES() : CryptoJS.CipherHelper {  
        return CryptoJS.AES  
    }  

    get DES(): CryptoJS.CipherHelper {  
        return CryptoJS.DES  
    }  

    // 哈希函数  
    MD5(message: string) {  
        return CryptoJS.MD5(message)  
    }  

    SHA256(message: string) {  
        return CryptoJS.SHA256(message)  
    }  

    // HMAC 签名  
    HmacSHA256(message: string, secretKey: string) {  
        return CryptoJS.HmacSHA256(message, secretKey)  
    }  
}  

export function useCrypto() {  
    return new CryptoImpl()  
}

有细心的小伙伴可能会发现,为何我要封装一个CryptoImpl类,直接导出CryptoJS不香吗?实际上,虽然直接导出CryptoJS是可行的,但封装一个专门的实现类可以更好地控制API暴露范围,提供更符合业务需求的接口,并为后续可能的功能扩展和维护提供便利(借口,凑字数而已)。

export function useCrypto() {  
    return CryptoJS //全网最简单的加密库实现(搬运工的日常)  
}

第三步:在Vue组件中使用 🎯
现在,我们可以在普通的 Vue 组件中直接使用这个原生加密库了:

<template>  
    <view class="demo-container">  
        <lime-button @click="encryptData">加密测试</lime-button>  
        <text>{{ encryptedText }}</text>  
    </view>  
</template>  

<script setup lang="uts">  
import { useCrypto } from '@/uni_modules/lime-crypto'  

const crypto = useCrypto()  
const encryptedText = ref('')  

const encryptData = () => {  
    // 使用鸿蒙原生加密库进行 AES 加密  
    const encrypted = crypto.AES.encrypt(  
        'Hello HarmonyOS',   
        'secret-key-12345',   
        {   
            mode: crypto.mode.CBC,  
            padding: crypto.pad.Pkcs7   
        }  
    )  

    encryptedText.value = encrypted.toString()  
    console.log('加密结果:', encryptedText.value)  

    // 使用 SHA256 哈希  
    const hash = crypto.SHA256('需要哈希的数据')  
    console.log('SHA256 结果:', hash.toString())  
}  
</script>

通过lime-crypto的实现,我们可以看到UniApp(X)的UTS能力真正实现了:"用前端熟悉的语法,调用原生API的便利"。就这?就这么简单!我上我也行了!成为鸿蒙开发大佬,从此走向人生的鼎峰!

开发者既能利用Vue框架的开发便捷性快速构建UI界面,又能在需要原生能力的关键地方直接调用鸿蒙原生API。这种灵活的技术架构,为鸿蒙应用开发提供了高效且强大的解决方案。

不过,当前的开发体验仍有一些可以改进的空间:ArkTS引擎在代码修改后需要重新构建、签名和安装,这增加了开发过程中的等待时间;另外,首次编译所需的时间相对较长。希望未来的版本能够优化这些方面,进一步提升开发效率。

结语:鸿蒙开发,触手可及 🌈

通过LimeUI组件库的开发实践,我深刻体会到:UniApp(X)极大地降低了鸿蒙应用开发的技术门槛。作为开发者,你完全可以利用已掌握的Vue技术栈,以熟悉的开发方式快速进入鸿蒙开发领域。

鸿蒙生态正处于高速发展阶段,对于开发者而言,这是一片充满机遇的蓝海。UniApp(X)作为连接Vue技术栈与鸿蒙生态的桥梁,为开发者提供了一条低门槛、高效率的技术路径。

对于正在观望鸿蒙开发的开发者来说,现在正是借助UniApp(X)进入鸿蒙生态的理想时机。带着你熟悉的技术积累,你会发现鸿蒙开发并非想象中那样困难,而是可以通过现有技能平稳过渡的技术领域。

毕竟,能用熟悉的技术拥抱未来,这本身就是一件超酷的事,不是吗?💻✨

正是借助UniApp(X)的强大生态与开发便利性,我开发的LimeUI组件库中每个组件都作为独立插件上传到UniApp市场,目前已成功开源发布超过100款组件插件,为开发者社区贡献自己的一份力量!🚀

LimeUI组件库还同时支持UniApp和UniAppX双框架,让开发者可以在两个技术栈中无缝使用同一套组件,极大地提升了开发效率和代码复用性!

如果您在使用过程中发现组件库有任何不完善或缺少的组件,请随时在插件市场留言您的需求和建议。每一条反馈都是促使LimeUI不断完善的宝贵动力,我会认真对待并持续优化组件库,为开发者提供更好的使用体验!

如果您觉得我写的内容对您有所帮助,欢迎点赞加关注,一键三连支持!我第一次写文章,如果有不足之处,还请各位轻喷,我会不断学习和进步!

欢迎访问我的插件市场主页:https://ext.dcloud.net.cn/publisher?id=242774

收起阅读 »

一个人用 uni-app 做鸿蒙日语学习 App 的踩坑之旅

鸿蒙征文

一个人用 uni-app 做鸿蒙日语学习 App 的踩坑之旅

APP名为:「日语词账」

鸿蒙市场地址:日语词账

缘起:为什么要做这个应用

说起来有点巧合。我学日语断断续续好几年了,试过很多背单词的 App,总觉得差点意思——要么功能太简单,要么塞满了广告,要么离线就废了。去年听说鸿蒙系统要独立成生态,我就想,要不自己搞一个?正好练练手,也给自己学日语用。

就这样,"日语词账"这个项目在我的业余时间里慢慢成型了。从去年 10 月开始写第一行代码,到今年 3 月在华为应用市场上架,前后花了大概半年时间。期间踩了不少坑,也学到了很多东西。今天就来聊聊这个过程。

技术选型:为什么选 uni-app

一开始我是纠结的。做鸿蒙应用,到底该用什么技术?

原生开发我是不敢想的,一个人的精力有限,iOS、Android、鸿蒙三套代码,想想就头大。Flutter 我看了看,鸿蒙支持还不够成熟,社区资源也少。React Native 我倒是熟悉,但对鸿蒙的适配好像也不太行。

最后选了 uni-app,主要是看中了它的跨平台能力。DCloud 团队对鸿蒙的支持确实做得不错,而且我本身就用 Vue,上手很快。更重要的是,写一套代码就能同时跑在多个平台上,这对个人开发者来说太友好了。

功能规划:想清楚再动手

一开始我就给自己定了个原则:功能不求多,但要实用。作为一个学日语的人,我太清楚学习 App 需要什么了。

首先是词汇库。我从网上找了各种资源,最后整理出了 12000 多条词汇,从 N5 到 N1 都有。这个过程挺耗时的,光是数据清洗和格式统一就花了两周。但这是基础,必须做好。

然后是语法。日语语法说复杂也复杂,说简单也简单。我收集了 500 多条常用语法点,每条都配上例句和解释。这个主要是方便自己查询用,毕竟学着学着总会忘。

发音练习这块,我本来想直接用 TTS(文字转语音)技术的,后来发现效果不太好。尤其是五十音,机器合成的声音总感觉怪怪的。最后我找了 103 个高质量的 MP3 音频文件,每个假名都是真人录的,虽然增加了包体积,但效果确实好很多。

测验系统是后来加的。我自己学的时候发现,光看不练很容易忘,必须得有个测验功能来巩固。做了个简单的选择题系统,可以选不同的题量,随机出题,答完立马知道对错。这个功能现在我自己用得最多。

鸿蒙适配:第一次真正的挑战

说实话,在做这个项目之前,我从来没碰过鸿蒙开发。网上的资料也不算特别多,心里还是有点虚的。不过好在 uni-app 把很多底层的东西都封装好了,上手倒也不算太难。

语音这个大坑

最头疼的是语音功能。日语学习 App 没有发音功能,那基本就是个废品。但问题是,不同平台的语音 API 差别太大了,鸿蒙又是个新系统,很多东西都不确定。

我最开始想用 Web Speech API,结果在鸿蒙上根本不work。然后又试了华为的 HMS AI TTS,需要配置一堆东西,还得联网下载语音包,体验不太好。最后我找到了鸿蒙的系统原生 TTS 接口,这个算是比较靠谱的,但也不是百分百可用。

后来我就想了个办法:五十音这些基础发音,我全部用预置的 MP3 文件;复杂的单词句子,再用 TTS 合成。这样既保证了基础发音的质量,又不会让包体积太大。而且万一 TTS 不可用,至少基础功能还在。

这个方案看起来简单,实际做起来挺麻烦的。要判断当前是什么平台,要检测 TTS 是否可用,要处理各种异常情况。不过最后效果还不错,至少我自己用的时候没出过什么问题。

证书签名的折磨

鸿蒙应用必须签名才能装,这个证书配置真的是折磨人。我第一次打包的时候,证书路径配错了,一直提示签名失败。改了半天才发现是相对路径和绝对路径的问题。

还有一次更离谱,我在华为开发者平台上创建了应用,拿到了证书,结果打包的时候又说 bundleName 不匹配。原来是我在不同地方填的包名不一致,有的是点号分隔,有的是下划线。统一改成 com.******.JapaneseWords 之后才搞定。

建议大家一定要保留好证书文件,而且要注意有效期。我有一次差点因为证书过期了无法更新版本,幸好提前发现了。

性能优化的必要性

12000 多条词汇,一次性全加载进来,手机直接卡死。这个问题我也是吃了亏才学乖的。

后来改成分页加载,每次只显示 30 条,往下滑的时候再加载更多。这样首屏速度快多了,从原来的 3 秒多降到了不到 1 秒。虽然实现起来多了点代码,但用户体验提升还是很明显的。

还有就是数据缓存。词汇数据是不会变的,没必要每次都重新加载。我加了个简单的缓存机制,第一次加载完就存在本地,下次直接读缓存。这个优化也很有效。

上架的酸甜苦辣

等代码写得差不多了,就该上架了。这个过程说快也快,说慢也慢,主要看运气。

首先要在华为开发者平台注册账号,创建应用。这一步倒是不难,就是填表格,上传图标、截图什么的。图标我自己用 Figma 画的,简简单单一个日语五十音的"あ"字,配上渐变色,看起来还挺专业。

截图是个细节活。我特意选了几个最能展示功能的页面,词汇学习、发音练习、测验系统,每个都截得清清楚楚。还专门买了个模拟器,确保截图尺寸标准,没有状态栏杂乱的信息。

然后就是隐私政策。这个是必须的,哪怕你的 App 压根不收集用户数据。我就用 GitHub Pages 搭了个简单的隐私政策页面,说明应用不收集任何个人信息,所有数据都存在本地。链接填上去就行。

提交审核的时候,心里还是有点紧张的。第一次提交,过了两天被打回来了,说是权限说明不够详细。我赶紧补充了权限使用说明,说明白为什么需要网络权限(在线 TTS)、为什么需要麦克风权限(未来可能加语音识别)。第二次提交,又等了三天,终于通过了!

看到应用状态变成"已上架"的那一刻,说实话挺激动的。虽然只是个小小的学习工具,但毕竟是自己从零到一做出来的。

上架后的意外收获

应用上架一周后,下载量就破百了。虽然不多,但对个人开发者来说已经很满足了。更让我惊喜的是,有用户给我留言,说这个 App 很实用,离线能用特别好,界面也挺干净。

还有人问能不能加词组功能,能不能加听力练习。这些反馈让我意识到,原来真的有人在用我做的东西,而且还有改进的空间。这种感觉挺奇妙的。

当然也有bug反馈。有个用户说测验系统偶尔会重复出题,我查了半天才发现是随机算法的问题。还有人说暗色主题的某些文字看不清,我赶紧改了颜色对比度。

这些反馈虽然有时候挺头疼的,但也让应用越来越好。到现在我还在坚持更新,每个月至少修一两个bug,加一点小功能。

一些经验和感悟

做完这个项目,回头看看,确实学到了不少东西。

关于技术选型,我觉得 uni-app 对个人开发者真的很友好。虽然有人说它性能不如原生,但对于工具类、内容类应用来说完全够用。而且一套代码多端运行,这个优势太明显了。我现在这个 App,不仅跑在鸿蒙上,iOS 和 Android 也没问题,微信小程序版本我也在计划中。

关于鸿蒙开发,其实没有想象中那么难。很多人觉得鸿蒙是新系统,资料少,不敢尝试。但实际上,华为的文档还是挺全的,DCloud 社区也很活跃,遇到问题基本都能找到答案。而且鸿蒙生态还在快速发展,现在入局正是好时机。

关于产品设计,我学到最大的一点就是:功能要简单,体验要好。我一开始想加很多功能,听力、阅读、作文,什么都想做。后来发现根本做不过来,而且用户也不一定需要。不如把核心功能做扎实,让用户真正能用起来。

关于性能优化,这个真的不能忽视。我一开始觉得,12000 条数据也不算多吧,结果直接卡死。后来才知道,前端优化是门学问,分页加载、虚拟列表、懒加载,这些都是必须的。现在应用打开速度快了,用户体验也好了很多。

还有一点,就是要舍得删代码。我写了很多功能,后来发现有些根本没人用,反而让应用变复杂了。后来狠心删了一些,应用反而更简洁了。

给想做鸿蒙应用的朋友一些建议

如果你也想做鸿蒙应用,我的建议是:别想太多,直接开始!

技术方面,如果你会 Vue,uni-app 上手很快。不会也没关系,uni-app 官网教程挺详细的,跟着做一遍基本就能上手了。

工具方面,HBuilderX 是必备的,虽然有点笨重,但鸿蒙开发离不开它。真机调试最好准备一台鸿蒙手机,模拟器有时候不太靠谱。

资源方面,多逛逛 DCloud 社区论坛,很多问题都有前人踩过坑。GitHub 上也有不少开源项目可以参考。遇到问题别憋着,该提 issue 提 issue,该发帖发帖。

心态方面,做个人项目不要给自己太大压力。能做成最好,做不成也当学习了。我这个项目断断续续做了半年,中间也有好几次想放弃,最后还是坚持下来了。

未来的计划

这个应用目前还在持续更新中。短期内,我打算加上语音识别功能,让发音练习可以自动评分。再优化一下 UI,现在看起来还是有点朴素。

长期来看,我想做个听力练习模块,再加一个阅读理解功能。如果用户量上来了,可能还会考虑做个 Web 版,让大家在电脑上也能用。

至于商业化,暂时还没想那么多。我更希望这是一个真正对学习者有帮助的工具,而不是一个赚钱的产品。如果哪天用户量真的起来了,再考虑做个会员功能,加一些高级特性也不迟。

写在最后

从去年 10 月到现在,"日语词账"这个项目陪我度过了好多个周末和深夜。有时候改一个bug改到凌晨两点,有时候为了一个功能想破头。但看到应用慢慢成型,看到有人在用,这种成就感真的无法形容。

做独立开发者不容易,但也很有乐趣。如果你也有什么想法,不妨试试看。鸿蒙生态还在发展,机会很多。uni-app 让跨平台开发变得简单,个人开发者也能做出不错的产品。

最重要的是,开始行动。不要等到一切都准备好了才开始,边做边学,边学边改,慢慢就会越来越好。

我的这个应用已经在 GitHub 开源了,代码、数据都在上面。如果你感兴趣,可以去看看。如果有什么问题,也欢迎交流。

一起加油吧!

写于2025年11月,深圳,一个普通的周末下午

继续阅读 »

一个人用 uni-app 做鸿蒙日语学习 App 的踩坑之旅

APP名为:「日语词账」

鸿蒙市场地址:日语词账

缘起:为什么要做这个应用

说起来有点巧合。我学日语断断续续好几年了,试过很多背单词的 App,总觉得差点意思——要么功能太简单,要么塞满了广告,要么离线就废了。去年听说鸿蒙系统要独立成生态,我就想,要不自己搞一个?正好练练手,也给自己学日语用。

就这样,"日语词账"这个项目在我的业余时间里慢慢成型了。从去年 10 月开始写第一行代码,到今年 3 月在华为应用市场上架,前后花了大概半年时间。期间踩了不少坑,也学到了很多东西。今天就来聊聊这个过程。

技术选型:为什么选 uni-app

一开始我是纠结的。做鸿蒙应用,到底该用什么技术?

原生开发我是不敢想的,一个人的精力有限,iOS、Android、鸿蒙三套代码,想想就头大。Flutter 我看了看,鸿蒙支持还不够成熟,社区资源也少。React Native 我倒是熟悉,但对鸿蒙的适配好像也不太行。

最后选了 uni-app,主要是看中了它的跨平台能力。DCloud 团队对鸿蒙的支持确实做得不错,而且我本身就用 Vue,上手很快。更重要的是,写一套代码就能同时跑在多个平台上,这对个人开发者来说太友好了。

功能规划:想清楚再动手

一开始我就给自己定了个原则:功能不求多,但要实用。作为一个学日语的人,我太清楚学习 App 需要什么了。

首先是词汇库。我从网上找了各种资源,最后整理出了 12000 多条词汇,从 N5 到 N1 都有。这个过程挺耗时的,光是数据清洗和格式统一就花了两周。但这是基础,必须做好。

然后是语法。日语语法说复杂也复杂,说简单也简单。我收集了 500 多条常用语法点,每条都配上例句和解释。这个主要是方便自己查询用,毕竟学着学着总会忘。

发音练习这块,我本来想直接用 TTS(文字转语音)技术的,后来发现效果不太好。尤其是五十音,机器合成的声音总感觉怪怪的。最后我找了 103 个高质量的 MP3 音频文件,每个假名都是真人录的,虽然增加了包体积,但效果确实好很多。

测验系统是后来加的。我自己学的时候发现,光看不练很容易忘,必须得有个测验功能来巩固。做了个简单的选择题系统,可以选不同的题量,随机出题,答完立马知道对错。这个功能现在我自己用得最多。

鸿蒙适配:第一次真正的挑战

说实话,在做这个项目之前,我从来没碰过鸿蒙开发。网上的资料也不算特别多,心里还是有点虚的。不过好在 uni-app 把很多底层的东西都封装好了,上手倒也不算太难。

语音这个大坑

最头疼的是语音功能。日语学习 App 没有发音功能,那基本就是个废品。但问题是,不同平台的语音 API 差别太大了,鸿蒙又是个新系统,很多东西都不确定。

我最开始想用 Web Speech API,结果在鸿蒙上根本不work。然后又试了华为的 HMS AI TTS,需要配置一堆东西,还得联网下载语音包,体验不太好。最后我找到了鸿蒙的系统原生 TTS 接口,这个算是比较靠谱的,但也不是百分百可用。

后来我就想了个办法:五十音这些基础发音,我全部用预置的 MP3 文件;复杂的单词句子,再用 TTS 合成。这样既保证了基础发音的质量,又不会让包体积太大。而且万一 TTS 不可用,至少基础功能还在。

这个方案看起来简单,实际做起来挺麻烦的。要判断当前是什么平台,要检测 TTS 是否可用,要处理各种异常情况。不过最后效果还不错,至少我自己用的时候没出过什么问题。

证书签名的折磨

鸿蒙应用必须签名才能装,这个证书配置真的是折磨人。我第一次打包的时候,证书路径配错了,一直提示签名失败。改了半天才发现是相对路径和绝对路径的问题。

还有一次更离谱,我在华为开发者平台上创建了应用,拿到了证书,结果打包的时候又说 bundleName 不匹配。原来是我在不同地方填的包名不一致,有的是点号分隔,有的是下划线。统一改成 com.******.JapaneseWords 之后才搞定。

建议大家一定要保留好证书文件,而且要注意有效期。我有一次差点因为证书过期了无法更新版本,幸好提前发现了。

性能优化的必要性

12000 多条词汇,一次性全加载进来,手机直接卡死。这个问题我也是吃了亏才学乖的。

后来改成分页加载,每次只显示 30 条,往下滑的时候再加载更多。这样首屏速度快多了,从原来的 3 秒多降到了不到 1 秒。虽然实现起来多了点代码,但用户体验提升还是很明显的。

还有就是数据缓存。词汇数据是不会变的,没必要每次都重新加载。我加了个简单的缓存机制,第一次加载完就存在本地,下次直接读缓存。这个优化也很有效。

上架的酸甜苦辣

等代码写得差不多了,就该上架了。这个过程说快也快,说慢也慢,主要看运气。

首先要在华为开发者平台注册账号,创建应用。这一步倒是不难,就是填表格,上传图标、截图什么的。图标我自己用 Figma 画的,简简单单一个日语五十音的"あ"字,配上渐变色,看起来还挺专业。

截图是个细节活。我特意选了几个最能展示功能的页面,词汇学习、发音练习、测验系统,每个都截得清清楚楚。还专门买了个模拟器,确保截图尺寸标准,没有状态栏杂乱的信息。

然后就是隐私政策。这个是必须的,哪怕你的 App 压根不收集用户数据。我就用 GitHub Pages 搭了个简单的隐私政策页面,说明应用不收集任何个人信息,所有数据都存在本地。链接填上去就行。

提交审核的时候,心里还是有点紧张的。第一次提交,过了两天被打回来了,说是权限说明不够详细。我赶紧补充了权限使用说明,说明白为什么需要网络权限(在线 TTS)、为什么需要麦克风权限(未来可能加语音识别)。第二次提交,又等了三天,终于通过了!

看到应用状态变成"已上架"的那一刻,说实话挺激动的。虽然只是个小小的学习工具,但毕竟是自己从零到一做出来的。

上架后的意外收获

应用上架一周后,下载量就破百了。虽然不多,但对个人开发者来说已经很满足了。更让我惊喜的是,有用户给我留言,说这个 App 很实用,离线能用特别好,界面也挺干净。

还有人问能不能加词组功能,能不能加听力练习。这些反馈让我意识到,原来真的有人在用我做的东西,而且还有改进的空间。这种感觉挺奇妙的。

当然也有bug反馈。有个用户说测验系统偶尔会重复出题,我查了半天才发现是随机算法的问题。还有人说暗色主题的某些文字看不清,我赶紧改了颜色对比度。

这些反馈虽然有时候挺头疼的,但也让应用越来越好。到现在我还在坚持更新,每个月至少修一两个bug,加一点小功能。

一些经验和感悟

做完这个项目,回头看看,确实学到了不少东西。

关于技术选型,我觉得 uni-app 对个人开发者真的很友好。虽然有人说它性能不如原生,但对于工具类、内容类应用来说完全够用。而且一套代码多端运行,这个优势太明显了。我现在这个 App,不仅跑在鸿蒙上,iOS 和 Android 也没问题,微信小程序版本我也在计划中。

关于鸿蒙开发,其实没有想象中那么难。很多人觉得鸿蒙是新系统,资料少,不敢尝试。但实际上,华为的文档还是挺全的,DCloud 社区也很活跃,遇到问题基本都能找到答案。而且鸿蒙生态还在快速发展,现在入局正是好时机。

关于产品设计,我学到最大的一点就是:功能要简单,体验要好。我一开始想加很多功能,听力、阅读、作文,什么都想做。后来发现根本做不过来,而且用户也不一定需要。不如把核心功能做扎实,让用户真正能用起来。

关于性能优化,这个真的不能忽视。我一开始觉得,12000 条数据也不算多吧,结果直接卡死。后来才知道,前端优化是门学问,分页加载、虚拟列表、懒加载,这些都是必须的。现在应用打开速度快了,用户体验也好了很多。

还有一点,就是要舍得删代码。我写了很多功能,后来发现有些根本没人用,反而让应用变复杂了。后来狠心删了一些,应用反而更简洁了。

给想做鸿蒙应用的朋友一些建议

如果你也想做鸿蒙应用,我的建议是:别想太多,直接开始!

技术方面,如果你会 Vue,uni-app 上手很快。不会也没关系,uni-app 官网教程挺详细的,跟着做一遍基本就能上手了。

工具方面,HBuilderX 是必备的,虽然有点笨重,但鸿蒙开发离不开它。真机调试最好准备一台鸿蒙手机,模拟器有时候不太靠谱。

资源方面,多逛逛 DCloud 社区论坛,很多问题都有前人踩过坑。GitHub 上也有不少开源项目可以参考。遇到问题别憋着,该提 issue 提 issue,该发帖发帖。

心态方面,做个人项目不要给自己太大压力。能做成最好,做不成也当学习了。我这个项目断断续续做了半年,中间也有好几次想放弃,最后还是坚持下来了。

未来的计划

这个应用目前还在持续更新中。短期内,我打算加上语音识别功能,让发音练习可以自动评分。再优化一下 UI,现在看起来还是有点朴素。

长期来看,我想做个听力练习模块,再加一个阅读理解功能。如果用户量上来了,可能还会考虑做个 Web 版,让大家在电脑上也能用。

至于商业化,暂时还没想那么多。我更希望这是一个真正对学习者有帮助的工具,而不是一个赚钱的产品。如果哪天用户量真的起来了,再考虑做个会员功能,加一些高级特性也不迟。

写在最后

从去年 10 月到现在,"日语词账"这个项目陪我度过了好多个周末和深夜。有时候改一个bug改到凌晨两点,有时候为了一个功能想破头。但看到应用慢慢成型,看到有人在用,这种成就感真的无法形容。

做独立开发者不容易,但也很有乐趣。如果你也有什么想法,不妨试试看。鸿蒙生态还在发展,机会很多。uni-app 让跨平台开发变得简单,个人开发者也能做出不错的产品。

最重要的是,开始行动。不要等到一切都准备好了才开始,边做边学,边学边改,慢慢就会越来越好。

我的这个应用已经在 GitHub 开源了,代码、数据都在上面。如果你感兴趣,可以去看看。如果有什么问题,也欢迎交流。

一起加油吧!

写于2025年11月,深圳,一个普通的周末下午

收起阅读 »

【鸿蒙征文】 炸裂!我用uni-app三天让旧应用通杀鸿蒙Next+元服务,华为商店已上架!2W奖励金即将到账。

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

就在其他开发者还在为鸿蒙适配焦头烂额时,我的多款uni-app旧应用已经悄然上架华为应用商店。更让人惊喜的是,连鸿蒙元服务都一并搞定!

一、缘起:鸿蒙风暴前的抉择

9月,华为宣布鸿蒙NEXT不再兼容安卓,整个互联网圈炸开了锅。我的心情和大家一样复杂——手头有10+款用uni-app开发的应用,做的最成功的应用已经在微信小程序、安卓和iOS稳定运行了8年多,累积获得用户总量50+万,服务厂商接近200个。为了我这50+万的粉丝,我必须继续披荆斩棘,用最快的速度适配鸿蒙Next,任务的紧急程度已经迫在眉睫,哪怕这50万人只有10个人用鸿蒙Next系统,我也要让他们拥有原生鸿蒙的最佳体验。问题来了,难道我真的要全部重写整个应用?噢,我滴神呀!救救我吧。

哈哈,,,转机来得比想象中更快。

作为uni-app的资深老玩家,当然是时刻追踪着uni-app适配鸿蒙的进度,期间也在不断学习原生鸿蒙ArkTS语言,但是作为Vue重度使用者(本人已使用Vue长达8年),ArkTS的语法的确让人感到繁杂,虽然都能看懂,无非还是前端的那些东西,但是真正写起来,还是不够舒畅。于是,我决定赌一把,继续使用uni-app去完成所有鸿蒙Next的适配,结果令人震惊:第一个应用仅仅只用了3天,最短的甚至不到8小时,就完成了从原有应用到鸿蒙NEXT+元服务的全适配!

二、实战全记录:72小时创造奇迹

第一阶段:环境搭建(2小时)

说实话,最开始我是忐忑的。但整个过程出乎意料的顺畅:

开发工具:

  • HUAWEI DevEco Studio
  • uni-app v3.99+(支持鸿蒙NEXT)
  • 现有项目直接导入

安装工具还是比较快的,就是下载工具和手机端模拟器比较费时间,这就占用了1个小时,接下来就是创建一个新应用,先把工具跑起来,模拟器跑起来。走一个空白的Hello Word的空项目。

如果是window系统,需要开启虚拟化Hyper-V 、window虚拟机监控平台的相关配置,如果出错了也会有对应的提示,不得不说DevEco工具还是非常人性化的。赞一个!

第二阶段:证书申请(30分钟)

这是最关键的也是最简单,如果申请过苹果的证书,就会非常得心应手。
4大证书分别是:

  1. p12:使用华为开发者工具DevEco 即可创建该证书,该证书不区分开发证书和生产证书。
  2. csr:使用华为开发者工具DevEco 即可创建该证书,该证书不区分开发证书和生产证书。
  3. cer:打开AGC平台需要登陆华为账号进行申请。在证书、APPID 和Profile菜单下操作,需要区分开发和生产证书类型。
  4. p7b:打开AGC平台需要登陆华为账号进行申请。该证书是根据应用来申请的。

申请p12和csr证书截图:

申请cer和p7b证书截图:

在HBX里面的完整配置

证书部分到此结束,还是非常简单滴!主要我这边是从注册账户到注册应用,再到注册证书,所以耗时多一点,如果再添加一个新应用的配置,那就是分分钟钟的事情了。

第三阶段:代码适配(核心8小时)

这里可能是大家最关心的部分——到底要改多少代码?

答案是:少得惊人!

我以一个物流类型的应用为案例进行举例,主要修改点包括:

  1. 登录模块:用华为账户替换以前的注册,华为账户和业务账户的绑定。
  2. 权限模块:调用华为的API获取手机号
  3. 账户注销:所有上架的应用都需要支持用户自主取消关联和账户注销。
  4. 分享功能:适配华为分享

一、登录模块主要调整的原因是:华为要求统一使用华为账户进行静默登录,那么如果以前已经有账户了,那就需要进行做关联即可。使用华为提供的code通过后台接口获取到对应的UnionID,OpenID,通过这些就可以换到手机号了,手机号的权限需要在后台开通,这个申请比较慢,说个小技巧提交申请后,立马就挂个工单,这样就审核的特别快了。

其实很多路子和微信小程序很相似,只是需要后端的配合,支持调用华为的API才行。

// 修改前:微信小程序端  

uni.login({  
  provider: 'weixin', //使用微信登录  
  success: function (loginRes) {  
    console.log(loginRes.authResult);  
  }  
});  

// 修改后:支持鸿蒙获取账户 获取后走接口拿手机号  
uni.login({  
  success: function (loginRes) {  
    console.log(loginRes.authResult);  
  }  
});  

二、权限模块的调整主要是为了满足在华为手机上能有拍照权限,坐标权限,这些自行申请,也都是比较简单的。

三、关于账户注销,这个还是要求必须得有的,其实,这个更简单,上架过iOS的都知道,这个是必须的,基本上无需调整,咱们这里其实也就是改成解除和华为账户的关联关系。
这里就不多说了。

四、分享等调用微信相关的,都需要根据鸿蒙的环境改成调用鸿蒙的即可。这个就需要大面积的排查代码了。简单是简单就是多,所以费时间。

// 修改前:通用分享  
uni.share({  
  provider: "weixin",  
  scene: "WXSceneSession",  
  type: 0,  
  title: "分享标题",  
  success: function (res) {  
    console.log("success:" + JSON.stringify(res));  
  }  
});  

// 修改后:鸿蒙分享  
uni.share({  
  provider: "harmony",  
  type: 0,  
  title: "分享标题",  
  success: function (res) {  
    console.log("鸿蒙分享成功:" + JSON.stringify(res));  
  }  
});

工作量统计:

  • 登录模块:2小时
  • 权限调试:2小时
  • 账户注销:1小时
  • 其他权限和证书适配打包APP:3小时
  • 总计:8小时

其实总结一句话:代码大改的地方真的很少,很少。

建议弟兄们,赶紧上手吧!

第四阶段:调试上架(8小时)

真机调试让我眼前一亮。鸿蒙设备的流畅度确实出色,应用启动速度比安卓版本就是快,用的华为navo12 真机运行非常流畅。

上架过程同样顺畅:

  • 华为开发者账号注册:1小时
  • 应用信息填写:1小时
  • 打包提交审核:1小时
  • 审核通过:一天会审核多次(比苹果快太多了!)

三、意外之喜:元服务的惊喜邂逅

在适配过程中,我发现鸿蒙元服务这个概念很有意思。简单来说,它让用户不用下载完整APP就能使用核心功能。

关键发现:uni-app对元服务的支持几乎是开箱即用!

// 在pages.json中配置元服务页面  
{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "isEntry": true,  
        "isAtomic": true  // 标记为元服务页面  
      }  
    }  
  ]  
}

我的一个工具类应用,通过元服务实现了:

  • 用户无需安装即可使用核心计算功能
  • 服务卡片直接展示关键信息
  • 转化率提升明显

四、技术对比:uni-app的降维打击

用过uni-app 开发的朋友都知道,他最大的优势就是全终端兼容,安卓+iOS+小程序+鸿蒙,可以说是天下无敌了,一套代码,真的是省心省力,真的是早用早享受。只要你会点前端,懂Vue,就没有任何技术难度。
当然为了让大家更直观地理解,我做了个对比表:

特性 原生鸿蒙开发 uni-app鸿蒙适配
学习成本 需要学习ArkTS等新技术 使用熟悉的vue语法
代码复用 从零开始 90%+代码可直接复用
开发周期 2-4周/应用 1-3天/应用
多端支持 仅鸿蒙 同时支持小程序、iOS、安卓
维护成本 独立代码库 统一代码库

最让我震撼的是: 旧应用的适配工作远比你担心的要少太多了,只要能跑起来,运行起来,就基本上没啥问题。在这里必须感谢一下uni-app 团队的技术支持,我在适配过程中,有专门的钉钉群提供技术服务,这也是我为何能在短短3天时间就能解决所有兼容的关键。在此一并感谢整个团队!

五、避坑指南:实战中遇到的坑

当然,过程并非完全一帆风顺。记录几个可能帮到大家的坑:

  1. 图片路径问题:鸿蒙对绝对路径更敏感,建议使用相对路径
  2. CSS兼容性:部分CSS3特性需要添加鸿蒙前缀
  3. API异步处理:鸿蒙的API调用更强调异步编程
  4. 代码运行不生效:如果感觉代码没问题,热更新不生效,记得重启整个应用,甚至重启HBX,重启模拟器,有时缓存问题不得不说很难受。
// 推荐使用async/await处理鸿蒙API  
async function initHarmonyFeatures() {  
  try {  
    const result = await uni.harmony.someAPI();  
    // 处理结果  
  } catch (error) {  
    console.error('API调用失败:', error);  
  }  
}

六、成果展示:数字说话

截至目前,我的6款应用全部成功上架:

  • 🎯 适配成功率:100%(6/6)
  • 最快适配记录:8小时(旧的成熟应用)
  • 🚀 平均适配时间:2天/应用
  • 💰 成本节约:相比原生开发,节省约85%成本
  • 📈 用户增长:鸿蒙渠道日均新增用户300+

七、给开发者同仁的真诚建议

  1. 立即行动:鸿蒙生态红利期就在眼前
  2. 从简单应用开始:先拿一个功能简单的应用试水
  3. 关注元服务:这是鸿蒙的独特优势,不要错过
  4. 利用uni-app社区:遇到的问题基本都能找到解决方案

结语:一个人的速度,一个时代的变革

有开发者朋友问我:"现在入场是不是太晚了?"

我的回答是:"当你知道的时候,就是最好的时机。"

uni-app + 鸿蒙的组合,让我在技术变革的浪潮中抓住了先机。 从焦虑观望到全面上架,只用了不到一周时间。这种效率,在过去的移动开发史上是从未有过的。

现在,轮到你了。

在这里顺带解释一下奖励金,华为最近的活动,只要上架成功,有活跃用户,或者参加uni-app的活动,都是可以拿到奖励金的。大家加油!

在得瑟下我们的用户量;


【实战资源分享】

欢迎在评论区交流适配经验,我会第一时间回复大家的问题!

继续阅读 »

就在其他开发者还在为鸿蒙适配焦头烂额时,我的多款uni-app旧应用已经悄然上架华为应用商店。更让人惊喜的是,连鸿蒙元服务都一并搞定!

一、缘起:鸿蒙风暴前的抉择

9月,华为宣布鸿蒙NEXT不再兼容安卓,整个互联网圈炸开了锅。我的心情和大家一样复杂——手头有10+款用uni-app开发的应用,做的最成功的应用已经在微信小程序、安卓和iOS稳定运行了8年多,累积获得用户总量50+万,服务厂商接近200个。为了我这50+万的粉丝,我必须继续披荆斩棘,用最快的速度适配鸿蒙Next,任务的紧急程度已经迫在眉睫,哪怕这50万人只有10个人用鸿蒙Next系统,我也要让他们拥有原生鸿蒙的最佳体验。问题来了,难道我真的要全部重写整个应用?噢,我滴神呀!救救我吧。

哈哈,,,转机来得比想象中更快。

作为uni-app的资深老玩家,当然是时刻追踪着uni-app适配鸿蒙的进度,期间也在不断学习原生鸿蒙ArkTS语言,但是作为Vue重度使用者(本人已使用Vue长达8年),ArkTS的语法的确让人感到繁杂,虽然都能看懂,无非还是前端的那些东西,但是真正写起来,还是不够舒畅。于是,我决定赌一把,继续使用uni-app去完成所有鸿蒙Next的适配,结果令人震惊:第一个应用仅仅只用了3天,最短的甚至不到8小时,就完成了从原有应用到鸿蒙NEXT+元服务的全适配!

二、实战全记录:72小时创造奇迹

第一阶段:环境搭建(2小时)

说实话,最开始我是忐忑的。但整个过程出乎意料的顺畅:

开发工具:

  • HUAWEI DevEco Studio
  • uni-app v3.99+(支持鸿蒙NEXT)
  • 现有项目直接导入

安装工具还是比较快的,就是下载工具和手机端模拟器比较费时间,这就占用了1个小时,接下来就是创建一个新应用,先把工具跑起来,模拟器跑起来。走一个空白的Hello Word的空项目。

如果是window系统,需要开启虚拟化Hyper-V 、window虚拟机监控平台的相关配置,如果出错了也会有对应的提示,不得不说DevEco工具还是非常人性化的。赞一个!

第二阶段:证书申请(30分钟)

这是最关键的也是最简单,如果申请过苹果的证书,就会非常得心应手。
4大证书分别是:

  1. p12:使用华为开发者工具DevEco 即可创建该证书,该证书不区分开发证书和生产证书。
  2. csr:使用华为开发者工具DevEco 即可创建该证书,该证书不区分开发证书和生产证书。
  3. cer:打开AGC平台需要登陆华为账号进行申请。在证书、APPID 和Profile菜单下操作,需要区分开发和生产证书类型。
  4. p7b:打开AGC平台需要登陆华为账号进行申请。该证书是根据应用来申请的。

申请p12和csr证书截图:

申请cer和p7b证书截图:

在HBX里面的完整配置

证书部分到此结束,还是非常简单滴!主要我这边是从注册账户到注册应用,再到注册证书,所以耗时多一点,如果再添加一个新应用的配置,那就是分分钟钟的事情了。

第三阶段:代码适配(核心8小时)

这里可能是大家最关心的部分——到底要改多少代码?

答案是:少得惊人!

我以一个物流类型的应用为案例进行举例,主要修改点包括:

  1. 登录模块:用华为账户替换以前的注册,华为账户和业务账户的绑定。
  2. 权限模块:调用华为的API获取手机号
  3. 账户注销:所有上架的应用都需要支持用户自主取消关联和账户注销。
  4. 分享功能:适配华为分享

一、登录模块主要调整的原因是:华为要求统一使用华为账户进行静默登录,那么如果以前已经有账户了,那就需要进行做关联即可。使用华为提供的code通过后台接口获取到对应的UnionID,OpenID,通过这些就可以换到手机号了,手机号的权限需要在后台开通,这个申请比较慢,说个小技巧提交申请后,立马就挂个工单,这样就审核的特别快了。

其实很多路子和微信小程序很相似,只是需要后端的配合,支持调用华为的API才行。

// 修改前:微信小程序端  

uni.login({  
  provider: 'weixin', //使用微信登录  
  success: function (loginRes) {  
    console.log(loginRes.authResult);  
  }  
});  

// 修改后:支持鸿蒙获取账户 获取后走接口拿手机号  
uni.login({  
  success: function (loginRes) {  
    console.log(loginRes.authResult);  
  }  
});  

二、权限模块的调整主要是为了满足在华为手机上能有拍照权限,坐标权限,这些自行申请,也都是比较简单的。

三、关于账户注销,这个还是要求必须得有的,其实,这个更简单,上架过iOS的都知道,这个是必须的,基本上无需调整,咱们这里其实也就是改成解除和华为账户的关联关系。
这里就不多说了。

四、分享等调用微信相关的,都需要根据鸿蒙的环境改成调用鸿蒙的即可。这个就需要大面积的排查代码了。简单是简单就是多,所以费时间。

// 修改前:通用分享  
uni.share({  
  provider: "weixin",  
  scene: "WXSceneSession",  
  type: 0,  
  title: "分享标题",  
  success: function (res) {  
    console.log("success:" + JSON.stringify(res));  
  }  
});  

// 修改后:鸿蒙分享  
uni.share({  
  provider: "harmony",  
  type: 0,  
  title: "分享标题",  
  success: function (res) {  
    console.log("鸿蒙分享成功:" + JSON.stringify(res));  
  }  
});

工作量统计:

  • 登录模块:2小时
  • 权限调试:2小时
  • 账户注销:1小时
  • 其他权限和证书适配打包APP:3小时
  • 总计:8小时

其实总结一句话:代码大改的地方真的很少,很少。

建议弟兄们,赶紧上手吧!

第四阶段:调试上架(8小时)

真机调试让我眼前一亮。鸿蒙设备的流畅度确实出色,应用启动速度比安卓版本就是快,用的华为navo12 真机运行非常流畅。

上架过程同样顺畅:

  • 华为开发者账号注册:1小时
  • 应用信息填写:1小时
  • 打包提交审核:1小时
  • 审核通过:一天会审核多次(比苹果快太多了!)

三、意外之喜:元服务的惊喜邂逅

在适配过程中,我发现鸿蒙元服务这个概念很有意思。简单来说,它让用户不用下载完整APP就能使用核心功能。

关键发现:uni-app对元服务的支持几乎是开箱即用!

// 在pages.json中配置元服务页面  
{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "isEntry": true,  
        "isAtomic": true  // 标记为元服务页面  
      }  
    }  
  ]  
}

我的一个工具类应用,通过元服务实现了:

  • 用户无需安装即可使用核心计算功能
  • 服务卡片直接展示关键信息
  • 转化率提升明显

四、技术对比:uni-app的降维打击

用过uni-app 开发的朋友都知道,他最大的优势就是全终端兼容,安卓+iOS+小程序+鸿蒙,可以说是天下无敌了,一套代码,真的是省心省力,真的是早用早享受。只要你会点前端,懂Vue,就没有任何技术难度。
当然为了让大家更直观地理解,我做了个对比表:

特性 原生鸿蒙开发 uni-app鸿蒙适配
学习成本 需要学习ArkTS等新技术 使用熟悉的vue语法
代码复用 从零开始 90%+代码可直接复用
开发周期 2-4周/应用 1-3天/应用
多端支持 仅鸿蒙 同时支持小程序、iOS、安卓
维护成本 独立代码库 统一代码库

最让我震撼的是: 旧应用的适配工作远比你担心的要少太多了,只要能跑起来,运行起来,就基本上没啥问题。在这里必须感谢一下uni-app 团队的技术支持,我在适配过程中,有专门的钉钉群提供技术服务,这也是我为何能在短短3天时间就能解决所有兼容的关键。在此一并感谢整个团队!

五、避坑指南:实战中遇到的坑

当然,过程并非完全一帆风顺。记录几个可能帮到大家的坑:

  1. 图片路径问题:鸿蒙对绝对路径更敏感,建议使用相对路径
  2. CSS兼容性:部分CSS3特性需要添加鸿蒙前缀
  3. API异步处理:鸿蒙的API调用更强调异步编程
  4. 代码运行不生效:如果感觉代码没问题,热更新不生效,记得重启整个应用,甚至重启HBX,重启模拟器,有时缓存问题不得不说很难受。
// 推荐使用async/await处理鸿蒙API  
async function initHarmonyFeatures() {  
  try {  
    const result = await uni.harmony.someAPI();  
    // 处理结果  
  } catch (error) {  
    console.error('API调用失败:', error);  
  }  
}

六、成果展示:数字说话

截至目前,我的6款应用全部成功上架:

  • 🎯 适配成功率:100%(6/6)
  • 最快适配记录:8小时(旧的成熟应用)
  • 🚀 平均适配时间:2天/应用
  • 💰 成本节约:相比原生开发,节省约85%成本
  • 📈 用户增长:鸿蒙渠道日均新增用户300+

七、给开发者同仁的真诚建议

  1. 立即行动:鸿蒙生态红利期就在眼前
  2. 从简单应用开始:先拿一个功能简单的应用试水
  3. 关注元服务:这是鸿蒙的独特优势,不要错过
  4. 利用uni-app社区:遇到的问题基本都能找到解决方案

结语:一个人的速度,一个时代的变革

有开发者朋友问我:"现在入场是不是太晚了?"

我的回答是:"当你知道的时候,就是最好的时机。"

uni-app + 鸿蒙的组合,让我在技术变革的浪潮中抓住了先机。 从焦虑观望到全面上架,只用了不到一周时间。这种效率,在过去的移动开发史上是从未有过的。

现在,轮到你了。

在这里顺带解释一下奖励金,华为最近的活动,只要上架成功,有活跃用户,或者参加uni-app的活动,都是可以拿到奖励金的。大家加油!

在得瑟下我们的用户量;


【实战资源分享】

欢迎在评论区交流适配经验,我会第一时间回复大家的问题!

收起阅读 »

2025年DCloud插件大赛获奖名单

插件大赛

本次插件大赛收到大量优质插件,尤其是大量常用插件已完成鸿蒙Next的兼容适配。

获奖名单

一等奖

奖项 分类 获奖作者 获奖插件
一等奖 前端组件 陌上华年 LimeUi 轻量高效的 Uni 生态组件库【鸿蒙Next】
一等奖 UTS插件 COOL团队 【支持鸿蒙】Cool Unix|UI组件库

二等奖

奖项 分类 获奖作者 获奖插件
二等奖 UTS插件 1530948626 安卓保活 ios保活 鸿蒙保活 保应用程序稳定后台运行(uniapp,uniappx保活 长期维护)
二等奖 UTS插件 照相 【z-paging-x下拉刷新、上拉加载】z-paging uniappx版已上线!
二等奖 UTS插件 tmui TM-UI-4.0原生应用开发解决方案套装
二等奖 UTS插件 前端码农boy 【Android+IOS+harmonyOS】银联云闪付支付
二等奖 前端组件 UxFrame 【支持原生鸿蒙】UxFrame 低代码高性能UI框架
二等奖 前端组件 TuiPlus TuiPlus 4.0 焕新发布 轻如鸿毛 快如闪电
二等奖 前端组件 rice_z RiceUI 基于uniappx 的UI框架【支持APP 鸿蒙 小程序 H5】
二等奖 前端组件 VK168 【开箱即用】uView Vue3 横空出世,继承uView1意志,再战江湖,风云再起!

三等奖

奖项 分类 获奖作者 获奖插件
三等奖 UTS插件 GraceUI uXui 【主流平台全兼容版】一款基于 uni-app x 的、免费、开源的 UI 框架
三等奖 UTS插件 珊瑚 Android端的AES、MD5、RSA、SHA、SM2、SM3、SM4加密解密
三等奖 UTS插件 shmily121314 usb-serial
三等奖 UTS插件 1530948626 Ble低功耗蓝牙uts插件 支持安卓ios 鸿蒙 微信小程序
三等奖 UTS插件 小白2023 sanfor-atrust(深信服-vpn插件)
三等奖 UTS插件 30天只能改一次 高德定位、地图、导航全功能,简单易用,支持安卓和iOS
三等奖 UTS插件 kux kux-marked
三等奖 HBuilderX 猫猫猫猫 GitHub Copilot
三等奖 前端组件 UviewPlus 零云®uview-plus3.0重磅发布,全面的Vue3鸿蒙跨端移动组件库,组件丰富维护更新稳定
三等奖 前端组件 不如摸鱼去 wot-design-uni 基于vue3+Typescript的高颜值组件库
三等奖 前端组件 uViewNext uView Next全面适配Vue3和鸿蒙,组件库更丰富,功能更全,更稳定,更高质量的UI框架
三等奖 前端组件 wenju 【ECharts组件】支持官方所有图表,支持vue3、分包、鸿蒙、nvue、uts、uniapp x
三等奖 项目模板 useryang 智慧医疗
三等奖 项目模板 小疯子呵 福袋抽奖-看广告变现

鼓励奖

因很多插件值得推荐,所以增加了贡献奖的名额。

奖项 分类 获奖作者 获奖插件
贡献奖 UTS插件 鑫时代 xsd-request
贡献奖 UTS插件 1530948626 android ios 原生gps前后台定位,系统定位
贡献奖 UTS插件 shmily121314 rs232-serial
贡献奖 UTS插件 kaka_ 蓝牙 USB Wi-Fi 打印机UTS插件.支持安卓 iOS 支持TSPL CPCL ESC
贡献奖 UTS插件 1530948626 安卓悬浮窗插件 可自定义内容 悬浮窗 画中画 可拖拽,可切换浮窗大小
贡献奖 UTS插件 csr_hb read-nfc
贡献奖 UTS插件 康爱公社 kux-dayjs
贡献奖 UTS插件 1530948626 android mqtt 消息推送
贡献奖 UTS插件 kux kux-crypto
贡献奖 UTS插件 Face_AI FaceAISDK
贡献奖 UTS插件 李子 【Android+iOS+HarmonyNext】腾讯云即时通讯(IM)UTS插件
贡献奖 UTS插件 billkes_bg 高德定位 UTS-API 插件 - 全平台定位解决方案
贡献奖 UTS插件 陌上华年 lime-sqlite
贡献奖 UTS插件 Greetty Telegram分享,WA分享,Twitter分享,Instagram分享
贡献奖 UTS插件 文艺程序猿 facebook登录
贡献奖 UTS插件 文艺程序猿 OneSignal海外App推送服务 支持iOS和Andriod
贡献奖 UTS插件 倜傥 苹果登录
贡献奖 UTS插件 UxFrame UxFrame 微信SDK
贡献奖 UTS插件 叶柳垂杨 TTS(安卓、ios、鸿蒙、web)将文字转成语音播报
贡献奖 UTS插件 文若不是苟或 文若IM
贡献奖 UTS插件 early_Summer es-paypal
贡献奖 UTS插件 天生DR UTS 苹果登录
贡献奖 uniCloud 希语 有奖猜歌游戏--改猜字谜答题游戏
贡献奖 uniCloud eric_peng 【精品】拍照算数批改
贡献奖 HBuilderX 智谱AI CodeGeeX: AI Code AutoComplete, Chat, Auto Comment
贡献奖 HBuilderX luhaoyu_ ucoder-AI编程助手
贡献奖 HBuilderX raise 日志桥,让cursor自动识别HBuilderX运行报错日志,自动修复错误代码
贡献奖 JS SDK Blue_ 安卓权限申请、权限申请的使用目的、华为上架、小米上架
贡献奖 JS SDK 一泽 request uni-ajax 请求
贡献奖 JS SDK jones2000 K线 分时 通达信指标 深度图 报价列表 订单流 筹码分布 hqchart.v2
贡献奖 前端组件 zerojs zero-markdown-view(markdown解析)
贡献奖 前端组件 lishanjun 全能文件选择上传3.0-纯前端
贡献奖 前端组件 ikun_ui 【ikun-qrcode】极简的二维码生成组件,使用view而非canvas避免层级问题
贡献奖 前端组件 193***@qq.com sse 客户端组件,支持兼容:v2、v3、安卓、ios、浏览器、微信小程序
贡献奖 前端组件 101***@qq.com 好用视频播放器
贡献奖 前端组件 月上柳梢 解决软件在运行时,未见向用户告知权限申请的目的,华为等上架被拒问题
贡献奖 前端组件 119***@qq.com 3D模型展示、动画播放、模型点击 - three.js(threeJs)
贡献奖 前端组件 FreeAlive 图片编辑器
贡献奖 前端组件 不如摸鱼去 微信小程序隐私保护弹出框 隐私协议弹出框 隐私弹框
贡献奖 前端组件 前端组件开发 基于原生input增强选择picker插件用于地图定位选择位置 页面跳转选择数据
贡献奖 前端组件 100***@qq.com rn-signature 电子签名、兼容小程序、H5、APP、鸿蒙 横屏展示
贡献奖 前端组件 SingmyAaronLan SinleUI - uni-app x UI 框架
贡献奖 项目模板 SheepJS Shopro商城 vue3+pinia+vite前端项目模板
贡献奖 项目模板 张宇凡 免费、商用数字人再开源,UI美得一塌糊涂!
贡献奖 项目模板 森林君 上门按摩专业版(仿东郊到家、上门预约类皆适用)
贡献奖 项目模板 jcodeapp 充电桩扫码充电小程序纯前端模板
贡献奖 项目模板 优雅草科技 优雅草蜻蜓hr人才招聘系统-前端模板文件开源-企业人才招聘系统
贡献奖 项目模板 tangniyuqi UniPet宠物领养平台前端开源项目
贡献奖 项目模板 用户2737414 社区论坛、校园论坛系统、圈子模板
贡献奖 项目模板 billkes_bg 考试答题系统模版

奖项设置:

本次大赛与往届有一个差别,就是更加普惠。本次没有特等奖,三等奖也发放价值2699元的纯血鸿蒙手机(全新机)。

一等奖(2名):

奖品:1万元插件包销 + 鸿蒙手机1部 + 插件市场置顶推荐半个月 + HBuilderX预置 + HBuilderX超大鼠标垫 + DCloud奖牌

二等奖(8名):

奖品:1000元插件包销 + 鸿蒙手机1部 + 插件市场置顶推荐1个星期 + HBuilderX超大鼠标垫 + DCloud奖牌

三等奖(14名):

奖品:200元uniCloud代金券 + 鸿蒙手机1部 + HBuilderX超大鼠标垫 + DCloud奖牌

说明:三等奖原设置为 20 名,但插件评分满足条件的插件数量不足,很遗憾本次只能评选出 14 名;开发者可继续迭代更新自己的插件,截止 2025 年 12 月 31 日之前,发布更新版本的插件作者,可邮件到service@dcloud.io进行评选申请,评委会审核通过后,会补发剩余的 6 个三等奖。

贡献奖(50名):

奖品:HBuilderX超大鼠标垫

奖品说明:

  1. “插件包销”,是指获奖插件通过插件市场销售,DCloud兜底包销。以1等奖的1万元包销为例,如果获奖插件在插件市场1年内销售额没有达到1万元,则由DCloud付差额给获奖者进行兜底。包销只针对付费插件,如免费插件获得二等奖及以上奖励,其中的包销奖励无效。包销插件需持续迭代,如插件作者放弃维护,则包销无效。

  2. “HBuilderX预置”,是在HBuilderX新建项目界面,可直接选择该项目模板。这为插件带来大量的流量。不适合预置的插件类型,无法领取此奖项。

  3. 本次插件大赛,目标是普惠更广大开发者,解决很多工程师缺少鸿蒙真机的困境,故本次奖励的鸿蒙手机为统一型号为nova 14 256GB

  4. 鸿蒙手机的奖励需满足两个条件:

    • 插件兼容鸿蒙平台
    • 插件作者通过DCloud专属链接到鸿蒙开发者平台注册一个新的开发者账号

除上述奖品外:

  • 二等奖及以上获奖插件作者,都将进入DCloud VIP技术支持群,享受优先的技术支付、问题反馈。
  • 所有获奖插件的集锦页面,还将通过HBuilderX工具、论坛、IM/QQ/微信群进行全量推广,给予优秀插件充分的曝光。

HBuilderX预置窗体界面如下:

奖牌照片如下:

奖品领取

请各位获奖作者尽快提交自己的邮寄地址,我们会陆续联系获奖人员发放奖品;
邮寄地址提交方式:登录ask社区,点击右上角个人头像,进入设置界面,设置界面下方补充快递邮寄地址。

已获奖的插件作者请继续升级迭代插件;

未获奖的今年还有机会,官方会继续为建设更好的uni-app x生态及更好的鸿蒙支持,推出其他计划。

不管是为了下次大赛获奖,还是为了把握uni-app x及鸿蒙替代的新浪潮,或者在插件市场通过售卖插件变现,都是值得期待的好事。

继续阅读 »

本次插件大赛收到大量优质插件,尤其是大量常用插件已完成鸿蒙Next的兼容适配。

获奖名单

一等奖

奖项 分类 获奖作者 获奖插件
一等奖 前端组件 陌上华年 LimeUi 轻量高效的 Uni 生态组件库【鸿蒙Next】
一等奖 UTS插件 COOL团队 【支持鸿蒙】Cool Unix|UI组件库

二等奖

奖项 分类 获奖作者 获奖插件
二等奖 UTS插件 1530948626 安卓保活 ios保活 鸿蒙保活 保应用程序稳定后台运行(uniapp,uniappx保活 长期维护)
二等奖 UTS插件 照相 【z-paging-x下拉刷新、上拉加载】z-paging uniappx版已上线!
二等奖 UTS插件 tmui TM-UI-4.0原生应用开发解决方案套装
二等奖 UTS插件 前端码农boy 【Android+IOS+harmonyOS】银联云闪付支付
二等奖 前端组件 UxFrame 【支持原生鸿蒙】UxFrame 低代码高性能UI框架
二等奖 前端组件 TuiPlus TuiPlus 4.0 焕新发布 轻如鸿毛 快如闪电
二等奖 前端组件 rice_z RiceUI 基于uniappx 的UI框架【支持APP 鸿蒙 小程序 H5】
二等奖 前端组件 VK168 【开箱即用】uView Vue3 横空出世,继承uView1意志,再战江湖,风云再起!

三等奖

奖项 分类 获奖作者 获奖插件
三等奖 UTS插件 GraceUI uXui 【主流平台全兼容版】一款基于 uni-app x 的、免费、开源的 UI 框架
三等奖 UTS插件 珊瑚 Android端的AES、MD5、RSA、SHA、SM2、SM3、SM4加密解密
三等奖 UTS插件 shmily121314 usb-serial
三等奖 UTS插件 1530948626 Ble低功耗蓝牙uts插件 支持安卓ios 鸿蒙 微信小程序
三等奖 UTS插件 小白2023 sanfor-atrust(深信服-vpn插件)
三等奖 UTS插件 30天只能改一次 高德定位、地图、导航全功能,简单易用,支持安卓和iOS
三等奖 UTS插件 kux kux-marked
三等奖 HBuilderX 猫猫猫猫 GitHub Copilot
三等奖 前端组件 UviewPlus 零云®uview-plus3.0重磅发布,全面的Vue3鸿蒙跨端移动组件库,组件丰富维护更新稳定
三等奖 前端组件 不如摸鱼去 wot-design-uni 基于vue3+Typescript的高颜值组件库
三等奖 前端组件 uViewNext uView Next全面适配Vue3和鸿蒙,组件库更丰富,功能更全,更稳定,更高质量的UI框架
三等奖 前端组件 wenju 【ECharts组件】支持官方所有图表,支持vue3、分包、鸿蒙、nvue、uts、uniapp x
三等奖 项目模板 useryang 智慧医疗
三等奖 项目模板 小疯子呵 福袋抽奖-看广告变现

鼓励奖

因很多插件值得推荐,所以增加了贡献奖的名额。

奖项 分类 获奖作者 获奖插件
贡献奖 UTS插件 鑫时代 xsd-request
贡献奖 UTS插件 1530948626 android ios 原生gps前后台定位,系统定位
贡献奖 UTS插件 shmily121314 rs232-serial
贡献奖 UTS插件 kaka_ 蓝牙 USB Wi-Fi 打印机UTS插件.支持安卓 iOS 支持TSPL CPCL ESC
贡献奖 UTS插件 1530948626 安卓悬浮窗插件 可自定义内容 悬浮窗 画中画 可拖拽,可切换浮窗大小
贡献奖 UTS插件 csr_hb read-nfc
贡献奖 UTS插件 康爱公社 kux-dayjs
贡献奖 UTS插件 1530948626 android mqtt 消息推送
贡献奖 UTS插件 kux kux-crypto
贡献奖 UTS插件 Face_AI FaceAISDK
贡献奖 UTS插件 李子 【Android+iOS+HarmonyNext】腾讯云即时通讯(IM)UTS插件
贡献奖 UTS插件 billkes_bg 高德定位 UTS-API 插件 - 全平台定位解决方案
贡献奖 UTS插件 陌上华年 lime-sqlite
贡献奖 UTS插件 Greetty Telegram分享,WA分享,Twitter分享,Instagram分享
贡献奖 UTS插件 文艺程序猿 facebook登录
贡献奖 UTS插件 文艺程序猿 OneSignal海外App推送服务 支持iOS和Andriod
贡献奖 UTS插件 倜傥 苹果登录
贡献奖 UTS插件 UxFrame UxFrame 微信SDK
贡献奖 UTS插件 叶柳垂杨 TTS(安卓、ios、鸿蒙、web)将文字转成语音播报
贡献奖 UTS插件 文若不是苟或 文若IM
贡献奖 UTS插件 early_Summer es-paypal
贡献奖 UTS插件 天生DR UTS 苹果登录
贡献奖 uniCloud 希语 有奖猜歌游戏--改猜字谜答题游戏
贡献奖 uniCloud eric_peng 【精品】拍照算数批改
贡献奖 HBuilderX 智谱AI CodeGeeX: AI Code AutoComplete, Chat, Auto Comment
贡献奖 HBuilderX luhaoyu_ ucoder-AI编程助手
贡献奖 HBuilderX raise 日志桥,让cursor自动识别HBuilderX运行报错日志,自动修复错误代码
贡献奖 JS SDK Blue_ 安卓权限申请、权限申请的使用目的、华为上架、小米上架
贡献奖 JS SDK 一泽 request uni-ajax 请求
贡献奖 JS SDK jones2000 K线 分时 通达信指标 深度图 报价列表 订单流 筹码分布 hqchart.v2
贡献奖 前端组件 zerojs zero-markdown-view(markdown解析)
贡献奖 前端组件 lishanjun 全能文件选择上传3.0-纯前端
贡献奖 前端组件 ikun_ui 【ikun-qrcode】极简的二维码生成组件,使用view而非canvas避免层级问题
贡献奖 前端组件 193***@qq.com sse 客户端组件,支持兼容:v2、v3、安卓、ios、浏览器、微信小程序
贡献奖 前端组件 101***@qq.com 好用视频播放器
贡献奖 前端组件 月上柳梢 解决软件在运行时,未见向用户告知权限申请的目的,华为等上架被拒问题
贡献奖 前端组件 119***@qq.com 3D模型展示、动画播放、模型点击 - three.js(threeJs)
贡献奖 前端组件 FreeAlive 图片编辑器
贡献奖 前端组件 不如摸鱼去 微信小程序隐私保护弹出框 隐私协议弹出框 隐私弹框
贡献奖 前端组件 前端组件开发 基于原生input增强选择picker插件用于地图定位选择位置 页面跳转选择数据
贡献奖 前端组件 100***@qq.com rn-signature 电子签名、兼容小程序、H5、APP、鸿蒙 横屏展示
贡献奖 前端组件 SingmyAaronLan SinleUI - uni-app x UI 框架
贡献奖 项目模板 SheepJS Shopro商城 vue3+pinia+vite前端项目模板
贡献奖 项目模板 张宇凡 免费、商用数字人再开源,UI美得一塌糊涂!
贡献奖 项目模板 森林君 上门按摩专业版(仿东郊到家、上门预约类皆适用)
贡献奖 项目模板 jcodeapp 充电桩扫码充电小程序纯前端模板
贡献奖 项目模板 优雅草科技 优雅草蜻蜓hr人才招聘系统-前端模板文件开源-企业人才招聘系统
贡献奖 项目模板 tangniyuqi UniPet宠物领养平台前端开源项目
贡献奖 项目模板 用户2737414 社区论坛、校园论坛系统、圈子模板
贡献奖 项目模板 billkes_bg 考试答题系统模版

奖项设置:

本次大赛与往届有一个差别,就是更加普惠。本次没有特等奖,三等奖也发放价值2699元的纯血鸿蒙手机(全新机)。

一等奖(2名):

奖品:1万元插件包销 + 鸿蒙手机1部 + 插件市场置顶推荐半个月 + HBuilderX预置 + HBuilderX超大鼠标垫 + DCloud奖牌

二等奖(8名):

奖品:1000元插件包销 + 鸿蒙手机1部 + 插件市场置顶推荐1个星期 + HBuilderX超大鼠标垫 + DCloud奖牌

三等奖(14名):

奖品:200元uniCloud代金券 + 鸿蒙手机1部 + HBuilderX超大鼠标垫 + DCloud奖牌

说明:三等奖原设置为 20 名,但插件评分满足条件的插件数量不足,很遗憾本次只能评选出 14 名;开发者可继续迭代更新自己的插件,截止 2025 年 12 月 31 日之前,发布更新版本的插件作者,可邮件到service@dcloud.io进行评选申请,评委会审核通过后,会补发剩余的 6 个三等奖。

贡献奖(50名):

奖品:HBuilderX超大鼠标垫

奖品说明:

  1. “插件包销”,是指获奖插件通过插件市场销售,DCloud兜底包销。以1等奖的1万元包销为例,如果获奖插件在插件市场1年内销售额没有达到1万元,则由DCloud付差额给获奖者进行兜底。包销只针对付费插件,如免费插件获得二等奖及以上奖励,其中的包销奖励无效。包销插件需持续迭代,如插件作者放弃维护,则包销无效。

  2. “HBuilderX预置”,是在HBuilderX新建项目界面,可直接选择该项目模板。这为插件带来大量的流量。不适合预置的插件类型,无法领取此奖项。

  3. 本次插件大赛,目标是普惠更广大开发者,解决很多工程师缺少鸿蒙真机的困境,故本次奖励的鸿蒙手机为统一型号为nova 14 256GB

  4. 鸿蒙手机的奖励需满足两个条件:

    • 插件兼容鸿蒙平台
    • 插件作者通过DCloud专属链接到鸿蒙开发者平台注册一个新的开发者账号

除上述奖品外:

  • 二等奖及以上获奖插件作者,都将进入DCloud VIP技术支持群,享受优先的技术支付、问题反馈。
  • 所有获奖插件的集锦页面,还将通过HBuilderX工具、论坛、IM/QQ/微信群进行全量推广,给予优秀插件充分的曝光。

HBuilderX预置窗体界面如下:

奖牌照片如下:

奖品领取

请各位获奖作者尽快提交自己的邮寄地址,我们会陆续联系获奖人员发放奖品;
邮寄地址提交方式:登录ask社区,点击右上角个人头像,进入设置界面,设置界面下方补充快递邮寄地址。

已获奖的插件作者请继续升级迭代插件;

未获奖的今年还有机会,官方会继续为建设更好的uni-app x生态及更好的鸿蒙支持,推出其他计划。

不管是为了下次大赛获奖,还是为了把握uni-app x及鸿蒙替代的新浪潮,或者在插件市场通过售卖插件变现,都是值得期待的好事。

收起阅读 »