This commit is contained in:
2023-11-07 17:11:57 +08:00
commit 1358e52fdb
643 changed files with 84924 additions and 0 deletions
@@ -0,0 +1,6 @@
## 0.0.3(2022-11-11)
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.2(2021-04-16)
- 修改插件package信息
## 0.0.1(2021-03-15)
- 初始化项目
@@ -0,0 +1,81 @@
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
+93
View File
@@ -0,0 +1,93 @@
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持)
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件(require)
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在
```
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0"
}
@@ -0,0 +1,70 @@
{
"passwordSecret": "passwordSecret-demo",
"tokenSecret": "tokenSecret-demo",
"tokenExpiresIn": 604800,
"tokenExpiresThreshold": 259200,
"tokenMaxLimit": 10,
"passwordErrorLimit": 6,
"bindTokenToDevice": false,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": true,
"forceInviteCode": false,
"preferedAppPlatform": "app-plus",
"preferedWebPlatform": "h5",
"removeDcloudAppid": false,
"app-plus": {
"tokenExpiresIn": 604800,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"h5-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-alipay": {
"oauth": {
"alipay": {
"appid": "",
"privateKey": ""
}
}
},
"mp-qq": {
"oauth": {
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"service": {
"sms": {
"name": "重要",
"codeExpiresIn": 180,
"smsKey": "",
"smsSecret": "",
"templateId": ""
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
}
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
### 文件名:
#### 1、`alipayCertPublicKey_RSA2.crt`
#### 2、`alipayRootCert.crt`
#### 3、`appCertPublicKey.crt`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,128 @@
const fs = require('fs');
const path = require('path');
module.exports = {
/**
* 统一支付回调地址,格式为 "服务空间SpaceID":"URL化完整地址"
* 这里的本地开发并不是指 http://localhost:8080/ 的地址,而是另外一个服务空间的ULR化地址(如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可)
* 回调的云函数地址,建议填 /http/vk-pay,因为vk-pay云函数已经写好了回调处理的逻辑,否则你需要自己写全部的回调逻辑。
* 其中vk-pay是可以改名的,只需要修改 uniCloud/cloudfunctions/vk-pay/package.json 文件中的 "path": "/http/vk-pay", 把 /http/vk-pay 改成 /http/xxxx 即可(需要重新上传云函数vk-pay)。
*/
"notifyUrl": {
// 本地开发环境,如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.bspapp.com/http/vk-pay",
// 线上正式环境
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://fc-mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.next.bspapp.com/http/vk-pay"
},
/**
* 微信
* 公共参数说明
* appId 微信后台的appId
* secret 微信后台的secret
* mchId 微信支付的商户id
* key 微信支付的api密钥(V2版本)
* pfx 微信支付的p12证书(退款需要)
*/
"wxpay": {
// 微信 - 小程序支付(微信支付申请JSAPI支付)
"mp-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - APP支付(微信支付申请APP支付)
"app-plus": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - H5电脑网站二维码支付(微信支付申请Native支付)
"h5": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 公众号支付(微信支付申请JSAPI支付)
"h5-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 手机外部浏览器H5支付(微信支付申请H5支付)
"mweb": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12'),
// 场景信息,必填
"sceneInfo": {
"h5_info": {
"type": "Wap", // 此值固定Wap
"wap_url": "https://www.xxxxxx.com", // 你的H5首页地址,必须和你发起支付的页面的域名一致。
"wap_name": "网站名称", // 你的H5网站名称
}
}
}
},
/**
* 支付宝(证书记得选java版本)
* 公共参数说明
* mchId 支付宝商户id 2088开头的那个(此参数可以去除)
* appId 支付宝开放平台的应用appId
* secret 支付宝开放平台的应用secret
* privateKey 应用私钥
* alipayPublicCertPath 支付宝公钥证书路径地址 与之对应的 alipayPublicCertContent 为支付宝公钥证书内容(值可以是字符串也可以是Buffer)
* alipayRootCertPath 支付宝根证书路径地址 与之对应的 alipayRootCertContent 为支付宝根证书内容(值可以是字符串也可以是Buffer)
* appCertPath 应用证书路径地址 与之对应的 appCertPathContent 为应用证书内容(值可以是字符串也可以是Buffer)
* sandbox 是否沙箱模式 true 沙箱模式 false 正常模式
*/
"alipay": {
// 支付宝 - 小程序支付配置
"mp-alipay": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - APP支付配置
"app-plus": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - H5支付配置(包含:网站二维码、手机H5,需申请支付宝当面付接口权限)
"h5": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - 转账到支付宝等资金转出接口,其中 appCertSn 和 alipayRootCertSn 通过工具获取
"transfer": {
"mchId": "",
"appId": "",
"privateKey": "",
"appCertSn": "", // 应用证书的序列号
"alipayRootCertSn": "", // 支付宝根证书的序列号
"sandbox": false
}
}
}
@@ -0,0 +1,3 @@
### 文件名:
#### 1、`wxpay.p12`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,141 @@
var uniIdConfig;
var uniPayConfig;
try {
uniIdConfig = require('../uni-id/config.json');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
try {
uniPayConfig = require('../uni-pay/config.js');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
module.exports = {
"uni": uniIdConfig,
"uni-pay": uniPayConfig,
"vk": {
"system": {
// 若serviceShutdow:true,则所有云函数无法访问(适用于需要临时关闭后端服务的情况,如迁移数据)
// 注意:本地调试时,需要重新启动本地服务才能生效。
"serviceShutdown": false,
"serviceShutdownDescription": "系统维护中,预计2小时恢复!",
// 云函数默认时区(中国为8)
"targetTimezone": 8
},
// 加密的密钥
"crypto": {
"aes": "dad4c0cb88c7d005bc5e844b8e79a9f0dad4c0cb88c7d005bc5e844b8e79a9f0", // 对称加密的密钥,建议长度在64位-128位(此处建议修改成自己的密钥)
},
// 当 context 内的下面值为空时,赋予默认的值(主要用于解决云函数 URL 后的默认 APPID 问题。
"context": {
"APPID": "", // 默认 dcloud_appid
"PLATFORM": "h5", // 可选项 h5、mp-weixin、app-plus、mp-alipay
"LOCALE": "zh-Hans", // 默认语言
"CLIENTIP":"127.0.0.1", // 默认客户端IP(主要用于解决本地调试无ip的问题)
},
"service": {
// 邮箱发送服务
"email": {
"codeExpiresIn": 180, // 邮件验证码有效期(单位秒)
// qq邮箱参数配置
"qq": {
"host": "smtp.qq.com",
"port": 465,
"secure": true,
"auth": {
"user": "你的邮箱@qq.com",
"pass": "邮箱授权码"
}
},
// 163邮箱配置
"163":{
"host": "smtp.163.com",
"port": 465,
"secure": true,
"auth": {
"user": "",
"pass": ""
}
}
},
// 日志服务
"log": {
// 用户登录日志
"login": {
"status": true // 是否开启用户登录日志
}
},
// 短信服务
"sms": {
// 阿里云短信服务
"aliyun": {
"enable": true, // 是否使用阿里云短信代替unicloud短信发送短信验证码
"accessKeyId": "LTAI4FyY6A493kVYn5ezyAw6", // 短信密钥key
"accessKeySecret": "776MyYZfRb8cQHTe5ji0jBVu1rHfa9", // 短信密钥secret
"signName": "亿利通", // 默认签名
"templateCode": {
"verifyCode": "SMS_210078693" // 验证码短信模板 - 配合uni-id需要
}
}
},
// 开放平台api
"openapi": {
// 百度开放平台 (主要用于身份证识别,营业执照识别等API)
// API Key申请地址:https://cloud.baidu.com/doc/OCR/s/rk3h7xzck 点击右上角注册
"baidu": {
"appid": "", // 对应的API Key
"appsecret": "" // 对应的Secret Key
}
}
},
"db": {
"unicloud": {
"maxLimit": 1000, // 最大limit限制(目前腾讯云最大1000,阿里云最大1000)
"cancelAddTime": false, // 取消 vk.baseDao.add 时自动生成 _add_time 和 _add_time_str
"cancelAddTimeStr": false, // 仅取消 vk.baseDao.add 时自动生成的 _add_time_str,若已设置 cancelAddTime 为true,则 cancelAddTimeStr 也会强制为true
"getTableData": {
"sortArr": [{ "name": "_add_time", "type": "desc" }], // vk.baseDao.getTableData 默认排序规则 vk.db.unicloud.getTableData.sortArr
}
}
},
// 其他小程序的密钥 当需要多个小程序绑定同一服务空间,并调用小程序服务端API时需要填写 暂只支持微信小程序
"oauth": {
// 微信小程序
"weixin": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 支付宝
"alipay": {
// 密钥列表
"list": [
{ "appid": "", "privateKey": "" },
{ "appid": "", "privateKey": "" }
]
},
// qq
"qq": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 抖音
"toutiao": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
}
}
}
};
@@ -0,0 +1,73 @@
## 重要说明
#### 框架目前的配置(不含`uni-id`)在 `cloudfunctions/common/uni-config-center/vk-unicloud/index.js` 中配置
#### `uni-id` 的配置是在 `cloudfunctions/common/uni-config-center/uni-id/config.json` 中配置
#### 每次修改完 配置 中的内容后如果不生效,则尝试以下操作。
```
1. 上传 uni-config-center 公共模块
2. 重新上传 router 云函数
如果是连接本地云函数,则需要重启本地服务才会生效。(hbx的原因)
```
### 由于`uni-id`配置无法打注释,故下方为`uni-id`配置的介绍
```js
{
"passwordSecret": "passwordSecret-demo",// 加密密码所用的密钥,修改会导致所用户之前的密码失效。如一定要修改,请查看https://uniapp.dcloud.io/uniCloud/uni-id?id=modifysecret
"tokenSecret": "tokenSecret-demo", // 生成token所用的密钥,修改会导致所有用户之前的token失效。
"tokenExpiresIn": 604800, // 全平台token过期时间,未指定过期时间的平台会使用此值,604800代表7天
"tokenExpiresThreshold": 3600, // 新增于uni-id 1.1.7版本,checkToken时如果token有效期小于此值则自动获取新token,如果不配置此参数则不开启自动获取新token功能
"passwordErrorLimit": 6, // 密码错误最大重试次数
"bindTokenToDevice": false, // 是否将token和设备绑定,设置为true会进行ua校验
"passwordErrorRetryTime": 3600, // 密码错误重试次数超限之后的冻结时间
"autoSetInviteCode": true, // 是否在用户注册时自动设置邀请码
"forceInviteCode": false, // 是否强制用户注册时必填邀请码,默认为false(需要注意的是目前只有短信验证码注册才可以填写邀请码)
"app-plus": {
"tokenExpiresIn": 604800, // app端 token过期时间
"oauth" : {
// App微信登录所用到的appid、appsecret需要在微信开放平台获取,注意:不是公众平台而是开放平台
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-weixin": {
"oauth" : {
// 微信小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-alipay": {
"oauth" : {
// 支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr
"alipay" : {
"appid" : "",
"privateKey" : ""
}
}
},
"service": {
// unicloud短信
"sms": {
"name": "重要", // 应用名称,对应短信模版的name
"codeExpiresIn": 180, // 验证码过期时间,单位为秒,注意一定要是60的整数倍
"smsKey": "你的smsKey", // 短信密钥key,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"smsSecret": "你的smsSecret", // 短信密钥secret,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"templateId":"你的短信模板ID" // 短信模板ID
},
// 一键登录
"univerify": {
"appid":"your appid", // uniapp的appid
"apiKey": "your apiKey", // 一键登录的apiKey
"apiSecret": "your apiSecret" // 一键登录的apiSecret
}
}
}
```
+111
View File
@@ -0,0 +1,111 @@
## 3.3.28(2022-07-27)
- 修复 app端微信登录返回的accessToken过期时间(expired)不正确的Bug
## 3.3.27(2022-07-27)
- 短信发送失败、微信登录失败等场景下输出原始错误方便排查错误
## 3.3.26(2022-07-08)
- 兼容配置放在uni-id下的逻辑,但是仍推荐使用uni-config-center
## 3.3.25(2022-06-30)
- 修复config文件不合法时未抛出具体错误的Bug
## 3.3.24(2022-06-28)
- 修复3.3.12引出的使用多应用配置时报错的Bug
## 3.3.23(2022-06-13)
- 修复上版本引出的部分依赖未找到的Bug
## 3.3.22(2022-06-13)
- 新增 preferedWebPlatform 配置用于解决HBuilderX 3.4.9版本起web端platform不一致的问题 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=prefered-web-platform)
## 3.3.21(2022-05-24)
- 修复createInstance传入clientInfo无效的Bug
## 3.3.20(2022-05-19)
- 调整以下错误码(账号已注册[uni-id-account-exists]、账号不存在[uni-id-account-not-exists]、账号已绑定[uni-id-account-bound])
## 3.3.19(2022-05-19)
- 修复 addUser 部分情况下会创建出重复账号的Bug
## 3.3.18(2022-05-12)
- 调整绑定、解绑邮箱手机号接口,只要传递code参数就进行验证码校验即使传递的值为undefined
## 3.3.17(2022-05-09)
- register_env内增加os_name字段用于区分注册时的客户端系统类型
## 3.3.16(2022-05-09)
- 修复 addUser接口添加的用户无法使用密码登录的Bug [详情](https://ask.dcloud.net.cn/question/144670)
## 3.3.15(2022-05-08)
- 修复config文件语法错误时报`this.t is not a function`的Bug 感谢@寒暄
## 3.3.14(2022-05-08)
- 新增 getWeixinUserInfo接口 用于获取app平台微信登录用户的用户信息 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#get-weixin-user-info)
- 新增 addUser接口 用于手动添加用户 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#add-user)
- 新增 resetPwdBySms接口 用于使用短信验证码重置密码 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#reset-pwd-by-sms)
- 新增 refreshToken接口 用于主动刷新用户token [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#refresh-token)
- 调整 用户注册时记录用户注册环境到 register_env 字段 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#user-table)
- 调整 用户注册时将注册 ip 移至 register_env 内
## 3.3.13(2022-03-04)
- createInstance方法支持传递clientInfo [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#create-instance)
- 修复`this.t is not a function`报错
## 3.3.12(2022-01-15)
- 新增 preferedAppPlatform 配置用于解决uni-app vue2版本vue3版本获取platform不一致的问题 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=prefered-app-platform)
- 修复 checkToken 未返回自定义token内容的Bug
## 3.3.11(2022-01-11)
- 修复用户名密码登录时多个应用出现重复用户名登录报错的Bug
## 3.3.10(2022-01-07)
- 新增 自定义国际化语言支持 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=custom-i8n)
- 修复 一键登录时未校验重复手机号是否已验证的Bug
- 修复 Apple登录时用户邮箱为空时报错的Bug
- 修复 登录接口未传username时错误提示不正确的Bug
## 3.3.9(2021-11-09)
- 去除重复的context.xxx未找到的提示语
## 3.3.8(2021-10-28)
- 新增 用户账户封禁接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=ban-account)
- 新增 用户账户注销接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=close-account)
- 修复 未传appid时用户重复注册的Bug
## 3.3.7(2021-10-08)
- 移除部分接口的废弃提示
## 3.3.6(2021-09-08)
- 修复 邀请码可能重复的Bug
## 3.3.5(2021-08-10)
- 修复版本号错误
## 3.3.4(2021-08-10)
- 微信、QQ、支付宝登录新增type参数用于指定当前是登录还是注册
## 3.3.3(2021-08-04)
- 修复使用数组形式的配置文件报错的Bug
## 3.3.2(2021-08-03)
- 修复上3.3.0版本引出的createInstance接口传入配置不生效的Bug 感谢[hmh](https://gitee.com/hmh)
## 3.3.1(2021-07-30)
- 修复 将设置用户允许登录的应用列表时传入空数组报错的Bug
## 3.3.0(2021-07-30)
- 新增 不同端应用配置隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-config)
- 新增 不同端用户隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-user)
+ 此版本升级需要开发者处理一下用户数据,请参考 [补齐用户dcloud_appid字段](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=makeup-dcloud-appid)
- 新增 QQ登录、注册相关功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=qq)
- 调整 不再支持绑定手机、邮箱时不填验证码直接绑定
## 3.2.1(2021-07-09)
- 撤销3.2.0版本所做的调整
## 3.2.0(2021-07-09)
- 【重要】支持不同端(管理端、用户端等)用户隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-user)
- 支持不同端(管理端、用户端等)配置文件隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-config)
## 3.1.3(2021-07-08)
- 移除插件内误传的node_modules
## 3.1.2(2021-07-08)
- 修复 微信小程序绑定微信账号时报错的Bug
## 3.1.1(2021-07-01)
- 使用新的错误码规范,兼容旧版 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=errcode)
- 修复微信登录、绑定时未返回用户accessToken的Bug
## 3.1.0(2021-04-19)
- 增加对用户名、邮箱、密码字段的两端去空格
- 默认忽略用户名、邮箱的大小写 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=case-sensitive)
- 修复 customToken导出async方法报错的Bug
## 3.0.12(2021-04-13)
- 调整bindTokenToDevice默认值为false
## 3.0.11(2021-04-12)
- 修复3.0.7版本引出的多个用户访问时可能出现30201报错的Bug
## 3.0.10(2021-04-08)
- 优化错误提示
## 3.0.9(2021-04-08)
- bindMobile接口支持通过一键登录的方式绑定
- 优化错误提示
## 3.0.8(2021-03-19)
- 修复 3.0.7版本某些情况下生成token报错的Bug
## 3.0.7(2021-03-19)
- 新增 支持uni-config-center,更新uni-id无须再担心配置被覆盖 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=uni-config-center)
- 新增 自定义token内容,可以缓存角色权限之外的更多信息到客户端 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=custom-token)
- 新增 支持传入context获取uni-id实例,防止单实例多并发时全局context混乱 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=create-instance)
## 3.0.6(2021-03-05)
- 新增[uniID.wxBizDataCrypt](https://uniapp.dcloud.io/uniCloud/uni-id?id=%e5%be%ae%e4%bf%a1%e6%95%b0%e6%8d%ae%e8%a7%a3%e5%af%86)方法
- 优化loginByApple方法,提高接口响应速度
## 3.0.5(2021-02-03)
- 调整为uni_modules目录规范
+85
View File
@@ -0,0 +1,85 @@
{
"id": "uni-id",
"displayName": "uni-id",
"version": "3.3.29",
"description": "简单、统一、可扩展的用户中心",
"keywords": [
"uniid",
"uni-id",
"用户管理",
"用户中心",
"短信验证码"
],
"repository": "https://gitee.com/dcloud/uni-id.git",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
**文档已移至[uni-id文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id)**
> 一般uni-id升级大版本时为不兼容更新,从低版本迁移到高版本请参考:[uni-id迁移指南](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=migration)
## 重要升级说明
**uni-id 3.x版本,搭配的uniCloud admin版本需大于1.2.10。**
### 缓存角色权限
自`uni-id 3.0.0`起,支持在token内缓存用户的角色权限,默认开启此功能,各登录接口的needPermission参数不再生效。如需关闭请在config内配置`"removePermissionAndRoleFromToken": true`。
为什么要缓存角色权限?要知道云数据库是按照读写次数来收取费用的,并且读写数据库会拖慢接口响应速度。未配置`"removePermissionAndRoleFromToken": true`的情况下,可以在调用checkToken接口时不查询数据库获取用户角色权限。
详细checkToken流程如下:
![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/ed45d350-5a4d-11eb-b997-9918a5dda011.jpg)
可以看出,旧版token(removePermissionAndRoleFromToken为true时生成的)在checkToken时如需返回权限需要进行两次数据库查询。新版token不需要查库即可返回权限信息。
**注意**
- 由于角色权限缓存在token内,可能会存在权限已经更新但是用户token未过期之前依然是旧版角色权限的情况。可以调短一些token过期时间来减少这种情况的影响。
- admin角色token内不包含permission,如需自行判断用户是否有某个权限,要注意admin角色需要额外判断一下,写法如下
```js
const {
role,
permission
} = await uniID.checkToken(event.uniIdToken)
if(role.includes('admin') || permission.includes('your permission id')) {
// 当前角色拥有'your permission id'对应的权限
}
```
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0"
}
@@ -0,0 +1,70 @@
{
"passwordSecret": "passwordSecret-demo",
"tokenSecret": "tokenSecret-demo",
"tokenExpiresIn": 604800,
"tokenExpiresThreshold": 259200,
"tokenMaxLimit": 10,
"passwordErrorLimit": 6,
"bindTokenToDevice": false,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": true,
"forceInviteCode": false,
"preferedAppPlatform": "app-plus",
"preferedWebPlatform": "h5",
"removeDcloudAppid": false,
"app-plus": {
"tokenExpiresIn": 604800,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"h5-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-alipay": {
"oauth": {
"alipay": {
"appid": "",
"privateKey": ""
}
}
},
"mp-qq": {
"oauth": {
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"service": {
"sms": {
"name": "重要",
"codeExpiresIn": 180,
"smsKey": "",
"smsSecret": "",
"templateId": ""
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
}
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
### 文件名:
#### 1、`alipayCertPublicKey_RSA2.crt`
#### 2、`alipayRootCert.crt`
#### 3、`appCertPublicKey.crt`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,128 @@
const fs = require('fs');
const path = require('path');
module.exports = {
/**
* 统一支付回调地址,格式为 "服务空间SpaceID":"URL化完整地址"
* 这里的本地开发并不是指 http://localhost:8080/ 的地址,而是另外一个服务空间的ULR化地址(如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可)
* 回调的云函数地址,建议填 /http/vk-pay,因为vk-pay云函数已经写好了回调处理的逻辑,否则你需要自己写全部的回调逻辑。
* 其中vk-pay是可以改名的,只需要修改 uniCloud/cloudfunctions/vk-pay/package.json 文件中的 "path": "/http/vk-pay", 把 /http/vk-pay 改成 /http/xxxx 即可(需要重新上传云函数vk-pay)。
*/
"notifyUrl": {
// 本地开发环境,如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.bspapp.com/http/vk-pay",
// 线上正式环境
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://fc-mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.next.bspapp.com/http/vk-pay"
},
/**
* 微信
* 公共参数说明
* appId 微信后台的appId
* secret 微信后台的secret
* mchId 微信支付的商户id
* key 微信支付的api密钥(V2版本)
* pfx 微信支付的p12证书(退款需要)
*/
"wxpay": {
// 微信 - 小程序支付(微信支付申请JSAPI支付)
"mp-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - APP支付(微信支付申请APP支付)
"app-plus": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - H5电脑网站二维码支付(微信支付申请Native支付)
"h5": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 公众号支付(微信支付申请JSAPI支付)
"h5-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 手机外部浏览器H5支付(微信支付申请H5支付)
"mweb": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12'),
// 场景信息,必填
"sceneInfo": {
"h5_info": {
"type": "Wap", // 此值固定Wap
"wap_url": "https://www.xxxxxx.com", // 你的H5首页地址,必须和你发起支付的页面的域名一致。
"wap_name": "网站名称", // 你的H5网站名称
}
}
}
},
/**
* 支付宝(证书记得选java版本)
* 公共参数说明
* mchId 支付宝商户id 2088开头的那个(此参数可以去除)
* appId 支付宝开放平台的应用appId
* secret 支付宝开放平台的应用secret
* privateKey 应用私钥
* alipayPublicCertPath 支付宝公钥证书路径地址 与之对应的 alipayPublicCertContent 为支付宝公钥证书内容(值可以是字符串也可以是Buffer)
* alipayRootCertPath 支付宝根证书路径地址 与之对应的 alipayRootCertContent 为支付宝根证书内容(值可以是字符串也可以是Buffer)
* appCertPath 应用证书路径地址 与之对应的 appCertPathContent 为应用证书内容(值可以是字符串也可以是Buffer)
* sandbox 是否沙箱模式 true 沙箱模式 false 正常模式
*/
"alipay": {
// 支付宝 - 小程序支付配置
"mp-alipay": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - APP支付配置
"app-plus": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - H5支付配置(包含:网站二维码、手机H5,需申请支付宝当面付接口权限)
"h5": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - 转账到支付宝等资金转出接口,其中 appCertSn 和 alipayRootCertSn 通过工具获取
"transfer": {
"mchId": "",
"appId": "",
"privateKey": "",
"appCertSn": "", // 应用证书的序列号
"alipayRootCertSn": "", // 支付宝根证书的序列号
"sandbox": false
}
}
}
@@ -0,0 +1,3 @@
### 文件名:
#### 1、`wxpay.p12`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,141 @@
var uniIdConfig;
var uniPayConfig;
try {
uniIdConfig = require('../uni-id/config.json');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
try {
uniPayConfig = require('../uni-pay/config.js');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
module.exports = {
"uni": uniIdConfig,
"uni-pay": uniPayConfig,
"vk": {
"system": {
// 若serviceShutdow:true,则所有云函数无法访问(适用于需要临时关闭后端服务的情况,如迁移数据)
// 注意:本地调试时,需要重新启动本地服务才能生效。
"serviceShutdown": false,
"serviceShutdownDescription": "系统维护中,预计2小时恢复!",
// 云函数默认时区(中国为8)
"targetTimezone": 8
},
// 加密的密钥
"crypto": {
"aes": "dad4c0cb88c7d005bc5e844b8e79a9f0dad4c0cb88c7d005bc5e844b8e79a9f0", // 对称加密的密钥,建议长度在64位-128位(此处建议修改成自己的密钥)
},
// 当 context 内的下面值为空时,赋予默认的值(主要用于解决云函数 URL 后的默认 APPID 问题。
"context": {
"APPID": "", // 默认 dcloud_appid
"PLATFORM": "h5", // 可选项 h5、mp-weixin、app-plus、mp-alipay
"LOCALE": "zh-Hans", // 默认语言
"CLIENTIP":"127.0.0.1", // 默认客户端IP(主要用于解决本地调试无ip的问题)
},
"service": {
// 邮箱发送服务
"email": {
"codeExpiresIn": 180, // 邮件验证码有效期(单位秒)
// qq邮箱参数配置
"qq": {
"host": "smtp.qq.com",
"port": 465,
"secure": true,
"auth": {
"user": "你的邮箱@qq.com",
"pass": "邮箱授权码"
}
},
// 163邮箱配置
"163":{
"host": "smtp.163.com",
"port": 465,
"secure": true,
"auth": {
"user": "",
"pass": ""
}
}
},
// 日志服务
"log": {
// 用户登录日志
"login": {
"status": true // 是否开启用户登录日志
}
},
// 短信服务
"sms": {
// 阿里云短信服务
"aliyun": {
"enable": true, // 是否使用阿里云短信代替unicloud短信发送短信验证码
"accessKeyId": "LTAI4FyY6A493kVYn5ezyAw6", // 短信密钥key
"accessKeySecret": "776MyYZfRb8cQHTe5ji0jBVu1rHfa9", // 短信密钥secret
"signName": "亿利通", // 默认签名
"templateCode": {
"verifyCode": "SMS_210078693" // 验证码短信模板 - 配合uni-id需要
}
}
},
// 开放平台api
"openapi": {
// 百度开放平台 (主要用于身份证识别,营业执照识别等API)
// API Key申请地址:https://cloud.baidu.com/doc/OCR/s/rk3h7xzck 点击右上角注册
"baidu": {
"appid": "", // 对应的API Key
"appsecret": "" // 对应的Secret Key
}
}
},
"db": {
"unicloud": {
"maxLimit": 1000, // 最大limit限制(目前腾讯云最大1000,阿里云最大1000)
"cancelAddTime": false, // 取消 vk.baseDao.add 时自动生成 _add_time 和 _add_time_str
"cancelAddTimeStr": false, // 仅取消 vk.baseDao.add 时自动生成的 _add_time_str,若已设置 cancelAddTime 为true,则 cancelAddTimeStr 也会强制为true
"getTableData": {
"sortArr": [{ "name": "_add_time", "type": "desc" }], // vk.baseDao.getTableData 默认排序规则 vk.db.unicloud.getTableData.sortArr
}
}
},
// 其他小程序的密钥 当需要多个小程序绑定同一服务空间,并调用小程序服务端API时需要填写 暂只支持微信小程序
"oauth": {
// 微信小程序
"weixin": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 支付宝
"alipay": {
// 密钥列表
"list": [
{ "appid": "", "privateKey": "" },
{ "appid": "", "privateKey": "" }
]
},
// qq
"qq": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 抖音
"toutiao": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
}
}
}
};
@@ -0,0 +1,73 @@
## 重要说明
#### 框架目前的配置(不含`uni-id`)在 `cloudfunctions/common/uni-config-center/vk-unicloud/index.js` 中配置
#### `uni-id` 的配置是在 `cloudfunctions/common/uni-config-center/uni-id/config.json` 中配置
#### 每次修改完 配置 中的内容后如果不生效,则尝试以下操作。
```
1. 上传 uni-config-center 公共模块
2. 重新上传 router 云函数
如果是连接本地云函数,则需要重启本地服务才会生效。(hbx的原因)
```
### 由于`uni-id`配置无法打注释,故下方为`uni-id`配置的介绍
```js
{
"passwordSecret": "passwordSecret-demo",// 加密密码所用的密钥,修改会导致所用户之前的密码失效。如一定要修改,请查看https://uniapp.dcloud.io/uniCloud/uni-id?id=modifysecret
"tokenSecret": "tokenSecret-demo", // 生成token所用的密钥,修改会导致所有用户之前的token失效。
"tokenExpiresIn": 604800, // 全平台token过期时间,未指定过期时间的平台会使用此值,604800代表7天
"tokenExpiresThreshold": 3600, // 新增于uni-id 1.1.7版本,checkToken时如果token有效期小于此值则自动获取新token,如果不配置此参数则不开启自动获取新token功能
"passwordErrorLimit": 6, // 密码错误最大重试次数
"bindTokenToDevice": false, // 是否将token和设备绑定,设置为true会进行ua校验
"passwordErrorRetryTime": 3600, // 密码错误重试次数超限之后的冻结时间
"autoSetInviteCode": true, // 是否在用户注册时自动设置邀请码
"forceInviteCode": false, // 是否强制用户注册时必填邀请码,默认为false(需要注意的是目前只有短信验证码注册才可以填写邀请码)
"app-plus": {
"tokenExpiresIn": 604800, // app端 token过期时间
"oauth" : {
// App微信登录所用到的appid、appsecret需要在微信开放平台获取,注意:不是公众平台而是开放平台
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-weixin": {
"oauth" : {
// 微信小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-alipay": {
"oauth" : {
// 支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr
"alipay" : {
"appid" : "",
"privateKey" : ""
}
}
},
"service": {
// unicloud短信
"sms": {
"name": "重要", // 应用名称,对应短信模版的name
"codeExpiresIn": 180, // 验证码过期时间,单位为秒,注意一定要是60的整数倍
"smsKey": "你的smsKey", // 短信密钥key,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"smsSecret": "你的smsSecret", // 短信密钥secret,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"templateId":"你的短信模板ID" // 短信模板ID
},
// 一键登录
"univerify": {
"appid":"your appid", // uniapp的appid
"apiKey": "your apiKey", // 一键登录的apiKey
"apiSecret": "your apiSecret" // 一键登录的apiSecret
}
}
}
```
@@ -0,0 +1,16 @@
{
"name": "uni-id",
"version": "3.3.28",
"description": "uni-id for uniCloud",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-id.git"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,320 @@
<template>
<view class="vk-data-verification-code">
<view @click="sendSmsCode" :style="customStyle">
<slot :tips="tips" :sec-num="secNum">{{ tips }}</slot>
</view>
</view>
</template>
<script>
const localeObj = {
"zh-Hans":{
"startText":"获取验证码",
"changeText":"X秒重新获取",
"endText":"重新获取",
"tryAgainInSeconds":"秒后再重试",
"pleaseEnterTheCorrectMobileNumber":"请输入正确的手机号码",
"sending":"发送中...",
"verificationCodeSent":"验证码已发送",
"triggerDayLevelFlowControl":"触发天级流控",
"pleaseTryAgainTomorrow":"短信发送频繁,请明日再试!",
"pleaseTryAgainIn1Hour":"短信发送频繁,请过1小时后再试!",
"triggerMinuteLevelFlowControl":"触发分钟级流控",
"pleaseTryAgainLater":"短信发送频繁,请稍后再试!",
},
"zh-Hant":{
"startText":"獲取驗證碼",
"changeText":"X秒重新獲取",
"endText":"重新獲取",
"tryAgainInSeconds":"秒後再重試",
"pleaseEnterTheCorrectMobileNumber":"請輸入正確的手機號碼",
"sending":"發送中...",
"verificationCodeSent":"驗證碼已發送",
"triggerDayLevelFlowControl":"觸發天級流控",
"pleaseTryAgainTomorrow":"短信發送頻繁,請明日再試!",
"pleaseTryAgainIn1Hour":"短信發送頻繁,請過1小時後再試!",
"triggerMinuteLevelFlowControl":"觸發分鐘級流控",
"pleaseTryAgainLater":"短信發送頻繁,請稍後再試!",
},
"en":{
"startText":"Get code",
"changeText":"X second reacquire",
"endText":"Reacquire",
"tryAgainInSeconds":"Try again in seconds",
"pleaseEnterTheCorrectMobileNumber":"Please enter the correct mobile number",
"sending":"Sending...",
"verificationCodeSent":"Verification code sent",
"triggerDayLevelFlowControl":"Trigger day level flow control",
"pleaseTryAgainTomorrow":"SMS sent frequently, please try again tomorrow!",
"pleaseTryAgainIn1Hour":"SMS sent frequently, please try again in 1 hour",
"triggerMinuteLevelFlowControl":"Trigger minute level flow control",
"pleaseTryAgainLater":"SMS sent frequently, please try again later",
}
};
/**
* verificationCode 验证码输入框
* @property {String} mode 模式,mobile 手机验证码 custom 自定义验证码 默认 mobile
* @property {String} mobile 接收短信的手机号
* @property {String} type 短信类型,如 register、login、bind、unbind
* @property {Number String} seconds 倒计时所需的秒数(默认60)
* @property {Object String} customStyle 自定义样式
* @property {String} start-text 开始前的提示语,见官网说明(默认获取验证码)
* @property {String} change-text 倒计时期间的提示语,必须带有字母"x",见官网说明(默认X秒重新获取)
* @property {String} end-text 倒计结束的提示语,见官网说明(默认重新获取)
* @property {Boolean} keep-running 是否在H5刷新或各端返回再进入时继续倒计时(默认false)
* @event {Function} change 倒计时期间,每秒触发一次
* @event {Function} start 开始倒计时触发
* @event {Function} end 结束倒计时触发
* @event {Function} send 自定义发送事件 当mode=custom时会有此事件
* @example <vk-data-verification-code seconds="60" :mobile="form1.mobile" type="register" custom-style="font-size: 28rpx;"></vk-data-verification-code>
*/
export default {
name: 'vk-data-verification-code',
emits: ['start', 'end', 'change','send','codeChange','error','success'],
props: {
// 模式,mobile 手机验证码 custom 自定义验证码 默认 mobile
mode: {
type: String,
default: 'mobile'
},
// 接收短信的手机号
mobile: {
type: String
},
// 短信类型,如 register、login、bind、unbind
type: {
type: String,
default: 'login'
},
customStyle: {
type: [String,Object]
},
// 倒计时总秒数
seconds: {
type: [String, Number],
default: 60
},
// 尚未开始时提示
startText: {
type: String,
default: ''
},
// 正在倒计时中的提示
changeText: {
type: String,
default: ''
},
// 倒计时结束时的提示
endText: {
type: String,
default: ''
},
// 是否在H5刷新或各端返回再进入时继续倒计时
keepRunning: {
type: Boolean,
default: false
},
// 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
uniqueKey: {
type: String,
default: ''
},
// 是否需要检测手机号对应的账号是否存在,默认false:不检测 设置为true:会检测,如果检测到用户不存在,则不发短信。
checkUserExist: {
type: Boolean,
default: false
},
},
data() {
return {
secNum: this.seconds,
timer: null,
canGetCode: true, // 是否可以执行验证码操作
tips: '',
locale:{}, // 语言
};
},
created(){
if (typeof vk !== "undefined") {
let locale = vk.pubfn.getLocale();
this.locale = localeObj[locale];
} else {
this.locale = localeObj["zh-Hans"];
}
},
mounted() {
this.checkKeepRunning();
},
watch: {
seconds: {
immediate: true,
handler(n) {
this.secNum = n;
}
}
},
// 计算属性
computed:{
startTextCom(){
return this.startText || this.locale.startText || "获取验证码";
},
changeTextCom(){
return this.changeText || this.locale.changeText || "X秒重新获取";
},
endTextCom(){
return this.endText || this.locale.endText || "重新获取";
}
},
methods: {
checkKeepRunning() {
// 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$uCountDownTimestamp'));
if (!lastTimestamp) return this.changeEvent(this.startTextCom);
// 当前秒的时间戳
let nowTimestamp = Math.floor(+new Date() / 1000);
// 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
if (this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
// 剩余尚未执行完的倒计秒数
this.secNum = lastTimestamp - nowTimestamp;
// 清除本地保存的变量
uni.removeStorageSync(this.uniqueKey + '_$uCountDownTimestamp');
// 开始倒计时
this.start();
} else {
// 如果不存在需要继续上一次的倒计时,执行正常的逻辑
this.changeEvent(this.startTextCom);
}
},
// 开始倒计时
start() {
// 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.$emit('start');
this.canGetCode = false;
// 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
this.changeEvent(this.changeTextCom.replace(/x|X/, this.secNum));
this.setTimeToStorage();
this.timer = setInterval(() => {
if (--this.secNum) {
// 用当前倒计时的秒数替换提示字符串中的"x"字母
this.changeEvent(this.changeTextCom.replace(/x|X/, this.secNum));
} else {
clearInterval(this.timer);
this.timer = null;
this.changeEvent(this.endTextCom);
this.secNum = this.seconds;
this.$emit('end');
this.canGetCode = true;
}
}, 1000);
},
// 重置,可以让用户再次获取验证码
reset(text) {
this.canGetCode = true;
clearInterval(this.timer);
this.secNum = this.seconds;
this.changeEvent(text || this.endTextCom);
},
changeEvent(text) {
this.codeChange(text);
this.$emit('change', text);
},
// 保存时间戳,为了防止倒计时尚未结束,H5刷新或者各端的右上角返回上一页再进来
setTimeToStorage() {
if (!this.keepRunning || !this.timer) return;
// 记录当前的时间戳,为了下次进入页面,如果还在倒计时内的话,继续倒计时
// 倒计时尚未结束,结果大于0;倒计时已经开始,就会小于初始值,如果等于初始值,说明没有开始倒计时,无需处理
if (this.secNum > 0 && this.secNum <= this.seconds) {
// 获取当前时间戳(+ new Date()为特殊写法),除以1000变成秒,再去除小数部分
let nowTimestamp = Math.floor(+new Date() / 1000);
// 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
uni.setStorage({
key: this.uniqueKey + '_$uCountDownTimestamp',
data: nowTimestamp + Number(this.secNum)
});
}
},
// 倒计时进行时
codeChange(text) {
this.tips = text;
this.$emit('codeChange', text);
},
// 发送短信验证码
sendSmsCode() {
let that = this;
let vk = uni.vk;
let {
mobile,
type,
canGetCode,
checkUserExist,
secNum,
mode,
} = that;
if (!canGetCode) {
vk.toast(`${secNum}${that.locale.tryAgainInSeconds}`, 'none');
return;
}
if (mode === "custom") {
that.$emit("send", { type: type });
return;
}
if (!vk.pubfn.test(mobile, 'mobile')) {
vk.toast(that.locale.pleaseEnterTheCorrectMobileNumber, 'none');
return;
}
that.tips = that.locale.sending;
vk.userCenter.sendSmsCode({
needAlert: false,
data: {
mobile,
type,
checkUserExist
},
success: function(data) {
vk.toast(that.locale.verificationCodeSent);
that.start();
that.$emit('success', data);
},
fail: function(err) {
that.tips = that.startTextCom;
if (err.errMsg && err.errMsg.indexOf('触发天级流控') > -1) {
vk.alert(that.locale.pleaseTryAgainTomorrow,()=>{ that.$emit('error', err); });
} else if (err.errMsg && err.errMsg.indexOf('触发小时级流控') > -1) {
vk.alert(that.locale.pleaseTryAgainIn1Hour,()=>{ that.$emit('error', err); });
} else if (err.errMsg && err.errMsg.indexOf('触发分钟级流控') > -1) {
vk.alert(that.locale.pleaseTryAgainLater,()=>{ that.$emit('error', err); });
} else if (err.msg) {
vk.alert(err.msg,()=>{ that.$emit('error', err); });
} else {
vk.alert(that.locale.pleaseTryAgainLater,()=>{ that.$emit('error', err); });
}
}
});
}
},
// 组件销毁的时候,清除定时器,否则定时器会继续存在,系统不会自动清除
// #ifndef VUE3
beforeDestroy() {
this.setTimeToStorage();
clearTimeout(this.timer);
this.timer = null;
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.setTimeToStorage();
clearTimeout(this.timer);
this.timer = null;
}
// #endif
};
</script>
<style lang="scss" scoped></style>
+7
View File
@@ -0,0 +1,7 @@
/**
* vk-unicloud框架客户端(前端)
* author VK
*/
import vk from './vk_modules/vk-unicloud-page'
export default vk;
+86
View File
@@ -0,0 +1,86 @@
{
"id": "vk-unicloud",
"displayName": "vk-unicloud-router开发框架核心库 - 已集成uni-id 框架内置了众多API。",
"version": "2.15.3",
"description": "此为vk-unicloud-router框架核心库(新手建议下载完整框架项目)已集成uni-id支持云函数url化。众多现成API,内置小白也能轻松上手的数据库API。使你项目刚起步进度就是百分之50",
"keywords": [
"vk-unicloud-router",
"云函数路由、云对象路由",
"vk云开发",
"内置uni-id、uview、数据库baseDao",
"内置众多API、工具包"
],
"author": "VK",
"repository": "https://gitee.com/vk-uni/vk-uni-cloud-router",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "370725567"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "https://vkdoc.fsq.pub/",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center","uni-id"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
+572
View File
@@ -0,0 +1,572 @@
### uniCloud云函数路由框架研究Q群:22466457 如有问题或建议可以在群内讨论。
### 插件名称:`vk-unicloud-router`
### 作者:VK
## 序
##### 如果你热爱编程,想快速入门云开发,欢迎使用`vk-unicloud`系列开发框架
##### 无需转变开发习惯,0成本上手云开发。
##### 框架内置了众多API、工具包,为你的业务扫平障碍。使你的项目刚起步进度就是50%(微信登录、短信、验证码、缓存、生成小程序码等等)
##### 从此你又get一个新技能,只需用js,轻松搞定前后台整体业务。
##### 完整框架项目地址:
##### `client端`框架地址: [https://ext.dcloud.net.cn/plugin?id=2204](https://ext.dcloud.net.cn/plugin?id=2204)
##### `admin端`框架地址:[https://ext.dcloud.net.cn/plugin?id=5043](https://ext.dcloud.net.cn/plugin?name=vk-unicloud-admin)
##### `client端`框架文档:[点击查看](https://vkdoc.fsq.pub/client/)
##### `admin端`框架文档:[点击查看](https://vkdoc.fsq.pub/admin/)
##### 框架学习Q群:`22466457` 欢迎萌新和大佬来使用和共同改进框架
## 主要功能 以及 对开发者的价值
#### 1、实现云函数路由模式(同时支持对云对象的路由)
* 1.1、路由模式不使用任何第三方包,兼容性强,运行稳定
* 1.2、减少云函数数量,云函数有个数限制(用一个云函数即可实现多个云函数效果)
* 1.3、部分通用的代码块可以放在公共区执行,具有公共函数的效果,增加代码复用率,便于维护。
* 1.4、可以在开发环境和生产环境中任意切换,开发时,用开发环境,开发过程中不会影响到线上环境。(具体切换方法在文档最后)
* 1.5、美化云函数请求过程中的日志排版,使请求日志一目了然。(再也不用为日志分散,难找而头疼)
* 1.6、可以强制关闭云端服务(适用于需要临时关闭后端服务的情况,如腾讯云迁移数据到阿里云时需要先关闭服务,否则迁移过程中会有新数据产生)
#### 2、实现 `全局过滤器`,过滤非法请求。您无需关心用户角色权限问题。
#### 3、众多现成的 `JS API`,让您无需重复造轮子。(持续增加中)
#### 4、众多现成的 `云函数 API`,为您的业务扫平障碍,让您可以专注于自身业务开发。(微信登录、短信、验证码、缓存、生成小程序码等等)(持续增加中)
#### 5、全面支持 `url化` 的云函数请求,您无需关心url化后参数的获取问题
#### 6、已集成 `uni-id` 当前版本: `3.3.28`(已支持真实发送邮箱验证码和短信验证码)
* 6.1、由于每个应用基本都会用上用户系统,官方建议所有 `uniCloud` 应用均使用 `uni-id` 作为用户体系,
有了统一的账户规范,并且围绕这套账户规范,有各种各样插件,那么开发者可以随意整合这些插件,让数据连同。
* 6.2、同时方便其他用户使用 `unicloud` 插件发布者发布的前后端一体插件,只需要导入一个云函数即可。
(如导入一个社区插件,可能要导入几十个云函数,而使用此云函数路由后,只需导入一个云函数,且已集成 `uni-id`,打通账号体系)。
#### 7、在 `uni-id` 基础上升级为 `vk.userCenter` 用户中心API 通过this.vk.userCenter 即可调用 [传送门](https://vkdoc.fsq.pub/client/vk.userCenter.html)
#### 8、封装 `uni.callFunction` 和 `uni.request` 使之合二为一 通过this.vk.callFunction 调用
#### 9、`vk.baseDao` 数据库API,使小白也能轻松上手对数据库的调用。 [传送门](https://vkdoc.fsq.pub/client/uniCloud/db/api.html)
#### 10、`vk.baseDao.selects` 数据库万能连表查询API [传送门](https://vkdoc.fsq.pub/client/uniCloud/db/selects.html)
#### 11、`商品SKU选择器组件`(打造uni插件市场功能最全的SKU选择器组件)[传送门](https://ext.dcloud.net.cn/plugin?id=2848)
#### 12、对 `Vuex` 进行了深度封装(支持持久化),现在可以很方便的使用Vuex进行读取和储存。[传送门](https://vkdoc.fsq.pub/client/pages/vuex.html)
#### 13、自定义过滤器(在业务云函数执行之前(或之后),统一拦截,进行过滤后再放行,支持设置多个过滤器,并按指定顺序执行)[传送门](https://vkdoc.fsq.pub/client/uniCloud/middleware/filter.html)
#### 14、支持设置需要登录的页面列表或不需要登录的页面列表(通过本地token检测,无网络请求,支持通配符写法)[传送门](https://vkdoc.fsq.pub/client/pages/config.html)
#### 15、支持设置可以被分享的页面列表或禁止分享的页面列表(支持通配符写法)[传送门](https://vkdoc.fsq.pub/client/pages/config.html)
#### 16、`vk.localStorage` 封装本地缓存的curd,同时具有监听缓存被更新和被删除的功能。[传送门](https://vkdoc.fsq.pub/client/pages/localStorage.html)
#### 17、美化云函数请求过程中的日志排版,使请求日志一目了然。
![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/4a44fbf8-6b5e-43a6-b443-51a108dec125.png)
#### 18、可以 `强制关闭` 云端服务(适用于需要临时关闭后端服务的情况,如腾讯云迁移数据到阿里云时需要先关闭服务,否则迁移过程中会有新数据产生)
#### 19、全局数据缓存API,现在可以很方便的在云函数中使用缓存。 [传送门](https://vkdoc.fsq.pub/client/uniCloud/cache/cache.html)
#### 20【全网首家】云函数URL化之URL重写 [传送门](https://vkdoc.fsq.pub/client/uniCloud/cloudfunctions/urlrewrite.html)
#### 21、千人群交流开发心得,助你避坑,作者亲自在群内解答框架使用问题,让你轻松上手 `uniCloud云开发`。
#### 22、其他好处…
### 目前已集成的服务端API
#### 1、微信小程序服务端API [传送门](https://vkdoc.fsq.pub/client/uniCloud/plus/weixin.html)
#### 2、微信公众号服务端API [传送门](https://vkdoc.fsq.pub/client/uniCloud/plus/weixin-h5.html)
#### 3、百度开放平台服务端API [传送门](https://vkdoc.fsq.pub/client/uniCloud/plus/baidu.html)
#### 4、短信发送 [传送门](https://vkdoc.fsq.pub/client/uniCloud/plus/sms.html)
#### 5、邮箱发送 [传送门](https://vkdoc.fsq.pub/client/uniCloud/plus/mail.html)
#### 6、Redis [传送门](https://vkdoc.fsq.pub/vk-redis/)
##### 插件部分API功能体验地址
![插件首页体验地址](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-vk-cloud-router-test/51de83e0-e9ae-11ea-81ea-f115fe74321c.png?x-oss-process=image/resize,h_250 "插件首页体验地址")
## 快速上手 - 安装步骤
## 后端(云函数端)安装步骤
打开 `uniCloud/cloudfunctions` 目录
#### 前置工作:修改云函数全局配置文件(用到微信登录的必须要配置)[微信小程序配置详细教程](https://vkdoc.fsq.pub/client/question/q12.html)
#### 注意:
* 1、开发微信小程序和APP微信登录需要额外配置 manifest.json 以及 uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 这2个配置文件,且改动配置后需要重新上传 公共模块 和 router 函数。
* 2、若你的电脑没有安装 Node.js,则无法使用 npm 命令。
* 3、Node.js 安装包及源码下载地址为:[https://nodejs.org/en/download/](https://nodejs.org/en/download/)
* 4、Node.js 安装教程:[https://www.runoob.com/nodejs/nodejs-install-setup.html](https://www.runoob.com/nodejs/nodejs-install-setup.html)
查看 `uni-id/config.json` 代码格式 [点击查看](https://vkdoc.fsq.pub/client/uniCloud/config/uni-id.html)
#### 正式安装
直接右键`uniCloud`目录 点击 `运行云服务空间初始化向导` 即可
## 前端(页面端)安装步骤
### Vue2.0版本
`main.js` 引入 `vk-unicloud-page` 库
```js
// 引入 vk框架前端
import vk from './uni_modules/vk-unicloud';
Vue.use(vk);
```
完整 `main.js` 示例
```js
import Vue from 'vue'
import App from './App'
import store from './store'
import config from '@/app.config.js'
// 引入 vk框架前端
import vk from './uni_modules/vk-unicloud';
Vue.use(vk);
// 初始化 vk框架
Vue.prototype.vk.init({
Vue, // Vue实例
config, // 全局配置
});
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
store,
...App
});
app.$mount();
```
### Vue3.0版本
`main.js` 引入 `vk-unicloud-page` 库
完整 `main.js` 示例
```js
import App from './App'
import store from './store'
import config from '@/app.config.js'
// 引入 vk框架前端
import vk from './uni_modules/vk-unicloud';
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
// 使用 vuex
app.use(store)
// 使用 vk框架前端
app.use(vk);
// 初始化 vk框架
app.config.globalProperties.vk.init({
Vue: app, // Vue实例
config, // 配置
});
return { app }
}
```
### 自 client端框架 2.6.0 起,不再内置任何 UI 框架,你可以选择自己喜欢的 UI 组件库进行开发。
### 集成 `vk-uview-ui`(vue2.0版)
___若不想集成 `vk-uview-ui` 可跳过此处___
适合开发:H5、App(vue版本)、微信小程序、支付宝小程序(其他小程序未验证)
> 插件市场导入 `vk-uview-ui` 框架:[点击前往](https://ext.dcloud.net.cn/plugin?id=6692)
* 1、main.js 引入 vk-uview-ui
```js
import uView from './uni_modules/vk-uview-ui';
Vue.use(uView);
```
* 2、App.vue 引入基础样式(注意style标签需声明scss属性支持)
```html
<style lang="scss">
@import "./uni_modules/vk-uview-ui/index.scss";
</style>
```
* 3、uni.scss 引入全局 scss 变量文件
```css
@import "@/uni_modules/vk-uview-ui/theme.scss";
```
### 集成 `vk-uview-ui`(vue3.0版)
___若不想集成 `vk-uview-ui` 可跳过此处___
适合开发:H5、App(vue版本)、微信小程序(其他小程序未验证)
> 插件市场导入 `vk-uview-ui` 框架:[点击前往](https://ext.dcloud.net.cn/plugin?id=6692)
不建议把老项目 升级到 Vue3.0 (升级非常麻烦,建议新项目才考虑是否使用Vue3.0)
___注意:目前(2020-11-18) `uniapp` 的 `Vue3.0` 版本只兼容:H5、App、微信小程序___
* 1、前置步骤:修改 `manifest.json` 内的 `vue` 版本为 `vue3`
* 2、项目根目录新增 `index.html` 文件,文件代码为
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<!--preload-links-->
<!--app-context-->
<!-- 配置H5的 web图标static/logo.png -->
<link rel="icon" href="./static/logo.png" />
</head>
<body>
<div id="app">
<!--app-html-->
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
```
* 3、main.js 引入 vk-uview-ui
```js
// 引入 uView UI
import uView from './uni_modules/vk-uview-ui';
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
// 使用 uView UI
app.use(uView)
return { app }
}
```
* 4、App.vue 引入基础样式(注意style标签需声明scss属性支持)
```html
<style lang="scss">
@import "./uni_modules/vk-uview-ui/index.scss";
</style>
```
* 5、uni.scss 引入全局 scss 变量文件
```css
@import "@/uni_modules/vk-uview-ui/theme.scss";
```
### 集成 `uview-ui`(nvue2.0版)
___若不想集成 `uview-ui` 可跳过此处___
适合开发:App(nvue版本),如果你只开发App,推荐用这个UI
> 插件市场导入 `uview-ui` 框架:[点击前往](https://ext.dcloud.net.cn/plugin?id=1593)
* 1、main.js 引入 uview-ui
```js
import uView from './uni_modules/uview-ui';
Vue.use(uView);
```
* 2、App.vue 引入基础样式(注意style标签需声明scss属性支持)
```html
<style lang="scss">
@import "./uni_modules/uview-ui/index.scss";
</style>
```
* 3、uni.scss 引入全局 scss 变量文件
```css
@import "@/uni_modules/uview-ui/theme.scss";
```
### 集成 `uview-ui`(nvue3.0版)
别想了,uniapp官方也还没支持。
### 组件库并不限制只能从以上选择,理论上支持任何UI组件库进行开发。
### 卸载 `uView1` 的步骤
* 1、main.js 删除 uView1
```js
import uView from 'uview-ui'
Vue.use(uView);
```
* 2、App.vue 删除基础样式
```html
<style lang="scss">
@import "uview-ui/index.scss";
</style>
```
* 3、uni.scss 删除全局 scss 变量文件
```css
@import "uview-ui/theme.scss";
```
* 4、pages.json 删除 easycom 规则
```js
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
},
```
* 5、package.json 删除 uview-ui 依赖
```js
"dependencies": {
"uview-ui": "^1.8.3"
},
```
* 6、package-lock.json 删除 uview-ui 依赖
```js
"dependencies": {
"uview-ui": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/uview-ui/-/uview-ui-1.8.3.tgz",
"integrity": "sha512-DqKc+qRrOZLPcyfWv4b0HspSS9n1Cd6BbgKiYEv9rjTAnWoqJV7rXsvWqZdr5iKGP5EMNbNS741GLNw4sIHbpw=="
}
}
```
### 完整安装文档:[查看完整安装文档](https://vkdoc.fsq.pub/client/quickstart.html)
## 云函数框架目录结构
```
.
├── common─────────────────# 自定义官方公共模块包
│ └── config──────────────────# 全局配置公共模块
│ └── uni-id──────────────────# uni-id官方公共模块
│ └── vk-unicloud─────────────# vk-unicloud公共模块
├── router─────────────────# 正式环境云函数主入口(函数路由器)
│ └── dao──────────────────# dao层(数据库交互公共API)
│ └── middleware───────────# 中间层(过滤器、拦截器)
│ └── node_modules─────────# npm包
│ └── service──────────────# 逻辑层(用于写业务逻辑)
│ ── └── 逻辑层目录结构在下方单独展示
│ └── util─────────────────# 工具包
│ ── └── pubFunction.js───────# 公共函数包
│ ── └── smsUtil.js───────────# 全局过滤器
│ └── config.js────────────# 入口函数初始化配置
│ └── index.js─────────────# 入口函数
│ └── package.json─────────# 第三方依赖配置文件(若使用npm,自动生成)
└─└── package-lock.json────# 第三方依赖配置文件(若使用npm,自动生成)
.
├── router-test───────────────# 函数路由(开发测试环境)
│ └── ...─────────# ...
│ └── ...─────────# ...
└── └── ...─────────# ...
```
### 逻辑层目录结构
#### `router/service` 目录为逻辑层
#### 以下的目录并非强制性,只是建议,便于统一开发规范。
```
.
├── service──────────────────────# 逻辑层(用于写业务逻辑)
│ └── admin────────────────# 后台管理端逻辑(admin框架开发中。。。)
│ ── └── xxxxxxxxxxxxxxx──────#
│ ── └── xxxxxxxxxxxxxxx──────#
│ ── └── xxxxxxxxxxxxxxx──────#
│ ── └── xxxxxxxxxxxxxxx──────#
│ └── client───────────────# 客户端逻辑(H5、小程序、APP)
│ ── └── user─────────────────# 用户服务(自己系统的逻辑)
│ ──── └── kh───────────────────# kh函数为必须登录后才能访问的函数
│ ──── └── pub──────────────────# pub函数为所有人都可以访问,不限制
│ ──── └── util─────────────────# 用户服务专用的工具包
│ ── └── order────────────────# 订单服务(自己系统的逻辑)
│ ──── └── kh───────────────────# kh函数为必须登录后才能访问的函数
│ ──── └── pub──────────────────# pub函数为所有人都可以访问,不限制
│ ──── └── util─────────────────# 订单服务专用的工具包
│ ── └── xxxxxxxxxxxxxxx──────#
│ ── └── xxxxxxxxxxxxxxx──────#
│ └── common───────────────# 公共逻辑(公共接口)
│ ── └── xxxxxxxxxxxxxxxx─────#
│ ── └── xxxxxxxxxxxxxxxx─────#
│ ── └── xxxxxxxxxxxxxxxx─────#
│ └── user─────────────────# 统一用户中心服务(已集成uniID)(用户中心作为核心,且为了方便插件升级,故与admin和client平级)
│ ── └── kh───────────────────# kh函数为必须登录后才能访问的函数(客户端用户)
│ ── └── pub──────────────────# pub函数为所有人都可以访问,不限制
│ ── └── sys──────────────────# sys函数为后端管理人员才能访问的函数(商家后台工作人员)
│ ── └── util─────────────────# 统一用户中心服务专用的工具包
│ └── plugs───────────────# 插件逻辑(插件专用)(会陆续新增一些实用性插件给开发者使用,如微信小程序发送订阅消息等等)
│ ──└── plugs-A───────────────────# 插件A
│ ────└── admin───────────────────# 插件A admin端
│ ────└── client──────────────────# 插件A client端
│ ──└── plugs-B───────────────────# 插件B
│ ────└── admin───────────────────# 插件B admin端
│ ────└── client──────────────────# 插件B client端
│ └── template─────────────# 云函数模板(插件内的云函数写法模板)
│ ── └── db_api───────────────# 数据库接口调用模板
│ └── muban.js─────────────# 云函数模板(新建一个云函数应复制粘贴这个文件)
│ └── muban_easy.js────────# 云函数模板(简易版)
└─────────────────────────────────
```
## 云函数service模板文件示例
```js
module.exports = {
/**
* 此函数名称
* @url user/sys/test1 前端调用的url参数地址
* @description 此函数描述
* @param {Object} data 请求参数
* @param {String} uniIdToken 用户token
* @param {String} userInfo 当前登录用户信息(同理,是可信任的)(只有kh函数才带此参数)
* @param {Object} util 公共工具包
* @param {Object} filterResponse 过滤器返回的数据
* @param {Object} originalParam 原始请求参数(包含了原始event和context)
* data 请求参数 说明
* @param {String} uid 当前登录用户id,若用户已登录且token有效,则data中会带uid参数
* (此参数是后端过滤器通过token获取并添加到data中的,是可信任的)(只有kh函数才带此参数)
* res 返回参数说明
* @param {Number} code 错误码,0表示成功
* @param {String} msg 详细信息
*/
main: async (event) => {
let { data = {}, userInfo, util, filterResponse, originalParam } = event;
let { customUtil, uniID, config, pubFun, vk , db, _ } = util;
let { uid } = data;
let res = { code : 0, msg : '' };
// 业务逻辑开始-----------------------------------------------------------
// 可写与数据库的交互逻辑等等
// 业务逻辑结束-----------------------------------------------------------
return res;
}
}
```
#### 成功时返回
```js
// 注意:所有成功请求必须返回code:0 code为其他值,代表失败,如兑换失败,您的积分不足!
// 如成功时返回 res.code = 0 表示执行成功;
return {
code : 0,
msg : '兑换成功!'
}
```
#### 失败时返回
```js
return {
code : -1,
msg : '兑换失败,您的积分不足!'
}
```
## this.vk.callFunction函数的参数说明
```js
/**
* 云函数请求封装 - 统一入口
* @description 通过云函数路由,1个云函数实现多个云函数的效果。
* @param {String} url 请求路径,该路径实为router云函数的service目录下的路径
* @param {Object} data 请求参数
* @param {String} title 遮罩层提示语,为空或不传则代表不显示遮罩层。
* @param {Boolean} isRequest 是否使用云函数url化地址访问云函数,默认false
* @param {Boolean} needAlert 为true代表请求错误时,会有alert弹窗提示。默认为true
* @param {Function} success 请求成功时,执行的回调函数
* @param {Function} fail 请求失败时,执行的回调函数
* @param {Function} complete 无论请求成功与否,都会执行的回调函数
*/
```
## 前端调用云函数示例
```js
this.vk.callFunction({
url: 'user/kh/setAvatar',
title:'请求中...',
data:{
avatar: "https://xxxxxxx.jpg"
},
success(data) {
// 修改成功
}
});
```
## 云函数url化方式调用云函数示例
[点击查看『云函数url化』方式调用云函数示例](https://vkdoc.fsq.pub/client/pages/callFunctionForUrl.html)
## 前端非法token拦截器
[点击查看『前端非法token拦截器』](https://vkdoc.fsq.pub/client/pages/interceptor1.html)
## vk.baseDao数据库API
[点击查看『vk.baseDao』数据库API](https://vkdoc.fsq.pub/client/uniCloud/db/api.html)
## 设置全局请求参数
[点击查看『设置全局请求参数』](https://vkdoc.fsq.pub/client/pages/updateRequestGlobalParam.html)
## 列表渲染前后端一体模板
[点击查看『列表渲染』前后端一体模板](https://vkdoc.fsq.pub/client/pages/list.html)
## 商品SKU选择器组件
[点击查看『商品SKU选择器组件』](https://gitee.com/vk-uni/vk-uni-cloud-router/wikis/pages?sort_id=2912199&doc_id=975983)
## 如何切换 正式环境 和 开发环境
[点击查看如何『切换正式环境和开发环境』](https://vkdoc.fsq.pub/client/question/q1.html)
## 本插件更新步骤
[点击查看本插件『更新步骤』](https://vkdoc.fsq.pub/client/changeGuide.html)
## uniCloud云函数路由框架研究Q群:`22466457` 如有问题或建议可以在群内讨论。
### 如果你觉得框架对你有用,可以在下方进行评论,也可以进行赞赏。
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
{
"name": "vk-unicloud",
"version": "2.15.3",
"description": "【云函数端SDK】VK云函数路由模式uniCloud开发框架,在router目录下执行 npm i vk-unicloud 进行安装和升级",
"main": "index.js",
"homepage": "https://gitee.com/vk-uni/vk-uni-cloud-router.git",
"repository": {
"type": "git",
"url": "git+https://gitee.com/vk-uni/vk-uni-cloud-router.git"
},
"files": [
"dist"
],
"author": "VK",
"license": "Apache-2.0"
}
@@ -0,0 +1,190 @@
/**
* vk-unicloud框架客户端(前端)
* author VK
*/
import userCenter from './libs/vk-unicloud/vk-unicloud-user-center'
import callFunctionUtil from './libs/vk-unicloud/vk-unicloud-callFunctionUtil'
import pubfn from './libs/function/index'
import modal from './libs/function/modal'
import navigate from './libs/function/vk.navigate'
import localStorage from './libs/function/vk.localStorage'
import sessionStorage from './libs/function/vk.sessionStorage'
import aliyunOSSUtil from './libs/function/aliyunOSSUtil'
import openapi from './libs/openapi/index'
import requestUtil from './libs/function/vk.request'
import importObject from './libs/function/vk.importObject'
import filters from './libs/function/vk.filters'
import mixin from './libs/mixin/mixin.js'
import initPermission from './libs/function/permission'
import storeMixin from './libs/store/mixin/mixin'
import initGlobalError from './libs/store/libs/error'
import consoleLog from './libs/install/console.log'
import updateManager from './libs/function/updateManager.js'
// #ifdef H5
import h5 from './libs/function/vk.h5'
// #endif
var vk = {
userCenter,
callFunctionUtil,
/**
* 发起一个云函数请求
*/
callFunction: callFunctionUtil.callFunction,
getToken: callFunctionUtil.getToken,
checkToken: callFunctionUtil.checkToken,
uploadFile: callFunctionUtil.uploadFile,
getConfig: callFunctionUtil.getConfig,
emitRefreshToken: callFunctionUtil.emitRefreshToken,
onRefreshToken: callFunctionUtil.onRefreshToken,
offRefreshToken: callFunctionUtil.offRefreshToken,
pubfn,
// #ifdef H5
h5,
// #endif
alert: modal.alert,
toast: modal.toast,
confirm: modal.confirm,
prompt: modal.prompt,
showActionSheet: modal.showActionSheet,
showLoading: modal.showLoading,
hideLoading: modal.hideLoading,
setLoading: modal.setLoading,
navigate,
// 保留当前页面,并进行页面跳转
navigateTo: navigate.navigateTo,
// 关闭当前页面,并进行页面跳转
redirectTo: navigate.redirectTo,
// 并关闭所有页面,并进行页面跳转
reLaunch: navigate.reLaunch,
// 并关闭所有非tab页面,并进行tab面跳转
switchTab: navigate.switchTab,
// 页面返回
navigateBack: navigate.navigateBack,
// 跳转到首页
navigateToHome: navigate.navigateToHome,
// 跳转到登录页
navigateToLogin: navigate.navigateToLogin,
// 跳转到小程序
navigateToMiniProgram: navigate.navigateToMiniProgram,
// 触发全局的自定义事件,附加参数都会传给监听器回调函数。
$emit: navigate.$emit,
// 监听全局的自定义事件,事件由 uni.$emit 触发,回调函数会接收事件触发函数的传入参数。
$on: navigate.$on,
// 触发全局的自定义事件,附加参数都会传给监听器回调函数。
$once: navigate.$once,
// 移除全局自定义事件监听器。
$off: navigate.$off,
// 本地持久
localStorage: localStorage,
// 本地会话缓存
sessionStorage: sessionStorage,
// 获取应用语言列表
getLocaleList: pubfn.getLocaleList,
// 获取应用当前语言
getLocale: pubfn.getLocale,
// 获取应用当前语言对象
getLocaleObject: pubfn.getLocaleObject,
// 设置应用当前语言
setLocale: pubfn.setLocale,
// 本地持久缓存
...localStorage,
// 本地会话缓存
...sessionStorage,
// 阿里云oss
aliyunOSSUtil,
// 更新管理器
updateManager,
// 开放API
openapi,
// 请求库
requestUtil,
// 发起URL请求
request: requestUtil.request,
// 导出云对象
importObject
};
// vk实例初始化
vk.init = function(obj = {}) {
let {
Vue, // Vue实例
config, // 配置
store, // vuex简写法则(新版本无需传store)
} = obj;
if (typeof store !== "undefined") {
// 兼容旧版本
Vue.use(store);
} else {
// 新版本无需传store
Vue.mixin(storeMixin);
if (config.globalError) {
Vue.use(initGlobalError);
}
}
// 自定义云函数路由配置
vk.callFunctionUtil.setConfig(config);
// 重写 console.log
Vue.use(consoleLog);
};
vk.getGlobalObject = function() {
if (typeof globalThis === "object") return globalThis;
if (typeof self === "object") return self;
if (typeof window === "object") return window;
if (typeof global === "object") return global;
};
// 加载拓展功能
vk.use = function(obj, util) {
for (let name in obj) {
if (obj[name] && typeof obj[name].init === "function") {
obj[name].init(util);
}
vk[name] = obj[name];
}
};
const install = Vue => {
// 全局混入
Vue.mixin(mixin);
// #ifndef VUE3
// 加载全局过滤器开始
for (let i in filters) {
Vue.filter(i, filters[i]);
}
Vue.prototype.vk = vk;
Vue.prototype.$fn = vk.pubfn;
// #endif
// #ifdef VUE3
// 将vk挂载到Vue实例
Vue.config.globalProperties.vk = vk;
Vue.config.globalProperties.$fn = vk.pubfn;
// #endif
// 将vk挂载到uni对象
if (typeof uni == "object") uni.vk = vk;
// 将vk挂载到全局
let vkGlobalThis = vk.getGlobalObject();
if (typeof vkGlobalThis == "object") vkGlobalThis.vk = vk;
let util = { vk };
// 加载插件
vk.use({
callFunctionUtil: vk.callFunctionUtil,
openapi: vk.openapi
}, util);
initPermission(Vue);
}
export default {
install
}
@@ -0,0 +1,274 @@
/**
* 阿里云oss工具类
*/
var aliyunOSSUtil = {};
var counterNum = 0;
/**
* 上传至阿里云oss
vk.callFunctionUtil.uploadFile({
filePath:tempFilePath,
fileType:"image",
provider:"aliyun",
index,
onUploadProgress:function(res){
// 上传过程中
if (res.progress > 0) {
if(list[index]){
list[index].progress = res.progress;
}
}
},
success:function(res){
// 上传成功
},
fail:function(res){
// 上传失败
}
});
*/
aliyunOSSUtil.uploadFile = function(obj) {
let {
filePath,
fileType = "image",
name = "file",
header = {
"x-oss-forbid-overwrite": true,
},
index = 0,
file = {},
needSave = false,
category_id,
title,
cloudPathRemoveChinese = true, // 删除文件名中的中文
cloudDirectory,
} = obj;
let vk = getApp().globalData.vk;
if (title) vk.showLoading(title);
let fileNameObj = createFileName(obj);
let aliyunOSS = getConfig();
let fileName = fileNameObj.fileFullName;
let formData = vk.pubfn.copyObject(aliyunOSS.uploadData);
formData["key"] = fileName; // 存储在oss的文件路径
/**
* 特别说明
* 若已知本地图片,则使用formData["name"] = filePath
* 若已知base64,则是用formData["file"] = file;// file base64字符串转成blob对象
*/
if (filePath.indexOf(";base64,") > -1) {
formData["file"] = dataURLtoBlob(filePath);
} else {
formData["name"] = filePath;
}
let Logger = {};
Logger.startTime = new Date().getTime();
Logger.filePath = filePath;
return new Promise((resolve, reject) => {
let uploadTask = uni.uploadFile({
url: aliyunOSS.action,
filePath,
name,
header,
formData: formData,
success: function(res) {
if (title) vk.hideLoading();
if (![200, 201].includes(res.statusCode)) {
if (typeof obj.fail === "function") obj.fail(res);
Logger.error = res;
} else {
// 上传成功
res.fileID = fileNameObj.url;
res.url = fileNameObj.url;
Logger.result = res;
if (typeof obj.success === "function") obj.success(res);
resolve(res);
if (needSave) {
// 保存文件记录到数据库
vk.userCenter.addUploadRecord({
data: {
url: res.fileID,
name: file.name,
size: file.size,
file_id: res.fileID,
provider: "aliyun",
category_id
},
filePath,
fileType,
success: function() {
if (typeof obj.addSuccess == "function") obj.addSuccess(res);
},
fail: function(res) {
if (typeof obj.addFail === "function") obj.addFail(res);
}
});
}
}
},
fail: function(res) {
if (title) vk.hideLoading();
Logger.error = res;
if (res.errMsg && res.errMsg.indexOf('fail url not in domain list') > -1) {
vk.toast('上传域名未在白名单中');
}
if (typeof obj.fail === "function") obj.fail(res);
},
complete: function() {
let vk = getApp().globalData.vk;
let config = vk.callFunctionUtil.config;
if (config.debug) {
Logger.endTime = new Date().getTime();
Logger.runTime = (Logger.endTime - Logger.startTime);
let colorArr = config.logger.colorArr;
let colorStr = colorArr[counterNum % colorArr.length];
counterNum++;
console.log("%c--------【开始】【阿里云oss文件上传】--------", 'color: ' + colorStr + ';font-size: 12px;font-weight: bold;');
console.log("【本地文件】: ", Logger.filePath);
console.log("【返回数据】: ", Logger.result);
console.log("【预览地址】: ", Logger.result.fileID);
console.log("【上传耗时】: ", Logger.runTime, "毫秒");
console.log("【上传时间】: ", vk.pubfn.timeFormat(Logger.startTime, "yyyy-MM-dd hh:mm:ss"));
if (Logger.error) console.error("【error】:", Logger.error);
console.log("%c--------【结束】【阿里云oss文件上传】--------", 'color: ' + colorStr + ';font-size: 12px;font-weight: bold;');
}
}
});
uploadTask.onProgressUpdate((res) => {
if (res.progress > 0) {
if (typeof obj.onUploadProgress === "function") obj.onUploadProgress(res);
}
});
});
};
export default aliyunOSSUtil;
// 获取配置
function getConfig() {
let vk = getApp().globalData.vk;
let aliyunOSS = vk.getConfig("service.aliyunOSS");
let configData = {};
if (aliyunOSS && aliyunOSS.uploadData && aliyunOSS.uploadData.OSSAccessKeyId) {
try {
// 只有开启按userId分组且开启了vk的vuex功能,才可以自动按userId分组
if (aliyunOSS.groupUserId && typeof vk.getVuex === "function") {
let userInfo = vk.getVuex("$user.userInfo");
if (vk.pubfn.isNotNull(userInfo) && userInfo._id) {
aliyunOSS.dirname += `/${userInfo._id}`;
}
}
} catch (err) {}
configData = {
uploadData: {
OSSAccessKeyId: aliyunOSS.uploadData.OSSAccessKeyId,
policy: aliyunOSS.uploadData.policy,
signature: aliyunOSS.uploadData.signature,
success_action_status: 200,
key: "test.png"
},
action: aliyunOSS.action,
dirname: aliyunOSS.dirname,
host: aliyunOSS.host,
};
}
return configData;
}
// 生成文件名
function createFileName(obj = {}) {
let {
file,
filePath,
index = 0,
cloudPathRemoveChinese = true,
cloudDirectory
} = obj;
let vk = getApp().globalData.vk;
let aliyunOSS = getConfig();
let dirname = aliyunOSS.dirname;
let host = aliyunOSS.host;
let fileObj = {};
let suffix = getFileSuffix(obj);
let oldName = index + "." + suffix;
if (file && file.name) {
let suffixName = file.name.substring(file.name.lastIndexOf(".") + 1);
if (suffixName && suffixName.length < 5) oldName = file.name;
// 只保留["数字","英文",".","-"]
if (cloudPathRemoveChinese) {
oldName = oldName.replace(/[^a-zA-Z.-d]/g, '');
}
if (oldName.indexOf(".") == 0) oldName = "0" + oldName;
}
let date = new Date();
let dateYYYYMMDD = vk.pubfn.timeFormat(date, "yyyy/MM/dd");
let dateTime = date.getTime().toString(); // 时间戳
// 时间戳后8位
let dateTimeEnd8 = dateTime.substring(dateTime.length - 8, dateTime.length);
let randomNumber = vk.pubfn.random(8); // 8位随机数
// 文件路径 = 固定路径名 + 业务路径
// 业务路径
let servicePath = "";
if (cloudDirectory) {
// 如果自定义了上传目录,则使用自定义的上传目录
if (cloudDirectory.lastIndexOf("/") !== cloudDirectory.length-1) {
cloudDirectory = cloudDirectory + "/";
}
servicePath = cloudDirectory;
} else {
// 否则,使用年月日作为上传目录
servicePath = dateYYYYMMDD + "/";
}
// 文件名 = 时间戳后8位 - 随机数8位 + 后缀名
let fileNickName = dateTimeEnd8 + randomNumber + "-" + oldName;
// 文件相对路径 = 业务目录 + 文件名
let fileRelativePath = servicePath + fileNickName;
// 文件名全称(包含文件路径) = 根目录 + 文件相对路径
let fileFullName = dirname + "/" + fileRelativePath;
// 外网地址 = 外网域名 + 文件名全称
let url = host + "/" + fileFullName;
fileObj.url = url;
fileObj.fileFullName = fileFullName;
fileObj.fileNickName = fileNickName;
return fileObj;
}
function getFileSuffix(obj = {}) {
let {
file,
filePath,
suffix = "png"
} = obj;
if (filePath) {
let suffixName = filePath.substring(filePath.lastIndexOf(".") + 1);
if (suffixName && suffixName.length < 5) suffix = suffixName;
}
if (file) {
if (file.path) {
let suffixName = file.path.substring(file.path.lastIndexOf(".") + 1);
if (suffixName && suffixName.length < 5) suffix = suffixName;
}
if (file.name) {
let suffixName = file.name.substring(file.name.lastIndexOf(".") + 1);
if (suffixName && suffixName.length < 5) suffix = suffixName;
}
}
return suffix;
}
function dataURLtoBlob(dataurl) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
@@ -0,0 +1,34 @@
let timeoutArr = [];
/**
* 防抖函数
* 防抖原理:一定时间内,只有最后一次或第一次调用,回调函数才会执行
* @param {Function} fn 要执行的回调函数
* @param {Number} time 延时的时间
* @param {Boolean} isImmediate 是否立即执行 默认true
* @param {String} timeoutName 定时器ID
* @return null
vk.pubfn.debounce(() => {
}, 1000);
*/
function debounce(fn, time = 500, isImmediate = true, timeoutName = "default") {
// 清除定时器
if(!timeoutArr[timeoutName]) timeoutArr[timeoutName] = null;
if (timeoutArr[timeoutName] !== null) clearTimeout(timeoutArr[timeoutName]);
// 立即执行一次
if (isImmediate) {
var callNow = !timeoutArr[timeoutName];
timeoutArr[timeoutName] = setTimeout(() => {
timeoutArr[timeoutName] = null;
}, time);
if (callNow){
if(typeof fn === 'function') return fn();
}
} else {
// 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时time毫秒后执行fn回调方法
timeoutArr[timeoutName] = setTimeout(() => {
if(typeof fn === 'function') return fn();
}, time);
}
}
export default debounce
@@ -0,0 +1,23 @@
// 判断arr是否为一个数组,返回一个bool值
function isArray (arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
// 深度克隆
function deepClone (obj) {
// 对常见的“非”值,直接返回原来值
if([null, undefined, NaN, false].includes(obj)) return obj;
if(typeof obj !== "object" && typeof obj !== 'function') {
//原始类型直接返回
return obj;
}
var o = isArray(obj) ? [] : {};
for(let i in obj) {
if(obj.hasOwnProperty(i)){
o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
}
}
return o;
}
export default deepClone;
@@ -0,0 +1,30 @@
import deepClone from "./deepClone";
// JS对象深度合并
function deepMerge(target = {}, source = {}) {
target = deepClone(target);
if (typeof target !== 'object' || typeof source !== 'object') return false;
for (var prop in source) {
if (!source.hasOwnProperty(prop)) continue;
if (prop in target) {
if (typeof target[prop] !== 'object') {
target[prop] = source[prop];
} else {
if (typeof source[prop] !== 'object') {
target[prop] = source[prop];
} else {
if (target[prop].concat && source[prop].concat) {
target[prop] = target[prop].concat(source[prop]);
} else {
target[prop] = deepMerge(target[prop], source[prop]);
}
}
}
} else {
target[prop] = source[prop];
}
}
return target;
}
export default deepMerge;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,326 @@
/**
* 函数 - 弹窗
*/
import { Message, MessageBox, Loading } from 'element-ui';
let loadingInstance = null
const localeObj = {
title: {
"zh-Hans": "提示",
"zh-Hant": "提示",
"en": "Tips"
},
confirmText: {
"zh-Hans": "确定",
"zh-Hant": "確定",
"en": "OK"
},
cancelText: {
"zh-Hans": "取消",
"zh-Hant": "取消",
"en": "Cancel"
},
placeholderText: {
"zh-Hans": "请输入",
"zh-Hant": "請輸入",
"en": "Please enter"
}
};
export default {
/**
vk.alert("内容");
vk.alert("内容","提示","好的",function(){
});
*/
alert: function(a = " ", b, c, d) {
let vk = uni.vk;
let locale = vk.getLocale();
let obj = {
title: localeObj.title[locale],
confirmText: localeObj.confirmText[locale],
placeholderText: localeObj.placeholderText[locale],
content: a,
showCancel: false
};
if (typeof d === 'function') {
obj.title = b;
obj.confirmText = c;
obj.success = d;
} else if (typeof c === 'function') {
obj.title = b;
obj.success = c;
} else if (typeof b === 'function') {
obj.success = b;
} else if (b != undefined) {
obj.title = b;
if (c != undefined) {
obj.confirmText = c;
}
}
if (typeof obj.content === 'number') {
obj.content = obj.content + "";
} else if (typeof obj.content === 'object') {
obj.content = JSON.stringify(obj.content);
}
return MessageBox({
title: obj.title,
message: obj.content,
confirmButtonText: obj.confirmText,
})
// return uni.showModal(obj);
},
/**
vk.confirm("内容","提示","确定","取消",(res) => {
if(res.confirm){
}
});
*/
confirm: function(a, b, c, d, e) {
let vk = uni.vk;
let locale = vk.getLocale();
let obj = {
showCancel: true,
cancelColor: "#999",
title: localeObj.title[locale],
confirmText: localeObj.confirmText[locale],
cancelText: localeObj.cancelText[locale],
placeholderText: localeObj.placeholderText[locale],
};
if (typeof a === "object") {
obj = a;
} else {
if (typeof a === "string") {
obj.content = a;
}
if (typeof e === 'function') {
obj.title = b;
obj.confirmText = c;
obj.cancelText = d;
obj.success = e;
} else if (typeof d === 'function') {
obj.title = b;
obj.confirmText = c;
obj.success = d;
} else if (typeof c === 'function') {
obj.title = b;
obj.success = c;
} else if (typeof b === 'function') {
obj.success = b;
} else if (b != undefined) {
obj.title = b;
if (c != undefined) {
obj.confirmText = c;
}
}
}
return uni.showModal(obj);
},
/**
vk.prompt("请输入","提示","确定","取消",function(res){
if(res.confirm){
console.log(res.content);
}
},"输入框内初始内容");
*/
prompt: function(a, b, c, d, e, f) {
let vk = uni.vk;
let locale = vk.getLocale();
let obj = {
showCancel: true,
editable: true,
cancelColor: "#999",
title: localeObj.title[locale],
confirmText: localeObj.confirmText[locale],
cancelText: localeObj.cancelText[locale],
placeholderText: localeObj.placeholderText[locale],
};
if (typeof a === "object") {
obj = a;
} else {
if (typeof a === "string") {
obj.placeholderText = a;
}
if (typeof e === 'function') {
obj.title = b;
obj.confirmText = c;
obj.cancelText = d;
obj.success = e;
obj.content = f;
} else if (typeof d === 'function') {
obj.title = b;
obj.confirmText = c;
obj.success = d;
obj.content = e;
} else if (typeof c === 'function') {
obj.title = b;
obj.success = c;
obj.content = d;
} else if (typeof b === 'function') {
obj.success = b;
obj.content = c;
}
}
return uni.showModal(obj);
},
/**
vk.toast("提示内容","none");
*/
toast: function(a, b, c, d, e) {
let vk = uni.vk;
if (typeof a === 'number') {
a = a.toString();
} else if (typeof a === 'object') {
a = JSON.stringify(a);
}
let title = a;
let icon = "none";
let type = "info";
let image = "";
let mask = false;
let duration = 1500;
let fn;
if (typeof e !== "undefined") {
if (typeof e == "function") fn = e;
if (typeof e == "number") duration = e;
if (typeof e == "boolean") mask = e;
}
if (typeof d !== "undefined") {
if (typeof d == "function") fn = d;
if (typeof d == "number") duration = d;
if (typeof d == "boolean") mask = d;
}
if (typeof c !== "undefined") {
if (typeof c == "function") fn = c;
if (typeof c == "number") duration = c;
if (typeof c == "boolean") mask = c;
}
if (typeof b !== "undefined") {
if (typeof b == "function") fn = b;
if (typeof b == "number") duration = b;
if (typeof b == "boolean") mask = b;
if (typeof b == 'string') {
if (b == "ok") b = "success";
if (b == "success" || b == "loading" || b == "none" || b == "error") {
icon = b;
} else {
image = b;
}
}
}
return Message({
message: title,
type: icon,
duration: duration,
onClose: function(res) {
if (typeof fn === 'function') {
setTimeout(function() {
fn(res);
}, duration);
}
}
})
// return uni.showToast({
// title: title,
// icon: icon,
// image: image,
// mask: mask,
// duration: duration,
// success: function(res) {
// if (typeof fn === 'function') {
// setTimeout(function() {
// fn(res);
// }, duration);
// }
// }
// });
},
/**
* 操作菜单
vk.showActionSheet({
title:"",
list:["位置","@好友"],
color:"rgb(0, 0, 0)",
success:function(res){
if(res.index==0){
}else if(res.index==1){
}
}
});
*/
showActionSheet: function(object) {
let vk = uni.vk;
let title = object.title;
let list = object.list;
let color = object.color || "#000000";
let success = object.success;
let fail = object.fail;
let complete = object.complete;
return uni.showActionSheet({
itemList: list,
itemColor: color,
success: function(res) {
let index = res.tapIndex;
let text = list[index];
let g = { index, text };
//console.log(g);
if (typeof success == "function") success(g);
},
fail: function(res) {
//console.log(res);
if (typeof fail == "function") fail(res);
},
complete: function(res) {
if (typeof complete == "function") complete(res);
}
});
},
showLoading: function(obj) {
if (typeof obj == "string") {
let title = obj;
obj = {
title: title,
mask: true
};
}
loadingInstance = Loading.service({
text: obj.title
})
// uni.showLoading(obj);
},
hideLoading: function() {
if(loadingInstance != null) {
loadingInstance.close();
}
uni.hideLoading();
},
// 设置当前页面的loading变量的值
setLoading: function(loading = true, obj = true) {
try {
let vk = uni.vk;
if (typeof obj === "boolean") {
let pages = getCurrentPages();
let page = pages[pages.length - 1];
let that = page.$vm;
that.loading = loading;
} else if (typeof obj === "object") {
let { data, name, that } = obj;
if (!data) data = that;
vk.pubfn.setData(data, name, loading);
} else if (typeof obj === "string") {
let pages = getCurrentPages();
let page = pages[pages.length - 1];
let that = page.$vm;
let name = obj;
vk.pubfn.setData(that, name, loading);
}
} catch (err) {}
},
}
@@ -0,0 +1,33 @@
function $hasRole(...args) {
const userRoles = this.$store.state.$user.userInfo.role || [];
return args.some(arg => {
if (Array.isArray(arg)) {
return arg.every(role => userRoles.includes(role));
} else {
return userRoles.includes(arg);
}
});
}
function $hasPermission(...args) {
const userPermission = this.$store.state.$user.permission || []
return args.some(arg => {
if (Array.isArray(arg)) {
return arg.every(permission => userPermission.includes(permission));
} else {
return userPermission.includes(arg);
}
});
}
export default function(Vue) {
// #ifndef VUE3
Vue.prototype.$hasRole = $hasRole;
Vue.prototype.$hasPermission = $hasPermission;
// #endif
// #ifdef VUE3
Vue.config.globalProperties.$hasRole = $hasRole;
Vue.config.globalProperties.$hasPermission = $hasPermission;
// #endif
}
@@ -0,0 +1,60 @@
/**
* 对象转url参数
* @param {*} data,对象
* @param {*} isPrefix,是否自动加上"?"
* 此函数参考uView
*/
function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
let newData = JSON.parse(JSON.stringify(data));
let prefix = isPrefix ? '?' : ''
let _result = []
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets';
for (let key in newData) {
let value = newData[key]
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue;
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(key + '[' + i + ']=' + value[i])
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach(_value => {
_result.push(key + '[]=' + _value)
})
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach(_value => {
_result.push(key + '=' + _value)
})
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = "";
value.forEach(_value => {
commaStr += (commaStr ? "," : "") + _value;
})
_result.push(key + '=' + commaStr)
break;
default:
value.forEach(_value => {
_result.push(key + '[]=' + _value)
})
}
} else {
_result.push(key + '=' + value)
}
}
return _result.length ? prefix + _result.join('&') : ''
}
export default queryParams;
@@ -0,0 +1,51 @@
// #ifdef H5
uni.setClipboardData = function setClipboardData({
data,
success,
fail,
complete
} = {}) {
try {
const x = window.scrollX
const y = window.scrollY
const input = document.createElement('input')
input.setAttribute('readonly', 'readonly')
input.setAttribute('value', data)
input.style.position = 'absolute'
input.style.width = '100px'
input.style.left = '-10000px'
document.body.appendChild(input)
input.focus()
window.scrollTo(x, y)
input.setSelectionRange(0, input.value.length)
document.execCommand('copy')
document.body.removeChild(input)
setTimeout(() => {
const res = {
errMsg: 'setClipboardData:ok',
data
}
success && success(res)
complete && complete(res)
})
} catch (e) {
setTimeout(() => {
const res = {
errMsg: 'setClipboardData:fail'
}
fail && fail(res)
complete && complete(res)
})
}
}
// #endif
// JS对象深度合并
function setClipboardData(obj = {}) {
uni.setClipboardData({
...obj
});
}
export default setClipboardData;
@@ -0,0 +1,37 @@
let timeoutArr = [];
let flagArr = [];
/**
* 节流函数
* 节流原理:在一定时间内,只能触发一次
* @param {Function} fn 要执行的回调函数
* @param {Number} time 延时的时间
* @param {Boolean} isImmediate 是否立即执行
* @param {String} timeoutName 定时器ID
* @return null
vk.pubfn.throttle(function() {
}, 1000);
*/
function throttle(fn, time = 500, isImmediate = true, timeoutName = "default") {
if(!timeoutArr[timeoutName]) timeoutArr[timeoutName] = null;
if (isImmediate) {
if (!flagArr[timeoutName]) {
flagArr[timeoutName] = true;
// 如果是立即执行,则在time毫秒内开始时执行
if(typeof fn === 'function') fn();
timeoutArr[timeoutName] = setTimeout(() => {
flagArr[timeoutName] = false;
}, time);
}
} else {
if (!flagArr[timeoutName]) {
flagArr[timeoutName] = true;
// 如果是非立即执行,则在time毫秒内的结束处执行
timeoutArr[timeoutName] = setTimeout(() => {
flagArr[timeoutName] = false;
if(typeof fn === 'function') fn();
}, time);
}
}
};
export default throttle
@@ -0,0 +1,642 @@
/**
* 时间工具类
*/
var util = {};
util.getTargetTimezone = function(val) {
if (typeof val === "number") {
return val;
}
let vk = uni.vk;
let defaultValue = 8;
let targetTimezone = defaultValue;
try {
const config = vk.callFunctionUtil.getConfig();
targetTimezone = config.targetTimezone;
if (typeof targetTimezone !== "number") {
targetTimezone = defaultValue;
}
} catch (err) {}
return targetTimezone;
};
// 尽可能的将参数转成正确的时间对象
util.getDateObject = function(date, targetTimezone) {
if (!date) return "";
let nowDate;
// 如果是字符串,且纯数字,则强制转数值
if (typeof date === "string" && !isNaN(date) && date.length >= 10) date = Number(date);
if (typeof date === "number") {
if (date.toString().length == 10) date *= 1000;
nowDate = new Date(date); // 转时间对象
} else if (typeof date === "object") {
nowDate = new Date(date.getTime()); // 新建一个时间对象
} else if (typeof date === "string") {
targetTimezone = util.getTargetTimezone(targetTimezone);
let targetTimezoneStr = targetTimezone;
let targetTimezoneF = targetTimezone >= 0 ? "+" : "";
if (targetTimezone >= 0 && targetTimezone < 10) {
targetTimezoneStr = `0${targetTimezone}`;
} else if (targetTimezone < 0 && targetTimezone > -10) {
targetTimezoneStr = `-0${targetTimezone*-1}`;
}
let arr1 = date.split(" ");
let arr1_1 = arr1[0] || "";
let arr1_2 = arr1[1] || "";
let arr2;
if (arr1_1.indexOf("-") > -1) {
arr2 = arr1_1.split("-");
} else {
arr2 = arr1_1.split("/");
}
let arr3 = arr1_2.split(":");
let dateObj = {
year: Number(arr2[0]),
month: Number(arr2[1]) || 1,
day: Number(arr2[2]) || 1,
hour: Number(arr3[0]) || 0,
minute: Number(arr3[1]) || 0,
second: Number(arr3[2]) || 0,
};
for (let key in dateObj) {
if (dateObj[key] >= 0 && dateObj[key] < 10) dateObj[key] = `0${dateObj[key]}`;
}
// 格式 2022-10-01T00:00:00+08:00
let dateStr = `${dateObj.year}-${dateObj.month}-${dateObj.day}T${dateObj.hour}:${dateObj.minute}:${dateObj.second}${targetTimezoneF}${targetTimezoneStr}:00`;
nowDate = new Date(dateStr);
}
return nowDate;
};
// 获取时间在不同时区下的时间对象
util.getTimeByTimeZone = function(date, targetTimezone) {
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
let timezoneOffset = nowDate.getTimezoneOffset();
let offset = timezoneOffset * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let targetTime = nowDate.getTime() + offset;
nowDate = new Date(targetTime);
return nowDate;
};
/**
* 日期格式化
* @param {Date || Number} date 需要格式化的时间
* vk.pubfn.timeFormat(new Date(),"yyyy-MM-dd hh:mm:ss");
*/
util.timeFormat = function(date, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone) {
try {
if (!date) return "";
targetTimezone = util.getTargetTimezone(targetTimezone);
let nowDate = util.getTimeByTimeZone(date, targetTimezone);
let opt = {
"M+": nowDate.getMonth() + 1, //月份
"d+": nowDate.getDate(), //日
"h+": nowDate.getHours(), //小时
"m+": nowDate.getMinutes(), //分
"s+": nowDate.getSeconds(), //秒
"q+": Math.floor((nowDate.getMonth() + 3) / 3), //季度
"S": nowDate.getMilliseconds(), //毫秒
"Z": `${targetTimezone >= 0 ? '+' : '-'}${Math.abs(targetTimezone).toString().padStart(2, '0')}:00` // 时区
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (nowDate.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};
/**
* 解析日期对象属性
* @param {Date || Number} date 需要转换的时间
* vk.pubfn.getDateInfo(new Date());
*/
util.getDateInfo = function(date = new Date(), targetTimezone) {
let nowDate = util.getTimeByTimeZone(date, targetTimezone);
let year = nowDate.getFullYear() + '';
let month = (nowDate.getMonth() + 1 < 10 ? '0' + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1);
let day = (nowDate.getDate() < 10 ? '0' + (nowDate.getDate()) : nowDate.getDate());
let hour = (nowDate.getHours() < 10 ? '0' + (nowDate.getHours()) : nowDate.getHours());
let minute = (nowDate.getMinutes() < 10 ? '0' + (nowDate.getMinutes()) : nowDate.getMinutes());
let second = (nowDate.getSeconds() < 10 ? '0' + (nowDate.getSeconds()) : nowDate.getSeconds());
let millisecond = nowDate.getMilliseconds(); //毫秒
let week = nowDate.getDay(); // 周
let quarter = Math.floor((nowDate.getMonth() + 3) / 3); //季度
return {
year: Number(year),
month: Number(month),
day: Number(day),
hour: Number(hour),
minute: Number(minute),
second: Number(second),
millisecond: Number(millisecond),
week: Number(week),
quarter: Number(quarter),
};
};
/**
* 获取时间范围
* @param {Date} date 日期对象 可以指定时间计算节点,默认使用当前时间进行计算
* 返回的是时间戳
* 返回数据如下:
{
todayStart 今日开始时间(时间戳)
todayEnd 今日结束时间(时间戳)
today12End 今日上午结束时间(时间戳)
monthStart 本月开始时间(时间戳)
monthEnd 本月结束时间(时间戳)
yearStart 本年开始时间(时间戳)
yearEnd 本年结束时间(时间戳)
weekStart 本周开始时间(时间戳)
weekEnd 本周结束时间(时间戳)
hourStart 当前小时开始时间(时间戳)
hourEnd 当前小时结束时间(时间戳)
yesterdayStart 昨天开始时间(时间戳)
yesterday12End 昨天上午结束时间(时间戳)
yesterdayEnd 昨天结束时间(时间戳)
lastMonthStart 上月开始时间(时间戳)
lastMonthEnd 上月结束时间(时间戳)
now 现在的时间点(含月年日时分秒)
months 本年度每月的开始和结束时间 months[1] 代表1月
days 本月每天的开始和结束时间 days[1] 代表1日
}
* vk.pubfn.getCommonTime();
*/
util.getCommonTime = function(date = new Date(), targetTimezone) {
let res = {};
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const { year, month, day, hour, minute, second, millisecond, week, quarter } = util.getDateInfo(nowDate, targetTimezone);
const oneDayTime = 24 * 60 * 60 * 1000;
// 现在的时间
res.now = {
year,
month,
day,
hour,
minute,
second,
millisecond,
week,
quarter,
date_str: util.timeFormat(nowDate, "yyyy-MM-dd hh:mm:ss", targetTimezone),
date_day_str: util.timeFormat(nowDate, "yyyy-MM-dd", targetTimezone),
date_month_str: util.timeFormat(nowDate, "yyyy-MM", targetTimezone),
};
// 获取本月最大天数
let month_last_day = new Date(year, month, 0).getDate();
// 获取今年12月最大天数
let year_last_day = new Date(year, 12, 0).getDate();
// 今日开始时间
res.todayStart = new Date(`${year}/${month}/${day}`).getTime() - timeDif;
// 今日12点时间
res.today12End = new Date(`${year}/${month}/${day}`).getTime() + (12 * 60 * 60 * 1000 - 1) - timeDif;
// 今日结束时间
res.todayEnd = new Date(`${year}/${month}/${day}`).getTime() + (oneDayTime - 1) - timeDif;
// 本月开始时间
res.monthStart = new Date(`${year}/${month}/1`).getTime() - timeDif;
// 本月结束时间
res.monthEnd = new Date(`${year}/${month}/${month_last_day}`).getTime() + (oneDayTime - 1) - timeDif;
// 本年开始时间
res.yearStart = new Date(`${year}/1/1`).getTime() - timeDif;
// 本年结束时间
res.yearEnd = new Date(`${year}/12/${year_last_day}`).getTime() + (oneDayTime - 1) - timeDif;
// 当前小时开始时间
res.hourStart = new Date(`${year}/${month}/${day} ${hour}:00:00`).getTime() - timeDif;
// 当前小时结束时间
res.hourEnd = new Date(`${year}/${month}/${day} ${hour}:59:59`).getTime() - timeDif;
// 计算上月开始-----------------------------------------------------------
let year_last = year;
let month_last = month - 1;
if (month_last === 0) {
month_last = 12;
year_last = year - 1;
}
let month_last_day_last = new Date(year_last, month_last, 0).getDate();
// 上月开始时间
res.lastMonthStart = new Date(`${year_last}/${month_last}/1`).getTime() - timeDif;
// 上月结束时间
res.lastMonthEnd = new Date(`${year_last}/${month_last}/${month_last_day_last}`).getTime() + (oneDayTime - 1) - timeDif;
// 计算上月结束-----------------------------------------------------------
// 昨天开始时间
res.yesterdayStart = res.todayStart - 1000 * 3600 * 24;
// 昨天上午结束时间
res.yesterday12End = res.today12End - 1000 * 3600 * 24;
// 昨天结束时间
res.yesterdayEnd = res.todayEnd - 1000 * 3600 * 24;
let weekObj = util.getWeekOffsetStartAndEnd(0, nowDate, targetTimezone);
// 本周开始时间
res.weekStart = weekObj.startTime;
// 本周结束时间
res.weekEnd = weekObj.endTime;
// 本年1-12月的起止时间
res.months = [];
res.months[0] = {
startTime: res.monthStart,
endTime: res.monthEnd,
startTimeStr: util.timeFormat(res.monthStart, "yyyy-MM-dd hh:mm:ss", targetTimezone),
endTimeStr: util.timeFormat(res.monthEnd, "yyyy-MM-dd hh:mm:ss", targetTimezone),
monthStart: res.monthStart, // 兼容旧版
monthEnd: res.monthEnd, // 兼容旧版
};
for (let i = 1; i <= 12; i++) {
// 获取此月最大天数
let month_last_day = new Date(year, i, 0).getDate();
// 此月开始时间
let startTime = new Date(`${year}/${i}/1`).getTime() - timeDif;
// 此月结束时间
let endTime = new Date(`${year}/${i}/${month_last_day}`).getTime() + (oneDayTime - 1) - timeDif;
res.months[i] = {
startTime,
endTime,
startTimeStr: util.timeFormat(startTime, "yyyy-MM-dd hh:mm:ss", targetTimezone),
endTimeStr: util.timeFormat(endTime, "yyyy-MM-dd hh:mm:ss", targetTimezone),
monthStart: startTime, // 兼容旧版
monthEnd: endTime, // 兼容旧版
};
}
// 本月1号到最后一天每天的起止时间
res.days = [];
res.days[0] = {
startTime: res.todayStart,
endTime: res.todayEnd,
startTimeStr: util.timeFormat(res.todayStart, "yyyy-MM-dd hh:mm:ss", targetTimezone),
endTimeStr: util.timeFormat(res.todayEnd, "yyyy-MM-dd hh:mm:ss", targetTimezone)
};
for (let i = 1; i <= month_last_day; i++) {
let nowTime = res.monthStart + ((i - 1) * oneDayTime);
let { startTime, endTime } = util.getDayOffsetStartAndEnd(0, nowTime, targetTimezone);
res.days[i] = {
startTime,
endTime,
startTimeStr: util.timeFormat(startTime, "yyyy-MM-dd hh:mm:ss", targetTimezone),
endTimeStr: util.timeFormat(endTime, "yyyy-MM-dd hh:mm:ss", targetTimezone)
};
}
for (let key in res) {
if (typeof res[key] === "number" && res[key].toString().length === 13) {
res[`${key}Str`] = util.timeFormat(res[key], "yyyy-MM-dd hh:mm:ss", targetTimezone);
}
}
return res;
};
/**
* 获得指定年份和月份后的该月的开始时间和结束时间
* 返回数据如下:(时间戳形式)
{
startTime 该月开始时间
endTime 该月结束时间
}
vk.pubfn.getMonthStartAndEnd({
year:2021
month:1
});
*/
util.getMonthStartAndEnd = function(obj, targetTimezone) {
targetTimezone = util.getTargetTimezone(targetTimezone);
let res = {
startTime: null,
endTime: null
};
let { year, month } = obj;
if (year > 0 && month > 0) {
const dif = new Date().getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let month_last_day = new Date(year, month, 0).getDate();
// 开始时间
res.startTime = new Date(`${year}/${month}/1`).getTime() - timeDif;
// 结束时间
res.endTime = new Date(`${year}/${month}/${month_last_day}`).getTime() + (24 * 60 * 60 * 1000 - 1) - timeDif;
}
return res;
}
/**
* 获得相对当前时间的偏移 count 小时的起止日期(返回小时的开始和结束时间戳)
* @param {Number} count 默认0(0代表当前小时 -1代表上一个小时 1代表下一个小时以此类推)
* @param {Date || Number} date 指定从哪个时间节点开始计算
* vk.pubfn.getHourOffsetStartAndEnd(0);
*/
util.getHourOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
let res = {};
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
// 一天的毫秒数
let offsetMillisecond = 1000 * 60 * 60;
// 相对于当前日期count个天的日期
nowDate = new Date(nowDate.getTime() + (offsetMillisecond * 1 * count));
let dateInfo = util.getDateInfo(nowDate);
// 获得当天的起始时间
res.startTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day} ${dateInfo.hour}:00:00`).getTime() - timeDif;
// 获得当天的结束时间
res.endTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day} ${dateInfo.hour}:00:00`).getTime() + (offsetMillisecond - 1) - timeDif;
return res;
}
/**
* 获得相对当前时间的偏移 count 天的起止日期(返回日的开始和结束)
* @param {Number} count 默认0(0代表今日 -1代表昨日 1代表明日以此类推)
* @param {Date || Number} date 指定从哪个时间节点开始计算
* vk.pubfn.getDayOffsetStartAndEnd(0);
*/
util.getDayOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
let res = {};
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
// 一天的毫秒数
let offsetMillisecond = 1000 * 60 * 60 * 24;
// 相对于当前日期count个天的日期
nowDate = new Date(nowDate.getTime() + (offsetMillisecond * 1 * count));
let dateInfo = util.getDateInfo(nowDate);
// 获得当天的起始时间
res.startTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day}`).getTime() - timeDif;
// 获得当天的结束时间
res.endTime = new Date(`${dateInfo.year}/${dateInfo.month}/${dateInfo.day}`).getTime() + (offsetMillisecond - 1) - timeDif;
return res;
}
/**
* 获得相对当前周count个周的起止日期(返回周的开始和结束)
* @param {Number} count 默认0(0代表本周 -1代表上周 1代表下周以此类推)
* vk.pubfn.getWeekOffsetStartAndEnd(0);
*/
util.getWeekOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let res = {};
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let nowDay = nowDate.getDay() === 0 ? 7 : nowDate.getDay();
nowDate.setDate(nowDate.getDate() - nowDay + 1 + count * 7);
let dateInfo1 = util.getDateInfo(nowDate);
nowDate.setDate(nowDate.getDate() + 7);
let dateInfo2 = util.getDateInfo(nowDate);
// 开始时间
res.startTime = new Date(`${dateInfo1.year}/${dateInfo1.month}/${dateInfo1.day}`).getTime() - timeDif;
// 结束时间
res.endTime = new Date(`${dateInfo2.year}/${dateInfo2.month}/${dateInfo2.day}`).getTime() - 1 - timeDif;
return res;
}
/**
* 获得相对当前时间的偏移 count 个月的起止日期(月的开始和结束时间戳)
* @param {Number} count 默认0(0代表本月 -1代表上月 1代表下月以此类推)
* @param {Date || Number} date 指定从哪个时间节点开始计算
* vk.pubfn.getMonthOffsetStartAndEnd(0);
*/
util.getMonthOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let res = {};
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let dateInfo = util.getDateInfo(nowDate);
let month = dateInfo.month + count;
let year = dateInfo.year;
if (month > 12) {
year = year + Math.floor(month / 12);
month = Math.abs(month) % 12;
} else if (month <= 0) {
year = year - 1 - Math.floor(Math.abs(month) / 12);
month = 12 - Math.abs(month) % 12;
}
let month_last_day = new Date(year, month, 0).getDate();
// 开始时间
res.startTime = new Date(`${year}/${month}/1`).getTime() - timeDif;
// 结束时间
res.endTime = new Date(`${year}/${month}/${month_last_day}`).getTime() + (24 * 60 * 60 * 1000 - 1) - timeDif;
return res;
}
/**
* 获得相对当前时间的偏移 count 个季度的起止日期(季度的开始和结束时间戳)
* @param {Number} count 默认0(0代表本季度 -1代表上个季度 1代表下个季度以此类推)
* @param {Date || Number} date 指定从哪个时间节点开始计算
* vk.pubfn.getQuarterOffsetStartAndEnd(0);
*/
util.getQuarterOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let res = {};
let nowDate = util.getDateObject(date);
nowDate.setMonth(nowDate.getMonth() + count * 3);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let dateInfo = util.getDateInfo(nowDate);
let month = dateInfo.month;
if ([1, 2, 3].indexOf(month) > -1) {
// 第1季度
month = 1;
} else if ([4, 5, 6].indexOf(month) > -1) {
// 第2季度
month = 4;
} else if ([7, 8, 9].indexOf(month) > -1) {
// 第3季度
month = 7;
} else if ([10, 11, 12].indexOf(month) > -1) {
// 第4季度
month = 10;
}
nowDate.setMonth(month - 1); // 因为0代表1月,所以这里要减1
let dateInfo1 = util.getDateInfo(nowDate);
nowDate.setMonth(nowDate.getMonth() + 3);
let dateInfo2 = util.getDateInfo(nowDate);
// 开始时间
res.startTime = new Date(`${dateInfo1.year}/${dateInfo1.month}/1`).getTime() - timeDif;
// 结束时间
res.endTime = new Date(`${dateInfo2.year}/${dateInfo2.month}/1`).getTime() - 1 - timeDif;
return res;
}
/**
* 获得相对当前时间的偏移 count 年的起止日期(年的开始和结束时间戳)
* @param {Number} count 默认0 (0代表今年 -1代表去年 1代表明年以此类推)
* @param {Date || Number} date 指定从哪个时间节点开始计算
* vk.pubfn.getYearOffsetStartAndEnd(0);
*/
util.getYearOffsetStartAndEnd = function(count = 0, date = new Date(), targetTimezone) {
let res = {};
let nowDate = util.getDateObject(date);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let dateInfo = util.getDateInfo(nowDate);
let year = dateInfo.year + count;
// 开始时间
res.startTime = new Date(`${year}/1/1`).getTime() - timeDif;
// 结束时间
res.endTime = new Date(`${year}/12/31`).getTime() + (24 * 60 * 60 * 1000 - 1) - timeDif;
return res;
};
/**
* 获得指定时间偏移 year年 month月 day天 hours时 minutes分 seconds秒前或后的时间戳
* 返回时间戳
vk.pubfn.getOffsetTime(new Date(), {
year:0,
month:0,
day:0,
hours:0,
minutes:0,
seconds:0,
mode:"after", // after 之后 before 之前
});
vk.pubfn.getOffsetTime(new Date(), {
y:0,
m:0,
d:0,
hh:0,
mm:0,
ss:0,
mode:"after" // after 之后 before 之前
});
*/
util.getOffsetTime = function(date = new Date(), obj, targetTimezone) {
let nowDate = util.getDateObject(date);
let dateInfo = util.getDateInfo(nowDate);
targetTimezone = util.getTargetTimezone(targetTimezone);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
let year = obj.year || obj.y || 0;
let month = obj.month || obj.m || 0;
let day = obj.day || obj.d || 0;
let hour = obj.hour || obj.hours || obj.hh || 0;
let minute = obj.minute || obj.minutes || obj.mm || 0;
let second = obj.second || obj.seconds || obj.ss || 0;
let { mode = "after" } = obj;
if (mode == "before") {
year *= -1;
month *= -1;
day *= -1;
hour *= -1;
minute *= -1;
second *= -1;
}
let offsetObj = {
year: dateInfo.year + year,
month: dateInfo.month + month,
day: dateInfo.day + day,
hour: dateInfo.hour + hour,
minute: dateInfo.minute + minute,
second: dateInfo.second + second
};
nowDate = new Date(offsetObj.year, offsetObj.month - 1, offsetObj.day, offsetObj.hour, offsetObj.minute, offsetObj.second);
return nowDate.getTime() - timeDif;
}
/**
* 判断是否是闰年
* @param {Number | Date} year 需要计算的年份或时间,默认使用当前时间的年份
* vk.pubfn.timeUtil.isLeapYear(year);
*/
util.isLeapYear = function(year) {
if (typeof year === "undefined") {
let { now } = util.getCommonTime();
year = now.year;
} else if (typeof year === "object") {
let { now } = util.getCommonTime(year);
year = now.year;
}
if (((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0)) {
return true;
} else {
return false;
}
}
/**
* 判断是否是清明节
* @param {Date} date 日期对象 可以指定时间计算节点,默认使用当前时间进行计算
vk.pubfn.timeUtil.isQingming(new Date());
*/
util.isQingming = function(data = new Date()) {
let { now } = util.getCommonTime(data);
let { year, month, day } = now;
let key = false;
// 清明节的日期是不固定的,规律是:闰年开始的前2年是4月4日,闰年开始的第3年和第4年是4月5日
if (util.isLeapYear(year) || util.isLeapYear(year - 1)) {
if (month === 4 && day === 4) {
key = true;
}
} else {
if (month === 4 && day === 5) {
key = true;
}
}
return key;
}
/**
* 日期对象转换(已弃用,建议不要使用此API)
* @param {Date || Number} date 需要转换的时间
* @param {Number} type 转换方式
* type = 0 返回 2020-08-03 12:12:12
* type = 1 返回 20200803121212
* type = 2 返回 { YYYY, MM, DD, hh, mm, ss }
*/
util.getFullTime = function(date, type = 0, targetTimezone) {
if (!date) return "";
targetTimezone = util.getTargetTimezone(targetTimezone);
let nowDate = util.getDateObject(date);
const dif = nowDate.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = nowDate.getTime() + timeDif;
nowDate = new Date(east8time);
let YYYY = nowDate.getFullYear() + '';
let MM = (nowDate.getMonth() + 1 < 10 ? '0' + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1);
let DD = (nowDate.getDate() < 10 ? '0' + (nowDate.getDate()) : nowDate.getDate());
let hh = (nowDate.getHours() < 10 ? '0' + (nowDate.getHours()) : nowDate.getHours());
let mm = (nowDate.getMinutes() < 10 ? '0' + (nowDate.getMinutes()) : nowDate.getMinutes());
let ss = (nowDate.getSeconds() < 10 ? '0' + (nowDate.getSeconds()) : nowDate.getSeconds());
if (type === 2) {
return {
YYYY: Number(YYYY),
MM: Number(MM),
DD: Number(DD),
hh: Number(hh),
mm: Number(mm),
ss: Number(ss),
year: Number(YYYY),
month: Number(MM),
day: Number(DD),
hour: Number(hh),
minute: Number(mm),
second: Number(ss),
};
} else if (type === 1) {
return YYYY + "" + MM + "" + DD + "" + hh + "" + mm + "" + ss;
} else {
return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss;
}
};
export default util;
@@ -0,0 +1,93 @@
import deepClone from './deepClone'
var util = {};
/**
* 将树形结构转成数组结构
* @param {Array} treeData 数据源
* @param {Object} treeProps 树结构配置 { id : "menu_id", children : "children" }
* vk.pubfn.treeToArray(treeData);
*/
util.treeToArray = function(treeData, treeProps) {
let newTreeData = deepClone(treeData);
return util.treeToArrayFn(newTreeData, treeProps);
};
util.treeToArrayFn = function(treeData, treeProps = {}, arr=[], current_parent_id) {
let { id="_id", parent_id="parent_id", children = "children", deleteChildren = true } = treeProps;
for(let i in treeData){
let item = treeData[i];
if(current_parent_id) item[parent_id] = current_parent_id;
arr.push(item);
if(item[children] && item[children].length>0){
arr = util.treeToArrayFn(item[children], treeProps, arr, item[id]);
}
if(deleteChildren){
delete item[children];
}
}
return arr;
};
/**
* 数组结构转树形结构
let tree = vk.pubfn.arrayToTree(arrayData,{
id:"code",
parent_id:"parent_code",
});
*/
util.arrayToTree = function(originalArrayData,treeProps) {
let arrayData = deepClone(originalArrayData);
let {
id="_id",
parent_id="parent_id",
children = "children",
deleteParentId = false,
need_field
} = treeProps;
let result = [];
let temp = {};
for (let i = 0; i < arrayData.length; i++) {
temp[arrayData[i][id]] = arrayData[i]; // 以id作为索引存储元素,可以无需遍历直接定位元素
}
for (let j = 0; j < arrayData.length; j++) {
let currentElement = arrayData[j];
let newCurrentElement = {};
if(need_field){
need_field = uniqueArr(need_field.concat([id,parent_id,children]));
for(let keyName in currentElement){
if(need_field.indexOf(keyName) === -1){
delete currentElement[keyName];
}
}
}
let tempCurrentElementParent = temp[currentElement[parent_id]]; // 临时变量里面的当前元素的父元素
if (tempCurrentElementParent) {
// 如果存在父元素
if (!tempCurrentElementParent[children]) {
// 如果父元素没有chindren键
tempCurrentElementParent[children] = []; // 设上父元素的children键
}
if(deleteParentId){
delete currentElement[parent_id];
}
tempCurrentElementParent[children].push(currentElement); // 给父元素加上当前元素作为子元素
} else {
// 不存在父元素,意味着当前元素是一级元素
result.push(currentElement);
}
}
return result;
};
// 最简单数组去重法
function uniqueArr(array) {
let n = []; //一个新的临时数组
//遍历当前数组
for (let i = 0; i < array.length; i++) {
//如果当前数组的第i已经保存进了临时数组,那么跳过,
//否则把当前项push到临时数组里面
if (n.indexOf(array[i]) == -1) n.push(array[i]);
}
return n;
}
export default util;
@@ -0,0 +1,61 @@
var updateManager = {};
/**
* 本API返回全局唯一的版本更新管理器对象: updateManager,用于管理小程序更新。
* vk.updateManager.updateReady();
*/
updateManager.updateReady = function(obj) {
// #ifdef MP
updateManagerByMP(obj);
// #endif
};
export default updateManager;
function updateManagerByMP(obj = {}) {
let {
title = "更新提示",
content = "新版本已经准备好,点击更新!",
autoUpdate = true,
showCancel = false,
confirmText = "一键更新"
} = obj;
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function(res) {
// 请求完新版本信息的回调
// console.log(res.hasUpdate);
});
updateManager.onUpdateReady(function(res) {
uni.showModal({
title,
content,
showCancel,
confirmText,
success(res) {
if (res.confirm) {
if (typeof obj.success === "function") {
obj.success({
applyUpdate: updateManager.applyUpdate
});
}
if (typeof obj.complete === "function") obj.complete();
if (autoUpdate) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
},
});
});
updateManager.onUpdateFailed(function(res) {
// 新的版本下载失败
console.error("onUpdateFailed", res);
if (typeof obj.fail === "function") obj.fail(res);
if (typeof obj.complete === "function") obj.complete();
});
}
@@ -0,0 +1,63 @@
/**
* vk.filters
* 全局过滤器
*/
import pubfn from './index.js'
var util = {};
// 将时间显示成1秒前、1天前
util.dateDiff = function(starttime, showType) {
return pubfn.dateDiff(starttime, showType);
}
util.dateDiff2 = function(starttime, showType) {
return pubfn.dateDiff2(starttime, showType);
};
// 计数器1
util.numStr = function(n) {
return pubfn.numStr(n);
};
util.timeStr = function(date) {
return pubfn.timeFormat(date);
};
/**
* 日期对象转字符串
* @description 最终转成 2020-08-01 12:12:12
* @param {Date || Number} date 需要转换的时间
* date参数支持
* 支持:时间戳
* 支持:2020-08
* 支持:2020-08-24
* 支持:2020-08-24 12
* 支持:2020-08-24 12:12
* 支持:2020-08-24 12:12:12
*/
util.timeFilter = function(date, fmt) {
return pubfn.timeFormat(date, fmt);
};
// 金额过滤器
util.priceFilter = function(n, defaultValue = " - ") {
return pubfn.priceFilter(n, defaultValue);
};
// 金额过滤器 - 只显示小数点左边
util.priceLeftFilter = function(n) {
return pubfn.priceLeftFilter(n);
};
// 金额过滤器 - 只显示小数点右边
util.priceRightFilter = function(n) {
return pubfn.priceRightFilter(n);
};
// 百分比过滤器
util.percentageFilter = function(n, needShowSymbol, defaultValue = " - ") {
return pubfn.percentageFilter(n, needShowSymbol, defaultValue);
};
// 折扣过滤器
util.discountFilter = function(n, needShowSymbol, defaultValue = " - ") {
return pubfn.discountFilter(n, needShowSymbol, defaultValue);
};
// 大小过滤器 sizeFilter(1024,3,["B","KB","MB","GB"])
util.sizeFilter = function(...obj) {
let res = pubfn.calcSize(...obj);
return res.title;
};
export default util;
@@ -0,0 +1,60 @@
// #ifdef H5
/**
* H5专用的一些函数,会涉及到window等H5专用的内置对象。
*/
var h5 = {};
// 引入 微信公众号 js_sdk 包
import wx from './weixin-js-sdk'
h5.wx = wx;
/**
* 获取当前H5所在的环境
* let env = vk.h5.getEnv();
*/
h5.getEnv = function() {
let ua = window.navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger' && (ua.match(/miniprogram/i) == 'miniprogram')) {
// 微信小程序
return "mp-weixin";
}
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
// 微信公众号
return "h5-weixin";
}
if (ua.match(/alipay/i) == 'alipay' && ua.match(/miniprogram/i) == 'miniprogram') {
return "mp-alipay";
}
if (ua.match(/alipay/i) == 'alipay') {
return "h5-alipay";
}
// 外部 H5
return "h5";
};
/**
* 获取当前项目根目录访问地址
* https://www.aaa.com/pages/index/index?a=1&b=1
* http://192.168.66.66:8080/pages/index/index?a=1&b=1
* 获取当前H5的url地址的https://www.aaa.com 部分
* let getRootPath = vk.h5.getRootPath();
* let getRootPath = vk.h5.getRootPath({ projectName:true });
*/
h5.getRootPath = function(obj={}) {
let {
projectName: _projectName = false
} = obj;
let wwwPath = window.location.href;
let pathName = window.location.pathname;
let pos = wwwPath.indexOf(pathName);
let localhostPaht = wwwPath.substring(0, pos);
let projectName = pathName.substring(0, pathName.substr(1).indexOf('/') + 1);
let rootPath = localhostPaht;
if (_projectName) {
rootPath = localhostPaht + projectName;
}
return rootPath;
};
export default h5;
// #endif
@@ -0,0 +1,59 @@
/**
* 导出云对象实例
* @param {String} name 云对象路径,如:client/pub
* @example const pubObject = uni.vk.importObject('client/pub'); // 导入云对象
* 注意,只能在声明 async 的函数内使用,如:
async test(){
let res = await pubObject.getList({
title: "请求中",
data: {
a: 1,
b: "2"
}
});
}
*/
var importObject = function(name, importObjectOptions = {}) {
const newObj = new Proxy(importObject, {
get: function(target, key, receiver) {
/**
* 导出云对象内的某个方法
* @param {Object} data 请求参数,如 { a:1, b:"2" } 云对象内可通过 let { a, b } = data; 获取参数
* @param {String} title 遮罩层提示语,为空或不传则代表不显示遮罩层。
* @param {Boolean} needAlert 为true代表请求错误时,会有弹窗提示。默认为true
* @param {Object} loading 与title二选一,格式为 { name: "loading", that: that } name是变量名,that是数据源,当发起请求时,自动that[name] = true; 请求结束后,自动that[name] = false;
*/
return async function(options) {
// 如果importObjectOptions中指定了easy为true,代表options的值就是请求参数
if (importObjectOptions.easy) {
options = {
data: options
}
}
// 如果importObjectOptions中指定了data,代表有默认请求参数,需要加到请求参数中
if (importObjectOptions.data) {
if (typeof importObjectOptions.data === "function") {
options.data = Object.assign({}, importObjectOptions.data(), options.data)
} else {
options.data = Object.assign({}, importObjectOptions.data, options.data)
}
}
return uni.vk.callFunction({
...importObjectOptions,
...options,
url: `${name}.${key}`
});
}
},
// set: function(target, key, value, receiver) {
// console.log("set");
// console.log("target",target);
// console.log("key",key);
// console.log("value",value);
// console.log("receiver", receiver);
// },
});
return newObj;
};
export default importObject;
@@ -0,0 +1,131 @@
/**
* vk依赖扩展 - localStorage 本地缓存
* pub - 公共的可随时删除的缓存
* kh - 跟登录账号有关联的缓存
* sys - 系统级缓存 - 一般不删除
*
* 储存缓存
* vk.setStorageSync(key, data);
* 获取缓存
* vk.getStorageSync(key);
*/
var localStorage = {};
/**
* 将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。
* @param {String} key 本地缓存中的指定的 key
* @param {Any} data 需要存储的内容,只支持原生类型、及能够通过 JSON.stringify 序列化的对象
*/
localStorage.setStorageSync = function(key, data) {
uni.setStorageSync(key, data);
watchLocalStorage({ type: "set", key, data });
};
/**
* 从本地缓存中异步获取指定 key 对应的内容。
* @param {String} key 本地缓存中的指定的 key
*/
localStorage.getStorageSync = function(key) {
let data = uni.getStorageSync(key);
// get 没有必要监听
// watchLocalStorage({ type:"get", key, data });
return data;
};
/**
* 同步获取当前 storage 的相关信息。
*/
localStorage.getStorageInfoSync = function() {
let info = uni.getStorageInfoSync();
let sizeInfo = calcSize(Number(info.currentSize), 1024, 3, ["KB", "MB", "GB"]);
info.sizeInfo = sizeInfo;
return info;
};
/**
* 从本地缓存中同步移除指定 key。
* @param {String} key 本地缓存中的指定的 key
*/
localStorage.removeStorageSync = function(key) {
uni.removeStorageSync(key);
watchLocalStorage({ type: "remove", key });
};
/**
* 从本地缓存中异步移除指定 key。
*/
localStorage.removeStorage = function(obj) {
uni.removeStorage({
key: obj.key,
success: (res) => {
watchLocalStorage({ type: "remove", key: obj.key });
if (obj.success) obj.success(res);
},
fail: obj.fail,
complete: obj.complete
});
};
/**
* 同步清理本地数据缓存。若key有值,则清除键值为指定字符串开头的缓存
* @param {String} key 本地缓存中的指定的 key
* vk.clearStorageSync();
*/
localStorage.clearStorageSync = function(key) {
if (key) {
let { keys } = uni.getStorageInfoSync();
if (keys) {
keys.map((keyName) => {
if (keyName.indexOf(key) == 0) {
localStorage.removeStorage({
key: keyName
});
}
});
}
} else {
uni.clearStorage();
watchLocalStorage({ type: "clear" });
}
};
export default localStorage;
/**
* 单位进制换算
* length 换算前大小
* ary 进制,如KB-MB-GB,进制1024
* precision 数值精度
* arr 进制的数组,如["B","KB","MB","GB"]
* calcSize(length,1024,3,["B","KB","MB","GB"]);
*/
function calcSize(length = 0, ary, precision, arr) {
var size = parseFloat(length);
var mySize = 0;
var type = "";
if (size < ary || arr.length <= 1) {
type = arr[0];
mySize = parseFloat(size.toFixed(precision));
} else {
for (var i = 1; i < arr.length; i++) {
var g = arr[i];
size = size / ary;
if (size < ary) {
type = g;
mySize = parseFloat(size.toFixed(precision));
break;
}
}
}
return {
size: mySize,
type: type,
title: mySize + " " + type
}
};
function watchLocalStorage(obj) {
if (typeof localStorage.watch === "function") {
localStorage.watch(obj);
}
}
@@ -0,0 +1,462 @@
/**
* 函数 - 页面导航
*/
// #ifndef VUE3
var config;
try {
config = require('@/app.config.js');
if (typeof config.default === "object") {
config = config.default;
}
} catch (e) {
config = {};
}
// #endif
// #ifdef VUE3
import config from '@/app.config.js'
// #endif
var util = {};
var lastNavigate = {
url: "",
time: 0
};
/**
* 保留当前页面,跳转到应用内的某个页面,使用vk.navigateBack可以返回到原页面。
* vk.navigateTo(url);
*/
util.navigateTo = function(obj) {
let vk = uni.vk;
if (typeof obj == "string") {
let url = obj;
obj = {
url: url
};
} else if (typeof obj == "undefined") {
obj = {};
}
if (!obj.url) {
vk.toast("url不能为空!");
return false;
}
let time = Date.now();
if (lastNavigate.url === obj.url && (time - lastNavigate.time) < 200) {
return false;
}
lastNavigate = { url: obj.url, time };
util.checkNeedLogin({
url: obj.url,
success: function(res) {
if (res.needLogin) {
obj.url = vk.pubfn.getPageFullPath(obj.url);
vk.navigate.setOriginalPage(obj);
obj.url = config.login.url;
// login拦截器开始-----------------------------------------------------------
let { interceptor = {} } = config;
if (typeof interceptor.login === "function") {
let key = interceptor.login({
vk,
params: obj,
res: {
...res,
code: 30204,
msg: "本地token校验未通过"
}
});
if (typeof key === "undefined" || key !== true) return false;
}
// login拦截器结束-----------------------------------------------------------
} else {
vk.navigate.setOriginalPage(null);
}
util._navigateTo(obj);
}
});
};
util._navigateTo = function(obj) {
let { interceptor = {} } = config;
if (typeof interceptor.navigateTo === "function") {
let vk = uni.vk;
obj.pagePath = vk.pubfn.getPageFullPath(obj.url);
let key = interceptor.navigateTo({
...obj,
vk
});
if (typeof key == "boolean" && key === false) return false;
}
let {
url,
animationType = "pop-in",
animationDuration = 300,
events,
mode = "navigateTo"
} = obj;
// 此处写法仅为支持vue3,vue3不支持uni[apiName]的形式调用
let navigateFn;
if (mode === "navigateTo") {
navigateFn = uni.navigateTo;
} else if (mode === "redirectTo") {
navigateFn = uni.redirectTo;
} else if (mode === "reLaunch") {
navigateFn = uni.reLaunch;
} else if (mode === "switchTab") {
navigateFn = uni.switchTab;
} else {
navigateFn = uni.navigateTo;
}
// 此处写法仅为支持vue3,vue3不支持uni[apiName]的形式调用
navigateFn({
url: url,
animationType: animationType,
animationDuration: animationDuration,
events: events, // 参考 https://uniapp.dcloud.io/api/router?id=navigateto
success: function(res) {
if (typeof obj.success == "function") obj.success(res);
},
fail: function(err) {
if (err.errMsg.indexOf("not found") > -1) {
let vk = uni.vk;
let errUrl = vk.pubfn.getPageFullPath(url);
vk.toast(`页面 ${errUrl} 不存在`, "none");
console.error(err);
return false;
}
uni.switchTab({
url: url,
success: obj.success,
fail: function() {
uni.redirectTo({
url: url,
success: obj.success,
fail: function(err) {
console.error(err);
if (typeof obj.fail == "function") obj.fail(err);
}
});
}
});
},
complete: function(res) {
if (typeof obj.complete == "function") obj.complete(res);
}
});
};
/**
* 关闭当前页面,跳转到应用内的某个页面。
* vk.redirectTo(url);
*/
util.redirectTo = function(obj) {
obj = util.paramsInit(obj);
obj.mode = "redirectTo";
util.navigateTo(obj);
};
/**
* 关闭所有页面,打开到应用内的某个页面。
* vk.reLaunch(url);
*/
util.reLaunch = function(obj) {
obj = util.paramsInit(obj);
obj.mode = "reLaunch";
util.navigateTo(obj);
};
/**
* 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。
* vk.switchTab(url);
*/
util.switchTab = function(obj) {
obj = util.paramsInit(obj);
obj.mode = "switchTab";
util.navigateTo(obj);
};
/**
* 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。
* vk.navigateBack();
*/
util.navigateBack = function(obj) {
let vk = uni.vk;
if (typeof obj == "number") {
let delta = obj;
obj = {
delta: delta
};
} else if (typeof obj == "undefined") {
obj = {};
}
let {
delta = 1,
animationType = "pop-out",
animationDuration = 300
} = obj;
uni.navigateBack({
delta: delta,
animationType: animationType,
animationDuration: animationDuration,
success: function() {
if (typeof obj.success == "function") obj.success();
},
fail: function(res) {
console.error(res);
if (typeof obj.fail == "function") obj.fail();
},
complete: function() {
if (typeof obj.complete == "function") obj.complete();
}
});
};
/**
* 跳转到登录前的页面
* vk.navigate.originalTo();
*/
util.originalTo = function() {
let vk = uni.vk;
let originalPage = vk.navigate.getOriginalPage();
vk.navigate.setOriginalPage(null);
util.redirectTo(originalPage);
};
/**
* 获取登录前的页面
* vk.navigate.getOriginalPage();
*/
util.getOriginalPage = function() {
if (typeof uni.vk.getVuex === "function") {
// 有安装vuex则使用vuex
return uni.vk.getVuex('$app.originalPage');
} else {
// 未安装则使用本地缓存
return uni.vk.getStorageSync('vk.navigate.originalPage');
}
};
/**
* 设置登录前的页面
* vk.navigate.setOriginalPage(originalPage);
*/
util.setOriginalPage = function(originalPage) {
uni.vk.navigate.originalPage = originalPage; // 兼容老版本
if (typeof uni.vk.getVuex === "function") {
// 有安装vuex则使用vuex
return uni.vk.setVuex('$app.originalPage', originalPage);
} else {
// 未安装则使用本地缓存
return uni.vk.setStorageSync('vk.navigate.originalPage', originalPage);
}
};
/**
* 跳转到首页
* vk.navigateToHome();
*/
util.navigateToHome = function(obj = {}) {
let vk = uni.vk;
let {
mode = "reLaunch"
} = obj;
vk[mode](config.index.url);
};
/**
* 跳转到登录页
* vk.navigateToLogin();
*/
util.navigateToLogin = function(obj = {}) {
let vk = uni.vk;
let {
mode = "reLaunch"
} = obj;
vk[mode](config.login.url);
};
/**
* 检测是否满足条件(内部方法)
util.checkWildcardTest({
url:url,
pagesRule:config.checkTokenPages,
success:function(res){
if(res.success){
}
}
})
*/
util.checkWildcardTest = function(obj) {
let vk = uni.vk;
let {
url,
pagesRule
} = obj;
// ../ 转成绝对路径
url = vk.pubfn.getPageFullPath(url);
let key = false;
if (vk.pubfn.isNotNull(pagesRule)) {
let { mode = 0, list = [] } = pagesRule;
if (mode > 0) {
let regExpKey = false;
let path = util.getPagePath(url);
for (let i = 0; i < list.length; i++) {
let pageRegExp = list[i];
regExpKey = vk.pubfn.wildcardTest(path, pageRegExp);
if (regExpKey) {
break;
}
}
if (mode === 1 && regExpKey) {
key = true;
} else if (mode === 2 && !regExpKey) {
key = true;
}
}
}
return {
url,
key
};
};
/**
* 检测是否需要登录(内部方法)
util.checkNeedLogin({
url:url,
success:function(res){
if(res.needLogin){
}
}
})
*/
util.checkNeedLogin = function(obj) {
let vk = uni.vk;
let { url, success } = obj;
let needLogin = false; // 用户是否需要重新登录
let pageNeedLogin = false; // 该页面是否需要登录才能访问
let pagesRule = config.checkTokenPages;
if (pagesRule) {
let res = util.checkWildcardTest({
url,
pagesRule
});
pageNeedLogin = res.key;
if (pageNeedLogin) {
// 本地判断token有效期(联网会浪费性能)
if (!vk.checkToken()) {
needLogin = true;
}
}
}
success({
url,
needLogin,
pageNeedLogin
});
};
// 获取?前面的地址
util.getPagePath = function(url) {
let pathIndex = url.indexOf("?");
let path = url;
if (pathIndex > -1) {
path = path.substring(0, pathIndex);
}
return path;
};
util.paramsInit = function(obj) {
let vk = uni.vk;
if (typeof obj == "string") {
let url = obj;
obj = {
url: url
};
} else if (typeof obj == "undefined") {
obj = {};
}
if (!obj.url) {
vk.toast("url不能为空!");
return false;
}
return obj;
}
/**
* 跳转到小程序
vk.navigateToMiniProgram({
appId: 'appId',
path: 'pages/index/index',
extraData:{
//发送数据携带的参数
},
success(res) {
// 打开成功
}
})
*/
util.navigateToMiniProgram = function(obj) {
let vk = uni.vk;
// #ifdef H5
vk.toast("不支持打开小程序", "none");
// #endif
// #ifndef H5
uni.navigateToMiniProgram(obj);
// #endif
};
/**
* 检测是否可以分享(内部方法)
util.checkAllowShare({
url:url,
});
*/
util.checkAllowShare = function(obj) {
let vk = uni.vk;
let { url, success } = obj;
let pagesRule = config.checkSharePages || {};
if (pagesRule && pagesRule.mode > 0) {
let res = util.checkWildcardTest({
url,
pagesRule
});
// #ifdef MP
let menus = pagesRule.menus || ['shareAppMessage', 'shareTimeline'];
if (res.key) {
//console.log("允许分享");
uni.showShareMenu({
withShareTicket: false,
menus
});
} else {
//console.log("禁止分享");
uni.hideShareMenu({
menus
})
}
// #endif
}
};
util.$emit = function(...obj) {
return uni.$emit(...obj);
};
util.$on = function(...obj) {
return uni.$on(...obj);
};
util.$once = function(...obj) {
return uni.$once(...obj);
};
util.$off = function(...obj) {
return uni.$off(...obj);
};
export default util;
@@ -0,0 +1,434 @@
var requestUtil = {};
requestUtil.config = {
// 请求配置
request: {
// 公共请求参数(每次请求都会带上的参数)
dataParam: {}
},
requestGlobalParamKeyName: "vk_url_request_global_param",
debug: process.env.NODE_ENV !== "production",
// 日志风格
logger: {
colorArr: [
"#0095ff",
"#67C23A"
]
},
};
var counterNum = 0;
/**
* request 请求库
* 注意: 该请求库目前主要是框架自身使用,且近期变动频率较高,目前请勿使用在你自己的业务中。
* @param {String} url 服务器接口地址
* @param {Object/String/ArrayBuffer} data 请求的参数 App(自定义组件编译模式)不支持ArrayBuffer类型
* @param {Object} header 设置请求的 header,header 中不能设置 Referer。 App、H5端会自动带上cookie,且H5端不可手动修改
* @param {String} method 默认 POST 可选 GET
* @param {Number} timeout 超时时间,单位 ms 默认3000(30秒)
* @param {String} dataType 默认json 如果设为 json,会尝试对返回的数据做一次 JSON.parse
* @param {String} responseType 默认 text 设置响应的数据类型。合法值:text、arraybuffer App和支付宝小程序不支持
* @param {Boolean} sslVerify 默认 true 是否需要验证ssl证书 仅App安卓端支持(HBuilderX 2.3.3+) 仅App安卓端支持(HBuilderX 2.3.3+)
* @param {Boolean} withCredentials 默认 false 跨域请求时是否携带凭证(cookies) 仅H5支持(HBuilderX 2.6.15+)
* @param {Boolean} firstIpv4 默认 false DNS解析时优先使用ipv4 仅 App-Android 支持 (HBuilderX 2.8.0+)
* @param {String} success 成功回调
* @param {String} fail 失败回调
* @param {String} complete 完成回调
* 以下为特殊参数
* @param {String} errorCodeName 服务器返回的错误码的字段名,若不为空,则会对返回结果进行判断
* @param {String} errorMsgName 服务器返回的错误码的字符串含义,若不为空,且errorCodeName对应的值不为0,则会alert弹窗
* @param {Boolean} needAlert 服务器返回的错误时,是否需要alert弹出提示
*
* 调用示例
vk.request({
url: `https://www.xxx.com/api/xxxx`,
method:"POST",
header:{
"content-type": "application/json; charset=utf-8",
},
data:{
},
success: function(data){
},
fail: function(err){
}
});
*/
requestUtil.request = function(obj = {}) {
let vk = uni.vk;
// 去除值为 undefined 的参数
if (typeof obj.data === "object") {
obj.data = vk.pubfn.copyObject(obj.data);
}
// 注入自定义全局参数开始-----------------------------------------------------------
let config = requestUtil.config;
let globalParam = uni.getStorageSync(config.requestGlobalParamKeyName) || {};
// 根据正则规格自动匹配全局请求参数
for (let i in globalParam) {
let customDate = globalParam[i];
if (customDate.regExp) {
if (typeof customDate.regExp === "object") {
// 数组形式
for (let i = 0; i < customDate.regExp.length; i++) {
let regExp = new RegExp(customDate.regExp[i]);
if (regExp.test(obj.url)) {
obj.data = Object.assign(customDate.data, obj.data);
break;
}
}
} else {
// 字符串形式
let regExp = new RegExp(customDate.regExp);
if (regExp.test(obj.url)) {
obj.data = Object.assign(customDate.data, obj.data);
}
}
}
}
// 根据指定globalParamName匹配全局请求参数
let customDate = requestUtil.getRequestGlobalParam(obj.globalParamName);
if (customDate && JSON.stringify(customDate) !== "{}") {
if (customDate.regExp) {
obj.data = Object.assign(customDate.data, obj.data); // 新版本
} else {
obj.data = Object.assign(customDate, obj.data); // 兼容旧版本
}
}
// 注入自定义全局参数结束-----------------------------------------------------------
if (!obj.method) obj.method = "POST"; // 默认POST请求
if (typeof obj.dataType === "undefined") obj.dataType = "json";
if (obj.dataType == "default" || obj.dataType === "") delete obj.dataType;
if (typeof obj.header === "undefined" && typeof obj.headers !== "undefined") {
obj.header = obj.headers;
}
// 自动注入token到请求头开始-----------------------------------------------------------
// 注意:自2.15.1开始,需要手动指定uniIdToken: true 才会自动添加token到请求头里
if (typeof vk.getToken === "function" && obj.uniIdToken) {
let uni_id_token;
if (obj.uniIdToken === true) {
uni_id_token = vk.getToken();
} else if (typeof obj.uniIdToken === "boolean") {
uni_id_token = obj.uniIdToken;
}
if (uni_id_token) {
if (!obj.header) obj.header = {};
obj.header["uni-id-token"] = uni_id_token;
}
}
// 自动注入token到请求头结束-----------------------------------------------------------
let interceptor = obj.interceptor;
delete obj.interceptor;
if (interceptor && typeof interceptor.invoke === "function") {
let interceptorRes = interceptor.invoke(obj);
if (interceptorRes === false) {
return;
}
}
if (typeof obj.timeout === "undefined") obj.timeout = 30000; // 超时时间,单位 ms(默认30秒)
let Logger = {};
if (config.debug) {
Logger.params = (typeof obj.data == "object") ? vk.pubfn.copyObject(obj.data) : obj.data;
Logger.startTime = new Date().getTime();
let url = obj.url;
let path = obj.url.split("?")[0];
if (path.indexOf("://") > -1) {
path = path.substring(path.indexOf("://") + 3);
Logger.domainName = path.substring(0, path.indexOf("/"));
} else {
Logger.domainName = "";
}
Logger.action = path.substring(path.indexOf("/") + 1);
Logger.url = url;
}
if (obj.title) vk.showLoading(obj.title);
if (obj.loading) vk.setLoading(true, obj.loading);
let promiseAction = new Promise(function(resolve, reject) {
uni.request({
...obj,
success: (res) => {
if (interceptor && typeof interceptor.success === "function") {
let interceptorRes = interceptor.success(res);
if (interceptorRes === false) {
return;
}
}
requestSuccess({
res,
params: obj,
Logger,
resolve,
reject
});
},
fail: (res) => {
if (interceptor && typeof interceptor.fail === "function") {
let interceptorRes = interceptor.fail(res);
if (interceptorRes === false) {
return;
}
}
requestFail({
res,
params: obj,
Logger,
reject
});
},
complete: (res) => {
if (interceptor && typeof interceptor.complete === "function") {
let interceptorRes = interceptor.complete(res);
if (interceptorRes === false) {
return;
}
}
requestComplete({
res,
params: obj,
Logger
});
}
});
});
promiseAction.catch(error => {});
return promiseAction;
}
// 请求成功回调
function requestSuccess(obj = {}) {
let vk = uni.vk;
let config = requestUtil.config;
let {
res = {},
params,
Logger,
resolve,
reject
} = obj;
let {
title,
needOriginalRes,
dataType,
errorCodeName,
errorMsgName,
success,
loading
} = params;
let data = res.data || {};
if (vk.pubfn.isNotNullAll(errorCodeName, data[errorCodeName])) {
data.code = data[errorCodeName];
delete data[errorCodeName];
}
if (vk.pubfn.isNotNullAll(errorMsgName, data[errorMsgName])) {
data.msg = data[errorMsgName];
if (typeof data[errorMsgName] === "string") {
delete data[errorMsgName];
}
}
if (config.debug) Logger.result = (typeof data == "object") ? vk.pubfn.copyObject(data) : data;
if ([1301, 1302, 30201, 30202, 30203, 30204].indexOf(data.code) > -1 && data.msg.indexOf("token") > -1) {
// 执行 login 拦截器函数(跳转到页面页面)
let { interceptor = {} } = vk.callFunctionUtil;
if (typeof interceptor.login === "function") {
interceptor.login({
res: data,
params,
vk
});
}
reject(data);
} else if (res.statusCode >= 400 || data.code) {
requestFail({
res: data,
params,
Logger,
reject
});
} else {
if (title) vk.hideLoading();
if (loading) vk.setLoading(false, loading);
if (needOriginalRes) data.originalRes = vk.pubfn.copyObject(res);
if (data.vk_uni_token) vk.callFunctionUtil.saveToken(data.vk_uni_token);
if (data.userInfo && data.needUpdateUserInfo) vk.callFunctionUtil.updateUserInfo(data);
if (typeof success === "function") success(data);
if (typeof resolve === "function") resolve(data);
}
}
// 请求失败回调
function requestFail(obj = {}) {
let vk = uni.vk;
let config = requestUtil.config;
let {
res = {},
params,
Logger,
reject
} = obj;
let {
title,
needAlert,
fail,
loading
} = params;
if (typeof needAlert === "undefined") {
needAlert = (typeof fail === "function") ? false : true;
}
let errMsg = "";
let sysErr = false;
if (typeof res.code !== "undefined") {
errMsg = res.msg;
} else {
sysErr = true;
errMsg = JSON.stringify(res);
}
if (errMsg.indexOf("fail timeout") > -1) {
sysErr = true;
errMsg = "请求超时,请重试!";
}
if (config.debug) Logger.error = res;
if (title) vk.hideLoading();
if (loading) vk.setLoading(false, loading);
let runKey = true;
// 自定义拦截器开始-----------------------------------------------------------
let { interceptor = {} } = vk.callFunctionUtil.getConfig();
if (interceptor.request && typeof interceptor.request.fail == "function") {
runKey = interceptor.request.fail({
vk,
res: res,
params: params
});
if (runKey === undefined) runKey = true;
}
// 自定义拦截器结束-----------------------------------------------------------
if (runKey) {
if (needAlert && vk.pubfn.isNotNull(errMsg)) {
if (sysErr) {
vk.toast("网络开小差了!", "none");
} else {
vk.alert(errMsg);
}
}
if (typeof fail === "function") fail(res);
if (typeof reject === "function") reject(res);
}
}
// 请求完成回调
function requestComplete(obj = {}) {
let vk = uni.vk;
let config = requestUtil.config;
let {
res = {},
params,
Logger
} = obj;
let {
title,
needOriginalRes,
complete
} = params;
if (config.debug) {
Logger.endTime = new Date().getTime();
Logger.runTime = (Logger.endTime - Logger.startTime);
let colorArr = config.logger.colorArr;
let colorStr = colorArr[counterNum % colorArr.length];
counterNum++;
console.log("%c--------【开始】【服务器请求】【" + Logger.action + "】--------", 'color: ' + colorStr + ';font-size: 12px;font-weight: bold;');
console.log("【请求地址】: ", Logger.url);
console.log("【请求参数】: ", Logger.params);
console.log("【返回数据】: ", Logger.result);
console.log("【请求状态】: ", res.statusCode, "【http状态码】");
console.log("【总体耗时】: ", Logger.runTime, "毫秒【含页面渲染】");
console.log("【请求时间】: ", vk.pubfn.timeFormat(Logger.startTime, "yyyy-MM-dd hh:mm:ss"));
if (Logger.error) {
let errorLog = console.warn || console.error;
if (Logger.error.err && Logger.error.err.stack) {
console.error("【Error】: ", Logger.error);
console.error("【Stack】: ", Logger.error.err.stack);
} else {
errorLog("【Error】: ", Logger.error);
}
}
console.log("%c--------【结束】【服务器请求】【" + Logger.action + "】--------", 'color: ' + colorStr + ';font-size: 12px;font-weight: bold;');
}
let data = res.data;
if (needOriginalRes) data.originalRes = vk.pubfn.copyObject(res);
if (typeof complete === "function") complete(data);
}
/**
* 修改请求配置中的公共请求参数
* 若把shop-manage改成*则代表全局
vk.requestUtil.updateRequestGlobalParam({
"shop-manage":{
regExp:"^xxx/kh/",
data:{
shop_id : shop_id
}
}
});
对应的request中增加参数globalParamName:"shop-manage"
vk.request({
url: 'xxx/xxxxxx',
title: '请求中...',
globalParamName:"shop-manage",// 如果设置了正则规则,则不需要此参数
data: {},
success(data) {
}
});
*/
requestUtil.updateRequestGlobalParam = (data = {}, setKey) => {
let vk = uni.vk;
let config = requestUtil.config;
if (setKey) {
// 覆盖对象
config.request.dataParam = data;
} else {
// 覆盖参数(有就覆盖,没有则新增)
config.request.dataParam = Object.assign(config.request.dataParam, data);
}
vk.setStorageSync(config.requestGlobalParamKeyName, config.request.dataParam);
}
/**
* 获取请求配置中的公共请求参数
vk.requestUtil.getRequestGlobalParam();
*/
requestUtil.getRequestGlobalParam = (globalParamName = "*") => {
let vk = uni.vk;
let config = requestUtil.config;
let data = config.request.dataParam;
if (!data || JSON.stringify(data) === "{}") {
data = uni.getStorageSync(config.requestGlobalParamKeyName) || {};
config.request.dataParam = data;
}
let param = data[globalParamName] || {};
return JSON.parse(JSON.stringify(param));
}
/**
* 删除请求配置中的公共请求参数
* globalParamName 不传代表删除所有
vk.requestUtil.deleteRequestGlobalParam(globalParamName);
*/
requestUtil.deleteRequestGlobalParam = (globalParamName) => {
let vk = uni.vk;
let config = requestUtil.config;
let globalParam = uni.getStorageSync(config.requestGlobalParamKeyName) || {};
if (globalParamName) {
delete globalParam[globalParamName];
} else {
globalParam = {};
}
config.request.dataParam = globalParam;
vk.setStorageSync(config.requestGlobalParamKeyName, globalParam);
}
export default requestUtil;
@@ -0,0 +1,146 @@
/**
* vk依赖扩展 - sessionStorage 本地会话缓存(仅H5可用)
* sessionStorage 属性允许你访问一个,对应当前源的 session Storage 对象。
* 它与 localStorage 相似,不同之处在于 localStorage 里面存储的数据没有过期时间设置
* 而存储在 sessionStorage 里面的数据在页面会话结束时会被清除。
* pub - 公共的可随时删除的缓存
* kh - 跟登录账号有关联的缓存
* sys - 系统级缓存 - 一般不删除
*
* 储存缓存
* vk.setSessionStorageSync(key, data);
* 获取缓存
* vk.getSessionStorageSync(key);
*/
var storage = {};
/**
* 将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。
* @param {String} key 本地缓存中的指定的 key
* @param {Any} data 需要存储的内容,只支持原生类型、及能够通过 JSON.stringify 序列化的对象
* vk.setSessionStorageSync(key, data);
*/
storage.setSessionStorageSync = function(key, data = "") {
// #ifdef H5
let type = typeof data;
let value = {
type: typeof data,
data: data
};
sessionStorage.setItem(key, JSON.stringify(value));
watchSessionStorage({ type: "set", key, data });
// #endif
// #ifndef H5
console.warn("非H5环境不支持此API");
// #endif
};
/**
* 从本地缓存中异步获取指定 key 对应的内容。
* @param {String} key 本地缓存中的指定的 key
* vk.getSessionStorageSync(key);
*/
storage.getSessionStorageSync = function(key) {
// #ifdef H5
let data;
try {
let value = JSON.parse(sessionStorage.getItem(key));
data = value.data;
} catch (err) {
data = sessionStorage.getItem(key);
}
// get 没有必要监听
// watchSessionStorage({ type:"get", key, data });
return data;
// #endif
// #ifndef H5
console.warn("非H5环境不支持此API");
// #endif
};
/**
* 从本地缓存中同步移除指定 key。
* @param {String} key 本地缓存中的指定的 key
* vk.removeSessionStorageSync(key);
*/
storage.removeSessionStorageSync = function(key) {
// #ifdef H5
sessionStorage.removeItem(key);
watchSessionStorage({ type: "remove", key });
// #endif
// #ifndef H5
console.warn("非H5环境不支持此API");
// #endif
};
/**
* 同步清理本地数据缓存。若key有值,则清除键值为指定字符串开头的缓存
* @param {String} key 本地缓存中的指定的 key
* vk.clearSessionStorageSync();
*/
storage.clearSessionStorageSync = function(key) {
// #ifdef H5
if (key) {
let keys = Object.keys(sessionStorage);
if (keys) {
keys.map((keyName) => {
if (keyName.indexOf(key) == 0) {
sessionStorage.removeItem(keyName);
}
});
}
} else {
sessionStorage.clear();
watchSessionStorage({ type: "clear" });
}
// #endif
// #ifndef H5
console.warn("非H5环境不支持此API");
// #endif
};
export default storage;
/**
* 单位进制换算
* length 换算前大小
* ary 进制,如KB-MB-GB,进制1024
* precision 数值精度
* arr 进制的数组,如["B","KB","MB","GB"]
* calcSize(length,1024,3,["B","KB","MB","GB"]);
*/
function calcSize(length = 0, ary, precision, arr) {
var size = parseFloat(length);
var mySize = 0;
var type = "";
if (size < ary || arr.length <= 1) {
type = arr[0];
mySize = parseFloat(size.toFixed(precision));
} else {
for (var i = 1; i < arr.length; i++) {
var g = arr[i];
size = size / ary;
if (size < ary) {
type = g;
mySize = parseFloat(size.toFixed(precision));
break;
}
}
}
return {
size: mySize,
type: type,
title: mySize + " " + type
}
};
function watchSessionStorage(obj) {
if (typeof storage.watch === "function") {
storage.watch(obj);
}
}
@@ -0,0 +1,890 @@
function n(o, e) {
if (!o.jWeixin) {
var n,
c = {
config: "preVerifyJSAPI",
onMenuShareTimeline: "menu:share:timeline",
onMenuShareAppMessage: "menu:share:appmessage",
onMenuShareQQ: "menu:share:qq",
onMenuShareWeibo: "menu:share:weiboApp",
onMenuShareQZone: "menu:share:QZone",
previewImage: "imagePreview",
getLocation: "geoLocation",
openProductSpecificView: "openProductViewWithPid",
addCard: "batchAddCard",
openCard: "batchViewCard",
chooseWXPay: "getBrandWCPayRequest",
openEnterpriseRedPacket: "getRecevieBizHongBaoRequest",
startSearchBeacons: "startMonitoringBeacons",
stopSearchBeacons: "stopMonitoringBeacons",
onSearchBeacons: "onBeaconsInRange",
consumeAndShareCard: "consumedShareCard",
openAddress: "editAddress"
},
a = (function() {
var e = {};
for (var n in c) e[c[n]] = n;
return e;
})(),
i = o.document,
t = i.title,
r = navigator.userAgent.toLowerCase(),
s = navigator.platform.toLowerCase(),
d = !(!s.match("mac") && !s.match("win")),
u = -1 != r.indexOf("wxdebugger"),
l = -1 != r.indexOf("micromessenger"),
p = -1 != r.indexOf("android"),
f = -1 != r.indexOf("iphone") || -1 != r.indexOf("ipad"),
m = (n =
r.match(/micromessenger\/(\d+\.\d+\.\d+)/) ||
r.match(/micromessenger\/(\d+\.\d+)/)) ?
n[1] :
"",
g = {
initStartTime: L(),
initEndTime: 0,
preVerifyStartTime: 0,
preVerifyEndTime: 0
},
h = {
version: 1,
appId: "",
initTime: 0,
preVerifyTime: 0,
networkType: "",
isPreVerifyOk: 1,
systemType: f ? 1 : p ? 2 : -1,
clientVersion: m,
url: encodeURIComponent(location.href)
},
v = {},
S = { _completes: [] },
y = { state: 0, data: {} };
O(function() {
g.initEndTime = L();
});
var I = !1,
_ = [],
w = {
config: function(e) {
B("config", (v = e));
var t = !1 !== v.check;
O(function() {
if (t)
M(
c.config, {
verifyJsApiList: C(v.jsApiList),
verifyOpenTagList: C(v.openTagList)
},
(function() {
(S._complete = function(e) {
(g.preVerifyEndTime = L()), (y.state = 1), (y.data = e);
}),
(S.success = function(e) {
h.isPreVerifyOk = 0;
}),
(S.fail = function(e) {
S._fail ? S._fail(e) : (y.state = -1);
});
var t = S._completes;
return (
t.push(function() {
!(function() {
if (
!(
d ||
u ||
v.debug ||
m < "6.0.2" ||
h.systemType < 0
)
) {
var i = new Image();
(h.appId = v.appId),
(h.initTime = g.initEndTime - g
.initStartTime),
(h.preVerifyTime =
g.preVerifyEndTime - g
.preVerifyStartTime),
w.getNetworkType({
isInnerInvoke: !0,
success: function(e) {
h.networkType =
e
.networkType;
var n =
"https://open.weixin.qq.com/sdk/report?v=" +
h.version +
"&o=" +
h
.isPreVerifyOk +
"&s=" +
h
.systemType +
"&c=" +
h
.clientVersion +
"&a=" +
h.appId +
"&n=" +
h
.networkType +
"&i=" +
h.initTime +
"&p=" +
h
.preVerifyTime +
"&u=" +
h.url;
i.src = n;
}
});
}
})();
}),
(S.complete = function(e) {
for (var n = 0, i = t.length; n < i; ++n) t[n]
();
S._completes = [];
}),
S
);
})()
),
(g.preVerifyStartTime = L());
else {
y.state = 1;
for (var e = S._completes, n = 0, i = e.length; n < i; ++n)
e[n]();
S._completes = [];
}
}),
w.invoke ||
((w.invoke = function(e, n, i) {
o.WeixinJSBridge && WeixinJSBridge.invoke(e, x(n), i);
}),
(w.on = function(e, n) {
o.WeixinJSBridge && WeixinJSBridge.on(e, n);
}));
},
ready: function(e) {
0 != y.state ? e() : (S._completes.push(e), !l && v.debug && e());
},
error: function(e) {
m < "6.0.2" || (-1 == y.state ? e(y.data) : (S._fail = e));
},
checkJsApi: function(e) {
M(
"checkJsApi", { jsApiList: C(e.jsApiList) },
((e._complete = function(e) {
if (p) {
var n = e.checkResult;
n && (e.checkResult = JSON.parse(n));
}
e = (function(e) {
var n = e.checkResult;
for (var i in n) {
var t = a[i];
t && ((n[t] = n[i]), delete n[i]);
}
return e;
})(e);
}),
e)
);
},
onMenuShareTimeline: function(e) {
P(
c.onMenuShareTimeline, {
complete: function() {
M(
"shareTimeline", {
title: e.title || t,
desc: e.title || t,
img_url: e.imgUrl || "",
link: e.link || location.href,
type: e.type || "link",
data_url: e.dataUrl || ""
},
e
);
}
},
e
);
},
onMenuShareAppMessage: function(n) {
P(
c.onMenuShareAppMessage, {
complete: function(e) {
"favorite" === e.scene ?
M("sendAppMessage", {
title: n.title || t,
desc: n.desc || "",
link: n.link || location.href,
img_url: n.imgUrl || "",
type: n.type || "link",
data_url: n.dataUrl || ""
}) :
M(
"sendAppMessage", {
title: n.title || t,
desc: n.desc || "",
link: n.link || location.href,
img_url: n.imgUrl || "",
type: n.type || "link",
data_url: n.dataUrl || ""
},
n
);
}
},
n
);
},
onMenuShareQQ: function(e) {
P(
c.onMenuShareQQ, {
complete: function() {
M(
"shareQQ", {
title: e.title || t,
desc: e.desc || "",
img_url: e.imgUrl || "",
link: e.link || location.href
},
e
);
}
},
e
);
},
onMenuShareWeibo: function(e) {
P(
c.onMenuShareWeibo, {
complete: function() {
M(
"shareWeiboApp", {
title: e.title || t,
desc: e.desc || "",
img_url: e.imgUrl || "",
link: e.link || location.href
},
e
);
}
},
e
);
},
onMenuShareQZone: function(e) {
P(
c.onMenuShareQZone, {
complete: function() {
M(
"shareQZone", {
title: e.title || t,
desc: e.desc || "",
img_url: e.imgUrl || "",
link: e.link || location.href
},
e
);
}
},
e
);
},
updateTimelineShareData: function(e) {
M(
"updateTimelineShareData", { title: e.title, link: e.link, imgUrl: e.imgUrl },
e
);
},
updateAppMessageShareData: function(e) {
M(
"updateAppMessageShareData", {
title: e.title,
desc: e.desc,
link: e.link,
imgUrl: e
.imgUrl
},
e
);
},
startRecord: function(e) {
M("startRecord", {}, e);
},
stopRecord: function(e) {
M("stopRecord", {}, e);
},
onVoiceRecordEnd: function(e) {
P("onVoiceRecordEnd", e);
},
playVoice: function(e) {
M("playVoice", { localId: e.localId }, e);
},
pauseVoice: function(e) {
M("pauseVoice", { localId: e.localId }, e);
},
stopVoice: function(e) {
M("stopVoice", { localId: e.localId }, e);
},
onVoicePlayEnd: function(e) {
P("onVoicePlayEnd", e);
},
uploadVoice: function(e) {
M(
"uploadVoice", {
localId: e.localId,
isShowProgressTips: 0 == e.isShowProgressTips ? 0 : 1
},
e
);
},
downloadVoice: function(e) {
M(
"downloadVoice", {
serverId: e.serverId,
isShowProgressTips: 0 == e.isShowProgressTips ? 0 : 1
},
e
);
},
translateVoice: function(e) {
M(
"translateVoice", {
localId: e.localId,
isShowProgressTips: 0 == e.isShowProgressTips ? 0 : 1
},
e
);
},
chooseImage: function(e) {
M(
"chooseImage", {
scene: "1|2",
count: e.count || 9,
sizeType: e.sizeType || ["original", "compressed"],
sourceType: e.sourceType || ["album", "camera"]
},
((e._complete = function(e) {
if (p) {
var n = e.localIds;
try {
n && (e.localIds = JSON.parse(n));
} catch (e) {}
}
}),
e)
);
},
getLocation: function(e) {},
previewImage: function(e) {
M(c.previewImage, { current: e.current, urls: e.urls }, e);
},
uploadImage: function(e) {
M(
"uploadImage", {
localId: e.localId,
isShowProgressTips: 0 == e.isShowProgressTips ? 0 : 1
},
e
);
},
downloadImage: function(e) {
M(
"downloadImage", {
serverId: e.serverId,
isShowProgressTips: 0 == e.isShowProgressTips ? 0 : 1
},
e
);
},
getLocalImgData: function(e) {
!1 === I ?
((I = !0),
M(
"getLocalImgData", { localId: e.localId },
((e._complete = function(e) {
if (((I = !1), 0 < _.length)) {
var n = _.shift();
wx.getLocalImgData(n);
}
}),
e)
)) :
_.push(e);
},
getNetworkType: function(e) {
M(
"getNetworkType", {},
((e._complete = function(e) {
e = (function(e) {
var n = e.errMsg;
e.errMsg = "getNetworkType:ok";
var i = e.subtype;
if ((delete e.subtype, i)) e.networkType = i;
else {
var t = n.indexOf(":"),
o = n.substring(t + 1);
switch (o) {
case "wifi":
case "edge":
case "wwan":
e.networkType = o;
break;
default:
e.errMsg = "getNetworkType:fail";
}
}
return e;
})(e);
}),
e)
);
},
openLocation: function(e) {
M(
"openLocation", {
latitude: e.latitude,
longitude: e.longitude,
name: e.name || "",
address: e.address || "",
scale: e.scale || 28,
infoUrl: e.infoUrl || ""
},
e
);
},
getLocation: function(e) {
M(
c.getLocation, { type: (e = e || {}).type || "wgs84" },
((e._complete = function(e) {
delete e.type;
}),
e)
);
},
hideOptionMenu: function(e) {
M("hideOptionMenu", {}, e);
},
showOptionMenu: function(e) {
M("showOptionMenu", {}, e);
},
closeWindow: function(e) {
M("closeWindow", {}, (e = e || {}));
},
hideMenuItems: function(e) {
M("hideMenuItems", { menuList: e.menuList }, e);
},
showMenuItems: function(e) {
M("showMenuItems", { menuList: e.menuList }, e);
},
hideAllNonBaseMenuItem: function(e) {
M("hideAllNonBaseMenuItem", {}, e);
},
showAllNonBaseMenuItem: function(e) {
M("showAllNonBaseMenuItem", {}, e);
},
scanQRCode: function(e) {
M(
"scanQRCode", {
needResult: (e = e || {}).needResult || 0,
scanType: e.scanType || ["qrCode", "barCode"]
},
((e._complete = function(e) {
if (f) {
var n = e.resultStr;
if (n) {
var i = JSON.parse(n);
e.resultStr = i && i.scan_code && i.scan_code.scan_result;
}
}
}),
e)
);
},
openAddress: function(e) {
M(
c.openAddress, {},
((e._complete = function(e) {
e = (function(e) {
return (
(e.postalCode = e.addressPostalCode),
delete e.addressPostalCode,
(e.provinceName = e.proviceFirstStageName),
delete e.proviceFirstStageName,
(e.cityName = e.addressCitySecondStageName),
delete e.addressCitySecondStageName,
(e.countryName = e.addressCountiesThirdStageName),
delete e.addressCountiesThirdStageName,
(e.detailInfo = e.addressDetailInfo),
delete e.addressDetailInfo,
e
);
})(e);
}),
e)
);
},
openProductSpecificView: function(e) {
M(
c.openProductSpecificView, {
pid: e.productId,
view_type: e.viewType || 0,
ext_info: e.extInfo
},
e
);
},
addCard: function(e) {
for (var n = e.cardList, i = [], t = 0, o = n.length; t < o; ++t) {
var r = n[t],
a = { card_id: r.cardId, card_ext: r.cardExt };
i.push(a);
}
M(
c.addCard, { card_list: i },
((e._complete = function(e) {
var n = e.card_list;
if (n) {
for (var i = 0, t = (n = JSON.parse(n)).length; i < t; ++i) {
var o = n[i];
(o.cardId = o.card_id),
(o.cardExt = o.card_ext),
(o.isSuccess = !!o.is_succ),
delete o.card_id,
delete o.card_ext,
delete o.is_succ;
}
(e.cardList = n), delete e.card_list;
}
}),
e)
);
},
chooseCard: function(e) {
M(
"chooseCard", {
app_id: v.appId,
location_id: e.shopId || "",
sign_type: e.signType || "SHA1",
card_id: e.cardId || "",
card_type: e.cardType || "",
card_sign: e.cardSign,
time_stamp: e.timestamp + "",
nonce_str: e.nonceStr
},
((e._complete = function(e) {
(e.cardList = e.choose_card_info), delete e.choose_card_info;
}),
e)
);
},
openCard: function(e) {
for (var n = e.cardList, i = [], t = 0, o = n.length; t < o; ++t) {
var r = n[t],
a = { card_id: r.cardId, code: r.code };
i.push(a);
}
M(c.openCard, { card_list: i }, e);
},
consumeAndShareCard: function(e) {
M(
c.consumeAndShareCard, { consumedCardId: e.cardId, consumedCode: e.code },
e
);
},
chooseWXPay: function(e) {
M(c.chooseWXPay, V(e), e);
},
openEnterpriseRedPacket: function(e) {
M(c.openEnterpriseRedPacket, V(e), e);
},
startSearchBeacons: function(e) {
M(c.startSearchBeacons, { ticket: e.ticket }, e);
},
stopSearchBeacons: function(e) {
M(c.stopSearchBeacons, {}, e);
},
onSearchBeacons: function(e) {
P(c.onSearchBeacons, e);
},
openEnterpriseChat: function(e) {
M(
"openEnterpriseChat", { useridlist: e.userIds, chatname: e.groupName },
e
);
},
launchMiniProgram: function(e) {
M(
"launchMiniProgram", {
targetAppId: e.targetAppId,
path: (function(e) {
if ("string" == typeof e && 0 < e.length) {
var n = e.split("?")[0],
i = e.split("?")[1];
return (n += ".html"), void 0 !== i ? n + "?" + i : n;
}
})(e.path),
envVersion: e.envVersion
},
e
);
},
openBusinessView: function(e) {
M(
"openBusinessView", {
businessType: e.businessType,
queryString: e.queryString || "",
envVersion: e.envVersion
},
((e._complete = function(n) {
if (p) {
var e = n.extraData;
if (e)
try {
n.extraData = JSON.parse(e);
} catch (e) {
n.extraData = {};
}
}
}),
e)
);
},
miniProgram: {
navigateBack: function(e) {
(e = e || {}),
O(function() {
M(
"invokeMiniProgramAPI", {
name: "navigateBack",
arg: {
delta: e
.delta || 1
}
},
e
);
});
},
navigateTo: function(e) {
O(function() {
M(
"invokeMiniProgramAPI", { name: "navigateTo", arg: { url: e.url } },
e
);
});
},
redirectTo: function(e) {
O(function() {
M(
"invokeMiniProgramAPI", { name: "redirectTo", arg: { url: e.url } },
e
);
});
},
switchTab: function(e) {
O(function() {
M(
"invokeMiniProgramAPI", { name: "switchTab", arg: { url: e.url } },
e
);
});
},
reLaunch: function(e) {
O(function() {
M(
"invokeMiniProgramAPI", { name: "reLaunch", arg: { url: e.url } },
e
);
});
},
postMessage: function(e) {
O(function() {
M(
"invokeMiniProgramAPI", { name: "postMessage", arg: e.data || {} },
e
);
});
},
getEnv: function(e) {
O(function() {
e({ miniprogram: "miniprogram" === o.__wxjs_environment });
});
}
}
},
T = 1,
k = {};
return (
i.addEventListener(
"error",
function(e) {
if (!p) {
var n = e.target,
i = n.tagName,
t = n.src;
if ("IMG" == i || "VIDEO" == i || "AUDIO" == i || "SOURCE" == i)
if (-1 != t.indexOf("wxlocalresource://")) {
e.preventDefault(), e.stopPropagation();
var o = n["wx-id"];
if ((o || ((o = T++), (n["wx-id"] = o)), k[o])) return;
(k[o] = !0),
wx.ready(function() {
wx.getLocalImgData({
localId: t,
success: function(e) {
n.src = e.localData;
}
});
});
}
}
},
!0
),
i.addEventListener(
"load",
function(e) {
if (!p) {
var n = e.target,
i = n.tagName;
n.src;
if ("IMG" == i || "VIDEO" == i || "AUDIO" == i || "SOURCE" == i) {
var t = n["wx-id"];
t && (k[t] = !1);
}
}
},
!0
),
e && (o.wx = o.jWeixin = w),
w
);
}
function M(n, e, i) {
o.WeixinJSBridge ?
WeixinJSBridge.invoke(n, x(e), function(e) {
A(n, e, i);
}) :
B(n, i);
}
function P(n, i, t) {
o.WeixinJSBridge ?
WeixinJSBridge.on(n, function(e) {
t && t.trigger && t.trigger(e), A(n, e, i);
}) :
B(n, t || i);
}
function x(e) {
return (
((e = e || {}).appId = v.appId),
(e.verifyAppId = v.appId),
(e.verifySignType = "sha1"),
(e.verifyTimestamp = v.timestamp + ""),
(e.verifyNonceStr = v.nonceStr),
(e.verifySignature = v.signature),
e
);
}
function V(e) {
return {
timeStamp: e.timestamp + "",
nonceStr: e.nonceStr,
package: e.package,
paySign: e.paySign,
signType: e.signType || "SHA1"
};
}
function A(e, n, i) {
("openEnterpriseChat" != e && "openBusinessView" !== e) ||
(n.errCode = n.err_code),
delete n.err_code,
delete n.err_desc,
delete n.err_detail;
var t = n.errMsg;
t ||
((t = n.err_msg),
delete n.err_msg,
(t = (function(e, n) {
var i = e,
t = a[i];
t && (i = t);
var o = "ok";
if (n) {
var r = n.indexOf(":");
"confirm" == (o = n.substring(r + 1)) && (o = "ok"),
"failed" == o && (o = "fail"),
-1 != o.indexOf("failed_") && (o = o.substring(7)),
-1 != o.indexOf("fail_") && (o = o.substring(5)),
("access denied" !=
(o = (o = o.replace(/_/g, " ")).toLowerCase()) &&
"no permission to execute" != o) ||
(o = "permission denied"),
"config" == i && "function not exist" == o && (o = "ok"),
"" == o && (o = "fail");
}
return (n = i + ":" + o);
})(e, t)),
(n.errMsg = t)),
(i = i || {})._complete && (i._complete(n), delete i._complete),
(t = n.errMsg || ""),
v.debug && !i.isInnerInvoke && alert(JSON.stringify(n));
var o = t.indexOf(":");
switch (t.substring(o + 1)) {
case "ok":
i.success && i.success(n);
break;
case "cancel":
i.cancel && i.cancel(n);
break;
default:
i.fail && i.fail(n);
}
i.complete && i.complete(n);
}
function C(e) {
if (e) {
for (var n = 0, i = e.length; n < i; ++n) {
var t = e[n],
o = c[t];
o && (e[n] = o);
}
return e;
}
}
function B(e, n) {
if (!(!v.debug || (n && n.isInnerInvoke))) {
var i = a[e];
i && (e = i),
n && n._complete && delete n._complete,
console.log('"' + e + '",', n || "");
}
}
function L() {
return new Date().getTime();
}
function O(e) {
l &&
(o.WeixinJSBridge ?
e() :
i.addEventListener &&
i.addEventListener("WeixinJSBridgeReady", e, !1));
}
};
export default n(window);
@@ -0,0 +1,18 @@
let debug = process.env.NODE_ENV !== 'production';
const install = Vue => {
let vk = Vue.prototype ? Vue.prototype.vk : Vue.config.globalProperties.vk;
if (vk) {
vk.log = console.log;
if (typeof vk.getConfig === "function") {
debug = vk.getConfig().debug;
}
if(!debug){
console.log = function(...obj){};
}
}
}
export default {
install
}
@@ -0,0 +1,40 @@
var isOnLaunch = true;
export default {
data() {
return {}
},
onLoad() {
// 将vk实例挂载到app.globalData上,方便在非Vue页面自身函数中调用vk实例(因为获取不到this)
let app = getApp({ allowDefault: true });
if (app && app.globalData && !app.globalData.vk) {
app.globalData.vk = this.vk;
}
if (this.vk) {
const url = this.vk.pubfn.getCurrentPageRoute();
// 检测是否可以分享(小程序专属)
this.vk.navigate.checkAllowShare({ url });
// 检测是否需要登录,只有首次启动的页面才需要检测,其他页面通过 vk.navigateTo 跳转前会自动判断。
if (isOnLaunch && !this.vk.checkToken() && getCurrentPages().length == 1) {
isOnLaunch = false; // 重新标记为非首次页面
const currentPage = this.vk.pubfn.getCurrentPage() || {};
let pagePath = currentPage.pagePath || `/${currentPage.route}` || url;
let fullPath = currentPage.fullPath;
let options = currentPage.options;
this.vk.pubfn.checkLogin({ url: pagePath, fullPath, options, isOnLaunch: true }); // 检测是否需要登录
}
}
},
created() {
// 将vk实例挂载到app.globalData上,方便在非Vue页面自身函数中调用vk实例(因为获取不到this)
let app = getApp({ allowDefault: true });
if (app && app.globalData && !app.globalData.vk) {
app.globalData.vk = this.vk;
}
},
methods: {
$getData(data, key, defaultValue) {
let { vk } = this;
return vk.pubfn.getData(data, key, defaultValue);
}
}
}
@@ -0,0 +1,170 @@
/**
* 百度API
*/
var baidu = {};
var vk = {};
var baiduOpenApiAccessTokenCacheName = "vk_openapi_baidu_open_token";
baidu.init = function(util={}) {
vk = util.vk;
};
// 百度开放平台api
baidu.open = {};
// 文字识别
baidu.open.ocr = {};
/**
* 营业执照识别
* 以下data参数三选一即可
* @param {File} file 文件对象
* @param {String} image 图像base64编码后进行urlencode
* @param {String} url 图片完整URL
*
vk.openapi.baidu.open.ocr.business_license({
title:"识别中...",
data: {
file: res.tempFiles[0]
},
success: (res) => {
that.data = res.data;
},
});
*/
baidu.open.ocr.business_license = function(obj={}){
obj.action = "ocr/v1/business_license";
let file = obj.data.file;
if(file){
vk.pubfn.fileToBase64({ file }).then(base64 => {
obj.data.image = base64;
delete obj.data.file;
baidu.request(obj);
});
}else{
baidu.request(obj);
}
}
/**
* 身份证识别
* 以下data参数三选一即可
* @param {File} file 文件对象
* @param {String} image 图像base64编码后进行urlencode
* @param {String} url 图片完整URL
vk.openapi.baidu.open.ocr.idcard({
title:"识别中...",
data: {
image:base64,
id_card_side:"front", // front:身份证含照片的一面 back:身份证带国徽的一面
},
success: (res) => {
that.data = res.data;
},
});
*/
baidu.open.ocr.idcard = function(obj={}){
obj.action = "ocr/v1/idcard";
let file = obj.data.file;
if(!obj.data.id_card_side) obj.data.id_card_side = "front";
if(file){
vk.pubfn.fileToBase64({ file }).then(base64 => {
obj.data.image = base64;
delete obj.data.file;
baidu.request(obj);
});
}else{
baidu.request(obj);
}
}
/**
* 百度开放平台通用请求接口
* @param {String} action 接口名称
* @param {String} actionVersion 接口版本名称 默认2.0
* @param {String} title loading文字
* @param {object} data 请求参数
* @param {String} success 成功回调
* @param {String} fail 失败回调
* @param {String} complete 完成回调
* 使用示例
vk.openapi.baidu.request({
action: 'ocr/v1/business_license',
title:"识别中...",
data: {
image:base64
},
success: (data) => {
that.data = data;
},
});
*/
baidu.request = function(obj={}){
let {
title
} = obj;
if(title) vk.showLoading(title);
let baiduApiAccessToken = vk.getStorageSync(baiduOpenApiAccessTokenCacheName);
if(baiduApiAccessToken && baiduApiAccessToken.expTime > new Date().getTime()){
// 获取缓存中的token
obj.accessToken = baiduApiAccessToken.accessToken;
request(obj);
}else{
// 发起请求获取token
vk.callFunction({
url: 'plugs/baidu/client/pub/getAccessToken',
success:function(tokenRes) {
vk.setStorageSync(baiduOpenApiAccessTokenCacheName,{
accessToken : tokenRes.access_token,
expTime : 2590000000 + new Date().getTime(),
});
obj.accessToken = tokenRes.access_token;
request(obj);
},
fail:function(res){
if(title) vk.hideLoading();
if(typeof obj.fail === "function") obj.fail(res);
if(typeof obj.complete === "function") obj.complete(res);
}
});
}
}
function request(obj={}){
let {
action,
actionVersion = "2.0",
accessToken,
header = { "content-type": "application/x-www-form-urlencoded" },
data,
title
} = obj;
vk.request({
url: `https://aip.baidubce.com/rest/${actionVersion}/${action}?access_token=${accessToken}`,
method:"POST",
header:{
"content-type": "application/x-www-form-urlencoded",
},
errorCodeName:"error_code",
errorMsgName:"error_msg",
data,
needAlert:true,
success: function(data){
if(title) vk.hideLoading();
if(data.code){
if(typeof obj.fail === "function") obj.fail(data);
}else{
if(typeof obj.success === "function") obj.success(data);
}
},
fail: function(data){
if(title) vk.hideLoading();
// 如果是因为token失效报的错误,则清空下本地token缓存
if(data && data.code === 110) vk.removeStorageSync(baiduOpenApiAccessTokenCacheName);
if(typeof obj.fail === "function") obj.fail(data);
},
complete: function(data){
if(typeof obj.complete === "function") obj.complete(data);
}
});
}
export default baidu;
@@ -0,0 +1,14 @@
/**
* 开放API
*/
import baidu from './baidu'
var openapi = {};
openapi.baidu = baidu;
openapi.init = function(util){
openapi.baidu.init(util);
}
// 微信小程序API
export default openapi;
@@ -0,0 +1,25 @@
const install = Vue => {
const debug = process.env.NODE_ENV !== 'production';
let vk = Vue.prototype ? Vue.prototype.vk : Vue.config.globalProperties.vk;
if (debug && vk) {
const oldErrorHandler = Vue.config.errorHandler;
Vue.config.errorHandler = function errorHandler(err, vm, info) {
const route = vm.$page && vm.$page.route;
let date = new Date();
let log = {
err: err.toString(),
info,
route,
time: date.getTime(),
timeStr: date.toLocaleTimeString()
};
if(vk.vuex) vk.vuex.dispatch('$error/add', log);
return oldErrorHandler(err, vm, info);
}
}
}
export default {
install
}
@@ -0,0 +1,67 @@
export default {
beforeCreate() {
// 将vuex方法挂在到vk实例中 beforeCreate created
let { vk, $store } = this;
if (typeof $store !== "undefined" && typeof vk.getVuexStore === "undefined") {
vk.getVuexStore = function() {
return $store;
};
vk.vuex = (name, value) => {
$store.commit('updateStore', { name, value });
};
/**
* (推荐) 设置vuex
* vk.vuex.set('$user.userInfo.avatar', avatar);
*/
vk.vuex.set = (name, value) => {
$store.commit('updateStore', { name, value });
};
/**
* (推荐) 读取vuex(具有解除对象内存印射功能,且任意一层数据为undefined,不会报错)
* vk.vuex.get('$user.userInfo.avatar');
*/
vk.vuex.get = (name, defaultValue) => {
let value = vk.pubfn.getData($store.state, name);
if (typeof value === "undefined") return (typeof defaultValue !== "undefined") ? defaultValue : "";
return JSON.parse(JSON.stringify(value));
};
vk.vuex.getters = (name) => {
return $store.getters[name];
};
/**
* 触发vuex的指定actions(异步)
* $user是模块名,addFootprint是对应模块的action名称
* vk.vuex.dispatch('$user/addFootprint', data);
*/
vk.vuex.dispatch = $store.dispatch;
/**
* 触发vuex的指定mutations
* $user是模块名,setFootprint是对应模块的action名称
* vk.vuex.commit('$user/setFootprint', data);
*/
vk.vuex.commit = $store.commit;
/* 另一种方式 */
/**
* vk.setVuex('$user.userInfo.avatar', avatar);
*/
vk.setVuex = vk.vuex.set;
/**
* vk.getVuex('$user.userInfo.avatar');
*/
vk.getVuex = vk.vuex.get;
/* 兼容老版本 */
vk.state = vk.vuex.get;
try {
if (!vk.checkToken()) {
vk.callFunctionUtil.deleteUserInfo({
log: false
});
}
} catch (err) {}
}
},
computed: {
}
}
+150
View File
@@ -0,0 +1,150 @@
## 1.5.1(2023-10-13)
* 【修复】`u-parse` 在小程序上部分情况下不显示图片的问题
* 【修复】`u-alert-tips` 使用 `closeText` 时 `close` 事件无效的问题 & 样式微调
## 1.5.0(2023-06-01)
* 【修复】`u-parse` 在vue3真机白屏问题和h5样式污染问题
* 【修复】`u-select` 默认值的问题
* 【修复】`u-parse` 在app和微信小程序中 `preview` 属性不生效的问题
## 1.4.8(2023-05-17)
* 【优化】部分组件属性的可选值的代码提示
* 【优化】`u-circle-progres` 组件 在进度条到100%的时候发出一个 `finish` 事件
* 【修复】`u-picker` 的时间选择器在ios下默认值无效的bug
* 【修复】`u-card` 圆角问题
* 【修复】`u-button` 支持 `info` 类型
## 1.4.7(2023-04-24)
* 【修复】`u-picker` 在vue3的App环境下默认值无效的bug
## 1.4.5(2022-12-05)
* 【修复】`u-row`、`u-col` 使用 `@click.stop` 会报错的问题,同时建议改用 `@click.native.stop` 来代替 `@click.stop`
## 1.4.4(2022-11-12)
* 【修复】`u-calendar` `u-modal` `u-picker` `u-popup` 组件在页面进入后马上需要弹窗时,无法正常弹窗的问题。
## 1.4.3(2022-10-22)
* 【优化】部分组件的细节
## 1.4.2(2022-10-15)
* 【修复】`1.4.1` 引出的 `u-subsection` 的部分问题
## 1.4.1(2022-10-14)
* 【修复】`u-count-to` 若设置了千分位符合,会错误显示-的问题
* 【修复】`u-subsection` 部分细节问题
## 1.4.0(2022-10-07)
* 【修复】`u-section` 点击更多,会触发两次事件的问题
* 【修复】`loadMore` 加载更多,icon-type为circle不会转动的问题
* 【修复】`u-subsection` current 有转入值时,变更值,样式不更新(需用 `v-model="current"` 代替 `:current="current"`)
## 1.3.13(2022-09-28)
* 【修复】`u-avatar-cropper` 组件在vue3中会报错的问题。
## 1.3.12(2022-08-30)
* 【优化】`u-keyboard` 组件内部细节。
## 1.3.11(2022-08-30)
* 【修复】`u-subsection` 组件的 `list` 属性不支持动态修改的问题。
## 1.3.10(2022-07-30)
* 【优化】上传组件部分细节
## 1.3.9(2022-07-07)
* 【更新】省市区数据源
* 【优化】`u-subsection` 组件支持在右上角显示数字角标
```html
<template>
<u-subsection :list="list"></u-subsection>
</template>
```
```js
export default {
data() {
return {
list: [
{
name: '待发货',
num: 10
},
{
name: '待付款',
num: 5
},
{
name: '待评价',
num: 15
}
]
}
}
}
```
## 1.3.8(2022-06-13)
* 【优化】组件 `u-icon`,使之更方便的兼容第三方icon(满足规则自动计算customPrefix)
**规则如下:**
* 当 `name` 中包含 `-icon-` 字符串时
* 如 `vk-icon-goods`,则组件的 `customPrefix` 属性自动识别为 `vk-icon` ,`name`属性 自动识别为 `goods`
* 如 `vk-2-icon-goods-list`,则组件的 `customPrefix` 属性自动识别为 `vk-2-icon` ,`name`属性 自动识别为 `goods-list`
## 1.3.7(2022-06-10)
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall` 在 `vue3` 模式下的细节问题。
## 1.3.6(2022-06-10)
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall` 在 `vue3` 模式下的细节问题。
## 1.3.5(2022-05-28)
* 【优化】组件 `u-mask` `u-popup` `u-select` `u-modal` `u-keyboard` `u-calendar` `u-action-sheet` `u-picker` 均新增 `blur` 属性,可用于设置弹出遮罩的模糊度,默认为0(不模糊)
* ![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/49b773a3-273f-4b1c-95e8-a42dcba1a53c.png)
## 1.3.4(2022-05-03)
* 【修复】`u-tabs` 组件细节问题。
## 1.1.4(2022-03-22)
* 【修复】`u-field` 组件 `arrowDirection` 属性无效的问题。
## 1.1.3(2022-03-21)
* 【优化】部分细节。
## 1.1.2(2022-03-21)
* 【优化】部分细节。
## 1.1.1(2022-03-17)
* 【优化】部分细节。
## 1.1.0(2022-03-12)
* 【重要】`u-picker` 组件新增 `regionDiscern` 方法 智能识别省市区街道地址
如将字符串 `浙江省杭州市西湖区希望路1333弄是啊我庭12号楼1203` 中识别为
```json
{
"province": {
"code": "330000",
"name": "浙江省"
},
"city": {
"code": "330100",
"name": "杭州市"
},
"area": {
"code": "330106",
"name": "西湖区"
},
"address": "龙井路1号",
"formatted_address": "浙江省杭州市西湖区龙井路1号"
}
```
而组件的 `addressDiscern` 方法还可以识别收货信息,如 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
## 1.0.13(2022-03-12)
* 【优化】部分细节。
## 1.0.12(2022-03-09)
* 【修复】`u-radio-group` 在 vue3 模式下,设置默认值可能会无效的问题。
## 1.0.11(2022-03-07)
* 【优化】部分细节。
## 1.0.10(2022-03-05)
* 【修复】`u-radio` 中的值相等的判断 == 改为 ===
* 【优化】部分注释的错别字。
## 1.0.9(2022-03-03)
* 【修复】`u-parse` 在 vue3模式下编译到app无法正常显示的问题。
## 1.0.8(2022-02-26)
* 【优化】`u-form` 组件新增2个属性 `inputAlign` 和 `clearable` 用于统一设置表单内所有 `u-input` 组件的对应属性默认值
* 【优化】更新城市数据源信息
## 1.0.7(2022-02-25)
* 【重要】`u-picker` 组件新增 `addressDiscern` 方法 智能识别收货信息
如在 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
即使这样的字符串也能识别 `!!!!~~~$张三~~~上海市嘉定区希望路1333弄是啊我庭12号楼1203【【【【13888888888】`
## 1.0.6(2022-02-24)
* 【优化】`u-form-item` 组件的 `prop` 属性支持 a.b 形式
## 1.0.5(2022-01-11)
* 【修复】`u-sticky` 组件 在微信小程序中无法正常吸顶的问题
## 1.0.4(2021-12-31)
* 【优化】`u-dropdown-item` 组件 0和"" 无法区分的问题。
* 【修复】`u-modal` 在Vue3版本中使用了mask-close-able属性无效的问题
## 1.0.3(2021-12-20)
【优化】u-icon在微信小程序下可能会显示null字符串的问题
## 1.0.2(2021-12-09)
* 1、【优化】`u-button` 组件新增 `timerId` 属性
* 之前的效果是:所有按钮一定时间内只能点击1次(`共用计算时间`)导致点击按钮A后无法马上点击按钮B
* 优化的效果是:每个按钮一定时间内只能点击1次(`分开计算时间`)且支持设置相同的 timerId 来达到指定按钮 `共用计算时间`
## 1.0.1(2021-11-22)
* 修复 u-parse 组件在微信小程序上的显示问题。
## 1.0.0(2021-11-18)
uView Vue3.0 横空出世,继承uView1.0意志,再战江湖,风云再起!by vk 2021-11-18
@@ -0,0 +1,246 @@
<template>
<u-popup
:blur="blur"
mode="bottom"
:border-radius="borderRadius"
:popup="false"
v-model="popupValue"
:maskCloseAble="maskCloseAble"
length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom"
@close="popupClose"
:z-index="uZIndex"
>
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
<text>{{ tips.text }}</text>
</view>
<block v-for="(item, index) in list" :key="index">
<view
@touchmove.stop.prevent
@tap="itemClick(index)"
:style="[itemStyle(index)]"
class="u-action-sheet-item u-line-1"
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
:hover-stay-time="150"
>
<text>{{ item[labelName] }}</text>
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">
{{ item.subText }}
</text>
</view>
</block>
<view class="u-gab" v-if="cancelBtn"></view>
<view
@touchmove.stop.prevent
class="u-actionsheet-cancel u-action-sheet-item"
hover-class="u-hover-class"
:hover-stay-time="150"
v-if="cancelBtn"
@tap="close"
>
{{ cancelText }}
</view>
</u-popup>
</template>
<script>
/**
* actionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
* @tutorial https://www.uviewui.com/components/actionSheet.html
* @property {Array<Object>} list 按钮的文字数组,见官方文档示例
* @property {Object} tips 顶部的提示文字,见官方文档示例
* @property {String} cancel-text 取消按钮的提示文字
* @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
* @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
* @property {Number String} z-index z-index值(默认1075)
* @property {String} cancel-text 取消按钮的提示文字
* @event {Function} click 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
emits: ["update:modelValue", "input", "click", "close"],
props: {
// 通过双向绑定控制组件的弹出与收起
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// 点击遮罩是否可以关闭actionsheet
maskCloseAble: {
type: Boolean,
default: true
},
// 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
list: {
type: Array,
default() {
// 如下
// return [{
// text: '确定',
// color: '',
// fontSize: ''
// }]
return [];
}
},
// 顶部的提示文字
tips: {
type: Object,
default() {
return {
text: "",
color: "",
fontSize: "26"
};
}
},
// 底部的取消按钮
cancelBtn: {
type: Boolean,
default: true
},
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 弹出的顶部圆角值
borderRadius: {
type: [String, Number],
default: 0
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 取消按钮的文字提示
cancelText: {
type: String,
default: "取消"
},
// 自定义label属性名
labelName: {
type: String,
default: "text"
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 顶部提示的样式
tipsStyle() {
let style = {};
if (this.tips.color) style.color = this.tips.color;
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + "rpx";
return style;
},
// 操作项目的样式
itemStyle() {
return index => {
let style = {};
if (this.list[index].color) style.color = this.list[index].color;
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + "rpx";
// 选项被禁用的样式
if (this.list[index].disabled) style.color = "#c0c4cc";
return style;
};
},
uZIndex() {
// 如果用户有传递z-index值,优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
data() {
return {
popupValue: false
};
},
watch: {
valueCom(v1, v2) {
this.popupValue = v1;
}
},
methods: {
// 点击取消按钮
close() {
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
// 这是一个vue发送事件的特殊用法
this.popupClose();
this.$emit("close");
},
// 弹窗关闭
popupClose() {
this.$emit("input", false);
this.$emit("update:modelValue", false);
},
// 点击某一个item
itemClick(index) {
// disabled的项禁止点击
if (this.list[index].disabled) return;
this.$emit("click", index);
this.$emit("input", false);
this.$emit("update:modelValue", false);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-tips {
font-size: 26rpx;
text-align: center;
padding: 34rpx 0;
line-height: 1.5;
color: $u-tips-color;
}
.u-action-sheet-item {
@include vue-flex;
line-height: 1;
justify-content: center;
align-items: center;
font-size: 32rpx;
padding: 34rpx 0;
flex-direction: column;
}
.u-action-sheet-item__subtext {
font-size: 24rpx;
color: $u-tips-color;
margin-top: 20rpx;
}
.u-gab {
height: 12rpx;
background-color: rgb(234, 234, 236);
}
.u-actionsheet-cancel {
color: $u-main-color;
}
</style>
@@ -0,0 +1,260 @@
<template>
<view class="u-alert-tips" v-if="show" :class="[
!show ? 'u-close-alert-tips': '',
type ? 'u-alert-tips--bg--' + type + '-light' : '',
type ? 'u-alert-tips--border--' + type + '-disabled' : '',
]" :style="{
backgroundColor: bgColor,
borderColor: borderColor
}">
<view class="u-icon-wrap">
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
</view>
<view class="u-alert-content" @click.stop="click">
<view class="u-alert-title" :style="[uTitleStyle]">
{{title}}
</view>
<view v-if="description" class="u-alert-desc" :style="[descStyle]">
{{description}}
</view>
</view>
<view class="u-icon-wrap">
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
:size="22" class="u-close-icon" :style="{
top: description ? '18rpx' : '24rpx'
}"></u-icon>
</view>
<text @click="close" v-if="closeAble && closeText" class="u-close-text" :style="{
top: description ? '18rpx' : '24rpx'
}">{{closeText}}</text>
</view>
</template>
<script>
/**
* alertTips 警告提示
* @description 警告提示,展现需要关注的信息
* @tutorial https://uviewui.com/components/alertTips.html
* @property {String} title 显示的标题文字
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
* @property {String} type 关闭按钮(默认为叉号icon图标)
* @property {String} icon 图标名称
* @property {Object} icon-style 图标的样式,对象形式
* @property {Object} title-style 标题的样式,对象形式
* @property {Object} desc-style 描述的样式,对象形式
* @property {String} close-able 用文字替代关闭图标,close-able为true时有效
* @property {Boolean} show-icon 是否显示左边的辅助图标
* @property {Boolean} show 显示或隐藏组件
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
*/
export default {
name: 'u-alert-tips',
emits: ["click", "close"],
props: {
// 显示文字
title: {
type: String,
default: ''
},
// 主题,success/warning/info/error
type: {
type: String,
default: 'warning'
},
// 辅助性文字
description: {
type: String,
default: ''
},
// 是否可关闭
closeAble: {
type: Boolean,
default: false
},
// 关闭按钮自定义文本
closeText: {
type: String,
default: ''
},
// 是否显示图标
showIcon: {
type: Boolean,
default: false
},
// 文字颜色,如果定义了color值,icon会失效
color: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 左边显示的icon
icon: {
type: String,
default: ''
},
// icon的样式
iconStyle: {
type: Object,
default() {
return {}
}
},
// 标题的样式
titleStyle: {
type: Object,
default() {
return {}
}
},
// 描述文字的样式
descStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
}
},
computed: {
uTitleStyle() {
let style = {};
// 如果有描述文字的话,标题进行加粗
style.fontWeight = this.description ? 500 : 'normal';
// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
return this.$u.deepMerge(style, this.titleStyle);
},
uIcon() {
// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
return this.icon ? this.icon : this.$u.type2icon(this.type);
},
uIconType() {
// 如果有设置图标的样式,优先使用,没有的话,则用type的样式
return Object.keys(this.iconStyle).length ? '' : this.type;
}
},
methods: {
// 点击内容
click() {
this.$emit('click');
},
// 点击关闭按钮
close() {
this.$emit('close');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-alert-tips {
@include vue-flex;
align-items: center;
padding: 16rpx 30rpx;
border-radius: 8rpx;
position: relative;
transition: all 0.3s linear;
border: 1px solid #fff;
&--bg--primary-light {
background-color: $u-type-primary-light;
}
&--bg--info-light {
background-color: $u-type-info-light;
}
&--bg--success-light {
background-color: $u-type-success-light;
}
&--bg--warning-light {
background-color: $u-type-warning-light;
}
&--bg--error-light {
background-color: $u-type-error-light;
}
&--border--primary-disabled {
border-color: $u-type-primary-disabled;
}
&--border--success-disabled {
border-color: $u-type-success-disabled;
}
&--border--error-disabled {
border-color: $u-type-error-disabled;
}
&--border--warning-disabled {
border-color: $u-type-warning-disabled;
}
&--border--info-disabled {
border-color: $u-type-info-disabled;
}
}
.u-close-alert-tips {
opacity: 0;
visibility: hidden;
}
.u-icon {
margin-right: 16rpx;
}
.u-alert-title {
font-size: 28rpx;
color: $u-main-color;
}
.u-alert-desc {
font-size: 26rpx;
text-align: left;
color: $u-content-color;
}
.u-close-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
}
.u-close-hover {
color: red;
}
.u-close-text {
font-size: 24rpx;
color: $u-tips-color;
position: absolute;
top: 20rpx;
right: 20rpx;
line-height: 1;
}
.u-alert-content {
width: 100%;
}
</style>
@@ -0,0 +1,290 @@
<template>
<view class="content">
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
<canvas
class="cropper"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
canvas-id="cropper"
id="cropper"
></canvas>
<canvas
class="cropper"
:disable-scroll="true"
:style="{
position: 'fixed',
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
height: `${cropperOpt.height * cropperOpt.pixelRatio}`
}"
canvas-id="targetId"
id="targetId"
></canvas>
</view>
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
<!-- #ifdef H5 -->
<view class="upload" @tap="uploadTap">选择图片</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="upload" @tap="uploadTap">重新选择</view>
<!-- #endif -->
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
</view>
</view>
</template>
<script>
import WeCropper from './weCropper.js';
export default {
props: {
// 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
// mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
boundStyle: {
type: Object,
default() {
return {
lineWidth: 4,
borderColor: 'rgb(245, 245, 245)',
mask: 'rgba(0, 0, 0, 0.35)'
};
}
}
// // 裁剪框宽度,单位rpx
// rectWidth: {
// type: [String, Number],
// default: 400
// },
// // 裁剪框高度,单位rpx
// rectHeight: {
// type: [String, Number],
// default: 400
// },
// // 输出图片宽度,单位rpx
// destWidth: {
// type: [String, Number],
// default: 400
// },
// // 输出图片高度,单位rpx
// destHeight: {
// type: [String, Number],
// default: 400
// },
// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
// fileType: {
// type: String,
// default: 'jpg',
// },
// // 生成的图片质量
// // H5上无效,目前不考虑使用此参数
// quality: {
// type: [Number, String],
// default: 1
// }
},
data() {
return {
// 底部导航的高度
bottomNavHeight: 50,
originWidth: 200,
width: 0,
height: 0,
cropperOpt: {
id: 'cropper',
targetId: 'targetCropper',
pixelRatio: 1,
width: 0,
height: 0,
scale: 2.5,
zoom: 8,
cut: {
x: (this.width - this.originWidth) / 2,
y: (this.height - this.originWidth) / 2,
width: this.originWidth,
height: this.originWidth
},
boundStyle: {
lineWidth: uni.upx2px(this.boundStyle.lineWidth),
mask: this.boundStyle.mask,
color: this.boundStyle.borderColor
}
},
// 裁剪框和输出图片的尺寸,高度默认等于宽度
// 输出图片宽度,单位px
destWidth: 200,
// 裁剪框宽度,单位px
rectWidth: 200,
// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
fileType: 'jpg',
src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
};
},
onLoad(option) {
let rectInfo = uni.getSystemInfoSync();
this.width = rectInfo.windowWidth;
this.height = rectInfo.windowHeight - this.bottomNavHeight;
this.cropperOpt.width = this.width;
this.cropperOpt.height = this.height;
this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
if (option.destWidth) this.destWidth = option.destWidth;
if (option.rectWidth) {
let rectWidth = Number(option.rectWidth);
this.cropperOpt.cut = {
x: (this.width - rectWidth) / 2,
y: (this.height - rectWidth) / 2,
width: rectWidth,
height: rectWidth
};
}
this.rectWidth = option.rectWidth;
if (option.fileType) this.fileType = option.fileType;
// 初始化
this.cropper = new WeCropper(this.cropperOpt)
.on('ready', ctx => {
// wecropper is ready for work!
})
.on('beforeImageLoad', ctx => {
// before picture loaded, i can do something
})
.on('imageLoad', ctx => {
// picture loaded
})
.on('beforeDraw', (ctx, instance) => {
// before canvas draw,i can do something
});
// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000'
});
uni.chooseImage({
count: 1, // 默认9
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: res => {
this.src = res.tempFilePaths[0];
// 获取裁剪图片资源后,给data添加src属性及其值
this.cropper.pushOrign(this.src);
}
});
},
methods: {
touchStart(e) {
this.cropper.touchStart(e);
},
touchMove(e) {
this.cropper.touchMove(e);
},
touchEnd(e) {
this.cropper.touchEnd(e);
},
getCropperImage(isPre = false) {
if(!this.src) return this.$u.toast('请先选择图片再裁剪');
let cropper_opt = {
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
destWidth: Number(this.destWidth),
fileType: this.fileType
};
this.cropper.getCropperImage(cropper_opt, (path, err) => {
if (err) {
uni.showModal({
title: '温馨提示',
content: err.message
});
} else {
if (isPre) {
uni.previewImage({
current: '', // 当前显示图片的 http 链接
urls: [path] // 需要预览的图片 http 链接列表
});
} else {
uni.$emit('uAvatarCropper', path);
this.$u.route({
type: 'back'
});
}
}
});
},
uploadTap() {
const self = this;
uni.chooseImage({
count: 1, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: (res) => {
self.src = res.tempFilePaths[0];
// 获取裁剪图片资源后,给data添加src属性及其值
self.cropper.pushOrign(this.src);
}
});
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.content {
background: rgba(255, 255, 255, 1);
}
.cropper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 11;
}
.cropper-buttons {
background-color: #000000;
color: #eee;
}
.cropper-wrapper {
position: relative;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: #000;
}
.cropper-buttons {
width: 100vw;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
font-size: 28rpx;
}
.cropper-buttons .upload,
.cropper-buttons .getCropperImage {
width: 50%;
text-align: center;
}
.cropper-buttons .upload {
text-align: left;
padding-left: 50rpx;
}
.cropper-buttons .getCropperImage {
text-align: right;
padding-right: 50rpx;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,270 @@
<template>
<view class="u-avatar" :style="[wrapStyle]" @tap="click">
<image
@error="loadError"
:style="[imgStyle]"
class="u-avatar__img"
v-if="!uText && avatar"
:src="avatar"
:mode="imgMode"
></image>
<text
class="u-line-1"
v-else-if="uText"
:style="{
fontSize: '38rpx'
}"
>
{{ uText }}
</text>
<slot v-else></slot>
<view
class="u-avatar__sex"
v-if="showSex"
:class="['u-avatar__sex--' + sexIcon]"
:style="[uSexStyle]"
>
<u-icon :name="sexIcon" size="20"></u-icon>
</view>
<view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
<u-icon :name="levelIcon" size="20"></u-icon>
</view>
</view>
</template>
<script>
let base64Avatar =
"data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
/**
* avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uviewui.com/components/avatar.html
* @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff)
* @property {String} src 头像路径,如加载失败,将会显示默认头像
* @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值,单位rpx(默认default)
* @value large 大
* @value default 常规
* @value mini 小
* @property {String} mode 显示类型,见上方说明(默认circle)
* @value circle 圆型
* @value square 方型
* @property {String} sex-icon 性别图标,man-男,woman-女(默认man)
* @value man 男
* @value woman 女
* @property {String} level-icon 等级图标(默认level)
* @property {String} sex-bg-color 性别图标背景颜色
* @property {String} level-bg-color 等级图标背景颜色
* @property {String} show-sex 是否显示性别图标(默认false)
* @property {String} show-level 是否显示等级图标(默认false)
* @property {String} img-mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值(默认aspectFill)
* @property {String} index 用户传递的标识符值,如果是列表循环,可穿v-for的index值
* @event {Function} click 头像被点击
* @example <u-avatar :src="src"></u-avatar>
*/
export default {
name: "u-avatar",
emits: ["click"],
props: {
// 背景颜色
bgColor: {
type: String,
default: "transparent"
},
// 头像路径
src: {
type: String,
default: ""
},
// 尺寸,large-大,default-中等,mini-小,如果为数值,则单位为rpx
// 宽度等于高度
size: {
type: [String, Number],
default: "default"
},
// 头像模型,square-带圆角方形,circle-圆形
mode: {
type: String,
default: "circle"
},
// 文字内容
text: {
type: String,
default: ""
},
// 图片的裁剪模型
imgMode: {
type: String,
default: "aspectFill"
},
// 标识符
index: {
type: [String, Number],
default: ""
},
// 右上角性别角标,man-男,woman-女
sexIcon: {
type: String,
default: "man"
},
// 右下角的等级图标
levelIcon: {
type: String,
default: "level"
},
// 右下角等级图标背景颜色
levelBgColor: {
type: String,
default: ""
},
// 右上角性别图标的背景颜色
sexBgColor: {
type: String,
default: ""
},
// 是否显示性别图标
showSex: {
type: Boolean,
default: false
},
// 是否显示等级图标
showLevel: {
type: Boolean,
default: false
}
},
data() {
return {
error: false,
// 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值
avatar: this.src ? this.src : base64Avatar
};
},
watch: {
src(n) {
// 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
if (!n) {
// 如果传入null或者'',或者undefined,显示默认头像
this.avatar = base64Avatar;
this.error = true;
} else {
this.avatar = n;
this.error = false;
}
}
},
computed: {
wrapStyle() {
let style = {};
style.height =
this.size == "large"
? "120rpx"
: this.size == "default"
? "90rpx"
: this.size == "mini"
? "70rpx"
: this.size + "rpx";
style.width = style.height;
style.flex = `0 0 ${style.height}`;
style.backgroundColor = this.bgColor;
style.borderRadius = this.mode == "circle" ? "500px" : "5px";
if (this.text) style.padding = `0 6rpx`;
return style;
},
imgStyle() {
let style = {};
style.borderRadius = this.mode == "circle" ? "500px" : "5px";
return style;
},
// 取字符串的第一个字符
uText() {
return String(this.text)[0];
},
// 性别图标的自定义样式
uSexStyle() {
let style = {};
if (this.sexBgColor) style.backgroundColor = this.sexBgColor;
return style;
},
// 等级图标的自定义样式
uLevelStyle() {
let style = {};
if (this.levelBgColor) style.backgroundColor = this.levelBgColor;
return style;
}
},
methods: {
// 图片加载错误时,显示默认头像
loadError() {
this.error = true;
this.avatar = base64Avatar;
},
click() {
this.$emit("click", this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-avatar {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
border-radius: 10px;
position: relative;
&__img {
width: 100%;
height: 100%;
}
&__sex {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
top: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
&--man {
background-color: $u-type-primary;
}
&--woman {
background-color: $u-type-error;
}
&--none {
background-color: $u-type-warning;
}
}
&__level {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
bottom: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
background-color: $u-type-warning;
}
}
</style>
@@ -0,0 +1,153 @@
<template>
<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
bottom: bottom + 'rpx',
right: right + 'rpx',
borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
zIndex: uZIndex,
opacity: opacity
}, customStyle]">
<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
<view class="u-back-top__content__tips">
{{tips}}
</view>
</view>
<slot v-else />
</view>
</template>
<script>
export default {
name: 'u-back-top',
props: {
// 返回顶部的形状,circle-圆形,square-方形
mode: {
type: String,
default: 'circle'
},
// 自定义图标
icon: {
type: String,
default: 'arrow-upward'
},
// 提示文字
tips: {
type: String,
default: ''
},
// 返回顶部滚动时间
duration: {
type: [Number, String],
default: 100
},
// 滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
// 距离顶部多少距离显示,单位rpx
top: {
type: [Number, String],
default: 400
},
// 返回顶部按钮到底部的距离,单位rpx
bottom: {
type: [Number, String],
default: 200
},
// 返回顶部按钮到右边的距离,单位rpx
right: {
type: [Number, String],
default: 40
},
// 层级
zIndex: {
type: [Number, String],
default: '9'
},
// 图标的样式,对象形式
iconStyle: {
type: Object,
default() {
return {
color: '#909399',
fontSize: '38rpx'
}
}
},
// 整个组件的样式
customStyle: {
type: Object,
default() {
return {}
}
}
},
watch: {
showBackTop(nVal, oVal) {
// 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
// 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
if(nVal) {
this.uZIndex = this.zIndex;
this.opacity = 1;
} else {
this.uZIndex = -1;
this.opacity = 0;
}
}
},
computed: {
showBackTop() {
// 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
// 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
return this.scrollTop > uni.upx2px(this.top);
},
},
data() {
return {
// 不透明度,为了让组件有一个显示和隐藏的过渡动画
opacity: 0,
// 组件的z-index值,隐藏时设置为-1,就会看不到
uZIndex: -1
}
},
methods: {
backToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-back-top {
width: 80rpx;
height: 80rpx;
position: fixed;
z-index: 9;
@include vue-flex;
flex-direction: column;
justify-content: center;
background-color: #E1E1E1;
color: $u-content-color;
align-items: center;
transition: opacity 0.4s;
&__content {
@include vue-flex;
flex-direction: column;
align-items: center;
&__tips {
font-size: 24rpx;
transform: scale(0.8);
line-height: 1;
}
}
}
</style>
@@ -0,0 +1,216 @@
<template>
<view v-if="show" class="u-badge" :class="[
isDot ? 'u-badge-dot' : '',
size == 'mini' ? 'u-badge-mini' : '',
type ? 'u-badge--bg--' + type : ''
]" :style="[{
top: offset[0] + 'rpx',
right: offset[1] + 'rpx',
fontSize: fontSize + 'rpx',
position: absolute ? 'absolute' : 'static',
color: color,
backgroundColor: bgColor
}, boxStyle]"
>
{{showText}}
</view>
</template>
<script>
/**
* badge 角标
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uviewui.com/components/badge.html
* @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
* @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
* @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
* @property {String Number} overflow-count 展示封顶的数字值(默认99)
* @property {String} type 使用预设的背景颜色(默认error)
* @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
* @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
* @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
* @property {String} color 字体颜色(默认#ffffff)
* @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
* @example <u-badge type="error" count="7"></u-badge>
*/
export default {
name: 'u-badge',
props: {
// primary,warning,success,error,info
type: {
type: String,
default: 'error'
},
// default, mini
size: {
type: String,
default: 'default'
},
//是否是圆点
isDot: {
type: Boolean,
default: false
},
// 显示的数值内容
count: {
type: [Number, String],
},
// 展示封顶的数字值
overflowCount: {
type: Number,
default: 99
},
// 当数值为 0 时,是否展示 Badge
showZero: {
type: Boolean,
default: false
},
// 位置偏移
offset: {
type: Array,
default: () => {
return [20, 20]
}
},
// 是否开启绝对定位,开启了offset才会起作用
absolute: {
type: Boolean,
default: true
},
// 字体大小
fontSize: {
type: [String, Number],
default: '24'
},
// 字体演示
color: {
type: String,
default: '#ffffff'
},
// badge的背景颜色
bgColor: {
type: String,
default: ''
},
// 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
isCenter: {
type: Boolean,
default: false
}
},
computed: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
let style = {};
if(this.isCenter) {
style.top = 0;
style.right = 0;
// Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
style.transform = "translateY(-50%) translateX(50%)";
} else {
style.top = this.offset[0] + 'rpx';
style.right = this.offset[1] + 'rpx';
style.transform = "translateY(0) translateX(0)";
}
// 如果尺寸为mini,后接上scale()
if(this.size == 'mini') {
style.transform = style.transform + " scale(0.8)";
}
return style;
},
// isDot类型时,不显示文字
showText() {
if(this.isDot) return '';
else {
if(this.count > this.overflowCount) return `${this.overflowCount}+`;
else return this.count;
}
},
// 是否显示组件
show() {
// 如果count的值为0,并且showZero设置为false,不显示组件
if(this.count == 0 && this.showZero == false) return false;
else return true;
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-badge {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
justify-content: center;
align-items: center;
line-height: 24rpx;
padding: 4rpx 8rpx;
border-radius: 100rpx;
z-index: 9;
&--bg--primary {
background-color: $u-type-primary;
}
&--bg--error {
background-color: $u-type-error;
}
&--bg--success {
background-color: $u-type-success;
}
&--bg--info {
background-color: $u-type-info;
}
&--bg--warning {
background-color: $u-type-warning;
}
}
.u-badge-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
line-height: 1;
}
.u-badge-mini {
transform: scale(0.8);
transform-origin: center center;
}
// .u-primary {
// background: $u-type-primary;
// color: #fff;
// }
// .u-error {
// background: $u-type-error;
// color: #fff;
// }
// .u-warning {
// background: $u-type-warning;
// color: #fff;
// }
// .u-success {
// background: $u-type-success;
// color: #fff;
// }
// .u-black {
// background: #585858;
// color: #fff;
// }
.u-info {
background-color: $u-type-info;
color: #fff;
}
</style>
@@ -0,0 +1,661 @@
<template>
<button
id="u-wave-btn"
class="u-btn u-line-1 u-fix-ios-appearance"
:class="[
'u-size-' + size,
plain ? 'u-btn--' + type + '--plain' : '',
loading ? 'u-loading' : '',
shape == 'circle' ? 'u-round-circle' : '',
hairLine ? showHairLineBorder : 'u-btn--bold-border',
'u-btn--' + type,
disabled ? `u-btn--${type}--disabled` : '',
]"
:hover-start-time="Number(hoverStartTime)"
:hover-stay-time="Number(hoverStayTime)"
:disabled="disabled"
:form-type="formType"
:open-type="openType"
:app-parameter="appParameter"
:hover-stop-propagation="hoverStopPropagation"
:send-message-title="sendMessageTitle"
send-message-path="sendMessagePath"
:lang="lang"
:data-name="dataName"
:session-from="sessionFrom"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
@getphonenumber="getphonenumber"
@getuserinfo="getuserinfo"
@error="error"
@opensetting="opensetting"
@launchapp="launchapp"
@chooseavatar="chooseavatar"
:style="[customStyle, {
overflow: ripple ? 'hidden' : 'visible'
}]"
@tap.stop="click($event)"
:hover-class="getHoverClass"
:loading="loading"
>
<slot></slot>
<view
v-if="ripple"
class="u-wave-ripple"
:class="[waveActive ? 'u-wave-active' : '']"
:style="{
top: rippleTop + 'px',
left: rippleLeft + 'px',
width: fields.targetWidth + 'px',
height: fields.targetWidth + 'px',
'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
}"
></view>
</button>
</template>
<script>
/**
* button 按钮
* @description Button 按钮
* @tutorial https://www.uviewui.com/components/button.html
* @property {String} size 按钮的大小
* @value large 大
* @value normal 常规
* @value mini 小
* @property {Boolean} ripple 是否开启点击水波纹效果
* @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
* @property {String} type 按钮的样式类型
* @value info 默认按钮
* @value primary 主要按钮
* @value error 危险按钮
* @value warning 警告按钮
* @value success 成功按钮
* @property {Boolean} plain 按钮是否镂空,背景色透明
* @property {Boolean} disabled 是否禁用
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
* @property {Boolean} shape 按钮外观形状,见文档说明
* @value square 矩形
* @value circle 圆角
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
* @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
* @property {String} openType 开放能力
* @value feedback 通用 - 打开“意见反馈”页面,用户可提交反馈内容并上传日志(App、微信小程序、QQ小程序)
* @value share 通用 - 触发用户转发(微信小程序、百度小程序、支付宝小程序、字节跳动小程序、飞书小程序、QQ小程序、快手小程序、京东小程序、360小程序)
* @value getUserInfo 通用 - 获取用户信息(微信小程序、百度小程序、QQ小程序、快手小程序、京东小程序、360小程序)
* @value contact 通用 - 打开客服会话,如果用户在会话中点击消息卡片后返回应用,可以从 回调中获得具体信息(微信小程序、百度小程序、快手小程序、字节小程序)
* @value getPhoneNumber 通用 - 获取用户手机号(微信小程序、百度小程序、字节跳动小程序、支付宝小程序、快手小程序、京东小程序。App平台另见一键登陆)
* @value launchApp 通用 - 小程序中打开APP,可以通过app-parameter属性设定向APP传的参数 (微信小程序、QQ小程序、快手小程序、京东小程序)
* @value openSetting 通用 - 打开授权设置页(微信小程序、QQ小程序、百度小程序、快手小程序、京东小程序、360小程序)
* @value chooseAvatar 微信小程序 - 获取用户头像
* @value uploadDouyinVideo 抖音小程序 - 发布抖音视频
* @value im 抖音小程序 - 跳转到抖音IM客服
* @value getAuthorize 支付宝小程序 - 授权
* @value lifestyle 支付宝小程序 - 关注生活号
* @value contactShare 支付宝小程序 - 分享到通讯录好友
* @value openGroupProfile QQ小程序 - 呼起QQ群资料卡页面,可以通过group-id属性设定需要打开的群资料卡的群号,同时manifest.json中必须配置groupIdList
* @value openGuildProfile QQ小程序 - 呼起频道页面,可以通过guild-id属性设定需要打开的频道ID
* @value openPublicProfile QQ小程序 - 打开公众号资料卡,可以通过public-id属性设定需要打开的公众号资料卡的号码,同时manifest.json中必须配置publicIdList
* @value shareMessageToFriend QQ小程序 - 在自定义开放数据域组件中,向指定好友发起分享据
* @value addFriend QQ小程序 - 添加好友, 对方需要通过该小程序进行授权,允许被加好友后才能调用成功用户授权
* @value addColorSign QQ小程序 - 添加彩签,点击后添加状态有用户提示,无回调
* @value addGroupApp QQ小程序 - 添加群应用(只有管理员或群主有权操作),添加后给button绑定@addgroupapp事件接收回调数据
* @value addToFavorites QQ小程序 - 收藏当前页面,点击按钮后会触发Page.onAddToFavorites方法
* @value chooseAddress 百度小程序 - 选择用户收货地址
* @value chooseInvoiceTitle 百度小程序 - 选择用户发票抬头
* @value login 百度小程序 - 登录,可以从@login回调中确认是否登录成功
* @value subscribe 百度小程序 - 订阅类模板消息,需要用户授权才可发送
* @value favorite 快手小程序 - 触发用户收藏
* @value watchLater 快手小程序 - 触发用户稍后再看
* @value openProfile 快手小程序 - 触发打开用户主页
* @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
* @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
* @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
* @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
* @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
* @event {Function} click 按钮点击
* @event {Function} getphonenumber open-type="getPhoneNumber"时有效
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
* @event {Function} error 当使用开放能力时,发生错误的回调
* @event {Function} opensetting 在打开授权设置页并关闭后回调
* @event {Function} launchapp 打开 APP 成功的回调
* @event {Function} chooseavatar 获取用户头像,可以从@chooseavatar回调中获取到头像信息,open-type="chooseAvatar"时有效
* @example <u-button>月落</u-button>
*/
export default {
name: 'u-button',
emits: ["click", "getphonenumber", "getuserinfo", "error", "opensetting", "launchapp", "chooseavatar"],
props: {
// 是否细边框
hairLine: {
type: Boolean,
default: true
},
// 按钮的预置样式,default,primary,error,warning,success
type: {
type: String,
default: 'default'
},
// 按钮尺寸,default,medium,mini
size: {
type: String,
default: 'default'
},
// 按钮形状,circle(两边为半圆),square(带圆角)
shape: {
type: String,
default: 'square'
},
// 按钮是否镂空
plain: {
type: Boolean,
default: false
},
// 是否禁止状态
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 开放能力,具体请看uniapp稳定关于button组件部分说明
// https://uniapp.dcloud.io/component/button
openType: {
type: String,
default: ''
},
// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
// 取值为submit(提交表单),reset(重置表单)
formType: {
type: String,
default: ''
},
// 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
// 只微信小程序、QQ小程序有效
appParameter: {
type: String,
default: ''
},
// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
hoverStopPropagation: {
type: Boolean,
default: false
},
// 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
lang: {
type: String,
default: 'en'
},
// 会话来源,open-type="contact"时有效。只微信小程序有效
sessionFrom: {
type: String,
default: ''
},
// 会话内消息卡片标题,open-type="contact"时有效
// 默认当前标题,只微信小程序有效
sendMessageTitle: {
type: String,
default: ''
},
// 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
// 默认当前分享路径,只微信小程序有效
sendMessagePath: {
type: String,
default: ''
},
// 会话内消息卡片图片,open-type="contact"时有效
// 默认当前页面截图,只微信小程序有效
sendMessageImg: {
type: String,
default: ''
},
// 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
// 用户点击后可以快速发送小程序消息,open-type="contact"时有效
showMessageCard: {
type: Boolean,
default: false
},
// 手指按(触摸)按钮时按钮时的背景颜色
hoverBgColor: {
type: String,
default: ''
},
// 水波纹的背景颜色
rippleBgColor: {
type: String,
default: ''
},
// 是否开启水波纹效果
ripple: {
type: Boolean,
default: false
},
// 按下的类名
hoverClass: {
type: String,
default: ''
},
// 自定义样式,对象形式
customStyle: {
type: Object,
default() {
return {};
}
},
// 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
dataName: {
type: String,
default: ''
},
// 节流,一定时间内只能触发一次
throttleTime: {
type: [String, Number],
default: 500
},
// 按住后多久出现点击态,单位毫秒
hoverStartTime: {
type: [String, Number],
default: 20
},
// 手指松开后点击态保留时间,单位毫秒
hoverStayTime: {
type: [String, Number],
default: 150
},
timerId: {
type: [String, Number]
},
},
computed: {
// 当没有传bgColor变量时,按钮按下去的颜色类名
getHoverClass() {
// 如果开启水波纹效果,则不启用hover-class效果
if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
let hoverClass = '';
hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
return hoverClass;
},
// 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
showHairLineBorder() {
if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
return '';
} else {
return 'u-hairline-border';
}
}
},
data() {
let btnTimerId = this.timerId || "button_" + Math.floor(Math.random() * 100000000 + 0);
return {
btnTimerId,
rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
fields: {}, // 波纹按钮节点信息
waveActive: false // 激活水波纹
};
},
methods: {
// 按钮点击
click(e) {
// 进行节流控制,每this.throttle毫秒内,只在开始处执行
this.$u.throttle(() => {
// 如果按钮时disabled和loading状态,不触发水波纹效果
if (this.loading === true || this.disabled === true) return;
// 是否开启水波纹效果
if (this.ripple) {
// 每次点击时,移除上一次的类,再次添加,才能触发动画效果
this.waveActive = false;
this.$nextTick(function() {
this.getWaveQuery(e);
});
}
this.$emit('click', e);
}, this.throttleTime, true, this.btnTimerId);
},
// 查询按钮的节点信息
getWaveQuery(e) {
this.getElQuery().then(res => {
// 查询返回的是一个数组节点
let data = res[0];
// 查询不到节点信息,不操作
if (!data.width || !data.width) return;
// 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
// 最终的方形(变换后的圆形)才能覆盖整个按钮
data.targetWidth = data.height > data.width ? data.height : data.width;
if (!data.targetWidth) return;
this.fields = data;
let touchesX = '',
touchesY = '';
// #ifdef MP-BAIDU
touchesX = e.changedTouches[0].clientX;
touchesY = e.changedTouches[0].clientY;
// #endif
// #ifdef MP-ALIPAY
touchesX = e.detail.clientX;
touchesY = e.detail.clientY;
// #endif
// #ifndef MP-BAIDU || MP-ALIPAY
touchesX = e.touches[0].clientX;
touchesY = e.touches[0].clientY;
// #endif
// 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
// 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
this.rippleTop = touchesY - data.top - data.targetWidth / 2;
this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
this.$nextTick(() => {
this.waveActive = true;
});
});
},
// 获取节点信息
getElQuery() {
return new Promise(resolve => {
let queryInfo = '';
// 获取元素节点信息,请查看uniapp相关文档
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
queryInfo = uni.createSelectorQuery().in(this);
//#ifdef MP-ALIPAY
queryInfo = uni.createSelectorQuery();
//#endif
queryInfo.select('.u-btn').boundingClientRect();
queryInfo.exec(data => {
resolve(data);
});
});
},
// 下面为对接uniapp官方按钮开放能力事件回调的对接
getphonenumber(res) {
this.$emit('getphonenumber', res);
},
getuserinfo(res) {
this.$emit('getuserinfo', res);
},
error(res) {
this.$emit('error', res);
},
opensetting(res) {
this.$emit('opensetting', res);
},
launchapp(res) {
this.$emit('launchapp', res);
},
chooseavatar(res) {
this.$emit('chooseavatar', res);
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-btn::after {
border: none;
}
.u-btn {
position: relative;
border: 0;
//border-radius: 10rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
// 避免边框某些场景可能被“裁剪”,不能设置为hidden
overflow: visible;
line-height: 1;
@include vue-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0 40rpx;
z-index: 1;
box-sizing: border-box;
transition: all 0.15s;
&--bold-border {
border: 1px solid #ffffff;
}
&--default {
color: $u-content-color;
border-color: #c0c4cc;
background-color: #ffffff;
}
&--primary {
color: #ffffff;
border-color: $u-type-primary;
background-color: $u-type-primary;
}
&--success {
color: #ffffff;
border-color: $u-type-success;
background-color: $u-type-success;
}
&--error {
color: #ffffff;
border-color: $u-type-error;
background-color: $u-type-error;
}
&--warning {
color: #ffffff;
border-color: $u-type-warning;
background-color: $u-type-warning;
}
&--default--disabled {
color: #ffffff;
border-color: #e4e7ed;
background-color: #ffffff;
}
&--primary--disabled {
color: #ffffff!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-disabled!important;
}
&--success--disabled {
color: #ffffff!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-disabled!important;
}
&--error--disabled {
color: #ffffff!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-disabled!important;
}
&--warning--disabled {
color: #ffffff!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-disabled!important;
}
&--primary--plain {
color: $u-type-primary!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-light!important;
}
&--success--plain {
color: $u-type-success!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-light!important;
}
&--error--plain {
color: $u-type-error!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-light!important;
}
&--warning--plain {
color: $u-type-warning!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-light!important;
}
&--info {
color: #ffffff;
border-color: $u-type-info;
background-color: $u-type-info;
}
&--info--disabled {
color: #ffffff!important;
border-color: $u-type-info-disabled!important;
background-color: $u-type-info-disabled!important;
}
&--info--plain {
color: $u-type-info!important;
border-color: $u-type-info-disabled!important;
background-color: $u-type-info-light!important;
}
}
.u-hairline-border:after {
content: ' ';
position: absolute;
pointer-events: none;
// 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
box-sizing: border-box;
// 中心点作为变形(scale())的原点
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
left: 0;
top: 0;
width: 199.8%;
height: 199.7%;
-webkit-transform: scale(0.5, 0.5);
transform: scale(0.5, 0.5);
border: 1px solid currentColor;
z-index: 1;
}
.u-wave-ripple {
z-index: 0;
position: absolute;
border-radius: 100%;
background-clip: padding-box;
pointer-events: none;
user-select: none;
transform: scale(0);
opacity: 1;
transform-origin: center;
}
.u-wave-ripple.u-wave-active {
opacity: 0;
transform: scale(2);
transition: opacity 1s linear, transform 0.4s linear;
}
.u-round-circle {
border-radius: 100rpx;
}
.u-round-circle::after {
border-radius: 100rpx;
}
.u-loading::after {
background-color: hsla(0, 0%, 100%, 0.35);
}
.u-size-default {
font-size: 30rpx;
height: 80rpx;
line-height: 80rpx;
}
.u-size-medium {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 26rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0 80rpx;
}
.u-size-mini {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 22rpx;
padding-top: 1px;
height: 50rpx;
line-height: 50rpx;
padding: 0 20rpx;
}
.u-primary-plain-hover {
color: #ffffff !important;
background: $u-type-primary-dark !important;
}
.u-default-plain-hover {
color: $u-type-primary-dark !important;
background: $u-type-primary-light !important;
}
.u-success-plain-hover {
color: #ffffff !important;
background: $u-type-success-dark !important;
}
.u-warning-plain-hover {
color: #ffffff !important;
background: $u-type-warning-dark !important;
}
.u-error-plain-hover {
color: #ffffff !important;
background: $u-type-error-dark !important;
}
.u-info-plain-hover {
color: #ffffff !important;
background: $u-type-info-dark !important;
}
.u-default-hover {
color: $u-type-primary-dark !important;
border-color: $u-type-primary-dark !important;
background-color: $u-type-primary-light !important;
}
.u-primary-hover {
background: $u-type-primary-dark !important;
color: #fff;
}
.u-success-hover {
background: $u-type-success-dark !important;
color: #fff;
}
.u-info-hover {
background: $u-type-info-dark !important;
color: #fff;
}
.u-warning-hover {
background: $u-type-warning-dark !important;
color: #fff;
}
.u-error-hover {
background: $u-type-error-dark !important;
color: #fff;
}
</style>
@@ -0,0 +1,666 @@
<template>
<u-popup :blur="blur" closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="popupValue" length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
<view class="u-calendar">
<view class="u-calendar__header">
<view class="u-calendar__header__text" v-if="!$slots['tooltip']">
{{toolTip}}
</view>
<slot v-else name="tooltip" />
</view>
<view class="u-calendar__action u-flex u-row-center">
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__text">{{ showTitle }}</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
</view>
</view>
<view class="u-calendar__week-day">
<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
</view>
<view class="u-calendar__content">
<!-- 前置空白部分 -->
<block v-for="(item, index) in weekdayArr" :key="index">
<view class="u-calendar__content__item"></view>
</block>
<view class="u-calendar__content__item" :class="{
'u-hover-class':openDisAbled(year,month,index+1),
'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
@tap="dateClick(index)">
<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
<view>{{ index + 1 }}</view>
</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
</view>
<view class="u-calendar__content__bg-month">{{month}}</view>
</view>
<view class="u-calendar__bottom">
<view class="u-calendar__bottom__choose">
<text>{{mode == 'date' ? activeDate : startDate}}</text>
<text v-if="endDate">至{{endDate}}</text>
</view>
<view class="u-calendar__bottom__btn">
<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script>
/**
* calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
* @tutorial http://uviewui.com/components/calendar.html
* @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
* @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
* @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
* @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
* @property {String Number} max-year 可切换的最大年份(默认2050)
* @property {String Number} min-year 可切换的最小年份(默认1950)
* @property {String Number} min-date 最小可选日期(默认1950-01-01)
* @property {String Number} max-date 最大可选日期(默认当前日期)
* @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
* @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
* @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
* @property {String} color 日期字体的默认颜色(默认#303133)
* @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
* @property {String Number} z-index 弹出时的z-index值(默认10075)
* @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
* @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
* @property {String} range-color 选择范围内字体颜色(默认#2979ff)
* @property {String} start-text 起始日期底部的提示文字(默认 '开始')
* @property {String} end-text 结束日期底部的提示文字(默认 '结束')
* @property {String} btn-type 底部确定按钮的主题(默认 'primary')
* @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
* @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
* @example <u-calendar v-model="show" :mode="mode"></u-calendar>
*/
export default {
name: 'u-calendar',
emits: ["update:modelValue", "input", "change"],
props: {
// 通过双向绑定控制组件的弹出与收起
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否允许通过点击遮罩关闭Picker
maskCloseAble: {
type: Boolean,
default: true
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 是否允许切换年份
changeYear: {
type: Boolean,
default: true
},
// 是否允许切换月份
changeMonth: {
type: Boolean,
default: true
},
// date-单个日期选择,range-开始日期+结束日期选择
mode: {
type: String,
default: 'date'
},
// 可切换的最大年份
maxYear: {
type: [Number, String],
default: 2050
},
// 可切换的最小年份
minYear: {
type: [Number, String],
default: 1950
},
// 最小可选日期(不在范围内日期禁用不可选)
minDate: {
type: [Number, String],
default: '1950-01-01'
},
/**
* 最大可选日期
* 默认最大值为今天,之后的日期不可选
* 2030-12-31
* */
maxDate: {
type: [Number, String],
default: ''
},
// 弹窗顶部左右两边的圆角值
borderRadius: {
type: [String, Number],
default: 20
},
// 月份切换按钮箭头颜色
monthArrowColor: {
type: String,
default: '#606266'
},
// 年份切换按钮箭头颜色
yearArrowColor: {
type: String,
default: '#909399'
},
// 默认日期字体颜色
color: {
type: String,
default: '#303133'
},
// 选中|起始结束日期背景色
activeBgColor: {
type: String,
default: '#2979ff'
},
// 选中|起始结束日期字体颜色
activeColor: {
type: String,
default: '#ffffff'
},
// 范围内日期背景色
rangeBgColor: {
type: String,
default: 'rgba(41,121,255,0.13)'
},
// 范围内日期字体颜色
rangeColor: {
type: String,
default: '#2979ff'
},
// mode=range时生效,起始日期自定义文案
startText: {
type: String,
default: '开始'
},
// mode=range时生效,结束日期自定义文案
endText: {
type: String,
default: '结束'
},
//按钮样式类型
btnType: {
type: String,
default: 'primary'
},
// 当前选中日期带选中效果
isActiveCurrent: {
type: Boolean,
default: true
},
// 切换年月是否触发事件 mode=date时生效
isChange: {
type: Boolean,
default: false
},
// 是否显示右上角的关闭图标
closeable: {
type: Boolean,
default: true
},
// 顶部的提示文字
toolTip: {
type: String,
default: '选择日期'
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
},
},
data() {
return {
popupValue:false,
// 星期几,值为1-7
weekday: 1,
weekdayArr:[],
// 当前月有多少天
days: 0,
daysArr:[],
showTitle: '',
year: 2020,
month: 0,
day: 0,
startYear: 0,
startMonth: 0,
startDay: 0,
endYear: 0,
endMonth: 0,
endDay: 0,
today: '',
activeDate: '',
startDate: '',
endDate: '',
isStart: true,
min: null,
max: null,
weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
dataChange() {
return `${this.mode}-${this.minDate}-${this.maxDate}`;
},
uZIndex() {
// 如果用户有传递z-index值,优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
watch: {
dataChange(val) {
this.init()
},
valueCom:{
immediate: true,
handler(val){
this.popupValue = val;
}
}
},
created() {
this.init()
},
methods: {
getColor(index, type) {
let color = type == 1 ? '' : this.color;
let day = index + 1
let date = `${this.year}-${this.month}-${day}`
let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
let start = this.startDate.replace(/\-/g, '/')
let end = this.endDate.replace(/\-/g, '/')
if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
color = type == 1 ? this.activeBgColor : this.activeColor;
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
color = type == 1 ? this.rangeBgColor : this.rangeColor;
}
return color;
},
init() {
let now = new Date();
this.year = now.getFullYear();
this.month = now.getMonth() + 1;
this.day = now.getDate();
this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
this.activeDate = this.today;
this.min = this.initDate(this.minDate);
this.max = this.initDate(this.maxDate || this.today);
this.startDate = "";
this.startYear = 0;
this.startMonth = 0;
this.startDay = 0;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.isStart = true;
this.changeData();
},
//日期处理
initDate(date) {
let fdate = date.split('-');
return {
year: Number(fdate[0] || 1920),
month: Number(fdate[1] || 1),
day: Number(fdate[2] || 1)
}
},
openDisAbled: function(year, month, day) {
let bool = true;
let date = `${year}/${month}/${day}`;
// let today = this.today.replace(/\-/g, '/');
let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
let timestamp = new Date(date).getTime();
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
bool = false;
}
return bool;
},
generateArray: function(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start);
},
formatNum: function(num) {
return num < 10 ? '0' + num : num + '';
},
//一个月有多少天
getMonthDay(year, month) {
let days = new Date(year, month, 0).getDate();
return days;
},
getWeekday(year, month) {
let date = new Date(`${year}/${month}/01 00:00:00`);
return date.getDay();
},
checkRange(year) {
let overstep = false;
if (year < this.minYear || year > this.maxYear) {
uni.showToast({
title: "日期超出范围啦~",
icon: 'none'
})
overstep = true;
}
return overstep;
},
changeMonthHandler(isAdd) {
if (isAdd) {
let month = this.month + 1;
let year = month > 12 ? this.year + 1 : this.year;
if (!this.checkRange(year)) {
this.month = month > 12 ? 1 : month;
this.year = year;
this.changeData();
}
} else {
let month = this.month - 1;
let year = month < 1 ? this.year - 1 : this.year;
if (!this.checkRange(year)) {
this.month = month < 1 ? 12 : month;
this.year = year;
this.changeData();
}
}
},
changeYearHandler(isAdd) {
let year = isAdd ? this.year + 1 : this.year - 1;
if (!this.checkRange(year)) {
this.year = year;
this.changeData();
}
},
changeData() {
this.days = this.getMonthDay(this.year, this.month);
this.daysArr=this.generateArray(1,this.days)
this.weekday = this.getWeekday(this.year, this.month);
this.weekdayArr=this.generateArray(1,this.weekday)
this.showTitle = `${this.year}年${this.month}月`;
if (this.isChange && this.mode == 'date') {
this.btnFix(true);
}
},
dateClick: function(day) {
day += 1;
if (!this.openDisAbled(this.year, this.month, day)) {
this.day = day;
let date = `${this.year}-${this.month}-${day}`;
if (this.mode == 'date') {
this.activeDate = date;
} else {
let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
if (this.isStart || compare) {
this.startDate = date;
this.startYear = this.year;
this.startMonth = this.month;
this.startDay = this.day;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.activeDate = "";
this.isStart = false;
} else {
this.endDate = date;
this.endYear = this.year;
this.endMonth = this.month;
this.endDay = this.day;
this.isStart = true;
}
}
}
},
close() {
// 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
this.$emit('input', false);
this.$emit("update:modelValue", false);
},
getWeekText(date) {
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
let week = date.getDay();
return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
},
btnFix(show) {
if (!show) {
this.close();
}
if (this.mode == 'date') {
let arr = this.activeDate.split('-')
let year = this.isChange ? this.year : Number(arr[0]);
let month = this.isChange ? this.month : Number(arr[1]);
let day = this.isChange ? this.day : Number(arr[2]);
//当前月有多少天
let days = this.getMonthDay(year, month);
let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
let weekText = this.getWeekText(result);
let isToday = false;
if (`${year}-${month}-${day}` == this.today) {
//今天
isToday = true;
}
this.$emit('change', {
year: year,
month: month,
day: day,
days: days,
result: result,
week: weekText,
isToday: isToday,
// switch: show //是否是切换年月操作
});
} else {
if (!this.startDate || !this.endDate) return;
let startMonth = this.formatNum(this.startMonth);
let startDay = this.formatNum(this.startDay);
let startDate = `${this.startYear}-${startMonth}-${startDay}`;
let startWeek = this.getWeekText(startDate)
let endMonth = this.formatNum(this.endMonth);
let endDay = this.formatNum(this.endDay);
let endDate = `${this.endYear}-${endMonth}-${endDay}`;
let endWeek = this.getWeekText(endDate);
this.$emit('change', {
startYear: this.startYear,
startMonth: this.startMonth,
startDay: this.startDay,
startDate: startDate,
startWeek: startWeek,
endYear: this.endYear,
endMonth: this.endMonth,
endDay: this.endDay,
endDate: endDate,
endWeek: endWeek
});
}
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-calendar {
color: $u-content-color;
&__header {
width: 100%;
box-sizing: border-box;
font-size: 30rpx;
background-color: #fff;
color: $u-main-color;
&__text {
margin-top: 30rpx;
padding: 0 60rpx;
@include vue-flex;
justify-content: center;
align-items: center;
}
}
&__action {
padding: 40rpx 0 40rpx 0;
&__icon {
margin: 0 16rpx;
}
&__text {
padding: 0 16rpx;
color: $u-main-color;
font-size: 32rpx;
line-height: 32rpx;
font-weight: bold;
}
}
&__week-day {
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
&__text {
flex: 1;
text-align: center;
}
}
&__content {
width: 100%;
@include vue-flex;
flex-wrap: wrap;
padding: 6px 0;
box-sizing: border-box;
background-color: #fff;
position: relative;
&--end-date {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&--start-date {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
&__item {
width: 14.2857%;
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
position: relative;
z-index: 2;
&__inner {
height: 84rpx;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 32rpx;
position: relative;
border-radius: 50%;
&__desc {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
transform: scale(0.75);
transform-origin: center center;
position: absolute;
left: 0;
text-align: center;
bottom: 2rpx;
}
}
&__tips {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
position: absolute;
left: 0;
transform: scale(0.8);
transform-origin: center center;
text-align: center;
bottom: 8rpx;
z-index: 2;
}
}
&__bg-month {
position: absolute;
font-size: 130px;
line-height: 130px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #e4e7ed;
z-index: 1;
}
}
&__bottom {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #fff;
padding: 0 40rpx 30rpx;
box-sizing: border-box;
font-size: 24rpx;
color: $u-tips-color;
&__choose {
height: 50rpx;
}
&__btn {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,267 @@
<template>
<view class="u-keyboard" @touchmove.stop.prevent="() => {}">
<view class="u-keyboard-grids">
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
<view :hover-stay-time="100" @touchstart="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
v-for="(item, j) in group" :key="j">
{{ item }}
</view>
</view>
<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
hover-class="u-hover-class">
<u-icon :size="38" name="backspace" :bold="true"></u-icon>
</view>
<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @click="changeCarInputMode">
<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
/
<text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "u-keyboard",
emits: ["change", "backspace"],
props: {
// 是否打乱键盘按键的顺序
random: {
type: Boolean,
default: false
}
},
data() {
return {
// 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
abc: false
};
},
computed: {
areaList() {
let data = [
'京',
'沪',
'粤',
'津',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'陕',
'蒙',
'吉',
'闽',
'贵',
'渝',
'川',
'青',
'琼',
'宁',
'挂',
'藏',
'港',
'澳',
'新',
'使',
'学'
];
let tmp = [];
// 打乱顺序
if (this.random) data = this.$u.randomArray(data);
// 切割成二维数组
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
},
EngKeyBoardList() {
let data = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
'Z',
'X',
'C',
'V',
'B',
'N',
'M'
];
let tmp = [];
if (this.random) data = this.$u.randomArray(data);
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
}
},
methods: {
// 点击键盘按钮
carInputClick(i, j) {
let value = '';
// 不同模式,获取不同数组的值
if (this.abc) value = this.EngKeyBoardList[i][j];
else value = this.areaList[i][j];
if(!this.abc) this.abc = true;
this.$emit('change', value);
if(this.vibrate) uni.vibrateShort();
},
// 修改汽车牌键盘的输入模式,中文|英文
changeCarInputMode() {
this.abc = !this.abc;
},
// 修改汽车牌键盘的输入模式,中文|英文
updateCarInputMode(abc) {
this.abc = abc;
},
// 点击退格键
backspaceClick() {
let count = 1;
this.backspaceFn(count);
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
this.timer = null;
this.timer = setInterval(() => {
count++;
this.backspaceFn(count);
}, 250);
},
backspaceFn(count){
this.$emit('backspace',count);
},
clearTimer() {
clearInterval(this.timer);
this.timer = null;
},
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-keyboard-grids {
background: rgb(215, 215, 217);
padding: 24rpx 0;
position: relative;
}
.u-keyboard-grids-item {
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-keyboard-grids-btn {
text-decoration: none;
width: 62rpx;
flex: 0 0 64rpx;
height: 80rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
font-size: 36rpx;
text-align: center;
line-height: 80rpx;
background-color: #fff;
margin: 8rpx 5rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx #888992;
font-weight: 500;
justify-content: center;
}
.u-carinput-hover {
background-color: rgb(185, 188, 195) !important;
}
.u-keyboard-back {
position: absolute;
width: 96rpx;
right: 22rpx;
bottom: 32rpx;
height: 80rpx;
background-color: rgb(185, 188, 195);
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
box-shadow: 0 2rpx 0rpx #888992;
}
.u-keyboard-change {
font-size: 24rpx;
box-shadow: 0 2rpx 0rpx #888992;
position: absolute;
width: 96rpx;
left: 22rpx;
line-height: 1;
bottom: 32rpx;
height: 80rpx;
background-color: #ffffff;
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
}
.u-keyboard-change .inactive.zh {
transform: scale(0.85) translateY(-10rpx);
}
.u-keyboard-change .inactive.en {
transform: scale(0.85) translateY(10rpx);
}
.u-keyboard-change .active {
color: rgb(237, 112, 64);
font-size: 30rpx;
}
.u-keyboard-change .zh {
transform: translateY(-10rpx);
}
.u-keyboard-change .en {
transform: translateY(10rpx);
}
</style>
@@ -0,0 +1,300 @@
<template>
<view
class="u-card"
@tap.stop="click"
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
:style="{
'--radius': borderRadius + 'rpx',
margin: margin,
boxShadow: boxShadow
}"
>
<view
v-if="showHead"
class="u-card__head"
:style="[{padding: padding + 'rpx'}, headStyle]"
:class="{
'u-border-bottom': headBorderBottom
}"
@tap="headClick"
>
<view v-if="!$slots.head" class="u-flex u-row-between">
<view class="u-card__head--left u-flex u-line-1" v-if="title">
<image
:src="thumb"
class="u-card__head--left__thumb"
mode="aspectFill"
v-if="thumb"
:style="{
height: thumbWidth + 'rpx',
width: thumbWidth + 'rpx',
borderRadius: thumbCircle ? '100rpx' : '6rpx'
}"
></image>
<text
class="u-card__head--left__title u-line-1"
:style="{
fontSize: titleSize + 'rpx',
color: titleColor
}"
>
{{ title }}
</text>
</view>
<view class="u-card__head--right u-line-1" v-if="subTitle">
<text
class="u-card__head__title__text"
:style="{
fontSize: subTitleSize + 'rpx',
color: subTitleColor
}"
>
{{ subTitle }}
</text>
</view>
</view>
<slot name="head" v-else />
</view>
<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
<view
v-if="showFoot"
class="u-card__foot"
@tap="footClick"
:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
:class="{
'u-border-top': footBorderTop
}"
>
<slot name="foot" />
</view>
</view>
</template>
<script>
/**
* card 卡片
* @description 卡片组件一般用于多个列表条目,且风格统一的场景
* @tutorial https://www.uviewui.com/components/card.html
* @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
* @property {String} title 头部左边的标题
* @property {String} title-color 标题颜色(默认#303133)
* @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
* @property {String} sub-title 头部右边的副标题
* @property {String} sub-title-color 副标题颜色(默认#909399)
* @property {String | Number} sub-title-size 副标题字体大小(默认26)
* @property {Boolean} border 是否显示边框(默认true)
* @property {String | Number} index 用于标识点击了第几个卡片
* @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
* @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
* @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
* @property {Object} head-style 头部自定义样式,对象形式
* @property {Object} body-style 中部自定义样式,对象形式
* @property {Object} foot-style 底部自定义样式,对象形式
* @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
* @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
* @property {Boolean} show-head 是否显示头部(默认true)
* @property {Boolean} show-head 是否显示尾部(默认true)
* @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
* @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
* @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
* @event {Function} click 整个卡片任意位置被点击时触发
* @event {Function} head-click 卡片头部被点击时触发
* @event {Function} body-click 卡片主体部分被点击时触发
* @event {Function} foot-click 卡片底部部分被点击时触发
* @example <u-card padding="30" title="card"></u-card>
*/
export default {
name: 'u-card',
emits: ["click", "head-click", "body-click", "foot-click"],
props: {
// 与屏幕两侧是否留空隙
full: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ''
},
// 标题颜色
titleColor: {
type: String,
default: '#303133'
},
// 标题字体大小,单位rpx
titleSize: {
type: [Number, String],
default: '30'
},
// 副标题
subTitle: {
type: String,
default: ''
},
// 副标题颜色
subTitleColor: {
type: String,
default: '#909399'
},
// 副标题字体大小,单位rpx
subTitleSize: {
type: [Number, String],
default: '26'
},
// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
border: {
type: Boolean,
default: true
},
// 用于标识点击了第几个
index: {
type: [Number, String, Object],
default: ''
},
// 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
margin: {
type: String,
default: '30rpx'
},
// card卡片的圆角
borderRadius: {
type: [Number, String],
default: '16'
},
// 头部自定义样式,对象形式
headStyle: {
type: Object,
default() {
return {};
}
},
// 主体自定义样式,对象形式
bodyStyle: {
type: Object,
default() {
return {};
}
},
// 底部自定义样式,对象形式
footStyle: {
type: Object,
default() {
return {};
}
},
// 头部是否下边框
headBorderBottom: {
type: Boolean,
default: true
},
// 底部是否有上边框
footBorderTop: {
type: Boolean,
default: true
},
// 标题左边的缩略图
thumb: {
type: String,
default: ''
},
// 缩略图宽高,单位rpx
thumbWidth: {
type: [String, Number],
default: '60'
},
// 缩略图是否为圆形
thumbCircle: {
type: Boolean,
default: false
},
// 给head,body,foot的内边距
padding: {
type: [String, Number],
default: '30'
},
// 是否显示头部
showHead: {
type: Boolean,
default: true
},
// 是否显示尾部
showFoot: {
type: Boolean,
default: true
},
// 卡片外围阴影,字符串形式
boxShadow: {
type: String,
default: 'none'
}
},
data() {
return {};
},
methods: {
click() {
this.$emit('click', this.index);
},
headClick() {
this.$emit('head-click', this.index);
},
bodyClick() {
this.$emit('body-click', this.index);
},
footClick() {
this.$emit('foot-click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-card {
position: relative;
overflow: hidden;
font-size: 28rpx;
background-color: #ffffff;
box-sizing: border-box;
&-full {
// 如果是与屏幕之间不留空隙,应该设置左右边距为0
margin-left: 0 !important;
margin-right: 0 !important;
width: 100%;
}
&--border:after {
border-radius: var(--radius,16rpx);
}
&__head {
&--left {
color: $u-main-color;
&__thumb {
margin-right: 16rpx;
}
&__title {
max-width: 400rpx;
}
}
&--right {
color: $u-tips-color;
margin-left: 6rpx;
}
}
&__body {
color: $u-content-color;
}
&__foot {
color: $u-tips-color;
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<view class="u-cell-box">
<view class="u-cell-title" v-if="title" :style="[titleStyle]">
{{title}}
</view>
<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
<slot />
</view>
</view>
</template>
<script>
/**
* cellGroup 单元格父组件Group
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 分组标题
* @property {Boolean} border 是否显示外边框(默认true)
* @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
* @example <u-cell-group title="设置喜好">
*/
export default {
name: "u-cell-group",
props: {
// 分组标题
title: {
type: String,
default: ''
},
// 是否显示分组list上下边框
border: {
type: Boolean,
default: true
},
// 分组标题的样式,对象形式,注意驼峰属性写法
// 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
titleStyle: {
type: Object,
default () {
return {};
}
}
},
data() {
return {
index: 0,
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell-box {
width: 100%;
}
.u-cell-title {
padding: 30rpx 32rpx 10rpx 32rpx;
font-size: 30rpx;
text-align: left;
color: $u-tips-color;
}
.u-cell-item-box {
background-color: #FFFFFF;
flex-direction: row;
}
</style>
@@ -0,0 +1,317 @@
<template>
<view
@tap="click"
class="u-cell"
:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
hover-stay-time="150"
:hover-class="hoverClass"
:style="{
backgroundColor: bgColor
}"
>
<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
<view class="u-flex" v-else>
<slot name="icon"></slot>
</view>
<view
class="u-cell_title"
:style="[
{
width: titleWidth ? titleWidth + 'rpx' : 'auto'
},
titleStyle
]"
>
<block v-if="title !== ''">{{ title }}</block>
<slot name="title" v-else></slot>
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
<block v-if="label !== ''">{{ label }}</block>
<slot name="label" v-else></slot>
</view>
</view>
<view class="u-cell__value" :style="[valueStyle]">
<block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
<slot v-else></slot>
</view>
<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
<slot name="right-icon"></slot>
</view>
<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
</view>
</template>
<script>
/**
* cellItem 单元格Item
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 左侧标题
* @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
* @property {Object} icon-style 左边图标的样式,对象形式
* @property {String} value 右侧内容
* @property {String} label 标题下方的描述信息
* @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
* @property {Boolean} border-top 是否显示cell的上边框(默认false)
* @property {Boolean} center 是否使内容垂直居中(默认false)
* @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
* // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
* @property {Boolean} arrow 是否显示右侧箭头(默认true)
* @property {Boolean} required 箭头方向,可选值(默认right)
* @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
* @property {Object} title-style 标题样式,对象形式
* @property {Object} value-style 右侧内容样式,对象形式
* @property {Object} label-style 标题下方描述信息的样式,对象形式
* @property {String} bg-color 背景颜色(默认transparent)
* @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
* @property {String Number} title-width 标题的宽度,单位rpx
* @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
*/
export default {
name: 'u-cell-item',
emits: ["click"],
props: {
// 左侧图标名称(只能uView内置图标),或者图标src
icon: {
type: String,
default: ''
},
// 左侧标题
title: {
type: [String, Number],
default: ''
},
// 右侧内容
value: {
type: [String, Number],
default: ''
},
// 标题下方的描述信息
label: {
type: [String, Number],
default: ''
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: true
},
// 是否显示上边框
borderTop: {
type: Boolean,
default: false
},
// 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
// 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
// borderGap: {
// type: Boolean,
// default: true
// },
// 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
hoverClass: {
type: String,
default: 'u-cell-hover'
},
// 是否显示右侧箭头
arrow: {
type: Boolean,
default: true
},
// 内容是否垂直居中
center: {
type: Boolean,
default: false
},
// 是否显示左边表示必填的星号
required: {
type: Boolean,
default: false
},
// 标题的宽度,单位rpx
titleWidth: {
type: [Number, String],
default: ''
},
// 右侧箭头方向,可选值:right|up|down,默认为right
arrowDirection: {
type: String,
default: 'right'
},
// 控制标题的样式
titleStyle: {
type: Object,
default() {
return {};
}
},
// 右侧显示内容的样式
valueStyle: {
type: Object,
default() {
return {};
}
},
// 描述信息的样式
labelStyle: {
type: Object,
default() {
return {};
}
},
// 背景颜色
bgColor: {
type: String,
default: 'transparent'
},
// 用于识别被点击的是第几个cell
index: {
type: [String, Number],
default: ''
},
// 是否使用lable插槽
useLabelSlot: {
type: Boolean,
default: false
},
// 左边图标的大小,单位rpx,只对传入icon字段时有效
iconSize: {
type: [Number, String],
default: 34
},
// 左边图标的样式,对象形式
iconStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
};
},
computed: {
arrowStyle() {
let style = {};
if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
else style.transform = 'rotate(0deg)';
return style;
}
},
methods: {
click() {
this.$emit('click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell {
@include vue-flex;
align-items: center;
position: relative;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
width: 100%;
padding: 26rpx 32rpx;
font-size: 28rpx;
line-height: 54rpx;
color: $u-content-color;
background-color: #fff;
text-align: left;
}
.u-cell_title {
font-size: 28rpx;
}
.u-cell__left-icon-wrap {
margin-right: 10rpx;
font-size: 32rpx;
}
.u-cell__right-icon-wrap {
margin-left: 10rpx;
color: #969799;
font-size: 28rpx;
}
.u-cell__left-icon-wrap,
.u-cell__right-icon-wrap {
@include vue-flex;
align-items: center;
height: 48rpx;
}
.u-cell-border:after {
position: absolute;
/* #ifndef APP-NVUE */
box-sizing: border-box;
content: ' ';
pointer-events: none;
border-bottom: 1px solid $u-border-color;
/* #endif */
right: 0;
left: 0;
top: 0;
transform: scaleY(0.5);
}
.u-cell-border {
position: relative;
}
.u-cell__label {
margin-top: 6rpx;
font-size: 26rpx;
line-height: 36rpx;
color: $u-tips-color;
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
}
.u-cell__value {
overflow: hidden;
text-align: right;
/* #ifndef APP-NVUE */
vertical-align: middle;
/* #endif */
color: $u-tips-color;
font-size: 26rpx;
}
.u-cell__title,
.u-cell__value {
flex: 1;
}
.u-cell--required {
/* #ifndef APP-NVUE */
overflow: visible;
/* #endif */
@include vue-flex;
align-items: center;
}
.u-cell--required:before {
position: absolute;
/* #ifndef APP-NVUE */
content: '*';
/* #endif */
left: 8px;
margin-top: 4rpx;
font-size: 14px;
color: $u-type-error;
}
.u-cell_right {
line-height: 1;
}
</style>
@@ -0,0 +1,178 @@
<template>
<view class="u-checkbox-group u-clearfix" :class="uFromData.inputAlign == 'right' ? 'flex-end' : ''"><slot></slot></view>
</template>
<script>
import Emitter from "../../libs/util/emitter.js";
/**
* checkboxGroup 开关选择器父组件Group
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} max 最多能选中多少个checkbox(默认999)
* @property {String Number} size 组件整体的大小,单位rpx(默认40)
* @property {Boolean} disabled 是否禁用所有checkbox(默认false)
* @property {String Number} icon-size 图标大小,单位rpx(默认20)
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
* @property {String} width 宽度,需带单位
* @property {String} width 宽度,需带单位
* @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
* @property {Boolean} wrap 是否每个checkbox都换行(默认false)
* @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
* @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
* @example <u-checkbox-group></u-checkbox-group>
*/
export default {
name: "u-checkbox-group",
emits: ["update:modelValue", "input", "change"],
mixins: [Emitter],
props: {
// 匹配某一个radio组件,如果某个radio的name值等于此值,那么这个radio就被会选中
value: {
type: [String, Number, Array, Boolean],
default: ""
},
modelValue: {
type: [String, Number, Array, Boolean],
default: ""
},
// 最多能选中多少个checkbox
max: {
type: [Number, String],
default: 999
},
// 所有选中项的 name
// value: {
// default: Array,
// default() {
// return []
// }
// },
// 是否禁用所有复选框
disabled: {
type: Boolean,
default: false
},
// 在表单内提交时的标识符
name: {
type: [Boolean, String],
default: ""
},
// 是否禁止点击提示语选中复选框
labelDisabled: {
type: Boolean,
default: false
},
// 形状,square为方形,circle为圆型
shape: {
type: String,
default: "square"
},
// 选中状态下的颜色
activeColor: {
type: String,
default: "#2979ff"
},
// 组件的整体大小
size: {
type: [String, Number],
default: 34
},
// 每个checkbox占u-checkbox-group的宽度
width: {
type: String,
default: "auto"
},
// 是否每个checkbox都换行
wrap: {
type: Boolean,
default: false
},
// 图标的大小,单位rpx
iconSize: {
type: [String, Number],
default: 20
}
},
data() {
return {
values: [],
uFromData: {
inputAlign: "left"
}
};
},
created() {
// 如果将children定义在data中,在微信小程序会造成循环引用而报错
this.children = [];
},
mounted() {
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
let parent = this.$u.$parent.call(this, "u-form");
if (parent) {
Object.keys(this.uFromData).map(key => {
this.uFromData[key] = parent[key];
});
}
},
methods: {
emitEvent(obj) {
let values = this.values || [];
if (obj.value) {
let index = values.indexOf(obj.name);
if (index === -1) {
values.push(obj.name);
}
} else {
let index = values.indexOf(obj.name);
if (index > -1) {
values.splice(index, 1);
}
}
this.$emit("change", values);
// 通过emit事件,设置父组件通过v-model双向绑定的值
this.$emit("input", values);
this.$emit("update:modelValue", values);
// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
setTimeout(() => {
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldChange", values);
}, 60);
},
_emitEvent(obj) {
let values = this.values || [];
if (obj.value) {
let index = values.indexOf(obj.name);
if (index === -1) {
values.push(obj.name);
}
} else {
let index = values.indexOf(obj.name);
if (index > -1) {
values.splice(index, 1);
}
}
//this.$emit("change", values);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox-group {
/* #ifndef MP || APP-NVUE */
display: inline-flex;
flex-wrap: wrap;
/* #endif */
}
.u-checkbox-group.flex-end {
/* #ifndef APP-NVUE */
display: inline-flex;
justify-content: flex-end;
flex-wrap: wrap;
/* #endif */
}
</style>
@@ -0,0 +1,336 @@
<template>
<view class="u-checkbox" :style="[checkboxStyle]">
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
<u-icon v-if="indeterminate" class="u-checkbox__icon-wrap__icon" name="minus" :size="checkboxIconSize" :color="iconColor" />
<u-icon v-else class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor" />
</view>
<view
class="u-checkbox__label"
@tap="onClickLabel"
:style="{
fontSize: $u.addUnit(labelSize)
}"
>
<slot />
</view>
</view>
</template>
<script>
/**
* checkbox 复选框
* @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} icon-size 图标大小,单位rpx(默认20)
* @property {String Number} label-size label字体大小,单位rpx(默认28)
* @property {String Number} name checkbox组件的标示符
* @property {String} shape 形状,外观形状,shape-方形,circle-圆形(默认circle)
* @property {Boolean} disabled 是否禁用
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
* @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
* @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
*/
export default {
name: "u-checkbox",
emits: ["update:modelValue", "input", "change"],
props: {
// 是否为选中状态
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// checkbox的名称
name: {
type: [String, Number],
default: ""
},
// 形状,square为方形,circle为圆型
shape: {
type: String,
default: ""
},
// 是否禁用
disabled: {
type: [String, Boolean],
default: ""
},
// 是否禁止点击提示语选中复选框
labelDisabled: {
type: [String, Boolean],
default: ""
},
// 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
activeColor: {
type: String,
default: ""
},
// 图标的大小,单位rpx
iconSize: {
type: [String, Number],
default: ""
},
// label的字体大小,rpx单位
labelSize: {
type: [String, Number],
default: ""
},
// 组件的整体大小
size: {
type: [String, Number],
default: ""
},
// 设置不确定状态,仅负责样式控制
indeterminate: {
type: Boolean,
default: false
}
},
data() {
return {
parentDisabled: false,
newParams: {}
};
},
created() {
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
this.parent = this.$u.$parent.call(this, "u-checkbox-group");
// 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
this.parent && this.parent.children.push(this);
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
isDisabled() {
return this.disabled !== "" ? this.disabled : this.parent ? this.parent.disabled : false;
},
// 是否禁用label点击
isLabelDisabled() {
return this.labelDisabled !== "" ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
},
// 组件尺寸,对应size的值,默认值为34rpx
checkboxSize() {
return this.size ? this.size : this.parent ? this.parent.size : 34;
},
// 组件的勾选图标的尺寸,默认20
checkboxIconSize() {
return this.iconSize ? this.iconSize : this.parent ? this.parent.iconSize : 20;
},
// 组件选中激活时的颜色
elActiveColor() {
return this.activeColor ? this.activeColor : this.parent ? this.parent.activeColor : "primary";
},
// 组件的形状
elShape() {
return this.shape ? this.shape : this.parent ? this.parent.shape : "square";
},
iconStyle() {
let style = {};
// 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
if (this.elActiveColor && this.valueCom && !this.isDisabled) {
style.borderColor = this.elActiveColor;
style.backgroundColor = this.elActiveColor;
}
style.width = this.$u.addUnit(this.checkboxSize);
style.height = this.$u.addUnit(this.checkboxSize);
return style;
},
// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
iconColor() {
if (this.indeterminate) return '#ffffff';
return this.valueCom ? "#ffffff" : "transparent";
},
iconClass() {
let classes = [];
classes.push("u-checkbox__icon-wrap--" + this.elShape);
if (this.valueCom == true) classes.push("u-checkbox__icon-wrap--checked");
if (this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled");
if (this.valueCom && this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled--checked");
if (this.indeterminate === true) classes.push('u-checkbox__icon-wrap--indeterminate')
// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
return classes.join(" ");
},
checkboxStyle() {
let style = {};
if (this.parent && this.parent.width) {
style.width = this.parent.width;
// #ifdef MP
// 各家小程序因为它们特殊的编译结构,使用float布局
style.float = "left";
// #endif
// #ifndef MP
// H5和APP使用flex布局
style.flex = `0 0 ${this.parent.width}`;
// #endif
}
if (this.parent && this.parent.wrap) {
style.width = "100%";
// #ifndef MP
// H5和APP使用flex布局,将宽度设置100%,即可自动换行
style.flex = "0 0 100%";
// #endif
}
return style;
}
},
mounted() {
this._emitEvent();
},
watch: {
valueCom: {
handler: function(newVal, oldVal) {
this._emitEvent();
}
}
},
methods: {
_emitEvent() {
let value = this.valueCom;
let obj = {
value,
name: this.name
};
// 执行父组件u-checkbox-group的事件方法
if (this.parent && this.parent.emitEvent) this.parent.emitEvent(obj);
},
onClickLabel() {
if (!this.isLabelDisabled && !this.isDisabled) {
this.setValue();
}
},
toggle() {
if (!this.isDisabled) {
this.setValue();
}
},
emitEvent() {
let obj = {
value: !this.valueCom,
name: this.name
};
this.$emit("change", obj);
// 执行父组件u-checkbox-group的事件方法
if (this.parent && this.parent.emitEvent) this.parent.emitEvent(obj);
},
// 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
setValue() {
let value = this.valueCom;
// 判断是否超过了可选的最大数量
let checkedNum = 0;
if (this.parent && this.parent.children) {
// 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
this.parent.children.map(val => {
if (val.value) checkedNum++;
});
}
// 如果原来为选中状态,那么可以取消
if (value == true) {
this.emitEvent();
this.$emit("input", !value);
this.$emit("update:modelValue", !value);
} else {
// 如果超出最多可选项,提示
if (this.parent && checkedNum >= this.parent.max) {
return this.$u.toast(`最多可选${this.parent.max}项`);
}
// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
this.emitEvent();
this.$emit("input", !value);
this.$emit("update:modelValue", !value);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
overflow: hidden;
user-select: none;
line-height: 1.8;
&__icon-wrap {
color: $u-content-color;
flex: none;
display: -webkit-flex;
@include vue-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 42rpx;
height: 42rpx;
color: transparent;
text-align: center;
transition-property: color, border-color, background-color;
font-size: 20px;
border: 1px solid #c8c9cc;
transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
&__icon {
line-height: 0;
}
/* #endif */
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 6rpx;
}
&--checked {
color: #fff;
background-color: $u-type-primary;
border-color: $u-type-primary;
}
&--disabled {
background-color: #ebedf0;
border-color: #c8c9cc;
}
&--disabled--checked {
color: #c8c9cc !important;
}
&--indeterminate {
color: #fff;
background-color: $u-type-primary;
border-color: $u-type-primary;
}
}
&__label {
word-wrap: break-word;
margin-left: 10rpx;
margin-right: 24rpx;
color: $u-content-color;
font-size: 30rpx;
&--disabled {
color: #c8c9cc;
}
}
}
</style>
@@ -0,0 +1,227 @@
<template>
<view
class="u-circle-progress"
:style="{
width: widthPx + 'px',
height: widthPx + 'px',
backgroundColor: bgColor
}"
>
<!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
<canvas
class="u-canvas-bg"
:canvas-id="elBgId"
:id="elBgId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<canvas
class="u-canvas"
:canvas-id="elId"
:id="elId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<slot></slot>
</view>
</template>
<script>
/**
* circleProgress 环形进度条
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
* @tutorial https://www.uviewui.com/components/circleProgress.html
* @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
* @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
* @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
* @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
* @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
* @property {String} type 如设置,active-color值将会失效
* @property {String} bg-color 整个组件背景颜色,默认为白色
* @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
*/
export default {
name: 'u-circle-progress',
emits: ["end","finish"],
props: {
// 圆环进度百分比值
percent: {
type: Number,
default: 0,
// 限制值在0到100之间
validator: val => {
return val >= 0 && val <= 100;
}
},
// 底部圆环的颜色(灰色的圆环)
inactiveColor: {
type: String,
default: '#ececec'
},
// 圆环激活部分的颜色
activeColor: {
type: String,
default: '#19be6b'
},
// 圆环线条的宽度,单位rpx
borderWidth: {
type: [Number, String],
default: 14
},
// 整个圆形的宽度,单位rpx
width: {
type: [Number, String],
default: 200
},
// 整个圆环执行一圈的时间,单位ms
duration: {
type: [Number, String],
default: 1500
},
// 主题类型
type: {
type: String,
default: ''
},
// 整个圆环进度区域的背景色
bgColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
// #ifdef MP-WEIXIN
elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
elId: 'uCircleProgressElId',
// #endif
// #ifndef MP-WEIXIN
elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
elId: this.$u.guid(),
// #endif
widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
progressContext: null, // 活动圆的canvas上下文
newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
};
},
watch: {
percent(nVal, oVal = 0) {
if (nVal > 100) nVal = 100;
if (nVal < 0) oVal = 0;
// 此值其实等于this.percent,命名一个新
this.newPercent = nVal;
this.oldPercent = oVal;
setTimeout(() => {
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
// 将此值减少或者新增到新的百分比值
this.drawCircleByProgress(oVal);
}, 50);
}
},
created() {
// 赋值,用于加载后第一个画圆使用
this.newPercent = this.percent;
this.oldPercent = 0;
},
computed: {
// 有type主题时,优先起作用
circleColor() {
if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
else return this.activeColor;
}
},
mounted() {
// 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
setTimeout(() => {
this.drawProgressBg();
this.drawCircleByProgress(this.oldPercent);
}, 50);
},
methods: {
drawProgressBg() {
let ctx = uni.createCanvasContext(this.elBgId, this);
ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
ctx.beginPath(); // 开始描绘路径
// 设置一个原点(110,110),半径为100的圆的路径到当前路径
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
ctx.stroke(); // 对路径进行描绘
ctx.draw();
},
drawCircleByProgress(progress) {
// 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
let ctx = this.progressContext;
if (!ctx) {
ctx = uni.createCanvasContext(this.elId, this);
this.progressContext = ctx;
}
// 表示进度的两端为圆形
ctx.setLineCap('round');
// 设置线条的宽度和颜色
ctx.setLineWidth(this.borderWidthPx);
ctx.setStrokeStyle(this.circleColor);
// 将总过渡时间除以100,得出每修改百分之一进度所需的时间
let time = Math.floor(this.duration / 100);
// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
// 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
ctx.beginPath();
// 半径为整个canvas宽度的一半
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
ctx.stroke();
ctx.draw();
// 如果变更后新值大于旧值,意味着增大了百分比
if (this.newPercent > this.oldPercent) {
// 每次递增百分之一
progress++;
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
if (progress > this.newPercent) return;
} else {
// 同理于上面
progress--;
if (progress < this.newPercent) return;
}
setTimeout(() => {
if (progress >= this.percent) {
this.$emit("end", progress);
}
if (progress >= 100) {
this.$emit("finish", progress);
}
// 定时器,每次操作间隔为time值,为了让进度条有动画效果
this.drawCircleByProgress(progress);
}, time);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-circle-progress {
position: relative;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
}
.u-canvas-bg {
position: absolute;
}
.u-canvas {
position: absolute;
}
</style>
@@ -0,0 +1,157 @@
<template>
<view class="u-col" :class="[
'u-col-' + span
]" :style="{
padding: `0 ${Number(gutter)/2 + 'rpx'}`,
marginLeft: 100 / 12 * offset + '%',
flex: `0 0 ${100 / 12 * span}%`,
alignItems: uAlignItem,
justifyContent: uJustify,
textAlign: textAlign
}"
@tap="click">
<slot></slot>
</view>
</template>
<script>
/**
* col 布局单元格
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
* @tutorial https://www.uviewui.com/components/layout.html
* @property {String Number} span 栅格占据的列数,总12等分(默认0)
* @property {String} text-align 文字水平对齐方式(默认left)
* @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
* @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
*/
export default {
name: "u-col",
emits: ["click"],
props: {
// 占父容器宽度的多少等分,总分为12份
span: {
type: [Number, String],
default: 12
},
// 指定栅格左侧的间隔数(总12栏)
offset: {
type: [Number, String],
default: 0
},
// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
justify: {
type: String,
default: 'start'
},
// 垂直对齐方式,可选值为top、center、bottom
align: {
type: String,
default: 'center'
},
// 文字对齐方式
textAlign: {
type: String,
default: 'left'
},
// 是否阻止事件传播
stop: {
type: Boolean,
default: true
}
},
data() {
return {
gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
}
},
created() {
this.parent = false;
},
mounted() {
// 获取父组件实例,并赋值给对应的参数
this.parent = this.$u.$parent.call(this, 'u-row');
if (this.parent) {
this.gutter = this.parent.gutter;
}
},
computed: {
uJustify() {
if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
else return this.justify;
},
uAlignItem() {
if (this.align == 'top') return 'flex-start';
if (this.align == 'bottom') return 'flex-end';
else return this.align;
}
},
methods: {
click(e) {
this.$emit('click',e);
}
}
}
</script>
<style lang="scss">
@import "../../libs/css/style.components.scss";
.u-col {
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
float: left;
/* #endif */
}
.u-col-0 {
width: 0;
}
.u-col-1 {
width: calc(100%/12);
}
.u-col-2 {
width: calc(100%/12 * 2);
}
.u-col-3 {
width: calc(100%/12 * 3);
}
.u-col-4 {
width: calc(100%/12 * 4);
}
.u-col-5 {
width: calc(100%/12 * 5);
}
.u-col-6 {
width: calc(100%/12 * 6);
}
.u-col-7 {
width: calc(100%/12 * 7);
}
.u-col-8 {
width: calc(100%/12 * 8);
}
.u-col-9 {
width: calc(100%/12 * 9);
}
.u-col-10 {
width: calc(100%/12 * 10);
}
.u-col-11 {
width: calc(100%/12 * 11);
}
.u-col-12 {
width: calc(100%/12 * 12);
}
</style>
@@ -0,0 +1,206 @@
<template>
<view class="u-collapse-item" :style="[itemStyle]">
<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
<block v-if="!$slots['title-all']">
<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
isShow && activeStyle && !arrow ? activeStyle : '']">
{{ title }}
</view>
<slot v-else name="title" />
<view class="u-icon-wrap">
<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
class="u-arrow-down-icon" name="arrow-down"></u-icon>
</view>
</block>
<slot v-else name="title-all" />
</view>
<view class="u-collapse-body" :style="[{
height: isShow ? height + 'px' : '0'
}]">
<view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
/**
* collapseItem 手风琴Item
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {String} title 面板标题
* @property {String Number} index 主要用于事件的回调,标识那个Item被点击
* @property {Boolean} disabled 面板是否可以打开或收起(默认false)
* @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
* @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
* @property {String} align 标题的对齐方式(默认left)
* @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
* @event {Function} change 某个item被打开或者收起时触发
* @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
*/
export default {
name: "u-collapse-item",
emits: ["change"],
props: {
// 标题
title: {
type: String,
default: ''
},
// 标题的对齐方式
align: {
type: String,
default: 'left'
},
// 是否可以点击收起
disabled: {
type: Boolean,
default: false
},
// collapse显示与否
open: {
type: Boolean,
default: false
},
// 唯一标识符
name: {
type: [Number, String],
default: ''
},
//活动样式
activeStyle: {
type: Object,
default () {
return {}
}
},
// 标识当前为第几个
index: {
type: [String, Number],
default: ''
}
},
data() {
return {
isShow: false,
elId: this.$u.guid(),
height: 0, // body内容的高度
headStyle: {}, // 头部样式,对象形式
bodyStyle: {}, // 主体部分样式
itemStyle: {}, // 每个item的整体样式
arrowColor: '', // 箭头的颜色
hoverClass: '', // 头部按下时的效果样式类
arrow: true, // 是否显示右侧箭头
};
},
watch: {
open(val) {
this.isShow = val;
}
},
created() {
this.parent = false;
// 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
this.isShow = this.open;
},
methods: {
// 异步获取内容,或者动态修改了内容时,需要重新初始化
init() {
this.parent = this.$u.$parent.call(this, 'u-collapse');
if(this.parent) {
this.nameSync = this.name ? this.name : this.parent.childrens.length;
// 不存在时才添加本实例
!this.parent.childrens.includes(this) && this.parent.childrens.push(this);
this.headStyle = this.parent.headStyle;
this.bodyStyle = this.parent.bodyStyle;
this.arrowColor = this.parent.arrowColor;
this.hoverClass = this.parent.hoverClass;
this.arrow = this.parent.arrow;
this.itemStyle = this.parent.itemStyle;
}
this.$nextTick(() => {
this.queryRect();
});
},
// 点击collapsehead头部
headClick() {
if (this.disabled) return;
if (this.parent && this.parent.accordion == true) {
this.parent.childrens.map(val => {
// 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
if (this != val) {
val.isShow = false;
}
});
}
this.isShow = !this.isShow;
// 触发本组件的事件
this.$emit('change', {
index: this.index,
show: this.isShow
})
// 只有在打开时才发出事件
if (this.isShow) this.parent && this.parent.onChange();
this.$forceUpdate();
},
// 查询内容高度
queryRect() {
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
// 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
this.$uGetRect('#' + this.elId).then(res => {
this.height = res.height;
})
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-collapse-head {
position: relative;
@include vue-flex;
justify-content: space-between;
align-items: center;
color: $u-main-color;
font-size: 30rpx;
line-height: 1;
padding: 24rpx 0;
text-align: left;
}
.u-collapse-title {
flex: 1;
overflow: hidden;
}
.u-arrow-down-icon {
transition: all 0.3s;
margin-right: 20rpx;
margin-left: 14rpx;
}
.u-arrow-down-icon-active {
transform: rotate(180deg);
transform-origin: center center;
}
.u-collapse-body {
overflow: hidden;
transition: all 0.3s;
}
.u-collapse-content {
overflow: hidden;
font-size: 28rpx;
color: $u-tips-color;
text-align: left;
}
</style>
@@ -0,0 +1,100 @@
<template>
<view class="u-collapse">
<slot />
</view>
</template>
<script>
/**
* collapse 手风琴
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {Boolean} accordion 是否手风琴模式(默认true)
* @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
* @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
* @property {Object} head-style 标题自定义样式,对象形式
* @property {Object} body-style 主体自定义样式,对象形式
* @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
* @example <u-collapse></u-collapse>
*/
export default {
name:"u-collapse",
emits: ["change"],
props: {
// 是否手风琴模式
accordion: {
type: Boolean,
default: true
},
// 头部的样式
headStyle: {
type: Object,
default () {
return {}
}
},
// 主体的样式
bodyStyle: {
type: Object,
default () {
return {}
}
},
// 每一个item的样式
itemStyle: {
type: Object,
default () {
return {}
}
},
// 是否显示右侧的箭头
arrow: {
type: Boolean,
default: true
},
// 箭头的颜色
arrowColor: {
type: String,
default: '#909399'
},
// 标题部分按压时的样式类,"none"为无效果
hoverClass: {
type: String,
default: 'u-hover-class'
}
},
created() {
this.childrens = []
},
data() {
return {
}
},
methods: {
// 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
init() {
this.childrens.forEach((vm, index) => {
vm.init();
})
},
// collapse item被点击,由collapse item调用父组件方法
onChange() {
let activeItem = [];
this.childrens.forEach((vm, index) => {
if (vm.isShow) {
activeItem.push(vm.nameSync);
}
})
// 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
if (this.accordion) activeItem = activeItem.join('');
this.$emit('change', activeItem);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,238 @@
<template>
<view
class="u-notice-bar"
:style="{
background: computeBgColor,
padding: padding
}"
:class="[
type ? `u-type-${type}-light-bg` : ''
]"
>
<view class="u-icon-wrap">
<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
</view>
<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
<view
class="u-news-item u-line-1"
:style="[textStyle]"
@tap="click(index)"
:class="['u-type-' + type]"
>
{{ item }}
</view>
</swiper-item>
</swiper>
<view class="u-icon-wrap">
<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
</view>
</view>
</template>
<script>
export default {
emits: ["close", "getMore", "end"],
props: {
// 显示的内容,数组
list: {
type: Array,
default() {
return [];
}
},
// 显示的主题,success|error|primary|info|warning
type: {
type: String,
default: 'warning'
},
// 是否显示左侧的音量图标
volumeIcon: {
type: Boolean,
default: true
},
// 是否显示右侧的右箭头图标
moreIcon: {
type: Boolean,
default: false
},
// 是否显示右侧的关闭图标
closeIcon: {
type: Boolean,
default: false
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 文字颜色,各图标也会使用文字颜色
color: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 滚动方向,row-水平滚动,column-垂直滚动
direction: {
type: String,
default: 'row'
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 字体大小,单位rpx
fontSize: {
type: [Number, String],
default: 26
},
// 滚动一个周期的时间长,单位ms
duration: {
type: [Number, String],
default: 2000
},
// 音量喇叭的大小
volumeSize: {
type: [Number, String],
default: 34
},
// 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
speed: {
type: Number,
default: 160
},
// 水平滚动时,是否采用衔接形式滚动
isCircular: {
type: Boolean,
default: true
},
// 滚动方向,horizontal-水平滚动,vertical-垂直滚动
mode: {
type: String,
default: 'horizontal'
},
// 播放状态,play-播放,paused-暂停
playState: {
type: String,
default: 'play'
},
// 是否禁止用手滑动切换
// 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
disableTouch: {
type: Boolean,
default: true
},
// 通知的边距
padding: {
type: [Number, String],
default: '18rpx 24rpx'
}
},
computed: {
// 计算字体颜色,如果没有自定义的,就用uview主题颜色
computeColor() {
if (this.color) return this.color;
// 如果是无主题,就默认使用content-color
else if(this.type == 'none') return '#606266';
else return this.type;
},
// 文字内容的样式
textStyle() {
let style = {};
if (this.color) style.color = this.color;
else if(this.type == 'none') style.color = '#606266';
style.fontSize = this.fontSize + 'rpx';
return style;
},
// 垂直或者水平滚动
vertical() {
if(this.mode == 'horizontal') return false;
else return true;
},
// 计算背景颜色
computeBgColor() {
if (this.bgColor) return this.bgColor;
else if(this.type == 'none') return 'transparent';
}
},
data() {
return {
// animation: false
};
},
methods: {
// 点击通告栏
click(index) {
this.$emit('click', index);
},
// 点击关闭按钮
close() {
this.$emit('close');
},
// 点击更多箭头按钮
getMore() {
this.$emit('getMore');
},
change(e) {
let index = e.detail.current;
if(index == this.list.length - 1) {
this.$emit('end');
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
padding: 18rpx 24rpx;
overflow: hidden;
}
.u-swiper {
font-size: 26rpx;
height: 32rpx;
@include vue-flex;
align-items: center;
flex: 1;
margin-left: 12rpx;
}
.u-swiper-item {
@include vue-flex;
align-items: center;
overflow: hidden;
}
.u-news-item {
overflow: hidden;
}
.u-right-icon {
margin-left: 12rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
.u-left-icon {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
</style>
@@ -0,0 +1,175 @@
<template>
<view class="u-count-down">
<slot>
<text class="u-count-down__text" :style="customStyle">{{ formattedTime }}</text>
</slot>
</view>
</template>
<script>
import { isSameSecond, parseFormat, parseTimeData } from "./utils";
/**
* u-count-down 倒计时
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
* @tutorial https://uviewui.com/components/countDown.html
* @property {String | Number} timestamp 倒计时时长,单位ms (默认 0 )
* @property {String} format 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 (默认 'HH:mm:ss' )
* @property {Boolean} autoStart 是否自动开始倒计时 (默认 true )
* @event {Function} end 倒计时结束时触发
* @event {Function} change 倒计时变化时触发
* @event {Function} start 开始倒计时
* @event {Function} pause 暂停倒计时
* @event {Function} reset 重设倒计时,若 auto-start 为 true,重设后会自动开始倒计时
* @example <u-count-down :timestamp="timestamp"></u-count-down>
*/
export default {
name: "u-count-down",
emits: ["change", "end", "finish"],
props: {
// 倒计时时长,单位ms
timestamp: {
type: [String, Number],
default: 0
},
// 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒
format: {
type: String,
default: "DD:HH:mm:ss"
},
// 是否自动开始倒计时
autoStart: {
type: Boolean,
default: true
},
customStyle: {
type: [String, Object],
default: ""
}
},
data() {
return {
timer: null,
// 各单位(天,时,分等)剩余时间
timeData: parseTimeData(0),
// 格式化后的时间,如"03:23:21"
formattedTime: "0",
// 倒计时是否正在进行中
runing: false,
endTime: 0, // 结束的毫秒时间戳
remainTime: 0 // 剩余的毫秒时间
};
},
watch: {
timestamp(n) {
this.reset();
},
format(newVal, oldVal) {
this.pause();
this.start();
}
},
mounted() {
this.init();
},
methods: {
init() {
this.reset();
},
// 开始倒计时
start() {
if (this.runing) return;
// 标识为进行中
this.runing = true;
// 结束时间戳 = 此刻时间戳 + 剩余的时间
this.endTime = Date.now() + this.remainTime;
this.toTick();
},
// 根据是否展示毫秒,执行不同操作函数
toTick() {
if (this.format.indexOf("SSS") > -1) {
this.microTick();
} else {
this.macroTick();
}
},
macroTick() {
this.clearTimeout();
// 每隔一定时间,更新一遍定时器的值
// 同时此定时器的作用也能带来毫秒级的更新
this.timer = setTimeout(() => {
// 获取剩余时间
const remain = this.getRemainTime();
// 重设剩余时间
if (!isSameSecond(remain, this.remainTime) || remain === 0) {
this.setRemainTime(remain);
}
// 如果剩余时间不为0,则继续检查更新倒计时
if (this.remainTime !== 0) {
this.macroTick();
}
}, 30);
},
microTick() {
this.clearTimeout();
this.timer = setTimeout(() => {
this.setRemainTime(this.getRemainTime());
if (this.remainTime !== 0) {
this.microTick();
}
}, 30);
},
// 获取剩余的时间
getRemainTime() {
// 取最大值,防止出现小于0的剩余时间值
return Math.max(this.endTime - Date.now(), 0);
},
// 设置剩余的时间
setRemainTime(remain) {
this.remainTime = remain;
// 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
const timeData = parseTimeData(remain);
this.$emit("change", timeData);
// 得出格式化后的时间
this.formattedTime = parseFormat(this.format, timeData);
// 如果时间已到,停止倒计时
if (remain <= 0) {
this.pause();
this.$emit("end");
this.$emit("finish");
}
},
// 重置倒计时
reset() {
this.pause();
this.remainTime = this.timestamp;
this.setRemainTime(this.remainTime);
if (this.autoStart) {
this.start();
}
},
// 暂停倒计时
pause() {
this.runing = false;
this.clearTimeout();
},
// 清空定时器
clearTimeout() {
clearTimeout(this.timer);
this.timer = null;
}
},
// #ifndef VUE3
beforeDestroy() {
this.clearTimeout();
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.clearTimeout();
},
// #endif
};
</script>
<style lang="scss"></style>
@@ -0,0 +1,62 @@
// 补0,如1 -> 01
function padZero(num, targetLength = 2) {
let str = `${num}`
while (str.length < targetLength) {
str = `0${str}`
}
return str
}
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
export function parseTimeData(time) {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
days,
hours,
minutes,
seconds,
milliseconds
}
}
export function parseFormat(format, timeData) {
let {
days,
hours,
minutes,
seconds,
milliseconds
} = timeData
// 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
if (format.indexOf('DD') === -1) {
hours += days * 24
} else {
// 对天补0
format = format.replace('DD', padZero(days))
}
// 其他同理于DD的格式化处理方式
if (format.indexOf('HH') === -1) {
minutes += hours * 60
} else {
format = format.replace('HH', padZero(hours))
}
if (format.indexOf('mm') === -1) {
seconds += minutes * 60
} else {
format = format.replace('mm', padZero(minutes))
}
if (format.indexOf('ss') === -1) {
milliseconds += seconds * 1000
} else {
format = format.replace('ss', padZero(seconds))
}
return format.replace('SSS', padZero(milliseconds, 3))
}
export function isSameSecond(time1, time2) {
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
}
@@ -0,0 +1,266 @@
<template>
<view
class="u-count-num"
:style="{
fontSize: fontSize + 'rpx',
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>
{{ displayValueCom }}
</view>
</template>
<script>
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
* @tutorial https://www.uviewui.com/components/countTo.html
* @property {String Number} nullVal 空值或NaN时显示的值,默认 -
* @property {String Number} start-val 开始值
* @property {String Number} end-val 结束值
* @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
* @property {Boolean} autoplay 是否自动开始滚动(默认true)
* @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
* @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
* @property {String} separator 千位分隔符,见官网说明
* @property {String} color 字体颜色(默认#303133)
* @property {String Number} font-size 字体大小,单位rpx(默认50)
* @property {Boolean} bold 字体是否加粗(默认false)
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
*/
export default {
name: "u-count-to",
emits: ["end"],
props: {
// 没有值时显示
nullVal: {
type: [Number, String],
default: "-"
},
// 开始的数值,默认从0增长到某一个数
startVal: {
type: [Number, String],
default: 0
},
// 要滚动的目标数值,必须
endVal: {
type: [Number, String],
default: 0,
required: true
},
// 滚动到目标数值的动画持续时间,单位为毫秒(ms)
duration: {
type: [Number, String],
default: 2000
},
// 设置数值后是否自动开始滚动
autoplay: {
type: Boolean,
default: true
},
// 要显示的小数位数
decimals: {
type: [Number, String],
default: 0
},
// 是否在即将到达目标数值的时候,使用缓慢滚动的效果
useEasing: {
type: Boolean,
default: true
},
// 十进制分割
decimal: {
type: [Number, String],
default: "."
},
// 字体颜色
color: {
type: String,
default: "#303133"
},
// 字体大小
fontSize: {
type: [Number, String],
default: 50
},
// 是否加粗字体
bold: {
type: Boolean,
default: false
},
// 千位分隔符,类似金额的分割(¥23,321.05中的",")
separator: {
type: String,
default: ""
}
},
data() {
return {
localStartVal: this.startVal,
displayValue: this.formatNumber(this.startVal),
printVal: null,
paused: false, // 是否暂停
localDuration: Number(this.duration),
startTime: null, // 开始的时间
timestamp: null, // 时间戳
remaining: null, // 停留的时间
rAF: null,
lastTime: 0 // 上一次的时间
};
},
computed: {
countDown() {
return this.startVal > this.endVal;
},
displayValueCom() {
let str;
let { displayValue, nullVal, endVal } = this;
if (isNaN(endVal)) {
str = nullVal;
} else {
str = displayValue;
}
return str;
}
},
watch: {
startVal() {
this.autoplay && this.start();
},
endVal() {
this.autoplay && this.start();
}
},
mounted() {
this.autoplay && this.start();
},
methods: {
easingFn(t, b, c, d) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
},
requestAnimationFrame(callback) {
const currTime = new Date().getTime();
// 为了使setTimteout的尽可能的接近每秒60帧的效果
const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
const id = setTimeout(() => {
callback(currTime + timeToCall);
}, timeToCall);
this.lastTime = currTime + timeToCall;
return id;
},
cancelAnimationFrame(id) {
clearTimeout(id);
},
// 开始滚动数字
start() {
this.localStartVal = this.startVal;
this.startTime = null;
this.localDuration = this.duration;
this.paused = false;
this.rAF = this.requestAnimationFrame(this.count);
},
// 暂定状态,重新再开始滚动;或者滚动状态下,暂停
reStart() {
if (this.paused) {
this.resume();
this.paused = false;
} else {
this.stop();
this.paused = true;
}
},
// 暂停
stop() {
this.cancelAnimationFrame(this.rAF);
},
// 重新开始(暂停的情况下)
resume() {
this.startTime = null;
this.localDuration = this.remaining;
this.localStartVal = this.printVal;
this.requestAnimationFrame(this.count);
},
// 重置
reset() {
this.startTime = null;
this.cancelAnimationFrame(this.rAF);
this.displayValue = this.formatNumber(this.startVal);
},
count(timestamp) {
if (!this.startTime) this.startTime = timestamp;
this.timestamp = timestamp;
const progress = timestamp - this.startTime;
this.remaining = this.localDuration - progress;
if (this.useEasing) {
if (this.countDown) {
this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
} else {
this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
}
} else {
if (this.countDown) {
this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
} else {
this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
}
}
if (this.countDown) {
this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
} else {
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
}
this.displayValue = this.formatNumber(this.printVal);
if (progress < this.localDuration) {
this.rAF = this.requestAnimationFrame(this.count);
} else {
this.$emit("end");
}
},
// 判断是否数字
isNumber(val) {
return !isNaN(parseFloat(val));
},
formatNumber(num) {
// 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
num = Number(num);
num = num.toFixed(Number(this.decimals));
num += "";
const x = num.split(".");
let x1 = x[0];
const x2 = x.length > 1 ? this.decimal + x[1] : "";
const rgx = /(\d+)(\d{3})/;
if (this.separator && !this.isNumber(this.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, "$1" + this.separator + "$2");
}
}
return x1 + x2;
},
// #ifndef VUE3
destroyed() {
this.cancelAnimationFrame(this.rAF);
},
// #endif
// #ifdef VUE3
unmounted() {
this.cancelAnimationFrame(this.rAF);
}
// #endif
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
}
</style>
@@ -0,0 +1,159 @@
<template>
<view class="u-divider" :style="{
height: height == 'auto' ? 'auto' : height + 'rpx',
backgroundColor: bgColor,
marginBottom: marginBottom + 'rpx',
marginTop: marginTop + 'rpx',
paddingBottom: paddingBottom + 'rpx',
}" @tap="click">
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
<view v-if="useSlot" class="u-divider-text" :style="{
color: color,
fontSize: fontSize + 'rpx'
}"><slot /></view>
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
</view>
</template>
<script>
/**
* divider 分割线
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
* @tutorial https://www.uviewui.com/components/divider.html
* @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
* @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
* @property {String} color 文字颜色(默认#909399)
* @property {String Number} fontSize 字体大小,单位rpx(默认26)
* @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
* @property {String Number} height 整个divider的高度,单位rpx(默认40)
* @property {String} type 将线条设置主题色(默认primary)
* @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
* @event {Function} click divider组件被点击时触发
* @example <u-divider color="#fa3534">长河落日圆</u-divider>
*/
export default {
name: 'u-divider',
props: {
// 单一边divider横线的宽度(数值),单位rpx。或者百分比
halfWidth: {
type: [Number, String],
default: 150
},
// divider横线的颜色,如设置,
borderColor: {
type: String,
default: '#dcdfe6'
},
// 主题色,可以是primary|info|success|warning|error之一值
type: {
type: String,
default: 'primary'
},
// 文字颜色
color: {
type: String,
default: '#909399'
},
// 文字大小,单位rpx
fontSize: {
type: [Number, String],
default: 26
},
// 整个divider的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 整个divider的高度单位rpx
height: {
type: [Number, String],
default: 'auto'
},
// 上边距
marginTop: {
type: [String, Number],
default: 0
},
// 下边距
marginBottom: {
type: [String, Number],
default: 0
},
// 下内边距(如果本组件下面没有其他元素了,则用marginBottom会无效,需要用paddingBottom)
paddingBottom: {
type: [String, Number],
default: 0
},
// 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
useSlot: {
type: Boolean,
default: true
}
},
computed: {
lineStyle() {
let style = {};
if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
else style.width = this.halfWidth + 'rpx';
// borderColor优先级高于type值
if(this.borderColor) style.borderColor = this.borderColor;
return style;
}
},
methods: {
click() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-divider {
width: 100%;
position: relative;
text-align: center;
@include vue-flex;
justify-content: center;
align-items: center;
overflow: hidden;
flex-direction: row;
}
.u-divider-line {
border-bottom: 1px solid $u-border-color;
transform: scale(1, 0.5);
transform-origin: center;
&--bordercolor--primary {
border-color: $u-type-primary;
}
&--bordercolor--success {
border-color: $u-type-success;
}
&--bordercolor--error {
border-color: $u-type-primary;
}
&--bordercolor--info {
border-color: $u-type-info;
}
&--bordercolor--warning {
border-color: $u-type-warning;
}
}
.u-divider-text {
white-space: nowrap;
padding: 0 16rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
</style>
@@ -0,0 +1,166 @@
<template>
<view
class="u-dropdown-item"
v-if="active"
@touchmove.stop.prevent="() => {}"
@tap.stop.prevent="() => {}"
>
<block v-if="!$slots.default && !$slots.$default">
<scroll-view
scroll-y="true"
:style="{
height: $u.addUnit(height)
}"
>
<view class="u-dropdown-item__options">
<u-cell-group>
<u-cell-item
@click="cellClick(item.value)"
:arrow="false"
:title="item.label"
v-for="(item, index) in options"
:key="index"
:title-style="{
color: value === item.value ? activeColor : inactiveColor
}"
>
<u-icon
v-if="valueCom === item.value"
name="checkbox-mark"
:color="activeColor"
size="32"
></u-icon>
</u-cell-item>
</u-cell-group>
</view>
</scroll-view>
</block>
<slot v-else />
</view>
</template>
<script>
/**
* dropdown-item 下拉菜单
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String | Number} v-model 双向绑定选项卡选择值
* @property {String} title 菜单项标题
* @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
* @property {Boolean} disabled 是否禁用此选项卡(默认false)
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
* @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
* @example <u-dropdown-item title="标题"></u-dropdown-item>
*/
export default {
name: "u-dropdown-item",
emits: ["update:modelValue", "input", "change"],
props: {
// 当前选中项的value值
value: {
type: [Number, String, Array],
default: ""
},
modelValue: {
type: [Number, String, Array],
default: ""
},
// 菜单项标题
title: {
type: [String, Number],
default: ""
},
// 选项数据,如果传入了默认slot,此参数无效
options: {
type: Array,
default() {
return [];
}
},
// 是否禁用此菜单项
disabled: {
type: Boolean,
default: false
},
// 下拉弹窗的高度
height: {
type: [Number, String],
default: "auto"
}
},
data() {
return {
active: false, // 当前项是否处于展开状态
activeColor: "#2979ff", // 激活时左边文字和右边对勾图标的颜色
inactiveColor: "#606266" // 未激活时左边文字和右边对勾图标的颜色
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
propsChange() {
return `${this.title}-${this.disabled}`;
}
},
watch: {
propsChange(n) {
// 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
// 将所有子组件数据重新整理一遍
if (this.parent) this.parent.init();
}
},
created() {
// 父组件的实例
this.parent = false;
},
methods: {
init() {
// 获取父组件u-dropdown
let parent = this.$u.$parent.call(this, "u-dropdown");
if (parent) {
this.parent = parent;
// 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
this.activeColor = parent.activeColor;
this.inactiveColor = parent.inactiveColor;
// 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
// push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
let exist = parent.children.find(val => {
return this === val;
});
if (!exist) parent.children.push(this);
if (parent.children.length == 1) this.active = true;
// 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
parent.menuList.push({
title: this.title,
disabled: this.disabled
});
}
},
// cell被点击
cellClick(value) {
// 修改通过v-model绑定的值
this.$emit("input", value);
this.$emit("update:modelValue", value);
// 通知父组件(u-dropdown)收起菜单
this.parent.close();
// 发出事件,抛出当前勾选项的value
this.$emit("change", value);
}
},
mounted() {
this.init();
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,299 @@
<template>
<view class="u-dropdown">
<view class="u-dropdown__menu" :style="{
height: $u.addUnit(height)
}" :class="{
'u-border-bottom': borderBottom
}">
<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
<view class="u-flex">
<text class="u-dropdown__menu__item__text" :style="{
color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
fontSize: $u.addUnit(titleSize)
}">{{item.title}}</text>
<view class="u-dropdown__menu__item__arrow" :class="{
'u-dropdown__menu__item__arrow--rotate': index === current
}">
<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
</view>
</view>
</view>
</view>
<view class="u-dropdown__content" :style="[contentStyle, {
transition: `opacity ${duration / 1000}s linear`,
top: $u.addUnit(height),
height: contentHeight + 'px'
}]"
@tap="maskClick" @touchmove.stop.prevent>
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
<slot></slot>
</view>
<view class="u-dropdown__content__mask"></view>
</view>
</view>
</template>
<script>
/**
* dropdown 下拉菜单
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
* @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
* @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
* @property {String | Number} height 标题菜单的高度,单位任意(默认80)
* @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
* @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
* @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
* @event {Function} open 下拉菜单被打开时触发
* @event {Function} close 下拉菜单被关闭时触发
* @example <u-dropdown></u-dropdown>
*/
export default {
name: 'u-dropdown',
emits: ["open", "close"],
props: {
// 菜单标题和选项的激活态颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 菜单标题和选项的未激活态颜色
inactiveColor: {
type: String,
default: '#606266'
},
// 点击遮罩是否关闭菜单
closeOnClickMask: {
type: Boolean,
default: true
},
// 点击当前激活项标题是否关闭菜单
closeOnClickSelf: {
type: Boolean,
default: true
},
// 过渡时间
duration: {
type: [Number, String],
default: 300
},
// 标题菜单的高度,单位任意,数值默认为rpx单位
height: {
type: [Number, String],
default: 80
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 标题的字体大小
titleSize: {
type: [Number, String],
default: 28
},
// 下拉出来的内容部分的圆角值
borderRadius: {
type: [Number, String],
default: 0
},
// 菜单右侧的icon图标
menuIcon: {
type: String,
default: 'arrow-down'
},
// 菜单右侧图标的大小
menuIconSize: {
type: [Number, String],
default: 26
}
},
data() {
return {
showDropdown: true, // 是否打开下来菜单,
menuList: [], // 显示的菜单
active: false, // 下拉菜单的状态
// 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
current: 99999,
// 外层内容的样式,初始时处于底层,且透明
contentStyle: {
zIndex: -1,
opacity: 0
},
// 让某个菜单保持高亮的状态
highlightIndex: 99999,
contentHeight: 0
}
},
computed: {
// 下拉出来部分的样式
popupStyle() {
let style = {};
// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
style.transform = `translateY(${this.active ? 0 : '-100%'})`
style['transition-duration'] = this.duration / 1000 + 's';
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
return style;
}
},
created() {
// 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
this.children = [];
},
mounted() {
this.getContentHeight();
},
methods: {
init() {
// 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
// 以保证数据的正确性
this.menuList = [];
this.children.map(child => {
child.init();
})
},
// 点击菜单
menuClick(index) {
// 判断是否被禁用
if (this.menuList[index].disabled) return;
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
if (index === this.current && this.closeOnClickSelf) {
this.close();
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
setTimeout(() => {
this.children[index].active = false;
}, this.duration)
return;
}
this.open(index);
},
// 打开下拉菜单
open(index) {
// 重置高亮索引,否则会造成多个菜单同时高亮
// this.highlightIndex = 9999;
// 展开时,设置下拉内容的样式
this.contentStyle = {
zIndex: 11,
}
// 标记展开状态以及当前展开项的索引
this.active = true;
this.current = index;
// 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
// 之所以不是因display: none,是因为nvue没有display这个属性
this.children.map((val, idx) => {
val.active = index == idx ? true : false;
})
this.$emit('open', this.current);
},
// 设置下拉菜单处于收起状态
close() {
this.$emit('close', this.current);
// 设置为收起状态,同时current归位,设置为空字符串
this.active = false;
this.current = 99999;
// 下拉内容的样式进行调整,不透明度设置为0
this.contentStyle = {
zIndex: -1,
opacity: 0
}
},
// 点击遮罩
maskClick() {
// 如果不允许点击遮罩,直接返回
if (!this.closeOnClickMask) return;
this.close();
},
// 外部手动设置某个菜单高亮
highlight(index = undefined) {
this.highlightIndex = index !== undefined ? index : 99999;
},
// 获取下拉菜单内容的高度
getContentHeight() {
// 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
// 才能让遮罩占满菜单一下,直到屏幕底部的高度
// this.$u.sys()为uView封装的获取设备信息的方法
let windowHeight = this.$u.sys().windowHeight;
this.$uGetRect('.u-dropdown__menu').then(res => {
// 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
// H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
// 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
// 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
this.contentHeight = windowHeight - res.bottom;
})
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-dropdown {
flex: 1;
width: 100%;
position: relative;
&__menu {
@include vue-flex;
position: relative;
z-index: 11;
height: 80rpx;
&__item {
flex: 1;
@include vue-flex;
justify-content: center;
align-items: center;
&__text {
font-size: 28rpx;
color: $u-content-color;
}
&__arrow {
margin-left: 6rpx;
transition: transform .3s;
align-items: center;
@include vue-flex;
&--rotate {
transform: rotate(180deg);
}
}
}
}
&__content {
position: absolute;
z-index: 8;
width: 100%;
left: 0px;
bottom: 0;
overflow: hidden;
&__mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, .3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
&__popup {
position: relative;
z-index: 10;
transition: all 0.3s;
transform: translate3D(0, -100%, 0);
overflow: hidden;
}
}
}
</style>
@@ -0,0 +1,193 @@
<template>
<view class="u-empty" v-if="show" :style="{
marginTop: marginTop + 'rpx'
}">
<u-icon
:name="src ? src : 'empty-' + mode"
:custom-style="iconStyle"
:label="text ? text : icons[mode]"
label-pos="bottom"
:label-color="color"
:label-size="fontSize"
:size="iconSize"
:color="iconColor"
margin-top="14"
></u-icon>
<view class="u-slot-wrap">
<slot name="bottom"></slot>
</view>
</view>
</template>
<script>
/**
* empty 内容为空
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
* @tutorial https://www.uviewui.com/components/empty.html
* @property {String} color 文字颜色(默认#c0c4cc)
* @property {String} text 文字提示(默认“无内容”)
* @property {String} src 自定义图标路径,如定义,mode参数会失效
* @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
* @property {String} mode 内置的图标,见官网说明(默认data)
* @property {String Number} img-width 图标的宽度,单位rpx(默认240)
* @property {String} img-height 图标的高度,单位rpx(默认auto)
* @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
* @property {Boolean} show 是否显示组件(默认true)
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
*/
export default {
name: "u-empty",
props: {
// 图标路径
src: {
type: String,
default: ''
},
// 提示文字
text: {
type: String,
default: ''
},
// 文字颜色
color: {
type: String,
default: '#c0c4cc'
},
// 图标的颜色
iconColor: {
type: String,
default: '#c0c4cc'
},
// 图标的大小
iconSize: {
type: [String, Number],
default: 120
},
// 文字大小,单位rpx
fontSize: {
type: [String, Number],
default: 26
},
// 选择预置的图标类型
mode: {
type: String,
default: 'data'
},
// 图标宽度,单位rpx
imgWidth: {
type: [String, Number],
default: 120
},
// 图标高度,单位rpx
imgHeight: {
type: [String, Number],
default: 'auto'
},
// 是否显示组件
show: {
type: Boolean,
default: true
},
// 组件距离上一个元素之间的距离
marginTop: {
type: [String, Number],
default: 0
},
iconStyle: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
icons: {
car: '购物车为空',
page: '页面不存在',
search: '没有搜索结果',
address: '没有收货地址',
wifi: '没有WiFi',
order: '订单为空',
coupon: '没有优惠券',
favor: '暂无收藏',
permission: '无权限',
history: '无历史记录',
news: '无新闻列表',
message: '消息列表为空',
list: '列表为空',
data: '数据为空'
},
// icons: [{
// icon: 'car',
// text: '购物车为空'
// },{
// icon: 'page',
// text: '页面不存在'
// },{
// icon: 'search',
// text: '没有搜索结果'
// },{
// icon: 'address',
// text: '没有收货地址'
// },{
// icon: 'wifi',
// text: '没有WiFi'
// },{
// icon: 'order',
// text: '订单为空'
// },{
// icon: 'coupon',
// text: '没有优惠券'
// },{
// icon: 'favor',
// text: '暂无收藏'
// },{
// icon: 'permission',
// text: '无权限'
// },{
// icon: 'history',
// text: '无历史记录'
// },{
// icon: 'news',
// text: '无新闻列表'
// },{
// icon: 'message',
// text: '消息列表为空'
// },{
// icon: 'list',
// text: '列表为空'
// },{
// icon: 'data',
// text: '数据为空'
// }],
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-empty {
@include vue-flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.u-image {
margin-bottom: 20rpx;
}
.u-slot-wrap {
@include vue-flex;
justify-content: center;
align-items: center;
margin-top: 20rpx;
}
</style>
@@ -0,0 +1,402 @@
<template>
<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
<view class="u-label" :class="[required ? 'u-required' : '']" :style="{
justifyContent: justifyContent,
flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
}">
<view class="u-icon-wrap" v-if="icon">
<u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
</view>
<slot name="icon"></slot>
<text class="u-label-text" :class="[$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
</view>
<view class="fild-body">
<view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="valueCom"
:placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
:focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
@tap="fieldClick" />
<input
v-else
:style="[fieldStyle]"
:type="type"
class="u-flex-1 u-field__input-wrap"
:value="valueCom"
:password="password || type === 'password'"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
:maxlength="inputMaxlength"
:focus="focus"
:confirmType="confirmType"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
@confirm="onConfirm"
@tap="fieldClick"
/>
</view>
<u-icon :size="clearSize" v-if="clearable && valueCom != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
<view class="u-button-wrap"><slot name="right" /></view>
<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
</view>
</view>
<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
paddingLeft: labelWidth + 'rpx'
}">{{ errorMessage }}</view>
</view>
</template>
<script>
/**
* field 输入框
* @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
* @tutorial https://www.uviewui.com/components/field.html
* @property {String} type 输入框的类型(默认text)
* @property {String} icon label左边的图标,限uView的图标名称
* @property {Object} icon-style 左边图标的样式,对象形式
* @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
* @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
* @property {String} label 输入框左边的文字提示
* @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
* @property {Number String} label-width label的宽度,单位rpx(默认130)
* @property {String} label-align label的文字对齐方式(默认left)
* @property {Object} field-style 自定义输入框的样式,对象形式
* @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
* @property {String} input-align 输入框内容对齐方式(默认left)
* @property {Boolean} border-bottom 是否显示field的下边框(默认true)
* @property {Boolean} border-top 是否显示field的上边框(默认false)
* @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
* @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
* @property {String} placeholder 输入框的提示文字
* @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点(默认false)
* @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
* @property {Boolean} disabled 是否不可输入(默认false)
* @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
* @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
* @event {Function} input 输入框内容发生变化时触发
* @event {Function} focus 输入框获得焦点时触发
* @event {Function} blur 输入框失去焦点时触发
* @event {Function} confirm 点击完成按钮时触发
* @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
* @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
* @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
*/
export default {
name:"u-field",
emits: ["update:modelValue", "input", "focus", "blur", "confirm", "right-icon-click", "click"],
props: {
value: [Number, String],
modelValue: [Number, String],
icon: String,
rightIcon: String,
arrowDirection: {
type: String,
default: 'right'
},
required: Boolean,
label: String,
password: Boolean,
clearable: {
type: Boolean,
default: true
},
// 左边标题的宽度单位rpx
labelWidth: {
type: [Number, String],
default: 130
},
// 对齐方式,left|center|right
labelAlign: {
type: String,
default: 'left'
},
inputAlign: {
type: String,
default: 'left'
},
iconColor: {
type: String,
default: '#606266'
},
autoHeight: {
type: Boolean,
default: true
},
errorMessage: {
type: [String, Boolean],
default: ''
},
placeholder: String,
placeholderStyle: String,
focus: Boolean,
fixed: Boolean,
type: {
type: String,
default: 'text'
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
confirmType: {
type: String,
default: 'done'
},
// lable的位置,可选为 left-左边,top-上边
labelPosition: {
type: String,
default: 'left'
},
// 输入框的自定义样式
fieldStyle: {
type: Object,
default() {
return {}
}
},
// 清除按钮的大小
clearSize: {
type: [Number, String],
default: 30
},
// lable左边的图标样式,对象形式
iconStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示上边框
borderTop: {
type: Boolean,
default: false
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: true
},
// 是否自动去除两端的空格
trim: {
type: Boolean,
default: true
}
},
data() {
return {
focused: false,
itemIndex: 0,
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
inputWrapStyle() {
let style = {};
style.textAlign = this.inputAlign;
// 判断lable的位置,如果是left的话,让input左边两边有间隙
if(this.labelPosition == 'left') {
style.margin = `0 8rpx`;
} else {
// 如果lable是top的,input的左边就没必要有间隙了
style.marginRight = `8rpx`;
}
return style;
},
rightIconStyle() {
let style = {};
let arrowDirectionObj = {
"top": "-90deg",
"bottom": "90deg",
"left": "180deg",
"right": "0deg",
};
let deg = arrowDirectionObj[this.arrowDirection] || "0deg";
style.transform = 'rotate('+deg+')';
return style;
},
labelStyle() {
let style = {};
if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
if(this.labelAlign == 'center') style.justifyContent = 'center';
if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
return style;
},
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
justifyContent() {
if(this.labelAlign == 'left') return 'flex-start';
if(this.labelAlign == 'center') return 'center';
if(this.labelAlign == 'right') return 'flex-end';
},
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength)
},
// label的位置
fieldInnerStyle() {
let style = {};
if(this.labelPosition == 'left') {
style.flexDirection = 'row';
} else {
style.flexDirection = 'column';
}
return style;
}
},
methods: {
onInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if(this.trim) value = this.$u.trim(value);
this.$emit('input', value);
this.$emit("update:modelValue", value);
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
setTimeout(() => {
this.focused = false;
}, 100)
this.$emit('blur', event);
},
onConfirm(e) {
this.$emit('confirm', e.detail.value);
},
onClear(event) {
this.$emit('input', '');
this.$emit("update:modelValue", '');
},
rightIconClick() {
this.$emit('right-icon-click');
this.$emit('click');
},
fieldClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-field {
font-size: 28rpx;
padding: 20rpx 28rpx;
text-align: left;
position: relative;
color: $u-main-color;
}
.u-field-inner {
@include vue-flex;
align-items: center;
}
.u-textarea-inner {
align-items: flex-start;
}
.u-textarea-class {
min-height: 96rpx;
width: auto;
font-size: 28rpx;
}
.fild-body {
@include vue-flex;
flex: 1;
align-items: center;
}
.u-arror-right {
margin-left: 8rpx;
}
.u-label-text {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
.u-label-left-gap {
margin-left: 6rpx;
}
.u-label-postion-top {
flex-direction: column;
align-items: flex-start;
}
.u-label {
width: 130rpx;
flex: 1 1 130rpx;
text-align: left;
position: relative;
@include vue-flex;
align-items: center;
}
.u-required::before {
content: '*';
position: absolute;
left: -16rpx;
font-size: 14px;
color: $u-type-error;
height: 9px;
line-height: 1;
}
.u-field__input-wrap {
position: relative;
overflow: hidden;
font-size: 28rpx;
height: 48rpx;
flex: 1;
width: auto;
}
.u-clear-icon {
@include vue-flex;
align-items: center;
}
.u-error-message {
color: $u-type-error;
font-size: 26rpx;
text-align: left;
}
.placeholder-style {
color: rgb(150, 151, 153);
}
.u-input-class {
font-size: 28rpx;
}
.u-button-wrap {
margin-left: 8rpx;
}
</style>

Some files were not shown because too many files have changed in this diff Show More