Merge pull request 'main' (#32) from main into feature-nxdev
Reviewed-on: #32
This commit is contained in:
@@ -1,42 +1,44 @@
|
||||
<template>
|
||||
<div class="login-layout">
|
||||
|
||||
<div class="side-visual">
|
||||
<div class="visual-mask"></div>
|
||||
<div class="brand-container">
|
||||
<h1 class="hero-title">Work<br>Together.</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 class="login-card">
|
||||
<div class="side-visual">
|
||||
<div class="brand-container">
|
||||
<h1 class="hero-title">Work<br>Together.</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>
|
||||
|
||||
<div class="side-form">
|
||||
<div class="form-wrapper">
|
||||
<div class="welcome-header">
|
||||
<h2>账号登录</h2>
|
||||
<p>请输入您的工作账号以继续</p>
|
||||
</div>
|
||||
<div class="side-form">
|
||||
<div class="form-wrapper">
|
||||
<div class="welcome-header">
|
||||
<h2>账号登录</h2>
|
||||
<p>请输入您的工作账号以继续</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<IconInput class="input"
|
||||
placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<IconInput class="input"
|
||||
placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/>
|
||||
|
||||
<IconInput class="input"
|
||||
placeholder="请输入密码" lab="密码" type="password" icon-name="user" v-model="form.password"/>
|
||||
<IconInput class="input"
|
||||
placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
|
||||
|
||||
<MyButton class="loginBtn" :loading="loading">
|
||||
登录
|
||||
<template #icon><i data-feather="arrow-right"></i></template>
|
||||
</MyButton>
|
||||
</form>
|
||||
<div class="login-btn-wrapper">
|
||||
<MyButton variant="pill" class="login-btn" :loading="loading">
|
||||
登录
|
||||
</MyButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="register-hint">
|
||||
还没有账号? <a href="#">联系管理员注册</a>
|
||||
<div class="register-hint">
|
||||
还没有账号? <router-link to="/auth/register">立即注册</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,6 +53,7 @@ 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, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -77,27 +80,31 @@ const rules = {
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules,form);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if(!(await v$.value.$validate())){
|
||||
message.error(v$.value.$errors[0].$message)
|
||||
return;
|
||||
const isFormCorrect = await v$.value.$validate()
|
||||
if (!isFormCorrect) {
|
||||
if (v$.value.$errors.length > 0) {
|
||||
message.error(v$.value.$errors[0].$message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try{
|
||||
loading.value = true;
|
||||
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)
|
||||
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)
|
||||
} finally{
|
||||
loading.value = false;
|
||||
}
|
||||
}finally{
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -106,138 +113,214 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.loginBtn) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.input){
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 撑满 Window 组件的内容区 */
|
||||
/* Soft Mesh Gradient Background */
|
||||
.login-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
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 {
|
||||
width: 42%;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #3b82f6 100%);
|
||||
flex: 1;
|
||||
/* Soft connectivity gradient */
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
padding: 60px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 增加一点背景纹理 */
|
||||
/* Abstract "Connection" Circles */
|
||||
.side-visual::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
opacity: 0.6;
|
||||
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: 48px;
|
||||
font-size: 44px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.hero-subtitle {
|
||||
font-size: 15px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.6;
|
||||
max-width: 300px;
|
||||
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: 30px;
|
||||
left: 40px;
|
||||
right: 40px;
|
||||
bottom: 40px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.dots span {
|
||||
display: inline-block;
|
||||
width: 4px; height: 4px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
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;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-header h2 {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.welcome-header p {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-btn-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
|
||||
.register-hint {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.register-hint a {
|
||||
color: #4f46e5;
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.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; }
|
||||
.register-hint a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.feather-arrow-right{
|
||||
width: 18px;
|
||||
/* Response 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;
|
||||
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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user