
win7安装微信开发者工具后打不开,卡死的现象
今天想研究下uniapp发布成微信小程序的方法,于是从微信的网站上下载了微信开发者工具:
https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
下载64位稳定版安装,打开,界面处理卡死的状态,一片黑。
搜索了一下下面的解决方案:
https://developers.weixin.qq.com/community/develop/doc/000a02d19141b0ddc11aa05d55b800?jumpto=comment&parent_commentid=0006a84c0b4968411e2a02e85510&commentid=0006a84c0b4968411e2a02e85510
将这个文件设置为下面贴的代码试试呢 ~/AppData/Local/微信开发者工具/User Data/localstorage_b72da75d79277d2f5f9c30c9177be57e.json
{
"show": false,
"currentCategory": "general",
"compiler": {
"clusterCompile": false,
"autoPreview": false,
"autoRemoteDebug": false
},
"general": {
"openLastModifiedProject": true,
"autoPreviewType": "mobile",
"autoRemoteDebugType": "mobile",
"maxLogLength": 300,
"enableNewFW": true,
"enableGPU": false,
"ignoreUnsafeProxy": false,
"locale": "zh",
"defaultWorkspace": "/Users/kunlideng/WeChatProjects"
},
"appearance": {
"theme": "dark",
"devtoolsTheme": "dark",
"fontFamily": "SF Mono",
"fontSize": 12,
"lineHeight": 20,
"simulatorAlignment": "left"
},
"edit": {
"tabSize": 2,
"insertSpaces": true,
"wrap": "on",
"minimap": false,
"gitIgnoreWindowsReturn": true,
"autoTypingsDetectEnabled": true,
"alwaysOpenFileInNewTab": false,
"autoSave": false,
"autoRefresh": false,
"saveBeforeCompile": false,
"saveBeforePreview": false,
"saveBeforeUpload": false
},
"proxy": {
"proxyType": "SYSTEM",
"proxyHost": "127.0.0.1",
"proxyPort": "12639"
},
"notification": {
"bbs": true,
"sys": true,
"alarm": true
},
"security": {
"enableServicePort": true,
"port": 19195
},
"geo": {
"enabled": false,
"latitude": 39.92,
"longitude": 116.46,
"speed": -1,
"accuracy": 65,
"altitude": 0,
"verticalAccuracy": 65,
"horizontalAccuracy": 65
},
"shortcuts": {
"_editingShortcuts": false,
"toggleToolbar": {
"modifiers": ["cmd", "shift"],
"key": "T"
},
"toggleSimulatorWindow": {
"modifiers": ["cmd", "alt"],
"key": "S"
},
"toggleEditorWindow": {
"modifiers": ["cmd", "shift"],
"key": "E"
},
"toggleFileTree": {
"modifiers": ["cmd", "shift"],
"key": "M"
},
"toggleDebugWindow": {
"key": "I",
"modifiers": ["cmd", "shift"]
},
"rebuild": {
"key": "B",
"modifiers": ["cmd"]
},
"format": {
"key": "F",
"modifiers": ["shift", "alt"]
},
"refresh": {
"key": "R",
"modifiers": ["cmd"]
},
"toggleForegroundBackgroundStatus": {
"key": "",
"modifiers": []
},
"documentationSearch": {
"key": "",
"modifiers": []
},
"gotoFile": {
"key": "P",
"modifiers": ["cmd"]
},
"gotoRecentFile": {
"key": "E",
"modifiers": ["cmd"]
},
"preview": {
"key": "P",
"modifiers": ["shift", "cmd"]
},
"upload": {
"key": "U",
"modifiers": ["shift", "cmd"]
}
},
"syncTime": 1584263702017
}
经试验有效,但处理时并未找到上述方法给出的目录与文件,后来找到类似的目录:

目录下也并没有方案中所说的文件,于是自己新建了一个方案中的文件,并把上述代码拷贝到文件中,结果微信开发者工具正常启动了。
今天想研究下uniapp发布成微信小程序的方法,于是从微信的网站上下载了微信开发者工具:
https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
下载64位稳定版安装,打开,界面处理卡死的状态,一片黑。
搜索了一下下面的解决方案:
https://developers.weixin.qq.com/community/develop/doc/000a02d19141b0ddc11aa05d55b800?jumpto=comment&parent_commentid=0006a84c0b4968411e2a02e85510&commentid=0006a84c0b4968411e2a02e85510
将这个文件设置为下面贴的代码试试呢 ~/AppData/Local/微信开发者工具/User Data/localstorage_b72da75d79277d2f5f9c30c9177be57e.json
{
"show": false,
"currentCategory": "general",
"compiler": {
"clusterCompile": false,
"autoPreview": false,
"autoRemoteDebug": false
},
"general": {
"openLastModifiedProject": true,
"autoPreviewType": "mobile",
"autoRemoteDebugType": "mobile",
"maxLogLength": 300,
"enableNewFW": true,
"enableGPU": false,
"ignoreUnsafeProxy": false,
"locale": "zh",
"defaultWorkspace": "/Users/kunlideng/WeChatProjects"
},
"appearance": {
"theme": "dark",
"devtoolsTheme": "dark",
"fontFamily": "SF Mono",
"fontSize": 12,
"lineHeight": 20,
"simulatorAlignment": "left"
},
"edit": {
"tabSize": 2,
"insertSpaces": true,
"wrap": "on",
"minimap": false,
"gitIgnoreWindowsReturn": true,
"autoTypingsDetectEnabled": true,
"alwaysOpenFileInNewTab": false,
"autoSave": false,
"autoRefresh": false,
"saveBeforeCompile": false,
"saveBeforePreview": false,
"saveBeforeUpload": false
},
"proxy": {
"proxyType": "SYSTEM",
"proxyHost": "127.0.0.1",
"proxyPort": "12639"
},
"notification": {
"bbs": true,
"sys": true,
"alarm": true
},
"security": {
"enableServicePort": true,
"port": 19195
},
"geo": {
"enabled": false,
"latitude": 39.92,
"longitude": 116.46,
"speed": -1,
"accuracy": 65,
"altitude": 0,
"verticalAccuracy": 65,
"horizontalAccuracy": 65
},
"shortcuts": {
"_editingShortcuts": false,
"toggleToolbar": {
"modifiers": ["cmd", "shift"],
"key": "T"
},
"toggleSimulatorWindow": {
"modifiers": ["cmd", "alt"],
"key": "S"
},
"toggleEditorWindow": {
"modifiers": ["cmd", "shift"],
"key": "E"
},
"toggleFileTree": {
"modifiers": ["cmd", "shift"],
"key": "M"
},
"toggleDebugWindow": {
"key": "I",
"modifiers": ["cmd", "shift"]
},
"rebuild": {
"key": "B",
"modifiers": ["cmd"]
},
"format": {
"key": "F",
"modifiers": ["shift", "alt"]
},
"refresh": {
"key": "R",
"modifiers": ["cmd"]
},
"toggleForegroundBackgroundStatus": {
"key": "",
"modifiers": []
},
"documentationSearch": {
"key": "",
"modifiers": []
},
"gotoFile": {
"key": "P",
"modifiers": ["cmd"]
},
"gotoRecentFile": {
"key": "E",
"modifiers": ["cmd"]
},
"preview": {
"key": "P",
"modifiers": ["shift", "cmd"]
},
"upload": {
"key": "U",
"modifiers": ["shift", "cmd"]
}
},
"syncTime": 1584263702017
}
经试验有效,但处理时并未找到上述方法给出的目录与文件,后来找到类似的目录:
目录下也并没有方案中所说的文件,于是自己新建了一个方案中的文件,并把上述代码拷贝到文件中,结果微信开发者工具正常启动了。
收起阅读 »
关于后退刷新上一页数据的想法
1.由mui.openWindow打开的webview,会被缓存起来.所以可以根据plus.webview.getWebviewById获取到上一页的webview对象
2.WebviewObject这个对应可以监听webview的事件,事件类型是WebviewEvent.
想法:为上一页的窗口监听事件,在当前页需要刷新上一页时,拿到对应webview.触发监听事件中写好的回调事件.
笨方法:监听器 + localStorage
以上均为想法,未测试
1.由mui.openWindow打开的webview,会被缓存起来.所以可以根据plus.webview.getWebviewById获取到上一页的webview对象
2.WebviewObject这个对应可以监听webview的事件,事件类型是WebviewEvent.
想法:为上一页的窗口监听事件,在当前页需要刷新上一页时,拿到对应webview.触发监听事件中写好的回调事件.
笨方法:监听器 + localStorage
以上均为想法,未测试
收起阅读 »
【UNIAPP坑与解决】VUE组件内部$getAppWebview出现Cannot read property '$getAppWebview' of undefined
开发当中,封装了一个vue组件,内部有webview。
为了获取这个webview的对象,又需要获取当前页面对象,于是查阅文档采用
this.$scope.$getAppWebview()
但运行时反复出现
Cannot read property '$getAppWebview' of undefined
起初以为是自己拼写和上下文的问题,查阅社区也没有相关讨论。
后来反复验证,发现vue组件内部不能调用这个方法,页面当中正常。
于是后来通过以下方式实现:
this.$parent.$scope.$getAppWebview();
不知道是不是BUG,但先发一篇文章,记录一下,万一有人遇到了呢
开发当中,封装了一个vue组件,内部有webview。
为了获取这个webview的对象,又需要获取当前页面对象,于是查阅文档采用
this.$scope.$getAppWebview()
但运行时反复出现
Cannot read property '$getAppWebview' of undefined
起初以为是自己拼写和上下文的问题,查阅社区也没有相关讨论。
后来反复验证,发现vue组件内部不能调用这个方法,页面当中正常。
于是后来通过以下方式实现:
this.$parent.$scope.$getAppWebview();
不知道是不是BUG,但先发一篇文章,记录一下,万一有人遇到了呢
收起阅读 »
Error in render: "TypeError: Cannot read property 'category_name' of undefined"
错误提示
[Vue warn]: Error in render: "TypeError: Cannot read property 'category_name' of undefined"
问题场景
<picker @change="bindPickerChange" :value="index" :range="category" range-key="category_name">
{{category[index].category_name}}
</picker>
这里category初始值是 category:[]
需要在给初始值时,写成
category:[
{category_id:"",category_name:''}
],
使用 {{category[index].category_name}} 时就不会出错了
错误提示
[Vue warn]: Error in render: "TypeError: Cannot read property 'category_name' of undefined"
问题场景
<picker @change="bindPickerChange" :value="index" :range="category" range-key="category_name">
{{category[index].category_name}}
</picker>
这里category初始值是 category:[]
需要在给初始值时,写成
category:[
{category_id:"",category_name:''}
],
使用 {{category[index].category_name}} 时就不会出错了
收起阅读 »
Expected String with value "1", got Number with value 1 解决办法
提示错误:
[Vue warn]: Invalid prop: type check failed for prop "value". Expected String with value "1", got Number with value 1.
错误原因:
这里的value值,需要的是String类型,而我不小心给的是Number类型的值
解决办法:
平时从接口获取到的ID值通常是Number类型,要对类型进行强转。
Vue里 Number转String 类型 使用 value = String(Number)
进行强转后,就不会提示错误了。
for (var i = 0, lenI = this.category.length; i < lenI; ++i) {
this.category[i].category_id = String(this.category[i].category_id)
}
提示错误:
[Vue warn]: Invalid prop: type check failed for prop "value". Expected String with value "1", got Number with value 1.
错误原因:
这里的value值,需要的是String类型,而我不小心给的是Number类型的值
解决办法:
平时从接口获取到的ID值通常是Number类型,要对类型进行强转。
Vue里 Number转String 类型 使用 value = String(Number)
进行强转后,就不会提示错误了。
for (var i = 0, lenI = this.category.length; i < lenI; ++i) {
this.category[i].category_id = String(this.category[i].category_id)
}
收起阅读 »

uni-app开发app项目(非nvue),怎么使用高德地图?
uni-app开发app项目,不是nvue的app:
1、怎么使用高德地图?
2、只能是SDK的方式吗?
uni-app开发app项目,不是nvue的app:
1、怎么使用高德地图?
2、只能是SDK的方式吗?

