1166 lines
32 KiB
Vue
1166 lines
32 KiB
Vue
<template>
|
||
<div class="security-settings" v-loading="statusLoading">
|
||
<div class="security-item-wrapper">
|
||
<div class="security-item">
|
||
<div class="security-left">
|
||
<div class="security-icon">
|
||
<i class="el-icon-lock"></i>
|
||
</div>
|
||
<div class="security-info">
|
||
<div class="security-title">双重验证</div>
|
||
<p class="security-desc">用于登录帐户、结算订单、提现、修改登录密码等,涉及账户安全的重要操作。</p>
|
||
</div>
|
||
</div>
|
||
<div class="security-right">
|
||
<span class="security-status" :class="getStatusClass">
|
||
{{ getStatusText }}
|
||
</span>
|
||
<el-button
|
||
:type="getButtonType"
|
||
@click="handleButtonClick"
|
||
:loading="loading"
|
||
class="security-btn"
|
||
>
|
||
{{ getButtonText }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="security-divider"></div>
|
||
</div>
|
||
|
||
<!-- 第一步:显示二维码和密钥 -->
|
||
<el-dialog
|
||
title="开启双重验证 - 步骤 1/2"
|
||
:visible.sync="step1Visible"
|
||
width="600px"
|
||
:close-on-click-modal="false"
|
||
@close="handleStep1Close"
|
||
>
|
||
<div class="step1-content">
|
||
<div class="instruction-text">
|
||
<p>请使用您手机上的谷歌身份验证器 (Google Authenticator) 或其它兼容应用程序扫描下方二维码,也可手动输入以下密钥。</p>
|
||
</div>
|
||
|
||
<div class="qr-section">
|
||
<div class="qr-code-wrapper" v-if="qrCodeUrl">
|
||
<img :src="getQrCodeSrc" alt="二维码" class="qr-code" />
|
||
</div>
|
||
<div v-else class="qr-loading">
|
||
<i class="el-icon-loading"></i>
|
||
<span>加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="secret-key-section">
|
||
<div class="secret-key-label">或手动输入密钥:</div>
|
||
<div class="secret-key-input-group">
|
||
<el-input
|
||
v-model="secretKey"
|
||
readonly
|
||
class="secret-key-input"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleCopySecret"
|
||
:disabled="!secretKey"
|
||
>
|
||
复制
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="warning-box">
|
||
<i class="el-icon-warning"></i>
|
||
<div class="warning-text">
|
||
<p>请妥善保存密钥,避免被盗或丢失。如遇手机丢失等情况,可通过该密钥恢复您的谷歌验证。如密钥丢失,需要提交工单通过人工客服重置,处理时间需7天。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="step1Visible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleNextToStep2" :disabled="!qrCodeUrl || !secretKey">
|
||
下一步
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 第二步:验证 -->
|
||
<el-dialog
|
||
title="开启双重验证 - 步骤 2/2"
|
||
:visible.sync="step2Visible"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
@close="handleStep2Close"
|
||
>
|
||
<el-form
|
||
ref="verifyForm"
|
||
:model="verifyForm"
|
||
:rules="verifyRules"
|
||
label-position="top"
|
||
>
|
||
<el-form-item label="登录密码" prop="password">
|
||
<el-input
|
||
v-model="verifyForm.password"
|
||
type="password"
|
||
placeholder="请输入密码(8-32位)"
|
||
show-password
|
||
clearable
|
||
/>
|
||
<!-- 密码规则提示 -->
|
||
<div class="password-tip">
|
||
<i class="el-icon-info"></i>
|
||
<span>密码需包含大小写字母、数字和特殊字符,长度8-32位</span>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="邮箱验证码" prop="emailCode">
|
||
<div class="code-input-group">
|
||
<el-input
|
||
v-model="verifyForm.emailCode"
|
||
placeholder="请输入邮箱验证码"
|
||
class="code-input"
|
||
maxlength="10"
|
||
clearable
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleSendEmailCode"
|
||
:loading="sendingCode"
|
||
:disabled="countdown > 0"
|
||
>
|
||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||
</el-button>
|
||
</div>
|
||
<div class="help-link">
|
||
<a href="javascript:void(0)" @click="handleCannotGetCode">无法获取验证码?</a>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="谷歌验证码" prop="googleCode">
|
||
<el-input
|
||
v-model="verifyForm.googleCode"
|
||
placeholder="请输入6位动态口令"
|
||
maxlength="6"
|
||
@input="handleGoogleCodeInput"
|
||
|
||
/>
|
||
|
||
<div class="help-link">
|
||
<a href="javascript:void(0)" @click="handleCannotGetGoogleCode">无法获取验证码?</a>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="handleBackToStep1">上一步</el-button>
|
||
<el-button type="primary" @click="handleConfirm" :loading="submitting">
|
||
确定
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 关闭双重验证弹窗 -->
|
||
<el-dialog
|
||
title="关闭双重验证"
|
||
:visible.sync="closeDialogVisible"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
@close="handleCloseDialogClose"
|
||
>
|
||
<el-form
|
||
ref="closeForm"
|
||
:model="closeForm"
|
||
:rules="closeRules"
|
||
label-position="top"
|
||
>
|
||
<el-form-item label="邮箱验证码" prop="emailCode">
|
||
<div class="code-input-group">
|
||
<el-input
|
||
v-model="closeForm.emailCode"
|
||
placeholder="请输入邮箱验证码"
|
||
class="code-input"
|
||
maxlength="10"
|
||
clearable
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleSendCloseEmailCode"
|
||
:loading="sendingCloseCode"
|
||
:disabled="closeCountdown > 0"
|
||
>
|
||
{{ closeCountdown > 0 ? `${closeCountdown}秒后重试` : '发送验证码' }}
|
||
</el-button>
|
||
</div>
|
||
|
||
</el-form-item>
|
||
|
||
<el-form-item label="谷歌验证码" prop="googleCode">
|
||
<el-input
|
||
v-model="closeForm.googleCode"
|
||
placeholder="请输入6位动态口令"
|
||
maxlength="6"
|
||
@input="handleCloseGoogleCodeInput"
|
||
clearable
|
||
/>
|
||
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="closeDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleConfirmClose" :loading="closing">
|
||
确定
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 开启双重验证弹窗 -->
|
||
<el-dialog
|
||
title="开启双重验证"
|
||
:visible.sync="openDialogVisible"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
@close="handleOpenDialogClose"
|
||
>
|
||
<el-form
|
||
ref="openForm"
|
||
:model="openForm"
|
||
:rules="openRules"
|
||
label-position="top"
|
||
>
|
||
<el-form-item label="邮箱验证码" prop="emailCode">
|
||
<div class="code-input-group">
|
||
<el-input
|
||
v-model="openForm.emailCode"
|
||
placeholder="请输入邮箱验证码"
|
||
class="code-input"
|
||
maxlength="10"
|
||
clearable
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleSendOpenEmailCode"
|
||
:loading="sendingOpenCode"
|
||
:disabled="openCountdown > 0"
|
||
>
|
||
{{ openCountdown > 0 ? `${openCountdown}秒后重试` : '发送验证码' }}
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="谷歌验证码" prop="googleCode">
|
||
<el-input
|
||
v-model="openForm.googleCode"
|
||
placeholder="请输入6位动态口令"
|
||
maxlength="6"
|
||
@input="handleOpenGoogleCodeInput"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="openDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleConfirmOpen" :loading="opening">
|
||
确定
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getBindInfo, bindGoogle, sendOpenGoogleCode,getGoogleStatus,closeStepTwo,sendCloseGoogleCode,openStepTwo } from '../../api/verification'
|
||
import { rsaEncrypt } from '../../utils/rsaEncrypt'
|
||
|
||
export default {
|
||
name: 'SecuritySettings',
|
||
data() {
|
||
/**
|
||
* 密码格式验证
|
||
* 8-32位,包含大小写字母、数字和特殊字符
|
||
*/
|
||
const validatePassword = (rule, value, callback) => {
|
||
if (!value) {
|
||
callback(new Error('请输入密码'))
|
||
return
|
||
}
|
||
|
||
// 密码验证正则:8-32位,包含大小写字母、数字和特殊字符(!@#¥%……&.*)
|
||
const regexPassword = /^(?!.*[\u4e00-\u9fa5])(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_]+$)(?![a-z0-9]+$)(?![a-z\W_]+$)(?![0-9\W_]+$)[a-zA-Z0-9\W_]{8,32}$/
|
||
|
||
if (!regexPassword.test(value)) {
|
||
callback(new Error('密码应包含大小写字母、数字和特殊字符,长度8-32位'))
|
||
return
|
||
}
|
||
|
||
callback()
|
||
}
|
||
|
||
return {
|
||
isEnabled: false, // 是否已开启双重验证
|
||
loading: false,
|
||
statusLoading: false, // 状态查询的 loading
|
||
step1Visible: false,
|
||
step2Visible: false,
|
||
closeDialogVisible: false, // 关闭双重验证弹窗
|
||
openDialogVisible: false, // 开启双重验证弹窗
|
||
qrCodeUrl: '',
|
||
secretKey: '',
|
||
sendingCode: false,
|
||
countdown: 0,
|
||
countdownTimer: null,
|
||
sendingCloseCode: false, // 发送关闭验证码的 loading
|
||
closeCountdown: 0, // 关闭验证码倒计时
|
||
closeCountdownTimer: null, // 关闭验证码倒计时定时器
|
||
sendingOpenCode: false, // 发送开启验证码的 loading
|
||
openCountdown: 0, // 开启验证码倒计时
|
||
openCountdownTimer: null, // 开启验证码倒计时定时器
|
||
closing: false, // 关闭双重验证的 loading
|
||
opening: false, // 开启双重验证的 loading
|
||
submitting: false,
|
||
verifyForm: {
|
||
password: '',
|
||
emailCode: '',
|
||
googleCode: ''
|
||
},
|
||
closeForm: {
|
||
emailCode: '',
|
||
googleCode: ''
|
||
},
|
||
openForm: {
|
||
emailCode: '',
|
||
googleCode: ''
|
||
},
|
||
verifyRules: {
|
||
password: [
|
||
{ required: true, validator: validatePassword, trigger: 'blur' }
|
||
],
|
||
emailCode: [
|
||
{ required: true, message: '请输入邮箱验证码', trigger: 'blur' },
|
||
{ min: 1, max: 10, message: '验证码长度为1-10位', trigger: 'blur' }
|
||
],
|
||
googleCode: [
|
||
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
|
||
{ pattern: /^\d{6}$/, message: '请输入6位数字', trigger: 'blur' }
|
||
]
|
||
},
|
||
closeRules: {
|
||
emailCode: [
|
||
{ required: true, message: '请输入邮箱验证码', trigger: 'blur' },
|
||
{ min: 1, max: 10, message: '验证码长度为1-10位', trigger: 'blur' }
|
||
],
|
||
googleCode: [
|
||
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
|
||
{ pattern: /^\d{6}$/, message: '请输入6位数字', trigger: 'blur' }
|
||
]
|
||
},
|
||
openRules: {
|
||
emailCode: [
|
||
{ required: true, message: '请输入邮箱验证码', trigger: 'blur' },
|
||
{ min: 1, max: 10, message: '验证码长度为1-10位', trigger: 'blur' }
|
||
],
|
||
googleCode: [
|
||
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
|
||
{ pattern: /^\d{6}$/, message: '请输入6位数字', trigger: 'blur' }
|
||
]
|
||
},
|
||
googleStatus: 1 // 谷歌验证状态:0 开启;1 未绑定;2 关闭
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 获取二维码图片源(处理 Base64 格式)
|
||
*/
|
||
getQrCodeSrc() {
|
||
if (!this.qrCodeUrl) return ''
|
||
// 如果已经是完整的 data URI,直接返回
|
||
if (this.qrCodeUrl.startsWith('data:')) {
|
||
return this.qrCodeUrl
|
||
}
|
||
// 如果是 Base64 字符串,添加前缀
|
||
return `data:image/png;base64,${this.qrCodeUrl}`
|
||
},
|
||
/**
|
||
* 获取状态显示文字
|
||
* 0 开启;1 未绑定;2 关闭
|
||
*/
|
||
getStatusText() {
|
||
switch (this.googleStatus) {
|
||
case 0:
|
||
return '已开启' // 0 开启 -> 显示已开启
|
||
case 1:
|
||
return '未绑定' // 1 未绑定
|
||
case 2:
|
||
return '已关闭' // 2 关闭 -> 显示已关闭
|
||
default:
|
||
return '未绑定'
|
||
}
|
||
},
|
||
/**
|
||
* 获取按钮文字
|
||
* 0 开启;1 未绑定;2 关闭
|
||
*/
|
||
getButtonText() {
|
||
switch (this.googleStatus) {
|
||
case 0:
|
||
return '关闭' // 0 开启 -> 按钮显示关闭
|
||
case 1:
|
||
return '设置' // 1 未绑定 -> 按钮显示设置
|
||
case 2:
|
||
return '开启' // 2 关闭 -> 按钮显示开启
|
||
default:
|
||
return '设置'
|
||
}
|
||
},
|
||
/**
|
||
* 获取状态样式类
|
||
* 0 开启;1 未绑定;2 关闭
|
||
*/
|
||
getStatusClass() {
|
||
return {
|
||
'status-enabled': this.googleStatus === 0, // 状态0(开启)显示绿色
|
||
'status-bound': this.googleStatus === 2 // 状态2(关闭)可以有不同的样式
|
||
}
|
||
},
|
||
/**
|
||
* 获取按钮类型
|
||
* 0 开启;1 未绑定;2 关闭
|
||
*/
|
||
getButtonType() {
|
||
switch (this.googleStatus) {
|
||
case 0:
|
||
return 'danger' // 0 开启 -> 关闭按钮使用危险色(红色)
|
||
case 1:
|
||
return 'primary' // 1 未绑定 -> 设置按钮使用主色(蓝色)
|
||
case 2:
|
||
return 'primary' // 2 关闭 -> 开启按钮使用主色(蓝色)
|
||
default:
|
||
return 'primary'
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.check2FAStatus()
|
||
},
|
||
beforeDestroy() {
|
||
if (this.countdownTimer) {
|
||
clearInterval(this.countdownTimer)
|
||
}
|
||
if (this.closeCountdownTimer) {
|
||
clearInterval(this.closeCountdownTimer)
|
||
}
|
||
if (this.openCountdownTimer) {
|
||
clearInterval(this.openCountdownTimer)
|
||
}
|
||
},
|
||
methods: {
|
||
/**
|
||
* 检查双重验证状态
|
||
* 返回值:0 开启;1 未绑定;2 关闭
|
||
*/
|
||
async check2FAStatus() {
|
||
this.statusLoading = true
|
||
try {
|
||
const res = await getGoogleStatus()
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
const status = res.data?.status ?? res.data ?? 1
|
||
// 0: 开启
|
||
// 1: 未绑定
|
||
// 2: 关闭
|
||
this.googleStatus = status
|
||
this.isEnabled = status === 0 // 兼容旧逻辑
|
||
} else {
|
||
// 如果接口调用失败,默认未绑定
|
||
this.googleStatus = 1
|
||
this.isEnabled = false
|
||
}
|
||
} catch (e) {
|
||
console.error('查询谷歌绑定状态失败', e)
|
||
// 出错时默认未绑定
|
||
this.googleStatus = 1
|
||
this.isEnabled = false
|
||
} finally {
|
||
this.statusLoading = false
|
||
}
|
||
},
|
||
/**
|
||
* 处理按钮点击
|
||
* 0 开启;1 未绑定;2 关闭
|
||
*/
|
||
handleButtonClick() {
|
||
switch (this.googleStatus) {
|
||
case 0:
|
||
// 0 开启 -> 点击关闭
|
||
this.handleDisable2FA()
|
||
break
|
||
case 1:
|
||
// 1 未绑定 -> 点击设置(开启)
|
||
this.handleEnable2FA()
|
||
break
|
||
case 2:
|
||
// 2 关闭 -> 点击开启
|
||
this.handleEnable2FA()
|
||
break
|
||
default:
|
||
this.handleEnable2FA()
|
||
}
|
||
},
|
||
/**
|
||
* 开启双重验证
|
||
* 状态 1(未绑定):需要先获取二维码和密钥
|
||
* 状态 2(已关闭):直接显示开启验证弹窗
|
||
*/
|
||
async handleEnable2FA() {
|
||
// 如果是未绑定状态,需要先获取二维码和密钥
|
||
if (this.googleStatus === 1) {
|
||
this.loading = true
|
||
try {
|
||
const res = await getBindInfo()
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
// img 是 Base64 格式的二维码图片
|
||
this.qrCodeUrl = res.data?.img || ''
|
||
// secret 是返回的密钥
|
||
this.secretKey = res.data?.secret || ''
|
||
console.log('getBindInfo 返回数据:', res.data) // 调试用
|
||
console.log('保存的 secretKey:', this.secretKey) // 调试用
|
||
if (this.qrCodeUrl || this.secretKey) {
|
||
this.step1Visible = true
|
||
} else {
|
||
this.$message.error('获取绑定信息失败,请稍后重试')
|
||
}
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '获取绑定信息失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('获取绑定信息失败', e)
|
||
this.$message.error('获取绑定信息失败,请稍后重试')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
} else {
|
||
// 如果是已关闭状态,直接显示开启验证弹窗
|
||
this.openDialogVisible = true
|
||
}
|
||
},
|
||
/**
|
||
* 关闭双重验证 - 显示弹窗
|
||
*/
|
||
handleDisable2FA() {
|
||
this.closeDialogVisible = true
|
||
},
|
||
/**
|
||
* 发送关闭双重验证的邮箱验证码
|
||
*/
|
||
async handleSendCloseEmailCode() {
|
||
if (this.closeCountdown > 0) return
|
||
|
||
this.sendingCloseCode = true
|
||
try {
|
||
const res = await sendCloseGoogleCode() // 不需要参数
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('验证码已发送到您的邮箱')
|
||
this.startCloseCountdown()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '发送验证码失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('发送验证码失败', e)
|
||
this.$message.error('发送验证码失败,请稍后重试')
|
||
} finally {
|
||
this.sendingCloseCode = false
|
||
}
|
||
},
|
||
/**
|
||
* 开始关闭验证码倒计时
|
||
*/
|
||
startCloseCountdown() {
|
||
this.closeCountdown = 60
|
||
this.closeCountdownTimer = setInterval(() => {
|
||
this.closeCountdown--
|
||
if (this.closeCountdown <= 0) {
|
||
clearInterval(this.closeCountdownTimer)
|
||
this.closeCountdownTimer = null
|
||
}
|
||
}, 1000)
|
||
},
|
||
/**
|
||
* 关闭双重验证弹窗的谷歌验证码输入(仅数字)
|
||
*/
|
||
handleCloseGoogleCodeInput(val) {
|
||
this.closeForm.googleCode = val.replace(/\D/g, '').slice(0, 6)
|
||
},
|
||
/**
|
||
* 确认关闭双重验证
|
||
*/
|
||
async handleConfirmClose() {
|
||
try {
|
||
const valid = await this.$refs.closeForm.validate()
|
||
if (!valid) return
|
||
|
||
this.closing = true
|
||
const params = {
|
||
eCode: this.closeForm.emailCode, // 邮箱验证码
|
||
gCode: this.closeForm.googleCode // 谷歌验证码
|
||
}
|
||
|
||
const res = await closeStepTwo(params)
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('双重验证已关闭')
|
||
this.closeDialogVisible = false
|
||
this.googleStatus = 2 // 设置为已绑定,关闭
|
||
this.isEnabled = false
|
||
this.handleCloseDialogClose()
|
||
// 刷新状态
|
||
this.check2FAStatus()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '关闭失败,请检查输入信息')
|
||
}
|
||
} catch (e) {
|
||
console.error('关闭双重验证失败', e)
|
||
this.$message.error('关闭失败,请稍后重试')
|
||
} finally {
|
||
this.closing = false
|
||
}
|
||
},
|
||
/**
|
||
* 关闭双重验证弹窗关闭时的处理
|
||
*/
|
||
handleCloseDialogClose() {
|
||
this.closeForm = {
|
||
emailCode: '',
|
||
googleCode: ''
|
||
}
|
||
this.$refs.closeForm && this.$refs.closeForm.clearValidate()
|
||
},
|
||
/**
|
||
* 发送开启双重验证的邮箱验证码
|
||
*/
|
||
async handleSendOpenEmailCode() {
|
||
if (this.openCountdown > 0) return
|
||
|
||
this.sendingOpenCode = true
|
||
try {
|
||
const res = await sendOpenGoogleCode() // 不需要参数
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('验证码已发送到您的邮箱')
|
||
this.startOpenCountdown()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '发送验证码失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('发送验证码失败', e)
|
||
this.$message.error('发送验证码失败,请稍后重试')
|
||
} finally {
|
||
this.sendingOpenCode = false
|
||
}
|
||
},
|
||
/**
|
||
* 开始开启验证码倒计时
|
||
*/
|
||
startOpenCountdown() {
|
||
this.openCountdown = 60
|
||
this.openCountdownTimer = setInterval(() => {
|
||
this.openCountdown--
|
||
if (this.openCountdown <= 0) {
|
||
clearInterval(this.openCountdownTimer)
|
||
this.openCountdownTimer = null
|
||
}
|
||
}, 1000)
|
||
},
|
||
/**
|
||
* 开启双重验证弹窗的谷歌验证码输入(仅数字)
|
||
*/
|
||
handleOpenGoogleCodeInput(val) {
|
||
this.openForm.googleCode = val.replace(/\D/g, '').slice(0, 6)
|
||
},
|
||
/**
|
||
* 确认开启双重验证
|
||
*/
|
||
async handleConfirmOpen() {
|
||
try {
|
||
const valid = await this.$refs.openForm.validate()
|
||
if (!valid) return
|
||
|
||
this.opening = true
|
||
const params = {
|
||
eCode: this.openForm.emailCode, // 邮箱验证码
|
||
gCode: this.openForm.googleCode // 谷歌验证码
|
||
}
|
||
|
||
const res = await openStepTwo(params)
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('双重验证已开启')
|
||
this.openDialogVisible = false
|
||
this.googleStatus = 0 // 设置为已绑定,开启
|
||
this.isEnabled = true
|
||
this.handleOpenDialogClose()
|
||
// 刷新状态
|
||
this.check2FAStatus()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '开启失败,请检查输入信息')
|
||
}
|
||
} catch (e) {
|
||
console.error('开启双重验证失败', e)
|
||
this.$message.error('开启失败,请稍后重试')
|
||
} finally {
|
||
this.opening = false
|
||
}
|
||
},
|
||
/**
|
||
* 开启双重验证弹窗关闭时的处理
|
||
*/
|
||
handleOpenDialogClose() {
|
||
this.openForm = {
|
||
emailCode: '',
|
||
googleCode: ''
|
||
}
|
||
this.$refs.openForm && this.$refs.openForm.clearValidate()
|
||
},
|
||
/**
|
||
* 复制密钥
|
||
*/
|
||
handleCopySecret() {
|
||
if (!this.secretKey) return
|
||
const input = document.createElement('input')
|
||
input.value = this.secretKey
|
||
document.body.appendChild(input)
|
||
input.select()
|
||
try {
|
||
document.execCommand('copy')
|
||
this.$message.success('密钥已复制到剪贴板')
|
||
} catch (e) {
|
||
this.$message.error('复制失败,请手动复制')
|
||
}
|
||
document.body.removeChild(input)
|
||
},
|
||
/**
|
||
* 第一步关闭
|
||
* 注意:不要清空 secretKey,因为第二步提交时需要用到
|
||
* 只有在用户真正取消(点击取消按钮或 X)时才清空
|
||
*/
|
||
handleStep1Close() {
|
||
this.qrCodeUrl = ''
|
||
// 不清空 secretKey,因为第二步提交时需要用到
|
||
// 如果用户点击取消,会在 handleStep2Close 中清空
|
||
},
|
||
/**
|
||
* 进入第二步
|
||
*/
|
||
handleNextToStep2() {
|
||
if (!this.qrCodeUrl && !this.secretKey) {
|
||
this.$message.warning('请先获取二维码或密钥')
|
||
return
|
||
}
|
||
this.step1Visible = false
|
||
this.step2Visible = true
|
||
},
|
||
/**
|
||
* 返回第一步
|
||
*/
|
||
handleBackToStep1() {
|
||
this.step2Visible = false
|
||
this.step1Visible = true
|
||
},
|
||
/**
|
||
* 第二步关闭
|
||
*/
|
||
handleStep2Close() {
|
||
this.verifyForm = {
|
||
password: '',
|
||
emailCode: '',
|
||
googleCode: ''
|
||
}
|
||
this.$refs.verifyForm && this.$refs.verifyForm.clearValidate()
|
||
},
|
||
/**
|
||
* 发送邮箱验证码
|
||
*/
|
||
async handleSendEmailCode() {
|
||
if (this.countdown > 0) return
|
||
|
||
this.sendingCode = true
|
||
try {
|
||
const res = await sendOpenGoogleCode()
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('验证码已发送到您的邮箱')
|
||
this.startCountdown()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '发送验证码失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('发送验证码失败', e)
|
||
this.$message.error('发送验证码失败,请稍后重试')
|
||
} finally {
|
||
this.sendingCode = false
|
||
}
|
||
},
|
||
/**
|
||
* 开始倒计时
|
||
*/
|
||
startCountdown() {
|
||
this.countdown = 60
|
||
this.countdownTimer = setInterval(() => {
|
||
this.countdown--
|
||
if (this.countdown <= 0) {
|
||
clearInterval(this.countdownTimer)
|
||
this.countdownTimer = null
|
||
}
|
||
}, 1000)
|
||
},
|
||
/**
|
||
* 双重验证码输入(仅数字)
|
||
*/
|
||
handleGoogleCodeInput(val) {
|
||
this.verifyForm.googleCode = val.replace(/\D/g, '').slice(0, 6)
|
||
},
|
||
/**
|
||
* 确认提交
|
||
*/
|
||
async handleConfirm() {
|
||
try {
|
||
const valid = await this.$refs.verifyForm.validate()
|
||
if (!valid) return
|
||
|
||
// 检查密钥是否存在
|
||
if (!this.secretKey) {
|
||
this.$message.warning('密钥不存在,请重新获取')
|
||
return
|
||
}
|
||
|
||
// 检查密码是否存在
|
||
if (!this.verifyForm.password) {
|
||
this.$message.warning('请输入密码')
|
||
return
|
||
}
|
||
|
||
this.submitting = true
|
||
|
||
// 对密码进行 RSA 加密
|
||
const encryptedPassword = await rsaEncrypt(this.verifyForm.password)
|
||
if (!encryptedPassword) {
|
||
this.$message.error('密码加密失败,请稍后重试')
|
||
this.submitting = false
|
||
return
|
||
}
|
||
|
||
const params = {
|
||
eCode: this.verifyForm.emailCode, // 邮箱验证码
|
||
gCode: this.verifyForm.googleCode, // 谷歌验证码
|
||
pwd: encryptedPassword, // RSA 加密后的密码
|
||
secret: this.secretKey // 上一步弹窗的密钥(getBindInfo 返回的 secret)
|
||
}
|
||
|
||
console.log('提交参数:', params) // 调试用,确认 secret 是否正确
|
||
|
||
const res = await bindGoogle(params)
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message.success('双重验证已成功开启')
|
||
this.step2Visible = false
|
||
this.googleStatus = 0 // 设置为已绑定,开启
|
||
this.isEnabled = true
|
||
this.handleStep2Close()
|
||
} else {
|
||
this.$message.error(res?.message || res?.msg || '绑定失败,请检查输入信息')
|
||
}
|
||
} catch (e) {
|
||
console.error('绑定失败', e)
|
||
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
},
|
||
/**
|
||
* 无法获取验证码
|
||
*/
|
||
handleCannotGetCode() {
|
||
this.$message.info('请检查邮箱垃圾箱,或联系客服')
|
||
},
|
||
/**
|
||
* 无法获取谷歌验证码
|
||
*/
|
||
handleCannotGetGoogleCode() {
|
||
this.$message.info('请确保已正确扫描二维码或输入密钥,并检查时间同步')
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.security-settings {
|
||
padding: 0;
|
||
}
|
||
|
||
.security-item-wrapper {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
overflow: visible; /* 改为 visible,允许内容溢出 */
|
||
width: 100%;
|
||
}
|
||
|
||
.security-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24px;
|
||
width: 100%;
|
||
min-width: 1000px; /* 设置最小宽度,确保有足够空间 */
|
||
}
|
||
|
||
.security-left {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
flex: 1;
|
||
min-width: 0; /* 允许 flex 子元素收缩 */
|
||
}
|
||
|
||
.security-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.security-icon i {
|
||
font-size: 24px;
|
||
color: #667eea;
|
||
}
|
||
|
||
.security-info {
|
||
flex: 1;
|
||
text-align: left;
|
||
min-width: 700px; /* 设置最小宽度,给文字更多显示空间 */
|
||
flex-shrink: 0; /* 防止被压缩 */
|
||
}
|
||
|
||
.security-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
margin: 0 0 8px 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.security-desc {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: #909399;
|
||
line-height: 1.6;
|
||
text-align: left;
|
||
}
|
||
|
||
.security-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.security-status {
|
||
font-size: 13px;
|
||
color: #f56c6c;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.security-status.status-enabled {
|
||
color: #67c23a;
|
||
}
|
||
|
||
.security-btn {
|
||
min-width: 70px;
|
||
padding: 7px 14px;
|
||
font-size: 13px;
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.security-btn:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.security-btn.el-button--primary:hover {
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||
}
|
||
|
||
.security-btn.el-button--danger:hover {
|
||
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
|
||
}
|
||
|
||
.security-divider {
|
||
height: 1px;
|
||
background: #e4e7ed;
|
||
margin: 0 24px;
|
||
}
|
||
|
||
/* 第一步对话框样式 */
|
||
.step1-content {
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.instruction-text {
|
||
margin-bottom: 24px;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.qr-section {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.qr-code-wrapper {
|
||
padding: 16px;
|
||
background: #f5f7fa;
|
||
border-radius: 8px;
|
||
border: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.qr-code {
|
||
width: 200px;
|
||
height: 200px;
|
||
display: block;
|
||
}
|
||
|
||
.qr-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px;
|
||
color: #909399;
|
||
gap: 12px;
|
||
}
|
||
|
||
.qr-loading i {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.secret-key-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.secret-key-label {
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.secret-key-input-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.secret-key-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.warning-box {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: #fef0f0;
|
||
border: 1px solid #fde2e2;
|
||
border-radius: 6px;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.warning-box i {
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.warning-text {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 第二步对话框样式 */
|
||
/* 调整弹窗内容区域的 padding,使左右对称 */
|
||
.security-settings :deep(.el-dialog__body) {
|
||
padding: 20px 24px;
|
||
text-align: left;
|
||
}
|
||
|
||
/* 第二步弹窗中 label 文字左对齐 */
|
||
.security-settings :deep(.el-form--label-top .el-form-item__label) {
|
||
text-align: left;
|
||
padding: 0 0 8px 0;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
/* 确保表单项内容左对齐 */
|
||
.security-settings :deep(.el-form--label-top .el-form-item__content) {
|
||
text-align: left;
|
||
}
|
||
|
||
/* 确保输入框左对齐 */
|
||
.security-settings :deep(.el-input) {
|
||
text-align: left;
|
||
}
|
||
|
||
.security-settings :deep(.el-input__inner) {
|
||
text-align: left;
|
||
}
|
||
|
||
.code-input-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.code-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.password-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 6px;
|
||
padding: 10px 12px;
|
||
background: #f0f9ff;
|
||
border: 1px solid #b3d8ff;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.password-tip span {
|
||
flex: 1;
|
||
}
|
||
|
||
.password-tip .el-icon-info {
|
||
color: #667eea;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.help-link {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.help-link a {
|
||
color: #667eea;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.help-link a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
</style>
|
||
|