feat: 新增锁屏功能

This commit is contained in:
WuYihui 2023-05-10 10:12:31 +08:00
parent 46ac7f88c9
commit e2fd349070
11 changed files with 570 additions and 0 deletions

View File

@ -33,6 +33,7 @@
"@zxcvbn-ts/core": "^3.0.0", "@zxcvbn-ts/core": "^3.0.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.3.4", "element-plus": "2.3.4",
@ -42,6 +43,7 @@
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.36", "pinia": "^2.0.36",
"pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.1", "qs": "^6.11.1",
"url": "^0.11.0", "url": "^0.11.0",

View File

@ -7,6 +7,14 @@ import { useRouter } from 'vue-router'
import { loginOutApi } from '@/api/login' import { loginOutApi } from '@/api/login'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import LockDialog from './components/LockDialog.vue'
import { ref, computed } from 'vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock'
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const tagsViewStore = useTagsViewStore() const tagsViewStore = useTagsViewStore()
@ -38,6 +46,13 @@ const loginOut = () => {
.catch(() => {}) .catch(() => {})
} }
const dialogVisible = ref<boolean>(false)
//
const lockScreen = () => {
dialogVisible.value = true
}
const toDocument = () => { const toDocument = () => {
window.open('https://element-plus-admin-doc.cn/') window.open('https://element-plus-admin-doc.cn/')
} }
@ -59,9 +74,36 @@ const toDocument = () => {
<div @click="toDocument">{{ t('common.document') }}</div> <div @click="toDocument">{{ t('common.document') }}</div>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem divided> <ElDropdownItem divided>
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem>
<div @click="loginOut">{{ t('common.loginOut') }}</div> <div @click="loginOut">{{ t('common.loginOut') }}</div>
</ElDropdownItem> </ElDropdownItem>
</ElDropdownMenu> </ElDropdownMenu>
</template> </template>
</ElDropdown> </ElDropdown>
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
<teleport to="body">
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</teleport>
</template> </template>
<style scoped lang="less">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s, transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'
import { ref, unref } from 'vue'
import { Dialog } from '@/components/Dialog'
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, computed } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/types/form'
import { ElButton } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
console.log('set: ', val)
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const rules = reactive({
password: [required()]
})
const schema: FormSchema[] = reactive([
{
label: t('lock.lockPassword'),
field: 'password',
component: 'Input',
componentProps: {
type: 'password',
showPassword: true
}
}
])
const { register, formRef, methods } = useForm({
schema
})
const { getFormData } = methods
const handleLock = () => {
unref(formRef)?.validate(async (valid) => {
if (valid) {
dialogVisible.value = false
const formData = await getFormData()
lockStore.setLockInfo({
isLock: true,
...formData
})
}
})
}
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">Archer</span>
</div>
<Form :is-col="false" :rules="rules" @register="register" />
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>
</Dialog>
</template>
<style lang="less" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
max-width: calc(100vw - 16px);
}
}
</style>

View File

