203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
|
|
import { ElMessage } from "element-plus"
|
|
|
|
import { useRouter } from "vue-router"
|
|
|
|
import { useGlobalStatusStore } from "@/stores/useGlobalStatusStore"
|
|
import { http } from "@/utils/axios"
|
|
import { getSafeLoginRedirect } from "@/utils/navigation.mjs"
|
|
|
|
export function usePage() {
|
|
const router = useRouter()
|
|
const globalStatus = useGlobalStatusStore()
|
|
const pending = computed(() => globalStatus.pendingLogin)
|
|
const code = ref("")
|
|
const loading = ref(false)
|
|
const resending = ref(false)
|
|
const method = ref("")
|
|
const retrySeconds = ref(0)
|
|
const remainingSeconds = ref(0)
|
|
const errorMessage = ref("")
|
|
let timer = null
|
|
let polling = false
|
|
let disposed = false
|
|
|
|
const expiresAt = computed(() => pending.value?.expiresAt || "")
|
|
const methods = computed(() => pending.value?.methods || [])
|
|
const sending = computed(() => pending.value?.deliveryStatus === "sending")
|
|
const canSubmit = computed(() => !loading.value && remainingSeconds.value > 0
|
|
&& methods.value.includes(method.value)
|
|
&& (method.value === "totp" ? /^\d{6}$/ : /^\d{8}$/).test(code.value.trim()))
|
|
const deliveryMessage = computed(() => {
|
|
switch (pending.value?.deliveryStatus) {
|
|
case "sending": return "正在发送邮件,您可以等待,也可以切换到手机动态码。"
|
|
case "sent": return "收件服务器已接收邮件,请检查收件箱和垃圾邮件。"
|
|
case "failed": return pending.value?.deliveryError || "邮件发送失败,请稍后重试。"
|
|
default: return "点击发送验证码后,我们会向绑定邮箱发送邮件。"
|
|
}
|
|
})
|
|
|
|
const savePending = (data, id) => {
|
|
if (disposed || pending.value?.challengeId !== id) return
|
|
globalStatus.setPendingLogin({ ...data, redirect: pending.value.redirect })
|
|
updateRemaining()
|
|
}
|
|
|
|
const pollStatus = async () => {
|
|
if (polling || !sending.value || remainingSeconds.value <= 0) return
|
|
polling = true
|
|
const id = pending.value.challengeId
|
|
try {
|
|
const result = await http.post("/api/v1/security/login/status", {
|
|
id, account: pending.value.account
|
|
}, { timeout: 4500 })
|
|
if (result.errorNo !== 0) throw new Error(result.errorMsg || "无法查询发送状态")
|
|
savePending(result.data, id)
|
|
errorMessage.value = ""
|
|
} catch (error) {
|
|
if (!disposed) errorMessage.value = "发送状态暂时无法更新,请检查网络;无需重复发送。"
|
|
} finally {
|
|
polling = false
|
|
}
|
|
}
|
|
|
|
const chooseMethod = value => {
|
|
if (!methods.value.includes(value) || loading.value) return
|
|
method.value = value
|
|
code.value = ""
|
|
errorMessage.value = ""
|
|
}
|
|
|
|
const updateRemaining = () => {
|
|
const timestamp = Date.parse(expiresAt.value)
|
|
remainingSeconds.value = Number.isFinite(timestamp)
|
|
? Math.max(0, Math.ceil((timestamp - Date.now()) / 1000))
|
|
: 0
|
|
retrySeconds.value = Math.max(0, Math.ceil((Date.parse(pending.value?.retryAt) - Date.now()) / 1000)) || 0
|
|
}
|
|
|
|
const startTimer = () => {
|
|
updateRemaining()
|
|
timer = window.setInterval(() => {
|
|
updateRemaining()
|
|
pollStatus()
|
|
}, 1000)
|
|
}
|
|
|
|
const stopTimer = () => {
|
|
if (timer !== null) {
|
|
window.clearInterval(timer)
|
|
timer = null
|
|
}
|
|
}
|
|
|
|
const goBackToLogin = async () => {
|
|
globalStatus.clearPendingLogin()
|
|
await router.replace({ name: "login" })
|
|
}
|
|
|
|
const verifyCode = async () => {
|
|
if (!pending.value?.challengeId) {
|
|
await goBackToLogin()
|
|
return
|
|
}
|
|
if (loading.value || !method.value) return
|
|
const pattern = method.value === "totp" ? /^\d{6}$/ : /^\d{8}$/
|
|
if (!pattern.test(code.value.trim())) {
|
|
ElMessage.warning(method.value === "totp" ? "请输入六位手机动态码" : "请输入八位邮箱验证码")
|
|
return
|
|
}
|
|
if (remainingSeconds.value <= 0) {
|
|
errorMessage.value = "验证码已过期,请重新登录"
|
|
ElMessage.warning(errorMessage.value)
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
errorMessage.value = ""
|
|
try {
|
|
const response = await http.post("/api/v1/security/login/verify", {
|
|
id: pending.value.challengeId,
|
|
account: pending.value.account,
|
|
code: code.value.trim(),
|
|
method: method.value
|
|
}, { timeout: 4500 })
|
|
if (response.errorNo !== 0) {
|
|
throw new Error(response.errorMsg || "验证码验证失败")
|
|
}
|
|
const redirect = getSafeLoginRedirect(pending.value.redirect)
|
|
globalStatus.clearPendingLogin()
|
|
globalStatus.setUser(response.data, true)
|
|
await router.replace(redirect)
|
|
} catch (error) {
|
|
errorMessage.value = error?.message || "验证码验证失败,请重试"
|
|
ElMessage.error(errorMessage.value)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const resendCode = async () => {
|
|
if (!pending.value?.challengeId || resending.value || sending.value || retrySeconds.value > 0) {
|
|
return
|
|
}
|
|
resending.value = true
|
|
errorMessage.value = ""
|
|
try {
|
|
const id = pending.value.challengeId
|
|
const response = await http.post("/api/v1/security/login/resend", {
|
|
id,
|
|
account: pending.value.account
|
|
}, { timeout: 4500 })
|
|
if (response.errorNo !== 0 || !response.data?.pending) {
|
|
throw new Error(response.errorMsg || "验证码发送失败")
|
|
}
|
|
savePending(response.data, id)
|
|
code.value = ""
|
|
} catch (error) {
|
|
if (error?.code === 'ECONNABORTED' || error?.code === 'ERR_NETWORK') {
|
|
errorMessage.value = "发送请求未确认,正在查询状态,请勿重复点击。"
|
|
if (pending.value) pending.value.deliveryStatus = "sending"
|
|
} else {
|
|
errorMessage.value = error?.message || "验证码发送失败,请稍后重试"
|
|
}
|
|
ElMessage.error(errorMessage.value)
|
|
} finally {
|
|
resending.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (!pending.value?.challengeId) {
|
|
router.replace({ name: "login" })
|
|
return
|
|
}
|
|
startTimer()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
disposed = true
|
|
stopTimer()
|
|
})
|
|
|
|
return {
|
|
pending,
|
|
code,
|
|
loading,
|
|
resending,
|
|
method,
|
|
methods,
|
|
sending,
|
|
canSubmit,
|
|
retrySeconds,
|
|
deliveryMessage,
|
|
chooseMethod,
|
|
remainingSeconds,
|
|
errorMessage,
|
|
verifyCode,
|
|
resendCode,
|
|
goBackToLogin
|
|
}
|
|
}
|