改造短信发送图形验证码流程
This commit is contained in:
@@ -12,17 +12,35 @@ export interface OrganizationMemberInfo {
|
||||
memberRoleId?: string
|
||||
}
|
||||
|
||||
export interface SendMobileCodePayload {
|
||||
phone: string
|
||||
imageRandomStr: string
|
||||
imageCode: string
|
||||
}
|
||||
|
||||
const basicAuth = () => {
|
||||
const raw = `${env.clientId}:${env.clientSecret}`
|
||||
return `Basic ${window.btoa(raw)}`
|
||||
}
|
||||
|
||||
export const sendMobileCode = async (phone: string) => {
|
||||
if (env.useMock) return mockApi.sendMobileCode(phone)
|
||||
return await http.get<boolean>(joinUrl(env.adminBase, 'platformUser/sendMobileCode', phone), {
|
||||
export const getImageCodeUrl = (imageRandomStr: string) => {
|
||||
const params = new URLSearchParams({
|
||||
randomStr: imageRandomStr,
|
||||
t: String(Date.now())
|
||||
})
|
||||
return `${joinUrl(env.authBase, 'code/image')}?${params.toString()}`
|
||||
}
|
||||
|
||||
export const sendMobileCode = async (payload: SendMobileCodePayload) => {
|
||||
if (env.useMock) return mockApi.sendMobileCode(payload)
|
||||
return await http.get<boolean>(joinUrl(env.adminBase, 'platformUser/sendMobileCode', payload.phone), {
|
||||
headers: {
|
||||
clientId: env.clientId,
|
||||
clientConfigId: env.clientConfigId
|
||||
},
|
||||
params: {
|
||||
imageRandomStr: payload.imageRandomStr,
|
||||
imageCode: payload.imageCode
|
||||
}
|
||||
}) as unknown as boolean
|
||||
}
|
||||
|
||||
@@ -129,9 +129,11 @@ const page = <T>(records: T[], pageNum: number, pageSize: number): PageResponse<
|
||||
}
|
||||
|
||||
export const mockApi = {
|
||||
async sendMobileCode(phone: string) {
|
||||
async sendMobileCode(payload: { phone: string; imageRandomStr?: string; imageCode?: string }) {
|
||||
await wait()
|
||||
return { data: true, msg: phone ? '5678nianxx' : '请输入手机号' }
|
||||
if (!payload.phone) return { data: false, msg: '请输入手机号' }
|
||||
if (!payload.imageRandomStr || !payload.imageCode) return { data: false, msg: '请输入图形验证码' }
|
||||
return { data: true, msg: '5678nianxx' }
|
||||
},
|
||||
|
||||
async loginByMobile(phone: string): Promise<TokenResponse> {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { BadgeCheck, Phone, ShieldCheck } from 'lucide-vue-next'
|
||||
import { sendMobileCode } from '@/api/auth'
|
||||
import { getImageCodeUrl, sendMobileCode } from '@/api/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -11,6 +11,9 @@ const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = ref('')
|
||||
const imageCode = ref('')
|
||||
const imageRandomStr = ref('')
|
||||
const imageCodeUrl = ref('')
|
||||
const code = ref('')
|
||||
const loading = ref(false)
|
||||
const sending = ref(false)
|
||||
@@ -18,6 +21,20 @@ const seconds = ref(0)
|
||||
|
||||
let timer: number | undefined
|
||||
|
||||
const createImageRandomStr = () => {
|
||||
if (window.crypto?.randomUUID) return window.crypto.randomUUID()
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
||||
const value = Math.floor(Math.random() * 16)
|
||||
return (char === 'x' ? value : (value & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
const refreshImageCode = () => {
|
||||
imageRandomStr.value = createImageRandomStr()
|
||||
imageCode.value = ''
|
||||
imageCodeUrl.value = getImageCodeUrl(imageRandomStr.value)
|
||||
}
|
||||
|
||||
const canSend = computed(() => /^1\d{10}$/.test(phone.value) && seconds.value === 0)
|
||||
|
||||
const startCountdown = () => {
|
||||
@@ -32,21 +49,35 @@ const startCountdown = () => {
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!canSend.value) {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
showToast('请输入正确手机号')
|
||||
return
|
||||
}
|
||||
if (!imageCode.value.trim()) {
|
||||
showToast('请输入图形验证码')
|
||||
return
|
||||
}
|
||||
sending.value = true
|
||||
try {
|
||||
const result = await sendMobileCode(phone.value)
|
||||
const result = await sendMobileCode({
|
||||
phone: phone.value,
|
||||
imageRandomStr: imageRandomStr.value,
|
||||
imageCode: imageCode.value.trim()
|
||||
})
|
||||
const message = typeof result === 'object' && result && 'msg' in result ? result.msg : ''
|
||||
if (message) {
|
||||
code.value = String(message)
|
||||
showToast(`验证码:${message}`)
|
||||
} else if (result === false) {
|
||||
throw new Error('验证码发送失败')
|
||||
} else {
|
||||
showToast('验证码已发送')
|
||||
}
|
||||
startCountdown()
|
||||
refreshImageCode()
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '验证码发送失败')
|
||||
refreshImageCode()
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
@@ -72,6 +103,8 @@ const handleLogin = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refreshImageCode)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,6 +130,28 @@ const handleLogin = async () => {
|
||||
>
|
||||
<template #left-icon><Phone :size="18" /></template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="imageCode"
|
||||
type="text"
|
||||
name="imageCode"
|
||||
maxlength="8"
|
||||
placeholder="请输入图形验证码"
|
||||
autocomplete="off"
|
||||
clearable
|
||||
>
|
||||
<template #left-icon><ShieldCheck :size="18" /></template>
|
||||
<template #button>
|
||||
<button
|
||||
class="captcha-image-button"
|
||||
type="button"
|
||||
aria-label="刷新图形验证码"
|
||||
@click="refreshImageCode"
|
||||
>
|
||||
<img v-if="imageCodeUrl" :src="imageCodeUrl" alt="图形验证码" />
|
||||
<span v-else>刷新</span>
|
||||
</button>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="code"
|
||||
type="text"
|
||||
@@ -227,6 +282,27 @@ const handleLogin = async () => {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.captcha-image-button {
|
||||
display: inline-flex;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-soft);
|
||||
color: var(--primary-deep);
|
||||
}
|
||||
|
||||
.captcha-image-button img {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.login-note {
|
||||
margin: 14px 0 0;
|
||||
color: var(--text-muted);
|
||||
|
||||
Reference in New Issue
Block a user