feat: 添加文档管理系统前后端基础功能
- 新增后端FastAPI应用,包含用户管理、文档管理和操作日志功能 - 实现JWT认证机制,支持用户注册、登录、登出操作 - 添加数据库模型定义,包括用户表、文档表和操作日志表 - 实现文档的增删改查功能及权限控制 - 添加管理员功能,支持用户管理和全局操作日志查看 - 新增前端界面,实现完整的用户交互体验 - 配置环境变量示例和Git忽略规则 - 编写详细的README文档,包含安装和使用说明
This commit is contained in:
617
app.js
Normal file
617
app.js
Normal file
@@ -0,0 +1,617 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user