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

825 lines
22 KiB
HTML

7 months ago
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录与注册</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
height: 100vh;
overflow: hidden;
7 months ago
background: #ffebee;
7 months ago
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
7 months ago
background: linear-gradient(to right, #ef5350, #e53935);
7 months ago
}
.bubble {
position: absolute;
bottom: -150px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
animation: rise linear infinite;
}
@keyframes rise {
0% {
transform: translateY(0) rotate(0);
opacity: 0.5;
}
100% {
transform: translateY(-120vh) rotate(360deg);
opacity: 0;
}
}
h1 {
font-weight: bold;
margin: 0;
margin-bottom: 15px;
}
p {
font-size: 14px;
font-weight: 100;
line-height: 20px;
letter-spacing: 0.5px;
margin: 20px 0 30px;
}
span {
font-size: 12px;
margin: 15px 0;
}
a {
7 months ago
color: #c62828;
7 months ago
font-size: 14px;
text-decoration: none;
margin: 15px 0;
}
button {
border-radius: 20px;
7 months ago
border: 1px solid #e53935;
background-color: #e53935;
7 months ago
color: #ffffff;
font-size: 12px;
font-weight: bold;
padding: 12px 45px;
letter-spacing: 1px;
text-transform: uppercase;
transition: transform 80ms ease-in;
cursor: pointer;
2 months ago
position: relative;
overflow: hidden;
7 months ago
}
button:active {
transform: scale(0.95);
}
button:focus {
outline: none;
}
button.ghost {
background-color: transparent;
border-color: #ffffff;
}
2 months ago
button:disabled {
background-color: #ccc;
border-color: #ccc;
cursor: not-allowed;
}
/* 加载动画样式 */
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.button-loading {
color: transparent !important;
}
7 months ago
form {
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 50px;
height: 100%;
text-align: center;
}
input {
background-color: #eee;
border: none;
width: 100%;
border-radius: 5px;
}
.container {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
position: relative;
overflow: hidden;
width: 768px;
max-width: 100%;
min-height: 600px;
}
.form-container {
position: absolute;
top: 0;
height: 100%;
transition: all 0.6s ease-in-out;
}
.sign-in-container {
left: 0;
width: 50%;
z-index: 2;
}
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
}
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
}
.overlay-container {
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transition: transform 0.6s ease-in-out;
z-index: 100;
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
}
.overlay {
7 months ago
background: #e53935;
background: linear-gradient(to right, #ef5350, #e53935);
7 months ago
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #ffffff;
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;
}
.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%);
}
.social-container {
margin: 20px 0;
}
.social-container a {
border: 1px solid #dddddd;
2 months ago
font-size: 1.2rem;
7 months ago
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0 5px;
height: 40px;
width: 40px;
transition: all 0.3s;
}
.social-container a:hover {
background-color: #f2f2f2;
}
2 months ago
.social-container .fa-github {
color: #000;
}
.social-container .fa-weixin {
color: #07c160;
}
.social-container .fa-google {
color: #db4437;
}
.error-message {
color: red;
font-size: 12px;
margin-top: 8px;
}
2 months ago
.login-tab {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.tab-item {
padding: 10px 15px;
cursor: pointer;
flex: 1;
text-align: center;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab-item.active {
color: #e53935;
border-bottom-color: #e53935;
}
.input-group {
position: relative;
margin: 8px 0;
width: 100%;
}
.phone-input-group {
display: flex;
width: 100%;
margin: 8px 0;
}
.region-select {
background-color: #eee;
border: none;
border-radius: 5px 0 0 5px;
width: 80px;
font-size: 12px;
}
.phone-input {
background-color: #eee;
border: none;
border-radius: 0 5px 5px 0;
flex: 1;
}
.verify-code-input {
padding-right: 100px;
}
.verify-code-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #e53935;
font-size: 12px;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
transition: all 0.3s;
}
.verify-code-btn:hover {
opacity: 0.8;
}
.verify-code-btn:disabled {
cursor: not-allowed;
}
7 months ago
</style>
</head>
<body layout='public'>
<div class="background" id="background">
<div v-for="(bubble, index) in bubbles" :key="index" class="bubble" :style="bubble.style"></div>
</div>
<div class="container" :class="{ 'right-panel-active': isSignUp }" id="container">
2 months ago
<!-- 注册表单 -->
7 months ago
<div class="form-container sign-up-container">
<form @submit="handleSignUp">
<h1>创建账户</h1>
<div class="social-container">
<a href="#" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></a>
<a href="#" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></a>
2 months ago
<a href="#" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></a>
</div>
<span>或使用您的信息进行注册</span>
<input type="text" placeholder="用户名" v:value="signUpForm.username" class="input-group" required />
<!-- 手机号输入框带区域选择 -->
<div class="phone-input-group">
<select class="region-select" v:value="signUpForm.region">
<option v-for="region in regions" :value="region.code" :disabled='!region.enabled'>
{{ region.code }} {{region.name}}</option>
</select>
<input type="text" placeholder="手机号" v:value="signUpForm.phone" class="phone-input" required />
</div>
<div class="input-group">
<input type="text" placeholder="验证码" v:value="signUpForm.verifyCode" class="verify-code-input" required />
<button type="button" class="verify-code-btn" @click="sendVerifyCode('signup')"
:disabled="smsCountdown > 0 || smsLoading">
<span v-if="!smsLoading">{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}</span>
<div v-if="smsLoading" class="loading-spinner"></div>
</button>
7 months ago
</div>
2 months ago
<input type="password" placeholder="密码" class="input-group" v:value="signUpForm.password" required />
<div class="error-message">{{ signUpError }}</div>
2 months ago
<button type="submit" :class="{ 'button-loading': signUpLoading }" :disabled="signUpLoading">
<span v-if="!signUpLoading">注册</span>
<div v-if="signUpLoading" class="loading-spinner"></div>
</button>
7 months ago
</form>
</div>
2 months ago
<!-- 登录表单 -->
7 months ago
<div class="form-container sign-in-container">
<form @submit="handleSignIn">
<h1>登录</h1>
<div class="social-container">
<a href="#" @click="handleSocialLogin('github')"><i class="fa-brands fa-github"></i></a>
<a href="#" @click="handleSocialLogin('weixin')"><i class="fa-brands fa-weixin"></i></a>
2 months ago
<a href="#" @click="handleSocialLogin('google')"><i class="fa-brands fa-google"></i></a>
7 months ago
</div>
<span>或使用您的账户</span>
2 months ago
<!-- 登录方式选择 -->
<div class="login-tab">
<div class="tab-item" :class="{ active: loginType === 'username' }" @click="switchLoginType('username')">
用户名登录
</div>
<div class="tab-item" :class="{ active: loginType === 'phone' }" @click="switchLoginType('phone')">
手机号登录
</div>
</div>
<!-- 用户名登录 -->
<div v-if="loginType === 'username'">
<input type="text" placeholder="用户名" class="input-group" v:value="signInForm.username" required />
<input type="password" placeholder="密码" class="input-group" v:value="signInForm.password" required />
</div>
<!-- 手机号登录 -->
<div v-if="loginType === 'phone'">
<!-- 手机号输入框带区域选择 -->
<div class="phone-input-group">
<select class="region-select" v:value="signInForm.region">
<option v-for="region in regions" :value="region.code" :disabled='!region.enabled'>
{{ region.code }} {{region.name}}</option>
</select>
<input type="text" placeholder="手机号" v:value="signInForm.phone" class="phone-input" required />
</div>
<div class="input-group">
<input type="text" placeholder="验证码" v:value="signInForm.verifyCode" class="verify-code-input" required />
<button type="button" class="verify-code-btn" @click="sendVerifyCode('signin')"
:disabled="smsCountdown > 0 || smsLoading">
<span v-if="!smsLoading">{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}</span>
<div v-if="smsLoading" class="loading-spinner"></div>
</button>
</div>
</div>
7 months ago
<a href="#">忘记密码?</a>
2 months ago
<div class="error-message">{{ signInError }}</div>
<button type="submit" :class="{ 'button-loading': signInLoading }" :disabled="signInLoading">
<span v-if="!signInLoading">登录</span>
<div v-if="signInLoading" class="loading-spinner"></div>
</button>
7 months ago
</form>
</div>
2 months ago
7 months ago
<div class="overlay-container">
<div class="overlay">
<div class="overlay-panel overlay-left">
<h1>欢迎回来!</h1>
<p>请使用您的个人信息登录,保持连接。</p>
<button class="ghost" @click="switchToSignIn">登录</button>
</div>
<div class="overlay-panel overlay-right">
<h1>你好,朋友!</h1>
<p>输入您的个人信息,开始您的旅程。</p>
<button class="ghost" @click="switchToSignUp">注册</button>
</div>
</div>
</div>
</div>
<script setup>
// 响应式数据
2 months ago
logout = $router.query.logout;
redirect = $router.query.redirect || '/';
7 months ago
isSignUp = false;
2 months ago
loginType = 'username'; // 'username' 或 'phone'
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
signInForm = {username: '', password: '', phone: '', verifyCode: '', region: '+86'};
7 months ago
bubbles = [];
signUpError = '';
2 months ago
signInError = '';
smsCountdown = 0; // 验证码倒计时
smsLoading = false; // 验证码发送加载状态
signUpLoading = false; // 注册按钮加载状态
signInLoading = false; // 登录按钮加载状态
// 常用国家/地区代码
regions = [
{code: '+86', name: '中国', enabled: true},
{code: '+1', name: '美国'},
{code: '+44', name: '英国'},
{code: '+81', name: '日本'},
{code: '+82', name: '韩国'},
{code: '+65', name: '新加坡'},
{code: '+852', name: '香港'},
{code: '+853', name: '澳门'},
{code: '+886', name: '台湾'},
{code: '+91', name: '印度'},
{code: '+33', name: '法国'},
{code: '+49', name: '德国'},
{code: '+7', name: '俄国'},
{code: '+61', name: '澳大利亚'},
{code: '+55', name: '巴西'},
{code: '+39', name: '意大利'},
{code: '+34', name: '西班牙'},
{code: '+31', name: '荷兰'},
{code: '+46', name: '瑞典'},
{code: '+47', name: '挪威'}
];
// 验证手机号格式(根据不同地区调整)
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);
}
};
// 验证密码是否符合要求
2 months ago
validatePassword = (password) => {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_]).{9,}$/;
return regex.test(password);
};
7 months ago
2 months ago
// 切换登录方式
switchLoginType = (type) => {
if (signInLoading) return; // 如果正在登录,禁止切换
loginType = type;
signInError = '';
// 清空表单
signInForm.username = '';
signInForm.password = '';
signInForm.phone = '';
signInForm.verifyCode = '';
};
7 months ago
// 切换到注册页面
switchToSignUp = () => {
2 months ago
if (signInLoading || signUpLoading) return; // 如果正在加载,禁止切换
7 months ago
isSignUp = true;
2 months ago
signUpError = '';
signInError = '';
7 months ago
};
// 切换到登录页面
switchToSignIn = () => {
2 months ago
if (signInLoading || signUpLoading) return; // 如果正在加载,禁止切换
7 months ago
isSignUp = false;
2 months ago
signUpError = '';
signInError = '';
};
// 发送验证码
sendVerifyCode = async (type) => {
if (smsLoading) return; // 防止重复点击
const phone = type === 'signup' ? signUpForm.phone : signInForm.phone;
const region = type === 'signup' ? signUpForm.region : signInForm.region;
if (!phone) {
const errorMsg = '请输入手机号';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
return;
}
if (!validatePhone(phone, region)) {
const errorMsg = '请输入正确的手机号格式';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
return;
}
smsLoading = true; // 开始加载
try {
// 清空错误信息
if (type === 'signup') {
signUpError = '';
} else {
signInError = '';
}
await $axios.post('/api/sms', {
phone: phone,
region: region,
purpose: type // 'signup' 或 'signin'
});
$message.success('验证码已发送');
// 开始倒计时
smsCountdown = 60;
const timer = setInterval(() => {
smsCountdown--;
if (smsCountdown <= 0) {
clearInterval(timer);
}
}, 1000);
} catch (error) {
const errorMsg = error.message || '发送验证码失败,请重试';
if (type === 'signup') {
signUpError = errorMsg;
} else {
signInError = errorMsg;
}
$message.warning(errorMsg);
} finally {
smsLoading = false; // 结束加载
}
7 months ago
};
// 处理第三方登录
handleSocialLogin = (provider) => {
2 months ago
$message.warning(`未开放 ${provider} 登录`);
7 months ago
};
function deriveKey(password, salt) {
return CryptoJS.PBKDF2(password, salt, {
2 months ago
keySize: 256 / 32,
iterations: 100,
hasher: CryptoJS.algo.SHA256
});
7 months ago
}
// 处理注册表单提交
handleSignUp = async (e) => {
e.preventDefault();
2 months ago
if (signUpLoading) return; // 防止重复提交
signUpError = '';
2 months ago
// 验证用户名
if (signUpForm.username.length < 6) {
signUpError = '用户名必须大于5位。';
return;
}
2 months ago
// 验证手机号
if (!validatePhone(signUpForm.phone, signUpForm.region)) {
signUpError = '请输入正确的手机号格式。';
return;
}
2 months ago
// 验证验证码
if (!signUpForm.verifyCode) {
signUpError = '请输入验证码。';
return;
}
// 验证密码
if (!validatePassword(signUpForm.password)) {
signUpError = '密码必须大于8位且包含大小写字母、下划线和数字。';
return;
}
signUpLoading = true; // 开始加载
7 months ago
try {
const response = await $axios.post('/api/user', {
7 months ago
username: signUpForm.username,
2 months ago
phone: signUpForm.phone,
region: signUpForm.region,
verify_code: signUpForm.verifyCode,
code: btoa(signUpForm.password),
}, {noretry: true});
2 months ago
signUpLoading = false; // 结束加载
$message.success('注册成功!');
// 清空表单
signUpForm = {username: '', phone: '', verifyCode: '', password: '', region: '+86'};
switchToSignIn();
7 months ago
} catch (error) {
signUpError = error.message || '注册失败,请重试。';
2 months ago
$message.warning(signUpError);
console.error(signUpError);
2 months ago
} finally {
signUpLoading = false; // 结束加载
7 months ago
}
};
// 处理登录表单提交
handleSignIn = async (e) => {
e.preventDefault();
2 months ago
if (signInLoading) return; // 防止重复提交
signInError = '';
7 months ago
try {
2 months ago
let loginData = {};
if (loginType === 'username') {
// 用户名密码登录
if (!signInForm.username) {
signInError = '请输入用户名';
return;
}
if (!signInForm.password) {
signInError = '请输入密码';
return;
}
loginData = {
username: signInForm.username,
code: btoa(signInForm.password),
type: 'username'
};
} else {
// 手机号验证码登录
if (!validatePhone(signInForm.phone, signInForm.region)) {
signInError = '请输入正确的手机号格式';
return;
}
if (!signInForm.verifyCode) {
signInError = '请输入验证码';
return;
}
loginData = {
phone: signInForm.phone,
region: signInForm.region,
verify_code: signInForm.verifyCode,
type: 'phone'
};
}
signInLoading = true; // 开始加载
const loginResponse = await $axios.post('/api/user/login', loginData, {noretry: true});
if (loginResponse && typeof loginResponse === 'string') {
2 months ago
localStorage.setItem('refresh', loginResponse);
window.location.href = redirect;
} else {
console.warn('登录失败,服务器返回异常数据', loginResponse);
$message.warning('服务器异常');
7 months ago
}
} catch (error) {
2 months ago
signInError = error.message || '登录失败,请检查您的凭据。';
console.warn(signInError);
$message.warning(signInError);
} finally {
signInLoading = false; // 结束加载
7 months ago
}
};
// 创建动态背景气泡
createBubbles = () => {
const numberOfBubbles = 20;
for (let i = 0; i < numberOfBubbles; i++) {
const size = Math.random() * 100 + 50;
const left = Math.random() * 100;
const delay = Math.random() * 15;
const duration = Math.random() * 15 + 10;
bubbles.push({
style: {
width: `${size}px`,
height: `${size}px`,
left: `${left}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`,
},
});
}
};
</script>
<script>
// 页面加载时创建气泡
createBubbles();
</script>
</body>
</html>