Просмотр исходного кода

登录页:表单抽到 LoginForm.vue 组件 + 记住密码 + 表单校验(字段级红边/inline错),接口错误回到按钮上方独立提示;修复登录失败被 401 拦截器整页 reload 抹掉错误的问题

画安 месяцев назад: 2
Родитель
Сommit
2173b1bcb7
3 измененных файлов с 344 добавлено и 365 удалено
  1. 336 207
      src/components/ui/LoginForm.vue
  2. 3 2
      src/utils/request.js
  3. 5 156
      src/views/Login.vue

+ 336 - 207
src/components/ui/LoginForm.vue

@@ -1,247 +1,376 @@
 <template>
-    <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" v-if="showCaptcha">
-                <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 class="captcha-img" v-model="captchaCode" />
-            </div>
-
-            <div class="hint" v-if="hint">{{ hint }}</div>
-            <button class="btn" @click="onLogin">立即登录</button>
+  <div class="login-form">
+    <div class="field-wrap">
+      <div class="field" :class="{ 'is-error': errors.username }">
+        <img class="field-icon" src="@/assets/i_user.png" alt="" />
+        <span class="field-label">用户名</span>
+        <input
+          class="inp"
+          v-model.trim="username"
+          @input="clearError('username')"
+          @keyup.enter="onLogin"
+        />
+      </div>
+      <div class="field-error" v-if="getErrorText('username')">{{ getErrorText('username') }}</div>
+    </div>
+
+    <div class="field-wrap">
+      <div class="field" :class="{ 'is-error': errors.password }">
+        <img class="field-icon" src="@/assets/i_lock.png" alt="" />
+        <span class="field-label">密码</span>
+        <input
+          class="inp"
+          type="password"
+          v-model.trim="password"
+          @input="clearError('password')"
+          @keyup.enter="onLogin"
+        />
+      </div>
+      <div class="field-error" v-if="getErrorText('password')">{{ getErrorText('password') }}</div>
+    </div>
+
+    <div class="field-wrap" v-if="showCaptcha">
+      <div class="field-row">
+        <div class="field cap-field" :class="{ 'is-error': errors.captcha }">
+          <img class="field-icon" src="@/assets/i_captcha.png" alt="" />
+          <span class="field-label">验证码</span>
+          <input
+            class="inp"
+            v-model.trim="captchaInput"
+            @input="clearError('captcha')"
+            @keyup.enter="onLogin"
+          />
         </div>
+        <CaptchaCanvas class="captcha-canvas" v-model="captchaCode" />
+      </div>
+      <div class="field-error" v-if="getErrorText('captcha')">{{ getErrorText('captcha') }}</div>
+    </div>
+
+    <div class="form-extras">
+      <label class="remember">
+        <input type="checkbox" v-model="rememberMe" />
+        <span class="remember-box"></span>
+        <span class="remember-text">记住密码</span>
+      </label>
     </div>
+
+    <div class="hint" v-if="hint">{{ hint }}</div>
+
+    <button class="btn-login" @click="onLogin" :disabled="loading">
+      {{ loading ? '登录中...' : '登录' }}
+    </button>
+  </div>
 </template>
 
 <script>
 import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
 import { apiLogin } from "@/api";
 
