- 新增后端FastAPI应用,包含用户管理、文档管理和操作日志功能 - 实现JWT认证机制,支持用户注册、登录、登出操作 - 添加数据库模型定义,包括用户表、文档表和操作日志表 - 实现文档的增删改查功能及权限控制 - 添加管理员功能,支持用户管理和全局操作日志查看 - 新增前端界面,实现完整的用户交互体验 - 配置环境变量示例和Git忽略规则 - 编写详细的README文档,包含安装和使用说明
617 lines
19 KiB
JavaScript
617 lines
19 KiB
JavaScript
// API基础URL
|
||
const API_BASE_URL = 'http://localhost:8001';
|
||
|
||
// 全局状态
|
||
let currentUser = null;
|
||
let accessToken = localStorage.getItem('accessToken');
|
||
let lastApiCalls = {}; // 记录上次API调用时间,防止重复请求
|
||
|
||
// DOM元素
|
||
const navMenu = document.getElementById('navMenu');
|
||
const userInfo = document.getElementById('userInfo');
|
||
const loginBtn = document.getElementById('loginBtn');
|
||
const registerBtn = document.getElementById('registerBtn');
|
||
const logoutBtn = document.getElementById('logoutBtn');
|
||
|
||
const loginSection = document.getElementById('loginSection');
|
||
const registerSection = document.getElementById('registerSection');
|
||
const mainSection = document.getElementById('mainSection');
|
||
|
||
const loginForm = document.getElementById('loginForm');
|
||
const registerForm = document.getElementById('registerForm');
|
||
const editProfileForm = document.getElementById('editProfileForm');
|
||
const createDocForm = document.getElementById('createDocForm');
|
||
const editDocForm = document.getElementById('editDocForm');
|
||
|
||
const currentUserName = document.getElementById('currentUserName');
|
||
const editProfileBtn = document.getElementById('editProfileBtn');
|
||
const viewLogsBtn = document.getElementById('viewLogsBtn');
|
||
const createDocBtn = document.getElementById('createDocBtn');
|
||
|
||
const documentsList = document.getElementById('documentsList');
|
||
const logsList = document.getElementById('logsList');
|
||
|
||
const message = document.getElementById('message');
|
||
|
||
// 模态框相关元素
|
||
const editProfileModal = document.getElementById('editProfileModal');
|
||
const createDocModal = document.getElementById('createDocModal');
|
||
const editDocModal = document.getElementById('editDocModal');
|
||
const logsModal = document.getElementById('logsModal');
|
||
|
||
// 用户管理相关元素
|
||
const userManagementSection = document.getElementById('userManagementSection');
|
||
const usersList = document.getElementById('usersList');
|
||
const refreshUsersBtn = document.getElementById('refreshUsersBtn');
|
||
|
||
// API请求函数
|
||
async function apiRequest(url, options = {}) {
|
||
const config = {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
},
|
||
...options
|
||
};
|
||
|
||
if (accessToken) {
|
||
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}${url}`, config);
|
||
|
||
if (response.status === 401) {
|
||
// Token过期,清除本地存储并重新登录
|
||
accessToken = null;
|
||
localStorage.removeItem('accessToken');
|
||
currentUser = null;
|
||
showLoginSection();
|
||
showMessage('登录已过期,请重新登录', 'error');
|
||
return null;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('API请求错误:', error);
|
||
showMessage(`请求失败: ${error.message}`, 'error');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 显示消息提示
|
||
function showMessage(text, type = 'info') {
|
||
message.textContent = text;
|
||
message.className = `message ${type}`;
|
||
message.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
message.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// 用户认证相关函数
|
||
async function login(username, password) {
|
||
const data = await apiRequest('/auth/login', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
|
||
if (data && data.access_token) {
|
||
accessToken = data.access_token;
|
||
localStorage.setItem('accessToken', accessToken);
|
||
await loadCurrentUser();
|
||
showMainSection();
|
||
showMessage('登录成功', 'success');
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function register(userData) {
|
||
// 将isAdmin添加到userData中
|
||
const registrationData = {
|
||
...userData,
|
||
is_admin: document.getElementById('registerIsAdmin') ? document.getElementById('registerIsAdmin').checked : false
|
||
};
|
||
|
||
const data = await apiRequest('/auth/register', {
|
||
method: 'POST',
|
||
body: JSON.stringify(registrationData)
|
||
});
|
||
|
||
if (data) {
|
||
showMessage('注册成功,请登录', 'success');
|
||
// 重置勾选框
|
||
if(document.getElementById('registerIsAdmin')) {
|
||
document.getElementById('registerIsAdmin').checked = false;
|
||
}
|
||
showLoginSection();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function logout() {
|
||
await apiRequest('/auth/logout', { method: 'POST' });
|
||
accessToken = null;
|
||
localStorage.removeItem('accessToken');
|
||
currentUser = null;
|
||
showLoginSection();
|
||
showMessage('已退出登录', 'info');
|
||
}
|
||
|
||
async function loadCurrentUser() {
|
||
const data = await apiRequest('/users/me');
|
||
if (data) {
|
||
currentUser = data;
|
||
userInfo.textContent = `欢迎, ${data.username}`;
|
||
currentUserName.textContent = data.username;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function updateUserProfile(updateData) {
|
||
const data = await apiRequest('/users/me', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(updateData)
|
||
});
|
||
|
||
if (data) {
|
||
await loadCurrentUser();
|
||
showMessage('个人信息更新成功', 'success');
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 文档管理相关函数
|
||
async function loadDocuments() {
|
||
const data = await apiRequest('/documents');
|
||
if (data) {
|
||
displayDocuments(data);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function createDocument(docData) {
|
||
const data = await apiRequest('/documents', {
|
||
method: 'POST',
|
||
body: JSON.stringify(docData)
|
||
});
|
||
|
||
if (data) {
|
||
await loadDocuments();
|
||
showMessage('文档创建成功', 'success');
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function updateDocument(docId, docData) {
|
||
const data = await apiRequest(`/documents/${docId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(docData)
|
||
});
|
||
|
||
if (data) {
|
||
await loadDocuments();
|
||
showMessage('文档更新成功', 'success');
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function deleteDocument(docId) {
|
||
const data = await apiRequest(`/documents/${docId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (data) {
|
||
await loadDocuments();
|
||
showMessage('文档删除成功', 'success');
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 操作日志相关函数
|
||
async function loadLogs() {
|
||
const data = await apiRequest('/logs/my');
|
||
if (data) {
|
||
displayLogs(data);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 用户管理相关函数
|
||
async function loadUsers() {
|
||
const data = await apiRequest('/users');
|
||
if (data) {
|
||
displayUsers(data);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function deleteUser(userId) {
|
||
if (confirm('确定要删除这个用户吗?此操作不可恢复。')) {
|
||
const data = await apiRequest(`/users/${userId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (data) {
|
||
await loadUsers();
|
||
showMessage('用户删除成功', 'success');
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 界面显示控制函数
|
||
function showLoginSection() {
|
||
loginSection.style.display = 'block';
|
||
registerSection.style.display = 'none';
|
||
mainSection.style.display = 'none';
|
||
loginBtn.style.display = 'inline-block';
|
||
registerBtn.style.display = 'inline-block';
|
||
logoutBtn.style.display = 'none';
|
||
userInfo.style.display = 'none';
|
||
}
|
||
|
||
function showRegisterSection() {
|
||
loginSection.style.display = 'none';
|
||
registerSection.style.display = 'block';
|
||
mainSection.style.display = 'none';
|
||
}
|
||
|
||
function showMainSection() {
|
||
loginSection.style.display = 'none';
|
||
registerSection.style.display = 'none';
|
||
mainSection.style.display = 'block';
|
||
loginBtn.style.display = 'none';
|
||
registerBtn.style.display = 'none';
|
||
logoutBtn.style.display = 'inline-block';
|
||
userInfo.style.display = 'inline-block';
|
||
|
||
// 检查是否为管理员,如果是则显示用户管理界面
|
||
if (currentUser && currentUser.is_admin) {
|
||
userManagementSection.style.display = 'block';
|
||
loadUsers();
|
||
} else {
|
||
userManagementSection.style.display = 'none';
|
||
}
|
||
|
||
loadDocuments();
|
||
}
|
||
|
||
// 显示/隐藏模态框函数
|
||
function showModal(modal) {
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function hideModal(modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
|
||
// 文档显示函数
|
||
function displayDocuments(documents) {
|
||
if (documents.length === 0) {
|
||
documentsList.innerHTML = `
|
||
<div class="empty-state">
|
||
<h3>暂无文档</h3>
|
||
<p>点击"创建新文档"按钮开始创建您的第一个文档</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
documentsList.innerHTML = documents.map(doc => `
|
||
<div class="document-item" onclick="openEditDocumentModal(${doc.id})">
|
||
<div class="document-header">
|
||
<div>
|
||
<div class="document-title">${escapeHtml(doc.title)}</div>
|
||
<div class="document-meta">
|
||
<span>类型: ${doc.file_type}</span>
|
||
<span>大小: ${formatFileSize(doc.file_size)}</span>
|
||
<span>${doc.is_public ? '公开' : '私有'}</span>
|
||
</div>
|
||
</div>
|
||
<div class="document-actions">
|
||
<button class="btn-secondary" onclick="event.stopPropagation(); openEditDocumentModal(${doc.id})">编辑</button>
|
||
</div>
|
||
</div>
|
||
${doc.description ? `<div class="document-description">${escapeHtml(doc.description)}</div>` : ''}
|
||
<div class="document-content-preview">${escapeHtml(doc.content.substring(0, 100))}${doc.content.length > 100 ? '...' : ''}</div>
|
||
<div class="document-meta">创建时间: ${formatDate(doc.created_at)}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// 用户显示函数
|
||
function displayUsers(users) {
|
||
if (users.length === 0) {
|
||
usersList.innerHTML = '<div class="empty-state"><h3>暂无用户</h3></div>';
|
||
return;
|
||
}
|
||
|
||
usersList.innerHTML = users.map(user => `
|
||
<div class="user-item">
|
||
<div class="user-header">
|
||
<div class="user-info">
|
||
<div class="user-username">${escapeHtml(user.username)}</div>
|
||
<div class="user-email">${escapeHtml(user.email)}</div>
|
||
${user.full_name ? `<div class="user-fullname">${escapeHtml(user.full_name)}</div>` : ''}
|
||
<div class="user-meta">
|
||
<span>ID: ${user.id}</span>
|
||
<span>注册时间: ${formatDate(user.created_at)}</span>
|
||
${user.is_admin ? '<span class="user-admin-badge">管理员</span>' : ''}
|
||
</div>
|
||
</div>
|
||
<div class="user-actions">
|
||
${user.id !== currentUser.id ? `<button class="btn-danger" onclick="deleteUser(${user.id})">删除</button>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// 日志显示函数
|
||
function displayLogs(logs) {
|
||
if (logs.length === 0) {
|
||
logsList.innerHTML = '<div class="empty-state"><h3>暂无操作日志</h3></div>';
|
||
return;
|
||
}
|
||
|
||
logsList.innerHTML = logs.map(log => `
|
||
<div class="log-item">
|
||
<div class="log-header">
|
||
<span class="log-action">${escapeHtml(log.operation)}</span>
|
||
<span class="log-time">${formatDate(log.created_at)}</span>
|
||
</div>
|
||
<div class="log-details">
|
||
详情: ${escapeHtml(log.details || '无')}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// 工具函数
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function formatDate(dateString) {
|
||
return new Date(dateString).toLocaleString('zh-CN');
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
// 模态框操作函数
|
||
function openEditProfileModal() {
|
||
if (!currentUser) return;
|
||
|
||
document.getElementById('editEmail').value = currentUser.email || '';
|
||
document.getElementById('editFullName').value = currentUser.full_name || '';
|
||
document.getElementById('editPassword').value = '';
|
||
|
||
showModal(editProfileModal);
|
||
}
|
||
|
||
function openCreateDocumentModal() {
|
||
document.getElementById('docTitle').value = '';
|
||
document.getElementById('docDescription').value = '';
|
||
document.getElementById('docContent').value = '';
|
||
document.getElementById('docType').value = 'txt';
|
||
document.getElementById('docPublic').checked = false;
|
||
|
||
showModal(createDocModal);
|
||
}
|
||
|
||
async function openEditDocumentModal(docId) {
|
||
const data = await apiRequest(`/documents/${docId}`);
|
||
if (!data) return;
|
||
|
||
document.getElementById('editDocId').value = docId;
|
||
document.getElementById('editDocTitle').value = data.title;
|
||
document.getElementById('editDocDescription').value = data.description || '';
|
||
document.getElementById('editDocContent').value = data.content;
|
||
document.getElementById('editDocType').value = data.file_type;
|
||
document.getElementById('editDocPublic').checked = data.is_public;
|
||
|
||
showModal(editDocModal);
|
||
}
|
||
|
||
async function openLogsModal() {
|
||
await loadLogs();
|
||
showModal(logsModal);
|
||
}
|
||
|
||
// 导航按钮事件
|
||
loginBtn.addEventListener('click', showLoginSection);
|
||
registerBtn.addEventListener('click', showRegisterSection);
|
||
logoutBtn.addEventListener('click', logout);
|
||
|
||
// 表单切换链接
|
||
document.getElementById('showRegister').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showRegisterSection();
|
||
});
|
||
|
||
document.getElementById('showLogin').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showLoginSection();
|
||
});
|
||
|
||
// 删除文档按钮
|
||
document.getElementById('deleteDocBtn').addEventListener('click', async (e) => {
|
||
e.preventDefault();
|
||
const docId = document.getElementById('editDocId').value;
|
||
|
||
if (confirm('确定要删除这个文档吗?此操作不可恢复。')) {
|
||
const success = await deleteDocument(docId);
|
||
if (success) {
|
||
hideModal(editDocModal);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 主界面按钮事件
|
||
editProfileBtn.addEventListener('click', openEditProfileModal);
|
||
viewLogsBtn.addEventListener('click', openLogsModal);
|
||
createDocBtn.addEventListener('click', openCreateDocumentModal);
|
||
|
||
// 用户管理按钮事件
|
||
refreshUsersBtn.addEventListener('click', loadUsers);
|
||
|
||
// 模态框关闭事件
|
||
document.getElementById('closeEditModal').addEventListener('click', () => hideModal(editProfileModal));
|
||
document.getElementById('closeCreateDocModal').addEventListener('click', () => hideModal(createDocModal));
|
||
document.getElementById('closeEditDocModal').addEventListener('click', () => hideModal(editDocModal));
|
||
document.getElementById('closeLogsModal').addEventListener('click', () => hideModal(logsModal));
|
||
|
||
// 点击模态框外部关闭
|
||
[editProfileModal, createDocModal, editDocModal, logsModal].forEach(modal => {
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
hideModal(modal);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 键盘事件:ESC键关闭模态框
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
[editProfileModal, createDocModal, editDocModal, logsModal].forEach(modal => {
|
||
if (modal.style.display === 'flex') {
|
||
hideModal(modal);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// 表单提交事件
|
||
loginForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const username = document.getElementById('loginUsername').value;
|
||
const password = document.getElementById('loginPassword').value;
|
||
|
||
await login(username, password);
|
||
});
|
||
|
||
registerForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const userData = {
|
||
username: document.getElementById('registerUsername').value,
|
||
email: document.getElementById('registerEmail').value,
|
||
full_name: document.getElementById('registerFullName').value,
|
||
password: document.getElementById('registerPassword').value,
|
||
is_admin: document.getElementById('registerIsAdmin').checked
|
||
};
|
||
|
||
await register(userData);
|
||
});
|
||
|
||
editProfileForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const updateData = {
|
||
email: document.getElementById('editEmail').value,
|
||
full_name: document.getElementById('editFullName').value
|
||
};
|
||
|
||
const newPassword = document.getElementById('editPassword').value;
|
||
if (newPassword) {
|
||
updateData.password = newPassword;
|
||
}
|
||
|
||
const success = await updateUserProfile(updateData);
|
||
if (success) {
|
||
hideModal(editProfileModal);
|
||
}
|
||
});
|
||
|
||
createDocForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const docData = {
|
||
title: document.getElementById('docTitle').value,
|
||
description: document.getElementById('docDescription').value,
|
||
content: document.getElementById('docContent').value,
|
||
file_type: document.getElementById('docType').value,
|
||
is_public: document.getElementById('docPublic').checked
|
||
};
|
||
|
||
const success = await createDocument(docData);
|
||
if (success) {
|
||
hideModal(createDocModal);
|
||
}
|
||
});
|
||
|
||
editDocForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const docId = document.getElementById('editDocId').value;
|
||
const docData = {
|
||
title: document.getElementById('editDocTitle').value,
|
||
description: document.getElementById('editDocDescription').value,
|
||
content: document.getElementById('editDocContent').value,
|
||
file_type: document.getElementById('editDocType').value,
|
||
is_public: document.getElementById('editDocPublic').checked
|
||
};
|
||
|
||
const success = await updateDocument(docId, docData);
|
||
if (success) {
|
||
hideModal(editDocModal);
|
||
}
|
||
});
|
||
|
||
// 事件监听器
|
||
let isInitialized = false; // 防止重复初始化
|
||
document.addEventListener('DOMContentLoaded', async function() {
|
||
// 防止重复初始化
|
||
if (isInitialized) {
|
||
console.log('页面已初始化,跳过重复初始化');
|
||
return;
|
||
}
|
||
isInitialized = true;
|
||
|
||
// 检查是否已登录
|
||
if (accessToken) {
|
||
try {
|
||
const success = await loadCurrentUser();
|
||
if (success) {
|
||
showMainSection();
|
||
} else {
|
||
// 如果loadCurrentUser返回false,说明token无效,清除token并显示登录界面
|
||
accessToken = null;
|
||
localStorage.removeItem('accessToken');
|
||
showLoginSection();
|
||
}
|
||
} catch (error) {
|
||
// 捕获可能的错误,清除token并显示登录界面
|
||
console.error('初始化错误:', error);
|
||
accessToken = null;
|
||
localStorage.removeItem('accessToken');
|
||
showLoginSection();
|
||
}
|
||
} else {
|
||
showLoginSection();
|
||
}
|
||
}); |