feat: 添加文档管理系统前后端基础功能

- 新增后端FastAPI应用,包含用户管理、文档管理和操作日志功能
- 实现JWT认证机制,支持用户注册、登录、登出操作
- 添加数据库模型定义,包括用户表、文档表和操作日志表
- 实现文档的增删改查功能及权限控制
- 添加管理员功能,支持用户管理和全局操作日志查看
- 新增前端界面,实现完整的用户交互体验
- 配置环境变量示例和Git忽略规则
- 编写详细的README文档,包含安装和使用说明
This commit is contained in:
starsac
2026-01-11 15:56:55 +08:00
commit bb9e208381
12 changed files with 2411 additions and 0 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
# JWT密钥生产环境请使用强密钥
SECRET_KEY=your-super-secret-jwt-key-change-this-in-production
# 数据库配置
DATABASE_URL=sqlite:///./document_management.db
# 应用配置
DEBUG=true
HOST=0.0.0.0
PORT=8000

84
.gitignore vendored Normal file
View File

@@ -0,0 +1,84 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.gitattributes
.pytest_cache/
.pyup.yml
isort.cfg
.docs/
mypy.ini
.pyre/
.pyre_configuration
# Virtual Environment
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Database
*.db
*.db-journal
# Environment variables
.env
.env.local
.env.dev
.env.test
.env.prod
.env.staging
# Logs
*.log
logs/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

310
README.md Normal file
View File

