Merge pull request 'main' (#32) from main into feature-nxdev

Reviewed-on: #32
This commit is contained in:
西街长安 2025-12-29 20:46:10 +08:00
commit 26a13c4165
5 changed files with 659 additions and 256 deletions

View File

@ -1,37 +1,21 @@
<script setup>
</script>
<template>
<div id="app">
<!--
<WindowLayout>
-->
<RouterView></RouterView>
<!---
</WindowLayout>
-->
<RouterView></RouterView>
<Alert></Alert>
</div>
</template>
<script setup>
import WindowLayout from './components/Window.vue'
import Alert from '@/components/messages/Alert.vue';
</script>
<style scoped>
#app {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
padding: 150px;
margin: 0;
box-sizing: border-box;
background: radial-gradient(circle at 50% 30%, #eef2ff, #e2e8f0);
padding: 0;
overflow: hidden;
}
</style>

View File

@ -109,55 +109,57 @@ label {
text-decoration: none;
}
/* 输入框样式:现代填充风格 */
/* 输入框样式:现代线框风格 */
.field-wrap {
position: relative;
display: flex;
align-items: center;
}
.field-wrap .icon {
/* .icon 是 <i> 占位符,定位父容器 */
position: absolute;
left: 12px;
width: 18px;
height: 18px;
color: #94a3b8; /* 默认图标颜色 */
color: #94a3b8;
transition: color 0.2s;
/* Feather icons 渲染后SVG会继承这个颜色 */
pointer-events: none; /* Icon shouldn't block input click */
z-index: 10;
}
/* 确保渲染后的 SVG 元素颜色能够正确继承 */
.field-wrap .icon > svg {
/* 确保 SVG 自身不被其他默认样式影响 */
display: block;
width: 100%;
height: 100%;
stroke: currentColor; /* 使用父元素的颜色 */
stroke: currentColor;
transition: stroke 0.2s;
}
.field-wrap input {
padding: 12px 12px 12px 40px;
background: #f1f5f9; /* 浅灰底色 */
border: 2px solid transparent;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
color: #1e293b;
outline: none;
transition: all 0.2s ease;
width: 100%;
}
/* 聚焦交互 */
.field-wrap input:focus {
background: #fff;
border-color: #4f46e5; /* 品牌色 */
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.1);
border-color: #6366f1; /* 品牌色 */
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); /* 柔和光晕 */
}
/* 错误状态 (Optional, if you pass a prop later) */
.field-wrap input.error {
border-color: #ef4444;
}
/* 🚨 关键:聚焦时图标颜色变化的选择器 */
/* 当 input 聚焦时,选择它后面的所有兄弟元素 (~),其中类名为 .icon 的元素,改变它的颜色 */
/* 由于我们在 .icon 元素上设置了 colorSVG 会自动继承 */
.field-wrap input:focus ~ .icon {
color: #4f46e5; /* 聚焦时的颜色 */
color: #6366f1; /* 聚焦时的颜色 */
}
</style>

View File

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

View File

@ -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,32 @@ 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)
message.error('登录请求异常')
} finally{
loading.value = false;
}
}finally{
loading.value = false;
}
}
onMounted(() => {
@ -106,138 +114,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>

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>