Login.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. <template>
  2. <LoginLayout >
  3. <!-- 背景 -->
  4. <template #background>
  5. <div class="login-bg"></div>
  6. </template>
  7. <template #main>
  8. <div class="page">
  9. <div class="ai-cloud-ring" aria-hidden="true"></div>
  10. <div class="split-door" :class="{ opening: isDoorOpening }" aria-hidden="true">
  11. <div class="door left"></div>
  12. <div class="door right" @animationend="handleDoorEnd"></div>
  13. </div>
  14. <div class="content">
  15. <!-- 左侧酷炫区域 -->
  16. <div class="left">
  17. <div class="ellipse-area">
  18. <img class="ellipse" :src="require('@/assets/ellipse-line.png')" alt="ellipse" />
  19. <!-- 沿椭圆的流光:不旋转椭圆图,只做覆盖层沿线跑动 -->
  20. <div class="ellipse-glow"></div>
  21. <div class="icon icon-1"><img :src="require('@/assets/icon-upload.png')" width="87px" height="87px" /></div>
  22. <div class="icon icon-2"><img :src="require('@/assets/icon-webcam.png')" width="87px" height="87px" /></div>
  23. <div class="icon icon-3"><img :src="require('@/assets/icon-shield.png')" width="87px" height="87px" /></div>
  24. <div class="icon icon-4"><img :src="require('@/assets/icon-setting.png')" width="87px" height="87px" /></div>
  25. </div>
  26. </div>
  27. <!-- 右侧登录面板 -->
  28. <div class="right">
  29. <div class="panel">
  30. <div class="panel-inner">
  31. <div class="panel-title">账号登录</div>
  32. <div class="field">
  33. <img class="i" :src="require('@/assets/i_user.png')" />
  34. <span class="field-label">账号</span>
  35. <input class="inp" v-model.trim="username" placeholder="请输入账号" />
  36. </div>
  37. <div class="field">
  38. <img class="i" :src="require('@/assets/i_lock.png')" />
  39. <span class="field-label">密码</span>
  40. <input class="inp" type="password" v-model.trim="password" placeholder="请输入密码" />
  41. </div>
  42. <div class="row">
  43. <div class="field cap-field">
  44. <img class="i" :src="require('@/assets/i_captcha.png')" />
  45. <span class="field-label">验证码</span>
  46. <input class="inp" v-model.trim="captchaInput" placeholder="请输入验证码" />
  47. </div>
  48. <CaptchaCanvas v-model="captchaCode" />
  49. </div>
  50. <div class="hint" v-if="hint">{{ hint }}</div>
  51. <button class="btn" @click="onLogin">立即登录</button>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="page-dim" :class="{ opening: isDoorOpening }" aria-hidden="true"></div>
  57. <div class="copyright">
  58. <img class="copyright-logo" :src="require('@/assets/images/logo.png')" />
  59. <div>北京东土正创科技有限公司</div>
  60. </div>
  61. </div>
  62. </template>
  63. </LoginLayout>
  64. </template>
  65. <script>
  66. import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
  67. import LoginLayout from "@/layouts/LoginLayout.vue";
  68. import { apiLogin, apiGetCaptcha } from "@/api";
  69. export default {
  70. name: "LoginPage",
  71. components: { CaptchaCanvas, LoginLayout },
  72. created() {
  73. // 提前预加载 Cesium 瓦片
  74. import('@/utils/cesiumPreloader').then(m => m.default.start());
  75. },
  76. data() {
  77. return {
  78. baseW: 1920,
  79. baseH: 1080,
  80. scale: 1,
  81. username: "admin",
  82. password: "123456",
  83. captchaCode: "",
  84. captchaInput: "",
  85. hint: "",
  86. isDoorOpening: false,
  87. doorNavigated: false,
  88. };
  89. },
  90. mounted() {
  91. this.updateScale();
  92. window.addEventListener('resize', this.updateScale, { passive: true });
  93. },
  94. beforeDestroy() {
  95. window.removeEventListener('resize', this.updateScale);
  96. },
  97. methods: {
  98. updateScale() {
  99. const w = window.innerWidth || this.baseW;
  100. const h = window.innerHeight || this.baseH;
  101. const s = Math.min(w / this.baseW, h / this.baseH);
  102. this.scale = s;
  103. this.$el && this.$el.style.setProperty('--s', s.toFixed(6));
  104. },
  105. async onLogin() {
  106. this.hint = "";
  107. let data;
  108. try {
  109. data = await apiLogin({
  110. username: this.username,
  111. password: this.password,
  112. captcha: this.captchaInput
  113. });
  114. } catch (e) {
  115. this.hint = e.message || '登录失败';
  116. return;
  117. }
  118. localStorage.setItem('token', data.token);
  119. this.doorNavigated = false;
  120. this.isDoorOpening = false;
  121. this.$nextTick(() => {
  122. this.isDoorOpening = true;
  123. setTimeout(() => {
  124. if (this.doorNavigated) return;
  125. this.doorNavigated = true;
  126. this.$router.push("/transition");
  127. }, 1000);
  128. });
  129. },
  130. handleDoorEnd() {
  131. if (!this.isDoorOpening || this.doorNavigated) return;
  132. this.doorNavigated = true;
  133. this.$router.push("/transition");
  134. },
  135. }
  136. };
  137. </script>
  138. <style scoped>
  139. .login-bg {
  140. background: url('@/assets/images/login-background.png') no-repeat center/cover;
  141. width: 100%;
  142. height: 100%;
  143. }
  144. .page{
  145. width: 100vw; height: 100vh;
  146. position: relative;
  147. overflow: hidden;
  148. --s: 1;
  149. }
  150. .content{
  151. position:absolute;
  152. inset: 0;
  153. display:grid;
  154. grid-template-columns: 1fr 1fr;
  155. gap: clamp(calc(var(--s) * 14px), 1.8vw, calc(var(--s) * 28px));
  156. padding: clamp(calc(var(--s) * 14px), 2.4vw, calc(var(--s) * 30px));
  157. padding-top: calc(var(--s) * 60px); /* 给头部留空间 */
  158. align-items:center;
  159. z-index: 1;
  160. }
  161. /* 左侧 */
  162. .left{ min-width: 0; }
  163. .ellipse-area{
  164. position: relative;
  165. width: 65vw;
  166. max-width: calc(var(--s) * 1150px);
  167. height: 70vh;
  168. }
  169. .ellipse{
  170. position: absolute;
  171. left: 0;
  172. top: 20%;
  173. width: 88%;
  174. height: auto;
  175. opacity: 0.75;
  176. }
  177. .ellipse-glow{
  178. position:absolute;
  179. left: 0;
  180. top: 14%;
  181. width: 92%;
  182. height: 60%;
  183. pointer-events:none;
  184. -webkit-mask: url("~@/assets/ellipse-line.png") center/contain no-repeat;
  185. mask: url("~@/assets/ellipse-line.png") center/contain no-repeat;
  186. background: linear-gradient(
  187. 90deg,
  188. rgba(0,0,0,0) 0%,
  189. rgba(70,220,255,0) 40%,
  190. rgba(70,220,255,0.85) 50%,
  191. rgba(70,220,255,0) 60%,
  192. rgba(0,0,0,0) 100%
  193. );
  194. background-size: 200% 100%;
  195. animation: ellipse-flow 2.2s linear infinite;
  196. filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(43,220,255,0.18));
  197. }
  198. @keyframes ellipse-flow{
  199. 0%{ background-position: 0% 0%; }
  200. 100%{ background-position: 200% 0%; }
  201. }
  202. .icon{
  203. position:absolute;
  204. width: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
  205. height: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
  206. border-radius: 50%;
  207. /* background: radial-gradient(circle at 30% 30%, rgba(120,220,255,0.4), rgba(10,35,80,0.4)); */
  208. border: calc(var(--s) * 1px) solid rgba(60,180,255,0.35);
  209. box-shadow:
  210. 0 0 calc(var(--s) * 18px) rgba(43,220,255,0.25),
  211. inset 0 0 calc(var(--s) * 12px) rgba(120,220,255,0.25);
  212. transform: translate(-50%, -50%);
  213. animation: icon-float 3s ease-in-out infinite;
  214. }
  215. /* 按你给的比例调过 */
  216. .icon-1{ left: 16.8%; top: 87%; } /* 盾牌 */
  217. .icon-2{ left: 41.5%; top: 84%; } /* 人像 */
  218. .icon-3{ left: 61.5%;top: 73%;} /* 中间图标 */
  219. .icon-4{ left: 80.5%; top: 56%;} /* 齿轮 */
  220. /* 单独给每个球不同浮动幅度(可选,效果更高级) */
  221. .icon-1{ --dy: calc(var(--s) * 7px); }
  222. .icon-2{ --dy: calc(var(--s) * 9px); }
  223. .icon-3{ --dy: calc(var(--s) * 8px); }
  224. .icon-4{ --dy: calc(var(--s) * 6px); }
  225. @keyframes icon-float{
  226. 0%,100%{ transform: translate(-50%,-50%) translateY(0); }
  227. 50%{ transform: translate(-50%,-50%) translateY(calc(var(--dy, calc(var(--s) * 8px)) * -1)); }
  228. }
  229. .icon-1{ animation-duration: 3.2s; animation-delay: -0.2s; }
  230. .icon-2{ animation-duration: 3.8s; animation-delay: -1.1s; }
  231. .icon-3{ animation-duration: 3.4s; animation-delay: -2.0s; }
  232. .icon-4{ animation-duration: 4.2s; animation-delay: -2.7s; }
  233. .icon{
  234. animation-name: icon-float, icon-glow;
  235. animation-timing-function: ease-in-out, ease-in-out;
  236. animation-iteration-count: infinite, infinite;
  237. }
  238. .icon-1{ animation-duration: 3.2s, 2.6s; animation-delay: -0.2s, -0.8s; }
  239. .icon-2{ animation-duration: 3.8s, 3.1s; animation-delay: -1.1s, -1.4s; }
  240. .icon-3{ animation-duration: 3.4s, 2.8s; animation-delay: -2.0s, -2.2s; }
  241. .icon-4{ animation-duration: 4.2s, 3.5s; animation-delay: -2.7s, -3.0s; }
  242. @keyframes icon-glow{
  243. 0%,100%{ filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.16)); }
  244. 50%{ filter: drop-shadow(0 0 calc(var(--s) * 16px) rgba(43,220,255,0.36)); }
  245. }
  246. /* 右侧面板 */
  247. .right{
  248. min-width: 0;
  249. display:flex;
  250. justify-content:center;
  251. }
  252. .panel{
  253. width: min(calc(var(--s) * 680px), 30vw);
  254. aspect-ratio: 900 / 680;
  255. position: relative;
  256. background: url("~@/assets/panel_frame.png") center/100% 100% no-repeat;
  257. display:flex;
  258. justify-content:center;
  259. align-items:center;
  260. }
  261. /* 用绝对比例控制内容区域 */
  262. .panel-inner{
  263. width: 88%; /* 输入框主宽度比例 */
  264. display:flex;
  265. flex-direction:column;
  266. align-items:center;
  267. }
  268. .panel-title{
  269. width: 85%;
  270. font-size: var(--fs-title);
  271. color: rgba(220,250,255,0.92);
  272. margin-bottom: calc(var(--s) * 30px);
  273. font-weight: 600;
  274. }
  275. .field{
  276. width: 85%;
  277. height: calc(var(--s) * 54px);
  278. display:flex;
  279. align-items:center;
  280. gap: calc(var(--s) * 10px);
  281. padding: 0 calc(var(--s) * 16px);
  282. background: linear-gradient(270deg, rgba(26,117,255,0.11) 0%, rgba(71,120,255,0.07) 100%);
  283. border-radius: calc(var(--s) * 8px);
  284. border: calc(var(--s) * 1px) solid #3D72B8;
  285. margin-bottom: calc(var(--s) * 18px);
  286. }
  287. .field{
  288. position: relative;
  289. transition: box-shadow .22s ease, border-color .22s ease, background .22s ease;
  290. }
  291. .field:focus-within{
  292. border-color: rgba(90, 220, 255, 0.95);
  293. background: linear-gradient(270deg, rgba(26,117,255,0.16) 0%, rgba(71,120,255,0.10) 100%);
  294. box-shadow:
  295. 0 0 0 calc(var(--s) * 1px) rgba(110, 230, 255, 0.55),
  296. 0 0 calc(var(--s) * 18px) rgba(43,220,255,0.28),
  297. inset 0 0 calc(var(--s) * 14px) rgba(120,220,255,0.18);
  298. }
  299. /* 可选:让左侧小图标也略微被点亮 */
  300. .field:focus-within .i{
  301. opacity: 1;
  302. filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.35));
  303. }
  304. .field-label{
  305. flex: 0 0 calc(var(--s) * 44px); /* “账号/密码/验证码”宽度固定 */
  306. color: rgba(220,245,255,0.75);
  307. font-size: calc(var(--s) * 13px);
  308. letter-spacing: calc(var(--s) * 1px);
  309. margin-right: calc(var(--s) * 5px);
  310. user-select: none;
  311. }
  312. /* 关键:等比、居中、不拉伸 */
  313. .field .i{
  314. width: calc(var(--s) * 20px);
  315. height: calc(var(--s) * 20px);
  316. flex: 0 0 calc(var(--s) * 20px);
  317. object-fit: contain;
  318. opacity: 0.92;
  319. }
  320. .inp{
  321. flex:1;
  322. height: 100%;
  323. border: none;
  324. outline: none;
  325. background: transparent;
  326. color: rgba(235,255,255,0.92);
  327. font-size: calc(var(--s) * 14px);
  328. }
  329. .inp::placeholder{
  330. color: rgba(190,225,255,0.55);
  331. }
  332. .row{
  333. width:85%;
  334. display:flex;
  335. gap: calc(var(--s) * 14px);
  336. margin-bottom: calc(var(--s) * 10px);
  337. }
  338. .cap-field{
  339. flex: 0 0 61.13%;
  340. }
  341. .captcha{
  342. flex: 1;
  343. }
  344. .btn{
  345. width: 85%;
  346. height: calc(var(--s) * 54px);
  347. border: none;
  348. border-radius: calc(var(--s) * 8px);
  349. color: rgba(235,255,255,0.95);
  350. font-size: var(--fs-base);
  351. font-weight: 600;
  352. cursor: pointer;
  353. background: linear-gradient( 180deg, rgba(119,161,255,0) 0%, #77A1FF 100%);
  354. border-radius: calc(var(--s) * 8px);
  355. border: calc(var(--s) * 1px) solid rgba(161,190,255,0.7);
  356. }
  357. .btn:hover{ filter: brightness(1.08); }
  358. .hint{
  359. margin-top: calc(var(--s) * 12px);
  360. margin-bottom: calc(var(--s) * 12px);
  361. color: rgba(255, 0, 0, 0.92);
  362. font-size: var(--fs-base);
  363. width: 85%;
  364. text-align: left;
  365. }
  366. /* 响应式重排:窄屏时上下布局 */
  367. @media (max-width: calc(var(--s) * 980px)){
  368. .content{
  369. grid-template-columns: 1fr;
  370. padding: calc(var(--s) * 18px);
  371. align-items: start;
  372. }
  373. .ellipse-area{ width: 100%; height: 46vh; }
  374. .right{ justify-content: flex-start; }
  375. }
  376. .ai-cloud-ring{
  377. position: absolute;
  378. inset: 0;
  379. pointer-events: none;
  380. z-index: 1; /* 在 bg(0) 上面,但不压住 header/title 可再调 */
  381. /* ✅必须和 .bg 使用同一张图 + 同样的 size/position,才能精确对齐 */
  382. background-image: url('~@/assets/images/login-background.png'); /* ← 改成你真实路径 */
  383. background-repeat: no-repeat;
  384. background-size: cover;
  385. background-position: center center;
  386. /* === 你只需要调这三个:云彩环的中心点与半径 === */
  387. --cx: 27%; /* 云彩环中心 x(大概在左侧 1/4) */
  388. --cy: 58%; /* 云彩环中心 y(大概在中下) */
  389. --r1: calc(var(--s) * 145px); /* 内圈挖空半径(越大,AI 字越干净) */
  390. --r2: calc(var(--s) * 188px); /* 云彩环最亮区域半径 */
  391. --r3: calc(var(--s) * 288px); /* 外圈渐隐半径(越大,环越厚/越散) */
  392. /* ✅只保留“环形”区域:透明(内) -> 显示(环) -> 透明(外) */
  393. -webkit-mask-image: radial-gradient(circle at var(--cx) var(--cy),
  394. rgba(0,0,0,0) var(--r1),
  395. rgba(0,0,0,1) var(--r2),
  396. rgba(0,0,0,0) var(--r3)
  397. );
  398. mask-image: radial-gradient(circle at var(--cx) var(--cy),
  399. rgba(0,0,0,0) var(--r1),
  400. rgba(0,0,0,1) var(--r2),
  401. rgba(0,0,0,0) var(--r3)
  402. );
  403. /* ✅增强可见度(科技蓝) */
  404. mix-blend-mode: lighten;
  405. filter: drop-shadow(0 0 calc(var(--s) * 40px) rgba(60,220,255,.55)) drop-shadow(0 0 calc(var(--s) * 110px) rgba(60,220,255,.25));
  406. /* ✅慢旋转 + 呼吸淡入淡出 */
  407. opacity: .35;
  408. transform-origin: var(--cx) var(--cy);
  409. animation: cloud-rotate 180s linear infinite, cloud-breathe 7.8s ease-in-out infinite;
  410. }
  411. @keyframes cloud-rotate{
  412. from{ transform: rotate(0deg); }
  413. to{ transform: rotate(360deg); }
  414. }
  415. @keyframes cloud-breathe{
  416. 0%{ opacity: .18; filter: drop-shadow(0 0 calc(var(--s) * 22px) rgba(60,220,255,.35)) drop-shadow(0 0 calc(var(--s) * 70px) rgba(60,220,255,.18)); }
  417. 50%{ opacity: .55; filter: drop-shadow(0 0 calc(var(--s) * 55px) rgba(60,220,255,.70)) drop-shadow(0 0 calc(var(--s) * 140px) rgba(60,220,255,.35)); }
  418. 100%{ opacity: .22; filter: drop-shadow(0 0 calc(var(--s) * 28px) rgba(60,220,255,.40)) drop-shadow(0 0 calc(var(--s) * 85px) rgba(60,220,255,.20)); }
  419. }
  420. /* 开门层:覆盖在 bg 上方(z-index 0),不挡 content(content 是 1) */
  421. .split-door{
  422. position: absolute;
  423. inset: 0;
  424. z-index: 0;
  425. pointer-events: none;
  426. opacity: 1;
  427. perspective: calc(var(--s) * 1200px);
  428. }
  429. .door{
  430. transform-origin: center;
  431. backface-visibility: hidden;
  432. }
  433. /* 两扇门:都用全屏做 cover,再裁半屏(关键:避免中间断裂) */
  434. .split-door .door{
  435. position: absolute;
  436. inset: 0;
  437. background: url("~@/assets/images/login-background.png") center/cover no-repeat; /* 跟 .bg 完全一致 */
  438. filter: brightness(1.03) contrast(1.04);
  439. will-change: transform;
  440. }
  441. /* 左半屏 */
  442. .split-door .door.left{
  443. clip-path: inset(0 50% 0 0);
  444. }
  445. /* 右半屏 */
  446. .split-door .door.right{
  447. clip-path: inset(0 0 0 50%);
  448. }
  449. /* 开门动作更慢更清晰 */
  450. .split-door.opening .door.left{
  451. animation: door-left 1.5s cubic-bezier(.18,.85,.22,1) forwards;
  452. }
  453. .split-door.opening .door.right{
  454. animation: door-right 1.5s cubic-bezier(.18,.85,.22,1) forwards;
  455. }
  456. .split-door.opening .door.left,
  457. .split-door.opening .door.right{
  458. animation-delay: .12s;
  459. }
  460. /* 中间能量缝(升级:更亮、更柔) */
  461. .split-door::after{
  462. content:"";
  463. position:absolute;
  464. left:50%;
  465. top:0;
  466. width:calc(var(--s) * 2px);
  467. height:100%;
  468. transform: translateX(-50%);
  469. opacity: 0;
  470. background: linear-gradient(to bottom,
  471. rgba(0,0,0,0),
  472. rgba(120,240,255,0.95),
  473. rgba(0,0,0,0)
  474. );
  475. filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(60,220,255,.85))
  476. drop-shadow(0 0 calc(var(--s) * 55px) rgba(60,220,255,.35));
  477. }
  478. .split-door.opening::after{
  479. opacity: 1;
  480. animation: seam-fade 0.95s ease forwards;
  481. }
  482. /* 可选:开门瞬间整体轻微“相机拉近”(更高级) */
  483. .split-door.opening{
  484. animation: camera-zoom 0.95s ease forwards;
  485. }
  486. @keyframes door-left{
  487. from{ transform: translateX(0) skewY(0deg); }
  488. to { transform: translateX(-54vw) skewY(-0.6deg); }
  489. }
  490. @keyframes door-right{
  491. from{ transform: translateX(0) skewY(0deg); }
  492. to { transform: translateX(54vw) skewY(0.6deg); }
  493. }
  494. @keyframes seam-fade{
  495. from{ opacity: 1; }
  496. to { opacity: 0; }
  497. }
  498. @keyframes camera-zoom{
  499. from{ transform: scale(1); }
  500. to { transform: scale(1.015); }
  501. }
  502. .page-dim{
  503. position: absolute;
  504. inset: 0;
  505. pointer-events: none;
  506. z-index: 999;
  507. background: #000;
  508. opacity: 0;
  509. }
  510. /* 开门时整体渐黑 */
  511. .page-dim.opening{
  512. animation: page-dim-in 1s ease forwards;
  513. }
  514. @keyframes page-dim-in{
  515. from{ opacity: 0; }
  516. to{ opacity: 0.95; } /* 想更暗就 0.95 */
  517. }
  518. /* 版权信息 */
  519. .copyright {
  520. display: flex;
  521. align-items: center;
  522. flex-direction: column;
  523. font-size: 18px;
  524. color: #FFF;
  525. line-height: 30px;
  526. text-align: center;
  527. position: absolute;
  528. bottom: 20px;
  529. left: 0;
  530. justify-content: center;
  531. width: 100%;
  532. }
  533. </style>