Files
simple-memo/static/html/login.html
T
sunct 3c3bf53ae4 docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
2026-07-31 14:12:32 +08:00

1662 lines
66 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 简记memo</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; min-height: 100vh; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-float { animation: float 3s ease-in-out infinite; }
.animate-fadeInUp { animation: fadeInUp 0.6s ease forwards; }
.delay-1 { animation-delay: 0.1s; }
.delay-2 { animation-delay: 0.2s; }
.delay-3 { animation-delay: 0.3s; }
.form-card {
background: #fff;
border-radius: 12px;
padding: 48px;
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
width: 560px;
max-width: 95%;
}
.form-input {
width: 100%;
padding: 12px 40px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
background: #f8fafc;
transition: all 0.2s;
}
.form-input:focus {
outline: none;
border-color: #4F9EDE;
box-shadow: 0 0 0 2px rgba(79, 158, 222, 0.1);
}
.btn-primary {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 600;
color: #fff;
cursor: pointer;
box-shadow: 0 4px 12px rgba(79, 158, 222, 0.3);
transition: all 0.2s;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(79, 158, 222, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
padding: 12px 16px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover:not(:disabled) {
background: #e2e8f0;
border-color: #cbd5e1;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.slide-captcha {
background: #f8fafc;
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
}
.slide-track {
width: 100%;
height: 40px;
background: #e2e8f0;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.slide-fill {
height: 100%;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
width: 0;
transition: width 0.1s linear;
}
.slide-btn {
position: absolute;
top: 50%;
left: 0;
width: 48px;
height: 48px;
margin: -24px 0 0 -4px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.2s;
}
.slide-btn:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.slide-btn.dragging {
cursor: grabbing;
box-shadow: 0 4px 16px rgba(79, 158, 222, 0.3);
}
.slide-icon {
width: 20px;
height: 20px;
fill: none;
stroke: #4F9EDE;
stroke-width: 2;
}
.slide-text {
position: absolute;
top: 50%;
left: 0;
right: 0;
text-align: center;
transform: translateY(-50%);
font-size: 13px;
color: #64748b;
pointer-events: none;
}
.slide-success {
color: #22c55e;
}
.slide-error {
color: #ef4444;
}
.form-switch {
text-align: center;
margin-top: 16px;
font-size: 13px;
color: #64748b;
}
.form-switch a {
color: #4F9EDE;
text-decoration: none;
font-weight: 500;
}
.form-switch a:hover {
text-decoration: underline;
}
.checkbox-group {
margin-bottom: 16px;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 6px;
cursor: pointer;
font-size: 12px;
color: #64748b;
}
.form-checkbox {
width: 14px;
height: 14px;
cursor: pointer;
margin-top: 3px;
}
.link-text {
color: #4F9EDE;
text-decoration: none;
}
.link-text:hover {
text-decoration: underline;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin-bottom: 24px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: #475569;
margin-bottom: 8px;
}
.form-label-inline {
display: inline-block;
font-size: 13px;
font-weight: 500;
color: #475569;
white-space: nowrap;
text-align: right;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
fill: none;
stroke: #94a3b8;
}
.error-message {
color: #ef4444;
font-size: 12px;
margin-top: 4px;
display: none;
}
.error-message.show {
display: block;
}
.login-container {
min-height: calc(100vh - 64px);
display: flex;
}
.login-left {
flex: 1;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 50%, #6366F1 100%);
padding: 60px;
display: flex;
flex-direction: column;
justify-content: center;
color: #fff;
position: relative;
overflow: hidden;
}
.login-right {
flex: 1;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
@media (max-width: 768px) {
body {
background: linear-gradient(160deg, #e8f4fd 0%, #f0f9ff 40%, #f8fafc 100%);
min-height: 100vh;
}
.login-container {
flex-direction: column;
min-height: calc(100vh - 56px);
padding-top: 56px;
}
.login-left {
display: none;
}
.login-right {
flex: none;
width: 100%;
min-height: calc(100vh - 56px);
padding: 24px 16px;
background: transparent;
display: flex;
align-items: flex-start;
justify-content: center;
}
.form-card {
width: 100%;
max-width: 420px;
padding: 32px 24px;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(79, 158, 222, 0.1), 0 1px 4px rgba(0, 0, 0, 0.04);
background: #ffffff;
animation: slideUp 0.45s cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
overflow: hidden;
}
.form-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4F9EDE, #60A5FA, #4F9EDE);
background-size: 200% 100%;
animation: shimmer 2.5s linear infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-title {
font-size: 22px;
font-weight: 700;
color: #1e293b;
margin-bottom: 28px;
text-align: center;
letter-spacing: -0.3px;
}
/* 手机端表单项间距 */
#loginForm > div[style*="margin-bottom"],
#registerForm > div[style*="margin-bottom"] {
margin-bottom: 16px !important;
}
.form-item {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
}
.form-label-inline {
width: auto !important;
font-size: 14px;
font-weight: 600;
color: #334155;
text-align: left !important;
margin-bottom: 2px;
}
/* 手机端表单行改为纵向布局 */
#loginForm > div > div:first-child,
#registerForm > div > div:first-child {
flex-direction: column !important;
align-items: stretch !important;
gap: 4px !important;
}
/* 输入容器占满宽度 */
#loginForm .input-wrapper,
#registerForm .input-wrapper,
#loginForm > div > div:first-child > div[style*="display: flex"],
#registerForm > div > div:first-child > div[style*="display: flex"] {
width: 100% !important;
flex: none !important;
max-width: 100% !important;
}
/* 验证码区域内部水平排列 */
#loginForm > div > div:first-child > div[style*="display: flex"],
#registerForm > div > div:first-child > div[style*="display: flex"] {
flex-direction: row !important;
align-items: center !important;
gap: 10px !important;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.form-input {
width: 100%;
padding: 13px 14px 13px 44px;
font-size: 15px;
font-weight: 400;
border-radius: 12px;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
color: #1e293b;
transition: all 0.2s ease;
}
.form-input::placeholder {
color: #94a3b8;
font-weight: 400;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: #4F9EDE;
background: #fff;
box-shadow: 0 0 0 3px rgba(79, 158, 222, 0.1);
}
.form-input:focus + .input-icon {
stroke: #4F9EDE;
}
.input-icon {
position: absolute;
left: 14px;
width: 18px;
height: 18px;
stroke: #94a3b8;
transition: stroke 0.2s ease;
pointer-events: none;
}
.captcha-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.captcha-wrapper .form-input {
flex: 1;
padding-right: 14px;
}
.captcha-img {
width: 100px;
height: 44px;
border-radius: 10px;
cursor: pointer;
border: 1.5px solid #e2e8f0;
transition: all 0.2s ease;
flex-shrink: 0;
}
.captcha-img:hover {
border-color: #4F9EDE;
}
.btn-primary {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
border-radius: 12px;
border: none;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
color: #fff;
box-shadow: 0 4px 14px rgba(79, 158, 222, 0.3);
transition: all 0.2s ease;
cursor: pointer;
margin-top: 4px;
}
.btn-primary:hover:not(:disabled),
.btn-primary:active:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(79, 158, 222, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
padding: 12px 16px;
font-size: 13px;
font-weight: 600;
width: 110px !important;
border-radius: 10px;
background: #f1f5f9;
border: 1.5px solid #e2e8f0;
color: #475569;
transition: all 0.2s ease;
cursor: pointer;
flex-shrink: 0;
}
.btn-secondary:hover:not(:disabled) {
background: #e2e8f0;
border-color: #cbd5e1;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.code-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.code-input-wrapper .form-input {
flex: 1;
}
#loginForm, #registerForm {
padding: 0;
}
.checkbox-group {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 20px;
margin-top: 4px;
}
.checkbox-group input[type="checkbox"] {
width: 16px;
height: 16px;
border-radius: 5px;
border: 1.5px solid #cbd5e1;
accent-color: #4F9EDE;
cursor: pointer;
margin-top: 2px;
flex-shrink: 0;
}
.checkbox-label {
font-size: 12px;
color: #64748b;
line-height: 1.5;
}
.checkbox-label a {
color: #4F9EDE;
font-weight: 500;
text-decoration: none;
}
.checkbox-label a:hover {
text-decoration: underline;
}
.form-switch {
margin-top: 20px;
text-align: center;
font-size: 13px;
color: #64748b;
}
.form-switch a {
color: #4F9EDE;
font-weight: 600;
text-decoration: none;
margin-left: 4px;
}
.form-switch a:hover {
text-decoration: underline;
}
.error-message {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
padding-left: 2px;
display: flex;
align-items: center;
gap: 4px;
}
}
@media (max-width: 480px) {
.login-right {
padding: 12px 16px;
align-items: center;
}
.form-card {
padding: 28px 20px;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(79, 158, 222, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
}
.form-card::before {
height: 4px;
}
.form-title {
font-size: 22px;
margin-bottom: 28px;
letter-spacing: -0.5px;
}
/* 小屏幕表单项间距 */
#loginForm > div[style*="margin-bottom"],
#registerForm > div[style*="margin-bottom"] {
margin-bottom: 18px !important;
}
.form-input {
padding: 14px 14px 14px 44px;
font-size: 15px;
border-radius: 12px;
border: 1.5px solid #e2e8f0;
}
.form-input::placeholder {
font-size: 14px;
color: #94a3b8;
}
.input-icon {
left: 14px;
width: 18px;
height: 18px;
}
.btn-primary {
padding: 15px;
font-size: 15px;
border-radius: 12px;
font-weight: 600;
margin-top: 8px;
}
.btn-secondary {
width: 110px !important;
font-size: 13px;
padding: 13px 12px;
border-radius: 12px;
font-weight: 500;
}
.captcha-img {
width: 100px;
height: 46px;
border-radius: 10px;
}
.checkbox-label {
font-size: 12px;
line-height: 1.5;
}
.form-switch {
font-size: 13px;
margin-top: 20px;
padding: 12px 0;
}
.form-label-inline {
font-size: 14px;
font-weight: 600;
color: #334155;
}
.error-message {
font-size: 12px;
margin-top: 6px;
padding-left: 4px;
}
/* 优化验证码区域 */
#loginForm > div > div:first-child > div[style*="display: flex"],
#registerForm > div > div:first-child > div[style*="display: flex"] {
gap: 12px !important;
}
/* 优化注册表单标签宽度 */
#registerForm .form-label-inline {
min-width: 70px;
}
/* 优化复选框 */
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
margin-top: 1px;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.login-right {
padding: 10px 12px;
}
.form-card {
padding: 24px 16px;
border-radius: 16px;
}
.form-title {
font-size: 20px;
margin-bottom: 24px;
}
.form-input {
padding: 12px 12px 12px 40px;
font-size: 14px;
}
.btn-primary {
padding: 13px;
font-size: 14px;
}
.captcha-img {
width: 90px;
height: 42px;
}
.btn-secondary {
width: 100px !important;
font-size: 12px;
padding: 12px 10px;
}
}
</style>
</head>
<body>
<div id="app">
<!-- 公共头部 -->
<div id="commonHeader"></div>
<!-- 登录主区域 -->
<div class="login-container">
<!-- 左侧展示区 -->
<div class="login-left">
<div style="position: absolute; top: 20px; right: 20px; width: 300px; height: 300px; background: rgba(255,255,255,0.05); border-radius: 50%;"></div>
<div style="position: absolute; bottom: 20px; left: 20px; width: 200px; height: 200px; background: rgba(255,255,255,0.05); border-radius: 50%;"></div>
<div style="position: relative; z-index: 1; max-width: 500px;">
<div class="animate-fadeInUp" style="margin-bottom: 24px;">
<div style="width: 80px; height: 80px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin-bottom: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);">
<img src="/static/images/logo.png" alt="简记memo" style="width: 48px; height: 48px; border-radius: 8px;" onerror="this.style.display='none'; this.parentElement.innerHTML='<svg style=\'width: 40px; height: 40px; fill: #fff;\' viewBox=\'0 0 24 24\'><path d=\'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5\'/></svg>';">
</div>
<h1 style="font-size: 36px; font-weight: 700; margin-bottom: 12px;">简记memo</h1>
<p style="font-size: 16px; opacity: 0.9;">让记录更简单</p>
</div>
<div class="animate-fadeInUp delay-1" style="margin-bottom: 32px;">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 16px;">为什么选择简记?</h2>
<ul style="list-style: none; display: flex; flex-direction: column; gap: 12px;">
<li style="display: flex; align-items: center; gap: 12px;">
<div style="width: 28px; height: 28px; background: rgba(255,255,255,0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center;"></div>
<span style="font-size: 14px;">简洁优雅的界面设计</span>
</li>
<li style="display: flex; align-items: center; gap: 12px;">
<div style="width: 28px; height: 28px; background: rgba(255,255,255,0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center;">📅</div>
<span style="font-size: 14px;">日历视图一目了然</span>
</li>
<li style="display: flex; align-items: center; gap: 12px;">
<div style="width: 28px; height: 28px; background: rgba(255,255,255,0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center;">😊</div>
<span style="font-size: 14px;">记录每一天的心情</span>
</li>
<li style="display: flex; align-items: center; gap: 12px;">
<div style="width: 28px; height: 28px; background: rgba(255,255,255,0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center;">💰</div>
<span style="font-size: 14px;">轻松管理收支账单</span>
</li>
</ul>
</div>
<div class="animate-fadeInUp delay-2">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="animate-float" style="width: 48px; height: 48px; background: rgba(255,255,255,0.15); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">📋</div>
<div class="animate-float" style="width: 48px; height: 48px; background: rgba(255,255,255,0.15); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; animation-delay: 0.5s;">😊</div>
<div class="animate-float" style="width: 48px; height: 48px; background: rgba(255,255,255,0.15); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; animation-delay: 1s;">💰</div>
<div class="animate-float" style="width: 48px; height: 48px; background: rgba(255,255,255,0.15); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; animation-delay: 1.5s;">🏆</div>
</div>
</div>
</div>
</div>
<!-- 右侧登录区 -->
<div class="login-right">
<div style="width: 100%; max-width: 580px;">
<!-- 登录表单 -->
<div id="loginForm" class="form-card animate-fadeInUp">
<h2 class="form-title">欢迎回来</h2>
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 60px;">邮箱</label>
<div class="input-wrapper" style="flex: 1;">
<input type="email" id="loginEmail" placeholder="请输入邮箱地址" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
</div>
<div id="loginEmailError" class="error-message"></div>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 60px;">密码</label>
<div class="input-wrapper" style="flex: 1;">
<input type="password" id="loginPassword" placeholder="请输入密码" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
</div>
<div id="loginPasswordError" class="error-message"></div>
</div>
<!-- 数字验证码 -->
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 60px;">验证码</label>
<div style="flex: 1; display: flex; gap: 10px;">
<input type="text" id="loginCaptchaInput" placeholder="请输入验证码" class="form-input" style="flex: 1; min-width: 0;">
<img id="loginCaptchaImg" src="" alt="验证码" style="width: 90px; height: 40px; border-radius: 6px; cursor: pointer; flex-shrink: 0;" onclick="refreshLoginCaptcha()">
</div>
</div>
<div id="loginCaptchaError" class="error-message"></div>
</div>
<button id="loginBtn" class="btn-primary" disabled>登录</button>
<p class="form-switch">
还没有账号?
<a href="javascript:void(0)" onclick="switchToRegister()">立即注册</a>
<span style="margin: 0 8px; color: #e2e8f0;">|</span>
<a href="javascript:void(0)" onclick="switchToForgot()" style="color: #64748b;">忘记密码</a>
</p>
</div>
<!-- 注册表单 -->
<div id="registerForm" class="form-card animate-fadeInUp" style="display: none;">
<h2 class="form-title">创建账号</h2>
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">邮箱</label>
<div class="input-wrapper" style="flex: 1;">
<input type="email" id="registerEmail" placeholder="请输入邮箱地址" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
</div>
<div id="registerEmailError" class="error-message"></div>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">密码</label>
<div class="input-wrapper" style="flex: 1;">
<input type="password" id="registerPassword" placeholder="请输入密码(至少6位)" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
</div>
<div id="registerPasswordError" class="error-message"></div>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">确认密码</label>
<div class="input-wrapper" style="flex: 1;">
<input type="password" id="registerConfirmPassword" placeholder="请再次输入密码" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
</div>
<div id="registerConfirmPasswordError" class="error-message"></div>
</div>
<!-- 图形验证码 -->
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">图形验证码</label>
<div style="flex: 1; display: flex; gap: 12px;">
<input type="text" id="registerCaptchaInput" placeholder="请输入验证码" class="form-input" style="flex: 2; min-width: 100px;">
<img id="registerCaptchaImg" src="" alt="验证码" style="width: 115px; height: 40px; border-radius: 6px; cursor: pointer; flex-shrink: 0; object-fit: cover;" onclick="refreshRegisterCaptcha()">
</div>
</div>
<div id="registerCaptchaError" class="error-message"></div>
</div>
<!-- 邮箱验证码 -->
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">邮箱验证码</label>
<div style="flex: 1; display: flex; gap: 12px;">
<input type="text" id="registerCode" placeholder="请输入邮箱验证码" class="form-input" style="flex: 2; min-width: 100px;">
<button id="sendRegisterCodeBtn" class="btn-secondary" disabled style="flex-shrink: 0; width: 115px;">发送验证码</button>
</div>
</div>
<div id="registerCodeError" class="error-message"></div>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="agreeTerms" class="form-checkbox">
<span class="checkbox-text">
我已阅读并同意
<a href="/agreement" class="link-text">用户协议</a>
<a href="/privacy" class="link-text">隐私政策</a>
</span>
</label>
</div>
<button id="registerBtn" class="btn-primary" disabled>注册</button>
<p class="form-switch">
已有账号?
<a href="javascript:void(0)" onclick="switchToLogin()">立即登录</a>
</p>
</div>
<!-- 找回密码表单 -->
<div id="forgotForm" class="form-card animate-fadeInUp" style="display: none;">
<h2 class="form-title">找回密码</h2>
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">邮箱</label>
<div class="input-wrapper" style="flex: 1;">
<input type="email" id="forgotEmail" placeholder="请输入注册邮箱" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
</div>
<div id="forgotEmailError" class="error-message"></div>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">新密码</label>
<div class="input-wrapper" style="flex: 1;">
<input type="password" id="forgotPassword" placeholder="请输入新密码(至少6位)" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
</div>
<div id="forgotPasswordError" class="error-message"></div>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">确认密码</label>
<div class="input-wrapper" style="flex: 1;">
<input type="password" id="forgotConfirmPassword" placeholder="请再次输入新密码" class="form-input">
<svg class="input-icon" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
</div>
<div id="forgotConfirmPasswordError" class="error-message"></div>
</div>
<!-- 图形验证码 -->
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">图形验证码</label>
<div style="flex: 1; display: flex; gap: 12px;">
<input type="text" id="forgotCaptchaInput" placeholder="请输入验证码" class="form-input" style="flex: 2; min-width: 100px;">
<img id="forgotCaptchaImg" src="" alt="验证码" style="width: 115px; height: 40px; border-radius: 6px; cursor: pointer; flex-shrink: 0; object-fit: cover;" onclick="refreshForgotCaptcha()">
</div>
</div>
<div id="forgotCaptchaError" class="error-message"></div>
</div>
<!-- 邮箱验证码 -->
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="form-label-inline" style="width: 70px;">邮箱验证码</label>
<div style="flex: 1; display: flex; gap: 12px;">
<input type="text" id="forgotCode" placeholder="请输入邮箱验证码" class="form-input" style="flex: 2; min-width: 100px;">
<button id="sendForgotCodeBtn" class="btn-secondary" disabled style="flex-shrink: 0; width: 115px;">发送验证码</button>
</div>
</div>
<div id="forgotCodeError" class="error-message"></div>
</div>
<button id="forgotBtn" class="btn-primary" disabled>重置密码</button>
<p class="form-switch">
想起密码了?
<a href="javascript:void(0)" onclick="switchToLogin()">立即登录</a>
</p>
</div>
</div>
</div>
</div>
<!-- 公共底部 -->
<div id="commonFooter"></div>
</div>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
<script src="/static/js/config.js"></script>
<script src="/static/js/common-components.js"></script>
<script src="/static/js/toast.js"></script>
<script>
let loginCaptchaValid = false;
let registerCaptchaValid = false;
let forgotCaptchaValid = false;
Vue.use(ELEMENT);
const app = new Vue({
el: '#app'
});
function showToast(message, type = 'success') {
if (typeof app.$message !== 'undefined') {
app.$message({
type: type,
message: message
});
} else {
$toast.error(message);
}
}
function checkLoginStatus() {
const userInfo = UserStore.get();
const token = userInfo && userInfo.token ? userInfo.token : localStorage.getItem('user_token');
if (token) {
window.location.href = '/web/calendar';
}
}
checkLoginStatus();
let loginCaptchaId = '';
let registerCaptchaId = '';
let forgotCaptchaId = '';
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/login');
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
initCommonComponents();
loadLoginCaptcha();
loadRegisterCaptcha();
loadForgotCaptcha();
document.getElementById('loginBtn').addEventListener('click', handleLogin);
document.getElementById('registerBtn').addEventListener('click', handleRegister);
document.getElementById('forgotBtn').addEventListener('click', handleForgot);
document.getElementById('sendRegisterCodeBtn').addEventListener('click', sendRegisterCode);
document.getElementById('sendForgotCodeBtn').addEventListener('click', sendForgotCode);
document.getElementById('loginCaptchaInput').addEventListener('input', validateLoginForm);
document.getElementById('registerEmail').addEventListener('input', validateRegisterCaptcha);
document.getElementById('registerCaptchaInput').addEventListener('input', validateRegisterCaptcha);
document.getElementById('registerPassword').addEventListener('input', clearPasswordError);
document.getElementById('registerConfirmPassword').addEventListener('input', clearConfirmPasswordError);
document.getElementById('registerCaptchaInput').addEventListener('input', clearCaptchaError);
document.getElementById('registerEmail').addEventListener('input', validateRegisterForm);
document.getElementById('registerPassword').addEventListener('input', validateRegisterForm);
document.getElementById('registerConfirmPassword').addEventListener('input', validateRegisterForm);
document.getElementById('registerCaptchaInput').addEventListener('input', validateRegisterForm);
document.getElementById('registerCode').addEventListener('input', validateRegisterForm);
// 找回密码表单事件
document.getElementById('forgotEmail').addEventListener('input', validateForgotCaptcha);
document.getElementById('forgotCaptchaInput').addEventListener('input', validateForgotCaptcha);
document.getElementById('forgotPassword').addEventListener('input', validateForgotForm);
document.getElementById('forgotConfirmPassword').addEventListener('input', validateForgotForm);
document.getElementById('forgotCaptchaInput').addEventListener('input', validateForgotForm);
document.getElementById('forgotCode').addEventListener('input', validateForgotForm);
});
function loadLoginCaptcha() {
fetch('/api/web/captcha/digit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
loginCaptchaId = data.data.captchaId;
document.getElementById('loginCaptchaImg').src = data.data.image;
}
});
}
function loadRegisterCaptcha() {
fetch('/api/web/captcha/digit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
registerCaptchaId = data.data.captchaId;
document.getElementById('registerCaptchaImg').src = data.data.image;
}
});
}
function refreshLoginCaptcha() {
loadLoginCaptcha();
document.getElementById('loginCaptchaInput').value = '';
}
function refreshRegisterCaptcha() {
loadRegisterCaptcha();
document.getElementById('registerCaptchaInput').value = '';
}
function validateLoginForm() {
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
const captcha = document.getElementById('loginCaptchaInput').value;
const btn = document.getElementById('loginBtn');
btn.disabled = !(email && password && captcha && captcha.length >= 4);
}
function validateRegisterForm() {
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const confirmPassword = document.getElementById('registerConfirmPassword').value;
const code = document.getElementById('registerCode').value;
const btn = document.getElementById('registerBtn');
// 注:图形验证码在发送邮箱验证码时已验证,此处不再校验
btn.disabled = !(email && password && confirmPassword && password === confirmPassword && code);
}
function switchToRegister() {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('forgotForm').style.display = 'none';
document.getElementById('registerForm').style.display = 'block';
resetRegisterCaptcha();
}
function switchToLogin() {
document.getElementById('registerForm').style.display = 'none';
document.getElementById('forgotForm').style.display = 'none';
document.getElementById('loginForm').style.display = 'block';
resetLoginCaptcha();
}
function switchToForgot() {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('registerForm').style.display = 'none';
document.getElementById('forgotForm').style.display = 'block';
resetForgotCaptcha();
}
function resetLoginCaptcha() {
loginCaptchaValid = false;
document.getElementById('loginBtn').disabled = true;
document.getElementById('loginCaptchaInput').value = '';
loadLoginCaptcha();
}
function refreshRegisterCaptcha() {
loadRegisterCaptcha();
}
function resetRegisterCaptcha() {
registerCaptchaValid = false;
document.getElementById('sendRegisterCodeBtn').disabled = true;
document.getElementById('registerCaptchaInput').value = '';
document.getElementById('registerCode').value = '';
document.getElementById('registerCode').disabled = true;
loadRegisterCaptcha();
}
function validateRegisterCaptcha() {
const captcha = document.getElementById('registerCaptchaInput').value;
const email = document.getElementById('registerEmail').value;
// 清除邮箱错误提示(当用户修改邮箱时)
document.getElementById('registerEmailError').textContent = '';
document.getElementById('registerEmailError').classList.remove('show');
if (captcha && captcha.length >= 4 && email) {
document.getElementById('sendRegisterCodeBtn').disabled = false;
} else {
document.getElementById('sendRegisterCodeBtn').disabled = true;
}
}
function sendRegisterCode() {
const email = document.getElementById('registerEmail').value;
const captcha = document.getElementById('registerCaptchaInput').value;
clearErrors(['registerEmailError', 'registerCaptchaError', 'registerCodeError']);
if (!email) {
showError('registerEmailError', '请输入邮箱地址');
return;
}
if (!captcha || captcha.length < 4) {
showError('registerCaptchaError', '请输入图形验证码');
return;
}
const btn = document.getElementById('sendRegisterCodeBtn');
btn.disabled = true;
fetch('/api/web/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
type: 'register',
captchaId: registerCaptchaId,
captcha: captcha
})
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
showToast('验证码已发送到您的邮箱', 'success');
document.getElementById('registerCode').disabled = false;
startCountdown(btn);
} else {
if (data.msg.includes('验证码') || data.msg.includes('captcha')) {
showError('registerCaptchaError', data.msg);
refreshRegisterCaptcha();
} else if (data.msg.includes('邮箱') || data.msg.includes('email')) {
showError('registerEmailError', data.msg);
} else {
showToast(data.msg, 'error');
}
btn.disabled = false;
}
})
.catch(err => {
console.error('发送验证码失败:', err);
showToast('发送验证码失败,请稍后重试', 'error');
btn.disabled = false;
});
}
function startCountdown(btn) {
let seconds = 60;
btn.textContent = `${seconds}秒后重发`;
const timer = setInterval(() => {
seconds--;
if (seconds <= 0) {
clearInterval(timer);
btn.textContent = '发送验证码';
btn.disabled = false;
} else {
btn.textContent = `${seconds}秒后重发`;
}
}, 1000);
}
function handleLogin() {
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
const captcha = document.getElementById('loginCaptchaInput').value;
clearErrors(['loginEmailError', 'loginPasswordError', 'loginCaptchaError']);
if (!email) {
showError('loginEmailError', '请输入邮箱地址');
return;
}
if (!password) {
showError('loginPasswordError', '请输入密码');
return;
}
if (!captcha || captcha.length < 4) {
showError('loginCaptchaError', '请输入验证码');
return;
}
fetch('/api/web/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
captchaId: loginCaptchaId,
captcha: captcha
})
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
const avatar = data.data.avatar || '/static/avatar/default.png';
localStorage.setItem('user_token', data.data.token);
localStorage.setItem('user_nickname', data.data.nickname || '用户');
localStorage.setItem('user_avatar', avatar);
localStorage.setItem('user_email', data.data.email || '');
// 存储token过期时间
if (data.data.expiresAt) {
localStorage.setItem('user_token_expires', data.data.expiresAt);
}
// 同步到 UserStore,供头部组件判断登录状态
UserStore.set({ token: data.data.token, nickname: data.data.nickname || '用户', avatar: avatar, email: data.data.email || '' });
window.location.href = '/web/calendar';
} else {
showError('loginCaptchaError', data.msg);
refreshLoginCaptcha();
}
})
.catch(err => {
console.error('登录失败:', err);
showToast('登录失败,请稍后重试', 'error');
refreshLoginCaptcha();
});
}
function handleRegister() {
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const confirmPassword = document.getElementById('registerConfirmPassword').value;
const code = document.getElementById('registerCode').value;
const agreeTerms = document.getElementById('agreeTerms').checked;
clearErrors(['registerEmailError', 'registerPasswordError', 'registerConfirmPasswordError', 'registerCaptchaError']);
if (!email) {
showError('registerEmailError', '请输入邮箱地址');
return;
}
if (!password) {
showError('registerPasswordError', '请输入密码');
return;
}
if (password.length < 6) {
showError('registerPasswordError', '密码至少需要6位');
return;
}
if (!confirmPassword) {
showError('registerConfirmPasswordError', '请输入确认密码');
return;
}
if (password !== confirmPassword) {
showError('registerConfirmPasswordError', '两次输入的密码不一致');
return;
}
if (!code) {
showError('registerCodeError', '请输入邮箱验证码');
return;
}
if (!agreeTerms) {
showToast('请先同意用户协议和隐私政策', 'warning');
return;
}
// 注:图形验证码在发送邮箱验证码时已验证,此处不再发送
fetch('/api/web/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
code
})
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
const avatar = data.data.avatar || '/static/avatar/default.png';
localStorage.setItem('user_token', data.data.token);
localStorage.setItem('user_nickname', data.data.nickname || '用户');
localStorage.setItem('user_avatar', avatar);
localStorage.setItem('user_email', data.data.email || '');
// 存储token过期时间
if (data.data.expiresAt) {
localStorage.setItem('user_token_expires', data.data.expiresAt);
}
// 同步到 UserStore,供头部组件判断登录状态
UserStore.set({ token: data.data.token, nickname: data.data.nickname || '用户', avatar: avatar, email: data.data.email || '' });
window.location.href = '/web/calendar';
} else {
showError('registerCaptchaError', data.msg);
refreshRegisterCaptcha();
}
})
.catch(err => {
console.error('注册失败:', err);
showToast('注册失败,请稍后重试', 'error');
refreshRegisterCaptcha();
});
}
function showError(elementId, message) {
const element = document.getElementById(elementId);
element.textContent = message;
element.classList.add('show');
}
function clearErrors(elementIds) {
elementIds.forEach(id => {
const element = document.getElementById(id);
element.textContent = '';
element.classList.remove('show');
});
}
function clearPasswordError() {
document.getElementById('registerPasswordError').textContent = '';
document.getElementById('registerPasswordError').classList.remove('show');
}
function clearConfirmPasswordError() {
document.getElementById('registerConfirmPasswordError').textContent = '';
document.getElementById('registerConfirmPasswordError').classList.remove('show');
}
function clearCaptchaError() {
document.getElementById('registerCaptchaError').textContent = '';
document.getElementById('registerCaptchaError').classList.remove('show');
}
// ==================== 找回密码相关函数 ====================
function loadForgotCaptcha() {
fetch('/api/web/captcha/digit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
forgotCaptchaId = data.data.captchaId;
document.getElementById('forgotCaptchaImg').src = data.data.image;
}
});
}
function refreshForgotCaptcha() {
loadForgotCaptcha();
document.getElementById('forgotCaptchaInput').value = '';
}
function resetForgotCaptcha() {
forgotCaptchaValid = false;
document.getElementById('sendForgotCodeBtn').disabled = true;
document.getElementById('forgotCaptchaInput').value = '';
document.getElementById('forgotCode').value = '';
document.getElementById('forgotCode').disabled = true;
loadForgotCaptcha();
}
function validateForgotCaptcha() {
const captcha = document.getElementById('forgotCaptchaInput').value;
const email = document.getElementById('forgotEmail').value;
document.getElementById('forgotEmailError').textContent = '';
document.getElementById('forgotEmailError').classList.remove('show');
if (captcha && captcha.length >= 4 && email) {
document.getElementById('sendForgotCodeBtn').disabled = false;
} else {
document.getElementById('sendForgotCodeBtn').disabled = true;
}
}
function validateForgotForm() {
const email = document.getElementById('forgotEmail').value;
const password = document.getElementById('forgotPassword').value;
const confirmPassword = document.getElementById('forgotConfirmPassword').value;
const code = document.getElementById('forgotCode').value;
const captcha = document.getElementById('forgotCaptchaInput').value;
const btn = document.getElementById('forgotBtn');
btn.disabled = !(email && password && confirmPassword && password === confirmPassword && code && captcha && captcha.length >= 4);
}
function sendForgotCode() {
const email = document.getElementById('forgotEmail').value;
const captcha = document.getElementById('forgotCaptchaInput').value;
clearErrors(['forgotEmailError', 'forgotCaptchaError', 'forgotCodeError']);
if (!email) {
showError('forgotEmailError', '请输入邮箱地址');
return;
}
if (!captcha || captcha.length < 4) {
showError('forgotCaptchaError', '请输入图形验证码');
return;
}
const btn = document.getElementById('sendForgotCodeBtn');
btn.disabled = true;
fetch('/api/web/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
type: 'reset_password',
captchaId: forgotCaptchaId,
captcha: captcha
})
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
showToast('验证码已发送到您的邮箱', 'success');
document.getElementById('forgotCode').disabled = false;
startCountdown(btn);
} else {
if (data.msg.includes('验证码') || data.msg.includes('captcha')) {
showError('forgotCaptchaError', data.msg);
refreshForgotCaptcha();
} else if (data.msg.includes('邮箱') || data.msg.includes('email')) {
showError('forgotEmailError', data.msg);
} else {
showToast(data.msg, 'error');
}
btn.disabled = false;
}
})
.catch(err => {
console.error('发送验证码失败:', err);
showToast('发送验证码失败,请稍后重试', 'error');
btn.disabled = false;
});
}
function handleForgot() {
const email = document.getElementById('forgotEmail').value;
const password = document.getElementById('forgotPassword').value;
const confirmPassword = document.getElementById('forgotConfirmPassword').value;
const code = document.getElementById('forgotCode').value;
const captcha = document.getElementById('forgotCaptchaInput').value;
clearErrors(['forgotEmailError', 'forgotPasswordError', 'forgotConfirmPasswordError', 'forgotCaptchaError']);
if (!email) {
showError('forgotEmailError', '请输入邮箱地址');
return;
}
if (!password) {
showError('forgotPasswordError', '请输入新密码');
return;
}
if (password.length < 6) {
showError('forgotPasswordError', '密码至少需要6位');
return;
}
if (!confirmPassword) {
showError('forgotConfirmPasswordError', '请输入确认密码');
return;
}
if (password !== confirmPassword) {
showError('forgotConfirmPasswordError', '两次输入的密码不一致');
return;
}
if (!code) {
showError('forgotCodeError', '请输入邮箱验证码');
return;
}
if (!captcha || captcha.length < 4) {
showError('forgotCaptchaError', '请输入图形验证码');
return;
}
fetch('/api/web/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
code,
captchaId: forgotCaptchaId,
captcha: captcha
})
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
showToast('密码重置成功,请使用新密码登录', 'success');
switchToLogin();
} else {
showError('forgotCaptchaError', data.msg);
refreshForgotCaptcha();
}
})
.catch(err => {
console.error('重置密码失败:', err);
showToast('重置密码失败,请稍后重试', 'error');
refreshForgotCaptcha();
});
}
</script>
</body>
</html>