Files
vue-driven-cloud-storage/frontend/reset-password.html
2026-06-13 18:45:12 +08:00

497 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码 - 玩玩云</title>
<link rel="stylesheet" href="libs/fontawesome/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* 暗色主题(默认) */
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--accent-1: #667eea;
--accent-2: #764ba2;
--glow: rgba(102, 126, 234, 0.4);
}
/* 亮色主题 */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.8);
--glass-border: rgba(102, 126, 234, 0.15);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--glow: rgba(90, 103, 216, 0.3);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* 动态背景 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
var(--bg-primary);
}
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
.container {
max-width: 450px;
width: 100%;
}
.card {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
body.light-theme .card {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
}
.logo {
font-size: 48px;
margin-bottom: 20px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
color: var(--text-primary);
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 30px;
font-size: 14px;
}
.status-icon {
font-size: 64px;
margin-bottom: 20px;
}
.status-icon.loading {
color: var(--accent-1);
animation: spin 1s linear infinite;
}
.status-icon.success {
color: #10b981;
}
.status-icon.error {
color: #ef4444;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.message {
font-size: 16px;
margin-bottom: 30px;
line-height: 1.6;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
.form-input {
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid var(--glass-border);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 15px;
transition: all 0.3s;
}
body.light-theme .form-input {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(102, 126, 234, 0.2);
}
.form-input:focus {
outline: none;
border-color: var(--accent-1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 32px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
border: none;
width: 100%;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
color: white;
box-shadow: 0 4px 15px var(--glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--glow);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.password-hint {
font-size: 12px;
color: var(--text-secondary);
margin-top: 8px;
}
.footer {
margin-top: 20px;
color: var(--text-secondary);
font-size: 13px;
}
.footer a {
color: var(--accent-1);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.alert {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
body.light-theme .alert-error {
color: #dc2626;
}
.alert-success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #6ee7b7;
}
body.light-theme .alert-success {
color: #059669;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<i class="fas fa-cloud"></i>
</div>
<h1 class="title">重置密码</h1>
<p class="subtitle">设置您的新密码</p>
<!-- 加载状态 -->
<div id="loading">
<div class="status-icon loading">
<i class="fas fa-spinner"></i>
</div>
<p class="message">正在验证链接...</p>
</div>
<!-- 错误状态 -->
<div id="error" class="hidden">
<div class="status-icon error">
<i class="fas fa-times-circle"></i>
</div>
<p class="message" id="errorMessage">链接无效或已过期</p>
<a href="app.html" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> 返回登录
</a>
</div>
<!-- 表单 -->
<div id="form" class="hidden">
<div id="formAlert" class="alert hidden"></div>
<form onsubmit="handleSubmit(event)">
<div class="form-group">
<label class="form-label">新密码</label>
<input type="password" id="password" class="form-input"
placeholder="请输入新密码" required minlength="8" maxlength="128">
<div class="password-hint">密码长度8-128位且包含字母、数字、特殊字符中的至少两种</div>
</div>
<div class="form-group">
<label class="form-label">确认密码</label>
<input type="password" id="confirmPassword" class="form-input"
placeholder="请再次输入新密码" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary">
<i class="fas fa-check"></i> 确认重置
</button>
</form>
</div>
<!-- 成功状态 -->
<div id="success" class="hidden">
<div class="status-icon success">
<i class="fas fa-check-circle"></i>
</div>
<p class="message">密码重置成功!<br>请使用新密码登录。</p>
<a href="app.html" class="btn btn-primary">
<i class="fas fa-right-to-bracket"></i> 前往登录
</a>
</div>
</div>
<div class="footer">
<a href="index.html"><i class="fas fa-arrow-left"></i> 返回首页</a>
</div>
</div>
<script>
let resetToken = '';
// 加载全局主题
async function loadTheme() {
try {
const res = await fetch('/api/public/theme');
const data = await res.json();
if (data.success && data.theme === 'light') {
document.body.classList.add('light-theme');
}
} catch (e) {
console.warn('[主题加载] 失败,使用默认主题:', e.message);
}
}
// 获取URL参数
function getParam(name) {
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
function getCookie(name) {
const prefix = `${name}=`;
const row = document.cookie
.split('; ')
.find(item => item.startsWith(prefix));
return row ? decodeURIComponent(row.substring(prefix.length)) : '';
}
async function ensureCsrfToken() {
let token = getCookie('csrf_token');
if (token) return token;
try {
const res = await fetch('/api/csrf-token', { credentials: 'include' });
const data = await res.json().catch(() => ({}));
token = data.csrfToken || data.token || getCookie('csrf_token');
} catch (error) {
console.warn('[CSRF] 获取 token 失败:', error.message);
}
return token || getCookie('csrf_token');
}
// 显示指定区块
function showSection(id) {
['loading', 'error', 'form', 'success'].forEach(s => {
document.getElementById(s).classList.add('hidden');
});
document.getElementById(id).classList.remove('hidden');
}
// 显示表单提示
function showFormAlert(type, message) {
const alert = document.getElementById('formAlert');
alert.className = `alert alert-${type}`;
alert.textContent = message;
alert.classList.remove('hidden');
}
function validateAccountPassword(password) {
const value = String(password || '');
if (value.length < 8) {
return { valid: false, message: '密码至少8个字符' };
}
if (value.length > 128) {
return { valid: false, message: '密码不能超过128个字符' };
}
const typeCount = [
/[a-zA-Z]/.test(value),
/[0-9]/.test(value),
/[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\\/`~]/.test(value)
].filter(Boolean).length;
if (typeCount < 2) {
return { valid: false, message: '密码必须包含字母、数字、特殊字符中的至少两种' };
}
const weakPasswords = [
'password', '12345678', '123456789', 'qwerty123', 'admin123',
'letmein', 'welcome', 'monkey', 'dragon', 'master'
];
if (weakPasswords.includes(value.toLowerCase())) {
return { valid: false, message: '密码过于简单,请使用更复杂的密码' };
}
return { valid: true };
}
// 验证token
async function validateToken() {
resetToken = getParam('resetToken') || getParam('token');
if (!resetToken) {
document.getElementById('errorMessage').textContent = '无效的重置链接,缺少令牌';
showSection('error');
return;
}
const cleanUrl = new URL(window.location.href);
cleanUrl.searchParams.delete('resetToken');
cleanUrl.searchParams.delete('token');
window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
// Token存在显示表单
showSection('form');
}
// 提交表单
async function handleSubmit(e) {
e.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const submitBtn = document.getElementById('submitBtn');
// 验证
const passwordCheck = validateAccountPassword(password);
if (!passwordCheck.valid) {
showFormAlert('error', passwordCheck.message);
return;
}
if (password !== confirmPassword) {
showFormAlert('error', '两次输入的密码不一致');
return;
}
// 提交
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
try {
const csrfToken = await ensureCsrfToken();
const res = await fetch('/api/password/reset', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: JSON.stringify({
token: resetToken,
new_password: password
})
});
const data = await res.json();
if (data.success) {
showSection('success');
} else {
showFormAlert('error', data.message || '重置失败,请重试');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-check"></i> 确认重置';
}
} catch (error) {
showFormAlert('error', '网络错误,请检查网络连接后重试');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-check"></i> 确认重置';
}
}
// 初始化
loadTheme();
validateToken();
</script>
</body>
</html>