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.

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