416 lines
20 KiB
HTML
416 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}注册 - 智联青云{% endblock %}
|
|
{% block navbar %}{% endblock %}
|
|
{% block content %}
|
|
<!-- 覆盖全局背景,确保全屏填充 -->
|
|
<div class="fixed inset-0 z-[-1] bg-gradient-to-b from-sky-400 via-sky-200 to-white"></div>
|
|
<!-- 云团动态背景画布 -->
|
|
<canvas id="cloudCanvas" class="fixed inset-0 z-[-1] pointer-events-none w-full h-full"></canvas>
|
|
|
|
<div class="relative z-10 flex flex-col justify-center min-h-screen py-12 sm:px-6 lg:px-8">
|
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
|
<h2 class="mt-6 text-center text-4xl font-extrabold text-white drop-shadow-md tracking-tight">注册新账户</h2>
|
|
<p class="mt-2 text-center text-sm text-sky-800 font-medium">
|
|
已有账户? <a href="/login" class="font-bold text-blue-600 hover:text-blue-800 transition-all duration-300 hover:scale-110 inline-block drop-shadow-sm">立即登录</a>
|
|
</p>
|
|
</div>
|
|
<div class="relative z-10 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
<div class="bg-white/70 backdrop-blur-xl py-8 px-4 sm:px-10 rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.1)] border border-white/60">
|
|
<!-- 选项卡 -->
|
|
<div class="flex border-b border-slate-200 mb-6">
|
|
<button id="tab-phone" onclick="switchTab('phone')" class="flex-1 pb-4 text-sm font-medium text-center text-primary border-b-2 border-primary transition-all duration-300 hover:scale-105">手机号注册</button>
|
|
<button id="tab-email" onclick="switchTab('email')" class="flex-1 pb-4 text-sm font-medium text-center text-slate-500 hover:text-slate-700 transition-all duration-300 hover:scale-105">邮箱注册</button>
|
|
</div>
|
|
|
|
<!-- 手机注册表单 -->
|
|
<form id="form-phone" class="space-y-6" onsubmit="handlePhoneRegister(event)">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">姓名</label>
|
|
<input id="phone-name" type="text" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">手机号码</label>
|
|
<input id="phone-number" type="tel" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">图形验证码</label>
|
|
<div class="mt-1 flex items-center space-x-2">
|
|
<input id="captcha-text-phone" type="text" class="input-futuristic focus:ring-primary focus:border-primary block flex-1 sm:text-sm rounded-md py-2 px-3" placeholder="请输入图形验证码">
|
|
<img id="captcha-img-phone" class="cursor-pointer h-10 transition-transform duration-300 hover:scale-110" onclick="fetchCaptcha('phone')" alt="验证码">
|
|
<button type="button" onclick="fetchCaptcha('phone')" class="p-2 text-slate-400 hover:text-slate-600 transition-all duration-300 hover:rotate-180" title="刷新验证码">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">短信验证码</label>
|
|
<div class="mt-1 flex rounded-md shadow-sm">
|
|
<input id="sms-code-phone" type="text" required class="input-futuristic focus:ring-primary focus:border-primary block w-full rounded-none rounded-l-md sm:text-sm py-2 px-3" placeholder="请输入短信验证码">
|
|
<button type="button" id="send-sms-btn-phone" onclick="handleSendSmsPhone()" class="btn-futuristic relative inline-flex items-center px-4 py-2 text-sm font-medium rounded-r-md">获取验证码</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">密码</label>
|
|
<input id="phone-password" type="password" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">确认密码</label>
|
|
<input id="phone-confirm" type="password" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<button type="submit" class="btn-futuristic w-full flex justify-center py-2 px-4 rounded-md text-sm font-medium text-white">注册</button>
|
|
</form>
|
|
|
|
<!-- 邮箱注册表单(已添加手机号绑定) -->
|
|
<form id="form-email" class="space-y-6 hidden" onsubmit="handleEmailRegister(event)">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">姓名</label>
|
|
<input id="reg-name" type="text" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">邮箱地址</label>
|
|
<input id="reg-email" type="email" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">手机号码(用于绑定)</label>
|
|
<input id="reg-phone" type="tel" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">图形验证码</label>
|
|
<div class="mt-1 flex items-center space-x-2">
|
|
<input id="captcha-text-reg" type="text" class="input-futuristic focus:ring-primary focus:border-primary block flex-1 sm:text-sm rounded-md py-2 px-3" placeholder="请输入图形验证码">
|
|
<img id="captcha-img-reg" class="cursor-pointer h-10 transition-transform duration-300 hover:scale-110" onclick="fetchCaptcha('reg')" alt="验证码">
|
|
<button type="button" onclick="fetchCaptcha('reg')" class="p-2 text-slate-400 hover:text-slate-600 transition-all duration-300 hover:rotate-180" title="刷新验证码">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">邮箱验证码</label>
|
|
<div class="mt-1 flex rounded-md shadow-sm">
|
|
<input id="reg-email-code" type="text" required class="input-futuristic focus:ring-primary focus:border-primary block w-full rounded-none rounded-l-md sm:text-sm py-2 px-3" placeholder="请输入邮箱验证码">
|
|
<button type="button" id="send-email-btn" onclick="handleSendEmailCode()" class="btn-futuristic relative inline-flex items-center px-4 py-2 text-sm font-medium rounded-r-md">获取验证码</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">密码</label>
|
|
<input id="reg-password" type="password" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700">确认密码</label>
|
|
<input id="reg-confirm" type="password" required class="input-futuristic mt-1 appearance-none block w-full px-3 py-2 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
|
</div>
|
|
<button type="submit" class="btn-futuristic w-full flex justify-center py-2 px-4 rounded-md text-sm font-medium text-white">注册</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let captchaId = '';
|
|
let countdown = 0;
|
|
|
|
// 切换选项卡
|
|
function switchTab(tab) {
|
|
document.getElementById('form-phone').classList.toggle('hidden', tab !== 'phone');
|
|
document.getElementById('form-email').classList.toggle('hidden', tab !== 'email');
|
|
document.getElementById('tab-phone').className = 'flex-1 pb-4 text-sm font-medium text-center transition-all duration-300 hover:scale-105 ' + (tab === 'phone' ? 'text-primary border-b-2 border-primary' : 'text-slate-500 hover:text-slate-700');
|
|
document.getElementById('tab-email').className = 'flex-1 pb-4 text-sm font-medium text-center transition-all duration-300 hover:scale-105 ' + (tab === 'email' ? 'text-primary border-b-2 border-primary' : 'text-slate-500 hover:text-slate-700');
|
|
}
|
|
|
|
// 获取图形验证码
|
|
async function fetchCaptcha(suffix) {
|
|
try {
|
|
const res = await fetch('/api/captcha');
|
|
const data = await res.json();
|
|
captchaId = data.captchaId;
|
|
document.getElementById('captcha-img-' + suffix).src = 'data:image/png;base64,' + data.img;
|
|
const input = document.getElementById('captcha-text-' + suffix);
|
|
if (input) input.value = '';
|
|
} catch(e) { console.error('获取验证码失败', e); }
|
|
}
|
|
|
|
// 手机注册发送短信验证码
|
|
async function handleSendSmsPhone() {
|
|
const phone = document.getElementById('phone-number').value;
|
|
const captchaText = document.getElementById('captcha-text-phone').value;
|
|
if (!/^1[3-9]\d{9}$/.test(phone)) { alert('请输入有效的中国手机号码'); return; }
|
|
if (!captchaText) { alert('请输入图形验证码'); return; }
|
|
const btn = document.getElementById('send-sms-btn-phone');
|
|
btn.disabled = true;
|
|
try {
|
|
const res = await fetch('/api/send-sms', {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({phone, captchaId, captchaText})
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
countdown = 60;
|
|
const timer = setInterval(() => {
|
|
countdown--;
|
|
btn.textContent = countdown > 0 ? countdown+'s后重发' : '获取验证码';
|
|
if(countdown<=0){clearInterval(timer);btn.disabled=false;}
|
|
}, 1000);
|
|
alert('验证码已发送');
|
|
} else {
|
|
alert(data.message);
|
|
btn.disabled = false;
|
|
if(data.refreshCaptcha) fetchCaptcha('phone');
|
|
}
|
|
} catch(e) {
|
|
alert('发送失败,请确保后端已启动');
|
|
btn.disabled = false;
|
|
}
|
|
fetchCaptcha('phone');
|
|
}
|
|
|
|
// 手机注册提交
|
|
async function handlePhoneRegister(e) {
|
|
e.preventDefault();
|
|
const password = document.getElementById('phone-password').value;
|
|
const confirm = document.getElementById('phone-confirm').value;
|
|
if (password !== confirm) { alert('两次输入的密码不一致'); return; }
|
|
const body = {
|
|
name: document.getElementById('phone-name').value,
|
|
phone: document.getElementById('phone-number').value,
|
|
password: password,
|
|
smsCode: document.getElementById('sms-code-phone').value
|
|
};
|
|
try {
|
|
const res = await fetch('/api/register-mobile', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify(body)
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('注册成功!');
|
|
window.location.href = '/';
|
|
} else {
|
|
alert(data.message);
|
|
}
|
|
} catch(e) {
|
|
alert('注册失败');
|
|
}
|
|
}
|
|
|
|
// 邮箱发送验证码(保持不变)
|
|
async function handleSendEmailCode() {
|
|
const email = document.getElementById('reg-email').value;
|
|
const captchaText = document.getElementById('captcha-text-reg').value;
|
|
if (!email) { alert('请先输入邮箱地址'); return; }
|
|
if (!captchaText) { alert('请输入图形验证码'); return; }
|
|
const btn = document.getElementById('send-email-btn');
|
|
btn.disabled = true;
|
|
try {
|
|
const res = await fetch('/api/send-email-code', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({email, captchaId, captchaText})
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
countdown = 60;
|
|
const timer = setInterval(() => {
|
|
countdown--;
|
|
btn.textContent = countdown > 0 ? countdown+'s后重发' : '获取验证码';
|
|
if(countdown<=0){clearInterval(timer);btn.disabled=false;}
|
|
}, 1000);
|
|
alert('验证码已发送到邮箱,请查收');
|
|
} else {
|
|
alert(data.message);
|
|
btn.disabled = false;
|
|
if(data.refreshCaptcha) fetchCaptcha('reg');
|
|
}
|
|
} catch(e) {
|
|
alert('发送失败');
|
|
btn.disabled = false;
|
|
}
|
|
fetchCaptcha('reg');
|
|
}
|
|
|
|
// 邮箱注册提交(修改,增加手机号)
|
|
async function handleEmailRegister(e) {
|
|
e.preventDefault();
|
|
const password = document.getElementById('reg-password').value;
|
|
const confirm = document.getElementById('reg-confirm').value;
|
|
if (password !== confirm) { alert('两次输入的密码不一致'); return; }
|
|
const body = {
|
|
name: document.getElementById('reg-name').value,
|
|
email: document.getElementById('reg-email').value,
|
|
phone: document.getElementById('reg-phone').value,
|
|
password: password,
|
|
emailCode: document.getElementById('reg-email-code').value
|
|
};
|
|
try {
|
|
const res = await fetch('/api/register', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify(body)
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('注册成功!');
|
|
window.location.href = '/';
|
|
} else {
|
|
alert(data.message);
|
|
}
|
|
} catch(e) {
|
|
alert('注册失败');
|
|
}
|
|
}
|
|
|
|
// 初始化图形验证码(默认手机选项卡)
|
|
fetchCaptcha('phone');
|
|
|
|
// ========== 云团动态背景动画 ==========
|
|
const canvas = document.getElementById('cloudCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
let width, height;
|
|
let clouds = [];
|
|
let mouse = { x: -1000, y: -1000 };
|
|
|
|
function resize() {
|
|
width = canvas.width = window.innerWidth;
|
|
height = canvas.height = window.innerHeight;
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
});
|
|
window.addEventListener('mouseleave', () => {
|
|
mouse.x = -1000;
|
|
mouse.y = -1000;
|
|
});
|
|
|
|
// 预渲染柔和的云朵粒子,提升性能和真实感
|
|
const puffCanvas = document.createElement('canvas');
|
|
puffCanvas.width = 200;
|
|
puffCanvas.height = 200;
|
|
const pctx = puffCanvas.getContext('2d');
|
|
const grad = pctx.createRadialGradient(100, 100, 0, 100, 100, 100);
|
|
grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
|
grad.addColorStop(0.2, 'rgba(255, 255, 255, 0.8)');
|
|
grad.addColorStop(0.5, 'rgba(255, 255, 255, 0.3)');
|
|
grad.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
pctx.fillStyle = grad;
|
|
pctx.beginPath();
|
|
pctx.arc(100, 100, 100, 0, Math.PI * 2);
|
|
pctx.fill();
|
|
|
|
class Cloud {
|
|
constructor() {
|
|
this.reset(true);
|
|
}
|
|
|
|
reset(initial = false) {
|
|
this.size = Math.random() * 150 + 100; // 更大的云团
|
|
this.x = initial ? Math.random() * width : -this.size * 2;
|
|
this.y = Math.random() * height * 0.8; // 主要分布在上方
|
|
this.speedX = Math.random() * 0.5 + 0.2;
|
|
this.speedY = (Math.random() - 0.5) * 0.1;
|
|
this.baseOpacity = Math.random() * 0.5 + 0.3;
|
|
this.opacity = this.baseOpacity;
|
|
this.scattered = false;
|
|
|
|
// 生成云朵形状(由多个柔和粒子组成)
|
|
this.parts = [];
|
|
const numParts = Math.floor(Math.random() * 6) + 6;
|
|
for (let i = 0; i < numParts; i++) {
|
|
// 云朵底部较平,顶部不规则
|
|
const nx = (Math.random() - 0.5) * this.size * 1.5;
|
|
const ny = (Math.random() * 0.5 - 0.8) * this.size * 0.5;
|
|
this.parts.push({
|
|
offsetX: nx,
|
|
offsetY: ny,
|
|
radius: Math.random() * this.size * 0.5 + this.size * 0.4,
|
|
vx: 0,
|
|
vy: 0
|
|
});
|
|
}
|
|
}
|
|
|
|
update() {
|
|
if (!this.scattered) {
|
|
this.x += this.speedX;
|
|
this.y += this.speedY;
|
|
|
|
// 鼠标交互(散开效果)
|
|
const dx = this.x - mouse.x;
|
|
const dy = this.y - mouse.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// 鼠标靠近时触发散开
|
|
if (dist < 250) {
|
|
this.scattered = true;
|
|
// 给每个粒子一个远离鼠标的速度
|
|
for (let part of this.parts) {
|
|
const pdx = (this.x + part.offsetX) - mouse.x;
|
|
const pdy = (this.y + part.offsetY) - mouse.y;
|
|
const pdist = Math.sqrt(pdx * pdx + pdy * pdy) || 1;
|
|
const force = Math.max(0, 300 - pdist) / 300;
|
|
part.vx = (pdx / pdist) * force * (Math.random() * 6 + 2);
|
|
part.vy = (pdy / pdist) * force * (Math.random() * 6 + 2);
|
|
}
|
|
}
|
|
} else {
|
|
// 散开状态:粒子移动并淡出
|
|
this.opacity -= 0.015;
|
|
for (let part of this.parts) {
|
|
part.offsetX += part.vx;
|
|
part.offsetY += part.vy;
|
|
part.radius += 0.5; // 散开时稍微膨胀
|
|
// 增加空气阻力
|
|
part.vx *= 0.95;
|
|
part.vy *= 0.95;
|
|
}
|
|
}
|
|
|
|
// 边界检测或完全淡出后重置
|
|
if (this.x > width + this.size * 2 || this.opacity <= 0 || this.y < -this.size * 2 || this.y > height + this.size * 2) {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
draw() {
|
|
if (this.opacity <= 0) return;
|
|
ctx.save();
|
|
ctx.globalAlpha = Math.max(0, this.opacity);
|
|
|
|
for (let part of this.parts) {
|
|
const cx = this.x + part.offsetX;
|
|
const cy = this.y + part.offsetY;
|
|
const r = part.radius;
|
|
|
|
// 视野外不绘制
|
|
if (cx + r < 0 || cx - r > width || cy + r < 0 || cy - r > height) continue;
|
|
|
|
// 使用预渲染的柔和云朵粒子
|
|
ctx.drawImage(puffCanvas, cx - r, cy - r, r * 2, r * 2);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// 初始化云朵
|
|
for (let i = 0; i < 12; i++) {
|
|
clouds.push(new Cloud());
|
|
}
|
|
|
|
function animate() {
|
|
ctx.clearRect(0, 0, width, height);
|
|
for (let cloud of clouds) {
|
|
cloud.update();
|
|
cloud.draw();
|
|
}
|
|
requestAnimationFrame(animate);
|
|
}
|
|
animate();
|
|
</script>
|
|
{% endblock %} |