HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

局域网文件传输

HTML5

我在两个浏览器之间用它来传文件。比如在Chrome上登录了账号,想把下载的文件发到另一个没登录的浏览器里,用这个就特别快。

https://iris.findtruman.io/web/lan-drop?share=L

继续阅读 »

我在两个浏览器之间用它来传文件。比如在Chrome上登录了账号,想把下载的文件发到另一个没登录的浏览器里,用这个就特别快。

https://iris.findtruman.io/web/lan-drop?share=L

收起阅读 »

壁纸生成器

HTML5+

我用它给我的代码编辑器生成了一张深灰色的背景图,替换掉了默认的黑色。感觉长时间看屏幕,眼睛舒服多了,代码都看得更清楚了。

https://iris.findtruman.io/web/wallpaper_generator?share=L

我用它给我的代码编辑器生成了一张深灰色的背景图,替换掉了默认的黑色。感觉长时间看屏幕,眼睛舒服多了,代码都看得更清楚了。

https://iris.findtruman.io/web/wallpaper_generator?share=L

经验分享 鸿蒙隐私弹窗如何处理

鸿蒙征文

基本背景

鸿蒙应用上架需要配置隐私协议、用户协议。这部分内容可参考 鸿蒙如何设置隐私协议弹窗

这里做技术实现说明。

方案一:自行绘制

目前已经上架的 hellouniapp应用,和一部分已上架用户案例,都是使用自己弹窗完成的。可使用页面组件弹窗、也可制作单独的页面进行路由跳转,用户的同意状态存储 storage 中,后续启动根据参数来动态判断。

这里相对容易,不做进一步说明。

注意:元服务中只能选择托管协议,鸿蒙应用可在托管协议、自定义填写 URL 二选一,如果选择自定义 URL,只能使用方案一自定绘制。

方案二:官方隐私协议托管

鸿蒙的 AGC 后台有隐私协议托管功能,通过填写标准化的文档,完成隐私协议的填写,按照要求填写内容,生成协议,在上架提审过程中选择即可。具体参鸿蒙文档 隐私管理服务

好处:

  • 隐私协议按照要求填写,不会因为格式出错被驳回,正确填写相关 sdk、权限说明即可。
  • 网页由鸿蒙托管访问有保障。
  • 可使用鸿蒙 api 唤起系统弹窗,视觉风格和鸿蒙统一。

整体步骤如下

1 后台填写隐私协议

得到 url 地址

2. 添加 debug 代码

代码中,编辑 harmony-configs/entry/src/main/module.json5 按照文档要求,添加三个字段,分别是

  • appgallery_privacy_hosted
  • appgallery_privacy_link_privacy_statement
  • appgallery_privacy_link_user_agreement

填写这个三个弹窗的作用是模拟线上正式环境,用于测试通过 api 唤起弹窗的效果,务必注意,并不是为了实现自动弹窗

3. 编写 uts 插件

编写 uts 插件,完成弹窗,这里提供代码片段,后续提供 uts 插件市场链接。

新建 uts 插件,比如 hamrony-privacy-dialog ,编辑 uni_modules/harmony-privacy-dialog/utssdk/app-harmony/index.uts

填写下面代码,这段代码导出了三个方法,和官方的 ets 代码完全一致,检查隐私协议信息、获取签署状态、主动唤起隐私弹窗。页面中引入代码调用即可。

import { privacyManager } from '@kit.AppGalleryKit';  
// import { hilog } from '@kit.PerformanceAnalysisKit';  
import { BusinessError } from '@kit.BasicServicesKit';  

export const getInfo = () => {  
    let err = ''  
    try {  
        let appPrivacyManageInfo : privacyManager.AppPrivacyMgmtInfo = privacyManager.getAppPrivacyMgmtInfo();  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type: " + appPrivacyManageInfo["type"]);  
        let privacyLinkInfoArray : privacyManager.AppPrivacyLink[] = appPrivacyManageInfo.privacyInfo;  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo size = " + privacyLinkInfoArray.length);  
        for (let i = 0; i < privacyLinkInfoArray.length; i++) {  
            console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type = " + privacyLinkInfoArray[i]["type"] + ", version = " + privacyLinkInfoArray[i]["versionCode"] + ", url = " + privacyLinkInfoArray[i]["url"]);  
        }  
    } catch (error) {  
        err = error.message  
        console.error(0, 'TAG', "GetAppPrivacyManageInfoPublic exception code: " + error.code + ", exception message: " + error.message);  
    }  
    return err  
}  

export const getStatus = () => {  
    try {  
        let appPrivacyResults : privacyManager.AppPrivacyResult[] = privacyManager.getAppPrivacyResult();  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult size = " + appPrivacyResults.length);  
        for (let i = 0; i < appPrivacyResults.length; i++) {  
            console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"]);  
        }  
    } catch (error) {  
        console.error(0, 'TAG', "GetAppPrivacyResultPublic exception code: " + error.code + ", exception message: " + error.message);  
    }  
}  
export const requestPrivacy = () => {  
    try {  
        const uiContext = UTSHarmony.getUIAbilityContext()  
        // const uiContext = this.getUIContext().getHostContext() as common.UIAbilityContext;  
        privacyManager.requestAppPrivacyConsent(uiContext).then((consentResult : privacyManager.ConsentResult) => {  
            let appPrivacyResults : privacyManager.AppPrivacyResult[] = consentResult["results"];  
            for (let i = 0; i < appPrivacyResults.length; i++) {  
                console.info(0, 'TAG', "GetAppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"] + ", signingTimeStamp = " + appPrivacyResults[i]["signingTimeStamp"]);  
            }  
        }).catch((error : BusinessError<Object>) => {  
            console.error(0, 'TAG', `requestAppPrivacyConsent failed, Code: ${error.code}, message: ${error.message}`);  
        });  
    } catch (error) {  
        console.error(0, 'TAG', "requestAppPrivacyConsent exception code: " + error.code + ", exception message: " + error.message);  
    }  
}

4 页面中引入

    import {  
        getInfo,  
        getStatus,requestPrivacy  
    } from '@/uni_modules/harmony-privacy-dialog'
继续阅读 »

基本背景

鸿蒙应用上架需要配置隐私协议、用户协议。这部分内容可参考 鸿蒙如何设置隐私协议弹窗

这里做技术实现说明。

方案一:自行绘制

目前已经上架的 hellouniapp应用,和一部分已上架用户案例,都是使用自己弹窗完成的。可使用页面组件弹窗、也可制作单独的页面进行路由跳转,用户的同意状态存储 storage 中,后续启动根据参数来动态判断。

这里相对容易,不做进一步说明。

注意:元服务中只能选择托管协议,鸿蒙应用可在托管协议、自定义填写 URL 二选一,如果选择自定义 URL,只能使用方案一自定绘制。

方案二:官方隐私协议托管

鸿蒙的 AGC 后台有隐私协议托管功能,通过填写标准化的文档,完成隐私协议的填写,按照要求填写内容,生成协议,在上架提审过程中选择即可。具体参鸿蒙文档 隐私管理服务

好处:

  • 隐私协议按照要求填写,不会因为格式出错被驳回,正确填写相关 sdk、权限说明即可。
  • 网页由鸿蒙托管访问有保障。
  • 可使用鸿蒙 api 唤起系统弹窗,视觉风格和鸿蒙统一。

整体步骤如下

1 后台填写隐私协议

得到 url 地址

2. 添加 debug 代码

代码中,编辑 harmony-configs/entry/src/main/module.json5 按照文档要求,添加三个字段,分别是

  • appgallery_privacy_hosted
  • appgallery_privacy_link_privacy_statement
  • appgallery_privacy_link_user_agreement

填写这个三个弹窗的作用是模拟线上正式环境,用于测试通过 api 唤起弹窗的效果,务必注意,并不是为了实现自动弹窗

3. 编写 uts 插件

编写 uts 插件,完成弹窗,这里提供代码片段,后续提供 uts 插件市场链接。

新建 uts 插件,比如 hamrony-privacy-dialog ,编辑 uni_modules/harmony-privacy-dialog/utssdk/app-harmony/index.uts

填写下面代码,这段代码导出了三个方法,和官方的 ets 代码完全一致,检查隐私协议信息、获取签署状态、主动唤起隐私弹窗。页面中引入代码调用即可。

import { privacyManager } from '@kit.AppGalleryKit';  
// import { hilog } from '@kit.PerformanceAnalysisKit';  
import { BusinessError } from '@kit.BasicServicesKit';  

export const getInfo = () => {  
    let err = ''  
    try {  
        let appPrivacyManageInfo : privacyManager.AppPrivacyMgmtInfo = privacyManager.getAppPrivacyMgmtInfo();  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type: " + appPrivacyManageInfo["type"]);  
        let privacyLinkInfoArray : privacyManager.AppPrivacyLink[] = appPrivacyManageInfo.privacyInfo;  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo size = " + privacyLinkInfoArray.length);  
        for (let i = 0; i < privacyLinkInfoArray.length; i++) {  
            console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type = " + privacyLinkInfoArray[i]["type"] + ", version = " + privacyLinkInfoArray[i]["versionCode"] + ", url = " + privacyLinkInfoArray[i]["url"]);  
        }  
    } catch (error) {  
        err = error.message  
        console.error(0, 'TAG', "GetAppPrivacyManageInfoPublic exception code: " + error.code + ", exception message: " + error.message);  
    }  
    return err  
}  

export const getStatus = () => {  
    try {  
        let appPrivacyResults : privacyManager.AppPrivacyResult[] = privacyManager.getAppPrivacyResult();  
        console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult size = " + appPrivacyResults.length);  
        for (let i = 0; i < appPrivacyResults.length; i++) {  
            console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"]);  
        }  
    } catch (error) {  
        console.error(0, 'TAG', "GetAppPrivacyResultPublic exception code: " + error.code + ", exception message: " + error.message);  
    }  
}  
export const requestPrivacy = () => {  
    try {  
        const uiContext = UTSHarmony.getUIAbilityContext()  
        // const uiContext = this.getUIContext().getHostContext() as common.UIAbilityContext;  
        privacyManager.requestAppPrivacyConsent(uiContext).then((consentResult : privacyManager.ConsentResult) => {  
            let appPrivacyResults : privacyManager.AppPrivacyResult[] = consentResult["results"];  
            for (let i = 0; i < appPrivacyResults.length; i++) {  
                console.info(0, 'TAG', "GetAppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"] + ", signingTimeStamp = " + appPrivacyResults[i]["signingTimeStamp"]);  
            }  
        }).catch((error : BusinessError<Object>) => {  
            console.error(0, 'TAG', `requestAppPrivacyConsent failed, Code: ${error.code}, message: ${error.message}`);  
        });  
    } catch (error) {  
        console.error(0, 'TAG', "requestAppPrivacyConsent exception code: " + error.code + ", exception message: " + error.message);  
    }  
}

4 页面中引入

    import {  
        getInfo,  
        getStatus,requestPrivacy  
    } from '@/uni_modules/harmony-privacy-dialog'
收起阅读 »

个人开发者承接app、小程序、网页外包,全职外包接单

小程序 外包接单

全职在家承接外包,多年外包经验,个人开发者,绝对实惠靠谱,有很多款线上应用(度是自己开发的,自己独立完成,可查)

可做商城类,社交类,工具类,任务平台类,mes 类,扫码收银/分账交易类 等,除了游戏和带颜色的,其他度可以开发

可承接安卓/IOS、各个端的小程序、H5网页、PC网页开发,从前端到后端,我全度会,一条龙服务

掌握技术
前端:uniapp 、uniappx、vue
后端:Golang 、php

uniapp 及 uniappx 开发的作品均有上架appstore

小程序方面也有很多

有需要开发的并能看得上我的请联系我哈

vx:wu1020yt

继续阅读 »

全职在家承接外包,多年外包经验,个人开发者,绝对实惠靠谱,有很多款线上应用(度是自己开发的,自己独立完成,可查)

可做商城类,社交类,工具类,任务平台类,mes 类,扫码收银/分账交易类 等,除了游戏和带颜色的,其他度可以开发

可承接安卓/IOS、各个端的小程序、H5网页、PC网页开发,从前端到后端,我全度会,一条龙服务

掌握技术
前端:uniapp 、uniappx、vue
后端:Golang 、php

uniapp 及 uniappx 开发的作品均有上架appstore

小程序方面也有很多

有需要开发的并能看得上我的请联系我哈

vx:wu1020yt

收起阅读 »

FIRE计算器

AMAP

我用它制定了一个“搞钱计划”。

比如我今年想多存五万块,

它能帮我倒推出每个月需要增加多少收入或者减少多少开支,目标特别清晰。

https://iris.findtruman.io/web/fire_calculator?share=L

继续阅读 »

我用它制定了一个“搞钱计划”。

比如我今年想多存五万块,

它能帮我倒推出每个月需要增加多少收入或者减少多少开支,目标特别清晰。

https://iris.findtruman.io/web/fire_calculator?share=L

收起阅读 »

APP扫码识别区是正方形怎么修改成长方形,微信小程序是长方形的

APP扫码识别区是正方形怎么修改成长方形,微信小程序是长方形的,扫码识别区太小了,很不方便

APP扫码识别区是正方形怎么修改成长方形,微信小程序是长方形的,扫码识别区太小了,很不方便

分享一下 在ios上 uni-app 通过oauth2登录遇到的一些阻碍和解决办法。

移动APP uni_app iOS

就当纪念一下抠了好几天的头。虽然实现了,也花了很多时间,但是没有什么成功了的实感,有种莫名其妙的感觉。
不论咋样,还是记录一下吧,百度之后竟然完全没有人用这个方式 在ios上登录,而ds和cursor一众ai费拉不堪,ds胡说八道的程度越来越严重了

oauth2的登录描述比较粗糙。

有一个最想知道的问题,想知道 ios中app和 webview 的 cookie传递。

过程:
1.根据文档设置 ios的schemes。注:urltypes 只能用string格式,不可用数组,否则打包 的link.info 中 没有 CFBundleURLTypes 的标签。
2.login接口后,后端会产生一个短暂的cookie会话,根据该会话证明过程中登录情况。若无法验证到该cookie会话,将直接跳回登录界面。
3.使用webview 打开 authorize 页面,如果验证成功,会通过schemes唤醒 app,接着验证获取token并跳转主页面。