BaseCloud - 云开发全栈快速开发框架
项目简介
BaseCloud是一套基于uniapp、uniCloud、uni-id的全栈开发框架,不依赖任何第三方框架,极度精简轻巧。
在开发前端界面时,除了适配移动端外,它对PC端也做了良好的适配;
在开发云函数时,它可以为您提供拦截器配置、路由管理、分页、列表、单数据快速查询等功能。除此之外,对于一些业务开发中的常用函数也已做好封装,拿来即用。
在BaseCloud的初始化项目模板中,为您实现了贯穿前后端的业务模块:管理员登录、用户管理、菜单管理、角色与权限管理、操作日志、系统参数配置等项目通用的基础后台管理功能,这一切全都基于云函数开发。
项目价值
基于BaseCloud的快速开发UI样式库,可以快速拼装前端界面,高还原度实现设计图效果,兼顾高效与灵活。
基于BaseCloud的云函数公用模块,你可以轻松实现单云函数、多云函数的路由管理、请求拦截管理与权限控制、常用业务函数快速开发。
基于BaseCloud的客户端缓存管理机制,你可以大幅度减少应用的云函数重复调用请求,未来云函数开始计费后,至少节省应用50%的流量费用。
基于BaseCloud的管理后台项目模板,你可以快速初始化一套自带用户、菜单、角色、权限、操作日志、系统参数管理的管理后台项目,在此基础上开始你的项目开发。
当然,这一切都只是刚刚开始,未来我们会基于BaseCloud推出更多贯穿前后端的业务模板,只要您的项目是基于BaseCloud框架,所有的业务模板拿来即用,5分钟快速集成到项目内,无需重复开发前端和后端。
对于开发者而言,基于BaseCloud的全栈快速开发框架,你可以封装自己的贯穿前后端的业务模块,发布到付费业务模块插件市场。
BaseCloud项目构成
common>base-cloud.scss
基础样式库,适配移动端和PC端,22kb。common>js>base-cloud-client.js
客户端SDK,14.2kb。cloudfunctions>common>base-cloud
云函数公共模块,13.9kb。components
PC端常用业务组件目录
项目截图
演示项目地址:https://base-cloud.joiny.cn <<
账号:admin
密码:123123123
快速开始
- 请先下载BaseCloud管理后台项目模板,并导入到Hbuilder中
- 右键点击cloudfunctions目录,选择一个服务空间,支持阿里云、腾讯云。
- 找到cloudfunctions目录下的db_init.json数据库初始化文件,右键选择“初始化数据库”。
- 右键点击cloudfunctions目录,选择上传所有云函数以及公共模块。
- 点击运行到浏览器,运行成功后,在浏览器中进入登录页,初始账号:admin ,初始密码:123123123
使用过程中如有问题或建议,请移步gitee提交issue。
项目结构介绍
请务必对照仔细浏览项目目录介绍,您阅读本项目的文档将会事半功倍。
服务端项目目录
├── cloudfunctions───────────# 云函数目录
│ └── admin──────────────────# 管理后台业务函数
│ └── controller──────────────────# 管理后台业务函数根目录
│ └── menu.js────────────────────────# 菜单管理业务函数
│ └── operateLog.js──────────────────# 操作日志业务函数与接口
│ └── paramConfig.js─────────────────# 系统参数配置业务函数
│ └── role.js────────────────────────# 角色管理业务函数
│ └── user.js────────────────────────# 用户管理业务函数
│ └── node_modules──────────────────# admin函数依赖公共模块
│ └── index.js──────────────────────# admin函数入口文件
│ └── api────────────────────# uni-id官方公共模块
│ └── clearlogs──────────────# 过期操作日志清理定时任务函数
│ └── common─────────────────# 公共模块
│ └── base-cloud──────────────────# base-cloud公共模块
│ └── intercepters──────────────────# 拦截器函数目录
│ └── authInter.js──────────────────# 用户权限拦截拦截函数
│ └── config.js─────────────────────# 公共模块配置文件,注册全局拦截器(重要!)
│ └── index.js──────────────────────# BaseCloud公共模块源码,开发阶段无需关心
│ └── db_init.json───────────# 数据库初始化文件,包含数据表和初始化数据
客户端项目目录
├── cloudfunctions────────# 云函数目录...
├── common────────────────# 静态资源文件目录
│ └── js──────────────────# js文件目录
│ └── base-cloud-client.js─────────────────# BaseCloud客户端SDK
│ └── clipBoard.js─────────────────────────# 支持web端复制API
│ └── md5.js───────────────────────────────# MD5加密函数,用于密码加密传输,客户端数据缓存等场景
│ └── base.scss────────────────────# BaseCloud样式类库入口文件
│ └── base-font.scss───────────────# BaseCloud图标样式文件
│ └── base-mobile.scss─────────────# BaseCloud移动端样式文件
│ └── base-pc.scss─────────────────# BaseCloud适配PC端样式文件
├── pages────────────────# 页面
├── static───────────────# 图片静态资源文件目录
├── uni.scss─────────────# scss变量配置文件
管理后台业务模块云函数目录结构
├── cloudfunctions─────────────────# 云函数目录
│ └── admin──────────────────# 管理后台业务函数
│ └── controller──────────────────# 管理后台业务函数根目录
│ └── menu.js────────────────────────# 菜单管理业务函数
│ └── getParentList()──────────────────# 查询上级菜单列表接口
│ └── globalData()─────────────────────# 查询登录用户信息、权限菜单列表接口
│ └── info()───────────────────────────# 查询菜单信息接口
│ └── save()───────────────────────────# 保存、更新菜单信息接口
│ └── delete()─────────────────────────# 删除菜单信息接口
│ └── list()───────────────────────────# 菜单列表查询接口
│ └── operateLog.js──────────────────# 操作日志业务函数与接口
│ └── paramConfig.js─────────────────# 系统参数配置业务函数
│ └── info()───────────────────────────# 查询参数配置项信息接口
│ └── save()───────────────────────────# 保存、更新参数配置项信息接口
│ └── delete()─────────────────────────# 删除参数配置项接口
│ └── list()───────────────────────────# 参数配置项列表查询接口
│ └── role.js────────────────────────# 角色管理业务函数
│ └── info()───────────────────────────# 查询角色信息接口
│ └── save()───────────────────────────# 保存、更新角色信息接口
│ └── delete()─────────────────────────# 删除角色接口
│ └── list()───────────────────────────# 角色列表查询接口
│ └── options()────────────────────────# 角色选项列表查询接口(供用户角色选择时使用)
│ └── user.js────────────────────────# 用户管理业务函数
│ └── login()──────────────────────────# 登录接口
│ └── checkToken()─────────────────────# token验证接口
│ └── logout()─────────────────────────# 退出登录接口
│ └── changeStatus()───────────────────# 切换用户禁用状态接口
│ └── info()───────────────────────────# 用户信息查询接口
│ └── save()───────────────────────────# 保存、更新用户信息接口
│ └── myInfo()─────────────────────────# 当前用户信息接口
│ └── modify()─────────────────────────# 修改当前用户信息(含密码)接口
│ └── list()───────────────────────────# 用户列表查询接口
│ └── delete()─────────────────────────# 删除用户接口
│ └── node_modules──────────────────# admin函数依赖公共模块
│ └── index.js──────────────────────# admin函数入口文件
=====================================================================
服务端公共模块使用说明文档
【使用公共模块来接管云函数,定义多个访问路径】
- 根据公共模块引入说明来引入base-cloud公共模块;
- 在云函数的入口文件
index.js
中引入公共模块,并接管云函数。
'use strict';
const BaseCloud = require("base-cloud");
exports.main = async ( event , ctx ) => {
var fnName = "admin" ; //当前云函数的名称
var controlerDir = `${__dirname}/controller` ; //存放业务函数根目录的绝对路径
return await new BaseCloud({ event, ctx , fnName }).invoke(controlerDir);
};
- 在云函数中指定的业务函数根目录(此处是controller,你也可以指定其他的目录),创建js文件。
并通过module.exports
来导出业务处理的函数,可以导出一个或多个。 - 客户端访问云函数的路径规则为:云函数名称/根目录下的js函数文件名称/js函数文件导出的函数名称;
如果js函数文件直接导出的是一个函数,则路径规则为:云函数名称/根目录下的js函数文件名称。
如下示例为在admin云函数中的controller>operateLog.js
文件中导出一个函数:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(res){
var {pageNumber , pageSize} = this.params ;
var page = await this.paginate({
pageNumber , pageSize ,
collection : OperateLog ,
eq : ["actionName","userName"],
like : ["name"],
orderBy : "createTime desc"
});
var list = page.list ;
list.forEach(item=>{
item.createTime = this.DateKit.toStr( item.createTime ,'seconds');
});
return {page};
};
此时客户端访问路径为: admin/operateLog
,客户端调用示例如下:
this.bcc.call({
url : "admin/operateLog" ,
data : {pageNumber: 1 , pageSize : 20},
success : e=>{}
});
如下示例为在admin云函数中的controller>role.js
文件中导出多个函数:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const Role = db.collection("t_role");
module.exports = {
info : async function(e){
var id = this.params.id ;
var typeList = TYPE_LIST ;
if (!id) {
return { typeList };
}
var data = this.findFirst( await Role.doc(id).get() );
return { data , typeList };
},
save : async function(e){
var data = this.getModel();
if (data.menuIds) {
data.menuIds = data.menuIds.split(',');
}
if (!data._id) {
data.createTime = this.DateKit.now();
await Role.add(data);
return this.ok();
}
data.updateTime = this.DateKit.now() ;
await this.updateById(Role , data);
return this.ok();
}
};
此时,通过admin/role/info
和 admin/role/save
两个路径可以分别访问 controller>role.js>info()
和controller>role.js>save()
,客户端调用示例如下:
this.bcc.call({
url : "admin/role/info" ,
data : {_id : 1},
success : e=>{}
});
【使用公共模块来配置全局的拦截器,以及拦截器的清理】
- 在
cloudfunctions > common > base-cloud
目录下,找到config.js
文件, - 如下代码所示,在inters中配置了两个拦截器,你可以直接在此处定义拦截器函数(如loginInter),也可以通过文件引入的方式来定义拦截器(如authInter),具体的使用说明,请看注释:
//通过文件来引入拦截器函数
const authInter = require("./intercepters/authInter") ;
module.exports = {
isDebug : true , //会输出一些日志到控制台,方便调试
inters:{ //配置全局拦截器
loginInter: { //直接在此处定义拦截器函数
handle : [] , //拦截的路径,此处留空表示拦截全部的路径
clear : [ //配置要清除拦截器的路径,注意:如果配置了handle则此处的配置无效。
"admin/user/login", //支持字符串、也支持正则表达式(详见示例项目中的authInter的配置规则)
"admin/user/checkToken",
] ,
invoke:async function(attrs){//拦截器函数,入参为上一个拦截器通过setAttr方法传递的所有的键值对
const {event , ctx , uniID } = this ;
var res = await uniID.checkToken(event.uniIdToken);
if(res.code){
return {
state : 'needLogin',
msg : "请登录"
};
}
//将user传入下一个拦截器,在拦截器函数的入参中可以获取到,也可以通过this.getAttr("user")来取到该值。
this.setAttr({user : res.userInfo});
//当前拦截器放行,不调用这个,拦截器不会放行,此次请求到此终止
this.next();
}
},
authInter ,
}
}
也就是说,你可以把你所有要配置的拦截器放到 config.js > inters
中去,每个拦截器注册时,在 handle
属性中定义要拦截的路径,路径支持正则表达式;
在clear
属性中,定义要清理拦截器的路径;使用invoke
属性来定义拦截器的函数。特别注意:如果在handle
中定义了拦截的路径,则clear
中的配置会被忽略。
在拦截器函数中,接收的参数为一个json,为所有拦截器通过this.setAttr(key , value)
方法存入的键值对。
在拦截器中,可以使用this.setAttr(key , value)
方法,将当前拦截器中的变量传递到下一个拦截器或者业务函数。
在下一个拦截器或业务函数的入参中可以接收,也可以使用this.getAttr(key)
方法,来取到指定的值。
如果拦截器拦截成功,不再继续执行,直接返回响应结果即可。如果拦截器放行,则需要主动调用this.next()
方法,来放行本次拦截。
公共模块配置的拦截器对所有引入base-cloud公共模块,并由base-cloud接管的云函数都有效,请合理配置拦截与清理拦截的规则。
修改公共模块后,除了上传公共模块,也需要上传依赖公共模块的云函数哦~
【在业务函数中可以使用的变量】
还是以一个云函数中的业务函数为例:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(attrs){ //此处的attr是所有拦截器中通过 this.setAttr(key,value)方法存入的键值对
var user = attrs.user ; //在loginInter拦截器中存入的user变量
var ctx = this.ctx ; //上下文,为入口函数的入参context
var event = this.event ; //为入口函数的入参event,本次请求云函数携带的event参数
var params = this.params ; //本次请求客户端通过data或url传递所有的参数
var fullPath = this.fullPath ; //本次请求的路径,如: admin/user/info
var actionName = this.action ; //本次请求的action,不含云函数名称,如: user/info
var fnName = this.fnName ; //本次请求的云函数的名称
var token = this.token ; //本次请求携带的token
var uniID = this.uniID ; //依赖的uniID模块,可以直接使用uniID的API
};
【在业务函数中可以使用的方法】
this.getModel(prefix , keepKeys) ;
- 第一个参数为
prefix
参数,指定要获取的参数的前缀符,未指定时,默认为x
。 - 第二个参数为
keepKeys
,指定一个或多个键名进行接收,多个使用英文逗号分开,如未指定则接收所有带有指定前缀的参数。
举个例子,比如用户修改个人信息的功能,在客户端传参示例:
this.bcc.call({
url : "admin/user/modify" ,
data : {
"x.password" : "123123123" ,
"x.mobile" : "15688585858" ,
"x.realAuth.contact_name" : "王大成" ,
"x.username" : "想改个名字能改吗" ,
"remark" : "个人的喜好"
}
})
此时我们需要只接收带x.
前缀的参数,统一存放到data
变量中,以便直接更新用户表的数据。
更新时,我们假设只允许用户修改password
和mobile
字段,不允许修改username
字段,
那么使用 this.getModel()
方法可以获取到符合我们条件的参数。
this.keep( jsonData , keepKeys) ;
保留jsonData中指定的键值对,用法同上。
module.exports = async function(e){
var data = this.getModel("x" , "mobile,password,realAuth");
};
this.findFirst(dataInDB);
从数据库返回的结果中获取一条数据,如果没有数据则返回null
var user = this.findFirst( await User.doc(id).get() );
if(null == user){
return {} ;
}
var {username , mobile} = user ;
//...
this.find(dataInDB);
从数据库返回的结果中获取列表数据,如果没有数据则返回 []
;
var dataInDB = await Role.orderBy("createTime","asc").get() ;
var list = this.find( dataInDB );
if(list.length == 0){
//..do something
}
async this.updateById( collection , updateData ) ;
根据主键_id来更新一条数据,updateData
中包含_id
字段和要更新的字段
var Role = uniCloud.database().collection("t_role") ;
await this.updateById( Role , data);
async this.paginate({ collection , where = {} , field = {} , orderBy , eq , like , pageNumber = 1, pageSize = 10 });
使用数据库普通查询方法(暂不支持聚合查询)来获取分页数据。参数说明见下方的示例代码中的注释:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(res){
var {pageNumber , pageSize} = this.params ;
var page = await this.paginate({
pageNumber , //分页页码,不传入默认为1
pageSize , //每页数据条数,不传入默认为10
collection : OperateLog , //要查询数据的集合对象
field:{ userName : true }, //指定返回字段,具体用法参见官方文档
where:{},//自定义的固定查询条件
eq : ["actionName","userName"], //筛选的相等条件,如果请求参数中该参数不为空则进行相等条件筛选
like : ["name"],//筛选的模糊查询条件,如果请求参数中该参数不为空则进行模糊查询筛选
orderBy : "createTime desc"
});
var list = page.list ;
list.forEach(item=>{
item.createTime = this.DateKit.toStr( item.createTime ,'seconds');
});
return {page};
};
分页查询方法返回的数据结构如下:
page: {
pageNumber: 1,//页码
lastPage: true,//是否最后一页
totalPage: 1,//总页码
list: [], //当前页数据
totalRow: 0, //总数据条数
pageSize: 10 //每页数据条数
}
this.ok(msg);
返回请求成功的响应结果,msg
不传入时,默认提示信息为:
{
state : "ok" ,
msg : "操作成功"
}
this.fail(msg, state)
返回请求成功的响应结果,msg
不传入时,默认提示信息为:
{
state : "fail" ,
msg : "系统异常,请稍后再试"
}
this.isRepeat( dataInDB , _id );
做保存更新一体的业务接口时,我们经常会判断某个字段是否已在数据库中存在值。
先根据该字段查询数据中的一条数据,然后使用该方法,来快速判断是否有重复的值。
如下为保存更新用户数据接口代码示例:
var data = this.getModel();
var {username , password , _id , roleIds , mobile } = data ;
var sameNameUser = this.findFirst(await User.where({username}).limit(1).get());
if( this.isRepeat(sameNameUser , _id) ){
return this.fail("用户名已存在");
}
async this.setMaxOrderNum(data, collection, where);
适用于具有orderNum
字段的数据表,自动生成最大的orderNum
的业务场景。
data
参数为即将要保存、更新的json数据,必填collection
为要更新的集合对象,必填where
为限定排序的条件,可选项
var Menu = uniCloud.database().collection("t_menu");
var data = this.getModel();
await this.setMaxOrderNum(data , Menu , {parentId : data.parentId } );
console.log(data.orderNum) ; //输出最大的orderNum
this.log(...arguments);
方便调试的方法,如果在 base-cloud > config.js
中配置了 isDebug
参数为 true
,使用该方法时,可以输出日志,否则不输出日志。
this.isNull(obj);
判断是否为空
this.isObject(obj);
判断是否为json对象
this.isEmptyObject(obj);
判断是否值为{}的json对象,不含有任何键值对的json
this.isFn(fn);
判断是否为函数
this.isNumber(number);
判断是否为数字
this.isArray(array);
判断是否为数组
this.isString(string);
判断是否为字符串
this.isDate(date);
判断是否为日期类型
this.isReg(reg);
判断是否为正则表达式
this.Datekit.now()
uniCloud默认是0时区的时间,使用该方法可以获取东八区当前时间的时间戳,符合国内的习惯。
this.DateKit.addMinutes(minutes , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定分钟数量,返回时间戳。
this.DateKit.addHours(hours , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定小时数量,返回时间戳。
this.DateKit.addDays(days , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定天数,返回时间戳。
this.DateKit.addMonths(months , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定月份,返回时间戳。
this.Datekit.toStr( timestamp , fileds );
传入一个时间戳时间,格式化为字符串,fileds
来指定格式化的时间精度,支持:second、minute、hour、day、month、year,不传默认为minute
this.DateKit.friendlyDate(timestamp);
传入一个时间戳时间,返回距离东八区当前时间的有多少天、时、分、秒。如:3天前、24分钟后
【服务端响应结果的约定】
一般操作类的接口,服务端会返回如下结果:
{
state : 'ok' ,
msg : "操作成功"
}
数据查询类的,正常情况下,服务端不再返回state、msg字段,直接返回要查询的数据结果,如:
{
list : [],
data :{}
}
state | 说明 |
---|---|
ok | 请求成功 |
fail | 请求失败,失败时需要返回msg字段,作为失败的说明信息 |
noAuth | 无操作权限 |
needLogin | 需要登录 |
... | 其他特殊场景下的状态描述 |
=====================================================================
客户端SDK使用说明文档
使用客户端sdk前,请确保已按如下方式,在 main.js
中注册全局对象。
import Vue from 'vue'
import App from './App'
import bcc from "./common/js/base-cloud-client.js" //引入客户端sdk文件
Vue.prototype.bcc = bcc ; //注册为全局对象
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
【调用云函数】
var data = e.detail.value ;
this.bcc.call({
url : 'admin/user/save' , //请求路径,直接以函数名称开头,开头不要加/,后面跟着路径
data : data , //请求参数
success : res => {
//当服务端返回state == 'ok' 或无state字段时,进入success回调
},
fail : res => {
//当服务端返回state == 'fail' 时,进入fail回调;
//如未定义fail回调,则默认提示服务端返回的msg字段
},
complete : res => {
//请求完成后的回调
},
});
【通过客户端本地缓存调用云函数】
本地没有缓存则请求云函数数据并将请求到的数据缓存至本地,否则直接使用本地缓存数据。
var data = e.detail.value ;
this.bcc.callInCache({
url : 'admin/user/list' , //请求路径,直接以函数名称开头,开头不要加/,后面跟着路径
data : data , //请求参数
success : res => {
//当服务端返回state == 'ok' 或无state字段时,进入success回调,并且将数据缓存至本地
},
fail : res => {
//当服务端返回state == 'fail' 时,进入fail回调;
//如未定义fail回调,则默认提示服务端返回的msg字段
},
complete : res => {
//请求完成后的回调
},
});
【主动清理客户端本地缓存】
当本地数据已经发生变更时,需要主动清理本地缓存数据,下次去请求最新的数据。
清理缓存时,将直接清理所有指定请求地址下的缓存(一个请求地址下,因参数不同可能会有多个本地缓存数据)。
应用场景示例:将用户列表数据存入本地缓存,当编辑用户信息、删除用户数据、更改用户状态等三个操作发生时。直接清理本地的用户列表数据缓存:
//传入要清理缓存的请求地址的路径
this.bcc.clearCache("admin/user/list");
【表单数据校验】
表单校验无须配置各种校验规则,直接将校验规则写入name即可。
表单的name一共分成四个部分,用|符号分割:
- 要传入服务端的name
- 表单的标题,如果标题为空表示该表单可以为空。标题内可以含有:请输入、请上传、请选择 这三类提示文字。
- 校验规则,目前支持:mobile、email、idcard、count(整数)、amount(金额)、字符长度与长度范围等六种常见的表单验证和非空验证。
- empty :表示可以为空,如果有值则进行校验,无值则放行。
示例代码:
/* 要传入服务端的name为:x.name ,角色名称不可为空,校验规则为:字符长度2~20之间。 */
<inputs name="x.name|角色名称|2~20" title="角色名称" :value="data.name"></inputs>
/* 要传入服务端的name为:x.type ,该字段可以为空 */
<radios title="类型" name="x.type" :list="typeList" :value="data.type"></radios>
/* 要传入服务端的name为:x.remark ,该字段可以为空, 如果有值时,校验字符长度在2~200之间*/
<textareas title="角色描述" name="x.remark|角色描述|0~200|empty"
:value="data.remark?data.remark:''" :maxlength="200" placeholder="选填"></textareas>
/* 要传入服务端的name为:x.mobile ,校验手机号码*/
<inputs name="x.mobile|联系电话|mobile" title="联系电话" :value="data.name"></inputs>
/* 要传入服务端的name为:code ,校验规则为长度为6的字符*/
<inputs name="code|验证码|6" title="验证码"></inputs>
/* 要传入服务端的name为:avatar ,未上传头像提交表单时会提示:请上传头像*/
<upload-images name="avatar|请上传头像" title="头像"></upload-images>
【提交表单数据】
直接将@submit接收到的参数e,传递给this.bbc.submit()函数即可,自动做表单校验,验证通过后,提交表单。
通过给form配置data-action属性来定义表单提交的地址;
给form配置data-back来定义请求成功后返回的页面的地址;
给form配置data-confirm来定义提交表单之前的确认弹窗的文字,未定义则不显示确认弹窗;
给form配置data-alert来定义提交表单成功以后的弹窗的文字,未定义则不显示弹窗;
给form配置data-redirect来定义请求成功后跳转的页面的地址;
给form配置data-clear来定义请求成功后清理本地缓存的请求url(不可含参数)。
vue:
<form @submit="submit" data-action="admin/role/save" data-back="/pages/role/roleList" data-clear="admin/role/list">
<inputs name="x.name|角色名称" title="角色名称" :value="data.name"></inputs>
<radios title="类型" name="x.type" :list="typeList" :value="data.type"></radios>
<textareas title="角色描述" name="x.remark|角色描述|empty"
:value="data.remark?data.remark:''" :maxlength="200" placeholder="选填"></textareas>
<labels :isTop="true" title="权限配置">
<menu-groups :list="menuList" name="x.menuIds" :value="data.menuIds"></menu-groups>
</labels>
<labels class="mt40">
<inputs type="hidden" name="x._id" :value="id" v-if="id"></inputs>
<button class="btn greenBg w80" form-type="submit">{{ !data._id ? '保存' : '修改'}}</button>
<button class="btn grayBg line w80" @click="bcc.goBack()">取消</button>
</labels>
</form>
js:
methods: {
submit:function(e){
this.bcc.submit(e , res=>{
//如果配置了第二个参数:成功回调函数,则当服务端返回state == 'ok' 或无state字段时,进入请求成功的回调
}, err=>{
//如果配置了第三个参数:失败回调函数,则当服务端返回state == 'fail' 时,进入fail回调
});
}
}
【表单数据主动验证】
通过@submit接收到参数e后,直接用该参数进行表单验证。
submit:function(e){
var res = this.bcc.checkData(e);
if (res.fail) { //表单校验未通过
return ;
}
uni.showLoading({
title:"请稍后…",
mask:true
});
var data = res.data ; //表单校验通过后拿到要向服务端提交的处理过的数据
if (data['x.password']) {
data['x.password'] = this.bcc.sign(data['x.password']);
}
this.bcc.call({
url : 'admin/user/save' ,
data : data ,
success : res => {
uni.hideLoading();
this.bcc.clearCache("admin/user/list");
this.bcc.goSuccessBack("/pages/user/userList","保存成功");
}
});
},
【辅助工具类】
//向后返回2层页面,如果页面栈不足2个页面的话,就返回到/pages/index/index
this.bcc.goBack("/pages/index/index",2);
//返回上一历史页,如果上一历史页不存在则返回/pages/user/userList,然后提示“保存成功”
this.bcc.goSuccessBack("/pages/user/userList","保存成功");
//MD5加密字符串
var sign = this.bcc.sign('string...');
//判断某个变量是否为空
var isNull = this.bcc.isNull(a);
//判断某个变量是否是数字
var isNumber = this.bcc.isNumber(a);
=====================================================================
基础样式类库使用说明
基于BaseCloud的基础样式类库,您可以高度还原UI设计图,快速搭建客户端界面。与传统UI框架不同的是,
并不直接定义任何界面组件,它通过对高频基础样式类的封装,使用自由搭配组合的class样式类来快速、随心所欲的呈现千变万化的UI界面。
我们希望每个项目都有统一的主题色,以保证项目的界面的干净整洁,故而定义了一个主题色,你可以在uni.scss
中,修改成自己喜欢的颜色,作为整个项目的主题色。
同时,由于部分组件的自定义颜色属性,我们也使用了默认主题色,所以,如果您有修改主题色的需求,
除了修改uni.scss
中的主题色色值外,还可以在Hbulider中使用ctrl + alt + F
快捷键,全项目搜索该色值并替换。
uni.scss
$main:#07c160; //主背景色 $lightMain: #dff5e2; //淡主色 $mainInverse:#fff; //与主色搭配的反色 $mainGradual:linear-gradient(to top right,#67D79F,#00A28A); //渐变主色 $mainGradualInverse:#fff; //与渐变主色搭配的反色
修改完毕后,请确保在 App.vue
文件中通过如下方式引入样式库文件:
App.vue
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style lang="scss">
@import './common/base-cloud.scss';
</style>
样式类库详细使用说明文档,请点击此处链接查看:
《UI样式类库详细使用使用文档》
特别需要说明的是,基于该基础样式类库,您可以快速构建任何UI界面。
项目初始,目前我们仅对PC端一些常用组件进行了封装,供您使用,后续随着贯穿前后端的业务模块的开发,我们会逐步提供更多的组件。
=====================================================================
PC端组件使用说明文档
【auth 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。
关于权限控制的业务逻辑:用户登录成功后,读取该用户所属角色拥有的权限菜单列表,存储到本地,键名为menuList,权限判断就是基于menuList进行的判断。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/save | 权限路径,该路径可包含参数,需在t_menu表中已添加数据 | |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
<auth url="admin/user/save">
<navigator url="/pages/user/userEdit">编辑</navigator>
</auth>
【auth-btn 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。点击按钮时,会发送请求。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/changeStatus?id=1 | 发送请求的路径,可以携带参数 | |
params | 否 | 无 | json类型 | :params="{id:1}" | 发送请求时携带的参数 |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
confirm | 否 | 无 | confirm="delete" | 发送请求之前的确认文字,如果是删除类请求需要确认,可以简写为delete | |
alert | 否 | 无 | 请求成功后弹窗的文字 | ||
showFail | 否 | true | 请求失败后,是否提示服务端返回的msg字段 | ||
@success | 否 | 无 | 请求成功后回调函数 | ||
@fail | 否 | 无 | 请求失败后回调函数 |
<auth-btn url="admin/user/changeStatus" :params="{id:1}">
禁用
</auth-btn>
【auth-nav 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。点击按钮时,会进行页面跳转。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/changeStatus?id=1 | 是否具有权限的路径 | |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
href | 是 | 无 | 要跳转的页面的链接,可以包含参数 |
<auth-nav :href="`/pages/user/userEdit?id=${item._id}`" url="admin/user/save" >
编辑
</auth-nav>
【switch-btn 组件】
用于权限控制的开关切换按钮,无权限仅展示,不可发送请求。
属性 | 类型 | 说明 |
---|---|---|
url | String | 权限地址,也是点击切换时的请求地址,可以携带参数,如无地址或无权限,则不可点击 |
params | json | 请求参数,有权限时,点击切换即可发送请求 |
checked | Boolean | 开关是否打开 |
disabled | Boolean | 开关是否禁用 |
color | String | 颜色,默认#07c160 |
【switchs 组件】
属性 | 类型 | 说明 | |||
---|---|---|---|---|---|
name | String | 表单的name | |||
value | Boolean | 开关是否打开 | |||
tip | String | 开关右侧的提示文字 | |||
disabled | Boolean | 开关是否禁用 | |||
color | String | 颜色,默认#07c160 | |||
title | 否 | 表单标题,不要标题,请设置titleWidth=0 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 表单的左侧标题占位的宽度 |
isVertical | 否 | false | 标题和开关是否垂直排列 |
【checkboxs 组件】
复选框组件,用于多选,支持v-model
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题,不要标题,请设置titleWidth=0 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 复选框表单的左侧标题占位的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | value='1,2,3,5' | 表单的value,支持v-model绑定,可以是数组,也可以是用英文逗号分开的多个值 | ||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 复选框的颜色 | ||
isVertical | 否 | false | 标题和复选框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<checkboxs title="角色" :list="roleList" name="x.roleIds|请选择角色" :value="data.roleIds"
titleName="x.roleNames" titleKey="name" valueKey="_id"></checkboxs>
【radios 组件】
单选框组件,用于单选,支持v-model
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | 表单的value,支持v-model绑定 | |||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 复选框的颜色 | ||
isVertical | 否 | false | 标题和复选框是否垂直排列 | ||
defaultFirst | 否 | true | 当value无值时,是否默认选中第一个选项 | ||
@change | 当选项发生改变时触发的回调函数 |
<radios title="菜单类型" :list="menuTypeList" :value="data.type"
name="x.type|菜单类型" @change="chooseMenuType"></radios>
【multi-selects 组件】
下拉多选组件,用于多选,支持v-model,可以搜索关键字筛选
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | value='1,2,3,5' | 表单的value,支持v-model绑定,可以是数组,也可以是用英文逗号分开的多个值 | ||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
remarkKey | 否 | remark | 选项列表中,对作为副标题的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 颜色 | ||
isVertical | 否 | false | 标题和选择框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<multi-selects title="角色" :list="roleList" name="x.roleIds|请选择角色"
:value="data.roleIds" titleName="x.roleNames" titleKey="name" valueKey="_id"></multi-selects>
【selects 组件】
下拉单选组件,用于单选,支持v-model,可以搜索关键字筛选
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | 表单的value,支持v-model绑定 | |||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
remarkKey | 否 | remark | 选项列表中,对作为副标题的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 颜色 | ||
isVertical | 否 | false | 标题和选择框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<selects title="父级菜单" :list="parentMenuList" name="x.parentId"
:value="data.parentId" titleKey="name" valueKey="_id"></selects>
【inputs 组件】
输入框组件,type支持hidden类型,输入框有内容时,可以点击清空图标清空。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
value | 否 | 表单的value,支持v-model绑定 | |||
hiddenValue | 否 | 传入该值时,输入框将变为禁用状态,对用户展示value的值,hiddenValue将会传到服务端 | |||
type | 否 | text | text、number、hidden | 表单类型,支持hidden | |
addOn | 否 | 输入框右侧的文字块的文字 | |||
addOnLeft | 否 | 输入框左侧的文字块的文字 | |||
isVertical | 否 | false | 标题和输入框是否垂直排列 | ||
showClearIcon | 否 | true | 是否显示清空图标 | ||
@tapAddOn | 当点击输入框右侧文字块时触发的回调函数 | ||||
@tapAddOnLeft | 当点击输入框左侧文字块时触发的回调函数 | ||||
其他属性与事件 | 与input组件一致 |
<inputs name="x.name|用户名" title="用户名" :value="data.name" :hiddenValue="data._id"></inputs>
【textareas 组件】
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
value | 否 | 表单的value,支持v-model绑定 | |||
isVertical | 否 | false | 标题和文本框是否垂直排列 | ||
showClearIcon | 否 | true | 是否显示清空图标 | ||
autoHeight | 否 | false | 是否自适应高度 | ||
height | 否 | 100 | 非自适应高度时的高度 | ||
其他属性与事件 | 与textarea组件一致 |
<textareas title="权限地址" @blur="inputBlur" name="x.url|权限地址"
:value="data.url" placeholder="多个权限地址请用英文分号隔开"></textareas>
【conditions 组件】
分页筛选条件组件
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
list | 否 | {title:"用户名",name:"name"},{title:"状态",name:"status",type:"select",list:[]} | 筛选条件,数组,基本属性为:name、title、type、list,详见/pages/user/userList示例 | ||
conditions | 否 | {} | json | 当前的筛选条件 | |
confirmText | 否 | 筛选 | 筛选按钮的文字 | ||
@confirm | 确认筛选时的回调事件,e.conditions |
<conditions :conditions="conditions" :list="conditonList" @confirm="submitSearch"></conditions>
data() {
return {
conditonList:[
{title:"用户名",name:"name"}, //默认是输入框类型的,只需提供这两个属性即可
//如果是下拉选择类型的,则需要提供list属性,两个键值对:title、value
{title:"状态",name:"status",type:"select",list:[{title:"正常",value:0},{title:"禁用",value:1}]},
],
conditions:{
name : ""
}
}
},
【copy 组件】
一键复制的功能
属性 | 说明 |
---|---|
text | 要复制的文字内容 |
showIcon | 文字右侧是否显示复制图标,默认true |
<copy :text="data.text" :showIcon="false"></copy>
【empty 组件】
属性 | 类型 | 说明 |
---|---|---|
list | Array | 列表数据,用于判断是否为空,展示数据为空的提示 |
loading | Boolean | 是否加载中,加载中的时候,会显示加载中的动画 |
tips | String | 当数据为空时的提示文字,默认:抱歉,暂无数据~ |
<empty :list="list" :loading="loading"></empty>
【images 组件】
图片显示、预览组件
属性 | 类型 | 说明 |
---|---|---|
width | Number | 图片的宽度 |
isRound | Boolean | 是否是圆形图片,否则是方形图片,默认false |
list | String,Array | 要展示的图片列表,可以是图片链接数组,也可以是英文逗号分开的多个图片链接 |
count | Number | 要展示的图片的数量,超出数量不展示,-1为不限制,默认-1 |
disabled | Boolean | 是否显示右上角的删除按钮,是否可以编辑,默认false |
@remove | 当删除图片时触发回调 |
【labels 组件】
表单标题组件,主要为了对齐其他的表单布局使用
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
isVertical | 否 | false | 标题和文本框是否垂直排列 | ||
isTop | 否 | false | 标题与右侧是否顶部对齐,否则垂直对齐 |
【layout 组件】
布局组件,所有页面使用
属性 | 类型 | 说明 |
---|---|---|
title | String | 当前页面的标题 |
loading | Boolean | 是否加载中,加载中的时候,会显示加载中的动画 |
pageKey | String | 当前页面的唯一标识,用于左侧菜单显示选中状态 |
slot="titleLeft" | 标题行左侧位置的插槽 | |
slot="titleRight" | 标题行右侧位置的插槽 |
【mores 组件】
当文本内容为多行时,只显示一行,点击该文字,可以展示显示全部,再次点击则收起。
<mores>{{item.content}}</mores>
【paginate 组件】
分页器组件,需要传入pageNumber(页码)属性和page(分页数据)属性。其中page属性详细结构如下,在BaseCloud的公共模块已对分页数据做了封装,直接调用即可返回该数据结构:
page: {
pageNumber: 1, //页码
lastPage: true, //是否最后一页
totalPage: 1, //总页码
list: [], //列表数据
totalRow: 0, //总数据条数
pageSize: 10 //每页条数
},
该组件会触发一个回调函数@switchPage,返回数据结构如下:
{
pageSizeChanged : true , //每页数据条数是否切换
pageNumber : 1 , //页码
pageSize : 5 //每页数据条数
}
【tables 组件】
属性 | 类型 | 说明 |
---|---|---|
list | Array | 列表数据 |
slot="thead" | 表格的标题栏,无须写tr | |
slot="tbody" | 表格的内容 |
<tables :list="list">
<block slot="thead">
<th>角色名称</th>
<th>类型</th>
<th class="autoWidth">权限描述</th>
<th>操作</th>
</block>
<block slot="tbody">
<tr v-for="( x , index) in list" :key="index">
<td>{{x.name}}</td>
<td>{{x.typeStr}}</td>
<td>{{x.remark}}</td>
<td>
<auth-nav :href="`/pages/role/roleEdit?id=${x._id}`"
url="admin/role/info" class="main bold plr5">
编辑
</auth-nav>
<auth-btn :url="`admin/role/delete?id=${x._id}`" confirm="delete"
@success="remove(index)" class="main bold plr5">
删除
</auth-btn>
</td>
</tr>
</block>
</tables>
【upload-images 组件】
图片上传组件,直接上传到云储存,支持多张图片上传
属性 | 类型 | 说明 |
---|---|---|
count | Number | 最多可以上传多少张图片,默认不限制-1 |
name | String | 表单的name |
value | Array | 默认显示的图片列表,可以是数组,也可以是英文逗号分割的图片地址 |
deleteUrl | String | 当删除图片时,如果有配置删除的请求地址,则会向该地址发送请求,传入fileID参数,从云存储删掉该图片 |
@change | 图片上传或删除时的回调 | |
@delete | 图片删除成功的回调 |
项目简介
BaseCloud是一套基于uniapp、uniCloud、uni-id的全栈开发框架,不依赖任何第三方框架,极度精简轻巧。
在开发前端界面时,除了适配移动端外,它对PC端也做了良好的适配;
在开发云函数时,它可以为您提供拦截器配置、路由管理、分页、列表、单数据快速查询等功能。除此之外,对于一些业务开发中的常用函数也已做好封装,拿来即用。
在BaseCloud的初始化项目模板中,为您实现了贯穿前后端的业务模块:管理员登录、用户管理、菜单管理、角色与权限管理、操作日志、系统参数配置等项目通用的基础后台管理功能,这一切全都基于云函数开发。
项目价值
基于BaseCloud的快速开发UI样式库,可以快速拼装前端界面,高还原度实现设计图效果,兼顾高效与灵活。
基于BaseCloud的云函数公用模块,你可以轻松实现单云函数、多云函数的路由管理、请求拦截管理与权限控制、常用业务函数快速开发。
基于BaseCloud的客户端缓存管理机制,你可以大幅度减少应用的云函数重复调用请求,未来云函数开始计费后,至少节省应用50%的流量费用。
基于BaseCloud的管理后台项目模板,你可以快速初始化一套自带用户、菜单、角色、权限、操作日志、系统参数管理的管理后台项目,在此基础上开始你的项目开发。
当然,这一切都只是刚刚开始,未来我们会基于BaseCloud推出更多贯穿前后端的业务模板,只要您的项目是基于BaseCloud框架,所有的业务模板拿来即用,5分钟快速集成到项目内,无需重复开发前端和后端。
对于开发者而言,基于BaseCloud的全栈快速开发框架,你可以封装自己的贯穿前后端的业务模块,发布到付费业务模块插件市场。
BaseCloud项目构成
common>base-cloud.scss
基础样式库,适配移动端和PC端,22kb。common>js>base-cloud-client.js
客户端SDK,14.2kb。cloudfunctions>common>base-cloud
云函数公共模块,13.9kb。components
PC端常用业务组件目录
项目截图
演示项目地址:https://base-cloud.joiny.cn <<
账号:admin
密码:123123123
快速开始
- 请先下载BaseCloud管理后台项目模板,并导入到Hbuilder中
- 右键点击cloudfunctions目录,选择一个服务空间,支持阿里云、腾讯云。
- 找到cloudfunctions目录下的db_init.json数据库初始化文件,右键选择“初始化数据库”。
- 右键点击cloudfunctions目录,选择上传所有云函数以及公共模块。
- 点击运行到浏览器,运行成功后,在浏览器中进入登录页,初始账号:admin ,初始密码:123123123
使用过程中如有问题或建议,请移步gitee提交issue。
项目结构介绍
请务必对照仔细浏览项目目录介绍,您阅读本项目的文档将会事半功倍。
服务端项目目录
├── cloudfunctions───────────# 云函数目录
│ └── admin──────────────────# 管理后台业务函数
│ └── controller──────────────────# 管理后台业务函数根目录
│ └── menu.js────────────────────────# 菜单管理业务函数
│ └── operateLog.js──────────────────# 操作日志业务函数与接口
│ └── paramConfig.js─────────────────# 系统参数配置业务函数
│ └── role.js────────────────────────# 角色管理业务函数
│ └── user.js────────────────────────# 用户管理业务函数
│ └── node_modules──────────────────# admin函数依赖公共模块
│ └── index.js──────────────────────# admin函数入口文件
│ └── api────────────────────# uni-id官方公共模块
│ └── clearlogs──────────────# 过期操作日志清理定时任务函数
│ └── common─────────────────# 公共模块
│ └── base-cloud──────────────────# base-cloud公共模块
│ └── intercepters──────────────────# 拦截器函数目录
│ └── authInter.js──────────────────# 用户权限拦截拦截函数
│ └── config.js─────────────────────# 公共模块配置文件,注册全局拦截器(重要!)
│ └── index.js──────────────────────# BaseCloud公共模块源码,开发阶段无需关心
│ └── db_init.json───────────# 数据库初始化文件,包含数据表和初始化数据
客户端项目目录
├── cloudfunctions────────# 云函数目录...
├── common────────────────# 静态资源文件目录
│ └── js──────────────────# js文件目录
│ └── base-cloud-client.js─────────────────# BaseCloud客户端SDK
│ └── clipBoard.js─────────────────────────# 支持web端复制API
│ └── md5.js───────────────────────────────# MD5加密函数,用于密码加密传输,客户端数据缓存等场景
│ └── base.scss────────────────────# BaseCloud样式类库入口文件
│ └── base-font.scss───────────────# BaseCloud图标样式文件
│ └── base-mobile.scss─────────────# BaseCloud移动端样式文件
│ └── base-pc.scss─────────────────# BaseCloud适配PC端样式文件
├── pages────────────────# 页面
├── static───────────────# 图片静态资源文件目录
├── uni.scss─────────────# scss变量配置文件
管理后台业务模块云函数目录结构
├── cloudfunctions─────────────────# 云函数目录
│ └── admin──────────────────# 管理后台业务函数
│ └── controller──────────────────# 管理后台业务函数根目录
│ └── menu.js────────────────────────# 菜单管理业务函数
│ └── getParentList()──────────────────# 查询上级菜单列表接口
│ └── globalData()─────────────────────# 查询登录用户信息、权限菜单列表接口
│ └── info()───────────────────────────# 查询菜单信息接口
│ └── save()───────────────────────────# 保存、更新菜单信息接口
│ └── delete()─────────────────────────# 删除菜单信息接口
│ └── list()───────────────────────────# 菜单列表查询接口
│ └── operateLog.js──────────────────# 操作日志业务函数与接口
│ └── paramConfig.js─────────────────# 系统参数配置业务函数
│ └── info()───────────────────────────# 查询参数配置项信息接口
│ └── save()───────────────────────────# 保存、更新参数配置项信息接口
│ └── delete()─────────────────────────# 删除参数配置项接口
│ └── list()───────────────────────────# 参数配置项列表查询接口
│ └── role.js────────────────────────# 角色管理业务函数
│ └── info()───────────────────────────# 查询角色信息接口
│ └── save()───────────────────────────# 保存、更新角色信息接口
│ └── delete()─────────────────────────# 删除角色接口
│ └── list()───────────────────────────# 角色列表查询接口
│ └── options()────────────────────────# 角色选项列表查询接口(供用户角色选择时使用)
│ └── user.js────────────────────────# 用户管理业务函数
│ └── login()──────────────────────────# 登录接口
│ └── checkToken()─────────────────────# token验证接口
│ └── logout()─────────────────────────# 退出登录接口
│ └── changeStatus()───────────────────# 切换用户禁用状态接口
│ └── info()───────────────────────────# 用户信息查询接口
│ └── save()───────────────────────────# 保存、更新用户信息接口
│ └── myInfo()─────────────────────────# 当前用户信息接口
│ └── modify()─────────────────────────# 修改当前用户信息(含密码)接口
│ └── list()───────────────────────────# 用户列表查询接口
│ └── delete()─────────────────────────# 删除用户接口
│ └── node_modules──────────────────# admin函数依赖公共模块
│ └── index.js──────────────────────# admin函数入口文件
=====================================================================
服务端公共模块使用说明文档
【使用公共模块来接管云函数,定义多个访问路径】
- 根据公共模块引入说明来引入base-cloud公共模块;
- 在云函数的入口文件
index.js
中引入公共模块,并接管云函数。
'use strict';
const BaseCloud = require("base-cloud");
exports.main = async ( event , ctx ) => {
var fnName = "admin" ; //当前云函数的名称
var controlerDir = `${__dirname}/controller` ; //存放业务函数根目录的绝对路径
return await new BaseCloud({ event, ctx , fnName }).invoke(controlerDir);
};
- 在云函数中指定的业务函数根目录(此处是controller,你也可以指定其他的目录),创建js文件。
并通过module.exports
来导出业务处理的函数,可以导出一个或多个。 - 客户端访问云函数的路径规则为:云函数名称/根目录下的js函数文件名称/js函数文件导出的函数名称;
如果js函数文件直接导出的是一个函数,则路径规则为:云函数名称/根目录下的js函数文件名称。
如下示例为在admin云函数中的controller>operateLog.js
文件中导出一个函数:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(res){
var {pageNumber , pageSize} = this.params ;
var page = await this.paginate({
pageNumber , pageSize ,
collection : OperateLog ,
eq : ["actionName","userName"],
like : ["name"],
orderBy : "createTime desc"
});
var list = page.list ;
list.forEach(item=>{
item.createTime = this.DateKit.toStr( item.createTime ,'seconds');
});
return {page};
};
此时客户端访问路径为: admin/operateLog
,客户端调用示例如下:
this.bcc.call({
url : "admin/operateLog" ,
data : {pageNumber: 1 , pageSize : 20},
success : e=>{}
});
如下示例为在admin云函数中的controller>role.js
文件中导出多个函数:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const Role = db.collection("t_role");
module.exports = {
info : async function(e){
var id = this.params.id ;
var typeList = TYPE_LIST ;
if (!id) {
return { typeList };
}
var data = this.findFirst( await Role.doc(id).get() );
return { data , typeList };
},
save : async function(e){
var data = this.getModel();
if (data.menuIds) {
data.menuIds = data.menuIds.split(',');
}
if (!data._id) {
data.createTime = this.DateKit.now();
await Role.add(data);
return this.ok();
}
data.updateTime = this.DateKit.now() ;
await this.updateById(Role , data);
return this.ok();
}
};
此时,通过admin/role/info
和 admin/role/save
两个路径可以分别访问 controller>role.js>info()
和controller>role.js>save()
,客户端调用示例如下:
this.bcc.call({
url : "admin/role/info" ,
data : {_id : 1},
success : e=>{}
});
【使用公共模块来配置全局的拦截器,以及拦截器的清理】
- 在
cloudfunctions > common > base-cloud
目录下,找到config.js
文件, - 如下代码所示,在inters中配置了两个拦截器,你可以直接在此处定义拦截器函数(如loginInter),也可以通过文件引入的方式来定义拦截器(如authInter),具体的使用说明,请看注释:
//通过文件来引入拦截器函数
const authInter = require("./intercepters/authInter") ;
module.exports = {
isDebug : true , //会输出一些日志到控制台,方便调试
inters:{ //配置全局拦截器
loginInter: { //直接在此处定义拦截器函数
handle : [] , //拦截的路径,此处留空表示拦截全部的路径
clear : [ //配置要清除拦截器的路径,注意:如果配置了handle则此处的配置无效。
"admin/user/login", //支持字符串、也支持正则表达式(详见示例项目中的authInter的配置规则)
"admin/user/checkToken",
] ,
invoke:async function(attrs){//拦截器函数,入参为上一个拦截器通过setAttr方法传递的所有的键值对
const {event , ctx , uniID } = this ;
var res = await uniID.checkToken(event.uniIdToken);
if(res.code){
return {
state : 'needLogin',
msg : "请登录"
};
}
//将user传入下一个拦截器,在拦截器函数的入参中可以获取到,也可以通过this.getAttr("user")来取到该值。
this.setAttr({user : res.userInfo});
//当前拦截器放行,不调用这个,拦截器不会放行,此次请求到此终止
this.next();
}
},
authInter ,
}
}
也就是说,你可以把你所有要配置的拦截器放到 config.js > inters
中去,每个拦截器注册时,在 handle
属性中定义要拦截的路径,路径支持正则表达式;
在clear
属性中,定义要清理拦截器的路径;使用invoke
属性来定义拦截器的函数。特别注意:如果在handle
中定义了拦截的路径,则clear
中的配置会被忽略。
在拦截器函数中,接收的参数为一个json,为所有拦截器通过this.setAttr(key , value)
方法存入的键值对。
在拦截器中,可以使用this.setAttr(key , value)
方法,将当前拦截器中的变量传递到下一个拦截器或者业务函数。
在下一个拦截器或业务函数的入参中可以接收,也可以使用this.getAttr(key)
方法,来取到指定的值。
如果拦截器拦截成功,不再继续执行,直接返回响应结果即可。如果拦截器放行,则需要主动调用this.next()
方法,来放行本次拦截。
公共模块配置的拦截器对所有引入base-cloud公共模块,并由base-cloud接管的云函数都有效,请合理配置拦截与清理拦截的规则。
修改公共模块后,除了上传公共模块,也需要上传依赖公共模块的云函数哦~
【在业务函数中可以使用的变量】
还是以一个云函数中的业务函数为例:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(attrs){ //此处的attr是所有拦截器中通过 this.setAttr(key,value)方法存入的键值对
var user = attrs.user ; //在loginInter拦截器中存入的user变量
var ctx = this.ctx ; //上下文,为入口函数的入参context
var event = this.event ; //为入口函数的入参event,本次请求云函数携带的event参数
var params = this.params ; //本次请求客户端通过data或url传递所有的参数
var fullPath = this.fullPath ; //本次请求的路径,如: admin/user/info
var actionName = this.action ; //本次请求的action,不含云函数名称,如: user/info
var fnName = this.fnName ; //本次请求的云函数的名称
var token = this.token ; //本次请求携带的token
var uniID = this.uniID ; //依赖的uniID模块,可以直接使用uniID的API
};
【在业务函数中可以使用的方法】
this.getModel(prefix , keepKeys) ;
- 第一个参数为
prefix
参数,指定要获取的参数的前缀符,未指定时,默认为x
。 - 第二个参数为
keepKeys
,指定一个或多个键名进行接收,多个使用英文逗号分开,如未指定则接收所有带有指定前缀的参数。
举个例子,比如用户修改个人信息的功能,在客户端传参示例:
this.bcc.call({
url : "admin/user/modify" ,
data : {
"x.password" : "123123123" ,
"x.mobile" : "15688585858" ,
"x.realAuth.contact_name" : "王大成" ,
"x.username" : "想改个名字能改吗" ,
"remark" : "个人的喜好"
}
})
此时我们需要只接收带x.
前缀的参数,统一存放到data
变量中,以便直接更新用户表的数据。
更新时,我们假设只允许用户修改password
和mobile
字段,不允许修改username
字段,
那么使用 this.getModel()
方法可以获取到符合我们条件的参数。
this.keep( jsonData , keepKeys) ;
保留jsonData中指定的键值对,用法同上。
module.exports = async function(e){
var data = this.getModel("x" , "mobile,password,realAuth");
};
this.findFirst(dataInDB);
从数据库返回的结果中获取一条数据,如果没有数据则返回null
var user = this.findFirst( await User.doc(id).get() );
if(null == user){
return {} ;
}
var {username , mobile} = user ;
//...
this.find(dataInDB);
从数据库返回的结果中获取列表数据,如果没有数据则返回 []
;
var dataInDB = await Role.orderBy("createTime","asc").get() ;
var list = this.find( dataInDB );
if(list.length == 0){
//..do something
}
async this.updateById( collection , updateData ) ;
根据主键_id来更新一条数据,updateData
中包含_id
字段和要更新的字段
var Role = uniCloud.database().collection("t_role") ;
await this.updateById( Role , data);
async this.paginate({ collection , where = {} , field = {} , orderBy , eq , like , pageNumber = 1, pageSize = 10 });
使用数据库普通查询方法(暂不支持聚合查询)来获取分页数据。参数说明见下方的示例代码中的注释:
'use strict';
const db = uniCloud.database();
const dbCmd = db.command ;
const $ = db.command.aggregate ;
const OperateLog = db.collection("t_operate_log");
module.exports = async function(res){
var {pageNumber , pageSize} = this.params ;
var page = await this.paginate({
pageNumber , //分页页码,不传入默认为1
pageSize , //每页数据条数,不传入默认为10
collection : OperateLog , //要查询数据的集合对象
field:{ userName : true }, //指定返回字段,具体用法参见官方文档
where:{},//自定义的固定查询条件
eq : ["actionName","userName"], //筛选的相等条件,如果请求参数中该参数不为空则进行相等条件筛选
like : ["name"],//筛选的模糊查询条件,如果请求参数中该参数不为空则进行模糊查询筛选
orderBy : "createTime desc"
});
var list = page.list ;
list.forEach(item=>{
item.createTime = this.DateKit.toStr( item.createTime ,'seconds');
});
return {page};
};
分页查询方法返回的数据结构如下:
page: {
pageNumber: 1,//页码
lastPage: true,//是否最后一页
totalPage: 1,//总页码
list: [], //当前页数据
totalRow: 0, //总数据条数
pageSize: 10 //每页数据条数
}
this.ok(msg);
返回请求成功的响应结果,msg
不传入时,默认提示信息为:
{
state : "ok" ,
msg : "操作成功"
}
this.fail(msg, state)
返回请求成功的响应结果,msg
不传入时,默认提示信息为:
{
state : "fail" ,
msg : "系统异常,请稍后再试"
}
this.isRepeat( dataInDB , _id );
做保存更新一体的业务接口时,我们经常会判断某个字段是否已在数据库中存在值。
先根据该字段查询数据中的一条数据,然后使用该方法,来快速判断是否有重复的值。
如下为保存更新用户数据接口代码示例:
var data = this.getModel();
var {username , password , _id , roleIds , mobile } = data ;
var sameNameUser = this.findFirst(await User.where({username}).limit(1).get());
if( this.isRepeat(sameNameUser , _id) ){
return this.fail("用户名已存在");
}
async this.setMaxOrderNum(data, collection, where);
适用于具有orderNum
字段的数据表,自动生成最大的orderNum
的业务场景。
data
参数为即将要保存、更新的json数据,必填collection
为要更新的集合对象,必填where
为限定排序的条件,可选项
var Menu = uniCloud.database().collection("t_menu");
var data = this.getModel();
await this.setMaxOrderNum(data , Menu , {parentId : data.parentId } );
console.log(data.orderNum) ; //输出最大的orderNum
this.log(...arguments);
方便调试的方法,如果在 base-cloud > config.js
中配置了 isDebug
参数为 true
,使用该方法时,可以输出日志,否则不输出日志。
this.isNull(obj);
判断是否为空
this.isObject(obj);
判断是否为json对象
this.isEmptyObject(obj);
判断是否值为{}的json对象,不含有任何键值对的json
this.isFn(fn);
判断是否为函数
this.isNumber(number);
判断是否为数字
this.isArray(array);
判断是否为数组
this.isString(string);
判断是否为字符串
this.isDate(date);
判断是否为日期类型
this.isReg(reg);
判断是否为正则表达式
this.Datekit.now()
uniCloud默认是0时区的时间,使用该方法可以获取东八区当前时间的时间戳,符合国内的习惯。
this.DateKit.addMinutes(minutes , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定分钟数量,返回时间戳。
this.DateKit.addHours(hours , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定小时数量,返回时间戳。
this.DateKit.addDays(days , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定天数,返回时间戳。
this.DateKit.addMonths(months , date);
date为可选参数,时间戳类型,不传入则使用东八区的当前时间时间戳,增加或减少指定月份,返回时间戳。
this.Datekit.toStr( timestamp , fileds );
传入一个时间戳时间,格式化为字符串,fileds
来指定格式化的时间精度,支持:second、minute、hour、day、month、year,不传默认为minute
this.DateKit.friendlyDate(timestamp);
传入一个时间戳时间,返回距离东八区当前时间的有多少天、时、分、秒。如:3天前、24分钟后
【服务端响应结果的约定】
一般操作类的接口,服务端会返回如下结果:
{
state : 'ok' ,
msg : "操作成功"
}
数据查询类的,正常情况下,服务端不再返回state、msg字段,直接返回要查询的数据结果,如:
{
list : [],
data :{}
}
state | 说明 |
---|---|
ok | 请求成功 |
fail | 请求失败,失败时需要返回msg字段,作为失败的说明信息 |
noAuth | 无操作权限 |
needLogin | 需要登录 |
... | 其他特殊场景下的状态描述 |
=====================================================================
客户端SDK使用说明文档
使用客户端sdk前,请确保已按如下方式,在 main.js
中注册全局对象。
import Vue from 'vue'
import App from './App'
import bcc from "./common/js/base-cloud-client.js" //引入客户端sdk文件
Vue.prototype.bcc = bcc ; //注册为全局对象
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
【调用云函数】
var data = e.detail.value ;
this.bcc.call({
url : 'admin/user/save' , //请求路径,直接以函数名称开头,开头不要加/,后面跟着路径
data : data , //请求参数
success : res => {
//当服务端返回state == 'ok' 或无state字段时,进入success回调
},
fail : res => {
//当服务端返回state == 'fail' 时,进入fail回调;
//如未定义fail回调,则默认提示服务端返回的msg字段
},
complete : res => {
//请求完成后的回调
},
});
【通过客户端本地缓存调用云函数】
本地没有缓存则请求云函数数据并将请求到的数据缓存至本地,否则直接使用本地缓存数据。
var data = e.detail.value ;
this.bcc.callInCache({
url : 'admin/user/list' , //请求路径,直接以函数名称开头,开头不要加/,后面跟着路径
data : data , //请求参数
success : res => {
//当服务端返回state == 'ok' 或无state字段时,进入success回调,并且将数据缓存至本地
},
fail : res => {
//当服务端返回state == 'fail' 时,进入fail回调;
//如未定义fail回调,则默认提示服务端返回的msg字段
},
complete : res => {
//请求完成后的回调
},
});
【主动清理客户端本地缓存】
当本地数据已经发生变更时,需要主动清理本地缓存数据,下次去请求最新的数据。
清理缓存时,将直接清理所有指定请求地址下的缓存(一个请求地址下,因参数不同可能会有多个本地缓存数据)。
应用场景示例:将用户列表数据存入本地缓存,当编辑用户信息、删除用户数据、更改用户状态等三个操作发生时。直接清理本地的用户列表数据缓存:
//传入要清理缓存的请求地址的路径
this.bcc.clearCache("admin/user/list");
【表单数据校验】
表单校验无须配置各种校验规则,直接将校验规则写入name即可。
表单的name一共分成四个部分,用|符号分割:
- 要传入服务端的name
- 表单的标题,如果标题为空表示该表单可以为空。标题内可以含有:请输入、请上传、请选择 这三类提示文字。
- 校验规则,目前支持:mobile、email、idcard、count(整数)、amount(金额)、字符长度与长度范围等六种常见的表单验证和非空验证。
- empty :表示可以为空,如果有值则进行校验,无值则放行。
示例代码:
/* 要传入服务端的name为:x.name ,角色名称不可为空,校验规则为:字符长度2~20之间。 */
<inputs name="x.name|角色名称|2~20" title="角色名称" :value="data.name"></inputs>
/* 要传入服务端的name为:x.type ,该字段可以为空 */
<radios title="类型" name="x.type" :list="typeList" :value="data.type"></radios>
/* 要传入服务端的name为:x.remark ,该字段可以为空, 如果有值时,校验字符长度在2~200之间*/
<textareas title="角色描述" name="x.remark|角色描述|0~200|empty"
:value="data.remark?data.remark:''" :maxlength="200" placeholder="选填"></textareas>
/* 要传入服务端的name为:x.mobile ,校验手机号码*/
<inputs name="x.mobile|联系电话|mobile" title="联系电话" :value="data.name"></inputs>
/* 要传入服务端的name为:code ,校验规则为长度为6的字符*/
<inputs name="code|验证码|6" title="验证码"></inputs>
/* 要传入服务端的name为:avatar ,未上传头像提交表单时会提示:请上传头像*/
<upload-images name="avatar|请上传头像" title="头像"></upload-images>
【提交表单数据】
直接将@submit接收到的参数e,传递给this.bbc.submit()函数即可,自动做表单校验,验证通过后,提交表单。
通过给form配置data-action属性来定义表单提交的地址;
给form配置data-back来定义请求成功后返回的页面的地址;
给form配置data-confirm来定义提交表单之前的确认弹窗的文字,未定义则不显示确认弹窗;
给form配置data-alert来定义提交表单成功以后的弹窗的文字,未定义则不显示弹窗;
给form配置data-redirect来定义请求成功后跳转的页面的地址;
给form配置data-clear来定义请求成功后清理本地缓存的请求url(不可含参数)。
vue:
<form @submit="submit" data-action="admin/role/save" data-back="/pages/role/roleList" data-clear="admin/role/list">
<inputs name="x.name|角色名称" title="角色名称" :value="data.name"></inputs>
<radios title="类型" name="x.type" :list="typeList" :value="data.type"></radios>
<textareas title="角色描述" name="x.remark|角色描述|empty"
:value="data.remark?data.remark:''" :maxlength="200" placeholder="选填"></textareas>
<labels :isTop="true" title="权限配置">
<menu-groups :list="menuList" name="x.menuIds" :value="data.menuIds"></menu-groups>
</labels>
<labels class="mt40">
<inputs type="hidden" name="x._id" :value="id" v-if="id"></inputs>
<button class="btn greenBg w80" form-type="submit">{{ !data._id ? '保存' : '修改'}}</button>
<button class="btn grayBg line w80" @click="bcc.goBack()">取消</button>
</labels>
</form>
js:
methods: {
submit:function(e){
this.bcc.submit(e , res=>{
//如果配置了第二个参数:成功回调函数,则当服务端返回state == 'ok' 或无state字段时,进入请求成功的回调
}, err=>{
//如果配置了第三个参数:失败回调函数,则当服务端返回state == 'fail' 时,进入fail回调
});
}
}
【表单数据主动验证】
通过@submit接收到参数e后,直接用该参数进行表单验证。
submit:function(e){
var res = this.bcc.checkData(e);
if (res.fail) { //表单校验未通过
return ;
}
uni.showLoading({
title:"请稍后…",
mask:true
});
var data = res.data ; //表单校验通过后拿到要向服务端提交的处理过的数据
if (data['x.password']) {
data['x.password'] = this.bcc.sign(data['x.password']);
}
this.bcc.call({
url : 'admin/user/save' ,
data : data ,
success : res => {
uni.hideLoading();
this.bcc.clearCache("admin/user/list");
this.bcc.goSuccessBack("/pages/user/userList","保存成功");
}
});
},
【辅助工具类】
//向后返回2层页面,如果页面栈不足2个页面的话,就返回到/pages/index/index
this.bcc.goBack("/pages/index/index",2);
//返回上一历史页,如果上一历史页不存在则返回/pages/user/userList,然后提示“保存成功”
this.bcc.goSuccessBack("/pages/user/userList","保存成功");
//MD5加密字符串
var sign = this.bcc.sign('string...');
//判断某个变量是否为空
var isNull = this.bcc.isNull(a);
//判断某个变量是否是数字
var isNumber = this.bcc.isNumber(a);
=====================================================================
基础样式类库使用说明
基于BaseCloud的基础样式类库,您可以高度还原UI设计图,快速搭建客户端界面。与传统UI框架不同的是,
并不直接定义任何界面组件,它通过对高频基础样式类的封装,使用自由搭配组合的class样式类来快速、随心所欲的呈现千变万化的UI界面。
我们希望每个项目都有统一的主题色,以保证项目的界面的干净整洁,故而定义了一个主题色,你可以在uni.scss
中,修改成自己喜欢的颜色,作为整个项目的主题色。
同时,由于部分组件的自定义颜色属性,我们也使用了默认主题色,所以,如果您有修改主题色的需求,
除了修改uni.scss
中的主题色色值外,还可以在Hbulider中使用ctrl + alt + F
快捷键,全项目搜索该色值并替换。
uni.scss
$main:#07c160; //主背景色 $lightMain: #dff5e2; //淡主色 $mainInverse:#fff; //与主色搭配的反色 $mainGradual:linear-gradient(to top right,#67D79F,#00A28A); //渐变主色 $mainGradualInverse:#fff; //与渐变主色搭配的反色
修改完毕后,请确保在 App.vue
文件中通过如下方式引入样式库文件:
App.vue
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style lang="scss">
@import './common/base-cloud.scss';
</style>
样式类库详细使用说明文档,请点击此处链接查看:
《UI样式类库详细使用使用文档》
特别需要说明的是,基于该基础样式类库,您可以快速构建任何UI界面。
项目初始,目前我们仅对PC端一些常用组件进行了封装,供您使用,后续随着贯穿前后端的业务模块的开发,我们会逐步提供更多的组件。
=====================================================================
PC端组件使用说明文档
【auth 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。
关于权限控制的业务逻辑:用户登录成功后,读取该用户所属角色拥有的权限菜单列表,存储到本地,键名为menuList,权限判断就是基于menuList进行的判断。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/save | 权限路径,该路径可包含参数,需在t_menu表中已添加数据 | |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
<auth url="admin/user/save">
<navigator url="/pages/user/userEdit">编辑</navigator>
</auth>
【auth-btn 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。点击按钮时,会发送请求。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/changeStatus?id=1 | 发送请求的路径,可以携带参数 | |
params | 否 | 无 | json类型 | :params="{id:1}" | 发送请求时携带的参数 |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
confirm | 否 | 无 | confirm="delete" | 发送请求之前的确认文字,如果是删除类请求需要确认,可以简写为delete | |
alert | 否 | 无 | 请求成功后弹窗的文字 | ||
showFail | 否 | true | 请求失败后,是否提示服务端返回的msg字段 | ||
@success | 否 | 无 | 请求成功后回调函数 | ||
@fail | 否 | 无 | 请求失败后回调函数 |
<auth-btn url="admin/user/changeStatus" :params="{id:1}">
禁用
</auth-btn>
【auth-nav 组件】
用于用户权限控制,当用户拥有操作权限时展现,否则不展现该元素。点击按钮时,会进行页面跳转。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
url | 是 | 无 | admin/user/changeStatus?id=1 | 是否具有权限的路径 | |
noAuth | 否 | false | true | false | 无权限时展现 |
isInline | 否 | true | true | false | 是否内联元素 |
href | 是 | 无 | 要跳转的页面的链接,可以包含参数 |
<auth-nav :href="`/pages/user/userEdit?id=${item._id}`" url="admin/user/save" >
编辑
</auth-nav>
【switch-btn 组件】
用于权限控制的开关切换按钮,无权限仅展示,不可发送请求。
属性 | 类型 | 说明 |
---|---|---|
url | String | 权限地址,也是点击切换时的请求地址,可以携带参数,如无地址或无权限,则不可点击 |
params | json | 请求参数,有权限时,点击切换即可发送请求 |
checked | Boolean | 开关是否打开 |
disabled | Boolean | 开关是否禁用 |
color | String | 颜色,默认#07c160 |
【switchs 组件】
属性 | 类型 | 说明 | |||
---|---|---|---|---|---|
name | String | 表单的name | |||
value | Boolean | 开关是否打开 | |||
tip | String | 开关右侧的提示文字 | |||
disabled | Boolean | 开关是否禁用 | |||
color | String | 颜色,默认#07c160 | |||
title | 否 | 表单标题,不要标题,请设置titleWidth=0 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 表单的左侧标题占位的宽度 |
isVertical | 否 | false | 标题和开关是否垂直排列 |
【checkboxs 组件】
复选框组件,用于多选,支持v-model
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题,不要标题,请设置titleWidth=0 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 复选框表单的左侧标题占位的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | value='1,2,3,5' | 表单的value,支持v-model绑定,可以是数组,也可以是用英文逗号分开的多个值 | ||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 复选框的颜色 | ||
isVertical | 否 | false | 标题和复选框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<checkboxs title="角色" :list="roleList" name="x.roleIds|请选择角色" :value="data.roleIds"
titleName="x.roleNames" titleKey="name" valueKey="_id"></checkboxs>
【radios 组件】
单选框组件,用于单选,支持v-model
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | 表单的value,支持v-model绑定 | |||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 复选框的颜色 | ||
isVertical | 否 | false | 标题和复选框是否垂直排列 | ||
defaultFirst | 否 | true | 当value无值时,是否默认选中第一个选项 | ||
@change | 当选项发生改变时触发的回调函数 |
<radios title="菜单类型" :list="menuTypeList" :value="data.type"
name="x.type|菜单类型" @change="chooseMenuType"></radios>
【multi-selects 组件】
下拉多选组件,用于多选,支持v-model,可以搜索关键字筛选
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | value='1,2,3,5' | 表单的value,支持v-model绑定,可以是数组,也可以是用英文逗号分开的多个值 | ||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
remarkKey | 否 | remark | 选项列表中,对作为副标题的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 颜色 | ||
isVertical | 否 | false | 标题和选择框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<multi-selects title="角色" :list="roleList" name="x.roleIds|请选择角色"
:value="data.roleIds" titleName="x.roleNames" titleKey="name" valueKey="_id"></multi-selects>
【selects 组件】
下拉单选组件,用于单选,支持v-model,可以搜索关键字筛选
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
titleName | 否 | 如果需要选中选项的标题也传服务端,请定义该字段 | |||
value | 否 | 表单的value,支持v-model绑定 | |||
list | 是 | [{title:"搞笑",value:1},{title:"言情",value:2}] | 选项列表,数组 | ||
titleKey | 否 | title | 选项列表中,对用户展示的文字的键值对的键名 | ||
valueKey | 否 | value | 选项列表中,对作为选项值的键值对的键名 | ||
remarkKey | 否 | remark | 选项列表中,对作为副标题的键值对的键名 | ||
disabledKey | 否 | disabled | 选项列表中,表示当前选项禁用的键值对的键名 | ||
color | 否 | #07c160 | 颜色 | ||
isVertical | 否 | false | 标题和选择框是否垂直排列 | ||
@change | 当选项发生改变时触发的回调函数 |
<selects title="父级菜单" :list="parentMenuList" name="x.parentId"
:value="data.parentId" titleKey="name" valueKey="_id"></selects>
【inputs 组件】
输入框组件,type支持hidden类型,输入框有内容时,可以点击清空图标清空。
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
value | 否 | 表单的value,支持v-model绑定 | |||
hiddenValue | 否 | 传入该值时,输入框将变为禁用状态,对用户展示value的值,hiddenValue将会传到服务端 | |||
type | 否 | text | text、number、hidden | 表单类型,支持hidden | |
addOn | 否 | 输入框右侧的文字块的文字 | |||
addOnLeft | 否 | 输入框左侧的文字块的文字 | |||
isVertical | 否 | false | 标题和输入框是否垂直排列 | ||
showClearIcon | 否 | true | 是否显示清空图标 | ||
@tapAddOn | 当点击输入框右侧文字块时触发的回调函数 | ||||
@tapAddOnLeft | 当点击输入框左侧文字块时触发的回调函数 | ||||
其他属性与事件 | 与input组件一致 |
<inputs name="x.name|用户名" title="用户名" :value="data.name" :hiddenValue="data._id"></inputs>
【textareas 组件】
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
name | 是 | 表单的name | |||
value | 否 | 表单的value,支持v-model绑定 | |||
isVertical | 否 | false | 标题和文本框是否垂直排列 | ||
showClearIcon | 否 | true | 是否显示清空图标 | ||
autoHeight | 否 | false | 是否自适应高度 | ||
height | 否 | 100 | 非自适应高度时的高度 | ||
其他属性与事件 | 与textarea组件一致 |
<textareas title="权限地址" @blur="inputBlur" name="x.url|权限地址"
:value="data.url" placeholder="多个权限地址请用英文分号隔开"></textareas>
【conditions 组件】
分页筛选条件组件
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
list | 否 | {title:"用户名",name:"name"},{title:"状态",name:"status",type:"select",list:[]} | 筛选条件,数组,基本属性为:name、title、type、list,详见/pages/user/userList示例 | ||
conditions | 否 | {} | json | 当前的筛选条件 | |
confirmText | 否 | 筛选 | 筛选按钮的文字 | ||
@confirm | 确认筛选时的回调事件,e.conditions |
<conditions :conditions="conditions" :list="conditonList" @confirm="submitSearch"></conditions>
data() {
return {
conditonList:[
{title:"用户名",name:"name"}, //默认是输入框类型的,只需提供这两个属性即可
//如果是下拉选择类型的,则需要提供list属性,两个键值对:title、value
{title:"状态",name:"status",type:"select",list:[{title:"正常",value:0},{title:"禁用",value:1}]},
],
conditions:{
name : ""
}
}
},
【copy 组件】
一键复制的功能
属性 | 说明 |
---|---|
text | 要复制的文字内容 |
showIcon | 文字右侧是否显示复制图标,默认true |
<copy :text="data.text" :showIcon="false"></copy>
【empty 组件】
属性 | 类型 | 说明 |
---|---|---|
list | Array | 列表数据,用于判断是否为空,展示数据为空的提示 |
loading | Boolean | 是否加载中,加载中的时候,会显示加载中的动画 |
tips | String | 当数据为空时的提示文字,默认:抱歉,暂无数据~ |
<empty :list="list" :loading="loading"></empty>
【images 组件】
图片显示、预览组件
属性 | 类型 | 说明 |
---|---|---|
width | Number | 图片的宽度 |
isRound | Boolean | 是否是圆形图片,否则是方形图片,默认false |
list | String,Array | 要展示的图片列表,可以是图片链接数组,也可以是英文逗号分开的多个图片链接 |
count | Number | 要展示的图片的数量,超出数量不展示,-1为不限制,默认-1 |
disabled | Boolean | 是否显示右上角的删除按钮,是否可以编辑,默认false |
@remove | 当删除图片时触发回调 |
【labels 组件】
表单标题组件,主要为了对齐其他的表单布局使用
属性 | 必填 | 默认值 | 可选值 | 示例值 | 说明 |
---|---|---|---|---|---|
title | 否 | 表单标题 | |||
titleWidth | 否 | 90 | 数值即可 | 100 | 左侧标题的宽度 |
isVertical | 否 | false | 标题和文本框是否垂直排列 | ||
isTop | 否 | false | 标题与右侧是否顶部对齐,否则垂直对齐 |
【layout 组件】
布局组件,所有页面使用
属性 | 类型 | 说明 |
---|---|---|
title | String | 当前页面的标题 |
loading | Boolean | 是否加载中,加载中的时候,会显示加载中的动画 |
pageKey | String | 当前页面的唯一标识,用于左侧菜单显示选中状态 |
slot="titleLeft" | 标题行左侧位置的插槽 | |
slot="titleRight" | 标题行右侧位置的插槽 |
【mores 组件】
当文本内容为多行时,只显示一行,点击该文字,可以展示显示全部,再次点击则收起。
<mores>{{item.content}}</mores>
【paginate 组件】
分页器组件,需要传入pageNumber(页码)属性和page(分页数据)属性。其中page属性详细结构如下,在BaseCloud的公共模块已对分页数据做了封装,直接调用即可返回该数据结构:
page: {
pageNumber: 1, //页码
lastPage: true, //是否最后一页
totalPage: 1, //总页码
list: [], //列表数据
totalRow: 0, //总数据条数
pageSize: 10 //每页条数
},
该组件会触发一个回调函数@switchPage,返回数据结构如下:
{
pageSizeChanged : true , //每页数据条数是否切换
pageNumber : 1 , //页码
pageSize : 5 //每页数据条数
}
【tables 组件】
属性 | 类型 | 说明 |
---|---|---|
list | Array | 列表数据 |
slot="thead" | 表格的标题栏,无须写tr | |
slot="tbody" | 表格的内容 |
<tables :list="list">
<block slot="thead">
<th>角色名称</th>
<th>类型</th>
<th class="autoWidth">权限描述</th>
<th>操作</th>
</block>
<block slot="tbody">
<tr v-for="( x , index) in list" :key="index">
<td>{{x.name}}</td>
<td>{{x.typeStr}}</td>
<td>{{x.remark}}</td>
<td>
<auth-nav :href="`/pages/role/roleEdit?id=${x._id}`"
url="admin/role/info" class="main bold plr5">
编辑
</auth-nav>
<auth-btn :url="`admin/role/delete?id=${x._id}`" confirm="delete"
@success="remove(index)" class="main bold plr5">
删除
</auth-btn>
</td>
</tr>
</block>
</tables>
【upload-images 组件】
图片上传组件,直接上传到云储存,支持多张图片上传
属性 | 类型 | 说明 |
---|---|---|
count | Number | 最多可以上传多少张图片,默认不限制-1 |
name | String | 表单的name |
value | Array | 默认显示的图片列表,可以是数组,也可以是英文逗号分割的图片地址 |
deleteUrl | String | 当删除图片时,如果有配置删除的请求地址,则会向该地址发送请求,传入fileID参数,从云存储删掉该图片 |
@change | 图片上传或删除时的回调 | |
@delete | 图片删除成功的回调 |

uni.showModal 弹窗自定义内容样式解决方案
前言
因为各种版本的手机上Modal 原生弹窗各不相同且无法修改内容及样式,所以需要一个高度自定义的弹窗以解决弹窗样式各端不同的问题
app解决思路
使用app-plus "background": "transparent" 可以实现伪弹窗(其实是打开一个背景透明的页面),缺点返回时会触发onShow需要进行处理,
app代码
//pages.json
{
"path": "components/modal/confirmModal/index",
"style": {
"navigationStyle": "custom",
"app-plus": {
"animationType": "fade-in",
"background": "transparent",
"backgroundColor": "rgba(0,0,0,0)",
"popGesture": "none"
}
}
}
//main.js
import showModal from '@/common/js/modal.js'
Vue.prototype.$showModal = showModal
//modal.js
let $showModal = function(option) {
let params = {
title: "",
content: "",
cancelText: "取消", // 取消按钮的文字
confirmText: "确定", // 确认按钮文字
showCancel: true, // 是否显示取消按钮,默认为 true
}
Object.assign(params, option)
// #ifdef APP-PLUS
let list = []
Object.keys(params).forEach(ele => {
list.push(ele + "=" + params[ele])
})
let paramsStr = list.join('&')
uni.navigateTo({
url: `/components/modal/confirmModal/index?${paramsStr}`
});
return new Promise((resolve, reject) => {
uni.$once("AppModalCancel", () => {
reject()
})
uni.$once("AppModalConfirm", (e) => {
resolve(e)
})
});
// #endif
// #ifndef APP-PLUS
return new Promise((resolve,reject)=>{
uni.showModal({
title:params.title,
content: params.content,
cancelText: params.cancelText,
confirmText: params.confirmText,
showCancel: params.showCancel,
success: (res) => {
if(res.confirm) {
resolve()
} else {
reject()
}
}
});
})
// #endif
}
export default $showModal
//页面调用方法
this.$showModal({
title: '确定删除吗',
content: '',
cancelText:'取消',
confirmText: '确认'
}).then(res =>{}).catch(err=>{})
confirmModal 必须使用nvue页面,不然页面是不透明的
<template>
<view class="app-modal">
<view class="app-modal__container">
<view class="app-modal__container__header" v-if="title">
<text class="app-modal__container__header__text">{{title}}</text>
</view>
<view class="app-modal__container__content">
<view v-if="input" class="content_input">
<input v-model="inputContent" class="input" placeholder-class="input" :placeholder="inputContent" maxlength="20" type="text"></input>
</view>
<text class="app-modal__container__content__text" :style="{textAlign: align}">{{content}}</text>
</view>
<view class="app-modal__container__footer">
<view v-if="showCancel" style="width: 226rpx" class="app-modal__container__footer-left" hover-class="app-modal__container__footer-hover" :hover-start-time="20" :hover-stay-time="70" @click="clickLeft" >
<text class="app-modal__container__footer-left__text">{{cancelText}}</text>
</view>
<view :style="{width: showCancel?'226rpx':'452rpx'}" class="app-modal__container__footer-right" hover-class="app-modal__container__footer-hover" :hover-start-time="20" :hover-stay-time="70" @click="clickRight" >
<text class="app-modal__container__footer-right__text" >{{confirmText}}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: "",
content: "",
input:false,
inputContent:'',
align: "center", // 对齐方式 left/center/right
cancelText: "取消", // 取消按钮的文字
confirmText: "确定", // 确认按钮颜色
showCancel: true, // 是否显示取消按钮,默认为 true
};
},
onBackPress(options) {
if (options.from === 'navigateBack') {
return false;
}
return true;
},
onLoad(options) {
if (options.showCancel) {
options.showCancel = JSON.parse(options.showCancel)
}
Object.assign(this.$data, options)
},
methods: {
clickLeft() {
// 先关闭后发送事件
this.closeModal();
uni.$emit('AppModalCancel')
},
clickRight() {
// 先关闭后发送事件
this.closeModal();
uni.$emit('AppModalConfirm',this.inputContent)
},
closeModal() {
uni.navigateBack();
}
}
}
</script>
<style lang="scss">
// nvue页面只支持flex布局
//样式自定义
</style>
H5弹窗解决思路
全局修改uni.showModal 弹窗样式
H5代码
/* #ifndef APP-PLUS */
//自己打开个H5modal 选择元素 改成自己需要的modal 样式
uni-modal {
.uni-modal {}
}
/* #endif */
前言
因为各种版本的手机上Modal 原生弹窗各不相同且无法修改内容及样式,所以需要一个高度自定义的弹窗以解决弹窗样式各端不同的问题
app解决思路
使用app-plus "background": "transparent" 可以实现伪弹窗(其实是打开一个背景透明的页面),缺点返回时会触发onShow需要进行处理,
app代码
//pages.json
{
"path": "components/modal/confirmModal/index",
"style": {
"navigationStyle": "custom",
"app-plus": {
"animationType": "fade-in",
"background": "transparent",
"backgroundColor": "rgba(0,0,0,0)",
"popGesture": "none"
}
}
}
//main.js
import showModal from '@/common/js/modal.js'
Vue.prototype.$showModal = showModal
//modal.js
let $showModal = function(option) {
let params = {
title: "",
content: "",
cancelText: "取消", // 取消按钮的文字
confirmText: "确定", // 确认按钮文字
showCancel: true, // 是否显示取消按钮,默认为 true
}
Object.assign(params, option)
// #ifdef APP-PLUS
let list = []
Object.keys(params).forEach(ele => {
list.push(ele + "=" + params[ele])
})
let paramsStr = list.join('&')
uni.navigateTo({
url: `/components/modal/confirmModal/index?${paramsStr}`
});
return new Promise((resolve, reject) => {
uni.$once("AppModalCancel", () => {
reject()
})
uni.$once("AppModalConfirm", (e) => {
resolve(e)
})
});
// #endif
// #ifndef APP-PLUS
return new Promise((resolve,reject)=>{
uni.showModal({
title:params.title,
content: params.content,
cancelText: params.cancelText,
confirmText: params.confirmText,
showCancel: params.showCancel,
success: (res) => {
if(res.confirm) {
resolve()
} else {
reject()
}
}
});
})
// #endif
}
export default $showModal
//页面调用方法
this.$showModal({
title: '确定删除吗',
content: '',
cancelText:'取消',
confirmText: '确认'
}).then(res =>{}).catch(err=>{})
confirmModal 必须使用nvue页面,不然页面是不透明的
<template>
<view class="app-modal">
<view class="app-modal__container">
<view class="app-modal__container__header" v-if="title">
<text class="app-modal__container__header__text">{{title}}</text>
</view>
<view class="app-modal__container__content">
<view v-if="input" class="content_input">
<input v-model="inputContent" class="input" placeholder-class="input" :placeholder="inputContent" maxlength="20" type="text"></input>
</view>
<text class="app-modal__container__content__text" :style="{textAlign: align}">{{content}}</text>
</view>
<view class="app-modal__container__footer">
<view v-if="showCancel" style="width: 226rpx" class="app-modal__container__footer-left" hover-class="app-modal__container__footer-hover" :hover-start-time="20" :hover-stay-time="70" @click="clickLeft" >
<text class="app-modal__container__footer-left__text">{{cancelText}}</text>
</view>
<view :style="{width: showCancel?'226rpx':'452rpx'}" class="app-modal__container__footer-right" hover-class="app-modal__container__footer-hover" :hover-start-time="20" :hover-stay-time="70" @click="clickRight" >
<text class="app-modal__container__footer-right__text" >{{confirmText}}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: "",
content: "",
input:false,
inputContent:'',
align: "center", // 对齐方式 left/center/right
cancelText: "取消", // 取消按钮的文字
confirmText: "确定", // 确认按钮颜色
showCancel: true, // 是否显示取消按钮,默认为 true
};
},
onBackPress(options) {
if (options.from === 'navigateBack') {
return false;
}
return true;
},
onLoad(options) {
if (options.showCancel) {
options.showCancel = JSON.parse(options.showCancel)
}
Object.assign(this.$data, options)
},
methods: {
clickLeft() {
// 先关闭后发送事件
this.closeModal();
uni.$emit('AppModalCancel')
},
clickRight() {
// 先关闭后发送事件
this.closeModal();
uni.$emit('AppModalConfirm',this.inputContent)
},
closeModal() {
uni.navigateBack();
}
}
}
</script>
<style lang="scss">
// nvue页面只支持flex布局
//样式自定义
</style>
H5弹窗解决思路
全局修改uni.showModal 弹窗样式
H5代码
/* #ifndef APP-PLUS */
//自己打开个H5modal 选择元素 改成自己需要的modal 样式
uni-modal {
.uni-modal {}
}
/* #endif */
收起阅读 »

uni-app混合开发方案 appbridge
前言
因业务需要 某些页面需要使用appbridge配合原生做混合开发,废话不多说直接上代码
function isAndroid() {
var ua = navigator.userAgent,
_isAndroid = ua.indexOf('Android') > -1 || ua.indexOf('Linux') > -1;
return _isAndroid;
}
function isIOS() {
return !!navigator.userAgent.match(/(i[^;]+\;(U;)? CPU.+Mac OS X)/);
}
function isPad() {
return navigator.userAgent.toLowerCase().match(/iPad/i) == "ipad";
}
function isWinPad() {
return navigator.userAgent.indexOf("Windows NT") >= 0;
}
/**
* js-app桥接
* @param {Function} callback
*/
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
return callback(WebViewJavascriptBridge);
}
/*IOS或者ipad*/
if (isIOS() || isPad()) {
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe)
}, 0);
}
else {
console.log("Android");
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function () {
callback(WebViewJavascriptBridge)
},
false
);
}
}
/**
* 调用app方法
* @param {String} handlerName 对应app方法名
* @param {Object} options 参数对象
* @param {Function} callback 调用成功回调
*/
function callHandler(handlerName, options, callback) {
console.log('callHandler:' + handlerName, options);
connectWebViewJavascriptBridge(function (bridge) {
bridge.callHandler(handlerName, options, function (result) {
if (isIOS() || isPad()) {
result = result || {};
}
else {
result = result || "{}";
result = JSON.parse(result) || {};
}
result['extras'] = options['extras'];
console.log('callback:' + handlerName, result);
callback && callback(result);
});
});
}
AppBridge.registerHandler = registerHandler;
/**
* 提供给app调用的方法
* @param {String} handlerName 方法名
*/
function registerHandler(handlerName, pageCallback) {
console.log('registerHandler:' + handlerName);
connectWebViewJavascriptBridge(function (bridge) {
bridge.registerHandler(handlerName, function (data, appCallback) {
if (isIOS() || isPad()) {
data = data || {};
}
else {
data = data || "{}";
data = JSON.parse(data) || {};
}
console.log('callback:' + handlerName, data);
if (pageCallback) {
pageCallback(data, appCallback);
}
else {
appCallback && appCallback(data);
}
});
});
}
AppBridge.isBridge = false;
// #ifdef H5
/*初始化js-bridge*/
connectWebViewJavascriptBridge(function (bridge) {
AppBridge.isBridge = true;
})
// #endif
export default AppBridge
使用isBridge 判断是否需要调用bridge方法
前言
因业务需要 某些页面需要使用appbridge配合原生做混合开发,废话不多说直接上代码
function isAndroid() {
var ua = navigator.userAgent,
_isAndroid = ua.indexOf('Android') > -1 || ua.indexOf('Linux') > -1;
return _isAndroid;
}
function isIOS() {
return !!navigator.userAgent.match(/(i[^;]+\;(U;)? CPU.+Mac OS X)/);
}
function isPad() {
return navigator.userAgent.toLowerCase().match(/iPad/i) == "ipad";
}
function isWinPad() {
return navigator.userAgent.indexOf("Windows NT") >= 0;
}
/**
* js-app桥接
* @param {Function} callback
*/
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
return callback(WebViewJavascriptBridge);
}
/*IOS或者ipad*/
if (isIOS() || isPad()) {
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe)
}, 0);
}
else {
console.log("Android");
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function () {
callback(WebViewJavascriptBridge)
},
false
);
}
}
/**
* 调用app方法
* @param {String} handlerName 对应app方法名
* @param {Object} options 参数对象
* @param {Function} callback 调用成功回调
*/
function callHandler(handlerName, options, callback) {
console.log('callHandler:' + handlerName, options);
connectWebViewJavascriptBridge(function (bridge) {
bridge.callHandler(handlerName, options, function (result) {
if (isIOS() || isPad()) {
result = result || {};
}
else {
result = result || "{}";
result = JSON.parse(result) || {};
}
result['extras'] = options['extras'];
console.log('callback:' + handlerName, result);
callback && callback(result);
});
});
}
AppBridge.registerHandler = registerHandler;
/**
* 提供给app调用的方法
* @param {String} handlerName 方法名
*/
function registerHandler(handlerName, pageCallback) {
console.log('registerHandler:' + handlerName);
connectWebViewJavascriptBridge(function (bridge) {
bridge.registerHandler(handlerName, function (data, appCallback) {
if (isIOS() || isPad()) {
data = data || {};
}
else {
data = data || "{}";
data = JSON.parse(data) || {};
}
console.log('callback:' + handlerName, data);
if (pageCallback) {
pageCallback(data, appCallback);
}
else {
appCallback && appCallback(data);
}
});
});
}
AppBridge.isBridge = false;
// #ifdef H5
/*初始化js-bridge*/
connectWebViewJavascriptBridge(function (bridge) {
AppBridge.isBridge = true;
})
// #endif
export default AppBridge
使用isBridge 判断是否需要调用bridge方法
收起阅读 »
图片旋转解决方案
前言:
目前测试图片旋转只会出现在ios H5版本上,咨询过同行和ios原生,图片旋转是会发生在canvas压缩或者裁剪上,原生暂未发现图片旋转。
解决思路:
H5版本使用exif-js获取Orientation 获取图片旋转方向,使用canvans进行旋转。
代码
// #ifndef APP-PLUS
import Exif from "exif-js"
// #endif
//H5图片入口
const rotatePic = async function(file,name,success) {
if(!isPicture(name)) return null;
let Orientation = 1;
await getImageTag(file,(e)=>{
if(e != undefined) Orientation = e;
})
var img = null;
var canvas = null;
await comprossImage(file, function(e) {
img = e.img;
canvas = e.canvas;
})
let baseStr = '';
//如果方向角不为1,都需要进行旋转
switch(Orientation){
case 6://需要顺时针(向右)90度旋转
console.log('(向右)90度旋转');
baseStr = rotateImg(img,'right',canvas);
break;
case 8://需要逆时针(向左)90度旋转
console.log('向左)90度旋转');
baseStr = rotateImg(img,'left',canvas);
break;
case 3://需要180度旋转 转两次
console.log('需要180度旋转');
baseStr = rotateImg(img,'right',canvas, 2);
break;
default:
baseStr = rotateImg(img,'',canvas);
break;
}
return baseStr
}
/**
* @desc 获取图片信息,使用exif.js库,具体用法请在github中搜索
* @param {Object} file 上传的图片文件
* @return {Promise<Any>} 读取是个异步操作,返回指定的图片信息
*/
const getImageTag = function (file, suc) {
if (!file) return 0;
return new Promise((resolve, reject) => {
/* eslint-disable func-names */
// 箭头函数会修改this,所以这里不能用箭头函数
let imgObj = new Image()
imgObj.src = file
console.log(imgObj)
let _this = this;
uni.hideLoading()
uni.getImageInfo({
src: file,
success(res) {
let obj = {
src:res.path
}
Exif.getData(obj, function () {
let or = Exif.getTag(this,'Orientation');//这个Orientation 就是我们判断需不需要旋转的值了,有1、3、6、8
resolve(suc(or))
});
}
})
});
}
//创建图片
const comprossImage = async (imgSrc, func) => {
if(!imgSrc) return 0;
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imgSrc,
success(res) {
let img = new Image();
img.src = res.path;
let canvas = document.createElement('canvas');
let obj = new Object();
obj.img = img;
obj.canvas = canvas;
resolve(func(obj));
}
});
})
}
//网上提供的旋转function
const rotateImg = (img, direction, canvas, times = 1) => {
console.log('开始旋转')
//最小与最大旋转方向,图片旋转4次后回到原方向
var min_step = 0;
var max_step = 3;
if (img == null)return;
//img的高度和宽度不能在img元素隐藏后获取,否则会出错
var height = img.height;
var width = img.width;
let maxWidth = 500;
let canvasWidth = width; //图片原始长宽
let canvasHeight = height;
let base = canvasWidth/canvasHeight;
console.log(maxWidth);
if(canvasWidth > maxWidth){
canvasWidth = maxWidth;
canvasHeight = Math.floor(canvasWidth/base);
}
width = canvasWidth;
height = canvasHeight;
var step = 0;
if (step == null) {
step = min_step;
}
if (direction == 'right') {
step += times;
//旋转到原位置,即超过最大值
step > max_step && (step = min_step);
} else if(direction == 'left'){
step -= times;
step < min_step && (step = max_step);
} else { //不旋转
step = 0;
}
//旋转角度以弧度值为参数
var degree = step * 90 * Math.PI / 180;
var ctx = canvas.getContext('2d');
console.log(degree)
console.log(step)
switch (step) {
case 1:
console.log('右旋转 90度')
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
// ctx.drawImage(img, 0, 0, width, height);
ctx.drawImage(img, 0, -height, width, height);
break;
case 2:
//console.log('旋转 180度')
canvas.width = width;
canvas.height = height;
ctx.rotate(degree);
ctx.drawImage(img, -width, -height, width, height);
break;
case 3:
console.log('左旋转 90度')
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, -width, 0, width, height);
break;
default: //不旋转
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
break;
}
let baseStr = canvas.toDataURL("image/jpeg", 1);
return baseStr;
}
ios 系统版本13.4以上不需要进行图片旋转 ios 13.4以上竖拍 Orientation 依然会得到6 但是图片已经被系统调整到正常 使用旋转后会导致图片被旋转2次
//ios 13.4以上不需要进行翻转
const getVersionRotate = () => {
var ua = navigator.userAgent.toLowerCase();
var ver= ua.match(/cpu iphone os (.*?) like mac os/);
let b = ver[1].replace(/_/g,".");
return toNum(b) >= toNum(13.4) ? false : true;
}
//计算版本号大小,转化大小
const toNum = (a) => {
var a=a.toString();
var c=a.split('.');
var num_place=["","0","00","000","0000"],r=num_place.reverse();
for (var i=0;i<c.length;i++){
var len=c[i].length;
c[i]=r[len]+c[i];
}
var res= c.join('');
return res;
}
// #ifndef APP-PLUS
//处理图片旋转 H5上传入口
if(getVersionRotate()) {
_file = await rotatePic (filePath, dir) || filePath;
}
// #endif
原生图片旋转解决思路
因为几个测试机未出现图片旋转问题,所以未进行旋转,解决思路为选择图片后 uni.getImageInfo(OBJECT) success中获取orientation 旋转参数,使用plus.zip.compressImage进行原生图片旋转
前言:
目前测试图片旋转只会出现在ios H5版本上,咨询过同行和ios原生,图片旋转是会发生在canvas压缩或者裁剪上,原生暂未发现图片旋转。
解决思路:
H5版本使用exif-js获取Orientation 获取图片旋转方向,使用canvans进行旋转。
代码
// #ifndef APP-PLUS
import Exif from "exif-js"
// #endif
//H5图片入口
const rotatePic = async function(file,name,success) {
if(!isPicture(name)) return null;
let Orientation = 1;
await getImageTag(file,(e)=>{
if(e != undefined) Orientation = e;
})
var img = null;
var canvas = null;
await comprossImage(file, function(e) {
img = e.img;
canvas = e.canvas;
})
let baseStr = '';
//如果方向角不为1,都需要进行旋转
switch(Orientation){
case 6://需要顺时针(向右)90度旋转
console.log('(向右)90度旋转');
baseStr = rotateImg(img,'right',canvas);
break;
case 8://需要逆时针(向左)90度旋转
console.log('向左)90度旋转');
baseStr = rotateImg(img,'left',canvas);
break;
case 3://需要180度旋转 转两次
console.log('需要180度旋转');
baseStr = rotateImg(img,'right',canvas, 2);
break;
default:
baseStr = rotateImg(img,'',canvas);
break;
}
return baseStr
}
/**
* @desc 获取图片信息,使用exif.js库,具体用法请在github中搜索
* @param {Object} file 上传的图片文件
* @return {Promise<Any>} 读取是个异步操作,返回指定的图片信息
*/
const getImageTag = function (file, suc) {
if (!file) return 0;
return new Promise((resolve, reject) => {
/* eslint-disable func-names */
// 箭头函数会修改this,所以这里不能用箭头函数
let imgObj = new Image()
imgObj.src = file
console.log(imgObj)
let _this = this;
uni.hideLoading()
uni.getImageInfo({
src: file,
success(res) {
let obj = {
src:res.path
}
Exif.getData(obj, function () {
let or = Exif.getTag(this,'Orientation');//这个Orientation 就是我们判断需不需要旋转的值了,有1、3、6、8
resolve(suc(or))
});
}
})
});
}
//创建图片
const comprossImage = async (imgSrc, func) => {
if(!imgSrc) return 0;
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imgSrc,
success(res) {
let img = new Image();
img.src = res.path;
let canvas = document.createElement('canvas');
let obj = new Object();
obj.img = img;
obj.canvas = canvas;
resolve(func(obj));
}
});
})
}
//网上提供的旋转function
const rotateImg = (img, direction, canvas, times = 1) => {
console.log('开始旋转')
//最小与最大旋转方向,图片旋转4次后回到原方向
var min_step = 0;
var max_step = 3;
if (img == null)return;
//img的高度和宽度不能在img元素隐藏后获取,否则会出错
var height = img.height;
var width = img.width;
let maxWidth = 500;
let canvasWidth = width; //图片原始长宽
let canvasHeight = height;
let base = canvasWidth/canvasHeight;
console.log(maxWidth);
if(canvasWidth > maxWidth){
canvasWidth = maxWidth;
canvasHeight = Math.floor(canvasWidth/base);
}
width = canvasWidth;
height = canvasHeight;
var step = 0;
if (step == null) {
step = min_step;
}
if (direction == 'right') {
step += times;
//旋转到原位置,即超过最大值
step > max_step && (step = min_step);
} else if(direction == 'left'){
step -= times;
step < min_step && (step = max_step);
} else { //不旋转
step = 0;
}
//旋转角度以弧度值为参数
var degree = step * 90 * Math.PI / 180;
var ctx = canvas.getContext('2d');
console.log(degree)
console.log(step)
switch (step) {
case 1:
console.log('右旋转 90度')
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
// ctx.drawImage(img, 0, 0, width, height);
ctx.drawImage(img, 0, -height, width, height);
break;
case 2:
//console.log('旋转 180度')
canvas.width = width;
canvas.height = height;
ctx.rotate(degree);
ctx.drawImage(img, -width, -height, width, height);
break;
case 3:
console.log('左旋转 90度')
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, -width, 0, width, height);
break;
default: //不旋转
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
break;
}
let baseStr = canvas.toDataURL("image/jpeg", 1);
return baseStr;
}
ios 系统版本13.4以上不需要进行图片旋转 ios 13.4以上竖拍 Orientation 依然会得到6 但是图片已经被系统调整到正常 使用旋转后会导致图片被旋转2次
//ios 13.4以上不需要进行翻转
const getVersionRotate = () => {
var ua = navigator.userAgent.toLowerCase();
var ver= ua.match(/cpu iphone os (.*?) like mac os/);
let b = ver[1].replace(/_/g,".");
return toNum(b) >= toNum(13.4) ? false : true;
}
//计算版本号大小,转化大小
const toNum = (a) => {
var a=a.toString();
var c=a.split('.');
var num_place=["","0","00","000","0000"],r=num_place.reverse();
for (var i=0;i<c.length;i++){
var len=c[i].length;
c[i]=r[len]+c[i];
}
var res= c.join('');
return res;
}
// #ifndef APP-PLUS
//处理图片旋转 H5上传入口
if(getVersionRotate()) {
_file = await rotatePic (filePath, dir) || filePath;
}
// #endif
原生图片旋转解决思路
因为几个测试机未出现图片旋转问题,所以未进行旋转,解决思路为选择图片后 uni.getImageInfo(OBJECT) success中获取orientation 旋转参数,使用plus.zip.compressImage进行原生图片旋转
收起阅读 »
关于使用条件编译时遇到的问题和一些想法
目前的条件编译是使用注释和目录区分的方式实现,但是在实际开发中会遇到几个问题。
1. pages.json中的注释会破坏JSON结构
在pages.json中使用注释的方式实现条件编译,会破坏JSON结构,会导致部分IDE报错,虽然不影响最终使用,但是在开发时还是会影响体验。
建议可以pages.json中,在保持JSON结构不变的情况下,支持对不同终端的配置。例:
{
"pages": [
{
"path": "pages/index/index"
}
],
"globalStyle": {
"enablePullDownRefresh": false
},
"platforms": {
"mp-weixin": {
"pages": [
{
"path": "pages/login/index"
}
],
"globalStyle": {
"enablePullDownRefresh": true
}
}
}
}
在编译微信小程序时,会把"pages/login/index"页面也编译进去,另外覆盖合并其他配置到默认配置上。
2. 在JS脚本中条件注释可能会导致IDE报错和代码质量检测失败。
比如以下代码:
function login(payload) {
// #ifdef H5
return redirectTo({
url: "/pages/login/index",
payload
});
// #endif
// #ifdef MP
return uni.login(payload);
// #endif
}
对于代码检查工具来说,return之后紧接着又是一个return,本身就是一段“有问题”的代码。而且如果逻辑复杂,会影响代码阅读。
对于此问题,我有2点建议:
1是支持使用环境变量做条件编译:比如:
function login(payload) {
if (process.env.UNI_PLATFORM === "h5") {
return redirectTo({
url: "/pages/login/index",
payload
});
}
if (process.env.UNI_PLATFORM === "mp-weixin") {
return uni.login(payload);
}
}
2是支持按平台区分文件。比如login.h5.js,login.mp-weixin.js。import 文件时,只需要import login即可,编译器自动优先编译对应的文件。在开发时,只要保持多端文件export一致即可。
login.js(没有对应平台文件时,才编译此文件)
login.h5.js
login.mp-weixin.js
3. 使用整体目录条件编译,会导致路由不一致。
如果要实现同样的路由,不同平台编译时编译不同的问题,就需要破坏路由,比如:
// pages.json
{
"pages": [
// #ifdef h5
{
"path": "pages/login/index"
},
// #endif
// #ifdef mp-weixin
{
"path": "platform/mp-weixin/login/index"
}
// #endif
]
}
本来都是登录页,需要保持路由的一致,但是如果按平台区分文件,那么就必须要拆成两个路由。
对此,我建议可以支持按文件后缀来区分平台:
比如在编译微信小程序时,优先编译/pages/login/index.mp-weixin.vue,如果文件不存在,才编译/pages/login/index.vue。这样既能保证路由一致,又可以通过区分文件实现条件编译。
以上是本人在开发中遇到的实际问题和产生的一些想法,如有不完善的地方还请大家指教。
目前的条件编译是使用注释和目录区分的方式实现,但是在实际开发中会遇到几个问题。
1. pages.json中的注释会破坏JSON结构
在pages.json中使用注释的方式实现条件编译,会破坏JSON结构,会导致部分IDE报错,虽然不影响最终使用,但是在开发时还是会影响体验。
建议可以pages.json中,在保持JSON结构不变的情况下,支持对不同终端的配置。例:
{
"pages": [
{
"path": "pages/index/index"
}
],
"globalStyle": {
"enablePullDownRefresh": false
},
"platforms": {
"mp-weixin": {
"pages": [
{
"path": "pages/login/index"
}
],
"globalStyle": {
"enablePullDownRefresh": true
}
}
}
}
在编译微信小程序时,会把"pages/login/index"页面也编译进去,另外覆盖合并其他配置到默认配置上。
2. 在JS脚本中条件注释可能会导致IDE报错和代码质量检测失败。
比如以下代码:
function login(payload) {
// #ifdef H5
return redirectTo({
url: "/pages/login/index",
payload
});
// #endif
// #ifdef MP
return uni.login(payload);
// #endif
}
对于代码检查工具来说,return之后紧接着又是一个return,本身就是一段“有问题”的代码。而且如果逻辑复杂,会影响代码阅读。
对于此问题,我有2点建议:
1是支持使用环境变量做条件编译:比如:
function login(payload) {
if (process.env.UNI_PLATFORM === "h5") {
return redirectTo({
url: "/pages/login/index",
payload
});
}
if (process.env.UNI_PLATFORM === "mp-weixin") {
return uni.login(payload);
}
}
2是支持按平台区分文件。比如login.h5.js,login.mp-weixin.js。import 文件时,只需要import login即可,编译器自动优先编译对应的文件。在开发时,只要保持多端文件export一致即可。
login.js(没有对应平台文件时,才编译此文件)
login.h5.js
login.mp-weixin.js
3. 使用整体目录条件编译,会导致路由不一致。
如果要实现同样的路由,不同平台编译时编译不同的问题,就需要破坏路由,比如:
// pages.json
{
"pages": [
// #ifdef h5
{
"path": "pages/login/index"
},
// #endif
// #ifdef mp-weixin
{
"path": "platform/mp-weixin/login/index"
}
// #endif
]
}
本来都是登录页,需要保持路由的一致,但是如果按平台区分文件,那么就必须要拆成两个路由。
对此,我建议可以支持按文件后缀来区分平台:
比如在编译微信小程序时,优先编译/pages/login/index.mp-weixin.vue,如果文件不存在,才编译/pages/login/index.vue。这样既能保证路由一致,又可以通过区分文件实现条件编译。
以上是本人在开发中遇到的实际问题和产生的一些想法,如有不完善的地方还请大家指教。
收起阅读 »