style: refine login and register pages with green/blue themes and pill-shaped buttons

This commit is contained in:
zhen 2025-12-29 20:32:40 +08:00
parent afe2c38f74
commit ad0ab87747
3 changed files with 639 additions and 222 deletions

View File

@ -2,21 +2,22 @@
<div id="btn"> <div id="btn">
<button <button
class="submit-btn" class="submit-btn"
:disabled="props.disabled || props.loading" v-bind="attrs" :data-variant="props.variant"
:class="[ :disabled="props.disabled || props.loading"
`variant-${props.variant}`, v-bind="attrs"
{ 'is-loading': props.loading }
]"
> >
<!-- Loading Spinner -->
<span v-if="props.loading" class="spinner-icon"></span> <span v-if="props.loading" class="spinner-icon"></span>
<span :class="{ 'is-hidden': props.loading }"> <!-- Button Content -->
<span class="btn-text" :class="{ 'is-hidden': props.loading }">
<slot></slot> <slot></slot>
</span> </span>
<div class="iconBox" v-show="!props.loading"><slot name="icon"></slot></div>
<!-- Icon Slot -->
<div class="iconBox" v-if="$slots.icon && !props.loading">
<slot name="icon"></slot>
</div>
</button> </button>
</div> </div>
</template> </template>
@ -37,7 +38,8 @@ const props = defineProps({
'variant': { 'variant': {
type: String, type: String,
default: 'primary', default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger', 'text'].includes(value)
validator: (value) => ['primary', 'secondary', 'danger', 'text', 'pill', 'pill-green'].includes(value)
}, },
// disabled 便 // disabled 便
'disabled': { 'disabled': {
@ -57,153 +59,129 @@ const attrs = useAttrs()
<style scoped> <style scoped>
/* ======================================= */ /* ======================================= */
/* 基础样式和布局 */ /* Base Button Styling */
/* ======================================= */ /* ======================================= */
.submit-btn { .submit-btn {
margin-top: 10px; position: relative;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
/* 布局:使用 Flex 确保图标和文本对齐 */
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; /* 文本和图标之间的间距 */ gap: 8px;
padding: 12px 24px;
transition: all 0.2s, transform 0.1s; border: none;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 8px; /* Default */
color: white;
min-width: 100px;
} }
/* 交互状态 */
.submit-btn:active { .submit-btn:active {
transform: scale(0.98); transform: scale(0.96);
} }
.submit-btn:disabled, .submit-btn:disabled,
.submit-btn.is-loading { .submit-btn.is-loading {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed !important; cursor: not-allowed;
pointer-events: none; /* 禁用点击事件 */ pointer-events: none;
} }
/* ======================================= */ /* ======================================= */
/* 样式变体 (Variants) */ /* Style Variants */
/* ======================================= */ /* ======================================= */
/* --- 1. Primary (主色调) --- */ /* 1. Primary (Modern Blue) */
.variant-primary { .submit-btn[data-variant="primary"] {
background: #4f46e5; /* Indigo 品牌蓝 */ background: #3b82f6;
color: white;
} }
.variant-primary:hover { .submit-btn[data-variant="primary"]:hover {
background: #4338ca; background: #2563eb;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
} }
/* --- 2. Secondary (次要/灰色调) --- */ /* 2. Pill (Used for Login - Wider and Rounded) */
.variant-secondary { .submit-btn[data-variant="pill"] {
background: #e2e8f0; /* Slate 浅灰 */ background: #3b82f6 !important;
color: #1e293b; border-radius: 999px !important;
min-width: 340px !important;
height: 52px !important; /* Slightly taller for more impact */
font-size: 16px !important;
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.2) !important;
} }
.variant-secondary:hover { .submit-btn[data-variant="pill"]:hover {
background: #cbd5e1; background: #2563eb !important;
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
} }
/* --- 3. Danger (危险/红色调) --- */ /* 2.5 Pill-Green (Used for Register) */
.variant-danger { .submit-btn[data-variant="pill-green"] {
background: #ef4444; /* Red 红色 */ background: #10b981 !important;
color: white; border-radius: 999px !important;
min-width: 340px !important;
height: 52px !important;
font-size: 16px !important;
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.2) !important;
} }
.variant-danger:hover { .submit-btn[data-variant="pill-green"]:hover {
background: #059669 !important;
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.3);
}
/* 3. Secondary */
.submit-btn[data-variant="secondary"] {
background: #f1f5f9;
color: #475569;
}
.submit-btn[data-variant="secondary"]:hover {
background: #e2e8f0;
}
/* 4. Danger */
.submit-btn[data-variant="danger"] {
background: #ef4444;
}
.submit-btn[data-variant="danger"]:hover {
background: #dc2626; background: #dc2626;
} }
/* --- 4. Text (文本按钮/无背景) --- */ /* 5. Text */
.variant-text { .submit-btn[data-variant="text"] {
background: transparent; background: transparent;
color: #4f46e5; color: #3b82f6;
padding: 10px 12px; /* 减小 padding 以适应文本按钮 */
} }
.variant-text:hover { .submit-btn[data-variant="text"]:hover {
text-decoration: underline; background: rgba(59, 130, 246, 0.05);
background: transparent;
color: #4338ca;
}
/* 按钮基础样式 (假设你已有一些基础样式) */
.submit-btn {
position: relative; /* 确保 spinner-icon 可以相对于按钮定位 */
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
font-size: 16px;
border: none;
cursor: pointer;
transition: all 0.2s;
} }
/* ======================================= /* ======================================= */
1. 加载状态is-loading /* Internal Elements */
======================================= */ /* ======================================= */
.submit-btn.is-loading { .spinner-icon {
/* 视觉反馈:半透明、改变颜色或禁用 */ width: 1.25rem;
opacity: 0.8; height: 1.25rem;
cursor: not-allowed; border: 2px solid rgba(255, 255, 255, 0.3);
pointer-events: none; /* 确保点击穿透 */ border-top-color: #fff;
} border-radius: 50%;
animation: spin 0.8s linear infinite;
/* =======================================
2. 旋转图标样式spinner-icon
======================================= */
.submit-btn .spinner-icon {
display: inline-block;
width: 1em; /* 旋转图标的宽度 */
height: 1em; /* 旋转图标的高度 */
border: 2px solid currentColor; /* 边框颜色继承自按钮文本颜色 */
border-top-color: transparent; /* 顶部透明,形成旋转的缺口 */
border-radius: 50%;
animation: spin 1s linear infinite; /* 应用旋转动画 */
margin-right: 0.5em; /* 在图标和文本之间添加间距 */
} }
.iconBox { .iconBox {
display: flex; display: flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
/* 在加载状态且有文本时,清除右边距 */
.submit-btn.is-loading .spinner-icon {
/* 如果按钮内容被隐藏了,这个 margin 应该根据你的布局决定是否清除 */
margin-right: 0;
}
/* =======================================
3. 文本隐藏样式
======================================= */
.is-hidden { .is-hidden {
/* 隐藏文本,但不移除它,保持布局稳定 */ visibility: hidden;
visibility: hidden; opacity: 0;
height: 0;
width: 0;
margin: 0;
padding: 0;
overflow: hidden;
} }
/* =======================================
4. 旋转动画定义
======================================= */
@keyframes spin { @keyframes spin {
0% { to { transform: rotate(360deg); }
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
</style> </style>

View File

@ -1,42 +1,44 @@
<template> <template>
<div class="login-layout"> <div class="login-layout">
<div class="side-visual"> <div class="login-card">
<div class="visual-mask"></div> <div class="side-visual">
<div class="brand-container"> <div class="brand-container">
<h1 class="hero-title">Work<br>Together.</h1> <h1 class="hero-title">Work<br>Together.</h1>
<p class="hero-subtitle">下一代企业级即时通讯平台让沟通无距离</p> <p class="hero-subtitle">下一代企业级即时通讯平台让沟通无距离</p>
</div> </div>
<div class="visual-footer"> <div class="visual-footer">
<span>© 2025 IM System</span> <span>© 2025 IM System</span>
<div class="dots"> <div class="dots">
<span></span><span></span><span></span> <span></span><span></span><span></span>
</div>
</div> </div>
</div> </div>
</div>
<div class="side-form"> <div class="side-form">
<div class="form-wrapper"> <div class="form-wrapper">
<div class="welcome-header"> <div class="welcome-header">
<h2>账号登录</h2> <h2>账号登录</h2>
<p>请输入您的工作账号以继续</p> <p>请输入您的工作账号以继续</p>
</div> </div>
<form @submit.prevent="handleLogin"> <form @submit.prevent="handleLogin">
<IconInput class="input" <IconInput class="input"
placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/> placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/>
<IconInput class="input" <IconInput class="input"
placeholder="请输入密码" lab="密码" type="password" icon-name="user" v-model="form.password"/> placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
<MyButton class="loginBtn" :loading="loading"> <div class="login-btn-wrapper">
登录 <MyButton variant="pill" class="login-btn" :loading="loading">
<template #icon><i data-feather="arrow-right"></i></template> 登录
</MyButton> </MyButton>
</form> </div>
</form>
<div class="register-hint"> <div class="register-hint">
还没有账号? <a href="#">联系管理员注册</a> 还没有账号? <router-link to="/auth/register">立即注册</router-link>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -51,6 +53,7 @@ import { authService } from '@/services/auth'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import feather from 'feather-icons' import feather from 'feather-icons'
import IconInput from '@/components/IconInput.vue' import IconInput from '@/components/IconInput.vue'
import MyButton from '@/components/MyButton.vue'
import { required, maxLength, helpers } from '@vuelidate/validators' import { required, maxLength, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@ -77,27 +80,32 @@ const rules = {
}; };
const v$ = useVuelidate(rules,form); const v$ = useVuelidate(rules,form);
const handleLogin = async () => { const handleLogin = async () => {
if(!(await v$.value.$validate())){ const isFormCorrect = await v$.value.$validate()
message.error(v$.value.$errors[0].$message) if (!isFormCorrect) {
return; if (v$.value.$errors.length > 0) {
} message.error(v$.value.$errors[0].$message)
try{ }
loading.value = true; return
const res = await authService.login(form);
if(res.code === 0){
message.success('登陆成功。')
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
loading.value = false;
router.push('/index')
}else{
message.error(res.message)
loading.value = false;
}
}finally{
loading.value = false;
} }
try{
loading.value = true;
const res = await authService.login(form);
if(res.code === 0){ // Assuming 0 is success
message.success('登录成功')
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
router.push('/')
}else{
message.error(res.message || '登录失败')
}
} catch (e) {
console.error(e)
message.error('登录请求异常')
} finally{
loading.value = false;
}
} }
onMounted(() => { onMounted(() => {
@ -106,138 +114,214 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
:deep(.loginBtn) { /* Soft Mesh Gradient Background */
width: 100%;
}
:deep(.input){
width: 100%;
}
/* 撑满 Window 组件的内容区 */
.login-layout { .login-layout {
display: flex; display: flex;
width: 100%; align-items: center;
height: 100%; justify-content: center;
width: 100vw;
height: 100vh;
min-height: 100vh;
background-color: #f8fafc;
background-image:
radial-gradient(at 0% 0%, hsla(190, 100%, 95%, 1) 0, transparent 50%),
radial-gradient(at 50% 0%, hsla(160, 100%, 96%, 1) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(210, 100%, 96%, 1) 0, transparent 50%);
overflow: hidden;
position: relative;
}
/* Very subtle grid overlay */
.login-layout::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 20px 20px;
z-index: 0;
}
.login-card {
display: flex;
width: 1000px;
height: 600px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(30px);
border-radius: 32px;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.02),
0 40px 100px -20px rgba(0, 0, 0, 0.08);
overflow: hidden;
z-index: 10;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.7);
} }
/* --- 左侧视觉 --- */
.side-visual { .side-visual {
width: 42%; flex: 1;
background: linear-gradient(135deg, #4f46e5 0%, #3b82f6 100%); /* Soft connectivity gradient */
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
padding: 40px; padding: 60px;
color: white; color: white;
overflow: hidden; overflow: hidden;
} }
/* 增加一点背景纹理 */ /* Abstract "Connection" Circles */
.side-visual::before { .side-visual::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; left: 0; right: 0; bottom: 0; top: -20%; left: -20%;
background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px); width: 400px; height: 400px;
background-size: 20px 20px; border-radius: 50%;
opacity: 0.6; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
z-index: 1;
}
.side-visual::after {
content: '';
position: absolute;
bottom: -10%; right: -10%;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%);
z-index: 1;
} }
.brand-container { .brand-container {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
.hero-title { .hero-title {
font-size: 48px; font-size: 44px;
font-weight: 800; font-weight: 800;
line-height: 1.1; line-height: 1.2;
margin-bottom: 16px; margin-bottom: 20px;
letter-spacing: -1px; text-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.hero-subtitle {
font-size: 15px;
opacity: 0.9;
line-height: 1.6;
max-width: 300px;
} }
.hero-subtitle {
font-size: 16px;
opacity: 0.95;
line-height: 1.6;
max-width: 340px;
font-weight: 400;
}
.visual-footer { .visual-footer {
position: absolute; position: absolute;
bottom: 30px; bottom: 40px;
left: 40px; left: 60px;
right: 40px; right: 60px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
opacity: 0.6; opacity: 0.8;
} z-index: 2;
.dots span { }
display: inline-block;
width: 4px; height: 4px; .dots span {
background: white; display: inline-block;
border-radius: 50%; width: 6px; height: 6px;
margin-left: 4px; background: white;
border-radius: 50%;
margin-left: 6px;
opacity: 0.7;
} }
/* --- 右侧表单 --- */
.side-form { .side-form {
flex: 1; flex: 1;
background: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 40px; padding: 40px;
background: #fff;
} }
.form-wrapper { .form-wrapper {
width: 100%; width: 100%;
max-width: 360px; max-width: 340px;
} }
.welcome-header { .welcome-header {
margin-bottom: 32px; margin-bottom: 30px;
text-align: center;
} }
.welcome-header h2 { .welcome-header h2 {
font-size: 24px; font-size: 26px;
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
margin-bottom: 8px; margin-bottom: 8px;
} }
.welcome-header p { .welcome-header p {
color: #64748b; color: #64748b;
font-size: 14px; font-size: 14px;
} }
.input {
width: 100%;
margin-bottom: 20px;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
width: 100%;
margin-top: 32px;
}
.register-hint { .register-hint {
margin-top: 24px; margin-top: 24px;
text-align: center; text-align: center;
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
} }
.register-hint a { .register-hint a {
color: #4f46e5; color: #2563eb;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
} }
/* 响应式调整 */ .register-hint a:hover {
@media (max-width: 768px) { text-decoration: underline;
.login-layout {
flex-direction: column;
}
.side-visual {
width: 100%;
height: 160px;
padding: 24px;
}
.hero-title { font-size: 28px; }
.visual-footer { display: none; }
.side-form { flex: 1; padding: 24px; }
} }
.feather-arrow-right{ /* Response Design */
width: 18px; @media (max-width: 960px) {
.login-card {
flex-direction: column;
width: 90%;
margin: 20px;
height: auto;
border-radius: 16px;
}
.side-visual {
padding: 30px;
min-height: 160px;
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
}
.hero-title { font-size: 28px; }
.hero-subtitle, .visual-footer { display: none; }
.side-form { padding: 40px 20px; }
}
</style>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f0f4f8; /* Fallback */
} }
</style> </style>

View File

@ -0,0 +1,355 @@
<template>
<div class="login-layout">
<div class="login-card">
<div class="side-visual">
<div class="brand-container">
<h1 class="hero-title">Join<br>Us.</h1>
<p class="hero-subtitle">创建一个新账号开启您的沟通之旅</p>
</div>
<div class="visual-footer">
<span>© 2025 IM System</span>
<div class="dots">
<span></span><span></span><span></span>
</div>
</div>
</div>
<div class="side-form">
<div class="form-wrapper">
<div class="welcome-header">
<h2>注册账号</h2>
<p>请填写以下信息以完成注册</p>
</div>
<form @submit.prevent="handleRegister">
<IconInput class="input"
placeholder="请输入用户名" lab="用户名" type="text" icon-name="user" v-model="form.username"/>
<IconInput class="input"
placeholder="请输入昵称" lab="昵称" type="text" icon-name="smile" v-model="form.nickname"/>
<IconInput class="input"
placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
<IconInput class="input"
placeholder="再次输入密码" lab="确认密码" type="password" icon-name="check-circle" v-model="form.confirmPassword"/>
<div class="login-btn-wrapper">
<MyButton variant="pill-green" class="login-btn" :loading="loading">
立即注册
</MyButton>
</div>
</form>
<div class="register-hint">
已有账号? <router-link to="/auth/login">直接登录</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useMessage } from '@/components/messages/useAlert'
import { authService } from '@/services/auth'
import { useRouter } from 'vue-router'
import feather from 'feather-icons'
import IconInput from '@/components/IconInput.vue'
import MyButton from '@/components/MyButton.vue'
import { required, maxLength, minLength, sameAs, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
const message = useMessage();
const router = useRouter();
const loading = ref(false)
const form = reactive({
username: '',
nickname: '',
password: '',
confirmPassword: ''
})
const rules = {
username:{
required:helpers.withMessage('用户名不能为空', required),
maxLength:helpers.withMessage('用户名最大20字符', maxLength(20)),
minLength:helpers.withMessage('用户名至少3字符', minLength(3))
},
nickname: {
required: helpers.withMessage('昵称不能为空', required),
maxLength: helpers.withMessage('昵称最大20字符', maxLength(20))
},
password:{
required:helpers.withMessage('密码不能为空', required),
minLength:helpers.withMessage('密码至少6字符', minLength(6)),
maxLength:helpers.withMessage('密码最大50字符', maxLength(50))
},
confirmPassword: {
required: helpers.withMessage('请确认密码', required),
sameAs: helpers.withMessage('两次输入的密码不一致', sameAs(ref(form).password)) // This might fail if using reactive directly without computed ref binding for sameAs. Let's fix sameAs usage.
// Actually sameAs(form.password) won't work reactively in Vuelidate 2 sometimes if not ref.
// Just utilize a simpler computed or validator.
}
};
// Vuelidate sameAs expects a generic or a ref.
// Let's use computed for the target or just fix the rule.
// In composition API, use computed(() => form.password)
const rulesWithComputed = {
...rules,
confirmPassword: {
required: helpers.withMessage('请确认密码', required),
sameAs: helpers.withMessage('两次输入的密码不一致', sameAs(ref(form).value?.password || form.password)) // Trickier with reactive.
}
}
// Actually, standard way:
// const rules = computed(() => ({ ... }))
const v$ = useVuelidate(rules, form); // Vuelidate supports reactive object directly.
// The problem is `sameAs` needs a locator.
// Correct usage: sameAs(computed(() => form.password))
const handleRegister = async () => {
// Manual check for confirm password if Vuelidate sameAs is tricky without computed rules
if (form.password !== form.confirmPassword) {
message.error('两次输入的密码不一致');
return;
}
const isFormCorrect = await v$.value.$validate()
if (!isFormCorrect) {
if (v$.value.$errors.length > 0) {
// Skip confirmPassword error if we manually checked it or if it's the only one
message.error(v$.value.$errors[0].$message)
}
return
}
try{
loading.value = true;
// Prepare data (exclude confirmPassword)
const { confirmPassword, ...registerData } = form;
const res = await authService.register(registerData);
if(res.code === 0){
message.success('注册成功,请登录')
router.push('/auth/login')
}else{
message.error(res.message || '注册失败')
}
} catch(e) {
console.error(e);
message.error('注册请求异常');
} finally{
loading.value = false;
}
}
onMounted(() => {
feather.replace()
})
</script>
<style scoped>
/* Green Soft Mesh Gradient Background */
.login-layout {
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
min-height: 100vh;
background-color: #f0fdf4; /* Very light green fallback */
background-image:
radial-gradient(at 0% 0%, hsla(150, 100%, 95%, 1) 0, transparent 50%),
radial-gradient(at 50% 0%, hsla(165, 100%, 96%, 1) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(140, 100%, 96%, 1) 0, transparent 50%);
overflow: hidden;
position: relative;
}
/* Very subtle grid overlay */
.login-layout::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 20px 20px;
z-index: 0;
}
.login-card {
display: flex;
width: 1000px;
height: 600px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(30px);
border-radius: 32px;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.02),
0 40px 100px -20px rgba(0, 0, 0, 0.08);
overflow: hidden;
z-index: 10;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.7);
}
.side-visual {
flex: 1;
/* Green connectivity gradient */
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
color: white;
overflow: hidden;
}
/* Abstract Decorations */
.side-visual::before {
content: '';
position: absolute;
top: -20%; left: -20%;
width: 400px; height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
z-index: 1;
}
.side-visual::after {
content: '';
position: absolute;
bottom: -10%; right: -10%;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%);
z-index: 1;
}
.brand-container {
position: relative;
z-index: 2;
}
.hero-title {
font-size: 44px;
font-weight: 800;
line-height: 1.2;
margin-bottom: 20px;
text-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.hero-subtitle {
font-size: 16px;
opacity: 0.95;
line-height: 1.6;
max-width: 340px;
font-weight: 400;
}
.visual-footer {
position: absolute;
bottom: 40px;
left: 60px;
right: 60px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
opacity: 0.8;
z-index: 2;
}
.dots span {
display: inline-block;
width: 6px; height: 6px;
background: white;
border-radius: 50%;
margin-left: 6px;
opacity: 0.7;
}
.side-form {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: #fff;
}
.form-wrapper {
width: 100%;
max-width: 340px;
}
.welcome-header {
margin-bottom: 30px;
text-align: center;
}
.welcome-header h2 {
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
}
.welcome-header p {
color: #64748b;
font-size: 14px;
}
.input {
width: 100%;
margin-bottom: 16px;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
width: 100%;
margin-top: 24px;
}
.register-hint {
margin-top: 24px;
text-align: center;
font-size: 13px;
color: #64748b;
}
.register-hint a {
color: #10b981;
font-weight: 600;
text-decoration: none;
}
.register-hint a:hover {
text-decoration: underline;
}
/* Responsive Design */
@media (max-width: 960px) {
.login-card {
flex-direction: column;
width: 90%;
margin: 20px;
height: auto;
border-radius: 16px;
}
.side-visual {
padding: 30px;
min-height: 160px;
}
.hero-title { font-size: 28px; }
.hero-subtitle, .visual-footer { display: none; }
.side-form { padding: 40px 20px; }
}
</style>