@ -0,0 +1,270 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { ElInput, ElButton } from 'element-plus'
import { resetRouter } from '@/router'
import { useRouter } from 'vue-router'
import { useCache } from '@/hooks/web/useCache'
import { useLockStore } from '@/store/modules/lock'
import { useI18n } from '@/hooks/web/useI18n'
import { useNow } from './useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@/components/Icon'
import { loginOutApi } from '@/api/login'
import { useTagsViewStore } from '@/store/modules/tagsView'
const tagsViewStore = useTagsViewStore()
const { wsCache } = useCache()
const { replace } = useRouter()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
//
async function unLock() {
if (!password.value) {
return
}
let pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
//
async function goLogin() {
const res = await loginOutApi().catch(() => {})
if (res) {
wsCache.clear()
tagsViewStore.delAllViews()
resetRouter() //
lockStore.resetLockInfo()
replace('/login')
}
}
function handleShowForm(show = false) {
showDate.value = show
}
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">Archer</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-lock-page';
// Small screen / tablet
@screen-sm: 576px;
// Medium screen / desktop
@screen-md: 768px;
// Large screen / wide desktop
@screen-lg: 992px;
// Extra large screen / full hd
@screen-xl: 1200px;
// Extra extra large screen / large desktop
@screen-2xl: 1600px;
@error-color: #ed6f6f;
.@{prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: @screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: @screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: @screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: @screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: @error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@ -0,0 +1,60 @@
import { dateUtil } from '@/utils/dateUtil'
import { reactive, toRefs } from 'vue'
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
export function useNow(immediate = true) {
let timer: IntervalHandle
const state = reactive({
year: 0,
month: 0,
week: '',
day: 0,
hour: '',
minute: '',
second: 0,
meridiem: ''
})
const update = () => {
const now = dateUtil()
const h = now.format('HH')
const m = now.format('mm')
const s = now.get('s')
state.year = now.get('y')
state.month = now.get('M') + 1
state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
state.day = now.get('date')
state.hour = h
state.minute = m
state.second = s
state.meridiem = now.format('A')
}
function start() {
update()
clearInterval(timer)
timer = setInterval(() => update(), 1000)
}
function stop() {
clearInterval(timer)
}
tryOnMounted(() => {
immediate && start()
})
tryOnUnmounted(() => {
stop()
})
return {
...toRefs(state),
start,
stop
}
}

View File

@ -42,6 +42,16 @@ export default {
delNoData: 'Please select the data to delete', delNoData: 'Please select the data to delete',
delSuccess: 'Deleted successfully' delSuccess: 'Deleted successfully'
}, },
lock: {
lockScreen: 'Lock screen',
lock: 'Lock',
lockPassword: 'Lock screen password',
unlock: 'Click to unlock',
backToLogin: 'Back to login',
entrySystem: 'Entry the system',
placeholder: 'Please enter the lock screen password',
message: 'Lock screen password error'
},
error: { error: {
noPermission: `Sorry, you don't have permission to access this page.`, noPermission: `Sorry, you don't have permission to access this page.`,
pageError: 'Sorry, the page you visited does not exist.', pageError: 'Sorry, the page you visited does not exist.',

View File

@ -42,6 +42,16 @@ export default {
delNoData: '请选择需要删除的数据', delNoData: '请选择需要删除的数据',
delSuccess: '删除成功' delSuccess: '删除成功'
}, },
lock: {
lockScreen: '锁定屏幕',
lock: '锁定',
lockPassword: '锁屏密码',
unlock: '点击解锁',
backToLogin: '返回登录',
entrySystem: '进入系统',
placeholder: '请输入锁屏密码',
message: '锁屏密码错误'
},
error: { error: {
noPermission: `抱歉,您无权访问此页面。`, noPermission: `抱歉,您无权访问此页面。`,
pageError: '抱歉,您访问的页面不存在。', pageError: '抱歉,您访问的页面不存在。',

View File

@ -1,8 +1,11 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const store = createPinia() const store = createPinia()
store.use(piniaPersist)
export const setupStore = (app: App<Element>) => { export const setupStore = (app: App<Element>) => {
app.use(store) app.use(store)
} }

51
src/store/modules/lock.ts Normal file
View File

@ -0,0 +1,51 @@
import { defineStore } from 'pinia'
import { store } from '../index'
interface lockInfo {
isLock?: boolean
password?: string
}
interface LockState {
lockInfo: lockInfo
}
export const useLockStore = defineStore('lock', {
state: (): LockState => {
return {
lockInfo: {
// isLock: false, // 是否锁定屏幕
// password: '' // 锁屏密码
}
}
},
getters: {
getLockInfo(): lockInfo {
return this.lockInfo
}
},
actions: {
setLockInfo(lockInfo: lockInfo) {
this.lockInfo = lockInfo
},
resetLockInfo() {
this.lockInfo = {}
},
unLock(password: string) {
if (this.lockInfo?.password === password) {
this.resetLockInfo()
return true
} else {
return false
}
}
},
persist: {
enabled: true,
strategies: [{ key: 'lock', storage: localStorage }]
}
})
export const useLockStoreWithOut = () => {
return useLockStore(store)
}

17
src/utils/dateUtil.ts Normal file
View File

@ -0,0 +1,17 @@
/**
* Independent time operation tool to facilitate subsequent switch to dayjs
*/
import dayjs from 'dayjs'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const DATE_FORMAT = 'YYYY-MM-DD'
export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
return dayjs(date).format(format)
}
export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
export const dateUtil = dayjs

3
types/global.d.ts vendored
View File

@ -14,6 +14,9 @@ declare global {
declare type LocaleType = 'zh-CN' | 'en' declare type LocaleType = 'zh-CN' | 'en'
declare type TimeoutHandle = ReturnType<typeof setTimeout>
declare type IntervalHandle = ReturnType<typeof setInterval>
declare type AxiosHeaders = declare type AxiosHeaders =
| 'application/json' | 'application/json'
| 'application/x-www-form-urlencoded' | 'application/x-www-form-urlencoded'