feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user