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