改造短信发送图形验证码流程

This commit is contained in:
andy
2026-06-30 17:25:52 +08:00
parent 654d154459
commit 4919a311c3
3 changed files with 105 additions and 9 deletions

View File

@@ -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
}

View File

@@ -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> {

View File

@@ -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);