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.

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