Files
hjc-web/app/components/CaptchaSlider.vue
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

152 lines
4.5 KiB
Vue
Raw 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>
<div class="captcha">
<div class="relative" :style="{ height: H + 'px' }">
<canvas
ref="canvasRef"
class="block w-full rounded-lg ring-1 ring-gray-200"
:style="{ height: H + 'px' }"
/>
<!-- 滑块手柄 -->
<div
ref="handleRef"
class="absolute top-0 flex items-center justify-center rounded-lg bg-white shadow-md ring-1 ring-gray-200 cursor-grab select-none active:cursor-grabbing"
:class="passed ? '!bg-green-500 !text-white' : 'text-indigo-600'"
:style="{ left: sliderX + 'px', width: SIZE + 'px', height: H + 'px' }"
@pointerdown.prevent="onDown"
>
<svg v-if="!passed" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6l6 6-6 6" />
</svg>
<svg v-else class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<div
v-if="!passed"
class="pointer-events-none absolute inset-0 flex items-center justify-center text-xs text-gray-400"
>
拖动滑块完成拼图验证
</div>
</div>
<div class="mt-1 flex items-center justify-end">
<button type="button" class="text-xs text-gray-400 hover:text-gray-600" @click="reload">看不清换一个</button>
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
passed: [payload: { token: string; x: number }]
failed: []
}>()
const H = 44
const SIZE = 42
const TOLERANCE = 10
const canvasRef = ref<HTMLCanvasElement>()
const handleRef = ref<HTMLElement>()
const W = ref(320)
const puzzleX = ref(0)
const token = ref('')
const sliderX = ref(0)
const dragging = ref(false)
const passed = ref(false)
const loading = ref(false)
const result = ref<{ token: string; x: number } | null>(null)
let moveStartX = 0
let startLeft = 0
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v))
}
async function reload() {
if (loading.value) return
loading.value = true
passed.value = false
sliderX.value = 0
result.value = null
await nextTick()
const width = canvasRef.value ? Math.round(canvasRef.value.clientWidth) || 320 : 320
try {
const res = await $fetch<{ token: string; puzzleX: number; width: number; size: number }>(
'/api/captcha/challenge',
{ method: 'POST', body: { width } }
)
token.value = res.token
puzzleX.value = res.puzzleX
W.value = res.width
draw()
} catch {
/* 失败可重试 */
} finally {
loading.value = false
}
}
function draw() {
const c = canvasRef.value
if (!c) return
const ctx = c.getContext('2d')
if (!ctx) return
c.width = W.value
c.height = H
const g = ctx.createLinearGradient(0, 0, W.value, H)
g.addColorStop(0, '#eef2ff')
g.addColorStop(1, '#e0e7ff')
ctx.fillStyle = g
ctx.fillRect(0, 0, W.value, H)
for (let i = 0; i < 36; i++) {
ctx.fillStyle = `rgba(99,102,241,${Math.random() * 0.15})`
ctx.beginPath()
ctx.arc(Math.random() * W.value, Math.random() * H, Math.random() * 2, 0, Math.PI * 2)
ctx.fill()
}
ctx.fillStyle = 'rgba(99,102,241,0.4)'
ctx.font = '13px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('拖动滑块完成拼图验证', W.value / 2, H / 2 + 4)
// 挖出缺口(露出底层)
ctx.globalCompositeOperation = 'destination-out'
ctx.fillRect(puzzleX.value, 0, SIZE, H)
ctx.globalCompositeOperation = 'source-over'
}
function onDown(e: PointerEvent) {
if (passed.value || loading.value) return
dragging.value = true
moveStartX = e.clientX
startLeft = sliderX.value
}
function onMove(e: PointerEvent) {
if (!dragging.value) return
sliderX.value = clamp(startLeft + (e.clientX - moveStartX), 0, W.value - SIZE)
}
function onUp() {
if (!dragging.value) return
dragging.value = false
if (Math.abs(sliderX.value - puzzleX.value) <= TOLERANCE) {
passed.value = true
result.value = { token: token.value, x: Math.round(sliderX.value) }
emit('passed', result.value)
} else {
sliderX.value = 0
emit('failed')
}
}
onMounted(async () => {
await reload()
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
})
onBeforeUnmount(() => {
window.removeEventListener('pointermove', onMove)
window.removeEventListener('pointerup', onUp)
})
defineExpose({ result, passed, reload })
</script>