其中遇到3个阻碍。

  1. 在login接口后的 cookie会话,没有 设置 domain 时,webview 打开 authorize页面,一直都是cookie验证会话失败,直接跳转回登录界面。在设置 domain后,第一次 登录 cookie会话验证失败,而第二次继续登录的验证都通过了。
    询问了ds等ai,给出的解释是 在ios时,网络请求和Cookie存储与App本身不在同一进程,默认不共享NSHTTPCookieStorage。所以通过webview 后端会拿不到cookie从而导致失败。而安卓的cookie在uni-app底层已经同步过,所以不存在该问题。但问题是为什么设置了domain后,除了第一次失败,后续的登录都成功了?是不是底层也对ios同步过?但为什么第一次不成功?强制手动给webview注入cookie,没有一个方式成功。设置延时任务也没有用,设置多长也没有用,第一次就是不能通过。
    后续与后端沟通,通过 生成token附带在 authorize的path后,后端使用token来验证会话解决该问题。

2.page.json中 设置过 // "condition": {
// //模式配置,仅开发期间生效
// "current": 0, //当前激活的模式(list 的索引项)
// "list": [{
// "name": "", //模式名称
// "path": "", //启动页面,必选
// "query": "" //启动参数,在页面的onLoad函数里面得到
// }]
// }。

从而导致 schemes唤醒后,无法获取schemes携带的数据。安卓无此问题。

3.后端schemes因为ios的安全机制是无法自动唤醒app,除非用户手动点。Universal Links 也没办法在同域名下进行唤醒。但是,实际上 传递数据已经触发,在plus.runtiem.arguments中是有数据的。所以直接搞了一个 定时任务监听这个数据,一旦有数据就可以直接后续的步骤,不需要用户手动点击。安卓可以直接通过schemes唤醒app并直接拿到数据,所以也不需要这个定时任务

继续阅读 »

就当纪念一下抠了好几天的头。虽然实现了,也花了很多时间,但是没有什么成功了的实感,有种莫名其妙的感觉。
不论咋样,还是记录一下吧,百度之后竟然完全没有人用这个方式 在ios上登录,而ds和cursor一众ai费拉不堪,ds胡说八道的程度越来越严重了

oauth2的登录描述比较粗糙。

有一个最想知道的问题,想知道 ios中app和 webview 的 cookie传递。

过程:
1.根据文档设置 ios的schemes。注:urltypes 只能用string格式,不可用数组,否则打包 的link.info 中 没有 CFBundleURLTypes 的标签。
2.login接口后,后端会产生一个短暂的cookie会话,根据该会话证明过程中登录情况。若无法验证到该cookie会话,将直接跳回登录界面。
3.使用webview 打开 authorize 页面,如果验证成功,会通过schemes唤醒 app,接着验证获取token并跳转主页面。

其中遇到3个阻碍。

  1. 在login接口后的 cookie会话,没有 设置 domain 时,webview 打开 authorize页面,一直都是cookie验证会话失败,直接跳转回登录界面。在设置 domain后,第一次 登录 cookie会话验证失败,而第二次继续登录的验证都通过了。
    询问了ds等ai,给出的解释是 在ios时,网络请求和Cookie存储与App本身不在同一进程,默认不共享NSHTTPCookieStorage。所以通过webview 后端会拿不到cookie从而导致失败。而安卓的cookie在uni-app底层已经同步过,所以不存在该问题。但问题是为什么设置了domain后,除了第一次失败,后续的登录都成功了?是不是底层也对ios同步过?但为什么第一次不成功?强制手动给webview注入cookie,没有一个方式成功。设置延时任务也没有用,设置多长也没有用,第一次就是不能通过。
    后续与后端沟通,通过 生成token附带在 authorize的path后,后端使用token来验证会话解决该问题。

2.page.json中 设置过 // "condition": {
// //模式配置,仅开发期间生效
// "current": 0, //当前激活的模式(list 的索引项)
// "list": [{
// "name": "", //模式名称
// "path": "", //启动页面,必选
// "query": "" //启动参数,在页面的onLoad函数里面得到
// }]
// }。

从而导致 schemes唤醒后,无法获取schemes携带的数据。安卓无此问题。

3.后端schemes因为ios的安全机制是无法自动唤醒app,除非用户手动点。Universal Links 也没办法在同域名下进行唤醒。但是,实际上 传递数据已经触发,在plus.runtiem.arguments中是有数据的。所以直接搞了一个 定时任务监听这个数据,一旦有数据就可以直接后续的步骤,不需要用户手动点击。安卓可以直接通过schemes唤醒app并直接拿到数据,所以也不需要这个定时任务

收起阅读 »

【鸿蒙征文】重拾儿时趣味的古诗 App 的开发过程记录

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

一款无广告重拾儿时趣味的古诗 App 的开发过程记录

一、🌈缘起:让古诗 “活” 起来,而非躺在角落🌈

做这款 App 的初衷,藏在三个真实需求里:
一次偶然的寻找碰上鸿蒙的星光闪烁:翻找学生时代的语文诗词,想重拾那份诗词氛围时,书本早已不见踪影,此时便想在手机上寻找这类APP,市面古诗 App 要么广告扎堆,要么功能枯燥,实在感受不到当年的那种氛围;
身边朋友想重拾古诗,却缺合适的背诵工具,也没有专属交流渠道(总不能天天在朋友圈发古诗);
古诗的意境与字词之美,不该只靠 “死记硬背”—— 结合互动、朗读、释义,让大家 “懂了再背”,才是真正的文化传承。
所以核心需求很明确:做一款 “不枯燥、能互动、够纯粹” 的古诗工具。既要满足学习需求,又要兼顾趣味性,还得适配鸿蒙系统(毕竟鸿蒙用户越来越多,生态也日趋完善)。

二、💡技术选型:为啥 “锁死” UniAPP?💡

选技术栈时纠结了 3 天,最终敲定 UniAPP,全靠 “实用主义”:
排除鸿蒙原生开发:适配鸿蒙虽丝滑,但单人开发要重新学原生语法,还无法兼顾其他平台,效率太低;
排除其他跨平台框架:要么对鸿蒙适配不完善,要么插件生态薄弱,像诗词朗读、音频上传等功能得手动造轮子;

选择 UniAPP 的核心原因:
跨平台省心:一套代码可打包鸿蒙 App,后续扩展安卓、iOS 版本无需重构,对个人开发者极度友好;

  • 鸿蒙适配 “自带 buff”:HBuilderX 提供现成打包模板和专属适配插件,不用手动修改大量配置;
  • 生态够全:uniCloud 云开发、音频组件、数据库插件,刚好满足古诗存储、朗读上传、接龙匹配等核心需求;
  • 学习成本低:有微信小程序开发基础,UniAPP 的 Vue 语法无缝衔接,1 周就能上手。
    简单说:用 UniAPP 开发鸿蒙,就像 “站在巨人肩膀上”—— 不用从零学鸿蒙原生,又能享受鸿蒙系统特性,对非专业开发者太友好了!

三、⚠️踩坑实录:鸿蒙适配那些 “磨人的小妖精”⚠️

开发前以为 “跨平台 = 一路顺畅”,结果真正适配鸿蒙时,还是踩了不少坑,分享 3 个最印象深刻的:

权限申请:鸿蒙的 “规矩” 和其他平台不一样
做朗读广场时,需要调用手机麦克风录音、存储音频文件。一开始直接复用了其他平台的权限代码,结果在鸿蒙上直接报错 —— 录音功能打不开!

  • 坑点:鸿蒙的 “媒体权限” 需要单独申请,而且必须配置ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA,还得在代码里动态弹窗说明 “为啥要要权限”(比如 “需要录音才能上传你的朗读作品”),光说 “需要权限” 会被系统拒绝;而且必须在鸿蒙的AGC后台进行同步申请该权限,因为该权限属于受限权限;而且!!!如果是使用的鸿蒙自家的隐私托管服务,必须要在隐私托管服务中同步该权限的声明
  • 解决:使用uts插件配置权限;然后在AGC后台同步申请该受限权限通过率直接 100%。
    // #ifdef APP-HARMONY  
    import "@/uni_modules/harmony-permissions"  
    // #endif 
 {  
  "module": {  
    "name": "uni_modules__harmony_permissions",  
    "type": "har",  
    "deviceTypes": [  
      "default",  
      "tablet",  
      "2in1"  
    ],  
    "requestPermissions": [  
      {  
        "name": "ohos.permission.INTERNET"  
      },{  
        "name": "ohos.permission.WRITE_MEDIA",  
        "reason": "$string:media_desc",  
        "usedScene": {"when": "inuse"}  
      },{  
        "name": "ohos.permission.READ_MEDIA",  
        "reason": "$string:media_desc",  
        "usedScene": {"when": "inuse"}  
      }  
    ]  
  }  
}

服务卡片适配:桌面卡片让古诗 “触手可及”
鸿蒙的桌面卡片是个大亮点,我想做个 “每日一句” 卡片,用户不用打开 App,桌面就能看到古诗。

  • 坑点:UniAPP 打包鸿蒙应用时,卡片的尺寸适配很麻烦,比如 3x2、2x2 的卡片布局会错乱,而且卡片数据刷新需要和主 App 同步;
  • 解决:用 UniAPP 提供的uni-app x编译模式(专门优化鸿蒙适配),卡片布局用flex弹性盒,固定宽高比;数据同步用uniCloud实时数据库,主 App 更新 “每日一句” 后,卡片自动刷新,不用用户手动操作 —— 现在不少用户说 “每天解锁手机先看一眼古诗,感觉很治愈”。

音频播放:后台播放老是 “断档”
朗读广场的核心是 “听别人读古诗”,但一开始用户反馈 “退到后台就停了”,体验很差。

  • 坑点:鸿蒙对后台音频播放有严格限制,普通的audio组件在后台会被系统暂停;
  • 解决:集成了鸿蒙的AVPlayer SDK(UniAPP 可以直接调用鸿蒙原生 SDK),在manifest.json里配置 “后台音频权限”,同时用 UniAPP 的onBackground生命周期监听,退后台时自动切换到鸿蒙原生播放器,现在后台播放能稳定运行,用户还能边听古诗边刷微信~

四、📦功能落地:每个模块都是 “心头好”📦

结合古诗的核心需求,几个功能模块都是 “边踩坑边优化” 出来的,挑 3 个重点说:

  1. 背一背:用 “艾宾浩斯曲线” 让背诵不费脑
    一开始只是简单做了 “列表 + 背诵打卡”,但用户反馈 “背了就忘”。后来用 UniAPP 的storage存储用户背诵记录,结合艾宾浩斯遗忘曲线,在对应时间点推送提醒(比如第 1 天、第 3 天、第 7 天),还加了 “遮字默写” 功能 —— 点击诗句能隐藏关键字,像考试一样检验效果。

    • 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
      <view class="poem-content">  
      <block v-if="poemStore.showFullContent">  
        <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="full-line">{{ line }}</text>  
      </block>  
      <block v-else>  
        <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="hide-line">□□□□□□□</text>  
      </block>  
      </view>  
      <button class="toggle-btn" @click="poemStore.toggleFullContent">  
      {{ poemStore.showFullContent ? '切换到背诵模式' : '查看完整诗句' }}  
      </button>
      <script setup lang="ts">  
      import { usePoemStore } from '@/stores/poemStore';  
      import { useRouter } from 'vue-router';  
      const poemStore = usePoemStore();  
      const router = useRouter();  
      const goBack = () => {  
      router.back();  
      };  
      const handleCollect = () => {  
      if (poemStore.currentPoem) {  
      poemStore.toggleCollect(poemStore.currentPoem.id);  
      }  
      };  
      </script>
  2. 诗词 / 成语接龙:云开发让匹配更流畅
    接龙功能需要实时匹配用户输入的诗句 / 成语,还要校验是否正确、有没有重复。一开始用本地数据库,结果数据量太大(收录了 2 万 + 古诗、1 万 + 成语),App 启动变慢。

    export interface Poem {  
    id: number;  
    title: string; // 诗名  
    author: string; // 作者(含朝代)  
    content: string[]; // 诗句数组  
    category: string; // 分类(如山水、边塞)  
    isCollected: boolean; // 是否收藏  
    }
    • 优化:改用uniCloud云开发,把诗词库、成语库存到云端,用云函数做匹配校验(比如输入 “举头望明月”,云函数自动查询以 “月” 开头的诗句),App 端只负责展示和输入,启动速度从 3 秒降到 1 秒,接龙延迟也几乎感知不到。
  3. 主题颜色:贴合古风的 “颜值小心思”
    考虑到用户可能在不同场景使用(比如晚上背古诗要护眼),做了 3 种古风主题:黛青(默认)、朱砂(暖调)、月白(护眼)。

    • 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
      import { defineStore } from 'pinia';  
      import { Poem } from '@/types/poem';  
      const mockPoems: Poem[] = [  
      {  
      id: 1,  
      title: '望庐山瀑布',  
      author: '唐·李白',  
      content: ['日照香炉生紫烟', '遥看瀑布挂前川', '飞流直下三千尺', '疑是银河落九天'],  
      category: '山水',  
      isCollected: false  
      },  
      {  
      id: 2,  
      title: '静夜思',  
      author: '唐·李白',  
      content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'],  
      category: '思乡',  
      isCollected: false  
      },  
      {  
      id: 3,  
      title: '春晓',  
      author: '唐·孟浩然',  
      content: ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少'],  
      category: '春景',  
      isCollected: false  
      }  
      ];  
      export const usePoemStore = defineStore('poem', {  
      state: () => ({  
      poems: mockPoems as Poem[], // 古诗列表  
      currentPoem: null as Poem | null, // 当前选中古诗  
      showFullContent: false // 详情页是否显示完整诗句(背诵模式控制)  
      }),  
      actions: {  
      // 选中古诗  
      selectPoem(poemId: number) {  
      this.currentPoem = this.poems.find(poem => poem.id === poemId) || null;  
      this.showFullContent = false; // 进入背诵模式,默认隐藏完整诗句  
      },  
      // 切换收藏状态  
      toggleCollect(poemId: number) {  
      const poem = this.poems.find(poem => poem.id === poemId);  
      if (poem) poem.isCollected = !poem.isCollected;  
      },  
      // 切换完整诗句显示(背诵/查看模式)  
      toggleFullContent() {  
      this.showFullContent = !this.showFullContent;  
      }  
      }  
      });

