style: refine login and register pages with green/blue themes and pill-shaped buttons
This commit is contained in:
parent
afe2c38f74
commit
ad0ab87747
@ -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>
|
||||||
@ -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>
|
||||||
355
frontend/web/src/views/auth/Register.vue
Normal file
355
frontend/web/src/views/auth/Register.vue
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user