HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

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

uniapp

开发当中,封装了一个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 - 云开发全栈快速开发框架

uniCloud 源码分享

项目简介

BaseCloud是一套基于uniapp、uniCloud、uni-id的全栈开发框架,不依赖任何第三方框架,极度精简轻巧。

在开发前端界面时,除了适配移动端外,它对PC端也做了良好的适配;

在开发云函数时,它可以为您提供拦截器配置、路由管理、分页、列表、单数据快速查询等功能。除此之外,对于一些业务开发中的常用函数也已做好封装,拿来即用。

在BaseCloud的初始化项目模板中,为您实现了贯穿前后端的业务模块:管理员登录、用户管理、菜单管理、角色与权限管理、操作日志、系统参数配置等项目通用的基础后台管理功能,这一切全都基于云函数开发。

项目价值

基于BaseCloud的快速开发UI样式库,可以快速拼装前端界面,高还原度实现设计图效果,兼顾高效与灵活。

基于BaseCloud的云函数公用模块,你可以轻松实现单云函数、多云函数的路由管理、请求拦截管理与权限控制、常用业务函数快速开发。

基于BaseCloud的客户端缓存管理机制,你可以大幅度减少应用的云函数重复调用请求,未来云函数开始计费后,至少节省应用50%的流量费用。

基于BaseCloud的管理后台项目模板,你可以快速初始化一套自带用户、菜单、角色、权限、操作日志、系统参数管理的管理后台项目,在此基础上开始你的项目开发。

当然,这一切都只是刚刚开始,未来我们会基于BaseCloud推出更多贯穿前后端的业务模板,只要您的项目是基于BaseCloud框架,所有的业务模板拿来即用,5分钟快速集成到项目内,无需重复开发前端和后端。

对于开发者而言,基于BaseCloud的全栈快速开发框架,你可以封装自己的贯穿前后端的业务模块,发布到付费业务模块插件市场。

BaseCloud项目构成

  1. common>base-cloud.scss 基础样式库,适配移动端和PC端,22kb。
  2. common>js>base-cloud-client.js 客户端SDK,14.2kb。
  3. cloudfunctions>common>base-cloud 云函数公共模块,13.9kb。
  4. components PC端常用业务组件目录

项目截图

演示项目地址:https://base-cloud.joiny.cn <<

账号:admin
密码:123123123

快速开始

  1. 请先下载BaseCloud管理后台项目模板,并导入到Hbuilder中
  2. 右键点击cloudfunctions目录,选择一个服务空间,支持阿里云、腾讯云。
  3. 找到cloudfunctions目录下的db_init.json数据库初始化文件,右键选择“初始化数据库”。
  4. 右键点击cloudfunctions目录,选择上传所有云函数以及公共模块。
  5. 点击运行到浏览器,运行成功后,在浏览器中进入登录页,初始账号: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函数入口文件

=====================================================================

服务端公共模块使用说明文档

【使用公共模块来接管云函数,定义多个访问路径】

  1. 根据公共模块引入说明来引入base-cloud公共模块;
  2. 在云函数的入口文件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);  
};  
  1. 在云函数中指定的业务函数根目录(此处是controller,你也可以指定其他的目录),创建js文件。
    并通过module.exports来导出业务处理的函数,可以导出一个或多个。
  2. 客户端访问云函数的路径规则为:云函数名称/根目录下的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/infoadmin/role/save 两个路径可以分别访问 controller>role.js>info()
controller>role.js>save(),客户端调用示例如下:

this.bcc.call({  
    url : "admin/role/info" ,  
    data : {_id : 1},  
    success : e=>{}  
});

【使用公共模块来配置全局的拦截器,以及拦截器的清理】

  1. cloudfunctions > common > base-cloud 目录下,找到 config.js 文件,
  2. 如下代码所示,在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) ;

  1. 第一个参数为prefix参数,指定要获取的参数的前缀符,未指定时,默认为x
  2. 第二个参数为keepKeys,指定一个或多个键名进行接收,多个使用英文逗号分开,如未指定则接收所有带有指定前缀的参数。

举个例子,比如用户修改个人信息的功能,在客户端传参示例:


this.bcc.call({  
    url : "admin/user/modify" ,  
    data : {  
        "x.password" : "123123123" ,  
        "x.mobile" : "15688585858" ,  
        "x.realAuth.contact_name" : "王大成" ,  
        "x.username" : "想改个名字能改吗" ,  
        "remark" : "个人的喜好"  
    }  
})  

