| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- <template>
- <LoginLayout >
- <!-- 背景 -->
- <template #background>
- <div class="login-bg"></div>
- </template>
- <template #main>
- <div class="page">
- <div class="ai-cloud-ring" aria-hidden="true"></div>
- <div class="split-door" :class="{ opening: isDoorOpening }" aria-hidden="true">
- <div class="door left"></div>
- <div class="door right" @animationend="handleDoorEnd"></div>
- </div>
- <div class="content">
- <!-- 左侧酷炫区域 -->
- <div class="left">
- <div class="ellipse-area">
- <img class="ellipse" :src="require('@/assets/ellipse-line.png')" alt="ellipse" />
- <!-- 沿椭圆的流光:不旋转椭圆图,只做覆盖层沿线跑动 -->
- <div class="ellipse-glow"></div>
- <div class="icon icon-1"><img :src="require('@/assets/icon-upload.png')" width="87px" height="87px" /></div>
- <div class="icon icon-2"><img :src="require('@/assets/icon-webcam.png')" width="87px" height="87px" /></div>
- <div class="icon icon-3"><img :src="require('@/assets/icon-shield.png')" width="87px" height="87px" /></div>
- <div class="icon icon-4"><img :src="require('@/assets/icon-setting.png')" width="87px" height="87px" /></div>
- </div>
- </div>
-
- <!-- 右侧登录面板 -->
- <div class="right">
- <div class="panel">
- <div class="panel-inner">
-
- <div class="panel-title">账号登录</div>
-
- <div class="field">
- <img class="i" :src="require('@/assets/i_user.png')" />
- <span class="field-label">账号</span>
- <input class="inp" v-model.trim="username" placeholder="请输入账号" />
- </div>
-
- <div class="field">
- <img class="i" :src="require('@/assets/i_lock.png')" />
- <span class="field-label">密码</span>
- <input class="inp" type="password" v-model.trim="password" placeholder="请输入密码" />
- </div>
-
- <div class="row">
- <div class="field cap-field">
- <img class="i" :src="require('@/assets/i_captcha.png')" />
- <span class="field-label">验证码</span>
- <input class="inp" v-model.trim="captchaInput" placeholder="请输入验证码" />
- </div>
- <CaptchaCanvas v-model="captchaCode" />
- </div>
-
- <div class="hint" v-if="hint">{{ hint }}</div>
- <button class="btn" @click="onLogin">立即登录</button>
-
- </div>
- </div>
- </div>
- </div>
- <div class="page-dim" :class="{ opening: isDoorOpening }" aria-hidden="true"></div>
- <div class="copyright">
- <img class="copyright-logo" :src="require('@/assets/images/logo.png')" />
- <div>北京东土正创科技有限公司</div>
- </div>
- </div>
- </template>
- </LoginLayout>
- </template>
- <script>
- import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
- import LoginLayout from "@/layouts/LoginLayout.vue";
- import { apiLogin, apiGetCaptcha } from "@/api";
- export default {
- name: "LoginPage",
- components: { CaptchaCanvas, LoginLayout },
- created() {
- // 提前预加载 Cesium 瓦片
- import('@/utils/cesiumPreloader').then(m => m.default.start());
- },
- data() {
- return {
- baseW: 1920,
- baseH: 1080,
- scale: 1,
- username: "admin",
- password: "123456",
- captchaCode: "",
- captchaInput: "",
- hint: "",
- isDoorOpening: false,
- doorNavigated: false,
- };
- },
- mounted() {
- this.updateScale();
- window.addEventListener('resize', this.updateScale, { passive: true });
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.updateScale);
- },
- methods: {
- updateScale() {
- const w = window.innerWidth || this.baseW;
- const h = window.innerHeight || this.baseH;
- const s = Math.min(w / this.baseW, h / this.baseH);
- this.scale = s;
- this.$el && this.$el.style.setProperty('--s', s.toFixed(6));
- },
- async onLogin() {
- this.hint = "";
- let data;
- try {
- data = await apiLogin({
- username: this.username,
- password: this.password,
- captcha: this.captchaInput
- });
- } catch (e) {
- this.hint = e.message || '登录失败';
- return;
- }
- localStorage.setItem('token', data.token);
- this.doorNavigated = false;
- this.isDoorOpening = false;
- this.$nextTick(() => {
- this.isDoorOpening = true;
- setTimeout(() => {
- if (this.doorNavigated) return;
- this.doorNavigated = true;
- this.$router.push("/transition");
- }, 1000);
- });
- },
- handleDoorEnd() {
- if (!this.isDoorOpening || this.doorNavigated) return;
- this.doorNavigated = true;
- this.$router.push("/transition");
- },
- }
- };
- </script>
- <style scoped>
- .login-bg {
- background: url('@/assets/images/login-background.png') no-repeat center/cover;
- width: 100%;
- height: 100%;
- }
- .page{
- width: 100vw; height: 100vh;
- position: relative;
- overflow: hidden;
- --s: 1;
- }
- .content{
- position:absolute;
- inset: 0;
- display:grid;
- grid-template-columns: 1fr 1fr;
- gap: clamp(calc(var(--s) * 14px), 1.8vw, calc(var(--s) * 28px));
- padding: clamp(calc(var(--s) * 14px), 2.4vw, calc(var(--s) * 30px));
- padding-top: calc(var(--s) * 60px); /* 给头部留空间 */
- align-items:center;
- z-index: 1;
- }
- /* 左侧 */
- .left{ min-width: 0; }
- .ellipse-area{
- position: relative;
- width: 65vw;
- max-width: calc(var(--s) * 1150px);
- height: 70vh;
- }
- .ellipse{
- position: absolute;
- left: 0;
- top: 20%;
- width: 88%;
- height: auto;
- opacity: 0.75;
- }
- .ellipse-glow{
- position:absolute;
- left: 0;
- top: 14%;
- width: 92%;
- height: 60%;
- pointer-events:none;
- -webkit-mask: url("~@/assets/ellipse-line.png") center/contain no-repeat;
- mask: url("~@/assets/ellipse-line.png") center/contain no-repeat;
- background: linear-gradient(
- 90deg,
- rgba(0,0,0,0) 0%,
- rgba(70,220,255,0) 40%,
- rgba(70,220,255,0.85) 50%,
- rgba(70,220,255,0) 60%,
- rgba(0,0,0,0) 100%
- );
- background-size: 200% 100%;
- animation: ellipse-flow 2.2s linear infinite;
- filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(43,220,255,0.18));
- }
- @keyframes ellipse-flow{
- 0%{ background-position: 0% 0%; }
- 100%{ background-position: 200% 0%; }
- }
- .icon{
- position:absolute;
- width: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
- height: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
- border-radius: 50%;
- /* background: radial-gradient(circle at 30% 30%, rgba(120,220,255,0.4), rgba(10,35,80,0.4)); */
- border: calc(var(--s) * 1px) solid rgba(60,180,255,0.35);
- box-shadow:
- 0 0 calc(var(--s) * 18px) rgba(43,220,255,0.25),
- inset 0 0 calc(var(--s) * 12px) rgba(120,220,255,0.25);
- transform: translate(-50%, -50%);
- animation: icon-float 3s ease-in-out infinite;
- }
- /* 按你给的比例调过 */
- .icon-1{ left: 16.8%; top: 87%; } /* 盾牌 */
- .icon-2{ left: 41.5%; top: 84%; } /* 人像 */
- .icon-3{ left: 61.5%;top: 73%;} /* 中间图标 */
- .icon-4{ left: 80.5%; top: 56%;} /* 齿轮 */
- /* 单独给每个球不同浮动幅度(可选,效果更高级) */
- .icon-1{ --dy: calc(var(--s) * 7px); }
- .icon-2{ --dy: calc(var(--s) * 9px); }
- .icon-3{ --dy: calc(var(--s) * 8px); }
- .icon-4{ --dy: calc(var(--s) * 6px); }
- @keyframes icon-float{
- 0%,100%{ transform: translate(-50%,-50%) translateY(0); }
- 50%{ transform: translate(-50%,-50%) translateY(calc(var(--dy, calc(var(--s) * 8px)) * -1)); }
- }
- .icon-1{ animation-duration: 3.2s; animation-delay: -0.2s; }
- .icon-2{ animation-duration: 3.8s; animation-delay: -1.1s; }
- .icon-3{ animation-duration: 3.4s; animation-delay: -2.0s; }
- .icon-4{ animation-duration: 4.2s; animation-delay: -2.7s; }
- .icon{
- animation-name: icon-float, icon-glow;
- animation-timing-function: ease-in-out, ease-in-out;
- animation-iteration-count: infinite, infinite;
- }
- .icon-1{ animation-duration: 3.2s, 2.6s; animation-delay: -0.2s, -0.8s; }
- .icon-2{ animation-duration: 3.8s, 3.1s; animation-delay: -1.1s, -1.4s; }
- .icon-3{ animation-duration: 3.4s, 2.8s; animation-delay: -2.0s, -2.2s; }
- .icon-4{ animation-duration: 4.2s, 3.5s; animation-delay: -2.7s, -3.0s; }
- @keyframes icon-glow{
- 0%,100%{ filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.16)); }
- 50%{ filter: drop-shadow(0 0 calc(var(--s) * 16px) rgba(43,220,255,0.36)); }
- }
- /* 右侧面板 */
- .right{
- min-width: 0;
- display:flex;
- justify-content:center;
- }
- .panel{
- width: min(calc(var(--s) * 680px), 30vw);
- aspect-ratio: 900 / 680;
- position: relative;
- background: url("~@/assets/panel_frame.png") center/100% 100% no-repeat;
- display:flex;
- justify-content:center;
- align-items:center;
- }
- /* 用绝对比例控制内容区域 */
- .panel-inner{
- width: 88%; /* 输入框主宽度比例 */
- display:flex;
- flex-direction:column;
- align-items:center;
- }
- .panel-title{
- width: 85%;
- font-size: var(--fs-title);
- color: rgba(220,250,255,0.92);
- margin-bottom: calc(var(--s) * 30px);
- font-weight: 600;
- }
- .field{
- width: 85%;
- height: calc(var(--s) * 54px);
- display:flex;
- align-items:center;
- gap: calc(var(--s) * 10px);
- padding: 0 calc(var(--s) * 16px);
- background: linear-gradient(270deg, rgba(26,117,255,0.11) 0%, rgba(71,120,255,0.07) 100%);
- border-radius: calc(var(--s) * 8px);
- border: calc(var(--s) * 1px) solid #3D72B8;
- margin-bottom: calc(var(--s) * 18px);
- }
- .field{
- position: relative;
- transition: box-shadow .22s ease, border-color .22s ease, background .22s ease;
- }
- .field:focus-within{
- border-color: rgba(90, 220, 255, 0.95);
- background: linear-gradient(270deg, rgba(26,117,255,0.16) 0%, rgba(71,120,255,0.10) 100%);
- box-shadow:
- 0 0 0 calc(var(--s) * 1px) rgba(110, 230, 255, 0.55),
- 0 0 calc(var(--s) * 18px) rgba(43,220,255,0.28),
- inset 0 0 calc(var(--s) * 14px) rgba(120,220,255,0.18);
- }
- /* 可选:让左侧小图标也略微被点亮 */
- .field:focus-within .i{
- opacity: 1;
- filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.35));
- }
- .field-label{
- flex: 0 0 calc(var(--s) * 44px); /* “账号/密码/验证码”宽度固定 */
- color: rgba(220,245,255,0.75);
- font-size: calc(var(--s) * 13px);
- letter-spacing: calc(var(--s) * 1px);
- margin-right: calc(var(--s) * 5px);
- user-select: none;
- }
- /* 关键:等比、居中、不拉伸 */
- .field .i{
- width: calc(var(--s) * 20px);
- height: calc(var(--s) * 20px);
- flex: 0 0 calc(var(--s) * 20px);
- object-fit: contain;
- opacity: 0.92;
- }
- .inp{
- flex:1;
- height: 100%;
- border: none;
- outline: none;
- background: transparent;
- color: rgba(235,255,255,0.92);
- font-size: calc(var(--s) * 14px);
- }
- .inp::placeholder{
- color: rgba(190,225,255,0.55);
- }
- .row{
- width:85%;
- display:flex;
- gap: calc(var(--s) * 14px);
- margin-bottom: calc(var(--s) * 10px);
- }
- .cap-field{
- flex: 0 0 61.13%;
- }
- .captcha{
- flex: 1;
- }
- .btn{
- width: 85%;
- height: calc(var(--s) * 54px);
- border: none;
- border-radius: calc(var(--s) * 8px);
- color: rgba(235,255,255,0.95);
- font-size: var(--fs-base);
- font-weight: 600;
- cursor: pointer;
- background: linear-gradient( 180deg, rgba(119,161,255,0) 0%, #77A1FF 100%);
- border-radius: calc(var(--s) * 8px);
- border: calc(var(--s) * 1px) solid rgba(161,190,255,0.7);
- }
- .btn:hover{ filter: brightness(1.08); }
- .hint{
- margin-top: calc(var(--s) * 12px);
- margin-bottom: calc(var(--s) * 12px);
- color: rgba(255, 0, 0, 0.92);
- font-size: var(--fs-base);
- width: 85%;
- text-align: left;
- }
- /* 响应式重排:窄屏时上下布局 */
- @media (max-width: calc(var(--s) * 980px)){
- .content{
- grid-template-columns: 1fr;
- padding: calc(var(--s) * 18px);
- align-items: start;
- }
- .ellipse-area{ width: 100%; height: 46vh; }
- .right{ justify-content: flex-start; }
- }
- .ai-cloud-ring{
- position: absolute;
- inset: 0;
- pointer-events: none;
- z-index: 1; /* 在 bg(0) 上面,但不压住 header/title 可再调 */
-
- /* ✅必须和 .bg 使用同一张图 + 同样的 size/position,才能精确对齐 */
- background-image: url('~@/assets/images/login-background.png'); /* ← 改成你真实路径 */
- background-repeat: no-repeat;
- background-size: cover;
- background-position: center center;
- /* === 你只需要调这三个:云彩环的中心点与半径 === */
- --cx: 27%; /* 云彩环中心 x(大概在左侧 1/4) */
- --cy: 58%; /* 云彩环中心 y(大概在中下) */
- --r1: calc(var(--s) * 145px); /* 内圈挖空半径(越大,AI 字越干净) */
- --r2: calc(var(--s) * 188px); /* 云彩环最亮区域半径 */
- --r3: calc(var(--s) * 288px); /* 外圈渐隐半径(越大,环越厚/越散) */
- /* ✅只保留“环形”区域:透明(内) -> 显示(环) -> 透明(外) */
- -webkit-mask-image: radial-gradient(circle at var(--cx) var(--cy),
- rgba(0,0,0,0) var(--r1),
- rgba(0,0,0,1) var(--r2),
- rgba(0,0,0,0) var(--r3)
- );
- mask-image: radial-gradient(circle at var(--cx) var(--cy),
- rgba(0,0,0,0) var(--r1),
- rgba(0,0,0,1) var(--r2),
- rgba(0,0,0,0) var(--r3)
- );
- /* ✅增强可见度(科技蓝) */
- mix-blend-mode: lighten;
- 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));
- /* ✅慢旋转 + 呼吸淡入淡出 */
- opacity: .35;
- transform-origin: var(--cx) var(--cy);
- animation: cloud-rotate 180s linear infinite, cloud-breathe 7.8s ease-in-out infinite;
- }
- @keyframes cloud-rotate{
- from{ transform: rotate(0deg); }
- to{ transform: rotate(360deg); }
- }
- @keyframes cloud-breathe{
- 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)); }
- 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)); }
- 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)); }
- }
- /* 开门层:覆盖在 bg 上方(z-index 0),不挡 content(content 是 1) */
- .split-door{
- position: absolute;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- opacity: 1;
- perspective: calc(var(--s) * 1200px);
- }
- .door{
- transform-origin: center;
- backface-visibility: hidden;
- }
- /* 两扇门:都用全屏做 cover,再裁半屏(关键:避免中间断裂) */
- .split-door .door{
- position: absolute;
- inset: 0;
- background: url("~@/assets/images/login-background.png") center/cover no-repeat; /* 跟 .bg 完全一致 */
- filter: brightness(1.03) contrast(1.04);
- will-change: transform;
- }
- /* 左半屏 */
- .split-door .door.left{
- clip-path: inset(0 50% 0 0);
- }
- /* 右半屏 */
- .split-door .door.right{
- clip-path: inset(0 0 0 50%);
- }
- /* 开门动作更慢更清晰 */
- .split-door.opening .door.left{
- animation: door-left 1.5s cubic-bezier(.18,.85,.22,1) forwards;
- }
- .split-door.opening .door.right{
- animation: door-right 1.5s cubic-bezier(.18,.85,.22,1) forwards;
- }
- .split-door.opening .door.left,
- .split-door.opening .door.right{
- animation-delay: .12s;
- }
- /* 中间能量缝(升级:更亮、更柔) */
- .split-door::after{
- content:"";
- position:absolute;
- left:50%;
- top:0;
- width:calc(var(--s) * 2px);
- height:100%;
- transform: translateX(-50%);
- opacity: 0;
- background: linear-gradient(to bottom,
- rgba(0,0,0,0),
- rgba(120,240,255,0.95),
- rgba(0,0,0,0)
- );
- filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(60,220,255,.85))
- drop-shadow(0 0 calc(var(--s) * 55px) rgba(60,220,255,.35));
- }
- .split-door.opening::after{
- opacity: 1;
- animation: seam-fade 0.95s ease forwards;
- }
- /* 可选:开门瞬间整体轻微“相机拉近”(更高级) */
- .split-door.opening{
- animation: camera-zoom 0.95s ease forwards;
- }
- @keyframes door-left{
- from{ transform: translateX(0) skewY(0deg); }
- to { transform: translateX(-54vw) skewY(-0.6deg); }
- }
- @keyframes door-right{
- from{ transform: translateX(0) skewY(0deg); }
- to { transform: translateX(54vw) skewY(0.6deg); }
- }
- @keyframes seam-fade{
- from{ opacity: 1; }
- to { opacity: 0; }
- }
- @keyframes camera-zoom{
- from{ transform: scale(1); }
- to { transform: scale(1.015); }
- }
- .page-dim{
- position: absolute;
- inset: 0;
- pointer-events: none;
- z-index: 999;
- background: #000;
- opacity: 0;
- }
- /* 开门时整体渐黑 */
- .page-dim.opening{
- animation: page-dim-in 1s ease forwards;
- }
- @keyframes page-dim-in{
- from{ opacity: 0; }
- to{ opacity: 0.95; } /* 想更暗就 0.95 */
- }
- /* 版权信息 */
- .copyright {
- display: flex;
- align-items: center;
- flex-direction: column;
- font-size: 18px;
- color: #FFF;
- line-height: 30px;
- text-align: center;
- position: absolute;
- bottom: 20px;
- left: 0;
- justify-content: center;
- width: 100%;
- }
- </style>
|