Browse Source
* feat: 锁屏功能 * feat: 锁屏样式调整 * feat: complete the lock-screen screen and support shortcut keys and preference configuration --------- Co-authored-by: vince <vince292007@gmail.com>pull/3993/head
committed by
GitHub
27 changed files with 482 additions and 48 deletions
@ -1,9 +1,14 @@ |
|||||
import { h } from 'vue'; |
import { defineComponent, h } from 'vue'; |
||||
|
|
||||
import { Icon } from '@iconify/vue'; |
import { Icon } from '@iconify/vue'; |
||||
|
|
||||
function createIconifyIcon(icon: string) { |
function createIconifyIcon(icon: string) { |
||||
return h(Icon, { icon }); |
return defineComponent({ |
||||
|
name: `SvgIcon-${icon}`, |
||||
|
setup(props, { attrs }) { |
||||
|
return () => h(Icon, { icon, ...props, ...attrs }); |
||||
|
}, |
||||
|
}); |
||||
} |
} |
||||
|
|
||||
export { createIconifyIcon }; |
export { createIconifyIcon }; |
||||
|
|||||
@ -0,0 +1,2 @@ |
|||||
|
export { default as LockScreen } from './lock-screen.vue'; |
||||
|
export { default as LockScreenModal } from './lock-screen-modal.vue'; |
||||
@ -0,0 +1,106 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { RegisterEmits } from './typings'; |
||||
|
|
||||
|
import { computed, reactive } from 'vue'; |
||||
|
|
||||
|
import { |
||||
|
Dialog, |
||||
|
DialogContent, |
||||
|
DialogDescription, |
||||
|
DialogHeader, |
||||
|
DialogTitle, |
||||
|
VbenAvatar, |
||||
|
VbenButton, |
||||
|
VbenInputPassword, |
||||
|
} from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
interface Props { |
||||
|
avatar?: string; |
||||
|
text?: string; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'LockScreenModal', |
||||
|
}); |
||||
|
withDefaults(defineProps<Props>(), { |
||||
|
avatar: '', |
||||
|
text: '', |
||||
|
}); |
||||
|
const emit = defineEmits<{ |
||||
|
submit: RegisterEmits['submit']; |
||||
|
}>(); |
||||
|
const formState = reactive({ |
||||
|
lockScreenPassword: '', |
||||
|
submitted: false, |
||||
|
}); |
||||
|
const openModal = defineModel<boolean>('open'); |
||||
|
const passwordStatus = computed(() => { |
||||
|
return formState.submitted && !formState.lockScreenPassword |
||||
|
? 'error' |
||||
|
: 'default'; |
||||
|
}); |
||||
|
|
||||
|
function handleClose() { |
||||
|
openModal.value = false; |
||||
|
} |
||||
|
|
||||
|
function handleSubmit() { |
||||
|
formState.submitted = true; |
||||
|
if (passwordStatus.value !== 'default') { |
||||
|
return; |
||||
|
} |
||||
|
emit('submit', { |
||||
|
lockScreenPassword: formState.lockScreenPassword, |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<Dialog :open="openModal"> |
||||
|
<DialogContent |
||||
|
class="top-0 h-full w-full -translate-y-0 border-none p-0 shadow-xl sm:top-[20%] sm:h-[unset] sm:w-[600px] sm:rounded-2xl" |
||||
|
@close="handleClose" |
||||
|
> |
||||
|
<DialogDescription /> |
||||
|
<DialogHeader> |
||||
|
<DialogTitle |
||||
|
class="border-border flex h-8 items-center px-5 font-normal" |
||||
|
> |
||||
|
{{ $t('widgets.lockScreen.title') }} |
||||
|
</DialogTitle> |
||||
|
</DialogHeader> |
||||
|
<div |
||||
|
class="mb-10 flex w-full flex-col items-center" |
||||
|
@keypress.enter.prevent="handleSubmit" |
||||
|
> |
||||
|
<div class="w-2/3"> |
||||
|
<div class="ml-2 flex w-full flex-col items-center"> |
||||
|
<VbenAvatar |
||||
|
:src="avatar" |
||||
|
class="size-24" |
||||
|
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500" |
||||
|
/> |
||||
|
<div class="text-foreground my-6 flex items-center font-medium"> |
||||
|
{{ text }} |
||||
|
</div> |
||||
|
</div> |
||||
|
<VbenInputPassword |
||||
|
v-model="formState.lockScreenPassword" |
||||
|
:error-tip="$t('widgets.lockScreen.placeholder')" |
||||
|
:label="$t('widgets.lockScreen.password')" |
||||
|
:placeholder="$t('widgets.lockScreen.placeholder')" |
||||
|
:status="passwordStatus" |
||||
|
name="password" |
||||
|
required |
||||
|
type="password" |
||||
|
/> |
||||
|
<VbenButton class="w-full" @click="handleSubmit"> |
||||
|
{{ $t('widgets.lockScreen.screenButton') }} |
||||
|
</VbenButton> |
||||
|
</div> |
||||
|
</div> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,170 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed, reactive, ref, watchEffect } from 'vue'; |
||||
|
|
||||
|
import { IcRoundLock } from '@vben-core/iconify'; |
||||
|
import { $t } from '@vben-core/locales'; |
||||
|
import { |
||||
|
VbenAvatar, |
||||
|
VbenButton, |
||||
|
VbenInputPassword, |
||||
|
} from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
import { useDateFormat, useNow } from '@vueuse/core'; |
||||
|
|
||||
|
interface Props { |
||||
|
avatar?: string; |
||||
|
cachedPassword?: string; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'LockScreen', |
||||
|
}); |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
avatar: '', |
||||
|
cachedPassword: undefined, |
||||
|
}); |
||||
|
|
||||
|
const emit = defineEmits<{ toLogin: []; unlock: [string] }>(); |
||||
|
|
||||
|
const now = useNow(); |
||||
|
const year = useDateFormat(now, 'YYYY'); |
||||
|
const month = useDateFormat(now, 'MM'); |
||||
|
const day = useDateFormat(now, 'DD'); |
||||
|
const week = useDateFormat(now, 'dddd'); |
||||
|
const hour = useDateFormat(now, 'HH'); |
||||
|
const meridiem = useDateFormat(now, 'A'); |
||||
|
const minute = useDateFormat(now, 'mm'); |
||||
|
|
||||
|
const showUnlockForm = ref(false); |
||||
|
const validPass = ref(true); |
||||
|
|
||||
|
const formState = reactive({ |
||||
|
password: '', |
||||
|
submitted: false, |
||||
|
}); |
||||
|
|
||||
|
const passwordStatus = computed(() => { |
||||
|
if (formState.submitted && !formState.password) { |
||||
|
return 'error'; |
||||
|
} |
||||
|
|
||||
|
if (formState.submitted && !validPass.value) { |
||||
|
return 'error'; |
||||
|
} |
||||
|
|
||||
|
return 'default'; |
||||
|
}); |
||||
|
|
||||
|
const errorTip = computed(() => { |
||||
|
return props.cachedPassword === undefined || !formState.password |
||||
|
? $t('widgets.lockScreen.placeholder') |
||||
|
: $t('widgets.lockScreen.errorPasswordTip'); |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (!formState.password) { |
||||
|
validPass.value = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function handleSubmit() { |
||||
|
formState.submitted = true; |
||||
|
if (passwordStatus.value !== 'default') { |
||||
|
return; |
||||
|
} |
||||
|
if (props.cachedPassword !== formState.password) { |
||||
|
validPass.value = false; |
||||
|
return; |
||||
|
} |
||||
|
emit('unlock', formState.password); |
||||
|
} |
||||
|
|
||||
|
function toggleUnlockForm() { |
||||
|
showUnlockForm.value = !showUnlockForm.value; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="bg-background fixed z-[2000] size-full"> |
||||
|
<transition name="slide-left"> |
||||
|
<div v-show="!showUnlockForm" class="size-full"> |
||||
|
<div |
||||
|
class="flex-col-center text-foreground/80 hover:text-foreground group my-4 cursor-pointer text-xl font-semibold" |
||||
|
@click="toggleUnlockForm" |
||||
|
> |
||||
|
<IcRoundLock |
||||
|
class="size-5 transition-all duration-300 group-hover:scale-125" |
||||
|
/> |
||||
|
<span>{{ $t('widgets.lockScreen.unlock') }}</span> |
||||
|
</div> |
||||
|
<div class="flex h-full justify-center px-[10%]"> |
||||
|
<div |
||||
|
class="bg-accent flex-center relative mb-14 mr-20 h-4/5 w-2/5 flex-auto rounded-3xl text-center text-[260px]" |
||||
|
> |
||||
|
<span class="absolute left-4 top-4 text-xl font-semibold">{{ |
||||
|
meridiem |
||||
|
}}</span> |
||||
|
{{ hour }} |
||||
|
</div> |
||||
|
<div |
||||
|
class="bg-accent flex-center mb-14 h-4/5 w-2/5 flex-auto rounded-3xl text-center text-[260px]" |
||||
|
> |
||||
|
{{ minute }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</transition> |
||||
|
|
||||
|
<transition name="slide-right"> |
||||
|
<div |
||||
|
v-if="showUnlockForm" |
||||
|
class="flex-center size-full" |
||||
|
@keypress.enter.prevent="handleSubmit" |
||||
|
> |
||||
|
<div class="flex-col-center mb-10 w-[300px]"> |
||||
|
<VbenAvatar :src="avatar" class="enter-x mb-6 size-20" /> |
||||
|
<div class="items-cente enter-x mb-2 w-full"> |
||||
|
<VbenInputPassword |
||||
|
v-model="formState.password" |
||||
|
:autofocus="true" |
||||
|
:error-tip="errorTip" |
||||
|
:label="$t('widgets.lockScreen.password')" |
||||
|
:placeholder="$t('widgets.lockScreen.placeholder')" |
||||
|
:status="passwordStatus" |
||||
|
name="password" |
||||
|
required |
||||
|
type="password" |
||||
|
/> |
||||
|
</div> |
||||
|
<VbenButton class="enter-x w-full" @click="handleSubmit"> |
||||
|
{{ $t('widgets.lockScreen.entry') }} |
||||
|
</VbenButton> |
||||
|
<VbenButton |
||||
|
class="enter-x my-2 w-full" |
||||
|
variant="ghost" |
||||
|
@click="$emit('toLogin')" |
||||
|
> |
||||
|
{{ $t('widgets.lockScreen.backToLogin') }} |
||||
|
</VbenButton> |
||||
|
<VbenButton |
||||
|
class="enter-x mr-2 w-full" |
||||
|
variant="ghost" |
||||
|
@click="toggleUnlockForm" |
||||
|
> |
||||
|
{{ $t('common.back') }} |
||||
|
</VbenButton> |
||||
|
</div> |
||||
|
</div> |
||||
|
</transition> |
||||
|
|
||||
|
<div |
||||
|
class="enter-y absolute bottom-5 w-full text-center text-gray-300 xl:text-xl 2xl:text-3xl" |
||||
|
> |
||||
|
<div v-if="showUnlockForm" class="enter-x mb-2 text-3xl"> |
||||
|
{{ hour }}:{{ minute }} <span class="text-lg">{{ meridiem }}</span> |
||||
|
</div> |
||||
|
<div class="text-3xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,9 @@ |
|||||
|
interface LockAndRegisterParams { |
||||
|
lockScreenPassword: string; |
||||
|
} |
||||
|
|
||||
|
interface RegisterEmits { |
||||
|
submit: [LockAndRegisterParams]; |
||||
|
} |
||||
|
|
||||
|
export type { LockAndRegisterParams, RegisterEmits }; |
||||
Loading…
Reference in new issue