此时我们需要只接收带x.前缀的参数,统一存放到data变量中,以便直接更新用户表的数据。
更新时,我们假设只允许用户修改passwordmobile字段,不允许修改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的业务场景。

  1. data 参数为即将要保存、更新的json数据,必填
  2. collection为要更新的集合对象,必填
  3. 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一共分成四个部分,用|符号分割:

  1. 要传入服务端的name
  2. 表单的标题,如果标题为空表示该表单可以为空。标题内可以含有:请输入、请上传、请选择 这三类提示文字。
  3. 校验规则,目前支持:mobile、email、idcard、count(整数)、amount(金额)、字符长度与长度范围等六种常见的表单验证和非空验证。
  4. 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项目构成

  1. common>base-cloud.scss 基础样式库,适配移动端和PC端,22kb。
  2. common>js>base-cloud-client.js 客户端SDK,14.2kb。
  3. cloudfunctions>common>base-cloud 云函数公共模块,13.9kb。
  4. components PC端常用业务组件目录

项目截图

演示项目地址:https://base-cloud.joiny.cn <<

账号:admin
密码:123123123

快速开始

  1. 请先下载BaseCloud管理后台项目模板,并导入到Hbuilder中
  2. 右键点击cloudfunctions目录,选择一个服务空间,支持阿里云、腾讯云。
  3. 找到cloudfunctions目录下的db_init.json数据库初始化文件,右键选择“初始化数据库”。
  4. 右键点击cloudfunctions目录,选择上传所有云函数以及公共模块。
  5. 点击运行到浏览器,运行成功后,在浏览器中进入登录页,初始账号: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函数入口文件

=====================================================================

服务端公共模块使用说明文档

【使用公共模块来接管云函数,定义多个访问路径】

  1. 根据公共模块引入说明来引入base-cloud公共模块;
  2. 在云函数的入口文件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);  
};  
  1. 在云函数中指定的业务函数根目录(此处是controller,你也可以指定其他的目录),创建js文件。
    并通过module.exports来导出业务处理的函数,可以导出一个或多个。
  2. 客户端访问云函数的路径规则为:云函数名称/根目录下的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/infoadmin/role/save 两个路径可以分别访问 controller>role.js>info()
controller>role.js>save(),客户端调用示例如下:

this.bcc.call({  
    url : "admin/role/info" ,  
    data : {_id : 1},  
    success : e=>{}  
});

【使用公共模块来配置全局的拦截器,以及拦截器的清理】

  1. cloudfunctions > common > base-cloud 目录下,找到 config.js 文件,
  2. 如下代码所示,在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) ;

  1. 第一个参数为prefix参数,指定要获取的参数的前缀符,未指定时,默认为x
  2. 第二个参数为keepKeys,指定一个或多个键名进行接收,多个使用英文逗号分开,如未指定则接收所有带有指定前缀的参数。

举个例子,比如用户修改个人信息的功能,在客户端传参示例:


this.bcc.call({  
    url : "admin/user/modify" ,  
    data : {  
        "x.password" : "123123123" ,  
        "x.mobile" : "15688585858" ,  
        "x.realAuth.contact_name" : "王大成" ,  
        "x.username" : "想改个名字能改吗" ,  
        "remark" : "个人的喜好"  
    }  
})  

此时我们需要只接收带x.前缀的参数,统一存放到data变量中,以便直接更新用户表的数据。
更新时,我们假设只允许用户修改passwordmobile字段,不允许修改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的业务场景。

  1. data 参数为即将要保存、更新的json数据,必填
  2. collection为要更新的集合对象,必填
  3. 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一共分成四个部分,用|符号分割:

  1. 要传入服务端的name
  2. 表单的标题,如果标题为空表示该表单可以为空。标题内可以含有:请输入、请上传、请选择 这三类提示文字。
  3. 校验规则,目前支持:mobile、email、idcard、count(整数)、amount(金额)、字符长度与长度范围等六种常见的表单验证和非空验证。
  4. 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

前言

因为各种版本的手机上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

uniapp 混合开发

前言

因业务需要 某些页面需要使用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。这样既能保证路由一致,又可以通过区分文件实现条件编译。

以上是本人在开发中遇到的实际问题和产生的一些想法,如有不完善的地方还请大家指教。

收起阅读 »

uni app蓝牙使用总结

App

1、支持低功耗BLE,不支持经典蓝牙(如蓝牙2.0)
2、只支持低功耗BLE协议
3、不支持蓝牙配对连接
4、目前官方文档说可以搜索得到所有蓝牙,可是真实情况是只能搜索到低功耗的蓝牙
5、在连接蓝牙后要延时一秒以上,否则获取蓝牙所有服务会失败,这个问题我是在论坛上找到的

继续阅读 »

1、支持低功耗BLE,不支持经典蓝牙(如蓝牙2.0)
2、只支持低功耗BLE协议
3、不支持蓝牙配对连接
4、目前官方文档说可以搜索得到所有蓝牙,可是真实情况是只能搜索到低功耗的蓝牙
5、在连接蓝牙后要延时一秒以上,否则获取蓝牙所有服务会失败,这个问题我是在论坛上找到的

收起阅读 »