Login.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <template>
  2. <div class="login-root">
  3. <!-- 视频背景 -->
  4. <video class="bg-video" autoplay muted loop playsinline @error="videoBgFailed = true">
  5. <source src="@/assets/login/login-bg.mp4" type="video/mp4" />
  6. </video>
  7. <div v-if="videoBgFailed" class="bg-fallback"></div>
  8. <!-- 顶部居中 KYLAND Logo -->
  9. <header class="top-bar">
  10. <img class="kyland-logo" src="@/assets/images/logo.png" alt="KYLAND" />
  11. </header>
  12. <!-- 主内容区:左右两栏 -->
  13. <div class="main-content">
  14. <!-- 左侧 -->
  15. <div class="left-panel">
  16. <div class="login-box">
  17. <!-- 标题 -->
  18. <div class="title-wrap">
  19. <img class="title-img" src="@/assets/login/title.png" alt="交通信号控制平台—灵·智" />
  20. </div>
  21. <!-- 表单 -->
  22. <div class="login-form">
  23. <div class="field">
  24. <img class="field-icon" src="@/assets/i_user.png" alt="" />
  25. <span class="field-label">用户名</span>
  26. <input class="inp" v-model.trim="username" placeholder="" />
  27. </div>
  28. <div class="field">
  29. <img class="field-icon" src="@/assets/i_lock.png" alt="" />
  30. <span class="field-label">密码</span>
  31. <input class="inp" type="password" v-model.trim="password" placeholder="" />
  32. </div>
  33. <div class="field-row" v-if="showCaptcha">
  34. <div class="field cap-field">
  35. <img class="field-icon" src="@/assets/i_captcha.png" alt="" />
  36. <span class="field-label">验证码</span>
  37. <input class="inp" v-model.trim="captchaInput" placeholder="" />
  38. </div>
  39. <CaptchaCanvas class="captcha-canvas" v-model="captchaCode" />
  40. </div>
  41. <div class="hint" v-if="hint">{{ hint }}</div>
  42. <button class="btn-login" @click="onLogin" :disabled="loading">
  43. {{ loading ? '登录中...' : '登录' }}
  44. </button>
  45. </div>
  46. </div>
  47. </div>
  48. <!-- 右侧留空(球体动画由视频背景实现) -->
  49. <div class="right-panel"></div>
  50. </div>
  51. <!-- 版权 -->
  52. <footer class="copyright">北京东土正创科技有限公司</footer>
  53. </div>
  54. </template>
  55. <script>
  56. import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
  57. import { apiLogin } from "@/api";
  58. export default {
  59. name: "LoginPage",
  60. components: { CaptchaCanvas },
  61. created() {
  62. import('@/utils/cesiumPreloader').then(m => m.default.start());
  63. },
  64. data() {
  65. return {
  66. videoBgFailed: false,
  67. username: "admin",
  68. password: "123456",
  69. captchaCode: "",
  70. captchaInput: "",
  71. hint: "",
  72. loading: false,
  73. loginFailedCount: Number(sessionStorage.getItem("loginFailedCount")) || 0,
  74. };
  75. },
  76. computed: {
  77. showCaptcha() { return this.loginFailedCount >= 3; },
  78. },
  79. methods: {
  80. async onLogin() {
  81. this.hint = "";
  82. if (this.showCaptcha) {
  83. if (!this.captchaInput) { this.hint = "请输入验证码"; return; }
  84. if (this.captchaInput.toLowerCase() !== this.captchaCode.toLowerCase()) {
  85. this.hint = "验证码错误"; this.captchaInput = ""; return;
  86. }
  87. }
  88. this.loading = true;
  89. try {
  90. const data = await apiLogin({ username: this.username, password: this.password, captcha: this.captchaInput });
  91. this.loginFailedCount = 0;
  92. sessionStorage.removeItem("loginFailedCount");
  93. localStorage.setItem("token", data.token);
  94. setTimeout(() => {
  95. this.$router.push('/transition').catch(() => {});
  96. }, 300);
  97. } catch (e) {
  98. this.hint = e.message || "登录失败";
  99. this.loginFailedCount++;
  100. sessionStorage.setItem("loginFailedCount", this.loginFailedCount);
  101. this.captchaInput = "";
  102. } finally {
  103. this.loading = false;
  104. }
  105. },
  106. },
  107. };
  108. </script>
  109. <style scoped>
  110. .login-root {
  111. width: 100vw;
  112. height: 100vh;
  113. position: relative;
  114. overflow: hidden;
  115. background: #050a17;
  116. display: flex;
  117. flex-direction: column;
  118. }
  119. .bg-video {
  120. position: absolute;
  121. top: 50%;
  122. left: 50%;
  123. transform: translate(-50%, -50%);
  124. min-width: 100%;
  125. min-height: 100%;
  126. width: auto;
  127. height: auto;
  128. object-fit: cover;
  129. z-index: 0;
  130. }
  131. .bg-fallback {
  132. position: absolute;
  133. inset: 0;
  134. background: url('@/assets/images/login-background.png') no-repeat center / cover;
  135. z-index: 0;
  136. }
  137. .top-bar {
  138. position: relative;
  139. z-index: 2;
  140. display: flex;
  141. justify-content: center;
  142. align-items: center;
  143. height: 10vh;
  144. flex-shrink: 0;
  145. }
  146. .kyland-logo {
  147. height: clamp(30px, 5vh, 56px);
  148. width: auto;
  149. }
  150. .main-content {
  151. position: relative;
  152. z-index: 2;
  153. flex: 1;
  154. display: grid;
  155. grid-template-columns: 45fr 55fr;
  156. align-items: center;
  157. padding: 0 8vw;
  158. min-height: 0;
  159. }
  160. .left-panel {
  161. display: flex;
  162. flex-direction: column;
  163. justify-content: center;
  164. align-items: center;
  165. }
  166. .login-box {
  167. width: 566px;
  168. height: 353px;
  169. background: transparent;
  170. border: none;
  171. box-shadow: none;
  172. padding: 0;
  173. box-sizing: border-box;
  174. display: flex;
  175. flex-direction: column;
  176. }
  177. .title-wrap {
  178. display: flex;
  179. align-items: center;
  180. margin-bottom: 22px;
  181. flex-shrink: 0;
  182. }
  183. .title-img {
  184. width: auto;
  185. max-width: 100%;
  186. }
  187. .login-form {
  188. display: flex;
  189. flex-direction: column;
  190. flex: 1;
  191. }
  192. .field {
  193. display: flex;
  194. align-items: center;
  195. height: 60px;
  196. width: 100%;
  197. padding: 0 20px;
  198. background: rgba(255, 255, 255, 0.08);
  199. border: 1px solid rgba(255, 255, 255, 0.15);
  200. border-radius: 16px;
  201. margin-bottom: 36px;
  202. transition: border-color 0.2s, background 0.2s;
  203. box-sizing: border-box;
  204. }
  205. .field:focus-within {
  206. border-color: rgba(80, 180, 255, 0.5);
  207. background: rgba(255, 255, 255, 0.12);
  208. }
  209. .field-icon {
  210. width: 22px;
  211. height: 22px;
  212. object-fit: contain;
  213. opacity: 0.7;
  214. flex-shrink: 0;
  215. margin-right: 10px;
  216. }
  217. .field-label {
  218. color: rgba(180, 215, 255, 0.75);
  219. font-size: 16px;
  220. white-space: nowrap;
  221. flex-shrink: 0;
  222. margin-right: 12px;
  223. }
  224. .inp {
  225. flex: 1;
  226. height: 100%;
  227. border: none;
  228. outline: none;
  229. background: transparent;
  230. color: #fff;
  231. font-size: 16px;
  232. }
  233. .inp::placeholder { color: rgba(140, 180, 255, 0.3); }
  234. .field-row {
  235. display: flex;
  236. gap: 12px;
  237. margin-bottom: 36px;
  238. }
  239. .cap-field { flex: 1; margin-bottom: 0; }
  240. .captcha-canvas {
  241. width: 120px;
  242. height: 60px;
  243. border-radius: 16px;
  244. overflow: hidden;
  245. border: 1px solid rgba(80, 160, 255, 0.35);
  246. flex-shrink: 0;
  247. }
  248. .hint {
  249. color: #ff4d4f;
  250. font-size: 13px;
  251. margin-bottom: 8px;
  252. margin-top: -24px;
  253. }
  254. .btn-login {
  255. width: 100%;
  256. height: 64px;
  257. background: linear-gradient(180deg, #3dd4f8 0%, #0fa8e8 50%, #0a90d8 100%);
  258. border: none;
  259. border-radius: 24px;
  260. color: #fff;
  261. font-size: 22px;
  262. letter-spacing: 0.4em;
  263. cursor: pointer;
  264. transition: filter 0.2s;
  265. flex-shrink: 0;
  266. box-shadow: 0 4px 20px rgba(10, 160, 230, 0.5);
  267. }
  268. .btn-login:hover { filter: brightness(1.1); }
  269. .btn-login:disabled { opacity: 0.6; cursor: not-allowed; }
  270. .right-panel {
  271. height: 100%;
  272. }
  273. .copyright {
  274. position: relative;
  275. z-index: 2;
  276. text-align: center;
  277. color: rgba(255, 255, 255, 0.85);
  278. font-size: clamp(12px, 1vw, 18px);
  279. padding-bottom: clamp(10px, 1.8vh, 22px);
  280. flex-shrink: 0;
  281. text-shadow: 0 0 8px rgba(0, 150, 255, 0.6), 0 1px 3px rgba(0, 0, 0, 0.8);
  282. letter-spacing: 1px;
  283. }
  284. </style>