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.

372 lines
8.1 KiB

4 months ago
  1. <script setup lang="ts">
  2. import { ref, onMounted, onUnmounted } from 'vue'
  3. import { getUserList } from '@/services/Index/user-manage'
  4. import type { User } from '@/types/Index'
  5. import { createWebSocket, DeviceContextStateMessage } from '@/websocket/socket.ts'
  6. import { getServerInfo } from '@/utils/getServerInfo.ts'
  7. import router from '@/router/router.ts'
  8. import { isBoardParamInited, login } from '@/services'
  9. import circleUrl from '@/assets/avatar.png'
  10. import { eMessage } from '@/pages/Index/utils'
  11. const stateUrl = getServerInfo('/api/v1/app/ws/state')
  12. const wsState = createWebSocket(stateUrl.wsUrl)
  13. const handleDeviceContextState = (data: DeviceContextStateMessage['data']) => {
  14. if (data.loginFlag) {
  15. sessionStorage.setItem('token', JSON.stringify(data.loginUser))
  16. router.push('/index')
  17. }
  18. }
  19. //用户列表
  20. const userList = ref<User[]>([])
  21. //获取用户列表
  22. const getUserListData = async () => {
  23. const res = await getUserList()
  24. userList.value = res.data
  25. }
  26. onMounted(() => {
  27. wsState.subscribe<DeviceContextStateMessage>(
  28. 'DeviceContext',
  29. handleDeviceContextState,
  30. )
  31. wsState.connect()
  32. getUserListData()
  33. })
  34. onUnmounted(() => {
  35. wsState.unsubscribe<DeviceContextStateMessage>(
  36. 'DeviceContext',
  37. handleDeviceContextState,
  38. )
  39. })
  40. const activeUser = ref<User>()
  41. const password = ref('')
  42. const inputPin = (pin: string) => {
  43. if (password.value.length >= 4) {
  44. return
  45. }
  46. if (password.value.length < 4) {
  47. password.value += pin
  48. }
  49. if (password.value.length === 4) {
  50. // 输入完成,执行登录操作
  51. submitPin()
  52. }
  53. }
  54. const passwordError = ref(false)
  55. const submitPin = async () => {
  56. let resData = await isBoardParamInited()
  57. if (!resData.data) {
  58. //设备正在初始化
  59. eMessage.error('设备正在初始化,请稍候重试')
  60. return
  61. }
  62. const params = {
  63. id: activeUser.value?.id,
  64. password: password.value,
  65. }
  66. const res = await login(params)
  67. if (res.success) {
  68. sessionStorage.setItem('token', JSON.stringify(res.data))
  69. await router.push('/index')
  70. } else {
  71. passwordError.value = true // 触发抖动动画
  72. // 动画结束后重置状态(2次抖动共0.6秒)
  73. setTimeout(() => {
  74. passwordError.value = false
  75. }, 600)
  76. password.value = ''
  77. }
  78. }
  79. const showPassword = ref(false)
  80. const next = () => {
  81. if (activeUser.value?.id) {
  82. showPassword.value = true
  83. }
  84. }
  85. const back = () => {
  86. showPassword.value = false;
  87. password.value = ''
  88. }
  89. const version = __APP_VERSION__
  90. </script>
  91. <template>
  92. <div class="login-box">
  93. <transition name="flip-card" mode="out-in">
  94. <div class="user-box" v-if="!showPassword">
  95. <p class="title">选择用户</p>
  96. <div class="user-list">
  97. <div class="user-info" v-for="user in userList" :key="user.id" @click="activeUser = user" :class="{ 'user-info-active': activeUser?.id === user.id }">
  98. <el-avatar :size="60" :src="circleUrl" />
  99. <span class="name">{{user.account}}</span>
  100. <el-icon v-show="activeUser?.id === user.id" color="#fff"><Select /></el-icon>
  101. </div>
  102. </div>
  103. <div class="next-box" :class="{'next-box-active': activeUser?.id}" @click="next">
  104. <el-icon><Right /></el-icon>
  105. </div>
  106. </div>
  107. <div class="password-box" v-else>
  108. <div class="user-info user-info-active">
  109. <el-avatar :size="40" :src="circleUrl" />
  110. <span class="name">{{activeUser!.account}}</span>
  111. </div>
  112. <p class="title">请输入4位PIN码</p>
  113. <div class="password-list">
  114. <div v-for="i in 4" :key="i" class="password-item" :class="{'password-item-fill': i <= password.length, 'password-error': passwordError}"></div>
  115. </div>
  116. <div class="pin-keypad">
  117. <div
  118. v-for="n in 9"
  119. :key="n"
  120. class="key"
  121. @click="inputPin(n.toString())"
  122. >
  123. {{ n }}
  124. </div>
  125. <div></div>
  126. <div class="key" @click="inputPin('0')">0</div>
  127. </div>
  128. <div class="next-box next-box-active" @click="back">
  129. <el-icon><Back /></el-icon>
  130. </div>
  131. </div>
  132. </transition>
  133. <div class="version">版本号v {{version}}</div>
  134. </div>
  135. </template>
  136. <style lang="less" scoped>
  137. @keyframes iconAnim {
  138. 0% { transform: scale(0.8); opacity: 0; }
  139. 100% { transform: scale(1); opacity: 1; }
  140. }
  141. @keyframes shake {
  142. 0%, 100% {
  143. transform: translateX(0);
  144. opacity: 1;
  145. }
  146. 25% {
  147. transform: translateX(-10px);
  148. opacity: 0.7;
  149. }
  150. 50% {
  151. transform: translateX(10px);
  152. opacity: 0.7;
  153. }
  154. 75% {
  155. transform: translateX(-7px);
  156. opacity: 0.7;
  157. }
  158. }
  159. .login-title {
  160. position: absolute;
  161. top: 10%;
  162. font-size: 50px;
  163. font-weight: bold;
  164. }
  165. .login-box {
  166. perspective: 1000px;
  167. width: 100%;
  168. height: 100%;
  169. background: #A0CEF2;
  170. display: flex;
  171. align-items: center;
  172. justify-content: center;
  173. position: relative;
  174. .user-box {
  175. width: 50%;
  176. height: 50%;
  177. background: #fff;
  178. border-radius: 30px;
  179. padding: 40px 30px;
  180. display: flex;
  181. flex-direction: column;
  182. align-items: center;
  183. .user-list {
  184. width: 100%;
  185. flex: 1;
  186. overflow: auto;
  187. .user-info {
  188. display: flex;
  189. align-items: center;
  190. padding: 20px;
  191. border-radius: 20px;
  192. .name {
  193. margin-left: 30px;
  194. font-size: 25px;
  195. font-weight: bold;
  196. }
  197. .el-icon {
  198. animation: iconAnim 0.3s ease forwards;
  199. font-size: 30px;
  200. margin-left: auto;
  201. }
  202. }
  203. .user-info-active {
  204. background: #3E8ED1;
  205. color: #fff;
  206. }
  207. }
  208. }
  209. .password-box {
  210. width: 50%;
  211. height: 50%;
  212. background: #fff;
  213. border-radius: 30px;
  214. padding: 40px 30px;
  215. display: flex;
  216. flex-direction: column;
  217. align-items: center;
  218. justify-content: space-between;
  219. .user-info {
  220. width: 50%;
  221. padding: 10px 0;
  222. display: flex;
  223. align-items: center;
  224. justify-content: center;
  225. border-radius: 20px;
  226. background: #3E8ED1;
  227. color: #fff;
  228. .name {
  229. margin-left: 30px;
  230. font-size: 25px;
  231. font-weight: bold;
  232. }
  233. }
  234. }
  235. }
  236. .title {
  237. font-size: 30px;
  238. font-weight: bold;
  239. }
  240. .password-list {
  241. display: flex;
  242. justify-content: center;
  243. padding: 20px 0;
  244. .password-item {
  245. width: 30px;
  246. height: 30px;
  247. border-radius: 50%;
  248. background: #CDCFD4;
  249. margin: 0 10px;
  250. }
  251. .password-item-fill {
  252. background: #3E8ED1;
  253. }
  254. }
  255. .pin-keypad {
  256. display: grid;
  257. grid-template-columns: repeat(3, 1fr);
  258. gap: 10px;
  259. margin-top: 30px;
  260. .key {
  261. width: 100px;
  262. height: 80px;
  263. background-color: #fff;
  264. border-radius: 15px;
  265. display: flex;
  266. align-items: center;
  267. justify-content: center;
  268. font-size: 32px;
  269. color: #333;
  270. cursor: pointer;
  271. transition: all 0.3s;
  272. border: 1px solid #E7EDF1;
  273. box-shadow: 0 3px 8px #E7EDF1;
  274. &:active {
  275. transform: scale(0.95);
  276. }
  277. }
  278. }
  279. .error-box {
  280. flex: 1;
  281. display: flex;
  282. justify-content: center;
  283. align-items: center;
  284. font-size: 20px;
  285. color: #ff0000;
  286. }
  287. .password-error {
  288. animation: shake 0.3s ease-in-out 2;
  289. }
  290. .next-box {
  291. width: 80px;
  292. height: 80px;
  293. border-radius: 50%;
  294. background: #ddd;
  295. display: flex;
  296. justify-content: center;
  297. align-items: center;
  298. margin: 20px auto;
  299. .el-icon {
  300. font-size: 30px;
  301. color: #bbb
  302. }
  303. }
  304. .next-box-active {
  305. background: #3E8ED1;
  306. .el-icon {
  307. color: #fff;
  308. }
  309. }
  310. .flip-card-leave-active,
  311. .flip-card-enter-active {
  312. transition: all 0.5s ease;
  313. backface-visibility: hidden; /* 隐藏背面 */
  314. }
  315. .flip-card-enter-from,
  316. .flip-card-leave-to {
  317. transform: rotateY(90deg);
  318. opacity: 0;
  319. }
  320. .flip-card-enter-active .user-box,
  321. .flip-card-leave-active .user-box,
  322. .flip-card-enter-active .password-box,
  323. .flip-card-leave-active .password-box {
  324. position: absolute;
  325. backface-visibility: hidden;
  326. transition: transform 0.5s ease;
  327. }
  328. .flip-card-enter .user-box {
  329. transform: rotateY(-90deg);
  330. }
  331. .flip-card-leave-to .user-box {
  332. transform: rotateY(90deg);
  333. }
  334. .flip-card-enter .password-box {
  335. transform: rotateY(90deg);
  336. }
  337. .flip-card-leave-to .password-box {
  338. transform: rotateY(-90deg);
  339. }
  340. .version {
  341. position: absolute;
  342. bottom: 10px;
  343. color: rgba(0,0,0,0.25);
  344. }
  345. </style>