feat: 后台管理添加用户管理功能 + 前端用户注册登录调用backend API
This commit is contained in:
@@ -435,6 +435,10 @@
|
||||
<span class="sidebar-icon">📊</span>
|
||||
<span>统计信息</span>
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="users">
|
||||
<span class="sidebar-icon">👥</span>
|
||||
<span>用户管理</span>
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="llm">
|
||||
<span class="sidebar-icon">🧠</span>
|
||||
<span>大模型配置</span>
|
||||
|
||||
252
www/admin.js
252
www/admin.js
@@ -81,6 +81,9 @@ async function loadPage(page) {
|
||||
case 'stats':
|
||||
await loadStatsPage(content);
|
||||
break;
|
||||
case 'users':
|
||||
await loadUsersPage(content);
|
||||
break;
|
||||
case 'llm':
|
||||
await loadLLMPage(content);
|
||||
break;
|
||||
@@ -177,6 +180,222 @@ async function loadStatsPage(content) {
|
||||
`;
|
||||
}
|
||||
|
||||
// ==================== 用户管理页面 ====================
|
||||
|
||||
let users = [];
|
||||
|
||||
async function loadUsersPage(content) {
|
||||
users = await fetchAPI('/api/admin/users');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="content-header">
|
||||
<h1 class="content-title">用户管理</h1>
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<input type="text" class="form-input" id="userSearch" placeholder="搜索用户名/手机/邮箱" style="width: 200px;" onkeyup="searchUsers(event)">
|
||||
<button class="add-btn" onclick="searchUsersBtn()">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(4, 1fr);">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-value">${users.length}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🆕</div>
|
||||
<div class="stat-value">${users.filter(u => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return u.created_at && u.created_at.startsWith(today);
|
||||
}).length}</div>
|
||||
<div class="stat-label">今日新增</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📧</div>
|
||||
<div class="stat-value">${users.filter(u => u.email).length}</div>
|
||||
<div class="stat-label">已绑定邮箱</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📱</div>
|
||||
<div class="stat-value">${users.length}</div>
|
||||
<div class="stat-label">已绑定手机</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>头像</th>
|
||||
<th>用户名</th>
|
||||
<th>手机号</th>
|
||||
<th>邮箱</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${users.length === 0 ? '<tr><td colspan="8" style="text-align: center; color: #999;">暂无用户</td></tr>' :
|
||||
users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td style="font-size: 24px;">${u.avatar || '👤'}</td>
|
||||
<td>${u.username}</td>
|
||||
<td>${u.phone}</td>
|
||||
<td>${u.email || '-'}</td>
|
||||
<td>${formatDate(u.created_at)}</td>
|
||||
<td>${u.last_login_at ? formatDate(u.last_login_at) : '从未登录'}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="action-btn edit" onclick="showEditUserModal(${u.id})">编辑</button>
|
||||
<button class="action-btn" style="background: #f59e0b; color: white;" onclick="showResetPasswordModal(${u.id})">重置密码</button>
|
||||
<button class="action-btn delete" onclick="deleteUser(${u.id})">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
async function searchUsers(event) {
|
||||
if (event && event.key !== 'Enter') return;
|
||||
const search = document.getElementById('userSearch').value.trim();
|
||||
users = await fetchAPI(`/api/admin/users?search=${encodeURIComponent(search)}`);
|
||||
loadUsersPage(document.getElementById('mainContent'));
|
||||
}
|
||||
|
||||
async function searchUsersBtn() {
|
||||
const search = document.getElementById('userSearch').value.trim();
|
||||
users = await fetchAPI(`/api/admin/users?search=${encodeURIComponent(search)}`);
|
||||
loadUsersPage(document.getElementById('mainContent'));
|
||||
}
|
||||
|
||||
function showEditUserModal(id) {
|
||||
const user = users.find(u => u.id === id);
|
||||
if (!user) return;
|
||||
|
||||
const avatars = ['👤', '😊', '😎', '🤓', '🦸', '🧙', '🥷', '👨', '👩', '🧑', '👴', '👵', '👦', '👧', '🤖', '👽', '🧛', '🧜', '🧚', '🦊'];
|
||||
|
||||
showModal('编辑用户', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户名</label>
|
||||
<input type="text" class="form-input" id="editUserName" value="${user.username}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">手机号(不可修改)</label>
|
||||
<input type="text" class="form-input" id="editUserPhone" value="${user.phone}" readonly style="background: #f5f7fa;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">邮箱</label>
|
||||
<input type="email" class="form-input" id="editUserEmail" value="${user.email || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">头像</label>
|
||||
<select class="form-select" id="editUserAvatar">
|
||||
${avatars.map(a => `<option value="${a}" ${a === (user.avatar || '👤') ? 'selected' : ''}>${a}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">个性签名</label>
|
||||
<input type="text" class="form-input" id="editUserSignature" value="${user.signature || ''}" maxlength="50">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">性别</label>
|
||||
<select class="form-select" id="editUserGender">
|
||||
<option value="" ${!user.gender ? 'selected' : ''}>未设置</option>
|
||||
<option value="male" ${user.gender === 'male' ? 'selected' : ''}>男</option>
|
||||
<option value="female" ${user.gender === 'female' ? 'selected' : ''}>女</option>
|
||||
<option value="other" ${user.gender === 'other' ? 'selected' : ''}>其他</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">年龄</label>
|
||||
<input type="number" class="form-input" id="editUserAge" value="${user.age || ''}" min="1" max="150">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">地区</label>
|
||||
<input type="text" class="form-input" id="editUserRegion" value="${user.region || ''}">
|
||||
</div>
|
||||
<button class="form-submit" onclick="updateUser(${id})">保存</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function updateUser(id) {
|
||||
const data = {
|
||||
username: document.getElementById('editUserName').value,
|
||||
email: document.getElementById('editUserEmail').value,
|
||||
avatar: document.getElementById('editUserAvatar').value,
|
||||
signature: document.getElementById('editUserSignature').value,
|
||||
gender: document.getElementById('editUserGender').value,
|
||||
age: document.getElementById('editUserAge').value ? parseInt(document.getElementById('editUserAge').value) : null,
|
||||
region: document.getElementById('editUserRegion').value
|
||||
};
|
||||
|
||||
if (!data.username) {
|
||||
showToast('用户名不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchAPI(`/api/admin/users/${id}`, 'PUT', data);
|
||||
closeModal();
|
||||
showToast('更新成功');
|
||||
loadPage('users');
|
||||
}
|
||||
|
||||
function showResetPasswordModal(id) {
|
||||
const user = users.find(u => u.id === id);
|
||||
if (!user) return;
|
||||
|
||||
showModal('重置密码', `
|
||||
<div style="padding: 16px; background: #f5f7fa; border-radius: 8px; margin-bottom: 16px;">
|
||||
<p><strong>用户:</strong> ${user.username}</p>
|
||||
<p><strong>手机:</strong> ${user.phone}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">新密码</label>
|
||||
<input type="text" class="form-input" id="resetPassword" placeholder="输入新密码(至少6位)">
|
||||
</div>
|
||||
<p style="color: #999; font-size: 12px;">⚠️ 重置后用户需使用新密码登录</p>
|
||||
<button class="form-submit" onclick="resetPassword(${id})">确认重置</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function resetPassword(id) {
|
||||
const password = document.getElementById('resetPassword').value;
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
showToast('密码长度至少6位');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchAPI(`/api/admin/users/${id}/password`, 'PUT', { password });
|
||||
closeModal();
|
||||
showToast('密码已重置');
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
const user = users.find(u => u.id === id);
|
||||
if (!user) return;
|
||||
|
||||
if (!confirm(`确定删除用户 "${user.username}"?\n此操作不可恢复!`)) return;
|
||||
|
||||
await fetchAPI(`/api/admin/users/${id}`, 'DELETE');
|
||||
showToast('删除成功');
|
||||
loadPage('users');
|
||||
}
|
||||
|
||||
// ==================== 大模型配置页面 ====================
|
||||
|
||||
async function loadLLMPage(content) {
|
||||
@@ -512,39 +731,6 @@ function showAddAgentModal() {
|
||||
<button class="form-submit" onclick="saveAgent()">保存</button>
|
||||
`);
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">描述</label>
|
||||
<input type="text" class="form-input" id="agentDescription" placeholder="智能体描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">System Prompt</label>
|
||||
<textarea class="form-textarea" id="agentPrompt" placeholder="系统提示词"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">LLM配置</label>
|
||||
<select class="form-select" id="agentLLM">
|
||||
<option value="">使用默认</option>
|
||||
${llmConfigs.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">热度</label>
|
||||
<input type="number" class="form-input" id="agentHeat" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">标签</label>
|
||||
<input type="text" class="form-input" id="agentTags" placeholder="多个标签用逗号分隔">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" id="agentEnableSearch"> 启用联网搜索
|
||||
</label>
|
||||
</div>
|
||||
<button class="form-submit" onclick="saveAgent()">保存</button>
|
||||
`);
|
||||
}
|
||||
|
||||
function showEditAgentModal(agentId) {
|
||||
const agent = agents.find(a => a.agent_id === agentId);
|
||||
|
||||
184
www/app.js
184
www/app.js
@@ -2447,18 +2447,34 @@ function handleLogin() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查本地存储的用户
|
||||
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
|
||||
const user = users.find(u => u.username === username && u.password === password);
|
||||
|
||||
if (user) {
|
||||
currentUser = { username: user.username, registeredAt: user.registeredAt };
|
||||
saveCurrentUser();
|
||||
showToast('登录成功');
|
||||
showMainPage();
|
||||
} else {
|
||||
showToast('用户名或密码错误');
|
||||
}
|
||||
// 调用后台登录API
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentUser = {
|
||||
id: data.user.id,
|
||||
username: data.user.username,
|
||||
phone: data.user.phone,
|
||||
email: data.user.email || '',
|
||||
avatar: data.user.avatar || '👤',
|
||||
signature: data.user.signature || '',
|
||||
registeredAt: Date.now()
|
||||
};
|
||||
saveCurrentUser();
|
||||
showToast('登录成功');
|
||||
showMainPage();
|
||||
} else {
|
||||
showToast(data.error || '用户名或密码错误');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
showToast('网络错误,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 注册页面 ====================
|
||||
@@ -2477,12 +2493,21 @@ function showRegisterPage() {
|
||||
<div class="auth-form">
|
||||
<div class="auth-input-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="registerUsername" placeholder="请输入用户名(3-20字符)" autocomplete="username">
|
||||
<input type="text" id="registerUsername" placeholder="请输入用户名(2-20字符)" autocomplete="username">
|
||||
</div>
|
||||
<div class="auth-input-group">
|
||||
<label>手机号 <span class="required">*</span></label>
|
||||
<input type="tel" id="registerPhone" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
|
||||
</div>
|
||||
<div class="auth-input-group" style="display: flex; gap: 8px;">
|
||||
<div style="flex: 1;">
|
||||
<label>验证码 <span class="required">*</span></label>
|
||||
<input type="text" id="registerCode" placeholder="请输入验证码" maxlength="6">
|
||||
</div>
|
||||
<div style="flex-shrink: 0; align-self: flex-end;">
|
||||
<button class="send-code-btn" id="getCodeBtn">获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-input-group">
|
||||
<label>邮箱 <span class="optional">(可选)</span></label>
|
||||
<input type="email" id="registerEmail" placeholder="请输入邮箱(可选)" autocomplete="email">
|
||||
@@ -2527,6 +2552,12 @@ function showRegisterPage() {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取验证码按钮
|
||||
const getCodeBtn = document.getElementById('getCodeBtn');
|
||||
if (getCodeBtn) {
|
||||
getCodeBtn.addEventListener('click', handleGetCode);
|
||||
}
|
||||
|
||||
// 回车注册
|
||||
document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') handleRegister();
|
||||
@@ -2539,6 +2570,50 @@ function showRegisterPage() {
|
||||
e.target.value = e.target.value.replace(/\D/g, '');
|
||||
});
|
||||
}
|
||||
|
||||
// 验证码输入限制(只允许数字)
|
||||
const codeInput = document.getElementById('registerCode');
|
||||
if (codeInput) {
|
||||
codeInput.addEventListener('input', (e) => {
|
||||
e.target.value = e.target.value.replace(/\D/g, '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码(模拟)
|
||||
let verifyCode = '';
|
||||
let codeExpireTime = 0;
|
||||
|
||||
function handleGetCode() {
|
||||
const phone = document.getElementById('registerPhone')?.value.trim();
|
||||
|
||||
if (!validatePhone(phone)) {
|
||||
showToast('请先输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成6位随机验证码
|
||||
verifyCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
codeExpireTime = Date.now() + 5 * 60 * 1000; // 5分钟有效
|
||||
|
||||
// 模拟发送(实际项目中应调用短信API)
|
||||
showToast(`验证码已发送: ${verifyCode}(演示)`);
|
||||
|
||||
// 60秒倒计时
|
||||
const getCodeBtn = document.getElementById('getCodeBtn');
|
||||
if (getCodeBtn) {
|
||||
getCodeBtn.disabled = true;
|
||||
let countdown = 60;
|
||||
const timer = setInterval(() => {
|
||||
getCodeBtn.textContent = `${countdown}s`;
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(timer);
|
||||
getCodeBtn.disabled = false;
|
||||
getCodeBtn.textContent = '获取验证码';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
@@ -2559,12 +2634,13 @@ function handleRegister() {
|
||||
const username = document.getElementById('registerUsername')?.value.trim();
|
||||
const phone = document.getElementById('registerPhone')?.value.trim();
|
||||
const email = document.getElementById('registerEmail')?.value.trim();
|
||||
const code = document.getElementById('registerCode')?.value.trim();
|
||||
const password = document.getElementById('registerPassword')?.value;
|
||||
const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value;
|
||||
|
||||
// 验证用户名
|
||||
if (!username || username.length < 3 || username.length > 20) {
|
||||
showToast('用户名需要3-20个字符');
|
||||
if (!username || username.length < 2 || username.length > 20) {
|
||||
showToast('用户名需要2-20个字符');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2574,8 +2650,20 @@ function handleRegister() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if (!code || code.length !== 6) {
|
||||
showToast('请输入6位验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查验证码是否正确(模拟)
|
||||
if (code !== verifyCode || Date.now() > codeExpireTime) {
|
||||
showToast('验证码不正确或已过期');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证邮箱(可选)
|
||||
if (!validateEmail(email)) {
|
||||
if (email && !validateEmail(email)) {
|
||||
showToast('请输入正确的邮箱格式');
|
||||
return;
|
||||
}
|
||||
@@ -2591,37 +2679,39 @@ function handleRegister() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
|
||||
if (users.find(u => u.username === username)) {
|
||||
showToast('用户名已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查手机号是否已注册
|
||||
if (users.find(u => u.phone === phone)) {
|
||||
showToast('该手机号已注册');
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册新用户
|
||||
const newUser = {
|
||||
username,
|
||||
phone,
|
||||
email: email || '',
|
||||
password,
|
||||
registeredAt: Date.now()
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
localStorage.setItem('registeredUsers', JSON.stringify(users));
|
||||
|
||||
// 自动登录
|
||||
currentUser = { username: newUser.username, phone: newUser.phone, registeredAt: newUser.registeredAt };
|
||||
saveCurrentUser();
|
||||
|
||||
showToast('注册成功');
|
||||
showMainPage();
|
||||
// 调用后台注册API
|
||||
fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
phone,
|
||||
email: email || '',
|
||||
password,
|
||||
code
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 注册成功,自动登录
|
||||
currentUser = {
|
||||
id: data.user_id,
|
||||
username: username,
|
||||
phone: phone,
|
||||
email: email || '',
|
||||
registeredAt: Date.now()
|
||||
};
|
||||
saveCurrentUser();
|
||||
showToast('注册成功');
|
||||
showMainPage();
|
||||
} else {
|
||||
showToast(data.error || '注册失败');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
showToast('网络错误,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 限制提示 ====================
|
||||
|
||||
Reference in New Issue
Block a user