675 lines
15 KiB
Plaintext
675 lines
15 KiB
Plaintext
<template>
|
|
<view class="xSlideVerify xSlideVerifyNodes"
|
|
:style="{width:_width,height:_height,backgroundColor:_color,borderRadius:_round}">
|
|
<view :style="{
|
|
borderRadius:_round,
|
|
width:'100%',
|
|
height:'100%'
|
|
}" class="xSlideVerifyBox2">
|
|
<view class="xSlideVerifyActive"
|
|
<!-- #ifndef MP -->
|
|
:style="{width:(totalWidth - barWidth)+'px',height:'100%'}"
|
|
<!-- #endif -->
|
|
<!-- #ifdef MP -->
|
|
:style="{width:`100%`,height:'100%'}"
|
|
<!-- #endif -->
|
|
>
|
|
<view class="xSlideVerifyActiveBg" :style="{
|
|
width:(percentage)+'%',
|
|
height:'100%',
|
|
'border-top-left-radius':_round,
|
|
'border-bottom-left-radius':_round,
|
|
backgroundColor:_activeColor
|
|
}">
|
|
<!--
|
|
@slot 激活时的标签插槽
|
|
-->
|
|
<slot name='activeLabel'>
|
|
<text :style="{color:'white',fontSize:'14px'}">{{_tips}}</text>
|
|
</slot>
|
|
</view>
|
|
</view>
|
|
<view ref="xSlideVerifyBar" class="xSlideVerifyBar xSlideVerifyNodes"
|
|
|
|
<!-- #ifdef WEB -->
|
|
@mousedown="mmStart"
|
|
<!-- #endif -->
|
|
|
|
@touchstart="mStart"
|
|
@touchmove="mMove" @touchend="mEnd" :style="{
|
|
'border-top-right-radius':_round,
|
|
'border-bottom-right-radius':_round,
|
|
backgroundColor:_btnColor,width:_height,height:'100%',left:boxLeft+'px'}">
|
|
<!--
|
|
@slot 拖动时的按钮插槽
|
|
-->
|
|
<slot name="btn">
|
|
<x-icon :font-size="_btnFontSize" :color="_btnFontColor" :dark-color="_btnFontColor"
|
|
:name="status==2?'check-double-line':'arrow-right-double-line'"></x-icon>
|
|
</slot>
|
|
</view>
|
|
<view
|
|
class="xSlideVerifyBar2" :style="{
|
|
borderRadius:_round,
|
|
backgroundColor:`${_successColor}`,
|
|
opacity:props.showVerifyBox&&status!=2?'0.1':'0',
|
|
width:_height,height:'100%',left:(_verifyPos)+'px'}">
|
|
<!--
|
|
@slot 目标虚拟框位置的插槽
|
|
-->
|
|
<slot name="target"></slot>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="xSlideVerifyTextBox" >
|
|
<!--
|
|
@slot 未激活时的标签插槽
|
|
-->
|
|
<slot name="label">
|
|
<text :style="{color:_tipsTextColor,opacity:_isDark?'0.5':'1'}">{{_tipsText}}</text>
|
|
</slot>
|
|
|
|
<!-- 标签动态 -->
|
|
<view @transitionend="aniEnd" ref="xSlideVerifyLabelAni" class="xSlideVerifyLabelAni"></view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
|
|
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
|
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
|
import { xConfig } from "../../config/xConfig.uts"
|
|
import { PropType, inject, getCurrentInstance, onMounted, watch, onBeforeUnmount } from "vue"
|
|
|
|
/**
|
|
* @name 滑动验证 xSlideVerify
|
|
* @description 可以防止机器人刷新页面,防止恶意注册,防止恶意评论等
|
|
* @page /pages/index/slide-verify
|
|
* @category 其它组件
|
|
* @constant 平台兼容
|
|
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
|
*/
|
|
defineOptions({name:"xSlideVerify"})
|
|
const i18n = xConfig.i18n;
|
|
const proxy = getCurrentInstance()!.proxy!
|
|
const emits = defineEmits([
|
|
/**
|
|
* 开始拖动验证时触发
|
|
*/
|
|
'start',
|
|
/**
|
|
* 验证成功时触发
|
|
*/
|
|
'success',
|
|
/**
|
|
* 验证失败时触发
|
|
*/
|
|
'fail',
|
|
/**
|
|
* 用户拖放结束时触发
|
|
*/
|
|
'completed',
|
|
/**
|
|
* 重置时触发
|
|
*/
|
|
'reset'])
|
|
const props = defineProps({
|
|
/**
|
|
* 宽,允许使用auto,百分比,数字,带单位的字符串
|
|
*/
|
|
width: {
|
|
type: String,
|
|
default: 'auto'
|
|
},
|
|
/**
|
|
* 高不允许使用auto
|
|
*/
|
|
height: {
|
|
type: String,
|
|
default: '50'
|
|
},
|
|
/**
|
|
* 未激活背景色
|
|
*/
|
|
color: {
|
|
type: String,
|
|
default: '#e9ecf0'
|
|
},
|
|
/**
|
|
* 激活时的状态背景色,空取全局
|
|
*/
|
|
activeColor: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/**
|
|
* 暗黑时的未激活背景色
|
|
*/
|
|
darkColor: {
|
|
type: String,
|
|
default: '#21232c'
|
|
},
|
|
/**
|
|
* 验证通过时的背景色
|
|
*/
|
|
successColor: {
|
|
type: String,
|
|
default: 'success'
|
|
},
|
|
/**
|
|
* 验证失败时的背景色
|
|
*/
|
|
failColor: {
|
|
type: String,
|
|
default: 'error'
|
|
},
|
|
/**
|
|
* 按钮背景
|
|
*/
|
|
btnColor: {
|
|
type: String,
|
|
default: 'white'
|
|
},
|
|
/**
|
|
* 暗黑按钮背景,如果为空取sheetDarkColor
|
|
*/
|
|
btnDarkColor: {
|
|
type: String,
|
|
default: '#3b3e4d'
|
|
},
|
|
/**
|
|
* 空取全局主题色
|
|
*/
|
|
btnFontColor: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 按钮上的图标大小
|
|
*/
|
|
btnFontSize: {
|
|
type: String,
|
|
default: "20"
|
|
},
|
|
/**
|
|
* 验证正确的位置
|
|
* 0-100是百分比,让用户滑动到哪个位置触发验证正确。
|
|
*/
|
|
verifyPos: {
|
|
type: Number,
|
|
default: 100
|
|
},
|
|
/**
|
|
* 是否显示目标指示框
|
|
*/
|
|
showVerifyBox:{
|
|
type:Boolean,
|
|
default:false
|
|
},
|
|
/**
|
|
* 默认的提示验证文本,请拖动到指定位置
|
|
*/
|
|
tipsText: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 失败时的文本,验证成功
|
|
*/
|
|
tipsTextSuccess: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 成功时的文本,验证失败
|
|
*/
|
|
tipsTextFail: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 底部提示文本颜色
|
|
*/
|
|
tipsTextColor: {
|
|
type: String,
|
|
default: "#b8b8b8"
|
|
},
|
|
/**
|
|
* 圆角
|
|
*/
|
|
round: {
|
|
type: String,
|
|
default: "25"
|
|
},
|
|
/**
|
|
* 验证失败时,是否自动重置
|
|
*/
|
|
resetAuto:{
|
|
type:Boolean,
|
|
default:true
|
|
}
|
|
})
|
|
//1初始状态,2成功,3失败
|
|
const status = ref(1)
|
|
const isCompleted = ref(false)
|
|
const _tipsText = computed(():string => {
|
|
if(props.tipsText == '') return i18n.t('tmui4x.slideVerify.tipsText');
|
|
return props.tipsText
|
|
})
|
|
const _tipsTextColor = computed(():string => props.tipsTextColor)
|
|
const _isDark = computed(():boolean => xConfig.dark=='dark')
|
|
const _tips = computed(():string => {
|
|
if (status.value == 1) return ''
|
|
if (status.value == 2) {
|
|
if(props.tipsTextSuccess == '') return i18n.t('tmui4x.slideVerify.tipsTextSuccess');
|
|
return props.tipsTextSuccess
|
|
}
|
|
if(props.tipsTextFail == '') return i18n.t('tmui4x.slideVerify.tipsTextFail');
|
|
return props.tipsTextFail
|
|
})
|
|
const _width = computed(():string => {
|
|
return checkIsCssUnit(props.width, xConfig.unit)
|
|
})
|
|
const _height = computed(():string => {
|
|
return checkIsCssUnit(props.height, xConfig.unit)
|
|
})
|
|
const _round = computed(():string => {
|
|
return checkIsCssUnit(props.round, xConfig.unit)
|
|
})
|
|
const _btnFontSize = computed(():string => {
|
|
return checkIsCssUnit(props.btnFontSize, xConfig.unit)
|
|
})
|
|
const _activeColor = computed(():string => {
|
|
if (status.value == 2 && isCompleted.value) return getDefaultColor(props.successColor)
|
|
if (status.value == 3 && isCompleted.value) return getDefaultColor(props.failColor)
|
|
let color = props.activeColor;
|
|
if (color == '') {
|
|
color = xConfig.color
|
|
}
|
|
return getDefaultColor(color)
|
|
})
|
|
const _successColor = computed(():string => {
|
|
return getDefaultColor(props.successColor)
|
|
})
|
|
const _color = computed(():string => {
|
|
let colorbg = props.color;
|
|
if (xConfig.dark == 'dark') {
|
|
colorbg = xConfig.inputDarkColor
|
|
if (props.darkColor != '') {
|
|
colorbg = props.darkColor
|
|
}
|
|
}
|
|
return getDefaultColor(colorbg)
|
|
})
|
|
const _btnColor = computed(():string => {
|
|
let colorbg = props.btnColor;
|
|
if (xConfig.dark == 'dark') {
|
|
colorbg = xConfig.sheetDarkColor
|
|
if (props.btnDarkColor != '') {
|
|
colorbg = props.btnDarkColor
|
|
}
|
|
}
|
|
return getDefaultColor(colorbg)
|
|
})
|
|
const _btnFontColor = computed(():string => {
|
|
if ((status.value == 2 && isCompleted.value)) return getDefaultColor(props.successColor)
|
|
let colorbg = props.btnFontColor;
|
|
if (colorbg == '') {
|
|
colorbg = xConfig.color
|
|
}
|
|
return getDefaultColor(colorbg)
|
|
})
|
|
const totalWidth = ref(0)
|
|
const barWidth = ref(0)
|
|
const barHeight = ref(0)
|
|
const _min = 0
|
|
const _max = 100
|
|
const percentage = ref(0)
|
|
const safePercentage = ref(0)
|
|
const _x = ref(0)
|
|
const _y = ref(0)
|
|
const _now_x = ref(0)
|
|
const isMoveing = ref(false)
|
|
const boxLeft = ref(0)
|
|
const anipos = ref(0)
|
|
let tid = 0
|
|
let aniTid = 56
|
|
const _verifyPos = computed(():number => props.verifyPos/100 * (totalWidth.value - barWidth.value))
|
|
const eventIds = Math.random().toString(16).substring(2,8)
|
|
const getNodeInfo = () => {
|
|
// xSlideVerifyBar
|
|
// xSlideVerify
|
|
uni.createSelectorQuery()
|
|
.in(proxy)
|
|
.selectAll('.xSlideVerifyNodes')
|
|
.boundingClientRect()
|
|
.exec((rect) => {
|
|
let nodes = rect[0] as NodeInfo[]
|
|
|
|
if (nodes.length != 2) return;
|
|
let node1 = nodes[0] as NodeInfo
|
|
let node2 = nodes[1] as NodeInfo
|
|
totalWidth.value = node1.width!
|
|
barWidth.value = node2.width!
|
|
barHeight.value = node2.width!
|
|
let node = proxy.$refs['xSlideVerifyBar'] as UniElement
|
|
node.style.setProperty('left','0px')
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 比例转值
|
|
*/
|
|
const proToValue = (val : number) : number => {
|
|
|
|
const minValue = Math.min(_min, _max);
|
|
const maxValue = Math.max(_min, _max);
|
|
if (minValue === maxValue) {
|
|
return minValue;
|
|
}
|
|
return Math.ceil((val / 100) * (maxValue - minValue) + minValue);
|
|
}
|
|
/**
|
|
* 值转比例
|
|
*/
|
|
const valueToPro = (val : number) : number => {
|
|
let min = Math.min(_min, _max);
|
|
let max = Math.max(_min, _max);
|
|
let realval = Math.max(Math.min(val, max), min);
|
|
return (realval - min) / (max - min) * 100;
|
|
}
|
|
// 位置宽转比例
|
|
const widthToPro = (val : number) : number => {
|
|
let maxWidth = totalWidth.value - barWidth.value;
|
|
let bl = val / maxWidth;
|
|
return bl * 100
|
|
}
|
|
const widthToPro2 = (val : number) : number => {
|
|
let maxWidth = totalWidth.value - barWidth.value;
|
|
let bl = val / maxWidth;
|
|
return bl * 100
|
|
}
|
|
// 比例转位置.
|
|
const ProToWidthByval = (percentage : number) : number => {
|
|
let maxWidth = totalWidth.value;
|
|
let val = maxWidth * percentage / 100;
|
|
return Math.ceil(val)
|
|
}
|
|
|
|
/**
|
|
* 复位
|
|
* @public
|
|
*/
|
|
const reset = () => {
|
|
status.value = 1
|
|
boxLeft.value = 0
|
|
isCompleted.value = false;
|
|
percentage.value = 0
|
|
// #ifdef APP-ANDROID||MP-WEIXIN||APP-HARMONY
|
|
let node = proxy.$refs['xSlideVerifyBar'] as UniElement|null
|
|
if(node==null) return;
|
|
node.style.setProperty("left",'0px');
|
|
// #endif
|
|
}
|
|
|
|
const __move = (mx:number,my:number)=>{
|
|
|
|
let node = proxy.$refs['xSlideVerifyBar'] as UniElement
|
|
|
|
let x = mx - _x.value
|
|
let diffX = my - _now_x.value
|
|
let maxX = totalWidth.value - barWidth.value;
|
|
|
|
x = Math.max(Math.min(maxX, x), 0)
|
|
|
|
|
|
// #ifdef APP-ANDROID||MP-WEIXIN||APP-HARMONY
|
|
node.style.setProperty("left", `${x}px`)
|
|
// #endif
|
|
// #ifdef APP-IOS||WEB
|
|
boxLeft.value = x
|
|
// #endif
|
|
|
|
|
|
percentage.value = Math.ceil(widthToPro(x));
|
|
safePercentage.value = Math.ceil(widthToPro2(x));
|
|
_now_x.value = x
|
|
}
|
|
|
|
const __moveStart = (mx:number,my:number)=>{
|
|
emits('start')
|
|
let node = proxy.$refs['xSlideVerifyBar'] as UniElement
|
|
let leftpos = parseInt(node.style.getPropertyValue("left")! as string)
|
|
_x.value = mx - leftpos
|
|
_now_x.value = mx
|
|
isMoveing.value = true
|
|
}
|
|
const mStart = (evt : UniTouchEvent) => {
|
|
evt.preventDefault()
|
|
if (isCompleted.value) return
|
|
isMoveing.value = true;
|
|
__moveStart(evt.changedTouches[0].clientX,evt.changedTouches[0].clientY)
|
|
}
|
|
const mMove = (evt : UniTouchEvent) => {
|
|
evt.preventDefault()
|
|
if (isCompleted.value) return
|
|
__move(evt.changedTouches[0].clientX,evt.changedTouches[0].clientY)
|
|
|
|
}
|
|
|
|
const mtransEnd = (x:number) => {
|
|
isMoveing.value = false
|
|
isMoveing.value = false;
|
|
if(Math.abs(_now_x.value - x)==0){
|
|
return;
|
|
}
|
|
isCompleted.value = true;
|
|
let diff = Math.abs(percentage.value-props.verifyPos);
|
|
// 给5个像素的容差。
|
|
if (diff >= 0 && diff <= 4) {
|
|
status.value = 2;
|
|
emits('success')
|
|
} else {
|
|
status.value = 3;
|
|
emits('fail')
|
|
if(props.resetAuto){
|
|
setTimeout(function () {
|
|
emits('reset')
|
|
reset()
|
|
}, 1500);
|
|
}
|
|
|
|
}
|
|
emits('completed')
|
|
}
|
|
const mEnd = (evt : UniTouchEvent) => {
|
|
mtransEnd(evt.changedTouches[0].clientX)
|
|
}
|
|
const appPlayPos = ()=>{
|
|
let el = proxy.$refs['xSlideVerifyLabelAni'] as UniElement;
|
|
el.style.setProperty("transition-duration", `1.5s`)
|
|
el.style.setProperty("left", totalWidth.value+'px')
|
|
// #ifdef APP-HARMONY
|
|
|
|
aniTid = setTimeout(function() {
|
|
let el = proxy.$refs['xSlideVerifyLabelAni'] as UniElement;
|
|
el.style.setProperty("transition-duration", `0s`)
|
|
el.style.setProperty("left", `-20%`)
|
|
appPlayPos()
|
|
}, 1500);
|
|
// #endif
|
|
|
|
}
|
|
|
|
|
|
// #ifdef WEB
|
|
const mmStart = (evt:UniMouseEvent) => {
|
|
evt.preventDefault()
|
|
window.xSliderEvents = eventIds
|
|
if (isCompleted.value) return
|
|
isMoveing.value = true;
|
|
__moveStart(evt.clientX,evt.clientY)
|
|
|
|
}
|
|
const mmMove = (evt:UniMouseEvent) => {
|
|
|
|
if (isCompleted.value||!isMoveing.value||window.xSliderEvents!=eventIds ) return
|
|
__move(evt.clientX,evt.clientY)
|
|
}
|
|
const mmEnd = (evt:UniMouseEvent) => {
|
|
if (!isMoveing.value||window.xSliderEvents!=eventIds ) return
|
|
isMoveing.value = false;
|
|
mtransEnd(evt.clientX)
|
|
}
|
|
|
|
document.body.addEventListener('mouseup', mmEnd);
|
|
document.body.addEventListener('mousemove', mmMove);
|
|
// #endif
|
|
|
|
const aniEnd = ()=>{
|
|
let el = proxy.$refs['xSlideVerifyLabelAni'] as UniElement;
|
|
el.style.setProperty("transition-duration", `1.5s`)
|
|
el.style.setProperty("left", `-20%`)
|
|
clearTimeout(tid)
|
|
tid = setTimeout(function() {
|
|
appPlayPos();
|
|
}, 50);
|
|
}
|
|
|
|
onMounted(() => {
|
|
|
|
// #ifdef APP||MP-WEIXIN
|
|
clearTimeout(tid)
|
|
tid = setTimeout(function() {
|
|
getNodeInfo()
|
|
appPlayPos();
|
|
}, 250);
|
|
// #endif
|
|
// #ifdef WEB
|
|
getNodeInfo()
|
|
// #endif
|
|
|
|
})
|
|
onBeforeUnmount(()=>{
|
|
clearTimeout(tid)
|
|
// #ifdef APP-HARMONY
|
|
clearTimeout(aniTid)
|
|
// #endif
|
|
// #ifdef WEB
|
|
document.body.removeEventListener('mouseup', mmEnd);
|
|
document.body.removeEventListener('mousemove', mmMove);
|
|
// #endif
|
|
})
|
|
|
|
|
|
// 手动重置
|
|
defineExpose({reset})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.xSlideVerify {
|
|
position: relative;
|
|
}
|
|
|
|
.xSlideVerifyBox {
|
|
position: absolute;
|
|
}
|
|
|
|
.xSlideVerifyTextBox {
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(196, 248, 182, 0.0);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
position: relative;
|
|
pointer-events: none;
|
|
}
|
|
.xSlideVerifyLabelAni{
|
|
pointer-events: none;
|
|
width: 80px;
|
|
height: 100%;
|
|
position: absolute;
|
|
left: 0px;
|
|
top: 0px;
|
|
z-index: 2;
|
|
background-image: linear-gradient(to right,rgba(255,255,255,0),rgba(255,255,255,0.3));
|
|
/* #ifdef WEB||MP */
|
|
animation: anidh 1s infinite linear;
|
|
/* #endif */
|
|
|
|
/* #ifdef APP-ANDROID || APP-IOS */
|
|
transition-duration: 1s;
|
|
transition-property: left,width;
|
|
transition-timing-function: linear;
|
|
/* #endif */
|
|
/* #ifdef APP-HARMONY */
|
|
transition-duration: 0s;
|
|
transition-property: left,width;
|
|
transition-timing-function: linear;
|
|
/* #endif */
|
|
}
|
|
/* #ifdef WEB||MP */
|
|
@keyframes anidh {
|
|
0%{
|
|
left: 0%;
|
|
}
|
|
100%{
|
|
left: 100%;
|
|
}
|
|
}
|
|
/* #endif */
|
|
|
|
.xSlideVerifyBox2 {
|
|
position: absolute;
|
|
left: 0px;
|
|
top: 0px;
|
|
z-index: 3
|
|
}
|
|
|
|
.xSlideVerifyBar {
|
|
position: absolute;
|
|
left: 0px;
|
|
top: 0px;
|
|
z-index: 5;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.xSlideVerifyBar2 {
|
|
position: absolute;
|
|
left: 0px;
|
|
top: 0px;
|
|
z-index: 6;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
.xSlideVerifyActiveBg {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
/* #ifndef APP-HARMONY */
|
|
transition-duration: 0.24s;
|
|
/* #endif */
|
|
/* #ifdef APP-HARMONY */
|
|
transition-duration: 0s;
|
|
/* #endif */
|
|
transition-property: background-color;
|
|
transition-timing-function: linear;
|
|
}
|
|
</style> |