登录功能接入验证码
This commit is contained in:
parent
4c2d41b699
commit
e0f6175fed
22
src/vendor/tianai-captcha/captcha/captcha.js
vendored
22
src/vendor/tianai-captcha/captcha/captcha.js
vendored
@ -15,9 +15,8 @@ const template = `
|
|||||||
</div>
|
</div>
|
||||||
<!-- 底部 -->
|
<!-- 底部 -->
|
||||||
<div class="slider-bottom">
|
<div class="slider-bottom">
|
||||||
<img class="logo" id="tianai-captcha-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAMFBMVEVHcEz3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkVmTmjZAAAAD3RSTlMASbTm8wh12hOGoCNiyTV98jvOAAABB0lEQVR42nVT0aIFEQiMorD0/397Lc5a7J0n1UylgIniLRKyDcbBDudZH2DYCAabn3PmTrjeUX+7rJGWx0SqVpzReAfTtKU5fgVCNfxWjB69USUDGwoOiauHpZEpSr0tCx8ILb3Dm3WgBbAlifAJk6+Ww6wqEUmpmIorQVZ1JtqKnDMjkb7AgIpO/wMCaQbuBuEtsBUxhuD9daUaZnApiQB8NAKotMwirGGr6mbXpPnHLHDmy6oy3FgP+1j8IBdVklFc01xUJwv3NR0rIeXV5zpzdlruiijzNq/ufOeKWzZLP3160u5P8RjT1M+HHFtx+PwGyOZqT/D8ROOfjOInTLBIHjy/hvwHxkwPu5cCE1QAAAAASUVORK5CYII=" id="tianai-captcha-logo"></img>
|
|
||||||
<div class="close-btn" id="tianai-captcha-slider-close-btn"></div>
|
|
||||||
<div class="refresh-btn" id="tianai-captcha-slider-refresh-btn"></div>
|
<div class="refresh-btn" id="tianai-captcha-slider-refresh-btn"></div>
|
||||||
|
<div class="close-btn" id="tianai-captcha-slider-close-btn"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -28,7 +27,6 @@ function createCaptchaByType(type, tac) {
|
|||||||
case "SLIDER":
|
case "SLIDER":
|
||||||
return new Slider(box, styleConfig);
|
return new Slider(box, styleConfig);
|
||||||
case "ROTATE":
|
case "ROTATE":
|
||||||
case "ROTATE_DEGREE":
|
|
||||||
return new Rotate(box, styleConfig);
|
return new Rotate(box, styleConfig);
|
||||||
case "CONCAT":
|
case "CONCAT":
|
||||||
return new Concat(box, styleConfig);
|
return new Concat(box, styleConfig);
|
||||||
@ -103,20 +101,12 @@ class TianAiCaptcha {
|
|||||||
loadStyle() {
|
loadStyle() {
|
||||||
// 设置样式
|
// 设置样式
|
||||||
const bgUrl = this.style.bgUrl;
|
const bgUrl = this.style.bgUrl;
|
||||||
const logoUrl = this.style.logoUrl;
|
|
||||||
if (bgUrl) {
|
if (bgUrl) {
|
||||||
// 背景图片
|
// 背景图片
|
||||||
this.config.domBindEl
|
this.config.domBindEl
|
||||||
.find("#tianai-captcha-bg-img")
|
.find("#tianai-captcha-bg-img")
|
||||||
.css("background-image", "url(" + bgUrl + ")");
|
.css("background-image", "url(" + bgUrl + ")");
|
||||||
}
|
}
|
||||||
if (logoUrl && logoUrl !== "") {
|
|
||||||
// logo
|
|
||||||
this.config.domBindEl.find("#tianai-captcha-logo").attr("src", logoUrl);
|
|
||||||
} else if (logoUrl === null) {
|
|
||||||
// 删除logo
|
|
||||||
this.config.domBindEl.find("#tianai-captcha-logo").css("display", "none");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyWindow() {
|
destroyWindow() {
|
||||||
@ -167,6 +157,12 @@ class TianAiCaptcha {
|
|||||||
// 清空
|
// 清空
|
||||||
const id = c.currentCaptchaData.currentCaptchaId;
|
const id = c.currentCaptchaData.currentCaptchaId;
|
||||||
c.currentCaptchaData = undefined;
|
c.currentCaptchaData = undefined;
|
||||||
|
if (this.config.onDataReady) {
|
||||||
|
Promise.resolve(this.config.onDataReady({ id, data }, c, this)).catch((error) => {
|
||||||
|
console.error("[TAC] onDataReady callback failed", error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 调用验证接口
|
// 调用验证接口
|
||||||
this.config.validCaptcha(id, data, c, this);
|
this.config.validCaptcha(id, data, c, this);
|
||||||
});
|
});
|
||||||
@ -180,9 +176,7 @@ class TianAiCaptcha {
|
|||||||
this.C.el.css("transform", "translateX(300px)");
|
this.C.el.css("transform", "translateX(300px)");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.C.destroy();
|
this.C.destroy();
|
||||||
if (callback) {
|
callback && callback();
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
|
|||||||
26
src/vendor/tianai-captcha/captcha/captcha.less
vendored
26
src/vendor/tianai-captcha/captcha/captcha.less
vendored
@ -1,7 +1,6 @@
|
|||||||
#tianai-captcha-parent {
|
#tianai-captcha-parent {
|
||||||
box-shadow: 0 0 11px 0 #999999;
|
box-shadow: 0 0 11px 0 #999999;
|
||||||
width: 318px;
|
width: 318px;
|
||||||
height: 318px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 997;
|
z-index: 997;
|
||||||
@ -47,32 +46,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slider-bottom {
|
.slider-bottom {
|
||||||
.close-btn {
|
.close-btn, .refresh-btn {
|
||||||
|
display: inline-block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-image: url("../images/icon.png");
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 0 -14px;
|
|
||||||
float: right;
|
|
||||||
margin-right: 2px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.close-btn {
|
||||||
|
background-image: url("../images/icon.png");
|
||||||
|
background-position: 0 -14px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
.refresh-btn {
|
.refresh-btn {
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background-image: url("../images/icon.png");
|
background-image: url("../images/icon.png");
|
||||||
background-position: 0 -167px;
|
background-position: 0 -167px;
|
||||||
background-repeat: no-repeat;
|
|
||||||
float: right;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
.logo {
|
text-align: right;
|
||||||
height: 30px;
|
margin-top: 8px;
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
height: 19px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.slider-move-shadow {
|
.slider-move-shadow {
|
||||||
animation: myanimation 2s infinite;
|
animation: myanimation 2s infinite;
|
||||||
|
|||||||
@ -70,7 +70,6 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 5px;
|
|
||||||
.tianai-captcha-slider-bg-div-slice {
|
.tianai-captcha-slider-bg-div-slice {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class CaptchaConfig {
|
|||||||
if (!args.requestCaptchaDataUrl) {
|
if (!args.requestCaptchaDataUrl) {
|
||||||
throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");
|
throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");
|
||||||
}
|
}
|
||||||
if (!args.validCaptchaUrl) {
|
if (!args.validCaptchaUrl && !args.onDataReady) {
|
||||||
throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");
|
throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");
|
||||||
}
|
}
|
||||||
this.bindEl = args.bindEl;
|
this.bindEl = args.bindEl;
|
||||||
@ -21,6 +21,9 @@ class CaptchaConfig {
|
|||||||
if (args.validFail) {
|
if (args.validFail) {
|
||||||
this.validFail = args.validFail;
|
this.validFail = args.validFail;
|
||||||
}
|
}
|
||||||
|
if (args.onDataReady) {
|
||||||
|
this.onDataReady = args.onDataReady;
|
||||||
|
}
|
||||||
if (args.requestHeaders) {
|
if (args.requestHeaders) {
|
||||||
this.requestHeaders = args.requestHeaders;
|
this.requestHeaders = args.requestHeaders;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.slider-img-div {
|
.slider-img-div {
|
||||||
|
|||||||
9
src/vendor/tianai-captcha/index.ts
vendored
9
src/vendor/tianai-captcha/index.ts
vendored
@ -2,9 +2,13 @@ import { CaptchaConfig, TianAiCaptcha } from "./captcha/captcha.js"
|
|||||||
|
|
||||||
export type TianAiCaptchaBindTarget = string | HTMLElement
|
export type TianAiCaptchaBindTarget = string | HTMLElement
|
||||||
|
|
||||||
|
export interface TianAiCaptchaResult {
|
||||||
|
id: string
|
||||||
|
data: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
export interface TianAiCaptchaStyle {
|
export interface TianAiCaptchaStyle {
|
||||||
btnUrl?: string
|
btnUrl?: string
|
||||||
logoUrl?: string | null
|
|
||||||
bgUrl?: string
|
bgUrl?: string
|
||||||
moveTrackMaskBgColor?: string
|
moveTrackMaskBgColor?: string
|
||||||
moveTrackMaskBorderColor?: string
|
moveTrackMaskBorderColor?: string
|
||||||
@ -14,11 +18,12 @@ export interface TianAiCaptchaStyle {
|
|||||||
export interface TianAiCaptchaConfig {
|
export interface TianAiCaptchaConfig {
|
||||||
bindEl: TianAiCaptchaBindTarget
|
bindEl: TianAiCaptchaBindTarget
|
||||||
requestCaptchaDataUrl: string
|
requestCaptchaDataUrl: string
|
||||||
validCaptchaUrl: string
|
validCaptchaUrl?: string
|
||||||
requestHeaders?: Record<string, string>
|
requestHeaders?: Record<string, string>
|
||||||
timeToTimestamp?: boolean
|
timeToTimestamp?: boolean
|
||||||
validSuccess?: (res: any, captcha: any, tac: TianAiCaptchaInstance) => void
|
validSuccess?: (res: any, captcha: any, tac: TianAiCaptchaInstance) => void
|
||||||
validFail?: (res: any, captcha: any, tac: TianAiCaptchaInstance) => void
|
validFail?: (res: any, captcha: any, tac: TianAiCaptchaInstance) => void
|
||||||
|
onDataReady?: (result: TianAiCaptchaResult, captcha: any, tac: TianAiCaptchaInstance) => void | Promise<void>
|
||||||
btnRefreshFun?: (el: Event, tac: TianAiCaptchaInstance) => void
|
btnRefreshFun?: (el: Event, tac: TianAiCaptchaInstance) => void
|
||||||
btnCloseFun?: (el: Event, tac: TianAiCaptchaInstance) => void
|
btnCloseFun?: (el: Event, tac: TianAiCaptchaInstance) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
prefix-icon="User"
|
prefix-icon="User"
|
||||||
size="large"
|
size="large"
|
||||||
@keyup.enter="login"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item label="密码" prop="password">
|
||||||
@ -29,35 +29,74 @@
|
|||||||
prefix-icon="Lock"
|
prefix-icon="Lock"
|
||||||
size="large"
|
size="large"
|
||||||
show-password
|
show-password
|
||||||
@keyup.enter="login"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button class="login-btn" type="primary" size="large" :loading="loading" @click="login" plain>
|
<el-button class="login-btn" type="primary" size="large" :loading="loading" @click="handleLogin" plain>
|
||||||
登 录
|
登 录
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="captchaDialogVisible"
|
||||||
|
class="captcha-dialog"
|
||||||
|
width="460px"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
|
title="安全验证"
|
||||||
|
:close-on-click-modal="!loading"
|
||||||
|
:close-on-press-escape="!loading"
|
||||||
|
:show-close="!loading"
|
||||||
|
@closed="handleCaptchaDialogClosed"
|
||||||
|
>
|
||||||
|
<p class="captcha-dialog-subtitle">请先完成验证码校验,再继续登录。</p>
|
||||||
|
<div ref="captchaBoxRef" class="captcha-box"></div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { VForm } from '../types'
|
import type { VForm } from '../types'
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
|
import {
|
||||||
|
createTianAiCaptcha,
|
||||||
|
type TianAiCaptchaConfig,
|
||||||
|
type TianAiCaptchaInstance,
|
||||||
|
type TianAiCaptchaResult
|
||||||
|
} from '@/vendor/tianai-captcha'
|
||||||
|
|
||||||
type UserInfo = {
|
type LoginFormState = {
|
||||||
username: string | null,
|
username: string | null,
|
||||||
password: string | null
|
password: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginRequest = LoginFormState & {
|
||||||
|
captchaId?: string,
|
||||||
|
captchaData?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginConfigResponse = {
|
||||||
|
captcha?: {
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loginForm = ref<VForm>()
|
const loginForm = ref<VForm>()
|
||||||
|
const captchaBoxRef = ref<HTMLElement | null>(null)
|
||||||
|
const captchaDialogVisible = ref(false)
|
||||||
|
const captchaInstance = ref<TianAiCaptchaInstance | null>(null)
|
||||||
|
const requestCaptchaDataUrl = buildApiUrl('/captcha/gen')
|
||||||
|
const captchaErrorPattern = /验证码|captcha/i
|
||||||
|
|
||||||
const userInfo: UserInfo = reactive({
|
const userInfo: LoginFormState = reactive({
|
||||||
username: null,
|
username: null,
|
||||||
password: null
|
password: null
|
||||||
})
|
})
|
||||||
@ -77,19 +116,130 @@ store.commit('logout')
|
|||||||
store.commit('clearTabs')
|
store.commit('clearTabs')
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|
||||||
async function login() {
|
async function openCaptchaDialog() {
|
||||||
|
captchaDialogVisible.value = true
|
||||||
|
await nextTick()
|
||||||
|
initCaptcha()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
loginForm.value?.validate(async (valid: boolean) => {
|
loginForm.value?.validate(async (valid: boolean) => {
|
||||||
if (!valid) return
|
if (!valid || loading.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const data = await http.post<UserInfo, any>('/common/login', userInfo).finally(() => {
|
|
||||||
loading.value = false
|
try {
|
||||||
|
const loginConfig = await http.get<any, LoginConfigResponse>('/common/loginConfig', {
|
||||||
|
params: {
|
||||||
|
username: userInfo.username,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (loginConfig.captcha?.enabled) {
|
||||||
|
loading.value = false
|
||||||
|
await openCaptchaDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitLogin(userInfo)
|
||||||
|
} catch {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCaptcha() {
|
||||||
|
if (!captchaBoxRef.value) {
|
||||||
|
captchaDialogVisible.value = false
|
||||||
|
ElMessage.error('验证码容器初始化失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyCaptcha(false)
|
||||||
|
|
||||||
|
const config: TianAiCaptchaConfig = {
|
||||||
|
bindEl: captchaBoxRef.value,
|
||||||
|
requestCaptchaDataUrl,
|
||||||
|
onDataReady: handleCaptchaSolved,
|
||||||
|
btnRefreshFun: (_el, tac) => {
|
||||||
|
tac.reloadCaptcha()
|
||||||
|
},
|
||||||
|
btnCloseFun: () => {
|
||||||
|
closeCaptchaDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaInstance.value = createTianAiCaptcha(config)
|
||||||
|
captchaInstance.value.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCaptchaSolved(result: TianAiCaptchaResult) {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submitLogin({
|
||||||
|
...userInfo,
|
||||||
|
captchaId: result.id,
|
||||||
|
captchaData: result.data,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (isCaptchaError(error)) {
|
||||||
|
captchaInstance.value?.reloadCaptcha()
|
||||||
|
} else {
|
||||||
|
closeCaptchaDialog()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitLogin(payload: LoginRequest) {
|
||||||
|
const data = await http.post<LoginRequest, any>('/common/login', payload)
|
||||||
|
|
||||||
|
closeCaptchaDialog(false)
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
store.commit('login', data)
|
store.commit('login', data)
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeCaptchaDialog(resetLoading = true) {
|
||||||
|
captchaDialogVisible.value = false
|
||||||
|
destroyCaptcha(false)
|
||||||
|
if (resetLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCaptchaDialogClosed() {
|
||||||
|
destroyCaptcha(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyCaptcha(showMessage = false) {
|
||||||
|
if (!captchaInstance.value) return
|
||||||
|
captchaInstance.value.destroyWindow()
|
||||||
|
captchaInstance.value = null
|
||||||
|
if (showMessage) {
|
||||||
|
console.info('[login captcha] destroy captcha instance')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiUrl(path: string) {
|
||||||
|
const normalizedBase = (import.meta.env.VITE_APP_API_BASE || '').replace(/\/+$/, '')
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||||
|
return `${normalizedBase}${normalizedPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCaptchaError(error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return captchaErrorPattern.test(error.message)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => destroyCaptcha())
|
||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.login-page {
|
.login-page {
|
||||||
@ -227,4 +377,15 @@ async function login() {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-dialog-subtitle {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-box {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user