Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe802b9f34 | ||
|
|
fb98a84ea5 |
259
README.md
259
README.md
@@ -2,29 +2,107 @@
|
||||
|
||||
## 🚀 功能概述
|
||||
|
||||
本工具旨在帮助管理员通过 API 自动批量退选学生的课程。
|
||||
本工具提供用户友好的 Web 界面来帮助管理员批量退选学生的课程,支持文件上传、数据预览和可视化操作,让批量退课更加直观便捷。
|
||||
|
||||
系统会从项目根目录下的 `退课列表.xlsx` 文件中读取学生 ID 和待退选的课程班 ID 列表,然后执行以下操作:
|
||||
## 🏗️ 项目架构
|
||||
|
||||
1. **获取学生课程**:通过 `GET` 请求获取每个学生当前已选的课程班列表。
|
||||
2. **匹配与退课**:在学生的已选课程中匹配需要退选的课程班 ID,并通过 `DELETE` 请求发起退课操作。
|
||||
3. **结果反馈**:在终端清晰地输出每一位学生的处理结果,并在操作结束后提供总结报告。
|
||||
```
|
||||
deleclass_副本2/
|
||||
├── src/ # 后端 API 服务
|
||||
│ └── main.py # Flask API 服务器
|
||||
├── toolbox-app/ # 前端 Web 应用
|
||||
│ ├── src/ # Vue.js 源代码
|
||||
│ ├── public/ # 静态资源
|
||||
│ └── package.json # 前端依赖配置
|
||||
├── requirements.txt # Python 依赖
|
||||
├── generate_template.py # Excel 模板生成工具
|
||||
├── .env.example # 环境变量模板
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 后端
|
||||
- **Python 3.x** - 主要编程语言
|
||||
- **Flask** - Web 框架,提供 API 服务
|
||||
- **openpyxl** - Excel 文件处理
|
||||
- **requests** - HTTP 请求库
|
||||
- **python-dotenv** - 环境变量管理
|
||||
|
||||
### 前端
|
||||
- **Vue.js 3** - 现代化前端框架
|
||||
- **Vite** - 快速构建工具
|
||||
- **Element Plus** - UI 组件库
|
||||
- **Axios** - HTTP 客户端
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
系统会处理 Excel 文件中的学生 ID 和课程班 ID 列表,执行以下操作:
|
||||
|
||||
1. **获取学生课程**:通过 `GET` 请求获取每个学生当前已选的课程班列表
|
||||
2. **匹配与退课**:在学生的已选课程中匹配需要退选的课程班 ID,并通过 `DELETE` 请求发起退课操作
|
||||
3. **结果反馈**:提供详细的处理结果和统计报告
|
||||
|
||||
> **🛡️ 安全第一**:为了防止误操作,本工具采用 **先预览,后确认** 的安全执行模式。
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 快速上手指南
|
||||
## 🚀 快速开始
|
||||
|
||||
### 第 1 步:环境配置 (`.env` 文件)
|
||||
### 1. 启动后端服务
|
||||
|
||||
在首次使用前,请在项目根目录下创建一个名为 `.env` 的文件,并将以下内容复制进去。
|
||||
```bash
|
||||
# 安装 Python 依赖
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# 配置环境变量(创建 .env 文件)
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件,填入您的 API 配置
|
||||
|
||||
# 启动 Flask 后端服务
|
||||
python3 src/main.py
|
||||
```
|
||||
|
||||
### 2. 启动前端应用
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd toolbox-app
|
||||
|
||||
# 安装 Node.js 依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 使用 Web 界面
|
||||
|
||||
1. 打开浏览器访问 `http://localhost:5173`
|
||||
2. 上传包含学生 ID 和课程班 ID 的 Excel 文件
|
||||
3. 预览退课数据,确认无误后执行批量退课
|
||||
4. 查看详细的操作结果和统计信息
|
||||
|
||||
## 📋 数据格式要求
|
||||
|
||||
Excel 文件应包含以下列:
|
||||
|
||||
| 学生ID | 课程班ID |
|
||||
| --- | --- |
|
||||
| 3831629 | 1820221, 1820210 |
|
||||
| 3831630 | 1820456 |
|
||||
|
||||
- **学生ID**: 学生的唯一标识符
|
||||
- **课程班ID**: 需要退选的一个或多个课程班 ID,多个 ID 之间用英文逗号 `,` 分隔
|
||||
|
||||
## ⚙️ 环境配置
|
||||
|
||||
创建 `.env` 文件并配置以下参数:
|
||||
|
||||
```dotenv
|
||||
# --- API 配置 ---
|
||||
BASE_URL=https://api.seiue.com
|
||||
|
||||
|
||||
# --- 身份认证 ---
|
||||
# ⚠️ 请将 <你的Token> 替换为有效的 Bearer Token
|
||||
AUTHORIZATION="Bearer <你的Token>"
|
||||
@@ -34,78 +112,129 @@ X_REFLECTION_ID="969269"
|
||||
SEMESTER_ID=61626
|
||||
```
|
||||
|
||||
> **⚠️ 注意:**
|
||||
> - **必须** 将 `AUTHORIZATION` 配置项中的 `<你的Token>` 替换为您自己的有效令牌。
|
||||
> - `.env` 文件包含了敏感信息,请 **绝对不要** 将其提交到任何公共代码仓库。
|
||||
> **⚠️ 重要提醒:**
|
||||
> - 必须将 `AUTHORIZATION` 中的 `<你的Token>` 替换为您的有效令牌
|
||||
> - `.env` 文件包含敏感信息,请勿提交到公共代码仓库
|
||||
|
||||
### 第 2 步:准备数据 (`退课列表.xlsx`)
|
||||
---
|
||||
|
||||
请确保项目根目录下存在 `退课列表.xlsx` 文件,并遵循以下格式:
|
||||
## 🔧 开发指南
|
||||
|
||||
| 学生ID | 课程班ID |
|
||||
| --- | --- |
|
||||
| 3831629 | 1820221, 1820210 |
|
||||
| 3831630 | 1820456 |
|
||||
### 本地开发环境搭建
|
||||
|
||||
- **学生ID**: 学生的唯一标识符。
|
||||
- **课程班ID**: 需要退选的一个或多个课程班 ID,多个 ID 之间请用 **英文逗号** `,` 分隔。
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone https://git.vcck.cn/nvex/toolbox.git
|
||||
cd toolbox
|
||||
```
|
||||
|
||||
### 第 3 步:安装依赖
|
||||
2. **后端开发**
|
||||
```bash
|
||||
# 安装 Python 依赖
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件
|
||||
|
||||
# 启动后端服务(开发模式)
|
||||
python3 src/main.py
|
||||
```
|
||||
|
||||
在项目根目录下打开终端,运行以下命令:
|
||||
3. **前端开发**
|
||||
```bash
|
||||
cd toolbox-app
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
> 👉 **执行安装命令**
|
||||
> ```bash
|
||||
> pip3 install -r requirements.txt
|
||||
> ```
|
||||
|
||||
这将安装运行本工具所需的所有库 (`requests`, `openpyxl`, `python-dotenv`)。
|
||||
|
||||
### 第 4 步:执行脚本
|
||||
|
||||
一切准备就绪后,在项目根目录下运行主程序:
|
||||
|
||||
> 👉 **执行主程序**
|
||||
> ```bash
|
||||
> python3 src/main.py
|
||||
> ```
|
||||
|
||||
#### 4.1) 预览变更
|
||||
|
||||
脚本启动后,将 **自动进入预览模式**。它会分析数据并显示即将发生的操作,但 **不会执行任何实际的退课**。
|
||||
### 项目结构说明
|
||||
|
||||
```
|
||||
--- 预览模式: 以下操作将不会实际执行 ---
|
||||
---
|
||||
➡️ [预览] 学生 3831629 将退课: 体育自主选修, 音乐
|
||||
ℹ️ 学生 3831630 无匹配课程班可退。
|
||||
---
|
||||
├── src/main.py # Flask API 服务器
|
||||
├── toolbox-app/
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Vue 组件
|
||||
│ │ ├── views/ # 页面视图
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ └── store/ # 状态管理
|
||||
│ ├── public/ # 静态资源
|
||||
│ └── vite.config.js # Vite 配置
|
||||
├── requirements.txt # Python 依赖
|
||||
├── generate_template.py # Excel 模板生成工具
|
||||
└── .env.example # 环境变量模板
|
||||
```
|
||||
|
||||
#### 4.2) 确认执行
|
||||
## 📦 部署指南
|
||||
|
||||
预览信息显示完毕后,程序会暂停并等待您的最终确认。
|
||||
### 生产环境部署
|
||||
|
||||
> 👉 **输入 `drop` 确认执行**
|
||||
> ```
|
||||
> 请检查以上预览信息。
|
||||
> 👉 如果确认要执行退课操作,请输入 'drop' 并按 Enter。输入其他任何内容将取消操作: drop
|
||||
> ```
|
||||
1. **后端部署**
|
||||
```bash
|
||||
# 使用 gunicorn 部署 Flask 应用
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 src.main:app
|
||||
```
|
||||
|
||||
- **确认执行**:精确输入 `drop` 并按回车键,脚本将开始执行实际的退课操作。
|
||||
- **取消操作**:输入任何其他内容(或直接按回车),程序将立即安全退出。
|
||||
2. **前端部署**
|
||||
```bash
|
||||
cd toolbox-app
|
||||
npm run build
|
||||
# 将 dist/ 目录部署到 Web 服务器
|
||||
```
|
||||
|
||||
#### 4.3) 查看结果
|
||||
3. **使用 Docker(可选)**
|
||||
```dockerfile
|
||||
# 示例 Dockerfile
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
EXPOSE 5000
|
||||
CMD ["python", "src/main.py"]
|
||||
```
|
||||
|
||||
确认执行后,脚本会处理每个学生,并实时打印结果。全部操作完成后,会显示最终的统计报告。
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 本项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 🆘 常见问题
|
||||
|
||||
### Q: 如何生成 Excel 模板?
|
||||
A: 运行 `python3 generate_template.py` 生成标准格式的 Excel 模板文件。
|
||||
|
||||
### Q: API 认证失败怎么办?
|
||||
A: 请检查 `.env` 文件中的 `AUTHORIZATION` 配置,确保 Token 有效且格式正确。
|
||||
|
||||
### Q: 前端无法连接后端?
|
||||
A: 确保后端服务已启动(默认端口 5000),检查前端配置中的 API 基础 URL。
|
||||
|
||||
### Q: Excel 文件格式错误?
|
||||
A: 确保 Excel 文件包含 "学生ID" 和 "课程班ID" 列,且数据格式符合要求。
|
||||
|
||||
```
|
||||
--- 开始执行批量退课 ---
|
||||
---
|
||||
✅ 学生 3831629 已退课: 体育自主选修, 音乐
|
||||
ℹ️ 学生 3831630 无匹配课程班,跳过。
|
||||
|
||||
--- 批量退课结果 ---
|
||||
✅ 成功: 1
|
||||
❌ 失败: 0
|
||||
```
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
|
||||
- 📧 提交 Issue: [项目 Issues](https://git.vcck.cn/nvex/toolbox/issues)
|
||||
- 💬 项目讨论: [项目讨论区](https://git.vcck.cn/nvex/toolbox/discussions)
|
||||
|
||||
---
|
||||
143
src/main.py
143
src/main.py
@@ -10,31 +10,22 @@ load_dotenv()
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# --- 从 .env 文件加载配置 ---
|
||||
BASE_URL = os.getenv("BASE_URL")
|
||||
AUTHORIZATION = os.getenv("AUTHORIZATION")
|
||||
SEMESTER_ID = os.getenv("SEMESTER_ID")
|
||||
X_ROLE = os.getenv("X_ROLE")
|
||||
X_SCHOOL_ID = os.getenv("X_SCHOOL_ID")
|
||||
X_REFLECTION_ID = os.getenv("X_REFLECTION_ID")
|
||||
|
||||
# Use absolute path for the template directory
|
||||
TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'toolbox-app', 'public'))
|
||||
TEMPLATE_FILENAME = '退课列表.xlsx'
|
||||
|
||||
def get_student_info_from_api(student_id):
|
||||
def get_student_info_from_api(base_url, authorization, x_role, x_school_id, x_reflection_id, student_id):
|
||||
"""通过 API 查询学生姓名和班级"""
|
||||
if not all([BASE_URL, AUTHORIZATION, X_ROLE, X_SCHOOL_ID, X_REFLECTION_ID]):
|
||||
return "配置缺失", None, "Error: Missing API configuration"
|
||||
if not all([base_url, authorization, x_role, x_school_id, x_reflection_id]):
|
||||
return None, None, f"缺少一个或多个 API 配置参数"
|
||||
|
||||
url = f"{base_url}/chalk/role/reflection/students"
|
||||
headers = {
|
||||
"Authorization": AUTHORIZATION,
|
||||
"X-Role": X_ROLE,
|
||||
"X-School-ID": X_SCHOOL_ID,
|
||||
"X-Reflection-ID": X_REFLECTION_ID
|
||||
'Authorization': authorization,
|
||||
'X-Role': x_role,
|
||||
'X-School-ID': x_school_id,
|
||||
'X-Reflection-ID': x_reflection_id
|
||||
}
|
||||
|
||||
url = f"{BASE_URL}/chalk/role/reflection/students"
|
||||
params = {
|
||||
"id_in": student_id,
|
||||
"policy": "admin"
|
||||
@@ -57,20 +48,19 @@ def get_student_info_from_api(student_id):
|
||||
app.logger.error(f"API request failed for student_id {student_id}: {e}")
|
||||
return "查询失败", None, f"Error fetching info for student {student_id}"
|
||||
|
||||
def get_class_info_from_api(student_id, class_ids):
|
||||
def get_class_info_from_api(base_url, authorization, x_role, x_school_id, x_reflection_id, semester_id, student_id):
|
||||
"""通过 API 查询课程班信息,返回ID和名称的字典"""
|
||||
if not all([BASE_URL, AUTHORIZATION, SEMESTER_ID, X_ROLE, X_SCHOOL_ID, X_REFLECTION_ID]):
|
||||
return None, "错误: 缺少 API 配置"
|
||||
if not all([base_url, authorization, x_role, x_school_id, x_reflection_id, semester_id]):
|
||||
return None, f"缺少一个或多个 API 配置参数"
|
||||
|
||||
url = f"{base_url}/scms/class/students/{student_id}/classes"
|
||||
headers = {
|
||||
"Authorization": AUTHORIZATION,
|
||||
"X-Role": X_ROLE,
|
||||
"X-School-ID": X_SCHOOL_ID,
|
||||
"X-Reflection-ID": X_REFLECTION_ID
|
||||
'Authorization': authorization,
|
||||
'X-Role': x_role,
|
||||
'X-School-ID': x_school_id,
|
||||
'X-Reflection-ID': x_reflection_id
|
||||
}
|
||||
|
||||
url = f"{BASE_URL}/scms/class/students/{student_id}/classes"
|
||||
params = {"semester_id": SEMESTER_ID, "paginated": "1", "expand": "course"}
|
||||
params = {"semester_id": semester_id, "paginated": "1", "expand": "course"}
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, params=params)
|
||||
@@ -92,18 +82,18 @@ def get_class_info_from_api(student_id, class_ids):
|
||||
app.logger.error(f"API request failed for student_id {student_id}: {e}")
|
||||
return None, f"为学生 {student_id} 获取名称时出错"
|
||||
|
||||
def get_school_name_from_api():
|
||||
def get_school_name_from_api(base_url, authorization, x_role, x_school_id):
|
||||
"""通过 API 查询学校名称"""
|
||||
if not all([BASE_URL, AUTHORIZATION, X_ROLE, X_SCHOOL_ID]):
|
||||
if not all([base_url, authorization, x_role, x_school_id]):
|
||||
return "配置缺失", "Error: Missing API configuration"
|
||||
|
||||
headers = {
|
||||
"Authorization": AUTHORIZATION,
|
||||
"X-Role": X_ROLE,
|
||||
"X-School-ID": X_SCHOOL_ID
|
||||
"Authorization": authorization,
|
||||
"X-Role": x_role,
|
||||
"X-School-ID": x_school_id
|
||||
}
|
||||
|
||||
url = f"{BASE_URL}/chalk/system/schools/{X_SCHOOL_ID}?expand=custom_constraints"
|
||||
|
||||
url = f"{base_url}/chalk/system/schools/{x_school_id}?expand=custom_constraints"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers)
|
||||
@@ -113,13 +103,27 @@ def get_school_name_from_api():
|
||||
return school_name, None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
app.logger.error(f"API request failed for school_id {X_SCHOOL_ID}: {e}")
|
||||
return "查询失败", f"Error fetching info for school {X_SCHOOL_ID}"
|
||||
app.logger.error(f"API request failed for school_id {x_school_id}: {e}")
|
||||
return "查询失败", f"Error fetching info for school {x_school_id}"
|
||||
|
||||
@app.route('/api/school-name', methods=['GET'])
|
||||
def get_school_name():
|
||||
"""Provides the school name."""
|
||||
school_name, error = get_school_name_from_api()
|
||||
"""Provides the school name based on headers."""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
school_id_header = request.headers.get('X-School-ID')
|
||||
base_url_header = request.headers.get('X-Base-URL')
|
||||
role_header = request.headers.get('X-Role')
|
||||
|
||||
if not all([auth_header, school_id_header, base_url_header, role_header]):
|
||||
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role'}), 400
|
||||
|
||||
school_name, error = get_school_name_from_api(
|
||||
base_url=base_url_header,
|
||||
authorization=auth_header,
|
||||
x_role=role_header,
|
||||
x_school_id=school_id_header
|
||||
)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
return jsonify({'school_name': school_name})
|
||||
@@ -158,6 +162,16 @@ def preview_file():
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'No selected file'}), 400
|
||||
|
||||
auth_header = request.headers.get('Authorization')
|
||||
school_id_header = request.headers.get('X-School-ID')
|
||||
base_url_header = request.headers.get('X-Base-URL')
|
||||
role_header = request.headers.get('X-Role')
|
||||
reflection_id_header = request.headers.get('X-Reflection-ID')
|
||||
semester_id_header = request.headers.get('X-Semester-ID')
|
||||
|
||||
if not all([auth_header, school_id_header, base_url_header, role_header, reflection_id_header, semester_id_header]):
|
||||
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role, X-Reflection-ID, X-Semester-ID'}), 400
|
||||
|
||||
try:
|
||||
df = pd.read_excel(file)
|
||||
df = df.where(pd.notnull(df), None)
|
||||
@@ -170,8 +184,24 @@ def preview_file():
|
||||
student_id = row['学生ID']
|
||||
class_ids_str = row['课程班ID']
|
||||
|
||||
student_name, admin_classes, info_error = get_student_info_from_api(student_id)
|
||||
class_details, class_error = get_class_info_from_api(student_id, class_ids_str)
|
||||
student_name, admin_classes, info_error = get_student_info_from_api(
|
||||
base_url=base_url_header,
|
||||
authorization=auth_header,
|
||||
x_role=role_header,
|
||||
x_school_id=school_id_header,
|
||||
x_reflection_id=reflection_id_header,
|
||||
student_id=student_id
|
||||
)
|
||||
class_details, class_error = get_class_info_from_api(
|
||||
base_url=base_url_header,
|
||||
authorization=auth_header,
|
||||
x_role=role_header,
|
||||
x_school_id=school_id_header,
|
||||
x_reflection_id=reflection_id_header,
|
||||
semester_id=semester_id_header,
|
||||
student_id=student_id,
|
||||
class_ids=class_ids_str
|
||||
)
|
||||
|
||||
if class_error:
|
||||
class_ids = [id.strip() for id in str(class_ids_str).split(',')]
|
||||
@@ -218,11 +248,20 @@ def withdraw_courses():
|
||||
if not withdraw_list:
|
||||
return jsonify({'error': 'Withdrawal list is required.'}), 400
|
||||
|
||||
auth_header = request.headers.get('Authorization')
|
||||
school_id_header = request.headers.get('X-School-ID')
|
||||
base_url_header = request.headers.get('X-Base-URL')
|
||||
role_header = request.headers.get('X-Role')
|
||||
reflection_id_header = request.headers.get('X-Reflection-ID')
|
||||
|
||||
if not all([auth_header, school_id_header, base_url_header, role_header, reflection_id_header]):
|
||||
return jsonify({'error': 'Missing required headers: Authorization, X-School-ID, X-Base-URL, X-Role, X-Reflection-ID'}), 400
|
||||
|
||||
headers = {
|
||||
"Authorization": AUTHORIZATION,
|
||||
"X-Role": X_ROLE,
|
||||
"X-School-ID": X_SCHOOL_ID,
|
||||
"X-Reflection-ID": X_REFLECTION_ID
|
||||
"Authorization": auth_header,
|
||||
"X-Role": role_header,
|
||||
"X-School-ID": school_id_header,
|
||||
"X-Reflection-ID": reflection_id_header
|
||||
}
|
||||
|
||||
student_withdrawals = {}
|
||||
@@ -247,17 +286,17 @@ def withdraw_courses():
|
||||
|
||||
payload = {
|
||||
"reflection_ids": [student_id],
|
||||
"class_ids": [int(cid) for cid in class_ids]
|
||||
"class_ids": [int(cid) for cid in class_ids] if class_ids else []
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.delete(f"{BASE_URL}/scms/class/class-selections", headers=headers, json=payload)
|
||||
if resp.status_code == 204:
|
||||
success_count += 1
|
||||
details.append(f"✅ 学生 {student_id} 已成功退课")
|
||||
else:
|
||||
failure_count += 1
|
||||
details.append(f"❌ 学生 {student_id} 退课失败:{resp.status_code} - {resp.text}")
|
||||
resp = requests.delete(f"{base_url_header}/scms/class/class-selections", headers=headers, json=payload)
|
||||
resp.raise_for_status()
|
||||
success_count += 1
|
||||
details.append(f"✅ 学生 {student_id} 已成功退课")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
failure_count += 1
|
||||
details.append(f"❌ 学生 {student_id} 退课失败:{e.response.status_code} - {e.response.text}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
failure_count += 1
|
||||
details.append(f"❌ 学生 {student_id} 退课失败: {e}")
|
||||
|
||||
@@ -1,5 +1,277 @@
|
||||
# Vue 3 + Vite
|
||||
# 批量退课工具 - Web 前端应用
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
## 🌐 项目概述
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
这是批量退课工具的 Web 前端应用,基于 Vue.js 3 + Vite 构建,提供用户友好的图形界面来执行批量退课操作。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
- 📁 **文件上传**: 支持拖拽上传 Excel 文件
|
||||
- 👀 **数据预览**: 上传后即时预览退课数据
|
||||
- 🔍 **数据验证**: 自动检查数据格式和完整性
|
||||
- ⚡ **批量操作**: 一键执行批量退课
|
||||
- 📊 **结果展示**: 详细的操作结果和统计信息
|
||||
- 🎨 **响应式设计**: 适配各种屏幕尺寸
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **Vue.js 3** - 渐进式 JavaScript 框架
|
||||
- **Vite** - 下一代前端构建工具
|
||||
- **Element Plus** - Vue 3 UI 组件库
|
||||
- **Axios** - HTTP 客户端
|
||||
- **Vue Router** - 官方路由管理器
|
||||
- **Pinia** - Vue 状态管理库
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- npm >= 8.0.0
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
应用将在 `http://localhost:5173` 启动
|
||||
|
||||
### 生产构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建文件将输出到 `dist/` 目录
|
||||
|
||||
### 预览生产构建
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
toolbox-app/
|
||||
├── public/ # 静态资源
|
||||
│ ├── vite.svg # 应用图标
|
||||
│ └── *.xlsx # 示例 Excel 文件
|
||||
├── src/
|
||||
│ ├── components/ # Vue 组件
|
||||
│ │ ├── CourseWithdrawal.vue # 主要的退课组件
|
||||
│ │ └── ...
|
||||
│ ├── views/ # 页面视图
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.js
|
||||
│ ├── store/ # 状态管理
|
||||
│ │ └── index.js
|
||||
│ ├── assets/ # 资源文件
|
||||
│ ├── style.css # 全局样式
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 应用入口
|
||||
├── index.html # HTML 模板
|
||||
├── vite.config.js # Vite 配置
|
||||
├── package.json # 项目配置
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### API 配置
|
||||
|
||||
前端应用通过 Axios 与后端 Flask API 通信。API 基础 URL 配置在组件中:
|
||||
|
||||
```javascript
|
||||
// 默认后端地址
|
||||
const API_BASE_URL = 'http://localhost:5000'
|
||||
```
|
||||
|
||||
如需修改后端地址,请在相关组件中更新此配置。
|
||||
|
||||
### 构建配置
|
||||
|
||||
Vite 配置文件 `vite.config.js`:
|
||||
|
||||
```javascript
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
host: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 📋 使用指南
|
||||
|
||||
### 1. 准备 Excel 文件
|
||||
|
||||
确保 Excel 文件包含以下列:
|
||||
|
||||
| 学生ID | 课程班ID |
|
||||
|--------|----------|
|
||||
| 3831629 | 1820221, 1820210 |
|
||||
| 3831630 | 1820456 |
|
||||
|
||||
### 2. 上传文件
|
||||
|
||||
- 点击上传区域选择文件,或直接拖拽文件到上传区域
|
||||
- 支持 `.xlsx` 和 `.xls` 格式
|
||||
- 文件大小限制:10MB
|
||||
|
||||
### 3. 预览数据
|
||||
|
||||
- 上传成功后,系统会自动解析并显示数据预览
|
||||
- 检查学生 ID 和课程班 ID 是否正确
|
||||
- 确认数据无误后点击"执行退课"
|
||||
|
||||
### 4. 执行退课
|
||||
|
||||
- 点击"执行退课"按钮开始批量操作
|
||||
- 系统会显示实时进度和结果
|
||||
- 操作完成后查看详细的统计报告
|
||||
|
||||
### 5. 查看结果
|
||||
|
||||
- 成功退课的记录会显示为绿色
|
||||
- 失败的记录会显示为红色,并包含错误信息
|
||||
- 跳过的记录会显示为灰色
|
||||
|
||||
## 🎨 界面特性
|
||||
|
||||
### 响应式设计
|
||||
- 支持桌面端、平板和移动端
|
||||
- 自适应布局,优化各种屏幕尺寸的使用体验
|
||||
|
||||
### 用户体验
|
||||
- 直观的文件上传界面
|
||||
- 实时的操作反馈
|
||||
- 清晰的结果展示
|
||||
- 友好的错误提示
|
||||
|
||||
### 视觉设计
|
||||
- 现代化的 UI 设计
|
||||
- 一致的色彩方案
|
||||
- 清晰的信息层次
|
||||
- 流畅的交互动画
|
||||
|
||||
## 🔍 开发指南
|
||||
|
||||
### 组件开发
|
||||
|
||||
主要组件 `CourseWithdrawal.vue` 包含:
|
||||
|
||||
- 文件上传功能
|
||||
- 数据预览表格
|
||||
- 批量退课执行
|
||||
- 结果展示
|
||||
|
||||
### 状态管理
|
||||
|
||||
使用 Pinia 管理应用状态:
|
||||
|
||||
```javascript
|
||||
// store/index.js
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
// 应用状态
|
||||
}),
|
||||
actions: {
|
||||
// 状态操作
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### API 集成
|
||||
|
||||
使用 Axios 进行 HTTP 请求:
|
||||
|
||||
```javascript
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:5000',
|
||||
timeout: 30000
|
||||
})
|
||||
```
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
### 静态部署
|
||||
|
||||
构建后将 `dist/` 目录部署到任何静态文件服务器:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# 将 dist/ 目录上传到服务器
|
||||
```
|
||||
|
||||
### Nginx 配置示例
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /path/to/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```dockerfile
|
||||
FROM nginx:alpine
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: 无法连接到后端服务?**
|
||||
A: 检查后端服务是否启动,确认 API 地址配置正确。
|
||||
|
||||
**Q: 文件上传失败?**
|
||||
A: 检查文件格式是否为 Excel,文件大小是否超过限制。
|
||||
|
||||
**Q: 数据预览为空?**
|
||||
A: 确认 Excel 文件包含"学生ID"和"课程班ID"列。
|
||||
|
||||
**Q: 退课操作失败?**
|
||||
A: 检查后端 API 配置和网络连接,查看浏览器控制台错误信息。
|
||||
|
||||
### 调试模式
|
||||
|
||||
开启 Vue DevTools 进行调试:
|
||||
|
||||
```bash
|
||||
# 开发模式自动启用 DevTools
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2024年12月*
|
||||
|
||||
34
toolbox-app/src/api.js
Normal file
34
toolbox-app/src/api.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios';
|
||||
import { useSettingsStore } from './store/settings';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'https://api.seiue.com',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const settingsStore = useSettingsStore();
|
||||
if (settingsStore.authorization) {
|
||||
config.headers['Authorization'] = settingsStore.authorization;
|
||||
}
|
||||
if (settingsStore.xSchoolId) {
|
||||
config.headers['X-School-ID'] = settingsStore.xSchoolId;
|
||||
}
|
||||
if (settingsStore.xRole) {
|
||||
config.headers['X-Role'] = settingsStore.xRole;
|
||||
}
|
||||
if (settingsStore.xReflectionId) {
|
||||
config.headers['X-Reflection-ID'] = settingsStore.xReflectionId;
|
||||
}
|
||||
if (settingsStore.semesterId) {
|
||||
config.headers['X-Semester-ID'] = settingsStore.semesterId;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useSettingsStore } from '../../store/settings';
|
||||
|
||||
const file = ref(null);
|
||||
const previewData = ref([]);
|
||||
@@ -8,6 +8,7 @@ const isParsing = ref(false);
|
||||
const isWithdrawing = ref(false);
|
||||
const fileInput = ref(null);
|
||||
const schoolName = ref('');
|
||||
const schoolNameError = ref('');
|
||||
const showFailedOnly = ref(false);
|
||||
const withdrawalResults = ref(null);
|
||||
|
||||
@@ -119,12 +120,19 @@ function clearPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await axios.get('http://127.0.0.1:5001/api/school-name');
|
||||
schoolName.value = response.data.school_name;
|
||||
} catch (error) {
|
||||
console.error('Error fetching school name:', error);
|
||||
if (settingsStore.authorization) {
|
||||
schoolNameError.value = '令牌失效,请检查您的 Authorization';
|
||||
} else {
|
||||
schoolNameError.value = '请先在设置页面填写配置信息';
|
||||
}
|
||||
}
|
||||
});
|
||||
function getResultItemClass(studentId) {
|
||||
@@ -146,6 +154,9 @@ function getResultIcon(studentId) {
|
||||
<div v-if="schoolName" class="school-info-tag">
|
||||
正在给 {{ schoolName }} 学校退课
|
||||
</div>
|
||||
<div v-if="schoolNameError" class="school-info-tag school-info-error">
|
||||
{{ schoolNameError }}
|
||||
</div>
|
||||
|
||||
<div class="step-section">
|
||||
<div class="step-header">
|
||||
@@ -391,6 +402,12 @@ function getResultIcon(studentId) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.school-info-error {
|
||||
background-color: #fff1f0;
|
||||
border-color: #ffa39e;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.admin-classes-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
|
||||
@@ -2,10 +2,14 @@ import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import api from './api';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
app.provide('api', api);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -2,11 +2,11 @@ import { defineStore } from 'pinia';
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
authorization: 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIzIiwiZXhwIjoxNzYxMTk0MjQzLCJqdGkiOiJmZjdkYTlkMS05ZGEzLTRhZDctYWZhZS04Njc3YWEzMzI0ZGMiLCJpYXQiOjE3NjExMDc4NDMsImlzcyI6Imh0dHBzOi8vbWRwLnNlaXVlLmNvbS9zZWl1ZSIsInN1YiI6IjEwMjg1MjYiLCJjdHgiOiJsb2dpbl9yaWQ9OTY5MjY5XHUwMDI2cmVhZG9ubHk9MFx1MDAyNnN0YWZmX2lkPTI0MCIsInNpZCI6MTAzNDkyOTJ9.bDuRFltrbFkV5NPxMAABFhTxv9zDQDBq4S9ZCRmQwohANrUD7HriP7vmospkrA_5skxJtyi3iK5OtINyIVGFP9zK7jcrfGJlhTrQimfgnHH-39mYsk08fOH-MhDvr2Q87ng-2gACIpNvyvzP-VCxo6IGOk9ZZWg2aT6xua-cV-8',
|
||||
semesterId: '61626',
|
||||
xRole: 'shadow',
|
||||
xSchoolId: '665',
|
||||
xReflectionId: '969269',
|
||||
authorization: '',
|
||||
semesterId: '',
|
||||
xRole: '',
|
||||
xSchoolId: '',
|
||||
xReflectionId: '',
|
||||
}),
|
||||
actions: {
|
||||
setSettings(newSettings) {
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
<form @submit.prevent="saveSettings">
|
||||
<div class="form-group">
|
||||
<label for="authorization">Authorization</label>
|
||||
<input type="text" id="authorization" v-model="settings.authorization">
|
||||
<input type="text" id="authorization" v-model="settings.authorization" placeholder="请输入您的 Authorization">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="semesterId">Semester ID</label>
|
||||
<input type="text" id="semesterId" v-model="settings.semesterId">
|
||||
<input type="text" id="semesterId" v-model="settings.semesterId" placeholder="请输入学期 ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="xRole">X-Role</label>
|
||||
<input type="text" id="xRole" v-model="settings.xRole">
|
||||
<input type="text" id="xRole" v-model="settings.xRole" placeholder="请输入您的角色">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="xSchoolId">X-School-ID</label>
|
||||
<input type="text" id="xSchoolId" v-model="settings.xSchoolId">
|
||||
<input type="text" id="xSchoolId" v-model="settings.xSchoolId" placeholder="请输入学校 ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="xReflectionId">X-Reflection-ID</label>
|
||||
<input type="text" id="xReflectionId" v-model="settings.xReflectionId">
|
||||
<input type="text" id="xReflectionId" v-model="settings.xReflectionId" placeholder="请输入 Reflection ID">
|
||||
</div>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user