五、✅ 后续功能想法 ✅

  • “想要更多主题颜色”→ 新增 “竹绿”“藤黄” 2 种配色;
  • “朗读广场想给作品点赞”→ 加了点赞功能,用uniCloud存储互动数据;
  • “背古诗想有奖励”→ 做了 “背诵勋章”,集齐 5 个勋章能解锁古诗插画壁纸。

虽然还未成功上架市场,但在开发中寻找用户体验过程中,最让我感动的是一条评论:“我家娃以前不爱背古诗,现在每天打卡接龙,还会给同学分享自己的朗读作品,谢谢开发者让传统文化变有趣~” 这种时候,觉得熬夜改 bug、反复适配都值了!

六、🌟星光不负:技术与文化的双向奔赴🌟

从一开始的 “想做个古诗工具”,到现在的 “让自己更了解古诗并爱上它”,用 UniAPP 开发鸿蒙 App 的这 3 个月,不仅让我摸清了跨平台开发的套路,更懂了 “技术是为需求服务”—— 不是堆功能,而是让应用真正有用起来、用得爽。
回头看,选择 UniAPP 是最正确的决定:它让我不用纠结原生语法,能把更多精力放在 “怎么让古诗更有趣” 上;而鸿蒙的开放能力(云开发、云调试、HarmonyOS SDK、云测试)等,又让 App 的体验更上一层楼。就像古诗里说的 “行则将至”,技术之路没有捷径,但只要朝着目标一步步走,星光总会照亮前路。
接下来,我还想加 AR 功能(用鸿蒙的 AR SDK,扫描实景生成古诗意境)、古诗合唱(朗读广场支持多人合拍),让传统文化通过技术 “活” 起来、“火” 起来。如果你也在做 UniAPP + 鸿蒙开发,或者对古诗 App 有更多想法,欢迎一起交流~ 毕竟,码向未来的路上,有人同行更精彩!

继续阅读 »

一款无广告重拾儿时趣味的古诗 App 的开发过程记录

一、🌈缘起:让古诗 “活” 起来,而非躺在角落🌈

做这款 App 的初衷,藏在三个真实需求里:
一次偶然的寻找碰上鸿蒙的星光闪烁:翻找学生时代的语文诗词,想重拾那份诗词氛围时,书本早已不见踪影,此时便想在手机上寻找这类APP,市面古诗 App 要么广告扎堆,要么功能枯燥,实在感受不到当年的那种氛围;
身边朋友想重拾古诗,却缺合适的背诵工具,也没有专属交流渠道(总不能天天在朋友圈发古诗);
古诗的意境与字词之美,不该只靠 “死记硬背”—— 结合互动、朗读、释义,让大家 “懂了再背”,才是真正的文化传承。
所以核心需求很明确:做一款 “不枯燥、能互动、够纯粹” 的古诗工具。既要满足学习需求,又要兼顾趣味性,还得适配鸿蒙系统(毕竟鸿蒙用户越来越多,生态也日趋完善)。

二、💡技术选型:为啥 “锁死” UniAPP?💡

选技术栈时纠结了 3 天,最终敲定 UniAPP,全靠 “实用主义”:
排除鸿蒙原生开发:适配鸿蒙虽丝滑,但单人开发要重新学原生语法,还无法兼顾其他平台,效率太低;
排除其他跨平台框架:要么对鸿蒙适配不完善,要么插件生态薄弱,像诗词朗读、音频上传等功能得手动造轮子;

选择 UniAPP 的核心原因:
跨平台省心:一套代码可打包鸿蒙 App,后续扩展安卓、iOS 版本无需重构,对个人开发者极度友好;

  • 鸿蒙适配 “自带 buff”:HBuilderX 提供现成打包模板和专属适配插件,不用手动修改大量配置;
  • 生态够全:uniCloud 云开发、音频组件、数据库插件,刚好满足古诗存储、朗读上传、接龙匹配等核心需求;
  • 学习成本低:有微信小程序开发基础,UniAPP 的 Vue 语法无缝衔接,1 周就能上手。
    简单说:用 UniAPP 开发鸿蒙,就像 “站在巨人肩膀上”—— 不用从零学鸿蒙原生,又能享受鸿蒙系统特性,对非专业开发者太友好了!

三、⚠️踩坑实录:鸿蒙适配那些 “磨人的小妖精”⚠️

开发前以为 “跨平台 = 一路顺畅”,结果真正适配鸿蒙时,还是踩了不少坑,分享 3 个最印象深刻的:

权限申请:鸿蒙的 “规矩” 和其他平台不一样
做朗读广场时,需要调用手机麦克风录音、存储音频文件。一开始直接复用了其他平台的权限代码,结果在鸿蒙上直接报错 —— 录音功能打不开!

  • 坑点:鸿蒙的 “媒体权限” 需要单独申请,而且必须配置ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA,还得在代码里动态弹窗说明 “为啥要要权限”(比如 “需要录音才能上传你的朗读作品”),光说 “需要权限” 会被系统拒绝;而且必须在鸿蒙的AGC后台进行同步申请该权限,因为该权限属于受限权限;而且!!!如果是使用的鸿蒙自家的隐私托管服务,必须要在隐私托管服务中同步该权限的声明
  • 解决:使用uts插件配置权限;然后在AGC后台同步申请该受限权限通过率直接 100%。
    // #ifdef APP-HARMONY  
    import "@/uni_modules/harmony-permissions"  
    // #endif 
 {  
  "module": {  
    "name": "uni_modules__harmony_permissions",  
    "type": "har",  
    "deviceTypes": [  
      "default",  
      "tablet",  
      "2in1"  
    ],  
    "requestPermissions": [  
      {  
        "name": "ohos.permission.INTERNET"  
      },{  
        "name": "ohos.permission.WRITE_MEDIA",  
        "reason": "$string:media_desc",  
        "usedScene": {"when": "inuse"}  
      },{  
        "name": "ohos.permission.READ_MEDIA",  
        "reason": "$string:media_desc",  
        "usedScene": {"when": "inuse"}  
      }  
    ]  
  }  
}

服务卡片适配:桌面卡片让古诗 “触手可及”
鸿蒙的桌面卡片是个大亮点,我想做个 “每日一句” 卡片,用户不用打开 App,桌面就能看到古诗。

  • 坑点:UniAPP 打包鸿蒙应用时,卡片的尺寸适配很麻烦,比如 3x2、2x2 的卡片布局会错乱,而且卡片数据刷新需要和主 App 同步;
  • 解决:用 UniAPP 提供的uni-app x编译模式(专门优化鸿蒙适配),卡片布局用flex弹性盒,固定宽高比;数据同步用uniCloud实时数据库,主 App 更新 “每日一句” 后,卡片自动刷新,不用用户手动操作 —— 现在不少用户说 “每天解锁手机先看一眼古诗,感觉很治愈”。

音频播放:后台播放老是 “断档”
朗读广场的核心是 “听别人读古诗”,但一开始用户反馈 “退到后台就停了”,体验很差。

  • 坑点:鸿蒙对后台音频播放有严格限制,普通的audio组件在后台会被系统暂停;
  • 解决:集成了鸿蒙的AVPlayer SDK(UniAPP 可以直接调用鸿蒙原生 SDK),在manifest.json里配置 “后台音频权限”,同时用 UniAPP 的onBackground生命周期监听,退后台时自动切换到鸿蒙原生播放器,现在后台播放能稳定运行,用户还能边听古诗边刷微信~

四、📦功能落地:每个模块都是 “心头好”📦

结合古诗的核心需求,几个功能模块都是 “边踩坑边优化” 出来的,挑 3 个重点说:

  1. 背一背:用 “艾宾浩斯曲线” 让背诵不费脑
    一开始只是简单做了 “列表 + 背诵打卡”,但用户反馈 “背了就忘”。后来用 UniAPP 的storage存储用户背诵记录,结合艾宾浩斯遗忘曲线,在对应时间点推送提醒(比如第 1 天、第 3 天、第 7 天),还加了 “遮字默写” 功能 —— 点击诗句能隐藏关键字,像考试一样检验效果。

    • 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
      <view class="poem-content">  
      <block v-if="poemStore.showFullContent">  
        <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="full-line">{{ line }}</text>  
      </block>  
      <block v-else>  
        <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="hide-line">□□□□□□□</text>  
      </block>  
      </view>  
      <button class="toggle-btn" @click="poemStore.toggleFullContent">  
      {{ poemStore.showFullContent ? '切换到背诵模式' : '查看完整诗句' }}  
      </button>
      <script setup lang="ts">  
      import { usePoemStore } from '@/stores/poemStore';  
      import { useRouter } from 'vue-router';  
      const poemStore = usePoemStore();  
      const router = useRouter();  
      const goBack = () => {  
      router.back();  
      };  
      const handleCollect = () => {  
      if (poemStore.currentPoem) {  
      poemStore.toggleCollect(poemStore.currentPoem.id);  
      }  
      };  
      </script>
  2. 诗词 / 成语接龙:云开发让匹配更流畅
    接龙功能需要实时匹配用户输入的诗句 / 成语,还要校验是否正确、有没有重复。一开始用本地数据库,结果数据量太大(收录了 2 万 + 古诗、1 万 + 成语),App 启动变慢。

    export interface Poem {  
    id: number;  
    title: string; // 诗名  
    author: string; // 作者(含朝代)  
    content: string[]; // 诗句数组  
    category: string; // 分类(如山水、边塞)  
    isCollected: boolean; // 是否收藏  
    }
    • 优化:改用uniCloud云开发,把诗词库、成语库存到云端,用云函数做匹配校验(比如输入 “举头望明月”,云函数自动查询以 “月” 开头的诗句),App 端只负责展示和输入,启动速度从 3 秒降到 1 秒,接龙延迟也几乎感知不到。
  3. 主题颜色:贴合古风的 “颜值小心思”
    考虑到用户可能在不同场景使用(比如晚上背古诗要护眼),做了 3 种古风主题:黛青(默认)、朱砂(暖调)、月白(护眼)。

    • 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
      import { defineStore } from 'pinia';  
      import { Poem } from '@/types/poem';  
      const mockPoems: Poem[] = [  
      {  
      id: 1,  
      title: '望庐山瀑布',  
      author: '唐·李白',  
      content: ['日照香炉生紫烟', '遥看瀑布挂前川', '飞流直下三千尺', '疑是银河落九天'],  
      category: '山水',  
      isCollected: false  
      },  
      {  
      id: 2,  
      title: '静夜思',  
      author: '唐·李白',  
      content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'],  
      category: '思乡',  
      isCollected: false  
      },  
      {  
      id: 3,  
      title: '春晓',  
      author: '唐·孟浩然',  
      content: ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少'],  
      category: '春景',  
      isCollected: false  
      }  
      ];  
      export const usePoemStore = defineStore('poem', {  
      state: () => ({  
      poems: mockPoems as Poem[], // 古诗列表  
      currentPoem: null as Poem | null, // 当前选中古诗  
      showFullContent: false // 详情页是否显示完整诗句(背诵模式控制)  
      }),  
      actions: {  
      // 选中古诗  
      selectPoem(poemId: number) {  
      this.currentPoem = this.poems.find(poem => poem.id === poemId) || null;  
      this.showFullContent = false; // 进入背诵模式,默认隐藏完整诗句  
      },  
      // 切换收藏状态  
      toggleCollect(poemId: number) {  
      const poem = this.poems.find(poem => poem.id === poemId);  
      if (poem) poem.isCollected = !poem.isCollected;  
      },  
      // 切换完整诗句显示(背诵/查看模式)  
      toggleFullContent() {  
      this.showFullContent = !this.showFullContent;  
      }  
      }  
      });

五、✅ 后续功能想法 ✅

  • “想要更多主题颜色”→ 新增 “竹绿”“藤黄” 2 种配色;
  • “朗读广场想给作品点赞”→ 加了点赞功能,用uniCloud存储互动数据;
  • “背古诗想有奖励”→ 做了 “背诵勋章”,集齐 5 个勋章能解锁古诗插画壁纸。

虽然还未成功上架市场,但在开发中寻找用户体验过程中,最让我感动的是一条评论:“我家娃以前不爱背古诗,现在每天打卡接龙,还会给同学分享自己的朗读作品,谢谢开发者让传统文化变有趣~” 这种时候,觉得熬夜改 bug、反复适配都值了!

六、🌟星光不负:技术与文化的双向奔赴🌟

从一开始的 “想做个古诗工具”,到现在的 “让自己更了解古诗并爱上它”,用 UniAPP 开发鸿蒙 App 的这 3 个月,不仅让我摸清了跨平台开发的套路,更懂了 “技术是为需求服务”—— 不是堆功能,而是让应用真正有用起来、用得爽。
回头看,选择 UniAPP 是最正确的决定:它让我不用纠结原生语法,能把更多精力放在 “怎么让古诗更有趣” 上;而鸿蒙的开放能力(云开发、云调试、HarmonyOS SDK、云测试)等,又让 App 的体验更上一层楼。就像古诗里说的 “行则将至”,技术之路没有捷径,但只要朝着目标一步步走,星光总会照亮前路。
接下来,我还想加 AR 功能(用鸿蒙的 AR SDK,扫描实景生成古诗意境)、古诗合唱(朗读广场支持多人合拍),让传统文化通过技术 “活” 起来、“火” 起来。如果你也在做 UniAPP + 鸿蒙开发,或者对古诗 App 有更多想法,欢迎一起交流~ 毕竟,码向未来的路上,有人同行更精彩!

收起阅读 »

基于vue3.5+vite7.2+tauri2.9搭建桌面客户端os系统后台模板

vue.js vite vue3

