329 lines
7.3 KiB
Vue
329 lines
7.3 KiB
Vue
<template>
|
||
<div class="login-layout">
|
||
|
||
<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 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"/>
|
||
|
||
<IconInput class="input"
|
||
placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
|
||
|
||
<div class="login-btn-wrapper">
|
||
<MyButton variant="pill" class="login-btn" :loading="loading">
|
||
登录
|
||
</MyButton>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="register-hint">
|
||
还没有账号? <router-link to="/auth/register">立即注册</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, helpers } from '@vuelidate/validators'
|
||
import useVuelidate from '@vuelidate/core'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { useSignalRStore } from '@/stores/signalr'
|
||
|
||
const message = useMessage();
|
||
const router = useRouter();
|
||
const authStore = useAuthStore();
|
||
const signalRStore = useSignalRStore();
|
||
|
||
const loading = ref(false)
|
||
const form = reactive({
|
||
username: '',
|
||
password: ''
|
||
})
|
||
|
||
const rules = {
|
||
username:{
|
||
required:helpers.withMessage('用户名不能为空', required),
|
||
maxLength:helpers.withMessage('用户名最大20字符', maxLength(20))
|
||
},
|
||
password:{
|
||
required:helpers.withMessage('密码不能为空', required),
|
||
maxLength:helpers.withMessage('密码最大50字符', maxLength(50))
|
||
}
|
||
};
|
||
|
||
const v$ = useVuelidate(rules,form);
|
||
|
||
const handleLogin = async () => {
|
||
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){ // Assuming 0 is success
|
||
message.success('登录成功')
|
||
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
|
||
signalRStore.initSignalR();
|
||
router.push('/messages')
|
||
}else{
|
||
message.error(res.message || '登录失败')
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally{
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
feather.replace()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Soft Mesh Gradient Background */
|
||
.login-layout {
|
||
display: flex;
|
||
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 {
|
||
flex: 1;
|
||
/* Soft connectivity gradient */
|
||
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: 60px;
|
||
color: white;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Abstract "Connection" Circles */
|
||
.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: 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: #2563eb;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.register-hint a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 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> |