| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- <template>
- <div class="slider-captcha" :class="statusClass">
- <div
- class="slider-track"
- ref="track"
- @mousedown.prevent="onDragStart"
- @touchstart.prevent="onDragStart"
- >
- <div class="slider-fill" :style="{ width: fillPercent + '%' }"></div>
- <div
- class="slider-thumb"
- :style="{ left: thumbLeft + 'px' }"
- :class="{ 'is-dragging': dragging }"
- >
- <i class="el-icon-d-arrow-right"></i>
- </div>
- </div>
- <div class="captcha-hint">
- <template v-if="status === 'pending'">
- <span class="hint-arrow">»</span>
- <span>请按住滑块拖动到最右侧完成验证</span>
- </template>
- <template v-else-if="status === 'success'">
- <i class="el-icon-success" style="color:#4CAF50"></i>
- <span style="color:#4CAF50">验证通过</span>
- </template>
- <template v-else-if="status === 'fail'">
- <i class="el-icon-error" style="color:#EF5350"></i>
- <span style="color:#EF5350">验证失败,请重试</span>
- </template>
- </div>
- </div>
- </template>
- <script>
- export default {
- name: 'SliderCaptcha',
- data() {
- return {
- trackWidth: 0,
- currentOffset: 0,
- dragging: false,
- status: 'pending' // pending | dragging | success | fail
- }
- },
- computed: {
- fillPercent() {
- if (this.trackWidth === 0) return 0
- return (this.currentOffset / this.trackWidth) * 100
- },
- thumbLeft() { return this.currentOffset },
- statusClass() {
- return {
- 'is-success': this.status === 'success',
- 'is-fail': this.status === 'fail',
- 'is-dragging': this.status === 'dragging'
- }
- }
- },
- mounted() {
- this.$nextTick(this.initCaptcha)
- },
- methods: {
- initCaptcha() {
- if (!this.$refs.track) return
- this.trackWidth = this.$refs.track.offsetWidth
- this.currentOffset = 0
- this.status = 'pending'
- },
- onDragStart(e) {
- if (this.status === 'success') return
- this.dragging = true
- this.status = 'dragging'
- var startX = e.clientX || (e.touches && e.touches[0].clientX)
- var startOffset = this.currentOffset
- var self = this
- var onMove = function(ev) {
- var clientX = ev.clientX || (ev.touches && ev.touches[0].clientX)
- if (clientX === undefined) return
- var newOffset = startOffset + (clientX - startX)
- if (newOffset < 0) newOffset = 0
- if (newOffset > self.trackWidth) newOffset = self.trackWidth
- self.currentOffset = newOffset
- }
- var onEnd = function() {
- document.removeEventListener('mousemove', onMove)
- document.removeEventListener('mouseup', onEnd)
- document.removeEventListener('touchmove', onMove)
- document.removeEventListener('touchend', onEnd)
- self.dragging = false
- self.onDragEnd()
- }
- document.addEventListener('mousemove', onMove)
- document.addEventListener('mouseup', onEnd)
- document.addEventListener('touchmove', onMove, { passive: true })
- document.addEventListener('touchend', onEnd)
- },
- onDragEnd() {
- if (this.currentOffset >= this.trackWidth * 0.88) {
- this.status = 'success'
- this.currentOffset = this.trackWidth
- this.$emit('success')
- } else {
- this.status = 'fail'
- this.$emit('fail')
- var self = this
- setTimeout(function() { self.initCaptcha() }, 1000)
- }
- },
- reset() {
- this.initCaptcha()
- }
- }
- }
- </script>
- <style scoped>
- .slider-captcha {
- user-select: none;
- -webkit-user-select: none;
- }
- .slider-track {
- position: relative;
- height: 40px;
- background: #EDF2F7;
- border-radius: 20px;
- cursor: pointer;
- overflow: hidden;
- border: 1px solid #E2E8F0;
- transition: border-color 0.3s;
- }
- .slider-track:hover { border-color: #5B9BD5; }
- .slider-fill {
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- background: linear-gradient(90deg, #8FC5E8, #5B9BD5);
- border-radius: 20px;
- pointer-events: none;
- }
- .slider-thumb {
- position: absolute;
- top: -2px;
- width: 44px;
- height: 44px;
- background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: grab;
- box-shadow: 0 2px 8px rgba(91, 155, 213, 0.4);
- z-index: 2;
- margin-left: -22px;
- transition: box-shadow 0.2s;
- }
- .slider-thumb i {
- color: #fff;
- font-size: 18px;
- font-weight: bold;
- }
- .slider-thumb:hover { box-shadow: 0 3px 12px rgba(91, 155, 213, 0.6); }
- .slider-thumb.is-dragging {
- cursor: grabbing;
- box-shadow: 0 3px 16px rgba(91, 155, 213, 0.7);
- }
- .captcha-hint {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- color: #909399;
- margin-top: 8px;
- justify-content: center;
- }
- .hint-arrow {
- color: #5B9BD5;
- font-weight: bold;
- animation: hintPulse 1.6s ease-in-out infinite;
- }
- @keyframes hintPulse {
- 0%, 100% { opacity: 0.3; transform: translateX(0); }
- 50% { opacity: 1; transform: translateX(4px); }
- }
- .is-success .slider-track { border-color: #4CAF50; }
- .is-success .slider-fill { background: linear-gradient(90deg, #81C784, #4CAF50); }
- .is-success .slider-thumb {
- background: linear-gradient(135deg, #81C784, #4CAF50);
- box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
- cursor: default;
- }
- .is-success .slider-thumb i { display: none; }
- .is-success .slider-thumb::before {
- content: '✓';
- color: #fff;
- font-size: 20px;
- font-weight: bold;
- position: absolute;
- }
- .is-fail .slider-track { border-color: #EF5350; }
- .is-fail .slider-fill { background: linear-gradient(90deg, #E57373, #EF5350); }
- .is-fail .slider-thumb {
- background: linear-gradient(135deg, #E57373, #EF5350);
- box-shadow: 0 2px 8px rgba(239, 83, 80, 0.4);
- }
- </style>
|