vue3-tauri2.9-os:最新原创研发vite7.2+tauri2.9+vue3 setup+pinia3+arcoDesign+echarts高颜值轻量级仿macOS/windows风格桌面os模式管理后台系统模板。支持可拖拽栅格布局桌面、JSON格式配置桌面菜单/Dock菜单。

项目技术知识

  • 跨平台框架:tauri^2.9
  • 前端技术框架:vite^7.2.2+vue^3.5.24+vue-router^4.6.3
  • UI组件库:@arco-design/web-vue^2.57.0
  • 状态管理:pinia^3.0.4
  • 拖拽插件:sortablejs^1.15.6
  • 滑屏插件:swiper^12.0.3
  • 图表组件:echarts^6.0.0
  • markdown编辑器:md-editor-v3^6.1.1
  • 模拟数据:mockjs^1.1.0

项目框架结构

使用最新版跨平台框架tauri2.9+vite7搭建项目模板,vue3 setup语法编码开发页面。

vue3-tauri2-os桌面版os系统已经更新到我的原创作品小铺。
tauri2.9+vite7+arco-design桌面端OS管理系统

热文推荐

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

继续阅读 »

vue3-tauri2.9-os:最新原创研发vite7.2+tauri2.9+vue3 setup+pinia3+arcoDesign+echarts高颜值轻量级仿macOS/windows风格桌面os模式管理后台系统模板。支持可拖拽栅格布局桌面、JSON格式配置桌面菜单/Dock菜单。

项目技术知识

  • 跨平台框架:tauri^2.9
  • 前端技术框架:vite^7.2.2+vue^3.5.24+vue-router^4.6.3
  • UI组件库:@arco-design/web-vue^2.57.0
  • 状态管理:pinia^3.0.4
  • 拖拽插件:sortablejs^1.15.6
  • 滑屏插件:swiper^12.0.3
  • 图表组件:echarts^6.0.0
  • markdown编辑器:md-editor-v3^6.1.1
  • 模拟数据:mockjs^1.1.0

项目框架结构

使用最新版跨平台框架tauri2.9+vite7搭建项目模板,vue3 setup语法编码开发页面。

vue3-tauri2-os桌面版os系统已经更新到我的原创作品小铺。
tauri2.9+vite7+arco-design桌面端OS管理系统

热文推荐

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

收起阅读 »

【鸿蒙征文】鸿蒙开发踩坑日记:一位前端开发者的血泪成长史

鸿蒙next 鸿蒙征文

日记一:环境搭建——“Hello, World!” 前的下马威

坑点: 模拟器启动失败,报错 HAXM is not installedVT-x is not available

踩坑过程:

信心满满地安装完 DevEco Studio,创建第一个 Hello World 项目,点击运行模拟器。结果,控制台报出一串红色错误,模拟器屏幕一片漆黑。心里“咯噔”一下,难道第一步就卡住了?

排查与解决:

  1. 检查BIOS: 重启电脑,狂按 F2/Del 键进入 BIOS 设置。找到 Intel Virtualization Technology(或 AMD 的 SVM Mode)选项,确保其状态为 Enabled。这是最根本的原因。
  2. 开启Windows功能: 在 Windows 搜索栏输入“启用或关闭 Windows 功能”,确保 Hyper-VWindows 虚拟机监控平台 已被勾选。完成后需要重启电脑。
  3. 选择正确的模拟器: 在 DevEco Studio 的设备管理器中,确保下载的是 API 9或更高版本的模拟器。低版本 API 的模拟器可能存在兼容性问题。

心得: 鸿蒙开发环境的搭建,第一步就是和硬件虚拟化打交道。这提醒我,移动开发生态已与 Web 开发那种“开箱即用”的体验截然不同。

日记二:UTS 类型系统 —— “像”TypeScript,但不是 TypeScript

坑点一:Map.forEach的消失

代码:

let myMap = new Map<string, number>();  
myMap.set('apple', 5);  
// 在 iOS 上运行良好,在鸿蒙上报错!  
myMap.forEach((value, key) => {  
  console.log(key, value);  
});

错误: TypeError: undefined is not callable

解决方案:

// 方案1:使用 for...of 循环  
for (let [key, value] of myMap.entries()) {  
  console.log(key, value);  
}  

// 方案2:更保险的做法,先判断平台或方法是否存在  
if (myMap.forEach) {  
  myMap.forEach((value, key) => { /* ... */ });  
} else {  
  for (let [key, value] of myMap.entries()) { /* ... */ }  
}

坑点二:隐式类型转换的终结

代码:

let data: string | null = getDataFromAPI();  
// Web 中常见的真值判断,在 UTS 中报错!  
if (data) {   
  processData(data);  
}

错误: The condition expression must be of boolean type.

解决方案:

// 必须进行显式的布尔判断  
if (data != null) {  
  processData(data);  
}  

