feat: 添加文档管理系统前后端基础功能
- 新增后端FastAPI应用,包含用户管理、文档管理和操作日志功能 - 实现JWT认证机制,支持用户注册、登录、登出操作 - 添加数据库模型定义,包括用户表、文档表和操作日志表 - 实现文档的增删改查功能及权限控制 - 添加管理员功能,支持用户管理和全局操作日志查看 - 新增前端界面,实现完整的用户交互体验 - 配置环境变量示例和Git忽略规则 - 编写详细的README文档,包含安装和使用说明
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal 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
84
.gitignore
vendored
Normal 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
310
README.md
Normal 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
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();
|
||||
}
|
||||
});
|
||||
38
auth.py
Normal file
38
auth.py
Normal 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
18
database.py
Normal 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
224
index.html
Normal 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">×</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">×</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">×</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">×</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
399
main.py
Normal 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
52
models.py
Normal 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
7
requirements.txt
Normal 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
110
schemas.py
Normal 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
542
style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user