feat(view): 新增锁屏功能

This commit is contained in:
nongyehong 2024-07-09 02:04:55 +08:00
parent 71a1dd9383
commit 14073438d5
22 changed files with 460 additions and 51 deletions

View File

@ -1,7 +1,7 @@
# 后端服务地址 # 后端服务地址
VITE_SERVICE_URL="http://127.0.0.1:9190" VITE_SERVICE_URL="http://192.168.10.5:9190"
# websocket服务地址 # websocket服务地址
VITE_WEBSOCKET_URL="ws://127.0.0.1:8090" VITE_WEBSOCKET_URL="ws://192.168.10.5:8090"
# 项目标题 # 项目标题
VITE_APP_TITLE="HuLa—IM" VITE_APP_TITLE="HuLa—IM"
# 标签后缀 # 标签后缀

View File

@ -1,4 +1,4 @@
# [1.6.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.4.0...v1.6.0) (2024-07-03) # [1.6.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.5.0...v1.6.0) (2024-07-03)
### Bug Fixes ### Bug Fixes
@ -10,6 +10,7 @@
### Features ### Features
* :sparkles: 发布v1.6.0版本 ([71a1dd9](https://github.com/nongyehong/HuLa-IM-Tauri/commit/71a1dd93833d4c9534945f28fe636115ef59e862))
* **component:** :sparkles: 新增GPT欢迎页面完善设置页面 ([9b771e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9b771e02ec31af1238f9662e839df6197f501376)) * **component:** :sparkles: 新增GPT欢迎页面完善设置页面 ([9b771e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9b771e02ec31af1238f9662e839df6197f501376))
* **component:** :sparkles: 新增GPT页面设置功能 ([4c85b4a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4c85b4afccdafe83aa0fcbd53e94ef5fc63a7a70)) * **component:** :sparkles: 新增GPT页面设置功能 ([4c85b4a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4c85b4afccdafe83aa0fcbd53e94ef5fc63a7a70))
* **component:** :sparkles: 新增GPT组件 ([7260840](https://github.com/nongyehong/HuLa-IM-Tauri/commit/7260840f4b50bcbb4dad8645a84ade8280de4036)) * **component:** :sparkles: 新增GPT组件 ([7260840](https://github.com/nongyehong/HuLa-IM-Tauri/commit/7260840f4b50bcbb4dad8645a84ade8280de4036))

View File

@ -49,6 +49,10 @@ HuLa is an instant messaging system developed with Tauri, Vite 5, Vue 3, and Typ
![img_4.png](preview/img_4.png) ![img_4.png](preview/img_4.png)
![img_5.png](preview/img_5.png)
![img_6.png](preview/img_6.png)
HuLa adopts a modular architecture design, with the front end built using Vue 3 for the user interface, enhanced by TypeScript for better code readability and maintainability. On the backend, we use the Tauri framework for packaging and distributing the application, leveraging its native integration with the operating system to offer users more functionality and higher performance. HuLa adopts a modular architecture design, with the front end built using Vue 3 for the user interface, enhanced by TypeScript for better code readability and maintainability. On the backend, we use the Tauri framework for packaging and distributing the application, leveraging its native integration with the operating system to offer users more functionality and higher performance.
## Installation and Running ## Installation and Running
@ -73,4 +77,4 @@ pnpm run tauri:build
``` ```
## SubmissionSpecification ## SubmissionSpecification
use **pnpm run commit** to invoke the _git commit_ interaction and follow the prompts to complete the input and selection of information use **pnpm run commit** to invoke the _git commit_ interaction and follow the prompts to complete the input and selection of information

View File

@ -49,6 +49,10 @@ HuLa 是一个基于 Tauri、Vite 5、Vue 3 和 TypeScript 构建的即时通讯
![img_4.png](preview/img_4.png) ![img_4.png](preview/img_4.png)
![img_5.png](preview/img_5.png)
![img_6.png](preview/img_6.png)
## 安装与运行 ## 安装与运行
```bash ```bash
@ -71,4 +75,4 @@ pnpm run tauri:build
``` ```
## 提交规范 ## 提交规范
执行 **pnpm run commit** 唤起 _git commit_ 交互,根据提示完成信息的输入和选择 执行 **pnpm run commit** 唤起 _git commit_ 交互,根据提示完成信息的输入和选择

View File

@ -7,7 +7,7 @@
<title>HuLa</title> <title>HuLa</title>
<!--引入iconpark图标库--> <!--引入iconpark图标库-->
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_97.8e01ce86874358e3d6e87829c9ed23c6.js"></script> <script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_99.048ec543532d88124abbf0d8647e30c9.js"></script>
</head> </head>
<body> <body>

BIN
preview/img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

BIN
preview/img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

@ -34,7 +34,7 @@
"http": { "http": {
"all": true, "all": true,
"request": true, "request": true,
"scope": ["http://127.0.0.1:9190/*", "https://**"] "scope": ["http://192.168.10.5:9190/*", "http://192.168.10.5:8090/*"]
}, },
"fs": { "fs": {
"all": true, "all": true,

View File

@ -1,8 +1,11 @@
<template> <template>
<NaiveProvider :message-max="3" :notific-max="3"> <NaiveProvider :message-max="3" :notific-max="3">
<div id="app"> <div v-if="!isLock" id="app">
<router-view /> <router-view />
</div> </div>
<!-- 锁屏页面 -->
<LockScreen v-else />
</NaiveProvider> </NaiveProvider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -10,10 +13,17 @@ import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { StoresEnum, ThemeEnum } from '@/enums' import { StoresEnum, ThemeEnum } from '@/enums'
import { onlineStatus } from '@/stores/onlineStatus.ts' import { onlineStatus } from '@/stores/onlineStatus.ts'
import LockScreen from '@/views/LockScreen.vue'
import router from '@/router'
const settingStore = setting() const settingStore = setting()
const OLStatusStore = onlineStatus() const OLStatusStore = onlineStatus()
const { themes } = storeToRefs(settingStore) const { themes, lockScreen } = storeToRefs(settingStore)
/** 不需要锁屏的页面 */
const LockExclusion = new Set(['/login', '/tray', '/qrCode', '/about', '/onlineStatus'])
const isLock = computed(() => {
return !LockExclusion.has(router.currentRoute.value.path) && lockScreen.value.enable
})
/** 禁止图片以及输入框的拖拽 */ /** 禁止图片以及输入框的拖拽 */
const preventDrag = (e: MouseEvent) => { const preventDrag = (e: MouseEvent) => {

View File

@ -43,33 +43,37 @@ export enum EventEnum {
/** 独立窗口 */ /** 独立窗口 */
ALONE = 'alone', ALONE = 'alone',
/** 共享屏幕 */ /** 共享屏幕 */
SHARE_SCREEN = 'shareScreen' SHARE_SCREEN = 'shareScreen',
/** 锁屏 */
LOCK_SCREEN = 'lockScreen'
} }
/** Mitt兄弟组件通信 */ /** Mitt兄弟组件通信 */
export enum MittEnum { export enum MittEnum {
/** 更新消息数量 */ /** 更新消息数量 */
UPDATE_MSG_TOTAL = 'updateMsgTotal', UPDATE_MSG_TOTAL,
/** 显示消息框 */ /** 显示消息框 */
MSG_BOX_SHOW = 'msgBoxShow', MSG_BOX_SHOW,
/** 发送消息 */ /** 发送消息 */
SEND_MESSAGE = 'sendMessage', SEND_MESSAGE,
/** 跳到发送信息 */ /** 跳到发送信息 */
TO_SEND_MSG = 'toSendMsg', TO_SEND_MSG,
/** 缩小窗口 */ /** 缩小窗口 */
SHRINK_WINDOW = 'shrinkWindow', SHRINK_WINDOW,
/** 详情页面显示 */ /** 详情页面显示 */
DETAILS_SHOW = 'detailsShow', DETAILS_SHOW,
/** 消息列表被清空或者暂无消息 */ /** 消息列表被清空或者暂无消息 */
NOT_MSG = 'notMsg', NOT_MSG,
/** 回复消息 */ /** 回复消息 */
REPLY_MEG = 'replyMeg', REPLY_MEG,
/** 手动触发InfoPopover */ /** 手动触发InfoPopover */
INFO_POPOVER = 'infoPopover', INFO_POPOVER,
/** 打开个人信息编辑窗口 */ /** 打开个人信息编辑窗口 */
OPEN_EDIT_INFO = 'openEditInfo', OPEN_EDIT_INFO,
/** 关闭个人信息浮窗 */ /** 关闭个人信息浮窗 */
CLOSE_INFO_SHOW = 'closeInfoShow' CLOSE_INFO_SHOW,
/** 左边菜单弹窗 */
LEFT_MODAL_SHOW
} }
/** 主题类型 */ /** 主题类型 */
@ -228,3 +232,11 @@ export enum WsResEnum {
/** ws连接错误 */ /** ws连接错误 */
WS_ERROR = 'wsError' WS_ERROR = 'wsError'
} }
/** 左边菜单弹出框类型 */
export enum ModalEnum {
/** 锁屏弹窗 */
LOCK_SCREEN,
/** 检查更新弹窗 */
CHECK_UPDATE
}

View File

@ -1,5 +1,9 @@
import { emit } from '@tauri-apps/api/event' import { emit } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import axios from 'axios'
import { delay } from 'lodash-es'
import { EventEnum } from '@/enums'
import { useWindow } from '@/hooks/useWindow.ts'
export const useLogin = () => { export const useLogin = () => {
/** /**
@ -12,7 +16,32 @@ export const useLogin = () => {
}) })
} }
/** 登出账号 */
const logout = async () => {
const { createWebviewWindow } = useWindow()
localStorage.removeItem('USER_INFO')
localStorage.removeItem('TOKEN')
// 清空axios请求头
const instance = axios.create()
instance.defaults.headers.common.Authorization = ''
// todo 退出账号 需要关闭其他的全部窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
/** 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
delay(async () => {
/** 如果图标在闪烁则先暂停闪烁 */
await invoke('tray_blink', { isRun: false }).catch((error) => {
console.error('暂停闪烁失败:', error)
})
/** 通知全部打开的窗口然后关闭 */
await emit(EventEnum.LOGOUT)
await emit('logout_success')
}, 300)
})
}
return { return {
setLoginState setLoginState,
logout
} }
} }

View File

@ -112,7 +112,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { itemsBottom, itemsTop, moreList } from '../config.ts' import { itemsBottom, itemsTop, moreList } from '../config.tsx'
import { leftHook } from '../hook.ts' import { leftHook } from '../hook.ts'
const dotShow = ref(false) const dotShow = ref(false)

View File

@ -1,11 +1,10 @@
import { useWindow } from '@/hooks/useWindow.ts' import { useWindow } from '@/hooks/useWindow.ts'
import { emit } from '@tauri-apps/api/event' import { MittEnum, ModalEnum } from '@/enums'
import { EventEnum } from '@/enums' import Mitt from '@/utils/Bus.ts'
import { delay } from 'lodash-es' import { useLogin } from '@/hooks/useLogin.ts'
import { invoke } from '@tauri-apps/api/tauri'
import axios from 'axios'
const { createWebviewWindow } = useWindow() const { createWebviewWindow } = useWindow()
const { logout } = useLogin()
/** /**
* *
* @param url * @param url
@ -109,8 +108,14 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '检查更新', label: '检查更新',
icon: 'arrow-circle-up', icon: 'arrow-circle-up',
click: () => { click: () => {
// todo 检查更新 Mitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.CHECK_UPDATE)
console.log('检查更新') }
},
{
label: '锁定屏幕',
icon: 'lock',
click: () => {
Mitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.LOCK_SCREEN)
} }
}, },
{ {
@ -133,24 +138,7 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '退出账号', label: '退出账号',
icon: 'power', icon: 'power',
click: async () => { click: async () => {
localStorage.removeItem('USER_INFO') await logout()
localStorage.removeItem('TOKEN')
// 清空axios请求头
const instance = axios.create()
instance.defaults.headers.common.Authorization = ''
// todo 退出账号 需要关闭其他的全部窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
/** 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
delay(async () => {
/** 如果图标在闪烁则先暂停闪烁 */
await invoke('tray_blink', { isRun: false }).catch((error) => {
console.error('暂停闪烁失败:', error)
})
/** 通知全部打开的窗口然后关闭 */
await emit(EventEnum.LOGOUT)
await emit('logout_success')
}, 300)
})
} }
} }
]) ])

