【鸿蒙征文】已解决隐私弹窗问题,华为应用商店已经 4 次驳回我的应用上线
一. 前言
不得不说,华为应用商店的审核还是过于严格了,最近提交的新版本应用又被拒绝了!已经提交了4个版本了,再这样下去,我就要崩溃了!
好多人已经对华为的这项审核怨言颇深了!
其实,华为审核严格是一方面,另一方面主要在于 uni-app,由于该应用是使用 uni-app 开发并打包的,所以受限于 uni-app 框架,而它给添加了太多没有用的东西,都集成在框架中,并且删除不掉。
而像本次华为应用商店驳回审核,需要修改的地方已经给罗列的很清楚了,只需要按照他们的说明整改即可,最起码我们能通过自己修改代码就可以完成,不用找官方解决,比较简单。
所以有两个问题需要整改:
- 隐私政策声明
- 申请权限时需告知用户使用目的
隐私政策文件的整改很简单,以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明即可。本篇文章我们直接进行申请权限时的整改,接下来进入正文!
二. 修改权限申请逻辑
前面提到华为应用商店的审核还是过于严格了,其实相比较其他国内应用市场(小米/VIVO/OPPO)来说,区别就在于用户在申请敏感权限时,需同步告知用户申请该权限的目的。对于申请的权限,都必须有明确、合理的使用场景和功能说明,禁止诱导或误导用户授权。
如下图所示:
目前应用内申请权限时是这样的:
接下来我们要进行整改,整改完成后是这样的:
三. 权限申请
1. 使用 plus.android.requestPermissions
统一通过 plus.android.requestPermissions 向系统请求权限,如果权限属于危险权限并且用户没有授权则会弹出系统提示框由用户授权确认。
plus.android.requestPermissions(permissions, successCallback, errorCallback)
2. 参数说明
-
permissions: 申请的权限列表,权限列表参考 Android 官方列表
-
successCallback: 申请权限成功回调函数,参考 AndroidSuccessCallback,返回申请权限的结果,可能被用户允许,回调函数的参数 event 包含以下属性:
- granted - Array[String]字符串数组,已获取权限列表;
- deniedPresent - Array[String]字符串数据,已拒绝(临时)的权限列表;
- deniedAlways - Array[String]字符串数据,永久拒绝的权限列表。
-
errorCallback: 申请权限失败回调函数,参考 AndroidErrorCallback
- 通常传入参数错误时触发此回调。
注意:Android 系统 6+版本(API 等级 23+),并且必须设置 targetSdkVersion>=23。
如果已经授权或被用户拒绝则返回结果。 授权结果在 successCallback 回调参数中可获取。
为了便于统一申请权限,封装为以下方法,可直接复制使用!
// Android权限查询
export function requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
// 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
[permissionID],
function (resultObj) {
var result = 0
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i]
console.log('已获取的权限:' + grantedPermission)
result = 1
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i]
console.log('拒绝本次申请的权限:' + deniedPresentPermission)
result = 0
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i]
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission)
result = -1
}
uni.setStorageSync('permisionStatus_' + permissionID, result)
resolve(result)
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
if (result != 1) {
// gotoAppPermissionSetting()
uni.showModal({
content: '权限已经被拒绝,请前往APP设置界面打开相应权限'
})
}
},
function (error) {
console.log('申请权限错误:' + error.code + ' = ' + error.message)
resolve({
code: error.code,
message: error.message
})
}
)
})
}
此文件来源于 https://ext.dcloud.net.cn/plugin?id=594 部分片段
使用方式:
requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE').then(
result => {
// result 表示:1已获取,0已拒绝,-1永久拒绝。可根据返回码定向处理
}
)
四. 原生弹窗
接下来我们应该构造一个弹窗类 NativePopup 用于在应用内申请权限时弹窗告知用户,说明申请权限的使用目的。
在这里,App 端使用 plus.nativeObj.view 绘制原生内容,参考:uni-app 中使用 5+界面控件、plus.nativeObj.view 规范
代码如下,可直接复制使用!
export class NativePopup {
constructor(options = {}) {
this.sysInfo = uni.getSystemInfoSync()
const {
bgColor = '#fff',
titleColor = '#000',
contentColor = '#272727'
} = options
this.bgColor = bgColor
this.titleColor = titleColor
this.contentColor = contentColor
}
createPopup = () => {
const { statusBarHeight, screenWidth } = this.sysInfo
const popupView = new plus.nativeObj.View('popupView', {
top: 0,
left: 0,
width: screenWidth,
height: 110 + statusBarHeight + 'px'
// backgroundColor: 'blue' // debug
})
popupView.addEventListener('click', this.close)
const bgPadding = 15
popupView.drawRect(
{
color: 'rgba(0, 0, 0, 0.1)',
radius: '10px'
},
{
top: statusBarHeight + 7 + 'px',
left: bgPadding - 2 + 'px',
width: screenWidth - bgPadding * 2 + 4 + 'px',
height: '100px'
}
)
popupView.drawRect(
{
color: this.bgColor,
radius: '10px'
},
{
top: statusBarHeight + 5 + 'px',
left: bgPadding + 'px',
width: screenWidth - bgPadding * 2 + 'px',
height: '100px'
}
)
const padding = 10
popupView.drawText(
this.title,
{
top: statusBarHeight + 10 + 'px',
left: padding + bgPadding + 'px',
height: '30px',
width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
},
{
size: '16px',
weight: 'bold',
align: 'left',
color: this.titleColor
},
{
onClick: function (e) {
console.log(e)
}
}
)
popupView.drawText(
this.content,
{
top: statusBarHeight + 40 + 'px',
height: '60px',
left: padding + bgPadding + 'px',
width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
},
{
size: '14px',
align: 'left',
color: this.contentColor,
whiteSpace: 'normal'
}
)
this.popupView = popupView
return popupView
}
show = (options = {}) => {
this.close()
const { title = '权限申请说明', content = '' } = options
this.title = title
this.content = content
this.createPopup()
this.popupView.show()
}
close = () => {
this.popupView && this.popupView.close()
}
}
export const popup = new NativePopup()
使用方式如下:
import { popup } from './nativePopup.js'
// 显示
popup.show({
title: '权限申请说明',
content: '为了xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
})
// 关闭
popup.close()
五. 监听权限申请
createRequestPermissionListener
华为应用商店审核时要求:APP在调用终端权限时,应同步告知用户申请该权限的目的,可使用 uni.createRequestPermissionListener(),在 app.vue 里全局监听。
在 Android 平台,可使用该 API 监听应用权限申请确认框的弹出和关闭。不管是哪处的业务代码在申请权限,当弹出和关闭权限申请确认框时均会触发本监听事件。
创建监听对象后,返回 RequestPermissionListener,然后调起 onConfirm 和 onComplete。
- 当权限申请的确认框在手机端弹出时,会触发
onConfirm,回调中会以数组方式提供权限名称列表。 - 当权限申请的确认框被用户关闭后,会触发
onComplete
所以,通过监听权限申请,在 onConfirm 回调中弹窗,可以实现不改动业务代码,全局处理权限弹窗问题!
以下代码已经声明了大部分默认权限申请说明信息,如有新增或调整,可以进行更改或传入!
import { popup } from './nativePopup.js'
import permisionUtil from './permission.js'
let permissionListener = null
const prefix = 'permisionStatus_'
const { uniPlatform, platform, osAndroidAPILevel } = uni.getSystemInfoSync()
const log = (...args) => {
console.log(...args)
}
// 默认权限申请说明信息,可以按照以下形式进行拓展
const defaultPermissionExplainMap = {
'android.permission.BLUETOOTH_SCAN': {
title: '蓝牙扫描权限申请说明',
content: '应用需要扫描附近的蓝牙设备,以便进行连接或数据传输。'
},
'android.permission.BLUETOOTH_CONNECT': {
title: '蓝牙连接权限申请说明',
content: '应用需要连接蓝牙设备,以便提供音频播放或数据通信功能。'
},
'android.permission.READ_MEDIA_IMAGE': {
title: '读取图片权限申请说明',
content: '应用需要访问您的图片库,以便加载和选择照片。'
}
}
export const createRequestPermissionListener = (permissionExplainMap = {}) => {
if (uniPlatform != 'app' || platform != 'android') return
if (typeof permissionExplainMap != 'object')
throw Error('permissionExplainMap 类型错误')
permissionListener =
permissionListener || uni.createRequestPermissionListener()
permissionListener.onRequest(e => {
log('onRequest', JSON.stringify(e))
})
permissionListener.onConfirm(e => {
const [permissionName] = e
const status = uni.getStorageSync(prefix + permissionName)
log('onConfirm permissionName', permissionName, status)
const content =
permissionExplainMap[permissionName] ||
defaultPermissionExplainMap[permissionName]
if (!status && content) popup.show(content)
})
permissionListener.onComplete(e => {
const [permissionName] = e
const status = uni.getStorageSync(prefix + permissionName)
log('onComplete permissionName', permissionName, status)
popup.close()
})
}
export const stopRequestPermissionListener = () => {
permissionListener && permissionListener.stop()
}
export { permisionUtil }
六. 用法说明
1. 引入全局监听
在 App.vue 的生命周期中开始监听,停止监听。
import { createRequestPermissionListener } from '@/uni_modules/permission/index.js'
export default {
onLaunch() {
createRequestPermissionListener()
},
onExit() {
stopRequestPermissionListener()
}
}
2. 申请权限
在应用使用权限之前进行检测权限申请,例如,在进行扫码前先申请相机权限:
注意:可以不进行主动申请权限,因为在全局已经做了监听,弹窗会自动弹出!但为了特殊情况(比如在使用原生的权限申请操作,无法监听到),建议在这种情况下提前申请权限!
import { requestAndroidPermission } from '@/uni_modules/permission/index.js'
async requestPermission() {
const status = await requestAndroidPermission('android.permission.CAMERA')
if (status != 1) {
// 权限被拒绝
return
}
}
七. 注意事项
- 如果权限已经申请并且允许之后,
onConfirm不会触发。 - 如果同时申请多个权限时,
onComplete可能会触发多次。 - 只能监听通过 uniapp 或 plus 提供的权限申请时弹出提示,如果你使用原生的权限申请操作,无法监听到!
八. 总结
本文主要介绍了如何解决华为应用市场审核的问题,主要涉及两个方面:
-
隐私政策声明
- 需要在隐私政策中明确声明应用使用的 SDK 信息
- 包括 SDK 名称、包名、使用目的、使用的权限、涉及的个人信息等
- 以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明
-
权限申请优化
- 在申请敏感权限时,需要同步告知用户申请该权限的目的
- 提供了完整的权限申请解决方案:
- 封装了 Android 权限申请方法
- 实现了原生弹窗组件用于权限说明
- 通过全局监听权限申请,自动显示权限说明
- 提供了常用权限的默认说明文案
- 特别针对华为应用市场做了渠道包判断
-
技术实现要点
- 使用
plus.android.requestPermissions进行权限申请 - 使用
plus.nativeObj.view实现原生弹窗 - 使用
uni.createRequestPermissionListener监听权限申请 - 通过
plus.runtime.channel判断应用渠道
- 使用
通过以上优化,可以有效解决华为应用市场的审核问题,提升应用的用户体验和合规性。同时,这些优化措施也可以作为其他应用市场的参考,提高应用的整体质量。
参考文档
Android 平台权限申请 requestPermissions
本文对应的源码已发布到插件市场,可直接使用(下载插件并导入HBuilderX):【DCloud插件市场】监听权限申请,解决华为应用商店上架问题
一. 前言
不得不说,华为应用商店的审核还是过于严格了,最近提交的新版本应用又被拒绝了!已经提交了4个版本了,再这样下去,我就要崩溃了!
好多人已经对华为的这项审核怨言颇深了!
其实,华为审核严格是一方面,另一方面主要在于 uni-app,由于该应用是使用 uni-app 开发并打包的,所以受限于 uni-app 框架,而它给添加了太多没有用的东西,都集成在框架中,并且删除不掉。
而像本次华为应用商店驳回审核,需要修改的地方已经给罗列的很清楚了,只需要按照他们的说明整改即可,最起码我们能通过自己修改代码就可以完成,不用找官方解决,比较简单。
所以有两个问题需要整改:
- 隐私政策声明
- 申请权限时需告知用户使用目的
隐私政策文件的整改很简单,以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明即可。本篇文章我们直接进行申请权限时的整改,接下来进入正文!
二. 修改权限申请逻辑
前面提到华为应用商店的审核还是过于严格了,其实相比较其他国内应用市场(小米/VIVO/OPPO)来说,区别就在于用户在申请敏感权限时,需同步告知用户申请该权限的目的。对于申请的权限,都必须有明确、合理的使用场景和功能说明,禁止诱导或误导用户授权。
如下图所示:
目前应用内申请权限时是这样的:
接下来我们要进行整改,整改完成后是这样的:
三. 权限申请
1. 使用 plus.android.requestPermissions
统一通过 plus.android.requestPermissions 向系统请求权限,如果权限属于危险权限并且用户没有授权则会弹出系统提示框由用户授权确认。
plus.android.requestPermissions(permissions, successCallback, errorCallback)
2. 参数说明
-
permissions: 申请的权限列表,权限列表参考 Android 官方列表
-
successCallback: 申请权限成功回调函数,参考 AndroidSuccessCallback,返回申请权限的结果,可能被用户允许,回调函数的参数 event 包含以下属性:
- granted - Array[String]字符串数组,已获取权限列表;
- deniedPresent - Array[String]字符串数据,已拒绝(临时)的权限列表;
- deniedAlways - Array[String]字符串数据,永久拒绝的权限列表。
-
errorCallback: 申请权限失败回调函数,参考 AndroidErrorCallback
- 通常传入参数错误时触发此回调。
注意:Android 系统 6+版本(API 等级 23+),并且必须设置 targetSdkVersion>=23。
如果已经授权或被用户拒绝则返回结果。 授权结果在 successCallback 回调参数中可获取。
为了便于统一申请权限,封装为以下方法,可直接复制使用!
// Android权限查询
export function requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
// 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
[permissionID],
function (resultObj) {
var result = 0
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i]
console.log('已获取的权限:' + grantedPermission)
result = 1
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i]
console.log('拒绝本次申请的权限:' + deniedPresentPermission)
result = 0
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i]
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission)
result = -1
}
uni.setStorageSync('permisionStatus_' + permissionID, result)
resolve(result)
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
if (result != 1) {
// gotoAppPermissionSetting()
uni.showModal({
content: '权限已经被拒绝,请前往APP设置界面打开相应权限'
})
}
},
function (error) {
console.log('申请权限错误:' + error.code + ' = ' + error.message)
resolve({
code: error.code,
message: error.message
})
}
)
})
}
此文件来源于 https://ext.dcloud.net.cn/plugin?id=594 部分片段
使用方式:
requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE').then(
result => {
// result 表示:1已获取,0已拒绝,-1永久拒绝。可根据返回码定向处理
}
)
四. 原生弹窗
接下来我们应该构造一个弹窗类 NativePopup 用于在应用内申请权限时弹窗告知用户,说明申请权限的使用目的。
在这里,App 端使用 plus.nativeObj.view 绘制原生内容,参考:uni-app 中使用 5+界面控件、plus.nativeObj.view 规范
代码如下,可直接复制使用!
export class NativePopup {
constructor(options = {}) {
this.sysInfo = uni.getSystemInfoSync()
const {
bgColor = '#fff',
titleColor = '#000',
contentColor = '#272727'
} = options
this.bgColor = bgColor
this.titleColor = titleColor
this.contentColor = contentColor
}
createPopup = () => {
const { statusBarHeight, screenWidth } = this.sysInfo
const popupView = new plus.nativeObj.View('popupView', {
top: 0,
left: 0,
width: screenWidth,
height: 110 + statusBarHeight + 'px'
// backgroundColor: 'blue' // debug
})
popupView.addEventListener('click', this.close)
const bgPadding = 15
popupView.drawRect(
{
color: 'rgba(0, 0, 0, 0.1)',
radius: '10px'
},
{
top: statusBarHeight + 7 + 'px',
left: bgPadding - 2 + 'px',
width: screenWidth - bgPadding * 2 + 4 + 'px',
height: '100px'
}
)
popupView.drawRect(
{
color: this.bgColor,
radius: '10px'
},
{
top: statusBarHeight + 5 + 'px',
left: bgPadding + 'px',
width: screenWidth - bgPadding * 2 + 'px',
height: '100px'
}
)
const padding = 10
popupView.drawText(
this.title,
{
top: statusBarHeight + 10 + 'px',
left: padding + bgPadding + 'px',
height: '30px',
width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
},
{
size: '16px',
weight: 'bold',
align: 'left',
color: this.titleColor
},
{
onClick: function (e) {
console.log(e)
}
}
)
popupView.drawText(
this.content,
{
top: statusBarHeight + 40 + 'px',
height: '60px',
left: padding + bgPadding + 'px',
width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
},
{
size: '14px',
align: 'left',
color: this.contentColor,
whiteSpace: 'normal'
}
)
this.popupView = popupView
return popupView
}
show = (options = {}) => {
this.close()
const { title = '权限申请说明', content = '' } = options
this.title = title
this.content = content
this.createPopup()
this.popupView.show()
}
close = () => {
this.popupView && this.popupView.close()
}
}
export const popup = new NativePopup()
使用方式如下:
import { popup } from './nativePopup.js'
// 显示
popup.show({
title: '权限申请说明',
content: '为了xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
})
// 关闭
popup.close()
五. 监听权限申请
createRequestPermissionListener
华为应用商店审核时要求:APP在调用终端权限时,应同步告知用户申请该权限的目的,可使用 uni.createRequestPermissionListener(),在 app.vue 里全局监听。
在 Android 平台,可使用该 API 监听应用权限申请确认框的弹出和关闭。不管是哪处的业务代码在申请权限,当弹出和关闭权限申请确认框时均会触发本监听事件。
创建监听对象后,返回 RequestPermissionListener,然后调起 onConfirm 和 onComplete。
- 当权限申请的确认框在手机端弹出时,会触发
onConfirm,回调中会以数组方式提供权限名称列表。 - 当权限申请的确认框被用户关闭后,会触发
onComplete
所以,通过监听权限申请,在 onConfirm 回调中弹窗,可以实现不改动业务代码,全局处理权限弹窗问题!
以下代码已经声明了大部分默认权限申请说明信息,如有新增或调整,可以进行更改或传入!
import { popup } from './nativePopup.js'
import permisionUtil from './permission.js'
let permissionListener = null
const prefix = 'permisionStatus_'
const { uniPlatform, platform, osAndroidAPILevel } = uni.getSystemInfoSync()
const log = (...args) => {
console.log(...args)
}
// 默认权限申请说明信息,可以按照以下形式进行拓展
const defaultPermissionExplainMap = {
'android.permission.BLUETOOTH_SCAN': {
title: '蓝牙扫描权限申请说明',
content: '应用需要扫描附近的蓝牙设备,以便进行连接或数据传输。'
},
'android.permission.BLUETOOTH_CONNECT': {
title: '蓝牙连接权限申请说明',
content: '应用需要连接蓝牙设备,以便提供音频播放或数据通信功能。'
},
'android.permission.READ_MEDIA_IMAGE': {
title: '读取图片权限申请说明',
content: '应用需要访问您的图片库,以便加载和选择照片。'
}
}
export const createRequestPermissionListener = (permissionExplainMap = {}) => {
if (uniPlatform != 'app' || platform != 'android') return
if (typeof permissionExplainMap != 'object')
throw Error('permissionExplainMap 类型错误')
permissionListener =
permissionListener || uni.createRequestPermissionListener()
permissionListener.onRequest(e => {
log('onRequest', JSON.stringify(e))
})
permissionListener.onConfirm(e => {
const [permissionName] = e
const status = uni.getStorageSync(prefix + permissionName)
log('onConfirm permissionName', permissionName, status)
const content =
permissionExplainMap[permissionName] ||
defaultPermissionExplainMap[permissionName]
if (!status && content) popup.show(content)
})
permissionListener.onComplete(e => {
const [permissionName] = e
const status = uni.getStorageSync(prefix + permissionName)
log('onComplete permissionName', permissionName, status)
popup.close()
})
}
export const stopRequestPermissionListener = () => {
permissionListener && permissionListener.stop()
}
export { permisionUtil }
六. 用法说明
1. 引入全局监听
在 App.vue 的生命周期中开始监听,停止监听。
import { createRequestPermissionListener } from '@/uni_modules/permission/index.js'
export default {
onLaunch() {
createRequestPermissionListener()
},
onExit() {
stopRequestPermissionListener()
}
}
2. 申请权限
在应用使用权限之前进行检测权限申请,例如,在进行扫码前先申请相机权限:
注意:可以不进行主动申请权限,因为在全局已经做了监听,弹窗会自动弹出!但为了特殊情况(比如在使用原生的权限申请操作,无法监听到),建议在这种情况下提前申请权限!
import { requestAndroidPermission } from '@/uni_modules/permission/index.js'
async requestPermission() {
const status = await requestAndroidPermission('android.permission.CAMERA')
if (status != 1) {
// 权限被拒绝
return
}
}
七. 注意事项
- 如果权限已经申请并且允许之后,
onConfirm不会触发。 - 如果同时申请多个权限时,
onComplete可能会触发多次。 - 只能监听通过 uniapp 或 plus 提供的权限申请时弹出提示,如果你使用原生的权限申请操作,无法监听到!
八. 总结
本文主要介绍了如何解决华为应用市场审核的问题,主要涉及两个方面:
-
隐私政策声明
- 需要在隐私政策中明确声明应用使用的 SDK 信息
- 包括 SDK 名称、包名、使用目的、使用的权限、涉及的个人信息等
- 以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明
-
权限申请优化
- 在申请敏感权限时,需要同步告知用户申请该权限的目的
- 提供了完整的权限申请解决方案:
- 封装了 Android 权限申请方法
- 实现了原生弹窗组件用于权限说明
- 通过全局监听权限申请,自动显示权限说明
- 提供了常用权限的默认说明文案
- 特别针对华为应用市场做了渠道包判断
-
技术实现要点
- 使用
plus.android.requestPermissions进行权限申请 - 使用
plus.nativeObj.view实现原生弹窗 - 使用
uni.createRequestPermissionListener监听权限申请 - 通过
plus.runtime.channel判断应用渠道
- 使用
通过以上优化,可以有效解决华为应用市场的审核问题,提升应用的用户体验和合规性。同时,这些优化措施也可以作为其他应用市场的参考,提高应用的整体质量。
参考文档
Android 平台权限申请 requestPermissions
本文对应的源码已发布到插件市场,可直接使用(下载插件并导入HBuilderX):【DCloud插件市场】监听权限申请,解决华为应用商店上架问题
uniapp+vue3 setup跨三端酒店预订小程序模板【h5+小程序+app端】
uniapp-vue3-hotel:一款全新自研的uni-app+vue3 setup+pinia2+uv-ui搭建跨端仿携程/同程旅游app酒店预约系统模板。提供了首页、酒店预订搜索、列表/详情、订单、聊天客服消息、我的等页面模块。支持编译到H5+小程序+APP端。
使用技术
- 开发工具:HbuilderX 4.84
- 技术框架:uni-app+vite5+vue3
- 状态管理:pinia2
- UI组件库:uni-ui+uv-ui(uniapp+vue3组件库)
- 弹框组件:uv3-popup(基于uniapp+vue3多端弹窗组件)
- 自定义组件:uv3-navbar导航条+uv3-tabbar菜单栏
- 缓存技术:pinia-plugin-unistorage
- 编译支持:web+小程序+app端
项目框架结构
使用uniapp+vite5搭建项目,vue3 setup语法开发。
目前uni-vue3-hotel酒店预订项目已经发布到我的原创作品小铺。
uniapp+vue3+pinia2+uvui跨多端酒店预订app系统
想要了解更多项目的详细介绍,可以去看看下面这篇文章。
最新版uni-app+vue3+uv-ui跨端仿携程酒店预订模板【H5+小程序+App端】
热文推荐
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
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仿微信桌面端聊天系统
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
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模板
uniapp-vue3-hotel:一款全新自研的uni-app+vue3 setup+pinia2+uv-ui搭建跨端仿携程/同程旅游app酒店预约系统模板。提供了首页、酒店预订搜索、列表/详情、订单、聊天客服消息、我的等页面模块。支持编译到H5+小程序+APP端。
使用技术
- 开发工具:HbuilderX 4.84
- 技术框架:uni-app+vite5+vue3
- 状态管理:pinia2
- UI组件库:uni-ui+uv-ui(uniapp+vue3组件库)
- 弹框组件:uv3-popup(基于uniapp+vue3多端弹窗组件)
- 自定义组件:uv3-navbar导航条+uv3-tabbar菜单栏
- 缓存技术:pinia-plugin-unistorage
- 编译支持:web+小程序+app端
项目框架结构
使用uniapp+vite5搭建项目,vue3 setup语法开发。
目前uni-vue3-hotel酒店预订项目已经发布到我的原创作品小铺。
uniapp+vue3+pinia2+uvui跨多端酒店预订app系统
想要了解更多项目的详细介绍,可以去看看下面这篇文章。
最新版uni-app+vue3+uv-ui跨端仿携程酒店预订模板【H5+小程序+App端】
热文推荐
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
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仿微信桌面端聊天系统
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
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模板
【弧形导航栏】中间凸起按钮和消息未读角标,支持鸿蒙
Tabbar 组件使用说明
概述
自定义底部导航栏组件,支持中间凸起按钮和消息未读角标功能。
插件地址:https://ext.dcloud.net.cn/plugin?id=25774
功能特性
- ✅ 5 个 tab 页面切换(健康、消息、守护、家庭、我的)
- ✅ 中间守护 tab 凸起设计
- ✅ 支持消息未读角标(红色圆点+白色数字)
- ✅ 支持本地切换模式和路由切换模式
- ✅ 平滑切换动画
Props
useLocalSwitch
- 类型:
Boolean - 默认值:
false - 说明: 是否使用本地切换模式。如果为
true,切换 tab 时会发出tab-change事件而不是进行路由跳转
badges
- 类型:
Object - 默认值:
{} - 说明: 未读消息角标配置,key 为页面路径,value 为未读数
- 示例:
{
'/pages/message/message': 5,
'/pages/health/health': 2,
'/pages/family/family': 10,
'/pages/my/my': 99
}
Events
tab-change
- 参数:
(pagePath: string)- 切换到的页面路径 - 触发时机: 当
useLocalSwitch为true时,点击 tab 触发 - 说明: 用于父组件监听 tab 切换事件
基本使用
1. 不带角标(默认模式)
<template>
<view>
<Tabbar />
</view>
</template>
<script setup>
import Tabbar from "@/components/tabbar/tabbar.vue";
</script>
2. 带消息未读角标
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { ref } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 5, // 消息页显示 5
"/pages/health/health": 2, // 健康页显示 2
"/pages/family/family": 120, // 家庭页显示 99+ (超过99)
});
</script>
3. 本地切换模式(用于自定义页面切换)
<template>
<view>
<Tabbar
:use-local-switch="true"
:badges="badgeData"
@tab-change="handleTabChange"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 3,
});
const handleTabChange = (pagePath) => {
console.log("切换到页面:", pagePath);
// 在这里处理自定义的页面切换逻辑
};
</script>
4. 动态更新角标数量
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 0,
});
// 模拟接收新消息
onMounted(() => {
// 3秒后更新消息数
setTimeout(() => {
badgeData.value["/pages/message/message"] = 5;
}, 3000);
});
</script>
5. 结合 Pinia 状态管理
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { computed } from "vue";
import { useMessageStore } from "@/stores/message";
import Tabbar from "@/components/tabbar/tabbar.vue";
const messageStore = useMessageStore();
// 从store中获取未读消息数
const badgeData = computed(() => ({
"/pages/message/message": messageStore.unreadCount,
"/pages/health/health": messageStore.healthNoticeCount,
"/pages/family/family": messageStore.familyNoticeCount,
}));
</script>
角标显示规则
-
数字范围:
0: 不显示角标1-99: 显示实际数字>99: 显示 "99+"
-
样式规范:
- 背景色:
#FF3B30(红色) - 文字色:
#FFFFFF(白色) - 位置: 图标右上角
- 形状: 圆形(固定尺寸)
- 尺寸: 16px x 16px(固定宽高)
- 背景色:
-
显示位置:
- 左侧 tab(健康、消息): 支持角标 ✅
- 中间 tab(守护): 不支持角标 ❌
- 右侧 tab(家庭、我的): 支持角标 ✅
注意事项
- 中间凸起的守护 tab 不支持角标显示
- 角标数据是响应式的,可以实时更新
- 角标仅在数字大于 0 时显示
- 建议使用 Pinia 进行全局的未读消息管理
- 在 HarmonyOS 系统中测试确保角标显示正常
插件地址:https://ext.dcloud.net.cn/plugin?id=25774
欢迎交流讨论~
Tabbar 组件使用说明
概述
自定义底部导航栏组件,支持中间凸起按钮和消息未读角标功能。
插件地址:https://ext.dcloud.net.cn/plugin?id=25774
功能特性
- ✅ 5 个 tab 页面切换(健康、消息、守护、家庭、我的)
- ✅ 中间守护 tab 凸起设计
- ✅ 支持消息未读角标(红色圆点+白色数字)
- ✅ 支持本地切换模式和路由切换模式
- ✅ 平滑切换动画
Props
useLocalSwitch
- 类型:
Boolean - 默认值:
false - 说明: 是否使用本地切换模式。如果为
true,切换 tab 时会发出tab-change事件而不是进行路由跳转
badges
- 类型:
Object - 默认值:
{} - 说明: 未读消息角标配置,key 为页面路径,value 为未读数
- 示例:
{
'/pages/message/message': 5,
'/pages/health/health': 2,
'/pages/family/family': 10,
'/pages/my/my': 99
}
Events
tab-change
- 参数:
(pagePath: string)- 切换到的页面路径 - 触发时机: 当
useLocalSwitch为true时,点击 tab 触发 - 说明: 用于父组件监听 tab 切换事件
基本使用
1. 不带角标(默认模式)
<template>
<view>
<Tabbar />
</view>
</template>
<script setup>
import Tabbar from "@/components/tabbar/tabbar.vue";
</script>
2. 带消息未读角标
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { ref } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 5, // 消息页显示 5
"/pages/health/health": 2, // 健康页显示 2
"/pages/family/family": 120, // 家庭页显示 99+ (超过99)
});
</script>
3. 本地切换模式(用于自定义页面切换)
<template>
<view>
<Tabbar
:use-local-switch="true"
:badges="badgeData"
@tab-change="handleTabChange"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 3,
});
const handleTabChange = (pagePath) => {
console.log("切换到页面:", pagePath);
// 在这里处理自定义的页面切换逻辑
};
</script>
4. 动态更新角标数量
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import Tabbar from "@/components/tabbar/tabbar.vue";
const badgeData = ref({
"/pages/message/message": 0,
});
// 模拟接收新消息
onMounted(() => {
// 3秒后更新消息数
setTimeout(() => {
badgeData.value["/pages/message/message"] = 5;
}, 3000);
});
</script>
5. 结合 Pinia 状态管理
<template>
<view>
<Tabbar :badges="badgeData" />
</view>
</template>
<script setup>
import { computed } from "vue";
import { useMessageStore } from "@/stores/message";
import Tabbar from "@/components/tabbar/tabbar.vue";
const messageStore = useMessageStore();
// 从store中获取未读消息数
const badgeData = computed(() => ({
"/pages/message/message": messageStore.unreadCount,
"/pages/health/health": messageStore.healthNoticeCount,
"/pages/family/family": messageStore.familyNoticeCount,
}));
</script>
角标显示规则
-
数字范围:
0: 不显示角标1-99: 显示实际数字>99: 显示 "99+"
-
样式规范:
- 背景色:
#FF3B30(红色) - 文字色:
#FFFFFF(白色) - 位置: 图标右上角
- 形状: 圆形(固定尺寸)
- 尺寸: 16px x 16px(固定宽高)
- 背景色:
-
显示位置:
- 左侧 tab(健康、消息): 支持角标 ✅
- 中间 tab(守护): 不支持角标 ❌
- 右侧 tab(家庭、我的): 支持角标 ✅
注意事项
- 中间凸起的守护 tab 不支持角标显示
- 角标数据是响应式的,可以实时更新
- 角标仅在数字大于 0 时显示
- 建议使用 Pinia 进行全局的未读消息管理
- 在 HarmonyOS 系统中测试确保角标显示正常
插件地址:https://ext.dcloud.net.cn/plugin?id=25774
欢迎交流讨论~
收起阅读 »腾讯云上传图片
之前使用vue 写过后台管理系统的腾讯云直传,使用的是cos-js-sdk-v5库,使用了三方库后就非常的简单代码也不多。
这次要在uniapp项目上写腾讯云直传,于是又把以前的代码拿来用了起来,在浏览器上调试好后,真机运行到手机上发现出问题了!
在uniapp中使用"cos-js-sdk-v5"库时,提示
warning: cos-js-sdk-v5 不支持 nodejs 环境使用,请改用 cos-nodejs-sdk-v5
按照提示安装 "cos-nodejs-sdk-v5",运行APP后直接报错APP无法正常启动,如下图所示:
卡住了不知道该怎么办?
然后就找到了腾讯云文档中心的这篇文章 uni-app 直传实践
思路是:使用uni.chooseImage选择图片文件,然后把文件类型传递给后端,后端返回腾讯云的相关信息,前端使用uni.uploadFile上传文件
之前使用vue 写过后台管理系统的腾讯云直传,使用的是cos-js-sdk-v5库,使用了三方库后就非常的简单代码也不多。
这次要在uniapp项目上写腾讯云直传,于是又把以前的代码拿来用了起来,在浏览器上调试好后,真机运行到手机上发现出问题了!
在uniapp中使用"cos-js-sdk-v5"库时,提示
warning: cos-js-sdk-v5 不支持 nodejs 环境使用,请改用 cos-nodejs-sdk-v5
按照提示安装 "cos-nodejs-sdk-v5",运行APP后直接报错APP无法正常启动,如下图所示:
卡住了不知道该怎么办?
然后就找到了腾讯云文档中心的这篇文章 uni-app 直传实践
思路是:使用uni.chooseImage选择图片文件,然后把文件类型传递给后端,后端返回腾讯云的相关信息,前端使用uni.uploadFile上传文件
低成本入局鸿蒙生态!Uniapp 适配鸿蒙实战分享,一次编码跑通多端
随着鸿蒙 OS(HarmonyOS)在手机、平板、智能穿戴等设备的全面普及,其分布式架构和全场景互联能力已成为开发者不可忽视的新赛道。而 Uniapp 作为 “一次开发,多端部署” 的标杆框架,早已实现对鸿蒙的成熟适配,让开发者无需从零学习鸿蒙原生开发(ArkTS/ArkUI),就能将现有 Uniapp 项目快速迁移至鸿蒙生态。本文结合实际项目适配经验,从环境搭建、核心适配、问题排查到优化升级,全程拆解 Uniapp 适配鸿蒙的关键步骤,助力开发者高效落地。
一、为什么选择 Uniapp 适配鸿蒙?
在决定适配前,先明确 Uniapp 适配鸿蒙的核心优势,避免重复造轮子:
技术栈复用:无需学习鸿蒙原生技术,Vue / 小程序开发者可直接上手,核心业务逻辑零修改或少量修改;
多端兼容性:适配鸿蒙后,项目仍可正常运行在 iOS、Android、微信小程序等平台,代码资产最大化利用;
官方深度支持:HBuilderX 持续迭代鸿蒙适配能力,内置编译、调试工具,降低适配门槛;
生态协同:Uniapp 可调用鸿蒙分布式能力(如设备互联、数据共享),让跨端应用具备鸿蒙特色优势。
简单说:Uniapp 是低成本切入鸿蒙生态的最优解之一,尤其适合已有 Uniapp 项目的团队快速拓展鸿蒙渠道。
二、前置准备:环境搭建与基础配置
适配前需完成环境搭建和项目基础配置,这是后续适配的前提,步骤如下:
1. 开发环境搭建
HBuilderX:安装 3.8.0 及以上版本(需支持鸿蒙编译),直接在官网下载即可;
DevEco Studio:安装 4.0 及以上版本(鸿蒙开发者工具),用于模拟器调试和应用打包,需注册鸿蒙开发者账号并完成实名认证;
鸿蒙模拟器配置:在 DevEco Studio 中创建模拟器(推荐 API Version 9+,手机 / 平板型号均可),确保模拟器与 HBuilderX 处于同一网络(如同一 Wi-Fi),避免调试连接失败。
2. 项目基础配置
新建 / 改造项目:若从零开发,选择 Uniapp “默认模板”(优先 Vue 3+Vite 架构,鸿蒙端对 Vue 3 兼容性更佳);若改造现有项目,确保项目无严重语法错误,且依赖库为最新版本;
manifest.json 配置:
打开项目根目录的manifest.json,在 “App 模块配置” 中勾选 “HarmonyOS”;
填写鸿蒙应用基础信息:应用名称、包名(需与鸿蒙开发者平台注册的包名一致)、版本号、图标等;
权限配置:在 “HarmonyOS 权限配置” 中声明所需权限(如网络、存储、相机等),鸿蒙对权限管控较严格,未声明的权限会直接导致功能失效。
pages.json 配置:添加鸿蒙端专属配置,指定使用 ArkUI 渲染(默认开启),示例:
json
{
"globalStyle": {
"harmonyos": {
"useArkUI": true, // 启用ArkUI渲染(必填)
"windowBackgroundColor": "#ffffff" // 鸿蒙端窗口背景色
}
}
}
3. 依赖兼容性检查
插件兼容:移除依赖 Android/iOS 原生 SDK 的 Uniapp 插件(如某些支付、地图插件),替换为跨端兼容插件(如uni-ui、uView Plus、uni-pay等);
第三方库兼容:优先使用纯 JS/TS 库(如 axios、lodash),避免使用依赖原生模块的库(如 node-sqlite3);若必须使用,需确认库已支持鸿蒙环境。
三、核心适配:组件、API 与布局的差异化处理
Uniapp 的组件和 API 在鸿蒙端大多兼容,但因鸿蒙系统特性,部分场景需针对性适配,核心集中在以下 3 个维度:
1. 组件适配:替换不兼容组件,对齐行为差异
Uniapp 内置组件在鸿蒙端的兼容性可达 90% 以上,但部分组件存在行为差异,需重点关注:
优先使用 Uniapp 跨端组件:如<view>、<text>、<image>、<button>等,避免直接使用鸿蒙原生 ArkUI 组件(如<Text>、<Image>),否则会破坏多端兼容性;
表单组件适配:
<input>组件:type="number"在鸿蒙端需补充input-mode="numeric",确保弹出数字软键盘;placeholder-style需用内联样式,避免样式失效;
<picker>组件:必须指定range-key(即使是简单数组),否则数据无法正常渲染,示例:
vue
<picker :range="array" range-key="name" @change="onPickerChange">
<view>选择内容</view>
</picker>
滚动组件适配:<scroll-view>横向滚动需显式设置scroll-x="true",且子组件需设置white-space: nowrap(避免换行),同时确保子组件宽度不超出容器;
不兼容组件替换:
<web-view>:鸿蒙端暂不支持,可改用uni.navigateTo跳转 H5 页面,或通过 Uniapp 插件集成鸿蒙原生WebComponent;
<video>:鸿蒙端不支持controls属性自动显示控制栏,需自定义控制按钮(播放 / 暂停、进度条等)。
2. API 适配:处理权限、网络与环境判断
Uniapp 的uni.xxxAPI 在鸿蒙端基本兼容,但部分与系统相关的 API 需特殊处理:
权限动态申请:鸿蒙的权限体系与 Android/iOS 不同,需先在manifest.json声明权限,再通过uni.requestPermissions动态申请,示例(申请存储权限):
// 鸿蒙存储权限标识:ohos.permission.WRITE_USER_STORAGE
uni.requestPermissions({
scope: 'ohos.permission.WRITE_USER_STORAGE',
success: (res) => {
if (res.granted) {
// 权限申请成功,执行文件读写操作
uni.saveFile({...});
} else {
uni.showToast({ title: '请开启存储权限以正常使用功能' });
}
}
});
网络请求适配:
鸿蒙端默认禁止http协议请求,需在manifest.json中添加配置开启:
"harmonyos": {
"network": {
"cleartextTraffic": true // 允许http请求(开发环境可用,生产环境建议改用https)
}
}
uni.request的timeout参数在鸿蒙端最小值为 1000ms,设置过小会导致请求失败;
环境判断与差异化逻辑:通过uni.getSystemInfo判断当前是否为鸿蒙环境,执行特殊逻辑:
uni.getSystemInfo({
success: (res) => {
// res.system 格式:"HarmonyOS 4.0.0"
this.isHarmonyOS = res.system.includes('HarmonyOS');
if (this.isHarmonyOS) {
// 鸿蒙端特殊处理(如替换组件、调整样式)
this.adaptHarmonyStyle();
}
}
});
路由与页面生命周期:
uni.navigateBack在鸿蒙端需指定delta参数(如delta: 1),否则可能无法正常返回上一页;
鸿蒙端页面生命周期与小程序一致(onLoad/onShow/onUnload),但onReady触发时机略晚,避免在onReady中执行依赖 DOM 的操作(可延迟 100ms)。
3. 布局适配:适配鸿蒙多设备尺寸与特性
鸿蒙支持手机、平板、折叠屏等多设备,布局适配需兼顾 “自适应” 与 “设备特性”:
优先使用 rpx 单位:Uniapp 的 rpx 单位在鸿蒙端同样生效(1rpx = 屏幕宽度 / 750),无需额外适配尺寸,确保布局在不同屏幕尺寸的鸿蒙设备上自适应;
避免固定布局:禁止使用px固定宽度 / 高度,优先使用flex布局 +flex-grow/flex-shrink,确保组件随屏幕伸缩;
平板 / 折叠屏适配:通过mediaQuery实现不同屏幕尺寸的布局切换,示例:
json
// pages.json中配置
{
"pages": [
{
"path": "pages/list/list",
"style": {
"mediaQuery": {
"min-width": "800px": { // 平板屏幕(宽度≥800px)
"layout": "grid",
"grid-template-columns": "1fr 1fr", // 双列布局
"grid-gap": "20rpx"
}
}
}
}
]
}
样式兼容性:
鸿蒙端不支持scoped样式中的::v-deep,Vue 3 项目需改用::deep,Vue 2 项目改用/deep/;
避免使用position: fixed(鸿蒙端可能出现层级异常),优先使用sticky或absolute+ 父容器定位;
<text>组件的line-height默认不继承,需显式设置(如line-height: 32rpx)。
四、调试与打包:避坑指南
适配过程中,调试和打包是容易踩坑的环节,分享关键注意事项:
1. 模拟器调试技巧
连接失败解决:
确认 HBuilderX 与 DevEco Studio 模拟器处于同一网络;
重启鸿蒙模拟器(在 DevEco Studio 中关闭后重新启动);
检查manifest.json的包名与鸿蒙开发者平台注册的包名一致;
日志查看:在 HBuilderX 的 “运行日志” 中查看鸿蒙端报错信息,若日志不完整,可在 DevEco Studio 中打开 “Logcat” 查看详细原生日志。
2. 打包发布注意事项
证书配置:需在鸿蒙开发者平台申请 “应用发布证书” 和 “Profile 文件”,并在 HBuilderX 的manifest.json中配置(“HarmonyOS 打包配置”);
版本号规范:鸿蒙应用的版本号(versionName)需遵循 “主版本。次版本。修订号” 格式(如 1.0.0),且需高于已发布版本;
安装失败排查:
检查证书是否过期或与包名不匹配;
确认设备系统版本≥API Version 9;
检查权限配置是否完整(缺少必要权限会导致安装失败)。
五、优化升级:让鸿蒙应用体验更原生
适配完成后,可通过以下优化提升应用的鸿蒙原生体验:
1. 接入鸿蒙分布式能力
Uniapp 支持通过插件调用鸿蒙分布式 API,让应用具备跨设备协同能力:
集成 “鸿蒙分布式路由” 插件,实现手机、平板、手表等设备间的页面跳转;
使用 “分布式数据管理” 插件,实现多设备间的数据同步(如购物车、收藏夹)。
2. 适配鸿蒙深色模式
鸿蒙系统支持深色模式,适配后可提升用户体验:
在manifest.json中开启深色模式支持:
json
"harmonyos": {
"darkMode": "auto", // 跟随系统主题
"theme": {
"light": "#ffffff", // 浅色模式背景色
"dark": "#1a1a1a" // 深色模式背景色
}
}
在样式中通过媒体查询适配深色模式:
css
/* 全局样式或页面样式 */
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
color: #ffffff;
}
.btn {
background-color: #333333;
border-color: #666666;
}
}
3. 性能优化
列表优化:长列表使用v-for+key,避免频繁修改 DOM;开启列表懒加载(uni-scroll-view的lower-threshold),减少一次性渲染数据量;
资源优化:压缩图片(使用 webp 格式)、懒加载非首屏图片;减少首屏网络请求,优先使用本地缓存数据;
动画优化:避免使用复杂 CSS 动画,优先使用uni.createAnimation;动画时长控制在 300ms 以内,提升流畅度。
六、总结
Uniapp 适配鸿蒙的核心逻辑是 “复用现有技术栈,补齐系统差异点”,整个过程无需从零开发,适配成本低、效率高。对于已有 Uniapp 项目的团队,仅需完成 “环境搭建→配置调整→组件 / API 差异化处理→调试打包” 四个步骤,即可快速切入鸿蒙生态;对于新项目,使用 Uniapp 开发可一次性覆盖 iOS、Android、小程序、鸿蒙等多端,最大化代码价值。
随着鸿蒙生态的持续壮大,Uniapp 对鸿蒙的适配能力也在不断升级(如支持更多鸿蒙原生 API、优化性能体验)。现在正是布局鸿蒙生态的黄金时期,借助 Uniapp 的跨端优势,开发者可快速抢占鸿蒙设备用户市场,为应用的多端发展增添新的增长点。
随着鸿蒙 OS(HarmonyOS)在手机、平板、智能穿戴等设备的全面普及,其分布式架构和全场景互联能力已成为开发者不可忽视的新赛道。而 Uniapp 作为 “一次开发,多端部署” 的标杆框架,早已实现对鸿蒙的成熟适配,让开发者无需从零学习鸿蒙原生开发(ArkTS/ArkUI),就能将现有 Uniapp 项目快速迁移至鸿蒙生态。本文结合实际项目适配经验,从环境搭建、核心适配、问题排查到优化升级,全程拆解 Uniapp 适配鸿蒙的关键步骤,助力开发者高效落地。
一、为什么选择 Uniapp 适配鸿蒙?
在决定适配前,先明确 Uniapp 适配鸿蒙的核心优势,避免重复造轮子:
技术栈复用:无需学习鸿蒙原生技术,Vue / 小程序开发者可直接上手,核心业务逻辑零修改或少量修改;
多端兼容性:适配鸿蒙后,项目仍可正常运行在 iOS、Android、微信小程序等平台,代码资产最大化利用;
官方深度支持:HBuilderX 持续迭代鸿蒙适配能力,内置编译、调试工具,降低适配门槛;
生态协同:Uniapp 可调用鸿蒙分布式能力(如设备互联、数据共享),让跨端应用具备鸿蒙特色优势。
简单说:Uniapp 是低成本切入鸿蒙生态的最优解之一,尤其适合已有 Uniapp 项目的团队快速拓展鸿蒙渠道。
二、前置准备:环境搭建与基础配置
适配前需完成环境搭建和项目基础配置,这是后续适配的前提,步骤如下:
1. 开发环境搭建
HBuilderX:安装 3.8.0 及以上版本(需支持鸿蒙编译),直接在官网下载即可;
DevEco Studio:安装 4.0 及以上版本(鸿蒙开发者工具),用于模拟器调试和应用打包,需注册鸿蒙开发者账号并完成实名认证;
鸿蒙模拟器配置:在 DevEco Studio 中创建模拟器(推荐 API Version 9+,手机 / 平板型号均可),确保模拟器与 HBuilderX 处于同一网络(如同一 Wi-Fi),避免调试连接失败。
2. 项目基础配置
新建 / 改造项目:若从零开发,选择 Uniapp “默认模板”(优先 Vue 3+Vite 架构,鸿蒙端对 Vue 3 兼容性更佳);若改造现有项目,确保项目无严重语法错误,且依赖库为最新版本;
manifest.json 配置:
打开项目根目录的manifest.json,在 “App 模块配置” 中勾选 “HarmonyOS”;
填写鸿蒙应用基础信息:应用名称、包名(需与鸿蒙开发者平台注册的包名一致)、版本号、图标等;
权限配置:在 “HarmonyOS 权限配置” 中声明所需权限(如网络、存储、相机等),鸿蒙对权限管控较严格,未声明的权限会直接导致功能失效。
pages.json 配置:添加鸿蒙端专属配置,指定使用 ArkUI 渲染(默认开启),示例:
json
{
"globalStyle": {
"harmonyos": {
"useArkUI": true, // 启用ArkUI渲染(必填)
"windowBackgroundColor": "#ffffff" // 鸿蒙端窗口背景色
}
}
}
3. 依赖兼容性检查
插件兼容:移除依赖 Android/iOS 原生 SDK 的 Uniapp 插件(如某些支付、地图插件),替换为跨端兼容插件(如uni-ui、uView Plus、uni-pay等);
第三方库兼容:优先使用纯 JS/TS 库(如 axios、lodash),避免使用依赖原生模块的库(如 node-sqlite3);若必须使用,需确认库已支持鸿蒙环境。
三、核心适配:组件、API 与布局的差异化处理
Uniapp 的组件和 API 在鸿蒙端大多兼容,但因鸿蒙系统特性,部分场景需针对性适配,核心集中在以下 3 个维度:
1. 组件适配:替换不兼容组件,对齐行为差异
Uniapp 内置组件在鸿蒙端的兼容性可达 90% 以上,但部分组件存在行为差异,需重点关注:
优先使用 Uniapp 跨端组件:如<view>、<text>、<image>、<button>等,避免直接使用鸿蒙原生 ArkUI 组件(如<Text>、<Image>),否则会破坏多端兼容性;
表单组件适配:
<input>组件:type="number"在鸿蒙端需补充input-mode="numeric",确保弹出数字软键盘;placeholder-style需用内联样式,避免样式失效;
<picker>组件:必须指定range-key(即使是简单数组),否则数据无法正常渲染,示例:
vue
<picker :range="array" range-key="name" @change="onPickerChange">
<view>选择内容</view>
</picker>
滚动组件适配:<scroll-view>横向滚动需显式设置scroll-x="true",且子组件需设置white-space: nowrap(避免换行),同时确保子组件宽度不超出容器;
不兼容组件替换:
<web-view>:鸿蒙端暂不支持,可改用uni.navigateTo跳转 H5 页面,或通过 Uniapp 插件集成鸿蒙原生WebComponent;
<video>:鸿蒙端不支持controls属性自动显示控制栏,需自定义控制按钮(播放 / 暂停、进度条等)。
2. API 适配:处理权限、网络与环境判断
Uniapp 的uni.xxxAPI 在鸿蒙端基本兼容,但部分与系统相关的 API 需特殊处理:
权限动态申请:鸿蒙的权限体系与 Android/iOS 不同,需先在manifest.json声明权限,再通过uni.requestPermissions动态申请,示例(申请存储权限):
// 鸿蒙存储权限标识:ohos.permission.WRITE_USER_STORAGE
uni.requestPermissions({
scope: 'ohos.permission.WRITE_USER_STORAGE',
success: (res) => {
if (res.granted) {
// 权限申请成功,执行文件读写操作
uni.saveFile({...});
} else {
uni.showToast({ title: '请开启存储权限以正常使用功能' });
}
}
});
网络请求适配:
鸿蒙端默认禁止http协议请求,需在manifest.json中添加配置开启:
"harmonyos": {
"network": {
"cleartextTraffic": true // 允许http请求(开发环境可用,生产环境建议改用https)
}
}
uni.request的timeout参数在鸿蒙端最小值为 1000ms,设置过小会导致请求失败;
环境判断与差异化逻辑:通过uni.getSystemInfo判断当前是否为鸿蒙环境,执行特殊逻辑:
uni.getSystemInfo({
success: (res) => {
// res.system 格式:"HarmonyOS 4.0.0"
this.isHarmonyOS = res.system.includes('HarmonyOS');
if (this.isHarmonyOS) {
// 鸿蒙端特殊处理(如替换组件、调整样式)
this.adaptHarmonyStyle();
}
}
});
路由与页面生命周期:
uni.navigateBack在鸿蒙端需指定delta参数(如delta: 1),否则可能无法正常返回上一页;
鸿蒙端页面生命周期与小程序一致(onLoad/onShow/onUnload),但onReady触发时机略晚,避免在onReady中执行依赖 DOM 的操作(可延迟 100ms)。
3. 布局适配:适配鸿蒙多设备尺寸与特性
鸿蒙支持手机、平板、折叠屏等多设备,布局适配需兼顾 “自适应” 与 “设备特性”:
优先使用 rpx 单位:Uniapp 的 rpx 单位在鸿蒙端同样生效(1rpx = 屏幕宽度 / 750),无需额外适配尺寸,确保布局在不同屏幕尺寸的鸿蒙设备上自适应;
避免固定布局:禁止使用px固定宽度 / 高度,优先使用flex布局 +flex-grow/flex-shrink,确保组件随屏幕伸缩;
平板 / 折叠屏适配:通过mediaQuery实现不同屏幕尺寸的布局切换,示例:
json
// pages.json中配置
{
"pages": [
{
"path": "pages/list/list",
"style": {
"mediaQuery": {
"min-width": "800px": { // 平板屏幕(宽度≥800px)
"layout": "grid",
"grid-template-columns": "1fr 1fr", // 双列布局
"grid-gap": "20rpx"
}
}
}
}
]
}
样式兼容性:
鸿蒙端不支持scoped样式中的::v-deep,Vue 3 项目需改用::deep,Vue 2 项目改用/deep/;
避免使用position: fixed(鸿蒙端可能出现层级异常),优先使用sticky或absolute+ 父容器定位;
<text>组件的line-height默认不继承,需显式设置(如line-height: 32rpx)。
四、调试与打包:避坑指南
适配过程中,调试和打包是容易踩坑的环节,分享关键注意事项:
1. 模拟器调试技巧
连接失败解决:
确认 HBuilderX 与 DevEco Studio 模拟器处于同一网络;
重启鸿蒙模拟器(在 DevEco Studio 中关闭后重新启动);
检查manifest.json的包名与鸿蒙开发者平台注册的包名一致;
日志查看:在 HBuilderX 的 “运行日志” 中查看鸿蒙端报错信息,若日志不完整,可在 DevEco Studio 中打开 “Logcat” 查看详细原生日志。
2. 打包发布注意事项
证书配置:需在鸿蒙开发者平台申请 “应用发布证书” 和 “Profile 文件”,并在 HBuilderX 的manifest.json中配置(“HarmonyOS 打包配置”);
版本号规范:鸿蒙应用的版本号(versionName)需遵循 “主版本。次版本。修订号” 格式(如 1.0.0),且需高于已发布版本;
安装失败排查:
检查证书是否过期或与包名不匹配;
确认设备系统版本≥API Version 9;
检查权限配置是否完整(缺少必要权限会导致安装失败)。
五、优化升级:让鸿蒙应用体验更原生
适配完成后,可通过以下优化提升应用的鸿蒙原生体验:
1. 接入鸿蒙分布式能力
Uniapp 支持通过插件调用鸿蒙分布式 API,让应用具备跨设备协同能力:
集成 “鸿蒙分布式路由” 插件,实现手机、平板、手表等设备间的页面跳转;
使用 “分布式数据管理” 插件,实现多设备间的数据同步(如购物车、收藏夹)。
2. 适配鸿蒙深色模式
鸿蒙系统支持深色模式,适配后可提升用户体验:
在manifest.json中开启深色模式支持:
json
"harmonyos": {
"darkMode": "auto", // 跟随系统主题
"theme": {
"light": "#ffffff", // 浅色模式背景色
"dark": "#1a1a1a" // 深色模式背景色
}
}
在样式中通过媒体查询适配深色模式:
css
/* 全局样式或页面样式 */
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
color: #ffffff;
}
.btn {
background-color: #333333;
border-color: #666666;
}
}
3. 性能优化
列表优化:长列表使用v-for+key,避免频繁修改 DOM;开启列表懒加载(uni-scroll-view的lower-threshold),减少一次性渲染数据量;
资源优化:压缩图片(使用 webp 格式)、懒加载非首屏图片;减少首屏网络请求,优先使用本地缓存数据;
动画优化:避免使用复杂 CSS 动画,优先使用uni.createAnimation;动画时长控制在 300ms 以内,提升流畅度。
六、总结
Uniapp 适配鸿蒙的核心逻辑是 “复用现有技术栈,补齐系统差异点”,整个过程无需从零开发,适配成本低、效率高。对于已有 Uniapp 项目的团队,仅需完成 “环境搭建→配置调整→组件 / API 差异化处理→调试打包” 四个步骤,即可快速切入鸿蒙生态;对于新项目,使用 Uniapp 开发可一次性覆盖 iOS、Android、小程序、鸿蒙等多端,最大化代码价值。
随着鸿蒙生态的持续壮大,Uniapp 对鸿蒙的适配能力也在不断升级(如支持更多鸿蒙原生 API、优化性能体验)。现在正是布局鸿蒙生态的黄金时期,借助 Uniapp 的跨端优势,开发者可快速抢占鸿蒙设备用户市场,为应用的多端发展增添新的增长点。
【解决】类似vue项目App.vue的template下添加的元素,全部页面都会显示;根组件
原作者的文章:https://ask.dcloud.net.cn/article/39345
原作者的github地址:vue-inset-loader
vue2+webpack版本:ste-vue-inset-loader
vue3+vite版本:vue3-inset-loader
原作者的文章:https://ask.dcloud.net.cn/article/39345
原作者的github地址:vue-inset-loader
vue2+webpack版本:ste-vue-inset-loader
vue3+vite版本:vue3-inset-loader
【鸿蒙征文】uni-app鸿蒙上架必备技能:应用适配深色模式
uni-app鸿蒙上架必备技能:应用适配深色模式
此文将介绍 uni-app 如何适配深色模式,文章内容通俗易懂,非常适合新手小白上手,从此再也不用担心如何适配深色模式了。
- 示例项目仓库地址:gitee仓库地址
为什么要适配深色模式?
- 鸿蒙应用商店上架强制要求
- 提升用户体验
- 符合现代应用设计趋势
开发环境要求
- HBuilderX 4.76+
- 当前必须手动在
harmony-configs/libs目录增加UniAppRuntime.har
注意: UniAppRuntime.har 文件请在示例项目中复制
快速上手(三步搞定)
第一步:启用深色模式支持
在 manifest.json 中添加配置:
{
// 鸿蒙App
"app-harmony": {
"darkmode": true,
"themeLocation": "theme.json",
"safearea": {
"bottom": {
"offset": "none"
}
}
},
// iOS 和 安卓
"app-plus": {
"darkmode": true,
"themeLocation": "theme.json",
"safearea": {
"bottom": {
"offset": "none"
}
},
},
// Web
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
},
// 微信小程序
"mp-weixin": {
"darkmode": true,
"themeLocation": "theme.json",
}
}
说明:darkmode: true 表示启用深色模式,themeLocation 指定主题配置文件位置。
第二步:创建主题配置文件
在项目根目录创建 theme.json,定义浅色和深色两套颜色:
{
"light": {
"navBgColor": "#ffffff",
"navTxtStyle": "black",
"bgColor": "#f5f5f5",
"tabBgColor": "#ffffff",
"tabFontColor": "#666666",
"tabSelectedColor": "#007aff",
"tabBorderStyle": "black"
},
"dark": {
"navBgColor": "#1a1a1a",
"navTxtStyle": "white",
"bgColor": "#000000",
"tabBgColor": "#1a1a1a",
"tabFontColor": "#999999",
"tabSelectedColor": "#0a84ff",
"tabBorderStyle": "white"
}
}
说明:
light:浅色模式下的颜色dark:深色模式下的颜色- 可以自定义任意颜色变量
- 此处的变量仅在
pages.json文件中使用
第三步:在 pages.json 中使用主题变量
使用 @变量名 的方式引用主题颜色:
{
"globalStyle": {
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "深色模式示例",
"navigationBarBackgroundColor": "@navBgColor",
"backgroundColor": "@bgColor"
},
"tabBar": {
"color": "@tabFontColor",
"selectedColor": "@tabSelectedColor",
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"list": [...]
}
}
说明:系统会根据当前模式自动选择对应的颜色值。
页面样式适配
上面操作的3步骤仅适配了顶部导航和底部tabbar,接下来将介绍页面样式如何适配深色模式。
核心: 使用 CSS 变量 + 媒体查询覆盖 CSS 变量的值
第一步:项目根目录创建 theme.scss 文件:
:root,
page {
// 背景色
--page-bg: #f5f5f5;
--card-bg: #ffffff;
// 文字色
--text-primary: #333333;
--text-secondary: #666666;
// 主题色
--primary-color: #007aff;
// 边框色
--border-color: #f0f0f0;
}
// 深色模式
@media (prefers-color-scheme: dark) {
:root,
page {
// 背景色
--page-bg: #000000;
--card-bg: #1a1a1a;
// 文字色
--text-primary: #ffffff;
--text-secondary: #999999;
// 主题色
--primary-color: #0a84ff;
// 边框色
--border-color: #2a2a2a;
}
}
page {
background-color: var(--page-bg);
}
第二步:在 App.vue 中引入:
<style lang="scss">
@import "./theme.scss";
</style>
第三步:在页面中使用 CSS 变量:
<style lang="scss" scoped>
.container {
background-color: var(--page-bg);
}
.card {
background-color: var(--card-bg);
color: var(--text-primary);
}
.text {
color: var(--text-secondary);
}
</style>
常见问题
1. 如何测试深色模式?
- 鸿蒙设备:设置 → 显示与亮度 → 深色模式 → 全天开启
- iOS/Android:在系统设置中切换外观
- 谷歌浏览器:设置 → 外观 → 主题
- 微信小程序:跟随微信App的设置
2. 底部tabbar的图标怎么适配?
建议使用通用的黑白图标,或者在 theme.json 中定义不同的图标路径。
核心要点总结
- 在
manifest.json中启用darkmode: true - 创建
theme.json定义颜色变量 - 在
pages.json中使用@变量名引用 - 使用 CSS 变量 + 媒体查询适配页面样式
- 测试浅色和深色两种模式下的显示效果
uni-app鸿蒙上架必备技能:应用适配深色模式
此文将介绍 uni-app 如何适配深色模式,文章内容通俗易懂,非常适合新手小白上手,从此再也不用担心如何适配深色模式了。
- 示例项目仓库地址:gitee仓库地址
为什么要适配深色模式?
- 鸿蒙应用商店上架强制要求
- 提升用户体验
- 符合现代应用设计趋势
开发环境要求
- HBuilderX 4.76+
- 当前必须手动在
harmony-configs/libs目录增加UniAppRuntime.har
注意: UniAppRuntime.har 文件请在示例项目中复制
快速上手(三步搞定)
第一步:启用深色模式支持
在 manifest.json 中添加配置:
{
// 鸿蒙App
"app-harmony": {
"darkmode": true,
"themeLocation": "theme.json",
"safearea": {
"bottom": {
"offset": "none"
}
}
},
// iOS 和 安卓
"app-plus": {
"darkmode": true,
"themeLocation": "theme.json",
"safearea": {
"bottom": {
"offset": "none"
}
},
},
// Web
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
},
// 微信小程序
"mp-weixin": {
"darkmode": true,
"themeLocation": "theme.json",
}
}
说明:darkmode: true 表示启用深色模式,themeLocation 指定主题配置文件位置。
第二步:创建主题配置文件
在项目根目录创建 theme.json,定义浅色和深色两套颜色:
{
"light": {
"navBgColor": "#ffffff",
"navTxtStyle": "black",
"bgColor": "#f5f5f5",
"tabBgColor": "#ffffff",
"tabFontColor": "#666666",
"tabSelectedColor": "#007aff",
"tabBorderStyle": "black"
},
"dark": {
"navBgColor": "#1a1a1a",
"navTxtStyle": "white",
"bgColor": "#000000",
"tabBgColor": "#1a1a1a",
"tabFontColor": "#999999",
"tabSelectedColor": "#0a84ff",
"tabBorderStyle": "white"
}
}
说明:
light:浅色模式下的颜色dark:深色模式下的颜色- 可以自定义任意颜色变量
- 此处的变量仅在
pages.json文件中使用
第三步:在 pages.json 中使用主题变量
使用 @变量名 的方式引用主题颜色:
{
"globalStyle": {
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "深色模式示例",
"navigationBarBackgroundColor": "@navBgColor",
"backgroundColor": "@bgColor"
},
"tabBar": {
"color": "@tabFontColor",
"selectedColor": "@tabSelectedColor",
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"list": [...]
}
}
说明:系统会根据当前模式自动选择对应的颜色值。
页面样式适配
上面操作的3步骤仅适配了顶部导航和底部tabbar,接下来将介绍页面样式如何适配深色模式。
核心: 使用 CSS 变量 + 媒体查询覆盖 CSS 变量的值
第一步:项目根目录创建 theme.scss 文件:
:root,
page {
// 背景色
--page-bg: #f5f5f5;
--card-bg: #ffffff;
// 文字色
--text-primary: #333333;
--text-secondary: #666666;
// 主题色
--primary-color: #007aff;
// 边框色
--border-color: #f0f0f0;
}
// 深色模式
@media (prefers-color-scheme: dark) {
:root,
page {
// 背景色
--page-bg: #000000;
--card-bg: #1a1a1a;
// 文字色
--text-primary: #ffffff;
--text-secondary: #999999;
// 主题色
--primary-color: #0a84ff;
// 边框色
--border-color: #2a2a2a;
}
}
page {
background-color: var(--page-bg);
}
第二步:在 App.vue 中引入:
<style lang="scss">
@import "./theme.scss";
</style>
第三步:在页面中使用 CSS 变量:
<style lang="scss" scoped>
.container {
background-color: var(--page-bg);
}
.card {
background-color: var(--card-bg);
color: var(--text-primary);
}
.text {
color: var(--text-secondary);
}
</style>
常见问题
1. 如何测试深色模式?
- 鸿蒙设备:设置 → 显示与亮度 → 深色模式 → 全天开启
- iOS/Android:在系统设置中切换外观
- 谷歌浏览器:设置 → 外观 → 主题
- 微信小程序:跟随微信App的设置
2. 底部tabbar的图标怎么适配?
建议使用通用的黑白图标,或者在 theme.json 中定义不同的图标路径。
核心要点总结
- 在
manifest.json中启用darkmode: true - 创建
theme.json定义颜色变量 - 在
pages.json中使用@变量名引用 - 使用 CSS 变量 + 媒体查询适配页面样式
- 测试浅色和深色两种模式下的显示效果
【鸿蒙征文】从现在起,你的非原生弹窗“组件”们(自定义Toast、Modal等)只需要配置一次!
介绍
👋 hello, 您那要是早上,那祝你早安。要是下午,那祝你午安。晚上看?那还是别看了,点个赞,明天再看
我是 skiyee 是本篇的作者,常活跃在 uni-app 生态领域
随着 Harmony 不断增强以及大力推广,更多的应用场景进入我们的眼帘
我就在想,要是 uni-app 也适配了 Harmony,那不就省事了,一键多端共同开发,不再需要花费更多的学习成本
您猜怎么着,uni-app 还真适配 Harmony Next 了,那么我们就可以结合 uni-app 原本的生态来开发了!
痛点
很多朋友在编写弹窗时,觉得原生的鸿蒙弹窗不好看,就想自定义一个美美滴。但发现,uni-app 中居然没有地方能够一键进行设置,全局就可以调用的
朋友们所期望的应该是像体验原生一般,如 uni.showToast({...}) 这种,在任何地方都能调起的
为了解决这个痛点,我结合 uni-app 使用的底层构建工具 vite 开发了一个模拟根组件能力的组件!@uni-ku/root
开始
安装
在我们的 HBuilder 中,选择我们的项目打开命令行窗口,输入命令
npm install -D @uni-ku/root
配置
在 vite.config.js 中引入 @uni-ku/root
// vite.config.js
import Uni from '@dcloudio/vite-plugin-uni'
import UniKuRoot from '@uni-ku/root'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
// 文档: https://github.com/uni-ku/root
UniKuRoot(),
Uni()
]
})
使用
创建关键 App.ku.vue 文件,并通过标签 <KuRootView /> 或 <ku-root-view /> 指定视图存放位置
<script setup>
import { ref } from 'vue'
const UniKuRoot = ref('Hello UniKu Root')
</script>
<template>
<div>{{ UniKuRoot }}</div>
<!-- 视图存放位置 -->
<KuRootView />
</template>
封装
编写自定义的 Toast 组件
<!-- components/GlobalToast.vue -->
<script setup>
import { useToast } from '@/composables/useToast'
const { globalToastState, hideToast } = useToast()
</script>
<template>
<div v-if="globalToastState" class="toast-wrapper" @click="hideToast">
<div class="toast-box">
welcome to use @uni-ku/root
</div>
</div>
</template>
<style scoped>
.toast-wrapper{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.toast-box{
background: white;
color: black;
}
</style>
实现 Toast 调起方法
// composables/useToast.js
import { ref } from 'vue'
const globalToastState = ref(false)
export function useToast() {
function showToast() {
globalToastState.value = true
}
function hideToast() {
globalToastState.value = false
}
return {
globalToastState,
showToast,
hideToast,
}
}
挂载至 App.ku.vue
<!-- App.ku.vue -->
<script setup>
import GlobalToast from '@/components/GlobalToast.vue'
</script>
<template>
<KuRootView />
<GlobalToast />
</template>
视图内部触发 Toast 组件
<!-- pages/index(或者其他页面).vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'
const { showToast } = useToast()
</script>
<template>
<view>
Hello UniKuRoot
</view>
<button @click="showToast">
视图内触发展示Toast
</button>
</template>
具体代码详见:示例
最终效果
打开我们的鸿蒙应用,来展示我们的最终效果
上面这个Toast只是一个示例,最终需要利用 UniKuRoot 去实现什么效果,是开发者自行可以想象的!
结语
结语就说些心里话吧,随着对 uni-app 的越来越多的理解,我觉得目前 uni-app 是跨平台小程序做的最棒的、App跨端方面是生态最好的。
虽然有些功能还不是很棒,但是目前仍在不断的更新迭代、不断的完善,我相信有开发人员和生态建设的爱好者们的参与会越来越棒!
鼓励
如果为此感到兴奋,可以通过以下渠道让我们知道!
生态
以下都是关于 UniApp 生态相关的仓库
介绍
👋 hello, 您那要是早上,那祝你早安。要是下午,那祝你午安。晚上看?那还是别看了,点个赞,明天再看
我是 skiyee 是本篇的作者,常活跃在 uni-app 生态领域
随着 Harmony 不断增强以及大力推广,更多的应用场景进入我们的眼帘
我就在想,要是 uni-app 也适配了 Harmony,那不就省事了,一键多端共同开发,不再需要花费更多的学习成本
您猜怎么着,uni-app 还真适配 Harmony Next 了,那么我们就可以结合 uni-app 原本的生态来开发了!
痛点
很多朋友在编写弹窗时,觉得原生的鸿蒙弹窗不好看,就想自定义一个美美滴。但发现,uni-app 中居然没有地方能够一键进行设置,全局就可以调用的
朋友们所期望的应该是像体验原生一般,如 uni.showToast({...}) 这种,在任何地方都能调起的
为了解决这个痛点,我结合 uni-app 使用的底层构建工具 vite 开发了一个模拟根组件能力的组件!@uni-ku/root
开始
安装
在我们的 HBuilder 中,选择我们的项目打开命令行窗口,输入命令
npm install -D @uni-ku/root
配置
在 vite.config.js 中引入 @uni-ku/root
// vite.config.js
import Uni from '@dcloudio/vite-plugin-uni'
import UniKuRoot from '@uni-ku/root'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
// 文档: https://github.com/uni-ku/root
UniKuRoot(),
Uni()
]
})
使用
创建关键 App.ku.vue 文件,并通过标签 <KuRootView /> 或 <ku-root-view /> 指定视图存放位置
<script setup>
import { ref } from 'vue'
const UniKuRoot = ref('Hello UniKu Root')
</script>
<template>
<div>{{ UniKuRoot }}</div>
<!-- 视图存放位置 -->
<KuRootView />
</template>
封装
编写自定义的 Toast 组件
<!-- components/GlobalToast.vue -->
<script setup>
import { useToast } from '@/composables/useToast'
const { globalToastState, hideToast } = useToast()
</script>
<template>
<div v-if="globalToastState" class="toast-wrapper" @click="hideToast">
<div class="toast-box">
welcome to use @uni-ku/root
</div>
</div>
</template>
<style scoped>
.toast-wrapper{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.toast-box{
background: white;
color: black;
}
</style>
实现 Toast 调起方法
// composables/useToast.js
import { ref } from 'vue'
const globalToastState = ref(false)
export function useToast() {
function showToast() {
globalToastState.value = true
}
function hideToast() {
globalToastState.value = false
}
return {
globalToastState,
showToast,
hideToast,
}
}
挂载至 App.ku.vue
<!-- App.ku.vue -->
<script setup>
import GlobalToast from '@/components/GlobalToast.vue'
</script>
<template>
<KuRootView />
<GlobalToast />
</template>
视图内部触发 Toast 组件
<!-- pages/index(或者其他页面).vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'
const { showToast } = useToast()
</script>
<template>
<view>
Hello UniKuRoot
</view>
<button @click="showToast">
视图内触发展示Toast
</button>
</template>
具体代码详见:示例
最终效果
打开我们的鸿蒙应用,来展示我们的最终效果
上面这个Toast只是一个示例,最终需要利用 UniKuRoot 去实现什么效果,是开发者自行可以想象的!
结语
结语就说些心里话吧,随着对 uni-app 的越来越多的理解,我觉得目前 uni-app 是跨平台小程序做的最棒的、App跨端方面是生态最好的。
虽然有些功能还不是很棒,但是目前仍在不断的更新迭代、不断的完善,我相信有开发人员和生态建设的爱好者们的参与会越来越棒!
鼓励
如果为此感到兴奋,可以通过以下渠道让我们知道!
生态
以下都是关于 UniApp 生态相关的仓库
收起阅读 »uniapp app 端如何可以监听到声音的分贝值?我现在用的是uni.getRecorderManager() 这个方法,不支持监听声音的分贝值。
我现在想实现一个功能,实时的去监听用户讲话,想如果用户的声音的大小就识别客户已经讲完话,从而去实现对应的逻辑。有什么办法可以实现吗?麻烦各位大神指导一下,谢谢!
我现在想实现一个功能,实时的去监听用户讲话,想如果用户的声音的大小就识别客户已经讲完话,从而去实现对应的逻辑。有什么办法可以实现吗?麻烦各位大神指导一下,谢谢!
我用tmx-iu开发了一个拍日出的app
大家好,分享一个用tmx-ui做的鸿蒙App - Aura(日落色彩记录工具)。
📱 App简介
自动提取照片主色生成渐变色卡。说实话老婆一开始还嘲笑我"还不如做个敲木鱼的"😂,但功能还是挺实用的。
核心功能:
- K-means聚类算法提取主色(5色渐变)
- 8种分享模板(Canvas渲染)
- 900+中国古典色库
欢迎下载体验 ➡️ https://appgallery.huawei.com/app/detail?id=auar0.0.1&channelId=SHARE&source=appshare
🎨 关于组件库
说实话,用tmx-ui组件库开发效率真的高。10天时间我做了2个App,Aura是其中一个。
组件库帮我省了大量时间:
x-sheet快速搭建布局结构x-navbar导航栏开箱即用x-button、x-icon这些基础组件样式统一- 类型定义特别完善,开发时智能提示很舒服
- 主题配置系统省去了大量颜色适配工作
大家好,分享一个用tmx-ui做的鸿蒙App - Aura(日落色彩记录工具)。
📱 App简介
自动提取照片主色生成渐变色卡。说实话老婆一开始还嘲笑我"还不如做个敲木鱼的"😂,但功能还是挺实用的。
核心功能:
- K-means聚类算法提取主色(5色渐变)
- 8种分享模板(Canvas渲染)
- 900+中国古典色库
欢迎下载体验 ➡️ https://appgallery.huawei.com/app/detail?id=auar0.0.1&channelId=SHARE&source=appshare
🎨 关于组件库
说实话,用tmx-ui组件库开发效率真的高。10天时间我做了2个App,Aura是其中一个。
组件库帮我省了大量时间:
x-sheet快速搭建布局结构x-navbar导航栏开箱即用x-button、x-icon这些基础组件样式统一- 类型定义特别完善,开发时智能提示很舒服
- 主题配置系统省去了大量颜色适配工作
cli关于pnpm安装vue-i18n的问题
直接pnpm i之后会提示类似这种错误
试了改版本或者删nodemodule都不行,据gemini说是pnpm的解析依赖导致的,然后要了个办法解决了
"dependencies": {
"vue-i18n": "9.14.5",
},
首先看你这里的版本号是多少,然后去仓库直接翻源码找到这个版本的包的package,
看这里依赖的版本是多少,然后在自己的依赖里加上这个强制pnpm去安装这几个版本就解决了
"pnpm": {
"overrides": {
"@vue/devtools-api": "^6.5.0",
"@intlify/core-base": "9.14.5",
"@intlify/shared": "9.14.5",
"@intlify/devtools-if": "9.14.5",
"@intlify/vue-devtools": "9.14.5"
}
},
补充------------,这样解决了h5运行,小程序似乎又会出现新问题
补充------------,原来把i18n降一下版本就没事了
直接pnpm i之后会提示类似这种错误
试了改版本或者删nodemodule都不行,据gemini说是pnpm的解析依赖导致的,然后要了个办法解决了
"dependencies": {
"vue-i18n": "9.14.5",
},
首先看你这里的版本号是多少,然后去仓库直接翻源码找到这个版本的包的package,
看这里依赖的版本是多少,然后在自己的依赖里加上这个强制pnpm去安装这几个版本就解决了
"pnpm": {
"overrides": {
"@vue/devtools-api": "^6.5.0",
"@intlify/core-base": "9.14.5",
"@intlify/shared": "9.14.5",
"@intlify/devtools-if": "9.14.5",
"@intlify/vue-devtools": "9.14.5"
}
},
补充------------,这样解决了h5运行,小程序似乎又会出现新问题
补充------------,原来把i18n降一下版本就没事了 收起阅读 »
【鸿蒙征文】Uni ECharts 2.1 发布:正式支持鸿蒙,零成本迁移、全平台兼容、跨端开发零负担!
Uni ECharts 是适用于 uni-app 的 Apache ECharts 组件,无需繁琐的步骤即可轻松在 uni-app 平台上使用 echarts。
官网 & 文档:https://uni-echarts.xiaohe.ink
插件市场:https://ext.dcloud.net.cn/plugin?id=22035
Github:https://github.com/xiaohe0601/uni-echarts
🏝️ 背景
🎵 “本来应该从从容容游刃有余,现在是匆匆忙忙连滚带爬,睁眼说瞎话,你在哽咽什么啦,你在哭什么哭,没出息!”
每当听见同事阿尹在工位旁哼起这首歌,我都忍不住陷入沉思 —— 那一刻,我看到的不只是他在 emo,更像是无数开发者在鸿蒙适配路上的缩影。
是的,在过去一段时间里,由于 uni-app 不支持鸿蒙模拟器调试,而我又苦于没有鸿蒙手机,导致 Uni ECharts 并不能在鸿蒙系统上顺利运行。有鸿蒙需求的开发者们用起来就像是在赶末班车 “匆匆忙忙、连滚带爬”,我是夜不能寐、如鲠在喉。
如今 uni-app 终于支持鸿蒙模拟器调试,痛定思痛,我再也坐不住了!这一次,一定要让这件事情画上一个完美的句号。
于是,我们决定不再将就,团队成员一拍即合 —— 必须让 Uni ECharts 能够在鸿蒙系统运行,与主流生态全面接轨。更重要的是,无需改动一行代码,真正做到 “一次开发、多端运行”,开发者从此 “从从容容、游刃有余”,不再哽咽,大家都会 “有出息”!
上文中的 “团队成员” 目前指的是我自己 🙃,如果你对维护 Uni ECharts 感兴趣的话欢迎到 Github 提交 PR 👏,一起用爱发电!
在此,对已经或将来为 Uni ECharts 贡献代码的开发者朋友们由衷表示感谢!🙏
项目地址:https://github.com/xiaohe0601/uni-echarts
🎉 2.1 正式发布
现在,Uni ECharts 成功完成了对鸿蒙的适配,所以 2.1 版本正式发布啦!
安装及使用方法与其他端别无二致,那么就一起来回顾一下吧 ~
👉 前往 Uni ECharts 官网 快速开始 查看完整内容
前置条件:
- echarts >= 5.3.0
- vue >= 3.3.0(目前 uni-app 尚未适配 Vue 3.5,推荐使用
3.4.x与 uni-app 保持一致)
安装
# pnpm
pnpm add echarts uni-echarts
# yarn
yarn add echarts uni-echarts
# npm
npm install echarts uni-echarts
配置
由于 Uni ECharts 发布到 npm 上的包是未经编译的 vue 文件,为了避免 Vite 对 Uni ECharts 依赖预构建 导致生成额外的 echarts 副本,当使用 npm 方式时需要手动配置 Vite 强制排除 uni-echarts 的预构建。
// vite.config.js[ts]
import { defineConfig } from "vite";
export default defineConfig({
// ...
optimizeDeps: {
exclude: [
"uni-echarts"
]
}
});
Vite 插件
自 2.0.0 开始,Uni ECharts 提供了 Vite 插件用于自动化处理一些繁琐、重复的工作,也为将来更多的高级功能提供了可能性。
// vite.config.js[ts]
import { UniEcharts } from "uni-echarts/vite";
import { defineConfig } from "vite";
export default defineConfig({
// ...
plugins: [
UniEcharts()
]
});
自动导入(可选)
Uni ECharts 可以配合 @uni-helper/vite-plugin-uni-components 和 unplugin-auto-import 实现组件和 API 的自动按需导入。
# pnpm
pnpm add -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
# yarn
yarn add --dev @uni-helper/vite-plugin-uni-components unplugin-auto-import
# npm
npm install -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
// vite.config.js[ts]
import Uni from "@dcloudio/vite-plugin-uni";
import UniComponents from "@uni-helper/vite-plugin-uni-components";
import { UniEchartsResolver } from "uni-echarts/resolver";
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
AutoImport({
resolvers: [
UniEchartsResolver()
]
}),
// 确保放在 `Uni()` 之前
UniComponents({
resolvers: [
UniEchartsResolver()
]
}),
Uni()
]
});
如果使用 pnpm 管理依赖,请在项目根目录下的 .npmrc 文件中添加如下内容,参见 issue 389。
shamefully-hoist=true # or public-hoist-pattern[]=@vue*
如果使用 TypeScript 可以在 tsconfig.json 中添加如下内容为自动导入的组件提供类型提示(需要 IDE 支持)。
{
"compilerOptions": {
"types": [
// ...
"uni-echarts/global"
]
}
}
使用
<template>
<uni-echarts custom-class="chart" :option="option"></uni-echarts>
</template>
<script setup>
import { PieChart } from "echarts/charts";
import { DatasetComponent, LegendComponent, TooltipComponent } from "echarts/components";
import * as echarts from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { ref } from "vue";
echarts.use([
LegendComponent,
TooltipComponent,
DatasetComponent,
PieChart,
CanvasRenderer
]);
const option = ref({
legend: {
top: 10,
left: "center"
},
tooltip: {
trigger: "item",
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1
// #endif
}
},
series: [
{
type: "pie",
radius: ["30%", "52%"],
label: {
show: false,
position: "center"
},
itemStyle: {
borderWidth: 2,
borderColor: "#ffffff",
borderRadius: 10
},
emphasis: {
label: {
show: true,
fontSize: 20
}
}
}
],
dataset: {
dimensions: ["来源", "数量"],
source: [
["Search Engine", 1048],
["Direct", 735],
["Email", 580],
["Union Ads", 484],
["Video Ads", 300]
]
}
});
</script>
<style scoped>
.chart {
height: 300px;
}
</style>
小程序端图表不显示?
请参考常见问题中 小程序端 class / style 无效 部分的说明。
❤️ 支持 & 鼓励
如果 Uni ECharts 对你有帮助,可以通过以下渠道对我们表示鼓励:
无论 ⭐️ 还是 💰 支持,我们铭记于心,这将是我们继续前进的动力,感谢您的支持!
Uni ECharts 是适用于 uni-app 的 Apache ECharts 组件,无需繁琐的步骤即可轻松在 uni-app 平台上使用 echarts。
官网 & 文档:https://uni-echarts.xiaohe.ink
插件市场:https://ext.dcloud.net.cn/plugin?id=22035
Github:https://github.com/xiaohe0601/uni-echarts
🏝️ 背景
🎵 “本来应该从从容容游刃有余,现在是匆匆忙忙连滚带爬,睁眼说瞎话,你在哽咽什么啦,你在哭什么哭,没出息!”
每当听见同事阿尹在工位旁哼起这首歌,我都忍不住陷入沉思 —— 那一刻,我看到的不只是他在 emo,更像是无数开发者在鸿蒙适配路上的缩影。
是的,在过去一段时间里,由于 uni-app 不支持鸿蒙模拟器调试,而我又苦于没有鸿蒙手机,导致 Uni ECharts 并不能在鸿蒙系统上顺利运行。有鸿蒙需求的开发者们用起来就像是在赶末班车 “匆匆忙忙、连滚带爬”,我是夜不能寐、如鲠在喉。
如今 uni-app 终于支持鸿蒙模拟器调试,痛定思痛,我再也坐不住了!这一次,一定要让这件事情画上一个完美的句号。
于是,我们决定不再将就,团队成员一拍即合 —— 必须让 Uni ECharts 能够在鸿蒙系统运行,与主流生态全面接轨。更重要的是,无需改动一行代码,真正做到 “一次开发、多端运行”,开发者从此 “从从容容、游刃有余”,不再哽咽,大家都会 “有出息”!
上文中的 “团队成员” 目前指的是我自己 🙃,如果你对维护 Uni ECharts 感兴趣的话欢迎到 Github 提交 PR 👏,一起用爱发电!
在此,对已经或将来为 Uni ECharts 贡献代码的开发者朋友们由衷表示感谢!🙏
项目地址:https://github.com/xiaohe0601/uni-echarts
🎉 2.1 正式发布
现在,Uni ECharts 成功完成了对鸿蒙的适配,所以 2.1 版本正式发布啦!
安装及使用方法与其他端别无二致,那么就一起来回顾一下吧 ~
👉 前往 Uni ECharts 官网 快速开始 查看完整内容
前置条件:
- echarts >= 5.3.0
- vue >= 3.3.0(目前 uni-app 尚未适配 Vue 3.5,推荐使用
3.4.x与 uni-app 保持一致)
安装
# pnpm
pnpm add echarts uni-echarts
# yarn
yarn add echarts uni-echarts
# npm
npm install echarts uni-echarts
配置
由于 Uni ECharts 发布到 npm 上的包是未经编译的 vue 文件,为了避免 Vite 对 Uni ECharts 依赖预构建 导致生成额外的 echarts 副本,当使用 npm 方式时需要手动配置 Vite 强制排除 uni-echarts 的预构建。
// vite.config.js[ts]
import { defineConfig } from "vite";
export default defineConfig({
// ...
optimizeDeps: {
exclude: [
"uni-echarts"
]
}
});
Vite 插件
自 2.0.0 开始,Uni ECharts 提供了 Vite 插件用于自动化处理一些繁琐、重复的工作,也为将来更多的高级功能提供了可能性。
// vite.config.js[ts]
import { UniEcharts } from "uni-echarts/vite";
import { defineConfig } from "vite";
export default defineConfig({
// ...
plugins: [
UniEcharts()
]
});
自动导入(可选)
Uni ECharts 可以配合 @uni-helper/vite-plugin-uni-components 和 unplugin-auto-import 实现组件和 API 的自动按需导入。
# pnpm
pnpm add -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
# yarn
yarn add --dev @uni-helper/vite-plugin-uni-components unplugin-auto-import
# npm
npm install -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
// vite.config.js[ts]
import Uni from "@dcloudio/vite-plugin-uni";
import UniComponents from "@uni-helper/vite-plugin-uni-components";
import { UniEchartsResolver } from "uni-echarts/resolver";
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
AutoImport({
resolvers: [
UniEchartsResolver()
]
}),
// 确保放在 `Uni()` 之前
UniComponents({
resolvers: [
UniEchartsResolver()
]
}),
Uni()
]
});
如果使用 pnpm 管理依赖,请在项目根目录下的 .npmrc 文件中添加如下内容,参见 issue 389。
shamefully-hoist=true # or public-hoist-pattern[]=@vue*
如果使用 TypeScript 可以在 tsconfig.json 中添加如下内容为自动导入的组件提供类型提示(需要 IDE 支持)。
{
"compilerOptions": {
"types": [
// ...
"uni-echarts/global"
]
}
}
使用
<template>
<uni-echarts custom-class="chart" :option="option"></uni-echarts>
</template>
<script setup>
import { PieChart } from "echarts/charts";
import { DatasetComponent, LegendComponent, TooltipComponent } from "echarts/components";
import * as echarts from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { ref } from "vue";
echarts.use([
LegendComponent,
TooltipComponent,
DatasetComponent,
PieChart,
CanvasRenderer
]);
const option = ref({
legend: {
top: 10,
left: "center"
},
tooltip: {
trigger: "item",
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1
// #endif
}
},
series: [
{
type: "pie",
radius: ["30%", "52%"],
label: {
show: false,
position: "center"
},
itemStyle: {
borderWidth: 2,
borderColor: "#ffffff",
borderRadius: 10
},
emphasis: {
label: {
show: true,
fontSize: 20
}
}
}
],
dataset: {
dimensions: ["来源", "数量"],
source: [
["Search Engine", 1048],
["Direct", 735],
["Email", 580],
["Union Ads", 484],
["Video Ads", 300]
]
}
});
</script>
<style scoped>
.chart {
height: 300px;
}
</style>
小程序端图表不显示?
请参考常见问题中 小程序端 class / style 无效 部分的说明。
❤️ 支持 & 鼓励
如果 Uni ECharts 对你有帮助,可以通过以下渠道对我们表示鼓励:
无论 ⭐️ 还是 💰 支持,我们铭记于心,这将是我们继续前进的动力,感谢您的支持!
收起阅读 »
































