From e7c1c9187705733130107d8a49c2e454a40e7071 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 19 Apr 2026 00:46:59 +0800 Subject: [PATCH] feat(auth): captcha UI on login and register pages Made-with: Cursor --- app/pages/login/index.vue | 56 +++++++++++++++++++++++++++++++++++++++++++- app/pages/register/index.vue | 56 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/app/pages/login/index.vue b/app/pages/login/index.vue index e5c2276..89a2705 100644 --- a/app/pages/login/index.vue +++ b/app/pages/login/index.vue @@ -11,6 +11,7 @@ definePageMeta({ type LoginFormState = { username: string password: string + captchaAnswer: string } type LoginResult = { @@ -25,6 +26,25 @@ const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/ const state = reactive({ username: '', password: '', + captchaAnswer: '', +}) + +const captchaIdRef = ref('') +const captchaImageSrc = ref('') + +async function refreshCaptcha() { + state.captchaAnswer = '' + captchaImageSrc.value = '' + const res = await fetchData<{ captchaId: string; imageSvg: string }>('/api/auth/captcha', { + method: 'GET', + notify: false, + }) + captchaIdRef.value = res.captchaId + captchaImageSrc.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(res.imageSvg)}` +} + +onMounted(() => { + void refreshCaptcha() }) const loading = ref(false) @@ -50,6 +70,10 @@ const validate = (formState: LoginFormState): FormError[] => { errors.push({ name: 'password', message: '密码至少 6 位' }) } + if (!formState.captchaAnswer?.trim()) { + errors.push({ name: 'captchaAnswer', message: '请输入验证码' }) + } + return errors } @@ -61,7 +85,12 @@ const onSubmit = async (_event: FormSubmitEvent) => { try { const res = await fetchData('/api/auth/login', { method: 'POST', - body: { username: state.username, password: state.password }, + body: { + username: state.username, + password: state.password, + captchaId: captchaIdRef.value, + captchaAnswer: state.captchaAnswer, + }, notify: false, }) @@ -77,6 +106,11 @@ const onSubmit = async (_event: FormSubmitEvent) => { } catch (error: unknown) { resultType.value = 'error' resultMessage.value = getApiErrorMessage(error) + try { + await refreshCaptcha() + } catch { + /* ignore captcha refresh errors */ + } } finally { loading.value = false } @@ -124,6 +158,26 @@ const onSubmit = async (_event: FormSubmitEvent) => { /> + +
+ 验证码 + + + 换一张 + +
+
+ 立即登录 diff --git a/app/pages/register/index.vue b/app/pages/register/index.vue index cac0932..abcc742 100644 --- a/app/pages/register/index.vue +++ b/app/pages/register/index.vue @@ -19,6 +19,7 @@ definePageMeta({ type RegisterFormState = { username: string password: string + captchaAnswer: string } const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/ @@ -26,6 +27,25 @@ const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/ const state = reactive({ username: '', password: '', + captchaAnswer: '', +}) + +const captchaIdRef = ref('') +const captchaImageSrc = ref('') + +async function refreshCaptcha() { + state.captchaAnswer = '' + captchaImageSrc.value = '' + const res = await fetchData<{ captchaId: string; imageSvg: string }>('/api/auth/captcha', { + method: 'GET', + notify: false, + }) + captchaIdRef.value = res.captchaId + captchaImageSrc.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(res.imageSvg)}` +} + +onMounted(() => { + void refreshCaptcha() }) const loading = ref(false) @@ -49,6 +69,10 @@ const validate = (formState: RegisterFormState): FormError[] => { errors.push({ name: 'password', message: '密码至少 6 位' }) } + if (!formState.captchaAnswer?.trim()) { + errors.push({ name: 'captchaAnswer', message: '请输入验证码' }) + } + return errors } @@ -60,7 +84,12 @@ const onSubmit = async (_event: FormSubmitEvent) => { try { await fetchData('/api/auth/register', { method: 'POST', - body: { username: state.username, password: state.password }, + body: { + username: state.username, + password: state.password, + captchaId: captchaIdRef.value, + captchaAnswer: state.captchaAnswer, + }, notify: false, }) @@ -75,6 +104,11 @@ const onSubmit = async (_event: FormSubmitEvent) => { } catch (error: unknown) { resultType.value = 'error' resultMessage.value = getApiErrorMessage(error) + try { + await refreshCaptcha() + } catch { + /* ignore captcha refresh errors */ + } } finally { loading.value = false } @@ -119,6 +153,26 @@ const onSubmit = async (_event: FormSubmitEvent) => { /> + +
+ 验证码 + + + 换一张 + +
+
+ 立即注册