From 8a35871ceaef3d3d69541914046bc16a80691379 Mon Sep 17 00:00:00 2001 From: huzhiyu_student <111@qq.com> Date: Mon, 21 Sep 2026 19:15:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=AD=A6=E7=94=9F=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 12 + .gitignore | 18 + Dockerfile | 22 + __init__.py | 0 api/Score.py | 132 ++ api/__init__.py | 0 api/advisors.py | 50 + api/c_lass.py | 74 + api/employment_api.py | 116 ++ api/statistics.py | 105 ++ api/students.py | 97 + api/teachers.py | 80 + dao/Score.py | 56 + dao/__init__.py | 0 dao/advisor/.___init__.py | Bin 0 -> 4096 bytes dao/advisor/._crud.py | Bin 0 -> 4096 bytes dao/advisor/._service.py | Bin 0 -> 4096 bytes dao/advisor/__init__.py | 0 dao/advisor/crud.py | 56 + dao/advisor/service.py | 41 + dao/c_lass.py | 68 + dao/employment.py | 175 ++ dao/statistics.py | 110 ++ dao/students_dao.py | 115 ++ dao/teachers_dao.py | 67 + database.py | 44 + main.py | 84 + model/Score.py | 18 + model/advisors.py | 14 + model/c_lass.py | 23 + model/employment.py | 20 + model/students.py | 41 + model/teachers.py | 16 + requirements.txt | 5 + scheme/Score.py | 15 + scheme/advisors.py | 12 + scheme/c_lass.py | 34 + scheme/employment.py | 65 + scheme/statistics.py | 36 + scheme/students.py | 67 + scheme/teachers.py | 52 + student_system/vue/.dockerignore | 12 + student_system/vue/Dockerfile | 37 + student_system/vue/docker-compose.yml | 41 + student_system/vue/index.html | 12 + student_system/vue/nginx.conf.template | 43 + student_system/vue/package-lock.json | 1821 +++++++++++++++++++ student_system/vue/package.json | 22 + student_system/vue/src/App.vue | 85 + student_system/vue/src/api/http.js | 45 + student_system/vue/src/api/index.js | 85 + student_system/vue/src/main.js | 13 + student_system/vue/src/router/index.js | 17 + student_system/vue/src/style.css | 50 + student_system/vue/src/views/Advisors.vue | 118 ++ student_system/vue/src/views/Classes.vue | 108 ++ student_system/vue/src/views/Dashboard.vue | 122 ++ student_system/vue/src/views/Employment.vue | 297 +++ student_system/vue/src/views/Scores.vue | 129 ++ student_system/vue/src/views/Statistics.vue | 298 +++ student_system/vue/src/views/Students.vue | 274 +++ student_system/vue/src/views/Teachers.vue | 125 ++ student_system/vue/vite.config.js | 29 + 63 files changed, 5723 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 __init__.py create mode 100644 api/Score.py create mode 100644 api/__init__.py create mode 100644 api/advisors.py create mode 100644 api/c_lass.py create mode 100644 api/employment_api.py create mode 100644 api/statistics.py create mode 100644 api/students.py create mode 100644 api/teachers.py create mode 100644 dao/Score.py create mode 100644 dao/__init__.py create mode 100644 dao/advisor/.___init__.py create mode 100644 dao/advisor/._crud.py create mode 100644 dao/advisor/._service.py create mode 100644 dao/advisor/__init__.py create mode 100644 dao/advisor/crud.py create mode 100644 dao/advisor/service.py create mode 100644 dao/c_lass.py create mode 100644 dao/employment.py create mode 100644 dao/statistics.py create mode 100644 dao/students_dao.py create mode 100644 dao/teachers_dao.py create mode 100644 database.py create mode 100644 main.py create mode 100644 model/Score.py create mode 100644 model/advisors.py create mode 100644 model/c_lass.py create mode 100644 model/employment.py create mode 100644 model/students.py create mode 100644 model/teachers.py create mode 100644 requirements.txt create mode 100644 scheme/Score.py create mode 100644 scheme/advisors.py create mode 100644 scheme/c_lass.py create mode 100644 scheme/employment.py create mode 100644 scheme/statistics.py create mode 100644 scheme/students.py create mode 100644 scheme/teachers.py create mode 100644 student_system/vue/.dockerignore create mode 100644 student_system/vue/Dockerfile create mode 100644 student_system/vue/docker-compose.yml create mode 100644 student_system/vue/index.html create mode 100644 student_system/vue/nginx.conf.template create mode 100644 student_system/vue/package-lock.json create mode 100644 student_system/vue/package.json create mode 100644 student_system/vue/src/App.vue create mode 100644 student_system/vue/src/api/http.js create mode 100644 student_system/vue/src/api/index.js create mode 100644 student_system/vue/src/main.js create mode 100644 student_system/vue/src/router/index.js create mode 100644 student_system/vue/src/style.css create mode 100644 student_system/vue/src/views/Advisors.vue create mode 100644 student_system/vue/src/views/Classes.vue create mode 100644 student_system/vue/src/views/Dashboard.vue create mode 100644 student_system/vue/src/views/Employment.vue create mode 100644 student_system/vue/src/views/Scores.vue create mode 100644 student_system/vue/src/views/Statistics.vue create mode 100644 student_system/vue/src/views/Students.vue create mode 100644 student_system/vue/src/views/Teachers.vue create mode 100644 student_system/vue/vite.config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b8f1d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +.git +.gitignore +.idea +.vscode +*.md +# 前端目录不需要进后端镜像(含巨大的 node_modules) +student_system/ +Dockerfile +docker-compose.yml +.dockerignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76b23f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +cat > .gitignore <<'EOF' +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd + +# 前端 +node_modules/ +dist/ + +# IDE +.idea/ +.vscode/ + +# 日志 +*.log +EOF diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6329e44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# 后端镜像:FastAPI + SQLAlchemy +FROM python:3.11-slim + +# PYTHONPATH=/app 让代码里的 `from sqlalchemy_fastapi_demo_1.xxx import ...` 能正常导入 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PYTHONPATH=/app + +WORKDIR /app + +# 先只装依赖:改业务代码时不会重装依赖,能复用缓存 +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 把整个项目复制到 /app/sqlalchemy_fastapi_demo_1(保留包名,导入才能对上) +COPY . /app/sqlalchemy_fastapi_demo_1/ + +EXPOSE 8004 + +# 注意模块路径要带包名;--host 必须是 0.0.0.0,否则容器外访问不到 +CMD ["uvicorn", "sqlalchemy_fastapi_demo_1.main:app", "--host", "0.0.0.0", "--port", "8004"] diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/Score.py b/api/Score.py new file mode 100644 index 0000000..0392dfe --- /dev/null +++ b/api/Score.py @@ -0,0 +1,132 @@ + +from fastapi import Query, Depends, HTTPException, APIRouter +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.dao.Score import ScoreDAO +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.scheme.Score import ScoreCreate, ScoreUpdate + +app_score=APIRouter() + +# 查询所有成绩 +@app_score.get("/",summary='查询所有成绩') +async def get_scores( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"), + db: Session = Depends(get_db) +): + scores=ScoreDAO.get_all(db, skip=skip, limit=limit) + return scores + +# 查询单个数据 +@app_score.get("/{stu_id}/{exam_id}",summary='查询单个成绩') +async def get_score( + stu_id: int, + exam_id: int, + db: Session = Depends(get_db) +): + score=ScoreDAO.get_by_id(db, stu_id,exam_id) + if not score: + raise HTTPException(status_code=404, detail="学生不存在") + return score + +# 创建学生 +@app_score.post("/",summary='创建学生成绩') +async def create_scores( + score:ScoreCreate, + db: Session = Depends(get_db) +): + # 检查学生在不在学生表 + existing_foreign_key=ScoreDAO.inspect_stu_id_unq(db, score.stu_id) + if not existing_foreign_key: + raise HTTPException(status_code=404, detail="该学生不存在") + if existing_foreign_key.is_deleted == 1: + raise HTTPException(status_code=404, detail="该学生不存在") + # 检查成绩是否已被占用 + existing_score=ScoreDAO.get_by_id(db, score.stu_id,score.exam_id) + if existing_score: + raise HTTPException(status_code=400, detail="学生成绩已存在,请勿重复添加") + new_score=ScoreDAO.post_score(db,score) + return new_score + +# 修改 +@app_score.put("/{stu_id}/{exam_id}",summary='修改信息') +async def update_scores( + stu_id: int, + exam_id: int, + score:ScoreUpdate, + db: Session = Depends(get_db) +): + # 只允许修改成绩,学生id和考试次序不允许修改,SQLAlchemy不能直接更新联合主键本身 + # 检查成绩是否已存在 + existing_foreign_key = ScoreDAO.inspect_stu_id_unq(db, score.stu_id) + if not existing_foreign_key: + raise HTTPException(status_code=404, detail="该学生不存在") + existing_score=ScoreDAO.get_by_id(db, stu_id,exam_id) + if not existing_score: + raise HTTPException(status_code=404, detail="学生成绩不存在") + # 直接调用DAO更新,只更新score字段 + b = ScoreDAO.put_score(db, stu_id, exam_id, score) + return b + +# 修改成绩:支持修改 stu_id、exam_id、score;原理:删除旧记录,新增新记录 + # stu_id、exam_id是联合主键。MySQL不允许update 直接修改主键字段,SQLAlchemy + # 底层执行会抛数据库异常。所以采用「删旧、建新」的方案模拟修改 + + # 风险大 + # 现在代码:删旧和新增是两次 + # db.commit()。 + # 如果删旧成功,新增中途崩掉,会出现旧数据没了,新数据没加上,数据丢失。 + + # 数据库这条记录实际上是新行,不是原来那一行。 + # 如果别的表有外键引用这条成绩记录,这个方案会出问题(旧记录删掉,关联跟着没了)。 +# @app_score.put("/{stu_id}/{exam_id}",summary='修改学生成绩(支持修改学生ID、考核序次、分数)') +# async def update_scores( +# stu_id: int, +# exam_id: int, +# score:ScoreUpdate, +# db: Session = Depends(get_db) +# ): +# # 1.找到旧记录 +# old_score = ScoreDAO.get_by_id(db, stu_id, exam_id) +# if not old_score: +# raise HTTPException(status_code=404, detail="待修改的成绩记录不存在") +# # 2.确定新值:前端没传的字段,沿用旧记录的值 +# new_stu = score.stu_id if score.stu_id is not None else old_score.stu_id +# new_exam_id = score.exam_id if score.exam_id is not None else old_score.exam_id +# new_score_val = score.score if score.score is not None else old_score.score +# # 3.校验新stu_id学生是否存在 +# if new_stu != old_score.stu_id: +# stu_obj = ScoreDAO.inspect_stu_id_unq(db, new_stu) +# if not stu_obj: +# raise HTTPException(status_code=404, detail="目标学生不存在") +# # 4.如果新主键组合和旧的不一样,要检查新组合是否冲突 +# if (new_stu, new_exam_id) != (old_score.stu_id, old_score.exam_id): +# conflict = ScoreDAO.get_by_id(db, new_stu, new_exam_id) +# if conflict: +# raise HTTPException(status_code=400, detail="新【学生ID+考核序次】组合已存在,不能使用") +# # 核心逻辑:删除旧记录,新增一条全新记录 +# # 删除旧数据 +# ScoreDAO.delete_score(db, stu_id, exam_id) +# # 组装成ScoreCreate对象,调用新增DAO +# from sqlalchemy_fastapi_demo_1.scheme.Score import ScoreCreate +# new_create_data = ScoreCreate( +# stu_id=new_stu, +# exam_id=new_exam_id, +# score=new_score_val +# ) +# new_db_score = ScoreDAO.post_score(db, new_create_data) +# return new_db_score + + + +# 删除 +@app_score.delete("/{stu_id}/{exam_id}",summary='删除学生成绩') +async def delete_scores( + stu_id: int, + exam_id: int, + db: Session = Depends(get_db) +): + score=ScoreDAO.delete_score(db, stu_id,exam_id) + if not score: + raise HTTPException(status_code=404, detail="学生成绩不存在") + return {"msg":"删除成功"} \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/advisors.py b/api/advisors.py new file mode 100644 index 0000000..91f3568 --- /dev/null +++ b/api/advisors.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter,Depends,HTTPException +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.scheme.advisors import AdvisorIn, AdvisorOut +from sqlalchemy_fastapi_demo_1.dao.advisor import service,crud + + +router = APIRouter() +@router.get("/read",response_model=AdvisorOut) +def read_advisor(advisor_id:int|None=None,advisor_name:str|None=None,db:Session=Depends(get_db)): + r_advisor=crud.search_advisor(db=db,advisor_id=advisor_id,advisor_name=advisor_name) + if r_advisor is not None: + return r_advisor + raise HTTPException(status_code=404,detail="顾问老师不存在") +@router.get("/list",response_model=list[AdvisorOut]) +def list_advisor(advisor_name:str|None=None,db:Session=Depends(get_db)): + return crud.list_advisors(db=db,advisor_name=advisor_name) + # r_advisor=crud.list_advisors(db=db,advisor_name=advisor_name,phone=phone) + # if r_advisor: + # return r_advisor + # raise HTTPException(status_code=404,detail="顾问老师不存在") +@router.post("/create",response_model=AdvisorOut) +def create_advisor(advisor_in:AdvisorIn,db:Session=Depends(get_db)): + c_advisor,error=service.create_advisor(db=db,advisor_id=advisor_in.advisor_id,advisor_name=advisor_in.advisor_name) + if error == "DUPLICATE": + raise HTTPException(status_code=409, detail="该顾问ID已被占用") + if error is not None: + # 兜底:Service 新增了返回码但这里没接住,不要静默当成成功 + raise HTTPException(status_code=500, detail="未处理的业务结果: " + error) + return c_advisor + + +@router.put("/update",response_model=AdvisorOut) +def update_advisor(advisor_in:AdvisorIn,db:Session=Depends(get_db)): + u_advisor,error=service.update_advisor(db=db,advisor_id=advisor_in.advisor_id,advisor_in=advisor_in) + if error == "NOT_FOUND": + raise HTTPException(status_code=404,detail="更新失败:顾问不存在") + if error is not None: + raise HTTPException(status_code=500, detail="未处理的业务结果: " + error) + return u_advisor +@router.delete("/delete/{advisor_id}") +def delete_advisor(advisor_id:int,db:Session=Depends(get_db)): + d_advisor,error=service.delete_advisor(db=db,advisor_id=advisor_id) + if error == "NOT_FOUND": + raise HTTPException(status_code=404,detail="顾问老师不存在") + if error == "HAS_STUDENTS": + raise HTTPException(status_code=409, detail="顾问老师下还有学生") + if error is not None: + raise HTTPException(status_code=500, detail="未处理的业务结果: " + error) + return {"message":"删除成功","delete_id":d_advisor} \ No newline at end of file diff --git a/api/c_lass.py b/api/c_lass.py new file mode 100644 index 0000000..2a56a06 --- /dev/null +++ b/api/c_lass.py @@ -0,0 +1,74 @@ +# # api/c_lass.py +# # 本文件定义班级的所有 API 路由(Controller 层) + +from fastapi import APIRouter, Depends, HTTPException, Query +# APIRouter=创建路由,Depends=依赖注入,HTTPException=错误响应,Query=查询参数校验 +from sqlalchemy.orm import Session # 会话类型 +from sqlalchemy_fastapi_demo_1.database import get_db # 导入会话生成器 +from sqlalchemy_fastapi_demo_1.dao.c_lass import create_c_lass, get_c_lass, get_all_c_lass, update_c_lass, delete_c_lass +# 把DAO层的5个函数都导入进来,接口里直接调用 +from sqlalchemy_fastapi_demo_1.scheme.c_lass import ClassCreate, ClassUpdate, ClassResponse + +# 路由对象,具体前缀(/c_lass)在 main.py 统一注册 +router = APIRouter()# 创建本模块的路由对象,main.py会把它注册进应用 + + +# ---------- 1. 新增班级 POST ---------- +# 完整地址:POST /c_lass/add +@router.post("/add", response_model=ClassResponse, summary="新增班级") +# 装饰器:注册一个POST接口,路径/add;response_model=返回时按ClassResponse格式化 +def add_c_lass(class_data: ClassCreate, db: Session = Depends(get_db)): + # class_data: ClassCreate = FastAPI自动校验请求体并转成对象;db = FastAPI自动注入会话 + # 先查重:如果这个班级ID已经存在(包括逻辑删除的),进入if + if get_c_lass(db, class_data.class_id): + raise HTTPException(status_code=400, detail="该班级ID已存在,不能重复添加") + return create_c_lass(db, class_data)# 通过查重,调DAO层真正插入,返回结果 + + +# ---------- 2. 根据id查询单个班级 GET ---------- +# 完整地址:GET /c_lass/{class_id} +@router.get("/{class_id}", response_model=ClassResponse, summary="根据id查询班级") +# GET接口,路径里带班级编号,如 GET /c_lass/101 +def get_class_by_id(class_id: int, db: Session = Depends(get_db)): # 调DAO查询 + db_class = get_c_lass(db, class_id) + if not db_class: + raise HTTPException(status_code=404, detail="该班级不存在") # 查不到就返回404,带提示信息 + return db_class + + +# ---------- 3. 分页查询所有班级 GET ---------- +# 完整地址:GET /c_lass/?skip=0&limit=10 +@router.get("/", response_model=list[ClassResponse], summary="分页查询班级列表") +# GET根路径,如 GET /c_lass/?skip=0&limit=10;response_model是列表 +def get_class_list( + skip: int = Query(0, ge=0, description="跳过的记录数"), # 查询参数skip,默认0,必须≥0 + limit: int = Query(10, ge=1, le=200, description="每页最大记录数"), # 查询参数limit,默认10,范围1~200 + db: Session = Depends(get_db), +): + return get_all_c_lass(db, skip, limit) # 调DAO分页查询 + + +# ---------- 4. 修改班级 PUT ---------- +# 完整地址:PUT /c_lass/{class_id} +@router.put("/{class_id}/", response_model=ClassResponse, summary="修改班级信息") +def update_class(class_id: int, update_data: ClassUpdate, db: Session = Depends(get_db)): +# def update_class(class_id: int, start_time: date, db: Session = Depends(get_db)): + + if not get_c_lass(db, class_id): # 先确认要改的班级存在(存在且没删) + raise HTTPException(status_code=404, detail="要修改的班级不存在") + return update_c_lass(db, class_id, update_data) # 调DAO执行局部更新 + + +# ---------- 5. 删除班级 DELETE ---------- +# 完整地址:DELETE /c_lass/{class_id} +@router.delete("/{class_id}", summary="删除班级") +def delete_class(class_id: int, db: Session = Depends(get_db)): + if not delete_c_lass(db, class_id): + raise HTTPException(status_code=404, detail="该班级不存在") + # 删除失败(查不到)返回404 + return {"message": "删除成功"} + + + + +#API 层只做三件事—— 接请求、校验 / 查重、调 DAO,自己不写 SQL \ No newline at end of file diff --git a/api/employment_api.py b/api/employment_api.py new file mode 100644 index 0000000..f27fc6d --- /dev/null +++ b/api/employment_api.py @@ -0,0 +1,116 @@ +# api/employment_api.py +# 本文件定义学生就业信息相关的所有 API 路由(Controller 层) +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional + +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.dao.employment import EmploymentBaseDAO,EmploymentOfferDAO +from sqlalchemy_fastapi_demo_1.model.employment import EmploymentBase, EmploymentOffer +from sqlalchemy_fastapi_demo_1.scheme.employment import EmploymentOfferUpdate, EmploymentBaseUpdate, EmploymentBaseCreate,EmploymentOfferCreate,EmploymentQuery,EmploymentOfferQueryResponse,EmploymentBaseQueryResponse +router = APIRouter() + +#-----------多条件查询就业协议信息----------- +@router.get("/offer/search/",response_model=List[EmploymentOfferQueryResponse],summary="多条件查询就业协议信息") +def search_offer( + stu_id: Optional[int] = Query(None, description="学生学号"), + offer_id: Optional[int] = Query(None, description="offer编号,需同时提供学生编号"), + skip: int = Query(0, ge=0, description="跳过的记录条数"), + limit: int = Query(10, ge=1, le=200, description="返回最大记录数"), + db: Session = Depends(get_db) +): + #捕获异常 + try: + data_list = EmploymentOfferDAO.different_choice_query( + db=db, stu_id=stu_id, offer_id=offer_id, skip=skip, limit=limit + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + return data_list +#-----------多条件查询就业基础信息----------- +@router.get("/base/search/",response_model=List[EmploymentBaseQueryResponse],summary="多条件查询就业基础信息") +def search_base( + query: EmploymentQuery = Depends(), + skip: int = Query(0, ge=0, description="跳过的记录条数"), + limit: int = Query(10, ge=1, le=200, description="返回最大记录数"), + db: Session = Depends(get_db) +): + # 多条件筛选就业基础信息 + 分页 + if query.min_salary is not None and query.max_salary is not None and query.min_salary > query.max_salary: + raise HTTPException(status_code=400, detail="最低薪资不能大于最高薪资") + + data_list = EmploymentBaseDAO.different_choice_query( + db=db, + stu_id=query.stu_id, + company_name=query.company_name, + min_salary=query.min_salary, + max_salary=query.max_salary, + skip=skip, + limit=limit + ) + return data_list +#-----------添加就业基础信息----------- +@router.post("/base/add/",response_model=EmploymentBaseQueryResponse,summary="添加就业基础信息") +def add_base(obj: EmploymentBaseCreate, db: Session = Depends(get_db)): + items = EmploymentBaseDAO.get_by_stu_id(db, obj.stu_id) + if items: + raise HTTPException(status_code=400, detail="该学生的就业基础信息已存在,不可重复新增") + + new_obj = EmploymentBase( + stu_id=obj.stu_id, + employment_open_time=obj.employment_open_time, + job_time=obj.job_time, + company_name=obj.company_name, + salary=obj.salary, + is_deleted=0 + ) + res = EmploymentBaseDAO.create(db, new_obj) + return res +#-----------添加就业协议信息----------- +@router.post("/offer/add/",response_model=EmploymentOfferQueryResponse,summary="添加就业协议信息") +def add_offer(obj: EmploymentOfferCreate, db: Session = Depends(get_db)): + new_obj = EmploymentOffer( + stu_id=obj.stu_id, + offer_id=obj.offer_id, + offer_time=obj.offer_time, + is_deleted=0 + ) + res = EmploymentOfferDAO.create(db, new_obj) + return res +#-----------修改就业基础信息----------- +@router.put("/base/{stu_id}/",response_model=EmploymentBaseQueryResponse,summary="修改就业基础信息") +def update_base(stu_id: int, obj: EmploymentBaseUpdate, db: Session = Depends(get_db)): + update_data = obj.model_dump(exclude_unset=True) + try: + res = EmploymentBaseDAO.update(db, stu_id, update_data) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + if not res: + raise HTTPException(status_code=404, detail="未找到该学生就业基础信息") + return res +#-----------修改就业协议信息----------- +@router.put("/offer/{offer_id}", response_model=EmploymentOfferQueryResponse,summary="修改就业协议信息") +def update_offer(stu_id:int,offer_id: int, obj: EmploymentOfferUpdate, db: Session = Depends(get_db)): + # offer 表主键是 (stu_id, offer_id),定位记录需要两者 + try: + res = EmploymentOfferDAO.update(db, stu_id, offer_id, obj.offer_time) + except ValueError as e: + # 捕获校验异常,返回标准400错误给前端 + raise HTTPException(status_code=400, detail=str(e)) + if not res: + raise HTTPException(status_code=404, detail="未找到该就业协议记录") + return res +#-----------逻辑删除就业基础信息----------- +@router.delete("/base/{stu_id}/",summary="删除就业基础信息") +def delete_base(stu_id: int, db: Session = Depends(get_db)): + ok = EmploymentBaseDAO.delete(db, stu_id) + if not ok: + raise HTTPException(status_code=404, detail="记录不存在或已经删除") + return {"code": 200, "msg": "删除成功"} +#-----------逻辑删除就业协议信息----------- +@router.delete("/offer/{stu_id}/{offer_id}",summary="修改就业协议信息") +def delete_offer(stu_id: int, offer_id: int, db: Session = Depends(get_db)): + ok = EmploymentOfferDAO.delete(db, stu_id, offer_id) + if not ok: + raise HTTPException(status_code=404, detail="记录不存在或已经删除") + return {"code": 200, "msg": "删除成功"} diff --git a/api/statistics.py b/api/statistics.py new file mode 100644 index 0000000..5d2e06f --- /dev/null +++ b/api/statistics.py @@ -0,0 +1,105 @@ +from fastapi import APIRouter,Depends,HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import func +from sqlalchemy_fastapi_demo_1.scheme.statistics import StudentAge, ClassCount, ScoreCount, ScoreAvg, Employment, EmploymentOff +from sqlalchemy_fastapi_demo_1.database import get_db +router = APIRouter() +from sqlalchemy_fastapi_demo_1.model.students import Student +from sqlalchemy_fastapi_demo_1.model.Score import Score +from sqlalchemy_fastapi_demo_1.model.employment import EmploymentBase,EmploymentOffer +from sqlalchemy_fastapi_demo_1.model.c_lass import Classinfo +from fastapi import FastAPI, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.dao.statistics import (students_age, class_statistics,score_above, score_fail, +avg_scores,top_salary,stu_every,class_avg) +# app = FastAPI(title="统计接口") + +# -动态年龄范围查询**:支持用户输入年龄阈值及比较条件(如大于、小于、等于、区间等),动态查询符合条件的学员信息。 +@router.get("/students/age") +def api_students_age(op: str = Query(..., description=">, <, =, >=, <=, between"), + age_value: int = Query(None),age_min: int = Query(None), + age_max: int = Query(None),db: Session = Depends(get_db)): + try: + rows = students_age(db, op, age_value, age_min, age_max) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + return [{"stu_id": s.stu_id, + "stu_name": s.stu_name, + "age": s.age, + "gender": s.gender, + "class_id": s.class_id,} for s in rows] + +# 多维度班级统计**:统计每个班级的总人数,以及按性别(男、女)细分的人数分布。 +@router.get("/classes/statistics") +def api_class_statistics(db: Session = Depends(get_db)): + return class_statistics(db) + + +# - 查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。 +@router.get("/scores/above") +def api_score_above( + score_input: int = Query(..., ge=0, le=100), + db: Session = Depends(get_db)): + rows = score_above(db, score_input) + return [{"stu_id": i[0], "stu_name": i[1], "exam_order": i[2], "score": i[3]} for i in rows] + +# - 查询有输入指定次数(如两次)以上不及格的学生的姓名、班级和不及格成绩明细。 +@router.get("/scores/fail") +def api_score_fail( + fail_times: int = Query(..., ge=1), + fail_score: int = Query(60, ge=0, le=100), + db: Session = Depends(get_db)): + rows = score_fail(db, fail_times, fail_score) + return [{"stu_name": i[0], "class_id": i[1], "exam_order": i[2], "score": i[3]} for i in rows] + +# 统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序 +@router.get("/scores/avg") +def api_avg_scores(order: str = Query("desc", pattern="^(asc|desc)$"), + db: Session = Depends(get_db)): + a = avg_scores(db, order) + return [{"exam_id": i.exam_id, + "class_id": i.class_id, + "avg_score": round(float(i.avg_score), 2)} for i in a] + +#就业统计 +# - 统计就业薪资排名 Top N(动态输入 N)的学生的姓名、班级、就业时间和就业公司。 +@router.get("/employment/top") +def api_top_salary(n: int = Query(5, ge=1), db: Session = Depends(get_db)): + return top_salary(db, n) + +# - 统计每个学生的就业时长(计算公式:offer下发时间 - 就业开放时间)。 +@router.get("/employment/every") +def api_stu_every(db: Session = Depends(get_db)): + return stu_every(db) + +# 每个班级的平均就业时长 +@router.get("/employment/class_avg") +def api_class_duration(db: Session = Depends(get_db)): + return class_avg(db) + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/students.py b/api/students.py new file mode 100644 index 0000000..8625698 --- /dev/null +++ b/api/students.py @@ -0,0 +1,97 @@ +# api/users.py +# 本文件定义学生相关的所有 API 路由(Controller 层) + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List,Optional + +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.dao.students_dao import StudentDAO +from sqlalchemy_fastapi_demo_1.scheme.students import StudentCreate, StudentUpdate, StudentResponse + +router = APIRouter(prefix="/students",tags=["学生管理接口"]) + +# 增加学生信息接口 +@router.post("/",response_model=StudentResponse) +def create_student_api( + student_in: StudentCreate, + db: Session = Depends(get_db) +): + student = StudentDAO.create_student(db=db, obj_in=student_in) + return student + + +# 查询所有学生接口 +@router.get("/all", response_model=List[StudentResponse]) +async def get_all_api( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"), + db: Session = Depends(get_db) # 依赖注入获得数据库会话 +): + students = StudentDAO.get_students_all(db, skip=skip, limit=limit) + return students # FastAPI 自动根据 response_model 转换为 JSON + +# 多条件查询接口 +@router.get("/search",response_model=List[StudentResponse]) +async def get_multi_condition_api( + stu_id: Optional[int] = Query(None, description="学生编号"), + stu_name: Optional[str] = Query(None, description="学生姓名(支持模糊查询)"), + class_id: Optional[int] = Query(None, description="班级编号"), + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db) +): + student = StudentDAO.query_multi_condition( + db = db, + stu_id = stu_id, + stu_name = stu_name, + class_id = class_id, + skip = skip, + limit = limit ) + return student + +# 根据id查询单个学生接口 +@router.get("/{stu_id}", response_model=StudentResponse) +async def get_student_by_id_api( + stu_id: int, + db: Session = Depends(get_db) +): + student = StudentDAO.get_student_by_id(db=db, stu_id=stu_id) + if not student: + raise HTTPException(status_code=404, detail="用户不存在") + return student + +# 更改学生信息接口 +@router.put("/{stu_id}",response_model=StudentResponse) +async def update_student_api( + stu_id: int, + stu_update: StudentUpdate, + db: Session = Depends(get_db) +): + """更新学生信息,支持部分字段更新""" + # 检查学生是否存在 + existing = StudentDAO.get_student_by_id(db, stu_id) + if not existing: + raise HTTPException(status_code=404, detail="学生不存在") + # 校验外键(班级和顾问是否存在) + if stu_update.advisor_id is not None: + advisor_exists = StudentDAO.inspect_advisor_id_unq(db, stu_update.advisor_id) + if not advisor_exists: + raise HTTPException(status_code=400, detail="该顾问不存在") + if stu_update.class_id is not None: + class_exists = StudentDAO.inspect_class_id_unq(db, stu_update.class_id) + if not class_exists: + raise HTTPException(status_code=400, detail="该班级不存在") + # 直接调用 DAO 更新,学号不可改等逻辑已在 DAO 层处理 + student = StudentDAO.update_student(db = db , stu_id = stu_id , obj = stu_update) + return student + +# 逻辑删除接口:不返回完整学生实体,返回简单提示 +@router.delete("/{stu_id}",status_code=204) +async def delete_student_api( + stu_id: int, + db: Session = Depends(get_db) +): + StudentDAO.delete_student(db, stu_id) + # 返回 None 表示 204 状态码(无内容) + return None diff --git a/api/teachers.py b/api/teachers.py new file mode 100644 index 0000000..a276eca --- /dev/null +++ b/api/teachers.py @@ -0,0 +1,80 @@ +from typing import List +from fastapi import APIRouter, Depends,Query +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.scheme.teachers import TeacherAdd, TeacherResponse,TeacherUpdate +from sqlalchemy_fastapi_demo_1.database import get_db +from sqlalchemy_fastapi_demo_1.dao.teachers_dao import TeacherDAO +from fastapi import HTTPException +router=APIRouter() +# ---------- 创建新教师 ---------- +@router.post("/teacher/add",response_model=TeacherResponse) +async def add_teacher(teacher:TeacherAdd,db:Session=Depends(get_db)): + existing_foreign_key=TeacherDAO.inspect_class_id_unq(db,teacher.class_id) + if existing_foreign_key:# 外键校验 + existing_teacher_id=TeacherDAO.inspect_teacher_id_unq(db,teacher.teacher_id) + if existing_teacher_id:# 主键校验 + raise HTTPException(status_code=409, detail="主键教师ID已被占用,包括软删除") # 资源冲突 + return TeacherDAO.add_teacher(db,teacher) + else: + raise HTTPException(status_code=404, detail="班级ID不存在,数据库无数据,或已被软删除") # 关联资源不存在 +# ---------- 软删除教师 ---------- +@router.delete("/teacher/delete", status_code=204) +async def light_delete_teacher(teacher_id:int=Query(...,ge=1,description='输入想要删除的教师ID'), + db:Session=Depends(get_db)): + """ + 删除用户,成功返回 204 No Content + """ + success = TeacherDAO.delete_light(db, teacher_id) + if not success: # 目标删除教师ID不存在(被硬删除或软删除) + raise HTTPException(status_code=404, detail="教师ID不存在") # 关联资源不存在 + # 返回 None 表示目标教师ID已删除 + return None +# ---------- 更新教师信息 ---------- +@router.put("/teacher/update",response_model=TeacherResponse) +async def update_teacher( + teacher_data: TeacherUpdate, + teacher_id:int=Query(...,ge=1,description='输入教师想要更新的教师ID'), + db: Session = Depends(get_db) +): + """ + 更新用户信息(只更新传入的字段,也就是非None) + """ + # 检查教师ID是否存在 + existing_teacher_id = TeacherDAO.inspect_teacher_id_unq(db,teacher_id) + if (not existing_teacher_id) or (existing_teacher_id.is_deleted==1): + raise HTTPException(status_code=404, detail="教师ID不存在,已被硬删除或软删除") # 关联资源不存在 + # 更新班级ID + if teacher_data.class_id is not None: + # 校验外键,校验班级ID是否在Class表存在 + existing_foreign_key=TeacherDAO.inspect_class_id_unq(db,teacher_data.class_id) + if not existing_foreign_key: + raise HTTPException(status_code=404, detail="班级ID不存在,已被硬删除或软删除") # 关联资源不存在 + updated = TeacherDAO.update(db, teacher_id, teacher_data) + return updated + updated = TeacherDAO.update(db, teacher_id, teacher_data) + return updated +# ---------- 查询所有教师(分页) ---------- +@router.get("/teacher/total_query",response_model=List[TeacherResponse]) +async def get_teachers( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"), + db: Session = Depends(get_db) # 依赖注入获得数据库会话 +): + """ + 获取用户列表,支持分页,从skip+1条开始返回 + """ + users = TeacherDAO.get_all(db, skip=skip, limit=limit) + return users # FastAPI 自动根据 response_model 转换为 JSON +# ---------- 根据 ID 查询单个教师 ---------- +@router.get("/teacher/single_query", response_model=TeacherResponse) +async def get_teacher( + teacher_id: int, + db: Session = Depends(get_db) +): + """ + 根据用户 ID 获取详细信息,不显示软删除 + """ + teacher = TeacherDAO.inspect_teacher_id_unq(db, teacher_id) + if (not teacher) or teacher.is_deleted==1: + raise HTTPException(status_code=404, detail="教师ID不存在") # 关联资源不存在 + return teacher \ No newline at end of file diff --git a/dao/Score.py b/dao/Score.py new file mode 100644 index 0000000..bfdf9c5 --- /dev/null +++ b/dao/Score.py @@ -0,0 +1,56 @@ +from sqlalchemy_fastapi_demo_1.model.Score import Score +from sqlalchemy_fastapi_demo_1.model.students import Student +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.scheme.Score import ScoreCreate,ScoreUpdate + +class ScoreDAO: + """用户数据访问对象,所有方法均为静态方法,方便调用""" + + @staticmethod + def inspect_stu_id_unq(db:Session,stu_id:int): + return db.query(Student).filter(Student.stu_id == stu_id,Student.is_deleted==0).first() + + + # 查所有数据 + @staticmethod + def get_all(db:Session,skip:int=0,limit:int=100): + return db.query(Score).offset(skip).limit(limit).all() + + # 查询单个数据 + @staticmethod + def get_by_id(db:Session,stu_id:int,exam_id:int): + return db.query(Score).filter(Score.stu_id == stu_id,Score.exam_id==exam_id).first() + + # 添加数据 + @staticmethod + def post_score(db:Session,score_create:ScoreCreate): + # 将 Pydantic 模型转为字典,并解包构建 SQLAlchemy 模型实例 + db_score=Score(**score_create.model_dump()) + db.add(db_score) #添加到会话 + db.commit() #提交事务,此时会执行 INSERT,并自动填充自增字段 + db.refresh(db_score) #刷新对象,获取数据库生成的默认值(如 created_at) + return db_score + + # 更改数据 + @staticmethod + def put_score(db:Session,score_id:int,exam_id:int,score_update:ScoreUpdate): + db_score=ScoreDAO.get_by_id(db,score_id,exam_id) + if not db_score: + return None + # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段) + put_score_data = score_update.model_dump(exclude_unset=True) + for k,v in put_score_data.items(): + setattr(db_score,k,v) # 动态设置属性 + db.commit() + db.refresh(db_score) + return db_score + + # 删除 + @staticmethod + def delete_score(db:Session,score_id:int,exam_id:int): + db_score=ScoreDAO.get_by_id(db,score_id,exam_id) + if not db_score: + return False + db.delete(db_score) + db.commit() + return True diff --git a/dao/__init__.py b/dao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dao/advisor/.___init__.py b/dao/advisor/.___init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c30a4b13758dfd72932c8a28f59dcf011336bca0 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zVAuh~%4k|Zrh~*K7#Kv8^K1M!>?9%-R+#R^5K#rdU0$*Bry`9;|ZnRz9t pz;0)LUSdv8r9w_>T8TnZPGVj*urCbNHKZw2|HHi^!yxznKLEpyIP3rb literal 0 HcmV?d00001 diff --git a/dao/advisor/._crud.py b/dao/advisor/._crud.py new file mode 100644 index 0000000000000000000000000000000000000000..b56f038ba11d6a269eb20979c98eae594b354723 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$t?npgeUESkIrP3tIkGz3ON zU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nu!jJsvkAgrAQzI6S*%c$TAW{6l$@%N ymS2>ukeOGKnpcvUpO=`EQ>l=XnpUEal#`g34eSd;bq#3>)&Fp>$S}zL{|^8>`za#; literal 0 HcmV?d00001 diff --git a/dao/advisor/._service.py b/dao/advisor/._service.py new file mode 100644 index 0000000000000000000000000000000000000000..b56f038ba11d6a269eb20979c98eae594b354723 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$t?npgeUESkIrP3tIkGz3ON zU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nu!jJsvkAgrAQzI6S*%c$TAW{6l$@%N ymS2>ukeOGKnpcvUpO=`EQ>l=XnpUEal#`g34eSd;bq#3>)&Fp>$S}zL{|^8>`za#; literal 0 HcmV?d00001 diff --git a/dao/advisor/__init__.py b/dao/advisor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dao/advisor/crud.py b/dao/advisor/crud.py new file mode 100644 index 0000000..690ab87 --- /dev/null +++ b/dao/advisor/crud.py @@ -0,0 +1,56 @@ +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.model.advisors import Advisor +from sqlalchemy_fastapi_demo_1.model.students import Student + +def create_advisor(db:Session,advisor_id:int,advisor_name:str): + advisor = Advisor(advisor_id=advisor_id,advisor_name=advisor_name) + db.add(advisor) + db.commit() + db.refresh(advisor) + return advisor + + +def search_advisor(db:Session,advisor_id:int|None=None,advisor_name:str|None=None): + if advisor_id is None and advisor_name is None: + return None + query=db.query(Advisor) + if advisor_id is not None: + query = query.filter(Advisor.advisor_id == advisor_id) + if advisor_name is not None: + query = query.filter(Advisor.advisor_name==advisor_name) + advisor = query.first() + return advisor + + +def list_advisors(db: Session,advisor_name: str|None =None): + query = db.query(Advisor) + if advisor_name is not None: + query = query.filter(Advisor.advisor_name.like(f"%{advisor_name}%")) + return query.all() + + +def list_students_of_advisor(db: Session, advisor_id: int): + """查这个顾问名下还没被逻辑删除的学生""" + return db.query(Student).filter( + Student.is_deleted == 0, + Student.advisor_id == advisor_id, + ).all() + + +def update_advisor(db:Session,advisor_id:int,advisor_name:str|None=None): + r_advisor = search_advisor(db=db,advisor_id=advisor_id) + if r_advisor is None: + return None + if advisor_name is not None: + r_advisor.advisor_name = advisor_name + db.commit() + return r_advisor + + +def delete_advisor(db:Session,advisor_id:int): + d_advisor = search_advisor(db=db,advisor_id=advisor_id) + if d_advisor is None: + return None + db.delete(d_advisor) + db.commit() + return advisor_id diff --git a/dao/advisor/service.py b/dao/advisor/service.py new file mode 100644 index 0000000..c62f63c --- /dev/null +++ b/dao/advisor/service.py @@ -0,0 +1,41 @@ +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from sqlalchemy_fastapi_demo_1.dao.advisor import crud +from sqlalchemy_fastapi_demo_1.scheme.advisors import AdvisorIn + +# 成功 (数据, None);失败 (None, "错误码")。 + +def create_advisor(db:Session,advisor_id:int,advisor_name:str): + exist=crud.search_advisor(db,advisor_id=advisor_id) + if exist is not None: + return None,"DUPLICATE" #ID主键已被占用 + try: + advisor = crud.create_advisor(db,advisor_id,advisor_name) + except IntegrityError: + db.rollback() + return None, "DUPLICATE" + return advisor, None + + +def update_advisor(db:Session,advisor_id:int,advisor_in:AdvisorIn): + exist = crud.search_advisor(db,advisor_id=advisor_id) + if exist is None: + return None, "NOT_FOUND" + u_advisor=crud.update_advisor(db,advisor_id=advisor_id,advisor_name=advisor_in.advisor_name) + if u_advisor is None: + return None, "NOT_FOUND" # 查过之后被并发删掉了 + return u_advisor,None + + +#删除成功返回Advisor ID,删除失败返回None。顾问如连接学生,则拒绝删除。 +def delete_advisor(db: Session, advisor_id: int): + exist = crud.search_advisor(db=db, advisor_id=advisor_id) + if exist is None: + return None, "NOT_FOUND" + # 查一下这个顾问名下有没有学生 + if crud.list_students_of_advisor(db, advisor_id): + return None, "HAS_STUDENTS" + result = crud.delete_advisor(db=db, advisor_id=advisor_id) + if result is None: + return None, "NOT_FOUND" # 查过之后被并发删掉了 + return result, None diff --git a/dao/c_lass.py b/dao/c_lass.py new file mode 100644 index 0000000..72aad0c --- /dev/null +++ b/dao/c_lass.py @@ -0,0 +1,68 @@ +# # dao/c_lass.py 数据访问层,直接操作数据库 +# # 本文件封装对 Classinfo表的所有数据库操作(增、删、改、查) + +from sqlalchemy.orm import Session # 会话类型,标注db参数 +from sqlalchemy_fastapi_demo_1.model.c_lass import Classinfo # # 导入ORM模型 +from sqlalchemy_fastapi_demo_1.scheme.c_lass import ClassCreate, ClassUpdate # 导入入参模型 +from typing import Optional, List # 类型注解用:Optional=可空,List=列表 + + + +# 新增班级 +def create_c_lass(db: Session, obj: ClassCreate):# 新增函数:接收会话db和校验后的数据obj,返回Classinfo对象 + db_obj = Classinfo(class_id=obj.class_id, start_time=obj.start_time) + # 把前端传来的数据组装成一个ORM对象(相当于内存里的一条新记录) + db.add(db_obj) # 把对象加入会话 + db.commit() # 提交事务:真正把INSERT语句发给MySQL执行,写进数据库 + db.refresh(db_obj) # 从数据库重新查一遍这条记录,把数据库自动生成的值(如默认值)刷新回对象 + return db_obj # 返回这个对象,方便API层序列化成JSON返回前端 + + +# 根据 id 查询单个班级:只查询 is_deleted=0 未删除 +def get_c_lass(db: Session, class_id: int) -> Optional[Classinfo]: + # 按ID查单个班级;Optional = 可能查不到(返回None) + return db.query(Classinfo).filter( + Classinfo.class_id == class_id,# 条件1:班级编号等于传入的id + Classinfo.is_deleted == 0 # 条件2:没被逻辑删除(只查0) + ).first() +# .first() = 取第一条;查不到就返回None,不会报错 + +# 分页查询所有班级:过滤已经逻辑删除的数据 +def get_all_c_lass(db: Session, skip: int = 0, limit: int = 100) -> List[Classinfo]: + # 分页查所有班级:skip=跳过几条,limit=最多取几条 + return db.query(Classinfo)\ + .filter(Classinfo.is_deleted == 0)\ + .offset(skip).limit(limit).all() # # 只查没删的; 跳过skip条,取limit条,返回列表 + + +# 修改班级(局部更新:传了哪个字段就改哪个) +def update_c_lass(db: Session, class_id: int, update_obj: ClassUpdate): + # 修改函数:按id找到班级,把传进来的字段改掉 + db_class = get_c_lass(db, class_id) + + # 只取前端真正传了的字段 + update_data = update_obj.model_dump(exclude_unset=True) + # 把前端传的修改数据转成字典;exclude_unset=True = 只保留前端真正传了的字段 + # (没传的字段不出现,就不会被误改成None——就是"局部更新") + for key, value in update_data.items():# 循环:把字典里每个字段的值,赋给ORM对象对应属性(内存里改好) + setattr(db_class, key, value) + db.commit() # 提交:把改动写进数据库 + db.refresh(db_class) # 从数据库重新查询这条记录,把数据库里的最新值刷新到 Python 对象上,让内存里的对象和数据库保持一致。 + return db_class # 返回改好的对象 + + +# 逻辑删除:不再db.delete,设置is_deleted=1 +def delete_c_lass(db: Session, class_id: int) -> bool: + # 删除函数:不是物理删除(不删数据库行),是打标记 + db_class = get_c_lass(db, class_id) # 先查这条班级 + if not db_class: + return False # 查不到返回False,API层返回404 + # 打删除标记 + db_class.is_deleted = 1 # 把逻辑删除标记改成1(=已删除 + db.commit() + return True # 返回True表示删除成功 + + +#DAO 层是所有 SQL 的家,API 层不写任何 SQL,只调这里的函数。 +# 查询统一过滤 `is_deleted == 0`,删了的数据还在数据库里,只是查不到 —— 这就是逻辑删除。 + diff --git a/dao/employment.py b/dao/employment.py new file mode 100644 index 0000000..b981043 --- /dev/null +++ b/dao/employment.py @@ -0,0 +1,175 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import and_, Date +from sqlalchemy_fastapi_demo_1.model.employment import EmploymentBase,EmploymentOffer +from sqlalchemy_fastapi_demo_1.model.students import Student +# ========== 就业基础信息 DAO ========== +class EmploymentBaseDAO: + @staticmethod + #根据学生编号查询 + def get_by_stu_id(db: Session, stu_id: int): + record = db.query(EmploymentBase).filter(and_( + EmploymentBase.stu_id == stu_id, + EmploymentBase.is_deleted == 0 + )).first() + return record + @staticmethod + #多条件查询 + def different_choice_query( + db: Session, + stu_id: int = None, + company_name: str = None, + min_salary: float = None, + max_salary: float = None, + skip: int = 0, + limit: int = 10 + ): + # 基础查询 + 过滤未删除 + query = db.query(EmploymentBase).filter(EmploymentBase.is_deleted == 0) + + # 动态拼接条件 + #学生id查询 + if stu_id is not None: + query = query.filter(EmploymentBase.stu_id == stu_id) + #公司名称查询 + if company_name is not None and company_name.strip() != "": + # 模糊匹配,%代表通配符 + query = query.filter(EmploymentBase.company_name.like(f"%{company_name}%")) + #工资范围查询 + if min_salary is not None: + query = query.filter(EmploymentBase.salary >= min_salary) + if max_salary is not None: + query = query.filter(EmploymentBase.salary <= max_salary) + + # 分页 + result = query.offset(skip).limit(limit).all() + return result + + @staticmethod + #新增学生就业基础信息 + def create(db: Session, obj: EmploymentBase): + + student = db.query(Student).filter( + Student.stu_id == obj.stu_id, + Student.is_deleted == 0 + ).first() + if not student: + raise HTTPException(status_code=400, detail="学生不存在或已逻辑删除,不能添加就业信息") + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + @staticmethod + #更新就业基础信息 + def update(db: Session, stu_id: int, update_data: dict): + record = EmploymentBaseDAO.get_by_stu_id(db, stu_id) + if not record: + return None + + if "company_name" in update_data: + record.company_name = update_data["company_name"] + if "salary" in update_data: + record.salary = update_data["salary"] + if "employment_open_time" in update_data: + record.employment_open_time = update_data["employment_open_time"] + if "job_time" in update_data: + record.job_time = update_data["job_time"] + # =========== 统一时间校验 =========== + final_emp_open = record.employment_open_time + final_job = record.job_time + # 两个时间都不为空才校验 + if final_emp_open and final_job: + if final_job < final_emp_open: + raise ValueError("job时间不能早于就业开放时间") + + db.commit() + db.refresh(record) + return record + + @staticmethod + #根据学生编号对就业基础表进行逻辑删除 + def delete(db: Session, stu_id: int): + record = EmploymentBaseDAO.get_by_stu_id(db, stu_id) + if not record: + return False + record.is_deleted = 1 + db.commit() + return True + +# ========== 就业Offer协议 DAO ========== +class EmploymentOfferDAO: + + @staticmethod + #添加就业协议记录 + def create(db: Session, obj_1: EmploymentOffer,obj_2:EmploymentBase): + # 时间校验 + if obj_1.offer_time < obj_2.employment_open_time: + raise ValueError("offer时间不能早于就业开放时间") + db.add(obj_1) + db.commit() + db.refresh(obj_1) + return obj_1 + + @staticmethod + #修改就业协议表 + def update(db: Session, stu_id: int, offer_id: int, offer_time:Date): + """修改offer""" + record_list = EmploymentOfferDAO.different_choice_query(db, stu_id, offer_id) + if not record_list: + return None + record = record_list[0] + #查询就业基础信息,拿到就业开放时间准备做时间校验 + base_record = db.query(EmploymentBase).filter( + EmploymentBase.stu_id == stu_id, + EmploymentBase.is_deleted == 0 + ).first() + #避免就业基础信息被逻辑删除了而就业基础协议记录还存在 + if not base_record: + raise ValueError("未找到该学生就业基础信息") + #时间校验 + if offer_time is not None: + if offer_time < base_record.employment_open_time: + raise ValueError("offer时间不能早于就业开放时间") + record.offer_time = offer_time + + db.commit() + db.refresh(record) + return record + + @staticmethod + #根据协议编号对就业协议表进行逻辑删除 + def delete(db: Session, stu_id: int,offer_id: int): + """offer逻辑删除""" + record_list = EmploymentOfferDAO.different_choice_query(db, stu_id,offer_id) + if not record_list: + return False + record = record_list[0] + record.is_deleted = 1 + db.commit() + return True + + @staticmethod + #多条件查询就业协议表 + def different_choice_query( + db: Session, + stu_id: int =None , + offer_id: int = None, + skip: int = 0, + limit: int = 10 + ): + #基础查询+过滤未删除 + query = db.query(EmploymentOffer).filter(EmploymentOffer.is_deleted == 0) + + #动态拼接条件 + #学生id查询(选填) + if stu_id is not None: + query = query.filter(EmploymentOffer.stu_id == stu_id) + # 同时传了offer编号 → 查单个(联合主键两个都查) + if offer_id is not None: + query = query.filter(EmploymentOffer.offer_id == offer_id) + elif stu_id is None and offer_id is not None: + raise ValueError("不能单独查询offer编号,必须同时提供学生编号") + # 分页 + result = query.offset(skip).limit(limit).all() + return result diff --git a/dao/statistics.py b/dao/statistics.py new file mode 100644 index 0000000..8eaaea9 --- /dev/null +++ b/dao/statistics.py @@ -0,0 +1,110 @@ +from sqlalchemy import func, case +from sqlalchemy_fastapi_demo_1.model.students import Student +from sqlalchemy_fastapi_demo_1.model.Score import Score +from sqlalchemy_fastapi_demo_1.model.employment import EmploymentBase,EmploymentOffer +from sqlalchemy_fastapi_demo_1.model.c_lass import Classinfo + +def students_age(db,op:str, + age_value: int = None, + age_min: int = None, + age_max: int = None): + + a=db.query(Student).filter(Student.is_deleted == 0) + if op == ">": + a=a.filter(Student.age > age_value) + elif op == "<": + a=a.filter(Student.age < age_value) + elif op == "=": + a=a.filter(Student.age == age_value) + elif op == ">=": + a=a.filter(Student.age >= age_value) + elif op == "<=": + a=a.filter(Student.age <= age_value) + elif op == "between": + if age_min is None or age_max is None: + raise ValueError("区间查询必须提供 age_min和age_max") + a=a.filter(Student.age.between(age_min, age_max)) + else: + raise ValueError("请使用 >, <, =, >=, <=, between") + return a.all() +# 多维度班级统计**:统计每个班级的总人数,以及按性别(男、女)细分的人数分布。 +def class_statistics(db): + a = (db.query(Student.class_id,func.count(Student.stu_id).label("total"), + func.sum(case((Student.gender == "男", 1), else_=0)).label("male"), + func.sum(case((Student.gender == "女", 1), else_=0) + ).label("female")) + .filter(Student.is_deleted==0).group_by(Student.class_id).all()) + list1 = [] + for i in a: + list1.append({"class_id": i.class_id,"total": i.total,"male": i.male,"female": i.female}) + return list1 + +# - 查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。 +def score_above(db,score_input): + result = [] + for stu in db.query(Student).filter(Student.is_deleted == 0).all(): + if stu.score and all(s.score >= score_input for s in stu.score): + for s in stu.score: + result.append((stu.stu_id, stu.stu_name, s.exam_id, s.score)) + return result +# - 查询有输入指定次数(如两次)以上不及格的学生的姓名、班级和不及格成绩明细。 +def score_fail(db, fail_times, fail_score=60): + result = [] + for stu in db.query(Student).filter(Student.is_deleted == 0).all(): + fails = [s for s in stu.score if s.score < fail_score] + if len(fails) >= fail_times: + for s in fails: + result.append((stu.stu_name, stu.class_id, s.score)) + return result + +# - 统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序。 +def avg_scores(db, order="desc"): + avg = func.avg(Score.score).label("avg_score") + a = (db.query(Score.exam_id,Student.class_id,avg) + .join(Score.student).filter(Student.is_deleted == 0) + .group_by(Score.exam_id,Student.class_id)) + a = a.order_by(avg.desc() if order == "desc" else avg.asc()) + return a.all() + +# - 统计就业薪资排名 Top N(动态输入 N)的学生的姓名、班级、就业时间和就业公司。 +def top_salary(db, n): + a = (db.query(Student.stu_name, + Student.class_id, + EmploymentBase.job_time, + EmploymentBase.company_name, + EmploymentBase.salary).join(Student, EmploymentBase.stu_id == Student.stu_id) + .filter(EmploymentBase.is_deleted == 0, Student.is_deleted == 0) + .order_by(EmploymentBase.salary.desc()).offset(n-1).limit(1).first()) + if not a: + return None + return {"name": a.stu_name,"class_id": a.class_id,"job_time": str(a.job_time) if a.job_time else None, + "company": a.company_name,"salary": a.salary} + +# - 统计每个学生的就业时长(计算公式:offer下发时间 - 就业开放时间)。 +def stu_every(db): + a = (db.query(Student.stu_id, + Student.stu_name, + EmploymentBase.employment_open_time, + EmploymentOffer.offer_time) + .join(EmploymentBase, EmploymentBase.stu_id == Student.stu_id) + .join(EmploymentOffer, EmploymentOffer.stu_id == Student.stu_id) + .filter(Student.is_deleted == 0,EmploymentBase.is_deleted == 0, + EmploymentOffer.is_deleted == 0).all()) + return [{"stu_id": sid,"name": name, + "days": (offer_t - open_t).days if open_t and offer_t else None} + for sid, name, open_t, offer_t in a] + +# - 统计每个班级的平均就业时长(仅统计进入就业阶段,即有就业开放时间的学生)。 +def class_avg(db): + days = func.datediff(EmploymentOffer.offer_time, + EmploymentBase.employment_open_time).label("days") + a = (db.query(Student.class_id, + func.avg(days).label("avg_days"), + func.count().label("count")) + .join(EmploymentBase, EmploymentBase.stu_id == Student.stu_id) + .join(EmploymentOffer, EmploymentOffer.stu_id == Student.stu_id) + .filter(Student.is_deleted == 0, + EmploymentBase.is_deleted == 0, + EmploymentOffer.is_deleted == 0, + EmploymentBase.employment_open_time.isnot(None)).group_by(Student.class_id).all()) + return [{"class_id": i.class_id, "avg_days": round(float(i.avg_days), 2)} for i in a] diff --git a/dao/students_dao.py b/dao/students_dao.py new file mode 100644 index 0000000..f66fd36 --- /dev/null +++ b/dao/students_dao.py @@ -0,0 +1,115 @@ +# dao/students_dao.py +# 本文件封装对 model 表的所有数据库操作(增、删、改、查) + + +from sqlalchemy.orm import Session +from fastapi import HTTPException +from sqlalchemy_fastapi_demo_1.model.c_lass import Classinfo +from sqlalchemy_fastapi_demo_1.model.advisors import Advisor +from sqlalchemy_fastapi_demo_1.model.students import Student +from sqlalchemy_fastapi_demo_1.scheme.students import StudentCreate, StudentUpdate +from typing import Optional, List + + +class StudentDAO: + + @staticmethod + def inspect_class_id_unq(db: Session, class_id: int): + """校验班级是否存在:去 c_lass 表查""" + return db.query(Classinfo).filter(Classinfo.class_id == class_id).first() + + @staticmethod + def inspect_advisor_id_unq(db: Session, advisor_id: int): + """校验顾问是否存在:去 advisors 表查""" + return db.query(Advisor).filter(Advisor.advisor_id == advisor_id).first() + + @staticmethod + def create_student(db: Session, obj_in: StudentCreate): + """新增学生,检查学号唯一性""" + exist = db.query(Student).filter(Student.stu_id == obj_in.stu_id).first() + # 只按 stu_id 判断,且包含逻辑删除的记录 + if exist: + if exist.is_deleted == 1: + raise HTTPException(status_code=400, detail="该学号已被占用(处于逻辑删除状态),请先恢复") + raise HTTPException(status_code=400, detail="该学生已存在") + db_obj = Student(**obj_in.model_dump()) + try: + db.add(db_obj) + db.commit() + db.refresh(db_obj) + except Exception: + db.rollback() + raise + return db_obj + + @staticmethod + def get_students_all(db: Session, skip: int = 0, limit: int = 100): + """分页查询全部学生""" + return db.query(Student).filter(Student.is_deleted == 0).offset(skip).limit(limit).all() + + @staticmethod + def get_student_by_id(db: Session, stu_id: int): + """根据学生编号单条查询""" + return db.query(Student).filter(Student.is_deleted == 0, + Student.stu_id == stu_id).first() + + @staticmethod + def query_multi_condition( + db: Session, + stu_id: Optional[int] = None, + stu_name: Optional[str] = None, + class_id: Optional[int] = None, + skip: int = 0, + limit: int = 100 + ) : + q = db.query(Student).filter(Student.is_deleted == 0) + """多条件选择性查询""" + # 只有参数不为None的时候,才加上查询条件 + if stu_id is not None: + q = q.filter(Student.stu_id == stu_id) + if stu_name is not None: + q = q.filter(Student.stu_name.like(f"%{stu_name}%")) + if class_id is not None: + q = q.filter(Student.class_id == class_id) + + q = q.offset(skip).limit(limit) + return q.all() + + @staticmethod + def update_student(db: Session,stu_id:int, obj: StudentUpdate): + db_obj = db.query(Student).filter(Student.stu_id == stu_id, + Student.is_deleted == 0).first() + if not db_obj: + raise HTTPException(status_code=404, detail="学生数据不存在或已删除") + + update_data = obj.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_obj, field, value) + try: + db.commit() + db.refresh(db_obj) + except Exception: + db.rollback() + raise + return db_obj + + @staticmethod + def delete_student(db: Session, stu_id: int): + db_obj = db.query(Student).filter(Student.stu_id == stu_id, + Student.is_deleted == 0).first() + if not db_obj: + raise HTTPException(status_code=404, detail="学生数据不存在或已删除") + + db_obj.is_deleted = 1 + try: + db.commit() + db.refresh(db_obj) + except Exception: + db.rollback() + raise + return True # 返回布尔值,而不是 dict + + + + + diff --git a/dao/teachers_dao.py b/dao/teachers_dao.py new file mode 100644 index 0000000..9108e35 --- /dev/null +++ b/dao/teachers_dao.py @@ -0,0 +1,67 @@ +# dao/teachers_dao.py +# 本文件封装对 Teacher 表的所有数据库操作(增、删、改、查) +from sqlalchemy.orm import Session +from sqlalchemy_fastapi_demo_1.scheme.teachers import TeacherAdd, TeacherUpdate +from sqlalchemy_fastapi_demo_1.model.teachers import Teacher +from sqlalchemy_fastapi_demo_1.model.c_lass import Classinfo + +class TeacherDAO: + @staticmethod + def add_teacher(db:Session,teacher_data:TeacherAdd): # 前端返回的teacher_data是1个Pydantic对象 + db_teacher=Teacher(**(teacher_data.model_dump())) + db.add(db_teacher) + db.commit() + db.refresh(db_teacher) + return db_teacher + @staticmethod + def inspect_teacher_id_unq(db: Session, teacher_id: int): + """ + 根据教师ID获取教师对象(用于主键唯一性检查,查询结果包含被软删除的对象) + """ + return db.query(Teacher).filter(Teacher.teacher_id == teacher_id).first() + @staticmethod + def inspect_class_id_unq(db: Session, class_id: int): + """ + 根据班级ID获取班级对象(用于外键检查,查询结果排除被软删除对象,即不包括is_deleted=1的对象) + """ + return db.query(Classinfo).filter(Classinfo.class_id == class_id,Classinfo.is_deleted==0).first() + @staticmethod + def delete_light(db: Session, teacher_id: int): + """ + 删除教师 + :return: True 表示删除成功,False 表示教师ID不存在,已被软删除或硬删除 + """ + db_teacher = TeacherDAO.inspect_teacher_id_unq(db, teacher_id) + if (not db_teacher) or db_teacher.is_deleted==1: + return False + db_teacher.is_deleted=1 # 标记为1逻辑删除 + db.commit() # 提交事务 + return True + + @staticmethod + def update(db: Session,teacher_id:int,teacher_data: TeacherUpdate): + """ + 更新用户信息(只更新传入的非空字段) + :param db: 数据库会话 + :param teacher_data: 包含要更新字段的 Pydantic 模型 + :return: 更新后的 User 对象,如果用户不存在则返回 None + """ + # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段,不排除设置了None的字段) + update_data = teacher_data.model_dump(exclude_unset=True) # 返回要更新字段的字典 + db_teacher=TeacherDAO.inspect_teacher_id_unq(db, teacher_id) + for key, value in update_data.items(): + setattr(db_teacher, key, value) # 动态设置属性 + db.commit() # 提交事务 + db.refresh(db_teacher) # 刷新对象,获取 onupdate 时间等 + return db_teacher + @staticmethod + def get_all(db: Session, skip: int = 0, limit: int = 100): + """ + 获取所有用户(支持分页,不显示软删除) + :param db: 数据库会话 + :param skip: 偏移量(跳过前 skip 条,从skip+1条开始返回) + :param limit: 最大返回条数 + :return: 用老师对象列表 + """ + return db.query(Teacher).filter(Teacher.is_deleted==0).offset(skip).limit(limit).all() + diff --git a/database.py b/database.py new file mode 100644 index 0000000..53a47e1 --- /dev/null +++ b/database.py @@ -0,0 +1,44 @@ + + + +import os +from urllib.parse import quote_plus + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +# 导入"会话工厂"和"ORM基类" + + +# 数据库连接信息:优先读环境变量,读不到才用括号里的默认值(本地直接跑不用改代码) +# 部署到 Docker / 服务器时,只改环境变量即可,不需要动源码 +DB_USER = os.getenv("MYSQL_USER", "root") +DB_PASSWORD = os.getenv("MYSQL_PASSWORD", "123456") +DB_HOST = os.getenv("MYSQL_HOST", "localhost") +DB_PORT = os.getenv("MYSQL_PORT", "3306") +DB_NAME = os.getenv("MYSQL_NAME", "sys") + +# 也可以直接给一个完整的 DATABASE_URL,优先级最高 +# 例:mysql+pymysql://root:123456@host.docker.internal:3306/0914tw?charset=utf8mb4 +# quote_plus 用于转义密码里可能出现的 @ # / 等特殊字符 +SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL") or ( + f"mysql+pymysql://{DB_USER}:{quote_plus(DB_PASSWORD)}" + f"@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4" +) +# =============================================== + +# pool_pre_ping=True:每次取连接前先探活,容器/网络断开重连时不会拿到死连接 +engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) +# 创建"引擎":引擎负责真正和MySQL建立连接、执行SQL。整个项目共用这一个引擎。 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() +#创建ORM基类:后面所有表模型(Classinfo等)都要继承Base,SQLAlchemy才知道它们是数据库表 + + +# 提供数据库会话,接口通过 Depends(get_db) 自动获取 +def get_db(): + db = SessionLocal() # 创建一个新的数据库会话 + try: + yield db # yield = 把会话"借出去"给接口函数用(执行到这里暂停,等接口用完) + finally: + db.close() # finally = 无论接口成功还是报错,最后一定关闭会话,释放数据库连接 diff --git a/main.py b/main.py new file mode 100644 index 0000000..cfbe041 --- /dev/null +++ b/main.py @@ -0,0 +1,84 @@ + +# 【路径兜底】本项目所有模块都用 `from sqlalchemy_fastapi_demo_1.xxx import ...` 导入, +# 这要求「本项目的父目录」在 sys.path 里。直接右键运行 main.py 时 Python 只会把 +# sqlalchemy_fastapi_demo_1 自身加入 sys.path,父目录不在其中,就会报 +# ModuleNotFoundError: No module named 'sqlalchemy_fastapi_demo_1'。 +# 这里主动把父目录补进去,保证 PyCharm 右键运行、命令行、Docker 三种方式都能跑。 +import os +import sys + +_PROJECT_PARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _PROJECT_PARENT_DIR not in sys.path: + sys.path.insert(0, _PROJECT_PARENT_DIR) + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from sqlalchemy_fastapi_demo_1.api import statistics +from sqlalchemy_fastapi_demo_1.database import engine, Base +# 统一用「包名.模块」的方式导入,和 dao/model/scheme 里的写法保持一致。 +# 裸写 `from api import ...` 只在工作目录恰好是本项目根时才能跑起来, +# 放到 Docker 或其它目录运行就会 ModuleNotFoundError。 +from sqlalchemy_fastapi_demo_1.api.c_lass import router as class_router + +from sqlalchemy_fastapi_demo_1.api import employment_api # 导入 employment_api 子路由 + +from sqlalchemy_fastapi_demo_1.api import Score + +from sqlalchemy_fastapi_demo_1.api.students import router as student_router + +from sqlalchemy_fastapi_demo_1.api.advisors import router as advisor_router +from sqlalchemy_fastapi_demo_1.api import teachers +Base.metadata.create_all(bind=engine) +# 首次运行:如果数据库里还没有 c_lass 表,放开下面这行注释自动建表。 +# 表已经建好的话,保持注释状态即可。 +# Base.metadata.create_all(bind=engine) + +# 创建 FastAPI 实例 +app = FastAPI( + title="FastAPI + SQLAlchemy 分层架构", + description="学生信息管理系统 - 班级模块", + version="1.0.0", +) + +# 跨域中间件(前端网页访问后端接口需要) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 开发期允许所有来源,上线应指定域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册班级路由,统一加前缀 /c_lass +app.include_router(class_router, prefix="/c_lass", tags=["班级管理模块"]) +# ⑫ 把班级路由注册进应用,统一加前缀 /c_lass +# 所以API层里 @router.post("/add") 的完整地址是 POST /c_lass/add +app.include_router(employment_api.router, prefix="/api/employment_api", tags=["就业模块管理"]) +app.include_router(Score.app_score, prefix="/api/score", tags=["成绩管理"]) +app.include_router(student_router,prefix="/api/student", tags=["学生信息管理"]) +app.include_router(advisor_router, prefix="/advisors", tags=["顾问老师模块"]) +app.include_router(teachers.router, prefix="/api/teachers", tags=["老师模块管理"]) +app.include_router(statistics.router,prefix="/api/statistics",tags=["统计模块"]) +# 根路径 +# @app.get("/") +# async def root(): +# return {"message": "欢迎访问!请访问 /docs 查看接口文档。"} + + +# 直接运行此文件时启动 uvicorn +if __name__ == "__main__": + import os + import uvicorn + + # 地址与端口同样从环境变量读取: + # - 本地开发:默认 0.0.0.0:8004,局域网内的前端也能连上 + # - Docker:容器必须监听 0.0.0.0,写 127.0.0.1 会导致外部访问不到 + uvicorn.run( + # 与上面的包名导入保持一致;运行入口请确保父目录在工作路径中(见 README 说明) + "sqlalchemy_fastapi_demo_1.main:app", + host=os.getenv("APP_HOST", "0.0.0.0"), + port=int(os.getenv("APP_PORT", "8004")), + # 容器里一般关掉热重载,可通过 APP_RELOAD=false 控制 + reload=os.getenv("APP_RELOAD", "true").lower() in ("1", "true", "yes"), + ) diff --git a/model/Score.py b/model/Score.py new file mode 100644 index 0000000..9c18dce --- /dev/null +++ b/model/Score.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer,ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy_fastapi_demo_1.database import Base + + +# 成绩表表模型 +class Score(Base): + __tablename__ = 'score' + # 联合主键stu_id,exam_order + # 外键关联student + stu_id=Column(Integer,ForeignKey('student.stu_id'),primary_key=True) + # 声明关联student + student=relationship('Student',back_populates='score') + exam_id=Column(Integer,primary_key=True) + score=Column(Integer,nullable=False) + + + diff --git a/model/advisors.py b/model/advisors.py new file mode 100644 index 0000000..01ffa5d --- /dev/null +++ b/model/advisors.py @@ -0,0 +1,14 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.orm import relationship + +from sqlalchemy_fastapi_demo_1.database import Base +#创建顾问老师表模型。 +class Advisor(Base): + __tablename__ = 'advisor' + advisor_id = Column(Integer, primary_key=True,autoincrement=False) + advisor_name = Column(String(50),nullable=False) + #引入时间模块,得到创建时时间。特别注意这里,datetime点now不能有括号,有括号时,就在创建这个表类的时候,就锚定了时间。 + #不写括号的话,就是当这个生成实例的时候,也就是具体往表里面注入内容的时候,才会生成一个对应的时间。 + # 反向关系:Student.advisor 的对端(属性名必须是 students,与 Student.advisor 的 back_populates 一致) + students = relationship("Student", back_populates="advisor") diff --git a/model/c_lass.py b/model/c_lass.py new file mode 100644 index 0000000..8b70dd1 --- /dev/null +++ b/model/c_lass.py @@ -0,0 +1,23 @@ + + +from sqlalchemy import Column, Integer, Date # 导入"列"和字段类型(整数、日期) +from sqlalchemy.orm import relationship # 导入"关系"函数,用来定义表间关联 +from sqlalchemy_fastapi_demo_1.database import Base # 导入database.py里的Base基类 + + +class Classinfo(Base):# 定义ORM模型类,继承Base,SQLAlchemy才知道它对应一张表 + # 表名必须和建表语句一致:c_lass + __tablename__ = "c_lass" # 指定表名,必须和MySQL里的表名一模一样,否则连不上表 + + class_id = Column(Integer, primary_key=True, index=True) # 主键列:整数类型;index=True表示建索引,按这个字段查会更快 + start_time = Column(Date, nullable=False) # 开班日期:日期类型;nullable=False = 不能为空(数据库层面强制) + # 逻辑删除:0未删除,1已删除,默认0 + is_deleted = Column(Integer, nullable=False, default=0) + + # 反向关系:Student.my_class / Teacher.classes 的对端 + students = relationship("Student", back_populates="my_class") + # 声明关系:一个班级下有很多学生(一对多)。"Student"是字符串,指向学生模型 + # back_populates="my_class":和Student模型里的my_class属性互相呼应 + teachers = relationship("Teacher", back_populates="classes") + # 同样:一个班级下有很多老师 + diff --git a/model/employment.py b/model/employment.py new file mode 100644 index 0000000..214ee00 --- /dev/null +++ b/model/employment.py @@ -0,0 +1,20 @@ +#导入创造引擎、定义类模块、数据类型模块、连接符模块 +from sqlalchemy import Column, Integer, String, Date, Float, ForeignKey +#导入基类 +from sqlalchemy_fastapi_demo_1.database import Base +#定义就业基础模块 +class EmploymentBase(Base): + __tablename__="employment_base" + stu_id=Column(Integer,ForeignKey("student.stu_id"),primary_key=True) + employment_open_time=Column(Date) + job_time=Column(Date) + company_name=Column(String(100)) + salary=Column(Float) + is_deleted=Column(Integer,default=0) +#定义就业协议模块 +class EmploymentOffer(Base): + __tablename__="employment_offer" + stu_id=Column(Integer,primary_key=True) + offer_id=Column(Integer,primary_key=True) + offer_time=Column(Date) + is_deleted=Column(Integer,default=0) diff --git a/model/students.py b/model/students.py new file mode 100644 index 0000000..0be1e3e --- /dev/null +++ b/model/students.py @@ -0,0 +1,41 @@ +# model/users.py +# 本文件定义 User 表的结构,映射到 MySQL 数据库 + +from sqlalchemy import Column, Integer, String, Float, ForeignKey,DateTime,Date +from sqlalchemy.orm import declarative_base,relationship +from sqlalchemy.sql import func +from sqlalchemy_fastapi_demo_1.database import Base + + +# model 类,对应 student表 +class Student(Base): + __tablename__ = "student" #指定表名 + + stu_id = Column(Integer, primary_key = True) #主键,学生编号 + stu_name = Column(String(10),nullable = False) #学生名字,非空约束 + native_place = Column(String(30),nullable = False) #籍贯,非空 + graduate_school = Column(String(50),nullable = False) #毕业院校 + major = Column(String(20),nullable = False) #专业 + enroll_time = Column(Date, comment = "入学时间",nullable = False) + graduate_time = Column(Date, comment = "毕业时间",nullable = False) + education = Column(String(10),nullable = False) #学历,非空 + age = Column(Integer,nullable = False) #年龄 + gender = Column(String(10),nullable = False) #性别 + # employment_open_time = Column(Date,nullable = False) #简历开放时间 + # company_name = Column(String(100),nullable = False) #公司名称 + # salary = Column(Float,nullable = False) #薪资 + # # 逻辑删除标记 + is_deleted = Column(Integer, nullable=False,default=0, comment="0正常;1逻辑删除") + # 外键:关联 C_lass 的 id + class_id = Column(Integer, ForeignKey("c_lass.class_id"), nullable=False) + # 关系属性:关联 Classinfo + my_class = relationship("Classinfo", back_populates="students") + # 外键:关联Advisor的id + advisor_id = Column(Integer,ForeignKey("advisor.advisor_id"),nullable=False) + # 关系属性:关联Advisor + advisor = relationship("Advisor", back_populates="students") + # 关系属性:关联成绩(Score.student 的对端) + score = relationship("Score", back_populates="student") + + + diff --git a/model/teachers.py b/model/teachers.py new file mode 100644 index 0000000..4b50c7f --- /dev/null +++ b/model/teachers.py @@ -0,0 +1,16 @@ +# 与数据库的表进行关联 +# 导入database文件,这里已经创建基类 +from sqlalchemy import Column, Integer, ForeignKey, String +from sqlalchemy.orm import relationship +from sqlalchemy_fastapi_demo_1.database import Base +# 关联数据库中的teacher表 +class Teacher(Base): + __tablename__ = 'teacher' + teacher_id = Column(Integer, primary_key=True) + class_id = Column(Integer,ForeignKey('c_lass.class_id'),nullable=False) + teacher_name = Column(String(50),nullable=False) + job_name=Column(String(50),nullable=False,comment='主讲、班主任、助教') + is_deleted = Column(Integer, default=0, nullable=False) + classes=relationship("Classinfo",back_populates="teachers") + def __repr__(self): + return f"" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..268cba1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +sqlalchemy==2.0.36 +pymysql==1.1.1 +pydantic==2.10.4 diff --git a/scheme/Score.py b/scheme/Score.py new file mode 100644 index 0000000..a436e5a --- /dev/null +++ b/scheme/Score.py @@ -0,0 +1,15 @@ +# 请求体 +from pydantic import BaseModel, Field + + + +class ScoreCreate(BaseModel): + stu_id: int=Field(...,ge=1,description='学生id必须大于等于1') + exam_id: int=Field(...,ge=1,description='考核序次必须大于等于1') + score: int=Field(...,ge=0,le=100,description='成绩必须在0-100之间') + + +class ScoreUpdate(BaseModel): + # stu_id: int = Field(None, ge=1, description='学生id必须大于等于1') + # exam_id: int = Field(None, ge=1, description='考核序次必须大于等于1') + score: int = Field(None, ge=0, le=100, description='成绩必须在0-100之间') diff --git a/scheme/advisors.py b/scheme/advisors.py new file mode 100644 index 0000000..281c9bb --- /dev/null +++ b/scheme/advisors.py @@ -0,0 +1,12 @@ +from datetime import datetime +from pydantic import BaseModel,Field +#引入Pydantic数据校验。 +class AdvisorIn(BaseModel): + advisor_id: int=Field(...,ge=1,description="顾问ID") + advisor_name: str=Field(...,min_length=2,max_length=8,description="姓名为2~8个字符") + +class AdvisorOut(BaseModel): + advisor_id: int + advisor_name: str + class Config: + from_attributes = True diff --git a/scheme/c_lass.py b/scheme/c_lass.py new file mode 100644 index 0000000..3f4d28e --- /dev/null +++ b/scheme/c_lass.py @@ -0,0 +1,34 @@ + + + +from pydantic import BaseModel, Field, ConfigDict # BaseModel=模型基类,Field=字段规则,ConfigDict=模型配置 +from datetime import date # Python的日期类型(只有年月日) +from typing import Optional # Optional[X] 表示"可以是X,也可以是None" + +# ---------------- 请求模型 ---------------- +# 新增班级:class_id、start_time 必填 +# 请求模型 = 前端传过来的数据长什么样,FastAPI自动帮你校验 +class ClassCreate(BaseModel):# 新增班级的入参模型:POST新增时,请求体必须符合这个结构 + class_id: int = Field(..., ge=1, description="班级编号,正整数,不能重复") + start_time: date = Field(..., description="开班日期,格式 yyyy-MM-dd") +# description:这行字会显示在Swagger文档里,方便前端看 + +# 修改班级:字段可选,前端传哪个改哪个(局部更新) +class ClassUpdate(BaseModel): + # class_id: int = Field( ge=1, description="班级编号,可选") 被注释掉 = 不允许修改班级编号 + start_time: Optional[date] = Field(None, description="开班日期,格式 yyyy-MM-dd") + # Optional[date] = 可以传日期也可以不传;Field(None) = 不传时默认值是None + + +# ---------------- 响应模型 ---------------- +# 响应模型:后端返回给前端的数据结构,from_attributes 允许直接从 ORM (Classinfo实例)的属性取值,不用手动一个个赋值 +class ClassResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + class_id: int # 返回字段:班级编号 + start_time: date# 返回字段:开班日期 + + + + +#逻辑:**三个模型管三种场景**—— 新增(必填)、修改(可选)、响应(输出)。 +# 前端传错类型 / 缺字段,FastAPI 直接返回 422,不会把脏数据送进数据库。 diff --git a/scheme/employment.py b/scheme/employment.py new file mode 100644 index 0000000..98fcc0b --- /dev/null +++ b/scheme/employment.py @@ -0,0 +1,65 @@ +# 就业开放时间和offer下发时间校验 +# 导入基类、过滤条件、校验器 +from pydantic import BaseModel, Field, model_validator, field_validator +# 导入时间模块 +from datetime import date, datetime +# 导入可选类型、列表类型 +from typing import Optional, List + +# ---------- 请求模型 ---------- + +# 修改就业记录 +class EmploymentOfferUpdate(BaseModel): + offer_time: Optional[date] = None + + +# 修改就业基础 +class EmploymentBaseUpdate(BaseModel): + employment_open_time: Optional[date] = None + job_time: Optional[date] = None + company_name: Optional[str] = None + salary: Optional[float] = None + +# 添加就业基础 +class EmploymentBaseCreate(BaseModel): + stu_id: int = Field(..., description="学生编号") + job_time: Optional[date] = Field(None, description="实际去就职时间") + employment_open_time: date = Field(description="就业开放时间:年-月-日") + company_name: str = Field(max_length=100, description="就业公司") + salary: float = Field(..., gt=0, description="就业薪资,保留2位小数") + +# 添加就业记录 +class EmploymentOfferCreate(BaseModel): + stu_id: int = Field(..., description="学生编号") + offer_id: int = Field(..., description="offer编号") + offer_time: date = Field(description="offer下发时间:年-月-日") + +# 多条件组合查询 +class EmploymentQuery(BaseModel): + stu_id: Optional[int] = None + company_name: Optional[str] = Field(None, max_length=100, description="公司名称") + min_salary: Optional[float] = None + max_salary: Optional[float] = None + + +# ---------- 响应模型 ---------- +# 就业记录查询响应 +class EmploymentOfferQueryResponse(BaseModel): + stu_id: int + offer_id: int + offer_time: date + + class Config: + from_attributes = True + + +# 就业基础查询响应 +class EmploymentBaseQueryResponse(BaseModel): + stu_id: int + employment_open_time: Optional[date] = None + job_time: Optional[date] = None + company_name: Optional[str] = None + salary: Optional[float] = None + + class Config: + from_attributes = True diff --git a/scheme/statistics.py b/scheme/statistics.py new file mode 100644 index 0000000..fa6bd6e --- /dev/null +++ b/scheme/statistics.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field, model_validator + +class StudentAge(BaseModel): + age_star: int | None = Field(None, gt=0) + age_end: int | None = Field(None, gt=0) + age_value: int | None = Field(None, gt=0) + + @model_validator(mode="after") + def check_age(self): + if self.age_value is not None: + if self.age_star is not None or self.age_end is not None: + raise ValueError('age_value和age_star/age_end区间不能同时存在') + else: + if self.age_star is None or self.age_end is None: + raise ValueError('区间需填写完整') + if self.age_star is not None and self.age_end is not None: + if self.age_star > self.age_end: + raise ValueError('输入起始值不能大于终止值') + return self + +class ClassCount(BaseModel): + class_id: int = Field(..., gt=0) + gender: str | None = Field(None, description="可填男/女") + +class ScoreCount(BaseModel): + score: int = Field(..., ge=0, le=100) + num: int | None = Field(None, gt=0) + +class ScoreAvg(BaseModel): + exam_order: int = Field(...) + +class Employment(BaseModel): + top: int = Field(...) + +class EmploymentOff(BaseModel): + class_id: int = Field(...) \ No newline at end of file diff --git a/scheme/students.py b/scheme/students.py new file mode 100644 index 0000000..ab640fa --- /dev/null +++ b/scheme/students.py @@ -0,0 +1,67 @@ +# scheme/student.py +from pydantic import BaseModel, Field,model_validator +from datetime import datetime, date +from typing import Optional + +# ---------- 请求体模型 ---------- +class StudentCreate(BaseModel): + stu_id: int = Field(..., ge = 1) + class_id: int = Field(..., ge = 1) + advisor_id: int = Field(..., ge=1, description="顾问老师id,对应 advisors.advisor_id") + stu_name: str = Field(..., min_length=1, max_length=10) + native_place: str = Field(..., min_length=1, max_length=30) + graduate_school: str = Field(..., min_length=1, max_length=50) + education: str = Field(..., min_length=1, max_length=10) + major: str = Field(..., min_length=1, max_length=20) + age: int = Field(..., ge = 18, le =40) + gender: str = Field(..., min_length=1, max_length=20) + graduate_time:date= Field(...) + enroll_time:date= Field(...) + + + # 模型校验,时间判断 + # @model_validator (mode = "after") + # def check_time(self): + # # 入学时间不能晚于就业时间 + # if self.graduate_time is not None: + # if self.graduate_time < self.enroll_time: + # raise ValueError ("毕业时间不能早于入学时间!!!") + # # 简历开放时间不能早于入学时间 + # # if self.employment_open_time < self.enroll_time: + # # raise ValueError("简历开放时间不能早于入学时间") + # # 简历开放时间不能晚于毕业时间 + # if self.graduate_time is not None: + # if self.graduate_time < self.employment_open_time: + # raise ValueError("简历开放时间不能晚于毕业时间!!!") + # # mode="after" 的校验器必须返回 self,否则 Pydantic v2 会把整个模型变成 None + # return self + + +class StudentUpdate(BaseModel): + class_id: Optional[int] = Field(None) + advisor_id: Optional[int] = Field(None, ge=1) + stu_name: Optional[str] = Field(None, min_length=1, max_length=10) + native_place: Optional[str] = Field(None, min_length=1, max_length=30) + graduate_school: Optional[str] = Field(None, min_length=1, max_length=50) + education: Optional[str] = Field(None, min_length=1, max_length=10) + major: Optional[str] = Field(None, min_length=1, max_length=20) + age: Optional[int] = Field(None, ge = 18, le =40) + gender: Optional[str] = Field(None, min_length=1, max_length=20) + +# ----------响应体模型------------- +class StudentResponse(BaseModel): + stu_id: int + class_id: int # 数据库非空,这里必须是 int + stu_name: str + native_place: str + graduate_school: Optional[str] + education: str + major: Optional[str] + # 敏感字段已在此模型中脱敏 + + class Config: + from_attributes = True #支持orm对象转换,让pydantic可以读取ORM数据库对象(SQLAlchemy模型) + + + + diff --git a/scheme/teachers.py b/scheme/teachers.py new file mode 100644 index 0000000..45a5120 --- /dev/null +++ b/scheme/teachers.py @@ -0,0 +1,52 @@ +# 正确 +from pydantic import Field + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, field_validator + + +# --------------------请求体模型----------------------------- +# 用于增加1行数据# 加了软删除标签 +class TeacherAdd(BaseModel): + teacher_id:int=Field(...,ge=1,description='教师ID是整数类型') + class_id:int=Field(...,ge=1,description='班级ID是整数类型') + teacher_name:str=Field(...,description='教师姓名') + job_name:str=Field(...,min_length=2,description='主讲老师、班主任、助教') + is_deleted:int=Field(0,ge=0,le=0,description='只能输入0,0代表未删除,1代表已经被软删除') + + @field_validator("job_name") + @classmethod + def validate_job_name(cls, v): + """校验 job_name 必须是 主讲/班主任/助教 之一""" + allowed = ["主讲老师", "班主任", "助教"] + if v not in allowed: + raise ValueError(f"job_name 必须是 {allowed} 之一,当前值: {v}") + return v +# 用于更新数据 +class TeacherUpdate(BaseModel): + class_id: Optional[int] = Field(None, ge=1, description='班级ID是整数类型') # ge=1 只对非 None 的值生效 + teacher_name: Optional[str] = Field(None, description='教师姓名') + job_name: Optional[str] = Field(None,min_length=2, description='主讲老师、班主任、助教') + + @field_validator("job_name") + @classmethod + def validate_job_name(cls, v): + """校验 job_name 必须是 主讲/班主任/助教 之一""" + allowed = ["主讲老师", "班主任", "助教"] + if v not in allowed: + raise ValueError(f"job_name 必须是 {allowed} 之一,当前值: {v}") + return v +# --------------------- 响应模型 ---------------------------- +class TeacherResponse(BaseModel): + teacher_id: int + class_id: int + teacher_name: str + job_name: str + + class Config: # 告诉 Pydantic:"可以从任意对象的属性中读取数据,而不只是从字典中读取。" + from_attributes = True # 支持 ORM 对象转换 + + + diff --git a/student_system/vue/.dockerignore b/student_system/vue/.dockerignore new file mode 100644 index 0000000..e729890 --- /dev/null +++ b/student_system/vue/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +.git +.gitignore +.vscode +.idea +*.md +npm-debug.log* +Dockerfile +docker-compose.yml +nginx.conf.template +.dockerignore diff --git a/student_system/vue/Dockerfile b/student_system/vue/Dockerfile new file mode 100644 index 0000000..373b040 --- /dev/null +++ b/student_system/vue/Dockerfile @@ -0,0 +1,37 @@ +# ====================== 第一阶段:构建 ====================== +# 用 Node 镜像把 Vue 源码打包成静态文件 +FROM node:20-alpine AS builder + +WORKDIR /app + +# 先只复制依赖清单并安装依赖:这样改业务代码时不会重新安装依赖,能复用缓存 +COPY package*.json ./ +RUN npm install --registry=https://registry.npmmirror.com + +# 再复制全部源码并打包(Vite / Vue CLI 默认产物目录都是 dist) +COPY . . +RUN npm run build + + +# ====================== 第二阶段:运行 ====================== +# 只保留打包结果,用体积小的 Nginx 托管,镜像里不含 node_modules +FROM nginx:1.27-alpine + +LABEL maintainer="student-system" description="学生管理系统 - Vue 前端" + +# 后端接口地址(nginx.conf.template 里会用到这两个变量做反向代理) +ENV BACKEND_HOST=fastapi \ + BACKEND_PORT=8004 + +# 删除 nginx 默认欢迎页 +RUN rm -rf /usr/share/nginx/html/* + +# nginx 官方镜像会自动把 /etc/nginx/templates/*.template 渲染成 /etc/nginx/conf.d/*.conf +COPY nginx.conf.template /etc/nginx/templates/default.conf.template + +# 从构建阶段拷贝打包好的静态文件 +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/student_system/vue/docker-compose.yml b/student_system/vue/docker-compose.yml new file mode 100644 index 0000000..50547f2 --- /dev/null +++ b/student_system/vue/docker-compose.yml @@ -0,0 +1,41 @@ +services: + # ================= 后端:FastAPI + SQLAlchemy ================= + # 构建上下文是后端代码根目录(本文件在 vue/ 下,../.. 即 sqlalchemy_fastapi_demo_1) + fastapi: + build: + context: ../.. + dockerfile: Dockerfile + image: student-system-api:1.0 + container_name: fastapi-backend + ports: + - "8004:8004" + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8004 + APP_RELOAD: "false" + # MySQL 跑在宿主机上:容器里的 localhost 指容器自己,必须用 host.docker.internal + MYSQL_HOST: host.docker.internal + MYSQL_PORT: 3306 + MYSQL_USER: root + MYSQL_PASSWORD: "123456" + MYSQL_NAME: 0914tw + extra_hosts: + - "host.docker.internal:host-gateway" # Linux 需要这行,Win/Mac Docker Desktop 自带 + restart: unless-stopped + + # ================= 前端:Vue + Nginx ================= + vue-frontend: + build: + context: . + dockerfile: Dockerfile + image: student-system-vue:1.0 + container_name: vue-frontend + ports: + - "8080:80" # 浏览器访问 http://localhost:8080 + environment: + # 填服务名,走 compose 内部网络;若后端在宿主机上直接跑,改成 host.docker.internal + BACKEND_HOST: fastapi + BACKEND_PORT: 8004 + depends_on: + - fastapi + restart: unless-stopped diff --git a/student_system/vue/index.html b/student_system/vue/index.html new file mode 100644 index 0000000..226c636 --- /dev/null +++ b/student_system/vue/index.html @@ -0,0 +1,12 @@ + + + + + + 学生信息管理系统 + + +
+ + + diff --git a/student_system/vue/nginx.conf.template b/student_system/vue/nginx.conf.template new file mode 100644 index 0000000..72c9d0b --- /dev/null +++ b/student_system/vue/nginx.conf.template @@ -0,0 +1,43 @@ +# 该模板会由容器启动时自动生成配置并渲染变量 ${BACKEND_HOST} ${BACKEND_PORT} +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # 开启压缩,减小传输体积 + gzip on; + gzip_min_length 1k; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + client_max_body_size 20m; + + # 单页应用(SPA)history 路由:找不到对应文件时回退到 index.html + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源加缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf)$ { + expires 7d; + access_log off; + } + + # 反向代理后端接口(覆盖本项目后端的所有路由前缀) + # 使用变量形式,后端容器没启动时 nginx 也能正常启动,只是请求会返回 502 + location ~ ^/(api|advisors|c_lass|docs|redoc|openapi\.json) { + resolver 127.0.0.11 valid=30s; # Docker 内嵌 DNS + set $ss_backend http://${BACKEND_HOST}:${BACKEND_PORT}; + proxy_pass $ss_backend; # 变量形式必须省略 URI 部分,原始请求路径会原样透传 + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + access_log /var/log/nginx/frontend_access.log; + error_log /var/log/nginx/frontend_error.log; +} diff --git a/student_system/vue/package-lock.json b/student_system/vue/package-lock.json new file mode 100644 index 0000000..7f088d6 --- /dev/null +++ b/student_system/vue/package-lock.json @@ -0,0 +1,1821 @@ +{ + "name": "student-system-vue", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "student-system-vue", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.9", + "element-plus": "^2.9.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", + "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", + "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", + "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", + "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", + "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", + "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", + "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", + "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", + "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", + "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", + "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", + "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", + "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", + "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", + "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", + "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", + "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", + "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", + "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", + "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", + "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", + "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", + "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", + "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", + "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.43", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.28", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-15.0.0.tgz", + "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "15.0.0", + "@vueuse/shared": "15.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-15.0.0.tgz", + "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-15.0.0.tgz", + "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.6", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.6.tgz", + "integrity": "sha512-GH0fMoMgVEKkHimoaKvg5B00NmfPbQ+qxX6/JrO7F+dLAC3atrZr2Y2MoccHMxS22M7QE12/s0DG0Zs49iwJ/A==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "15.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.11" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.63.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.3.tgz", + "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.3", + "@rollup/rollup-android-arm64": "4.63.3", + "@rollup/rollup-darwin-arm64": "4.63.3", + "@rollup/rollup-darwin-x64": "4.63.3", + "@rollup/rollup-freebsd-arm64": "4.63.3", + "@rollup/rollup-freebsd-x64": "4.63.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", + "@rollup/rollup-linux-arm-musleabihf": "4.63.3", + "@rollup/rollup-linux-arm64-gnu": "4.63.3", + "@rollup/rollup-linux-arm64-musl": "4.63.3", + "@rollup/rollup-linux-loong64-gnu": "4.63.3", + "@rollup/rollup-linux-loong64-musl": "4.63.3", + "@rollup/rollup-linux-ppc64-gnu": "4.63.3", + "@rollup/rollup-linux-ppc64-musl": "4.63.3", + "@rollup/rollup-linux-riscv64-gnu": "4.63.3", + "@rollup/rollup-linux-riscv64-musl": "4.63.3", + "@rollup/rollup-linux-s390x-gnu": "4.63.3", + "@rollup/rollup-linux-x64-gnu": "4.63.3", + "@rollup/rollup-linux-x64-musl": "4.63.3", + "@rollup/rollup-openbsd-x64": "4.63.3", + "@rollup/rollup-openharmony-arm64": "4.63.3", + "@rollup/rollup-win32-arm64-msvc": "4.63.3", + "@rollup/rollup-win32-ia32-msvc": "4.63.3", + "@rollup/rollup-win32-x64-gnu": "4.63.3", + "@rollup/rollup-win32-x64-msvc": "4.63.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/student_system/vue/package.json b/student_system/vue/package.json new file mode 100644 index 0000000..c9f0531 --- /dev/null +++ b/student_system/vue/package.json @@ -0,0 +1,22 @@ +{ + "name": "student-system-vue", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "学生信息管理系统 - Vue3 + Element Plus 前端", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host" + }, + "dependencies": { + "axios": "^1.7.9", + "element-plus": "^2.9.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^5.4.11" + } +} diff --git a/student_system/vue/src/App.vue b/student_system/vue/src/App.vue new file mode 100644 index 0000000..16a4473 --- /dev/null +++ b/student_system/vue/src/App.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/student_system/vue/src/api/http.js b/student_system/vue/src/api/http.js new file mode 100644 index 0000000..dc7a2b2 --- /dev/null +++ b/student_system/vue/src/api/http.js @@ -0,0 +1,45 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +// baseURL 为空表示同源:Docker 部署时由 Nginx 反向代理到后端; +// 本地开发时由 vite.config.js 的 proxy 转发。 +const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE || '', + timeout: 15000 +}) + +/** + * 把 FastAPI 的错误信息提取成可读文本 + * - 401/403/404 等:detail 是字符串 + * - 422 参数校验失败:detail 是数组 + */ +function pickError(error) { + const detail = error?.response?.data?.detail + if (Array.isArray(detail)) { + return detail + .map((d) => { + const field = (d.loc || []).filter((x) => x !== 'body' && x !== 'query').join('.') + return field ? `${field}: ${d.msg}` : d.msg + }) + .join('; ') + } + if (typeof detail === 'string') return detail + if (error?.response?.status === 500) return '服务器内部错误(500),请查看后端日志' + if (error?.code === 'ECONNABORTED') return '请求超时' + return error.message || '请求失败' +} + +http.interceptors.response.use( + (res) => res, + (error) => { + ElMessage.error(pickError(error)) + return Promise.reject(error) + } +) + +/** 统一取响应体,204(空响应)返回 null */ +export function unwrap(res) { + return res.data === undefined || res.data === '' ? null : res.data +} + +export default http diff --git a/student_system/vue/src/api/index.js b/student_system/vue/src/api/index.js new file mode 100644 index 0000000..960a0b9 --- /dev/null +++ b/student_system/vue/src/api/index.js @@ -0,0 +1,85 @@ +import http, { unwrap } from './http' + +/** 组装 query 串,自动丢弃空值 */ +function qs(params = {}) { + const p = new URLSearchParams() + Object.entries(params).forEach(([k, v]) => { + if (v !== undefined && v !== null && v !== '') p.append(k, v) + }) + const s = p.toString() + return s ? `?${s}` : '' +} + +// ============ 学生 ============ +export const studentApi = { + // 注意:路径末尾的斜杠是后端定义的,不能省 + list: (params) => http.get(`/api/student/students/all${qs(params)}`).then(unwrap), + search: (params) => http.get(`/api/student/students/search${qs(params)}`).then(unwrap), + get: (stuId) => http.get(`/api/student/students/${stuId}`).then(unwrap), + create: (data) => http.post('/api/student/students/', data).then(unwrap), + update: (stuId, data) => http.put(`/api/student/students/${stuId}`, data).then(unwrap), + remove: (stuId) => http.delete(`/api/student/students/${stuId}`).then(unwrap) +} + +// ============ 班级 ============ +export const classApi = { + list: (params) => http.get(`/c_lass/${qs(params)}`).then(unwrap), + get: (classId) => http.get(`/c_lass/${classId}`).then(unwrap), + add: (data) => http.post('/c_lass/add', data).then(unwrap), + update: (classId, data) => http.put(`/c_lass/${classId}/`, data).then(unwrap), + remove: (classId) => http.delete(`/c_lass/${classId}`).then(unwrap) +} + +// ============ 成绩 ============ +export const scoreApi = { + list: (params) => http.get(`/api/score/${qs(params)}`).then(unwrap), + get: (stuId, examId) => http.get(`/api/score/${stuId}/${examId}`).then(unwrap), + create: (data) => http.post('/api/score/', data).then(unwrap), + update: (stuId, examId, data) => http.put(`/api/score/${stuId}/${examId}`, data).then(unwrap), + remove: (stuId, examId) => http.delete(`/api/score/${stuId}/${examId}`).then(unwrap) +} + +// ============ 老师 ============ +export const teacherApi = { + list: (params) => http.get(`/api/teachers/teacher/total_query${qs(params)}`).then(unwrap), + get: (teacherId) => http.get(`/api/teachers/teacher/single_query${qs({ teacher_id: teacherId })}`).then(unwrap), + add: (data) => http.post('/api/teachers/teacher/add', data).then(unwrap), + update: (teacherId, data) => + http.put(`/api/teachers/teacher/update${qs({ teacher_id: teacherId })}`, data).then(unwrap), + remove: (teacherId) => http.delete(`/api/teachers/teacher/delete${qs({ teacher_id: teacherId })}`).then(unwrap) +} + +// ============ 顾问老师 ============ +export const advisorApi = { + list: (advisorName) => http.get(`/advisors/list${qs({ advisor_name: advisorName })}`).then(unwrap), + read: (params) => http.get(`/advisors/read${qs(params)}`).then(unwrap), + create: (data) => http.post('/advisors/create', data).then(unwrap), + update: (data) => http.put('/advisors/update', data).then(unwrap), + remove: (advisorId) => http.delete(`/advisors/delete/${advisorId}`).then(unwrap) +} + +// ============ 就业 ============ +export const employmentApi = { + baseList: (params) => http.get(`/api/employment_api/base/search/${qs(params)}`).then(unwrap), + baseAdd: (data) => http.post('/api/employment_api/base/add/', data).then(unwrap), + baseUpdate: (stuId, data) => http.put(`/api/employment_api/base/${stuId}/`, data).then(unwrap), + baseRemove: (stuId) => http.delete(`/api/employment_api/base/${stuId}/`).then(unwrap), + + offerList: (params) => http.get(`/api/employment_api/offer/search/${qs(params)}`).then(unwrap), + offerAdd: (data) => http.post('/api/employment_api/offer/add/', data).then(unwrap), + offerUpdate: (stuId, offerId, data) => + http.put(`/api/employment_api/offer/${offerId}${qs({ stu_id: stuId })}`, data).then(unwrap), + offerRemove: (stuId, offerId) => http.delete(`/api/employment_api/offer/${stuId}/${offerId}`).then(unwrap) +} + +// ============ 统计 ============ +export const statisticsApi = { + age: (params) => http.get(`/api/statistics/students/age${qs(params)}`).then(unwrap), + classes: () => http.get('/api/statistics/classes/statistics').then(unwrap), + scoreAbove: (score) => http.get(`/api/statistics/scores/above${qs({ score_input: score })}`).then(unwrap), + scoreFail: (params) => http.get(`/api/statistics/scores/fail${qs(params)}`).then(unwrap), + scoreAvg: (order) => http.get(`/api/statistics/scores/avg${qs({ order })}`).then(unwrap), + empTop: (n) => http.get(`/api/statistics/employment/top${qs({ n })}`).then(unwrap), + empEvery: () => http.get('/api/statistics/employment/every').then(unwrap), + empClassAvg: () => http.get('/api/statistics/employment/class_avg').then(unwrap) +} diff --git a/student_system/vue/src/main.js b/student_system/vue/src/main.js new file mode 100644 index 0000000..2293472 --- /dev/null +++ b/student_system/vue/src/main.js @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import 'element-plus/dist/index.css' +import './style.css' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(ElementPlus, { locale: zhCn }) +app.use(router) +app.mount('#app') diff --git a/student_system/vue/src/router/index.js b/student_system/vue/src/router/index.js new file mode 100644 index 0000000..5ae7b8e --- /dev/null +++ b/student_system/vue/src/router/index.js @@ -0,0 +1,17 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { path: '/', name: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '首页概览' } }, + { path: '/students', name: 'students', component: () => import('@/views/Students.vue'), meta: { title: '学生信息管理' } }, + { path: '/classes', name: 'classes', component: () => import('@/views/Classes.vue'), meta: { title: '班级管理' } }, + { path: '/scores', name: 'scores', component: () => import('@/views/Scores.vue'), meta: { title: '成绩管理' } }, + { path: '/teachers', name: 'teachers', component: () => import('@/views/Teachers.vue'), meta: { title: '老师管理' } }, + { path: '/advisors', name: 'advisors', component: () => import('@/views/Advisors.vue'), meta: { title: '顾问老师' } }, + { path: '/employment', name: 'employment', component: () => import('@/views/Employment.vue'), meta: { title: '就业管理' } }, + { path: '/statistics', name: 'statistics', component: () => import('@/views/Statistics.vue'), meta: { title: '统计查询' } } +] + +export default createRouter({ + history: createWebHistory(), + routes +}) diff --git a/student_system/vue/src/style.css b/student_system/vue/src/style.css new file mode 100644 index 0000000..cbcf1bf --- /dev/null +++ b/student_system/vue/src/style.css @@ -0,0 +1,50 @@ +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +body { + font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif; + background: #f0f2f5; + color: #303133; +} + +/* 统一的页面卡片 */ +.page-card { + background: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); +} + +/* 工具栏:搜索框 + 按钮 */ +.toolbar { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-end; + margin-bottom: 12px; +} + +.toolbar .spacer { + flex: 1; +} + +.field-label { + font-size: 12px; + color: #909399; + margin-bottom: 4px; +} + +.stat-value { + font-size: 26px; + font-weight: bold; + color: #409eff; +} diff --git a/student_system/vue/src/views/Advisors.vue b/student_system/vue/src/views/Advisors.vue new file mode 100644 index 0000000..c6a9114 --- /dev/null +++ b/student_system/vue/src/views/Advisors.vue @@ -0,0 +1,118 @@ + + + diff --git a/student_system/vue/src/views/Classes.vue b/student_system/vue/src/views/Classes.vue new file mode 100644 index 0000000..550e6af --- /dev/null +++ b/student_system/vue/src/views/Classes.vue @@ -0,0 +1,108 @@ + + + diff --git a/student_system/vue/src/views/Dashboard.vue b/student_system/vue/src/views/Dashboard.vue new file mode 100644 index 0000000..e40e15f --- /dev/null +++ b/student_system/vue/src/views/Dashboard.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/student_system/vue/src/views/Employment.vue b/student_system/vue/src/views/Employment.vue new file mode 100644 index 0000000..bc1298d --- /dev/null +++ b/student_system/vue/src/views/Employment.vue @@ -0,0 +1,297 @@ + + + diff --git a/student_system/vue/src/views/Scores.vue b/student_system/vue/src/views/Scores.vue new file mode 100644 index 0000000..48cd966 --- /dev/null +++ b/student_system/vue/src/views/Scores.vue @@ -0,0 +1,129 @@ + + + diff --git a/student_system/vue/src/views/Statistics.vue b/student_system/vue/src/views/Statistics.vue new file mode 100644 index 0000000..a7c3fa0 --- /dev/null +++ b/student_system/vue/src/views/Statistics.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/student_system/vue/src/views/Students.vue b/student_system/vue/src/views/Students.vue new file mode 100644 index 0000000..fdcd6b7 --- /dev/null +++ b/student_system/vue/src/views/Students.vue @@ -0,0 +1,274 @@ + + + diff --git a/student_system/vue/src/views/Teachers.vue b/student_system/vue/src/views/Teachers.vue new file mode 100644 index 0000000..f1f6ad0 --- /dev/null +++ b/student_system/vue/src/views/Teachers.vue @@ -0,0 +1,125 @@ + + + diff --git a/student_system/vue/vite.config.js b/student_system/vue/vite.config.js new file mode 100644 index 0000000..f17b27c --- /dev/null +++ b/student_system/vue/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +// 本地开发时的后端地址(Docker 部署时前端走同源 Nginx 反代,用不到它) +// 后端 uvicorn 监听 0.0.0.0:8004,本机访问直接用 127.0.0.1,不依赖局域网 IP +const backend = process.env.VITE_BACKEND || 'http://127.0.0.1:8004' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + server: { + port: 5173, + host: true, + proxy: { + '/api': { target: backend, changeOrigin: true }, + '/advisors': { target: backend, changeOrigin: true }, + '/c_lass': { target: backend, changeOrigin: true }, + '/docs': { target: backend, changeOrigin: true }, + '/openapi.json': { target: backend, changeOrigin: true } + } + }, + build: { + outDir: 'dist', + chunkSizeWarningLimit: 1500 + } +})