lock.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <template>
  2. <div class="lock-container">
  3. <!-- 动态粒子背景 -->
  4. <canvas ref="particleCanvas" class="particle-bg"></canvas>
  5. <!-- 时钟 -->
  6. <div class="lock-time">{{ currentTime }}</div>
  7. <div class="lock-date">{{ currentDate }}</div>
  8. <!-- 锁屏卡片 -->
  9. <div class="lock-card">
  10. <div class="avatar-wrap">
  11. <img :src="userStore.avatar" class="lock-avatar" @error="onAvatarError" />
  12. <div class="lock-icon">🔒</div>
  13. </div>
  14. <div class="lock-username">{{ userStore.realName }}</div>
  15. <div class="lock-hint">系统已锁定,请输入密码解锁</div>
  16. <div class="input-wrap" :class="{ shake: isShaking }">
  17. <input ref="passwordInput" v-model="password" type="password" placeholder="请输入登录密码" class="lock-input" @keydown.enter="handleUnlock" autocomplete="off" />
  18. <button class="unlock-btn" @click="handleUnlock" :disabled="loading">
  19. <span v-if="!loading">→</span>
  20. <span v-else class="loading-dot">···</span>
  21. </button>
  22. </div>
  23. <div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
  24. <div class="lock-footer">
  25. <a href="javascript:;" @click="goLogin">退出重新登录</a>
  26. </div>
  27. </div>
  28. </div>
  29. </template>
  30. <script setup>
  31. import { useRouter } from 'vue-router'
  32. import useUserStore from '@/store/modules/user'
  33. import useLockStore from '@/store/modules/lock'
  34. import { unlockScreen } from '@/api/login'
  35. import defAva from '@/assets/images/profile.jpg'
  36. const router = useRouter()
  37. const userStore = useUserStore()
  38. const lockStore = useLockStore()
  39. const password = ref('')
  40. const loading = ref(false)
  41. const errorMsg = ref('')
  42. const isShaking = ref(false)
  43. const currentTime = ref('')
  44. const currentDate = ref('')
  45. const passwordInput = ref(null)
  46. const particleCanvas = ref(null)
  47. let timer = null
  48. let animationId = null
  49. let particles = []
  50. const onAvatarError = (e) => {
  51. e.target.src = defAva
  52. }
  53. const startClock = () => {
  54. const update = () => {
  55. const now = new Date()
  56. const pad = n => String(n).padStart(2, '0')
  57. currentTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
  58. const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  59. currentDate.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${days[now.getDay()]}`
  60. }
  61. update()
  62. timer = setInterval(update, 1000)
  63. }
  64. const handleUnlock = async () => {
  65. if (!password.value) {
  66. showError('请输入密码')
  67. return
  68. }
  69. loading.value = true
  70. errorMsg.value = ''
  71. try {
  72. await unlockScreen(password.value)
  73. const lockPath = lockStore.lockPath
  74. lockStore.unlockScreen()
  75. router.replace(lockPath)
  76. } catch (err) {
  77. const msg = err.message || err.toString()
  78. showError(msg)
  79. password.value = ''
  80. nextTick(() => passwordInput.value?.focus())
  81. } finally {
  82. loading.value = false
  83. }
  84. }
  85. const showError = (msg) => {
  86. errorMsg.value = msg
  87. isShaking.value = true
  88. setTimeout(() => { isShaking.value = false }, 600)
  89. }
  90. const goLogin = () => {
  91. lockStore.unlockScreen()
  92. userStore.logOut().then(() => {
  93. router.push('/login')
  94. })
  95. }
  96. const initParticles = () => {
  97. const canvas = particleCanvas.value
  98. if (!canvas) return
  99. const ctx = canvas.getContext('2d')
  100. const resize = () => {
  101. canvas.width = window.innerWidth
  102. canvas.height = window.innerHeight
  103. }
  104. resize()
  105. window.addEventListener('resize', resize)
  106. particles = Array.from({ length: 80 }, () => ({
  107. x: Math.random() * canvas.width,
  108. y: Math.random() * canvas.height,
  109. r: Math.random() * 2 + 1,
  110. dx: (Math.random() - 0.5) * 0.6,
  111. dy: (Math.random() - 0.5) * 0.6,
  112. alpha: Math.random() * 0.5 + 0.2
  113. }))
  114. const draw = () => {
  115. ctx.clearRect(0, 0, canvas.width, canvas.height)
  116. particles.forEach(p => {
  117. ctx.beginPath()
  118. ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
  119. ctx.fillStyle = `rgba(255,255,255,${p.alpha})`
  120. ctx.fill()
  121. p.x += p.dx
  122. p.y += p.dy
  123. if (p.x < 0 || p.x > canvas.width) p.dx *= -1
  124. if (p.y < 0 || p.y > canvas.height) p.dy *= -1
  125. })
  126. for (let i = 0; i < particles.length; i++) {
  127. for (let j = i + 1; j < particles.length; j++) {
  128. const a = particles[i], b = particles[j]
  129. const dist = Math.hypot(a.x - b.x, a.y - b.y)
  130. if (dist < 120) {
  131. ctx.beginPath()
  132. ctx.moveTo(a.x, a.y)
  133. ctx.lineTo(b.x, b.y)
  134. ctx.strokeStyle = `rgba(255,255,255,${0.15 * (1 - dist / 120)})`
  135. ctx.lineWidth = 0.5
  136. ctx.stroke()
  137. }
  138. }
  139. }
  140. animationId = requestAnimationFrame(draw)
  141. }
  142. draw()
  143. }
  144. onMounted(() => {
  145. startClock()
  146. initParticles()
  147. nextTick(() => passwordInput.value?.focus())
  148. })
  149. onBeforeUnmount(() => {
  150. clearInterval(timer)
  151. cancelAnimationFrame(animationId)
  152. })
  153. </script>
  154. <style scoped>
  155. /* 样式与原文件完全一致,无需改动 */
  156. .lock-container {
  157. position: fixed;
  158. inset: 0;
  159. background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
  160. display: flex;
  161. flex-direction: column;
  162. align-items: center;
  163. justify-content: center;
  164. z-index: 9999;
  165. font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
  166. overflow: hidden;
  167. }
  168. .particle-bg {
  169. position: absolute;
  170. inset: 0;
  171. z-index: 0;
  172. }
  173. .lock-time {
  174. position: relative;
  175. z-index: 1;
  176. font-size: 72px;
  177. font-weight: 200;
  178. color: #fff;
  179. letter-spacing: 4px;
  180. text-shadow: 0 0 40px rgba(255,255,255,0.3);
  181. margin-bottom: 8px;
  182. font-variant-numeric: tabular-nums;
  183. }
  184. .lock-date {
  185. position: relative;
  186. z-index: 1;
  187. font-size: 15px;
  188. color: rgba(255,255,255,0.6);
  189. margin-bottom: 48px;
  190. letter-spacing: 2px;
  191. }
  192. .lock-card {
  193. position: relative;
  194. z-index: 1;
  195. background: rgba(255, 255, 255, 0.08);
  196. backdrop-filter: blur(20px);
  197. -webkit-backdrop-filter: blur(20px);
  198. border: 1px solid rgba(255, 255, 255, 0.15);
  199. border-radius: 24px;
  200. padding: 40px 48px;
  201. width: 360px;
  202. display: flex;
  203. flex-direction: column;
  204. align-items: center;
  205. box-shadow: 0 25px 60px rgba(0,0,0,0.4);
  206. }
  207. .avatar-wrap {
  208. position: relative;
  209. margin-bottom: 16px;
  210. }
  211. .lock-avatar {
  212. width: 80px;
  213. height: 80px;
  214. border-radius: 50%;
  215. border: 3px solid rgba(255,255,255,0.3);
  216. object-fit: cover;
  217. display: block;
  218. }
  219. .lock-icon {
  220. position: absolute;
  221. bottom: -4px;
  222. right: -4px;
  223. background: rgba(255,255,255,0.15);
  224. border-radius: 50%;
  225. width: 26px;
  226. height: 26px;
  227. display: flex;
  228. align-items: center;
  229. justify-content: center;
  230. font-size: 13px;
  231. backdrop-filter: blur(8px);
  232. }
  233. .lock-username {
  234. color: #fff;
  235. font-size: 18px;
  236. font-weight: 600;
  237. margin-bottom: 6px;
  238. letter-spacing: 1px;
  239. }
  240. .lock-hint {
  241. color: rgba(255,255,255,0.5);
  242. font-size: 13px;
  243. margin-bottom: 28px;
  244. }
  245. .input-wrap {
  246. width: 100%;
  247. display: flex;
  248. align-items: center;
  249. background: rgba(255,255,255,0.1);
  250. border: 1px solid rgba(255,255,255,0.2);
  251. border-radius: 50px;
  252. padding: 4px 4px 4px 20px;
  253. transition: border-color 0.3s;
  254. }
  255. .input-wrap:focus-within {
  256. border-color: rgba(255,255,255,0.6);
  257. background: rgba(255,255,255,0.13);
  258. }
  259. .input-wrap.shake {
  260. animation: shake 0.5s ease;
  261. }
  262. @keyframes shake {
  263. 0%, 100% { transform: translateX(0); }
  264. 20% { transform: translateX(-8px); }
  265. 40% { transform: translateX(8px); }
  266. 60% { transform: translateX(-6px); }
  267. 80% { transform: translateX(6px); }
  268. }
  269. .lock-input {
  270. flex: 1;
  271. background: transparent;
  272. border: none;
  273. outline: none;
  274. color: #fff;
  275. font-size: 15px;
  276. padding: 10px 0;
  277. }
  278. .lock-input::placeholder {
  279. color: rgba(255,255,255,0.35);
  280. }
  281. .unlock-btn {
  282. width: 42px;
  283. height: 42px;
  284. border-radius: 50%;
  285. background: linear-gradient(135deg, #667eea, #764ba2);
  286. border: none;
  287. color: #fff;
  288. font-size: 18px;
  289. cursor: pointer;
  290. transition: transform 0.2s, opacity 0.2s;
  291. display: flex;
  292. align-items: center;
  293. justify-content: center;
  294. flex-shrink: 0;
  295. }
  296. .unlock-btn:hover:not(:disabled) {
  297. transform: scale(1.08);
  298. }
  299. .unlock-btn:disabled {
  300. opacity: 0.6;
  301. cursor: not-allowed;
  302. }
  303. .loading-dot {
  304. font-size: 13px;
  305. letter-spacing: 1px;
  306. }
  307. .error-msg {
  308. margin-top: 14px;
  309. color: #ff7675;
  310. font-size: 13px;
  311. text-align: center;
  312. animation: fadeIn 0.3s ease;
  313. }
  314. @keyframes fadeIn {
  315. from { opacity: 0; transform: translateY(-4px); }
  316. to { opacity: 1; transform: translateY(0); }
  317. }
  318. .lock-footer {
  319. margin-top: 24px;
  320. }
  321. .lock-footer a {
  322. color: rgba(255,255,255,0.4);
  323. font-size: 13px;
  324. text-decoration: none;
  325. transition: color 0.2s;
  326. }
  327. .lock-footer a:hover {
  328. color: rgba(255,255,255,0.8);
  329. }
  330. </style>