A8000
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
6.5 KiB

8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
  1. <template>
  2. <div class="login-container">
  3. <aside class="sidebar">
  4. <ul>
  5. <li
  6. v-for="user in userList"
  7. :key="user.id"
  8. :class="[
  9. 'user',
  10. { selected: user.account === selectedUser?.account },
  11. ]"
  12. @click="selectUser(user)"
  13. >
  14. {{ user.account }}
  15. </li>
  16. </ul>
  17. </aside>
  18. <main class="main-content">
  19. <div class="pin-container">
  20. <p :class="['pin-prompt', { shake: isShaking }]">{{ loginStatus }}</p>
  21. <div class="pin-indicators">
  22. <span
  23. v-for="n in maxPinLength"
  24. :key="n"
  25. :class="['pin-dot', { filled: n <= pin.length }]"
  26. ></span>
  27. </div>
  28. <div class="pin-keypad">
  29. <div
  30. v-for="n in 9"
  31. :key="n"
  32. class="key"
  33. @click="inputPin(n.toString())"
  34. >
  35. {{ n }}
  36. </div>
  37. <div class="key" @click="clearPin">重输</div>
  38. <div class="key" @click="inputPin('0')">0</div>
  39. <div class="key" @click="submitPin">确定</div>
  40. </div>
  41. </div>
  42. </main>
  43. </div>
  44. </template>
  45. <script setup lang="ts">
  46. import { onMounted, onUnmounted, ref, watch } from 'vue'
  47. import { login, getUserList } from '../../services/index'
  48. import type { User } from '../../types/Index'
  49. import { useRouter } from 'vue-router'
  50. import { getServerInfo } from '@/utils/getServerInfo'
  51. import { createWebSocket, DeviceContextStateMessage } from '@/websocket/socket'
  52. const router = useRouter()
  53. const stateUrl = getServerInfo('/api/v1/app/ws/state')
  54. const wsState = createWebSocket(stateUrl.wsUrl)
  55. // 用户角色
  56. // const roles = ref(['护士', '管理员', '测试人员'])
  57. // const selectedRole = ref('护士')
  58. //用户列表
  59. const userList = ref<User[]>([])
  60. //获取用户列表
  61. const getUserListData = async () => {
  62. const res = await getUserList()
  63. userList.value = res.data
  64. }
  65. const handleDeviceContextState = (data: DeviceContextStateMessage['data']) => {
  66. if (data.loginFlag) {
  67. sessionStorage.setItem('token', JSON.stringify(data.loginUser))
  68. router.push('/index')
  69. }
  70. }
  71. onMounted(() => {
  72. wsState.subscribe<DeviceContextStateMessage>(
  73. 'DeviceContext',
  74. handleDeviceContextState,
  75. )
  76. wsState.connect()
  77. setTimeout(() => {
  78. getUserListData()
  79. }, 500)
  80. })
  81. onUnmounted(() => {
  82. wsState.unsubscribe<DeviceContextStateMessage>(
  83. 'DeviceContext',
  84. handleDeviceContextState,
  85. )
  86. })
  87. //选中的用户
  88. const selectedUser = ref<User | null>(null)
  89. // PIN 相关
  90. const pin = ref('')
  91. const maxPinLength = 4
  92. const loginStatus = ref('请输入您的pin码')
  93. const isShaking = ref(false) // 控制抖动效果的触发
  94. // 切换角色时清空PIN
  95. const selectUser = (user: User) => {
  96. selectedUser.value = user
  97. pin.value = ''
  98. loginStatus.value = '请输入您的pin码'
  99. }
  100. // 输入PIN码
  101. const inputPin = (key: string) => {
  102. if (selectedUser.value === null) {
  103. loginStatus.value = '请选择用户'
  104. return
  105. }
  106. if (pin.value.length >= maxPinLength) {
  107. return
  108. }
  109. pin.value += key
  110. if (pin.value.length === maxPinLength) {
  111. return
  112. }
  113. }
  114. // 重置PIN码
  115. const clearPin = () => {
  116. pin.value = ''
  117. loginStatus.value = '请输入您的pin码'
  118. }
  119. // 点击确认验证PIN码
  120. // 模拟后端请求验证PIN
  121. const submitPin = async () => {
  122. if (selectedUser.value === null) {
  123. loginStatus.value = '请选择用户'
  124. return
  125. } else if (pin.value.length !== maxPinLength) {
  126. loginStatus.value = '请输入合法的pin码'
  127. return
  128. }
  129. const params = {
  130. // account: selectedUser.value?.account,
  131. id: selectedUser.value?.id,
  132. password: pin.value,
  133. }
  134. const res = await login(params)
  135. console.log(res)
  136. if (res.success) {
  137. loginStatus.value = '登录成功'
  138. sessionStorage.setItem('token', JSON.stringify(res.data))
  139. router.push('/index')
  140. } else {
  141. loginStatus.value = `${res.info}`
  142. pin.value = ''
  143. }
  144. }
  145. // 监听 loginStatus 变化并触发抖动效果
  146. watch(loginStatus, () => {
  147. isShaking.value = true
  148. setTimeout(() => {
  149. isShaking.value = false
  150. }, 500) // 动画持续时间为0.5秒
  151. })
  152. </script>
  153. <style scoped lang="less">
  154. .login-container {
  155. display: flex;
  156. align-items: center;
  157. justify-content: center;
  158. width: 100vw;
  159. height: 100vh;
  160. background: linear-gradient(to bottom, #a1d0f5, #bce0fe); // 垂直线性渐变
  161. padding: 15px; // 减小padding
  162. box-sizing: border-box;
  163. }
  164. .sidebar {
  165. width: 220px;
  166. height: 80vh;
  167. background-color: #e1f0fb;
  168. border-radius: 20px;
  169. padding: 20px 10px;
  170. display: flex;
  171. flex-direction: column;
  172. align-items: center;
  173. box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
  174. margin-right: 20px;
  175. ul {
  176. list-style: none;
  177. padding: 0;
  178. width: 100%;
  179. }
  180. .user {
  181. padding: 12px 0;
  182. font-size: 24px;
  183. color: #666;
  184. cursor: pointer;
  185. transition: all 0.3s;
  186. border-radius: 10px;
  187. margin: 10px 0;
  188. &:hover {
  189. background-color: #b3d9f9;
  190. color: #fff;
  191. }
  192. }
  193. .selected {
  194. background-color: #53b6f3;
  195. color: white;
  196. font-weight: bold;
  197. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  198. }
  199. }
  200. .main-content {
  201. display: flex;
  202. align-items: center;
  203. justify-content: center;
  204. .pin-container {
  205. background-color: #fff;
  206. border-radius: 20px;
  207. padding: 30px 40px;
  208. box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
  209. text-align: center;
  210. .pin-prompt {
  211. font-size: 28px;
  212. color: #333;
  213. margin-bottom: 20px;
  214. font-weight: bold;
  215. &.shake {
  216. animation: shake 0.5s ease;
  217. color: #d9534f; // 抖动时变色
  218. }
  219. }
  220. .pin-indicators {
  221. display: flex;
  222. justify-content: center;
  223. margin: 15px 0;
  224. .pin-dot {
  225. width: 25px;
  226. height: 25px;
  227. background-color: #d5e6f2;
  228. border-radius: 50%;
  229. margin: 0 10px;
  230. transition: background-color 0.3s;
  231. &.filled {
  232. background-color: #53b6f3;
  233. }
  234. }
  235. }
  236. .pin-keypad {
  237. display: grid;
  238. grid-template-columns: repeat(3, 1fr);
  239. gap: 20px;
  240. margin-top: 30px;
  241. .key {
  242. width: 90px;
  243. height: 90px;
  244. background-color: #f0f8ff;
  245. border-radius: 15px;
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. font-size: 32px;
  250. color: #333;
  251. cursor: pointer;
  252. transition: all 0.3s;
  253. box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
  254. &:hover {
  255. background-color: #53b6f3;
  256. color: #fff;
  257. }
  258. }
  259. }
  260. }
  261. }
  262. .login-status {
  263. margin-top: 20px;
  264. font-size: 22px;
  265. color: #d9534f;
  266. font-weight: bold;
  267. }
  268. </style>