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