|
|
@@ -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>
|