View File

@ -4,7 +4,7 @@ import { useUserStore } from '@/stores/user.ts'
import { useCachedStore } from '@/stores/cached.ts' import { useCachedStore } from '@/stores/cached.ts'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { onlineStatus } from '@/stores/onlineStatus.ts' import { onlineStatus } from '@/stores/onlineStatus.ts'
import { itemsTop } from '@/layout/left/config.ts' import { itemsTop } from '@/layout/left/config.tsx'
import { EventEnum, IsYetEnum, MittEnum, MsgEnum, ThemeEnum } from '@/enums' import { EventEnum, IsYetEnum, MittEnum, MsgEnum, ThemeEnum } from '@/enums'
import { BadgeType, UserInfoType } from '@/services/types.ts' import { BadgeType, UserInfoType } from '@/services/types.ts'
import { useChatStore } from '@/stores/chat.ts' import { useChatStore } from '@/stores/chat.ts'
@ -82,6 +82,8 @@ export const leftHook = () => {
return sessionList.value.reduce((total, item) => total + item.unreadCount, 0) return sessionList.value.reduce((total, item) => total + item.unreadCount, 0)
}) })
/* =================================== 方法 =============================================== */
/** 跟随系统主题模式切换主题 */ /** 跟随系统主题模式切换主题 */
const followOS = () => { const followOS = () => {
themeColor.value = prefers.matches ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)' themeColor.value = prefers.matches ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)'

View File

@ -6,12 +6,35 @@
<ActionList /> <ActionList />
<!-- 编辑资料弹窗 --> <!-- 编辑资料弹窗 -->
<InfoPopover /> <InfoPopover />
<!-- 弹出框 -->
<component :is="componentMap" />
</main> </main>
</template> </template>
<script lang="ts" setup> <script lang="tsx" setup>
import LeftAvatar from './components/LeftAvatar.vue' import LeftAvatar from './components/LeftAvatar.vue'
import ActionList from './components/ActionList.vue' import ActionList from './components/ActionList.vue'
import InfoPopover from './components/InfoPopover.vue' import InfoPopover from './components/InfoPopover.vue'
import Mitt from '@/utils/Bus.ts'
import { lock, LockScreen, CheckUpdate } from './model.tsx'
import { DefineComponent, DefineSetupFnComponent } from 'vue'
import { MittEnum, ModalEnum } from '@/enums'
const componentMap = shallowRef<DefineComponent>()
/** 弹窗组件内容映射 */
const componentMapping: Record<number, DefineSetupFnComponent<DefineComponent>> = {
[ModalEnum.LOCK_SCREEN]: LockScreen,
[ModalEnum.CHECK_UPDATE]: CheckUpdate
}
onMounted(() => {
Mitt.on(MittEnum.LEFT_MODAL_SHOW, (event) => {
componentMap.value = componentMapping[event as ModalEnum] as DefineComponent
nextTick(() => {
lock.value.modalShow = true
})
})
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

93
src/layout/left/model.tsx Normal file
View File

@ -0,0 +1,93 @@
import { NAvatar, NButton, NFlex, NFormItem, NInput, NModal, NForm } from 'naive-ui'
import { FormInst } from 'naive-ui'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
import { emit } from '@tauri-apps/api/event'
import { EventEnum } from '@/enums'
const formRef = ref<FormInst | null>()
const formValue = ref({
lockPassword: ''
})
export const lock = ref({
modalShow: false,
loading: false,
rules: {
lockPassword: {
required: true,
message: '请输入锁屏密码',
trigger: ['input', 'blur']
}
},
async handleLock() {
const settingStore = setting()
const { lockScreen } = storeToRefs(settingStore)
formRef.value?.validate((errors) => {
if (errors) return
lock.value.loading = true
lockScreen.value.password = formValue.value.lockPassword
lockScreen.value.enable = true
setTimeout(async () => {
/** 发送锁屏事件,当打开的窗口接受到后会自动锁屏 */
await emit(EventEnum.LOCK_SCREEN)
lock.value.loading = false
lock.value.modalShow = false
formValue.value.lockPassword = ''
}, 1000)
})
formRef.value?.restoreValidation()
}
})
/*============================================ model =====================================================*/
export const LockScreen = defineComponent(() => {
const settingStore = setting()
const { login } = storeToRefs(settingStore)
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
<div class="flex flex-col gap-10px p-10px select-none">
<NFlex vertical justify="center" align="center" size={20}>
<span class="text-(14px center)"></span>
<NAvatar bordered round size={80} src={login.value.accountInfo.avatar} />
<p class="text-(14px center [--text-color]) truncate w-200px">{login.value.accountInfo.name}</p>
</NFlex>
<NForm ref={formRef} model={formValue.value} rules={lock.value.rules}>
<NFormItem label-placement="left" label="锁屏密码" path={'lockPassword'} class="w-full">
<NInput
show-password-on="click"
v-model:value={formValue.value.lockPassword}
class="border-(1px solid #ccc)"
size="small"
type="password"
placeholder="请输入锁屏密码"
/>
</NFormItem>
</NForm>
<NButton loading={lock.value.loading} onClick={lock.value.handleLock} class="w-full" color="#13987f">
</NButton>
</div>
</div>
</NModal>
)
})
export const CheckUpdate = defineComponent(() => {
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
<p>123</p>
</div>
</NModal>
)
})

View File

@ -15,6 +15,10 @@ export const setting = defineStore(StoresEnum.SETTING, {
}, },
/** 是否启用ESC关闭窗口 */ /** 是否启用ESC关闭窗口 */
escClose: true, escClose: true,
lockScreen: {
enable: false,
password: ''
},
/** 系统托盘 */ /** 系统托盘 */
tips: { tips: {
type: CloseBxEnum.HIDE, type: CloseBxEnum.HIDE,

View File

@ -34,6 +34,8 @@ declare module 'vue' {
NDropdown: (typeof import('naive-ui'))['NDropdown'] NDropdown: (typeof import('naive-ui'))['NDropdown']
NEllipsis: (typeof import('naive-ui'))['NEllipsis'] NEllipsis: (typeof import('naive-ui'))['NEllipsis']
NFlex: (typeof import('naive-ui'))['NFlex'] NFlex: (typeof import('naive-ui'))['NFlex']
NForm: (typeof import('naive-ui'))['NForm']
NFormItem: (typeof import('naive-ui'))['NFormItem']
NIcon: (typeof import('naive-ui'))['NIcon'] NIcon: (typeof import('naive-ui'))['NIcon']
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper'] NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
NImage: (typeof import('naive-ui'))['NImage'] NImage: (typeof import('naive-ui'))['NImage']

View File

@ -15,6 +15,13 @@ declare namespace STO {
} }
/** 是否启用ESC关闭窗口 */ /** 是否启用ESC关闭窗口 */
escClose: boolean escClose: boolean
/** 是否锁屏 */
lockScreen: {
/** 是否启用锁屏 */
enable: boolean
/** 锁屏密码 */
password: string
}
/** 登录设置 */ /** 登录设置 */
login: { login: {
autoLogin: boolean autoLogin: boolean

View File

@ -13,3 +13,8 @@ dayjs.extend(weekday)
export const handRelativeTime = (time: string) => { export const handRelativeTime = (time: string) => {
return dayjs(time).fromNow() return dayjs(time).fromNow()
} }
/** 获取指定日期的星期 */
export const getWeekday = (time: string) => {
return dayjs(time).format('ddd')
}

219
src/views/LockScreen.vue Normal file
View File

@ -0,0 +1,219 @@
<template>
<!-- 锁屏页面 -->
<div class="login-box select-none absolute top-0 left-0 w-full h-full z-9999 transition-all duration-300 ease-in-out">
<ActionBar :current-label="appWindow.label" :shrink="false" />
<Transition name="slide-fade" appear>
<!-- 壁纸界面 -->
<div v-if="!isUnlockPage" data-tauri-drag-region @click.stop="isUnlockPage = true" class="size-full">
<n-flex vertical align="center" :size="120" class="size-full mt-60px">
<n-flex vertical align="center" :size="20">
<p class="text-(80px #808080) font-bold">{{ currentTime }}</p>
<n-flex align="center" :size="30" class="text-(26px #909090)">
<p>{{ currentMonthAndDate }}</p>
<p>{{ currentWeekday }}</p>
</n-flex>
</n-flex>
<n-flex
vertical
justify="center"
align="center"
:size="20"
style="transition: all 0.6s ease-in-out"
class="cursor-pointer p-12px rounded-8px hover:bg-#13987f33">
<svg class="size-24px color-#808080 p-4px bg-#e3e3e3 rounded-8px"><use href="#search"></use></svg>
<p class="text-(16px #909090) w-240px line-clamp-2">
这是一个开源的即时通讯(IM)应用它采用了一些最新的前端技术包括 TauriVue3Vite5UnoCSS
TypeScript这个项目的目标是提供一个高效稳定且易于使用的即时通讯平台
</p>
</n-flex>
</n-flex>
</div>
<!-- 解锁界面 -->
<n-flex v-else data-tauri-drag-region vertical align="center" justify="center" :size="10" class="h-full">
<n-flex vertical align="center" justify="center" :size="30" class="mt--75px">
<n-avatar round bordered :size="120" :src="login.accountInfo.avatar" />
<p class="text-(18px #606060)">{{ login.accountInfo.name }}</p>
<!-- 密码输入框 -->
<n-input
v-if="!isLogining && !isWrongPassword"
ref="inputInstRef"
style="
width: 320px;
border: 2px solid rgba(104, 104, 104, 0.2);
border-bottom-color: rgba(19, 152, 127, 0.8);
"
placeholder="锁屏密码"
show-password-on="click"
type="password"
@keyup.enter.prevent="unlock"
v-model:value="password">
<template #suffix>
<n-popover trigger="hover">
<template #trigger>
<svg
@click.stop="unlock"
class="size-16px mr-6px p-[4px_6px] rounded-8px cursor-pointer transition-all duration-300 ease-in-out hover:(bg-#13987fe6 color-#e3e3e3)">
<use href="#arrow-right"></use>
</svg>
</template>
<p>进入系统</p>
</n-popover>
</template>
</n-input>
<!-- 登录时显示的文字 -->
<n-flex vertical align="center" justify="center" :size="30" v-if="isLogining && !isWrongPassword">
<img class="size-32px" src="@/assets/img/loading-one.svg" alt="" />
<p class="text-(18px #404040)">登录中...</p>
</n-flex>
<!-- 密码不正常时显示 -->
<n-flex v-if="isWrongPassword" vertical justify="center" align="center" :size="30">
<p class="text-(16px #707070)">密码不正确, 请再试一次</p>
<n-button @click="init" secondary class="w-120px"> 确定 </n-button>
</n-flex>
</n-flex>
<n-flex v-if="!isLogining && !isWrongPassword" justify="space-between" align="center" :size="0" class="options">
<n-button quaternary color="#606060" @click="isUnlockPage = false">返回</n-button>
<n-button @click="logout" quaternary color="#707070">退出登录</n-button>
<n-button quaternary color="#707070">忘记密码</n-button>
<n-button @click="unlock" quaternary type="primary">进入系统</n-button>
</n-flex>
</n-flex>
</Transition>
</div>
</template>
<script setup lang="ts">
import { appWindow } from '@tauri-apps/api/window'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
import { useLogin } from '@/hooks/useLogin.ts'
import { onKeyStroke } from '@vueuse/core'
import { InputInst } from 'naive-ui'
import { getWeekday } from '@/utils/Day.ts'
import dayjs from 'dayjs'
const settingStore = setting()
const { lockScreen, login } = storeToRefs(settingStore)
const { logout } = useLogin()
/** 解锁密码 */
const password = ref('')
/** 是否是解锁页面 */
const isUnlockPage = ref(false)
/** 是否是登录中 */
const isLogining = ref(false)
/** 密码不正确时显示 */
const isWrongPassword = ref(false)
/** 当前时间 */
const currentTime = ref(dayjs().format('HH:mm'))
/** 当前月份和日期 */
const currentMonthAndDate = ref(dayjs().format('MM/DD'))
// const currentMonthAndDate = ref(new Date().toLocaleDateString('chinese', { month: 'long', day: 'numeric' }))
/** 当前星期 */
const currentWeekday = ref(getWeekday(new Date().toLocaleString()))
/** 计算当前时间的定时器 */
let intervalId: NodeJS.Timeout | null = null
/** 密码输入框实例 */
const inputInstRef = ref<InputInst | null>(null)
watch(isUnlockPage, (val) => {
if (val) {
/** 延迟 300ms 后自动获取焦点,不然会触发一次回车事件 */
setTimeout(() => {
inputInstRef.value?.focus()
}, 300)
}
})
watch(isWrongPassword, (val) => {
if (val) {
onKeyStroke('Enter', (e) => {
e.preventDefault()
init()
})
}
})
const unlock = () => {
if (password.value === '') {
window.$message.error('请输入密码')
} else {
isLogining.value = true
if (password.value === lockScreen.value.password) {
setTimeout(() => {
lockScreen.value.enable = false
isLogining.value = false
}, 1000)
} else {
setTimeout(() => {
isWrongPassword.value = true
}, 300)
}
}
}
/** 重置登录状态 */
const init = () => {
if (isWrongPassword.value) {
isWrongPassword.value = false
isLogining.value = false
setTimeout(() => {
inputInstRef.value?.focus()
}, 600)
password.value = ''
}
}
onMounted(() => {
console.log(currentWeekday.value)
intervalId = setInterval(() => {
currentTime.value = dayjs().format('HH:mm')
currentMonthAndDate.value = dayjs().format('MM/DD')
currentWeekday.value = getWeekday(new Date().toLocaleString())
}, 1000)
if (!isUnlockPage.value) {
onKeyStroke('Enter', (e) => {
e.preventDefault()
isUnlockPage.value = true
})
}
})
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
</script>
<style scoped lang="scss">
@import '@/styles/scss/global/login-bg';
.options {
@apply w-320px;
p {
@apply text-(14px [--chat-text-color]) cursor-pointer select-none p-8px bg-#ccc rounded-lg;
}
}
/*
进入和离开动画可以使用不同
持续时间和速度曲线
*/
.slide-fade-enter-active {
transition: all 0.2s ease-out;
}
.slide-fade-leave-active {
transition: all 0.6s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(20px);
opacity: 0;
}
</style>

View File

@ -35,7 +35,7 @@
</n-flex> </n-flex>
<component :is="division" /> <component :is="division" />
<n-flex @click="exit(0)" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]"> <n-flex @click="handleExit" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]">
<span>退出</span> <span>退出</span>
</n-flex> </n-flex>
</n-flex> </n-flex>
@ -56,9 +56,13 @@ import { onlineStatus } from '@/stores/onlineStatus.ts'
import { appWindow } from '@tauri-apps/api/window' import { appWindow } from '@tauri-apps/api/window'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { useWsLoginStore } from '@/stores/ws.ts' import { useWsLoginStore } from '@/stores/ws.ts'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
const { checkWinExist, createWebviewWindow, resizeWindow } = useWindow() const { checkWinExist, createWebviewWindow, resizeWindow } = useWindow()
const OLStatusStore = onlineStatus() const OLStatusStore = onlineStatus()
const settingStore = setting()
const { lockScreen } = storeToRefs(settingStore)
const isLoginWin = ref(true) const isLoginWin = ref(true)
const loginStore = useWsLoginStore() const loginStore = useWsLoginStore()
const loginQrCode = computed(() => loginStore.loginQrCode) const loginQrCode = computed(() => loginStore.loginQrCode)
@ -68,6 +72,8 @@ const division = () => {
} }
const handleExit = () => { const handleExit = () => {
/** 退出时关闭锁屏 */
lockScreen.value.enable = false
exit(0) exit(0)
if (loginQrCode.value) { if (loginQrCode.value) {
localStorage.removeItem('wsLogin') localStorage.removeItem('wsLogin')