@@ -0,0 +1,310 @@
# 文档在线管理系统
一个基于FastAPI的文档在线管理系统后端API提供用户管理、文档管理和操作日志记录功能。
## 功能特性
### 用户管理模块
- 用户注册/删除
- 用户登录/登出
- 用户信息维护
- 用户操作记录
- 会话管理机制
### 文档管理模块
- 文档创建、查看、更新、删除
- 文档权限控制(私有/公开)
- 文档类型和大小管理
### 操作日志模块
- 完整的用户操作记录
- 管理员可查看所有操作日志
- 用户可查看自己的操作日志
## 技术栈
- **后端框架**: FastAPI
- **数据库**: SQLite (支持其他数据库)
- **认证**: JWT Token
- **ORM**: SQLAlchemy
## 安装和运行
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 配置环境变量
复制环境变量示例文件:
```bash
cp .env.example .env
```
编辑 `.env` 文件,设置你的配置:
```env
SECRET_KEY=your-super-secret-jwt-key
DATABASE_URL=sqlite:///./document_management.db
DEBUG=true
HOST=0.0.0.0
PORT=8000
```
### 3. 运行应用
```bash
# 方式1: 使用uvicorn直接运行
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 方式2: 使用python运行
python main.py
```
### 4. 访问API文档
应用启动后可以访问以下地址查看API文档
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## API接口说明
### 认证接口
#### 用户注册
```http
POST /auth/register
Content-Type: application/json
{
"username": "testuser",
"email": "test@example.com",
"password": "password123",
"full_name": "测试用户"
}
```
#### 用户登录
```http
POST /auth/login
Content-Type: application/json
{
"username": "testuser",
"password": "password123"
}
```
#### 用户登出
```http
POST /auth/logout
Authorization: Bearer <token>
```
### 用户管理接口
#### 获取当前用户信息
```http
GET /users/me
Authorization: Bearer <token>
```
#### 更新用户信息
```http
PUT /users/me
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "newemail@example.com",
"full_name": "新姓名"
}
```
#### 获取所有用户(仅管理员)
```http
GET /users?skip=0&limit=100
Authorization: Bearer <token>
```
#### 删除用户(仅管理员)
```http
DELETE /users/{user_id}
Authorization: Bearer <token>
```
### 文档管理接口
#### 创建文档
```http
POST /documents
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "我的文档",
"content": "文档内容",
"description": "文档描述",
"file_type": "txt",
"is_public": false
}
```
#### 获取文档列表
```http
GET /documents?skip=0&limit=100
Authorization: Bearer <token>
```
#### 获取文档详情
```http
GET /documents/{document_id}
Authorization: Bearer <token>
```
#### 更新文档
```http
PUT /documents/{document_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "更新后的标题",
"content": "更新后的内容"
}
```
#### 删除文档
```http
DELETE /documents/{document_id}
Authorization: Bearer <token>
```
### 操作日志接口
#### 获取我的操作日志
```http
GET /logs/my?skip=0&limit=100
Authorization: Bearer <token>
```
#### 获取所有操作日志(仅管理员)
```http
GET /logs?skip=0&limit=100
Authorization: Bearer <token>
```
## 数据库结构
### Users表用户表
- id: 主键
- username: 用户名(唯一)
- email: 邮箱(唯一)
- hashed_password: 加密密码
- full_name: 全名
- is_admin: 是否管理员
- is_active: 是否激活
- created_at: 创建时间
- updated_at: 更新时间
### Documents表文档表
- id: 主键
- title: 文档标题
- content: 文档内容
- description: 文档描述
- file_type: 文件类型
- file_size: 文件大小
- is_public: 是否公开
- owner_id: 所有者ID外键
- created_at: 创建时间
- updated_at: 更新时间
### UserOperationLogs表用户操作日志表
- id: 主键
- user_id: 用户ID外键
- operation: 操作类型
- details: 操作详情
- ip_address: IP地址
- user_agent: 用户代理
- created_at: 创建时间
## 安全特性
1. **JWT认证**: 基于Token的认证机制
2. **权限控制**: 用户只能访问自己的数据
3. **输入验证**: 使用Pydantic进行数据验证
4. **CORS支持**: 支持跨域请求
### ⚠️ 重要安全说明
**简化认证模式**: 当前系统使用简化认证模式,密码以明文形式存储。
- **仅适用于开发环境**: 此模式仅用于开发和测试目的
- **生产环境警告**: 在生产环境中强烈建议使用bcrypt等安全加密算法
- **安全升级**: 如需升级到生产环境,请参考扩展功能建议中的安全升级部分
## 部署说明
### 本地部署
```bash
# 1. 克隆项目
git clone <repository-url>
cd web_security
# 2. 创建虚拟环境
python -m venv .venv
# 3. 激活虚拟环境
# Windows:
.venv\\Scripts\\activate
# Linux/Mac:
source .venv/bin/activate
# 4. 安装依赖
pip install -r requirements.txt
# 5. 运行应用
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
### 生产环境部署
1. 修改 `.env` 文件中的配置:
- 设置强密钥
- 关闭DEBUG模式
- 使用生产数据库
2. 使用生产级服务器如uvicorn with gunicorn
3. 配置反向代理如Nginx
## 开发说明
### 项目结构
```
web_security/
├── main.py # 主应用文件
├── database.py # 数据库配置
├── models.py # 数据模型
├── schemas.py # Pydantic模式
├── auth.py # 认证模块
├── requirements.txt # 依赖列表
├── .env.example # 环境变量示例
└── README.md # 项目说明
```
### 扩展功能建议
1. **文件上传**: 支持文档文件上传
2. **文档分享**: 文档分享和协作功能
3. **版本控制**: 文档版本管理
4. **搜索功能**: 文档内容搜索
5. **权限分级**: 更细粒度的权限控制
6. **安全升级**: 升级到生产级安全认证
- 安装bcrypt: `pip install bcrypt passlib[bcrypt]`
- 更新auth.py中的密码处理函数
- 重新创建数据库以加密现有密码
## 许可证
MIT License

617
app.js Normal file
View 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();
}
});

38
auth.py Normal file
View File

@@ -0,0 +1,38 @@
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Optional
import os
# JWT配置
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain_password: str, stored_password: str) -> bool:
"""验证密码 - 简化版本,直接比较字符串"""
return plain_password == stored_password
def get_password_hash(password: str) -> str:
"""生成密码哈希 - 简化版本,直接返回原密码"""
return password
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""创建访问令牌"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[dict]:
"""验证令牌"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None

18
database.py Normal file
View File

@@ -0,0 +1,18 @@
from sqlalchemy.engine.create import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.session import sessionmaker
# SQLite数据库URL
SQLALCHEMY_DATABASE_URL = "sqlite:///./document_management.db"
# 创建数据库引擎
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
# 创建SessionLocal类
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 创建Base类
Base = declarative_base()

224
index.html Normal file
View File

@@ -0,0 +1,224 @@
<!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="style.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-container">
<h1 class="nav-logo">📄 文档管理系统</h1>
<div class="nav-menu" id="navMenu">
<span class="user-info" id="userInfo" style="display: none;"></span>
<button class="nav-btn" id="loginBtn">登录</button>
<button class="nav-btn" id="registerBtn">注册</button>
<button class="nav-btn" id="logoutBtn" style="display: none;">登出</button>
</div>
</div>
</nav>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 登录表单 -->
<div id="loginSection" class="form-section">
<h2>用户登录</h2>
<form id="loginForm" class="auth-form">
<div class="form-group">
<label for="loginUsername">用户名:</label>
<input type="text" id="loginUsername" required>
</div>
<div class="form-group">
<label for="loginPassword">密码:</label>
<input type="password" id="loginPassword" required>
</div>
<button type="submit" class="btn-primary">登录</button>
<p>还没有账户?<a href="#" id="showRegister">立即注册</a></p>
</form>
</div>
<!-- 注册表单 -->
<div id="registerSection" class="form-section" style="display: none;">
<h2>用户注册</h2>
<form id="registerForm" class="auth-form">
<div class="form-group">
<label for="registerUsername">用户名:</label>
<input type="text" id="registerUsername" required>
</div>
<div class="form-group">
<label for="registerEmail">邮箱:</label>
<input type="email" id="registerEmail" required>
</div>
<div class="form-group">
<label for="registerFullName">姓名:</label>
<input type="text" id="registerFullName">
</div>
<div class="form-group">
<label for="registerPassword">密码:</label>
<input type="password" id="registerPassword" required>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="registerIsAdmin"> 注册为管理员
</label>
</div>
<button type="submit" class="btn-primary">注册</button>
<p>已有账户?<a href="#" id="showLogin">立即登录</a></p>
</form>
</div>
<!-- 主界面(登录后显示) -->
<div id="mainSection" style="display: none;">
<!-- 用户信息面板 -->
<div class="user-panel">
<h2>欢迎, <span id="currentUserName"></span>!</h2>
<div class="user-actions">
<button class="btn-secondary" id="editProfileBtn">编辑个人信息</button>
<button class="btn-secondary" id="viewLogsBtn">查看操作日志</button>
</div>
</div>
<!-- 用户管理(仅管理员可见) -->
<div id="userManagementSection" class="user-management-section" style="display: none;">
<div class="section-header">
<h2>用户管理</h2>
<button class="btn-primary" id="refreshUsersBtn">刷新用户列表</button>
</div>
<!-- 用户列表 -->
<div id="usersList" class="users-list">
<!-- 用户列表将通过JavaScript动态生成 -->
</div>
</div>
<!-- 文档管理 -->
<div class="documents-section">
<div class="section-header">
<h2>文档管理</h2>
<button class="btn-primary" id="createDocBtn">创建新文档</button>
</div>
<!-- 文档列表 -->
<div id="documentsList" class="documents-list">
<!-- 文档列表将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 编辑个人信息模态框 -->
<div id="editProfileModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeEditModal">&times;</span>
<h3>编辑个人信息</h3>
<form id="editProfileForm">
<div class="form-group">
<label for="editEmail">邮箱:</label>
<input type="email" id="editEmail" required>
</div>
<div class="form-group">
<label for="editFullName">姓名:</label>
<input type="text" id="editFullName">
</div>
<div class="form-group">
<label for="editPassword">新密码(可选):</label>
<input type="password" id="editPassword" placeholder="留空则不修改密码">
</div>
<button type="submit" class="btn-primary">保存修改</button>
</form>
</div>
</div>
<!-- 创建文档模态框 -->
<div id="createDocModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeCreateDocModal">&times;</span>
<h3>创建新文档</h3>
<form id="createDocForm">
<div class="form-group">
<label for="docTitle">标题:</label>
<input type="text" id="docTitle" required>
</div>
<div class="form-group">
<label for="docDescription">描述:</label>
<textarea id="docDescription" rows="3"></textarea>
</div>
<div class="form-group">
<label for="docContent">内容:</label>
<textarea id="docContent" rows="6" required></textarea>
</div>
<div class="form-group">
<label for="docType">文件类型:</label>
<select id="docType">
<option value="txt">文本文件</option>
<option value="md">Markdown</option>
<option value="html">HTML</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="docPublic"> 公开文档
</label>
</div>
<button type="submit" class="btn-primary">创建文档</button>
</form>
</div>
</div>
<!-- 编辑文档模态框 -->
<div id="editDocModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeEditDocModal">&times;</span>
<h3>编辑文档</h3>
<form id="editDocForm">
<input type="hidden" id="editDocId">
<div class="form-group">
<label for="editDocTitle">标题:</label>
<input type="text" id="editDocTitle" required>
</div>
<div class="form-group">
<label for="editDocDescription">描述:</label>
<textarea id="editDocDescription" rows="3"></textarea>
</div>
<div class="form-group">
<label for="editDocContent">内容:</label>
<textarea id="editDocContent" rows="6" required></textarea>
</div>
<div class="form-group">
<label for="editDocType">文件类型:</label>
<select id="editDocType">
<option value="txt">文本文件</option>
<option value="md">Markdown</option>
<option value="html">HTML</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="editDocPublic"> 公开文档
</label>
</div>
<button type="submit" class="btn-primary">保存修改</button>
<button type="button" class="btn-danger" id="deleteDocBtn">删除文档</button>
</form>
</div>
</div>
<!-- 操作日志模态框 -->
<div id="logsModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeLogsModal">&times;</span>
<h3>操作日志</h3>
<div id="logsList" class="logs-list">
<!-- 日志列表将通过JavaScript动态生成 -->
</div>
</div>
</div>
</main>
<!-- 消息提示 -->
<div id="message" class="message" style="display: none;"></div>
<script src="app.js"></script>
</body>
</html>

399
main.py Normal file
View File

@@ -0,0 +1,399 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm.session import Session
from typing import List, Optional
import uvicorn
import os
from database import engine, SessionLocal, Base
from models import User, Document, UserOperationLog
from schemas import (
UserCreate, UserLogin, UserResponse, UserUpdate,
DocumentCreate, DocumentResponse, DocumentUpdate,
UserOperationLogResponse
)
from auth import create_access_token, verify_token, get_password_hash, verify_password
# 创建数据库表
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="文档在线管理系统",
description="一个基于FastAPI的文档在线管理系统",
version="1.0.0"
)
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 依赖注入 - 获取数据库会话
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 依赖注入 - 获取当前用户
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
db: Session = Depends(get_db)
):
token = credentials.credentials
payload = verify_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的token"
)
user_id = payload.get("sub")
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在"
)
return user
def create_operation_log(db: Session, user_id: int, operation: str, details: str = ""):
"""创建用户操作记录"""
log = UserOperationLog(
user_id=user_id,
operation=operation,
details=details
)
db.add(log)
db.commit()
db.refresh(log)
return log
# 用户认证相关路由
@app.post("/auth/register", response_model=UserResponse)
def register(user: UserCreate, db: Session = Depends(get_db)):
# 检查用户名是否已存在
existing_user = db.query(User).filter(User.username == user.username).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在"
)
# 检查邮箱是否已存在
existing_email = db.query(User).filter(User.email == user.email).first()
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被注册"
)
# 创建新用户
hashed_password = get_password_hash(user.password)
db_user = User(
username=user.username,
email=user.email,
hashed_password=hashed_password,
full_name=user.full_name,
is_admin=user.is_admin # 设置管理员权限
)
db.add(db_user)
db.commit()
db.refresh(db_user)
# 记录操作日志
create_operation_log(db, db_user.id, "用户注册", f"用户 {user.username} 注册成功")
return db_user
@app.post("/auth/login")
def login(user: UserLogin, db: Session = Depends(get_db)):
# 验证用户
db_user = db.query(User).filter(User.username == user.username).first()
if not db_user or not verify_password(user.password, db_user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
# 生成token
access_token = create_access_token(data={"sub": str(db_user.id)})
# 记录操作日志
create_operation_log(db, db_user.id, "用户登录", "用户登录成功")
return {
"access_token": access_token,
"token_type": "bearer",
"user": UserResponse.from_orm(db_user)
}
@app.post("/auth/logout")
def logout(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
# 记录操作日志
create_operation_log(db, current_user.id, "用户登出", "用户登出成功")
return {"message": "登出成功"}
# 用户管理路由
@app.get("/users/me", response_model=UserResponse)
def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
@app.put("/users/me", response_model=UserResponse)
def update_user_me(
user_update: UserUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
update_data = user_update.dict(exclude_unset=True)
# 如果更新密码,需要重新哈希
if "password" in update_data:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
for field, value in update_data.items():
setattr(current_user, field, value)
db.commit()
db.refresh(current_user)
# 记录操作日志
create_operation_log(db, current_user.id, "更新用户信息", "用户更新个人信息")
return current_user
@app.get("/users", response_model=List[UserResponse])
def read_users(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 只有管理员可以查看所有用户
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限查看用户列表"
)
users = db.query(User).offset(skip).limit(limit).all()
# 记录操作日志
create_operation_log(db, current_user.id, "查看用户列表", "管理员查看所有用户")
return users
@app.delete("/users/{user_id}")
def delete_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 只有管理员可以删除用户
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限删除用户"
)
if user_id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能删除自己的账户"
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在"
)
db.delete(user)
db.commit()
# 记录操作日志
create_operation_log(db, current_user.id, "删除用户", f"删除用户 {user.username}")
return {"message": "用户删除成功"}
# 文档管理路由
@app.post("/documents", response_model=DocumentResponse)
def create_document(
document: DocumentCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
db_document = Document(
**document.dict(),
owner_id=current_user.id
)
db.add(db_document)
db.commit()
db.refresh(db_document)
# 记录操作日志
create_operation_log(db, current_user.id, "创建文档", f"创建文档: {document.title}")
return db_document
@app.get("/documents", response_model=List[DocumentResponse])
def read_documents(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 管理员可以查看所有文档,普通用户只能查看自己的文档
if current_user.is_admin:
documents = db.query(Document).offset(skip).limit(limit).all()
else:
documents = db.query(Document).filter(
Document.owner_id == current_user.id
).offset(skip).limit(limit).all()
# 记录操作日志
log_action = "查看所有文档列表" if current_user.is_admin else "查看自己的文档"
create_operation_log(db, current_user.id, "查看文档列表", log_action)
return documents
@app.get("/documents/{document_id}", response_model=DocumentResponse)
def read_document(
document_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 管理员可以查看任何文档,普通用户只能查看自己的文档
if current_user.is_admin:
document = db.query(Document).filter(Document.id == document_id).first()
else:
document = db.query(Document).filter(
Document.id == document_id,
Document.owner_id == current_user.id
).first()
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="文档不存在"
)
# 记录操作日志
log_action = f"管理员查看文档" if current_user.is_admin else f"查看文档: {document.title}"
create_operation_log(db, current_user.id, "查看文档", log_action)
return document
@app.put("/documents/{document_id}", response_model=DocumentResponse)
def update_document(
document_id: int,
document_update: DocumentUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 管理员可以编辑任何文档,普通用户只能编辑自己的文档
if current_user.is_admin:
document = db.query(Document).filter(Document.id == document_id).first()
else:
document = db.query(Document).filter(
Document.id == document_id,
Document.owner_id == current_user.id
).first()
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="文档不存在"
)
update_data = document_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(document, field, value)
db.commit()
db.refresh(document)
# 记录操作日志
log_action = f"管理员更新文档" if current_user.is_admin else f"更新文档: {document.title}"
create_operation_log(db, current_user.id, "更新文档", log_action)
return document
@app.delete("/documents/{document_id}")
def delete_document(
document_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 管理员可以删除任何文档,普通用户只能删除自己的文档
if current_user.is_admin:
document = db.query(Document).filter(Document.id == document_id).first()
else:
document = db.query(Document).filter(
Document.id == document_id,
Document.owner_id == current_user.id
).first()
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="文档不存在"
)
db.delete(document)
db.commit()
# 记录操作日志
log_action = f"管理员删除文档" if current_user.is_admin else f"删除文档: {document.title}"
create_operation_log(db, current_user.id, "删除文档", log_action)
return {"message": "文档删除成功"}
# 操作日志路由
@app.get("/logs/my", response_model=List[UserOperationLogResponse])
def read_my_logs(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
logs = db.query(UserOperationLog).filter(
UserOperationLog.user_id == current_user.id
).order_by(UserOperationLog.created_at.desc()).offset(skip).limit(limit).all()
return logs
@app.get("/logs", response_model=List[UserOperationLogResponse])
def read_all_logs(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 只有管理员可以查看所有日志
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限查看所有操作日志"
)
logs = db.query(UserOperationLog).order_by(
UserOperationLog.created_at.desc()
).offset(skip).limit(limit).all()
return logs
@app.get("/")
def read_root():
return {"message": "文档在线管理系统API"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

52
models.py Normal file
View File

@@ -0,0 +1,52 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(100), nullable=True)
is_admin = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
documents = relationship("Document", back_populates="owner")
operation_logs = relationship("UserOperationLog", back_populates="user")
class Document(Base):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=True)
description = Column(String(500), nullable=True)
file_type = Column(String(50), default="txt") # txt, pdf, doc, etc.
file_size = Column(Integer, default=0) # 文件大小(字节)
is_public = Column(Boolean, default=False)
owner_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
owner = relationship("User", back_populates="documents")
class UserOperationLog(Base):
__tablename__ = "user_operation_logs"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
operation = Column(String(100), nullable=False) # 操作类型
details = Column(Text, nullable=True) # 操作详情
ip_address = Column(String(50), nullable=True) # IP地址
user_agent = Column(String(500), nullable=True) # 用户代理
created_at = Column(DateTime(timezone=True), server_default=func.now())
# 关系
user = relationship("User", back_populates="operation_logs")

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==1.4.54
python-jose[cryptography]==3.5.0
python-multipart==0.0.6
python-dotenv==1.0.0
pydantic==1.10.26

110
schemas.py Normal file
View File

@@ -0,0 +1,110 @@
from sqlalchemy.orm import Session
from pydantic import BaseModel, validator, EmailStr
from typing import Optional
from datetime import datetime
import re
class UserBase(BaseModel):
username: str
email: Optional[EmailStr] = None
full_name: Optional[str] = None
class UserCreate(UserBase):
password: str
is_admin: bool = False # 新增管理员字段默认为False
@validator('username')
def username_alphanumeric(cls, v):
if not v.replace('_', '').isalnum():
raise ValueError('用户名只能包含字母、数字和下划线')
if len(v) < 3 or len(v) > 20:
raise ValueError('用户名长度必须在3-20个字符之间')
return v
@validator('password')
def password_strength(cls, v):
if len(v) < 6:
raise ValueError('密码长度至少6位')
return v
class UserLogin(BaseModel):
username: str
password: str
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
full_name: Optional[str] = None
password: Optional[str] = None
@validator('password')
def password_strength(cls, v):
if v and len(v) < 6:
raise ValueError('密码长度至少6位')
return v
class UserResponse(UserBase):
id: int
is_admin: bool # 添加管理员标识
is_active: bool
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# 文档相关模式
class DocumentBase(BaseModel):
title: str
content: Optional[str] = None
description: Optional[str] = None
file_type: str = "txt"
file_size: int = 0
is_public: bool = False
class DocumentCreate(DocumentBase):
pass
class DocumentUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
description: Optional[str] = None
file_type: Optional[str] = None
is_public: Optional[bool] = None
class DocumentResponse(DocumentBase):
id: int
owner_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# 操作日志相关模式
class UserOperationLogBase(BaseModel):
operation: str
details: Optional[str] = None
ip_address: Optional[str] = None
user_agent: Optional[str] = None
class UserOperationLogResponse(UserOperationLogBase):
id: int
user_id: int
created_at: datetime
class Config:
orm_mode = True
# Token响应模式
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None

542
style.css Normal file
View File

@@ -0,0 +1,542 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #667eea;
min-height: 100vh;
}
/* 导航栏样式 */
.navbar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-logo {
font-size: 1.5rem;
font-weight: bold;
color: #4a5568;
}
.nav-menu {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
color: #4a5568;
font-weight: 500;
}
.nav-btn {
padding: 0.5rem 1rem;
border: 2px solid #667eea;
background: transparent;
color: #667eea;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: #667eea;
color: white;
}
/* 主要内容区域 */
.main-content {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
/* 表单区域样式 */
.form-section {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: 2rem auto;
}
.form-section h2 {
text-align: center;
margin-bottom: 1.5rem;
color: #4a5568;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 500;
color: #4a5568;
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin: 0;
}
/* 按钮样式 */
.btn-primary {
background-color: #667eea;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.btn-danger {
background: #e53e3e;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-danger:hover {
background: #c53030;
}
/* 用户面板样式 */
.user-panel {
background: white;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.user-panel h2 {
color: #4a5568;
margin-bottom: 1rem;
}
.user-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* 用户管理区域 */
.user-management-section {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
/* 文档管理区域 */
.documents-section {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.section-header h2 {
color: #4a5568;
}
/* 文档列表样式 */
.documents-list {
display: grid;
gap: 1rem;
}
.document-item {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s ease;
cursor: pointer;
}
.document-item:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.document-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.document-title {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 0.5rem;
}
.document-meta {
display: flex;
gap: 1rem;
color: #718096;
font-size: 0.875rem;
}
.document-description {
color: #4a5568;
margin-bottom: 0.5rem;
}
.document-content-preview {
color: #718096;
font-size: 0.875rem;
line-height: 1.4;
max-height: 3rem;
overflow: hidden;
text-overflow: ellipsis;
}
.document-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.modal-content h3 {
color: #4a5568;
margin-bottom: 1.5rem;
}
.close {
position: absolute;
top: 1rem;
right: 1.5rem;
font-size: 1.5rem;
cursor: pointer;
color: #718096;
}
.close:hover {
color: #4a5568;
}
/* 用户列表样式 */
.users-list {
display: grid;
gap: 1rem;
}
.user-item {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.user-item:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.user-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.user-info {
flex: 1;
}
.user-username {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 0.25rem;
}
.user-email {
color: #718096;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.user-fullname {
color: #4a5568;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.user-meta {
display: flex;
gap: 1rem;
color: #718096;
font-size: 0.75rem;
}
.user-admin-badge {
background: #667eea;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.user-actions {
display: flex;
gap: 0.5rem;
}
/* 日志列表样式 */
.logs-list {
max-height: 400px;
overflow-y: auto;
}
.log-item {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.5rem;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.log-action {
font-weight: 600;
color: #2d3748;
}
.log-time {
color: #718096;
font-size: 0.875rem;
}
.log-details {
color: #4a5568;
font-size: 0.875rem;
}
/* 消息提示样式 */
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 6px;
color: white;
font-weight: 500;
z-index: 3000;
animation: slideIn 0.3s ease;
}
.message.success {
background: #48bb78;
}
.message.error {
background: #f56565;
}
.message.info {
background: #4299e1;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
padding: 1rem;
}
.main-content {
padding: 0 1rem;
margin: 1rem auto;
}
.form-section {
margin: 1rem auto;
padding: 1.5rem;
}
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.user-actions {
flex-direction: column;
}
.document-header {
flex-direction: column;
gap: 0.5rem;
}
.modal-content {
width: 95%;
padding: 1.5rem;
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: #718096;
}
.empty-state h3 {
margin-bottom: 0.5rem;
color: #4a5568;
}