Files
2026-06-03 10:16:37 +08:00

245 lines
6.5 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view :class="rootClass" :style="rootStyle">
<!-- 背景提示文字 -->
<view class="wd-slide-verify__text">
<slot name="text">
<text class="wd-slide-verify__text-inner">
{{ slideVerifyText }}
</text>
</slot>
</view>
<!-- 滑过区域 -->
<view class="wd-slide-verify__track" :style="trackStyle">
<view class="wd-slide-verify__track-text">
<slot name="success-text">
<text class="wd-slide-verify__track-text--success">
{{ slideVerifySuccessText }}
</text>
</slot>
</view>
</view>
<!-- 滑块 -->
<view
class="wd-slide-verify__button"
@touchstart.prevent="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend="onTouchEnd"
:style="buttonStyle"
>
<slot v-if="isPass" name="success-icon">
<view
class="wd-slide-verify__button-icon--success"
:style="{
backgroundColor: activeBackgroundColor
}"
>
<wd-icon :name="successIcon" :size="successIconSize" color="#fff" />
</view>
</slot>
<slot v-else name="icon">
<view class="wd-slide-verify__button-icon">
<wd-icon :name="icon" :size="iconSize" />
</view>
</slot>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-slide-verify',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { ref, computed, onBeforeUnmount, type CSSProperties } from 'vue'
import wdIcon from '../wd-icon/wd-icon.vue'
import { slideVerifyProps, type SlideVerifyExpose } from './types'
import { useTouch } from '../composables/useTouch'
import { useTranslate } from '../composables/useTranslate'
import { objToStyle, addUnit, isDef } from '../common/util'
const props = defineProps(slideVerifyProps)
const emit = defineEmits(['success', 'fail'])
const touch = useTouch()
const { translate } = useTranslate('slideVerify')
const slideVerifyText = computed(() => {
return isDef(props.text) && props.text !== '' ? props.text : translate('text')
})
const slideVerifySuccessText = computed(() => {
return isDef(props.successText) && props.successText !== '' ? props.successText : translate('successText')
})
const rootClass = computed(() => {
return [
'wd-slide-verify',
{
'is-disabled': props.disabled,
'is-success': isPass.value,
'is-dragging': isDragging.value
},
props.customClass
]
})
const rootStyle = computed(() => {
const style: CSSProperties = {
width: addUnit(props.width),
height: addUnit(props.height),
backgroundColor: props.backgroundColor
}
return `${objToStyle(style)}${props.customStyle}`
})
const buttonStyle = computed(() => {
const size = props.height
const style: CSSProperties = {
width: addUnit(size),
height: addUnit(size),
transform: `translate(${currentPosition.value}px, 0)`,
transition: isResetting.value ? 'all 0.3s ease' : 'none',
'--wd-slide-verify-button-size': addUnit(size)
}
return objToStyle(style)
})
const trackStyle = computed(() => {
const style: CSSProperties = {
width: `${currentPosition.value}px`,
background: props.activeBackgroundColor,
'--wot-slide-verify-track-width': addUnit(props.width)
}
return objToStyle(style)
})
/**
* 从字符串或数字中解析出有效的数字。
*
* - 对于 number 类型,直接返回原值(可能包括 Infinity、-Infinity
* - 对于 string 类型,使用 `parseFloat` 解析前缀中的数字。
* - 当无法解析出有效数字(结果为 NaN返回 0 作为安全默认值。
*
* 注意:后续使用该函数的逻辑(如 `maxPosition` 计算)会额外通过 `isFinite`
* 等判断过滤掉 Infinity / 非法值,因此这里不会主动抛错,而是保证返回一个 number。
*/
const parseNumber = (value: string | number): number => {
if (typeof value === 'number') return value
const num = parseFloat(String(value))
return isNaN(num) ? 0 : num
}
// 最大位置,避免超出了范围导致展示异常
const maxPosition = computed(() => {
const width = parseNumber(props.width)
const height = parseNumber(props.height)
if (!isFinite(width) || !isFinite(height) || width <= 0 || height <= 0) {
return 0
}
return Math.max(0, width - height)
})
// 完成状态判断
const isComplete = computed(() => {
const distance = Math.abs(maxPosition.value - currentPosition.value)
return distance <= parseNumber(props.tolerance) // 容差范围内完成
})
// 位置状态
const currentPosition = ref<number>(0)
const startPosition = ref<number>(0)
// 成功状态
const isPass = ref<boolean>(false)
// 拖动状态
const isDragging = ref<boolean>(false)
// 回弹
const isResetting = ref(false)
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max))
const updatePosition = (position: number) => {
// 限制位置在允许范围内
currentPosition.value = clamp(position, 0, maxPosition.value)
}
const isDisabled = computed(() => props.disabled || isPass.value)
const onTouchStart = (event: TouchEvent): void => {
if (isDisabled.value || isDragging.value) return
touch.touchStart(event)
startPosition.value = currentPosition.value
isDragging.value = true
}
const onTouchMove = (event: TouchEvent): void => {
if (isDisabled.value || !isDragging.value) return
touch.touchMove(event)
updatePosition(startPosition.value + touch.deltaX.value)
}
// 控制回弹
const timer = ref<ReturnType<typeof setTimeout> | null>(null)
const onTouchEnd = (): void => {
if (isDisabled.value || !isDragging.value) return
isDragging.value = false
if (isComplete.value) {
// 完成
updatePosition(maxPosition.value)
isPass.value = true
emit('success')
} else {
isResetting.value = true
// 失败回到起点
updatePosition(0)
emit('fail')
timer.value = setTimeout(() => {
isResetting.value = false
}, 300)
}
}
onBeforeUnmount(() => {
if (timer.value !== null) {
clearTimeout(timer.value)
timer.value = null
}
})
/**
* 重置验证组件到初始状态
*/
const reset = () => {
if (timer.value !== null) {
clearTimeout(timer.value)
timer.value = null
}
isResetting.value = true
currentPosition.value = 0
startPosition.value = 0
isPass.value = false
isDragging.value = false
timer.value = setTimeout(() => {
isResetting.value = false
}, 300)
}
defineExpose<SlideVerifyExpose>({ reset })
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>