
unicloud-db的动态多条件and查询
方法比较笨,如果有更好的方法欢迎大家贴一贴
search() {
//表单内容赋值
const params = this.searchFormData;
//定义
let dbParams={};
//此处只想到用if语句去逐个添加条件
if(params.date){
dbParams['date'] =params.date;
}
if(params.classification){
dbParams['classification'] =params.classification;
}
if(params.tags.length!=0){
dbParams['tags'] =params.tags[0];
}
if(params.content){
//模糊查询
dbParams['content'] =new RegExp(params.content,'i');
}
console.log("搜索条件",dbParams)
//搜索
db.collection('schedule')
.where(dbParams)
.get()
.then((res) => {
console.log('res',res)
if (res.success) {
let data = res.result.data;
if (res.result.errCode === 0) {
console.log(data)
}
}
}).catch((err) => {})
}
方法比较笨,如果有更好的方法欢迎大家贴一贴
search() {
//表单内容赋值
const params = this.searchFormData;
//定义
let dbParams={};
//此处只想到用if语句去逐个添加条件
if(params.date){
dbParams['date'] =params.date;
}
if(params.classification){
dbParams['classification'] =params.classification;
}
if(params.tags.length!=0){
dbParams['tags'] =params.tags[0];
}
if(params.content){
//模糊查询
dbParams['content'] =new RegExp(params.content,'i');
}
console.log("搜索条件",dbParams)
//搜索
db.collection('schedule')
.where(dbParams)
.get()
.then((res) => {
console.log('res',res)
if (res.success) {
let data = res.result.data;
if (res.result.errCode === 0) {
console.log(data)
}
}
}).catch((err) => {})
}
收起阅读 »

uniapp蓝牙连接打印机
打印机选用的是佳博的GP2124T和GP1224T这两种型号
这两种型号已经测试通过,只适配了安卓,ios没有测试
打印机选用的是佳博的GP2124T和GP1224T这两种型号
这两种型号已经测试通过,只适配了安卓,ios没有测试

app-plus.titleNView + onNavigationBarButtonTap 实现自定义导航栏跳转

