You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/login.html

683 lines
20 KiB
HTML

12 months ago
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
2 months ago
<meta name="description" content="登录与注册页面" details="提供用户登录和注册功能的页面,支持用户名/手机号登录及第三方登录">
12 months ago
<title>登录与注册</title>
<style>
body {
2 months ago
font-family: var(--font-family);
12 months ago
height: 100vh;
overflow: hidden;
2 months ago
background-color: var(--bg-color-secondary);
12 months ago
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
2 months ago
/* 背景装饰 */
.bg-decoration {
12 months ago
position: absolute;
top: 0;
left: 0;
2 months ago
width: 100%;
height: 100%;
z-index: 0;
12 months ago
overflow: hidden;
}
2 months ago
.circle {
12 months ago
position: absolute;
border-radius: 50%;
2 months ago
background: radial-gradient(circle, color-mix(in srgb, var(--color-primary), transparent 70%), transparent);
filter: blur(40px);
animation: float 10s infinite ease-in-out;
12 months ago
}
2 months ago
.circle:nth-child(1) {
width: 500px;
height: 500px;
top: -100px;
left: -100px;
animation-delay: 0s;
12 months ago
}
2 months ago
.circle:nth-child(2) {
width: 400px;
height: 400px;
bottom: -50px;
right: -50px;
background: radial-gradient(circle, color-mix(in srgb, var(--color-secondary), transparent 70%), transparent);
animation-delay: -5s;
12 months ago
}
2 months ago
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30px, 20px); }
12 months ago
}
2 months ago
/* 主容器 */
.container {
background-color: var(--bg-color);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
position: relative;
overflow: hidden;
width: 850px;
max-width: 95%;
min-height: 600px;
z-index: 1;
12 months ago
}
2 months ago
.form-container {
position: absolute;
top: 0;
height: 100%;
transition: all 0.6s ease-in-out;
background: var(--bg-color);
12 months ago
}
2 months ago
.sign-in-container {
left: 0;
width: 50%;
z-index: 2;
12 months ago
}
2 months ago
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
12 months ago
}
2 months ago
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
12 months ago
}
2 months ago
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
animation: show 0.6s;
12 months ago
}
2 months ago
@keyframes show {
0%, 49.99% { opacity: 0; z-index: 1; }
50%, 100% { opacity: 1; z-index: 5; }
7 months ago
}
2 months ago
.form-container-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 40px;
text-align: center;
7 months ago
}
2 months ago
/* 标题与文本 */
h1 {
font-weight: bold;
margin: 0 0 10px;
color: var(--text-color);
font-size: 28px;
}
7 months ago
2 months ago
.subtitle {
font-size: 14px;
color: var(--text-color-secondary);
margin-bottom: 20px;
7 months ago
}
2 months ago
.social-container {
margin: 15px 0 20px;
display: flex;
gap: 15px;
7 months ago
}
2 months ago
.social-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--border-color);
12 months ago
display: flex;
align-items: center;
justify-content: center;
2 months ago
color: var(--text-color-secondary);
transition: all 0.3s;
cursor: pointer;
}
.social-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background-color: color-mix(in srgb, var(--color-primary), transparent 95%);
12 months ago
}
2 months ago
/* 切换 Tab */
.login-tab {
display: flex;
margin-bottom: 25px;
position: relative;
background: var(--bg-color-secondary);
border-radius: var(--radius-full);
padding: 4px;
12 months ago
width: 100%;
}
2 months ago
.tab-item {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 14px;
cursor: pointer;
border-radius: var(--radius-full);
color: var(--text-color-secondary);
transition: all 0.3s ease;
12 months ago
position: relative;
2 months ago
z-index: 1;
12 months ago
}
2 months ago
.tab-item.active {
color: var(--color-primary);
background: var(--bg-color);
box-shadow: var(--shadow-sm);
font-weight: 600;
12 months ago
}
2 months ago
/* 输入框区域 */
.input-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 10px;
12 months ago
}
2 months ago
.input-wrapper {
width: 100%;
12 months ago
}
2 months ago
.phone-row {
display: flex;
gap: 10px;
}
.verify-row {
display: flex;
gap: 10px;
12 months ago
}
2 months ago
/* 链接与错误信息 */
.forgot-password {
color: var(--text-color-tertiary);
font-size: 13px;
text-decoration: none;
margin: 15px 0;
align-self: flex-end;
transition: color 0.3s;
}
.forgot-password:hover {
color: var(--color-primary);
12 months ago
}
2 months ago
.error-message {
color: var(--color-danger);
font-size: 13px;
min-height: 20px;
margin-bottom: 10px;
text-align: left;
width: 100%;
}
/* 侧边遮罩 */
12 months ago
.overlay-container {
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transition: transform 0.6s ease-in-out;
z-index: 100;
2 months ago
border-top-right-radius: var(--radius-xl);
border-bottom-right-radius: var(--radius-xl);
12 months ago
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
2 months ago
border-radius: var(--radius-xl) 0 0 var(--radius-xl);
12 months ago
}
.overlay {
2 months ago
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
12 months ago
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
2 months ago
color: #fff;
12 months ago
position: relative;
left: -100%;
height: 100%;
width: 200%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.container.right-panel-active .overlay {
transform: translateX(50%);
}
.overlay-panel {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 40px;
text-align: center;
top: 0;
height: 100%;
width: 50%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
2 months ago
.overlay-panel h1 {
color: #fff;
}
.overlay-panel p {
font-size: 14px;
font-weight: 300;
line-height: 24px;
margin: 20px 0 30px;
color: rgba(255, 255, 255, 0.9);
}
12 months ago
.overlay-left {
transform: translateX(-20%);
}
.container.right-panel-active .overlay-left {
transform: translateX(0);
}
.overlay-right {
right: 0;
transform: translateX(0);
}
.container.right-panel-active .overlay-right {
transform: translateX(20%);
}
2 months ago
/* 按钮覆盖样式 */
.btn-ghost {
background-color: transparent;
border-color: #fff;
color: #fff;
7 months ago
}
2 months ago
.btn-ghost:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
7 months ago
}
2 months ago
/* v-btn 通用样式微调 */
v-btn {
font-weight: 600;
letter-spacing: 0.5px;
7 months ago
}
12 months ago
</style>
</head>
<body layout='public'>
2 months ago
<!-- 背景动画 -->
<div class="bg-decoration">
<div class="circle"></div>
<div class="circle"></div>
12 months ago
</div>
<div class="container" :class="{ 'right-panel-active': isSignUp }" id="container">
2 months ago
7 months ago
<!-- 注册表单 -->
12 months ago
<div class="form-container sign-up-container">
2 months ago
<div class="form-container-inner">
12 months ago
<h1>创建账户</h1>
2 months ago
<div class="subtitle">填写以下信息开始您的旅程</div>
12 months ago
<div class="social-container">
2 months ago
<div class="social-btn" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></div>
<div class="social-btn" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></div>
<div class="social-btn" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></div>
7 months ago
</div>
2 months ago
<div class="subtitle" style="margin: 0 0 15px; font-size: 12px;">或使用手机/邮箱注册</div>
<div class="input-group">
<div class="input-wrapper">
<v-input v:value="signUpForm.username" placeholder="用户名" prefix-icon="fa fa-user"></v-input>
</div>
<!-- 手机号输入框带区域选择 -->
<div v-if='$G.cfg.sms' class="phone-row">
<div style="width: 110px;">
<v-input type="select" v:value="signUpForm.region" :opts="{options: regions}" :clearable="false"></v-input>
</div>
<div style="flex: 1;">
<v-input v:value="signUpForm.phone" placeholder="手机号" prefix-icon="fa fa-mobile"></v-input>
</div>
</div>
7 months ago
2 months ago
<div v-if='$G.cfg.sms' class="verify-row">
<div style="flex: 1;">
<v-input v:value="signUpForm.verifyCode" placeholder="验证码" prefix-icon="fa fa-shield"></v-input>
</div>
<v-btn variant="outline" :disabled="smsCountdown > 0 || smsLoading" :click="() => sendVerifyCode('signup')" style="min-width: 100px;">
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</v-btn>
</div>
<div class="input-wrapper">
<v-input type="password" v:value="signUpForm.password" placeholder="密码" prefix-icon="fa fa-lock"></v-input>
</div>
12 months ago
</div>
2 months ago
<div class="error-message">{{ signUpError }}</div>
2 months ago
<v-btn round block size="large" :loading="signUpLoading" :click="handleSignUp">立即注册</v-btn>
</div>
12 months ago
</div>
7 months ago
<!-- 登录表单 -->
12 months ago
<div class="form-container sign-in-container">
2 months ago
<div class="form-container-inner">
<h1>欢迎回来</h1>
<div class="subtitle">登录您的账户以继续</div>
12 months ago
<div class="social-container">
2 months ago
<div class="social-btn" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></div>
<div class="social-btn" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></div>
<div class="social-btn" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></div>
12 months ago
</div>
2 months ago
<div class="subtitle" style="margin: 0 0 15px; font-size: 12px;">或使用您的账户</div>
7 months ago
<!-- 登录方式选择 -->
2 months ago
<div class="login-tab" v-if="$G.cfg.sms">
7 months ago
<div class="tab-item" :class="{ active: loginType === 'username' }" @click="switchLoginType('username')">
2 months ago
账号密码
7 months ago
</div>
<div class="tab-item" :class="{ active: loginType === 'phone' }" @click="switchLoginType('phone')">
2 months ago
手机验证码
7 months ago
</div>
</div>
<!-- 用户名登录 -->
2 months ago
<div v-if="loginType === 'username'" class="input-group">
<div class="input-wrapper">
<v-input v:value="signInForm.username" placeholder="用户名" prefix-icon="fa fa-user"></v-input>
</div>
<div class="input-wrapper">
<v-input type="password" v:value="signInForm.password" placeholder="密码" prefix-icon="fa fa-lock"></v-input>
</div>
7 months ago
</div>
<!-- 手机号登录 -->
2 months ago
<div v-if="loginType === 'phone'" class="input-group">
<div class="phone-row">
<div style="width: 110px;">
<v-input type="select" v:value="signInForm.region" :opts="{options: regions}" :clearable="false"></v-input>
</div>
<div style="flex: 1;">
<v-input v:value="signInForm.phone" placeholder="手机号" prefix-icon="fa fa-mobile"></v-input>
</div>
7 months ago
</div>
2 months ago
<div class="verify-row">
<div style="flex: 1;">
<v-input v:value="signInForm.verifyCode" placeholder="验证码" prefix-icon="fa fa-shield"></v-input>
</div>
<v-btn variant="outline" :disabled="smsCountdown > 0 || smsLoading" :click="() => sendVerifyCode('signin')" style="min-width: 100px;">
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</v-btn>
7 months ago
</div>
</div>
2 months ago
<a href="#" class="forgot-password">忘记密码?</a>
7 months ago
<div class="error-message">{{ signInError }}</div>
2 months ago
<v-btn round block size="large" :loading="signInLoading" :click="handleSignIn">登 录</v-btn>
</div>
12 months ago
</div>
7 months ago
2 months ago
<!-- 覆盖层 -->
12 months ago
<div class="overlay-container">
<div class="overlay">
<div class="overlay-panel overlay-left">
2 months ago
<h1>已有账户?</h1>
12 months ago
<p>请使用您的个人信息登录,保持连接。</p>
2 months ago
<v-btn variant="outline" round class="btn-ghost" :click="switchToSignIn" size="large" style="width: 120px;">去登录</v-btn>
12 months ago
</div>
<div class="overlay-panel overlay-right">
2 months ago
<h1>新朋友?</h1>
12 months ago
<p>输入您的个人信息,开始您的旅程。</p>
2 months ago
<v-btn variant="outline" round class="btn-ghost" :click="switchToSignUp" size="large" style="width: 120px;">去注册</v-btn>
12 months ago
</div>
</div>
</div>
</div>
<script setup>
// 响应式数据
7 months ago
logout = $router.query.logout;
redirect = $router.query.redirect || '/';
12 months ago
isSignUp = false;
7 months ago
loginType = 'username'; // 'username' 或 'phone'
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
signInForm = {username: '', password: '', phone: '', verifyCode: '', region: '+86'};
signUpError = '';
7 months ago
signInError = '';
smsCountdown = 0; // 验证码倒计时
smsLoading = false; // 验证码发送加载状态
signUpLoading = false; // 注册按钮加载状态
signInLoading = false; // 登录按钮加载状态
2 months ago
// 常用国家/地区代码 - 转换为 v-select 格式
7 months ago
regions = [
2 months ago
{value: '+86', label: '+86 中国'},
{value: '+1', label: '+1 美国'},
{value: '+44', label: '+44 英国'},
{value: '+81', label: '+81 日本'},
{value: '+82', label: '+82 韩国'},
{value: '+65', label: '+65 新加坡'},
{value: '+852', label: '+852 香港'},
{value: '+853', label: '+853 澳门'},
{value: '+886', label: '+886 台湾'},
{value: '+91', label: '+91 印度'},
{value: '+33', label: '+33 法国'},
{value: '+49', label: '+49 德国'},
{value: '+7', label: '+7 俄国'},
{value: '+61', label: '+61 澳大利亚'},
{value: '+55', label: '+55 巴西'},
{value: '+39', label: '+39 意大利'},
{value: '+34', label: '+34 西班牙'},
{value: '+31', label: '+31 荷兰'},
{value: '+46', label: '+46 瑞典'},
{value: '+47', label: '+47 挪威'}
7 months ago
];
// 验证手机号格式(根据不同地区调整)
validatePhone = (phone, region) => {
if (region === '+86') {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
} else if (region === '+1') {
const regex = /^\d{10}$/;
return regex.test(phone);
} else {
const regex = /^\d{7,15}$/;
return regex.test(phone);
}
};
// 验证密码是否符合要求
7 months ago
validatePassword = (password) => {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_]).{9,}$/;
return regex.test(password);
};
12 months ago
7 months ago
// 切换登录方式
switchLoginType = (type) => {
2 months ago
if (signInLoading) return;
7 months ago
loginType = type;
signInError = '';
signInForm.username = '';
signInForm.password = '';
signInForm.phone = '';
signInForm.verifyCode = '';
};
12 months ago
// 切换到注册页面
switchToSignUp = () => {
2 months ago
if (signInLoading || signUpLoading) return;
12 months ago
isSignUp = true;
7 months ago
signUpError = '';
signInError = '';
12 months ago
};
// 切换到登录页面
switchToSignIn = () => {
2 months ago
if (signInLoading || signUpLoading) return;
12 months ago
isSignUp = false;
7 months ago
signUpError = '';
signInError = '';
};
// 发送验证码
sendVerifyCode = async (type) => {
2 months ago
if (smsLoading) return;
7 months ago
const phone = type === 'signup' ? signUpForm.phone : signInForm.phone;
const region = type === 'signup' ? signUpForm.region : signInForm.region;
if (!phone) {
const errorMsg = '请输入手机号';
2 months ago
type === 'signup' ? signUpError = errorMsg : signInError = errorMsg;
7 months ago
return;
}
if (!validatePhone(phone, region)) {
const errorMsg = '请输入正确的手机号格式';
2 months ago
type === 'signup' ? signUpError = errorMsg : signInError = errorMsg;
7 months ago
return;
}
2 months ago
smsLoading = true;
7 months ago
try {
2 months ago
type === 'signup' ? signUpError = '' : signInError = '';
await $axios.post('/api/sms', { phone, region, purpose: type });
7 months ago
$message.success('验证码已发送');
smsCountdown = 60;
const timer = setInterval(() => {
smsCountdown--;
2 months ago
if (smsCountdown <= 0) clearInterval(timer);
7 months ago
}, 1000);
} catch (error) {
const errorMsg = error.message || '发送验证码失败,请重试';
2 months ago
type === 'signup' ? signUpError = errorMsg : signInError = errorMsg;
7 months ago
$message.warning(errorMsg);
} finally {
2 months ago
smsLoading = false;
7 months ago
}
12 months ago
};
// 处理第三方登录
handleSocialLogin = (provider) => {
7 months ago
$message.warning(`未开放 ${provider} 登录`);
12 months ago
};
// 处理注册表单提交
2 months ago
handleSignUp = async () => {
if (signUpLoading) return;
signUpError = '';
7 months ago
if (signUpForm.username.length < 5) {
7 months ago
signUpError = '用户名必须大于5位。';
return;
}
2 months ago
if ($G.cfg.sms) {
if (!validatePhone(signUpForm.phone, signUpForm.region)) {
signUpError = '请输入正确的手机号格式。';
return;
}
if (!signUpForm.verifyCode) {
signUpError = '请输入验证码。';
return;
}
}
7 months ago
if (!validatePassword(signUpForm.password)) {
signUpError = '密码必须大于8位且包含大小写字母、下划线和数字。';
return;
}
2 months ago
signUpLoading = true;
12 months ago
try {
2 months ago
await $axios.post('/api/user', {
12 months ago
username: signUpForm.username,
7 months ago
phone: signUpForm.phone,
region: signUpForm.region,
verify_code: signUpForm.verifyCode,
code: btoa(signUpForm.password),
}, {noretry: true});
2 months ago
7 months ago
$message.success('注册成功!');
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
switchToSignIn();
12 months ago
} catch (error) {
signUpError = error.message || '注册失败,请重试。';
7 months ago
$message.warning(signUpError);
} finally {
2 months ago
signUpLoading = false;
12 months ago
}
};
// 处理登录表单提交
2 months ago
handleSignIn = async () => {
if (signInLoading) return;
7 months ago
signInError = '';
12 months ago
try {
7 months ago
let loginData = {};
if (loginType === 'username') {
2 months ago
if (!signInForm.username) { signInError = '请输入用户名'; return; }
if (!signInForm.password) { signInError = '请输入密码'; return; }
loginData = { username: signInForm.username, code: btoa(signInForm.password), type: 'username' };
7 months ago
} else {
2 months ago
if (!signInForm.phone) { signInError = '请输入手机号'; return; }
if (!signInForm.verifyCode) { signInError = '请输入验证码'; return; }
loginData = { phone: signInForm.phone, region: signInForm.region, verify_code: signInForm.verifyCode, type: 'phone' };
7 months ago
}
2 months ago
signInLoading = true;
await $axios.post('/api/token', loginData, {noretry: true});
$message.success('登录成功!');
$router.push(redirect);
12 months ago
} catch (error) {
2 months ago
signInError = error.message || '登录失败,请重试';
7 months ago
$message.warning(signInError);
} finally {
2 months ago
signInLoading = false;
12 months ago
}
};
</script>
</body>
</html>