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'; |
|||
|
|||
function createIconifyIcon(icon: string) { |
|||
return h(Icon, { icon }); |
|||
return defineComponent({ |
|||
name: `SvgIcon-${icon}`, |
|||
setup(props, { attrs }) { |
|||
return () => h(Icon, { icon, ...props, ...attrs }); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
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