fix: captcha 開關

This commit is contained in:
skytek_xinliang
2026-05-22 09:51:11 +08:00
parent 59d04a4d7e
commit 8cf5aacf21
4 changed files with 111 additions and 62 deletions
+84
View File
@@ -0,0 +1,84 @@
import type { CaptchaResponse } from '@/types/api'
import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { normalizeError } from '@/services/error'
import { authApi } from '@/services/modules/auth'
interface UseLoginCaptchaOptions {
enabled: MaybeRefOrGetter<boolean>
}
export function useLoginCaptcha(options: UseLoginCaptchaOptions) {
const captcha = ref<CaptchaResponse | null>(null)
const captchaValue = ref('')
const captchaLoading = ref(false)
const captchaErrorMessage = ref<string | null>(null)
const enabled = computed(() => toValue(options.enabled))
const formCaptcha = computed(() => {
if (!enabled.value || !captcha.value) return undefined
return {
imgUrl: captcha.value.dntCaptchaImgUrl,
id: captcha.value.dntCaptchaId,
tokenValue: captcha.value.dntCaptchaTokenValue,
}
})
async function loadCaptcha() {
if (!enabled.value) return null
captchaLoading.value = true
captchaErrorMessage.value = null
try {
const { data } = await authApi.getCaptcha()
captcha.value = data
return data
} catch (error_) {
const normalizedError = normalizeError(error_)
captcha.value = null
captchaErrorMessage.value = normalizedError.message
throw normalizedError
} finally {
captchaLoading.value = false
}
}
async function refreshCaptcha() {
if (!enabled.value) return null
captchaValue.value = ''
return await loadCaptcha()
}
function setCaptchaValue(value: string) {
if (!enabled.value) return
captchaValue.value = value
}
function getLoginCaptchaPayload() {
if (!enabled.value) return undefined
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
}
return {
DNTCaptchaInputText: captchaValue.value,
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
}
}
return {
captchaValue,
captchaLoading,
captchaErrorMessage,
formCaptcha,
loadCaptcha,
refreshCaptcha,
setCaptchaValue,
getLoginCaptchaPayload,
}
}
+9 -42
View File
@@ -1,4 +1,4 @@
import type { CaptchaResponse, LoginPayload, User } from '@/types/api'
import type { LoginPayload, User } from '@/types/api'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { normalizeError } from '@/services/error'
@@ -16,32 +16,12 @@ export const useAuthStore = defineStore('auth', () => {
const token = tokenService.token
const loading = ref(false)
const error = ref<string | null>(null)
const captcha = ref<CaptchaResponse | null>(null)
const captchaLoading = ref(false)
const captchaErrorMessage = ref<string | null>(null)
// 只針對 login 取消重複請求,避免競態與重複提交
const loginController = ref<AbortController | null>(null)
const isAuthenticated = computed(() => !!token.value)
const roles = computed(() => (user.value?.role ? [user.value.role] : []))
const getCaptcha = async () => {
captchaLoading.value = true
captchaErrorMessage.value = null
try {
const { data } = await authApi.getCaptcha()
captcha.value = data
return data
} catch (error_) {
const normalizedError = normalizeError(error_)
captcha.value = null
captchaErrorMessage.value = normalizedError.message
throw normalizedError
} finally {
captchaLoading.value = false
}
}
const login = async (payload: LoginPayload) => {
loginController.value?.abort()
loginController.value = new AbortController()
@@ -49,24 +29,15 @@ export const useAuthStore = defineStore('auth', () => {
error.value = null
try {
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
}
const requestPayload = {
UserID: payload.UserID,
Password: payload.Password,
DNTCaptchaInputText: payload.DNTCaptchaInputText,
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
}
const formData = new FormData()
formData.append('UserID', requestPayload.UserID)
formData.append('Password', requestPayload.Password)
formData.append('DNTCaptchaInputText', requestPayload.DNTCaptchaInputText)
formData.append('DNTCaptchaText', requestPayload.DNTCaptchaText)
formData.append('DNTCaptchaToken', requestPayload.DNTCaptchaToken)
formData.append('UserID', payload.UserID)
formData.append('Password', payload.Password)
if (payload.captcha) {
formData.append('DNTCaptchaInputText', payload.captcha.DNTCaptchaInputText)
formData.append('DNTCaptchaText', payload.captcha.DNTCaptchaText)
formData.append('DNTCaptchaToken', payload.captcha.DNTCaptchaToken)
}
const { data } = await authApi.login(formData, {
signal: loginController.value.signal,
@@ -130,10 +101,6 @@ export const useAuthStore = defineStore('auth', () => {
}
return {
getCaptcha,
captcha,
captchaLoading,
captchaErrorMessage,
user,
token,
loading,
+7 -1
View File
@@ -16,8 +16,14 @@ export interface CaptchaResponse {
dntCaptchaTextValue: string
}
export interface LoginCaptchaPayload {
DNTCaptchaInputText: string
DNTCaptchaText: string
DNTCaptchaToken: string
}
export interface LoginPayload {
UserID: string
Password: string
DNTCaptchaInputText: string
captcha?: LoginCaptchaPayload
}
+11 -19
View File
@@ -58,6 +58,7 @@ import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import HyakkaouAcademyImage from '@/assets/logo.png'
import PageLogin from '@/components/PageLogin.vue'
import { useLoginCaptcha } from '@/composables/useLoginCaptcha'
import { useAuthStore } from '@/stores/auth'
import {
type LoginAnnouncementListItem,
@@ -94,6 +95,7 @@ const formPositionLayout = ref<LayoutType>('side-left')
// 功能開關:是否啟用驗證碼
const withCaptcha = ref(true)
const loginCaptcha = useLoginCaptcha({ enabled: withCaptcha })
// 文字內容(i18n
const title = computed(() => t('pages.login.title'))
@@ -117,9 +119,6 @@ const forgotPasswordHref = ref('/forgot-password')
const forgotPasswordTarget = ref<string | undefined>(undefined)
// 記住帳號的 localStorage key
const rememberStorageKey = ref('login.remember.username')
// 驗證碼 API
const captchaValue = ref('')
// 驗證與對話框狀態
const dialogVisible = ref(false)
const dialogTitle = ref('')
@@ -155,16 +154,10 @@ const form = computed(() => ({
rememberStorageKey: rememberStorageKey.value,
// 功能開關:是否顯示驗證碼
withCaptcha: withCaptcha.value,
captcha: authStore.captcha
? {
imgUrl: authStore.captcha.dntCaptchaImgUrl,
id: authStore.captcha.dntCaptchaId,
tokenValue: authStore.captcha.dntCaptchaTokenValue,
}
: undefined,
captchaValue: captchaValue.value,
captchaLoading: authStore.captchaLoading,
captchaErrorMessage: authStore.captchaErrorMessage ?? '',
captcha: loginCaptcha.formCaptcha.value,
captchaValue: loginCaptcha.captchaValue.value,
captchaLoading: loginCaptcha.captchaLoading.value,
captchaErrorMessage: loginCaptcha.captchaErrorMessage.value ?? '',
captchaVerified: false,
forgotPassword: {
text: forgotPasswordText.value,
@@ -192,12 +185,11 @@ function handleChangeLocale(nextLocale: string) {
}
async function handleCaptchaRefresh() {
captchaValue.value = ''
await authStore.getCaptcha()
await loginCaptcha.refreshCaptcha().catch(() => undefined)
}
function handleCaptchaChange(value: string) {
captchaValue.value = value
loginCaptcha.setCaptchaValue(value)
}
function handleToggleLayout() {
@@ -213,7 +205,7 @@ function handleSelectAnnouncement(item: LoginAnnouncementListItem) {
}
async function onLogin(data: Record<string, unknown>) {
if (withCaptcha.value && !captchaValue.value) {
if (withCaptcha.value && !loginCaptcha.captchaValue.value) {
dialogTitle.value = t('common.notice')
dialogMessage.value = t('pages.login.alert.verifyRequired')
dialogVisible.value = true
@@ -234,7 +226,7 @@ async function onLogin(data: Record<string, unknown>) {
await authStore.login({
UserID: userId,
Password: password,
DNTCaptchaInputText: captchaValue.value,
captcha: loginCaptcha.getLoginCaptchaPayload(),
})
menuStore.getMenu(authStore.user?.id ?? '')
@@ -262,6 +254,6 @@ async function onLogin(data: Record<string, unknown>) {
onMounted(() => {
loginAnnouncementsStore.hydrate()
loginAnnouncementsStore.fetchMobileAnnouncements()
authStore.getCaptcha()
void loginCaptcha.loadCaptcha().catch(() => undefined)
})
</script>