// 或者判断字符串长度  
if (data?.length > 0) {  
  processData(data;  
}

心得: UTS 的强类型特性像一位严格的老师,逼着我写出更严谨、更少歧义的代码。虽然初期不适应,但从代码质量角度看,是件好事。

日记三:页面布局与样式 —— CSS 的“阉割版”体验

坑点一:100vh的陷阱与滚动失效

代码:

<template>  
  <view class="container">  
    <scroll-view scroll-y class="scroll-area">  
      <!-- 长内容 -->  
    </scroll-view>  
    <view class="fixed-bottom">我是一个底部固定栏</view>  
  </view>  
</template>  

<style>  
.container {  
  height: 100vh; /* 鸿蒙不支持 vh! */  
}  
.scroll-area {  
  height: 100%; /* 继承自一个高度无效的容器,滚动失效 */  
}  
</style>

现象: 页面无法滚动,scroll-view区域高度为 0。

解决方案:

<template>  
  <!-- 关键:让 scroll-view 作为根节点或充满整个页面 -->  
  <scroll-view scroll-y class="page-root">  
    <!-- 所有内容,包括原本想固定的元素,都放在里面 -->  
    <view class="content">可滚动内容</view>  
    <view class="bottom-placeholder"></view>  
    <view class="fixed-bottom">利用 absolute 或 fixed 定位模拟固定</view>  
  </scroll-view>  
</template>  

<style>  
.page-root {  
  width: 100%;  
  height: 100%; /* 使用 100% 而不是 100vh */  
  position: relative; /* 为固定定位元素提供参考 */  
}  
.fixed-bottom {  
  position: absolute;  
  bottom: 0;  
  width: 100%;  
}  
.bottom-placeholder {  
  height: 100rpx; /* 为固定底部留出占位空间,避免内容被遮挡 */  
}

坑点二:Flex 布局的微妙差异

在 Web 中,display: flex的容器默认是 flex-direction: row。在鸿蒙的 text组件嵌套 span时,我发现 Flex 布局的表现有时与 Web 有细微差别,导致文字排版错乱。

解决方案: 尽量显式地写明 flex-direction,避免依赖默认值。对于复杂布局,多使用 align-itemsjustify-content进行微调。

心得: 必须彻底抛弃 Web 的“文档流”思维,拥抱原生应用的“盒子模型”和明确的滚动容器概念。布局要更“笨拙”但更精确。

日记四:数据库操作 —— 最诡异的“数据隐身术”

坑点:查询结果对象“看起来有数据,却取不出来”

代码:

// 执行查询  
const result = db.query(‘SELECT name, count FROM ingredients’);  
const rows = result.maps;  
console.log(‘结果行数:’, rows.length); // 输出: 结果行数: 5  
console.log(‘第一行:’, JSON.stringify(rows[0])); // 输出: 第一行: {}  

// 以下代码在 iOS 上正常,在鸿蒙上为 undefined  
const name = rows[0][‘name’];

现象: 数据“幽灵”,日志显示有数据,但代码无法访问。

根源: 鸿蒙平台返回的 rows不是普通对象数组,而是 Map<string, any>[] JSON.stringify对 Map 序列化会得到空对象 {}

解决方案:

// 封装一个强大的兼容性取值函数  
function getRowValue(row: any, key: string, defaultValue: any = null): any {  
  if (row == null) return defaultValue;  

  // 方法1:判断是否是 Map  
  if (row instanceof Map) {  
    return row.get(key) ?? defaultValue;  
  }  
  // 方法2:判断是否拥有 get 方法 (更通用)  
  else if (typeof row.get === ‘function’) {  
    return row.get(key) ?? defaultValue;  
  }  
  // 方法3:默认为普通对象  
  else {  
    return row[key] ?? defaultValue;  
  }  
}  

// 使用  
const name = getRowValue(rows[0], ‘name’);  
const count = getRowValue(rows[0], ‘count’, 0);

心得: 跨平台开发中,“数据序列化/反序列化”是头号隐形杀手。不能相信表面现象,必须对关键数据的实际类型进行运行时判断。

日记五:网络请求与生命周期 —— 异步世界的时序陷阱

坑点:onLoad中发起请求,在 onReady中访问数据却为 null

代码:

export default {  
  data() {  
    return {  
      recipeData: null  
    }  
  },  
  onLoad() {  
    uni.request({  
      url: ‘/api/recipe’,  
      success: (res) => {  
        this.recipeData = res.data; // 异步赋值  
      }  
    });  
  },  
  onReady() {  
    // 企图使用数据渲染视图,但 recipeData 很可能还是 null!  
    this.renderChart(this.recipeData); // 报错!  
  }  
}

解决方案:

  1. 使用状态管理: 引入 Pinia,在请求成功的回调中提交 mutation 改变状态,组件通过 computed 属性响应式地获取数据。

  2. 条件渲染: 在模板中根据数据是否存在进行判断。

    <template>  
     <view>  
       <chart v-if=“recipeData” :data=“recipeData”></chart>  
       <loading v-else>加载中…</loading>  
     </view>  
    </template>  
  3. 使用 Promise/async-await: 确保在数据准备好后再执行后续操作。

    async onLoad() {  
     try {  
       this.recipeData = await this.fetchRecipeData();  
       // 数据获取成功后,再执行需要数据的操作  
       this.$nextTick(() => {  
         this.renderChart(this.recipeData);  
       });  
     } catch (error) {  
       console.error(‘数据加载失败:’, error);  
     }  
    }  

心得: 生命周期钩子和异步操作的配合,是前端开发永恒的课题。在鸿蒙这种更接近原生的环境中,对时序的控制要求更为严格。

总结:我的踩坑生存法则

  1. 假设一切都有平台差异: 对任何 API、组件、样式属性,都抱有一丝怀疑,优先查阅鸿蒙专属文档。
  2. 日志是最好(有时是唯一)的调试工具: 不仅要打印值,还要打印类型(typeof, instanceof)。
  3. 封装兼容层: 将平台差异(如数据库取值、网络库)封装成统一的工具函数,是长期项目的必备架构。
  4. 小步快跑,频繁测试: 每实现一个小功能,就在鸿蒙模拟器或真机上跑一遍,避免错误累积。
  5. 拥抱社区: uni-app 的官方论坛、钉钉群和 GitHub issues 是解决问题的宝库,你踩的坑,大概率已经有人踩过并分享了解决方案。

踩坑虽苦,但每解决一个坑,对鸿蒙平台和跨平台开发的理解就加深一层。现在回头看,这些坑都成了我宝贵的经验财富。希望这份日记,能为你照亮前行的路,助你少走弯路!

继续阅读 »

日记一:环境搭建——“Hello, World!” 前的下马威

坑点: 模拟器启动失败,报错 HAXM is not installedVT-x is not available

踩坑过程:

信心满满地安装完 DevEco Studio,创建第一个 Hello World 项目,点击运行模拟器。结果,控制台报出一串红色错误,模拟器屏幕一片漆黑。心里“咯噔”一下,难道第一步就卡住了?

排查与解决:

  1. 检查BIOS: 重启电脑,狂按 F2/Del 键进入 BIOS 设置。找到 Intel Virtualization Technology(或 AMD 的 SVM Mode)选项,确保其状态为 Enabled。这是最根本的原因。
  2. 开启Windows功能: 在 Windows 搜索栏输入“启用或关闭 Windows 功能”,确保 Hyper-VWindows 虚拟机监控平台 已被勾选。完成后需要重启电脑。
  3. 选择正确的模拟器: 在 DevEco Studio 的设备管理器中,确保下载的是 API 9或更高版本的模拟器。低版本 API 的模拟器可能存在兼容性问题。

心得: 鸿蒙开发环境的搭建,第一步就是和硬件虚拟化打交道。这提醒我,移动开发生态已与 Web 开发那种“开箱即用”的体验截然不同。

日记二:UTS 类型系统 —— “像”TypeScript,但不是 TypeScript

坑点一:Map.forEach的消失

代码:

let myMap = new Map<string, number>();  
myMap.set('apple', 5);  
// 在 iOS 上运行良好,在鸿蒙上报错!  
myMap.forEach((value, key) => {  
  console.log(key, value);  
});

错误: TypeError: undefined is not callable

解决方案:

// 方案1:使用 for...of 循环  
for (let [key, value] of myMap.entries()) {  
  console.log(key, value);  
}  

// 方案2:更保险的做法,先判断平台或方法是否存在  
if (myMap.forEach) {  
  myMap.forEach((value, key) => { /* ... */ });  
} else {  
  for (let [key, value] of myMap.entries()) { /* ... */ }  
}

坑点二:隐式类型转换的终结

代码:

let data: string | null = getDataFromAPI();  
// Web 中常见的真值判断,在 UTS 中报错!  
if (data) {   
  processData(data);  
}

错误: The condition expression must be of boolean type.

解决方案:

// 必须进行显式的布尔判断  
if (data != null) {  
  processData(data);  
}  

// 或者判断字符串长度  
if (data?.length > 0) {  
  processData(data;  
}

心得: UTS 的强类型特性像一位严格的老师,逼着我写出更严谨、更少歧义的代码。虽然初期不适应,但从代码质量角度看,是件好事。

日记三:页面布局与样式 —— CSS 的“阉割版”体验

坑点一:100vh的陷阱与滚动失效

代码:

<template>  
  <view class="container">  
    <scroll-view scroll-y class="scroll-area">  
      <!-- 长内容 -->  
    </scroll-view>  
    <view class="fixed-bottom">我是一个底部固定栏</view>  
  </view>  
</template>  

<style>  
.container {  
  height: 100vh; /* 鸿蒙不支持 vh! */  
}  
.scroll-area {  
  height: 100%; /* 继承自一个高度无效的容器,滚动失效 */  
}  
</style>

现象: 页面无法滚动,scroll-view区域高度为 0。

解决方案:

<template>  
  <!-- 关键:让 scroll-view 作为根节点或充满整个页面 -->  
  <scroll-view scroll-y class="page-root">  
    <!-- 所有内容,包括原本想固定的元素,都放在里面 -->  
    <view class="content">可滚动内容</view>  
    <view class="bottom-placeholder"></view>  
    <view class="fixed-bottom">利用 absolute 或 fixed 定位模拟固定</view>  
  </scroll-view>  
</template>  

<style>  
.page-root {  
  width: 100%;  
  height: 100%; /* 使用 100% 而不是 100vh */  
  position: relative; /* 为固定定位元素提供参考 */  
}  
.fixed-bottom {  
  position: absolute;  
  bottom: 0;  
  width: 100%;  
}  
.bottom-placeholder {  
  height: 100rpx; /* 为固定底部留出占位空间,避免内容被遮挡 */  
}

坑点二:Flex 布局的微妙差异

在 Web 中,display: flex的容器默认是 flex-direction: row。在鸿蒙的 text组件嵌套 span时,我发现 Flex 布局的表现有时与 Web 有细微差别,导致文字排版错乱。

解决方案: 尽量显式地写明 flex-direction,避免依赖默认值。对于复杂布局,多使用 align-itemsjustify-content进行微调。

心得: 必须彻底抛弃 Web 的“文档流”思维,拥抱原生应用的“盒子模型”和明确的滚动容器概念。布局要更“笨拙”但更精确。

日记四:数据库操作 —— 最诡异的“数据隐身术”

坑点:查询结果对象“看起来有数据,却取不出来”

代码:

// 执行查询  
const result = db.query(‘SELECT name, count FROM ingredients’);  
const rows = result.maps;  
console.log(‘结果行数:’, rows.length); // 输出: 结果行数: 5  
console.log(‘第一行:’, JSON.stringify(rows[0])); // 输出: 第一行: {}  

// 以下代码在 iOS 上正常,在鸿蒙上为 undefined  
const name = rows[0][‘name’];

现象: 数据“幽灵”,日志显示有数据,但代码无法访问。

根源: 鸿蒙平台返回的 rows不是普通对象数组,而是 Map<string, any>[] JSON.stringify对 Map 序列化会得到空对象 {}

解决方案:

// 封装一个强大的兼容性取值函数  
function getRowValue(row: any, key: string, defaultValue: any = null): any {  
  if (row == null) return defaultValue;  

  // 方法1:判断是否是 Map  
  if (row instanceof Map) {  
    return row.get(key) ?? defaultValue;  
  }  
  // 方法2:判断是否拥有 get 方法 (更通用)  
  else if (typeof row.get === ‘function’) {  
    return row.get(key) ?? defaultValue;  
  }  
  // 方法3:默认为普通对象  
  else {  
    return row[key] ?? defaultValue;  
  }  
}  

// 使用  
const name = getRowValue(rows[0], ‘name’);  
const count = getRowValue(rows[0], ‘count’, 0);

心得: 跨平台开发中,“数据序列化/反序列化”是头号隐形杀手。不能相信表面现象,必须对关键数据的实际类型进行运行时判断。

日记五:网络请求与生命周期 —— 异步世界的时序陷阱

坑点:onLoad中发起请求,在 onReady中访问数据却为 null

代码:

export default {  
  data() {  
    return {  
      recipeData: null  
    }  
  },  
  onLoad() {  
    uni.request({  
      url: ‘/api/recipe’,  
      success: (res) => {  
        this.recipeData = res.data; // 异步赋值  
      }  
    });  
  },  
  onReady() {  
    // 企图使用数据渲染视图,但 recipeData 很可能还是 null!  
    this.renderChart(this.recipeData); // 报错!  
  }  
}

解决方案:

  1. 使用状态管理: 引入 Pinia,在请求成功的回调中提交 mutation 改变状态,组件通过 computed 属性响应式地获取数据。

  2. 条件渲染: 在模板中根据数据是否存在进行判断。

    <template>  
     <view>  
       <chart v-if=“recipeData” :data=“recipeData”></chart>  
       <loading v-else>加载中…</loading>  
     </view>  
    </template>  
  3. 使用 Promise/async-await: 确保在数据准备好后再执行后续操作。

    async onLoad() {  
     try {  
       this.recipeData = await this.fetchRecipeData();  
       // 数据获取成功后,再执行需要数据的操作  
       this.$nextTick(() => {  
         this.renderChart(this.recipeData);  
       });  
     } catch (error) {  
       console.error(‘数据加载失败:’, error);  
     }  
    }  

心得: 生命周期钩子和异步操作的配合,是前端开发永恒的课题。在鸿蒙这种更接近原生的环境中,对时序的控制要求更为严格。

总结:我的踩坑生存法则

  1. 假设一切都有平台差异: 对任何 API、组件、样式属性,都抱有一丝怀疑,优先查阅鸿蒙专属文档。
  2. 日志是最好(有时是唯一)的调试工具: 不仅要打印值,还要打印类型(typeof, instanceof)。
  3. 封装兼容层: 将平台差异(如数据库取值、网络库)封装成统一的工具函数,是长期项目的必备架构。
  4. 小步快跑,频繁测试: 每实现一个小功能,就在鸿蒙模拟器或真机上跑一遍,避免错误累积。
  5. 拥抱社区: uni-app 的官方论坛、钉钉群和 GitHub issues 是解决问题的宝库,你踩的坑,大概率已经有人踩过并分享了解决方案。

踩坑虽苦,但每解决一个坑,对鸿蒙平台和跨平台开发的理解就加深一层。现在回头看,这些坑都成了我宝贵的经验财富。希望这份日记,能为你照亮前行的路,助你少走弯路!

收起阅读 »

【鸿蒙征文】uni-app 打通鸿蒙踩坑日记:从迷茫到上架的完整心路历程

鸿蒙next 鸿蒙征文

缘起:为什么选择鸿蒙

作为一名前端开发者,我一直在关注各种新兴的技术生态。当华为宣布鸿蒙系统将独立发展时,我就意识到这可能是下一个重要的技术风口。但真正促使我动手的,还是实际的需求痛点。

我们团队有一个成熟的 uni-app 项目,已经在 iOS 和 Android 上稳定运行了两年。但随着华为设备市场份额的不断扩大,越来越多的用户反馈希望有鸿蒙版本。看着应用商店里竞品陆续上线鸿蒙版本,我决定亲自趟一趟这趟"浑水"。

从今年 3 月开始,我花了近三个月时间,完成了从技术调研到实际上架的完整过程。今天就来分享这段充满挑战又收获满满的经历。

技术选型:为什么坚持 uni-app

在开始之前,团队内部有过激烈的讨论:是重新开发原生鸿蒙应用,还是基于现有 uni-app 项目进行适配?

重新开发原生鸿蒙的优势

  • 性能最优,体验最佳
  • 可以充分利用鸿蒙的特有能力
  • 技术栈更纯粹

但最终我们还是选择了 uni-app 适配,原因很现实:

  1. 开发成本:团队熟悉 Vue 技术栈,重新学习 ArkTS 成本太高
  2. 维护成本:三套代码意味着三倍的维护工作量
  3. 迭代速度:业务需求变化快,需要快速响应
  4. DCloud 的承诺:官方明确表示会持续加强鸿蒙支持

事后证明,这个选择是正确的,但过程远比想象中曲折。

环境搭建:第一个坑就栽了跟头

按照官方文档,我信心满满地开始环境搭建。HBuilderX、DevEco Studio、Node.js,一切看起来都很简单。

第一个坑:模拟器选择

文档说"下载 API 19 模拟器即可",但我下载后始终无法启动。各种错误提示看得我头皮发麻:

模拟器启动失败:硬件加速未开启  
VT-x 不可用,请检查 BIOS 设置

我花了整整两天时间:

  • 重启电脑进入 BIOS 开启虚拟化
  • 在 Windows 功能中启用 Hyper-V
  • 更新显卡驱动
  • 重装 DevEco Studio

最后发现是杀毒软件冲突。关闭某数字卫士后,模拟器终于正常启动了。

经验教训:环境问题不要死磕,及时换思路。后来我直接使用真机调试,效率反而更高。

项目迁移:看似顺利的开始

将现有 uni-app 项目迁移到鸿蒙,最初进展出乎意料的顺利。

基础页面适配

大部分 Vue 页面无需修改即可运行,uni-app 的跨端能力确实令人印象深刻:

<template>  
  <view class="container">  
    <text class="title">{{ title }}</text>  
    <button @click="handleClick">点击我</button>  
  </view>  
</template>  

<script>  
export default {  
  data() {  
    return {  
      title: 'Hello HarmonyOS'  
    }  
  },  
  methods: {  
    handleClick() {  
      uni.showToast({  
        title: '点击成功'  
      })  
    }  
  }  
}  
</script>  

<style>  
.container {  
  padding: 20px;  
}  
.title {  
  font-size: 18px;  
  color: #333;  
}  
</style>

路由配置

pages.json的配置也完全兼容:

{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "navigationBarTitleText": "首页"  
      }  
    },  
    {  
      "path": "pages/detail/detail",   
      "style": {  
        "navigationBarTitleText": "详情页"  
      }  
    }  
  ]  
}

静态资源

static目录下的图片、字体等资源都能正常加载。前 80% 的工作在两天内就完成了,我甚至觉得鸿蒙开发不过如此。

但接下来的 20%,花了我 80% 的时间。

深水区:平台特定适配

导航栏差异

第一个平台差异出现在导航栏。在 iOS 和 Android 上正常的导航栏,在鸿蒙上出现了错位。

问题现象

  • 标题位置偏移
  • 返回按钮点击区域异常
  • 状态栏颜色不匹配

解决方案

pages.json中为鸿蒙平台单独配置:

{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "navigationBarTitleText": "首页",  
        "app-plus": {  
          "titleNView": {  
            // iOS/Android 配置  
          }  
        },  
        "harmony": {  
          "navigationBarBackgroundColor": "#FFFFFF",  
          "navigationBarTextStyle": "black",  
          "navigationBarTitleText": "首页",  
          "navigationStyle": "default"  
        }  
      }  
    }  
  ]  
}

组件兼容性问题

更大的挑战来自组件兼容性。我们项目中使用了大量第三方组件,有些在鸿蒙上表现异常。

案例:图片裁剪组件

在 iOS/Android 上正常的图片裁剪组件,在鸿蒙上出现以下问题:

  • 触摸事件响应错乱
  • 裁剪框位置计算错误
  • 性能卡顿明显

排查过程

  1. 首先怀疑是触摸事件机制差异
  2. 尝试修改事件处理逻辑,效果有限
  3. 深入组件源码,发现使用了 DOM API
  4. 鸿蒙不支持部分 DOM API

最终方案

寻找替代组件,选择明确支持鸿蒙的图片裁剪库。这个过程让我意识到:不是所有 web 生态都能无缝迁移到鸿蒙

API 兼容性

uni-app 的 API 在鸿蒙上的支持程度不一:

完全兼容的

  • uni.showToast
  • uni.request
  • uni.getStorage
  • uni.chooseImage

需要适配的

  • uni.getSystemInfo返回的信息结构不同
  • uni.onKeyboardHeightChange在鸿蒙上触发机制不同
  • 部分设备 API 需要鸿蒙特定实现

不支持的

  • 某些平台特定的扩展 API

我们建立了兼容性检查清单,对每个 API 进行测试:

// 兼容性封装示例  
export const getSafeArea = () => {  
  return new Promise((resolve, reject) => {  
    uni.getSystemInfo({  
      success: (res) => {  
        // 鸿蒙平台特殊处理  
        if (res.platform === 'harmony') {  
          resolve({  
            top: res.statusBarHeight || 0,  
            bottom: 0,  
            left: 0,  
            right: 0  
          })  
        } else {  
          resolve(res.safeArea)  
        }  
      },  
      fail: reject  
    })  
  })  
}

性能优化:从卡顿到流畅

首次在真机上运行应用时,性能问题让我震惊:列表滚动卡顿、页面切换白屏、内存占用过高。

列表性能优化

问题:商品列表页有 1000+ 项目,滚动时严重卡顿。

分析

  • 鸿蒙的列表渲染机制与 web 不同
  • 大量 DOM 节点导致内存压力
  • 图片加载没有做优化

解决方案

  1. 虚拟列表
<template>  
  <view class="list-container">  
    <unicloud-db   
      ref="udb"  
      collection="goods"  
      where="category_id == categoryId"  
      orderby="create_time desc"  
      :page-size="20"  
      @load="onListLoad"  
      v-slot:default="{data, loading, error}">  

      <virtual-list   
        :data="data"  
        :item-size="100"  
        key-field="_id">  
        <template v-slot:default="{item}">  
          <goods-item :item="item" />  
        </template>  
      </virtual-list>  

    </unicloud-db>  
  </view>  
</template>
  1. 图片懒加载
<image   
  :src="item.image"   
  mode="aspectFill"  
  lazy-load  
  :fade-show="false"  
  @load="onImageLoad"  
  @error="onImageError">  
</image>
  1. 内存管理
// 监听页面生命周期  
onPageScroll(e) {  
  // 可视区域外的图片取消加载  
  this.checkVisibleImages()  
},  

onHide() {  
  // 页面隐藏时释放大资源  
  this.clearCache()  
}

启动速度优化

问题:应用冷启动需要 3-5 秒,体验较差。

优化措施

  1. 代码分包
// pages.json  
{  
  "subPackages": [  
    {  
      "root": "pagesA",  
      "pages": [  
        "page1/page1",  
        "page2/page2"  
      ]  
    },  
    {  
      "root": "pagesB",   
      "pages": [  
        "page3/page3",  
        "page4/page4"  
      ]  
    }  
  ]  
}
  1. 预加载关键资源
