|
|
@@ -109,18 +109,43 @@
|
|
|
style="margin-bottom: 20px"
|
|
|
/>
|
|
|
|
|
|
- <el-form :model="confirmForm" label-width="80px">
|
|
|
+ <el-form :model="confirmForm" label-width="100px" ref="formRef">
|
|
|
<el-form-item label="登录密码">
|
|
|
- <el-input v-model="confirmForm.password" type="password" show-password placeholder="请输入当前登录密码" />
|
|
|
+ <!-- Fake fields to trick browser -->
|
|
|
+ <input style="display:none" type="text" name="fakeusernameremembered"/>
|
|
|
+ <input style="display:none" type="password" name="fakepasswordremembered"/>
|
|
|
+
|
|
|
+ <el-input
|
|
|
+ v-model="confirmForm.password"
|
|
|
+ type="password"
|
|
|
+ show-password
|
|
|
+ placeholder="请输入当前登录密码"
|
|
|
+ autocomplete="new-password"
|
|
|
+ name="new-password-field-random"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
+
|
|
|
<el-form-item label="验证码">
|
|
|
<div class="captcha-row">
|
|
|
- <el-input v-model="confirmForm.captcha_code" placeholder="验证码" style="width: 150px" />
|
|
|
+ <el-input v-model="confirmForm.captcha_code" placeholder="图形验证码" style="width: 150px" />
|
|
|
<div class="captcha-img" @click="refreshCaptcha" v-if="captchaImage">
|
|
|
<img :src="captchaImage" alt="captcha" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="短信验证码">
|
|
|
+ <div class="captcha-row">
|
|
|
+ <el-input v-model="confirmForm.sms_code" placeholder="短信验证码" style="width: 150px" />
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :disabled="smsCooldown > 0 || !confirmForm.captcha_code"
|
|
|
+ @click="sendSms"
|
|
|
+ >
|
|
|
+ {{ smsCooldown > 0 ? `${smsCooldown}s 后重发` : '发送短信' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
@@ -133,12 +158,13 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|
|
+import { ref, reactive, onMounted, computed, watch, onUnmounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import {
|
|
|
getBackups,
|
|
|
previewRestore,
|
|
|
restoreBackup,
|
|
|
+ sendRestoreSms,
|
|
|
type BackupRecord
|
|
|
} from '../../../api/backup'
|
|
|
import api from '../../../utils/request'
|
|
|
@@ -163,9 +189,12 @@ const restoring = ref(false)
|
|
|
const confirmForm = reactive({
|
|
|
password: '',
|
|
|
captcha_id: '',
|
|
|
- captcha_code: ''
|
|
|
+ captcha_code: '',
|
|
|
+ sms_code: ''
|
|
|
})
|
|
|
const captchaImage = ref('')
|
|
|
+const smsCooldown = ref(0)
|
|
|
+let timer: any = null
|
|
|
|
|
|
// --- Methods: Step 1 ---
|
|
|
const loadBackups = async () => {
|
|
|
@@ -212,7 +241,6 @@ const loadPreview = async () => {
|
|
|
mappingData.value = csvHeaders.value.map(header => {
|
|
|
// Try exact match
|
|
|
let match = dbColumns.value.find(col => col === header)
|
|
|
- // Try simple normalization (e.g. secret -> app_secret?) - Optional
|
|
|
return {
|
|
|
csv: header,
|
|
|
db: match || ''
|
|
|
@@ -244,13 +272,44 @@ const refreshCaptcha = async () => {
|
|
|
|
|
|
const openConfirmDialog = () => {
|
|
|
confirmForm.password = ''
|
|
|
+ confirmForm.sms_code = ''
|
|
|
refreshCaptcha()
|
|
|
confirmVisible.value = true
|
|
|
}
|
|
|
|
|
|
+const sendSms = async () => {
|
|
|
+ if (!confirmForm.captcha_code) {
|
|
|
+ ElMessage.warning('请先输入图形验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await sendRestoreSms({
|
|
|
+ captcha_id: confirmForm.captcha_id,
|
|
|
+ captcha_code: confirmForm.captcha_code
|
|
|
+ })
|
|
|
+ ElMessage.success('短信验证码已发送')
|
|
|
+
|
|
|
+ // Start Cooldown
|
|
|
+ smsCooldown.value = 60
|
|
|
+ timer = setInterval(() => {
|
|
|
+ smsCooldown.value--
|
|
|
+ if (smsCooldown.value <= 0) {
|
|
|
+ clearInterval(timer)
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+
|
|
|
+ } catch (e: any) {
|
|
|
+ // Refresh captcha on failure as it's one-time use
|
|
|
+ refreshCaptcha()
|
|
|
+ const msg = e.response?.data?.detail || '发送短信失败'
|
|
|
+ ElMessage.error(msg)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const handleRestore = async () => {
|
|
|
- if (!confirmForm.password || !confirmForm.captcha_code) {
|
|
|
- ElMessage.warning('请输入密码和验证码')
|
|
|
+ if (!confirmForm.password || !confirmForm.sms_code) {
|
|
|
+ ElMessage.warning('请输入密码和短信验证码')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -268,19 +327,15 @@ const handleRestore = async () => {
|
|
|
restore_type: restoreType.value as any,
|
|
|
field_mapping,
|
|
|
password: confirmForm.password,
|
|
|
- captcha_id: confirmForm.captcha_id,
|
|
|
- captcha_code: confirmForm.captcha_code
|
|
|
+ sms_code: confirmForm.sms_code
|
|
|
})
|
|
|
|
|
|
ElMessage.success(res.data.message || '还原成功')
|
|
|
confirmVisible.value = false
|
|
|
activeStep.value = 0
|
|
|
} catch (e: any) {
|
|
|
- // Handle error message
|
|
|
const msg = e.response?.data?.detail || '还原失败'
|
|
|
ElMessage.error(msg)
|
|
|
- // Refresh captcha on failure
|
|
|
- refreshCaptcha()
|
|
|
} finally {
|
|
|
restoring.value = false
|
|
|
}
|
|
|
@@ -304,6 +359,10 @@ const formatDate = (dateStr: string) => {
|
|
|
onMounted(() => {
|
|
|
loadBackups()
|
|
|
})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -351,4 +410,3 @@ onMounted(() => {
|
|
|
height: 100%;
|
|
|
}
|
|
|
</style>
|
|
|
-
|