hasValue函数规范写法
如果要判断一个str(举例)是否空值,一般的写法是
static hasValue(str: string): boolean {
return str && str !== "";
}
这样的写法在严格类型检查的情况下是会报错的
也就是当我们配置了
tsconfig.json
{
"compilerOptions": {
/* 严格的类型检查选项 */
"strict": true,
}
}
会有这样的报错
TS2322: Type 'string | boolean' is not assignable to type 'boolean'. Type 'string' is not assignable to type 'boolean'.
在咨询后改为了
static hasValue(str: string): boolean {
return !!str && str !== "";
}
报错消失,这是因为判断里面是str可能是不符合规范(大佬原话)
如果要判断一个str(举例)是否空值,一般的写法是
static hasValue(str: string): boolean {
return str && str !== "";
}
这样的写法在严格类型检查的情况下是会报错的
也就是当我们配置了
tsconfig.json
{
"compilerOptions": {
/* 严格的类型检查选项 */
"strict": true,
}
}
会有这样的报错
TS2322: Type 'string | boolean' is not assignable to type 'boolean'. Type 'string' is not assignable to type 'boolean'.
在咨询后改为了
static hasValue(str: string): boolean {
return !!str && str !== "";
}
报错消失,这是因为判断里面是str可能是不符合规范(大佬原话)
收起阅读 »
uni小程序, 嵌入原生app, 文件下载到指定目录的实现方式
uni小程序, 嵌入原生app, 文件下载到指定目录的实现方式
- 实现该功能的时候, 两个问题需要解决, 1.uni.openDocument,只能打开几个常规后缀的文件 2. 下载文件下到了沙盒里面. _download开头的内部路径, 用户下次找不到了
为解决这两个问题, 我也是做了很多尝试, 现在将最终实现方式记录分享, 希望对你有些帮助
- 使用 plus.runtime.openFile 打开文件, 没有文件格式限制, 遇到打不开的文件时, 会有异常回调可以使用, 提示用户安装相关软件即可.
- 利用plus.downloader.createDownload 下载文件文档上说明只能保存到_dowloads, _doc,等几个固定开头的沙盒文件夹里, 所以只能扩展原生Moudle, 将文件移动到Download文件夹下
一下是代码部分,仅供参考
- 原生部分代码, 自定义Module,复制文件,一定要提前获取到用户的读写数据的权限!!!!!!!!!!!!!!!!!!!!!!!!!!!, 否则会提示权限问题, 文件无法复制
public class UniUesOaHeModule extends UniModule {
/**
* 输出日志
*/
@UniJSMethod(uiThread = false)
public void uniLog(String s) {
XLog.debug(s);
}
/**
* 沙盒文件移入媒体库
*/
@UniJSMethod(uiThread = false)
public void scanIntoMedia(String filePath, UniJSCallback callback) {
if (StringUtil.isEmpty(filePath)) {
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "error");
data.put("message", "文件路径不能为空");
callback.invokeAndKeepAlive(data);
}
}
if (mUniSDKInstance != null) {
String[] split = filePath.split("/");
String fileName = split[split.length -1];
String targetPath = "/storage/emulated/0/Download/" + fileName;
FileUtil fileUtil = FileUtil.INSTANCE;
XLog.debug("开始转义目录");
XLog.debug("由:[ " + filePath + " ] 转移至 : [ "+ targetPath + " ]");
//tod
fileUtil.copyFileWithFileChannel(new File(filePath), new File(targetPath));
//对于小米来说, 拷贝到Downloads文件夹下, 系统就会自动扫描到了, 不知道其他机型怎么样
//图片等常规文件,能出现在 `最近`里, dwg在Download里能找到, 但是不会在 `最近` 里显示,可能是系统原因
Context context = mUniSDKInstance.getContext();
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.parse(targetPath);
XLog.debug("将要扫描的地址是: " + contentUri.getPath());
mediaScanIntent.setData(contentUri);
context.sendBroadcast(mediaScanIntent);
XLog.debug("发送扫描指令");
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "success");
data.put("message", "添加到Download目录成功");
data.put("filePathAfterMove", "file:/"+ targetPath);
callback.invokeAndKeepAlive(data);
}
}else{
XLog.debug("实例不存在");
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "error");
data.put("message", "实例不存在");
callback.invokeAndKeepAlive(data);
}
}
/**
* 下面的不管用, 可能是只能扫描固定的几个媒体库路径, 上面的直接复制到Downloads下, 不用扫描都能发现
*/
//
// if (StringUtil.isEmpty(filePath)) {
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "error");
// data.put("message", "文件路径不能为空");
// callback.invokeAndKeepAlive(data);
// }
// }
// if (mUniSDKInstance != null) {
// Context context = mUniSDKInstance.getContext();
// try {
// MediaScannerConnection.scanFile(context, new String[]{filePath}, null,
// new MediaScannerConnection.OnScanCompletedListener() {
// public void onScanCompleted(String path, Uri uri) {
// XLog.debug("扫描的路径是: " + path + ":");
// XLog.debug("返回的uri: " + uri);
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "success");
// data.put("message", "媒体库扫描完成!!!!");
// callback.invokeAndKeepAlive(data);
// }
// }
// });
// } catch (Exception e) {
// e.printStackTrace();
// }
// } else {
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "error");
// data.put("message", "移入媒体库失败, 找不到mUniSDKInstance实例");
// callback.invokeAndKeepAlive(data);
// }
// }
}
}
- FileUtil, 只贴这一个复制文件的函数
/**
* 复制文件
*/
fun copyFileWithFileChannel(fileSource: File, fileDest:File) {
var fi: FileInputStream? = null
var fo: FileOutputStream? = null
var `in`: FileChannel? = null
var out: FileChannel? = null
try {
fi = FileInputStream(fileSource)
fo = FileOutputStream(fileDest)
`in` = fi.channel//得到对应的文件通道
out = fo.channel//得到对应的文件通道
`in`!!.transferTo(0, `in`.size(), out)//连接两个通道,并且从in通道读取,然后写入out通道
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
fi!!.close()
`in`!!.close()
fo!!.close()
out!!.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
别忘记注册组件
UniSDKEngine.registerModule("UniUesOaHeModule", UniUesOaHeModule::class.java)
uni小程序代码, download2是最终使用的方法, 其他的是测试时使用的
<template>
<!-- 这是图纸展示页面 -->
<page-head title="无匹配数据" v-if="designDrawingArray.length == 0"></page-head>
<view v-else>
<!-- {{designDrawingArray}} -->
<view v-for="(item, index) in designDrawingArray" :key="item.id">
<!-- {{ item.name }} - {{ item.remark }} -->
<uni-card :title="item.name" :sub-title="item.remark">
<view v-if="item.attachmentList.length > 0">
<uni-list>
<uni-list-item v-for="(attachmentItem, attachmentIndex) in item.attachmentList"
:key="attachmentItem.id" :title="attachmentItem.attachName"
@click="download2(attachmentItem.attachUrl,attachmentItem.attachSize)" showArrow link>
</uni-list-item>
</uni-list>
</view>
<view v-else>
暂无图纸
</view>
</uni-card>
</view>
</view>
</template>
<script>
import config from "@/common/config.js"
var uniUesOaHeModule = uni.requireNativePlugin("UniUesOaHeModule")
export default {
data() {
return {
designDrawingArray: []
}
},
onLoad(e) {
var contractType = e.contractType
var contractId = e.contractId
this.queryHeDesignDrawingWithAttachmentList(contractType, contractId)
},
methods: {
queryHeDesignDrawingWithAttachmentList(contractType, contractId) {
this.$http.post({
url: '/engineering/queryHeDesignDrawingWithAttachmentList.action',
data: {
contractType: contractType,
contractId: contractId
},
success: (res) => {
console.log(res)
if (res.status == 200) {
this.designDrawingArray = res.data
}
}
})
},
// 该方法不再使用, openDocument只能打开常用格式的文件, 不能打开dwg
downLoadAttachment(url) {
var downloadUrl = config.serverUrl + url
console.log("下载地址---" + downloadUrl)
uni.showModal({
title: '提示',
content: '确定下载该图纸?',
success: function(res) {
if (res.confirm) {
uni.showLoading({
title: '下载中'
})
var self = this
uni.downloadFile({
url: downloadUrl,
success: (res) => {
//保存到本地
console.log("下载成功,临时路径是---" + res.tempFilePath)
uni.saveFile({
tempFilePath: res.tempFilePath, //文件的临时路径
success: function(res) {
const savedFilePath = res.savedFilePath;
uni.showToast({
title: '将临时文件保存完成,保存的地址为:' +
savedFilePath,
icon: 'none'
});
uni.hideLoading();
uni.showModal({
title: '提示',
content: '打开下载文件?',
success: function(res) {
// 打开文件
if (res.confirm) {
uni.showToast({
title: '点击了确定打开文件',
icon: 'none'
});
uni.openDocument({
filePath: savedFilePath,
showMenu: true,
success: function(
res
) {
console
.log(
'打开文档成功'
);
uni.showToast({
title: '打开文档成功',
icon: 'none'
});
},
fail: function(
res
) {
console
.log(
'打开文档失败'
);
uni.showToast({
title: '打开文档失败',
icon: 'none'
});
}
});
}
}
})
},
fail: function(err) {}
});
},
fail: (err) => {
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
})
}
}
});
},
download2(url,fileSize) {
var logger = this.$log
var downloadUrl = config.serverUrl + url
logger(uniUesOaHeModule, "下载地址---" + downloadUrl)
//测试方便,使用390576, 正式使用下面50M
// if(fileSize > 1000 * 1000 * 50){
if(fileSize > 390576){
logger(uniUesOaHeModule, "文件大于50M,使用浏览器下载: "+ fileSize)
plus.runtime.openURL(downloadUrl)
}else{
logger(uniUesOaHeModule, "文件小于50M: "+ fileSize)
var pathArray = url.split('/')
var fileName = pathArray[pathArray.length - 1]
uni.showLoading({
title: '下载中'
})
let dtask = plus.downloader.createDownload(downloadUrl, {
//利用保存路径,实现下载文件的重命名,文档上说明只能保存到_dowloads, _doc,等几个固定开头的沙盒文件夹里,
// filename:localFile
}, function(d, status) {
uni.hideLoading();
if (status == 200) {
//下载成功,d.filename是文件在保存在本地的相对路径,使用下面的API可转为平台绝对路径
logger(uniUesOaHeModule, "下载完成了,现在要去打开文件,文件地址也就是d.fileName: " + d.filename)
let fileSaveUrl = plus.io.convertLocalFileSystemURL(d.filename);
logger(uniUesOaHeModule, "将d.fileName 沙盒文件路径转化为android平台的路径: " + fileSaveUrl)
//这样下载打开文件, 除非用户自己操作或移动到自己知道的文件夹下, 下次他就找不到了, 因此调用原生Module,将这个文件放到/Download下,下次能看到
uniUesOaHeModule.scanIntoMedia(fileSaveUrl,
(ret) => {
console.log(JSON.stringify(ret))
if(ret.code == 'success'){
//文件转移到Download目录成功了
plus.runtime.openFile(ret.filePathAfterMove,{},function(error){
logger(uniUesOaHeModule,"打开文件有问题: "+ JSON.stringify(error))
uni.showToast({
title: '没有能打开该文件的软件,请安装',
icon: 'none',
duration: 3000
});
});
}
})
} else {
uni.showToast({
title: '下载失败',
icon: 'none',
duration: 3000
});
plus.downloader.clear(); //清除下载任务
}
})
dtask.addEventListener("statechanged", function(task, status) {
logger(uniUesOaHeModule, "task:" + JSON.stringify(task) + "===========status" + status)
switch (task.state) {
case 2:
break;
case 3:
let prg = parseInt((parseFloat(task.downloadedSize) / parseFloat(task.totalSize)) *100);
// logger(uniUesOaHeModule, prg + "%")
break;
case 4:
break;
}
}, false);
dtask.start();
}
},
test(url, fileSize){
var logger = this.$log
var downloadUrl = config.serverUrl + url
logger(uniUesOaHeModule, "下载地址---" + downloadUrl)
// uni.showToast({
// title: '测试打开外部文件',
// icon: 'none'
// });
// plus.runtime.openFile("file://storage/emulated/0/Download/e96cca6878b14911940c4ad9db8e1ff7_a(9).jpg",{},function(error){
// // logger(uniUesOaHeModule,"打开文件出错: "+ JSON.stringify(error))
// console.log(JSON.stringify(error))
// uni.showToast({
// title: '1111111111',
// icon: 'none'
// });
// });
//大于50M, 建议使用浏览器下载
if(fileSize > 1000 * 1000 * 50){
logger(uniUesOaHeModule, "文件大于50M: "+ fileSize)
}else{
logger(uniUesOaHeModule, "文件小于50M: "+ fileSize)
}
plus.runtime.openURL(downloadUrl)
}
}
}
</script>
<style>
</style>
uni小程序, 嵌入原生app, 文件下载到指定目录的实现方式
- 实现该功能的时候, 两个问题需要解决, 1.uni.openDocument,只能打开几个常规后缀的文件 2. 下载文件下到了沙盒里面. _download开头的内部路径, 用户下次找不到了
为解决这两个问题, 我也是做了很多尝试, 现在将最终实现方式记录分享, 希望对你有些帮助
- 使用 plus.runtime.openFile 打开文件, 没有文件格式限制, 遇到打不开的文件时, 会有异常回调可以使用, 提示用户安装相关软件即可.
- 利用plus.downloader.createDownload 下载文件文档上说明只能保存到_dowloads, _doc,等几个固定开头的沙盒文件夹里, 所以只能扩展原生Moudle, 将文件移动到Download文件夹下
一下是代码部分,仅供参考
- 原生部分代码, 自定义Module,复制文件,一定要提前获取到用户的读写数据的权限!!!!!!!!!!!!!!!!!!!!!!!!!!!, 否则会提示权限问题, 文件无法复制
public class UniUesOaHeModule extends UniModule {
/**
* 输出日志
*/
@UniJSMethod(uiThread = false)
public void uniLog(String s) {
XLog.debug(s);
}
/**
* 沙盒文件移入媒体库
*/
@UniJSMethod(uiThread = false)
public void scanIntoMedia(String filePath, UniJSCallback callback) {
if (StringUtil.isEmpty(filePath)) {
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "error");
data.put("message", "文件路径不能为空");
callback.invokeAndKeepAlive(data);
}
}
if (mUniSDKInstance != null) {
String[] split = filePath.split("/");
String fileName = split[split.length -1];
String targetPath = "/storage/emulated/0/Download/" + fileName;
FileUtil fileUtil = FileUtil.INSTANCE;
XLog.debug("开始转义目录");
XLog.debug("由:[ " + filePath + " ] 转移至 : [ "+ targetPath + " ]");
//tod
fileUtil.copyFileWithFileChannel(new File(filePath), new File(targetPath));
//对于小米来说, 拷贝到Downloads文件夹下, 系统就会自动扫描到了, 不知道其他机型怎么样
//图片等常规文件,能出现在 `最近`里, dwg在Download里能找到, 但是不会在 `最近` 里显示,可能是系统原因
Context context = mUniSDKInstance.getContext();
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.parse(targetPath);
XLog.debug("将要扫描的地址是: " + contentUri.getPath());
mediaScanIntent.setData(contentUri);
context.sendBroadcast(mediaScanIntent);
XLog.debug("发送扫描指令");
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "success");
data.put("message", "添加到Download目录成功");
data.put("filePathAfterMove", "file:/"+ targetPath);
callback.invokeAndKeepAlive(data);
}
}else{
XLog.debug("实例不存在");
if (callback != null) {
JSONObject data = new JSONObject();
data.put("code", "error");
data.put("message", "实例不存在");
callback.invokeAndKeepAlive(data);
}
}
/**
* 下面的不管用, 可能是只能扫描固定的几个媒体库路径, 上面的直接复制到Downloads下, 不用扫描都能发现
*/
//
// if (StringUtil.isEmpty(filePath)) {
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "error");
// data.put("message", "文件路径不能为空");
// callback.invokeAndKeepAlive(data);
// }
// }
// if (mUniSDKInstance != null) {
// Context context = mUniSDKInstance.getContext();
// try {
// MediaScannerConnection.scanFile(context, new String[]{filePath}, null,
// new MediaScannerConnection.OnScanCompletedListener() {
// public void onScanCompleted(String path, Uri uri) {
// XLog.debug("扫描的路径是: " + path + ":");
// XLog.debug("返回的uri: " + uri);
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "success");
// data.put("message", "媒体库扫描完成!!!!");
// callback.invokeAndKeepAlive(data);
// }
// }
// });
// } catch (Exception e) {
// e.printStackTrace();
// }
// } else {
// if (callback != null) {
// JSONObject data = new JSONObject();
// data.put("code", "error");
// data.put("message", "移入媒体库失败, 找不到mUniSDKInstance实例");
// callback.invokeAndKeepAlive(data);
// }
// }
}
}
- FileUtil, 只贴这一个复制文件的函数
/**
* 复制文件
*/
fun copyFileWithFileChannel(fileSource: File, fileDest:File) {
var fi: FileInputStream? = null
var fo: FileOutputStream? = null
var `in`: FileChannel? = null
var out: FileChannel? = null
try {
fi = FileInputStream(fileSource)
fo = FileOutputStream(fileDest)
`in` = fi.channel//得到对应的文件通道
out = fo.channel//得到对应的文件通道
`in`!!.transferTo(0, `in`.size(), out)//连接两个通道,并且从in通道读取,然后写入out通道
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
fi!!.close()
`in`!!.close()
fo!!.close()
out!!.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
别忘记注册组件
UniSDKEngine.registerModule("UniUesOaHeModule", UniUesOaHeModule::class.java)
uni小程序代码, download2是最终使用的方法, 其他的是测试时使用的
<template>
<!-- 这是图纸展示页面 -->
<page-head title="无匹配数据" v-if="designDrawingArray.length == 0"></page-head>
<view v-else>
<!-- {{designDrawingArray}} -->
<view v-for="(item, index) in designDrawingArray" :key="item.id">
<!-- {{ item.name }} - {{ item.remark }} -->
<uni-card :title="item.name" :sub-title="item.remark">
<view v-if="item.attachmentList.length > 0">
<uni-list>
<uni-list-item v-for="(attachmentItem, attachmentIndex) in item.attachmentList"
:key="attachmentItem.id" :title="attachmentItem.attachName"
@click="download2(attachmentItem.attachUrl,attachmentItem.attachSize)" showArrow link>
</uni-list-item>
</uni-list>
</view>
<view v-else>
暂无图纸
</view>
</uni-card>
</view>
</view>
</template>
<script>
import config from "@/common/config.js"
var uniUesOaHeModule = uni.requireNativePlugin("UniUesOaHeModule")
export default {
data() {
return {
designDrawingArray: []
}
},
onLoad(e) {
var contractType = e.contractType
var contractId = e.contractId
this.queryHeDesignDrawingWithAttachmentList(contractType, contractId)
},
methods: {
queryHeDesignDrawingWithAttachmentList(contractType, contractId) {
this.$http.post({
url: '/engineering/queryHeDesignDrawingWithAttachmentList.action',
data: {
contractType: contractType,
contractId: contractId
},
success: (res) => {
console.log(res)
if (res.status == 200) {
this.designDrawingArray = res.data
}
}
})
},
// 该方法不再使用, openDocument只能打开常用格式的文件, 不能打开dwg
downLoadAttachment(url) {
var downloadUrl = config.serverUrl + url
console.log("下载地址---" + downloadUrl)
uni.showModal({
title: '提示',
content: '确定下载该图纸?',
success: function(res) {
if (res.confirm) {
uni.showLoading({
title: '下载中'
})
var self = this
uni.downloadFile({
url: downloadUrl,
success: (res) => {
//保存到本地
console.log("下载成功,临时路径是---" + res.tempFilePath)
uni.saveFile({
tempFilePath: res.tempFilePath, //文件的临时路径
success: function(res) {
const savedFilePath = res.savedFilePath;
uni.showToast({
title: '将临时文件保存完成,保存的地址为:' +
savedFilePath,
icon: 'none'
});
uni.hideLoading();
uni.showModal({
title: '提示',
content: '打开下载文件?',
success: function(res) {
// 打开文件
if (res.confirm) {
uni.showToast({
title: '点击了确定打开文件',
icon: 'none'
});
uni.openDocument({
filePath: savedFilePath,
showMenu: true,
success: function(
res
) {
console
.log(
'打开文档成功'
);
uni.showToast({
title: '打开文档成功',
icon: 'none'
});
},
fail: function(
res
) {
console
.log(
'打开文档失败'
);
uni.showToast({
title: '打开文档失败',
icon: 'none'
});
}
});
}
}
})
},
fail: function(err) {}
});
},
fail: (err) => {
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
})
}
}
});
},
download2(url,fileSize) {
var logger = this.$log
var downloadUrl = config.serverUrl + url
logger(uniUesOaHeModule, "下载地址---" + downloadUrl)
//测试方便,使用390576, 正式使用下面50M
// if(fileSize > 1000 * 1000 * 50){
if(fileSize > 390576){
logger(uniUesOaHeModule, "文件大于50M,使用浏览器下载: "+ fileSize)
plus.runtime.openURL(downloadUrl)
}else{
logger(uniUesOaHeModule, "文件小于50M: "+ fileSize)
var pathArray = url.split('/')
var fileName = pathArray[pathArray.length - 1]
uni.showLoading({
title: '下载中'
})
let dtask = plus.downloader.createDownload(downloadUrl, {
//利用保存路径,实现下载文件的重命名,文档上说明只能保存到_dowloads, _doc,等几个固定开头的沙盒文件夹里,
// filename:localFile
}, function(d, status) {
uni.hideLoading();
if (status == 200) {
//下载成功,d.filename是文件在保存在本地的相对路径,使用下面的API可转为平台绝对路径
logger(uniUesOaHeModule, "下载完成了,现在要去打开文件,文件地址也就是d.fileName: " + d.filename)
let fileSaveUrl = plus.io.convertLocalFileSystemURL(d.filename);
logger(uniUesOaHeModule, "将d.fileName 沙盒文件路径转化为android平台的路径: " + fileSaveUrl)
//这样下载打开文件, 除非用户自己操作或移动到自己知道的文件夹下, 下次他就找不到了, 因此调用原生Module,将这个文件放到/Download下,下次能看到
uniUesOaHeModule.scanIntoMedia(fileSaveUrl,
(ret) => {
console.log(JSON.stringify(ret))
if(ret.code == 'success'){
//文件转移到Download目录成功了
plus.runtime.openFile(ret.filePathAfterMove,{},function(error){
logger(uniUesOaHeModule,"打开文件有问题: "+ JSON.stringify(error))
uni.showToast({
title: '没有能打开该文件的软件,请安装',
icon: 'none',
duration: 3000
});
});
}
})
} else {
uni.showToast({
title: '下载失败',
icon: 'none',
duration: 3000
});
plus.downloader.clear(); //清除下载任务
}
})
dtask.addEventListener("statechanged", function(task, status) {
logger(uniUesOaHeModule, "task:" + JSON.stringify(task) + "===========status" + status)
switch (task.state) {
case 2:
break;
case 3:
let prg = parseInt((parseFloat(task.downloadedSize) / parseFloat(task.totalSize)) *100);
// logger(uniUesOaHeModule, prg + "%")
break;
case 4:
break;
}
}, false);
dtask.start();
}
},
test(url, fileSize){
var logger = this.$log
var downloadUrl = config.serverUrl + url
logger(uniUesOaHeModule, "下载地址---" + downloadUrl)
// uni.showToast({
// title: '测试打开外部文件',
// icon: 'none'
// });
// plus.runtime.openFile("file://storage/emulated/0/Download/e96cca6878b14911940c4ad9db8e1ff7_a(9).jpg",{},function(error){
// // logger(uniUesOaHeModule,"打开文件出错: "+ JSON.stringify(error))
// console.log(JSON.stringify(error))
// uni.showToast({
// title: '1111111111',
// icon: 'none'
// });
// });
//大于50M, 建议使用浏览器下载
if(fileSize > 1000 * 1000 * 50){
logger(uniUesOaHeModule, "文件大于50M: "+ fileSize)
}else{
logger(uniUesOaHeModule, "文件小于50M: "+ fileSize)
}
plus.runtime.openURL(downloadUrl)
}
}
}
</script>
<style>
</style>
收起阅读 »

24h无人共享自助洗车整套方案+仿小米Lite商城一体前后端项目需要的来
个人需要开店免费使用,想自己做品牌需要收费,车牌识别,led屏显,语音等功能SDK均已对接好,有整店输出方案,24h无人管理,有想法合作的来。
tel/微信:18536745515
个人需要开店免费使用,想自己做品牌需要收费,车牌识别,led屏显,语音等功能SDK均已对接好,有整店输出方案,24h无人管理,有想法合作的来。
tel/微信:18536745515

uniapp项目作为独立分包嵌入主包需要适配的问题-组件绝对路径引用
- uniapp编译打包组件引用为绝对路径,作为子包需要二次编译引入路径
main();
function main() {
const allFiles = getAllFiles('dist/build/yourBuildPath');
for (let i = 0; i < allFiles.length; i++) {
// console.log(allFiles[i]);
// 同步读取文件内容
handleJSONFile(allFiles[i]);
}
}
/**
* 递归遍历,获取指定文件夹下面的所有文件路径
*/
function getAllFiles(filePath) {
let allFilePaths = [];
if (fs.existsSync(filePath)) {
const files = fs.readdirSync(filePath);
for (let i = 0; i < files.length; i++) {
const file = files[i]; // 文件名称(不包含文件路径)
const currentFilePath = `${filePath}/${file}`;
const stats = fs.lstatSync(currentFilePath);
if (/components|pages|node-modules/.test(currentFilePath)) {
if (stats.isDirectory()) {
allFilePaths = allFilePaths.concat(getAllFiles(currentFilePath));
} else {
if (/.json/.test(currentFilePath)) {
allFilePaths.push(currentFilePath);
}
}
}
}
} else {
console.warn(`指定的目录${filePath}不存在!`);
}
return allFilePaths;
}
function handleJSONFile(filePath) {
fs.readFile(filePath, 'utf8', function(err, data) {
const jsonData = JSON.parse(data);
console.log(filePath, jsonData);
if (jsonData.usingComponents && typeof jsonData.usingComponents === 'object') {
const usingComponents = {};
Object.keys(jsonData.usingComponents).forEach((keys) => {
usingComponents[keys] = `/yourSubpackagePath${jsonData.usingComponents[keys]}`;
});
jsonData.usingComponents = usingComponents;
}
fs.writeFileSync(filePath, JSON.stringify(jsonData));
});
}
- uniapp编译打包组件引用为绝对路径,作为子包需要二次编译引入路径
main();
function main() {
const allFiles = getAllFiles('dist/build/yourBuildPath');
for (let i = 0; i < allFiles.length; i++) {
// console.log(allFiles[i]);
// 同步读取文件内容
handleJSONFile(allFiles[i]);
}
}
/**
* 递归遍历,获取指定文件夹下面的所有文件路径
*/
function getAllFiles(filePath) {
let allFilePaths = [];
if (fs.existsSync(filePath)) {
const files = fs.readdirSync(filePath);
for (let i = 0; i < files.length; i++) {
const file = files[i]; // 文件名称(不包含文件路径)
const currentFilePath = `${filePath}/${file}`;
const stats = fs.lstatSync(currentFilePath);
if (/components|pages|node-modules/.test(currentFilePath)) {
if (stats.isDirectory()) {
allFilePaths = allFilePaths.concat(getAllFiles(currentFilePath));
} else {
if (/.json/.test(currentFilePath)) {
allFilePaths.push(currentFilePath);
}
}
}
}
} else {
console.warn(`指定的目录${filePath}不存在!`);
}
return allFilePaths;
}
function handleJSONFile(filePath) {
fs.readFile(filePath, 'utf8', function(err, data) {
const jsonData = JSON.parse(data);
console.log(filePath, jsonData);
if (jsonData.usingComponents && typeof jsonData.usingComponents === 'object') {
const usingComponents = {};
Object.keys(jsonData.usingComponents).forEach((keys) => {
usingComponents[keys] = `/yourSubpackagePath${jsonData.usingComponents[keys]}`;
});
jsonData.usingComponents = usingComponents;
}
fs.writeFileSync(filePath, JSON.stringify(jsonData));
});
}
收起阅读 »

玩转 uniapp 全端开发
玩转 uniapp 全端开发
视频链接
https://www.bilibili.com/video/BV1iG4y167ef/
uniapp 介绍
uni-app
是一个使用 Vue.js (opens new window)开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各
种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。
是目前全端开发框架的佼佼者
多端体验
uniapp 优势
uniapp 生态介绍
uniapp项目创建
uniapp 项目开发方式分为两种
- vue-cli (只开 h5端 或者 只开发 微信小程序端)
- HBuilderX 可视化 (多端开发首选 只开发 手机APP)
vue-cli
创建项目
-
全局安装vue-cli
npm install -g @vue/cli@4
-
创建项目
my-project
项目名称vue create -p dcloudio/uni-preset-vue my-project
-
选择模版- 选择默认模版
-
成功
运行项目
npm run dev:平台代号
npm run build:平台代号
值 | 平台 |
---|---|
app-plus | app平台生成打包资源(支持npm run build:app-plus,可用于持续集成。不支持run,运行调试仍需在HBuilderX中操作) |
h5 | H5 |
mp-alipay | 支付宝小程序 |
mp-baidu | 百度小程序 |
mp-weixin | 微信小程序 |
mp-toutiao | 字节跳动小程序 |
mp-lark | 飞书小程序 |
mp-qq | qq 小程序 |
mp-360 | 360 小程序 |
mp-kuaishou | 快手小程序 |
mp-jd | 京东小程序 |
mp-xhs | 小红书小程序 |
quickapp-webview | 快应用(webview) |
quickapp-webview-union | 快应用联盟 |
quickapp-webview-huawei | 快应用华为 |
HBuilderX
如果你要使用uiapp开发多端,那么就必须要选择和它配套的编辑工具了 HBuilderX
。考虑到后期要使用更多的 uniapp的功能,建议提前注册一个uniapp的开发账号。 注册
创建项目
-
新建项目
-
选择项目
-
创建成功
-
编辑器中,敲入
u
+代码 即可调出HBuilderX的代码提示
运行项目
第一次运行,可能需要安装插件,等待即可
uniapp 开发环境搭建
uniapp 是全端开发框架,假如我们想要开发全端,那么首先需要搭建好各个端对应的环境。以下拿比较典型的 微信小程序、H5 和 安卓App来演示。 发布环境的讲解在后续
微信小程序
下载开发者工具
下载安装成功后,会在桌面上显示出来一个图标
注册微信小程序开发者账号
另外 想要开发一款微信小程序,必须要注册微信开发者账号,同时获取对应的appid。
获取appid
打开服务端口
在 HBuilderX 中运行项目
H5
如果使用内置的浏览器预览页面,它是自带跨域的。
同时每一个vue页面中的样式,也是默认自己加上
scoped
的。
运行H5比较简单,只需要电脑上安装好浏览器就行,或者使用 HBuilderX自带内置浏览器也可以
App
由于电脑操作系统限制,我们只演示 android
。
分为两种:
- 运行到模拟器
- 运行到真机
运行到模拟器
安卓模拟器可以自由选择,这里我使用的是 Android studio 内置的模拟器
安装 android studio 模拟器步骤如下:
-
-
打开安装包,然后 勾选上 安装虚拟机
-
打开
Android Studio
-
-
选择要安装的手机型号
-
选择安装对应的安卓系统版本 下载过程比较慢,因为系统镜像比较大
-
下载成功了,回到设备列表页面,运行起来
-
开机
-
成功
-
现在可以回到HBuilderX中来运行项目到模拟器里面了
-
HBuilderX会自动检测你电脑上的模拟器或者真实安卓手机
-
成功
-
运行到真实手机
-
准备一台正常的安卓手机,开启开发人员选项和允许USB调试
-
连接数据线到电脑上 ,如果弹出什么菜单全部点击允许
-
这个时候,重新回到 HBuilderX中,点击运行项目到 App上
-
此时你的手机会弹出窗口,提示你安装软件,最后成功显示
App 调试 模拟器调试
-
运行模拟器
-
运行项目
-
打开webview调试 该选项只能调试页面标签和样式,不能调试js
-
点击调试
-
此时会打开一个页面调试工具,开始调试
-
模拟器上也会跟着发生变化
-
此时可以开启 js调试
-
此时会弹出一个新的窗口 我们可以在这里进行调试
App 真机调试
调试方式和 调试模拟器类似。直接操作即可
uniapp 项目结构介绍
https://uniapp.dcloud.net.cn/tutorial/project.html
┌─uniCloud 云空间目录,阿里云为uniCloud-aliyun,腾讯云为uniCloud-tcb
│─components 符合vue组件规范的uni-app组件目录
│ └─comp-a.vue 可复用的a组件
├─hybrid App端存放本地html文件的目录
├─platforms 存放各平台专用页面的目录
├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
│ └─list
│ └─list.vue list页面
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─uni_modules 存放[uni_module](/uni_modules)。
├─wxcomponents 存放小程序组件的目录
├─nativeplugins App原生插件 详见
├─unpackage 非工程代码,一般存放运行或发行的编译结果
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json 配置应用名称、appid、logo、版本等打包信息
├─pages.json 配置页面路由、导航条、选项卡等页面类信息
└─uni.scss 这里是uni-app内置的常用样式变量
uniapp 开发规范介绍
为了实现多端兼容,综合考虑编译速度、运行性能等因素,uni-app
约定了如下开发规范
页面和组件文件遵循vue的规范
- 比如 新建页面
goods.vue
- 比如 新建组件
it-item.vue
内置标签使用小程序的规范
<view>小程序中的块级标签</view>
数据绑定和事件处理使用vue的规范
<template>
<view>
<view class="item" v-for="item in list" :key="item" @click="handleClick(item)">{{item}}</view>
</view>
</template>
<script>
export default {
data(){
return {
list:['a','b','c']
}
},
methods:{
handleClick(letter){
console.log(letter)
}
}
}
</script>
能力接口API 使用 微信小程序的规范
比如弹出显示框,发送网络请求等
wx.showToast({
title: '成功',
icon: 'success',
duration: 2000
})
wx.request({
url: 'example.php', //仅为示例,并非真实的接口地址
data: {
x: '',
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
}
})
考虑到跨端,我们将会使用 uniapp
统一封装的API。简称 uni api
uniapp 生命周期
uniapp中,生命周期分类三大类
- 应用生命周期 小程序规范
- 页面生命周期 小程序规范
- 组件生命周期 vue规范
应用生命周期 App.vue
函数名 | 说明 |
---|---|
onLaunch | 当uni-app 初始化完成时触发(全局只触发一次) |
onShow | 当 uni-app 启动,或从后台进入前台显示 |
onHide | 当 uni-app 从前台进入后台 |
onError | 当 uni-app 报错时触发 |
onUniNViewMessage | 对 nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯(opens new window) |
onUnhandledRejection | 对未处理的 Promise 拒绝事件监听函数(2.8.1+) |
onPageNotFound | 页面不存在监听函数 |
onThemeChange | 监听系统主题变化 |
页面生命周期
函数名 | 说明 | 平台差异说明 | 最低版本 |
---|---|---|---|
onInit | 监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad | 百度小程序 | 3.1.0+ |
onLoad | 监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例 | ||
onShow | 监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面 | ||
onReady | 监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发 | ||
onHide | 监听页面隐藏 | ||
onUnload | 监听页面卸载 | ||
onResize | 监听窗口尺寸变化 | App、微信小程序、快手小程序 | |
onPullDownRefresh | 监听用户下拉动作,一般用于下拉刷新,参考示例 | ||
onReachBottom | 页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。具体见下方注意事项 | ||
onTabItemTap | 点击 tab 时触发,参数为Object,具体见下方注意事项 | 微信小程序、QQ小程序、支付宝小程序、百度小程序、H5、App、快手小程序、京东小程序 | |
onShareAppMessage | 用户点击右上角分享 | 微信小程序、QQ小程序、支付宝小程序、字节小程序、飞书小程序、快手小程序、京东小程序 | |
onPageScroll | 监听页面滚动,参数为Object | nvue暂不支持 | |
onNavigationBarButtonTap | 监听原生标题栏按钮点击事件,参数为Object | App、H5 | |
onBackPress | 监听页面返回,返回 event = {from:backbutton、 navigateBack} ,backbutton 表示来源是左上角返回按钮或 android 返回键;navigateBack表示来源是 uni.navigateBack ;详细说明及使用:onBackPress 详解 (opens new window)。支付宝小程序只有真机能触发,只能监听非navigateBack引起的返回,不可阻止默认行为。 | app、H5、支付宝小程序 | |
onNavigationBarSearchInputChanged | 监听原生标题栏搜索输入框输入内容变化事件 | App、H5 | 1.6.0 |
onNavigationBarSearchInputConfirmed | 监听原生标题栏搜索输入框搜索事件,用户点击软键盘上的“搜索”按钮时触发。 | App、H5 | 1.6.0 |
onNavigationBarSearchInputClicked | 监听原生标题栏搜索输入框点击事件(pages.json 中的 searchInput 配置 disabled 为 true 时才会触发) | App、H5 | 1.6.0 |
onShareTimeline | 监听用户点击右上角转发到朋友圈 | 微信小程序 | 2.8.1+ |
onAddToFavorites | 监听用户点击右上角收藏 | 微信小程序 | 2.8.1+ |
组件生命周期
函数名 | 说明 | 平台差异说明 | 最低版本 |
beforeCreate | 在实例初始化之前被调用。详见(opens new window) | ||
created | 在实例创建完成后被立即调用。详见(opens new window) | ||
beforeMount | 在挂载开始之前被调用。详见(opens new window) | ||
mounted | 挂载到实例上去之后调用。详见 (opens new window)注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTick Vue官方文档(opens new window) |
||
beforeUpdate | 数据更新时调用,发生在虚拟 DOM 打补丁之前。详见(opens new window) | 仅H5平台支持 | |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。详见(opens new window) | 仅H5平台支持 | |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。详见(opens new window) | ||
destroyed | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。详见(opens new window) |
响应式单位 rpx
https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#尺寸单位
相对长度单位,功能类似于web端的 rem 和 vw,小程序首先推出,uniapp也是直接支持。一种根据屏幕宽度自适应的动态单位。以 750 宽的屏幕为基准,750rpx 恰好为屏幕宽度。
其中uniapp做了以下设置,
- 默认的设计稿宽度为
375px
因此存在1px = 2rpx
- 默认 rpx支持最大宽度为
960px
,超出则 按照 设计稿宽度375px
来设置
scoped
- vue开发的h5,单页面应用程序,每一个vue文件如果直接使用 class,多个文件的样式 冲突
- 微信小程序 真正 多页面应用程序 物理隔离,页面中使用 class,不会相互影响
- uniapp做了一个设置,写vue代码,不需要主动加上
scoped
,打包成h5端的时候自动添加上去,打包成 微信小程序端 不需要添加 scoped。
uniapp 多端开发
条件编译
条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。
写法:以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。
- #ifdef:if defined 仅在某平台存在
- #ifndef:if not defined 除了某平台均存在
- %PLATFORM%:平台名称
标签中
<!-- #ifdef %PLATFORM% -->
平台特有的组件
<!-- #endif -->
js中
// #ifdef %PLATFORM%
平台特有的API实现
// #endif
css中
/* #ifdef %PLATFORM% */
平台特有样式
/* #endif */
条件编译写法 | 说明 |
---|---|
#ifdef APP-PLUS 需条件编译的代码 #endif | 仅出现在 App 平台下的代码 |
#ifndef H5 需条件编译的代码 <br />#endif | 除了 H5 平台,其它平台均存在的代码 |
#ifdef H5 || MP-WEIXIN 需条件编译的代码 #endif | 在 H5 平台或微信小程序平台存在的代码(这里只有||,不可能出现&&,因为没有交集) |
%PLATFORM% 可取值如下:
值 | 生效条件 |
---|---|
VUE3 | HBuilderX 3.2.0+ 详情(opens new window) |
APP-PLUS-NVUE或APP-NVUE | App nvue |
MP-WEIXIN | 微信小程序 |
MP-ALIPAY | 支付宝小程序 |
MP-BAIDU | 百度小程序 |
MP-TOUTIAO | 字节跳动小程序 |
MP-LARK | 飞书小程序 |
MP-QQ | QQ小程序 |
MP-KUAISHOU | 快手小程序 |
MP-JD | 京东小程序 |
MP-360 | 360小程序 |
MP | 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/飞书小程序/QQ小程序/360小程序 |
QUICKAPP-WEBVIEW | 快应用通用(包含联盟、华为) |
QUICKAPP-WEBVIEW-UNION | 快应用联盟 |
QUICKAPP-WEBVIEW-HUAWEI | 快应用华为 |
支持的文件
- .vue
- .js
- .css
- pages.json
- 各预编译语言文件,如:.scss、.less、.stylus、.ts、.pug
static 目录的条件编译
在不同平台,引用的静态资源可能也存在差异,通过 static 的的条件编译可以解决此问题,static 目录下新建不同平台的专有目录(目录名称同 %PLATFORM%
值域,但字母均为小写),专有目录下的静态资源只有在特定平台才会编译进去。
如以下目录结构,a.png
只有在微信小程序平台才会编译进去,b.png
在所有平台都会被编译。
┌─static
│ ├─mp-weixin
│ │ └─a.png
│ └─b.png
├─main.js
├─App.vue
├─manifest.json
└─pages.json
整体目录条件编译
如果想把各平台的页面文件更彻底的分开,也可以在uni-app项目根目录创建platforms
目录,然后在下面进一步创建app-plus
、mp-weixin
等子目录,存放不同平台的文件。
注意
platforms
目录下只支持放置页面文件(即页面vue文件),如果需要对其他资源条件编译建议使用static 目录的条件编译(opens new window)
flex布局
尽量使用flex布局,因为全平台都支持
尺寸单位
- uniapp通用单位
px
,rpx
- vue页面中支持
rem
、vh
、vw
- nvue 不支持
百分比单位
css变量
CSS 变量 | 描述 | App | 小程序 | H5 |
---|---|---|---|---|
--status-bar-height | 系统状态栏高度 | 系统状态栏高度 (opens new window)、nvue 注意见下 | 25px | 0 |
--window-top | 内容区域距离顶部的距离 | 0 | 0 | NavigationBar 的高度 |
--window-bottom | 内容区域距离底部的距离 | 0 | 0 | TabBar 的高度 |
背景图片
-
支持 base64 格式图片。
-
支持网络路径图片。
-
小程序不支持在 css 中使用本地文件。需以 base64 方式方可使用。
-
使用本地路径背景图片需注意:
- 为方便开发者,在背景图片小于 40kb 时,
uni-app
编译到不支持本地背景图的平台时,会自动将其转化为 base64 格式; - 本地背景图片的引用路径推荐使用以
~@
开头的绝对路径。
.test2 { background-image: url('~@/static/logo.png'); }
- 为方便开发者,在背景图片小于 40kb 时,
uview & uni ui
uview
和 uni ui
都是 和uniapp配套的全端UI框架,可以单独使用,也可以共同使用
uview ui
由于目前 uview 2.x 版本的坑不少,因此我们拿比较稳定的 uview 1.8.6 来演示
另外,uview ui 的引入方式分为两种,主要是取决于你的项目是如何创建的:
- vue-cli
- HBuilderX
uview ui + vue-cli
-
安装依赖
npm i uview-ui@1.8.4 sass
-
在
src/main.js
文件中 全局引入 js库import uView from "uview-ui"; Vue.use(uView);
-
在 uni.scss 中 引入 uview 的 sass 主题库
@import "uview-ui/theme.scss";
-
在 App.vue 中 引入 uview 的 sass 主题库
<style lang="scss"> @import "uview-ui/index.scss"; </style>
-
pages.json 中 配置 easycom
{ "easycom": { "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" }, // 此为本身已有的内容 "pages": [ // ...... ] }
-
在 页面中 使用 uview的按钮
<u-button >默认按钮</u-button> <u-button type="primary">主要按钮</u-button> <u-button type="success">成功按钮</u-button> <u-button type="info">信息按钮</u-button> <u-button type="warning">警告按钮</u-button> <u-button type="error">危险按钮</u-button>
-
成功
uview ui + HBuilderX
-
点击导入插件
-
导入成功,可以看到目录下多了
components
文件夹 -
接着在
uni.scss
中导入 uview的主题样式文件theme.scss
@import './theme.scss';
-
在页面上添加测试代码
<u-button >默认按钮</u-button> <u-button type="primary">主要按钮</u-button> <u-button type="success">成功按钮</u-button> <u-button type="info">信息按钮</u-button> <u-button type="warning">警告按钮</u-button> <u-button type="error">危险按钮</u-button>
-
点击运行
-
成功
uni ui
uni ui + vue-cli
-
安装相关依赖
如果
node
版本小于 16 ,sass-loader 请使用低于 @11.0.0 的版本如果
node
版本大于 16 ,sass-loader
建议使用v8.x
版本npm i sass sass-loader@10.1.1 @dcloudio/uni-ui
-
配置easycom
在
pages.json
中进行配置{ "easycom": { "autoscan": true, "custom": { // uni-ui 规则如下配置 "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" } }, // 其他内容 pages:[ // ... ] }
-
组件中使用
<uni-title title="上报统计数据"></uni-title> <uni-title type="h1" title="h1 一级标题 "></uni-title> <uni-title type="h1" title="h1 一级标题" color="#027fff"></uni-title> <uni-title type="h2" title="h2 居中" align="center"></uni-title>
uni ui + HBuilderX
-
使用 HBuilderX 导入插件
-
项目中会多
uni_modules
文件夹 -
页面中导入代码
<uni-title title="上报统计数据"></uni-title> <uni-title type="h1" title="h1 一级标题 "></uni-title> <uni-title type="h1" title="h1 一级标题" color="#027fff"></uni-title> <uni-title type="h2" title="h2 居中" align="center"></uni-title>
-
观察效果
uniapp 发布到多端
H5
普通发布
项目开发完成后,可以在 HBuilderX中来 打包成 H5项目
-
打包成H5
-
填写信息
-
此时,生成的h5项目会放在
unpackage\dist\build\h5
发布到uniapp的云环境
需要提前开通 uniCloud 的云服务
微信小程序
在HBuilderX中,想要发布微信小程序,有两种方式
普通发布
-
点击发行微信小程序
-
填写 appid
-
此时会自动打开 微信开发者工具,然后点击上传即可
-
然后填写 版本信息即可
-
成功后,回到微信开发者后台,打开 管理 版本管理 手动点击 提交审核。等待审核
HBuilderX 发布
其实还可以利用 HBuilderX 直接发布,不用打开微信开发者工具
-
首先登录你的微信开发者后台
-
打开 开发管理 小程序代码上传密钥 重置
-
下载 小程序代码上传密钥
-
在企业开发中,记得要开启 IP白名单,降低风险
-
选择上传密钥
-
上传成功
App
在HBuilderX中发布App的方式分为两种
- 本地离线打包
- 云打包
云打包
云打包的意思是利用 dcloud提供的能力,将你本地代码上传到 dcloud 服务器上,在云上打包完成再下载回本地。
-
打开
mainifest.json
设置 uniapp 应用标识AppID
这个是uniapp应用的id,不是微信小程序的id -
设置 App 支持CPU类型
App常用其他设置 支持CPU类型
-
设置使用原生隐私正则提示框
-
发行 - 原生App - 云打包
-
填写相关信息
-
打包成功
uniapp 其他资源
-
uniapp优秀案例源码地址
-
uniapp 官方交流QQ群
玩转 uniapp 全端开发
视频链接
https://www.bilibili.com/video/BV1iG4y167ef/
uniapp 介绍
uni-app
是一个使用 Vue.js (opens new window)开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各
种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。
是目前全端开发框架的佼佼者
多端体验
uniapp 优势
uniapp 生态介绍
uniapp项目创建
uniapp 项目开发方式分为两种
- vue-cli (只开 h5端 或者 只开发 微信小程序端)
- HBuilderX 可视化 (多端开发首选 只开发 手机APP)
vue-cli
创建项目
-
全局安装vue-cli
npm install -g @vue/cli@4
-
创建项目
my-project
项目名称vue create -p dcloudio/uni-preset-vue my-project
-
选择模版- 选择默认模版
-
成功
运行项目
npm run dev:平台代号
npm run build:平台代号
值 | 平台 |
---|---|
app-plus | app平台生成打包资源(支持npm run build:app-plus,可用于持续集成。不支持run,运行调试仍需在HBuilderX中操作) |
h5 | H5 |
mp-alipay | 支付宝小程序 |
mp-baidu | 百度小程序 |
mp-weixin | 微信小程序 |
mp-toutiao | 字节跳动小程序 |
mp-lark | 飞书小程序 |
mp-qq | qq 小程序 |
mp-360 | 360 小程序 |
mp-kuaishou | 快手小程序 |
mp-jd | 京东小程序 |
mp-xhs | 小红书小程序 |
quickapp-webview | 快应用(webview) |
quickapp-webview-union | 快应用联盟 |
quickapp-webview-huawei | 快应用华为 |
HBuilderX
如果你要使用uiapp开发多端,那么就必须要选择和它配套的编辑工具了 HBuilderX
。考虑到后期要使用更多的 uniapp的功能,建议提前注册一个uniapp的开发账号。 注册
创建项目
-
新建项目
-
选择项目
-
创建成功
-
编辑器中,敲入
u
+代码 即可调出HBuilderX的代码提示
运行项目
第一次运行,可能需要安装插件,等待即可
uniapp 开发环境搭建
uniapp 是全端开发框架,假如我们想要开发全端,那么首先需要搭建好各个端对应的环境。以下拿比较典型的 微信小程序、H5 和 安卓App来演示。 发布环境的讲解在后续
微信小程序
下载开发者工具
下载安装成功后,会在桌面上显示出来一个图标
注册微信小程序开发者账号
另外 想要开发一款微信小程序,必须要注册微信开发者账号,同时获取对应的appid。
获取appid
打开服务端口
在 HBuilderX 中运行项目
H5
如果使用内置的浏览器预览页面,它是自带跨域的。
同时每一个vue页面中的样式,也是默认自己加上
scoped
的。
运行H5比较简单,只需要电脑上安装好浏览器就行,或者使用 HBuilderX自带内置浏览器也可以
App
由于电脑操作系统限制,我们只演示 android
。
分为两种:
- 运行到模拟器
- 运行到真机
运行到模拟器
安卓模拟器可以自由选择,这里我使用的是 Android studio 内置的模拟器
安装 android studio 模拟器步骤如下:
-
-
打开安装包,然后 勾选上 安装虚拟机
-
打开
Android Studio
-
-
选择要安装的手机型号
-
选择安装对应的安卓系统版本 下载过程比较慢,因为系统镜像比较大
-
下载成功了,回到设备列表页面,运行起来
-
开机
-
成功
-
现在可以回到HBuilderX中来运行项目到模拟器里面了
-
HBuilderX会自动检测你电脑上的模拟器或者真实安卓手机
-
成功
-
运行到真实手机
-
准备一台正常的安卓手机,开启开发人员选项和允许USB调试
-
连接数据线到电脑上 ,如果弹出什么菜单全部点击允许
-
这个时候,重新回到 HBuilderX中,点击运行项目到 App上
-
此时你的手机会弹出窗口,提示你安装软件,最后成功显示
App 调试 模拟器调试
-
运行模拟器
-
运行项目
-
打开webview调试 该选项只能调试页面标签和样式,不能调试js
-
点击调试
-
此时会打开一个页面调试工具,开始调试
-
模拟器上也会跟着发生变化
-
此时可以开启 js调试
-
此时会弹出一个新的窗口 我们可以在这里进行调试
App 真机调试
调试方式和 调试模拟器类似。直接操作即可
uniapp 项目结构介绍
https://uniapp.dcloud.net.cn/tutorial/project.html
┌─uniCloud 云空间目录,阿里云为uniCloud-aliyun,腾讯云为uniCloud-tcb
│─components 符合vue组件规范的uni-app组件目录
│ └─comp-a.vue 可复用的a组件
├─hybrid App端存放本地html文件的目录
├─platforms 存放各平台专用页面的目录
├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
│ └─list
│ └─list.vue list页面
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─uni_modules 存放[uni_module](/uni_modules)。
├─wxcomponents 存放小程序组件的目录
├─nativeplugins App原生插件 详见
├─unpackage 非工程代码,一般存放运行或发行的编译结果
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json 配置应用名称、appid、logo、版本等打包信息
├─pages.json 配置页面路由、导航条、选项卡等页面类信息
└─uni.scss 这里是uni-app内置的常用样式变量
uniapp 开发规范介绍
为了实现多端兼容,综合考虑编译速度、运行性能等因素,uni-app
约定了如下开发规范
页面和组件文件遵循vue的规范
- 比如 新建页面
goods.vue
- 比如 新建组件
it-item.vue
内置标签使用小程序的规范
<view>小程序中的块级标签</view>
数据绑定和事件处理使用vue的规范
<template>
<view>
<view class="item" v-for="item in list" :key="item" @click="handleClick(item)">{{item}}</view>
</view>
</template>
<script>
export default {
data(){
return {
list:['a','b','c']
}
},
methods:{
handleClick(letter){
console.log(letter)
}
}
}
</script>
能力接口API 使用 微信小程序的规范
比如弹出显示框,发送网络请求等
wx.showToast({
title: '成功',
icon: 'success',
duration: 2000
})
wx.request({
url: 'example.php', //仅为示例,并非真实的接口地址
data: {
x: '',
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
}
})
考虑到跨端,我们将会使用 uniapp
统一封装的API。简称 uni api
uniapp 生命周期
uniapp中,生命周期分类三大类
- 应用生命周期 小程序规范
- 页面生命周期 小程序规范
- 组件生命周期 vue规范
应用生命周期 App.vue
函数名 | 说明 |
---|---|
onLaunch | 当uni-app 初始化完成时触发(全局只触发一次) |
onShow | 当 uni-app 启动,或从后台进入前台显示 |
onHide | 当 uni-app 从前台进入后台 |
onError | 当 uni-app 报错时触发 |
onUniNViewMessage | 对 nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯(opens new window) |
onUnhandledRejection | 对未处理的 Promise 拒绝事件监听函数(2.8.1+) |
onPageNotFound | 页面不存在监听函数 |
onThemeChange | 监听系统主题变化 |
页面生命周期
函数名 | 说明 | 平台差异说明 | 最低版本 |
---|---|---|---|
onInit | 监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad | 百度小程序 | 3.1.0+ |
onLoad | 监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例 | ||
onShow | 监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面 | ||
onReady | 监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发 | ||
onHide | 监听页面隐藏 | ||
onUnload | 监听页面卸载 | ||
onResize | 监听窗口尺寸变化 | App、微信小程序、快手小程序 | |
onPullDownRefresh | 监听用户下拉动作,一般用于下拉刷新,参考示例 | ||
onReachBottom | 页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。具体见下方注意事项 | ||
onTabItemTap | 点击 tab 时触发,参数为Object,具体见下方注意事项 | 微信小程序、QQ小程序、支付宝小程序、百度小程序、H5、App、快手小程序、京东小程序 | |
onShareAppMessage | 用户点击右上角分享 | 微信小程序、QQ小程序、支付宝小程序、字节小程序、飞书小程序、快手小程序、京东小程序 | |
onPageScroll | 监听页面滚动,参数为Object | nvue暂不支持 | |
onNavigationBarButtonTap | 监听原生标题栏按钮点击事件,参数为Object | App、H5 | |
onBackPress | 监听页面返回,返回 event = {from:backbutton、 navigateBack} ,backbutton 表示来源是左上角返回按钮或 android 返回键;navigateBack表示来源是 uni.navigateBack ;详细说明及使用:onBackPress 详解 (opens new window)。支付宝小程序只有真机能触发,只能监听非navigateBack引起的返回,不可阻止默认行为。 | app、H5、支付宝小程序 | |
onNavigationBarSearchInputChanged | 监听原生标题栏搜索输入框输入内容变化事件 | App、H5 | 1.6.0 |
onNavigationBarSearchInputConfirmed | 监听原生标题栏搜索输入框搜索事件,用户点击软键盘上的“搜索”按钮时触发。 | App、H5 | 1.6.0 |
onNavigationBarSearchInputClicked | 监听原生标题栏搜索输入框点击事件(pages.json 中的 searchInput 配置 disabled 为 true 时才会触发) | App、H5 | 1.6.0 |
onShareTimeline | 监听用户点击右上角转发到朋友圈 | 微信小程序 | 2.8.1+ |
onAddToFavorites | 监听用户点击右上角收藏 | 微信小程序 | 2.8.1+ |
组件生命周期
函数名 | 说明 | 平台差异说明 | 最低版本 |
beforeCreate | 在实例初始化之前被调用。详见(opens new window) | ||
created | 在实例创建完成后被立即调用。详见(opens new window) | ||
beforeMount | 在挂载开始之前被调用。详见(opens new window) | ||
mounted | 挂载到实例上去之后调用。详见 (opens new window)注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTick Vue官方文档(opens new window) |
||
beforeUpdate | 数据更新时调用,发生在虚拟 DOM 打补丁之前。详见(opens new window) | 仅H5平台支持 | |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。详见(opens new window) | 仅H5平台支持 | |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。详见(opens new window) | ||
destroyed | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。详见(opens new window) |
响应式单位 rpx
https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#尺寸单位
相对长度单位,功能类似于web端的 rem 和 vw,小程序首先推出,uniapp也是直接支持。一种根据屏幕宽度自适应的动态单位。以 750 宽的屏幕为基准,750rpx 恰好为屏幕宽度。
其中uniapp做了以下设置,
- 默认的设计稿宽度为
375px
因此存在1px = 2rpx
- 默认 rpx支持最大宽度为
960px
,超出则 按照 设计稿宽度375px
来设置
scoped
- vue开发的h5,单页面应用程序,每一个vue文件如果直接使用 class,多个文件的样式 冲突
- 微信小程序 真正 多页面应用程序 物理隔离,页面中使用 class,不会相互影响
- uniapp做了一个设置,写vue代码,不需要主动加上
scoped
,打包成h5端的时候自动添加上去,打包成 微信小程序端 不需要添加 scoped。
uniapp 多端开发
条件编译
条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。
写法:以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。
- #ifdef:if defined 仅在某平台存在
- #ifndef:if not defined 除了某平台均存在
- %PLATFORM%:平台名称
标签中
<!-- #ifdef %PLATFORM% -->
平台特有的组件
<!-- #endif -->
js中
// #ifdef %PLATFORM%
平台特有的API实现
// #endif
css中
/* #ifdef %PLATFORM% */
平台特有样式
/* #endif */
条件编译写法 | 说明 |
---|---|
#ifdef APP-PLUS 需条件编译的代码 #endif | 仅出现在 App 平台下的代码 |
#ifndef H5 需条件编译的代码 <br />#endif | 除了 H5 平台,其它平台均存在的代码 |
#ifdef H5 || MP-WEIXIN 需条件编译的代码 #endif | 在 H5 平台或微信小程序平台存在的代码(这里只有||,不可能出现&&,因为没有交集) |
%PLATFORM% 可取值如下:
值 | 生效条件 |
---|---|
VUE3 | HBuilderX 3.2.0+ 详情(opens new window) |
APP-PLUS-NVUE或APP-NVUE | App nvue |
MP-WEIXIN | 微信小程序 |
MP-ALIPAY | 支付宝小程序 |
MP-BAIDU | 百度小程序 |
MP-TOUTIAO | 字节跳动小程序 |
MP-LARK | 飞书小程序 |
MP-QQ | QQ小程序 |
MP-KUAISHOU | 快手小程序 |
MP-JD | 京东小程序 |
MP-360 | 360小程序 |
MP | 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/飞书小程序/QQ小程序/360小程序 |
QUICKAPP-WEBVIEW | 快应用通用(包含联盟、华为) |
QUICKAPP-WEBVIEW-UNION | 快应用联盟 |
QUICKAPP-WEBVIEW-HUAWEI | 快应用华为 |
支持的文件
- .vue
- .js
- .css
- pages.json
- 各预编译语言文件,如:.scss、.less、.stylus、.ts、.pug
static 目录的条件编译
在不同平台,引用的静态资源可能也存在差异,通过 static 的的条件编译可以解决此问题,static 目录下新建不同平台的专有目录(目录名称同 %PLATFORM%
值域,但字母均为小写),专有目录下的静态资源只有在特定平台才会编译进去。
如以下目录结构,a.png
只有在微信小程序平台才会编译进去,b.png
在所有平台都会被编译。
┌─static
│ ├─mp-weixin
│ │ └─a.png
│ └─b.png
├─main.js
├─App.vue
├─manifest.json
└─pages.json
整体目录条件编译
如果想把各平台的页面文件更彻底的分开,也可以在uni-app项目根目录创建platforms
目录,然后在下面进一步创建app-plus
、mp-weixin
等子目录,存放不同平台的文件。
注意
platforms
目录下只支持放置页面文件(即页面vue文件),如果需要对其他资源条件编译建议使用static 目录的条件编译(opens new window)
flex布局
尽量使用flex布局,因为全平台都支持
尺寸单位
- uniapp通用单位
px
,rpx
- vue页面中支持
rem
、vh
、vw
- nvue 不支持
百分比单位
css变量
CSS 变量 | 描述 | App | 小程序 | H5 |
---|---|---|---|---|
--status-bar-height | 系统状态栏高度 | 系统状态栏高度 (opens new window)、nvue 注意见下 | 25px | 0 |
--window-top | 内容区域距离顶部的距离 | 0 | 0 | NavigationBar 的高度 |
--window-bottom | 内容区域距离底部的距离 | 0 | 0 | TabBar 的高度 |
背景图片
-
支持 base64 格式图片。
-
支持网络路径图片。
-
小程序不支持在 css 中使用本地文件。需以 base64 方式方可使用。
-
使用本地路径背景图片需注意:
- 为方便开发者,在背景图片小于 40kb 时,
uni-app
编译到不支持本地背景图的平台时,会自动将其转化为 base64 格式; - 本地背景图片的引用路径推荐使用以
~@
开头的绝对路径。
.test2 { background-image: url('~@/static/logo.png'); }
- 为方便开发者,在背景图片小于 40kb 时,
uview & uni ui
uview
和 uni ui
都是 和uniapp配套的全端UI框架,可以单独使用,也可以共同使用
uview ui
由于目前 uview 2.x 版本的坑不少,因此我们拿比较稳定的 uview 1.8.6 来演示
另外,uview ui 的引入方式分为两种,主要是取决于你的项目是如何创建的:
- vue-cli
- HBuilderX
uview ui + vue-cli
-
安装依赖
npm i uview-ui@1.8.4 sass
-
在
src/main.js
文件中 全局引入 js库import uView from "uview-ui"; Vue.use(uView);
-
在 uni.scss 中 引入 uview 的 sass 主题库
@import "uview-ui/theme.scss";
-
在 App.vue 中 引入 uview 的 sass 主题库
<style lang="scss"> @import "uview-ui/index.scss"; </style>
-
pages.json 中 配置 easycom
{ "easycom": { "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" }, // 此为本身已有的内容 "pages": [ // ...... ] }
-
在 页面中 使用 uview的按钮
<u-button >默认按钮</u-button> <u-button type="primary">主要按钮</u-button> <u-button type="success">成功按钮</u-button> <u-button type="info">信息按钮</u-button> <u-button type="warning">警告按钮</u-button> <u-button type="error">危险按钮</u-button>
-
成功
uview ui + HBuilderX
-
点击导入插件
-
导入成功,可以看到目录下多了
components
文件夹 -
接着在
uni.scss
中导入 uview的主题样式文件theme.scss
@import './theme.scss';
-
在页面上添加测试代码
<u-button >默认按钮</u-button> <u-button type="primary">主要按钮</u-button> <u-button type="success">成功按钮</u-button> <u-button type="info">信息按钮</u-button> <u-button type="warning">警告按钮</u-button> <u-button type="error">危险按钮</u-button>
-
点击运行
-
成功
uni ui
uni ui + vue-cli
-
安装相关依赖
如果
node
版本小于 16 ,sass-loader 请使用低于 @11.0.0 的版本如果
node
版本大于 16 ,sass-loader
建议使用v8.x
版本npm i sass sass-loader@10.1.1 @dcloudio/uni-ui
-
配置easycom
在
pages.json
中进行配置{ "easycom": { "autoscan": true, "custom": { // uni-ui 规则如下配置 "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" } }, // 其他内容 pages:[ // ... ] }
-
组件中使用
<uni-title title="上报统计数据"></uni-title> <uni-title type="h1" title="h1 一级标题 "></uni-title> <uni-title type="h1" title="h1 一级标题" color="#027fff"></uni-title> <uni-title type="h2" title="h2 居中" align="center"></uni-title>
uni ui + HBuilderX
-
使用 HBuilderX 导入插件
-
项目中会多
uni_modules
文件夹 -
页面中导入代码
<uni-title title="上报统计数据"></uni-title> <uni-title type="h1" title="h1 一级标题 "></uni-title> <uni-title type="h1" title="h1 一级标题" color="#027fff"></uni-title> <uni-title type="h2" title="h2 居中" align="center"></uni-title>
-
观察效果
uniapp 发布到多端
H5
普通发布
项目开发完成后,可以在 HBuilderX中来 打包成 H5项目
-
打包成H5
-
填写信息
-
此时,生成的h5项目会放在
unpackage\dist\build\h5
发布到uniapp的云环境
需要提前开通 uniCloud 的云服务
微信小程序
在HBuilderX中,想要发布微信小程序,有两种方式
普通发布
-
点击发行微信小程序
-
填写 appid
-
此时会自动打开 微信开发者工具,然后点击上传即可
-
然后填写 版本信息即可
-
成功后,回到微信开发者后台,打开 管理 版本管理 手动点击 提交审核。等待审核
HBuilderX 发布
其实还可以利用 HBuilderX 直接发布,不用打开微信开发者工具
-
首先登录你的微信开发者后台
-
打开 开发管理 小程序代码上传密钥 重置
-
下载 小程序代码上传密钥
-
在企业开发中,记得要开启 IP白名单,降低风险
-
选择上传密钥
-
上传成功
App
在HBuilderX中发布App的方式分为两种
- 本地离线打包
- 云打包
云打包
云打包的意思是利用 dcloud提供的能力,将你本地代码上传到 dcloud 服务器上,在云上打包完成再下载回本地。
-
打开
mainifest.json
设置 uniapp 应用标识AppID
这个是uniapp应用的id,不是微信小程序的id -
设置 App 支持CPU类型
App常用其他设置 支持CPU类型
-
设置使用原生隐私正则提示框
-
发行 - 原生App - 云打包
-
填写相关信息
-
打包成功
uniapp 其他资源
-
uniapp优秀案例源码地址
-
uniapp 官方交流QQ群