// app.vue  
onLaunch() {  
  // 预加载首屏必要数据  
  this.preloadEssentialData()  

  // 异步加载非关键资源  
  setTimeout(() => {  
    this.preloadSecondaryResources()  
  }, 1000)  
}
  1. 减少同步操作
// 优化前:同步阻塞  
const userInfo = uni.getStorageSync('userInfo')  
const settings = uni.getStorageSync('settings')  

// 优化后:异步并行  
Promise.all([  
  this.getStorage('userInfo'),  
  this.getStorage('settings')  
]).then(([userInfo, settings]) => {  
  // 处理数据  
})

经过优化后,启动时间缩短到 1-2 秒,列表滚动帧率稳定在 60fps。

打包发布:证书的"坑爹"之旅

如果说代码适配是技术挑战,那证书和打包就是流程挑战。

证书配置的坑

第一个坑:证书类型混淆

我一开始把调试证书当成发布证书使用,结果打包时各种错误:

错误: 证书类型不匹配  
错误: 签名验证失败

解决方案

  • 调试证书:用于开发阶段真机调试
  • 发布证书:用于应用商店上架
  • 两者不能混用

第二个坑:证书有效期

发布证书有一年有效期,但测试证书只有三个月。我第一次提交审核时,证书差点过期。

经验:设置证书到期提醒,提前一个月更新。

打包流程优化

手动打包效率低下,我建立了自动化流程:

// package.json 脚本  
{  
  "scripts": {  
    "build:harmony:debug": "uni build --platform harmony --mode development",  
    "build:harmony:release": "uni build --platform harmony --mode production",  
    "pack:harmony": "node scripts/pack-harmony.js",  
    "deploy:harmony": "npm run build:harmony:release && npm run pack:harmony"  
  }  
}  
// scripts/pack-harmony.js  
const { execSync } = require('child_process')  
const fs = require('fs')  
const path = require('path')  

console.log('🚀 开始鸿蒙应用打包...')  

// 检查构建产物  
const buildPath = path.join(__dirname, '../dist/build/harmony')  
if (!fs.existsSync(buildPath)) {  
  console.error('❌ 构建产物不存在,请先执行构建')  
  process.exit(1)  
}  

// 执行鸿蒙特定打包逻辑  
try {  
  execSync('node scripts/harmony-specific-pack.js', { stdio: 'inherit' })  
  console.log('✅ 鸿蒙应用打包完成')  
} catch (error) {  
  console.error('❌ 打包失败:', error)  
  process.exit(1)  
}

上架审核:意料之外的拒绝

第一次提交审核时,我信心满满,觉得经过充分测试的应用肯定能一次通过。结果被打脸了。

审核拒绝原因

  1. 隐私政策不完整缺少数据存储说明未说明第三方 SDK 数据收集
  2. 权限说明不清晰相机权限用途描述过于简单位置权限必要性未充分说明
  3. 应用截图不规范截图包含状态栏时间"17:30"部分截图尺寸不一致

解决方案

隐私政策完善

## 数据存储说明  
本应用使用本地存储保存用户偏好设置,使用华为云存储备份用户数据。所有数据均加密存储。  

## 第三方 SDK 说明  
- 华为账号 SDK:用于用户登录认证  
- 华为推送 SDK:用于消息推送  
- 微信分享 SDK:用于内容分享

权限说明重写

## 相机权限  
用途:用于扫描商品条形码、拍摄商品照片  
必要性:核心功能需要,无法替代  
数据处理:图片仅在本地处理,不上传服务器  

## 位置权限    
用途:用于推荐附近门店、配送地址定位  
必要性:增值功能需要,非核心功能  
数据处理:位置信息加密传输到服务器

截图规范

  • 使用模拟器统一截图
  • 隐藏状态栏敏感信息
  • 确保所有截图尺寸一致
  • 展示核心功能流程

第二次提交后,顺利通过审核。

监控与反馈:上架只是开始

应用上架后,真正的挑战才开始。用户反馈和线上监控发现了我们测试中未发现的问题。

线上问题排查

案例:特定机型闪退

有用户反馈在华为 Mate 40 上频繁闪退,但我们测试机上正常。

排查过程

  1. 查看 AGC 崩溃日志
  2. 发现内存溢出错误
  3. 定位到图片加载组件
  4. 特定机型内存管理更严格

解决方案

// 图片加载优化  
loadImage(url) {  
  return new Promise((resolve, reject) => {  
    const img = new Image()  

    // 限制图片尺寸  
    if (this.isLowMemoryDevice()) {  
      url = this.addImageSizeParams(url, '800x600')  
    }  

    img.onload = () => {  
      // 及时释放引用  
      img.onload = null  
      img.onerror = null  
      resolve(img)  
    }  

    img.onerror = reject  
    img.src = url  
  })  
}

用户反馈处理

建立用户反馈响应机制:

  • 24 小时内响应严重问题
  • 每周汇总用户反馈
  • 每月发布优化版本

经验总结与建议

技术层面

  1. 组件选择要谨慎优先选择官方组件验证第三方组件兼容性准备备用方案
  2. 性能优化要前置开始就考虑性能问题建立性能监控体系定期进行性能回归测试

流程层面

  1. 证书管理要规范区分调试和发布证书设置证书到期提醒备份证书文件
  2. 审核准备要充分仔细阅读审核指南准备完整的说明材料提前测试审核流程
  3. 监控体系要完善建立崩溃监控收集用户反馈定期分析使用数据

给后来者的建议

  1. 不要畏惧鸿蒙开发uni-app 已经提供了很好的支持大部分代码可以复用社区资源越来越丰富
  2. 从小功能开始尝试先移植简单页面逐步增加复杂度积累经验再处理核心功能
  3. 善用官方资源DCloud 文档很详细华为开发者社区活跃官方技术支持响应快
  4. 加入开发者社区学习他人经验分享自己的收获共同推动生态发展

未来规划

鸿蒙版本的成功上线,给了我们很大信心。下一步计划:

  1. 深度集成鸿蒙特性原子化服务探索分布式能力应用鸿蒙特有 UI 交互
  2. 性能持续优化启动速度优化到 1 秒内内存占用降低 30%功耗优化
  3. 多端协同与 iOS/Android 版本功能同步数据互通体验优化统一的设计语言

写在最后

三个月的鸿蒙适配之旅,让我深刻体会到:技术转型虽然痛苦,但收获远超预期。

最大的收获不是技术上架,而是思维转变:从"web 思维"到"多端思维",从"功能实现"到"体验优化"。

鸿蒙生态还在快速发展,现在入局正是好时机。uni-app 为跨端开发提供了很好的基础,让我们能够以较低成本探索新平台。

如果你也在考虑鸿蒙开发,我的建议是:不要观望,直接开始。从简单的 demo 开始,逐步深入,你会发现鸿蒙开发没有想象中那么难。

在这个过程中,你会遇到各种问题,但也会收获解决问题的成就感。更重要的是,你将成为新生态的早期参与者,这本身就是宝贵的机会。

希望这篇文章能帮助到正在或准备进行鸿蒙开发的你。如果遇到问题,欢迎交流讨论,我们一起推动鸿蒙生态的繁荣!

继续阅读 »

缘起:为什么选择鸿蒙

作为一名前端开发者,我一直在关注各种新兴的技术生态。当华为宣布鸿蒙系统将独立发展时,我就意识到这可能是下一个重要的技术风口。但真正促使我动手的,还是实际的需求痛点。

我们团队有一个成熟的 uni-app 项目,已经在 iOS 和 Android 上稳定运行了两年。但随着华为设备市场份额的不断扩大,越来越多的用户反馈希望有鸿蒙版本。看着应用商店里竞品陆续上线鸿蒙版本,我决定亲自趟一趟这趟"浑水"。

从今年 3 月开始,我花了近三个月时间,完成了从技术调研到实际上架的完整过程。今天就来分享这段充满挑战又收获满满的经历。

技术选型:为什么坚持 uni-app

在开始之前,团队内部有过激烈的讨论:是重新开发原生鸿蒙应用,还是基于现有 uni-app 项目进行适配?

重新开发原生鸿蒙的优势

  • 性能最优,体验最佳
  • 可以充分利用鸿蒙的特有能力
  • 技术栈更纯粹

但最终我们还是选择了 uni-app 适配,原因很现实:

  1. 开发成本:团队熟悉 Vue 技术栈,重新学习 ArkTS 成本太高
  2. 维护成本:三套代码意味着三倍的维护工作量
  3. 迭代速度:业务需求变化快,需要快速响应
  4. DCloud 的承诺:官方明确表示会持续加强鸿蒙支持

事后证明,这个选择是正确的,但过程远比想象中曲折。

环境搭建:第一个坑就栽了跟头

按照官方文档,我信心满满地开始环境搭建。HBuilderX、DevEco Studio、Node.js,一切看起来都很简单。

第一个坑:模拟器选择

文档说"下载 API 19 模拟器即可",但我下载后始终无法启动。各种错误提示看得我头皮发麻:

模拟器启动失败:硬件加速未开启  
VT-x 不可用,请检查 BIOS 设置

我花了整整两天时间:

  • 重启电脑进入 BIOS 开启虚拟化
  • 在 Windows 功能中启用 Hyper-V
  • 更新显卡驱动
  • 重装 DevEco Studio

最后发现是杀毒软件冲突。关闭某数字卫士后,模拟器终于正常启动了。

经验教训:环境问题不要死磕,及时换思路。后来我直接使用真机调试,效率反而更高。

项目迁移:看似顺利的开始

将现有 uni-app 项目迁移到鸿蒙,最初进展出乎意料的顺利。

基础页面适配

大部分 Vue 页面无需修改即可运行,uni-app 的跨端能力确实令人印象深刻:

<template>  
  <view class="container">  
    <text class="title">{{ title }}</text>  
    <button @click="handleClick">点击我</button>  
  </view>  
</template>  

<script>  
export default {  
  data() {  
    return {  
      title: 'Hello HarmonyOS'  
    }  
  },  
  methods: {  
    handleClick() {  
      uni.showToast({  
        title: '点击成功'  
      })  
    }  
  }  
}  
</script>  

<style>  
.container {  
  padding: 20px;  
}  
.title {  
  font-size: 18px;  
  color: #333;  
}  
</style>

路由配置

pages.json的配置也完全兼容:

{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "navigationBarTitleText": "首页"  
      }  
    },  
    {  
      "path": "pages/detail/detail",   
      "style": {  
        "navigationBarTitleText": "详情页"  
      }  
    }  
  ]  
}

静态资源

static目录下的图片、字体等资源都能正常加载。前 80% 的工作在两天内就完成了,我甚至觉得鸿蒙开发不过如此。

但接下来的 20%,花了我 80% 的时间。

深水区:平台特定适配

导航栏差异

第一个平台差异出现在导航栏。在 iOS 和 Android 上正常的导航栏,在鸿蒙上出现了错位。

问题现象

  • 标题位置偏移
  • 返回按钮点击区域异常
  • 状态栏颜色不匹配

解决方案

pages.json中为鸿蒙平台单独配置:

{  
  "pages": [  
    {  
      "path": "pages/index/index",  
      "style": {  
        "navigationBarTitleText": "首页",  
        "app-plus": {  
          "titleNView": {  
            // iOS/Android 配置  
          }  
        },  
        "harmony": {  
          "navigationBarBackgroundColor": "#FFFFFF",  
          "navigationBarTextStyle": "black",  
          "navigationBarTitleText": "首页",  
          "navigationStyle": "default"  
        }  
      }  
    }  
  ]  
}

组件兼容性问题

更大的挑战来自组件兼容性。我们项目中使用了大量第三方组件,有些在鸿蒙上表现异常。

案例:图片裁剪组件

在 iOS/Android 上正常的图片裁剪组件,在鸿蒙上出现以下问题:

  • 触摸事件响应错乱
  • 裁剪框位置计算错误
  • 性能卡顿明显

排查过程

  1. 首先怀疑是触摸事件机制差异
  2. 尝试修改事件处理逻辑,效果有限
  3. 深入组件源码,发现使用了 DOM API
  4. 鸿蒙不支持部分 DOM API

最终方案

寻找替代组件,选择明确支持鸿蒙的图片裁剪库。这个过程让我意识到:不是所有 web 生态都能无缝迁移到鸿蒙

API 兼容性

uni-app 的 API 在鸿蒙上的支持程度不一:

完全兼容的

  • uni.showToast
  • uni.request
  • uni.getStorage
  • uni.chooseImage

需要适配的

  • uni.getSystemInfo返回的信息结构不同
  • uni.onKeyboardHeightChange在鸿蒙上触发机制不同
  • 部分设备 API 需要鸿蒙特定实现

不支持的

  • 某些平台特定的扩展 API

我们建立了兼容性检查清单,对每个 API 进行测试:

// 兼容性封装示例  
export const getSafeArea = () => {  
  return new Promise((resolve, reject) => {  
    uni.getSystemInfo({  
      success: (res) => {  
        // 鸿蒙平台特殊处理  
        if (res.platform === 'harmony') {  
          resolve({  
            top: res.statusBarHeight || 0,  
            bottom: 0,  
            left: 0,  
            right: 0  
          })  
        } else {  
          resolve(res.safeArea)  
        }  
      },  
      fail: reject  
    })  
  })  
}

性能优化:从卡顿到流畅

首次在真机上运行应用时,性能问题让我震惊:列表滚动卡顿、页面切换白屏、内存占用过高。

列表性能优化

问题:商品列表页有 1000+ 项目,滚动时严重卡顿。

分析

  • 鸿蒙的列表渲染机制与 web 不同
  • 大量 DOM 节点导致内存压力
  • 图片加载没有做优化

解决方案

  1. 虚拟列表
<template>  
  <view class="list-container">  
    <unicloud-db   
      ref="udb"  
      collection="goods"  
      where="category_id == categoryId"  
      orderby="create_time desc"  
      :page-size="20"  
      @load="onListLoad"  
      v-slot:default="{data, loading, error}">  

      <virtual-list   
        :data="data"  
        :item-size="100"  
        key-field="_id">  
        <template v-slot:default="{item}">  
          <goods-item :item="item" />  
        </template>  
      </virtual-list>  

    </unicloud-db>  
  </view>  
