150 lines
3.2 KiB
Vue
150 lines
3.2 KiB
Vue
<template>
|
||
<teleport to="body">
|
||
<div class="lock-screen" role="dialog" aria-modal="true">
|
||
<div class="lock-backdrop" />
|
||
<div class="lock-panel">
|
||
<h2 class="lock-title">已锁定</h2>
|
||
<p class="lock-desc">30 分钟无操作,请输入密码解锁</p>
|
||
<el-input
|
||
ref="passwordInput"
|
||
v-model="password"
|
||
type="password"
|
||
placeholder="请输入登录密码"
|
||
show-password
|
||
@keyup.enter="onUnlock"
|
||
/>
|
||
<div class="lock-actions">
|
||
<el-button type="primary" :loading="loading" @click="onUnlock">解锁</el-button>
|
||
<el-button @click="onLogout">退出登录</el-button>
|
||
</div>
|
||
<p v-if="errorMessage" class="lock-error">{{ errorMessage }}</p>
|
||
</div>
|
||
</div>
|
||
</teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { nextTick, onMounted, ref, watch } from "vue";
|
||
import { ElMessage } from "element-plus";
|
||
import { useAuthStore } from "../store/auth";
|
||
import { useSessionStore } from "../store/session";
|
||
import { UNLOCK_MAX_ATTEMPTS, unlockWithPassword, forceLogout } from "../session/sessionManager";
|
||
|
||
const auth = useAuthStore();
|
||
const session = useSessionStore();
|
||
const password = ref("");
|
||
const loading = ref(false);
|
||
const errorMessage = ref("");
|
||
const passwordInput = ref<{ focus?: () => void } | null>(null);
|
||
|
||
const focusPassword = () => {
|
||
nextTick(() => {
|
||
passwordInput.value?.focus?.();
|
||
});
|
||
};
|
||
|
||
const onUnlock = async () => {
|
||
if (loading.value) return;
|
||
errorMessage.value = "";
|
||
const email = auth.user?.email;
|
||
if (!email) {
|
||
ElMessage.error("无法获取用户信息,请重新登录");
|
||
forceLogout();
|
||
return;
|
||
}
|
||
if (!password.value) {
|
||
errorMessage.value = "请输入密码";
|
||
focusPassword();
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try {
|
||
await unlockWithPassword(email, password.value);
|
||
password.value = "";
|
||
} catch (err: any) {
|
||
session.incrementUnlockAttempt();
|
||
const message = err?.response?.data?.detail || err?.response?.data?.message || "密码错误";
|
||
errorMessage.value = message;
|
||
if (session.unlockAttempts >= UNLOCK_MAX_ATTEMPTS) {
|
||
ElMessage.error("错误次数过多,请重新登录");
|
||
forceLogout();
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
focusPassword();
|
||
}
|
||
};
|
||
|
||
const onLogout = () => {
|
||
forceLogout();
|
||
};
|
||
|
||
onMounted(() => {
|
||
focusPassword();
|
||
});
|
||
|
||
watch(
|
||
() => session.locked,
|
||
(value) => {
|
||
if (value) {
|
||
focusPassword();
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.lock-screen {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 2000;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.lock-backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(7, 10, 20, 0.35);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.lock-panel {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: min(420px, 92vw);
|
||
background: #ffffff;
|
||
border-radius: 16px;
|
||
padding: 28px;
|
||
box-shadow: 0 18px 48px rgba(15, 20, 35, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.lock-title {
|
||
margin: 0;
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.lock-desc {
|
||
margin: 0;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.lock-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.lock-error {
|
||
margin: 0;
|
||
color: #d8342c;
|
||
font-size: 13px;
|
||
}
|
||
</style>
|