|
|
<script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' import { getUserList } from '@/services/Index/user-manage' import type { User } from '@/types/Index' import { createWebSocket, DeviceContextStateMessage } from '@/websocket/socket.ts' import { getServerInfo } from '@/utils/getServerInfo.ts' import router from '@/router/router.ts' import { isBoardParamInited, login } from '@/services' import circleUrl from '@/assets/avatar.png' import { eMessage } from '@/pages/Index/utils'
const stateUrl = getServerInfo('/api/v1/app/ws/state') const wsState = createWebSocket(stateUrl.wsUrl)
const handleDeviceContextState = (data: DeviceContextStateMessage['data']) => { if (data.loginFlag) { sessionStorage.setItem('token', JSON.stringify(data.loginUser)) router.push('/index') } }
//用户列表
const userList = ref<User[]>([]) //获取用户列表
const getUserListData = async () => { const res = await getUserList() userList.value = res.data }
onMounted(() => { wsState.subscribe<DeviceContextStateMessage>( 'DeviceContext', handleDeviceContextState, ) wsState.connect() getUserListData() })
onUnmounted(() => { wsState.unsubscribe<DeviceContextStateMessage>( 'DeviceContext', handleDeviceContextState, ) })
const activeUser = ref<User>()
const password = ref('')
const inputPin = (pin: string) => { if (password.value.length >= 4) { return } if (password.value.length < 4) { password.value += pin } if (password.value.length === 4) { // 输入完成,执行登录操作
submitPin() } }
const passwordError = ref(false)
const submitPin = async () => { let resData = await isBoardParamInited() if (!resData.data) { //设备正在初始化
eMessage.error('设备正在初始化,请稍候重试') return } const params = { id: activeUser.value?.id, password: password.value, } const res = await login(params) if (res.success) { sessionStorage.setItem('token', JSON.stringify(res.data)) await router.push('/index') } else { passwordError.value = true // 触发抖动动画
// 动画结束后重置状态(2次抖动共0.6秒)
setTimeout(() => { passwordError.value = false }, 600) password.value = '' } }
const showPassword = ref(false) const next = () => { if (activeUser.value?.id) { showPassword.value = true }
} const back = () => { showPassword.value = false }
const version = __APP_VERSION__ </script> <template> <div class="login-box"> <transition name="flip-card" mode="out-in"> <div class="user-box" v-if="!showPassword"> <p class="title">选择用户</p> <div class="user-list"> <div class="user-info" v-for="user in userList" :key="user.id" @click="activeUser = user" :class="{ 'user-info-active': activeUser?.id === user.id }"> <el-avatar :size="60" :src="circleUrl" /> <span class="name">{{user.account}}</span> <el-icon v-show="activeUser?.id === user.id" color="#fff"><Select /></el-icon> </div> </div> <div class="next-box" :class="{'next-box-active': activeUser?.id}" @click="next"> <el-icon><Right /></el-icon> </div> </div>
<div class="password-box" v-else> <div class="user-info user-info-active"> <el-avatar :size="40" :src="circleUrl" /> <span class="name">{{activeUser!.account}}</span> </div> <p class="title">请输入4位PIN码</p> <div class="password-list"> <div v-for="i in 4" :key="i" class="password-item" :class="{'password-item-fill': i <= password.length, 'password-error': passwordError}"></div> </div> <div class="pin-keypad"> <div v-for="n in 9" :key="n" class="key" @click="inputPin(n.toString())" > {{ n }} </div> <div></div> <div class="key" @click="inputPin('0')">0</div> </div> <div class="next-box next-box-active" @click="back"> <el-icon><Back /></el-icon> </div> </div> </transition> <div class="version">版本号:v {{version}}</div> </div>
</template> <style lang="less" scoped> @keyframes iconAnim { 0% { transform: scale(0.8); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
@keyframes shake { 0%, 100% { transform: translateX(0); opacity: 1; } 25% { transform: translateX(-10px); opacity: 0.7; } 50% { transform: translateX(10px); opacity: 0.7; } 75% { transform: translateX(-7px); opacity: 0.7; } } .login-title { position: absolute; top: 10%; font-size: 50px; font-weight: bold; } .login-box { perspective: 1000px; width: 100%; height: 100%; background: #A0CEF2; display: flex; align-items: center; justify-content: center; position: relative; .user-box { width: 50%; height: 50%; background: #fff; border-radius: 30px; padding: 40px 30px; display: flex; flex-direction: column; align-items: center; .user-list { width: 100%; flex: 1; overflow: auto; .user-info { display: flex; align-items: center; padding: 20px; border-radius: 20px; .name { margin-left: 30px; font-size: 25px; font-weight: bold; } .el-icon { animation: iconAnim 0.3s ease forwards; font-size: 30px; margin-left: auto;
} } .user-info-active { background: #3E8ED1; color: #fff; } } } .password-box { width: 50%; height: 50%; background: #fff; border-radius: 30px; padding: 40px 30px; display: flex; flex-direction: column; align-items: center; justify-content: space-between; .user-info { width: 50%; padding: 10px 0; display: flex; align-items: center; justify-content: center; border-radius: 20px; background: #3E8ED1; color: #fff; .name { margin-left: 30px; font-size: 25px; font-weight: bold; } } } } .title { font-size: 30px; font-weight: bold; } .password-list { display: flex; justify-content: center; padding: 20px 0; .password-item { width: 30px; height: 30px; border-radius: 50%; background: #CDCFD4; margin: 0 10px; } .password-item-fill { background: #3E8ED1; } } .pin-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 30px;
.key { width: 100px; height: 80px; background-color: #fff; border-radius: 15px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #333; cursor: pointer; transition: all 0.3s; border: 1px solid #E7EDF1; box-shadow: 0 3px 8px #E7EDF1;
&:active { transform: scale(0.95); } } } .error-box { flex: 1; display: flex; justify-content: center; align-items: center; font-size: 20px; color: #ff0000; } .password-error { animation: shake 0.3s ease-in-out 2; } .next-box { width: 80px; height: 80px; border-radius: 50%; background: #ddd; display: flex; justify-content: center; align-items: center; margin: 20px auto; .el-icon { font-size: 30px; color: #bbb } } .next-box-active { background: #3E8ED1; .el-icon { color: #fff; } }
.flip-card-leave-active, .flip-card-enter-active { transition: all 0.5s ease; backface-visibility: hidden; /* 隐藏背面 */ }
.flip-card-enter-from, .flip-card-leave-to { transform: rotateY(90deg); opacity: 0; }
.flip-card-enter-active .user-box, .flip-card-leave-active .user-box, .flip-card-enter-active .password-box, .flip-card-leave-active .password-box { position: absolute; backface-visibility: hidden; transition: transform 0.5s ease; }
.flip-card-enter .user-box { transform: rotateY(-90deg); }
.flip-card-leave-to .user-box { transform: rotateY(90deg); }
.flip-card-enter .password-box { transform: rotateY(90deg); }
.flip-card-leave-to .password-box { transform: rotateY(-90deg); } .version { position: absolute; bottom: 10px; color: rgba(0,0,0,0.25); } </style>
|