129 lines
3.0 KiB
Vue
129 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
|
|
const { t } = useAdminLocale();
|
|
|
|
const input = ref('');
|
|
const code = ref('');
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
const honeypot = ref('');
|
|
|
|
function generateCode() {
|
|
code.value = String(Math.floor(1000 + Math.random() * 9000));
|
|
}
|
|
|
|
function drawCaptcha() {
|
|
const canvas = canvasRef.value;
|
|
if (!canvas) return;
|
|
const w = 108, h = 44;
|
|
canvas.width = w;
|
|
canvas.height = h;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.fillStyle = '#3d413b';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
for (let i = 0; i < 28; i++) {
|
|
ctx.fillStyle = `rgba(255,255,255,${0.2 + Math.random() * 0.32})`;
|
|
ctx.beginPath();
|
|
ctx.arc(Math.random() * w, Math.random() * h, Math.random() * 2.2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.22 + Math.random() * 0.26})`;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(Math.random() * w, Math.random() * h);
|
|
ctx.lineTo(Math.random() * w, Math.random() * h);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(w / 2, h / 2);
|
|
ctx.rotate((Math.random() - 0.5) * 0.12);
|
|
ctx.font = 'italic bold 26px Arial, sans-serif';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(code.value, 0, 1);
|
|
ctx.restore();
|
|
}
|
|
|
|
function refresh() {
|
|
generateCode();
|
|
input.value = '';
|
|
drawCaptcha();
|
|
}
|
|
|
|
function validate(): boolean {
|
|
if (honeypot.value) { refresh(); return false; }
|
|
return input.value.trim() === code.value;
|
|
}
|
|
|
|
onMounted(refresh);
|
|
defineExpose({ validate, refresh });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="captcha-row">
|
|
<input v-model="honeypot" type="text" name="website" tabindex="-1"
|
|
autocomplete="off" class="hp-field" aria-hidden="true" />
|
|
<input v-model="input" type="text" inputmode="numeric" maxlength="4"
|
|
class="captcha-input" :placeholder="t('login.captcha_ph')" autocomplete="off" />
|
|
<canvas ref="canvasRef" class="captcha-canvas"
|
|
:title="t('login.captcha_refresh')" role="button" tabindex="0"
|
|
@click="refresh" @keydown.enter="refresh" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.captcha-row {
|
|
display: flex;
|
|
gap: 0;
|
|
align-items: stretch;
|
|
height: 44px;
|
|
}
|
|
.hp-field {
|
|
position: absolute;
|
|
left: -9999px;
|
|
width: 1px;
|
|
height: 1px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.captcha-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 0 14px;
|
|
border: 1px solid var(--border);
|
|
border-right: none;
|
|
border-radius: 8px 0 0 8px;
|
|
background: #ffffff;
|
|
color: var(--text);
|
|
font-size: 15px;
|
|
font-weight: 650;
|
|
outline: none;
|
|
}
|
|
.captcha-input::placeholder {
|
|
color: #aaa49a;
|
|
}
|
|
.captcha-input:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
|
}
|
|
.captcha-canvas {
|
|
flex-shrink: 0;
|
|
width: 108px;
|
|
height: 44px;
|
|
cursor: pointer;
|
|
display: block;
|
|
border-radius: 0 8px 8px 0;
|
|
border: 1px solid #3d413b;
|
|
border-left: none;
|
|
box-sizing: border-box;
|
|
}
|
|
</style>
|