</template>
  1. 图片懒加载
<image   
  :src="item.image"   
  mode="aspectFill"  
  lazy-load  
  :fade-show="false"  
  @load="onImageLoad"  
  @error="onImageError">  
</image>
  1. 内存管理
// 监听页面生命周期  
onPageScroll(e) {  
  // 可视区域外的图片取消加载  
  this.checkVisibleImages()  
},  

onHide() {  
  // 页面隐藏时释放大资源  
  this.clearCache()  
}

启动速度优化

问题:应用冷启动需要 3-5 秒,体验较差。

优化措施

  1. 代码分包
// pages.json  
{  
  "subPackages": [  
    {  
      "root": "pagesA",  
      "pages": [  
        "page1/page1",  
        "page2/page2"  
      ]  
    },  
    {  
      "root": "pagesB",   
      "pages": [  
        "page3/page3",  
        "page4/page4"  
      ]  
    }  
  ]  
}
  1. 预加载关键资源
// app.vue  
onLaunch() {  
  // 预加载首屏必要数据  
  this.preloadEssentialData()  

  // 异步加载非关键资源  
  setTimeout(() => {  
    this.preloadSecondaryResources()  
  }, 1000)  
}
  1. 减少同步操作
// 优化前:同步阻塞  
const userInfo = uni.getStorageSync('userInfo')  
const settings = uni.getStorageSync('settings')  

// 优化后:异步并行  
Promise.all([  
  this.getStorage('userInfo'),  
  this.getStorage('settings')  
]).then(([userInfo, settings]) => {  
  // 处理数据  
})

经过优化后,启动时间缩短到 1-2 秒,列表滚动帧率稳定在 60fps。

打包发布:证书的"坑爹"之旅

如果说代码适配是技术挑战,那证书和打包就是流程挑战。

证书配置的坑

第一个坑:证书类型混淆

我一开始把调试证书当成发布证书使用,结果打包时各种错误:

错误: 证书类型不匹配  
错误: 签名验证失败

解决方案

  • 调试证书:用于开发阶段真机调试
  • 发布证书:用于应用商店上架
  • 两者不能混用

第二个坑:证书有效期

发布证书有一年有效期,但测试证书只有三个月。我第一次提交审核时,证书差点过期。

经验:设置证书到期提醒,提前一个月更新。

打包流程优化

手动打包效率低下,我建立了自动化流程:

// package.json 脚本  
{  
  "scripts": {  
    "build:harmony:debug": "uni build --platform harmony --mode development",  
    "build:harmony:release": "uni build --platform harmony --mode production",  
    "pack:harmony": "node scripts/pack-harmony.js",  
    "deploy:harmony": "npm run build:harmony:release && npm run pack:harmony"  
  }  
}  
// scripts/pack-harmony.js  
const { execSync } = require('child_process')  
const fs = require('fs')  
const path = require('path')  

console.log('🚀 开始鸿蒙应用打包...')  

// 检查构建产物  
const buildPath = path.join(__dirname, '../dist/build/harmony')  
if (!fs.existsSync(buildPath)) {  
  console.error('❌ 构建产物不存在,请先执行构建')  
  process.exit(1)  
}  

// 执行鸿蒙特定打包逻辑  
try {  
  execSync('node scripts/harmony-specific-pack.js', { stdio: 'inherit' })  
  console.log('✅ 鸿蒙应用打包完成')  
} catch (error) {  
  console.error('❌ 打包失败:', error)  
  process.exit(1)  
}

上架审核:意料之外的拒绝

第一次提交审核时,我信心满满,觉得经过充分测试的应用肯定能一次通过。结果被打脸了。

审核拒绝原因

  1. 隐私政策不完整缺少数据存储说明未说明第三方 SDK 数据收集
  2. 权限说明不清晰相机权限用途描述过于简单位置权限必要性未充分说明
  3. 应用截图不规范截图包含状态栏时间"17:30"部分截图尺寸不一致

解决方案

隐私政策完善

## 数据存储说明  
本应用使用本地存储保存用户偏好设置,使用华为云存储备份用户数据。所有数据均加密存储。  

## 第三方 SDK 说明  
- 华为账号 SDK:用于用户登录认证  
- 华为推送 SDK:用于消息推送  
- 微信分享 SDK:用于内容分享

权限说明重写

## 相机权限  
用途:用于扫描商品条形码、拍摄商品照片  
必要性:核心功能需要,无法替代  
数据处理:图片仅在本地处理,不上传服务器  

## 位置权限    
用途:用于推荐附近门店、配送地址定位  
必要性:增值功能需要,非核心功能  
数据处理:位置信息加密传输到服务器

截图规范

  • 使用模拟器统一截图
  • 隐藏状态栏敏感信息
  • 确保所有截图尺寸一致
  • 展示核心功能流程

第二次提交后,顺利通过审核。

监控与反馈:上架只是开始

应用上架后,真正的挑战才开始。用户反馈和线上监控发现了我们测试中未发现的问题。

线上问题排查

案例:特定机型闪退

有用户反馈在华为 Mate 40 上频繁闪退,但我们测试机上正常。

排查过程

  1. 查看 AGC 崩溃日志
  2. 发现内存溢出错误
  3. 定位到图片加载组件
  4. 特定机型内存管理更严格

解决方案

// 图片加载优化  
loadImage(url) {  
  return new Promise((resolve, reject) => {  
    const img = new Image()  

    // 限制图片尺寸  
    if (this.isLowMemoryDevice()) {  
      url = this.addImageSizeParams(url, '800x600')  
    }  

    img.onload = () => {  
      // 及时释放引用  
      img.onload = null  
      img.onerror = null  
      resolve(img)  
    }  

    img.onerror = reject  
    img.src = url  
  })  
}

用户反馈处理

建立用户反馈响应机制:

  • 24 小时内响应严重问题
  • 每周汇总用户反馈
  • 每月发布优化版本

经验总结与建议

技术层面

  1. 组件选择要谨慎优先选择官方组件验证第三方组件兼容性准备备用方案
  2. 性能优化要前置开始就考虑性能问题建立性能监控体系定期进行性能回归测试

流程层面

  1. 证书管理要规范区分调试和发布证书设置证书到期提醒备份证书文件
  2. 审核准备要充分仔细阅读审核指南准备完整的说明材料提前测试审核流程
  3. 监控体系要完善建立崩溃监控收集用户反馈定期分析使用数据

给后来者的建议

  1. 不要畏惧鸿蒙开发uni-app 已经提供了很好的支持大部分代码可以复用社区资源越来越丰富
  2. 从小功能开始尝试先移植简单页面逐步增加复杂度积累经验再处理核心功能
  3. 善用官方资源DCloud 文档很详细华为开发者社区活跃官方技术支持响应快
  4. 加入开发者社区学习他人经验分享自己的收获共同推动生态发展

未来规划

鸿蒙版本的成功上线,给了我们很大信心。下一步计划:

  1. 深度集成鸿蒙特性原子化服务探索分布式能力应用鸿蒙特有 UI 交互
  2. 性能持续优化启动速度优化到 1 秒内内存占用降低 30%功耗优化
  3. 多端协同与 iOS/Android 版本功能同步数据互通体验优化统一的设计语言

写在最后

三个月的鸿蒙适配之旅,让我深刻体会到:技术转型虽然痛苦,但收获远超预期。

最大的收获不是技术上架,而是思维转变:从"web 思维"到"多端思维",从"功能实现"到"体验优化"。

鸿蒙生态还在快速发展,现在入局正是好时机。uni-app 为跨端开发提供了很好的基础,让我们能够以较低成本探索新平台。

如果你也在考虑鸿蒙开发,我的建议是:不要观望,直接开始。从简单的 demo 开始,逐步深入,你会发现鸿蒙开发没有想象中那么难。

在这个过程中,你会遇到各种问题,但也会收获解决问题的成就感。更重要的是,你将成为新生态的早期参与者,这本身就是宝贵的机会。

希望这篇文章能帮助到正在或准备进行鸿蒙开发的你。如果遇到问题,欢迎交流讨论,我们一起推动鸿蒙生态的繁荣!

收起阅读 »

【鸿蒙征文】从零到上线:uni-app vue2适配鸿蒙 NEXT +uts实况窗的实战成长纪实

鸿蒙next 鸿蒙征文

作为一个靠 uni-app 做跨端开发的 “老玩家”,之前已经用它上线了五款 Vue2 的 App —— 不过这次多了个新目标:适配鸿蒙 NEXT 系统。目前成功打包上架鸿蒙两款APP,中间踩了不少坑,也攒了些实打实的经验,今天就把整个过程捋一捋,希望能帮到同样在做鸿蒙适配的朋友。​

一、技术选型:为什么选 uni-app 做鸿蒙开发?​

其实最开始也纠结过要不要用鸿蒙原生开发,但手里的项目是现成的 Vue2 代码,还要兼顾微信小程序 ——uni-app 的 “一次开发多端跑” 刚好戳中需求。后来查了文档才知道,uni-app 对鸿蒙 NEXT 的支持已经比较成熟了,尤其是 UTS 插件能直接调用鸿蒙原生能力,不用从头写原生代码,省了不少事。这也是我最终敲定用 uni-app 做适配的核心原因:能复用老项目代码,还能兼顾跨端,效率比重新开发高太多。​

二、鸿蒙迁移实战

1.手里的 App 都是 Vue2 写的,但鸿蒙只支持 Vue3 的 uni-app 项目,所以第一步就得把 Vue2 迁移到 Vue3。​需要适配vue3包括项目代码和相关组件。
首先是生命周期,Vue2 里的beforeDestroy得改成beforeUnmount,destroyed改成unmounted,一开始没注意,控制台报了一堆错;然后是 v-model,Vue3 里父子组件传值得写成:modelValue="xxx"和@update:modelValue="xxx=$event",之前的写法直接用不了;还有路由,Vue2 里是new VueRouter(),Vue3 得用createRouter和createWebHistory,这些基础语法得逐个核对。​
组件方面,之前用的 Element UI 在 Vue3 里不兼容,只能换成 Element Plus。这里要注意,Element Plus 的引入方式和 Element UI 不一样,得在 main.js 里用createApp(App).use(ElementPlus),还得单独引入样式文件,不然组件样式会乱。我当时漏引了样式,页面加载出来全是裸奔的 HTML 元素,排查了半天才发现问题。

​2.引入harmony-configs,注意里面的client_id的取值。

3.华为登录:必须做的 “硬性要求”​
鸿蒙上架有个规定:只要 App 接入了第三方登录(比如微信、QQ 登录),就必须集成华为登录。不过好在 uni-app 已经封装了华为登录的 API,不用自己写原生代码,直接调用uni.login({provider: 'huawei'})就行。​
但这里有个细节:调用登录前得确保harmony-configs已经初始化,不然会报 “client_id 未配置” 的错。我当时是在 main.js 里先初始化harmony-configs,再调用登录接口,代码大概是这样:​

import harmonyConfigs from '@dcloudio/harmony-configs'​  
// 初始化,client_id从AGC拿​  
harmonyConfigs.init({​  
  clientId: '这里填你的client_id',​  
  scope: 'openid profile email'​  
})​  
​  
// 后续调用华为登录​  
uni.login({​  
  provider: 'huawei',​  
  success: (res) => {​  
    // 拿到code后去后端换token​  
    console.log('华为登录code:', res.code)​  
  },​  
  fail: (err) => {​  
    uni.showToast({ title: '登录失败:' + err.errMsg, icon: 'none' })​  
  }​  
})​

4.发布到鸿蒙开发平台,开发服务中要添加发布证书/调试证书 SHA256证书/公钥指纹。
迁移完代码,就得搭鸿蒙开发环境了。首先得装 DevEco Studio,我下的是 5.0 版本。然后是证书 —— 这是我踩的第二个 “坑”。​
鸿蒙开发需要调试证书和发布证书,还得配置 SHA256 指纹。步骤其实不复杂:先在 DevEco Studio 里生成.p12 格式的密钥库,再用这个密钥库生成.csr 文件,然后去华为 AGC 平台(鸿蒙开发平台)上传.csr,申请发布证书(.cer 格式)和 Profile 文件(.p7b 格式)。最后要在 AGC 的 “应用信息” 里填 SHA256 指纹 —— 这里一定要注意,调试证书和发布证书的指纹要分开填,我一开始把调试指纹当成发布指纹填了,导致后来打包发布版登录失败,折腾了好久才发现。​
另外,引入harmony-configs的时候,里面的client_id得从 AGC 平台拿 —— 在 “我的应用” 里找到对应的 App,进入 “配置” 页面,OAuth 2.0 客户端 ID 就是client_id,填错了后续华为登录会调不通。

5.鸿蒙隐私政策完善:请在相应场景补充HarmonyOS NEXT操作系统的介绍/选项/设备标识符等信息。可参考《审核指南》第1.14项:https://developer.huawei.com/consumer/cn/doc/app/50104-01

三、鸿蒙原生能力:uts实现实况窗接入

鸿蒙 NEXT 的实况窗是个特色功能,我做的App 是计时工具,刚好能用实况窗显示剩余时间,提升用户体验。这里就用到了 uni-app 的 UTS 插件,分享下具体怎么集成的。

UTS 插件结构

uni_modules/harmony-liveview/  
├── package.json          # 插件配置  
├── utssdk/  
│   ├── interface.uts    # TypeScript 接口定义  
│   └── app-harmony/  
│       └── index.uts    # 鸿蒙平台实现  
├── readme.md            # 使用文档  
├── changelog.md         # 更新日志  
└── 使用说明.md          # 本文件

核心 API

  • isLiveViewEnabled() - 检查实况窗是否可用
  • startLiveView() - 启动实况窗
  • updateLiveView() - 更新实况窗显示
  • pauseLiveView() - 暂停/继续计时
  • stopLiveView() - 停止实况窗

完整流程:

