commit
00b864b5ef
184 changed files with 39326 additions and 0 deletions
Binary file not shown.
@ -0,0 +1,5 @@ |
|||
/node_modules/ |
|||
/.hbuilderx/ |
|||
/unpackage/ |
|||
|
|||
.idea |
|||
@ -0,0 +1,123 @@ |
|||
<script> |
|||
export default { |
|||
onLaunch: function(options) { |
|||
|
|||
}, |
|||
onShow: function() { |
|||
console.log('App Show') |
|||
}, |
|||
onHide: function() { |
|||
console.log('App Hide') |
|||
}, |
|||
methods: { |
|||
|
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
/*每个页面公共css */ |
|||
</style> |
|||
<style lang="scss"> |
|||
|
|||
@import "./global"; |
|||
@import "uview-ui/index.scss"; |
|||
.timeReduce{ |
|||
position: absolute; |
|||
right: 30rpx; |
|||
top: 20rpx; |
|||
} |
|||
/deep/ .u-empty { |
|||
margin-top: 50% !important; |
|||
} |
|||
|
|||
.tCenter { |
|||
text-align: center |
|||
} |
|||
|
|||
html { |
|||
// margin-top: 80rpx; |
|||
} |
|||
|
|||
/*每个页面公共css */ |
|||
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */ |
|||
::v-deep .u-input__input { |
|||
color: #9299AD !important; |
|||
} |
|||
.f13{ |
|||
font-size: 26rpx; |
|||
} |
|||
.f15{ |
|||
font-size: 30rpx; |
|||
} |
|||
// 向上距离 |
|||
.m20 { |
|||
margin-top: 20rpx; |
|||
} |
|||
|
|||
.relative { |
|||
position: relative; |
|||
} |
|||
|
|||
.borBottom { |
|||
border-bottom: 2rpx solid #484848; |
|||
; |
|||
} |
|||
|
|||
.content { |
|||
// width: 750rpx; |
|||
// background: #fff; |
|||
// min-height: 100vh; |
|||
// position: relative; |
|||
// z-index: 10; |
|||
// padding: 0 32rpx; |
|||
// padding-bottom: 200rpx; |
|||
} |
|||
|
|||
.container { |
|||
padding: 0 32rpx |
|||
} |
|||
|
|||
.flex { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.flex2 { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
.btnColor { |
|||
background: linear-gradient(270.87deg, #FFD464 0.82%, #FF7575 102.54%) !important; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
.plain { |
|||
border: 1px solid #FFFFFF; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
.home_item_top { |
|||
margin-top: 40rpx; |
|||
} |
|||
|
|||
.title_con image { |
|||
width: 36rpx; |
|||
height: 36rpx; |
|||
margin-left: 16rpx; |
|||
} |
|||
|
|||
.disabled { |
|||
background: #464648 !important; |
|||
color: #999999 !important; |
|||
} |
|||
|
|||
.FF9F32 { |
|||
color: #FF9F32; |
|||
} |
|||
</style> |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,14 @@ |
|||
let isTest = false; |
|||
|
|||
import constant from "../utils/constant"; |
|||
// const constant = isTest ? {
|
|||
// baseUrl: `http://kaka-carddealer-admin.weirui0755.com/prod-api`,
|
|||
// }:{
|
|||
// baseUrl: '/prod-api',
|
|||
// }
|
|||
// console.log(constant.address)
|
|||
export default { |
|||
constant: { |
|||
baseUrl: constant.baseUrl, |
|||
}, |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
export default { |
|||
'401': '认证失败,无法访问系统资源', |
|||
'403': '当前操作没有权限', |
|||
'404': '访问资源不存在', |
|||
'default': '系统未知错误,请反馈给管理员' |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
import Vue from 'vue' |
|||
import axios from 'axios' |
|||
import "./uniapp-axios-adapter" |
|||
import {withRepeatGuard} from "./with-repeat-guard"; |
|||
import {tansParams} from "../utils/atom"; |
|||
import {withAuthentication} from "./with-authentication"; |
|||
import Constant from "../utils/constant"; |
|||
|
|||
const service = axios.create({ |
|||
withCredentials: false, //表示跨域请求时是否需要使用凭证
|
|||
crossDomain: true, |
|||
baseURL: Constant.baseUrl, |
|||
timeout: 160000 |
|||
}) |
|||
|
|||
// withAuthentication(service)
|
|||
withRepeatGuard(service) |
|||
|
|||
/** |
|||
* 请求拦截 |
|||
* |
|||
* 除 Axios 默认配置, 还接受以下配置 |
|||
* $withCredentials: 是否携带 token, 默认为 true |
|||
* $withLoading: 是否显示 loading, 默认 true |
|||
* $withRepeatGuard: 是否开启重复请求拦截, 默认 true |
|||
*/ |
|||
service.interceptors.request.use( |
|||
config => { |
|||
uni.showLoading({ |
|||
title: 'loading', |
|||
mask: true |
|||
}) |
|||
|
|||
// get请求映射params参数
|
|||
if (config.method === 'get' && config.params) { |
|||
let url = config.url + '?' + tansParams(config.params); |
|||
url = url.slice(0, -1); |
|||
config.params = {}; |
|||
config.url = url; |
|||
} |
|||
|
|||
return config; |
|||
}, |
|||
); |
|||
|
|||
service.interceptors.response.use( |
|||
response => { |
|||
uni.hideLoading(); |
|||
return response; |
|||
}, |
|||
error => { |
|||
uni.hideLoading(); |
|||
return Promise.reject(error); |
|||
}, |
|||
) |
|||
|
|||
export default service |
|||
@ -0,0 +1,27 @@ |
|||
export default { |
|||
set (key, value) { |
|||
if (key != null && value != null) { |
|||
uni.setStorageSync(key, value) |
|||
} |
|||
}, |
|||
get (key) { |
|||
if (key == null) { |
|||
return null |
|||
} |
|||
return uni.getStorageSync(key) |
|||
}, |
|||
setJSON (key, jsonValue) { |
|||
if (jsonValue != null) { |
|||
this.set(key, JSON.stringify(jsonValue)) |
|||
} |
|||
}, |
|||
getJSON (key) { |
|||
const value = this.get(key) |
|||
if (value != null && value !== '') { |
|||
return JSON.parse(value) |
|||
} |
|||
}, |
|||
remove (key) { |
|||
return uni.removeStorageSync(key); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import axios from "axios"; |
|||
import settle from "axios/lib/core/settle"; |
|||
import buildURL from "axios/lib/helpers/buildURL"; |
|||
|
|||
axios.defaults.adapter = function(config) { |
|||
return new Promise((resolve, reject) => { |
|||
const url = config.baseURL + buildURL(config.url, config.params, config.paramsSerializer) |
|||
uni.request({ |
|||
method: config.method.toUpperCase(), |
|||
url: url, |
|||
header: config.headers, |
|||
data: config.data, |
|||
dataType: config.dataType, |
|||
responseType: config.responseType, |
|||
sslVerify: config.sslVerify, |
|||
complete: function complete(response) { |
|||
response = { |
|||
data: response.data, |
|||
status: response.statusCode, |
|||
errMsg: response.errMsg, |
|||
header: response.header, |
|||
config: config |
|||
}; |
|||
|
|||
settle(resolve, reject, response); |
|||
}, |
|||
}) |
|||
}) |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
import storage from "./storage"; |
|||
import errorCode from "./errorCode"; |
|||
import store from "../store"; |
|||
|
|||
/** |
|||
* 处理凭证信息 |
|||
* @param axiosInstance {AxiosInstance} |
|||
*/ |
|||
export const withAuthentication = (axiosInstance) => { |
|||
axiosInstance.interceptors.request.use((config) => { |
|||
const $withCredentials = config.$withCredentials !== false; |
|||
|
|||
if ($withCredentials) { |
|||
const token = storage.get('credential') |
|||
if (token) { |
|||
config.headers.Authorization = `Bearer ${token}` |
|||
} |
|||
} |
|||
return config |
|||
}) |
|||
|
|||
axiosInstance.interceptors.response.use(res => { |
|||
// 未设置状态码则默认成功状态
|
|||
const code = res.data.code || 200; |
|||
// 获取错误信息
|
|||
const msg = errorCode[code] || res.data.msg || errorCode['default'] |
|||
// console.log(res)
|
|||
if (code === 401) { |
|||
// if (!isRelogin.show) {
|
|||
// isRelogin.show = true;
|
|||
// MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
|||
// isRelogin.show = false;
|
|||
|
|||
// }).catch(() => {
|
|||
// isRelogin.show = false;
|
|||
// });
|
|||
// }
|
|||
store.dispatch('LogOut') |
|||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。') |
|||
} else if (code === 500) { |
|||
uni.showToast({ |
|||
title: msg, |
|||
icon: 'none' |
|||
}) |
|||
return Promise.reject(new Error(msg)) |
|||
} else if (code === 601) { |
|||
uni.showToast({ |
|||
title: msg, |
|||
icon: 'none' |
|||
}) |
|||
return Promise.reject('error') |
|||
} else if (code !== 200) { |
|||
uni.showToast({ |
|||
title: msg, |
|||
icon: 'none' |
|||
}) |
|||
return Promise.reject('error') |
|||
} else { |
|||
return res.data |
|||
} |
|||
}, |
|||
error => { |
|||
console.log('err' + error) |
|||
let { |
|||
message |
|||
} = error; |
|||
if (message == "Network Error") { |
|||
message = "后端接口连接异常"; |
|||
} else if (message.includes("timeout")) { |
|||
message = "系统接口请求超时"; |
|||
} else if (message.includes("Request failed with status code")) { |
|||
message = "系统接口" + message.substr(message.length - 3) + "异常"; |
|||
} |
|||
uni.showToast({ |
|||
title: message, |
|||
icon: 'none', |
|||
duration: 5 * 1000 |
|||
}) |
|||
return Promise.reject(error) |
|||
} |
|||
) |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
import storage from "./storage"; |
|||
|
|||
/** |
|||
* 避免重复处理 |
|||
* @param axiosInstance {AxiosInstance} |
|||
*/ |
|||
export const withRepeatGuard = (axiosInstance) => { |
|||
axiosInstance.interceptors.request.use((config) => { |
|||
// 如果该请求允许重复
|
|||
if (config.$repeatable) { |
|||
return config |
|||
} |
|||
// 如果该请求不允许重复, 并且是 post 或者 put 请求, 则判断是否已经有相同的请求在处理
|
|||
if (config.method === 'post' || config.method === 'put') { |
|||
const requestObj = { |
|||
url: config.url, |
|||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, |
|||
time: new Date().getTime() |
|||
} |
|||
const sessionObj = storage.getJSON('sessionObj') |
|||
if (sessionObj === undefined || sessionObj === null || sessionObj === '') { |
|||
storage.setJSON('sessionObj', requestObj) |
|||
} else { |
|||
const s_url = sessionObj.url; // 请求地址
|
|||
const s_data = sessionObj.data; // 请求数据
|
|||
const s_time = sessionObj.time; // 请求时间
|
|||
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
|
|||
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { |
|||
const message = '数据正在处理,请勿重复提交'; |
|||
console.warn(`[${s_url}]: ` + message) |
|||
return Promise.reject(new Error(message)) |
|||
} else { |
|||
storage.setJSON('sessionObj', requestObj) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return config; |
|||
}) |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
<template> |
|||
<view> |
|||
<view class="btn" :style="{'--bgColor':bgColor,'width':btnWidth}" @click="$u.throttle(emitClick, 900)"> |
|||
<text>{{ btnTitle }}</text> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name:"Btn", |
|||
props: { |
|||
btnTitle: { |
|||
type: String, |
|||
default () { |
|||
return '确认' |
|||
} |
|||
}, |
|||
btnWidth: { |
|||
type: String, |
|||
default () { |
|||
return '' |
|||
} |
|||
}, |
|||
bgColor: { |
|||
type: String, |
|||
default () { |
|||
return '#006BFF' |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
}; |
|||
}, |
|||
methods: { |
|||
emitClick() { |
|||
this.$emit('emitClick') |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.btn { |
|||
margin: 0 auto; |
|||
height: 92rpx; |
|||
line-height: 92rpx; |
|||
border-radius: 200rpx; |
|||
text-align: center; |
|||
letter-spacing: 3px; |
|||
color: #FFFFFF; |
|||
background: #006BFF; |
|||
// position:relative; |
|||
// left: 50%; |
|||
// transform: translateX(-50%); |
|||
// bottom:-150rpx; |
|||
// bottom:-200rpx; |
|||
margin-top: 80rpx; |
|||
.btnTttle { |
|||
font-size: 36rpx; |
|||
line-height: 32rpx; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,141 @@ |
|||
<template> |
|||
<view class="cardHeader"> |
|||
<view class="flex"> |
|||
<image src="../static/images/logo.png" mode="" class="logo"></image> |
|||
<view class="title">IPPSWAP</view> |
|||
</view> |
|||
<view class="lang" @click="showLang"> |
|||
<text class="lang-text"> |
|||
{{langTrue=='English'?'EN':'CN'}} |
|||
</text> |
|||
</view> |
|||
<view class="select-con" v-show="isShowLang"> |
|||
<view class="item" v-for="(item, index) in languageData" :key="index" @click="changeLanguage(item,index)" |
|||
:style="{color:index==current?'#fff':''}"> |
|||
{{ item.title }} |
|||
</view> |
|||
|
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import languageData from '@/utils/language/config' |
|||
export default { |
|||
name: 'Header', |
|||
props: { |
|||
title: { |
|||
type: String | Number, |
|||
default () { |
|||
return '' |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
current: uni.getStorageSync('current') || 0, |
|||
isShowLang: false, |
|||
i: 1, |
|||
languageData, |
|||
language: languageData[0].language || 'zh_TW', |
|||
langTrue: languageData[0].title || '繁体中文', |
|||
} |
|||
}, |
|||
mounted() { |
|||
if (uni.getStorageSync('langTrue')) { |
|||
this.langTrue = uni.getStorageSync('langTrue') |
|||
this.language = this._i18n.locale |
|||
} |
|||
}, |
|||
methods: { |
|||
changeLanguage(e, i) { |
|||
const index = i || 0 |
|||
this.current = i |
|||
const current = this.languageData[index] |
|||
this._i18n.locale = current.language |
|||
this.$store.commit('setLanguage', current.language) |
|||
this.langTrue = current.title |
|||
uni.setStorageSync('langTrue', current.title) |
|||
uni.setStorageSync('current', this.current) |
|||
this.isShowLang = false |
|||
this.i++ |
|||
}, |
|||
showLang() { |
|||
this.i++ |
|||
if (this.i % 2 == 0) { |
|||
this.isShowLang = true |
|||
} else { |
|||
this.isShowLang = false |
|||
} |
|||
}, |
|||
} |
|||
|
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.select-con { |
|||
position: absolute; |
|||
right: 32rpx; |
|||
top: 105rpx; |
|||
z-index: 10; |
|||
background: url('../static/images/Union.png') no-repeat; |
|||
background-size: cover; |
|||
color: rgba(167, 181, 229, 1); |
|||
width: 176rpx; |
|||
height: 178rpx; |
|||
padding: 40rpx 30rpx; |
|||
|
|||
.item:last-child { |
|||
margin-top: 30rpx; |
|||
} |
|||
} |
|||
|
|||
.cardHeader { |
|||
position: relative; |
|||
box-sizing: border-box; |
|||
height: 106rpx; |
|||
padding: 20rpx 32rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
background: radial-gradient(104.53% 5436.39% at -4.53% 16.35%, #7F97EC 3.77%, #8D6CEA 11.22%, #5944D7 24.63%, #2D2EA8 36.47%, #182390 63.92%, #16228E 100%) |
|||
/* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */ |
|||
; |
|||
|
|||
.logo { |
|||
width: 62rpx; |
|||
height: 58rpx; |
|||
} |
|||
|
|||
.title { |
|||
height: 100%; |
|||
font-size: 40rpx; |
|||
display: table; |
|||
color: #fff; |
|||
margin-left: 24rpx; |
|||
} |
|||
|
|||
.lang { |
|||
text-align: center; |
|||
font-size: 28rpx; |
|||
font-weight: 700; |
|||
width: 66rpx; |
|||
height: 66rpx; |
|||
line-height: 60rpx; |
|||
border: 2rpx solid transparent; |
|||
border-radius: 12rpx; |
|||
background-clip: padding-box, border-box; |
|||
background-origin: padding-box, border-box; |
|||
background-image: linear-gradient(180deg, #222794 25%, #592A86 77.78%), linear-gradient(to right, rgba(230, 232, 156, 1), rgba(247, 135, 171, 1)); |
|||
|
|||
.lang-text { |
|||
background: linear-gradient(to bottom, rgba(230, 232, 156, 1), rgba(247, 135, 171, 1)); |
|||
-webkit-background-clip: text; |
|||
color: transparent; |
|||
} |
|||
} |
|||
|
|||
} |
|||
</style> |
|||
@ -0,0 +1,127 @@ |
|||
|
|||
|
|||
$deep-color: #333333; |
|||
$gray-color: #9299AD; |
|||
$primary-color: #5C5BEE; |
|||
$light-primary-color: #E8E8FF; |
|||
$text-red:#D91C1C; |
|||
$text-org:#EE991A; |
|||
$bg-light-org:#FFF1DB; |
|||
$text-green:#39B872; |
|||
$bg-white:#fff; |
|||
$text-white:#fff; |
|||
|
|||
:export { |
|||
deepColor: $deep-color; |
|||
grayColor: $gray-color; |
|||
primaryColor: $primary-color; |
|||
lightPrimaryColor: $light-primary-color; |
|||
} |
|||
|
|||
uni-app { |
|||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, |
|||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; |
|||
} |
|||
.text-blue{ |
|||
color: #34ACF0; |
|||
} |
|||
.text-red{ |
|||
color: #D91C1C; |
|||
} |
|||
.text-org{ |
|||
color: #EE991A; |
|||
} |
|||
.bg-light-org{ |
|||
background: #FFF1DB; |
|||
} |
|||
.text-green{ |
|||
color: #39B872; |
|||
} |
|||
.bg-white{ |
|||
background-color: #fff; |
|||
} |
|||
.text-white{ |
|||
color: #fff; |
|||
} |
|||
.text-deep { |
|||
color: $deep-color; |
|||
} |
|||
.bg-deep { |
|||
background-color: $deep-color; |
|||
} |
|||
|
|||
// 灰色 |
|||
.text-gray { |
|||
color: $gray-color; |
|||
} |
|||
.bg-gray { |
|||
background-color: $gray-color; |
|||
} |
|||
|
|||
// 主题深色 |
|||
.text-deep-primary { |
|||
// color: #5C5BEE !important; |
|||
color: $primary-color; |
|||
} |
|||
.bg-deep-primary { |
|||
background-color: $primary-color; |
|||
} |
|||
|
|||
// 主题浅色 |
|||
.text-light-primary { |
|||
color: $light-primary-color; |
|||
} |
|||
.bg-light-primary { |
|||
background-color: $light-primary-color; |
|||
} |
|||
|
|||
// 浅主题色按钮 |
|||
.btn-light-primary { |
|||
background-color: $light-primary-color; |
|||
color: $primary-color; |
|||
border: none; |
|||
border-radius: 6px; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease-in-out; |
|||
padding-left: 0; |
|||
padding-right: 0; |
|||
|
|||
&:after { |
|||
border: none; |
|||
} |
|||
} |
|||
|
|||
// 深主题色按钮 |
|||
.btn-deep-primary { |
|||
background-color: $primary-color; |
|||
color: #fff; |
|||
border: none; |
|||
border-radius: 6px; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease-in-out; |
|||
padding-left: 0; |
|||
padding-right: 0; |
|||
|
|||
&:after { |
|||
border: none; |
|||
} |
|||
} |
|||
|
|||
.a-line-overflow { |
|||
white-space: nowrap;/*强制在一行显示*/ |
|||
overflow: hidden; |
|||
text-overflow: ellipsis;/*溢出显示省略号*/ |
|||
} |
|||
|
|||
.page-content { |
|||
width: 750rpx; |
|||
position: relative; |
|||
padding: 0 32rpx; |
|||
} |
|||
|
|||
.uni-input-input { |
|||
// 修改输入框文字的颜色 |
|||
color: #333333; |
|||
} |
|||
@ -0,0 +1,168 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" |
|||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"> |
|||
|
|||
<script> |
|||
window.onload = function() { |
|||
document.addEventListener('touchstart', function(event) { |
|||
if (event.touches.length > 1) { |
|||
event.preventDefault(); |
|||
} |
|||
}); |
|||
document.addEventListener('gesturestart', function(event) { |
|||
event.preventDefault(); |
|||
}); |
|||
}; |
|||
</script> |
|||
<title></title> |
|||
<style> |
|||
body { |
|||
background: #090E11; |
|||
height: 100%; |
|||
} |
|||
|
|||
#app { |
|||
height: 100%; |
|||
} |
|||
|
|||
.lds-roller { |
|||
display: inline-block; |
|||
position: relative; |
|||
width: 80px; |
|||
height: 80px; |
|||
left: 50%; |
|||
top: 50%; |
|||
box-sizing: border-box; |
|||
transform: translate(-50%, -50%); |
|||
} |
|||
|
|||
.lds-roller div { |
|||
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; |
|||
transform-origin: 40px 40px; |
|||
} |
|||
|
|||
.lds-roller div:after { |
|||
content: " "; |
|||
display: block; |
|||
position: absolute; |
|||
width: 7px; |
|||
height: 7px; |
|||
border-radius: 50%; |
|||
background: #fff; |
|||
margin: -4px 0 0 -4px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(1) { |
|||
animation-delay: -0.036s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(1):after { |
|||
top: 63px; |
|||
left: 63px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(2) { |
|||
animation-delay: -0.072s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(2):after { |
|||
top: 68px; |
|||
left: 56px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(3) { |
|||
animation-delay: -0.108s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(3):after { |
|||
top: 71px; |
|||
left: 48px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(4) { |
|||
animation-delay: -0.144s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(4):after { |
|||
top: 72px; |
|||
left: 40px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(5) { |
|||
animation-delay: -0.18s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(5):after { |
|||
top: 71px; |
|||
left: 32px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(6) { |
|||
animation-delay: -0.216s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(6):after { |
|||
top: 68px; |
|||
left: 24px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(7) { |
|||
animation-delay: -0.252s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(7):after { |
|||
top: 63px; |
|||
left: 17px; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(8) { |
|||
animation-delay: -0.288s; |
|||
} |
|||
|
|||
.lds-roller div:nth-child(8):after { |
|||
top: 56px; |
|||
left: 12px; |
|||
} |
|||
|
|||
@keyframes lds-roller { |
|||
0% { |
|||
transform: rotate(0deg); |
|||
} |
|||
|
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
</style> |
|||
<!--preload-links--> |
|||
<!--app-context--> |
|||
|
|||
|
|||
<!-- <link rel="icon" href="./static/images/logo2.png" /> --> |
|||
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script> |
|||
<script> |
|||
// VConsole will be exported to `window.VConsole` by default. |
|||
var vConsole = new window.VConsole(); |
|||
</script> --> |
|||
|
|||
</head> |
|||
<body> |
|||
<div id="app"> |
|||
<div class="lds-roller"> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
</div> |
|||
<!--app-html--> |
|||
</div> |
|||
<!-- <script type="module" src="/main.js"></script> --> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,51 @@ |
|||
import App from './App' |
|||
import uView from "uview-ui"; |
|||
import utils from './utils/index.js' |
|||
import store from './store' |
|||
import './utils/noScale.js' |
|||
import VueI18n from 'vue-i18n' |
|||
// 多国语言
|
|||
import EN from './utils/language/en_US.js' |
|||
import ZH from './utils/language/zh_TW.js' |
|||
|
|||
// #ifndef VUE3
|
|||
import Vue from 'vue' |
|||
Vue.use(uView); |
|||
// 自定义顶部部导航栏
|
|||
import tabBar from 'component/header.vue' |
|||
Vue.component('tab-bar', tabBar) |
|||
import VueClipboard from 'vue-clipboard2' |
|||
Vue.use(VueClipboard) |
|||
// 全局边框颜色变量
|
|||
Vue.prototype.borderColor = '#E3E2EC' |
|||
Vue.use(VueI18n); |
|||
const i18n = new VueI18n({ |
|||
locale: store.state.language, // 默认选择的语言
|
|||
messages: { |
|||
'en_US': EN, |
|||
'zh_TW': ZH, |
|||
} |
|||
}) |
|||
Vue.prototype.$utils = utils |
|||
Vue.prototype.$store = store |
|||
Vue.config.productionTip = false |
|||
App.mpType = 'app' |
|||
const app = new Vue({ |
|||
i18n, |
|||
store, |
|||
...App |
|||
}) |
|||
app.$mount() |
|||
// #endif
|
|||
|
|||
// #ifdef VUE3
|
|||
import { |
|||
createSSRApp |
|||
} from 'vue' |
|||
export function createApp() { |
|||
const app = createSSRApp(App) |
|||
return { |
|||
app |
|||
} |
|||
} |
|||
// #endif
|
|||
@ -0,0 +1,96 @@ |
|||
{ |
|||
"name" : "kakapay", |
|||
"appid" : "__UNI__0088A51", |
|||
"description" : "", |
|||
"versionName" : "1.0.0", |
|||
"versionCode" : "100", |
|||
"transformPx" : false, |
|||
/* 5+App特有相关 */ |
|||
"app-plus" : { |
|||
"usingComponents" : true, |
|||
"nvueStyleCompiler" : "uni-app", |
|||
"compilerVersion" : 3, |
|||
"splashscreen" : { |
|||
"alwaysShowBeforeRender" : true, |
|||
"waiting" : true, |
|||
"autoclose" : true, |
|||
"delay" : 0 |
|||
}, |
|||
/* 模块配置 */ |
|||
"modules" : {}, |
|||
/* 应用发布信息 */ |
|||
"distribute" : { |
|||
/* android打包配置 */ |
|||
"android" : { |
|||
"permissions" : [ |
|||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", |
|||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>", |
|||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", |
|||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", |
|||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.CAMERA\"/>", |
|||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
|||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", |
|||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", |
|||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
|||
"<uses-feature android:name=\"android.hardware.camera\"/>", |
|||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" |
|||
] |
|||
}, |
|||
/* ios打包配置 */ |
|||
"ios" : {}, |
|||
/* SDK配置 */ |
|||
"sdkConfigs" : {} |
|||
} |
|||
}, |
|||
/* 快应用特有相关 */ |
|||
"quickapp" : {}, |
|||
/* 小程序特有相关 */ |
|||
"mp-weixin" : { |
|||
"appid" : "", |
|||
"setting" : { |
|||
"urlCheck" : false |
|||
}, |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-alipay" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-baidu" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-toutiao" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"uniStatistics" : { |
|||
"enable" : false |
|||
}, |
|||
"vueVersion" : "2", |
|||
"h5" : { |
|||
"publicPath" : "/", |
|||
"devServer" : { |
|||
"disableHostCheck" : true, |
|||
"proxy" : { |
|||
"/htmlApi" : { |
|||
"ws" : false, |
|||
"target" : "http://kaka-carddealer-admin.weirui0755.com/prod-api", |
|||
"changeOrigin" : true, |
|||
"secure" : false, |
|||
"pathRewrite" : { |
|||
"^/htmlApi" : "" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"title" : "kakapay", |
|||
"optimization" : { |
|||
"treeShaking" : { |
|||
"enable" : true |
|||
} |
|||
}, |
|||
"template" : "index.html" |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,12 @@ |
|||
{ |
|||
"dependencies": { |
|||
"ant-design-vue": "^3.2.13", |
|||
"axios": "^1.1.3", |
|||
"crypto-js": "^4.1.1", |
|||
"decimal.js": "^10.4.3", |
|||
"swiper": "^8.4.5", |
|||
"vue-clipboard2": "^0.3.3", |
|||
"vue-i18n": "^9.2.2", |
|||
"web3": "^1.8.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
{ |
|||
"easycom": { |
|||
"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue" |
|||
}, |
|||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages |
|||
{ |
|||
"path": "pages/index/index", |
|||
"style": { |
|||
"navigationBarTitleText": "" |
|||
// "enablePullDownRefresh": true |
|||
} |
|||
} |
|||
], |
|||
"globalStyle": { |
|||
// "mp-alipay": { |
|||
// /* 支付宝小程序特有相关 */ |
|||
// "transparentTitle": "always", |
|||
// "allowsBounceVertical": "NO" |
|||
// }, |
|||
"navigationBarBackgroundColor": "#fff", |
|||
"navigationBarTitleText": "码商", |
|||
"navigationStyle": "custom", |
|||
"navigationBarTextStyle": "black", |
|||
"rpxCalcMaxDeviceWidth": 960, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960 |
|||
"rpxCalcBaseDeviceWidth": 375, // 设备实际宽度超出 rpx 计算所支持的最大宽度时,rpx计算所采用的固定屏幕宽度,单位 px,默认值为 375 |
|||
"app-plus": { |
|||
"background": "#293154", |
|||
"titleNView": false //去掉app+h5顶部导航 |
|||
} |
|||
}, |
|||
|
|||
"tabBar": { |
|||
// "color": "#646D7E", |
|||
// "selectedColor": "#4A8CF5", |
|||
// "list": [ |
|||
// { |
|||
// "pagePath": "pages/home/home", |
|||
// "text": "首页", |
|||
// "iconPath": "/static/tongyonh/icon-home-28Selected.png", |
|||
// "selectedIconPath": "/static/tongyonh/icon-home-28Selected2.png" |
|||
// }, |
|||
// { |
|||
// "pagePath": "pages/mine/mine", |
|||
// "text": "我的", |
|||
// "iconPath": "/static/tongyonh/icon-home-28Selected.png", |
|||
// "selectedIconPath": "/static/tongyonh/icon-home-28Selected2.png" |
|||
// } |
|||
// ] |
|||
} |
|||
} |
|||
@ -0,0 +1,217 @@ |
|||
<template> |
|||
<view class="content"> |
|||
<tab-bar /> |
|||
<view class="container"> |
|||
|
|||
<view class="top"> |
|||
<view class="title f44 text-primary"> |
|||
{{ i18n.Home }}<br /><br /> |
|||
</view> |
|||
<view class="text-deep-primary"> |
|||
{{ i18n.cx }}<br /><br /><br /> |
|||
{{ i18n.zw }}<br /><br /><br /> |
|||
{{ i18n.yuan }}<br /><br /><br /> |
|||
{{ i18n.we }}<br /><br /><br /> |
|||
{{ i18n.ji }} |
|||
</view> |
|||
</view> |
|||
<view class="bottom"> |
|||
<view class="item"> |
|||
<view class="flex2 label-title"> |
|||
<image src="@/static/images/com_icon_srank.452821d11.png" mode=""></image> |
|||
<text class="text-primary f36">{{ i18n.contract }}</text> |
|||
</view> |
|||
<view class="m48"> |
|||
<u-input v-model="form.contract" placeholder="" :clearable="false" :disabled="true" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="item m48"> |
|||
<view class="flex2 label-title"> |
|||
<image src="@/static/images/invite_table_title.e3146df81.png" mode=""></image> |
|||
<text class="text-primary f36">{{ i18n.address }}</text> |
|||
</view> |
|||
<view class="m48"> |
|||
<u-input v-model="form.supAddress" placeholder="" :clearable="false" :disabled="true" /> |
|||
</view> |
|||
</view> |
|||
<general-button @emitClick="submit" :btnTitle="i18n.confirm"></general-button> |
|||
|
|||
<view class="item m48"> |
|||
<view class="flex2 label-title"> |
|||
<image src="@/static/images/com_icon_yaoqing.c8e1575f1.png" mode=""></image> |
|||
<text class="text-primary f36">{{ i18n.link }}</text> |
|||
</view> |
|||
<view class="m48 text-primary" style="word-break: break-all;"> |
|||
https://ippswap.com/community?inviteCode={{form.address}} |
|||
</view> |
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import GeneralButton from '@/component/GeneralButton' |
|||
import { |
|||
web3x |
|||
} from "@/utils/web3x/web3x.js" |
|||
export default { |
|||
components: { |
|||
GeneralButton, |
|||
}, |
|||
data() { |
|||
return { |
|||
form: { |
|||
address: '', |
|||
contract: '0x6b175474e89094c44da98b954eedeac495271d0f', |
|||
supAddress: '', |
|||
}, |
|||
} |
|||
}, |
|||
computed: { |
|||
i18n() { |
|||
return this.$t("tabBar"); |
|||
}, |
|||
}, |
|||
onShow() { |
|||
|
|||
}, |
|||
onPullDownRefresh() { |
|||
|
|||
}, |
|||
onLoad(val) { |
|||
if (val) { |
|||
this.form.supAddress = val.inviteCode |
|||
} |
|||
this.init() |
|||
}, |
|||
onReachBottom() { |
|||
|
|||
}, |
|||
// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕 |
|||
onReady() {}, |
|||
methods: { |
|||
init() { |
|||
web3x.connectViaInPage() |
|||
.then(res => { |
|||
this.form.address = web3x.selectedAddress |
|||
// 当前钱包地址 |
|||
console.log("当前钱包地址", web3x.selectedAddress); |
|||
}) |
|||
}, |
|||
submit() { |
|||
if (!this.form.supAddress) { |
|||
uni.$u.toast(this.$t("tabBar").empty) |
|||
return |
|||
} |
|||
web3x.connectViaInPage() |
|||
.then(async res => { |
|||
uni.showLoading({ |
|||
title: '', |
|||
mask: true |
|||
}) |
|||
// 当前钱包地址 |
|||
console.log("当前钱包地址", web3x.selectedAddress); |
|||
// 授权当前钱包的 USDT 给 IPPT (参数为 IPPT 合约地址) |
|||
await web3x.usdt.approveMAX("0x622d7b79a904e00e5fcab06396ff009e441f0186") |
|||
uni.hideLoading() |
|||
// 调用IPPT 合约绑定上级关系 |
|||
// 0xFb4FC7Ddb8c4aa6b944703CE1e89D2B9Aa67a400: 上级地址 |
|||
// web3x.selectedAddress: 当前钱包地址 |
|||
await web3x.ippt.$creatCode(this.form.supAddress, web3x.selectedAddress) |
|||
}) |
|||
}, |
|||
go(val) { |
|||
uni.navigateTo({ |
|||
url: val, |
|||
}) |
|||
}, |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.m48 { |
|||
margin-top: 48rpx; |
|||
} |
|||
|
|||
::v-deep .uni-input-input { |
|||
color: #fff; |
|||
min-height: 88rpx !important; |
|||
} |
|||
|
|||
::v-deep .u-input__input { |
|||
color: #fff; |
|||
min-height: 88rpx !important; |
|||
} |
|||
|
|||
::v-deep .u-input { |
|||
border: 2rpx solid transparent; |
|||
border-radius: 100rpx; |
|||
background-clip: padding-box, border-box; |
|||
border-color: transparent !important; |
|||
background-origin: padding-box, border-box; |
|||
background-image: linear-gradient(180deg, #141644 25%, #141644 77.78%), linear-gradient(to right, rgba(126, 223, 135, 1), rgba(255, 158, 137, 1)); |
|||
height: 88rpx; |
|||
padding: 0 40rpx !important; |
|||
} |
|||
|
|||
.text-primary { |
|||
color: #fff; |
|||
} |
|||
|
|||
.text-deep-primary { |
|||
color: rgba(167, 181, 229, 1); |
|||
} |
|||
|
|||
.f44 { |
|||
font-size: 44rpx; |
|||
} |
|||
|
|||
.f28 { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.f36 { |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
.content { |
|||
width: 750rpx; |
|||
background: #0B1016; |
|||
min-height: 100vh; |
|||
position: relative; |
|||
z-index: 10; |
|||
padding-bottom: 100rpx; |
|||
background-image: url('../../static/images/community_bg_h51.png'); |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
background-position: 0px 130rpx; |
|||
|
|||
.container { |
|||
padding: 0rpx 32rpx; |
|||
|
|||
.top { |
|||
padding-top: 64rpx; |
|||
} |
|||
|
|||
.bottom { |
|||
border: 1px solid rgba(119, 120, 177, 1); |
|||
background: #212A51; |
|||
padding: 32rpx 32rpx 40rpx 32rpx; |
|||
border-radius: 24rpx; |
|||
margin-top: 36rpx; |
|||
|
|||
.label-title { |
|||
image { |
|||
width: 52rpx; |
|||
height: 52rpx; |
|||
margin-right: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@ -0,0 +1,26 @@ |
|||
import Vue from 'vue' |
|||
import Vuex from 'vuex' |
|||
|
|||
Vue.use(Vuex) |
|||
|
|||
const store = new Vuex.Store({ |
|||
state: { |
|||
// 中英化
|
|||
language: uni.getStorageSync("language") || 'zh_TW', |
|||
}, |
|||
mutations: { |
|||
// 设置中英文
|
|||
setLanguage: (state, language) => { |
|||
const obj = state |
|||
obj.language = language |
|||
uni.setStorageSync("language", language) |
|||
}, |
|||
|
|||
}, |
|||
actions: { |
|||
|
|||
|
|||
} |
|||
}) |
|||
|
|||
export default store |
|||
@ -0,0 +1,78 @@ |
|||
/** |
|||
* 这里是uni-app内置的常用样式变量 |
|||
* |
|||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 |
|||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App |
|||
* |
|||
*/ |
|||
@import "uview-ui/theme.scss"; |
|||
@import "uview-ui/index.scss"; |
|||
|
|||
/** |
|||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 |
|||
* |
|||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 |
|||
*/ |
|||
|
|||
/* 颜色变量 */ |
|||
|
|||
/* 行为相关颜色 */ |
|||
$uni-color-primary: #007aff; |
|||
$uni-color-success: #4cd964; |
|||
$uni-color-warning: #f0ad4e; |
|||
$uni-color-error: #dd524d; |
|||
|
|||
/* 文字基本颜色 */ |
|||
$uni-text-color:#333;//基本色 |
|||
$uni-text-color-inverse:#fff;//反色 |
|||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 |
|||
$uni-text-color-placeholder: #808080; |
|||
$uni-text-color-disable:#c0c0c0; |
|||
|
|||
/* 背景颜色 */ |
|||
$uni-bg-color:#ffffff; |
|||
$uni-bg-color-grey:#f8f8f8; |
|||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色 |
|||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 |
|||
|
|||
/* 边框颜色 */ |
|||
$uni-border-color:#c8c7cc; |
|||
|
|||
/* 尺寸变量 */ |
|||
|
|||
/* 文字尺寸 */ |
|||
$uni-font-size-sm:12px; |
|||
$uni-font-size-base:14px; |
|||
$uni-font-size-lg:16; |
|||
|
|||
/* 图片尺寸 */ |
|||
$uni-img-size-sm:20px; |
|||
$uni-img-size-base:26px; |
|||
$uni-img-size-lg:40px; |
|||
|
|||
/* Border Radius */ |
|||
$uni-border-radius-sm: 2px; |
|||
$uni-border-radius-base: 3px; |
|||
$uni-border-radius-lg: 6px; |
|||
$uni-border-radius-circle: 50%; |
|||
|
|||
/* 水平间距 */ |
|||
$uni-spacing-row-sm: 5px; |
|||
$uni-spacing-row-base: 10px; |
|||
$uni-spacing-row-lg: 15px; |
|||
|
|||
/* 垂直间距 */ |
|||
$uni-spacing-col-sm: 4px; |
|||
$uni-spacing-col-base: 8px; |
|||
$uni-spacing-col-lg: 12px; |
|||
|
|||
/* 透明度 */ |
|||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 |
|||
|
|||
/* 文章场景相关 */ |
|||
$uni-color-title: #2C405A; // 文章标题颜色 |
|||
$uni-font-size-title:20px; |
|||
$uni-color-subtitle: #555555; // 二级标题颜色 |
|||
$uni-font-size-subtitle:26px; |
|||
$uni-color-paragraph: #3F536E; // 文章段落颜色 |
|||
$uni-font-size-paragraph:15px; |
|||
@ -0,0 +1,150 @@ |
|||
|
|||
/** |
|||
* 格式化金额, 保留两位小数 |
|||
* @param o |
|||
* @param o.amount {number | string} 金额 |
|||
* @param o.precision {number} 精度, 默认 100 (分) |
|||
* @param o.thousand {boolean} 是否需要千分位, 默认 true |
|||
* @param o.decimal {boolean} 是否需要小数点, 默认 true |
|||
* @param o.decimalLength {number} 小数点位数, 默认 2 |
|||
* @param o.decimalSeparator {string} 小数点分隔符, 默认 '.' |
|||
* @param o.thousandSeparator {string} 千分位分隔符, 默认 ',' |
|||
* @return {string | number} |
|||
*/ |
|||
export const amountFormat = (o) => { |
|||
if (o.amount === undefined || o.amount === null) { |
|||
return o.amount; |
|||
} |
|||
let amount = o.amount; |
|||
// 如果 amount 是字符串, 则转换为数字
|
|||
if (typeof amount === 'string') { |
|||
amount = parseFloat(amount); |
|||
} |
|||
let precision = o.precision || 100; |
|||
let thousand = o.thousand === undefined ? true : o.thousand; |
|||
let decimal = o.decimal === undefined ? true : o.decimal; |
|||
let decimalLength = o.decimalLength || 2; |
|||
let decimalSeparator = o.decimalSeparator || '.'; |
|||
let thousandSeparator = o.thousandSeparator || ','; |
|||
amount = amount / precision; |
|||
amount = amount.toFixed(decimalLength); |
|||
if (thousand) { |
|||
amount = amount.replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandSeparator); |
|||
} |
|||
if (decimal) { |
|||
amount = amount.replace('.', decimalSeparator); |
|||
} |
|||
return amount; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 参数处理 |
|||
* @param {*} params 参数 |
|||
*/ |
|||
export function tansParams(params) { |
|||
let result = '' |
|||
for (const propName of Object.keys(params)) { |
|||
const value = params[propName]; |
|||
var part = encodeURIComponent(propName) + "="; |
|||
if (value !== null && value !== "" && typeof (value) !== "undefined") { |
|||
if (typeof value === 'object') { |
|||
for (const key of Object.keys(value)) { |
|||
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { |
|||
let params = propName + '[' + key + ']'; |
|||
var subPart = encodeURIComponent(params) + "="; |
|||
result += subPart + encodeURIComponent(value[key]) + "&"; |
|||
} |
|||
} |
|||
} else { |
|||
result += part + encodeURIComponent(value) + "&"; |
|||
} |
|||
} |
|||
} |
|||
return result |
|||
} |
|||
|
|||
|
|||
// 精度计算乘法
|
|||
export function NumberMul(arg1, arg2) { |
|||
let m = 0 |
|||
const s1 = arg1.toString() |
|||
const s2 = arg2.toString() |
|||
try { |
|||
m += s1.split('.')[1].length |
|||
} catch (e) {} |
|||
try { |
|||
m += s2.split('.')[1].length |
|||
} catch (e) {} |
|||
|
|||
return Number(s1.replace('.', '')) * Number(s2.replace('.', '')) / Math.pow(10, m) |
|||
} |
|||
|
|||
// 精度计算除法
|
|||
// 除数,被除数, 保留的小数点后的位数
|
|||
export function NumberDiv(arg1, arg2) { |
|||
arg1 = parseFloat(arg1) |
|||
arg2 = parseFloat(arg2) |
|||
let t1 = 0 |
|||
let t2 = 0 |
|||
let r1; let r2 |
|||
try { |
|||
t1 = arg1.toString().split('.')[1].length |
|||
} catch (e) {} |
|||
try { |
|||
t2 = arg2.toString().split('.')[1].length |
|||
} catch (e) {} |
|||
r1 = Number(arg1.toString().replace('.', '')) |
|||
r2 = Number(arg2.toString().replace('.', '')) |
|||
return Mul(r1 / r2, Math.pow(10, t2 - t1)) |
|||
} |
|||
|
|||
export function Mul(arg1, arg2) { |
|||
arg1 = parseFloat(arg1) |
|||
arg2 = parseFloat(arg2) |
|||
let m = 0 |
|||
const s1 = arg1.toString() |
|||
const s2 = arg2.toString() |
|||
try { |
|||
m += s1.split('.')[1].length |
|||
} catch (e) {} |
|||
try { |
|||
m += s2.split('.')[1].length |
|||
} catch (e) {} |
|||
return Number(s1.replace('.', '')) * Number(s2.replace('.', '')) / Math.pow(10, m) |
|||
} |
|||
|
|||
export const GetADateTime = { |
|||
// 获取今天起始时间日期
|
|||
getTodayBegin() { |
|||
const date = new Date(); |
|||
const year = date.getFullYear(); |
|||
let month = date.getMonth() + 1; |
|||
let day = date.getDate(); |
|||
// 年月日时分秒
|
|||
// 月和日如果是一位数的话,前面补0
|
|||
if (month < 10) { |
|||
month = '0' + month; |
|||
} |
|||
if (day < 10) { |
|||
day = '0' + day; |
|||
} |
|||
return year + '-' + month + '-' + day + ' 00:00:00'; |
|||
}, |
|||
// 获取今天结束时间日期
|
|||
getTodayEnd() { |
|||
const date = new Date(); |
|||
const year = date.getFullYear(); |
|||
let month = date.getMonth() + 1; |
|||
let day = date.getDate(); |
|||
// 年月日时分秒
|
|||
// 月和日如果是一位数的话,前面补0
|
|||
if (month < 10) { |
|||
month = '0' + month; |
|||
} |
|||
if (day < 10) { |
|||
day = '0' + day; |
|||
} |
|||
return year + '-' + month + '-' + day + ' 23:59:59'; |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
import Theme from '../global.scss'; |
|||
|
|||
const SHOW_DIALOG=true |
|||
|
|||
const env = "dev"; |
|||
// const env = "stage";
|
|||
// const env = "prod";
|
|||
const IS_TEST = env === "dev" || env === "stage"; |
|||
|
|||
const baseUrl = IS_TEST ? 'http://kaka-carddealer-admin.weirui0755.com/stage-api' |
|||
: '/prod-api' |
|||
|
|||
export default { |
|||
baseUrl, |
|||
IS_TEST, |
|||
SHOW_DIALOG, |
|||
/** |
|||
* @type {{deepColor: string; grayColor: string; primaryColor: string; lightPrimaryColor: string;}} |
|||
*/ |
|||
Theme, |
|||
appVariant: { |
|||
title: "Yuntong Supply Chain" |
|||
} |
|||
} |
|||
@ -0,0 +1,204 @@ |
|||
var utils = { |
|||
|
|||
// 获取今天+1
|
|||
addDate() { |
|||
let nowDate = new Date(); |
|||
nowDate.setDate(nowDate.getDate() + 1); |
|||
var day = nowDate.getDate(); |
|||
let date = { |
|||
year: nowDate.getFullYear(), |
|||
month: nowDate.getMonth() + 1, |
|||
date: day, |
|||
} |
|||
|
|||
// console.log(date);
|
|||
if (date.date <= 9) { |
|||
return date.year + '-' + 0 + date.month + '-' + 0 + date.date; |
|||
} else { |
|||
return date.year + '-' + 0 + date.month + '-' + date.date; |
|||
} |
|||
// console.log(this.systemDate);
|
|||
}, |
|||
|
|||
// 获取今天
|
|||
addDate2() { |
|||
let nowDate = new Date(); |
|||
let date = { |
|||
year: nowDate.getFullYear(), |
|||
month: nowDate.getMonth() + 1, |
|||
date: nowDate.getDate(), |
|||
} |
|||
// console.log(date);
|
|||
if (date.date <= 9) { |
|||
return date.year + '-' + 0 + date.month + '-' + 0 + date.date; |
|||
} else { |
|||
return date.year + '-' + 0 + date.month + '-' + date.date; |
|||
} |
|||
// console.log(this.systemDate);
|
|||
}, |
|||
|
|||
|
|||
|
|||
getSubStr: function(str) { |
|||
var subStr1 = str.substr(0, 5); |
|||
var subStr2 = str.substr(str.length - 5, 20); |
|||
var subStr = subStr1 + "..." + subStr2; |
|||
return subStr; |
|||
}, |
|||
checkEmail: function(email) { |
|||
return RegExp( |
|||
/^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/ |
|||
) |
|||
.test(email); |
|||
}, |
|||
checkMobile: function(mobile) { |
|||
return RegExp(/^1[34578]\d{9}$/).test(mobile); |
|||
}, |
|||
caculateTime: function(timeZome, time) { |
|||
return time + (timeZome * 1000 * 60 * 60); |
|||
}, |
|||
formatyymmdd: function(time) { |
|||
var date = new Date(time) |
|||
console.log(date, 5555655) |
|||
var localTime = date.getTime(); |
|||
var localOffset = date.getTimezoneOffset() * 60000 //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset //utc即GMT时间
|
|||
var offset = 8; //东8区
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing); |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
console.log(M + '/' + D + '/' + Y, 5454545454); |
|||
return M + '/' + D + '/' + Y; |
|||
|
|||
}, |
|||
formatyymmdd2: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime(); |
|||
var localOffset = date.getTimezoneOffset() * 60000; //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = 8; //东8区
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing); |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
return Y + M + D; |
|||
}, |
|||
formatyymmddhhmmss: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime() |
|||
var localOffset = date.getTimezoneOffset() * 60000 //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = uni.getStorageSync('coinTypeInfo').system_timezone //时区拿接口的
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing) |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
// return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
|
|||
return D + '/' + M + '/' + Y + ' ' + h + ':' + m + ':' + s; |
|||
}, |
|||
// 日月时分秒
|
|||
formatmmddhhmmss: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime() |
|||
var localOffset = date.getTimezoneOffset() * 60000 //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = uni.getStorageSync('coinTypeInfo').system_timezone //时区拿接口的
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing) |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
// return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
|
|||
return D + '/' + M + '/' + ' ' + h + ':' + m + ':' + s; |
|||
}, |
|||
|
|||
|
|||
formatyymmddhhmm: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime() |
|||
var localOffset = date.getTimezoneOffset() * 60000 //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = uni.getStorageSync('coinTypeInfo').system_timezone //时区拿接口的
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing) |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
// return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
|
|||
return D + '/' + M + '/' + Y + ' ' + h + ':' + m; |
|||
}, |
|||
|
|||
|
|||
getformatyymmddhhmmss: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime() |
|||
var localOffset = date.getTimezoneOffset() * 60000 //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = uni.getStorageSync('coinTypeInfo').system_timezone //时区拿接口的
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing) |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
// return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
|
|||
return D + '/' + M + '/' + Y + ' ' + h + ':' + m + ':' + s; |
|||
}, |
|||
|
|||
formathhmm: function(time) { |
|||
var date = new Date(time) |
|||
var localTime = date.getTime(); |
|||
var localOffset = date.getTimezoneOffset() * 60000; //获得当地时间偏移的毫秒数
|
|||
var utc = localTime + localOffset; //utc即GMT时间
|
|||
var offset = 8; //东8区
|
|||
var beijing = utc + (3600000 * offset); |
|||
date = new Date(beijing); |
|||
var Y = date.getFullYear() |
|||
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) |
|||
var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() |
|||
var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() |
|||
var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() |
|||
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() |
|||
return h + ':' + m; |
|||
}, |
|||
//客户端时间转换为北京时间
|
|||
getBeijingtime: function() { |
|||
//获得当前运行环境时间
|
|||
let d = new Date(); |
|||
let currentDate = new Date(); |
|||
let tmpHours = currentDate.getHours(); |
|||
//算得时区
|
|||
let time_zone = -d.getTimezoneOffset() / 60; |
|||
if (time_zone < 0) { |
|||
time_zone = Math.abs(time_zone) + 8; |
|||
currentDate.setHours(tmpHours + time_zone); |
|||
} else { |
|||
time_zone -= 8; |
|||
currentDate.setHours(tmpHours - time_zone); |
|||
} |
|||
return currentDate; |
|||
}, |
|||
} |
|||
|
|||
export default utils |
|||
@ -0,0 +1,10 @@ |
|||
export default [ |
|||
{ |
|||
title: '繁体中文', |
|||
language: 'zh_TW' |
|||
}, |
|||
{ |
|||
title: 'English', |
|||
language: 'en_US', |
|||
}, |
|||
] |
|||
@ -0,0 +1,18 @@ |
|||
// zh_TW.js
|
|||
export default { |
|||
tabBar: { |
|||
Home: 'Dear Ipswap Foundation Member:', |
|||
contract: 'Mapping smart contracts', |
|||
address: 'My inviter address', |
|||
confirm: 'confirm', |
|||
link: 'My invitation link', |
|||
cx: 'Due to its unique innovation, IPPSWAP has become a popular choice for partners in the industry, developing rapidly and attracting attention from various fields. The fund is investigating the reasons for the transfer of LP assets. We will be committed to creating a safe, transparent, and fair investment environment, enhancing risk control capabilities and information transparency, and protecting the rights and interests of all investors.', |
|||
zw: 'As the most innovative and profitable foundation, we always uphold a responsible attitude towards member investment. We are now launching a new ippswap mining plan to allow everyone to participate more flexibly and efficiently in mining, and gain more profits.', |
|||
yuan: 'Original ippswap participants are required to participate in airdrop mapping and receive 50% of the original ipp token chips for free. The duration of receiving airdrop and mapping is 25 days, and the token mining plan will be launched after 25 days. Members who fail to participate in air drop collection and mapping within the specified time limit will be deemed to have waived their permission.', |
|||
we: 'We believe that through a new mining plan, the ippswap fund will become a more prosperous and active community, bringing more opportunities and benefits to all participants. Welcome to join us! Thank you for your support and trust.', |
|||
ji: 'ippswap Foundation', |
|||
empty:'Inviter address is empty', |
|||
}, |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,19 @@ |
|||
// zh_TW.js
|
|||
export default { |
|||
tabBar: { |
|||
Home: '尊敬的ippswap基金會會員:', |
|||
contract: '映射智慧合約', |
|||
address: '我的邀請人地址', |
|||
confirm: '確認', |
|||
link: '我的邀請連結', |
|||
cx: '因其獨特的創新,ippswap已成為圈內廣大夥伴的熱門選擇,發展迅猛,引起各方領域的關注。基金正在調查LP資產轉出原因,我們將致力於創建安全、透明、公正的投資環境,並提升風控能力和資訊公開透明度,保障所有投資者權益。', |
|||
zw: '作為最有創新意識和最有盈利能力的基金會,我們始終秉持對會員投資負責的態度,現開啟新的ippswap挖礦計畫,讓大家更靈活、高效地參與挖礦,獲得更多收益。', |
|||
yuan: '原ippswap參與人員需參與空投映射,免費獲得原ipp代幣50%籌碼。領取空投及映射時長為25天,25天後將代幣挖礦計畫開啟。未在規定時限參與空投領取及映射的會員將視作放棄許可權。', |
|||
we: '我們相信,通過全新的挖礦計畫,ippswap基金將成為更繁榮、活躍的社區,為所有參與者帶來更多機會和福利。歡迎加入我們!謝謝大家的支持和信任。', |
|||
ji: 'ippswap基金會', |
|||
empty:'邀請人地址為空', |
|||
|
|||
}, |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,131 @@ |
|||
const initListPageContext = () => ({ |
|||
// 每页
|
|||
pageSize: 10, |
|||
// 当前页
|
|||
page: 1, |
|||
// 总数量
|
|||
total: 0, |
|||
}) |
|||
|
|||
export default { |
|||
onPullDownRefresh () { |
|||
// 重置页数
|
|||
this.listPageContext.page = 1; |
|||
this.getData(this.listPageContext) |
|||
.finally(() => { |
|||
uni.stopPullDownRefresh(); |
|||
}); |
|||
}, |
|||
/** |
|||
* 尝试给页面打补丁 |
|||
* 获取当前以加载的数据 |
|||
* 仅在没有数据的时候加载并且数据已经存在的情况下调用 |
|||
* @return {*} |
|||
*/ |
|||
onShow () { |
|||
if (this.status === "loading" || this.originData.length === 0) { |
|||
return; |
|||
} |
|||
|
|||
return this._getData({page: 1, pageSize: this.originData.length || this.listPageContext.pageSize}) |
|||
.then(res => { |
|||
this.originData = res.data; |
|||
this.listPageContext.total = res.total; |
|||
}) |
|||
}, |
|||
onReachBottom () { |
|||
this._onReachBottom(); |
|||
}, |
|||
data () { |
|||
return { |
|||
/** |
|||
* 用于 loadmore 组件 |
|||
* @type {'loadmore' | 'loading' | 'nomore'} |
|||
*/ |
|||
status: 'loadmore', |
|||
loadText: { |
|||
loadmore: '轻轻上拉', |
|||
loading: '努力加载中', |
|||
nomore: '没有更多了' |
|||
}, |
|||
// 原始数据
|
|||
originData: [], |
|||
listPageContext: initListPageContext(), |
|||
} |
|||
}, |
|||
computed: { |
|||
// 是否还有更多数据
|
|||
hasMore () { |
|||
return this.originData.length < this.listPageContext.total |
|||
}, |
|||
// 视图使用的列表数据
|
|||
dataForUI () { |
|||
return this.originData.map((item, index) => this._mapFrom(item, index)) |
|||
}, |
|||
}, |
|||
methods: { |
|||
_getData (paging) { |
|||
throw new Error('请在组件中实现 _getData 方法') |
|||
}, |
|||
_mapFrom (remoteItem, index) { |
|||
throw new Error('请在组件中实现 _mapFrom 方法') |
|||
}, |
|||
_uniqueId (remoteItem) { |
|||
throw new Error('请在组件中实现 _uniqueId 方法') |
|||
}, |
|||
getData (paging) { |
|||
paging = paging || this.listPageContext |
|||
this.status = 'loading' |
|||
return this._getData(paging) |
|||
.then(res => { |
|||
if (this.listPageContext.page === 1) { |
|||
this.originData = res.data |
|||
} else { |
|||
this.originData = this.originData.concat(res.data) |
|||
} |
|||
this.listPageContext.total = res.total |
|||
|
|||
// 判断是否还有更多
|
|||
if (this.originData.length >= this.listPageContext.total) { |
|||
this.status = 'nomore' |
|||
} else { |
|||
this.status = 'loadmore' |
|||
} |
|||
}) |
|||
.catch(() => { |
|||
this.status = 'loadmore' |
|||
}) |
|||
}, |
|||
patchOriginData (index, newData) { |
|||
const oldData = this.originData[index] |
|||
|
|||
this.originData = [ |
|||
...this.originData.slice(0, index), |
|||
{...oldData, ...newData}, |
|||
...this.originData.slice(index + 1), |
|||
] |
|||
}, |
|||
// 删除某条数据
|
|||
deleteOriginData (index) { |
|||
this.originData = [ |
|||
...this.originData.slice(0, index), |
|||
...this.originData.slice(index + 1), |
|||
] |
|||
}, |
|||
resetListPage () { |
|||
this.originData = [] |
|||
this.listPageContext = initListPageContext() |
|||
}, |
|||
_onReachBottom() { |
|||
// 如果没有更多数据了,就不再加载
|
|||
// 如果数据为 0, 则属于误触发,不再加载
|
|||
// 如果正在加载中,也不再加载
|
|||
if (!this.hasMore || this.originData.length === 0 || this.status === "loading") { |
|||
return |
|||
} |
|||
// 页数 +1
|
|||
this.listPageContext.page += 1; |
|||
this.getData(this.listPageContext); |
|||
} |
|||
}, |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
window.onload = function() { |
|||
|
|||
document.addEventListener('touchstart', function(event) { |
|||
if (event.touches.length > 1) { |
|||
event.preventDefault(); |
|||
} |
|||
|
|||
}); |
|||
|
|||
document.addEventListener('gesturestart', function(event) { |
|||
event.preventDefault(); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
|
|||
// 禁用双指放大
|
|||
document.documentElement.addEventListener('touchstart', function(event) { |
|||
if (event.touches.length > 1) { |
|||
event.preventDefault(); |
|||
} |
|||
}, { |
|||
passive: false |
|||
}); |
|||
|
|||
// 禁用双击放大
|
|||
var lastTouchEnd = 0; |
|||
document.documentElement.addEventListener('touchend', function(event) { |
|||
var now = Date.now(); |
|||
if (now - lastTouchEnd <= 300) { |
|||
event.preventDefault(); |
|||
} |
|||
lastTouchEnd = now; |
|||
}, { |
|||
passive: false |
|||
}); |
|||
@ -0,0 +1,19 @@ |
|||
[ |
|||
{ |
|||
"inputs": [], |
|||
"stateMutability": "nonpayable", |
|||
"type": "constructor" |
|||
}, |
|||
{ |
|||
"inputs": [ |
|||
{"internalType": "address", "name": "superior", "type": "address"}, |
|||
{"internalType": "address", "name": "plivate", "type": "address"} |
|||
], |
|||
"name": "creatCode", |
|||
"outputs": [ |
|||
{"internalType": "bool", "name": "", "type": "bool"} |
|||
], |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
} |
|||
] |
|||
@ -0,0 +1,439 @@ |
|||
[ |
|||
{ |
|||
"inputs": [], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "constructor" |
|||
}, |
|||
{ |
|||
"anonymous": false, |
|||
"inputs": [ |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "owner", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "spender", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"indexed": false, |
|||
"internalType": "uint256", |
|||
"name": "value", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "Approval", |
|||
"type": "event" |
|||
}, |
|||
{ |
|||
"anonymous": false, |
|||
"inputs": [ |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "previousOwner", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "newOwner", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"name": "OwnershipTransferred", |
|||
"type": "event" |
|||
}, |
|||
{ |
|||
"anonymous": false, |
|||
"inputs": [ |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "from", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"indexed": true, |
|||
"internalType": "address", |
|||
"name": "to", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"indexed": false, |
|||
"internalType": "uint256", |
|||
"name": "value", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "Transfer", |
|||
"type": "event" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "_decimals", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "uint8", |
|||
"name": "", |
|||
"type": "uint8" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "_name", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "string", |
|||
"name": "", |
|||
"type": "string" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "_symbol", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "string", |
|||
"name": "", |
|||
"type": "string" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "owner", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "address", |
|||
"name": "spender", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"name": "allowance", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "spender", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "amount", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "approve", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "account", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"name": "balanceOf", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "decimals", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "spender", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "subtractedValue", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "decreaseAllowance", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "getOwner", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "spender", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "addedValue", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "increaseAllowance", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "amount", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "mint", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "name", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "string", |
|||
"name": "", |
|||
"type": "string" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "owner", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [], |
|||
"name": "renounceOwnership", |
|||
"outputs": [], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "symbol", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "string", |
|||
"name": "", |
|||
"type": "string" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": true, |
|||
"inputs": [], |
|||
"name": "totalSupply", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "view", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "recipient", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "amount", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "transfer", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "sender", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "address", |
|||
"name": "recipient", |
|||
"type": "address" |
|||
}, |
|||
{ |
|||
"internalType": "uint256", |
|||
"name": "amount", |
|||
"type": "uint256" |
|||
} |
|||
], |
|||
"name": "transferFrom", |
|||
"outputs": [ |
|||
{ |
|||
"internalType": "bool", |
|||
"name": "", |
|||
"type": "bool" |
|||
} |
|||
], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
}, |
|||
{ |
|||
"constant": false, |
|||
"inputs": [ |
|||
{ |
|||
"internalType": "address", |
|||
"name": "newOwner", |
|||
"type": "address" |
|||
} |
|||
], |
|||
"name": "transferOwnership", |
|||
"outputs": [], |
|||
"payable": false, |
|||
"stateMutability": "nonpayable", |
|||
"type": "function" |
|||
} |
|||
] |
|||
@ -0,0 +1,418 @@ |
|||
import Web3 from "web3"; |
|||
import IPPTAbi from './ippt-abi.json' |
|||
import USDTAbi from './usdt-abi.json' |
|||
|
|||
// 将科学计数法形式的数字或字符串转换为可读的字符串
|
|||
function scientificNotationToString(param) { |
|||
let strParam = String(param); |
|||
let flag = /e/.test(strParam); |
|||
if (!flag) return param.toString(); |
|||
|
|||
// 指数符号 true: 正,false: 负
|
|||
let sysbol = true; |
|||
if (/e-/.test(strParam)) { |
|||
sysbol = false; |
|||
} |
|||
// 指数
|
|||
let index = Number(strParam.match(/\d+$/)[0]); |
|||
// 基数
|
|||
let basis = strParam.match(/^[\d\.]+/)[0].replace(/\./, ""); |
|||
|
|||
if (sysbol) { |
|||
return basis.padEnd(index + 1, "0"); |
|||
} else { |
|||
return basis.padStart(index + basis.length, "0").replace(/^0/, "0."); |
|||
} |
|||
} |
|||
|
|||
// 将数字转换到指定精度
|
|||
function toDecimals(value, decimals) { |
|||
return Number(value) * 10 ** Number(decimals); |
|||
} |
|||
|
|||
// 从指定精度转换数字
|
|||
function fromDecimals(value, decimals) { |
|||
return value / Math.pow(10, decimals); |
|||
} |
|||
|
|||
class ERC20 { |
|||
decimals = 0; |
|||
|
|||
/** |
|||
* 合约实例 |
|||
* @type {Contract} |
|||
*/ |
|||
contract; |
|||
|
|||
/** |
|||
* 合约地址 |
|||
* @type {string} |
|||
*/ |
|||
contractAddress; |
|||
|
|||
/** |
|||
* 合约 ABI |
|||
*/ |
|||
abi; |
|||
|
|||
// 当前的钱包地址, 当 connected 为 true 时可用
|
|||
get selectedAddress () { |
|||
return window.ethereum.selectedAddress; |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @return {*} |
|||
*/ |
|||
get ethereum() { |
|||
return window.ethereum |
|||
} |
|||
|
|||
/** |
|||
* @param contractAddress{string} |
|||
* @param abi{Array} |
|||
* @param web3{Web3} |
|||
*/ |
|||
constructor (contractAddress, abi, web3) { |
|||
this.contractAddress = contractAddress; |
|||
this.abi = abi; |
|||
this.contract = new web3.eth.Contract(abi, contractAddress); |
|||
} |
|||
|
|||
/** |
|||
* 加密 abi |
|||
* @param methodDefinition {string} - 方法名 |
|||
* @param parameters {Array<{type: "uint256" | "address"; value: number | string}>} - 参数 |
|||
* @return {string} |
|||
*/ |
|||
encodeABI(methodDefinition, parameters) { |
|||
let m = Web3.utils.sha3(methodDefinition).slice(0, 10); |
|||
parameters.forEach((p) => { |
|||
switch (p.type) { |
|||
case "uint256": |
|||
case "address": |
|||
m += p.value |
|||
.toString() |
|||
.toLowerCase() |
|||
.replace("0x", "") |
|||
.padStart(64, "0"); |
|||
break; |
|||
} |
|||
}); |
|||
return m; |
|||
} |
|||
|
|||
handleError(err) { |
|||
const message = err ? err.message ? err.message : err.toString() : 'Unknown Error' |
|||
if (message.includes("User denied")) { |
|||
uni.showToast({ |
|||
title: 'failed', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 相当于 contract.methods.xx.send |
|||
* @param contractSendMethod |
|||
* @param sendOptions |
|||
* @return {PromiEvent<Eth.Contract>} |
|||
*/ |
|||
send (contractSendMethod, sendOptions) { |
|||
const promiEvent = contractSendMethod.send(sendOptions); |
|||
promiEvent.catch(err => this.handleError(err)); |
|||
return promiEvent; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取代币余额 |
|||
* |
|||
* @param account{string} |
|||
* |
|||
* @return Promise<number> |
|||
*/ |
|||
async $balanceOf (account) { |
|||
const balance = await this.contract.methods.balanceOf(account).call({from: this.selectedAddress}) |
|||
const decimals = await this.$decimals() |
|||
return Number(fromDecimals(Number(balance), decimals)) |
|||
} |
|||
|
|||
/** |
|||
* 获取授权给某个合约的数量 |
|||
* @param owner {string} - 钱包地址 |
|||
* @param spender {string} - 授权地址 |
|||
* @return {Promise<number>} |
|||
*/ |
|||
$allowance(owner, spender) { |
|||
return this.contract.methods.allowance(owner, spender).call() |
|||
.then(async (res) => { |
|||
return Number(fromDecimals(Number(res), await this.$decimals())) |
|||
}) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取 vnft 合约的精度 |
|||
* |
|||
* @return Promise<number> |
|||
*/ |
|||
$decimals () { |
|||
if (this.decimals) { |
|||
return Promise.resolve(this.decimals) |
|||
} |
|||
return this.contract.methods |
|||
.decimals() |
|||
.call() |
|||
.then(res => { |
|||
this.decimals = Number(res) |
|||
return this.decimals |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 授权代币给某个合约 |
|||
* @param spender - 授权地址 |
|||
* @param amount - 授权金额 |
|||
* @return {PromiEvent<Contract>} |
|||
*/ |
|||
$approve(spender, amount) { |
|||
amount = toDecimals(amount, this.decimals); |
|||
return this.send(this.contract.methods.approve(spender, scientificNotationToString(amount)), {from: this.selectedAddress}) |
|||
} |
|||
|
|||
/** |
|||
* 授权最大数量 |
|||
* @param spender |
|||
* @return {PromiEvent<Eth.Contract>} |
|||
*/ |
|||
approveMAX(spender) { |
|||
return this.send(this.contract.methods.approve(spender, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), {from: this.selectedAddress}) |
|||
} |
|||
|
|||
async $burn(amount, sendOptions) { |
|||
amount = toDecimals(amount, await this.$decimals()); |
|||
return this.send(this.contract.methods.burn(amount), sendOptions); |
|||
} |
|||
|
|||
/** |
|||
* 转移 token |
|||
* @param recipient - 接受者 |
|||
* @param amount - 转移数量 |
|||
* @return {PromiEvent<Eth.Contract>} |
|||
*/ |
|||
async $transfer(recipient, amount) { |
|||
amount = toDecimals(amount, await this.$decimals()); |
|||
// debugger
|
|||
return this.send(this.contract.methods.transfer(recipient, amount.toString()), {from: this.selectedAddress}); |
|||
} |
|||
|
|||
} |
|||
|
|||
class USDT extends ERC20 { |
|||
decimals = 18; |
|||
} |
|||
|
|||
class IPPT extends ERC20 { |
|||
/** |
|||
* 调用IPPT 合约绑定上级关系 |
|||
* creatCode(address superior, address plivate) public returns (bool) |
|||
*/ |
|||
async $creatCode (superior, plivate) { |
|||
const data = this.encodeABI("creatCode(address,address)", [ |
|||
{ type: "address", value: superior }, |
|||
{ type: "address", value: plivate }, |
|||
]); |
|||
|
|||
const params = [ |
|||
{ |
|||
data: data, |
|||
from: this.selectedAddress, |
|||
to: this.contractAddress, |
|||
}, |
|||
]; |
|||
return this.ethereum |
|||
.request({ |
|||
method: "eth_sendTransaction", |
|||
params, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* // 链接钱包, 所有 web3x.xx 方法都需要先执行 connectViaInPage 成功
|
|||
* web3x.connectViaInPage() |
|||
* .then(res => { |
|||
* // 当前钱包地址
|
|||
* console.log("当前钱包地址", web3x.selectedAddress); |
|||
* |
|||
* // 授权当前钱包的 USDT 给 IPPT (参数为 IPPT 合约地址)
|
|||
* web3x.usdt.approveMAX("0x6b175474e89094c44da98b954eedeac495271d0f") |
|||
* .on("confirmation", hash => { |
|||
* console.log("授权成功", hash); |
|||
* }) |
|||
* |
|||
* // 调用IPPT 合约绑定上级关系
|
|||
* // 0xFb4FC7Ddb8c4aa6b944703CE1e89D2B9Aa67a400: 上级地址
|
|||
* // web3x.selectedAddress: 当前钱包地址
|
|||
* web3x.ippt.$creatCode("0xFb4FC7Ddb8c4aa6b944703CE1e89D2B9Aa67a400", web3x.selectedAddress) |
|||
* .on("confirmation", hash => { |
|||
* console.log("绑定上级关系成功", res); |
|||
* }) |
|||
* }) |
|||
* // 切换到 BSC 测试网, 需先调用 web3x.addBscTestnet()
|
|||
* web3x.switchToBscTestnet |
|||
* // 添加 BSC 测试网
|
|||
* web3x.addBscTestnet |
|||
**/ |
|||
class Web3X { |
|||
/** |
|||
* 是否已链接 |
|||
* @type {boolean} |
|||
*/ |
|||
connected = false; |
|||
|
|||
/** |
|||
* web3 实例 |
|||
* @type {Web3} |
|||
*/ |
|||
#web3; |
|||
|
|||
|
|||
/** |
|||
* @type {IPPT} |
|||
*/ |
|||
ippt |
|||
|
|||
/** |
|||
* @type {ERC20} |
|||
*/ |
|||
usdt |
|||
|
|||
/** |
|||
* @type {string} |
|||
*/ |
|||
ipptContractAddress |
|||
|
|||
/** |
|||
* usdt 合约地址 |
|||
* @type {string} |
|||
*/ |
|||
usdtContractAddress |
|||
|
|||
constructor (ipptContractAddress, usdtContractAddress) { |
|||
this.ipptContractAddress = ipptContractAddress; |
|||
this.usdtContractAddress = usdtContractAddress; |
|||
} |
|||
|
|||
// 当前的钱包地址, 当 connected 为 true 时可用
|
|||
get selectedAddress () { |
|||
return window.ethereum.selectedAddress; |
|||
} |
|||
|
|||
/** |
|||
* 是否存在运行环境中 |
|||
* @return {boolean} |
|||
*/ |
|||
hasRuntime () { |
|||
return !!window.ethereum; |
|||
} |
|||
|
|||
// 调用 rpc 方法
|
|||
async #request (method, params) { |
|||
if (!this.hasRuntime()) { |
|||
throw new Error('No runtime'); |
|||
} |
|||
return await window.ethereum.request({ |
|||
method, |
|||
params, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* switch to a new chain (by chain ID) |
|||
*/ |
|||
async switchChain (chainId) { |
|||
return await this.#request('wallet_switchEthereumChain', [{ chainId }]); |
|||
} |
|||
|
|||
/** |
|||
* add a new chain (by chain object) |
|||
*/ |
|||
async addChain (chainParams) { |
|||
return await this.#request('wallet_addEthereumChain', [chainParams]); |
|||
} |
|||
|
|||
/** |
|||
* 切换到BSC 测试网, 需先调用 web3x.addBscTestnet() |
|||
* @example |
|||
* web3x.connectViaInPage() |
|||
* .then(() => { |
|||
* web3x.addBscTestnet() |
|||
* .then(() => { |
|||
* web3x.switchToBscTestnet() |
|||
* }) |
|||
* }) |
|||
*/ |
|||
async switchToBscTestnet () { |
|||
return await this.switchChain('0x61'); |
|||
} |
|||
|
|||
/** |
|||
* 添加 BSC 测试网 |
|||
* @example |
|||
* web3x.connectViaInPage() |
|||
* .then(() => { |
|||
* web3x.addBscTestnet() |
|||
* .then(() => { |
|||
* web3x.switchToBscTestnet() |
|||
* }) |
|||
* }) |
|||
*/ |
|||
async addBscTestnet () { |
|||
return await this.addChain({ |
|||
chainId: '0x61', |
|||
chainName: 'Binance Smart Chain Testnet', |
|||
nativeCurrency: { |
|||
name: 'BNB', |
|||
symbol: 'bnb', |
|||
decimals: 18, |
|||
}, |
|||
rpcUrls: ['https://data-seed-prebsc-1-s1.binance.org:8545/'], |
|||
blockExplorerUrls: ['https://testnet.bscscan.com'], |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 链接钱包 |
|||
* @return {Promise<string[]>} - 16 进制的钱包地址数组 |
|||
*/ |
|||
async connectViaInPage () { |
|||
return this.#request('eth_requestAccounts') |
|||
.then(res => { |
|||
this.#web3 = new Web3(window.ethereum); |
|||
this.ippt = new IPPT(this.ipptContractAddress, IPPTAbi, this.#web3); |
|||
this.usdt = new USDT(this.usdtContractAddress, USDTAbi, this.#web3); |
|||
this.connected = true; |
|||
return Promise.resolve(res) |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 监听钱包地址改变 |
|||
* @param callback{() => void} - 回调函数, 参数为新的钱包地址 |
|||
*/ |
|||
onAccountsChanged (callback) { |
|||
return window.ethereum.on('accountsChanged', callback) |
|||
} |
|||
|
|||
} |
|||
|
|||
export const web3x = new Web3X( |
|||
// BSC链连接是https://endpoints.omniatech.io/v1/bsc/mainnet/public
|
|||
// BSC链Id是56
|
|||
// ippt 合约
|
|||
"0x622d7b79a904e00e5fcab06396ff009e441f0186", |
|||
// usdt 合约
|
|||
"0x0a70dDf7cDBa3E8b6277C9DDcAf2185e8B6f539f" |
|||
); |
|||
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2020 www.uviewui.com |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -0,0 +1,106 @@ |
|||
<p align="center"> |
|||
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;"> |
|||
</p> |
|||
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3> |
|||
<h3 align="center">多平台快速开发的UI框架</h3> |
|||
|
|||
|
|||
## 说明 |
|||
|
|||
uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水 |
|||
|
|||
## 特性 |
|||
|
|||
- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序 |
|||
- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用 |
|||
- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨 |
|||
- 众多的常用页面和布局,让您专注逻辑,事半功倍 |
|||
- 详尽的文档支持,现代化的演示效果 |
|||
- 按需引入,精简打包体积 |
|||
|
|||
|
|||
## 安装 |
|||
|
|||
```bash |
|||
# npm方式安装 |
|||
npm i uview-ui |
|||
``` |
|||
|
|||
## 快速上手 |
|||
|
|||
1. `main.js`引入uView库 |
|||
```js |
|||
// main.js |
|||
import uView from 'uview-ui'; |
|||
Vue.use(uView); |
|||
``` |
|||
|
|||
2. `App.vue`引入基础样式(注意style标签需声明scss属性支持) |
|||
```css |
|||
/* App.vue */ |
|||
<style lang="scss"> |
|||
@import "uview-ui/index.scss"; |
|||
</style> |
|||
``` |
|||
|
|||
3. `uni.scss`引入全局scss变量文件 |
|||
```css |
|||
/* uni.scss */ |
|||
@import "uview-ui/theme.scss"; |
|||
``` |
|||
|
|||
4. `pages.json`配置easycom规则(按需引入) |
|||
|
|||
```js |
|||
// pages.json |
|||
{ |
|||
"easycom": { |
|||
// npm安装的方式不需要前面的"@/",下载安装的方式需要"@/" |
|||
// npm安装方式 |
|||
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" |
|||
// 下载安装方式 |
|||
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue" |
|||
}, |
|||
// 此为本身已有的内容 |
|||
"pages": [ |
|||
// ...... |
|||
] |
|||
} |
|||
``` |
|||
|
|||
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容 |
|||
|
|||
## 使用方法 |
|||
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。 |
|||
|
|||
```html |
|||
<template> |
|||
<u-button>按钮</u-button> |
|||
</template> |
|||
``` |
|||
|
|||
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容 |
|||
|
|||
## 链接 |
|||
|
|||
- [官方文档](https://uviewui.com/) |
|||
- [更新日志](https://uviewui.com/components/changelog.html) |
|||
- [升级指南](https://uviewui.com/components/changelog.html) |
|||
- [关于我们](https://uviewui.com/cooperation/about.html) |
|||
|
|||
## 预览 |
|||
|
|||
您可以通过**微信**扫码,查看最佳的演示效果。 |
|||
<br> |
|||
<br> |
|||
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" > |
|||
|
|||
<!-- ## 捐赠uView的研发 |
|||
|
|||
uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。 |
|||
|
|||
<img src="https://uviewui.com/common/wechat.png" width="220" > |
|||
<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" > |
|||
--> |
|||
## 版权信息 |
|||
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。 |
|||
@ -0,0 +1,190 @@ |
|||
<template> |
|||
<u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble" |
|||
length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex"> |
|||
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]"> |
|||
{{tips.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.text}}</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", |
|||
props: { |
|||
// 点击遮罩是否可以关闭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 |
|||
}, |
|||
// 通过双向绑定控制组件的弹出与收起 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 弹出的顶部圆角值 |
|||
borderRadius: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 弹出的z-index值 |
|||
zIndex: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 取消按钮的文字提示 |
|||
cancelText: { |
|||
type: String, |
|||
default: '取消' |
|||
} |
|||
}, |
|||
computed: { |
|||
// 顶部提示的样式 |
|||
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; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击取消按钮 |
|||
close() { |
|||
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数 |
|||
// 这是一个vue发送事件的特殊用法 |
|||
this.popupClose(); |
|||
this.$emit('close'); |
|||
}, |
|||
// 弹窗关闭 |
|||
popupClose() { |
|||
this.$emit('input', false); |
|||
}, |
|||
// 点击某一个item |
|||
itemClick(index) { |
|||
// disabled的项禁止点击 |
|||
if(this.list[index].disabled) return; |
|||
this.$emit('click', index); |
|||
this.$emit('input', 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; |
|||
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,256 @@ |
|||
<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" @tap.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 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', |
|||
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; |
|||
} |
|||
</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
@ -0,0 +1,244 @@ |
|||
<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) |
|||
* @property {String} mode 显示类型,见上方说明(默认circle) |
|||
* @property {String} sex-icon 性别图标,man-男,woman-女(默认man) |
|||
* @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', |
|||
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,后接上scal() |
|||
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,596 @@ |
|||
<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" |
|||
: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 按钮的大小 |
|||
* @property {Boolean} ripple 是否开启点击水波纹效果 |
|||
* @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效 |
|||
* @property {String} type 按钮的样式类型 |
|||
* @property {Boolean} plain 按钮是否镂空,背景色透明 |
|||
* @property {Boolean} disabled 是否禁用 |
|||
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true) |
|||
* @property {Boolean} shape 按钮外观形状,见文档说明 |
|||
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈) |
|||
* @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件 |
|||
* @property {String} open-type 开放能力 |
|||
* @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 成功的回调 |
|||
* @example <u-button>月落</u-button> |
|||
*/ |
|||
export default { |
|||
name: 'u-button', |
|||
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: 1000 |
|||
}, |
|||
// 按住后多久出现点击态,单位毫秒 |
|||
hoverStartTime: { |
|||
type: [String, Number], |
|||
default: 20 |
|||
}, |
|||
// 手指松开后点击态保留时间,单位毫秒 |
|||
hoverStayTime: { |
|||
type: [String, Number], |
|||
default: 150 |
|||
}, |
|||
}, |
|||
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() { |
|||
return { |
|||
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); |
|||
}, |
|||
// 查询按钮的节点信息 |
|||
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); |
|||
} |
|||
} |
|||
}; |
|||
</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; |
|||
} |
|||
} |
|||
|
|||
.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,640 @@ |
|||
<template> |
|||
<u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" 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)">{{ i18n.confirm }}</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', |
|||
props: { |
|||
safeAreaInsetBottom: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否允许通过点击遮罩关闭Picker |
|||
maskCloseAble: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 通过双向绑定控制组件的弹出与收起 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 弹出的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: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// 星期几,值为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: { |
|||
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() |
|||
} |
|||
}, |
|||
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); |
|||
}, |
|||
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,257 @@ |
|||
<template> |
|||
<view class="u-keyboard" @touchmove.stop.prevent="() => {}"> |
|||
<view class="u-keyboard-grids"> |
|||
<block> |
|||
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i"> |
|||
<view :hover-stay-time="100" @tap="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" @tap="changeCarInputMode"> |
|||
<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text> |
|||
/ |
|||
<text class="en" :class="[abc ? 'active' : 'inactive']">英</text> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "u-keyboard", |
|||
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]; |
|||
this.$emit('change', value); |
|||
}, |
|||
// 修改汽车牌键盘的输入模式,中文|英文 |
|||
changeCarInputMode() { |
|||
this.abc = !this.abc; |
|||
}, |
|||
// 点击退格键 |
|||
backspaceClick() { |
|||
this.$emit('backspace'); |
|||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器 |
|||
this.timer = null; |
|||
this.timer = setInterval(() => { |
|||
this.$emit('backspace'); |
|||
}, 250); |
|||
}, |
|||
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,299 @@ |
|||
<template> |
|||
<view |
|||
class="u-card" |
|||
@tap.stop="click" |
|||
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }" |
|||
:style="{ |
|||
borderRadius: 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="aspectfull" |
|||
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', |
|||
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: 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,316 @@ |
|||
<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', |
|||
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,123 @@ |
|||
<template> |
|||
<view class="u-checkbox-group u-clearfix"> |
|||
<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', |
|||
mixins: [Emitter], |
|||
props: { |
|||
// 最多能选中多少个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 { |
|||
} |
|||
}, |
|||
created() { |
|||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
methods: { |
|||
emitEvent() { |
|||
let values = []; |
|||
this.children.map(val => { |
|||
if(val.value) values.push(val.name); |
|||
}) |
|||
this.$emit('change', values); |
|||
// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证 |
|||
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时 |
|||
setTimeout(() => { |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch('u-form-item', 'on-form-change', values); |
|||
}, 60) |
|||
} |
|||
} |
|||
} |
|||
</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 */ |
|||
} |
|||
</style> |
|||
@ -0,0 +1,284 @@ |
|||
<template> |
|||
<view class="u-checkbox" :style="[checkboxStyle]"> |
|||
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]"> |
|||
<u-icon 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 形状,见官网说明(默认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", |
|||
props: { |
|||
// checkbox的名称 |
|||
name: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 形状,square为方形,circle为原型 |
|||
shape: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否为选中状态 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否禁用 |
|||
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: '' |
|||
}, |
|||
}, |
|||
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: { |
|||
// 是否禁用,如果父组件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.value && !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() { |
|||
return this.value ? '#ffffff' : 'transparent'; |
|||
}, |
|||
iconClass() { |
|||
let classes = []; |
|||
classes.push('u-checkbox__icon-wrap--' + this.elShape); |
|||
if (this.value == true) classes.push('u-checkbox__icon-wrap--checked'); |
|||
if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled'); |
|||
if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked'); |
|||
// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效 |
|||
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; |
|||
} |
|||
}, |
|||
methods: { |
|||
onClickLabel() { |
|||
if (!this.isLabelDisabled && !this.isDisabled) { |
|||
this.setValue(); |
|||
} |
|||
}, |
|||
toggle() { |
|||
if (!this.isDisabled) { |
|||
this.setValue(); |
|||
} |
|||
}, |
|||
emitEvent() { |
|||
this.$emit('change', { |
|||
value: !this.value, |
|||
name: this.name |
|||
}) |
|||
// 执行父组件u-checkbox-group的事件方法 |
|||
// 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间 |
|||
setTimeout(() => { |
|||
if(this.parent && this.parent.emitEvent) this.parent.emitEvent(); |
|||
}, 80); |
|||
}, |
|||
// 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值 |
|||
setValue() { |
|||
// 判断是否超过了可选的最大数量 |
|||
let checkedNum = 0; |
|||
if(this.parent && this.parent.children) { |
|||
// 只要父组件的某一个子元素的value为true,就加1(已有的选中数量) |
|||
this.parent.children.map(val => { |
|||
if (val.value) checkedNum++; |
|||
}) |
|||
} |
|||
// 如果原来为选中状态,那么可以取消 |
|||
if (this.value == true) { |
|||
this.emitEvent(); |
|||
this.$emit('input', !this.value); |
|||
} else { |
|||
// 如果超出最多可选项,提示 |
|||
if(this.parent && checkedNum >= this.parent.max) { |
|||
return this.$u.toast(`最多可选${this.parent.max}项`); |
|||
} |
|||
// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中 |
|||
this.emitEvent(); |
|||
this.$emit('input', !this.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; |
|||
} |
|||
} |
|||
|
|||
&__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,220 @@ |
|||
<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', |
|||
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(() => { |
|||
// 定时器,每次操作间隔为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,156 @@ |
|||
<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", |
|||
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'); |
|||
} |
|||
} |
|||
} |
|||
</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,204 @@ |
|||
<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", |
|||
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.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,99 @@ |
|||
<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", |
|||
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,237 @@ |
|||
<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 { |
|||
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,318 @@ |
|||
<template> |
|||
<view class="u-countdown"> |
|||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"> |
|||
<view class="u-countdown-time" :style="[letterStyle]"> |
|||
{{ d }} |
|||
</view> |
|||
</view> |
|||
<view |
|||
class="u-countdown-colon" |
|||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}" |
|||
v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))" |
|||
> |
|||
{{ separator == 'colon' ? ':' : '天' }} |
|||
</view> |
|||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showHours"> |
|||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}"> |
|||
{{ h }} |
|||
</view> |
|||
</view> |
|||
<view |
|||
class="u-countdown-colon" |
|||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}" |
|||
v-if="showHours" |
|||
> |
|||
{{ separator == 'colon' ? ':' : '时' }} |
|||
</view> |
|||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes"> |
|||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}"> |
|||
{{ i }} |
|||
</view> |
|||
</view> |
|||
<view |
|||
class="u-countdown-colon" |
|||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}" |
|||
v-if="showMinutes" |
|||
> |
|||
{{ separator == 'colon' ? ':' : '分' }} |
|||
</view> |
|||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds"> |
|||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}"> |
|||
{{ s }} |
|||
</view> |
|||
</view> |
|||
<view |
|||
class="u-countdown-colon" |
|||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}" |
|||
v-if="showSeconds && separator == 'zh'" |
|||
> |
|||
秒 |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* countDown 倒计时 |
|||
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。 |
|||
* @tutorial https://www.uviewui.com/components/countDown.html |
|||
* @property {String Number} timestamp 倒计时,单位为秒 |
|||
* @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true) |
|||
* @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon) |
|||
* @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30) |
|||
* @property {String} separator-color 分隔符的颜色(默认#303133) |
|||
* @property {String Number} font-size 倒计时字体大小,单位rpx(默认30) |
|||
* @property {Boolean} show-border 是否显示倒计时数字的边框(默认false) |
|||
* @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true) |
|||
* @property {String} border-color 数字边框的颜色(默认#303133) |
|||
* @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff) |
|||
* @property {String} color 倒计时数字的颜色(默认#303133) |
|||
* @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto) |
|||
* @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true) |
|||
* @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true) |
|||
* @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true) |
|||
* @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true) |
|||
* @event {Function} end 倒计时结束 |
|||
* @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数 |
|||
* @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down> |
|||
*/ |
|||
export default { |
|||
name: 'u-count-down', |
|||
props: { |
|||
// 倒计时的时间,秒为单位 |
|||
timestamp: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 是否自动开始倒计时 |
|||
autoplay: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒" |
|||
separator: { |
|||
type: String, |
|||
default: 'colon' |
|||
}, |
|||
// 分隔符的大小,单位rpx |
|||
separatorSize: { |
|||
type: [Number, String], |
|||
default: 30 |
|||
}, |
|||
// 分隔符颜色 |
|||
separatorColor: { |
|||
type: String, |
|||
default: "#303133" |
|||
}, |
|||
// 字体颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#303133' |
|||
}, |
|||
// 字体大小,单位rpx |
|||
fontSize: { |
|||
type: [Number, String], |
|||
default: 30 |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#fff' |
|||
}, |
|||
// 数字框高度,单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: 'auto' |
|||
}, |
|||
// 是否显示数字框 |
|||
showBorder: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 边框颜色 |
|||
borderColor: { |
|||
type: String, |
|||
default: '#303133' |
|||
}, |
|||
// 是否显示秒 |
|||
showSeconds: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示分钟 |
|||
showMinutes: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示小时 |
|||
showHours: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示“天” |
|||
showDays: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 当"天"的部分为0时,不显示 |
|||
hideZeroDay: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
watch: { |
|||
// 监听时间戳的变化 |
|||
timestamp(newVal, oldVal) { |
|||
// 如果倒计时间发生变化,清除定时器,重新开始倒计时 |
|||
this.clearTimer(); |
|||
this.start(); |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
d: '00', // 天的默认值 |
|||
h: '00', // 小时的默认值 |
|||
i: '00', // 分钟的默认值 |
|||
s: '00', // 秒的默认值 |
|||
timer: null ,// 定时器 |
|||
seconds: 0, // 记录不停倒计过程中变化的秒数 |
|||
}; |
|||
}, |
|||
computed: { |
|||
// 倒计时item的样式,item为分别的时分秒部分的数字 |
|||
itemStyle() { |
|||
let style = {}; |
|||
if(this.height) { |
|||
style.height = this.height + 'rpx'; |
|||
style.width = this.height + 'rpx'; |
|||
} |
|||
if(this.showBorder) { |
|||
style.borderStyle = 'solid'; |
|||
style.borderColor = this.borderColor; |
|||
style.borderWidth = '1px'; |
|||
} |
|||
if(this.bgColor) { |
|||
style.backgroundColor = this.bgColor; |
|||
} |
|||
return style; |
|||
}, |
|||
// 倒计时数字的样式 |
|||
letterStyle() { |
|||
let style = {}; |
|||
if(this.fontSize) style.fontSize = this.fontSize + 'rpx'; |
|||
if(this.color) style.color = this.color; |
|||
return style; |
|||
} |
|||
}, |
|||
mounted() { |
|||
// 如果自动倒计时 |
|||
this.autoplay && this.timestamp && this.start(); |
|||
}, |
|||
methods: { |
|||
// 倒计时 |
|||
start() { |
|||
// 避免可能出现的倒计时重叠情况 |
|||
this.clearTimer(); |
|||
if (this.timestamp <= 0) return; |
|||
this.seconds = Number(this.timestamp); |
|||
this.formatTime(this.seconds); |
|||
this.timer = setInterval(() => { |
|||
this.seconds--; |
|||
// 发出change事件 |
|||
this.$emit('change', this.seconds); |
|||
if (this.seconds < 0) { |
|||
return this.end(); |
|||
} |
|||
this.formatTime(this.seconds); |
|||
}, 1000); |
|||
}, |
|||
// 格式化时间 |
|||
formatTime(seconds) { |
|||
// 小于等于0的话,结束倒计时 |
|||
seconds <= 0 && this.end(); |
|||
let [day, hour, minute, second] = [0, 0, 0, 0]; |
|||
day = Math.floor(seconds / (60 * 60 * 24)); |
|||
// 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中 |
|||
// hour为给后面计算秒和分等用的(基于显示天的前提下计算) |
|||
hour = Math.floor(seconds / (60 * 60)) - day * 24; |
|||
// showHour为需要显示的小时 |
|||
let showHour = null; |
|||
if(this.showDays) { |
|||
showHour = hour; |
|||
} else { |
|||
// 如果不显示天数,将“天”部分的时间折算到小时中去 |
|||
showHour = Math.floor(seconds / (60 * 60)); |
|||
} |
|||
minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60; |
|||
second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60; |
|||
// 如果小于10,在前面补上一个"0" |
|||
showHour = showHour < 10 ? '0' + showHour : showHour; |
|||
minute = minute < 10 ? '0' + minute : minute; |
|||
second = second < 10 ? '0' + second : second; |
|||
day = day < 10 ? '0' + day : day; |
|||
this.d = day; |
|||
this.h = showHour; |
|||
this.i = minute; |
|||
this.s = second; |
|||
}, |
|||
// 停止倒计时 |
|||
end() { |
|||
this.clearTimer(); |
|||
this.$emit('end', {}); |
|||
}, |
|||
// 清除定时器 |
|||
clearTimer() { |
|||
if(this.timer) { |
|||
// 清除定时器 |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
} |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-countdown { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-countdown-item { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 2rpx; |
|||
border-radius: 6rpx; |
|||
white-space: nowrap; |
|||
transform: translateZ(0); |
|||
} |
|||
|
|||
.u-countdown-time { |
|||
margin: 0; |
|||
padding: 0; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.u-countdown-colon { |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
padding: 0 5rpx; |
|||
line-height: 1; |
|||
align-items: center; |
|||
padding-bottom: 4rpx; |
|||
} |
|||
|
|||
.u-countdown-scale { |
|||
transform: scale(0.9); |
|||
transform-origin: center center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,241 @@ |
|||
<template> |
|||
<view |
|||
class="u-count-num" |
|||
:style="{ |
|||
fontSize: fontSize + 'rpx', |
|||
fontWeight: bold ? 'bold' : 'normal', |
|||
color: color |
|||
}" |
|||
> |
|||
{{ displayValue }} |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* countTo 数字滚动 |
|||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。 |
|||
* @tutorial https://www.uviewui.com/components/countTo.html |
|||
* @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', |
|||
props: { |
|||
// 开始的数值,默认从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; |
|||
} |
|||
}, |
|||
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; |
|||
}, |
|||
destroyed() { |
|||
this.cancelAnimationFrame(this.rAF); |
|||
} |
|||
} |
|||
}; |
|||
</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,153 @@ |
|||
<template> |
|||
<view class="u-divider" :style="{ |
|||
height: height == 'auto' ? 'auto' : height + 'rpx', |
|||
backgroundColor: bgColor, |
|||
marginBottom: marginBottom + 'rpx', |
|||
marginTop: marginTop + '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 |
|||
}, |
|||
// 是否使用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,132 @@ |
|||
<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="value == 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', |
|||
props: { |
|||
// 当前选中项的value值 |
|||
value: { |
|||
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: { |
|||
// 监听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); |
|||
// 通知父组件(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,298 @@ |
|||
<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', |
|||
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,384 @@ |
|||
<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="[this.$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="value" |
|||
: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="value" |
|||
:password="password || this.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 && value != '' && 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", |
|||
props: { |
|||
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, |
|||
value: [Number, String], |
|||
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: { |
|||
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 = {}; |
|||
if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)'; |
|||
if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)'; |
|||
else style.transform = 'roate(0deg)'; |
|||
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); |
|||
}, |
|||
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', ''); |
|||
}, |
|||
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> |
|||
@ -0,0 +1,431 @@ |
|||
<template> |
|||
<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}"> |
|||
<view class="u-form-item__body" :style="{ |
|||
flexDirection: elLabelPosition == 'left' ? 'row' : 'column' |
|||
}"> |
|||
<!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" --> |
|||
<view class="u-form-item--left" :style="{ |
|||
width: uLabelWidth, |
|||
flex: `0 0 ${uLabelWidth}`, |
|||
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx', |
|||
}"> |
|||
<!-- 为了块对齐 --> |
|||
<view class="u-form-item--left__content" v-if="required || leftIcon || label"> |
|||
<!-- nvue不支持伪元素before --> |
|||
<text v-if="required" class="u-form-item--left__content--required">*</text> |
|||
<view class="u-form-item--left__content__icon" v-if="leftIcon"> |
|||
<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon> |
|||
</view> |
|||
<view class="u-form-item--left__content__label" :style="[elLabelStyle, { |
|||
'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end' |
|||
}]"> |
|||
{{label}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="u-form-item--right u-flex"> |
|||
<view class="u-form-item--right__content"> |
|||
<view class="u-form-item--right__content__slot "> |
|||
<slot /> |
|||
</view> |
|||
<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon"> |
|||
<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon> |
|||
<slot name="right" /> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{ |
|||
paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0', |
|||
}">{{validateMessage}}</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import Emitter from '../../libs/util/emitter.js'; |
|||
import schema from '../../libs/util/async-validator'; |
|||
// 去除警告信息 |
|||
schema.warning = function() {}; |
|||
|
|||
/** |
|||
* form-item 表单item |
|||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。 |
|||
* @tutorial http://uviewui.com/components/form.html |
|||
* @property {String} label 左侧提示文字 |
|||
* @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的 |
|||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框 |
|||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方 |
|||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90) |
|||
* @property {Object} label-style lable的样式,对象形式 |
|||
* @property {String} label-align lable的对齐方式 |
|||
* @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址 |
|||
* @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址 |
|||
* @property {Object} left-icon-style 左侧图标的样式,对象形式 |
|||
* @property {Object} right-icon-style 右侧图标的样式,对象形式 |
|||
* @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false) |
|||
* @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item> |
|||
*/ |
|||
|
|||
export default { |
|||
name: 'u-form-item', |
|||
mixins: [Emitter], |
|||
inject: { |
|||
uForm: { |
|||
default () { |
|||
return null |
|||
} |
|||
} |
|||
}, |
|||
props: { |
|||
// input的label提示语 |
|||
label: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 绑定的值 |
|||
prop: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示表单域的下划线边框 |
|||
borderBottom: { |
|||
type: [String, Boolean], |
|||
default: '' |
|||
}, |
|||
// label的位置,left-左边,top-上边 |
|||
labelPosition: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// label的宽度,单位rpx |
|||
labelWidth: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// lable的样式,对象形式 |
|||
labelStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// lable字体的对齐方式 |
|||
labelAlign: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 右侧图标 |
|||
rightIcon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 左侧图标 |
|||
leftIcon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 左侧图标的样式 |
|||
leftIconStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 左侧图标的样式 |
|||
rightIconStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 |
|||
required: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
initialValue: '', // 存储的默认值 |
|||
// isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成 |
|||
validateState: '', // 是否校验成功 |
|||
validateMessage: '', // 校验失败的提示语 |
|||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色, |
|||
errorType: ['message'], |
|||
fieldValue: '', // 获取当前子组件input的输入的值 |
|||
// 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中 |
|||
parentData: { |
|||
borderBottom: true, |
|||
labelWidth: 90, |
|||
labelPosition: 'left', |
|||
labelStyle: {}, |
|||
labelAlign: 'left', |
|||
} |
|||
}; |
|||
}, |
|||
watch: { |
|||
validateState(val) { |
|||
this.broadcastInputError(); |
|||
}, |
|||
// 监听u-form组件的errorType的变化 |
|||
"uForm.errorType"(val) { |
|||
this.errorType = val; |
|||
this.broadcastInputError(); |
|||
}, |
|||
}, |
|||
computed: { |
|||
// 计算后的label宽度,由于需要多个判断,故放到computed中 |
|||
uLabelWidth() { |
|||
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto |
|||
return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this |
|||
.elLabelWidth)) : '100%'; |
|||
}, |
|||
showError() { |
|||
return type => { |
|||
// 如果errorType数组中含有none,或者toast提示类型 |
|||
if (this.errorType.indexOf('none') >= 0) return false; |
|||
else if (this.errorType.indexOf(type) >= 0) return true; |
|||
else return false; |
|||
} |
|||
}, |
|||
// label的宽度 |
|||
elLabelWidth() { |
|||
// label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值 |
|||
return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData |
|||
.labelWidth : |
|||
90); |
|||
}, |
|||
// label的样式 |
|||
elLabelStyle() { |
|||
return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle : |
|||
{}); |
|||
}, |
|||
// label的位置,左侧或者上方 |
|||
elLabelPosition() { |
|||
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : |
|||
'left'); |
|||
}, |
|||
// label的对齐方式 |
|||
elLabelAlign() { |
|||
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left'); |
|||
}, |
|||
// label的下划线 |
|||
elBorderBottom() { |
|||
// 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值 |
|||
return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom : |
|||
true; |
|||
} |
|||
}, |
|||
methods: { |
|||
broadcastInputError() { |
|||
// 子组件发出事件,第三个参数为true或者false,true代表有错误 |
|||
this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border')); |
|||
}, |
|||
// 判断是否需要required校验 |
|||
setRules() { |
|||
let that = this; |
|||
// 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成 |
|||
// 从父组件u-form拿到当前u-form-item需要验证 的规则 |
|||
// let rules = this.getRules(); |
|||
// if (rules.length) { |
|||
// this.isRequired = rules.some(rule => { |
|||
// // 如果有必填项,就返回,没有的话,就是undefined |
|||
// return rule.required; |
|||
// }); |
|||
// } |
|||
|
|||
// blur事件 |
|||
this.$on('on-form-blur', that.onFieldBlur); |
|||
// change事件 |
|||
this.$on('on-form-change', that.onFieldChange); |
|||
}, |
|||
|
|||
// 从u-form的rules属性中,取出当前u-form-item的校验规则 |
|||
getRules() { |
|||
// 父组件的所有规则 |
|||
let rules = this.parent.rules; |
|||
rules = rules ? rules[this.prop] : []; |
|||
// 保证返回的是一个数组形式 |
|||
return [].concat(rules || []); |
|||
}, |
|||
|
|||
// blur事件时进行表单校验 |
|||
onFieldBlur() { |
|||
this.validation('blur'); |
|||
}, |
|||
|
|||
// change事件进行表单校验 |
|||
onFieldChange() { |
|||
this.validation('change'); |
|||
}, |
|||
|
|||
// 过滤出符合要求的rule规则 |
|||
getFilteredRule(triggerType = '') { |
|||
let rules = this.getRules(); |
|||
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证 |
|||
if (!triggerType) return rules; |
|||
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件 |
|||
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change'] |
|||
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性 |
|||
return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1); |
|||
}, |
|||
|
|||
// 校验数据 |
|||
validation(trigger, callback = () => {}) { |
|||
// 检验之间,先获取需要校验的值 |
|||
this.fieldValue = this.parent.model[this.prop]; |
|||
// blur和change是否有当前方式的校验规则 |
|||
let rules = this.getFilteredRule(trigger); |
|||
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为 |
|||
// 对count变量的统计错误而无法进入上一层的回调 |
|||
if (!rules || rules.length === 0) { |
|||
return callback(''); |
|||
} |
|||
// 设置当前的装填,标识为校验中 |
|||
this.validateState = 'validating'; |
|||
// 调用async-validator的方法 |
|||
let validator = new schema({ |
|||
[this.prop]: rules |
|||
}); |
|||
validator.validate({ |
|||
[this.prop]: this.fieldValue |
|||
}, { |
|||
firstFields: true |
|||
}, (errors, fields) => { |
|||
// 记录状态和报错信息 |
|||
this.validateState = !errors ? 'success' : 'error'; |
|||
this.validateMessage = errors ? errors[0].message : ''; |
|||
// 调用回调方法 |
|||
callback(this.validateMessage); |
|||
}); |
|||
}, |
|||
|
|||
// 清空当前的u-form-item |
|||
resetField() { |
|||
this.parent.model[this.prop] = this.initialValue; |
|||
// 设置为`success`状态,只是为了清空错误标记 |
|||
this.validateState = 'success'; |
|||
} |
|||
}, |
|||
|
|||
// 组件创建完成时,将当前实例保存到u-form中 |
|||
mounted() { |
|||
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用 |
|||
this.parent = this.$u.$parent.call(this, 'u-form'); |
|||
if (this.parent) { |
|||
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData |
|||
Object.keys(this.parentData).map(key => { |
|||
this.parentData[key] = this.parent[key]; |
|||
}); |
|||
// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验 |
|||
if (this.prop) { |
|||
// 将本实例添加到父组件中 |
|||
this.parent.fields.push(this); |
|||
this.errorType = this.parent.errorType; |
|||
// 设置初始值 |
|||
this.initialValue = this.fieldValue; |
|||
// 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的 |
|||
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空 |
|||
this.$nextTick(() => { |
|||
this.setRules(); |
|||
}) |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// 组件销毁前,将实例从u-form的缓存中移除 |
|||
beforeDestroy() { |
|||
// 如果当前没有prop的话表示当前不要进行删除(因为没有注入) |
|||
if (this.parent && this.prop) { |
|||
this.parent.fields.map((item, index) => { |
|||
if (item === this) this.parent.fields.splice(index, 1); |
|||
}) |
|||
} |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-form-item { |
|||
@include vue-flex; |
|||
// align-items: flex-start; |
|||
padding: 20rpx 0; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
box-sizing: border-box; |
|||
line-height: $u-form-item-height; |
|||
flex-direction: column; |
|||
|
|||
&__border-bottom--error:after { |
|||
border-color: $u-type-error; |
|||
} |
|||
|
|||
&__body { |
|||
@include vue-flex; |
|||
} |
|||
|
|||
&--left { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
|
|||
&__content { |
|||
position: relative; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
padding-right: 10rpx; |
|||
flex: 1; |
|||
|
|||
&__icon { |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
&--required { |
|||
position: absolute; |
|||
left: -16rpx; |
|||
vertical-align: middle; |
|||
color: $u-type-error; |
|||
padding-top: 6rpx; |
|||
} |
|||
|
|||
&__label { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&--right { |
|||
flex: 1; |
|||
|
|||
&__content { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
|
|||
&__slot { |
|||
flex: 1; |
|||
/* #ifndef MP */ |
|||
@include vue-flex; |
|||
align-items: center; |
|||
/* #endif */ |
|||
} |
|||
|
|||
&__icon { |
|||
margin-left: 10rpx; |
|||
color: $u-light-color; |
|||
font-size: 30rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&__message { |
|||
font-size: 24rpx; |
|||
line-height: 24rpx; |
|||
color: $u-type-error; |
|||
margin-top: 12rpx; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,134 @@ |
|||
<template> |
|||
<view class="u-form"><slot /></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* form 表单 |
|||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。 |
|||
* @tutorial http://uviewui.com/components/form.html |
|||
* @property {Object} model 表单数据对象 |
|||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框 |
|||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方 |
|||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90) |
|||
* @property {Object} label-style lable的样式,对象形式 |
|||
* @property {String} label-align lable的对齐方式 |
|||
* @property {Object} rules 通过ref设置,见官网说明 |
|||
* @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message']) |
|||
* @example <u-form :model="form" ref="uForm"></u-form> |
|||
*/ |
|||
|
|||
export default { |
|||
name: 'u-form', |
|||
props: { |
|||
// 当前form的需要验证字段的集合 |
|||
model: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 验证规则 |
|||
// rules: { |
|||
// type: [Object, Function, Array], |
|||
// default() { |
|||
// return {}; |
|||
// } |
|||
// }, |
|||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色, |
|||
// border-bottom-下边框呈现红色,none-无提示 |
|||
errorType: { |
|||
type: Array, |
|||
default() { |
|||
return ['message', 'toast'] |
|||
} |
|||
}, |
|||
// 是否显示表单域的下划线边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// label的位置,left-左边,top-上边 |
|||
labelPosition: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// label的宽度,单位rpx |
|||
labelWidth: { |
|||
type: [String, Number], |
|||
default: 90 |
|||
}, |
|||
// lable字体的对齐方式 |
|||
labelAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// lable的样式,对象形式 |
|||
labelStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
provide() { |
|||
return { |
|||
uForm: this |
|||
}; |
|||
}, |
|||
data() { |
|||
return { |
|||
rules: {} |
|||
}; |
|||
}, |
|||
created() { |
|||
// 存储当前form下的所有u-form-item的实例 |
|||
// 不能定义在data中,否则微信小程序会造成循环引用而报错 |
|||
this.fields = []; |
|||
}, |
|||
methods: { |
|||
setRules(rules) { |
|||
this.rules = rules; |
|||
}, |
|||
// 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法 |
|||
resetFields() { |
|||
this.fields.map(field => { |
|||
field.resetField(); |
|||
}); |
|||
}, |
|||
// 校验全部数据 |
|||
validate(callback) { |
|||
return new Promise(resolve => { |
|||
// 对所有的u-form-item进行校验 |
|||
let valid = true; // 默认通过 |
|||
let count = 0; // 用于标记是否检查完毕 |
|||
let errorArr = []; // 存放错误信息 |
|||
this.fields.map(field => { |
|||
// 调用每一个u-form-item实例的validation的校验方法 |
|||
field.validation('', error => { |
|||
// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过 |
|||
if (error) { |
|||
valid = false; |
|||
errorArr.push(error); |
|||
} |
|||
// 当历遍了所有的u-form-item时,调用promise的then方法 |
|||
if (++count === this.fields.length) { |
|||
resolve(valid); // 进入promise的then方法 |
|||
// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息 |
|||
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) { |
|||
this.$u.toast(errorArr[0]); |
|||
} |
|||
// 调用回调方法 |
|||
if (typeof callback == 'function') callback(valid); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
|||
@ -0,0 +1,74 @@ |
|||
<template> |
|||
<u-modal v-model="show" :title-style="titleStyle" :confirm-text="i18n.confirm" confirm-color="#7081FF" |
|||
:title="i18n.UpdateTips" @confirm="confirm"> |
|||
<view class="u-update-content"> |
|||
<rich-text :nodes="tipUpdateContent"></rich-text> |
|||
</view> |
|||
</u-modal> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
show: false, |
|||
// :title-style="contentStyle" :content-style="contentStyle" :cancel-style="contentStyle" :confirm-style="contentStyle" |
|||
// contentStyle:{ |
|||
// backgroundColor: '#111747' |
|||
// }, |
|||
titleStyle: { |
|||
color: '#7081FF' |
|||
}, |
|||
appUrl: '', |
|||
tipUpdateContent: '' |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.show = true; |
|||
this.appUrl = option.appUrl |
|||
this.tipUpdateContent = option.tipUpdateContent |
|||
}, |
|||
onHide() { |
|||
//退出app |
|||
// #ifdef APP-PLUS |
|||
if (plus.os.name.toLowerCase() === 'android') { |
|||
plus.runtime.quit(); |
|||
} else { |
|||
const threadClass = plus.ios.importClass("NSThread"); |
|||
const mainThread = plus.ios.invoke(threadClass, "mainThread"); |
|||
plus.ios.invoke(mainThread, "exit"); |
|||
} |
|||
// #endif |
|||
}, |
|||
methods: { |
|||
// cancel() { |
|||
// this.closeModal(); |
|||
// }, |
|||
confirm() { |
|||
plus.runtime.openURL(this.appUrl) |
|||
// this.closeModal(); |
|||
} |
|||
// , |
|||
// closeModal() { |
|||
// uni.navigateBack(); |
|||
// } |
|||
}, |
|||
computed: { |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
// .u-full-content { |
|||
// background-color: $primaryPageBoxColor !important; |
|||
// } |
|||
|
|||
.u-update-content { |
|||
font-size: 26rpx; |
|||
color: #7081FF; |
|||
line-height: 1.7; |
|||
padding: 30rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,54 @@ |
|||
<template> |
|||
<view class="u-gap" :style="[gapStyle]"></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* gap 间隔槽 |
|||
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量 |
|||
* @tutorial https://www.uviewui.com/components/gap.html |
|||
* @property {String} bg-color 背景颜色(默认#f3f4f6) |
|||
* @property {String Number} height 分割槽高度,单位rpx(默认30) |
|||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0) |
|||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0) |
|||
* @example <u-gap height="80" bg-color="#bbb"></u-gap> |
|||
*/ |
|||
export default { |
|||
name: "u-gap", |
|||
props: { |
|||
bgColor: { |
|||
type: String, |
|||
default: 'transparent ' // 背景透明 |
|||
}, |
|||
// 高度 |
|||
height: { |
|||
type: [String, Number], |
|||
default: 30 |
|||
}, |
|||
// 与上一个组件的距离 |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 与下一个组件的距离 |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
}, |
|||
computed: { |
|||
gapStyle() { |
|||
return { |
|||
backgroundColor: this.bgColor, |
|||
height: this.height + 'rpx', |
|||
marginTop: this.marginTop + 'rpx', |
|||
marginBottom: this.marginBottom + 'rpx' |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
|||
@ -0,0 +1,126 @@ |
|||
<template> |
|||
<view class="u-grid-item" :hover-class="parentData.hoverClass" |
|||
:hover-stay-time="200" @tap="click" :style="{ |
|||
background: bgColor, |
|||
width: width, |
|||
}"> |
|||
<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']"> |
|||
<slot /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* gridItem 提示 |
|||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用 |
|||
* @tutorial https://www.uviewui.com/components/grid.html |
|||
* @property {String} bg-color 宫格的背景颜色(默认#ffffff) |
|||
* @property {String Number} index 点击宫格时,返回的值 |
|||
* @property {Object} custom-style 自定义样式,对象形式 |
|||
* @event {Function} click 点击宫格触发 |
|||
* @example <u-grid-item></u-grid-item> |
|||
*/ |
|||
export default { |
|||
name: "u-grid-item", |
|||
props: { |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
}, |
|||
// 点击时返回的index |
|||
index: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 自定义样式,对象形式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
padding: '30rpx 0' |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
parentData: { |
|||
hoverClass: '', // 按下去的时候,是否显示背景灰色 |
|||
col: 3, // 父组件划分的宫格数 |
|||
border: true, // 是否显示边框,根据父组件决定 |
|||
} |
|||
}; |
|||
}, |
|||
created() { |
|||
// 父组件的实例 |
|||
this.updateParentData(); |
|||
// this.parent在updateParentData()中定义 |
|||
this.parent.children.push(this); |
|||
}, |
|||
computed: { |
|||
// 每个grid-item的宽度 |
|||
width() { |
|||
return 100 / Number(this.parentData.col) + '%'; |
|||
}, |
|||
}, |
|||
methods: { |
|||
// 获取父组件的参数 |
|||
updateParentData() { |
|||
// 此方法写在mixin中 |
|||
this.getParentData('u-grid'); |
|||
}, |
|||
click() { |
|||
this.$emit('click', this.index); |
|||
this.parent && this.parent.click(this.index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-grid-item { |
|||
box-sizing: border-box; |
|||
background: #fff; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: relative; |
|||
flex-direction: column; |
|||
|
|||
/* #ifdef MP */ |
|||
position: relative; |
|||
float: left; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.u-grid-item-hover { |
|||
background: #f7f7f7 !important; |
|||
} |
|||
|
|||
.u-grid-marker-box { |
|||
position: absolute; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
line-height: 0; |
|||
} |
|||
|
|||
.u-grid-marker-wrap { |
|||
position: absolute; |
|||
} |
|||
|
|||
.u-grid-item-box { |
|||
padding: 30rpx 0; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-direction: column; |
|||
flex: 1; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,108 @@ |
|||
<template> |
|||
<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* grid 宫格布局 |
|||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。 |
|||
* @tutorial https://www.uviewui.com/components/grid.html |
|||
* @property {String Number} col 宫格的列数(默认3) |
|||
* @property {Boolean} border 是否显示宫格的边框(默认true) |
|||
* @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false) |
|||
* @event {Function} click 点击宫格触发 |
|||
* @example <u-grid :col="3" @click="click"></u-grid> |
|||
*/ |
|||
export default { |
|||
name: 'u-grid', |
|||
props: { |
|||
// 分成几列 |
|||
col: { |
|||
type: [Number, String], |
|||
default: 3 |
|||
}, |
|||
// 是否显示边框 |
|||
border: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 |
|||
align: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// 宫格按压时的样式类,"none"为无效果 |
|||
hoverClass: { |
|||
type: String, |
|||
default: 'u-hover-class' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
index: 0, |
|||
} |
|||
}, |
|||
watch: { |
|||
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件 |
|||
parentData() { |
|||
if(this.children.length) { |
|||
this.children.map(child => { |
|||
// 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值) |
|||
typeof(child.updateParentData) == 'function' && child.updateParentData(); |
|||
}) |
|||
} |
|||
}, |
|||
}, |
|||
created() { |
|||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
computed: { |
|||
// 计算父组件的值是否发生变化 |
|||
parentData() { |
|||
return [this.hoverClass, this.col, this.size, this.border]; |
|||
}, |
|||
// 宫格对齐方式 |
|||
gridStyle() { |
|||
let style = {}; |
|||
switch(this.align) { |
|||
case 'left': |
|||
style.justifyContent = 'flex-start'; |
|||
break; |
|||
case 'center': |
|||
style.justifyContent = 'center'; |
|||
break; |
|||
case 'right': |
|||
style.justifyContent = 'flex-end'; |
|||
break; |
|||
default: style.justifyContent = 'flex-start'; |
|||
}; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
click(index) { |
|||
this.$emit('click', index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-grid { |
|||
width: 100%; |
|||
/* #ifdef MP */ |
|||
position: relative; |
|||
box-sizing: border-box; |
|||
overflow: hidden; |
|||
/* #endif */ |
|||
|
|||
/* #ifndef MP */ |
|||
@include vue-flex; |
|||
flex-wrap: wrap; |
|||
align-items: center; |
|||
/* #endif */ |
|||
} |
|||
</style> |
|||
@ -0,0 +1,336 @@ |
|||
<template> |
|||
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]"> |
|||
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image> |
|||
<text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass" |
|||
@touchstart="touchstart"> |
|||
<text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass" |
|||
class="u-icon__decimal"> |
|||
</text> |
|||
</text> |
|||
<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 --> |
|||
<text v-if="label !== ''" class="u-icon__label" :style="{ |
|||
color: labelColor, |
|||
fontSize: $u.addUnit(labelSize), |
|||
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0, |
|||
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0, |
|||
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0, |
|||
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0, |
|||
}">{{ label }} |
|||
</text> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* icon 图标 |
|||
* @description 基于字体的图标集,包含了大多数常见场景的图标。 |
|||
* @tutorial https://www.uviewui.com/components/icon.html |
|||
* @property {String} name 图标名称,见示例图标集 |
|||
* @property {String} color 图标颜色(默认inherit) |
|||
* @property {String | Number} size 图标字体大小,单位rpx(默认32) |
|||
* @property {String | Number} label-size label字体大小,单位rpx(默认28) |
|||
* @property {String} label 图标右侧的label文字(默认28) |
|||
* @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right) |
|||
* @property {String} label-color label字体颜色(默认#606266) |
|||
* @property {Object} custom-style icon的样式,对象形式 |
|||
* @property {String} custom-prefix 自定义字体图标库时,需要写上此值 |
|||
* @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6) |
|||
* @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right) |
|||
* @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出 |
|||
* @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网 |
|||
* @property {String} width 显示图片小图标时的宽度 |
|||
* @property {String} height 显示图片小图标时的高度 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {Boolean} show-decimal-icon 是否为DecimalIcon |
|||
* @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效 |
|||
* @property {String | Number} percent 显示的百分比,仅Decimal时有效 |
|||
* @event {Function} click 点击图标时触发 |
|||
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon> |
|||
*/ |
|||
export default { |
|||
name: 'u-icon', |
|||
props: { |
|||
// 图标类名 |
|||
name: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 图标颜色,可接受主题色 |
|||
color: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 字体大小,单位rpx |
|||
size: { |
|||
type: [Number, String], |
|||
default: 'inherit' |
|||
}, |
|||
// 是否显示粗体 |
|||
bold: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 点击图标的时候传递事件出去的index(用于区分点击了哪一个) |
|||
index: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 触摸图标时的类名 |
|||
hoverClass: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 自定义扩展前缀,方便用户扩展自己的图标库 |
|||
customPrefix: { |
|||
type: String, |
|||
default: 'uicon' |
|||
}, |
|||
// 图标右边或者下面的文字 |
|||
label: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// label的位置,只能右边或者下边 |
|||
labelPos: { |
|||
type: String, |
|||
default: 'right' |
|||
}, |
|||
// label的大小 |
|||
labelSize: { |
|||
type: [String, Number], |
|||
default: '28' |
|||
}, |
|||
// label的颜色 |
|||
labelColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// label与图标的距离(横向排列) |
|||
marginLeft: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginRight: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// 图片的mode |
|||
imgMode: { |
|||
type: String, |
|||
default: 'widthFix' |
|||
}, |
|||
// 自定义样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 用于显示图片小图标时,图片的宽度 |
|||
width: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 用于显示图片小图标时,图片的高度 |
|||
height: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 用于解决某些情况下,让图标垂直居中的用途 |
|||
top: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否为DecimalIcon |
|||
showDecimalIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 背景颜色,可接受主题色,仅Decimal时有效 |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#ececec' |
|||
}, |
|||
// 显示的百分比,仅Decimal时有效 |
|||
percent: { |
|||
type: [Number, String], |
|||
default: '50' |
|||
} |
|||
}, |
|||
computed: { |
|||
customClass() { |
|||
let classes = [] |
|||
classes.push(this.customPrefix + '-' + this.name) |
|||
// uView的自定义图标类名为u-iconfont |
|||
if (this.customPrefix == 'uicon') { |
|||
classes.push('u-iconfont') |
|||
} else { |
|||
classes.push(this.customPrefix) |
|||
} |
|||
// 主题色,通过类配置 |
|||
if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) { |
|||
classes.push('u-icon__icon--' + this.inactiveColor) |
|||
} else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color) |
|||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别 |
|||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名 |
|||
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU |
|||
classes = classes.join(' ') |
|||
//#endif |
|||
return classes |
|||
}, |
|||
iconStyle() { |
|||
let style = {} |
|||
style = { |
|||
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size), |
|||
fontWeight: this.bold ? 'bold' : 'normal', |
|||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中 |
|||
top: this.$u.addUnit(this.top) |
|||
} |
|||
// 非主题色值时,才当作颜色值 |
|||
if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) { |
|||
style.color = this.inactiveColor |
|||
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color |
|||
|
|||
return style |
|||
}, |
|||
// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式 |
|||
isImg() { |
|||
return this.name.indexOf('/') !== -1 |
|||
}, |
|||
imgStyle() { |
|||
let style = {} |
|||
// 如果设置width和height属性,则优先使用,否则使用size属性 |
|||
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size) |
|||
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size) |
|||
return style |
|||
}, |
|||
decimalIconStyle() { |
|||
let style = {} |
|||
style = { |
|||
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size), |
|||
fontWeight: this.bold ? 'bold' : 'normal', |
|||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中 |
|||
top: this.$u.addUnit(this.top), |
|||
width: this.percent + '%' |
|||
} |
|||
// 非主题色值时,才当作颜色值 |
|||
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color |
|||
return style |
|||
}, |
|||
decimalIconClass() { |
|||
let classes = [] |
|||
classes.push(this.customPrefix + '-' + this.name) |
|||
// uView的自定义图标类名为u-iconfont |
|||
if (this.customPrefix == 'uicon') { |
|||
classes.push('u-iconfont') |
|||
} else { |
|||
classes.push(this.customPrefix) |
|||
} |
|||
// 主题色,通过类配置 |
|||
if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color) |
|||
else classes.push('u-icon__icon--primary') |
|||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别 |
|||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名 |
|||
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU |
|||
classes = classes.join(' ') |
|||
//#endif |
|||
return classes |
|||
} |
|||
}, |
|||
methods: { |
|||
click() { |
|||
this.$emit('click', this.index) |
|||
}, |
|||
touchstart() { |
|||
this.$emit('touchstart', this.index) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
@import '../../iconfont.css'; |
|||
|
|||
.u-icon { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
|
|||
&--left { |
|||
flex-direction: row-reverse; |
|||
align-items: center; |
|||
} |
|||
|
|||
&--right { |
|||
flex-direction: row; |
|||
align-items: center; |
|||
} |
|||
|
|||
&--top { |
|||
flex-direction: column-reverse; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&--bottom { |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&__icon { |
|||
position: relative; |
|||
|
|||
&--primary { |
|||
color: $u-type-primary; |
|||
} |
|||
|
|||
&--success { |
|||
color: $u-type-success; |
|||
} |
|||
|
|||
&--error { |
|||
color: $u-type-error; |
|||
} |
|||
|
|||
&--warning { |
|||
color: $u-type-warning; |
|||
} |
|||
|
|||
&--info { |
|||
color: $u-type-info; |
|||
} |
|||
} |
|||
|
|||
&__decimal { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
display: inline-block; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
&__img { |
|||
height: auto; |
|||
will-change: transform; |
|||
} |
|||
|
|||
&__label { |
|||
line-height: 1; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,266 @@ |
|||
<template> |
|||
<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]"> |
|||
<image |
|||
v-if="!isError" |
|||
:src="src" |
|||
:mode="mode" |
|||
@error="onErrorHandler" |
|||
@load="onLoadHandler" |
|||
:lazy-load="lazyLoad" |
|||
class="u-image__image" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius) |
|||
}" |
|||
></image> |
|||
<view |
|||
v-if="showLoading && loading" |
|||
class="u-image__loading" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius), |
|||
backgroundColor: this.bgColor |
|||
}" |
|||
> |
|||
<slot v-if="$slots.loading" name="loading" /> |
|||
<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon> |
|||
</view> |
|||
<view |
|||
v-if="showError && isError && !loading" |
|||
class="u-image__error" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius) |
|||
}" |
|||
> |
|||
<slot v-if="$slots.error" name="error" /> |
|||
<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* Image 图片 |
|||
* @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。 |
|||
* @tutorial https://uviewui.com/components/image.html |
|||
* @property {String} src 图片地址 |
|||
* @property {String} mode 裁剪模式,见官网说明 |
|||
* @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%) |
|||
* @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto) |
|||
* @property {String} shape 图片形状,circle-圆形,square-方形(默认square) |
|||
* @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0) |
|||
* @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true) |
|||
* @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false) |
|||
* @property {String} loading-icon 加载中的图标,或者小图片(默认 photo) |
|||
* @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle) |
|||
* @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true) |
|||
* @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true) |
|||
* @property {Boolean} fade 是否需要淡入效果(默认 true) |
|||
* @property {String Number} width 传入图片路径时图片的宽度 |
|||
* @property {String Number} height 传入图片路径时图片的高度 |
|||
* @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false) |
|||
* @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500) |
|||
* @event {Function} click 点击图片时触发 |
|||
* @event {Function} error 图片加载失败时触发 |
|||
* @event {Function} load 图片加载成功时触发 |
|||
* @example <u-image width="100%" height="300rpx" :src="src"></u-image> |
|||
*/ |
|||
export default { |
|||
name: 'u-image', |
|||
props: { |
|||
// 图片地址 |
|||
src: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 裁剪模式 |
|||
mode: { |
|||
type: String, |
|||
default: 'aspectFill' |
|||
}, |
|||
// 宽度,单位任意 |
|||
width: { |
|||
type: [String, Number], |
|||
default: '100%' |
|||
}, |
|||
// 高度,单位任意 |
|||
height: { |
|||
type: [String, Number], |
|||
default: 'auto' |
|||
}, |
|||
// 图片形状,circle-圆形,square-方形 |
|||
shape: { |
|||
type: String, |
|||
default: 'square' |
|||
}, |
|||
// 圆角,单位任意 |
|||
borderRadius: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序 |
|||
lazyLoad: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 开启长按图片显示识别微信小程序码菜单 |
|||
showMenuByLongpress: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 加载中的图标,或者小图片 |
|||
loadingIcon: { |
|||
type: String, |
|||
default: 'photo' |
|||
}, |
|||
// 加载失败的图标,或者小图片 |
|||
errorIcon: { |
|||
type: String, |
|||
default: 'error-circle' |
|||
}, |
|||
// 是否显示加载中的图标或者自定义的slot |
|||
showLoading: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示加载错误的图标或者自定义的slot |
|||
showError: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否需要淡入效果 |
|||
fade: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 只支持网络资源,只对微信小程序有效 |
|||
webp: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 过渡时间,单位ms |
|||
duration: { |
|||
type: [String, Number], |
|||
default: 500 |
|||
}, |
|||
// 背景颜色,用于深色页面加载图片时,为了和背景色融合 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#f3f4f6' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// 图片是否加载错误,如果是,则显示错误占位图 |
|||
isError: false, |
|||
// 初始化组件时,默认为加载中状态 |
|||
loading: true, |
|||
// 不透明度,为了实现淡入淡出的效果 |
|||
opacity: 1, |
|||
// 过渡时间,因为props的值无法修改,故需要一个中间值 |
|||
durationTime: this.duration, |
|||
// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景 |
|||
backgroundStyle: {} |
|||
}; |
|||
}, |
|||
watch: { |
|||
src: { |
|||
immediate: true, |
|||
handler (n) { |
|||
if(!n) { |
|||
// 如果传入null或者'',或者false,或者undefined,标记为错误状态 |
|||
this.isError = true; |
|||
this.loading = false; |
|||
} else { |
|||
this.isError = false; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
wrapStyle() { |
|||
let style = {}; |
|||
// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位 |
|||
style.width = this.$u.addUnit(this.width); |
|||
style.height = this.$u.addUnit(this.height); |
|||
// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值 |
|||
style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius); |
|||
// 如果设置圆角,必须要有hidden,否则可能圆角无效 |
|||
style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible'; |
|||
if (this.fade) { |
|||
style.opacity = this.opacity; |
|||
style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`; |
|||
} |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击图片 |
|||
onClick() { |
|||
this.$emit('click'); |
|||
}, |
|||
// 图片加载失败 |
|||
onErrorHandler(err) { |
|||
this.loading = false; |
|||
this.isError = true; |
|||
this.$emit('error', err); |
|||
}, |
|||
// 图片加载完成,标记loading结束 |
|||
onLoadHandler() { |
|||
this.loading = false; |
|||
this.isError = false; |
|||
this.$emit('load'); |
|||
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色 |
|||
// 否则无需fade效果时,png图片依然能看到下方的背景色 |
|||
if (!this.fade) return this.removeBgColor(); |
|||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果 |
|||
this.opacity = 0; |
|||
// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色) |
|||
// 到图片展示的过程中的淡入效果 |
|||
this.durationTime = 0; |
|||
// 延时50ms,否则在浏览器H5,过渡效果无效 |
|||
setTimeout(() => { |
|||
this.durationTime = this.duration; |
|||
this.opacity = 1; |
|||
setTimeout(() => { |
|||
this.removeBgColor(); |
|||
}, this.durationTime); |
|||
}, 50); |
|||
}, |
|||
// 移除图片的背景色 |
|||
removeBgColor() { |
|||
// 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景 |
|||
this.backgroundStyle = { |
|||
backgroundColor: 'transparent' |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import '../../libs/css/style.components.scss'; |
|||
|
|||
.u-image { |
|||
position: relative; |
|||
transition: opacity 0.5s ease-in-out; |
|||
|
|||
&__image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
&__loading, |
|||
&__error { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: $u-bg-color; |
|||
color: $u-tips-color; |
|||
font-size: 46rpx; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,89 @@ |
|||
<template> |
|||
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" --> |
|||
<view> |
|||
<view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]"> |
|||
<view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]"> |
|||
<slot v-if="useSlot" /> |
|||
<block v-else> |
|||
<text>{{ index }}</text> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* indexAnchor 索引列表锚点 |
|||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用 |
|||
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props |
|||
* @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false) |
|||
* @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效 |
|||
* @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}" |
|||
* @event {Function} default 锚点位置显示内容,默认为索引字符 |
|||
* @example <u-index-anchor :index="item" /> |
|||
*/ |
|||
export default { |
|||
name: "u-index-anchor", |
|||
props: { |
|||
useSlot: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
index: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
customStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
active: false, |
|||
wrapperStyle: {}, |
|||
anchorStyle: {} |
|||
} |
|||
}, |
|||
created() { |
|||
this.parent = false; |
|||
}, |
|||
mounted() { |
|||
this.parent = this.$u.$parent.call(this, 'u-index-list'); |
|||
if(this.parent) { |
|||
this.parent.children.push(this); |
|||
this.parent.updateData(); |
|||
} |
|||
}, |
|||
computed: { |
|||
customAnchorStyle() { |
|||
return Object.assign(this.anchorStyle, this.customStyle); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-index-anchor { |
|||
box-sizing: border-box; |
|||
padding: 14rpx 24rpx; |
|||
color: #606266; |
|||
width: 100%; |
|||
font-weight: 500; |
|||
font-size: 28rpx; |
|||
line-height: 1.2; |
|||
background-color: rgb(245, 245, 245); |
|||
} |
|||
|
|||
.u-index-anchor--active { |
|||
right: 0; |
|||
left: 0; |
|||
color: #2979ff; |
|||
background-color: #fff; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,315 @@ |
|||
<template> |
|||
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" --> |
|||
<view> |
|||
<view class="u-index-bar"> |
|||
<slot /> |
|||
<view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove" |
|||
@touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop"> |
|||
<view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}" |
|||
:data-index="index"> |
|||
{{ item }} |
|||
</view> |
|||
</view> |
|||
<view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{ |
|||
zIndex: alertZIndex |
|||
}"> |
|||
<text>{{indexList[touchmoveIndex]}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
var indexList = function() { |
|||
var indexList = []; |
|||
var charCodeOfA = 'A'.charCodeAt(0); |
|||
for (var i = 0; i < 26; i++) { |
|||
indexList.push(String.fromCharCode(charCodeOfA + i)); |
|||
} |
|||
return indexList; |
|||
}; |
|||
|
|||
/** |
|||
* indexList 索引列表 |
|||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用 |
|||
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props |
|||
* @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入 |
|||
* @property {Array} index-list 索引字符列表,数组(默认A-Z) |
|||
* @property {Number String} z-index 锚点吸顶时的层级(默认965) |
|||
* @property {Boolean} sticky 是否开启锚点自动吸顶(默认true) |
|||
* @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0) |
|||
* @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff) |
|||
* @event {Function} select 选中右边索引字符时触发 |
|||
* @example <u-index-list :scrollTop="scrollTop"></u-index-list> |
|||
*/ |
|||
export default { |
|||
name: "u-index-list", |
|||
props: { |
|||
sticky: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
scrollTop: { |
|||
type: [Number, String], |
|||
default: 0, |
|||
}, |
|||
offsetTop: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
indexList: { |
|||
type: Array, |
|||
default () { |
|||
return indexList() |
|||
} |
|||
}, |
|||
activeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
} |
|||
}, |
|||
created() { |
|||
// #ifdef H5 |
|||
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44; |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0; |
|||
// #endif |
|||
// 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
data() { |
|||
return { |
|||
activeAnchorIndex: 0, |
|||
showSidebar: true, |
|||
// children: [], |
|||
touchmove: false, |
|||
touchmoveIndex: 0, |
|||
} |
|||
}, |
|||
watch: { |
|||
scrollTop() { |
|||
this.updateData() |
|||
} |
|||
}, |
|||
computed: { |
|||
// 弹出toast的z-index值 |
|||
alertZIndex() { |
|||
return this.$u.zIndex.toast; |
|||
} |
|||
}, |
|||
methods: { |
|||
updateData() { |
|||
this.timer && clearTimeout(this.timer); |
|||
this.timer = setTimeout(() => { |
|||
this.showSidebar = !!this.children.length; |
|||
this.setRect().then(() => { |
|||
this.onScroll(); |
|||
}); |
|||
}, 0); |
|||
}, |
|||
setRect() { |
|||
return Promise.all([ |
|||
this.setAnchorsRect(), |
|||
this.setListRect(), |
|||
this.setSiderbarRect() |
|||
]); |
|||
}, |
|||
setAnchorsRect() { |
|||
return Promise.all(this.children.map((anchor, index) => anchor |
|||
.$uGetRect('.u-index-anchor-wrapper') |
|||
.then((rect) => { |
|||
Object.assign(anchor, { |
|||
height: rect.height, |
|||
top: rect.top |
|||
}); |
|||
}))); |
|||
}, |
|||
setListRect() { |
|||
return this.$uGetRect('.u-index-bar').then((rect) => { |
|||
Object.assign(this, { |
|||
height: rect.height, |
|||
top: rect.top + this.scrollTop |
|||
}); |
|||
}); |
|||
}, |
|||
setSiderbarRect() { |
|||
return this.$uGetRect('.u-index-bar__sidebar').then(rect => { |
|||
this.sidebar = { |
|||
height: rect.height, |
|||
top: rect.top |
|||
}; |
|||
}); |
|||
}, |
|||
getActiveAnchorIndex() { |
|||
const { |
|||
children |
|||
} = this; |
|||
const { |
|||
sticky |
|||
} = this; |
|||
for (let i = this.children.length - 1; i >= 0; i--) { |
|||
const preAnchorHeight = i > 0 ? children[i - 1].height : 0; |
|||
const reachTop = sticky ? preAnchorHeight : 0; |
|||
if (reachTop >= children[i].top) { |
|||
return i; |
|||
} |
|||
} |
|||
return -1; |
|||
}, |
|||
onScroll() { |
|||
const { |
|||
children = [] |
|||
} = this; |
|||
if (!children.length) { |
|||
return; |
|||
} |
|||
const { |
|||
sticky, |
|||
stickyOffsetTop, |
|||
zIndex, |
|||
scrollTop, |
|||
activeColor |
|||
} = this; |
|||
const active = this.getActiveAnchorIndex(); |
|||
this.activeAnchorIndex = active; |
|||
if (sticky) { |
|||
let isActiveAnchorSticky = false; |
|||
if (active !== -1) { |
|||
isActiveAnchorSticky = |
|||
children[active].top <= 0; |
|||
} |
|||
children.forEach((item, index) => { |
|||
if (index === active) { |
|||
let wrapperStyle = ''; |
|||
let anchorStyle = { |
|||
color: `${activeColor}` |
|||
}; |
|||
if (isActiveAnchorSticky) { |
|||
wrapperStyle = { |
|||
height: `${children[index].height}px` |
|||
}; |
|||
anchorStyle = { |
|||
position: 'fixed', |
|||
top: `${stickyOffsetTop}px`, |
|||
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`, |
|||
color: `${activeColor}` |
|||
}; |
|||
} |
|||
item.active = active; |
|||
item.wrapperStyle = wrapperStyle; |
|||
item.anchorStyle = anchorStyle; |
|||
} else if (index === active - 1) { |
|||
const currentAnchor = children[index]; |
|||
const currentOffsetTop = currentAnchor.top; |
|||
const targetOffsetTop = index === children.length - 1 ? |
|||
this.top : |
|||
children[index + 1].top; |
|||
const parentOffsetHeight = targetOffsetTop - currentOffsetTop; |
|||
const translateY = parentOffsetHeight - currentAnchor.height; |
|||
const anchorStyle = { |
|||
position: 'relative', |
|||
transform: `translate3d(0, ${translateY}px, 0)`, |
|||
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`, |
|||
color: `${activeColor}` |
|||
}; |
|||
item.active = active; |
|||
item.anchorStyle = anchorStyle; |
|||
} else { |
|||
item.active = false; |
|||
item.anchorStyle = ''; |
|||
item.wrapperStyle = ''; |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
onTouchMove(event) { |
|||
this.touchmove = true; |
|||
const sidebarLength = this.children.length; |
|||
const touch = event.touches[0]; |
|||
const itemHeight = this.sidebar.height / sidebarLength; |
|||
let clientY = 0; |
|||
clientY = touch.clientY; |
|||
let index = Math.floor((clientY - this.sidebar.top) / itemHeight); |
|||
if (index < 0) { |
|||
index = 0; |
|||
} else if (index > sidebarLength - 1) { |
|||
index = sidebarLength - 1; |
|||
} |
|||
this.touchmoveIndex = index; |
|||
this.scrollToAnchor(index); |
|||
}, |
|||
onTouchStop() { |
|||
this.touchmove = false; |
|||
this.scrollToAnchorIndex = null; |
|||
}, |
|||
scrollToAnchor(index) { |
|||
if (this.scrollToAnchorIndex === index) { |
|||
return; |
|||
} |
|||
this.scrollToAnchorIndex = index; |
|||
const anchor = this.children.find((item) => item.index === this.indexList[index]); |
|||
if (anchor) { |
|||
this.$emit('select', anchor.index); |
|||
uni.pageScrollTo({ |
|||
duration: 0, |
|||
scrollTop: anchor.top + this.scrollTop |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-index-bar { |
|||
position: relative |
|||
} |
|||
|
|||
.u-index-bar__sidebar { |
|||
position: fixed; |
|||
top: 50%; |
|||
right: 0; |
|||
@include vue-flex; |
|||
flex-direction: column; |
|||
text-align: center; |
|||
transform: translateY(-50%); |
|||
user-select: none; |
|||
z-index: 99; |
|||
} |
|||
|
|||
.u-index-bar__index { |
|||
font-weight: 500; |
|||
padding: 8rpx 18rpx; |
|||
font-size: 22rpx; |
|||
line-height: 1 |
|||
} |
|||
|
|||
.u-indexed-list-alert { |
|||
position: fixed; |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
right: 90rpx; |
|||
top: 50%; |
|||
margin-top: -60rpx; |
|||
border-radius: 24rpx; |
|||
font-size: 50rpx; |
|||
color: #fff; |
|||
background-color: rgba(0, 0, 0, 0.65); |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0; |
|||
z-index: 9999999; |
|||
} |
|||
|
|||
.u-indexed-list-alert text { |
|||
line-height: 50rpx; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,387 @@ |
|||
<template> |
|||
<view |
|||
class="u-input" |
|||
:class="{ |
|||
'u-input--border': border, |
|||
'u-input--error': validateState |
|||
}" |
|||
:style="{ |
|||
padding: `0 ${border ? 20 : 0}rpx`, |
|||
borderColor: borderColor, |
|||
textAlign: inputAlign |
|||
}" |
|||
@tap.stop="inputClick" |
|||
> |
|||
<textarea |
|||
v-if="type == 'textarea'" |
|||
class="u-input__input u-input__textarea" |
|||
:style="[getStyle]" |
|||
:value="defaultValue" |
|||
:placeholder="placeholder" |
|||
:placeholderStyle="placeholderStyle" |
|||
:disabled="disabled" |
|||
:maxlength="inputMaxlength" |
|||
:fixed="fixed" |
|||
:focus="focus" |
|||
:autoHeight="autoHeight" |
|||
:selection-end="uSelectionEnd" |
|||
:selection-start="uSelectionStart" |
|||
:cursor-spacing="getCursorSpacing" |
|||
:show-confirm-bar="showConfirmbar" |
|||
@input="handleInput" |
|||
@blur="handleBlur" |
|||
@focus="onFocus" |
|||
@confirm="onConfirm" |
|||
/> |
|||
<input |
|||
v-else |
|||
class="u-input__input" |
|||
:type="type == 'password' ? 'text' : type" |
|||
:style="[getStyle]" |
|||
:value="defaultValue" |
|||
:password="type == 'password' && !showPassword" |
|||
:placeholder="placeholder" |
|||
:placeholderStyle="placeholderStyle" |
|||
:disabled="disabled || type === 'select'" |
|||
:maxlength="inputMaxlength" |
|||
:focus="focus" |
|||
:confirmType="confirmType" |
|||
:cursor-spacing="getCursorSpacing" |
|||
:selection-end="uSelectionEnd" |
|||
:selection-start="uSelectionStart" |
|||
:show-confirm-bar="showConfirmbar" |
|||
@focus="onFocus" |
|||
@blur="handleBlur" |
|||
@input="handleInput" |
|||
@confirm="onConfirm" |
|||
/> |
|||
<view class="u-input__right-icon u-flex"> |
|||
<view class="u-input__right-icon__clear u-input__right-icon__item" @tap="onClear" v-if="clearable && value != '' && focused"> |
|||
<u-icon size="32" name="close-circle-fill" color="#c0c4cc"/> |
|||
</view> |
|||
<view class="u-input__right-icon__clear u-input__right-icon__item" v-if="passwordIcon && type == 'password'"> |
|||
<u-icon size="32" :name="!showPassword ? 'eye' : 'eye-fill'" color="#c0c4cc" @click="showPassword = !showPassword"/> |
|||
</view> |
|||
<view class="u-input__right-icon--select u-input__right-icon__item" v-if="type == 'select'" :class="{ |
|||
'u-input__right-icon--select--reverse': selectOpen |
|||
}"> |
|||
<u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import Emitter from '../../libs/util/emitter.js'; |
|||
|
|||
/** |
|||
* input 输入框 |
|||
* @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。 |
|||
* @tutorial http://uviewui.com/components/input.html |
|||
* @property {String} type 模式选择,见官网说明 |
|||
* @property {Boolean} clearable 是否显示右侧的清除图标(默认true) |
|||
* @property {} v-model 用于双向绑定输入框的值 |
|||
* @property {String} input-align 输入框文字的对齐方式(默认left) |
|||
* @property {String} placeholder placeholder显示值(默认 '请输入内容') |
|||
* @property {Boolean} disabled 是否禁用输入框(默认false) |
|||
* @property {String Number} maxlength 输入框的最大可输入长度(默认140) |
|||
* @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1) |
|||
* @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1) |
|||
* @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0) |
|||
* @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;") |
|||
* @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done) |
|||
* @property {Object} custom-style 自定义输入框的样式,对象形式 |
|||
* @property {Boolean} focus 是否自动获得焦点(默认false) |
|||
* @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false) |
|||
* @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true) |
|||
* @property {Boolean} border 是否显示边框(默认false) |
|||
* @property {String} border-color 输入框的边框颜色(默认#dcdfe6) |
|||
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true) |
|||
* @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100) |
|||
* @example <u-input v-model="value" :type="type" :border="border" /> |
|||
*/ |
|||
export default { |
|||
name: 'u-input', |
|||
mixins: [Emitter], |
|||
props: { |
|||
value: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 输入框的类型,textarea,text,number |
|||
type: { |
|||
type: String, |
|||
default: 'text' |
|||
}, |
|||
inputAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
placeholder: { |
|||
type: String, |
|||
default: '请输入内容' |
|||
}, |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
maxlength: { |
|||
type: [Number, String], |
|||
default: 140 |
|||
}, |
|||
placeholderStyle: { |
|||
type: String, |
|||
default: 'color: #c0c4cc;' |
|||
}, |
|||
confirmType: { |
|||
type: String, |
|||
default: 'done' |
|||
}, |
|||
// 输入框的自定义样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true |
|||
fixed: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否自动获得焦点 |
|||
focus: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 密码类型时,是否显示右侧的密码图标 |
|||
passwordIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// input|textarea是否显示边框 |
|||
border: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 输入框的边框颜色 |
|||
borderColor: { |
|||
type: String, |
|||
default: '#dcdfe6' |
|||
}, |
|||
autoHeight: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态 |
|||
// open-打开,close-关闭 |
|||
selectOpen: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 高度,单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 是否可清空 |
|||
clearable: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 指定光标与键盘的距离,单位 px |
|||
cursorSpacing: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 光标起始位置,自动聚焦时有效,需与selection-end搭配使用 |
|||
selectionStart: { |
|||
type: [Number, String], |
|||
default: -1 |
|||
}, |
|||
// 光标结束位置,自动聚焦时有效,需与selection-start搭配使用 |
|||
selectionEnd: { |
|||
type: [Number, String], |
|||
default: -1 |
|||
}, |
|||
// 是否自动去除两端的空格 |
|||
trim: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示键盘上方带有”完成“按钮那一栏 |
|||
showConfirmbar:{ |
|||
type:Boolean, |
|||
default:true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
defaultValue: this.value, |
|||
inputHeight: 70, // input的高度 |
|||
textareaHeight: 100, // textarea的高度 |
|||
validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色 |
|||
focused: false, // 当前是否处于获得焦点的状态 |
|||
showPassword: false, // 是否预览密码 |
|||
lastValue: '', // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间 |
|||
}; |
|||
}, |
|||
watch: { |
|||
value(nVal, oVal) { |
|||
this.defaultValue = nVal; |
|||
// 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件 |
|||
if(nVal != oVal && this.type == 'select') this.handleInput({ |
|||
detail: { |
|||
value: nVal |
|||
} |
|||
}) |
|||
}, |
|||
}, |
|||
computed: { |
|||
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值 |
|||
inputMaxlength() { |
|||
return Number(this.maxlength); |
|||
}, |
|||
getStyle() { |
|||
let style = {}; |
|||
// 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度 |
|||
style.minHeight = this.height ? this.height + 'rpx' : this.type == 'textarea' ? |
|||
this.textareaHeight + 'rpx' : this.inputHeight + 'rpx'; |
|||
style = Object.assign(style, this.customStyle); |
|||
return style; |
|||
}, |
|||
// |
|||
getCursorSpacing() { |
|||
return Number(this.cursorSpacing); |
|||
}, |
|||
// 光标起始位置 |
|||
uSelectionStart() { |
|||
return String(this.selectionStart); |
|||
}, |
|||
// 光标结束位置 |
|||
uSelectionEnd() { |
|||
return String(this.selectionEnd); |
|||
} |
|||
}, |
|||
created() { |
|||
// 监听u-form-item发出的错误事件,将输入框边框变红色 |
|||
this.$on('on-form-item-error', this.onFormItemError); |
|||
}, |
|||
methods: { |
|||
/** |
|||
* change 事件 |
|||
* @param event |
|||
*/ |
|||
handleInput(event) { |
|||
let value = event.detail.value; |
|||
// 判断是否去除空格 |
|||
if(this.trim) value = this.$u.trim(value); |
|||
// vue 原生的方法 return 出去 |
|||
this.$emit('input', value); |
|||
// 当前model 赋值 |
|||
this.defaultValue = value; |
|||
// 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上 |
|||
// 尚未更新到u-form-item,导致获取的值为空,从而校验混论 |
|||
// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱 |
|||
setTimeout(() => { |
|||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 |
|||
// #ifdef MP-TOUTIAO |
|||
if(this.$u.trim(value) == this.lastValue) return ; |
|||
this.lastValue = value; |
|||
// #endif |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch('u-form-item', 'on-form-change', value); |
|||
}, 40) |
|||
}, |
|||
/** |
|||
* blur 事件 |
|||
* @param event |
|||
*/ |
|||
handleBlur(event) { |
|||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错 |
|||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时 |
|||
setTimeout(() => { |
|||
this.focused = false; |
|||
}, 100) |
|||
// vue 原生的方法 return 出去 |
|||
this.$emit('blur', event.detail.value); |
|||
setTimeout(() => { |
|||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 |
|||
// #ifdef MP-TOUTIAO |
|||
if(this.$u.trim(value) == this.lastValue) return ; |
|||
this.lastValue = value; |
|||
// #endif |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch('u-form-item', 'on-form-blur', event.detail.value); |
|||
}, 40) |
|||
}, |
|||
onFormItemError(status) { |
|||
this.validateState = status; |
|||
}, |
|||
onFocus(event) { |
|||
this.focused = true; |
|||
this.$emit('focus'); |
|||
}, |
|||
onConfirm(e) { |
|||
this.$emit('confirm', e.detail.value); |
|||
}, |
|||
onClear(event) { |
|||
this.$emit('input', ''); |
|||
}, |
|||
inputClick() { |
|||
this.$emit('click'); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-input { |
|||
position: relative; |
|||
flex: 1; |
|||
@include vue-flex; |
|||
|
|||
&__input { |
|||
//height: $u-form-item-height; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
flex: 1; |
|||
} |
|||
|
|||
&__textarea { |
|||
width: auto; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
padding: 10rpx 0; |
|||
line-height: normal; |
|||
flex: 1; |
|||
} |
|||
|
|||
&--border { |
|||
border-radius: 6rpx; |
|||
border-radius: 4px; |
|||
border: 1px solid $u-form-item-border-color; |
|||
} |
|||
|
|||
&--error { |
|||
border-color: $u-type-error!important; |
|||
} |
|||
|
|||
&__right-icon { |
|||
|
|||
&__item { |
|||
margin-left: 10rpx; |
|||
} |
|||
|
|||
&--select { |
|||
transition: transform .4s; |
|||
|
|||
&--reverse { |
|||
transform: rotate(-180deg); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,217 @@ |
|||
<template> |
|||
<u-popup class="" :mask="mask" :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" |
|||
:safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :zIndex="uZIndex"> |
|||
<slot /> |
|||
<view class="u-tooltip" v-if="tooltip"> |
|||
<view class="u-tooltip-item u-tooltip-cancel" hover-class="u-tooltip-cancel-hover" @tap="onCancel"> |
|||
{{cancelBtn ? cancelText : ''}} |
|||
</view> |
|||
<view v-if="showTips" class="u-tooltip-item u-tooltip-tips"> |
|||
{{tips ? tips : mode == 'number' ? '数字键盘' : mode == 'card' ? '身份证键盘' : '车牌号键盘'}} |
|||
</view> |
|||
<view v-if="confirmBtn" @tap="onConfirm" class="u-tooltip-item u-tooltips-submit" hover-class="u-tooltips-submit-hover"> |
|||
{{confirmBtn ? confirmText : ''}} |
|||
</view> |
|||
</view> |
|||
<block v-if="mode == 'number' || mode == 'card'"> |
|||
<u-number-keyboard :random="random" @backspace="backspace" @change="change" :mode="mode" :dotEnabled="dotEnabled"></u-number-keyboard> |
|||
</block> |
|||
<block v-else> |
|||
<u-car-keyboard :random="random" @backspace="backspace" @change="change"></u-car-keyboard> |
|||
</block> |
|||
</u-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* keyboard 键盘 |
|||
* @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。 |
|||
* @tutorial https://www.uviewui.com/components/keyboard.html |
|||
* @property {String} mode 键盘类型,见官网基本使用的说明(默认number) |
|||
* @property {Boolean} dot-enabled 是否显示"."按键,只在mode=number时有效(默认true) |
|||
* @property {Boolean} tooltip 是否显示键盘顶部工具条(默认true) |
|||
* @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符 |
|||
* @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮(默认true) |
|||
* @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮(默认true) |
|||
* @property {Boolean} mask 是否显示遮罩(默认true) |
|||
* @property {String} confirm-text 确认按钮的文字 |
|||
* @property {String} cancel-text 取消按钮的文字 |
|||
* @property {Number String} z-index 弹出键盘的z-index值(默认1075) |
|||
* @property {Boolean} random 是否打乱键盘按键的顺序(默认false) |
|||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false) |
|||
* @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘(默认true) |
|||
* @event {Function} change 按键被点击(不包含退格键被点击) |
|||
* @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击 |
|||
* @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击 |
|||
* @event {Function} backspace 键盘退格键被点击 |
|||
* @example <u-keyboard mode="number" v-model="show"></u-keyboard> |
|||
*/ |
|||
export default { |
|||
name: "u-keyboard", |
|||
props: { |
|||
// 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘 |
|||
mode: { |
|||
type: String, |
|||
default: 'number' |
|||
}, |
|||
// 是否显示键盘的"."符号 |
|||
dotEnabled: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示顶部工具条 |
|||
tooltip: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示工具条中间的提示 |
|||
showTips: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 工具条中间的提示文字 |
|||
tips: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示工具条左边的"取消"按钮 |
|||
cancelBtn: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示工具条右边的"完成"按钮 |
|||
confirmBtn: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否打乱键盘按键的顺序 |
|||
random: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 |
|||
safeAreaInsetBottom: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否允许通过点击遮罩关闭键盘 |
|||
maskCloseAble: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 通过双向绑定控制键盘的弹出与收起 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩 |
|||
mask: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// z-index值 |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 取消按钮的文字 |
|||
cancelText: { |
|||
type: String, |
|||
default: '取消' |
|||
}, |
|||
// 确认按钮的文字 |
|||
confirmText: { |
|||
type: String, |
|||
default: '确认' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
//show: false |
|||
} |
|||
}, |
|||
computed: { |
|||
uZIndex() { |
|||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup; |
|||
} |
|||
}, |
|||
methods: { |
|||
change(e) { |
|||
this.$emit('change', e); |
|||
}, |
|||
// 键盘关闭 |
|||
popupClose() { |
|||
// 通过发送input这个特殊的事件名,可以修改父组件传给props的value的变量,也即双向绑定 |
|||
this.$emit('input', false); |
|||
}, |
|||
// 输入完成 |
|||
onConfirm() { |
|||
this.popupClose(); |
|||
this.$emit('confirm'); |
|||
}, |
|||
// 取消输入 |
|||
onCancel() { |
|||
this.popupClose(); |
|||
this.$emit('cancel'); |
|||
}, |
|||
// 退格键 |
|||
backspace() { |
|||
this.$emit('backspace'); |
|||
}, |
|||
// 关闭键盘 |
|||
// close() { |
|||
// this.show = false; |
|||
// }, |
|||
// // 打开键盘 |
|||
// open() { |
|||
// this.show = true; |
|||
// } |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-keyboard { |
|||
position: relative; |
|||
z-index: 1003; |
|||
} |
|||
|
|||
.u-tooltip { |
|||
@include vue-flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.u-tooltip-item { |
|||
color: #333333; |
|||
flex: 0 0 33.333333%; |
|||
text-align: center; |
|||
padding: 20rpx 10rpx; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.u-tooltips-submit { |
|||
text-align: right; |
|||
flex-grow: 1; |
|||
flex-wrap: 0; |
|||
padding-right: 40rpx; |
|||
color: $u-type-primary; |
|||
} |
|||
|
|||
.u-tooltip-cancel { |
|||
text-align: left; |
|||
flex-grow: 1; |
|||
flex-wrap: 0; |
|||
padding-left: 40rpx; |
|||
color: #888888; |
|||
} |
|||
|
|||
.u-tooltips-submit-hover { |
|||
color: $u-type-success; |
|||
} |
|||
|
|||
.u-tooltip-cancel-hover { |
|||
color: #333333; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,244 @@ |
|||
<template> |
|||
<view class="u-wrap" :style="{ |
|||
opacity: Number(opacity), |
|||
borderRadius: borderRadius + 'rpx', |
|||
// 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值) |
|||
transition: `opacity ${time / 1000}s ease-in-out` |
|||
}" |
|||
:class="'u-lazy-item-' + elIndex"> |
|||
<view :class="'u-lazy-item-' + elIndex"> |
|||
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" v-if="!isError" class="u-lazy-item" |
|||
:src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded" @error="loadError" @tap="clickImg"></image> |
|||
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" class="u-lazy-item error" v-else :src="errorImg" |
|||
:mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* lazyLoad 懒加载 |
|||
* @description 懒加载使用的场景为:页面有很多图片时,APP会同时加载所有的图片,导致页面卡顿,各个位置的图片出现前后不一致等. |
|||
* @tutorial https://www.uviewui.com/components/lazyLoad.html |
|||
* @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片 |
|||
* @property {String} image 图片路径 |
|||
* @property {String} loading-img 预加载时的占位图 |
|||
* @property {String} error-img 图片加载出错时的占位图 |
|||
* @property {String} threshold 触发加载时的位置,见上方说明,单位 rpx(默认300) |
|||
* @property {String Number} duration 图片加载成功时,淡入淡出时间,单位ms(默认) |
|||
* @property {String} effect 图片加载成功时,淡入淡出的css动画效果(默认ease-in-out) |
|||
* @property {Boolean} is-effect 图片加载成功时,是否启用淡入淡出效果(默认true) |
|||
* @property {String Number} border-radius 图片圆角值,单位rpx(默认0) |
|||
* @property {String Number} height 图片高度,注意:实际高度可能受img-mode参数影响(默认450) |
|||
* @property {String Number} mg-mode 图片的裁剪模式,详见image组件裁剪模式(默认widthFix) |
|||
* @event {Function} click 点击图片时触发 |
|||
* @event {Function} load 图片加载成功时触发 |
|||
* @event {Function} error 图片加载失败时触发 |
|||
* @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load> |
|||
*/ |
|||
export default { |
|||
name: 'u-lazy-load', |
|||
props: { |
|||
index: { |
|||
type: [Number, String] |
|||
}, |
|||
// 要显示的图片 |
|||
image: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 图片裁剪模式 |
|||
imgMode: { |
|||
type: String, |
|||
default: 'widthFix' |
|||
}, |
|||
// 占位图片路径 |
|||
loadingImg: { |
|||
type: String, |
|||
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII=' |
|||
}, |
|||
// 加载失败的错误占位图 |
|||
errorImg: { |
|||
type: String, |
|||
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162trcPDw5ubm7i4uNzc3Obm5s3NzaGhoeHh4cjIyKenp9LS0r29vbKysuvr67sDMEkAAAlpSURBVHja7NzpYqMgAIVRUVHc8/5PO66R1WAbOzX97q+ZtDEpR0AWTR7kVyWhCAAhgABCAAGEAAIIAQQQAggBBBACCCAEEEAIIIAQQAgggBBAACGAAEIAAYQAQgABhAACCAEEEAIIIAQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAsqeX5QWHKIcs/Ptl03lfL4zDFPWfBGmSpPn+IZzSH5KkCL5B+n+oklwz6Iz//R2QzFOabzhEmiRirAmZt/bl0w/dpMbLqeeo4wEdpC7zR5WAPKziHKtO7ql+ReKvIa9BxgNaL5ZtEkpeAGIVp5jKJa09xVo9vgSSzQcszdYvmOqjQNSQ6pHK6rO1n1Xj32788miwHLaZz1Tl9i/yayDlYJ/60/+lp8GSY7OY1B8E4p55bWmfquFk22GLuUUxi78cX+m+BjL2GLkhMrV+/muS6Sfic0CEp5T1Yu2OQdTzsKV0MJV73KVjroyTffxfuv5Tf3fd6iLT9wz8YdVHgUzF2Is9/Xhi5sYJqP1w/GUpjOiHVbaI0w2L+pg3GZzvtokcgHxWDXHaiy78l3sPke01qphamT5c+dqyeAGSumdL/mkggauTam0e3L/mPEiqtzKDbl0Z1Wn8xOa4ySo8X/7TQIJnY/seEKWf12UmC72CKP9xYjr19RPT7NNA+oMO+R0gwmlotAry+C6I0f59ch8yXVQOr0BKYcXt1IUYRyCt+Ur9HGsrQKI79WY9sY9ARPKlzFOFdb41ioD8b5Bp+mqeeRKAxINkESBFGpOpKhgv9OuYpH8A8l4Qa3qp60Kl2/k+rG2sWafuuyCBafb2j4JkgZUob3nWcmicpkxEgmTLLGejTxnWSWCi8lPmsk6DlIHFJv24ojiYyYoGacwL8zXTLEAVaDI/Ybb3NIgKDSv2oXpmHkvNs+PTpMASEdlk7fOZeRk37fwJ6yGnQarQsGIfqqcvx43rTOXY6jf7uKXdRzdLDRPbjIrx1cIj3Kr4KyBFezzgUGuR5893qkOQ19fR2uVBaU+r16LphJNOiatK7PeBZK/Kb+tUn71rcQjSvARpghfH/yG/D2RetTuI3N5QrMWdP46brP7FmKZ//CGQ9At9SL01DLkzY/Vs8Z97fQZ7gelw7jHqCz+/Wile5J4g3Vc79eb5a6oLSue+Ve83gaSv2jp5PxCzjzwFUm9zw9MllSMil1kS4d2E9SaQ1xNo9wMxx0+nQNLnew/WDHvveMAHYm08mofl3TFI/8pD3Q6kMAv6DIi2jTCwRJUvNdDYrrJum9oHhusCbWALonwxBRk1vXMnEGWuT5wAmfYuVGUYpJ7fUZujCN92hvzwWlrFgxSfANKb10DxIMbShnfrynyZZV30imA7P43ArXXHbvBVkTCIuGy25AdBrHmNeBCpL214QdLp9LZarG3IMWrmW0ehtuO7F2PS09UcgqS3B7FKPhpknrStD0HGF/vQRne37LwLG8EbHT4WxN7/Fg0yD9Yr/3br4nnstA+0Il6QxzdBmg8A6a2/IRbkcK9h/uzV8zywF/oSkOyageCPglRWgcWClHnEzs9q/t/SENVXgFijlsq3VtXdCsRp4qObrLLLgjuzSq3fX89ZZW6AfxNIzF6X9FYgThN/fk093KkvHX/hbWd+DqS/FUhlf+G3gohEXzVs3g9iDluWoaW8fL73QhB34u+tIHIf19nLuF4Q98a09Eynnl56q+ePgEhnX+dbQOp6H5XnJ0ACd8dFgkwf12nTOTcEqd2pom+CFF02TIPw6dKmrLS5qOtBpo8b5quUtrwrSGbuqPkeSJqllTFHO02NPxdMrm+y5LKdWyWXjw4vA5nGEtnjuyCFyHqNYvEolzmASm3zK1Eg5zr13lhqV1tlksnVw8Pkwgri7O07AVKLJkutRYw87bPlRpBpNXE8xGb+fhBlvEGrGPLqViu5sILIx9dAmqF1705sxF4M8+R8P5dOdQwi12fMnATpjJ2JSt/POIvU9wPJEs/jduJAjLvU0yFT0i64Yb1bsVi79dA4pEy3TzoHMq2O7Re4vXm5O9+l290NpE4CU+YRIMNye2iaqbVS2AUnn2fsekthYKReVNutVedA5juttyIXrT38mOds+ps9DWhwL7GWc61/DVKPzVN9UHDarf1icU98IOU8tm6L031Iq63t1tKzj3fe/FCpO4F0/i0Z2+yvA1KeGBjqj1qYx8/zoxpKZ1Yl367I1k+sfcft/QPy9csXy/32qX1qLZsrryG5BGQaRj0vc/b7N54XXq293TCLB5HO42Fy517obW19b+qjl3CHp0fdLJcWvmdy1etESi/uAdJrs1hTaUklHuW8qSDdC3UfXVR5cnD3rAFSSqtFb7z7eapErx7rC739jCXfbK3aWiipjXo8UbmxXPa7QQq9R289j2Gr88N7Ag5AlHPRKc37pNZv0CZtX1tVMG6rm8qW1/KlCgQvcMss933ybwXZz3dReW5yce4ByZtHFIhwT9kmjxg8BzbKDUe1PB9edBJqSN7/KM1LmqyuMZ5BpeTUw1aD/uDI0relPfSHa/Wn8Pxq1BNfxy/h3IdwOJqIKumb9CHvTqMefyY82RoQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAGEAAIIAQQQAgggBBBACCAEEEAIIIAQQAAhgABCACGAAEIAAYQAAggBBBACCAEEEAIIIAQQQAggfyL/BBgA8PgLdH0TBtkAAAAASUVORK5CYII=' |
|||
}, |
|||
// 图片进入可见区域前多少像素时,单位rpx,开始加载图片 |
|||
// 负数为图片超出屏幕底部多少距离后触发懒加载,正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上) |
|||
threshold: { |
|||
type: [Number, String], |
|||
default: 100 |
|||
}, |
|||
// 淡入淡出动画的过渡时间 |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 500 |
|||
}, |
|||
// 渡效果的速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显 |
|||
// linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n); |
|||
effect: { |
|||
type: String, |
|||
default: 'ease-in-out' |
|||
}, |
|||
// 是否使用过渡效果 |
|||
isEffect: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 圆角值 |
|||
borderRadius: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 图片高度,单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: '450' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isShow: false, |
|||
opacity: 1, |
|||
time: this.duration, |
|||
loadStatus: '', // 默认是懒加载中的状态 |
|||
isError: false, // 图片加载失败 |
|||
elIndex: this.$u.guid() |
|||
} |
|||
}, |
|||
computed: { |
|||
// 将threshold从rpx转为px |
|||
getThreshold() { |
|||
// 先取绝对值,因为threshold可能是负数,最后根据this.threshold是正数或者负数,重新还原 |
|||
let thresholdPx = uni.upx2px(Math.abs(this.threshold)); |
|||
return this.threshold < 0 ? -thresholdPx : thresholdPx; |
|||
}, |
|||
// 计算图片的高度,可能为auto,带%,或者直接数值 |
|||
imgHeight() { |
|||
return this.$u.addUnit(this.height); |
|||
} |
|||
}, |
|||
created() { |
|||
// 由于一些特殊原因,不能将此变量放到data中定义 |
|||
this.observer = {}; |
|||
}, |
|||
watch: { |
|||
isShow(nVal) { |
|||
// 如果是不开启过渡效果,直接返回 |
|||
if (!this.isEffect) return; |
|||
this.time = 0; |
|||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果 |
|||
this.opacity = 0; |
|||
// 延时30ms,否则在浏览器H5,过渡效果无效 |
|||
setTimeout(() => { |
|||
this.time = this.duration; |
|||
this.opacity = 1; |
|||
}, 30) |
|||
}, |
|||
// 图片路径发生变化时,需要重新标记一些变量,否则会一直卡在某一个状态,比如isError |
|||
image(n) { |
|||
if(!n) { |
|||
// 如果传入null或者'',或者undefined,标记为错误状态 |
|||
this.isError = true; |
|||
} else { |
|||
this.init(); |
|||
this.isError = false; |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
// 用于重新初始化 |
|||
init() { |
|||
this.isError = false; |
|||
this.loadStatus = ''; |
|||
}, |
|||
// 点击图片触发的事件,loadlazy-还是懒加载中状态,loading-图片正在加载,loaded-图片加加载完成 |
|||
clickImg() { |
|||
let whichImg = ''; |
|||
// 如果isShow为false,意味着图片还没开始加载,点击的只能是最开始的占位图 |
|||
if (this.isShow == false) whichImg = 'lazyImg'; |
|||
// 如果isError为true,意味着图片加载失败,这是只剩下错误的占位图,所以点击的只能是错误占位图 |
|||
// 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~ |
|||
else if (this.isError == true) whichImg = 'errorImg'; |
|||
// 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了 |
|||
else whichImg = 'realImg'; |
|||
// 只通知当前图片的index |
|||
this.$emit('click', this.index); |
|||
}, |
|||
// 图片加载完成事件,可能是加载占位图时触发,也可能是加载真正的图片完成时触发,通过isShow区分 |
|||
imgLoaded() { |
|||
// 占位图加载完成 |
|||
if (this.loadStatus == '') { |
|||
this.loadStatus = 'lazyed'; |
|||
} |
|||
// 真正的图片加载完成 |
|||
else if (this.loadStatus == 'lazyed') { |
|||
this.loadStatus = 'loaded'; |
|||
this.$emit('load', this.index); |
|||
} |
|||
}, |
|||
// 错误的图片加载完成 |
|||
errorImgLoaded() { |
|||
this.$emit('error', this.index); |
|||
}, |
|||
// 图片加载失败 |
|||
loadError() { |
|||
this.isError = true; |
|||
}, |
|||
disconnectObserver(observerName) { |
|||
const observer = this[observerName]; |
|||
observer && observer.disconnect(); |
|||
}, |
|||
}, |
|||
beforeDestroy() { |
|||
// 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉 |
|||
//observer.disconnect(); |
|||
}, |
|||
mounted() { |
|||
// 此uOnReachBottom事件由mixin.js发出,目的是让页面到底时,保证所有图片都进行加载,做到绝对稳定且可靠 |
|||
this.$nextTick(() => { |
|||
uni.$once('uOnReachBottom', () => { |
|||
if (!this.isShow) this.isShow = true; |
|||
}); |
|||
}) |
|||
// mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果 |
|||
setTimeout(() => { |
|||
// 这里是组件内获取布局状态,不能用uni.createIntersectionObserver,而必须用this.createIntersectionObserver |
|||
this.disconnectObserver('contentObserver'); |
|||
const contentObserver = uni.createIntersectionObserver(this); |
|||
// 要理解这里怎么计算的,请看这个: |
|||
// https://blog.csdn.net/qq_25324335/article/details/83687695 |
|||
contentObserver.relativeToViewport({ |
|||
bottom: this.getThreshold, |
|||
}).observe('.u-lazy-item-' + this.elIndex, (res) => { |
|||
if (res.intersectionRatio > 0) { |
|||
// 懒加载状态改变 |
|||
this.isShow = true; |
|||
// 如果图片已经加载,去掉监听,减少性能的消耗 |
|||
this.disconnectObserver('contentObserver'); |
|||
} |
|||
}) |
|||
this.contentObserver = contentObserver; |
|||
}, 30) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-wrap { |
|||
background-color: #eee; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-lazy-item { |
|||
width: 100%; |
|||
// 骗系统开启硬件加速 |
|||
transform: transition3d(0, 0, 0); |
|||
// 防止图片加载“闪一下” |
|||
will-change: transform; |
|||
/* #ifndef APP-NVUE */ |
|||
display: block; |
|||
/* #endif */ |
|||
} |
|||
</style> |
|||
@ -0,0 +1,147 @@ |
|||
<template> |
|||
<view class="u-progress" :style="{ |
|||
borderRadius: round ? '100rpx' : 0, |
|||
height: height + 'rpx', |
|||
backgroundColor: inactiveColor |
|||
}"> |
|||
<view :class="[ |
|||
type ? `u-type-${type}-bg` : '', |
|||
striped ? 'u-striped' : '', |
|||
striped && stripedActive ? 'u-striped-active' : '' |
|||
]" class="u-active" :style="[progressStyle]"> |
|||
<slot v-if="$slots.default || $slots.$default" /> |
|||
<block v-else-if="showPercent"> |
|||
{{percent + '%'}} |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* lineProgress 线型进度条 |
|||
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。 |
|||
* @tutorial https://www.uviewui.com/components/lineProgress.html |
|||
* @property {String Number} percent 进度条百分比值,为数值类型,0-100 |
|||
* @property {Boolean} round 进度条两端是否为半圆(默认true) |
|||
* @property {String} type 如设置,active-color值将会失效 |
|||
* @property {String} active-color 进度条激活部分的颜色(默认#19be6b) |
|||
* @property {String} inactive-color 进度条的底色(默认#ececec) |
|||
* @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true) |
|||
* @property {String Number} height 进度条的高度,单位rpx(默认28) |
|||
* @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false) |
|||
* @property {Boolean} striped-active 条纹是否具有动态效果(默认false) |
|||
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress> |
|||
*/ |
|||
export default { |
|||
name: "u-line-progress", |
|||
props: { |
|||
// 两端是否显示半圆形 |
|||
round: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 主题颜色 |
|||
type: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 激活部分的颜色 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#19be6b' |
|||
}, |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#ececec' |
|||
}, |
|||
// 进度百分比,数值 |
|||
percent: { |
|||
type: Number, |
|||
default: 0 |
|||
}, |
|||
// 是否在进度条内部显示百分比的值 |
|||
showPercent: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 进度条的高度,单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: 28 |
|||
}, |
|||
// 是否显示条纹 |
|||
striped: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 条纹是否显示活动状态 |
|||
stripedActive: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
} |
|||
}, |
|||
computed: { |
|||
progressStyle() { |
|||
let style = {}; |
|||
style.width = this.percent + '%'; |
|||
if(this.activeColor) style.backgroundColor = this.activeColor; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-progress { |
|||
overflow: hidden; |
|||
height: 15px; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
width: 100%; |
|||
border-radius: 100rpx; |
|||
} |
|||
|
|||
.u-active { |
|||
width: 0; |
|||
height: 100%; |
|||
align-items: center; |
|||
@include vue-flex; |
|||
justify-items: flex-end; |
|||
justify-content: space-around; |
|||
font-size: 20rpx; |
|||
color: #ffffff; |
|||
transition: all 0.4s ease; |
|||
} |
|||
|
|||
.u-striped { |
|||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); |
|||
background-size: 39px 39px; |
|||
} |
|||
|
|||
.u-striped-active { |
|||
animation: progress-stripes 2s linear infinite; |
|||
} |
|||
|
|||
@keyframes progress-stripes { |
|||
0% { |
|||
background-position: 0 0; |
|||
} |
|||
|
|||
100% { |
|||
background-position: 39px 0; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,84 @@ |
|||
<template> |
|||
<view class="u-line" :style="[lineStyle]"> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* line 线条 |
|||
* @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单 |
|||
* @tutorial https://www.uviewui.com/components/line.html |
|||
* @property {String} color 线条的颜色(默认#e4e7ed) |
|||
* @property {String} length 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等 |
|||
* @property {String} direction 线条的方向,row-横向,col-竖向(默认row) |
|||
* @property {String} border-style 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线(默认solid) |
|||
* @property {Boolean} hair-line 是否显示细线条(默认true) |
|||
* @property {String} margin 线条与上下左右元素的间距,字符串形式,如"30rpx" |
|||
* @example <u-line color="red"></u-line> |
|||
*/ |
|||
export default { |
|||
name: 'u-line', |
|||
props: { |
|||
color: { |
|||
type: String, |
|||
default: '#e4e7ed' |
|||
}, |
|||
// 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等 |
|||
length: { |
|||
type: String, |
|||
default: '100%' |
|||
}, |
|||
// 线条方向,col-竖向,row-横向 |
|||
direction: { |
|||
type: String, |
|||
default: 'row' |
|||
}, |
|||
// 是否显示细边框 |
|||
hairLine: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx" |
|||
margin: { |
|||
type: String, |
|||
default: '0' |
|||
}, |
|||
// 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线 |
|||
borderStyle: { |
|||
type: String, |
|||
default: 'solid' |
|||
} |
|||
}, |
|||
computed: { |
|||
lineStyle() { |
|||
let style = {}; |
|||
style.margin = this.margin; |
|||
// 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了 |
|||
if(this.direction == 'row') { |
|||
// 此处采用兼容分开写,兼容nvue的写法 |
|||
style.borderBottomWidth = '1px'; |
|||
style.borderBottomStyle = this.borderStyle; |
|||
style.width = this.$u.addUnit(this.length); |
|||
if(this.hairLine) style.transform = 'scaleY(0.5)'; |
|||
} else { |
|||
// 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了 |
|||
style.borderLeftWidth = '1px'; |
|||
style.borderLeftStyle = this.borderStyle; |
|||
style.height = this.$u.addUnit(this.length); |
|||
if(this.hairLine) style.transform = 'scaleX(0.5)'; |
|||
} |
|||
style.borderColor = this.color; |
|||
return style; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-line { |
|||
vertical-align: middle; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,89 @@ |
|||
<template> |
|||
<text class="u-link" @tap.stop="openLink" :style="{ |
|||
color: color, |
|||
fontSize: fontSize + 'rpx', |
|||
borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none', |
|||
paddingBottom: underLine ? '0rpx' : '0' |
|||
}"> |
|||
<slot></slot> |
|||
</text> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* link 超链接 |
|||
* @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。 |
|||
* @tutorial https://www.uviewui.com/components/link.html |
|||
* @property {String} color 文字颜色(默认#606266) |
|||
* @property {String Number} font-size 字体大小,单位rpx(默认28) |
|||
* @property {Boolean} under-line 是否显示下划线(默认false) |
|||
* @property {String} href 跳转的链接,要带上http(s) |
|||
* @property {String} line-color 下划线颜色,默认同color参数颜色 |
|||
* @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”) |
|||
* @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link> |
|||
*/ |
|||
export default { |
|||
name: "u-link", |
|||
props: { |
|||
// 文字颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 字体大小,单位rpx |
|||
fontSize: { |
|||
type: [String, Number], |
|||
default: 28 |
|||
}, |
|||
// 是否显示下划线 |
|||
underLine: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 要跳转的链接 |
|||
href: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 小程序中复制到粘贴板的提示语 |
|||
mpTips: { |
|||
type: String, |
|||
default: '链接已复制,请在浏览器打开' |
|||
}, |
|||
// 下划线颜色 |
|||
lineColor: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
methods: { |
|||
openLink() { |
|||
// #ifdef APP-PLUS |
|||
plus.runtime.openURL(this.href) |
|||
// #endif |
|||
// #ifdef H5 |
|||
window.open(this.href) |
|||
// #endif |
|||
// #ifdef MP |
|||
uni.setClipboardData({ |
|||
data: this.href, |
|||
success: () => { |
|||
uni.hideToast(); |
|||
this.$nextTick(() => { |
|||
this.$u.toast(this.mpTips); |
|||
}) |
|||
} |
|||
}); |
|||
// #endif |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-link { |
|||
line-height: 1; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,25 @@ |
|||
<template> |
|||
<view class="u-loading-page"> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
|
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
} |
|||
}, |
|||
methods: { |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
|
|||
</style> |
|||
@ -0,0 +1,103 @@ |
|||
<template> |
|||
<view v-if="show" class="u-loading" :class="mode == 'circle' ? 'u-loading-circle' : 'u-loading-flower'" :style="[cricleStyle]"> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* loading 加载动画 |
|||
* @description 警此组件为一个小动画,目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。 |
|||
* @tutorial https://www.uviewui.com/components/loading.html |
|||
* @property {String} mode 模式选择,见官网说明(默认circle) |
|||
* @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#c7c7c7) |
|||
* @property {String Number} size 加载图标的大小,单位rpx(默认34) |
|||
* @property {Boolean} show 是否显示动画(默认true) |
|||
* @example <u-loading mode="circle"></u-loading> |
|||
*/ |
|||
export default { |
|||
name: "u-loading", |
|||
props: { |
|||
// 动画的类型 |
|||
mode: { |
|||
type: String, |
|||
default: 'circle' |
|||
}, |
|||
// 动画的颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#c7c7c7' |
|||
}, |
|||
// 加载图标的大小,单位rpx |
|||
size: { |
|||
type: [String, Number], |
|||
default: '34' |
|||
}, |
|||
// 是否显示动画 |
|||
show: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
computed: { |
|||
// 加载中圆圈动画的样式 |
|||
cricleStyle() { |
|||
let style = {}; |
|||
style.width = this.size + 'rpx'; |
|||
style.height = this.size + 'rpx'; |
|||
if (this.mode == 'circle') style.borderColor = `#e4e4e4 #e4e4e4 #e4e4e4 ${this.color ? this.color : '#c7c7c7'}`; |
|||
return style; |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-loading-circle { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
vertical-align: middle; |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
background: 0 0; |
|||
border-radius: 50%; |
|||
border: 2px solid; |
|||
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e; |
|||
animation: u-circle 1s linear infinite; |
|||
} |
|||
|
|||
.u-loading-flower { |
|||
width: 20px; |
|||
height: 20px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
-webkit-animation: a 1s steps(12) infinite; |
|||
animation: u-flower 1s steps(12) infinite; |
|||
background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat; |
|||
background-size: 100%; |
|||
} |
|||
|
|||
@keyframes u-flower { |
|||
0% { |
|||
-webkit-transform: rotate(0deg); |
|||
transform: rotate(0deg); |
|||
} |
|||
|
|||
to { |
|||
-webkit-transform: rotate(1turn); |
|||
transform: rotate(1turn); |
|||
} |
|||
} |
|||
|
|||
@-webkit-keyframes u-circle { |
|||
0% { |
|||
transform: rotate(0); |
|||
} |
|||
|
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,203 @@ |
|||
<template> |
|||
<view class="u-load-more-wrap" :style="{ |
|||
backgroundColor: bgColor, |
|||
marginBottom: marginBottom + 'rpx', |
|||
marginTop: marginTop + 'rpx', |
|||
height: $u.addUnit(height) |
|||
}"> |
|||
<u-line color="#d4d4d4" length="50"></u-line> |
|||
<!-- 加载中和没有更多的状态才显示两边的横线 --> |
|||
<view :class="status == 'loadmore' || status == 'nomore' ? 'u-more' : ''" class="u-load-more-inner"> |
|||
<view class="u-loadmore-icon-wrap"> |
|||
<u-loading class="u-loadmore-icon" :color="iconColor" :mode="iconType == 'circle' ? 'circle' : 'flower'" :show="status == 'loading' && icon"></u-loading> |
|||
</view> |
|||
<!-- 如果没有更多的状态下,显示内容为dot(粗点),加载特定样式 --> |
|||
<view class="u-line-1" :style="[loadTextStyle]" :class="[(status == 'nomore' && isDot == true) ? 'u-dot-text' : 'u-more-text']" @tap="loadMore"> |
|||
{{ showText }} |
|||
</view> |
|||
</view> |
|||
<u-line color="#d4d4d4" length="50"></u-line> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* loadmore 加载更多 |
|||
* @description 此组件一般用于标识页面底部加载数据时的状态。 |
|||
* @tutorial https://www.uviewui.com/components/loadMore.html |
|||
* @property {String} status 组件状态(默认loadmore) |
|||
* @property {String} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff) |
|||
* @property {Boolean} icon 加载中时是否显示图标(默认true) |
|||
* @property {String} icon-type 加载中时的图标类型(默认circle) |
|||
* @property {String} icon-color icon-type为circle时有效,加载中的动画图标的颜色(默认#b7b7b7) |
|||
* @property {Boolean} is-dot status为nomore时,内容显示为一个"●"(默认false) |
|||
* @property {String} color 字体颜色(默认#606266) |
|||
* @property {String Number} margin-top 到上一个相邻元素的距离 |
|||
* @property {String Number} margin-bottom 到下一个相邻元素的距离 |
|||
* @property {Object} load-text 自定义显示的文字,见上方说明示例 |
|||
* @event {Function} loadmore status为loadmore时,点击组件会发出此事件 |
|||
* @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" /> |
|||
*/ |
|||
export default { |
|||
name: "u-loadmore", |
|||
props: { |
|||
// 组件背景色 |
|||
bgColor: { |
|||
type: String, |
|||
default: 'transparent' |
|||
}, |
|||
// 是否显示加载中的图标 |
|||
icon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 字体大小 |
|||
fontSize: { |
|||
type: String, |
|||
default: '28' |
|||
}, |
|||
// 字体颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态 |
|||
status: { |
|||
type: String, |
|||
default: 'loadmore' |
|||
}, |
|||
// 加载中状态的图标,flower-花朵状图标,circle-圆圈状图标 |
|||
iconType: { |
|||
type: String, |
|||
default: 'circle' |
|||
}, |
|||
// 显示的文字 |
|||
loadText: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
loadmore: '加载更多', |
|||
loading: '正在加载...', |
|||
nomore: '没有更多了' |
|||
} |
|||
} |
|||
}, |
|||
// 在“没有更多”状态下,是否显示粗点 |
|||
isDot: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 加载中显示圆圈动画时,动画的颜色 |
|||
iconColor: { |
|||
type: String, |
|||
default: '#b7b7b7' |
|||
}, |
|||
// 上边距 |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 下边距 |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 高度,单位rpx |
|||
height: { |
|||
type: [String, Number], |
|||
default: 'auto' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// 粗点 |
|||
dotText: "●" |
|||
} |
|||
}, |
|||
computed: { |
|||
// 加载的文字显示的样式 |
|||
loadTextStyle() { |
|||
return { |
|||
color: this.color, |
|||
fontSize: this.fontSize + 'rpx', |
|||
position: 'relative', |
|||
zIndex: 1, |
|||
backgroundColor: this.bgColor, |
|||
// 如果是加载中状态,动画和文字需要距离近一点 |
|||
} |
|||
}, |
|||
// 加载中圆圈动画的样式 |
|||
cricleStyle() { |
|||
return { |
|||
borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}` |
|||
} |
|||
}, |
|||
// 加载中花朵动画形式 |
|||
// 动画由base64图片生成,暂不支持修改 |
|||
flowerStyle() { |
|||
return { |
|||
} |
|||
}, |
|||
// 显示的提示文字 |
|||
showText() { |
|||
let text = ''; |
|||
if(this.status == 'loadmore') text = this.loadText.loadmore; |
|||
else if(this.status == 'loading') text = this.loadText.loading; |
|||
else if(this.status == 'nomore' && this.isDot) text = this.dotText; |
|||
else text = this.loadText.nomore; |
|||
return text; |
|||
} |
|||
}, |
|||
methods: { |
|||
loadMore() { |
|||
// 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发 |
|||
if(this.status == 'loadmore') this.$emit('loadmore'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
/* #ifdef MP */ |
|||
// 在mp.scss中,赋予了u-line为flex: 1,这里需要一个明确的长度,所以重置掉它 |
|||
// 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用 |
|||
u-line { |
|||
flex: none; |
|||
} |
|||
/* #endif */ |
|||
|
|||
.u-load-more-wrap { |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-load-more-inner { |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0 12rpx; |
|||
} |
|||
|
|||
.u-more { |
|||
position: relative; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-dot-text { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.u-loadmore-icon-wrap { |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
.u-loadmore-icon { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,123 @@ |
|||
<template> |
|||
<view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle]" @tap="click" @touchmove.stop.prevent="() => {}" :class="{ |
|||
'u-mask-zoom': zoom, |
|||
'u-mask-show': show |
|||
}"> |
|||
<slot /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* mask 遮罩 |
|||
* @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景 |
|||
* @tutorial https://www.uviewui.com/components/mask.html |
|||
* @property {Boolean} show 是否显示遮罩(默认false) |
|||
* @property {String Number} z-index z-index 层级(默认1070) |
|||
* @property {Object} custom-style 自定义样式对象,见上方说明 |
|||
* @property {String Number} duration 动画时长,单位毫秒(默认300) |
|||
* @property {Boolean} zoom 是否使用scale对遮罩进行缩放(默认true) |
|||
* @property {Boolean} mask-click-able 遮罩是否可点击,为false时点击不会发送click事件(默认true) |
|||
* @event {Function} click mask-click-able为true时,点击遮罩发送此事件 |
|||
* @example <u-mask :show="show" @click="show = false"></u-mask> |
|||
*/ |
|||
export default { |
|||
name: "u-mask", |
|||
props: { |
|||
// 是否显示遮罩 |
|||
show: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 层级z-index |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 用户自定义样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放 |
|||
zoom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 遮罩的过渡时间,单位为ms |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 300 |
|||
}, |
|||
// 是否可以通过点击遮罩进行关闭 |
|||
maskClickAble: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
zoomStyle: { |
|||
transform: '' |
|||
}, |
|||
scale: 'scale(1.2, 1.2)' |
|||
} |
|||
}, |
|||
watch: { |
|||
show(n) { |
|||
if(n && this.zoom) { |
|||
// 当展示遮罩的时候,设置scale为1,达到缩小(原来为1.2)的效果 |
|||
this.zoomStyle.transform = 'scale(1, 1)'; |
|||
} else if(!n && this.zoom) { |
|||
// 当隐藏遮罩的时候,设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果 |
|||
this.zoomStyle.transform = this.scale; |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
maskStyle() { |
|||
let style = {}; |
|||
style.backgroundColor = "rgba(0, 0, 0, 0.6)"; |
|||
if(this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask; |
|||
else style.zIndex = -1; |
|||
style.transition = `all ${this.duration / 1000}s ease-in-out`; |
|||
// 判断用户传递的对象是否为空,不为空就进行合并 |
|||
if (Object.keys(this.customStyle).length) style = { |
|||
...style, |
|||
...this.customStyle |
|||
}; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
click() { |
|||
if (!this.maskClickAble) return; |
|||
this.$emit('click'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-mask { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
opacity: 0; |
|||
transition: transform 0.3s; |
|||
} |
|||
|
|||
.u-mask-show { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.u-mask-zoom { |
|||
transform: scale(1.2, 1.2); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,311 @@ |
|||
<template> |
|||
<view class="u-char-box"> |
|||
<view class="u-char-flex"> |
|||
<input :disabled="disabledKeyboard" :value="valueModel" type="number" :focus="focus" :maxlength="maxlength" class="u-input" @input="getVal"/> |
|||
<view v-for="(item, index) in loopCharArr" :key="index"> |
|||
<view :class="[breathe && charArrLength == index ? 'u-breathe' : '', 'u-char-item', |
|||
charArrLength === index && mode == 'box' ? 'u-box-active' : '', |
|||
mode === 'box' ? 'u-box' : '']" :style="{ |
|||
fontWeight: bold ? 'bold' : 'normal', |
|||
fontSize: fontSize + 'rpx', |
|||
width: width + 'rpx', |
|||
height: width + 'rpx', |
|||
color: inactiveColor, |
|||
borderColor: charArrLength === index && mode == 'box' ? activeColor : inactiveColor |
|||
}"> |
|||
<view class="u-placeholder-line" :style="{ |
|||
display: charArrLength === index ? 'block' : 'none', |
|||
height: width * 0.5 +'rpx' |
|||
}" |
|||
v-if="mode !== 'middleLine'" |
|||
></view> |
|||
<view v-if="mode === 'middleLine' && charArrLength <= index" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-middle-line-active' : '']" |
|||
class="u-middle-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view> |
|||
<view v-if="mode === 'bottomLine'" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-buttom-line-active' : '']" |
|||
class="u-bottom-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view> |
|||
<block v-if="!dotFill"> {{ charArr[index] ? charArr[index] : ''}}</block> |
|||
<block v-else> |
|||
<text class="u-dot">{{ charArr[index] ? '●' : ''}}</text> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* messageInput 验证码输入框 |
|||
* @description 该组件一般用于验证用户短信验证码的场景,也可以结合uView的键盘组件使用 |
|||
* @tutorial https://www.uviewui.com/components/messageInput.html |
|||
* @property {String Number} maxlength 输入字符个数(默认4) |
|||
* @property {Boolean} dot-fill 是否用圆点填充(默认false) |
|||
* @property {String} mode 模式选择,见上方"基本使用"说明(默认box) |
|||
* @property {String Number} value 预置值 |
|||
* @property {Boolean} breathe 是否开启呼吸效果,见上方说明(默认true) |
|||
* @property {Boolean} focus 是否自动获取焦点(默认false) |
|||
* @property {Boolean} bold 字体和输入横线是否加粗(默认true) |
|||
* @property {String Number} font-size 字体大小,单位rpx(默认60) |
|||
* @property {String} active-color 当前激活输入框的样式(默认#2979ff) |
|||
* @property {String} inactive-color 非激活输入框的样式,文字颜色同此值(默认#606266) |
|||
* @property {String | Number} width 输入框宽度,单位rpx,高等于宽(默认80) |
|||
* @property {Boolean} disabled-keyboard 禁止点击输入框唤起系统键盘(默认false) |
|||
* @event {Function} change 输入内容发生改变时触发,具体见官网说明 |
|||
* @event {Function} finish 输入字符个数达maxlength值时触发,见官网说明 |
|||
* @example <u-message-input mode="bottomLine"></u-message-input> |
|||
*/ |
|||
export default { |
|||
name: "u-message-input", |
|||
props: { |
|||
// 最大输入长度 |
|||
maxlength: { |
|||
type: [Number, String], |
|||
default: 4 |
|||
}, |
|||
// 是否用圆点填充 |
|||
dotFill: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 显示模式,box-盒子模式,bottomLine-横线在底部模式,middleLine-横线在中部模式 |
|||
mode: { |
|||
type: String, |
|||
default: "box" |
|||
}, |
|||
// 预置值 |
|||
value: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 当前激活输入item,是否带有呼吸效果 |
|||
breathe: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否自动获取焦点 |
|||
focus: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 字体是否加粗 |
|||
bold: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 字体大小 |
|||
fontSize: { |
|||
type: [String, Number], |
|||
default: 60 |
|||
}, |
|||
// 激活样式 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 未激活的样式 |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 输入框的大小,单位rpx,宽等于高 |
|||
width: { |
|||
type: [Number, String], |
|||
default: '80' |
|||
}, |
|||
// 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true |
|||
disabledKeyboard: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
watch: { |
|||
// maxlength: { |
|||
// // 此值设置为true,会在组件加载后无需maxlength变化就会执行一次本监听函数,无需再created生命周期中处理 |
|||
// immediate: true, |
|||
// handler(val) { |
|||
// this.maxlength = Number(val); |
|||
// } |
|||
// }, |
|||
value: { |
|||
immediate: true, |
|||
handler(val) { |
|||
// 转为字符串 |
|||
val = String(val); |
|||
// 超出部分截掉 |
|||
this.valueModel = val.substring(0, this.maxlength); |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
valueModel: "" |
|||
} |
|||
}, |
|||
computed: { |
|||
// 是否显示呼吸灯效果 |
|||
animationClass() { |
|||
return (index) => { |
|||
if (this.breathe && this.charArr.length == index) return 'u-breathe'; |
|||
else return ''; |
|||
} |
|||
}, |
|||
// 用于显示字符 |
|||
charArr() { |
|||
return this.valueModel.split(''); |
|||
}, |
|||
charArrLength() { |
|||
return this.charArr.length; |
|||
}, |
|||
// 根据长度,循环输入框的个数,因为头条小程序数值不能用于v-for |
|||
loopCharArr() { |
|||
return new Array(this.maxlength); |
|||
} |
|||
}, |
|||
methods: { |
|||
getVal(e) { |
|||
let { |
|||
value |
|||
} = e.detail |
|||
this.valueModel = value; |
|||
// 判断长度是否超出了maxlength值,理论上不会发生,因为input组件设置了maxlength属性值 |
|||
if (String(value).length > this.maxlength) return; |
|||
// 未达到maxlength之前,发送change事件,达到后发送finish事件 |
|||
this.$emit('change', value); |
|||
if (String(value).length == this.maxlength) { |
|||
this.$emit('finish', value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
@keyframes breathe { |
|||
0% { |
|||
opacity: 0.3; |
|||
} |
|||
|
|||
50% { |
|||
opacity: 1; |
|||
} |
|||
|
|||
100% { |
|||
opacity: 0.3; |
|||
} |
|||
} |
|||
|
|||
.u-char-box { |
|||
text-align: center; |
|||
} |
|||
|
|||
.u-char-flex { |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
flex-wrap: wrap; |
|||
position: relative; |
|||
} |
|||
|
|||
.u-input { |
|||
position: absolute; |
|||
top: 0; |
|||
left: -100%; |
|||
width: 200%; |
|||
height: 100%; |
|||
text-align: left; |
|||
z-index: 9; |
|||
opacity: 0; |
|||
background: none; |
|||
} |
|||
|
|||
.u-char-item { |
|||
position: relative; |
|||
width: 90rpx; |
|||
height: 90rpx; |
|||
margin: 10rpx 10rpx; |
|||
font-size: 60rpx; |
|||
font-weight: bold; |
|||
color: $u-main-color; |
|||
line-height: 90rpx; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-middle-line { |
|||
border: none; |
|||
} |
|||
|
|||
.u-box { |
|||
box-sizing: border-box; |
|||
border: 2rpx solid #cccccc; |
|||
border-radius: 6rpx; |
|||
} |
|||
|
|||
.u-box-active { |
|||
overflow: hidden; |
|||
animation-timing-function: ease-in-out; |
|||
animation-duration: 1500ms; |
|||
animation-iteration-count: infinite; |
|||
animation-direction: alternate; |
|||
border: 2rpx solid $u-type-primary; |
|||
} |
|||
|
|||
.u-middle-line-active { |
|||
background: $u-type-primary; |
|||
} |
|||
|
|||
.u-breathe { |
|||
animation: breathe 2s infinite ease; |
|||
} |
|||
|
|||
.u-placeholder-line { |
|||
/* #ifndef APP-NVUE */ |
|||
display: none; |
|||
/* #endif */ |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 2rpx; |
|||
height: 40rpx; |
|||
background: #333333; |
|||
animation: twinkling 1.5s infinite ease; |
|||
} |
|||
|
|||
.u-animation-breathe { |
|||
animation-name: breathe; |
|||
} |
|||
|
|||
.u-dot { |
|||
font-size: 34rpx; |
|||
line-height: 34rpx; |
|||
} |
|||
|
|||
.u-middle-line { |
|||
height: 4px; |
|||
background: #000000; |
|||
width: 80%; |
|||
position: absolute; |
|||
border-radius: 2px; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
} |
|||
|
|||
.u-buttom-line-active { |
|||
background: $u-type-primary; |
|||
} |
|||
|
|||
.u-bottom-line { |
|||
height: 4px; |
|||
background: #000000; |
|||
width: 80%; |
|||
position: absolute; |
|||
border-radius: 2px; |
|||
bottom: 0; |
|||
left: 50%; |
|||
transform: translate(-50%); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,283 @@ |
|||
<template> |
|||
<view> |
|||
<u-popup :zoom="zoom" mode="center" :popup="false" :z-index="uZIndex" v-model="value" :length="width" |
|||
:mask-close-able="maskCloseAble" :border-radius="borderRadius" @close="popupClose" :negative-top="negativeTop"> |
|||
<view class="u-model"> |
|||
<view v-if="showTitle" class="u-model__title u-line-1" :style="[titleStyle]">{{ title }}</view> |
|||
<view class="u-model__content"> |
|||
<view :style="[contentStyle]" v-if="$slots.default || $slots.$default"> |
|||
<slot /> |
|||
</view> |
|||
<view v-else class="u-model__content__message" :style="[contentStyle]">{{ content }}</view> |
|||
</view> |
|||
<view class="u-model__footer u-border-top" v-if="showCancelButton || showConfirmButton"> |
|||
<view v-if="showCancelButton" :hover-stay-time="100" hover-class="u-model__btn--hover" class="u-model__footer__button" |
|||
:style="[cancelBtnStyle]" @tap="cancel"> |
|||
{{cancelText}} |
|||
</view> |
|||
<view v-if="showConfirmButton || $slots['confirm-button']" :hover-stay-time="100" :hover-class="asyncClose ? 'none' : 'u-model__btn--hover'" |
|||
class="u-model__footer__button hairline-left" :style="[confirmBtnStyle]" @tap="confirm"> |
|||
<slot v-if="$slots['confirm-button']" name="confirm-button"></slot> |
|||
<block v-else> |
|||
<u-loading mode="circle" :color="confirmColor" v-if="loading"></u-loading> |
|||
<block v-else> |
|||
{{confirmText}} |
|||
</block> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</u-popup> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* modal 模态框 |
|||
* @description 弹出模态框,常用于消息提示、消息确认、在当前页面内完成特定的交互操作 |
|||
* @tutorial https://www.uviewui.com/components/modal.html |
|||
* @property {Boolean} value 是否显示模态框 |
|||
* @property {String | Number} z-index 层级 |
|||
* @property {String} title 模态框标题(默认"提示") |
|||
* @property {String | Number} width 模态框宽度(默认600) |
|||
* @property {String} content 模态框内容(默认"内容") |
|||
* @property {Boolean} show-title 是否显示标题(默认true) |
|||
* @property {Boolean} async-close 是否异步关闭,只对确定按钮有效(默认false) |
|||
* @property {Boolean} show-confirm-button 是否显示确认按钮(默认true) |
|||
* @property {Stringr | Number} negative-top modal往上偏移的值 |
|||
* @property {Boolean} show-cancel-button 是否显示取消按钮(默认false) |
|||
* @property {Boolean} mask-close-able 是否允许点击遮罩关闭modal(默认false) |
|||
* @property {String} confirm-text 确认按钮的文字内容(默认"确认") |
|||
* @property {String} cancel-text 取消按钮的文字内容(默认"取消") |
|||
* @property {String} cancel-color 取消按钮的颜色(默认"#606266") |
|||
* @property {String} confirm-color 确认按钮的文字内容(默认"#2979ff") |
|||
* @property {String | Number} border-radius 模态框圆角值,单位rpx(默认16) |
|||
* @property {Object} title-style 自定义标题样式,对象形式 |
|||
* @property {Object} content-style 自定义内容样式,对象形式 |
|||
* @property {Object} cancel-style 自定义取消按钮样式,对象形式 |
|||
* @property {Object} confirm-style 自定义确认按钮样式,对象形式 |
|||
* @property {Boolean} zoom 是否开启缩放模式(默认true) |
|||
* @event {Function} confirm 确认按钮被点击 |
|||
* @event {Function} cancel 取消按钮被点击 |
|||
* @example <u-modal :src="title" :content="content"></u-modal> |
|||
*/ |
|||
export default { |
|||
name: 'u-modal', |
|||
props: { |
|||
// 是否显示Modal |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 层级z-index |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 标题 |
|||
title: { |
|||
type: [String], |
|||
default: '提示' |
|||
}, |
|||
// 弹窗宽度,可以是数值(rpx),百分比,auto等 |
|||
width: { |
|||
type: [Number, String], |
|||
default: 600 |
|||
}, |
|||
// 弹窗内容 |
|||
content: { |
|||
type: String, |
|||
default: '内容' |
|||
}, |
|||
// 是否显示标题 |
|||
showTitle: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示确认按钮 |
|||
showConfirmButton: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示取消按钮 |
|||
showCancelButton: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 确认文案 |
|||
confirmText: { |
|||
type: String, |
|||
default: '确认' |
|||
}, |
|||
// 取消文案 |
|||
cancelText: { |
|||
type: String, |
|||
default: '取消' |
|||
}, |
|||
// 确认按钮颜色 |
|||
confirmColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 取消文字颜色 |
|||
cancelColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 圆角值 |
|||
borderRadius: { |
|||
type: [Number, String], |
|||
default: 16 |
|||
}, |
|||
// 标题的样式 |
|||
titleStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 内容的样式 |
|||
contentStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 取消按钮的样式 |
|||
cancelStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 确定按钮的样式 |
|||
confirmStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 是否开启缩放效果 |
|||
zoom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否异步关闭,只对确定按钮有效 |
|||
asyncClose: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否允许点击遮罩关闭modal |
|||
maskCloseAble: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 给一个负的margin-top,往上偏移,避免和键盘重合的情况 |
|||
negativeTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
loading: false, // 确认按钮是否正在加载中 |
|||
} |
|||
}, |
|||
computed: { |
|||
cancelBtnStyle() { |
|||
return Object.assign({ |
|||
color: this.cancelColor |
|||
}, this.cancelStyle); |
|||
}, |
|||
confirmBtnStyle() { |
|||
return Object.assign({ |
|||
color: this.confirmColor |
|||
}, this.confirmStyle); |
|||
}, |
|||
uZIndex() { |
|||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup; |
|||
} |
|||
}, |
|||
watch: { |
|||
// 如果是异步关闭时,外部修改v-model的值为false时,重置内部的loading状态 |
|||
// 避免下次打开的时候,状态混乱 |
|||
value(n) { |
|||
if (n === true) this.loading = false; |
|||
} |
|||
}, |
|||
methods: { |
|||
confirm() { |
|||
// 异步关闭 |
|||
if (this.asyncClose) { |
|||
this.loading = true; |
|||
} else { |
|||
this.$emit('input', false); |
|||
} |
|||
this.$emit('confirm'); |
|||
}, |
|||
cancel() { |
|||
this.$emit('cancel'); |
|||
this.$emit('input', false); |
|||
// 目前popup弹窗关闭有一个延时操作,此处做一个延时 |
|||
// 避免确认按钮文字变成了"确定"字样,modal还没消失,造成视觉不好的效果 |
|||
setTimeout(() => { |
|||
this.loading = false; |
|||
}, 300); |
|||
}, |
|||
// 点击遮罩关闭modal,设置v-model的值为false,否则无法第二次弹起modal |
|||
popupClose() { |
|||
this.$emit('input', false); |
|||
}, |
|||
// 清除加载中的状态 |
|||
clearLoading() { |
|||
this.loading = false; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-model { |
|||
height: auto; |
|||
overflow: hidden; |
|||
font-size: 32rpx; |
|||
background-color: #fff; |
|||
|
|||
&__btn--hover { |
|||
background-color: rgb(230, 230, 230); |
|||
} |
|||
|
|||
&__title { |
|||
padding-top: 48rpx; |
|||
font-weight: 500; |
|||
text-align: center; |
|||
color: $u-main-color; |
|||
} |
|||
|
|||
&__content { |
|||
&__message { |
|||
padding: 48rpx; |
|||
font-size: 30rpx; |
|||
text-align: center; |
|||
color: $u-content-color; |
|||
} |
|||
} |
|||
|
|||
&__footer { |
|||
@include vue-flex; |
|||
|
|||
&__button { |
|||
flex: 1; |
|||
height: 100rpx; |
|||
line-height: 100rpx; |
|||
font-size: 32rpx; |
|||
box-sizing: border-box; |
|||
cursor: pointer; |
|||
text-align: center; |
|||
border-radius: 4rpx; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,315 @@ |
|||
<template> |
|||
<view class=""> |
|||
<view class="u-navbar" :style="[navbarStyle]" :class="{ 'u-navbar-fixed': isFixed, 'u-border-bottom': borderBottom }"> |
|||
<view class="u-status-bar" :style="{ height: statusBarHeight + 'px' }"></view> |
|||
<view class="u-navbar-inner" :style="[navbarInnerStyle]"> |
|||
<view class="u-back-wrap" v-if="isBack" @tap="goBack"> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon :name="backIconName" :color="backIconColor" :size="backIconSize"></u-icon> |
|||
</view> |
|||
<view class="u-icon-wrap u-back-text u-line-1" v-if="backText" :style="[backTextStyle]">{{ backText }}</view> |
|||
</view> |
|||
<view class="u-navbar-content-title" v-if="title" :style="[titleStyle]"> |
|||
<view |
|||
class="u-title u-line-1" |
|||
:style="{ |
|||
color: titleColor, |
|||
fontSize: titleSize + 'rpx', |
|||
fontWeight: titleBold ? 'bold' : 'normal' |
|||
}"> |
|||
{{ title }} |
|||
</view> |
|||
</view> |
|||
<view class="u-slot-content"> |
|||
<slot></slot> |
|||
</view> |
|||
<view class="u-slot-right"> |
|||
<slot name="right"></slot> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<!-- 解决fixed定位后导航栏塌陷的问题 --> |
|||
<view class="u-navbar-placeholder" v-if="isFixed && !immersive" :style="{ width: '100%', height: Number(navbarHeight) + statusBarHeight + 'px' }"></view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
// 获取系统状态栏的高度 |
|||
let systemInfo = uni.getSystemInfoSync(); |
|||
let menuButtonInfo = {}; |
|||
// 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容) |
|||
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ |
|||
menuButtonInfo = uni.getMenuButtonBoundingClientRect(); |
|||
// #endif |
|||
/** |
|||
* navbar 自定义导航栏 |
|||
* @description 此组件一般用于在特殊情况下,需要自定义导航栏的时候用到,一般建议使用uniapp自带的导航栏。 |
|||
* @tutorial https://www.uviewui.com/components/navbar.html |
|||
* @property {String Number} height 导航栏高度(不包括状态栏高度在内,内部自动加上),注意这里的单位是px(默认44) |
|||
* @property {String} back-icon-color 左边返回图标的颜色(默认#606266) |
|||
* @property {String} back-icon-name 左边返回图标的名称,只能为uView自带的图标(默认arrow-left) |
|||
* @property {String Number} back-icon-size 左边返回图标的大小,单位rpx(默认30) |
|||
* @property {String} back-text 返回图标右边的辅助提示文字 |
|||
* @property {Object} back-text-style 返回图标右边的辅助提示文字的样式,对象形式(默认{ color: '#606266' }) |
|||
* @property {String} title 导航栏标题,如设置为空字符,将会隐藏标题占位区域 |
|||
* @property {String Number} title-width 导航栏标题的最大宽度,内容超出会以省略号隐藏,单位rpx(默认250) |
|||
* @property {String} title-color 标题的颜色(默认#606266) |
|||
* @property {String Number} title-size 导航栏标题字体大小,单位rpx(默认32) |
|||
* @property {Function} custom-back 自定义返回逻辑方法 |
|||
* @property {String Number} z-index 固定在顶部时的z-index值(默认980) |
|||
* @property {Boolean} is-back 是否显示导航栏左边返回图标和辅助文字(默认true) |
|||
* @property {Object} background 导航栏背景设置,见官网说明(默认{ background: '#ffffff' }) |
|||
* @property {Boolean} is-fixed 导航栏是否固定在顶部(默认true) |
|||
* @property {Boolean} immersive 沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效(默认false) |
|||
* @property {Boolean} border-bottom 导航栏底部是否显示下边框,如定义了较深的背景颜色,可取消此值(默认true) |
|||
* @example <u-navbar back-text="返回" title="剑未配妥,出门已是江湖"></u-navbar> |
|||
*/ |
|||
export default { |
|||
name: "u-navbar", |
|||
props: { |
|||
// 导航栏高度,单位px,非rpx |
|||
height: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 返回箭头的颜色 |
|||
backIconColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 左边返回的图标 |
|||
backIconName: { |
|||
type: String, |
|||
default: 'nav-back' |
|||
}, |
|||
// 左边返回图标的大小,rpx |
|||
backIconSize: { |
|||
type: [String, Number], |
|||
default: '44' |
|||
}, |
|||
// 返回的文字提示 |
|||
backText: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 返回的文字的 样式 |
|||
backTextStyle: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
color: '#606266' |
|||
} |
|||
} |
|||
}, |
|||
// 导航栏标题 |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 标题的宽度,如果需要自定义右侧内容,且右侧内容很多时,可能需要减少这个宽度,单位rpx |
|||
titleWidth: { |
|||
type: [String, Number], |
|||
default: '250' |
|||
}, |
|||
// 标题的颜色 |
|||
titleColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 标题字体是否加粗 |
|||
titleBold: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 标题的字体大小 |
|||
titleSize: { |
|||
type: [String, Number], |
|||
default: 32 |
|||
}, |
|||
isBack: { |
|||
type: [Boolean, String], |
|||
default: true |
|||
}, |
|||
// 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色 |
|||
background: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
background: '#ffffff' |
|||
} |
|||
} |
|||
}, |
|||
// 导航栏是否固定在顶部 |
|||
isFixed: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效 |
|||
immersive: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示导航栏的下边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
zIndex: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 自定义返回逻辑 |
|||
customBack: { |
|||
type: Function, |
|||
default: null |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
menuButtonInfo: menuButtonInfo, |
|||
statusBarHeight: systemInfo.statusBarHeight |
|||
}; |
|||
}, |
|||
computed: { |
|||
// 导航栏内部盒子的样式 |
|||
navbarInnerStyle() { |
|||
let style = {}; |
|||
// 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离 |
|||
style.height = this.navbarHeight + 'px'; |
|||
// // 如果是各家小程序,导航栏内部的宽度需要减少右边胶囊的宽度 |
|||
// #ifdef MP |
|||
let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left; |
|||
style.marginRight = rightButtonWidth + 'px'; |
|||
// #endif |
|||
return style; |
|||
}, |
|||
// 整个导航栏的样式 |
|||
navbarStyle() { |
|||
let style = {}; |
|||
style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.navbar; |
|||
// 合并用户传递的背景色对象 |
|||
Object.assign(style, this.background); |
|||
return style; |
|||
}, |
|||
// 导航中间的标题的样式 |
|||
titleStyle() { |
|||
let style = {}; |
|||
// #ifndef MP |
|||
style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px'; |
|||
style.right = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px'; |
|||
// #endif |
|||
// #ifdef MP |
|||
// 此处是为了让标题显示区域即使在小程序有右侧胶囊的情况下也能处于屏幕的中间,是通过绝对定位实现的 |
|||
let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left; |
|||
style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px'; |
|||
style.right = rightButtonWidth - (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + rightButtonWidth + |
|||
'px'; |
|||
// #endif |
|||
style.width = uni.upx2px(this.titleWidth) + 'px'; |
|||
return style; |
|||
}, |
|||
// 转换字符数值为真正的数值 |
|||
navbarHeight() { |
|||
// #ifdef APP-PLUS || H5 |
|||
return this.height ? this.height : 44; |
|||
// #endif |
|||
// #ifdef MP |
|||
// 小程序特别处理,让导航栏高度 = 胶囊高度 + 两倍胶囊顶部与状态栏底部的距离之差(相当于同时获得了导航栏底部与胶囊底部的距离) |
|||
// 此方法有缺陷,暂不用(会导致少了几个px),采用直接固定值的方式 |
|||
// return menuButtonInfo.height + (menuButtonInfo.top - this.statusBarHeight) * 2;//导航高度 |
|||
let height = systemInfo.platform == 'ios' ? 44 : 48; |
|||
return this.height ? this.height : height; |
|||
// #endif |
|||
} |
|||
}, |
|||
created() {}, |
|||
methods: { |
|||
goBack() { |
|||
// 如果自定义了点击返回按钮的函数,则执行,否则执行返回逻辑 |
|||
if (typeof this.customBack === 'function') { |
|||
// 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this |
|||
// 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文 |
|||
this.customBack.bind(this.$u.$parent.call(this))(); |
|||
} else { |
|||
uni.navigateBack(); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-navbar { |
|||
width: 100%; |
|||
} |
|||
|
|||
.u-navbar-fixed { |
|||
position: fixed; |
|||
left: 0; |
|||
right: 0; |
|||
top: 0; |
|||
z-index: 991; |
|||
} |
|||
|
|||
.u-status-bar { |
|||
width: 100%; |
|||
} |
|||
|
|||
.u-navbar-inner { |
|||
@include vue-flex; |
|||
justify-content: space-between; |
|||
position: relative; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-back-wrap { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
flex-grow: 0; |
|||
padding: 14rpx 14rpx 14rpx 24rpx; |
|||
} |
|||
|
|||
.u-back-text { |
|||
padding-left: 4rpx; |
|||
font-size: 30rpx; |
|||
} |
|||
|
|||
.u-navbar-content-title { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex: 1; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
height: 60rpx; |
|||
text-align: center; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.u-navbar-centent-slot { |
|||
flex: 1; |
|||
} |
|||
|
|||
.u-title { |
|||
line-height: 60rpx; |
|||
font-size: 32rpx; |
|||
flex: 1; |
|||
} |
|||
|
|||
.u-navbar-right { |
|||
flex: 1; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.u-slot-content { |
|||
flex: 1; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,272 @@ |
|||
<template> |
|||
<view class="u-notice-bar-wrap" v-if="isShow" :style="{ |
|||
borderRadius: borderRadius + 'rpx', |
|||
}"> |
|||
<block v-if="mode == 'horizontal' && isCircular"> |
|||
<u-row-notice |
|||
:type="type" |
|||
:color="color" |
|||
:bgColor="bgColor" |
|||
:list="list" |
|||
:volumeIcon="volumeIcon" |
|||
:moreIcon="moreIcon" |
|||
:volumeSize="volumeSize" |
|||
:closeIcon="closeIcon" |
|||
:mode="mode" |
|||
:fontSize="fontSize" |
|||
:speed="speed" |
|||
:playState="playState" |
|||
:padding="padding" |
|||
@getMore="getMore" |
|||
@close="close" |
|||
@click="click" |
|||
></u-row-notice> |
|||
</block> |
|||
<block v-if="mode == 'vertical' || (mode == 'horizontal' && !isCircular)"> |
|||
<u-column-notice |
|||
:type="type" |
|||
:color="color" |
|||
:bgColor="bgColor" |
|||
:list="list" |
|||
:volumeIcon="volumeIcon" |
|||
:moreIcon="moreIcon" |
|||
:closeIcon="closeIcon" |
|||
:mode="mode" |
|||
:volumeSize="volumeSize" |
|||
:disable-touch="disableTouch" |
|||
:fontSize="fontSize" |
|||
:duration="duration" |
|||
:playState="playState" |
|||
:padding="padding" |
|||
@getMore="getMore" |
|||
@close="close" |
|||
@click="click" |
|||
@end="end" |
|||
></u-column-notice> |
|||
</block> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
/** |
|||
* noticeBar 滚动通知 |
|||
* @description 该组件用于滚动通告场景,有多种模式可供选择 |
|||
* @tutorial https://www.uviewui.com/components/noticeBar.html |
|||
* @property {Array} list 滚动内容,数组形式,见上方说明 |
|||
* @property {String} type 显示的主题(默认warning) |
|||
* @property {Boolean} volume-icon 是否显示小喇叭图标(默认true) |
|||
* @property {Boolean} more-icon 是否显示右边的向右箭头(默认false) |
|||
* @property {Boolean} close-icon 是否显示关闭图标(默认false) |
|||
* @property {Boolean} autoplay 是否自动播放(默认true) |
|||
* @property {String} color 文字颜色 |
|||
* @property {String Number} bg-color 背景颜色 |
|||
* @property {String} mode 滚动模式(默认horizontal) |
|||
* @property {Boolean} show 是否显示(默认true) |
|||
* @property {String Number} font-size 字体大小,单位rpx(默认28) |
|||
* @property {String Number} volume-size 左边喇叭的大小(默认34) |
|||
* @property {String Number} duration 滚动周期时长,只对步进模式有效,横向衔接模式无效,单位ms(默认2000) |
|||
* @property {String Number} speed 水平滚动时的滚动速度,即每秒移动多少距离,只对水平衔接方式有效,单位rpx(默认160) |
|||
* @property {String Number} font-size 字体大小,单位rpx(默认28) |
|||
* @property {Boolean} is-circular mode为horizontal时,指明是否水平衔接滚动(默认true) |
|||
* @property {String} play-state 播放状态,play - 播放,paused - 暂停(默认play) |
|||
* @property {String Nubmer} border-radius 通知栏圆角(默认为0) |
|||
* @property {String Nubmer} padding 内边距,字符串,与普通的内边距css写法一直(默认"18rpx 24rpx") |
|||
* @property {Boolean} no-list-hidden 列表为空时,是否显示组件(默认false) |
|||
* @property {Boolean} disable-touch 是否禁止通过手动滑动切换通知,只有mode = vertical,或者mode = horizontal且is-circular = false时有效(默认true) |
|||
* @event {Function} click 点击通告文字触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效 |
|||
* @event {Function} close 点击右侧关闭图标触发 |
|||
* @event {Function} getMore 点击右侧向右图标触发 |
|||
* @event {Function} end 列表的消息每次被播放一个周期时触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效 |
|||
* @example <u-notice-bar :more-icon="true" :list="list"></u-notice-bar> |
|||
*/ |
|||
export default { |
|||
name: "u-notice-bar", |
|||
props: { |
|||
// 显示的内容,数组 |
|||
list: { |
|||
type: Array, |
|||
default() { |
|||
return []; |
|||
} |
|||
}, |
|||
// 显示的主题,success|error|primary|info|warning |
|||
type: { |
|||
type: String, |
|||
default: 'warning' |
|||
}, |
|||
// 是否显示左侧的音量图标 |
|||
volumeIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 音量喇叭的大小 |
|||
volumeSize: { |
|||
type: [Number, String], |
|||
default: 34 |
|||
}, |
|||
// 是否显示右侧的右箭头图标 |
|||
moreIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示右侧的关闭图标 |
|||
closeIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否自动播放 |
|||
autoplay: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 文字颜色,各图标也会使用文字颜色 |
|||
color: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 滚动方向,horizontal-水平滚动,vertical-垂直滚动 |
|||
mode: { |
|||
type: String, |
|||
default: 'horizontal' |
|||
}, |
|||
// 是否显示 |
|||
show: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 字体大小,单位rpx |
|||
fontSize: { |
|||
type: [Number, String], |
|||
default: 28 |
|||
}, |
|||
// 滚动一个周期的时间长,单位ms |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 2000 |
|||
}, |
|||
// 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度 |
|||
speed: { |
|||
type: [Number, String], |
|||
default: 160 |
|||
}, |
|||
// 水平滚动时,是否采用衔接形式滚动 |
|||
// 水平衔接模式,采用的是swiper组件,水平滚动 |
|||
isCircular: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 播放状态,play-播放,paused-暂停 |
|||
playState: { |
|||
type: String, |
|||
default: 'play' |
|||
}, |
|||
// 是否禁止用手滑动切换 |
|||
// 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 |
|||
disableTouch: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 滚动通知设置圆角 |
|||
borderRadius: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 通知的边距 |
|||
padding: { |
|||
type: [Number, String], |
|||
default: '18rpx 24rpx' |
|||
}, |
|||
// list列表为空时,是否显示组件 |
|||
noListHidden: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
computed: { |
|||
// 如果设置show为false,或者设置了noListHidden为true,且list长度又为零的话,隐藏组件 |
|||
isShow() { |
|||
if(this.show == false || (this.noListHidden == true && this.list.length == 0)) return false; |
|||
else return true; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击通告栏 |
|||
click(index) { |
|||
this.$emit('click', index); |
|||
}, |
|||
// 点击关闭按钮 |
|||
close() { |
|||
this.$emit('close'); |
|||
}, |
|||
// 点击更多箭头按钮 |
|||
getMore() { |
|||
this.$emit('getMore'); |
|||
}, |
|||
// 滚动一个周期结束,只对垂直,或者水平步进形式有效 |
|||
end() { |
|||
this.$emit('end'); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-notice-bar-wrap { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-notice-bar { |
|||
padding: 18rpx 24rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-direction-row { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.u-left-icon { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-notice-box { |
|||
flex: 1; |
|||
@include vue-flex; |
|||
overflow: hidden; |
|||
margin-left: 12rpx; |
|||
} |
|||
|
|||
.u-right-icon { |
|||
margin-left: 12rpx; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-notice-content { |
|||
line-height: 1; |
|||
white-space: nowrap; |
|||
font-size: 26rpx; |
|||
animation: u-loop-animation 10s linear infinite both; |
|||
text-align: right; |
|||
// 这一句很重要,为了能让滚动左右连接起来 |
|||
padding-left: 100%; |
|||
} |
|||
|
|||
@keyframes u-loop-animation { |
|||
0% { |
|||
transform: translate3d(0, 0, 0); |
|||
} |
|||
|
|||
100% { |
|||
transform: translate3d(-100%, 0, 0); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,371 @@ |
|||
<template> |
|||
<view class="u-numberbox"> |
|||
<view @click="iconminus" class="u-icon-minus" @touchstart.stop.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" |
|||
:class="{ 'u-icon-disabled': disabled || inputVal <= min }" :style="{ |
|||
background: bgColor, |
|||
height: inputHeight + 'rpx', |
|||
color: color |
|||
}"> |
|||
<u-icon name="minus" :size="size"></u-icon> |
|||
</view> |
|||
<input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" |
|||
:class="{ 'u-input-disabled': disabled }" v-model="inputVal" class="u-number-input" @blur="onBlur" |
|||
@focus="onFocus" type="number" :style="{ |
|||
color: color, |
|||
fontSize: size + 'rpx', |
|||
background: bgColor, |
|||
height: inputHeight + 'rpx', |
|||
width: inputWidth + 'rpx' |
|||
}" /> |
|||
<view @click="iconplus" class="u-icon-plus" @touchstart.stop.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" |
|||
:class="{ 'u-icon-disabled': disabled || inputVal >= max }" :style="{ |
|||
background: bgColor, |
|||
height: inputHeight + 'rpx', |
|||
color: color |
|||
}"> |
|||
<u-icon name="plus" :size="size"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* numberBox 步进器 |
|||
* @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入 |
|||
* @tutorial https://www.uviewui.com/components/numberBox.html |
|||
* @property {Number} value 输入框初始值(默认1) |
|||
* @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5) |
|||
* @property {Number} min 用户可输入的最小值(默认0) |
|||
* @property {Number} max 用户可输入的最大值(默认99999) |
|||
* @property {Number} step 步长,每次加或减的值(默认1) |
|||
* @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false) |
|||
* @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false) |
|||
* @property {Boolean} positive-integer 是否只能输入正整数(默认true) |
|||
* @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26) |
|||
* @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233) |
|||
* @property {String | Number} input-width 输入框宽度,单位rpx(默认80) |
|||
* @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50) |
|||
* @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框 |
|||
* @property {Boolean} long-press 是否开启长按连续递增或递减(默认true) |
|||
* @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250) |
|||
* @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200) |
|||
* @event {Function} change 输入框内容发生变化时触发,对象形式 |
|||
* @event {Function} blur 输入框失去焦点时触发,对象形式 |
|||
* @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式 |
|||
* @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式 |
|||
* @example <u-number-box :min="1" :max="100"></u-number-box> |
|||
*/ |
|||
export default { |
|||
name: "u-number-box", |
|||
props: { |
|||
// 预显示的数字 |
|||
value: { |
|||
type: Number, |
|||
default: 1 |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#F2F3F5' |
|||
}, |
|||
// 最小值 |
|||
min: { |
|||
type: Number, |
|||
default: 0 |
|||
}, |
|||
// 最大值 |
|||
max: { |
|||
type: Number, |
|||
default: 99999 |
|||
}, |
|||
// 步进值,每次加或减的值 |
|||
step: { |
|||
type: Number, |
|||
default: 1 |
|||
}, |
|||
// 是否禁用加减操作 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// input的字体大小,单位rpx |
|||
size: { |
|||
type: [Number, String], |
|||
default: 26 |
|||
}, |
|||
// 加减图标的颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#323233' |
|||
}, |
|||
// input宽度,单位rpx |
|||
inputWidth: { |
|||
type: [Number, String], |
|||
default: 80 |
|||
}, |
|||
// input高度,单位rpx |
|||
inputHeight: { |
|||
type: [Number, String], |
|||
default: 50 |
|||
}, |
|||
// index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可 |
|||
index: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话 |
|||
// 设置disabled为false,disabledInput为true即可 |
|||
disabledInput: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 输入框于键盘之间的距离 |
|||
cursorSpacing: { |
|||
type: [Number, String], |
|||
default: 100 |
|||
}, |
|||
// 是否开启长按连续递增或递减 |
|||
longPress: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 开启长按触发后,每触发一次需要多久 |
|||
pressTime: { |
|||
type: [Number, String], |
|||
default: 250 |
|||
}, |
|||
// 是否只能输入大于或等于0的整数(正整数) |
|||
positiveInteger: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
watch: { |
|||
value(v1, v2) { |
|||
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误 |
|||
if (!this.changeFromInner) { |
|||
this.inputVal = v1; |
|||
// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true, |
|||
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处 |
|||
// 将changeFromInner设置为false |
|||
this.$nextTick(function() { |
|||
this.changeFromInner = false; |
|||
}) |
|||
} |
|||
}, |
|||
inputVal(v1, v2) { |
|||
// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串 |
|||
if (v1 == '') return; |
|||
let value = 0; |
|||
// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值 |
|||
let tmp = this.$u.test.number(v1); |
|||
if (tmp && v1 >= this.min && v1 <= this.max) value = v1; |
|||
else value = v2; |
|||
// 判断是否只能输入大于等于0的整数 |
|||
if (this.positiveInteger) { |
|||
// 小于0,或者带有小数点, |
|||
if (v1 < 0 || String(v1).indexOf('.') !== -1) { |
|||
value = v2; |
|||
// 双向绑定input的值,必须要使用$nextTick修改显示的值 |
|||
this.$nextTick(() => { |
|||
this.inputVal = v2; |
|||
}) |
|||
} |
|||
} |
|||
// 发出change事件 |
|||
this.handleChange(value, 'change'); |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态 |
|||
timer: null, // 用作长按的定时器 |
|||
changeFromInner: false, // 值发生变化,是来自内部还是外部 |
|||
innerChangeTimer: null, // 内部定时器 |
|||
}; |
|||
}, |
|||
created() { |
|||
this.inputVal = Number(this.value); |
|||
}, |
|||
computed: { |
|||
getCursorSpacing() { |
|||
// 先将值转为px单位,再转为数值 |
|||
return Number(uni.upx2px(this.cursorSpacing)); |
|||
} |
|||
}, |
|||
methods: { |
|||
iconminus(){ |
|||
// this.$emit("minus",'minus') |
|||
this.handleChange('value', 'minus'); |
|||
}, |
|||
iconplus(){ |
|||
this.handleChange('value', 'plus'); |
|||
}, |
|||
// 点击退格键 |
|||
btnTouchStart(callback) { |
|||
// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能 |
|||
this[callback](); |
|||
// 如果没开启长按功能,直接返回 |
|||
if (!this.longPress) return; |
|||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器 |
|||
this.timer = null; |
|||
this.timer = setInterval(() => { |
|||
// 执行加或减函数 |
|||
this[callback](); |
|||
}, this.pressTime); |
|||
}, |
|||
clearTimer() { |
|||
this.$nextTick(() => { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
}) |
|||
}, |
|||
minus() { |
|||
this.computeVal('minus'); |
|||
}, |
|||
plus() { |
|||
this.computeVal('plus'); |
|||
}, |
|||
// 为了保证小数相加减出现精度溢出的问题 |
|||
calcPlus(num1, num2) { |
|||
let baseNum, baseNum1, baseNum2; |
|||
try { |
|||
baseNum1 = num1.toString().split('.')[1].length; |
|||
} catch (e) { |
|||
baseNum1 = 0; |
|||
} |
|||
try { |
|||
baseNum2 = num2.toString().split('.')[1].length; |
|||
} catch (e) { |
|||
baseNum2 = 0; |
|||
} |
|||
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); |
|||
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度 |
|||
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision); |
|||
}, |
|||
// 为了保证小数相加减出现精度溢出的问题 |
|||
calcMinus(num1, num2) { |
|||
let baseNum, baseNum1, baseNum2; |
|||
try { |
|||
baseNum1 = num1.toString().split('.')[1].length; |
|||
} catch (e) { |
|||
baseNum1 = 0; |
|||
} |
|||
try { |
|||
baseNum2 = num2.toString().split('.')[1].length; |
|||
} catch (e) { |
|||
baseNum2 = 0; |
|||
} |
|||
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); |
|||
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; |
|||
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision); |
|||
}, |
|||
computeVal(type) { |
|||
uni.hideKeyboard(); |
|||
if (this.disabled) return; |
|||
let value = 0; |
|||
// 减 |
|||
if (type === 'minus') { |
|||
value = this.calcMinus(this.inputVal, this.step); |
|||
|
|||
} else if (type === 'plus') { |
|||
value = this.calcPlus(this.inputVal, this.step); |
|||
} |
|||
// 判断是否小于最小值和大于最大值 |
|||
if (value < this.min || value > this.max) { |
|||
return; |
|||
} |
|||
this.inputVal = value; |
|||
this.handleChange(value, type); |
|||
}, |
|||
// 处理用户手动输入的情况 |
|||
onBlur(event) { |
|||
let val = 0; |
|||
let value = event.detail.value; |
|||
// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值 |
|||
// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0 |
|||
if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min; |
|||
val = +value; |
|||
if (val > this.max) { |
|||
val = this.max; |
|||
} else if (val < this.min) { |
|||
val = this.min; |
|||
} |
|||
this.$nextTick(() => { |
|||
this.inputVal = val; |
|||
}) |
|||
this.handleChange(val, 'blur'); |
|||
}, |
|||
// 输入框获得焦点事件 |
|||
onFocus() { |
|||
this.$emit('focus'); |
|||
}, |
|||
handleChange(value, type) { |
|||
if (this.disabled) return; |
|||
// 清除定时器,避免造成混乱 |
|||
if (this.innerChangeTimer) { |
|||
clearTimeout(this.innerChangeTimer); |
|||
this.innerChangeTimer = null; |
|||
} |
|||
// 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果 |
|||
this.changeFromInner = true; |
|||
// 一定时间内,清除changeFromInner标记,否则内部值改变后 |
|||
// 外部通过程序修改value值,将会无效 |
|||
this.innerChangeTimer = setTimeout(() => { |
|||
this.changeFromInner = false; |
|||
}, 150); |
|||
this.$emit('input', Number(value)); |
|||
this.$emit(type, { |
|||
// 转为Number类型 |
|||
value: Number(value), |
|||
index: this.index |
|||
}) |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-numberbox { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-number-input { |
|||
position: relative; |
|||
text-align: center; |
|||
padding: 0; |
|||
margin: 0 6rpx; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-icon-plus, |
|||
.u-icon-minus { |
|||
width: 60rpx; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-icon-plus { |
|||
border-radius: 0 8rpx 8rpx 0; |
|||
} |
|||
|
|||
.u-icon-minus { |
|||
border-radius: 8rpx 0 0 8rpx; |
|||
} |
|||
|
|||
.u-icon-disabled { |
|||
color: #c8c9cc !important; |
|||
background: #f7f8fa !important; |
|||
} |
|||
|
|||
.u-input-disabled { |
|||
color: #c8c9cc !important; |
|||
background-color: #f2f3f5 !important; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,158 @@ |
|||
<template> |
|||
<view class="u-keyboard" @touchmove.stop.prevent="() => {}"> |
|||
<view class="u-keyboard-grids"> |
|||
<view |
|||
class="u-keyboard-grids-item" |
|||
:class="[btnBgGray(index) ? 'u-bg-gray' : '', index <= 2 ? 'u-border-top' : '', index < 9 ? 'u-border-bottom' : '', (index + 1) % 3 != 0 ? 'u-border-right' : '']" |
|||
:style="[itemStyle(index)]" |
|||
v-for="(item, index) in numList" |
|||
:key="index" |
|||
:hover-class="hoverClass(index)" |
|||
:hover-stay-time="100" |
|||
@tap="keyboardClick(item)"> |
|||
<view class="u-keyboard-grids-btn">{{ item }}</view> |
|||
</view> |
|||
<view class="u-keyboard-grids-item u-bg-gray" hover-class="u-hover-class" :hover-stay-time="100" @touchstart.stop="backspaceClick" |
|||
@touchend="clearTimer"> |
|||
<view class="u-keyboard-back u-keyboard-grids-btn"> |
|||
<u-icon name="backspace" :size="38" :bold="true"></u-icon> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
// 键盘的类型,number-数字键盘,card-身份证键盘 |
|||
mode: { |
|||
type: String, |
|||
default: 'number' |
|||
}, |
|||
// 是否显示键盘的"."符号 |
|||
dotEnabled: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否打乱键盘按键的顺序 |
|||
random: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
backspace: 'backspace', // 退格键内容 |
|||
dot: '.', // 点 |
|||
timer: null, // 长按多次删除的事件监听 |
|||
cardX: 'X' // 身份证的X符号 |
|||
}; |
|||
}, |
|||
computed: { |
|||
// 键盘需要显示的内容 |
|||
numList() { |
|||
let tmp = []; |
|||
if (!this.dotEnabled && this.mode == 'number') { |
|||
if (!this.random) { |
|||
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; |
|||
} else { |
|||
return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]); |
|||
} |
|||
} else if (this.dotEnabled && this.mode == 'number') { |
|||
if (!this.random) { |
|||
return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]; |
|||
} else { |
|||
return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]); |
|||
} |
|||
} else if (this.mode == 'card') { |
|||
if (!this.random) { |
|||
return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]; |
|||
} else { |
|||
return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]); |
|||
} |
|||
} |
|||
}, |
|||
// 按键的样式,在非乱序&&数字键盘&&不显示点按钮时,index为9时,按键占位两个空间 |
|||
itemStyle() { |
|||
return index => { |
|||
let style = {}; |
|||
if (this.mode == 'number' && !this.dotEnabled && index == 9) style.flex = '0 0 66.6666666666%'; |
|||
return style; |
|||
}; |
|||
}, |
|||
// 是否让按键显示灰色,只在非乱序&&数字键盘&&且允许点按键的时候 |
|||
btnBgGray() { |
|||
return index => { |
|||
if (!this.random && index == 9 && (this.mode != 'number' || (this.mode == 'number' && this.dotEnabled))) return true; |
|||
else return false; |
|||
}; |
|||
}, |
|||
hoverClass() { |
|||
return index => { |
|||
if (!this.random && index == 9 && (this.mode == 'number' && this.dotEnabled || this.mode == 'card')) return 'u-hover-class'; |
|||
else return 'u-keyboard-hover'; |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击退格键 |
|||
backspaceClick() { |
|||
this.$emit('backspace'); |
|||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器 |
|||
this.timer = null; |
|||
this.timer = setInterval(() => { |
|||
this.$emit('backspace'); |
|||
}, 250); |
|||
}, |
|||
clearTimer() { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
}, |
|||
// 获取键盘显示的内容 |
|||
keyboardClick(val) { |
|||
// 允许键盘显示点模式和触发非点按键时,将内容转为数字类型 |
|||
if (this.dotEnabled && val != this.dot && val != this.cardX) val = Number(val); |
|||
this.$emit('change', val); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-keyboard { |
|||
position: relative; |
|||
z-index: 1003; |
|||
} |
|||
|
|||
.u-keyboard-grids { |
|||
@include vue-flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.u-keyboard-grids-item { |
|||
flex: 0 0 33.3333333333%; |
|||
text-align: center; |
|||
font-size: 50rpx; |
|||
color: #333; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 110rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.u-bg-gray { |
|||
background-color: $u-border-color; |
|||
} |
|||
|
|||
.u-keyboard-back { |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
.u-keyboard-hover { |
|||
background-color: #e7e6eb; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,100 @@ |
|||
const cfg = require('./config.js'), |
|||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); |
|||
|
|||
function CssHandler(tagStyle) { |
|||
var styles = Object.assign(Object.create(null), cfg.userAgentStyles); |
|||
for (var item in tagStyle) |
|||
styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item]; |
|||
this.styles = styles; |
|||
} |
|||
CssHandler.prototype.getStyle = function(data) { |
|||
this.styles = new parser(data, this.styles).parse(); |
|||
} |
|||
CssHandler.prototype.match = function(name, attrs) { |
|||
var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : ''; |
|||
if (attrs.class) { |
|||
var items = attrs.class.split(' '); |
|||
for (var i = 0, item; item = items[i]; i++) |
|||
if (tmp = this.styles['.' + item]) |
|||
matched += tmp + ';'; |
|||
} |
|||
if (tmp = this.styles['#' + attrs.id]) |
|||
matched += tmp + ';'; |
|||
return matched; |
|||
} |
|||
module.exports = CssHandler; |
|||
|
|||
function parser(data, init) { |
|||
this.data = data; |
|||
this.floor = 0; |
|||
this.i = 0; |
|||
this.list = []; |
|||
this.res = init; |
|||
this.state = this.Space; |
|||
} |
|||
parser.prototype.parse = function() { |
|||
for (var c; c = this.data[this.i]; this.i++) |
|||
this.state(c); |
|||
return this.res; |
|||
} |
|||
parser.prototype.section = function() { |
|||
return this.data.substring(this.start, this.i); |
|||
} |
|||
// 状态机
|
|||
parser.prototype.Space = function(c) { |
|||
if (c == '.' || c == '#' || isLetter(c)) { |
|||
this.start = this.i; |
|||
this.state = this.Name; |
|||
} else if (c == '/' && this.data[this.i + 1] == '*') |
|||
this.Comment(); |
|||
else if (!cfg.blankChar[c] && c != ';') |
|||
this.state = this.Ignore; |
|||
} |
|||
parser.prototype.Comment = function() { |
|||
this.i = this.data.indexOf('*/', this.i) + 1; |
|||
if (!this.i) this.i = this.data.length; |
|||
this.state = this.Space; |
|||
} |
|||
parser.prototype.Ignore = function(c) { |
|||
if (c == '{') this.floor++; |
|||
else if (c == '}' && !--this.floor) { |
|||
this.list = []; |
|||
this.state = this.Space; |
|||
} |
|||
} |
|||
parser.prototype.Name = function(c) { |
|||
if (cfg.blankChar[c]) { |
|||
this.list.push(this.section()); |
|||
this.state = this.NameSpace; |
|||
} else if (c == '{') { |
|||
this.list.push(this.section()); |
|||
this.Content(); |
|||
} else if (c == ',') { |
|||
this.list.push(this.section()); |
|||
this.Comma(); |
|||
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_') |
|||
this.state = this.Ignore; |
|||
} |
|||
parser.prototype.NameSpace = function(c) { |
|||
if (c == '{') this.Content(); |
|||
else if (c == ',') this.Comma(); |
|||
else if (!cfg.blankChar[c]) this.state = this.Ignore; |
|||
} |
|||
parser.prototype.Comma = function() { |
|||
while (cfg.blankChar[this.data[++this.i]]); |
|||
if (this.data[this.i] == '{') this.Content(); |
|||
else { |
|||
this.start = this.i--; |
|||
this.state = this.Name; |
|||
} |
|||
} |
|||
parser.prototype.Content = function() { |
|||
this.start = ++this.i; |
|||
if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length; |
|||
var content = this.section(); |
|||
for (var i = 0, item; item = this.list[i++];) |
|||
if (this.res[item]) this.res[item] += ';' + content; |
|||
else this.res[item] = content; |
|||
this.list = []; |
|||
this.state = this.Space; |
|||
} |
|||
@ -0,0 +1,580 @@ |
|||
/** |
|||
* html 解析器 |
|||
* @tutorial https://github.com/jin-yufeng/Parser
|
|||
* @version 20201029 |
|||
* @author JinYufeng |
|||
* @listens MIT |
|||
*/ |
|||
const cfg = require('./config.js'), |
|||
blankChar = cfg.blankChar, |
|||
CssHandler = require('./CssHandler.js'), |
|||
windowWidth = uni.getSystemInfoSync().windowWidth; |
|||
var emoji; |
|||
|
|||
function MpHtmlParser(data, options = {}) { |
|||
this.attrs = {}; |
|||
this.CssHandler = new CssHandler(options.tagStyle, windowWidth); |
|||
this.data = data; |
|||
this.domain = options.domain; |
|||
this.DOM = []; |
|||
this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0; |
|||
options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http'; |
|||
this.options = options; |
|||
this.state = this.Text; |
|||
this.STACK = []; |
|||
// 工具函数
|
|||
this.bubble = () => { |
|||
for (var i = this.STACK.length, item; item = this.STACK[--i];) { |
|||
if (cfg.richOnlyTags[item.name]) return false; |
|||
item.c = 1; |
|||
} |
|||
return true; |
|||
} |
|||
this.decode = (val, amp) => { |
|||
var i = -1, |
|||
j, en; |
|||
while (1) { |
|||
if ((i = val.indexOf('&', i + 1)) == -1) break; |
|||
if ((j = val.indexOf(';', i + 2)) == -1) break; |
|||
if (val[i + 1] == '#') { |
|||
en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j)); |
|||
if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1); |
|||
} else { |
|||
en = val.substring(i + 1, j); |
|||
if (cfg.entities[en] || en == amp) |
|||
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1); |
|||
} |
|||
} |
|||
return val; |
|||
} |
|||
this.getUrl = url => { |
|||
if (url[0] == '/') { |
|||
if (url[1] == '/') url = this.options.prot + ':' + url; |
|||
else if (this.domain) url = this.domain + url; |
|||
} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://')) |
|||
url = this.domain + '/' + url; |
|||
return url; |
|||
} |
|||
this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>'); |
|||
this.section = () => this.data.substring(this.start, this.i); |
|||
this.parent = () => this.STACK[this.STACK.length - 1]; |
|||
this.siblings = () => this.STACK.length ? this.parent().children : this.DOM; |
|||
} |
|||
MpHtmlParser.prototype.parse = function() { |
|||
if (emoji) this.data = emoji.parseEmoji(this.data); |
|||
for (var c; c = this.data[this.i]; this.i++) |
|||
this.state(c); |
|||
if (this.state == this.Text) this.setText(); |
|||
while (this.STACK.length) this.popNode(this.STACK.pop()); |
|||
return this.DOM; |
|||
} |
|||
// 设置属性
|
|||
MpHtmlParser.prototype.setAttr = function() { |
|||
var name = this.attrName.toLowerCase(), |
|||
val = this.attrVal; |
|||
if (cfg.boolAttrs[name]) this.attrs[name] = 'T'; |
|||
else if (val) { |
|||
if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp')); |
|||
else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp'); |
|||
else if (name.substr(0, 5) != 'data-') this.attrs[name] = val; |
|||
} |
|||
this.attrVal = ''; |
|||
while (blankChar[this.data[this.i]]) this.i++; |
|||
if (this.isClose()) this.setNode(); |
|||
else { |
|||
this.start = this.i; |
|||
this.state = this.AttrName; |
|||
} |
|||
} |
|||
// 设置文本节点
|
|||
MpHtmlParser.prototype.setText = function() { |
|||
var back, text = this.section(); |
|||
if (!text) return; |
|||
text = (cfg.onText && cfg.onText(text, () => back = true)) || text; |
|||
if (back) { |
|||
this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i); |
|||
let j = this.start + text.length; |
|||
for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]); |
|||
return; |
|||
} |
|||
if (!this.pre) { |
|||
// 合并空白符
|
|||
var flag, tmp = []; |
|||
for (let i = text.length, c; c = text[--i];) |
|||
if (!blankChar[c]) { |
|||
tmp.unshift(c); |
|||
if (!flag) flag = 1; |
|||
} else { |
|||
if (tmp[0] != ' ') tmp.unshift(' '); |
|||
if (c == '\n' && flag == void 0) flag = 0; |
|||
} |
|||
if (flag == 0) return; |
|||
text = tmp.join(''); |
|||
} |
|||
this.siblings().push({ |
|||
type: 'text', |
|||
text: this.decode(text) |
|||
}); |
|||
} |
|||
// 设置元素节点
|
|||
MpHtmlParser.prototype.setNode = function() { |
|||
var node = { |
|||
name: this.tagName.toLowerCase(), |
|||
attrs: this.attrs |
|||
}, |
|||
close = cfg.selfClosingTags[node.name]; |
|||
if (this.options.nodes.length) node.type = 'node'; |
|||
this.attrs = {}; |
|||
if (!cfg.ignoreTags[node.name]) { |
|||
// 处理属性
|
|||
var attrs = node.attrs, |
|||
style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''), |
|||
styleObj = {}; |
|||
if (attrs.id) { |
|||
if (this.options.compress & 1) attrs.id = void 0; |
|||
else if (this.options.useAnchor) this.bubble(); |
|||
} |
|||
if ((this.options.compress & 2) && attrs.class) attrs.class = void 0; |
|||
switch (node.name) { |
|||
case 'a': |
|||
case 'ad': // #ifdef APP-PLUS
|
|||
case 'iframe': |
|||
// #endif
|
|||
this.bubble(); |
|||
break; |
|||
case 'font': |
|||
if (attrs.color) { |
|||
styleObj['color'] = attrs.color; |
|||
attrs.color = void 0; |
|||
} |
|||
if (attrs.face) { |
|||
styleObj['font-family'] = attrs.face; |
|||
attrs.face = void 0; |
|||
} |
|||
if (attrs.size) { |
|||
var size = parseInt(attrs.size); |
|||
if (size < 1) size = 1; |
|||
else if (size > 7) size = 7; |
|||
var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large']; |
|||
styleObj['font-size'] = map[size - 1]; |
|||
attrs.size = void 0; |
|||
} |
|||
break; |
|||
case 'embed': |
|||
// #ifndef APP-PLUS
|
|||
var src = node.attrs.src || '', |
|||
type = node.attrs.type || ''; |
|||
if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8')) |
|||
node.name = 'video'; |
|||
else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes( |
|||
'.aac')) |
|||
node.name = 'audio'; |
|||
else break; |
|||
if (node.attrs.autostart) |
|||
node.attrs.autoplay = 'T'; |
|||
node.attrs.controls = 'T'; |
|||
// #endif
|
|||
// #ifdef APP-PLUS
|
|||
this.bubble(); |
|||
break; |
|||
// #endif
|
|||
case 'video': |
|||
case 'audio': |
|||
if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]); |
|||
else this[`${node.name}Num`]++; |
|||
if (node.name == 'video') { |
|||
if (this.videoNum > 3) |
|||
node.lazyLoad = 1; |
|||
if (attrs.width) { |
|||
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px'); |
|||
attrs.width = void 0; |
|||
} |
|||
if (attrs.height) { |
|||
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px'); |
|||
attrs.height = void 0; |
|||
} |
|||
} |
|||
if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T'; |
|||
attrs.source = []; |
|||
if (attrs.src) { |
|||
attrs.source.push(attrs.src); |
|||
attrs.src = void 0; |
|||
} |
|||
this.bubble(); |
|||
break; |
|||
case 'td': |
|||
case 'th': |
|||
if (attrs.colspan || attrs.rowspan) |
|||
for (var k = this.STACK.length, item; item = this.STACK[--k];) |
|||
if (item.name == 'table') { |
|||
item.flag = 1; |
|||
break; |
|||
} |
|||
} |
|||
if (attrs.align) { |
|||
if (node.name == 'table') { |
|||
if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'; |
|||
else styleObj['float'] = attrs.align; |
|||
} else styleObj['text-align'] = attrs.align; |
|||
attrs.align = void 0; |
|||
} |
|||
// 压缩 style
|
|||
var styles = style.split(';'); |
|||
style = ''; |
|||
for (var i = 0, len = styles.length; i < len; i++) { |
|||
var info = styles[i].split(':'); |
|||
if (info.length < 2) continue; |
|||
let key = info[0].trim().toLowerCase(), |
|||
value = info.slice(1).join(':').trim(); |
|||
if (value[0] == '-' || value.includes('safe')) |
|||
style += `;${key}:${value}`; |
|||
else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) |
|||
styleObj[key] = value; |
|||
} |
|||
if (node.name == 'img') { |
|||
if (attrs.src && !attrs.ignore) { |
|||
if (this.bubble()) |
|||
attrs.i = (this.imgNum++).toString(); |
|||
else attrs.ignore = 'T'; |
|||
} |
|||
if (attrs.ignore) { |
|||
style += ';-webkit-touch-callout:none'; |
|||
styleObj['max-width'] = '100%'; |
|||
} |
|||
var width; |
|||
if (styleObj.width) width = styleObj.width; |
|||
else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : parseFloat(attrs.width) + 'px'; |
|||
if (width) { |
|||
styleObj.width = width; |
|||
attrs.width = '100%'; |
|||
if (parseInt(width) > windowWidth) { |
|||
styleObj.height = ''; |
|||
if (attrs.height) attrs.height = void 0; |
|||
} |
|||
} |
|||
if (styleObj.height) { |
|||
attrs.height = styleObj.height; |
|||
styleObj.height = ''; |
|||
} else if (attrs.height && !attrs.height.includes('%')) |
|||
attrs.height = parseFloat(attrs.height) + 'px'; |
|||
} |
|||
for (var key in styleObj) { |
|||
var value = styleObj[key]; |
|||
if (!value) continue; |
|||
if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1; |
|||
// 填充链接
|
|||
if (value.includes('url')) { |
|||
var j = value.indexOf('('); |
|||
if (j++ != -1) { |
|||
while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++; |
|||
value = value.substr(0, j) + this.getUrl(value.substr(j)); |
|||
} |
|||
} |
|||
// 转换 rpx
|
|||
else if (value.includes('rpx')) |
|||
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px'); |
|||
else if (key == 'white-space' && value.includes('pre') && !close) |
|||
this.pre = node.pre = true; |
|||
style += `;${key}:${value}`; |
|||
} |
|||
style = style.substr(1); |
|||
if (style) attrs.style = style; |
|||
if (!close) { |
|||
node.children = []; |
|||
if (node.name == 'pre' && cfg.highlight) { |
|||
this.remove(node); |
|||
this.pre = node.pre = true; |
|||
} |
|||
this.siblings().push(node); |
|||
this.STACK.push(node); |
|||
} else if (!cfg.filter || cfg.filter(node, this) != false) |
|||
this.siblings().push(node); |
|||
} else { |
|||
if (!close) this.remove(node); |
|||
else if (node.name == 'source') { |
|||
var parent = this.parent(); |
|||
if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src) |
|||
parent.attrs.source.push(node.attrs.src); |
|||
} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href; |
|||
} |
|||
if (this.data[this.i] == '/') this.i++; |
|||
this.start = this.i + 1; |
|||
this.state = this.Text; |
|||
} |
|||
// 移除标签
|
|||
MpHtmlParser.prototype.remove = function(node) { |
|||
var name = node.name, |
|||
j = this.i; |
|||
// 处理 svg
|
|||
var handleSvg = () => { |
|||
var src = this.data.substring(j, this.i + 1); |
|||
node.attrs.xmlns = 'http://www.w3.org/2000/svg'; |
|||
for (var key in node.attrs) { |
|||
if (key == 'viewbox') src = ` viewBox="${node.attrs.viewbox}"` + src; |
|||
else if (key != 'style') src = ` ${key}="${node.attrs[key]}"` + src; |
|||
} |
|||
src = '<svg' + src; |
|||
var parent = this.parent(); |
|||
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline')) |
|||
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style; |
|||
this.siblings().push({ |
|||
name: 'img', |
|||
attrs: { |
|||
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'), |
|||
style: node.attrs.style, |
|||
ignore: 'T' |
|||
} |
|||
}) |
|||
} |
|||
if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++); |
|||
while (1) { |
|||
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) { |
|||
if (name == 'pre' || name == 'svg') this.i = j; |
|||
else this.i = this.data.length; |
|||
return; |
|||
} |
|||
this.start = (this.i += 2); |
|||
while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++; |
|||
if (this.section().toLowerCase() == name) { |
|||
// 代码块高亮
|
|||
if (name == 'pre') { |
|||
this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data |
|||
.substr(this.i - 5); |
|||
return this.i = j; |
|||
} else if (name == 'style') |
|||
this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7)); |
|||
else if (name == 'title') |
|||
this.DOM.title = this.data.substring(j + 1, this.i - 7); |
|||
if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length; |
|||
if (name == 'svg') handleSvg(); |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
// 节点出栈处理
|
|||
MpHtmlParser.prototype.popNode = function(node) { |
|||
// 空白符处理
|
|||
if (node.pre) { |
|||
node.pre = this.pre = void 0; |
|||
for (let i = this.STACK.length; i--;) |
|||
if (this.STACK[i].pre) |
|||
this.pre = true; |
|||
} |
|||
var siblings = this.siblings(), |
|||
len = siblings.length, |
|||
childs = node.children; |
|||
if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false)) |
|||
return siblings.pop(); |
|||
var attrs = node.attrs; |
|||
// 替换一些标签名
|
|||
if (cfg.blockTags[node.name]) node.name = 'div'; |
|||
else if (!cfg.trustTags[node.name]) node.name = 'span'; |
|||
// 处理列表
|
|||
if (node.c && (node.name == 'ul' || node.name == 'ol')) { |
|||
if ((node.attrs.style || '').includes('list-style:none')) { |
|||
for (let i = 0, child; child = childs[i++];) |
|||
if (child.name == 'li') |
|||
child.name = 'div'; |
|||
} else if (node.name == 'ul') { |
|||
var floor = 1; |
|||
for (let i = this.STACK.length; i--;) |
|||
if (this.STACK[i].name == 'ul') floor++; |
|||
if (floor != 1) |
|||
for (let i = childs.length; i--;) |
|||
childs[i].floor = floor; |
|||
} else { |
|||
for (let i = 0, num = 1, child; child = childs[i++];) |
|||
if (child.name == 'li') { |
|||
child.type = 'ol'; |
|||
child.num = ((num, type) => { |
|||
if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26); |
|||
if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26); |
|||
if (type == 'i' || type == 'I') { |
|||
num = (num - 1) % 99 + 1; |
|||
var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'], |
|||
ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'], |
|||
res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || ''); |
|||
if (type == 'i') return res.toLowerCase(); |
|||
return res; |
|||
} |
|||
return num; |
|||
})(num++, attrs.type) + '.'; |
|||
} |
|||
} |
|||
} |
|||
// 处理表格
|
|||
if (node.name == 'table') { |
|||
var padding = parseFloat(attrs.cellpadding), |
|||
spacing = parseFloat(attrs.cellspacing), |
|||
border = parseFloat(attrs.border); |
|||
if (node.c) { |
|||
if (isNaN(padding)) padding = 2; |
|||
if (isNaN(spacing)) spacing = 2; |
|||
} |
|||
if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`; |
|||
if (node.flag && node.c) { |
|||
// 有 colspan 或 rowspan 且含有链接的表格转为 grid 布局实现
|
|||
attrs.style = `${attrs.style || ''};${spacing ? `;grid-gap:${spacing}px` : ';border-left:0;border-top:0'}`; |
|||
var row = 1, |
|||
col = 1, |
|||
colNum, |
|||
trs = [], |
|||
children = [], |
|||
map = {}; |
|||
(function f(ns) { |
|||
for (var i = 0; i < ns.length; i++) { |
|||
if (ns[i].name == 'tr') trs.push(ns[i]); |
|||
else f(ns[i].children || []); |
|||
} |
|||
})(node.children) |
|||
for (let i = 0; i < trs.length; i++) { |
|||
for (let j = 0, td; td = trs[i].children[j]; j++) { |
|||
if (td.name == 'td' || td.name == 'th') { |
|||
while (map[row + '.' + col]) col++; |
|||
var cell = { |
|||
name: 'div', |
|||
c: 1, |
|||
attrs: { |
|||
style: (td.attrs.style || '') + (border ? `;border:${border}px solid gray` + (spacing ? '' : |
|||
';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') |
|||
}, |
|||
children: td.children |
|||
} |
|||
if (td.attrs.colspan) { |
|||
cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + parseInt(td.attrs.colspan)); |
|||
if (!td.attrs.rowspan) cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + 1); |
|||
col += parseInt(td.attrs.colspan) - 1; |
|||
} |
|||
if (td.attrs.rowspan) { |
|||
cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + parseInt(td.attrs.rowspan)); |
|||
if (!td.attrs.colspan) cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + 1); |
|||
for (var k = 1; k < td.attrs.rowspan; k++) map[(row + k) + '.' + col] = 1; |
|||
} |
|||
children.push(cell); |
|||
col++; |
|||
} |
|||
} |
|||
if (!colNum) { |
|||
colNum = col - 1; |
|||
attrs.style += `;grid-template-columns:repeat(${colNum},auto)` |
|||
} |
|||
col = 1; |
|||
row++; |
|||
} |
|||
node.children = children; |
|||
} else { |
|||
attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`; |
|||
if (border || padding) |
|||
(function f(ns) { |
|||
for (var i = 0, n; n = ns[i]; i++) { |
|||
if (n.name == 'th' || n.name == 'td') { |
|||
if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style || ''}`; |
|||
if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style || ''}`; |
|||
} else f(n.children || []); |
|||
} |
|||
})(childs) |
|||
} |
|||
if (this.options.autoscroll) { |
|||
var table = Object.assign({}, node); |
|||
node.name = 'div'; |
|||
node.attrs = { |
|||
style: 'overflow:scroll' |
|||
} |
|||
node.children = [table]; |
|||
} |
|||
} |
|||
this.CssHandler.pop && this.CssHandler.pop(node); |
|||
// 自动压缩
|
|||
if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div') |
|||
siblings[len - 1] = childs[0]; |
|||
} |
|||
// 状态机
|
|||
MpHtmlParser.prototype.Text = function(c) { |
|||
if (c == '<') { |
|||
var next = this.data[this.i + 1], |
|||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); |
|||
if (isLetter(next)) { |
|||
this.setText(); |
|||
this.start = this.i + 1; |
|||
this.state = this.TagName; |
|||
} else if (next == '/') { |
|||
this.setText(); |
|||
if (isLetter(this.data[++this.i + 1])) { |
|||
this.start = this.i + 1; |
|||
this.state = this.EndTag; |
|||
} else this.Comment(); |
|||
} else if (next == '!' || next == '?') { |
|||
this.setText(); |
|||
this.Comment(); |
|||
} |
|||
} |
|||
} |
|||
MpHtmlParser.prototype.Comment = function() { |
|||
var key; |
|||
if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->'; |
|||
else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>'; |
|||
else key = '>'; |
|||
if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length; |
|||
else this.i += key.length - 1; |
|||
this.start = this.i + 1; |
|||
this.state = this.Text; |
|||
} |
|||
MpHtmlParser.prototype.TagName = function(c) { |
|||
if (blankChar[c]) { |
|||
this.tagName = this.section(); |
|||
while (blankChar[this.data[this.i]]) this.i++; |
|||
if (this.isClose()) this.setNode(); |
|||
else { |
|||
this.start = this.i; |
|||
this.state = this.AttrName; |
|||
} |
|||
} else if (this.isClose()) { |
|||
this.tagName = this.section(); |
|||
this.setNode(); |
|||
} |
|||
} |
|||
MpHtmlParser.prototype.AttrName = function(c) { |
|||
if (c == '=' || blankChar[c] || this.isClose()) { |
|||
this.attrName = this.section(); |
|||
if (blankChar[c]) |
|||
while (blankChar[this.data[++this.i]]); |
|||
if (this.data[this.i] == '=') { |
|||
while (blankChar[this.data[++this.i]]); |
|||
this.start = this.i--; |
|||
this.state = this.AttrValue; |
|||
} else this.setAttr(); |
|||
} |
|||
} |
|||
MpHtmlParser.prototype.AttrValue = function(c) { |
|||
if (c == '"' || c == "'") { |
|||
this.start++; |
|||
if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length; |
|||
this.attrVal = this.section(); |
|||
this.i++; |
|||
} else { |
|||
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++); |
|||
this.attrVal = this.section(); |
|||
} |
|||
this.setAttr(); |
|||
} |
|||
MpHtmlParser.prototype.EndTag = function(c) { |
|||
if (blankChar[c] || c == '>' || c == '/') { |
|||
var name = this.section().toLowerCase(); |
|||
for (var i = this.STACK.length; i--;) |
|||
if (this.STACK[i].name == name) break; |
|||
if (i != -1) { |
|||
var node; |
|||
while ((node = this.STACK.pop()).name != name) this.popNode(node); |
|||
this.popNode(node); |
|||
} else if (name == 'p' || name == 'br') |
|||
this.siblings().push({ |
|||
name, |
|||
attrs: {} |
|||
}); |
|||
this.i = this.data.indexOf('>', this.i); |
|||
this.start = this.i + 1; |
|||
if (this.i == -1) this.i = this.data.length; |
|||
else this.state = this.Text; |
|||
} |
|||
} |
|||
module.exports = MpHtmlParser; |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue