1296 lines
36 KiB
Vue
1296 lines
36 KiB
Vue
<template>
|
||
<div class="login-wrapper">
|
||
<!-- 左右分栏双面板容器 -->
|
||
<div class="login-split-container">
|
||
|
||
<!-- 左侧:明亮科技感极光大面板 -->
|
||
<div class="login-left-brand">
|
||
<!-- 方案D:化学分子科技网格插图 -->
|
||
<div class="brand-bg-pattern"></div>
|
||
|
||
<div class="brand-content-wrapper">
|
||
<!-- 顶部系统微标 -->
|
||
<div class="brand-mini-logo">
|
||
<div class="mini-logo-icon">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||
<path d="M2 17l10 5 10-5"/>
|
||
<path d="M2 12l10 5 10-5"/>
|
||
</svg>
|
||
</div>
|
||
<span class="mini-logo-text">华邦制药 · CTMS</span>
|
||
</div>
|
||
|
||
<!-- 中部标语区 -->
|
||
<div class="brand-slogan-block">
|
||
<h1 class="brand-main-title">
|
||
<span class="system-name">临床试验管理系统</span>
|
||
</h1>
|
||
<p class="brand-sub-desc">
|
||
全流程覆盖临床试验立项、执行、监查与归档,以规范化管理保障数据质量,助力研究团队高效推进项目合规落地。
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 特征卡片网格 -->
|
||
<div class="brand-features-grid">
|
||
<div class="feature-card delay-1">
|
||
<div class="feature-icon-wrapper">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M12 20h9M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>
|
||
</svg>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3 class="feature-title">项目全流程管理</h3>
|
||
<p class="feature-desc">覆盖立项审批、方案配置、人员分工到进度追踪,多中心协同操作,信息集中透明可查。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="feature-card delay-2">
|
||
<div class="feature-icon-wrapper">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||
</svg>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3 class="feature-title">合规核查与数据溯源</h3>
|
||
<p class="feature-desc">深度契合 GCP 与 NMPA 规范,记录完整操作轨迹与数据变更历史,确保每一条记录真实可追溯。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="feature-card delay-3">
|
||
<div class="feature-icon-wrapper">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M3 3v18h18M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"/>
|
||
</svg>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3 class="feature-title">偏差与安全事件管理</h3>
|
||
<p class="feature-desc">结构化记录研究偏差与不良事件,支持分级处理与闭环追踪,保障受试者安全与项目数据完整性。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 左侧底部声明 -->
|
||
<div class="brand-legal-info">
|
||
<p class="legal-item">Compliance with GCP & NMPA Regulations</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:极简登录面板 -->
|
||
<div class="login-right-form">
|
||
<!-- 兼容提示已去除 -->
|
||
|
||
<div class="login-card-container">
|
||
<!-- 顶部 Logo 组合 -->
|
||
<div class="login-brand-header">
|
||
<div class="brand-logo-icon">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||
<path d="M2 17l10 5 10-5"/>
|
||
<path d="M2 12l10 5 10-5"/>
|
||
</svg>
|
||
</div>
|
||
<div class="brand-divider"></div>
|
||
<span class="brand-system-text">CTMS</span>
|
||
</div>
|
||
|
||
<!-- 当前登录方式 -->
|
||
<div class="login-tabs">
|
||
<span class="tab-item active">账号登录</span>
|
||
</div>
|
||
|
||
<div v-if="showDesktopServerSettings" class="desktop-server-status">
|
||
<span class="desktop-server-label">服务器</span>
|
||
<code>{{ desktopServerUrl || "未配置" }}</code>
|
||
<RouterLink to="/desktop/server-settings" class="desktop-server-action">设置</RouterLink>
|
||
</div>
|
||
|
||
<!-- 退出通知 -->
|
||
<div v-if="logoutNotice" class="logout-notice" :class="'logout-notice--' + logoutNotice.type">
|
||
<div class="ln-icon">
|
||
<svg v-if="logoutNotice.type === 'warning'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"/><path d="M12 7v6"/><path d="M12 17h.01"/>
|
||
</svg>
|
||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M20 6 9 17l-5-5"/>
|
||
</svg>
|
||
</div>
|
||
<div class="ln-body">
|
||
<strong>{{ logoutNotice.title }}</strong>
|
||
<span>{{ logoutNotice.message }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 登录错误横幅 -->
|
||
<transition name="error-slide">
|
||
<div v-if="loginError" class="login-error-banner">
|
||
<div class="leb-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/>
|
||
</svg>
|
||
</div>
|
||
<div class="leb-body">
|
||
<strong>{{ loginError.title }}</strong>
|
||
<span v-if="loginError.message">{{ loginError.message }}</span>
|
||
</div>
|
||
<button class="leb-close" @click="loginError = null" aria-label="关闭">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M18 6 6 18M6 6l12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 登录表单 -->
|
||
<el-form ref="formRef" :model="form" :rules="rules" @keyup.enter="onSubmit" label-position="top" class="login-form">
|
||
<el-form-item label="账号" prop="email">
|
||
<div class="email-unified-box">
|
||
<input
|
||
id="email"
|
||
v-model="form.emailLocal"
|
||
type="text"
|
||
placeholder="请输入账号"
|
||
name="username"
|
||
autocomplete="username"
|
||
class="email-local-field"
|
||
@paste="handleAccountPaste"
|
||
/>
|
||
<span class="email-at-sign">@</span>
|
||
<div class="email-domain-wrapper">
|
||
<select
|
||
v-model="form.emailDomain"
|
||
class="email-domain-field"
|
||
aria-label="邮箱域名"
|
||
:disabled="availableEmailDomains.length === 0"
|
||
>
|
||
<option v-for="domain in availableEmailDomains" :key="domain" :value="domain">{{ domain }}</option>
|
||
</select>
|
||
<svg class="email-domain-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="密码" prop="password">
|
||
<el-input
|
||
id="password" v-model="form.password" type="password"
|
||
placeholder="请输入密码" show-password size="large"
|
||
name="ctms-login-password" autocomplete="new-password" class="login-input">
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<div class="login-options">
|
||
<el-checkbox v-model="form.agreeProtocol" class="protocol-checkbox">
|
||
<span class="protocol-text">
|
||
我已阅读并同意
|
||
<button type="button" class="protocol-link" @click.stop.prevent="openProtocolDialog">《用户协议》</button>
|
||
登录即代表同意当前协议
|
||
</span>
|
||
</el-checkbox>
|
||
</div>
|
||
|
||
<el-button type="primary" :loading="loading" @click="onSubmit" size="large" class="login-btn">
|
||
<span>登 录</span>
|
||
</el-button>
|
||
|
||
<!-- 忘记密码与注册 -->
|
||
<div class="forgot-register-row">
|
||
<RouterLink to="/forgot-password" class="forgot-link">忘记密码?</RouterLink>
|
||
<RouterLink to="/register" class="register-link">新用户注册</RouterLink>
|
||
</div>
|
||
</el-form>
|
||
|
||
<!-- 右侧底部客户支持与版权 -->
|
||
<div class="login-right-footer">
|
||
<div class="footer-row">
|
||
<span class="footer-label">技术开发:</span>
|
||
<a href="mailto:zhouchengcheng@huapont.cn" class="footer-link-text">医学事务部·周成成 (zhouchengcheng@huapont.cn)</a>
|
||
</div>
|
||
|
||
<div class="footer-row copyright-line">
|
||
<span class="lang-switch">CHINA(中国) <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg></span>
|
||
<span class="meta-sep">|</span>
|
||
<span class="copyright-text">© {{ currentYear }} 重庆华邦制药有限公司 版权所有</span>
|
||
<span class="meta-sep">|</span>
|
||
<span class="privacy-link">隐私政策</span>
|
||
</div>
|
||
|
||
<div class="footer-row compliance-line">
|
||
<span class="version-tag">v{{ appVersion }}</span>
|
||
<span class="meta-sep">|</span>
|
||
<span class="compliance-text">本系统依照 GCP 及 NMPA 规范管理数据</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 协议弹窗 -->
|
||
<el-dialog v-model="protocolDialogVisible" title="用户协议" width="860px"
|
||
class="protocol-dialog" append-to-body align-center>
|
||
<div class="protocol-content">
|
||
<section v-for="section in protocolSections" :key="section.title" class="protocol-section">
|
||
<h3>{{ section.title }}</h3>
|
||
<p v-for="p in section.paragraphs" :key="p">{{ p }}</p>
|
||
</section>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" size="large" class="protocol-confirm-btn" @click="confirmProtocol">确 定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref, onMounted, onUnmounted, watch } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||
// 从 package.json 读取版本号,构建时由 Vite 注入
|
||
const appVersion = (import.meta.env.VITE_APP_VERSION as string) || "1.0.0";
|
||
// 动态获取当前年份,避免硬编码
|
||
const currentYear = new Date().getFullYear();
|
||
import { useAuthStore } from "../store/auth";
|
||
import { useStudyStore } from "../store/study";
|
||
import { fetchEmailDomains } from "../api/auth";
|
||
import { TEXT, requiredMessage } from "../locales";
|
||
import { DESKTOP_SERVER_URL_CHANGED_EVENT, getDesktopServerUrl, isTauriRuntime } from "../runtime";
|
||
import {
|
||
consumeLogoutReason,
|
||
LOGOUT_REASON_AUTH_EXPIRED,
|
||
LOGOUT_REASON_MANUAL,
|
||
LOGOUT_REASON_TIMEOUT,
|
||
} from "../session/sessionManager";
|
||
import { authProtocolSections } from "../content/authProtocol";
|
||
|
||
const auth = useAuthStore();
|
||
const router = useRouter();
|
||
const AGREE_PROTOCOL_KEY = "ctms_agree_protocol";
|
||
|
||
const formRef = ref<FormInstance>();
|
||
const form = reactive({ email: "", emailLocal: "", emailDomain: "", password: "", agreeProtocol: false });
|
||
const configuredEmailDomains = ref<string[]>([]);
|
||
|
||
const rules: FormRules<typeof form> = {
|
||
email: [
|
||
{ required: true, message: requiredMessage(TEXT.modules.auth.emailLabel), trigger: "blur" },
|
||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||
],
|
||
password: [{ required: true, message: requiredMessage(TEXT.modules.auth.passwordLabel), trigger: "blur" }],
|
||
};
|
||
|
||
const loading = ref(false);
|
||
const protocolDialogVisible = ref(false);
|
||
const logoutNotice = ref<{ type: "info" | "warning"; title: string; message: string } | null>(null);
|
||
const loginError = ref<{ title: string; message?: string } | null>(null);
|
||
const protocolSections = authProtocolSections;
|
||
const showDesktopServerSettings = isTauriRuntime();
|
||
const desktopServerUrl = ref(getDesktopServerUrl());
|
||
|
||
const refreshDesktopServerUrl = () => {
|
||
desktopServerUrl.value = getDesktopServerUrl();
|
||
};
|
||
|
||
const normalizeDomain = (value: string) => value.trim().toLowerCase().replace(/^@/, "");
|
||
const availableEmailDomains = computed(() => configuredEmailDomains.value);
|
||
|
||
const syncEmailFromParts = () => {
|
||
const local = form.emailLocal.trim().toLowerCase();
|
||
const domain = normalizeDomain(form.emailDomain);
|
||
form.emailDomain = domain;
|
||
form.email = local && domain ? `${local}@${domain}` : "";
|
||
};
|
||
|
||
const applyEmailValue = (email: string) => {
|
||
const normalized = email.trim().toLowerCase();
|
||
const separator = normalized.lastIndexOf("@");
|
||
if (separator > 0) {
|
||
form.emailLocal = normalized.slice(0, separator);
|
||
const domain = normalizeDomain(normalized.slice(separator + 1));
|
||
form.emailDomain = configuredEmailDomains.value.includes(domain)
|
||
? domain
|
||
: configuredEmailDomains.value[0] || "";
|
||
} else {
|
||
form.emailLocal = normalized;
|
||
form.emailDomain = configuredEmailDomains.value[0] || "";
|
||
}
|
||
syncEmailFromParts();
|
||
};
|
||
|
||
const loadEmailDomains = async () => {
|
||
try {
|
||
const { data } = await fetchEmailDomains();
|
||
configuredEmailDomains.value = Array.from(new Set(
|
||
data.items.map(normalizeDomain).filter(Boolean)
|
||
));
|
||
if (!configuredEmailDomains.value.includes(form.emailDomain)) {
|
||
form.emailDomain = configuredEmailDomains.value[0] || "";
|
||
}
|
||
} catch {
|
||
configuredEmailDomains.value = [];
|
||
form.emailDomain = "";
|
||
}
|
||
};
|
||
|
||
const handleAccountPaste = (event: ClipboardEvent) => {
|
||
const pasted = event.clipboardData?.getData("text").trim() || "";
|
||
if (!pasted.includes("@")) return;
|
||
event.preventDefault();
|
||
applyEmailValue(pasted);
|
||
};
|
||
|
||
onMounted(async () => {
|
||
window.addEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, refreshDesktopServerUrl);
|
||
await loadEmailDomains();
|
||
const reason = consumeLogoutReason();
|
||
if (reason === LOGOUT_REASON_TIMEOUT) {
|
||
logoutNotice.value = { type: "warning", title: "已自动退出登录", message: "30 分钟未检测到任何操作。为保护项目数据,请重新登录后继续。" };
|
||
} else if (reason === LOGOUT_REASON_AUTH_EXPIRED) {
|
||
logoutNotice.value = { type: "warning", title: "登录状态已失效", message: "当前会话无法继续使用,请重新登录。" };
|
||
} else if (reason === LOGOUT_REASON_MANUAL) {
|
||
logoutNotice.value = { type: "info", title: "已安全退出", message: "本机登录状态已清除,需要时可再次登录。" };
|
||
}
|
||
applyEmailValue(localStorage.getItem("ctms_last_login_email") || "");
|
||
form.agreeProtocol = localStorage.getItem(AGREE_PROTOCOL_KEY) === "true";
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, refreshDesktopServerUrl);
|
||
});
|
||
|
||
watch(() => form.agreeProtocol, (checked) => localStorage.setItem(AGREE_PROTOCOL_KEY, String(checked)));
|
||
watch(() => [form.emailLocal, form.emailDomain], syncEmailFromParts);
|
||
|
||
const openProtocolDialog = () => { protocolDialogVisible.value = true; };
|
||
const confirmProtocol = () => { form.agreeProtocol = true; protocolDialogVisible.value = false; };
|
||
|
||
const onSubmit = async () => {
|
||
if (!formRef.value) return;
|
||
const valid = await formRef.value.validate();
|
||
if (!valid) return;
|
||
if (!form.agreeProtocol) { ElMessage.warning("请先阅读并同意用户协议"); return; }
|
||
loginError.value = null; // 清除上次错误
|
||
loading.value = true;
|
||
try {
|
||
await auth.login(form.email, form.password);
|
||
const studyStore = useStudyStore();
|
||
const userKey = auth.user?.email || form.email;
|
||
await studyStore.restoreStudyForUser(userKey, { preferActive: !!auth.user?.is_admin });
|
||
if (studyStore.currentStudy) {
|
||
router.push("/project/overview");
|
||
} else {
|
||
router.push(auth.user?.is_admin ? "/admin/users" : "/admin/projects");
|
||
}
|
||
} catch (error: any) {
|
||
const status = error?.response?.status;
|
||
const detail: string = error?.response?.data?.detail || error?.response?.data?.message || "";
|
||
// 根据状态码和后端信息做差异化映射
|
||
if (!status || status >= 500) {
|
||
loginError.value = { title: "服务暂时不可用", message: "请稍后重试,如持续出现请联系管理员" };
|
||
} else if (status === 429) {
|
||
loginError.value = { title: "操作过于频繁", message: "登录尝试次数过多,请稍后再试" };
|
||
} else if (status === 403 || detail.includes("未激活") || detail.includes("审核") || detail.includes("pending")) {
|
||
loginError.value = { title: "账号不可用", message: "请联系系统管理员确认账号状态" };
|
||
} else if (detail.includes("锁定") || detail.includes("locked") || detail.includes("disabled")) {
|
||
loginError.value = { title: "账号已被锁定", message: "请联系系统管理员解锁账号" };
|
||
} else if (status === 401) {
|
||
loginError.value = { title: "邮箱或密码不正确", message: "请检查后重新输入,多次失败可能导致账号锁定" };
|
||
} else {
|
||
loginError.value = { title: "登录失败", message: detail || TEXT.modules.auth.authFailed };
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@400;600;800&display=swap");
|
||
|
||
/* ═══════════════════════
|
||
根容器
|
||
═══════════════════════ */
|
||
.login-wrapper {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: stretch;
|
||
justify-content: stretch;
|
||
overflow: hidden;
|
||
background: #ffffff;
|
||
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||
}
|
||
|
||
.login-split-container {
|
||
display: flex;
|
||
width: 100vw;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ═══════════════════════
|
||
左侧:明亮极光科技面板
|
||
═══════════════════════ */
|
||
.login-left-brand {
|
||
flex: 1;
|
||
background: linear-gradient(135deg, #e0f2fe 0%, #e0e7ff 50%, #f3e8ff 100%);
|
||
padding: 60px 80px;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 渐变圆形球光效 */
|
||
.brand-glow-sphere {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(80px);
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
.sphere-1 {
|
||
width: 480px;
|
||
height: 480px;
|
||
background: rgba(99, 102, 241, 0.16);
|
||
top: 15%;
|
||
left: -10%;
|
||
animation: floatSphere1 20s infinite alternate ease-in-out;
|
||
}
|
||
|
||
.sphere-2 {
|
||
width: 420px;
|
||
height: 420px;
|
||
background: rgba(6, 182, 212, 0.14);
|
||
bottom: 10%;
|
||
right: -5%;
|
||
animation: floatSphere2 25s infinite alternate ease-in-out;
|
||
}
|
||
|
||
@keyframes floatSphere1 {
|
||
0% { transform: translate(0, 0) scale(1); }
|
||
100% { transform: translate(30px, 40px) scale(1.1); }
|
||
}
|
||
|
||
@keyframes floatSphere2 {
|
||
0% { transform: translate(0, 0) scale(1); }
|
||
100% { transform: translate(-40px, -30px) scale(1.08); }
|
||
}
|
||
|
||
.brand-bg-pattern {
|
||
position: absolute;
|
||
bottom: 5%;
|
||
right: -5%;
|
||
width: 440px;
|
||
height: 440px;
|
||
background-image: url("data:image/svg+xml,%3Csvg width='380' height='380' viewBox='0 0 380 380' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M60 120 L130 80 L200 120 L200 200 L130 240 L60 200 Z' stroke='rgba(99, 102, 241, 0.12)' stroke-width='1.2'/%3E%3Cpath d='M200 120 L270 80 L340 120 L340 200 L270 240 L200 200' stroke='rgba(6, 182, 212, 0.1)' stroke-width='1.2'/%3E%3Ccircle cx='60' cy='120' r='4' fill='rgba(99, 102, 241, 0.25)'/%3E%3Ccircle cx='60' cy='120' r='8' stroke='rgba(99, 102, 241, 0.08)' stroke-width='1'/%3E%3Ccircle cx='130' cy='80' r='3' fill='rgba(99, 102, 241, 0.18)'/%3E%3Ccircle cx='200' cy='120' r='4' fill='rgba(99, 102, 241, 0.25)'/%3E%3Ccircle cx='200' cy='200' r='3' fill='rgba(99, 102, 241, 0.18)'/%3E%3Ccircle cx='130' cy='240' r='4' fill='rgba(99, 102, 241, 0.2)'/%3E%3Ccircle cx='60' cy='200' r='3' fill='rgba(99, 102, 241, 0.15)'/%3E%3Ccircle cx='270' cy='80' r='4' fill='rgba(6, 182, 212, 0.2)'/%3E%3Ccircle cx='340' cy='120' r='5' fill='rgba(6, 182, 212, 0.25)'/%3E%3Ccircle cx='340' cy='120' r='10' stroke='rgba(6, 182, 212, 0.08)' stroke-width='1'/%3E%3Ccircle cx='340' cy='200' r='3' fill='rgba(6, 182, 212, 0.15)'/%3E%3Ccircle cx='270' cy='240' r='4' fill='rgba(6, 182, 212, 0.2)'/%3E%3Cpath d='M130 80 L130 30 M270 80 L270 30' stroke='rgba(99, 102, 241, 0.08)' stroke-width='1.2' stroke-dasharray='2 2'/%3E%3Ccircle cx='130' cy='30' r='2' fill='rgba(99, 102, 241, 0.12)'/%3E%3Ccircle cx='270' cy='30' r='2' fill='rgba(6, 182, 212, 0.12)'/%3E%3C/svg%3E");
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
opacity: 0.8;
|
||
animation: bgPatternFloat 30s infinite alternate ease-in-out;
|
||
}
|
||
|
||
@keyframes bgPatternFloat {
|
||
0% { transform: translate(0, 0) rotate(0deg); }
|
||
100% { transform: translate(-10px, 15px) rotate(2deg); }
|
||
}
|
||
|
||
.brand-content-wrapper {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 迷你Logo */
|
||
.brand-mini-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.mini-logo-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 55px;
|
||
height: 55px;
|
||
background: #2563eb;
|
||
border-radius: 14px;
|
||
color: white;
|
||
}
|
||
|
||
.mini-logo-text {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* Slogan 区 */
|
||
.brand-slogan-block {
|
||
margin: 32px 0 0;
|
||
max-width: 520px;
|
||
}
|
||
|
||
.brand-main-title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
font-size: 38px;
|
||
font-weight: 800;
|
||
line-height: 1.25;
|
||
letter-spacing: -1px;
|
||
margin: 0 0 20px;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.company-name {
|
||
background: linear-gradient(135deg, #1d4ed8, #0891b2);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.system-name {
|
||
color: #0f172a;
|
||
}
|
||
|
||
.brand-sub-desc {
|
||
font-size: 15px;
|
||
color: #64748b;
|
||
line-height: 1.8;
|
||
margin: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 特征卡片网格 */
|
||
.brand-features-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
margin: 32px 0;
|
||
width: 100%;
|
||
max-width: 520px;
|
||
}
|
||
|
||
.feature-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
padding: 18px 22px;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||
border-radius: 16px;
|
||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||
cursor: default;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
animation: cardFadeIn 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||
}
|
||
|
||
.feature-card.delay-1 { animation-delay: 0.15s; }
|
||
.feature-card.delay-2 { animation-delay: 0.3s; }
|
||
.feature-card.delay-3 { animation-delay: 0.45s; }
|
||
|
||
@keyframes cardFadeIn {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.feature-card:hover {
|
||
background: rgba(255, 255, 255, 0.55);
|
||
border-color: rgba(37, 99, 235, 0.22);
|
||
transform: translateY(-3px);
|
||
box-shadow:
|
||
0 12px 30px -10px rgba(37, 99, 235, 0.08),
|
||
0 4px 12px -2px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.feature-icon-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
background: rgba(37, 99, 235, 0.06);
|
||
border-radius: 10px;
|
||
color: #2563eb;
|
||
flex-shrink: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.feature-card:hover .feature-icon-wrapper {
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.35);
|
||
}
|
||
|
||
.feature-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.feature-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
margin: 0;
|
||
line-height: 1.4;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.feature-card:hover .feature-title {
|
||
color: #0f172a;
|
||
}
|
||
|
||
.feature-desc {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
line-height: 1.6;
|
||
margin: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 底部声明 */
|
||
.brand-legal-info {
|
||
border-top: 1px solid rgba(148, 163, 184, 0.15);
|
||
padding-top: 24px;
|
||
}
|
||
|
||
.legal-item {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 11px;
|
||
color: #94a3b8;
|
||
font-weight: 600;
|
||
letter-spacing: 1px;
|
||
text-transform: uppercase;
|
||
margin: 0;
|
||
}
|
||
|
||
/* ═══════════════════════
|
||
右侧:极简白底登录面板
|
||
═══════════════════════ */
|
||
.login-right-form {
|
||
flex: 1;
|
||
background: #ffffff;
|
||
padding: 60px 80px;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 上方浏览器兼容建议 */
|
||
.browser-tip {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 90%;
|
||
max-width: 600px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.download-link {
|
||
color: #2563eb;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
}
|
||
.download-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 卡片容器 */
|
||
.login-card-container {
|
||
width: clamp(480px, 34vw, 560px);
|
||
max-width: calc(100vw - 40px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 顶部品牌Logo */
|
||
.login-brand-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.brand-logo-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 34px;
|
||
height: 34px;
|
||
background: #2563eb;
|
||
border-radius: 9px;
|
||
color: white;
|
||
}
|
||
|
||
.brand-divider {
|
||
width: 1.5px;
|
||
height: 18px;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.brand-system-text {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
color: #1e293b;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
/* Tabs */
|
||
.login-tabs {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 36px;
|
||
border-bottom: 1.5px solid #f1f5f9;
|
||
}
|
||
|
||
.tab-item {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
padding-bottom: 10px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
transition: color 0.2s;
|
||
user-select: none;
|
||
}
|
||
|
||
.tab-item.active {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.tab-item.active::after {
|
||
content: "";
|
||
position: absolute;
|
||
bottom: -1.5px;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 2px;
|
||
background: #2563eb;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.desktop-server-status {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin: -18px 0 24px;
|
||
padding: 10px 12px;
|
||
border: 1px solid #dbe7f5;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.desktop-server-label {
|
||
color: #64748b;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.desktop-server-status code {
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
color: #1e293b;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||
}
|
||
|
||
.desktop-server-action {
|
||
color: #2563eb;
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.desktop-server-action:hover {
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
/* ═══════════════════════
|
||
通知与错误提示
|
||
═══════════════════════ */
|
||
.logout-notice {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
margin-bottom: 24px;
|
||
padding: 12px 14px;
|
||
border-radius: 10px;
|
||
border: 1px solid;
|
||
}
|
||
|
||
.logout-notice--warning {
|
||
color: #b45309;
|
||
background: #fffbeb;
|
||
border-color: #fde68a;
|
||
}
|
||
|
||
.logout-notice--info {
|
||
color: #047857;
|
||
background: #f0fdf4;
|
||
border-color: #bbf7d0;
|
||
}
|
||
|
||
.ln-icon { width: 16px; height: 16px; flex-shrink: 0; margin-top: 1px; }
|
||
.ln-icon svg { width: 100%; height: 100%; }
|
||
.ln-body { display: flex; flex-direction: column; gap: 2px; }
|
||
.ln-body strong { font-size: 12px; font-weight: 700; }
|
||
.ln-body span { font-size: 11px; opacity: 0.9; }
|
||
|
||
.login-error-banner {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
padding: 12px 14px;
|
||
border-radius: 10px;
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.leb-icon { width: 16px; height: 16px; flex-shrink: 0; margin-top: 1px; }
|
||
.leb-icon svg { width: 100%; height: 100%; }
|
||
.leb-body { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||
.leb-body strong { font-size: 12px; font-weight: 700; }
|
||
.leb-body span { font-size: 11px; opacity: 0.9; }
|
||
.leb-close {
|
||
flex-shrink: 0;
|
||
appearance: none;
|
||
background: none;
|
||
border: none;
|
||
padding: 2px;
|
||
cursor: pointer;
|
||
color: #f87171;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.leb-close:hover { color: #b91c1c; }
|
||
.leb-close svg { width: 12px; height: 12px; }
|
||
|
||
/* ═══════════════════════
|
||
表单输入框 (下划线风格)
|
||
═══════════════════════ */
|
||
.login-form :deep(.el-form-item) {
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.login-form :deep(.el-form-item__label) {
|
||
font-weight: 600;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
padding-bottom: 6px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.login-form :deep(.el-form-item__error) {
|
||
margin-top: 6px;
|
||
font-size: 11px;
|
||
color: #ef4444;
|
||
}
|
||
|
||
.login-input :deep(.el-input__wrapper) {
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
border-radius: 0 !important;
|
||
border-bottom: 1.5px solid #e2e8f0;
|
||
padding: 6px 0 !important;
|
||
transition: border-color 0.25s ease;
|
||
}
|
||
|
||
.login-input :deep(.el-input__wrapper:hover) {
|
||
border-bottom-color: #cbd5e1;
|
||
}
|
||
|
||
.login-input :deep(.el-input__wrapper.is-focus) {
|
||
border-bottom-color: #2563eb !important;
|
||
}
|
||
|
||
.login-input :deep(.el-input__inner) {
|
||
height: 38px;
|
||
font-size: 15px;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.login-input :deep(.el-input__inner::placeholder) {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
/* 账号与邮箱域名组合输入 */
|
||
.email-unified-box {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
border-bottom: 1.5px solid #e2e8f0;
|
||
transition: border-color 0.25s ease;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.email-unified-box:focus-within {
|
||
border-bottom-color: #2563eb;
|
||
}
|
||
|
||
.email-local-field,
|
||
.email-domain-field {
|
||
min-width: 0;
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
font-family: inherit;
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.email-local-field {
|
||
flex: 1;
|
||
color: #0f172a;
|
||
padding: 6px 0;
|
||
}
|
||
|
||
.email-local-field::placeholder {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.email-at-sign {
|
||
flex-shrink: 0;
|
||
padding: 0 4px;
|
||
color: #64748b;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
user-select: none;
|
||
}
|
||
|
||
.email-domain-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
max-width: 55%;
|
||
}
|
||
|
||
.email-domain-field {
|
||
width: auto;
|
||
max-width: 100%;
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
color: #1e3a5f;
|
||
font-weight: 600;
|
||
padding: 6px 22px 6px 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.email-domain-field:disabled {
|
||
color: #94a3b8;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.email-domain-chevron {
|
||
position: absolute;
|
||
right: 2px;
|
||
top: 50%;
|
||
width: 14px;
|
||
height: 14px;
|
||
color: #94a3b8;
|
||
pointer-events: none;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
/* 协议区 */
|
||
.login-options {
|
||
margin: 4px 0 24px;
|
||
}
|
||
|
||
.protocol-checkbox :deep(.el-checkbox__input .el-checkbox__inner) {
|
||
border-color: #cbd5e1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.protocol-checkbox :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||
background: #2563eb;
|
||
border-color: #2563eb;
|
||
}
|
||
|
||
.protocol-checkbox :deep(.el-checkbox__label) {
|
||
padding-left: 8px;
|
||
white-space: normal;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.protocol-text {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.protocol-link {
|
||
appearance: none;
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
font: inherit;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
color: #2563eb;
|
||
}
|
||
.protocol-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 按钮 */
|
||
.login-btn {
|
||
width: 100%;
|
||
height: 48px;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
background: #2563eb;
|
||
border: none;
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.login-btn:hover {
|
||
background: #1d4ed8;
|
||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.3);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.login-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 注册与忘记密码左右分开 */
|
||
.forgot-register-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.forgot-link, .register-link {
|
||
font-size: 13px;
|
||
color: #2563eb;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
transition: color 0.15s;
|
||
}
|
||
|
||
.forgot-link:hover, .register-link:hover {
|
||
color: #1d4ed8;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ═══════════════════════
|
||
右侧底部页脚 (支持与版权)
|
||
═══════════════════════ */
|
||
.login-right-footer {
|
||
margin-top: 40px;
|
||
border-top: 1px solid #f1f5f9;
|
||
padding-top: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.footer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
font-size: 11px;
|
||
color: #94a3b8;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.footer-label {
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
}
|
||
|
||
.footer-link-text {
|
||
color: #64748b;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.footer-link-text:hover {
|
||
text-decoration: underline;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.copyright-line {
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.lang-switch {
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
.meta-sep {
|
||
color: #e2e8f0;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.privacy-link {
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
.privacy-link:hover {
|
||
color: #475569;
|
||
}
|
||
|
||
.compliance-line {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.version-tag {
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
}
|
||
|
||
.compliance-text {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
/* ═══════════════════════
|
||
协议弹窗
|
||
═══════════════════════ */
|
||
.protocol-content {
|
||
max-height: 58vh;
|
||
overflow-y: auto;
|
||
padding: 2px 8px 2px 0;
|
||
}
|
||
|
||
.protocol-section { margin-bottom: 26px; }
|
||
.protocol-section:last-child { margin-bottom: 0; }
|
||
.protocol-section h3 { margin: 0 0 14px; font-size: 18px; font-weight: 800; line-height: 1.4; color: #111827; }
|
||
.protocol-section p { margin: 0 0 10px; font-size: 15px; font-weight: 400; line-height: 1.8; color: #6b7280; }
|
||
.protocol-section p:last-child { margin-bottom: 0; }
|
||
|
||
.protocol-confirm-btn {
|
||
min-width: 112px;
|
||
height: 44px;
|
||
border-radius: 22px;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
letter-spacing: 2px;
|
||
background: #2563eb;
|
||
border: none;
|
||
box-shadow: 0 8px 20px -4px rgba(37, 99, 235, 0.4);
|
||
}
|
||
.protocol-confirm-btn:hover {
|
||
background: #1d4ed8;
|
||
}
|
||
|
||
:global(.protocol-dialog .el-dialog) {
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
box-shadow: 0 24px 64px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
:global(.protocol-dialog .el-dialog__header) {
|
||
padding: 24px 28px 12px;
|
||
margin-right: 0;
|
||
background: #fafafa;
|
||
}
|
||
|
||
:global(.protocol-dialog .el-dialog__title) {
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: #111827;
|
||
}
|
||
|
||
:global(.protocol-dialog .el-dialog__body) { padding: 22px 36px 10px; }
|
||
:global(.protocol-dialog .el-dialog__footer) { padding: 18px 28px 24px; }
|
||
|
||
/* ═══════════════════════
|
||
响应式断点适配
|
||
═══════════════════════ */
|
||
|
||
/* 大屏保持 1:1 分栏,通过扩大有效内容区改善内容密度 */
|
||
@media (min-width: 1440px) {
|
||
.login-left-brand {
|
||
flex: 1;
|
||
padding: clamp(64px, 5vw, 96px);
|
||
}
|
||
|
||
.login-right-form {
|
||
flex: 1;
|
||
padding: clamp(64px, 6vw, 112px);
|
||
}
|
||
|
||
.brand-slogan-block,
|
||
.brand-features-grid {
|
||
max-width: 640px;
|
||
}
|
||
|
||
.brand-main-title {
|
||
font-size: clamp(38px, 2.4vw, 48px);
|
||
}
|
||
|
||
.brand-sub-desc {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.feature-card {
|
||
padding: 20px 24px;
|
||
}
|
||
|
||
.login-card-container {
|
||
max-width: 440px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.login-left-brand {
|
||
display: none;
|
||
}
|
||
.login-right-form {
|
||
padding: 40px 24px;
|
||
width: 100%;
|
||
}
|
||
.browser-tip {
|
||
display: none;
|
||
}
|
||
.login-card-container {
|
||
max-width: 440px;
|
||
}
|
||
.login-right-footer {
|
||
margin-top: 40px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.login-brand-header {
|
||
margin-bottom: 28px;
|
||
}
|
||
.login-tabs {
|
||
margin-bottom: 24px;
|
||
}
|
||
.login-right-footer {
|
||
margin-top: 32px;
|
||
}
|
||
}
|
||
</style>
|