<!-- 锁屏 -->
|
<template>
|
<div class="layout-lock-screen">
|
<!-- 开发者工具警告覆盖层 -->
|
<div
|
v-if="showDevToolsWarning"
|
class="fixed top-0 left-0 z-[999999] flex-cc w-full h-full text-white bg-gradient-to-br from-[#1e1e1e] to-black animate-fade-in"
|
>
|
<div class="p-5 text-center select-none">
|
<div class="mb-7.5 text-5xl">🔒</div>
|
<h1 class="m-0 mb-5 text-3xl font-semibold text-danger">系统已锁定</h1>
|
<p class="max-w-125 m-0 text-lg leading-relaxed text-white">
|
检测到开发者工具已打开<br />
|
为了系统安全,请关闭开发者工具后继续使用
|
</p>
|
<div class="mt-7.5 text-sm text-gray-400">Security Lock Activated</div>
|
</div>
|
</div>
|
|
<!-- 锁屏弹窗 -->
|
<div v-if="!isLock">
|
<ElDialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
|
<div class="flex-c flex-col">
|
<img class="w-16 h-16 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
|
<div class="mt-7.5 mb-3.5 text-base font-medium">{{ userInfo.userName }}</div>
|
<ElForm
|
ref="formRef"
|
:model="formData"
|
:rules="rules"
|
class="w-[90%]"
|
@submit.prevent="handleLock"
|
>
|
<ElFormItem prop="password">
|
<ElInput
|
v-model="formData.password"
|
type="password"
|
:placeholder="$t('lockScreen.lock.inputPlaceholder')"
|
:show-password="true"
|
autocomplete="new-password"
|
ref="lockInputRef"
|
class="w-full mt-9"
|
@keyup.enter="handleLock"
|
>
|
<template #suffix>
|
<ElIcon class="c-p" @click="handleLock">
|
<Lock />
|
</ElIcon>
|
</template>
|
</ElInput>
|
</ElFormItem>
|
<ElButton type="primary" class="w-full mt-0.5" @click="handleLock" v-ripple>
|
{{ $t('lockScreen.lock.btnText') }}
|
</ElButton>
|
</ElForm>
|
</div>
|
</ElDialog>
|
</div>
|
|
<!-- 解锁界面 -->
|
<div v-else class="unlock-content">
|
<div class="flex-c flex-col w-80">
|
<img class="w-16 h-16 mt-5 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
|
<div class="mt-3 mb-3.5 text-base font-medium">
|
{{ userInfo.userName }}
|
</div>
|
<ElForm
|
ref="unlockFormRef"
|
:model="unlockForm"
|
:rules="rules"
|
class="w-full !px-2.5"
|
@submit.prevent="handleUnlock"
|
>
|
<ElFormItem prop="password">
|
<ElInput
|
v-model="unlockForm.password"
|
type="password"
|
:placeholder="$t('lockScreen.unlock.inputPlaceholder')"
|
:show-password="true"
|
autocomplete="new-password"
|
ref="unlockInputRef"
|
class="mt-5"
|
>
|
<template #suffix>
|
<ElIcon class="c-p" @click="handleUnlock">
|
<Unlock />
|
</ElIcon>
|
</template>
|
</ElInput>
|
</ElFormItem>
|
|
<ElButton type="primary" class="w-full mt-2" @click="handleUnlock" v-ripple>
|
{{ $t('lockScreen.unlock.btnText') }}
|
</ElButton>
|
<div class="w-full text-center">
|
<ElButton
|
text
|
class="mt-2.5 !text-g-600 hover:!text-theme hover:!bg-transparent"
|
@click="toLogin"
|
>
|
{{ $t('lockScreen.unlock.backBtnText') }}
|
</ElButton>
|
</div>
|
</ElForm>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { Lock, Unlock } from '@element-plus/icons-vue'
|
|
import { useI18n } from 'vue-i18n'
|
import CryptoJS from 'crypto-js'
|
import { useUserStore } from '@/store/modules/user'
|
import { mittBus } from '@/utils/sys'
|
const { t } = useI18n()
|
const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
|
const userStore = useUserStore()
|
const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
|
const visible = ref(false)
|
const lockInputRef = ref(null)
|
const unlockInputRef = ref(null)
|
const showDevToolsWarning = ref(false)
|
const formRef = ref()
|
const unlockFormRef = ref()
|
const formData = reactive({
|
password: ''
|
})
|
const unlockForm = reactive({
|
password: ''
|
})
|
const rules = computed(() => ({
|
password: [
|
{
|
required: true,
|
message: t('lockScreen.lock.inputPlaceholder'),
|
trigger: 'blur'
|
}
|
]
|
}))
|
const isMobile = () => {
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
navigator.userAgent
|
)
|
}
|
const disableDevTools = () => {
|
const handleContextMenu = (e) => {
|
if (isLock.value) {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
}
|
document.addEventListener('contextmenu', handleContextMenu, true)
|
const handleKeyDown = (e) => {
|
if (!isLock.value) return
|
if (e.key === 'F12') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.shiftKey) {
|
const key = e.key.toLowerCase()
|
if (['i', 'j', 'c', 'k'].includes(key)) {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 'u') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 's') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 'a') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 'p') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 'f') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.altKey && e.key === 'Tab') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key === 'Tab') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.key.toLowerCase() === 'w') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if ((e.ctrlKey && e.key.toLowerCase() === 'r') || e.key === 'F5') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'r') {
|
e.preventDefault()
|
e.stopPropagation()
|
return false
|
}
|
}
|
document.addEventListener('keydown', handleKeyDown, true)
|
const handleSelectStart = (e) => {
|
if (isLock.value) {
|
e.preventDefault()
|
return false
|
}
|
}
|
document.addEventListener('selectstart', handleSelectStart, true)
|
const handleDragStart = (e) => {
|
if (isLock.value) {
|
e.preventDefault()
|
return false
|
}
|
}
|
document.addEventListener('dragstart', handleDragStart, true)
|
let devtools = { open: false }
|
const threshold = 160
|
let devToolsInterval = null
|
const checkDevTools = () => {
|
if (!isLock.value || isMobile()) return
|
const isDevToolsOpen =
|
window.outerHeight - window.innerHeight > threshold ||
|
window.outerWidth - window.innerWidth > threshold
|
if (isDevToolsOpen && !devtools.open) {
|
devtools.open = true
|
showDevToolsWarning.value = true
|
} else if (!isDevToolsOpen && devtools.open) {
|
devtools.open = false
|
showDevToolsWarning.value = false
|
}
|
}
|
if (!isMobile()) {
|
devToolsInterval = setInterval(checkDevTools, 500)
|
}
|
return () => {
|
document.removeEventListener('contextmenu', handleContextMenu, true)
|
document.removeEventListener('keydown', handleKeyDown, true)
|
document.removeEventListener('selectstart', handleSelectStart, true)
|
document.removeEventListener('dragstart', handleDragStart, true)
|
if (devToolsInterval) {
|
clearInterval(devToolsInterval)
|
}
|
}
|
}
|
const verifyPassword = (inputPassword, storedPassword) => {
|
try {
|
const decryptedPassword = CryptoJS.AES.decrypt(storedPassword, ENCRYPT_KEY).toString(
|
CryptoJS.enc.Utf8
|
)
|
return inputPassword === decryptedPassword
|
} catch (error) {
|
console.error('密码解密失败:', error)
|
return false
|
}
|
}
|
const handleKeydown = (event) => {
|
if (event.altKey && event.key.toLowerCase() === '¬') {
|
event.preventDefault()
|
visible.value = true
|
}
|
}
|
const handleDialogOpen = () => {
|
setTimeout(() => {
|
lockInputRef.value?.input?.focus()
|
}, 100)
|
}
|
const handleLock = async () => {
|
if (!formRef.value) return
|
await formRef.value.validate((valid, fields) => {
|
if (valid) {
|
const encryptedPassword = CryptoJS.AES.encrypt(formData.password, ENCRYPT_KEY).toString()
|
userStore.setLockStatus(true)
|
userStore.setLockPassword(encryptedPassword)
|
visible.value = false
|
formData.password = ''
|
} else {
|
console.error('表单验证失败:', fields)
|
}
|
})
|
}
|
const handleUnlock = async () => {
|
if (!unlockFormRef.value) return
|
await unlockFormRef.value.validate((valid, fields) => {
|
if (valid) {
|
const isValid = verifyPassword(unlockForm.password, lockPassword.value)
|
if (isValid) {
|
try {
|
userStore.setLockStatus(false)
|
userStore.setLockPassword('')
|
unlockForm.password = ''
|
visible.value = false
|
showDevToolsWarning.value = false
|
} catch (error) {
|
console.error('更新store失败:', error)
|
}
|
} else {
|
const inputElement = unlockInputRef.value?.$el
|
if (inputElement) {
|
inputElement.classList.add('shake-animation')
|
setTimeout(() => {
|
inputElement.classList.remove('shake-animation')
|
}, 300)
|
}
|
ElMessage.error(t('lockScreen.pwdError'))
|
unlockForm.password = ''
|
}
|
} else {
|
console.error('表单验证失败:', fields)
|
}
|
})
|
}
|
const toLogin = () => {
|
userStore.logOut()
|
}
|
const openLockScreen = () => {
|
visible.value = true
|
}
|
watch(isLock, (newValue) => {
|
if (newValue) {
|
document.body.style.overflow = 'hidden'
|
setTimeout(() => {
|
unlockInputRef.value?.input?.focus()
|
}, 100)
|
} else {
|
document.body.style.overflow = 'auto'
|
showDevToolsWarning.value = false
|
}
|
})
|
let cleanupDevTools = null
|
onMounted(() => {
|
mittBus.on('openLockScreen', openLockScreen)
|
document.addEventListener('keydown', handleKeydown)
|
if (isLock.value) {
|
visible.value = true
|
setTimeout(() => {
|
unlockInputRef.value?.input?.focus()
|
}, 100)
|
}
|
cleanupDevTools = disableDevTools()
|
})
|
onUnmounted(() => {
|
document.removeEventListener('keydown', handleKeydown)
|
document.body.style.overflow = 'auto'
|
if (cleanupDevTools) {
|
cleanupDevTools()
|
cleanupDevTools = null
|
}
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.layout-lock-screen :deep(.el-dialog) {
|
border-radius: 10px;
|
}
|
|
.unlock-content {
|
position: fixed;
|
inset: 0;
|
z-index: 2500;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
overflow: hidden;
|
background-color: #fff;
|
background-image: url('@imgs/lock/bg_light.webp');
|
background-size: cover;
|
transition: transform 0.3s ease-in-out;
|
}
|
|
.dark {
|
.unlock-content {
|
background-image: url('@imgs/lock/bg_dark.webp');
|
}
|
}
|
|
@keyframes fade-in {
|
from {
|
opacity: 0;
|
transform: scale(0.9);
|
}
|
|
to {
|
opacity: 1;
|
transform: scale(1);
|
}
|
}
|
|
.animate-fade-in {
|
animation: fade-in 0.3s ease-in-out;
|
}
|
|
@keyframes shake {
|
0%,
|
100% {
|
transform: translateX(0);
|
}
|
|
10%,
|
30%,
|
50%,
|
70%,
|
90% {
|
transform: translateX(-10px);
|
}
|
|
20%,
|
40%,
|
60%,
|
80% {
|
transform: translateX(10px);
|
}
|
}
|
|
.shake-animation {
|
animation: shake 0.5s ease-in-out;
|
}
|
</style>
|