|
|
<template> <div class="login-container"> <aside class="sidebar"> <ul> <li v-for="user in userList" :key="user.id" :class="[ 'user', { selected: user.account === selectedUser?.account }, ]" @click="selectUser(user)" > {{ user.account }} </li> </ul> </aside> <main class="main-content"> <div class="pin-container"> <p :class="['pin-prompt', { shake: isShaking }]">{{ loginStatus }}</p> <div class="pin-indicators"> <span v-for="n in maxPinLength" :key="n" :class="['pin-dot', { filled: n <= pin.length }]" ></span> </div> <div class="pin-keypad"> <div v-for="n in 9" :key="n" class="key" @click="inputPin(n.toString())" > {{ n }} </div> <div class="key" @click="clearPin">重输</div> <div class="key" @click="inputPin('0')">0</div> <div class="key" @click="submitPin">确定</div> </div> </div> </main> </div> </template>
<script setup lang="ts"> import { onMounted, onUnmounted, ref, watch } from 'vue' import { login, getUserList } from '../../services/index' import type { User } from '../../types/Index' import { useRouter } from 'vue-router' import { getServerInfo } from '@/utils/getServerInfo' import { createWebSocket, DeviceContextStateMessage } from '@/websocket/socket' const router = useRouter()
const stateUrl = getServerInfo('/api/v1/app/ws/state') const wsState = createWebSocket(stateUrl.wsUrl)
// 用户角色
// const roles = ref(['护士', '管理员', '测试人员'])
// const selectedRole = ref('护士')
//用户列表
const userList = ref<User[]>([]) //获取用户列表
const getUserListData = async () => { const res = await getUserList() userList.value = res.data } const handleDeviceContextState = (data: DeviceContextStateMessage['data']) => { if (data.loginFlag) { sessionStorage.setItem('token', JSON.stringify(data.loginUser)) router.push('/index') } } onMounted(() => { wsState.subscribe<DeviceContextStateMessage>( 'DeviceContext', handleDeviceContextState, ) wsState.connect()
setTimeout(() => { getUserListData() }, 500) }) onUnmounted(() => { wsState.unsubscribe<DeviceContextStateMessage>( 'DeviceContext', handleDeviceContextState, ) }) //选中的用户
const selectedUser = ref<User | null>(null) // PIN 相关
const pin = ref('') const maxPinLength = 4 const loginStatus = ref('请输入您的pin码') const isShaking = ref(false) // 控制抖动效果的触发
// 切换角色时清空PIN
const selectUser = (user: User) => { selectedUser.value = user pin.value = '' loginStatus.value = '请输入您的pin码' }
// 输入PIN码
const inputPin = (key: string) => { if (selectedUser.value === null) { loginStatus.value = '请选择用户' return } if (pin.value.length >= maxPinLength) { return } pin.value += key if (pin.value.length === maxPinLength) { return } }
// 重置PIN码
const clearPin = () => { pin.value = '' loginStatus.value = '请输入您的pin码' }
// 点击确认验证PIN码
// 模拟后端请求验证PIN
const submitPin = async () => { if (selectedUser.value === null) { loginStatus.value = '请选择用户' return } else if (pin.value.length !== maxPinLength) { loginStatus.value = '请输入合法的pin码' return } const params = { // account: selectedUser.value?.account,
id: selectedUser.value?.id, password: pin.value, } const res = await login(params) console.log(res) if (res.success) { loginStatus.value = '登录成功' sessionStorage.setItem('token', JSON.stringify(res.data)) router.push('/index') } else { loginStatus.value = `${res.info}` pin.value = '' } } // 监听 loginStatus 变化并触发抖动效果
watch(loginStatus, () => { isShaking.value = true setTimeout(() => { isShaking.value = false }, 500) // 动画持续时间为0.5秒
}) </script> <style scoped lang="less"> .login-container { display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; background: linear-gradient(to bottom, #a1d0f5, #bce0fe); // 垂直线性渐变
padding: 15px; // 减小padding
box-sizing: border-box; }
.sidebar { width: 220px; height: 80vh; background-color: #e1f0fb; border-radius: 20px; padding: 20px 10px; display: flex; flex-direction: column; align-items: center; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); margin-right: 20px;
ul { list-style: none; padding: 0; width: 100%; }
.user { padding: 12px 0; font-size: 24px; color: #666; cursor: pointer; transition: all 0.3s; border-radius: 10px; margin: 10px 0;
&:hover { background-color: #b3d9f9; color: #fff; } }
.selected { background-color: #53b6f3; color: white; font-weight: bold; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } }
.main-content { display: flex; align-items: center; justify-content: center;
.pin-container { background-color: #fff; border-radius: 20px; padding: 30px 40px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); text-align: center;
.pin-prompt { font-size: 28px; color: #333; margin-bottom: 20px; font-weight: bold;
&.shake { animation: shake 0.5s ease; color: #d9534f; // 抖动时变色
} }
.pin-indicators { display: flex; justify-content: center; margin: 15px 0;
.pin-dot { width: 25px; height: 25px; background-color: #d5e6f2; border-radius: 50%; margin: 0 10px; transition: background-color 0.3s;
&.filled { background-color: #53b6f3; } } }
.pin-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-top: 30px;
.key { width: 90px; height: 90px; background-color: #f0f8ff; border-radius: 15px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #333; cursor: pointer; transition: all 0.3s; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
&:hover { background-color: #53b6f3; color: #fff; } } } } }
.login-status { margin-top: 20px; font-size: 22px; color: #d9534f; font-weight: bold; } </style>
|