| | |
| | | <script setup lang="ts"> |
| | | import type { FormInst } from 'naive-ui' |
| | | import { fetchCaptcha } from '@/service/api/login' |
| | | import { useAuthStore } from '@/store' |
| | | import { local } from '@/utils' |
| | | // import { fetchCaptcha } from '@/service/api/login' |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | |
| | | validator: (rule, value) => { |
| | | if (!value) |
| | | return new Error('请输入验证码') |
| | | if (value.toLowerCase() !== captchaCode.value.toLowerCase()) |
| | | return new Error('验证码错误') |
| | | return true |
| | | }, |
| | | }, |
| | |
| | | const isRemember = ref(false) |
| | | const isLoading = ref(false) |
| | | const captchaUrl = ref('') |
| | | const captchaCode = ref('') |
| | | const captchaUuid = ref('') |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | function generateCaptcha() { |
| | | const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
| | | let code = '' |
| | | for (let i = 0; i < 4; i++) { |
| | | code += chars[Math.floor(Math.random() * chars.length)] |
| | | async function generateCaptcha() { |
| | | const res = await fetchCaptcha() |
| | | if (!res.code || res.code !== 200) { |
| | | captchaUrl.value = '' |
| | | captchaUuid.value = '' |
| | | return |
| | | } |
| | | captchaCode.value = code |
| | | |
| | | // Create canvas for image |
| | | const canvas = document.createElement('canvas') |
| | | canvas.width = 120 |
| | | canvas.height = 40 |
| | | const ctx = canvas.getContext('2d') |
| | | if (ctx) { |
| | | // Background |
| | | ctx.fillStyle = '#f3f4f6' |
| | | ctx.fillRect(0, 0, 120, 40) |
| | | |
| | | // Text |
| | | ctx.font = 'bold 24px Arial' |
| | | ctx.fillStyle = '#333' |
| | | ctx.textBaseline = 'middle' |
| | | ctx.textAlign = 'center' |
| | | |
| | | // Add noise/lines |
| | | for (let i = 0; i < 5; i++) { |
| | | ctx.strokeStyle = `rgba(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255},0.5)` |
| | | ctx.beginPath() |
| | | ctx.moveTo(Math.random() * 120, Math.random() * 40) |
| | | ctx.lineTo(Math.random() * 120, Math.random() * 40) |
| | | ctx.stroke() |
| | | } |
| | | |
| | | // Draw characters with rotation |
| | | const width = 120 / 4 |
| | | for (let i = 0; i < 4; i++) { |
| | | ctx.save() |
| | | ctx.translate(15 + i * width, 20) |
| | | ctx.rotate((Math.random() - 0.5) * 0.4) |
| | | ctx.fillText(code[i], 0, 0) |
| | | ctx.restore() |
| | | } |
| | | |
| | | captchaUrl.value = canvas.toDataURL() |
| | | } |
| | | const rawImg = String(res.img || '') |
| | | captchaUrl.value = rawImg |
| | | ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) |
| | | : '' |
| | | captchaUuid.value = String(res.uuid || '') |
| | | } |
| | | |
| | | // async function getCaptcha() { |
| | | // const { data } = await fetchCaptcha() |
| | | // if (data) { |
| | | // captchaUrl.value = data.img |
| | | // captchaId.value = data.id |
| | | // } |
| | | // } |
| | | |
| | | function handleLogin() { |
| | | formRef.value?.validate(async (errors) => { |
| | | if (errors) { |
| | | generateCaptcha() |
| | | void generateCaptcha() |
| | | formValue.value.captcha = '' |
| | | return |
| | | } |
| | | |
| | | isLoading.value = true |
| | | const { account, pwd } = formValue.value |
| | | const { account, pwd, captcha } = formValue.value |
| | | |
| | | if (isRemember.value) |
| | | local.set('loginAccount', { account, pwd }) |
| | | else local.remove('loginAccount') |
| | | |
| | | await authStore.loginEncrypted(account, pwd) |
| | | const success = await authStore.loginEncrypted(account, pwd, captcha, captchaUuid.value) |
| | | if (!success) { |
| | | formValue.value.captcha = '' |
| | | void generateCaptcha() |
| | | } |
| | | isLoading.value = false |
| | | }) |
| | | } |
| | | onMounted(() => { |
| | | checkUserAccount() |
| | | generateCaptcha() |
| | | void generateCaptcha() |
| | | }) |
| | | function checkUserAccount() { |
| | | const loginAccount = local.get('loginAccount') |
| | |
| | | <n-form-item path="captcha"> |
| | | <div class="flex items-center w-full gap-4"> |
| | | <n-input v-model:value="formValue.captcha" placeholder="请输入验证码" clearable @keyup.enter="handleLogin" /> |
| | | <div class="cursor-pointer h-40px w-120px flex items-center justify-center bg-gray-100 rounded overflow-hidden" @click="generateCaptcha"> |
| | | <div class="cursor-pointer h-40px w-120px flex items-center justify-center bg-gray-100 rounded overflow-hidden" @click="void generateCaptcha()"> |
| | | <img v-if="captchaUrl" :src="captchaUrl" alt="验证码" class="w-full h-full object-fill"> |
| | | <span v-else class="text-xs text-gray-400">加载中...</span> |
| | | </div> |