+const REMEMBER_KEY = "login.remembered";
+// 标记字段已出错但消息文字显示在另一个字段下(例如账号密码错误,两个框都标红,文字只在密码下显示一次)
+const ERROR_BORDER_ONLY = "_";
+
 export default {
-    name: "LoginForm",
-    components: { CaptchaCanvas },
-    data() {
-        return {
-            username: "admin",
-            password: "123456",
-            captchaCode: "",
-            captchaInput: "",
-            hint: "",
-            loginFailedCount: Number(sessionStorage.getItem("loginFailedCount")) || 0,
-        };
+  name: "LoginForm",
+  components: { CaptchaCanvas },
+  data() {
+    return {
+      username: "",
+      password: "",
+      captchaCode: "",
+      captchaInput: "",
+      rememberMe: false,
+      loading: false,
+      hint: "",   // 接口返回的登录错误(账号密码错/服务异常等),独立显示在按钮上方
+      errors: { username: "", password: "", captcha: "" },
+      loginFailedCount: Number(sessionStorage.getItem("loginFailedCount")) || 0,
+    };
+  },
+  computed: {
+    showCaptcha() {
+      return this.loginFailedCount >= 3;
+    },
+  },
+  created() {
+    this.loadRemembered();
+  },
+  methods: {
+    loadRemembered() {
+      try {
+        const raw = localStorage.getItem(REMEMBER_KEY);
+        if (!raw) {
+          this.username = "admin";
+          this.password = "123456";
+          return;
+        }
+        const obj = JSON.parse(atob(raw));
+        if (obj && obj.u) {
+          this.username = obj.u;
+          this.password = obj.p || "";
+          this.rememberMe = true;
+        }
+      } catch (e) {
+        this.username = "admin";
+        this.password = "123456";
+      }
+    },
+    saveRemembered() {
+      try {
+        const payload = btoa(JSON.stringify({ u: this.username, p: this.password }));
+        localStorage.setItem(REMEMBER_KEY, payload);
+      } catch (e) {
+        // ignore
+      }
     },
-    computed: {
-        showCaptcha() {
-            return this.loginFailedCount >= 3;
-        },
+    clearRemembered() {
+      localStorage.removeItem(REMEMBER_KEY);
     },
-    methods: {
-        async onLogin() { console.log('do login')
-            this.hint = "";
-
-            if (this.showCaptcha) {
-                if (!this.captchaInput) {
-                    this.hint = "请输入验证码";
-                    return;
-                }
-                if (this.captchaInput.toLowerCase() !== this.captchaCode.toLowerCase()) {
-                    this.hint = "验证码错误";
-                    this.captchaInput = "";
-                    return;
-                }
-            }
-
-            let data;
-            try {
-                data = await apiLogin({
-                    username: this.username,
-                    password: this.password,
-                    captcha: this.captchaInput,
-                });
-
-                this.loginFailedCount = 0;
-                sessionStorage.removeItem("loginFailedCount");
-
-                this.$router.push("/transition");
-            } catch (e) {
-                this.hint = e.message || "登录失败";
-                this.loginFailedCount++;
-                sessionStorage.setItem("loginFailedCount", this.loginFailedCount);
-                this.captchaInput = "";
-                return;
-            }
-
-            localStorage.setItem("token", data.token);
-            this.$emit("login-success");
-        },
+    clearError(field) {
+      if (this.errors[field]) this.$set(this.errors, field, "");
+      // 凭证错误是用户名+密码同时标红,改任一字段都把两个一起清掉,并清掉接口错误提示
+      if (field === "username" || field === "password") {
+        if (this.errors.username === ERROR_BORDER_ONLY) this.$set(this.errors, "username", "");
+        if (this.errors.password === ERROR_BORDER_ONLY) this.$set(this.errors, "password", "");
+        if (this.hint) this.hint = "";
+      }
     },
+    getErrorText(field) {
+      const v = this.errors[field];
+      return v === ERROR_BORDER_ONLY ? "" : v;
+    },
+    validate() {
+      this.errors = { username: "", password: "", captcha: "" };
+      let ok = true;
+      if (!this.username) {
+        this.$set(this.errors, "username", "请输入用户名");
+        ok = false;
+      } else if (this.username.length < 2) {
+        this.$set(this.errors, "username", "用户名至少 2 个字符");
+        ok = false;
+      }
+      if (!this.password) {
+        this.$set(this.errors, "password", "请输入密码");
+        ok = false;
+      } else if (this.password.length < 6) {
+        this.$set(this.errors, "password", "密码至少 6 个字符");
+        ok = false;
+      }
+      if (this.showCaptcha) {
+        if (!this.captchaInput) {
+          this.$set(this.errors, "captcha", "请输入验证码");
+          ok = false;
+        } else if (this.captchaInput.toLowerCase() !== this.captchaCode.toLowerCase()) {
+          this.$set(this.errors, "captcha", "验证码错误");
+          this.captchaInput = "";
+          ok = false;
+        }
+      }
+      return ok;
+    },
+    async onLogin() {
+      if (this.loading) return;
+      this.hint = "";
+      if (!this.validate()) return;
+      this.loading = true;
+      try {
+        const data = await apiLogin({
+          username: this.username,
+          password: this.password,
+          captcha: this.captchaInput,
+        });
+        this.loginFailedCount = 0;
+        sessionStorage.removeItem("loginFailedCount");
+        localStorage.setItem("token", data.token);
+        if (this.rememberMe) this.saveRemembered();
+        else this.clearRemembered();
+        this.$emit("login-success", data);
+      } catch (e) {
+        const msg = e.message || "登录失败";
+        // 后端返回的消息里若明确提到验证码,标红验证码字段(仍走字段级 inline 错)
+        if (this.showCaptcha && /验证码/.test(msg)) {
+          this.$set(this.errors, "captcha", msg);
+          this.captchaInput = "";
+        } else {
+          // 账号或密码错误:两个输入框标红,错误消息回到原来位置(按钮上方独立 hint)
+          this.$set(this.errors, "username", ERROR_BORDER_ONLY);
+          this.$set(this.errors, "password", ERROR_BORDER_ONLY);
+          this.hint = msg;
+        }
+        this.loginFailedCount++;
+        sessionStorage.setItem("loginFailedCount", this.loginFailedCount);
+      } finally {
+        this.loading = false;
+      }
+    },
+  },
 };
 </script>
 
 <style scoped>
-/* ================== 表单主面板 ================== */
-.panel {
-    /* 严格按照设计稿尺寸 */
-    width: 637px;
-    height: 514px;
-    /* 调整内边距,给背景外框留出空间并让内容居中 */
-    padding: 60px 75px;
-    box-sizing: border-box;
-
-    background: url("~@/assets/panel_frame.png") center/100% 100% no-repeat;
-
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-
-    transform: scale(var(--s, 1));
-    /* 将缩放基准点设为右侧垂直居中。这样缩小的时候,表单会贴着右侧边距变小,不会往屏幕中间飘 */
-    transform-origin: right center;
+.login-form {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  width: 100%;
 }
 
-.panel-inner {
-    width: 100%;
-    display: flex;
-    flex-direction: column;
+/* 把行间距挪到 .field-wrap 上,让 .field-error 能贴在输入框正下方 */
+.field-wrap {
+  margin-bottom: 22px;
 }
 
-/* ================== 标题 ================== */
-.panel-title {
-    font-size: 28px;
-    /* 根据大面板放大标题 */
-    color: #ffffff;
-    margin-bottom: 35px;
-    font-weight: 600;
-    text-align: left;
-    letter-spacing: 2px;
-}
-
-/* ================== 输入框容器 ================== */
 .field {
-    width: 100%;
-    height: 58px;
-    /* 加高输入框以匹配大面板 */
-    display: flex;
-    align-items: center;
-    padding: 0 20px;
-
-    background: rgba(26, 117, 255, 0.12);
-    border-radius: 6px;
-    border: 1px solid rgba(71, 120, 255, 0.35);
-    margin-bottom: 26px;
-    /* 加大行距 */
-
-    position: relative;
-    transition: all 0.25s ease;
-    box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  height: 60px;
+  width: 100%;
+  padding: 0 20px;
+  background: rgba(255, 255, 255, 0.08);
+  border: 1px solid rgba(255, 255, 255, 0.15);
+  border-radius: 16px;
+  transition: border-color 0.2s, background 0.2s;
+  box-sizing: border-box;
 }
-
 .field:focus-within {
-    border-color: rgba(90, 220, 255, 0.95);
-    background: rgba(26, 117, 255, 0.22);
-    box-shadow:
-        0 0 14px rgba(43, 220, 255, 0.25),
-        inset 0 0 10px rgba(120, 220, 255, 0.15);
+  border-color: rgba(80, 180, 255, 0.5);
+  background: rgba(255, 255, 255, 0.12);
+}
+.field.is-error {
+  border-color: rgba(255, 77, 79, 0.7);
+  background: rgba(255, 77, 79, 0.08);
 }
 
-/* ================== 输入框内部元素 ================== */
-.field .i {
-    width: 22px;
-    height: 22px;
-    object-fit: contain;
-    opacity: 0.9;
+.field-icon {
+  width: 22px;
+  height: 22px;
+  object-fit: contain;
+  opacity: 0.7;
+  flex-shrink: 0;
+  margin-right: 10px;
 }
 
 .field-label {
-    color: rgba(220, 245, 255, 0.9);
-    font-size: 16px;
-    margin-left: 12px;
-    margin-right: 20px;
-    white-space: nowrap;
+  color: rgba(180, 215, 255, 0.75);
+  font-size: 16px;
+  white-space: nowrap;
+  flex-shrink: 0;
+  margin-right: 12px;
 }
 
 .inp {
-    flex: 1;
-    height: 100%;
-    border: none;
-    outline: none;
-    background: transparent;
-    color: #ffffff;
-    font-size: 16px;
+  flex: 1;
+  height: 100%;
+  border: none;
+  outline: none;
+  background: transparent;
+  color: #fff;
+  font-size: 16px;
 }
-
 .inp::placeholder {
-    color: rgba(190, 225, 255, 0.35);
-    font-size: 15px;
+  color: rgba(140, 180, 255, 0.3);
 }
 
-/* ================== 验证码同行布局 ================== */
-.row {
-    width: 100%;
-    display: flex;
-    gap: 16px;
-    margin-bottom: 26px;
+.field-row {
+  display: flex;
+  gap: 12px;
 }
-
 .cap-field {
-    flex: 1;
-    margin-bottom: 0;
-}
-
-.captcha-img {
-    width: 140px;
-    /* 放大的验证码图片宽度 */
-    height: 58px;
-    /* 与输入框等高 */
-    border-radius: 6px;
-    overflow: hidden;
-    border: 1px solid rgba(71, 120, 255, 0.35);
-}
-
-/* ================== 登录按钮 ================== */
-.btn {
-    width: 100%;
-    height: 58px;
-    margin-top: 10px;
-    color: #ffffff;
-    font-size: 26px;
-    letter-spacing: 4px;
-    cursor: pointer;
-    font-family: var(--title-font-family);
-    background: linear-gradient(180deg, rgba(119, 161, 255, 0) 0%, #77A1FF 100%);
-    border-radius: 8px;
-    border: 1px solid rgba(161, 190, 255, 0.7);
-    box-shadow: 0 4px 15px rgba(26, 117, 255, 0.3);
-    transition: all 0.2s ease;
-}
-
-.btn:hover {
-    filter: brightness(1.15);
-    box-shadow: 0 4px 20px rgba(26, 117, 255, 0.5);
-}
-
-/* ================== 提示文字 ================== */
+  flex: 1;
+}
+.captcha-canvas {
+  width: 120px;
+  height: 60px;
+  border-radius: 16px;
+  overflow: hidden;
+  border: 1px solid rgba(80, 160, 255, 0.35);
+  flex-shrink: 0;
+}
+
+.field-error {
+  color: #ff4d4f;
+  font-size: 13px;
+  line-height: 1.4;
+  margin-top: 6px;
+  padding-left: 4px;
+}
+
 .hint {
-    margin-top: -10px;
-    margin-bottom: 15px;
-    color: #ff4d4f;
-    font-size: 14px;
+  color: #ff4d4f;
+  font-size: 13px;
+  line-height: 1.4;
+  margin-bottom: 8px;
+  margin-top: -6px;
+}
+
+.form-extras {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 14px;
+  margin-top: -4px;
+}
+
+.remember {
+  display: inline-flex;
+  align-items: center;
+  cursor: pointer;
+  user-select: none;
+  color: rgba(180, 215, 255, 0.85);
+  font-size: 14px;
+}
+.remember input[type="checkbox"] {
+  position: absolute;
+  opacity: 0;
+  pointer-events: none;
+}
+.remember-box {
+  width: 16px;
+  height: 16px;
+  border-radius: 4px;
+  border: 1px solid rgba(140, 200, 255, 0.5);
+  background: rgba(255, 255, 255, 0.06);
+  margin-right: 8px;
+  position: relative;
+  transition: all 0.2s;
+}
+.remember input[type="checkbox"]:checked + .remember-box {
+  background: linear-gradient(180deg, #3dd4f8 0%, #0fa8e8 100%);
+  border-color: rgba(80, 180, 255, 0.8);
+}
+.remember input[type="checkbox"]:checked + .remember-box::after {
+  content: "";
+  position: absolute;
+  left: 4px;
+  top: 1px;
+  width: 5px;
+  height: 9px;
+  border: solid #fff;
+  border-width: 0 2px 2px 0;
+  transform: rotate(45deg);
+}
+.remember-text {
+  line-height: 1;
+}
+
+.btn-login {
+  width: 100%;
+  height: 64px;
+  background: linear-gradient(180deg, #3dd4f8 0%, #0fa8e8 50%, #0a90d8 100%);
+  border: none;
+  border-radius: 24px;
+  color: #fff;
+  font-size: 22px;
+  letter-spacing: 0.4em;
+  cursor: pointer;
+  transition: filter 0.2s;
+  flex-shrink: 0;
+  box-shadow: 0 4px 20px rgba(10, 160, 230, 0.5);
+}
+.btn-login:hover {
+  filter: brightness(1.1);
+}
+.btn-login:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
 }
-</style>
+</style>

+ 3 - 2
src/utils/request.js

@@ -71,8 +71,9 @@ class RequestHttp {
           console.error('Business Error:', data.message);
         }
 
-        // Token 过期
-        if (data.code === 401) {
+        // Token 过期:仅在非 skipGlobalError 的请求上触发整站跳登录页,
+        // 否则像登录接口(凭证错也是 401)会被强制 reload,覆盖掉本地 hint。
+        if (data.code === 401 && !config.skipGlobalError) {
           console.warn('Token已过期,请重新登录');
           localStorage.removeItem('token');
           window.location.href = '/login';

+ 5 - 156
src/views/Login.vue

@@ -25,34 +25,7 @@
           </div>
 
           <!-- 表单 -->
-          <div class="login-form">
-            <div class="field">
-              <img class="field-icon" src="@/assets/i_user.png" alt="" />
-              <span class="field-label">用户名</span>
-              <input class="inp" v-model.trim="username" placeholder="" />
-            </div>
-
-            <div class="field">
-              <img class="field-icon" src="@/assets/i_lock.png" alt="" />
-              <span class="field-label">密码</span>
-              <input class="inp" type="password" v-model.trim="password" placeholder="" />
-            </div>
-
-            <div class="field-row" v-if="showCaptcha">
-              <div class="field cap-field">
-                <img class="field-icon" src="@/assets/i_captcha.png" alt="" />
-                <span class="field-label">验证码</span>
-                <input class="inp" v-model.trim="captchaInput" placeholder="" />
-              </div>
-              <CaptchaCanvas class="captcha-canvas" v-model="captchaCode" />
-            </div>
-
-            <div class="hint" v-if="hint">{{ hint }}</div>
-
-            <button class="btn-login" @click="onLogin" :disabled="loading">
-              {{ loading ? '登录中...' : '登录' }}
-            </button>
-          </div>
+          <LoginForm @login-success="onLoginSuccess" />
         </div>
       </div>
 
@@ -68,13 +41,12 @@
 </template>
 
 <script>
-import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
-import { apiLogin } from "@/api";
+import LoginForm from "@/components/ui/LoginForm.vue";
 import brand from "@/utils/brand";
 
 export default {
   name: "LoginPage",
-  components: { CaptchaCanvas },
+  components: { LoginForm },
   created() {
     import('@/utils/cesiumPreloader').then(m => m.default.start());
   },
@@ -82,42 +54,11 @@ export default {
     return {
       brand,
       videoBgFailed: false,
-      username: "admin",
-      password: "123456",
-      captchaCode: "",
-      captchaInput: "",
-      hint: "",
-      loading: false,
-      loginFailedCount: Number(sessionStorage.getItem("loginFailedCount")) || 0,
     };
   },
-  computed: {
-    showCaptcha() { return this.loginFailedCount >= 3; },
-  },
   methods: {
-    async onLogin() {
-      this.hint = "";
-      if (this.showCaptcha) {
-        if (!this.captchaInput) { this.hint = "请输入验证码"; return; }
-        if (this.captchaInput.toLowerCase() !== this.captchaCode.toLowerCase()) {
-          this.hint = "验证码错误"; this.captchaInput = ""; return;
-        }
-      }
-      this.loading = true;
-      try {
-        const data = await apiLogin({ username: this.username, password: this.password, captcha: this.captchaInput });
-        this.loginFailedCount = 0;
-        sessionStorage.removeItem("loginFailedCount");
-        localStorage.setItem("token", data.token);
-        this.$router.push('/main').catch(() => {});
-      } catch (e) {
-        this.hint = e.message || "登录失败";
-        this.loginFailedCount++;
-        sessionStorage.setItem("loginFailedCount", this.loginFailedCount);
-        this.captchaInput = "";
-      } finally {
-        this.loading = false;
-      }
+    onLoginSuccess() {
+      this.$router.push('/main').catch(() => {});
     },
   },
 };
@@ -185,7 +126,6 @@ export default {
 
 .login-box {
   width: 566px;
-  height: 353px;
   background: transparent;
   border: none;
   box-shadow: none;
@@ -215,97 +155,6 @@ export default {
   text-transform: none;
 }
 
-.login-form {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-}
-
-.field {
-  display: flex;
-  align-items: center;
-  height: 60px;
-  width: 100%;
-  padding: 0 20px;
-  background: rgba(255, 255, 255, 0.08);
-  border: 1px solid rgba(255, 255, 255, 0.15);
-  border-radius: 16px;
-  margin-bottom: 36px;
-  transition: border-color 0.2s, background 0.2s;
-  box-sizing: border-box;
-}
-.field:focus-within {
-  border-color: rgba(80, 180, 255, 0.5);
-  background: rgba(255, 255, 255, 0.12);
-}
-
-.field-icon {
-  width: 22px;
-  height: 22px;
-  object-fit: contain;
-  opacity: 0.7;
-  flex-shrink: 0;
-  margin-right: 10px;
-}
-
-.field-label {
-  color: rgba(180, 215, 255, 0.75);
-  font-size: 16px;
-  white-space: nowrap;
-  flex-shrink: 0;
-  margin-right: 12px;
-}
-
-.inp {
-  flex: 1;
-  height: 100%;
-  border: none;
-  outline: none;
-  background: transparent;
-  color: #fff;
-  font-size: 16px;
-}
-.inp::placeholder { color: rgba(140, 180, 255, 0.3); }
-
-.field-row {
-  display: flex;
-  gap: 12px;
-  margin-bottom: 36px;
-}
-.cap-field { flex: 1; margin-bottom: 0; }
-.captcha-canvas {
-  width: 120px;
-  height: 60px;
-  border-radius: 16px;
-  overflow: hidden;
-  border: 1px solid rgba(80, 160, 255, 0.35);
-  flex-shrink: 0;
-}
-
-.hint {
-  color: #ff4d4f;
-  font-size: 13px;
-  margin-bottom: 8px;
-  margin-top: -24px;
-}
-
-.btn-login {
-  width: 100%;
-  height: 64px;
-  background: linear-gradient(180deg, #3dd4f8 0%, #0fa8e8 50%, #0a90d8 100%);
-  border: none;
-  border-radius: 24px;
-  color: #fff;
-  font-size: 22px;
-  letter-spacing: 0.4em;
-  cursor: pointer;
-  transition: filter 0.2s;
-  flex-shrink: 0;
-  box-shadow: 0 4px 20px rgba(10, 160, 230, 0.5);
-}
-.btn-login:hover  { filter: brightness(1.1); }
-.btn-login:disabled { opacity: 0.6; cursor: not-allowed; }
-
 .right-panel {
   height: 100%;
 }