1.UTS 插件创建:自己搭了个简单的插件​
我在uni_modules下建了个harmony-liveview文件夹,专门放实况窗相关的代码。结构很简单:utssdk文件夹里放接口定义(interface.uts)和鸿蒙原生实现(app-harmony/index.uts),还有个package.json配置插件信息。​
interface.uts主要定义了几个核心方法的接口,比如检查实况窗是否可用、启动、停止、更新,这样 Vue 组件里调用的时候有类型提示;​
index.uts是真正对接鸿蒙原生 LiveView API 的地方,比如startLiveView方法里,会先检查设备是否支持实况窗,再调用鸿蒙的LiveViewManager.start()启动,还加了定时更新数据的逻辑,默认 10 秒更一次。​

2.uniapp vue组件集成:和业务逻辑绑在一起​
我的计时页面是clock.vue,在里面导入了 UTS 插件的方法,然后把实况窗的启动 / 停止和计时功能绑在一起:用户点 “开始” 按钮,调用startTimer的时候,自动触发startHarmonyLiveView启动实况窗;点 “停止” 的时候,调用stopTimer,同时触发stopHarmonyLiveView停止实况窗。​这里要注意,启动实况窗前得先调用isLiveViewEnabled()检查设备是否支持,不然在不支持的设备上会报错。我还加了个提示,不支持的话就弹个 Toast 告诉用户,体验会好一些。​

3.本地调试,先申请权限也需要添加设备token,参考官方文档,添加后才能进行测试,只有调试没有问题才可以提交单独的实况窗开通审核。

4.实况窗上线:审核比想象中严格​
实况窗要上线,得先在 AGC 申请权限,还得提交设计方案 —— 包括功能场景、界面布局图、资源占用情况。我当时提交的方案里,因为没写清楚 “为什么需要实时更新”,被审核打回来了,后来补充了 “计时工具需要用户在桌面就能看到剩余时间,不用打开 App” 的说明,才通过。​
另外,本地测试的时候,要在 AGC 里添加测试设备的 Token,不然实况窗调不起来。我一开始没加,以为是代码有问题,查了半天文档才知道要配置设备 Token,这点大家要注意。​

UTS代码片段:

参考资料:

  • 鸿蒙 LiveView Kit 官方文档
  • uni-app UTS 插件开发文档
  • https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-Live-View-Flight
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/liveview-create-locally

四、上架前的 “避坑指南”:我踩过的 3 个关键问题​

1.发布版白屏:云调试救了我​
本地调试的时候一切正常,但打包发布版提审,鸿蒙审核那边反馈说白屏。我自己下载发布包安装,也遇到了同样的问题,不知道哪里出了错。后来想起 AGC 有个 “云调试” 功能,能选不同型号的鸿蒙设备测试,还能看日志。我用云调试跑了一遍,发现是 SHA256证书/公钥指纹的问题。

2.登录失败:指纹配置错了​
调试版登录没问题,但发布版一直报 “Fingerprint verification error”。查了 AGC 的日志,发现是发布证书的 SHA256 指纹没配置 —— 我之前只填了调试指纹,发布指纹忘了填。补填之后,还得在manifest.json里把versionCode改大一点(比如从 100 改成 101),再让测试的手机把旧版本删掉,不然缓存会导致配置不生效。​

3.鸿蒙备案:提前一周做准备​
鸿蒙上架前必须完成 鸿蒙App 备案。备案提交后,审核界面的校验不是立马生效的,我当时等了差不多 1 天半才显示备案通过。​

五、总结:几个实用的小经验

1.上线前可以用 AGC 的云调试测一遍,很多本地测不出来的问题(比如白屏、登录失败),云调试能快速帮你找到原因;​
2.Vue2 迁 Vue3 的时候,别着急删老代码,先建个分支慢慢改,遇到问题还能回滚;​
3.实况窗的设计方案要写详细,尤其是 “为什么需要这个功能”,不然审核容易被打回;​
4.证书和指纹一定要区分调试版和发布版,别搞混了。

总的来说,用 uni-app 适配鸿蒙 NEXT 的流程不算复杂,只要把 Vue3 迁移、证书配置、华为登录这几个关键点搞定,后续上架就顺理成章了。中间虽然踩了不少坑,但解决问题的过程也是技术成长的过程。希望这篇记录能帮到大家,也欢迎大家在评论区分享自己的适配经验,一起交流进步!

继续阅读 »

作为一个靠 uni-app 做跨端开发的 “老玩家”,之前已经用它上线了五款 Vue2 的 App —— 不过这次多了个新目标:适配鸿蒙 NEXT 系统。目前成功打包上架鸿蒙两款APP,中间踩了不少坑,也攒了些实打实的经验,今天就把整个过程捋一捋,希望能帮到同样在做鸿蒙适配的朋友。​

一、技术选型:为什么选 uni-app 做鸿蒙开发?​

其实最开始也纠结过要不要用鸿蒙原生开发,但手里的项目是现成的 Vue2 代码,还要兼顾微信小程序 ——uni-app 的 “一次开发多端跑” 刚好戳中需求。后来查了文档才知道,uni-app 对鸿蒙 NEXT 的支持已经比较成熟了,尤其是 UTS 插件能直接调用鸿蒙原生能力,不用从头写原生代码,省了不少事。这也是我最终敲定用 uni-app 做适配的核心原因:能复用老项目代码,还能兼顾跨端,效率比重新开发高太多。​

二、鸿蒙迁移实战

1.手里的 App 都是 Vue2 写的,但鸿蒙只支持 Vue3 的 uni-app 项目,所以第一步就得把 Vue2 迁移到 Vue3。​需要适配vue3包括项目代码和相关组件。
首先是生命周期,Vue2 里的beforeDestroy得改成beforeUnmount,destroyed改成unmounted,一开始没注意,控制台报了一堆错;然后是 v-model,Vue3 里父子组件传值得写成:modelValue="xxx"和@update:modelValue="xxx=$event",之前的写法直接用不了;还有路由,Vue2 里是new VueRouter(),Vue3 得用createRouter和createWebHistory,这些基础语法得逐个核对。​
组件方面,之前用的 Element UI 在 Vue3 里不兼容,只能换成 Element Plus。这里要注意,Element Plus 的引入方式和 Element UI 不一样,得在 main.js 里用createApp(App).use(ElementPlus),还得单独引入样式文件,不然组件样式会乱。我当时漏引了样式,页面加载出来全是裸奔的 HTML 元素,排查了半天才发现问题。

​2.引入harmony-configs,注意里面的client_id的取值。

3.华为登录:必须做的 “硬性要求”​
鸿蒙上架有个规定:只要 App 接入了第三方登录(比如微信、QQ 登录),就必须集成华为登录。不过好在 uni-app 已经封装了华为登录的 API,不用自己写原生代码,直接调用uni.login({provider: 'huawei'})就行。​
但这里有个细节:调用登录前得确保harmony-configs已经初始化,不然会报 “client_id 未配置” 的错。我当时是在 main.js 里先初始化harmony-configs,再调用登录接口,代码大概是这样:​

import harmonyConfigs from '@dcloudio/harmony-configs'​  
// 初始化,client_id从AGC拿​  
harmonyConfigs.init({​  
  clientId: '这里填你的client_id',​  
  scope: 'openid profile email'​  
})​  
​  
// 后续调用华为登录​  
uni.login({​  
  provider: 'huawei',​  
  success: (res) => {​  
    // 拿到code后去后端换token​  
    console.log('华为登录code:', res.code)​  
  },​  
  fail: (err) => {​  
    uni.showToast({ title: '登录失败:' + err.errMsg, icon: 'none' })​  
  }​  
})​

4.发布到鸿蒙开发平台,开发服务中要添加发布证书/调试证书 SHA256证书/公钥指纹。
迁移完代码,就得搭鸿蒙开发环境了。首先得装 DevEco Studio,我下的是 5.0 版本。然后是证书 —— 这是我踩的第二个 “坑”。​
鸿蒙开发需要调试证书和发布证书,还得配置 SHA256 指纹。步骤其实不复杂:先在 DevEco Studio 里生成.p12 格式的密钥库,再用这个密钥库生成.csr 文件,然后去华为 AGC 平台(鸿蒙开发平台)上传.csr,申请发布证书(.cer 格式)和 Profile 文件(.p7b 格式)。最后要在 AGC 的 “应用信息” 里填 SHA256 指纹 —— 这里一定要注意,调试证书和发布证书的指纹要分开填,我一开始把调试指纹当成发布指纹填了,导致后来打包发布版登录失败,折腾了好久才发现。​
另外,引入harmony-configs的时候,里面的client_id得从 AGC 平台拿 —— 在 “我的应用” 里找到对应的 App,进入 “配置” 页面,OAuth 2.0 客户端 ID 就是client_id,填错了后续华为登录会调不通。

5.鸿蒙隐私政策完善:请在相应场景补充HarmonyOS NEXT操作系统的介绍/选项/设备标识符等信息。可参考《审核指南》第1.14项:https://developer.huawei.com/consumer/cn/doc/app/50104-01

三、鸿蒙原生能力:uts实现实况窗接入

鸿蒙 NEXT 的实况窗是个特色功能,我做的App 是计时工具,刚好能用实况窗显示剩余时间,提升用户体验。这里就用到了 uni-app 的 UTS 插件,分享下具体怎么集成的。

UTS 插件结构

uni_modules/harmony-liveview/  
├── package.json          # 插件配置  
├── utssdk/  
│   ├── interface.uts    # TypeScript 接口定义  
│   └── app-harmony/  
│       └── index.uts    # 鸿蒙平台实现  
├── readme.md            # 使用文档  
├── changelog.md         # 更新日志  
└── 使用说明.md          # 本文件

核心 API

  • isLiveViewEnabled() - 检查实况窗是否可用
  • startLiveView() - 启动实况窗
  • updateLiveView() - 更新实况窗显示
  • pauseLiveView() - 暂停/继续计时
  • stopLiveView() - 停止实况窗

完整流程:

1.UTS 插件创建:自己搭了个简单的插件​
我在uni_modules下建了个harmony-liveview文件夹,专门放实况窗相关的代码。结构很简单:utssdk文件夹里放接口定义(interface.uts)和鸿蒙原生实现(app-harmony/index.uts),还有个package.json配置插件信息。​
interface.uts主要定义了几个核心方法的接口,比如检查实况窗是否可用、启动、停止、更新,这样 Vue 组件里调用的时候有类型提示;​
index.uts是真正对接鸿蒙原生 LiveView API 的地方,比如startLiveView方法里,会先检查设备是否支持实况窗,再调用鸿蒙的LiveViewManager.start()启动,还加了定时更新数据的逻辑,默认 10 秒更一次。​

2.uniapp vue组件集成:和业务逻辑绑在一起​
我的计时页面是clock.vue,在里面导入了 UTS 插件的方法,然后把实况窗的启动 / 停止和计时功能绑在一起:用户点 “开始” 按钮,调用startTimer的时候,自动触发startHarmonyLiveView启动实况窗;点 “停止” 的时候,调用stopTimer,同时触发stopHarmonyLiveView停止实况窗。​这里要注意,启动实况窗前得先调用isLiveViewEnabled()检查设备是否支持,不然在不支持的设备上会报错。我还加了个提示,不支持的话就弹个 Toast 告诉用户,体验会好一些。​

3.本地调试,先申请权限也需要添加设备token,参考官方文档,添加后才能进行测试,只有调试没有问题才可以提交单独的实况窗开通审核。

4.实况窗上线:审核比想象中严格​
实况窗要上线,得先在 AGC 申请权限,还得提交设计方案 —— 包括功能场景、界面布局图、资源占用情况。我当时提交的方案里,因为没写清楚 “为什么需要实时更新”,被审核打回来了,后来补充了 “计时工具需要用户在桌面就能看到剩余时间,不用打开 App” 的说明,才通过。​
另外,本地测试的时候,要在 AGC 里添加测试设备的 Token,不然实况窗调不起来。我一开始没加,以为是代码有问题,查了半天文档才知道要配置设备 Token,这点大家要注意。​

UTS代码片段:

参考资料:

  • 鸿蒙 LiveView Kit 官方文档
  • uni-app UTS 插件开发文档
  • https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-Live-View-Flight
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/liveview-create-locally

四、上架前的 “避坑指南”:我踩过的 3 个关键问题​

1.发布版白屏:云调试救了我​
本地调试的时候一切正常,但打包发布版提审,鸿蒙审核那边反馈说白屏。我自己下载发布包安装,也遇到了同样的问题,不知道哪里出了错。后来想起 AGC 有个 “云调试” 功能,能选不同型号的鸿蒙设备测试,还能看日志。我用云调试跑了一遍,发现是 SHA256证书/公钥指纹的问题。

2.登录失败:指纹配置错了​
调试版登录没问题,但发布版一直报 “Fingerprint verification error”。查了 AGC 的日志,发现是发布证书的 SHA256 指纹没配置 —— 我之前只填了调试指纹,发布指纹忘了填。补填之后,还得在manifest.json里把versionCode改大一点(比如从 100 改成 101),再让测试的手机把旧版本删掉,不然缓存会导致配置不生效。​

3.鸿蒙备案:提前一周做准备​
鸿蒙上架前必须完成 鸿蒙App 备案。备案提交后,审核界面的校验不是立马生效的,我当时等了差不多 1 天半才显示备案通过。​

五、总结:几个实用的小经验

1.上线前可以用 AGC 的云调试测一遍,很多本地测不出来的问题(比如白屏、登录失败),云调试能快速帮你找到原因;​
2.Vue2 迁 Vue3 的时候,别着急删老代码,先建个分支慢慢改,遇到问题还能回滚;​
3.实况窗的设计方案要写详细,尤其是 “为什么需要这个功能”,不然审核容易被打回;​
4.证书和指纹一定要区分调试版和发布版,别搞混了。

总的来说,用 uni-app 适配鸿蒙 NEXT 的流程不算复杂,只要把 Vue3 迁移、证书配置、华为登录这几个关键点搞定,后续上架就顺理成章了。中间虽然踩了不少坑,但解决问题的过程也是技术成长的过程。希望这篇记录能帮到大家,也欢迎大家在评论区分享自己的适配经验,一起交流进步!

收起阅读 »